// // SKPresentationOptionsSheetController.swift // PDF Reader Pro // // Created by liujiajie on 2024/1/17. // import Cocoa typealias closePresentationControllerCallBack = (_ vc: SKPresentationOptionsSheetController) -> () let RIGHTARROW_CHARACTER: unichar = 0x2192 let PAGE_COLUMNID = "page" let IMAGE_COLUMNID = "image" let TRANSITIONSTYLE_KEY = "transitionStyle" let DURATION_KEY = "duration" let SHOULDRESTRICT_KEY = "shouldRestrict" let PROPERTIES_KEY = "properties" let CONTENTOBJECT_BINDINGNAME = "contentObject" let MAX_PAGE_COLUMN_WIDTH: CGFloat = 100.0 let TABLE_OFFSET: CGFloat = 8.0 //var SKTransitionPropertiesObservationContext = UnsafeMutableRawPointer(mutating: "SKTransitionPropertiesObservationContext") var SKPDFViewTransitionsObservationContext: CChar? class SKPresentationOptionsSheetController: KMBaseWindowController { @IBOutlet var notesDocumentPopUpButton: NSPopUpButton! @IBOutlet var tableView: KMBotaTableView! @IBOutlet var separateCheckButton: NSButton! @IBOutlet var boxes, transitionLabels, transitionControls, buttons: NSArray! @IBOutlet var effectPopUpButton: NSPopUpButton! @IBOutlet var effectLabel: NSTextField! @IBOutlet var durationLabel: NSTextField! @IBOutlet var durationSlider: NSSlider! @IBOutlet var durationTF: NSTextField! @IBOutlet var extentLabel: NSTextField! @IBOutlet var screeBtn: NSButtonCell! @IBOutlet var pageBtn: NSButtonCell! @IBOutlet var synchoronizBox: NSBox! @IBOutlet var cancelBtn: NSButton! @IBOutlet var okBtn: NSButton! @IBOutlet var pageTransitionBox: NSBox! var closeCallBack: closePresentationControllerCallBack? var separate = false var transition: KMTransitionInfo = KMTransitionInfo() private var transitions_: NSArray? var transitions: NSArray? { get { return self.transitions_ } set{ if self.transitions_ != newValue { // (undoRedoManager?.prepare(withInvocationTarget: self) as AnyObject).transitions = transitions self.transitions_ = newValue stopObservingTransitions(infos: transitions as! [KMTransitionInfo]) startObservingTransitions(transitions as! [KMTransitionInfo]) } } } var currentTransitions: NSArray? { get{ return self.separate ? self.transitions : [transition as Any] } } var pageTransitions: NSArray? { get{ if self.separate && self.transitions?.count ?? 0 > 0 { return [transitions?.value(forKey: PROPERTIES_KEY) as Any] } return nil } } var notesDocument: NSDocument? { get{ _ = self.window return self.notesDocumentPopUpButton.selectedItem?.representedObject as? NSDocument } } var isScrolling: Bool { get{ let scroller = self.tableView?.enclosingScrollView?.verticalScroller as? KMScroller return scroller?.isScrolling ?? false } } var undoRedoManager = UndoManager() weak var controller: KMMainViewController? weak var transitionController: SKTransitionController? private weak var proxyDelegate_: SKTransitionControllerDelegate? // class func keyPathsForValuesAffectingCurrentTransitions() -> Set { // return Set(["separate", "transitions", "transition"]) // } override var windowNibName: NSNib.Name?{ return "TransitionSheet" } override init(window: NSWindow?) { super.init(window: window) } required init?(coder: NSCoder) { super.init(coder: coder) } override func windowDidLoad() { super.windowDidLoad() let count = SKTransitionController.transitionNames().count for i in 0.. 0.0 { SKResizeWindow(self.window, dw) SKShiftAndResizeViews(self.boxes as? [Any], -dw, dw) SKShiftAndResizeView(self.separateCheckButton, -dw, 0.0) } // collapse the table self.window?.setFrame(NSInsetRect(window?.frame ?? .zero, 0.5 * (NSWidth(tableView.enclosingScrollView!.frame) + TABLE_OFFSET), 0.0), display: false) self.tableView.registerForDraggedTypes(KMTransitionInfo.readableTypesForPasteboard(pasteboard: NSPasteboard(name: NSPasteboard.Name.drag)) as! [NSPasteboard.PasteboardType]) self.tableView.delegate = self self.tableView.dataSource = self self.tableView.botaDelegate = self self.tableView.setTypeSelectHelper(SKTypeSelectHelper(matchOption: .SKFullStringMatch)) self.tableView.hasImageToolTips = true self.tableView.backgroundColor = NSColor.mainSourceListBackgroundColor() if (transitionController?.pageTransitions != nil && transitionController?.pageTransitions.count ?? 0 > 0) { self.undoRedoManager.disableUndoRegistration() self.separate = true self.undoRedoManager.enableUndoRegistration() } // set the current notes document and observe changes for the popup self.handleDocumentsDidChangeNotification(notification: nil) let docIndex = self.notesDocumentPopUpButton.indexOfItem(withRepresentedObject: self.controller?.myDocument/*presentationNotesDocument*/) self.notesDocumentPopUpButton.selectItem(at: docIndex > 0 ? docIndex : 0) NotificationCenter.default.addObserver(self, selector: #selector(handleDocumentsDidChangeNotification(notification:)), name: NSNotification.Name("SKDocumentDidShowNotification"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleDocumentsDidChangeNotification(notification:)), name: NSNotification.Name("SKDocumentControllerDidRemoveDocumentNotification"), object: nil) } override func initDefaultValue() { super.initDefaultValue() self.effectPopUpButton.target = self self.effectPopUpButton.action = #selector(effectButtonAction) self.durationTF.formatter = NumberFormatter() self.durationTF.delegate = self self.screeBtn.target = self self.screeBtn.action = #selector(extentAction) self.pageBtn.target = self self.pageBtn.action = #selector(extentAction) } func stopObservingTransitions(infos: [KMTransitionInfo]) { // for info in infos { // info.removeObserver(self, forKeyPath: TRANSITIONSTYLE_KEY) // info.removeObserver(self, forKeyPath: DURATION_KEY) // info.removeObserver(self, forKeyPath: SHOULDRESTRICT_KEY) // } } // func creatTransitionController() { // transitionController = SKTransitionController(for: controller?.listView)//controller?.mainViewController.listView.transitionController // let options: NSKeyValueObservingOptions = [.new, .old] // transitionController?.addObserver(self, forKeyPath: "transitionStyle", options: options, context: &SKPDFViewTransitionsObservationContext) // transitionController?.addObserver(self, forKeyPath: "duration", options: options, context: &SKPDFViewTransitionsObservationContext) // transitionController?.addObserver(self, forKeyPath: "shouldRestrict", options: options, context: &SKPDFViewTransitionsObservationContext) // transitionController?.addObserver(self, forKeyPath: "pageTransitions", options: options, context: &SKPDFViewTransitionsObservationContext) // } func startObservingTransitions(_ infos: [KMTransitionInfo]) { // for info in infos { // info.addObserver(self, forKeyPath: TRANSITIONSTYLE_KEY, options: [.new, .old], context: &SKTransitionPropertiesObservationContext) // info.addObserver(self, forKeyPath: DURATION_KEY, options: [.new, .old], context: &SKTransitionPropertiesObservationContext) // info.addObserver(self, forKeyPath: SHOULDRESTRICT_KEY, options: [.new, .old], context: &SKTransitionPropertiesObservationContext) // } } func setValue(_ value: Any?, forKey key: String, ofTransition info: KMTransitionInfo) { info.setValue(value, forKey: key) } // override class func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { // if context == &SKTransitionPropertiesObservationContext { // guard let info = object as? KMTransitionInfo else { // super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) // return // } // let newValue = change?[.newKey] // let oldValue = change?[.oldKey] // // let newNonNullValue = newValue as? NSNull == nil ? newValue : nil // let oldNonNullValue = oldValue as? NSNull == nil ? oldValue : nil // //// if (newNonNullValue != nil || oldNonNullValue != nil) && newNonNullValue != oldNonNullValue { //// (undoManager?.prepare(withInvocationTarget: self) as AnyObject).setValue(oldNonNullValue, forKey: keyPath, ofTransition: info) //// } // if (newNonNullValue != nil || oldNonNullValue != nil){ // // } // } else { // super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) // } // } // MARK: - Private private func makeTransitions() { if transitions != nil { return } let tableColumn = self.tableView?.tableColumn(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: PAGE_COLUMNID)) let cell = tableColumn?.dataCell var labelWidth: CGFloat = 0.0 var array = [Any]() let dictionary: NSDictionary? = self.transition.properties let arr: NSArray? = self.transitionController?.pageTransitions as NSArray? let ptEnum = arr?.objectEnumerator() var tn: KMThumbnail? = nil for next in (controller?.leftSideViewController.thumbnails)! { if tn != nil { let info = KMTransitionInfo() info.thumbnail = tn info.label = "\(tn!.label)"+"→"+"\(next.label)" info.properties = ((ptEnum?.nextObject()) != nil) ? ptEnum?.nextObject() as? NSDictionary : dictionary array.append(info) (cell as AnyObject).setStringValue(info.label) labelWidth = max(labelWidth, ceil(((cell as AnyObject).cellSize?.width)!)) } tn = next } labelWidth = min(labelWidth, MAX_PAGE_COLUMN_WIDTH) tableColumn?.minWidth = labelWidth tableColumn?.maxWidth = labelWidth tableColumn?.width = labelWidth var frame: NSRect = self.tableView?.enclosingScrollView?.frame ?? .zero let wi: CGFloat = 61//tableColumn?.value(forKeyPath: "@sum.width") as! CGFloat frame.size.width = 19.0 + wi self.tableView?.enclosingScrollView?.frame = frame self.transitions = array as NSArray } @objc dynamic func changePageTransitionType(sep: Bool) { // SKImageToolTipWindow.sharedToolTipWindow().orderOut(nil) self.separate = sep self.separateCheckButton.state = sep ? .on : .off let window = self.window let isVisible = window?.isVisible ?? false var frame = window?.frame ?? NSRect.zero let scrollView = tableView?.enclosingScrollView var extraWidth: CGFloat let firstResponder = window?.firstResponder var editor: NSTextView? = nil if let textView = firstResponder as? NSTextView { editor = textView if textView.isFieldEditor { // firstResponder = textView.delegate } } if let editor = editor, window?.firstResponder != editor { window?.makeFirstResponder(firstResponder) } if sep { makeTransitions() extraWidth = (scrollView?.frame.width ?? 0) + TABLE_OFFSET frame.size.width += extraWidth frame.origin.x -= floor(0.5 * extraWidth) window?.setFrame(frame, display: isVisible, animate: isVisible) scrollView?.isHidden = false } else { scrollView?.isHidden = true extraWidth = (scrollView?.frame.width ?? 0) + TABLE_OFFSET frame.size.width -= extraWidth frame.origin.x += floor(0.5 * extraWidth) window?.setFrame(frame, display: isVisible, animate: isVisible) } (self.undoRedoManager.prepare(withInvocationTarget: self) as AnyObject).changePageTransitionType(sep: sep == false) } func selectedInfos() -> [KMTransitionInfo]? { if self.separate == false { return [self.transition] } else { guard let trans = self.transitions else { return nil } let ris = self.tableView.selectedRowIndexes var infos: [KMTransitionInfo] = [] for ri in ris { if ri >= trans.count { continue } if let info = trans.object(at: ri) as? KMTransitionInfo { infos.append(info) } } return infos } } private func _updateInfo(key: String, value: Any) { if key == KMTransitionStyleName { let _value = value as? SKAnimationTransitionStyle ?? .noTransition if self.separate == false { self.transition.transitionStyle = _value } else { guard let trans = self.transitions else { return } let ris = self.tableView.selectedRowIndexes for ri in ris { if ri >= trans.count { continue } let info = trans.object(at: ri) as? KMTransitionInfo info?.transitionStyle = _value } } } else if key == KMDurationName { let _value = value as? Float ?? 0 if self.separate == false { self.transition.duration = _value } else { guard let trans = self.transitions else { return } let ris = self.tableView.selectedRowIndexes for ri in ris { if ri >= trans.count { continue } let info = trans.object(at: ri) as? KMTransitionInfo info?.duration = _value } } } else if key == KMShouldRestrictName { let _value = value as? Bool ?? false if self.separate == false { self.transition.shouldRestrict = _value } else { guard let trans = self.transitions else { return } let ris = self.tableView.selectedRowIndexes for ri in ris { if ri >= trans.count { continue } let info = trans.object(at: ri) as? KMTransitionInfo info?.shouldRestrict = _value } } } } // Actions @objc func effectButtonAction(sender: NSPopUpButton) { let index = sender.indexOfSelectedItem let names = SKTransitionController.transitionNames() as? [String] let name = names?.safe_element(for: index) as? String ?? "" let type = SKTransitionController.style(forName: name) guard let infos = self.selectedInfos(), infos.isEmpty == false else { NSSound.beep() return } for info in infos { if info.transitionStyle != type { // undo // let oldValue = info.transitionStyle self.undo_effectAction(info: info, key: KMTransitionStyleName, value: type, popup: nil) } } // self._updateInfo(key: KMTransitionStyleName, value: type) } @objc dynamic func undo_effectAction(info: KMTransitionInfo, key: String, value: Any, popup: NSPopUpButton?) { if key == KMTransitionStyleName { let oldValue = info.transitionStyle (self.undoRedoManager.prepare(withInvocationTarget: self) as AnyObject).undo_effectAction(info: info, key: key, value: oldValue, popup: self.effectPopUpButton) info.transitionStyle = value as? SKAnimationTransitionStyle ?? .noTransition popup?.selectItem(at: Int(info.transitionStyle.rawValue)) } } @objc func extentAction(sender: NSMatrix) { // let isScreen = sender.isEqual(to: self.screeBtn) // self.screeBtn.state = isScreen ? .on : .off // self.pageBtn.state = isScreen ? .off : .on // let value = self.pageBtn. == .on let value = sender.selectedTag() == 1 ? true : false // (self.undoRedoManager.prepare(withInvocationTarget: self) as AnyObject).extentAction(sender: sender) self.undoRedoManager.registerUndo(withTarget: self) { target in if value { target.screeBtn.performClick(nil) } else { target.pageBtn.performClick(nil) } } self._updateInfo(key: KMShouldRestrictName, value: value) } @IBAction func pageTransitionAction(_ sender: NSButton) { self.separate = sender.state == .on self.changePageTransitionType(sep: self.separate) Task { @MainActor in self.tableView.reloadData() } } @IBAction func changeDurationSlider(_ sender: NSSlider) { // self.durationTF.stringValue = sender.stringValue guard let infos = self.selectedInfos(), infos.isEmpty == false else { NSSound.beep() return } for info in infos { if info.duration != sender.floatValue { // undo self.undo_sliderAction(info: info, key: KMDurationName, value: sender.floatValue, slider: nil, textF: self.durationTF) } } // 更新数据 // self._updateInfo(key: KMDurationName, value: sender.floatValue) } @objc func undo_sliderAction(info: KMTransitionInfo, key: String, value: Any, slider: NSSlider?, textF: NSTextField?) { if key == KMDurationName { let oldValue = info.duration (self.undoRedoManager.prepare(withInvocationTarget: self) as AnyObject).undo_sliderAction(info: info, key: key, value: oldValue, slider: self.durationSlider, textF: self.durationTF) let duration = value as? Float ?? 0 slider?.floatValue = duration textF?.floatValue = duration } } @IBAction func cancelAction(_ sender: Any) { if let handle = self.closeCallBack { handle(self) } } @IBAction func okAction(_ sender: Any) { if let handle = self.closeCallBack { handle(self) } if self.undoRedoManager.canUndo { self.transitionController?.transitionStyle = transition.transitionStyle self.transitionController?.duration = CGFloat(transition.duration) self.transitionController?.shouldRestrict = transition.shouldRestrict self.transitionController?.pageTransitions = pageTransitions as? [Any] (self.controller?.undoManager)?.setActionName(NSLocalizedString("Change Transitions", comment: "Undo action name")) } // self.controller?.myDocument/*presentationNotesDocument*/ = notesDocument } // Noti Actions @objc private func handleDocumentsDidChangeNotification(notification: Notification?) { let currentDoc = self.notesDocumentPopUpButton.selectedItem?.representedObject as? NSDocument while self.notesDocumentPopUpButton.numberOfItems > 1 { self.notesDocumentPopUpButton.removeItem(at: self.notesDocumentPopUpButton.numberOfItems - 1) } // var doc: NSDocument? let document = self.controller?.myDocument let documents = NSMutableArray() let pageCount = self.controller?.document?.pageCount ?? 0 for doc in NSDocumentController.shared.documents { guard let mainDoc = doc as? KMMainDocument else { continue } if mainDoc.isHome { continue } if mainDoc.isEqual(to: document) { continue } if let pdfDoc = mainDoc.mainViewController?.document { if pdfDoc.pageCount == pageCount { documents.add(doc) } } } let sortDescriptor = NSSortDescriptor(key: "displayName", ascending: true) documents.sort(using: [sortDescriptor]) for doc in documents { self.notesDocumentPopUpButton.addItem(withTitle: (doc as AnyObject).displayName) self.notesDocumentPopUpButton.lastItem?.representedObject = doc } let docIndex = self.notesDocumentPopUpButton.indexOfItem(withRepresentedObject: currentDoc) self.notesDocumentPopUpButton.selectItem(at: docIndex == -1 ? 0 : docIndex) } } // MARK: - NSTextFieldDelegate extension SKPresentationOptionsSheetController: NSTextFieldDelegate { func controlTextDidChange(_ obj: Notification) { if self.durationTF.isEqual(to: obj.object) { self.durationSlider.floatValue = self.durationTF.floatValue // 更新数据 self._updateInfo(key: KMDurationName, value: self.durationTF.floatValue) } } } // MARK: - NSWindowDelegate extension SKPresentationOptionsSheetController: NSWindowDelegate { func windowWillReturnUndoManager(_ window: NSWindow) -> UndoManager? { return self.undoRedoManager } } // MARK: - SKTransitionControllerDelegate extension SKPresentationOptionsSheetController: SKTransitionControllerDelegate { func transitionController(_ controller: SKTransitionController!, valueDidChanged info: [AnyHashable : Any]!) { // 消息转发 self.proxyDelegate_?.transitionController(controller, valueDidChanged: info) } } // MARK: - NSTableViewDelegate, NSTableViewDataSource extension SKPresentationOptionsSheetController: NSTableViewDelegate, NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { return self.transitions?.count ?? 0 } func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { let identifier = tableColumn?.identifier let info = transitions?[row] as? KMTransitionInfo if let id = identifier?.rawValue, id == PAGE_COLUMNID { let cell = tableColumn?.dataCell as? NSTextFieldCell cell?.stringValue = info?.label ?? "" return info?.label ?? "" } else if let id = identifier?.rawValue, id == IMAGE_COLUMNID { let cell = tableColumn?.dataCell as? NSImageCell let page = self.controller?.listView.document.page(at: UInt(info?.thumbnail?.pageIndex ?? 0)) if let data = page?.PDFListViewTIFFData(for: page?.bounds ?? .zero) { let image = NSImage(data: data) cell?.image = image return image } } return nil } func tableViewSelectionDidChange(_ notification: Notification) { if self.tableView.isEqual(to: notification.object) == false { return } let ris = self.tableView.selectedRowIndexes if ris.isEmpty { return } var info: KMTransitionInfo? if ris.count == 1 { info = self.transitions?[ris.first ?? 0] as? KMTransitionInfo self.pageTransitionBox.title = KMLocalizedString("Page Transition") + (info?.label ?? "") // self.effectPopUpButton.selectItem(at: Int(info?.transitionStyle.rawValue ?? 0)) // let durationV = info?.duration ?? 0 // self.durationSlider.animator().floatValue = durationV // self.durationTF.floatValue = durationV // let shouldRestrict = info?.shouldRestrict ?? false // if shouldRestrict { // 1 // self.pageBtn.state = .on // self.screeBtn.state = .off // } else { // self.screeBtn.state = .on // self.pageBtn.state = .off // } } else { self.pageTransitionBox.title = KMLocalizedString("Page Transition") info = self.transition } self.effectPopUpButton.selectItem(at: Int(info?.transitionStyle.rawValue ?? 0)) let durationV = info?.duration ?? 0 self.durationSlider.animator().floatValue = durationV self.durationTF.floatValue = durationV let shouldRestrict = info?.shouldRestrict ?? false if shouldRestrict { // 1 self.pageBtn.state = .on self.screeBtn.state = .off } else { self.screeBtn.state = .on self.pageBtn.state = .off } } func tableView(_ tableView: NSTableView, writeRowsWith rowIndexes: IndexSet, to pboard: NSPasteboard) -> Bool { if rowIndexes.count == 1 { pboard.clearContents() pboard.writeObjects([transitions?[rowIndexes.first ?? 0] as! NSPasteboardWriting]) return true } else { return false } } func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation { if info.draggingPasteboard.canReadObject(forClasses: [KMTransitionInfo.self], options: [:]) { if dropOperation == .above { tableView.setDropRow(-1, dropOperation: .on) } return .every } return [] } func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool { let pboard = info.draggingPasteboard if dropOperation == .on { guard let infos = pboard.readObjects(forClasses: [KMTransitionInfo.self], options: [:]) as? [KMTransitionInfo], infos.count > 0 else { return false } let propertie: NSDictionary? = infos[0].properties if row == -1 { transitions?.setValue(propertie, forKey: PROPERTIES_KEY) } else { let transit: KMTransitionInfo = transitions?[row] as! KMTransitionInfo transit.properties = propertie } return true } return false } } extension SKPresentationOptionsSheetController: KMBotaTableViewDelegate { func tableView(_ aTableView: NSTableView, imageContextForRow rowIndex: Int) -> AnyObject? { return controller?.document?.page(at: UInt(rowIndex)) } func tableView(_ aTableView: NSTableView, copyRowsWithIndexes rowIndexes: IndexSet) { let pboard = NSPasteboard.general pboard.clearContents() pboard.writeObjects([transitions?[rowIndexes.first ?? 0] as! NSPasteboardWriting]) } func tableView(_ aTableView: NSTableView, canCopyRowsWithIndexes rowIndexes: IndexSet) -> Bool { return true } func tableView(_ aTableView: NSTableView, pasteFromPasteboard pboard: NSPasteboard) { guard let infos = pboard.readObjects(forClasses: [KMTransitionInfo.self], options: [:]) as? [KMTransitionInfo], infos.count > 0 else { return } let arr = transitions?.objects(at: tableView.selectedRowIndexes) as? NSArray arr?.setValue(infos[0].properties, forKey: PROPERTIES_KEY) } func tableView(_ aTableView: NSTableView, canPasteFromPasteboard pboard: NSPasteboard) -> Bool { return (tableView.selectedRow != -1 && pboard.canReadObject(forClasses: [KMTransitionInfo.self], options: [:])) } func tableView(_ aTableView: NSTableView, typeSelectHelperSelectionStrings aTypeSelectHelper: SKTypeSelectHelper) -> NSArray { return transitions?.value(forKeyPath: "thumbnail.label") as! NSArray } }