// // KMAnnotationLinkViewController.swift // PDF Master // // Created by wanjun on 2023/10/11. // import Cocoa let URLPlaceholder = "https://www.pdfreaderpro.com" let EmailPlaceholder = "support@pdfreaderpro.com" enum KMAnnotationLinkType: Int { case Page case URL case Email } enum KMAnnotationLinkState: Int { case Normal case Hover case Selected } class KMAnnotationLinkViewController: KMAnnotationPropertyBaseController { var annotationModel: CPDFAnnotationModel? var _content: String = "" var pageCount: Int = 0 var _isCreateLink: Bool = false @IBOutlet weak var linkStyleView: NSView! @IBOutlet var linkPageBox: KMBox! @IBOutlet var linkPageImageView: NSImageView! @IBOutlet weak var pageImageThumible: NSImageView! @IBOutlet weak var targetButton: KMCoverButton! @IBOutlet weak var targetLabel: NSTextField! @IBOutlet weak var tagrgetBox: NSBox! @IBOutlet var linkUrlBox: KMBox! @IBOutlet var linkUrlImageView: NSImageView! @IBOutlet var linkEmailBox: KMBox! @IBOutlet var linkEmailImageView: NSImageView! @IBOutlet var contentBox: NSBox! @IBOutlet var urlView: NSView! @IBOutlet var urlLabel: NSTextField! @IBOutlet var inputUrlTextField: NSTextField! @IBOutlet weak var inputUrlBox: NSBox! @IBOutlet var goButton: NSButton! @IBOutlet var errorLabel: NSTextField! @IBOutlet weak var goButtonTopComstraint: NSLayoutConstraint! var annotation: CPDFLinkAnnotation? var _linkType: KMAnnotationLinkType = .Page var mouseDownBox: KMBox? var pageRecord: String = "" var urlRecord: String = "" var emailRecord: String = "" var startPage: String = "" var isGo: Bool = false var boxNormalBorderColor: NSColor = NSColor(red: 223.0/255.0, green: 225.0/255.0, blue: 229.0/255.0, alpha: 1) var boxErrorBorderColor: NSColor = NSColor(red: 243/255.0, green: 70/255.0, blue: 91/255.0, alpha: 1) var targetButtonState: KMAnnotationLinkState = .Normal //MARK: Init Methods override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) self.boxNormalBorderColor = NSColor(red: 223.0/255.0, green: 225.0/255.0, blue: 229.0/255.0, alpha: 1) self.boxErrorBorderColor = NSColor(red: 243/255.0, green: 70/255.0, blue: 91/255.0, alpha: 1) } required init?(coder: NSCoder) { super.init(coder: coder) } deinit { NotificationCenter.default.removeObserver(self) inputUrlTextField.delegate = nil print("\(#function)") } //MARK: View Methods override func viewDidLoad() { super.viewDidLoad() // Do view setup here. self.annotation = self.annotationModel?.annotation as? CPDFLinkAnnotation self.pageRecord = "" self.urlRecord = "" self.emailRecord = "" self.linkStyleView.wantsLayer = true self.linkPageImageView.wantsLayer = true self.linkUrlImageView.wantsLayer = true self.linkEmailImageView.wantsLayer = true self.linkStyleView.layer?.backgroundColor = NSColor(red: 223.0/255.0, green: 225.0/255.0, blue: 229.0/255.0, alpha: 1.0).cgColor self.linkStyleView.layer?.cornerRadius = 6.0 self.linkStyleView.layer?.masksToBounds = true let boxArr: [KMBox] = [linkPageBox, linkUrlBox, linkEmailBox] for box in boxArr { box.downCallback = { [weak self] downEntered, mouseBox, event in guard let self = self else { return } if downEntered { if self.mouseDownBox === mouseBox { return } var mouseDownInt = 0 if self.linkPageBox === mouseBox { self.inputUrlTextField.stringValue = "" self.linkType = .Page mouseDownInt = 0 } else if self.linkUrlBox === mouseBox { self.inputUrlTextField.stringValue = "" self.linkType = .URL mouseDownInt = 1 } else if self.linkEmailBox === mouseBox { self.inputUrlTextField.stringValue = "" self.linkType = .Email mouseDownInt = 2 } UserDefaults.standard.set(mouseDownInt, forKey: "kmLinkSelectIndex") UserDefaults.standard.synchronize() self.mouseDownBox = mouseBox } } } self.isGo = true self.targetLabel.stringValue = NSLocalizedString("Locate the target page", tableName: "MainMenu", comment: "") self.inputUrlTextField.wantsLayer = true self.inputUrlTextField.focusRingType = .none self.linkPageImageView.layer?.cornerRadius = 4.0 self.linkUrlImageView.layer?.cornerRadius = 4.0 self.linkEmailImageView.layer?.cornerRadius = 4.0 let inputUrlTextFieldCell = self.inputUrlTextField.cell as! NSTextFieldCell inputUrlTextFieldCell.allowedInputSourceLocales = [NSAllRomanInputSourcesLocaleIdentifier] if self.linkType == .Email { self.linkType = .Email self.mouseDownBox = linkEmailBox } else if linkType == .URL { self.linkType = .URL self.mouseDownBox = linkUrlBox } else if linkType == .Page { self.linkType = .Page self.mouseDownBox = linkPageBox } self.targetButton.coverAction = { [weak self] button, action in guard let self = self, self.targetButtonState != .Selected else { return } if action == .enter { self.updateTargetBoxState(state: .Hover) } else if action == .exit { self.updateTargetBoxState(state: .Normal) } } NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in guard let self = self else { return event } if event.keyCode == 53 { if self.inputUrlTextField.isEditable { self.pdfview?.annotationType = .unkown } } return event } } override func viewDidAppear() { super.viewDidAppear() self.inputUrlTextField.delegate = self } override func setupUI() { super.setupUI() let fontName = "SFProText-Regular" self.urlLabel.font = NSFont(name: fontName, size: 12) self.urlLabel.textColor = NSColor(red: 97.0/255.0, green: 100.0/255.0, blue: 105.0/255.0, alpha: 1) self.inputUrlBox.borderColor = self.boxNormalBorderColor self.inputUrlBox.fillColor = NSColor.white self.errorLabel.textColor = self.boxErrorBorderColor self.errorLabel.font = NSFont(name: fontName, size: 12) self.tagrgetBox.fillColor = NSColor.white self.updateTargetBoxState(state: .Normal) self.targetLabel.font = NSFont(name: fontName, size: 14) self.targetLabel.textColor = NSColor(red: 37/255.0, green: 38/255.0, blue: 41/255.0, alpha: 1) self.goButton.title = NSLocalizedString("Go", comment: "") self.goButton.attributedTitle = NSAttributedString(string: self.goButton.title, attributes: [.foregroundColor: NSColor(red: 23.0/255.0, green: 112.0/255.0, blue: 244.0/255.0, alpha: 1)]) } //MARK: Set、Get var linkType: KMAnnotationLinkType { get { return _linkType } set { _linkType = newValue switch _linkType { case .Page: self.createLinkPageView() self.inputUrlTextField.becomeFirstResponder() case .URL: self.createLinkURLView() self.inputUrlTextField.becomeFirstResponder() case .Email: self.createLinkEmailView() self.inputUrlTextField.becomeFirstResponder() default: break } } } var content: String { get { return _content } set { _content = newValue if _content != "" { if _content.count > 0 { let typeLocal = _content.first ?? Character("\0") if typeLocal == "0" { _content = (_content as NSString).substring(from: 1) self.linkType = .Page self.inputUrlTextField.stringValue = _content self.mouseDownBox = self.linkPageBox } else { if content.dropFirst().hasPrefix("mailto:") { _content = (_content as NSString).substring(from: 8) self.linkType = .Email self.mouseDownBox = self.linkEmailBox } else { _content = (_content as NSString).substring(from: 1) self.linkType = .URL self.mouseDownBox = self.linkUrlBox } } } } else { _content = "http://" self.linkType = .URL self.mouseDownBox = self.linkUrlBox } } } override var pdfview: CPDFListView? { get { return _pdfview } set { _pdfview = newValue self.startPage = String(format: "%ld", _pdfview!.currentPageIndex+1) } } var isCreateLink: Bool { get { return _isCreateLink } set { _isCreateLink = newValue if _isCreateLink { self.linkType = .Page self.mouseDownBox = self.linkPageBox } } } //MARK: private func createLinkPageView() { self.annotationLinkSelectBox(self.linkPageBox) self.urlLabel.stringValue = NSLocalizedString("Page", comment: "") self.inputUrlTextField.formatter = TextFieldFormatter() if self.pdfview!.document?.pageCount ?? 0 > 1 { self.inputUrlTextField.placeholderString = NSLocalizedString("Enter target page", comment: "") } else { self.inputUrlTextField.placeholderString = "1" } if let destination = self.annotation!.destination() { let pageIndex = destination.pageIndex self.inputUrlTextField.stringValue = "\(pageIndex + 1)" self.pageRecord = "\(pageIndex + 1)" } else { self.pageRecord = "1" } if self.inputUrlTextField.stringValue.count == 0 { self.inputUrlTextField.stringValue = self.pageRecord } self.tagrgetBox.isHidden = false self.pageImageThumible.isHidden = true self.goButton.isHidden = true self.goButtonTopComstraint.constant = 20 self.errorLabel.stringValue = NSLocalizedString("Page number out of range", comment: "") if self.inputUrlTextField.stringValue.count == 0 { } else { if let dexPage = Int(self.inputUrlTextField.stringValue) { self.dealThumibleImage(dexPage: UInt(dexPage)) } } } func createLinkURLView() { self.annotationLinkSelectBox(self.linkUrlBox) self.urlLabel.stringValue = NSLocalizedString("URL:", comment: "") self.inputUrlTextField.formatter = nil self.inputUrlTextField.placeholderString = "https://www.pdfreaderpro.com" if let urlString = self.annotation?.url() { var modifiedURL = urlString if modifiedURL.hasPrefix("http://") { modifiedURL = (modifiedURL as NSString).substring(from: 7) } else if modifiedURL.hasPrefix("https://://") { modifiedURL = (modifiedURL as NSString).substring(from: 8) } self.inputUrlTextField.stringValue = modifiedURL self.urlRecord = modifiedURL } if self.inputUrlTextField.stringValue.count == 0 { self.inputUrlTextField.stringValue = self.urlRecord } self.tagrgetBox.isHidden = true self.pageImageThumible.isHidden = true self.goButton.isHidden = true self.goButtonTopComstraint.constant = -390 self.errorLabel.stringValue = NSLocalizedString("Invalid Email. Please re-enter correct email.", comment: "") self.goButton.title = NSLocalizedString("Go", comment: "") self.goButton.attributedTitle = NSAttributedString(string: goButton.title, attributes: [NSAttributedString.Key.foregroundColor: NSColor(red: 23.0/255.0, green: 112.0/255.0, blue: 244.0/255.0, alpha: 1)]) if self.inputUrlTextField.stringValue.count > 0 { self.goButton.isHidden = false } } func createLinkEmailView() { self.annotationLinkSelectBox(self.linkEmailBox) self.urlLabel.stringValue = NSLocalizedString("Email:", comment: "") self.inputUrlTextField.formatter = nil self.inputUrlTextField.placeholderString = "support@pdfreaderpro.com" if let urlString = annotation?.url(), urlString.contains("mailto") { if urlString.hasPrefix("mailto:") { let email = String(urlString.suffix(from: urlString.index(urlString.startIndex, offsetBy: 7))) inputUrlTextField.stringValue = email emailRecord = email } if inputUrlTextField.stringValue.isEmpty { inputUrlTextField.stringValue = emailRecord } } tagrgetBox.isHidden = true pageImageThumible.isHidden = true goButton.isHidden = true goButtonTopComstraint.constant = -390 errorLabel.stringValue = NSLocalizedString("Invalid Email. Please re-enter correct email.", comment: "") goButton.title = NSLocalizedString("Go", comment: "") goButton.attributedTitle = NSAttributedString(string: goButton.title, attributes: [ .foregroundColor: NSColor(red: 23.0/255.0, green: 112.0/255.0, blue: 244.0/255.0, alpha: 1) ]) if !inputUrlTextField.stringValue.isEmpty { goButton.isHidden = false } } func isValidateEmail(_ email: String) -> Bool { let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}" let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegex) return emailTest.evaluate(with: email) } func judgeWebURL(_ urlString: String) -> String { var updatedURL = urlString if !updatedURL.hasPrefix("http://") && !updatedURL.hasPrefix("https://") { updatedURL = "https://" + updatedURL } if updatedURL == "https://" { updatedURL = "" } return updatedURL } func judgeEmailURL(_ urlString: String) -> String { var updatedURL = urlString if !updatedURL.hasPrefix("mailto:") { updatedURL = "mailto:" + updatedURL } if updatedURL == "mailto:" { updatedURL = "" } return updatedURL } func annotationLinkSelectBox(_ box: KMBox) { if box.isEqual(self.linkPageBox) { self.linkPageBox.fillColor = NSColor.white self.linkEmailBox.fillColor = NSColor.clear self.linkUrlBox.fillColor = NSColor.clear self.linkPageImageView.image = NSImage(named: "KMImageNamePropertybarLinkPageSel") self.linkUrlImageView.image = NSImage(named: "KMImageNamePropertybarLinkUrlNor") self.linkEmailImageView.image = NSImage(named: "KMImageNamePropertybarLinkEmailNor") let dexPage = Int(self.pageRecord) ?? 0 if self.pageRecord.count < 1 { return } if dexPage < 1 || dexPage > self.pageCount { self.errorLabel.isHidden = false } else { self.errorLabel.isHidden = true } } else if box.isEqual(self.linkUrlBox) { self.linkUrlBox.fillColor = NSColor.white self.linkEmailBox.fillColor = NSColor.clear self.linkPageBox.fillColor = NSColor.clear self.linkPageImageView.image = NSImage(named: "KMImageNamePropertybarLinkPageNor") self.linkUrlImageView.image = NSImage(named: "KMImageNamePropertybarLinkUrlSel") self.linkEmailImageView.image = NSImage(named: "KMImageNamePropertybarLinkEmailNor") self.errorLabel.isHidden = true } else if box.isEqual(self.linkEmailBox) { self.linkEmailBox.fillColor = NSColor.white self.linkUrlBox.fillColor = NSColor.clear self.linkPageBox.fillColor = NSColor.clear self.linkPageImageView.image = NSImage(named: "KMImageNamePropertybarLinkPageNor") self.linkUrlImageView.image = NSImage(named: "KMImageNamePropertybarLinkUrlNor") self.linkEmailImageView.image = NSImage(named: "KMImageNamePropertybarLinkEmailSel") self.errorLabel.isHidden = true } self.updateInputUrlBoxState() } func updateInputUrlBoxState() { DispatchQueue.main.async { if !self.errorLabel.isHidden { self.inputUrlBox.borderColor = self.boxErrorBorderColor } else { self.inputUrlBox.borderColor = self.boxNormalBorderColor } } } func updateTargetBoxState(state: KMAnnotationLinkState) { self.targetButtonState = state if Thread.isMainThread { if state == .Normal { self.tagrgetBox.borderColor = self.boxNormalBorderColor } else if state == .Hover { self.tagrgetBox.borderColor = NSColor(red: 104/255.0, green: 172/255.0, blue: 248/255.0, alpha: 1.0) } else if state == .Selected { self.tagrgetBox.borderColor = NSColor(red: 23/255.0, green: 112/255.0, blue: 244/255.0, alpha: 1.0) } } else { DispatchQueue.main.async { if state == .Normal { self.tagrgetBox.borderColor = self.boxNormalBorderColor } else if state == .Hover { self.tagrgetBox.borderColor = NSColor(red: 104/255.0, green: 172/255.0, blue: 248/255.0, alpha: 1.0) } else if state == .Selected { self.tagrgetBox.borderColor = NSColor(red: 23/255.0, green: 112/255.0, blue: 244/255.0, alpha: 1.0) } } } } func dealThumibleImage(dexPage: UInt) { if dexPage > 0 && dexPage <= self.pageCount && self.pageRecord.count > 0 { self.pageImageThumible.isHidden = false self.goButton.isHidden = false let goPage = Int(self.inputUrlTextField.stringValue)! - 1 if let page = self.pdfview?.document.page(at: UInt(goPage)) { self.pageImageThumible.image = page.thumbnail(of: page.bounds(for: .mediaBox).size) } } else { self.pageImageThumible.isHidden = true self.goButton.isHidden = true } } //MARK: Action @IBAction func goButtonAction(_ sender: NSButton) { switch self.linkType { case .Page: if let linkAnnotation = self.pdfview?.activeAnnotation as? CPDFLinkAnnotation { linkAnnotation.setURL(nil) var dexPage: UInt = 0 if self.isGo { self.isGo = false self.startPage = "\(self.pdfview!.currentPageIndex + 1)" if let goPageIndex = UInt(inputUrlTextField.stringValue) { dexPage = goPageIndex } goButton.title = NSLocalizedString("Go Back", comment: "") goButton.attributedTitle = NSAttributedString(string: goButton.title, attributes: [ .foregroundColor: NSColor(red: 23.0/255.0, green: 112.0/255.0, blue: 244.0/255.0, alpha: 1) ]) } else { self.isGo = true if let startPageIndex = UInt(self.startPage) { dexPage = startPageIndex } self.goButton.title = NSLocalizedString("Go", comment: "") self.goButton.attributedTitle = NSAttributedString(string: goButton.title, attributes: [ .foregroundColor: NSColor(red: 23.0/255.0, green: 112.0/255.0, blue: 244.0/255.0, alpha: 1) ]) } if dexPage < 1 || dexPage > self.pageCount { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = NSLocalizedString("Invalid page range or the page number is out of range. Please try again.", comment: "") alert.runModal() return } if let goPageIndex = UInt(self.inputUrlTextField.stringValue), let destination = self.targetDestination ?? CPDFDestination(document: self.pdfview!.document, pageIndex: Int(goPageIndex) - 1) { linkAnnotation.setDestination(destination) self.pdfview!.setNeedsDisplay(self.annotation) if self.isGo, let startDest = self.startDestination { self.pdfview!.go(to: startDest) return } else if let targetDest = targetDestination { self.pdfview!.go(to: targetDest) return } if let dest = CPDFDestination(document: self.pdfview!.document, pageIndex: Int(dexPage) - 1) { self.pdfview!.go(toPageIndex: dest.pageIndex, animated: true) } } } self.updateTargetBoxState(state: .Selected) self.pdfview!.isSetLinkDestinationArea = true case .URL: if let linkAnnotation = self.pdfview!.activeAnnotation as? CPDFLinkAnnotation { linkAnnotation.setDestination(nil) let linkUrlPath = self.judgeWebURL(inputUrlTextField.stringValue) linkAnnotation.setURL(linkUrlPath) self.pdfview!.setNeedsDisplay(self.annotation) if let url = URL(string: linkAnnotation.url() ?? "") { NSWorkspace.shared.open(url) } } case .Email: if let linkAnnotation = self.pdfview?.activeAnnotation as? CPDFLinkAnnotation { linkAnnotation.setDestination(nil) if !isValidateEmail(inputUrlTextField.stringValue) { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = NSLocalizedString("Invalid email address. Please enter a valid email address.", comment: "") alert.runModal() return } let linkUrlPath = self.judgeEmailURL(self.inputUrlTextField.stringValue) linkAnnotation.setURL(linkUrlPath) self.pdfview!.setNeedsDisplay(self.annotation) if let url = URL(string: linkAnnotation.url() ?? "") { NSWorkspace.shared.open(url) } } default: break } } } extension KMAnnotationLinkViewController: NSTextFieldDelegate { @objc func controlTextDidChange(_ obj: Notification) { guard let textField = obj.object as? NSTextField else { return } self.errorLabel.isHidden = true if self.mouseDownBox == self.linkPageBox { self.pageRecord = textField.stringValue } else if self.mouseDownBox == self.linkUrlBox { self.urlRecord = textField.stringValue } else if mouseDownBox == linkEmailBox { self.emailRecord = textField.stringValue } if self.linkType == .Page { (self.pdfview?.activeAnnotation as! CPDFLinkAnnotation as CPDFLinkAnnotation).setURL(nil) let dexPage = Int(self.inputUrlTextField.stringValue) ?? 1 if (dexPage < 1 || dexPage > self.pageCount) && self.pageRecord.count > 0 { (self.pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setDestination(nil) self.errorLabel.isHidden = false self.dealThumibleImage(dexPage: UInt(dexPage)) self.updateInputUrlBoxState() return } else if self.pageRecord.count > 0 { self.errorLabel.isHidden = true } let page: CPDFPage = (self.pdfview?.document.page(at: UInt(dexPage - 1)))! let destination: CPDFDestination = CPDFDestination(document: self.pdfview!.document, pageIndex: dexPage - 1, at: CGPoint(x: 0, y: page.bounds.size.height), zoom: self.pdfview!.scaleFactor) (self.pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setDestination(destination) self.pdfview?.setNeedsDisplayAnnotationViewFor(self.pdfview?.activeAnnotation.page) // 显示预览图 self.dealThumibleImage(dexPage: UInt(dexPage)) self.updateInputUrlBoxState() } else if self.linkType == .URL { (pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setDestination(nil) let linkUrlPath: String = self.urlRecord if linkUrlPath == "" { (self.pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setURL(nil) } else { (self.pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setURL(linkUrlPath) } self.pdfview?.setNeedsDisplayAnnotationViewFor(self.pdfview?.activeAnnotation.page) } else if self.linkType == .Email { (self.pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setDestination(nil) let linkUrlPath = self.judgeEmailURL(self.emailRecord) if linkUrlPath == "" { (self.pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setURL(nil) } else { (self.pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setURL(linkUrlPath) } self.pdfview?.setNeedsDisplayAnnotationViewFor(pdfview?.activeAnnotation.page) } } func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { if commandSelector == #selector(insertNewline(_:)) { self.view.window?.makeFirstResponder(self.pdfview) return true } return false } override func mouseDown(with event: NSEvent) { super.mouseDown(with: event) self.view.window?.makeFirstResponder(self.pdfview) } }