// // KMNDisplayViewController.swift // PDF Reader Pro // // Created by Niehaoyu on 2024/10/27. // import Cocoa import KMComponentLibrary @objc protocol KMNDisplayViewControllerDelegate: AnyObject { //Display Mode @objc optional func displayViewControllerDidDisplayModeChanged(_ controller: KMNDisplayViewController) //阅读模式 @objc optional func displayViewControllerDidReadModeUpdated(_ controller: KMNDisplayViewController) //PPT @objc optional func displayViewControllerDidGotoSlideShow(_ controller: KMNDisplayViewController) //SplitView @objc optional func displayViewControllerDidSplitModeChanged(_ controller: KMNDisplayViewController) //SplitView @objc optional func displayViewControllerDidSplitFileChanged(_ controller: KMNDisplayViewController) //SplitView-showToolbar @objc optional func displayViewControllerDidToolbarStateChanged(_ controller: KMNDisplayViewController) } class KMNDisplayViewController: NSViewController { @IBOutlet var scrollView: NSScrollView! @IBOutlet var contendBox: NSBox! @IBOutlet var titleLabel: NSTextField! @IBOutlet var displayModeView: NSView! @IBOutlet var displayModeLabel: NSTextField! @IBOutlet var singlePageSelector: ComponentCSelector! @IBOutlet var twoPageSelector: ComponentCSelector! @IBOutlet var bookSelector: ComponentCSelector! @IBOutlet var readModeSelector: ComponentCSelector! @IBOutlet var fullScreenSelector: ComponentCSelector! @IBOutlet var slideShowSelector: ComponentCSelector! @IBOutlet var continueScrollCheckBox: ComponentCheckBox! @IBOutlet var pageBreaksCheckBox: ComponentCheckBox! //Split @IBOutlet var splitViewBGView: NSView! @IBOutlet var splitViewLabel: NSTextField! @IBOutlet var noSplitSelector: ComponentCSelector! @IBOutlet var verticalSelector: ComponentCSelector! @IBOutlet var horizontalSelector: ComponentCSelector! @IBOutlet var split_selFileButton: ComponentButton! @IBOutlet var split_fileInfoView: NSView! @IBOutlet var split_fileLabel: NSTextField! @IBOutlet var split_syncScrollCheckbox: ComponentCheckBox! @IBOutlet var split_showToolbarCheckbox: ComponentCheckBox! @IBOutlet var splitViewBGHeightConst: NSLayoutConstraint! @IBOutlet var split_SyncScrollTopConst: NSLayoutConstraint! //Themes @IBOutlet var themesBGView: NSView! @IBOutlet var themesLabel: NSTextField! @IBOutlet var themesContendView: NSView! @IBOutlet var scrollViewHeightConst: NSLayoutConstraint! @IBOutlet var boxHeightConst: NSLayoutConstraint! private var themesArray: [NSColor] = [] private var themeEditItem: ComponentCColorItem? var isCustomSelected = false //防止多个相同的自定义颜色item,出现同时选中的情况, var pdfView: CPDFListView? var viewManager: KMPDFViewManager? weak open var delegate: KMNDisplayViewControllerDelegate? deinit { print("\n\(self.className) deinit.\n") } override func viewWillLayout() { super.viewWillLayout() if pdfView?.viewSplitMode == .disable { //502 scrollViewHeightConst.constant = min(boxHeightConst.constant+5, self.view.frame.size.height) } else { if let _ = viewManager?.splitPDFFileURL { //614 scrollViewHeightConst.constant = min(boxHeightConst.constant+5, self.view.frame.size.height) } else { scrollViewHeightConst.constant = min(boxHeightConst.constant+5, self.view.frame.size.height) } } if view.window?.styleMask.contains(.fullScreen) == true { fullScreenSelector.properties.state = .pressed } else { fullScreenSelector.properties.state = .normal } fullScreenSelector.reloadData() } override func viewDidLoad() { super.viewDidLoad() // Do view setup here. scrollView.documentView = contendBox scrollView.backgroundColor = NSColor.clear setupPropertys() reloadData() } func setupPropertys() { view.wantsLayer = true view.layer?.backgroundColor = ComponentLibrary.shared.getComponentColorFromKey("colorBg/layout-middle").cgColor titleLabel.textColor = ComponentLibrary.shared.getComponentColorFromKey("colorText/2") titleLabel.font = ComponentLibrary.shared.getFontFromKey("mac/body-m-bold") titleLabel.stringValue = KMLocalizedString("View") displayModeLabel.stringValue = KMLocalizedString("Display Mode") displayModeLabel.textColor = ComponentLibrary.shared.getComponentColorFromKey("colorText/2") displayModeLabel.font = ComponentLibrary.shared.getFontFromKey("mac/body-s-medium") singlePageSelector.properties = ComponentCSelectorProperty.init(size: .m, state: .normal, text: KMLocalizedString("Single Page"), iconImage: NSImage(named: "display_singlePage")) twoPageSelector.properties = ComponentCSelectorProperty.init(size: .m, state: .normal, text: KMLocalizedString("Two Page"), iconImage: NSImage(named: "display_twoPage")) bookSelector.properties = ComponentCSelectorProperty.init(size: .m, state: .normal, text: KMLocalizedString("Book"), iconImage: NSImage(named: "display_book")) readModeSelector.properties = ComponentCSelectorProperty.init(size: .m, state: .normal, text: KMLocalizedString("Read Mode"), iconImage: NSImage(named: "display_readMode")) fullScreenSelector.properties = ComponentCSelectorProperty.init(size: .m, state: .normal, text: KMLocalizedString("Full Screen"), iconImage: NSImage(named: "display_fullscreen")) slideShowSelector.properties = ComponentCSelectorProperty.init(size: .m, state: .normal, text: KMLocalizedString("Slide Show"), iconImage: NSImage(named: "display_slideShow")) singlePageSelector.setTarget(self, action: #selector(selectorClicked(_:))) twoPageSelector.setTarget(self, action: #selector(selectorClicked(_:))) bookSelector.setTarget(self, action: #selector(selectorClicked(_:))) readModeSelector.setTarget(self, action: #selector(selectorClicked(_:))) fullScreenSelector.setTarget(self, action: #selector(selectorClicked(_:))) slideShowSelector.setTarget(self, action: #selector(selectorClicked(_:))) continueScrollCheckBox.properties = ComponentCheckBoxProperty(size: .s, state: .normal, text: KMLocalizedString("Continuous Scroll"), checkboxType: .normal) pageBreaksCheckBox.properties = ComponentCheckBoxProperty(size: .s, state: .normal, text: KMLocalizedString("Page Breaks"), checkboxType: .normal) continueScrollCheckBox.setTarget(self, action: #selector(checkBoxClicked(_:))) pageBreaksCheckBox.setTarget(self, action: #selector(checkBoxClicked(_:))) //Split splitViewLabel.stringValue = KMLocalizedString("Split View") splitViewLabel.textColor = ComponentLibrary.shared.getComponentColorFromKey("colorText/2") splitViewLabel.font = ComponentLibrary.shared.getFontFromKey("mac/body-s-medium") noSplitSelector.properties = ComponentCSelectorProperty.init(size: .m, state: .normal, text: KMLocalizedString("No Split"), iconImage: NSImage(named: "display_noSplit")) verticalSelector.properties = ComponentCSelectorProperty.init(size: .m, state: .normal, text: KMLocalizedString("Vertical"), iconImage: NSImage(named: "display_verticalSplit")) horizontalSelector.properties = ComponentCSelectorProperty.init(size: .m, state: .normal, text: KMLocalizedString("Horizontal"), iconImage: NSImage(named: "display_horizontalSplit")) noSplitSelector.setTarget(self, action: #selector(selectorClicked(_:))) verticalSelector.setTarget(self, action: #selector(selectorClicked(_:))) horizontalSelector.setTarget(self, action: #selector(selectorClicked(_:))) split_selFileButton.properties = ComponentButtonProperty(type: .default_tertiary, size: .s, buttonText: KMLocalizedString("Select File"), keepPressState: false) split_selFileButton.setTarget(self, action: #selector(splitSelFileButtonClicked(_:))) split_fileInfoView.wantsLayer = true split_fileInfoView.layer?.borderWidth = 1 if let value = ComponentLibrary.shared.getComponentValueFromKey("radius/xs") { let currentValue = value as! CGFloat split_fileInfoView.layer?.cornerRadius = currentValue } split_fileInfoView.layer?.borderColor = ComponentLibrary.shared.getComponentColorFromKey("colorBorder/4").cgColor split_fileInfoView.layer?.backgroundColor = ComponentLibrary.shared.getComponentColorFromKey("colorFill/4").cgColor split_syncScrollCheckbox.properties = ComponentCheckBoxProperty(size: .s, state: .normal, text: KMLocalizedString("Sync Scroll and Zoom"), checkboxType: .normal) split_showToolbarCheckbox.properties = ComponentCheckBoxProperty(size: .s, state: .normal, text: KMLocalizedString("Show Bottom Toolbar"), checkboxType: .normal) split_syncScrollCheckbox.setTarget(self, action: #selector(checkBoxClicked(_:))) split_showToolbarCheckbox.setTarget(self, action: #selector(checkBoxClicked(_:))) //Themes themesLabel.stringValue = KMLocalizedString("Themes") themesLabel.textColor = ComponentLibrary.shared.getComponentColorFromKey("colorText/2") themesLabel.font = ComponentLibrary.shared.getFontFromKey("mac/body-s-medium") } func reloadData() { let mode = pdfView?.displayMode() ?? .singlePage let book = pdfView?.displaysAsBook ?? false singlePageSelector.properties.state = .normal twoPageSelector.properties.state = .normal bookSelector.properties.state = .normal readModeSelector.properties.state = .normal fullScreenSelector.properties.state = .normal slideShowSelector.properties.state = .normal bookSelector.properties.state = .normal if mode == .singlePage { singlePageSelector.properties.state = .pressed continueScrollCheckBox.properties.checkboxType = .normal } else if mode == .singlePageContinuous { singlePageSelector.properties.state = .pressed continueScrollCheckBox.properties.checkboxType = .selected } else if mode == .twoUp { bookSelector.properties.state = book ? .pressed : .normal twoPageSelector.properties.state = book ? .normal : .pressed continueScrollCheckBox.properties.checkboxType = .normal } else if mode == .twoUpContinuous { bookSelector.properties.state = book ? .pressed : .normal twoPageSelector.properties.state = book ? .normal : .pressed continueScrollCheckBox.properties.checkboxType = .selected } let pageBreaks = pdfView?.displaysPageBreaks ?? false pageBreaksCheckBox.properties.checkboxType = pageBreaks ? .selected : .normal singlePageSelector.reloadData() twoPageSelector.reloadData() bookSelector.reloadData() readModeSelector.reloadData() fullScreenSelector.reloadData() slideShowSelector.reloadData() continueScrollCheckBox.reloadData() pageBreaksCheckBox.reloadData() //SplitView noSplitSelector.properties.state = .normal verticalSelector.properties.state = .normal horizontalSelector.properties.state = .normal if pdfView?.viewSplitMode == .disable { noSplitSelector.properties.state = .pressed } else if pdfView?.viewSplitMode == .horizontal { horizontalSelector.properties.state = .pressed } else if pdfView?.viewSplitMode == .vertical { verticalSelector.properties.state = .pressed } split_syncScrollCheckbox.properties.checkboxType = viewManager?.splitSyncScroll == true ? .selected : .normal split_showToolbarCheckbox.properties.checkboxType = viewManager?.splitShowBottomBar == true ? .selected : .normal if let fileUrl = viewManager?.splitPDFFileURL { split_fileLabel.stringValue = fileUrl.lastPathComponent } noSplitSelector.reloadData() horizontalSelector.reloadData() verticalSelector.reloadData() split_syncScrollCheckbox.reloadData() split_showToolbarCheckbox.reloadData() refreshSplitView() //Themes var data: Data? = nil if let themesData = UserDefaults.standard.object(forKey: "kmPDFViewModeThemesArray") as? Data { data = themesData } let appArray = NSKeyedUnarchiver.unarchiveObject(with: data ?? Data()) as? [Any] ?? [] let mutableArray = appArray if mutableArray.count > 0 { themesArray = mutableArray as! [NSColor] } else { let normalColor = NSColor.white let softColor = NSColor(deviceRed: 245/255.0, green: 237/255.0, blue: 203/255.0, alpha: 1.0) let nightColor = NSColor.black let greenColor = NSColor(deviceRed: 201/255.0, green: 228/255.0, blue: 198/255.0, alpha: 1.0) themesArray = [normalColor, softColor, greenColor, nightColor] } setUpThemesItems() } //MARK: - Split func refreshSplitView() { if pdfView?.viewSplitMode == .disable { splitViewBGHeightConst.constant = 90 split_selFileButton.isHidden = true split_fileInfoView.isHidden = true split_syncScrollCheckbox.isHidden = true split_showToolbarCheckbox.isHidden = true } else { if (viewManager?.splitPDFFileURL) != nil { splitViewBGHeightConst.constant = 246 split_SyncScrollTopConst.constant = 52 split_selFileButton.isHidden = false split_fileInfoView.isHidden = false split_syncScrollCheckbox.isHidden = false split_showToolbarCheckbox.isHidden = false } else { splitViewBGHeightConst.constant = 202 split_SyncScrollTopConst.constant = 8 split_selFileButton.isHidden = false split_fileInfoView.isHidden = true split_syncScrollCheckbox.isHidden = false split_showToolbarCheckbox.isHidden = false } } if pdfView?.viewSplitMode == .disable { boxHeightConst.constant = 502 scrollViewHeightConst.constant = min(boxHeightConst.constant, self.view.frame.size.height) } else { if let _ = viewManager?.splitPDFFileURL { boxHeightConst.constant = 658 + 12 scrollViewHeightConst.constant = min(boxHeightConst.constant, self.view.frame.size.height) } else { boxHeightConst.constant = 614 + 12 scrollViewHeightConst.constant = min(boxHeightConst.constant, self.view.frame.size.height) } } } //MARK: - Themes func setUpThemesItems() { let subviews = themesContendView.subviews for view in subviews { if view is ComponentCColorItem { view.removeFromSuperview() } else if view is ComponentButton { view.removeFromSuperview() } } //MAX = 10 var xValue: CGFloat = 0 var yValue = CGRectGetHeight(themesContendView.frame) - 32 for index in 0...themesArray.count-1 { let color = themesArray[index] if index%5 == 0 { xValue = 0 } if index/5 == 1 { yValue = 0 } let colorItem = ComponentCColorItem.init() colorItem.delegate = self colorItem.frame = CGRectMake(xValue, yValue, 32, 32) colorItem.properties = ComponentCColorProperty(colorType: .color, state: .normal, isCustom: false, color: color) xValue += 32 xValue += 18 if index == 0 { colorItem.properties?.identifier = "normal" } else if index == 1 { colorItem.properties?.identifier = "soft" } else if index == 2 { colorItem.properties?.identifier = "night" } else if index == 3 { colorItem.properties?.identifier = "green" } else { colorItem.properties?.identifier = "custom" } if pdfView?.displayMode == .normal && index == 0 { colorItem.properties?.state = .pressed } else if pdfView?.displayMode == .soft && index == 1 { colorItem.properties?.state = .pressed } else if pdfView?.displayMode == .night && index == 3 { colorItem.properties?.state = .pressed } else if pdfView?.displayMode == .green && index == 2 { colorItem.properties?.state = .pressed } else if pdfView?.displayMode == .custom && color == pdfView?.displayModeCustomColor && isCustomSelected == false { colorItem.properties?.state = .pressed isCustomSelected = true } else { colorItem.properties?.state = .normal } colorItem.reloadData() themesContendView.addSubview(colorItem) } if themesArray.count < 10 { if themesArray.count%5 == 0 { xValue = 0 } if themesArray.count/5 == 1 { yValue = 0 } let addButton = ComponentButton.init() addButton.frame = CGRectMake(xValue, yValue, 32, 32) addButton.properties = ComponentButtonProperty(type: .text_gray, size: .s, onlyIcon: true, icon: NSImage(named: "toolbar_plus"), keepPressState: false) addButton.setTarget(self, action: #selector(themeAddButtonClicked(_:))) themesContendView.addSubview(addButton) } } func refreshColorItems() { let subviews = themesContendView.subviews for view in subviews { if let item = view as? ComponentCColorItem { if item.properties?.identifier == "normal" && pdfView?.displayMode == .normal { item.properties?.state = .pressed } else if item.properties?.identifier == "soft" && pdfView?.displayMode == .soft { item.properties?.state = .pressed } else if item.properties?.identifier == "night" && pdfView?.displayMode == .night { item.properties?.state = .pressed } else if item.properties?.identifier == "green" && pdfView?.displayMode == .green { item.properties?.state = .pressed } else if pdfView?.displayMode == .custom && item.properties?.color == pdfView?.displayModeCustomColor && isCustomSelected == false { item.properties?.state = .pressed isCustomSelected = true } else { item.properties?.state = .normal } item.reloadData() } } } @objc func saveThemeColors() { themesArray.removeAll() let subviews = themesContendView.subviews for view in subviews { if view is ComponentCColorItem { if let color = (view as! ComponentCColorItem).properties?.color { themesArray.append(color) } } } refreshColorItems() let data = NSKeyedArchiver.archivedData(withRootObject: themesArray) UserDefaults.standard.set(data, forKey: "kmPDFViewModeThemesArray") UserDefaults.standard.synchronize() } //MARK: - Action @objc func selectorClicked(_ sender: ComponentCSelector) { if sender == singlePageSelector { singlePageSelector.properties.state = .pressed twoPageSelector.properties.state = .normal bookSelector.properties.state = .normal updatePDFViewDisplayInfo() } else if sender == twoPageSelector { singlePageSelector.properties.state = .normal twoPageSelector.properties.state = .pressed bookSelector.properties.state = .normal updatePDFViewDisplayInfo() } else if sender == bookSelector { singlePageSelector.properties.state = .normal twoPageSelector.properties.state = .normal bookSelector.properties.state = .pressed updatePDFViewDisplayInfo() } else if sender == readModeSelector { if let manager = viewManager { manager.isPDFReadMode = !manager.isPDFReadMode } readModeSelector.properties.state = .normal delegate?.displayViewControllerDidReadModeUpdated?(self) } else if sender == fullScreenSelector { view.window?.toggleFullScreen(nil) if view.window?.styleMask.contains(.fullScreen) == true { fullScreenSelector.properties.state = .pressed } else { fullScreenSelector.properties.state = .normal } fullScreenSelector.reloadData() } else if sender == slideShowSelector { slideShowSelector.properties.state = .normal delegate?.displayViewControllerDidGotoSlideShow?(self) } else if sender == noSplitSelector { pdfView?.viewSplitMode = .disable refreshSplitView() updatePDFSplitViewInfo() } else if sender == verticalSelector { pdfView?.viewSplitMode = .vertical refreshSplitView() updatePDFSplitViewInfo() } else if sender == horizontalSelector { pdfView?.viewSplitMode = .horizontal refreshSplitView() updatePDFSplitViewInfo() } reloadData() } @objc func checkBoxClicked(_ sender: ComponentCheckBox) { if sender == continueScrollCheckBox { updatePDFViewDisplayInfo() } else if sender == pageBreaksCheckBox { pdfView?.displaysPageBreaks = pageBreaksCheckBox.properties.checkboxType == .selected ? true : false updatePDFViewDisplayInfo() } else if sender == split_syncScrollCheckbox { viewManager?.splitSyncScroll = split_syncScrollCheckbox.properties.checkboxType == .selected ? true : false } else if sender == split_showToolbarCheckbox { viewManager?.splitShowBottomBar = split_showToolbarCheckbox.properties.checkboxType == .selected ? true : false delegate?.displayViewControllerDidToolbarStateChanged?(self) } reloadData() } @objc func themeAddButtonClicked(_ sender: ComponentButton) { let color = KMAppearance.Layout.l_1Color() themesArray.append(color) pdfView?.setPageBackgroundColorWith(color, viewMode: .other) setUpThemesItems() let subviews = themesContendView.subviews for view in subviews { if view is ComponentCColorItem { themeEditItem = (view as! ComponentCColorItem) } } showColorPanel() saveThemeColors() } @objc private func showColorPanel() { let panel = NSColorPanel.shared panel.setTarget(self) panel.setAction(#selector(colorPanelAction)) panel.orderFront(nil) } @objc private func colorPanelAction(sender: NSColorPanel) { if let item = themeEditItem { item.properties?.color = sender.color item.reloadData() pdfView?.setPageBackgroundColorWith(sender.color, viewMode: .other) NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(saveThemeColors), object: nil) self.perform(#selector(saveThemeColors), with: nil, afterDelay: 0.35) } } @objc func splitSelFileButtonClicked(_ sender: ComponentButton) { let openPanel = NSOpenPanel() openPanel.allowedFileTypes = ["pdf","PDF"] openPanel.allowsMultipleSelection = false openPanel.beginSheetModal(for: self.view.window!) { [weak self] result in if (result == .OK) { guard let weakSelf = self else { return } let fileURL = openPanel.url let pdfDoc = CPDFDocument(url: fileURL) if let data = pdfDoc?.isLocked, data { DispatchQueue.main.asyncAfter(deadline: .now()+0.5) { NSWindowController.checkPassword(url: fileURL ?? NSURL.fileURL(withPath: ""), type: .owner) { result, pwd in if (pwd.isEmpty == false) { } } } } else { weakSelf.viewManager?.splitPDFFileURL = fileURL weakSelf.reloadData() } weakSelf.delegate?.displayViewControllerDidSplitFileChanged?(weakSelf) } } } //MARK: - Update func updatePDFViewDisplayInfo() { var pdfViewMode: CPDFDisplayViewMode = .singlePage if singlePageSelector.properties.state == .pressed { if continueScrollCheckBox.properties.checkboxType == .selected { pdfViewMode = .singlePageContinuous } pdfView?.setDisplay(pdfViewMode) } else if twoPageSelector.properties.state == .pressed { pdfViewMode = .twoUp if continueScrollCheckBox.properties.checkboxType == .selected { pdfViewMode = .twoUpContinuous } pdfView?.setDisplay(pdfViewMode) } else if bookSelector.properties.state == .pressed { //书本模式 pdfView?.displaysAsBook = true pdfView?.displayTwoUp = true if continueScrollCheckBox.properties.checkboxType == .selected { pdfView?.displayDirection = .vertical } else { pdfView?.displayDirection = .horizontal } } delegate?.displayViewControllerDidDisplayModeChanged?(self) } func updatePDFSplitViewInfo() { delegate?.displayViewControllerDidSplitModeChanged?(self) } //MARK: - Mouse } //MARK: - ComponentCColorDelegate extension KMNDisplayViewController: ComponentCColorDelegate { func componentCColorDidChooseColor(_ view: NSView, _ color: NSColor?) { if view is ComponentCColorItem { if (view as! ComponentCColorItem) != themeEditItem { themeEditItem = nil if NSColorPanel.shared.isVisible { NSColorPanel.shared.setTarget(nil) NSColorPanel.shared.setAction(nil) NSColorPanel.shared.close() } } } let subviews = themesContendView.subviews let index = subviews.firstIndex(of: view) ?? 0 var model: KMPDFViewMode = .normal if index <= 3 { if index == 0 { model = .normal } else if index == 1 { model = .soft } else if index == 2 { model = .green } else if index == 3 { model = .night } } else { model = .other } pdfView?.setPageBackgroundColorWith(color, viewMode: model) for subview in subviews { if subview is ComponentCColorItem { if subview != view { (subview as! ComponentCColorItem).properties?.state = .normal (subview as! ComponentCColorItem).reloadData() } } } } func componentCColorDidRightMouseUpWithStrings(_ view: NSView) -> [String] { let subviews = themesContendView.subviews let index = subviews.firstIndex(of: view) ?? 0 if index <= 3 { } else { if view is ComponentCColorItem { if (view as! ComponentCColorItem) != themeEditItem { themeEditItem = (view as! ComponentCColorItem) } } return [KMLocalizedString("Delete")] } return [] } func componentCColorDidRightMenuItemClicked(_ view: NSView, menuItemProperty: ComponentMenuitemProperty?) { if let item = themeEditItem, let color = item.properties?.color { if let index = themesArray.firstIndex(of: color) { themesArray.remove(at: index) } } self.pdfView?.setPageBackgroundColorWith(NSColor.white, viewMode: .normal) self.setUpThemesItems() self.saveThemeColors() } }