// // KMAnnotationSelectLinkViewController.swift // PDF Reader Pro // // Created by wanjun on 2023/11/23. // import Cocoa let URLPlaceholder = "https://www.pdfreaderpro.com" let EmailPlaceholder = "support@pdfreaderpro.com" enum KMAnnotationLinkType: UInt { case page = 0 case url case email } @objcMembers class KMAnnotationSelectLinkViewController: NSViewController, NSTextFieldDelegate { var _annotations: [CPDFAnnotation]? var pdfDocument: CPDFDocument? weak var _pdfview: CPDFListView? @IBOutlet var linkPageBox: KMBox! @IBOutlet var linkPageImageView: NSImageView! @IBOutlet var linkPageLabel: NSTextField! @IBOutlet var linkUrlBox: KMBox! @IBOutlet var linkUrlImageView: NSImageView! @IBOutlet var linkUrlLabel: NSTextField! @IBOutlet var linkEmailBox: KMBox! @IBOutlet var linkEmailImageView: NSImageView! @IBOutlet var linkEmailLabel: NSTextField! @IBOutlet var contentBox: NSBox! @IBOutlet var contentBoxLayoutConstraint: NSLayoutConstraint! @IBOutlet var pageView: NSView! @IBOutlet var pageLabel: NSTextField! @IBOutlet var inputPageTextField: NSTextField! @IBOutlet var allPageLabel: NSTextField! @IBOutlet var urlView: NSView! @IBOutlet var urlLabel: NSTextField! @IBOutlet var inputUrlTextField: NSTextField! @IBOutlet var goButton: NSButton! @IBOutlet var errorLabel: NSTextField! 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 // MARK: Init Methods deinit { NotificationCenter.default.removeObserver(self) inputUrlTextField.delegate = nil inputPageTextField.delegate = nil } override func viewDidLoad() { super.viewDidLoad() // Do view setup here. pageRecord = "" urlRecord = "" emailRecord = "" linkPageLabel.stringValue = NSLocalizedString("Go To Page", comment: "") linkPageLabel.toolTip = NSLocalizedString("Go To Page", comment: "") linkPageLabel.textColor = KMAppearance.Layout.h1Color() linkUrlLabel.stringValue = NSLocalizedString("Hyperlink", comment: "") linkUrlLabel.toolTip = NSLocalizedString("Hyperlink", comment: "") linkUrlLabel.textColor = KMAppearance.Layout.h1Color() linkEmailLabel.stringValue = NSLocalizedString("Email", comment: "") linkEmailLabel.toolTip = NSLocalizedString("Email", comment: "") linkEmailLabel.textColor = KMAppearance.Layout.h1Color() errorLabel.stringValue = NSLocalizedString("Page number out of range", comment: "") errorLabel.textColor = KMAppearance.Status.errColor() let boxArr: [KMBox] = [linkPageBox, linkUrlBox, linkEmailBox] for box in boxArr { box.moveCallback = { [weak self] mouseEntered, mouseBox in guard let self = self else { return } if mouseEntered { if self.mouseDownBox != mouseBox { if box == self.linkPageBox { self.linkPageBox.fillColor = KMAppearance.Status.selColor() self.linkPageImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkPageSel") self.linkPageLabel.textColor = KMAppearance.Layout.h0Color() } else if box == self.linkUrlBox { self.linkUrlBox.fillColor = KMAppearance.Status.selColor() self.linkUrlImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkUrlSel") self.linkUrlLabel.textColor = KMAppearance.Layout.h0Color() } else if box == self.linkEmailBox { self.linkEmailBox.fillColor = KMAppearance.Status.selColor() self.linkEmailImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkEmailSel") self.linkEmailLabel.textColor = KMAppearance.Layout.h0Color() } } } else { if self.mouseDownBox != mouseBox { box.fillColor = NSColor.clear if box == self.linkPageBox { self.linkPageImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkPage") self.linkPageLabel.textColor = KMAppearance.Layout.h1Color() } else if box == self.linkUrlBox { self.linkUrlImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkUrl") self.linkUrlLabel.textColor = KMAppearance.Layout.h1Color() } else if box == self.linkEmailBox { self.linkEmailImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkEmail") self.linkEmailLabel.textColor = KMAppearance.Layout.h1Color() } } } } box.downCallback = { [weak self] downEntered, mouseBox, event in guard let self = self else { return } if downEntered { if !IAPProductsManager.default().isAvailableAllFunction() { if mouseBox == self.linkUrlBox || mouseBox == self.linkEmailBox { KMPurchaseCompareWindowController.sharedInstance().showWindow(nil) return } } if self.mouseDownBox == mouseBox { return } var mouseDownInt = 0 if mouseBox == self.linkPageBox { self.inputPageTextField.stringValue = "" self.linkType = .page mouseDownInt = 0 } else if mouseBox == self.linkUrlBox { self.inputUrlTextField.stringValue = "" self.linkType = .url mouseDownInt = 1 } else if mouseBox == self.linkEmailBox { self.inputUrlTextField.stringValue = "" self.linkType = .email mouseDownInt = 2 } UserDefaults.standard.set(mouseDownInt, forKey: "kmLinkSelectIndex") UserDefaults.standard.synchronize() self.mouseDownBox = mouseBox } } } isGo = true pageLabel.stringValue = NSLocalizedString("Page Number:", comment: "") pageLabel.textColor = KMAppearance.Layout.h1Color() allPageLabel.textColor = KMAppearance.Layout.h2Color() goButton.title = NSLocalizedString("Go", comment: "") goButton.setTitleColor(KMAppearance.Layout.w0Color()) goButton.wantsLayer = true goButton.layer?.backgroundColor = KMAppearance.Interactive.m0Color().cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color()) inputPageTextField.backgroundColor = KMAppearance.Layout.l1Color() inputUrlTextField.backgroundColor = KMAppearance.Layout.l1Color() inputPageTextField.wantsLayer = true inputUrlTextField.wantsLayer = true inputPageTextField.layer?.borderWidth = 1.0 inputUrlTextField.layer?.borderWidth = 1.0 inputPageTextField.layer?.borderColor = KMAppearance.Interactive.a0Color().cgColor inputUrlTextField.layer?.borderColor = KMAppearance.Interactive.a0Color().cgColor inputPageTextField.layer?.cornerRadius = 1.0 inputUrlTextField.layer?.cornerRadius = 1.0 // inputUrlTextField.inputContext?.allowedInputSourceLocales = [NSAllRomanInputSourcesLocaleIdentifier] let cell = self.inputUrlTextField.cell as? NSTextFieldCell cell?.allowedInputSourceLocales = [NSAllRomanInputSourcesLocaleIdentifier] if let annotationURL = annotation?.url() { var urlString = annotationURL.fileURL.absoluteString if annotationURL.hasPrefix("mailto:") { linkType = .email mouseDownBox = linkEmailBox } else { linkType = .url mouseDownBox = linkUrlBox } } else { linkType = .page mouseDownBox = linkPageBox } } override func viewDidAppear() { super.viewDidAppear() inputPageTextField.delegate = self inputUrlTextField.delegate = self if let url = annotation?.url { inputUrlTextField.becomeFirstResponder() } else { inputPageTextField.becomeFirstResponder() } } // MARK: Get & Set var linkType: KMAnnotationLinkType { set { _linkType = newValue contentBoxLayoutConstraint.constant = 46.0 switch _linkType { case .page: contentBoxLayoutConstraint.constant = 24.0 createLinkPageView() contentBox.contentView = pageView inputPageTextField.becomeFirstResponder() case .url: createLinkURLView() contentBox.contentView = urlView inputUrlTextField.becomeFirstResponder() case .email: createLinkEmailView() contentBox.contentView = urlView inputUrlTextField.becomeFirstResponder() default: break } } get { return _linkType } } weak var pdfview: CPDFListView? { set { _pdfview = newValue // startPage = _pdfview?.currentPage().label self.startPage = "\((self._pdfview?.currentPage().pageIndex() ?? 1)+1)" } get { return _pdfview } } var annotations: [CPDFAnnotation]? { set { _annotations = newValue annotation = _annotations?.first as? CPDFLinkAnnotation } get { return _annotations } } // MARK: Private func getURLViewHeightLayoutConstraint() -> CGFloat { return urlLabel.frame.height + 4 + inputUrlTextField.frame.height } func getPageViewHeightLayoutConstraint() -> CGFloat { return inputPageTextField.frame.height } func createLinkPageView() { annotationLinkSelectBox(linkPageBox) inputPageTextField.formatter = NumberFormatter() if pdfDocument!.pageCount > 1 { inputPageTextField.placeholderString = "1-\(pdfDocument!.pageCount)" } else { inputPageTextField.placeholderString = "1" } allPageLabel.stringValue = "/\(pdfDocument!.pageCount)" if let destination = annotation?.destination() { if let page = destination.page() { let pageIndex = page.document?.index(for: page) ?? 0 inputPageTextField.stringValue = "\(pageIndex + 1)" pageRecord = "\(pageIndex + 1)" } } if inputPageTextField.stringValue.isEmpty { inputPageTextField.stringValue = pageRecord! } let pageFloat = Float(inputPageTextField.stringValue) ?? 0 if pageFloat < 1 || UInt(pageFloat) > self.pdfDocument!.pageCount { goButton.layer!.backgroundColor = KMAppearance.Interactive.m0Color().withAlphaComponent(0.4).cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color().withAlphaComponent(0.4)) } else { goButton.layer!.backgroundColor = KMAppearance.Interactive.m0Color().cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color()) } } func createLinkURLView() { annotationLinkSelectBox(linkUrlBox) urlLabel.stringValue = NSLocalizedString("URL:", comment: "") urlLabel.textColor = KMAppearance.Layout.h1Color() inputUrlTextField.placeholderString = "https://www.pdfreaderpro.com" if let annotationURL = annotation?.url() { var urlString = annotationURL.fileURL.absoluteString if !annotationURL.hasPrefix("mailto:") { if urlString.hasPrefix("http://") { urlString = String(urlString.suffix(from: urlString.index(urlString.startIndex, offsetBy: 7))) } else if urlString.hasPrefix("https://") { urlString = String(urlString.suffix(from: urlString.index(urlString.startIndex, offsetBy: 8))) } inputUrlTextField.stringValue = annotationURL//urlString urlRecord = urlString }else { inputUrlTextField.stringValue = "" } }else { inputUrlTextField.stringValue = "" } if urlRecord!.isEmpty { goButton.layer!.backgroundColor = KMAppearance.Interactive.m0Color().withAlphaComponent(0.4).cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color().withAlphaComponent(0.4)) } else { goButton.layer!.backgroundColor = KMAppearance.Interactive.m0Color().cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color()) } } func createLinkEmailView() { annotationLinkSelectBox(linkEmailBox) urlLabel.stringValue = NSLocalizedString("Email:", comment: "") urlLabel.textColor = KMAppearance.Layout.h1Color() inputUrlTextField.placeholderString = "support@pdfreaderpro.com" if let annotationURL = annotation?.url() { // if let urlString = annotation?.url().fileURL.absoluteString, urlString.hasPrefix("mailto:") { // let trimmedString = String(urlString.suffix(from: urlString.index(urlString.startIndex, offsetBy: 7))) // inputUrlTextField.stringValue = trimmedString // emailRecord = trimmedString // } var urlString = annotationURL.fileURL.absoluteString if annotationURL.hasPrefix("mailto:") { let trimmedString = String(urlString.suffix(from: urlString.index(urlString.startIndex, offsetBy: 7))) inputUrlTextField.stringValue = annotationURL emailRecord = trimmedString } }else { inputUrlTextField.stringValue = "" } if emailRecord!.count > 0 { goButton.layer?.backgroundColor = KMAppearance.Interactive.m0Color().cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color()) } else { goButton.layer?.backgroundColor = KMAppearance.Interactive.m0Color().withAlphaComponent(0.4).cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color().withAlphaComponent(0.4)) } } 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 modifiedURLString = urlString if !modifiedURLString.hasPrefix("http://") && !modifiedURLString.hasPrefix("https://") { modifiedURLString = "https://" + modifiedURLString } if modifiedURLString == "https://" { modifiedURLString = "" } return modifiedURLString } func judgeEmailURL(_ urlString: String) -> String { var modifiedURLString = urlString if !modifiedURLString.hasPrefix("mailto:") { modifiedURLString = "mailto:" + modifiedURLString } if modifiedURLString == "mailto:" { modifiedURLString = "" } return modifiedURLString } func annotationLinkSelectBox(_ box: KMBox) { if box.isEqual(linkPageBox) { linkPageBox.fillColor = KMAppearance.Status.selColor() linkUrlBox.fillColor = NSColor.clear linkEmailBox.fillColor = NSColor.clear linkPageImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkPageSel") linkUrlImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkUrl") linkEmailImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkEmail") linkPageLabel.textColor = KMAppearance.Layout.h0Color() linkUrlLabel.textColor = KMAppearance.Layout.h1Color() linkEmailLabel.textColor = KMAppearance.Layout.h1Color() if let dexPage = Int(pageRecord!), pageRecord!.count >= 1 { if dexPage < 1 || dexPage > self.pdfDocument!.pageCount { errorLabel.isHidden = false } else { errorLabel.isHidden = true } } } else if box.isEqual(linkUrlBox) { linkUrlBox.fillColor = KMAppearance.Status.selColor() linkPageBox.fillColor = NSColor.clear linkEmailBox.fillColor = NSColor.clear linkPageImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkPage") linkUrlImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkUrlSel") linkEmailImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkEmail") linkUrlLabel.textColor = KMAppearance.Layout.h0Color() linkPageLabel.textColor = KMAppearance.Layout.h1Color() linkEmailLabel.textColor = KMAppearance.Layout.h1Color() errorLabel.isHidden = true } else if box.isEqual(linkEmailBox) { linkEmailBox.fillColor = KMAppearance.Status.selColor() linkPageBox.fillColor = NSColor.clear linkUrlBox.fillColor = NSColor.clear linkPageImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkPage") linkUrlImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkUrl") linkEmailImageView.image = NSImage(named: "KMImageNameUXIconPropertybarLinkEmailSel") linkEmailLabel.textColor = KMAppearance.Layout.h0Color() linkPageLabel.textColor = KMAppearance.Layout.h1Color() linkUrlLabel.textColor = KMAppearance.Layout.h1Color() errorLabel.isHidden = true } } // MARK: Public Method // MARK: Action @IBAction func goButtonAction(_ sender: Any) { if linkType == .page { guard let pageFloat = Float(inputPageTextField.stringValue) else { return } if pageFloat < 1 || UInt(pageFloat) > self.pdfDocument!.pageCount { return } } else if linkType == .url { if urlRecord!.count <= 0 { return } } else if linkType == .email { if emailRecord!.count <= 0 { return } } goButton.layer?.backgroundColor = KMAppearance.Interactive.m0Color().cgColor Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(timered(_:)), userInfo: nil, repeats: false) switch linkType { case .page: guard let activeAnnotation = pdfview?.activeAnnotation as? CPDFLinkAnnotation else { return } activeAnnotation.setURL(nil) let dexPage: Int if isGo { isGo = false dexPage = Int(inputPageTextField.stringValue) ?? 0 goButton.title = NSLocalizedString("Go Back", comment: "Tool tip message") } else { isGo = true dexPage = Int(startPage!) ?? 0 goButton.title = NSLocalizedString("Go", comment: "") } goButton.setTitleColor(KMAppearance.Layout.w0Color()) guard dexPage >= 1 && dexPage <= pdfDocument!.pageCount else { 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 } let goPage = Int(inputPageTextField.stringValue) ?? 0 if let page = pdfDocument!.page(at: UInt(goPage - 1)) { let bounds = page.bounds(for: .cropBox) let destination = CPDFDestination(page: page, at: NSPoint(x: 0, y: bounds.size.height)) (pdfview?.activeAnnotation as! CPDFLinkAnnotation as CPDFLinkAnnotation).setDestination(destination) pdfview!.needsDisplay = true } if let page = pdfDocument!.page(at: UInt(dexPage - 1)) { let bounds = page.bounds(for: .cropBox) let dest = CPDFDestination(page: page, at: NSPoint(x: 0, y: bounds.size.height)) pdfview!.go(to: dest) } case .url: guard let activeAnnotation = pdfview?.activeAnnotation as? CPDFLinkAnnotation else { return } activeAnnotation.setDestination(nil) let linkUrlPath = judgeWebURL(inputUrlTextField.stringValue) activeAnnotation.setURL(linkUrlPath) pdfview!.needsDisplay = true if let url = (pdfview?.activeAnnotation as! CPDFLinkAnnotation).url() { if let data = URL(string: url) { NSWorkspace.shared.open(data) } } case .email: guard let activeAnnotation = pdfview?.activeAnnotation as? CPDFLinkAnnotation else { return } activeAnnotation.setDestination(nil) if !isValidateEmail(inputUrlTextField.stringValue) { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = NSLocalizedString("Invalid Email. Please try again.", comment: "") alert.runModal() return } let linkUrlPath = judgeEmailURL(inputUrlTextField.stringValue) activeAnnotation.setURL(linkUrlPath) pdfview!.needsDisplay = true if let url = (pdfview?.activeAnnotation as! CPDFLinkAnnotation).url() { NSWorkspace.shared.open(URL(string: url)!) } default: break } } @objc func timered(_ timer: Timer) { goButton.layer!.backgroundColor = KMAppearance.Interactive.m0Color().cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color()) } // MARK: NSTextFieldDelegate Methods @objc func controlTextDidChange(_ notification: Notification) { guard let textField = notification.object as? NSTextField else { return } if mouseDownBox!.isEqual(linkPageBox) { pageRecord = textField.stringValue } else if mouseDownBox!.isEqual(linkUrlBox) { urlRecord = textField.stringValue } else if mouseDownBox!.isEqual(linkEmailBox) { emailRecord = textField.stringValue } if linkType == KMAnnotationLinkType.page { (pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setURL(nil) let dexPage = Int(inputPageTextField.stringValue) ?? 0 if (dexPage < 1 || dexPage > pdfDocument!.pageCount) && pageRecord!.count > 0 { goButton.layer!.backgroundColor = KMAppearance.Interactive.m0Color().withAlphaComponent(0.4).cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color().withAlphaComponent(0.4)) errorLabel.isHidden = false return } else if pageRecord!.count > 0 { goButton.layer!.backgroundColor = KMAppearance.Interactive.m0Color().cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color()) errorLabel.isHidden = true } else { goButton.layer!.backgroundColor = KMAppearance.Interactive.m0Color().withAlphaComponent(0.4).cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color().withAlphaComponent(0.4)) } let pageIndex = UInt(max(dexPage-1, 0)) let page = pdfDocument!.page(at: pageIndex) let bounds = page?.bounds(for: .cropBox) ?? .zero let destination = CPDFDestination(page: page, at: NSMakePoint(0, bounds.size.height)) (pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setDestination(destination) pdfview?.setNeedsDisplay(pdfview!.visibleRect) } else if linkType == KMAnnotationLinkType.url { (pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setDestination(nil) let linkUrlPath: String = judgeWebURL(urlRecord!) if linkUrlPath == "" { (pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setURL(nil) } else { (pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setURL(linkUrlPath) } if urlRecord!.count > 0 { goButton.layer!.backgroundColor = KMAppearance.Interactive.m0Color().cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color()) } else { goButton.layer!.backgroundColor = KMAppearance.Interactive.m0Color().withAlphaComponent(0.4).cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color().withAlphaComponent(0.4)) } pdfview!.setNeedsDisplay(pdfview!.visibleRect) } else if linkType == KMAnnotationLinkType.email { (pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setDestination(nil) let linkUrlPath: String = judgeEmailURL(emailRecord!) if linkUrlPath == "" { (pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setURL(nil) } else { (pdfview?.activeAnnotation as? CPDFLinkAnnotation)?.setURL(linkUrlPath) } if emailRecord!.count > 0 { goButton.layer!.backgroundColor = KMAppearance.Interactive.m0Color().cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color()) } else { goButton.layer!.backgroundColor = KMAppearance.Interactive.m0Color().withAlphaComponent(0.4).cgColor goButton.setTitleColor(KMAppearance.Layout.w0Color().withAlphaComponent(0.4)) } pdfview?.setNeedsDisplay(pdfview!.visibleRect) } } }