// // KMPasswordInputWindow.swift // PDF Reader Pro // // Created by tangchao on 2022/11/29. // import Cocoa import PDFKit @objc enum KMPasswordInputWindowType: Int { case open = 1 case owner = 2 } @objc enum KMPasswordInputWindowResult: Int { case cancel = 1 case success = 2 } typealias KMPasswordInputWindowItemClick = (KMPasswordInputWindow, Int, String) -> () @objcMembers class KMPasswordInputWindow: NSWindow, NibLoadable { @IBOutlet weak var titleLabel: NSTextField! @IBOutlet weak var despLabel: NSTextField! @IBOutlet weak var secureTextFiled: KMSecureTextFiled! @IBOutlet weak var iconImageView: NSImageView! @IBOutlet weak var passwordErrorLabel: NSTextField! @IBOutlet weak var cancelButton: NSButton! @IBOutlet weak var confirmButton: NSButton! var confirmButtonVC: KMDesignButton? var documentURL: URL? var itemClick: KMPasswordInputWindowItemClick? var type: KMPasswordInputWindowType = .open { didSet { self.titleLabel?.stringValue = NSLocalizedString("Permission Password", comment: "") var fileName = NSLocalizedString("", comment: "") if (self.documentURL != nil) { fileName.append("\(self.documentURL!.lastPathComponent)") } self.despLabel?.maximumNumberOfLines = 3 self.despLabel?.lineBreakMode = .byTruncatingTail self.despLabel?.cell?.truncatesLastVisibleLine = true let ps = NSMutableParagraphStyle() ps.lineSpacing = 5 let despLabelString = "\"\(fileName)\"\(NSLocalizedString("This PDF is password protected. Please enter the password below to access this PDF.", comment: ""))" self.despLabel?.attributedStringValue = NSAttributedString(string: despLabelString, attributes: [.foregroundColor : KMAppearance.Layout.h0Color(), .font : NSFont.SFProTextRegularFont(14), .paragraphStyle : ps]) } } var canEncrpty = false static var permissionsStatus: CPDFDocumentPermissions = .none deinit { KMPrint("KMPasswordInputWindow 已释放了") } static var nibName: String? { return "KMPasswordInputWindow" } static func createFromNib(in bundle: Bundle) -> Self? { guard let nibName = self.nibName else { return nil } var topLevelArray: NSArray? = nil bundle.loadNibNamed(NSNib.Name(nibName), owner: self, topLevelObjects: &topLevelArray) guard let results = topLevelArray else { return nil } let views = Array(results).filter { $0 is Self } return views.last as? Self } class func createWindow() -> Self? { KMPasswordInputWindow.permissionsStatus = .none return self.createFromNib(in: MainBundle) } // func window() -> Self? { // KMPasswordInputWindow.canEncrpty = false // KMPasswordInputWindow.permissionsStatus = .none // // var topLevelArray: NSArray? = nil // Bundle.main.loadNibNamed(NSNib.Name("KMPasswordInputWindow"), owner: self, topLevelObjects: &topLevelArray) // guard let results = topLevelArray else { // return nil // } // // var passwordInputWindow: KMPasswordInputWindow! // for object in results { // let window: NSObject = object as! NSObject // if window.isKind(of: KMPasswordInputWindow.self) { // passwordInputWindow = window as! KMPasswordInputWindow? // } // } // // guard let myWindow = passwordInputWindow else { // return nil // } // return myWindow as? Self // } override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) } override func awakeFromNib() { super.awakeFromNib() self.titleLabel.stringValue = NSLocalizedString("Permission Password", comment: "") self.titleLabel.textColor = KMAppearance.Layout.h0Color() self.titleLabel.font = NSFont.SFProTextRegularFont(16) self.despLabel.stringValue = NSLocalizedString("This PDF is password protected. Please enter the password below to access this PDF.", comment: "") self.despLabel.textColor = KMAppearance.Layout.h0Color() self.despLabel.font = NSFont.SFProTextRegularFont(14) self.despLabel.isSelectable = false let ps = NSMutableParagraphStyle() ps.lineSpacing = 5 self.despLabel.maximumNumberOfLines = 2 self.despLabel.lineBreakMode = .byTruncatingTail ps.lineBreakMode = .byTruncatingTail self.despLabel.attributedStringValue = NSAttributedString(string: despLabel.stringValue, attributes: [.foregroundColor : KMAppearance.Layout.h0Color(), .font : NSFont.SFProTextRegularFont(14), .paragraphStyle : ps]) self.iconImageView.image = NSImage(named: "KMImageNameSecureIcon") self.secureTextFiled.backgroundView.wantsLayer = true self.secureTextFiled.backgroundView.layer?.borderWidth = 1 // self.secureTextFiled.backgroundView.layer?.borderColor = NSColor.buttonBorderColor().cgColor self.secureTextFiled.backgroundView.layer?.cornerRadius = 4 self.secureTextFiled.placeholderString = NSLocalizedString("Password", comment: "") let rightView = NSView() rightView.frame = NSMakeRect(0, 0, 40, 32); self.secureTextFiled.rightView = rightView let clearButton = NSButton() rightView.addSubview(clearButton) clearButton.frame = NSMakeRect(10, 6, 20, 20) clearButton.wantsLayer = true clearButton.image = NSImage(named: "KMImageNameSecureClearIcon") clearButton.isBordered = false clearButton.target = self clearButton.action = #selector(clearButtonAction) rightView.isHidden = true self.secureTextFiled.becomeFirstResponderHandler = { [unowned self] securetextFiled in let mySecureTextField: KMSecureTextFiled = securetextFiled as! KMSecureTextFiled mySecureTextField.backgroundView.wantsLayer = true mySecureTextField.backgroundView.layer?.borderColor = NSColor.km_init(hex: "#1770F4").cgColor if mySecureTextField.password().isEmpty { self.secureTextFiled.rightView?.isHidden = true } else { self.secureTextFiled.rightView?.isHidden = false } self.passwordErrorLabel.isHidden = true } self.secureTextFiled.valueDidChange = { [unowned self] view, string in view.backgroundView.layer?.borderColor = NSColor.km_init(hex: "#1770F4").cgColor self.passwordErrorLabel.isHidden = true if string.isEmpty { view.rightView?.isHidden = true self.dealConfirmButtonEnabledState(enabled: false) } else { view.rightView?.isHidden = false self.dealConfirmButtonEnabledState(enabled: true) } } self.secureTextFiled.enterAction = { [unowned self] in self.confirmButtonAction() } self.passwordErrorLabel.stringValue = NSLocalizedString("Incorrect password. Please try again.", comment: "") self.passwordErrorLabel.font = NSFont.systemFont(ofSize: 12) self.passwordErrorLabel.wantsLayer = true self.passwordErrorLabel.textColor = NSColor.km_init(hex: "#F3465B") self.passwordErrorLabel.isHidden = true for button in [cancelButton, confirmButton] { // button?.wantsLayer = true // button?.layer?.cornerRadius = 4 // button?.bezelStyle = .roundRect // button?.setButtonType(.momentaryPushIn) button!.target = self if ((button?.isEqual(to: cancelButton))!) { // button?.layer?.borderWidth = 1 // button?.layer?.borderColor = NSColor.buttonBorderColor().cgColor // button?.title = NSLocalizedString("Cancel", comment: "") // button?.title = "" // button?.setTitleColor(NSColor.buttonTitleColor()) // button?.font = NSFont.SFProTextRegularFont(14) // button?.action = #selector(cancelButtonAction) } else { // button?.title = NSLocalizedString("Open", comment: "") // button?.attributedTitle = NSMutableAttributedString(string: button!.title, attributes: [.foregroundColor : NSColor.white]) // button?.font = NSFont.SFProTextRegularFont(14) // button?.action = #selector(confirmButtonAction) // button?.title = "" } } let cancelButtonVC = KMDesignButton(withType: .Text) // self.cancelButton.addSubview(cancelButtonVC.view) cancelButtonVC.view.frame = self.cancelButton.bounds cancelButtonVC.view.autoresizingMask = [.width, .height] cancelButtonVC.stringValue = NSLocalizedString("Cancel", comment: "") cancelButtonVC.button(type: .Sec_Icon, size: .m) cancelButtonVC.target = self cancelButtonVC.action = #selector(cancelButtonAction) cancelButtonVC.button.keyEquivalent = KMKeyEquivalent.esc.string() self.cancelButton.title = NSLocalizedString("Cancel", comment: "") self.cancelButton.action = #selector(cancelButtonAction) let confirmButtonVC = KMDesignButton(withType: .Text) // self.confirmButton.addSubview(confirmButtonVC.view) confirmButtonVC.view.frame = self.confirmButton.bounds confirmButtonVC.view.autoresizingMask = [.width, .height] confirmButtonVC.stringValue = NSLocalizedString("Open", comment: "") confirmButtonVC.button(type: .Cta, size: .m) confirmButtonVC.target = self confirmButtonVC.action = #selector(confirmButtonAction) self.confirmButtonVC = confirmButtonVC self.confirmButtonVC?.button.keyEquivalent = KMKeyEquivalent.enter self.confirmButton.title = NSLocalizedString("Open", comment: "") self.confirmButton.action = #selector(confirmButtonAction) self.dealConfirmButtonEnabledState(enabled: true) } // MARK: - Actions @objc func cancelButtonAction() { guard let callback = self.itemClick else { return } callback(self, 1, "") } @objc func confirmButtonAction() { if (!self.canEncrpty) { return } guard let documentURL = self.documentURL else { return } if (self.type == .open) { let document: CPDFDocument = CPDFDocument(url: documentURL) if document.permissionsStatus == .none { let reuslt = document.unlock(withPassword: secureTextFiled.password()) /// CPDFDocumentPermissionsNone 解锁失败 /// CPDFDocumentPermissionsUser 输入的开启密码 /// CPDFDocumentPermissionsOwner 输入的权限密码 KMPasswordInputWindow.permissionsStatus = document.permissionsStatus if document.permissionsStatus != CPDFDocumentPermissions.none { /// 密码正确 guard let callback = self.itemClick else { return } callback(self ,2, secureTextFiled.password()) } else { /// 密码错误 self.passwordErrorLabel.isHidden = false self.passwordErrorLabel.stringValue = NSLocalizedString("Incorrect password. Please try again.", comment: "") self.secureTextFiled.backgroundView.layer?.borderColor = NSColor.km_init(hex: "#F3465B").cgColor } } return } /// 权限密码类型 let document: CPDFDocument = CPDFDocument(url: documentURL) if (document.isLocked) { if document.permissionsStatus == CPDFDocumentPermissions.none { let reuslt = document.unlock(withPassword: secureTextFiled.password()) KMPasswordInputWindow.permissionsStatus = document.permissionsStatus if document.permissionsStatus == .owner { /// 密码正确 guard let callback = self.itemClick else { return } callback(self, 2, secureTextFiled.password()) } else { /// 密码错误 self.passwordErrorLabel.isHidden = false self.passwordErrorLabel.stringValue = NSLocalizedString("Incorrect password. Please try again.", comment: "") self.secureTextFiled.backgroundView.layer?.borderColor = NSColor.km_init(hex: "#F3465B").cgColor } } } else { if document.permissionsStatus == CPDFDocumentPermissions.user { document.unlock(withPassword: secureTextFiled.password()) KMPasswordInputWindow.permissionsStatus = document.permissionsStatus if document.permissionsStatus == .owner { /// 密码正确 guard let callback = self.itemClick else { return } callback(self, 2, secureTextFiled.password()) } else { /// 密码错误 self.passwordErrorLabel.isHidden = false self.passwordErrorLabel.stringValue = NSLocalizedString("Incorrect password. Please try again.", comment: "") self.secureTextFiled.backgroundView.layer?.borderColor = NSColor.km_init(hex: "#F3465B").cgColor } } } } @objc func clearButtonAction() { self.secureTextFiled.clear() } func dealConfirmButtonEnabledState(enabled: Bool) { self.canEncrpty = enabled // confirmButton.wantsLayer = true // confirmButton.layer?.backgroundColor = NSColor.buttonFunctionBackgroundColor(enabled: enabled).cgColor // confirmButton?.title = NSLocalizedString("Open", comment: "") // var color = NSColor.buttonTitleColor(enabled: enabled) // if (enabled) { // color = NSColor.km_init(hex: "#FFFFFF") // } // confirmButton?.attributedTitle = NSMutableAttributedString(string: confirmButton!.title, attributes: [.foregroundColor : color]) // if (self.confirmButtonVC != nil) { // self.confirmButtonVC?.enabled = enabled // } self.confirmButton.isEnabled = enabled } override func mouseUp(with event: NSEvent) { super.mouseUp(with: event) self.makeFirstResponder(nil) // self.secureTextFiled.backgroundView.layer?.borderColor = NSColor.buttonBorderColor().cgColor self.passwordErrorLabel.isHidden = true } } extension KMPasswordInputWindow { @objc class func openWindow(window: NSWindow, type: KMPasswordInputWindowType = .open, url: URL, callback: @escaping (KMPasswordInputWindowResult, String?)->Void) -> KMPasswordInputWindow { let passwordWindow = KMPasswordInputWindow.createWindow() passwordWindow?.documentURL = url passwordWindow?.type = type passwordWindow?.itemClick = { pwdWin, index, string in if let sheetParent = pwdWin.sheetParent { sheetParent.endSheet(pwdWin) } if index == 1 { /// 关闭 callback(.cancel, "") return } /// 解密成功 callback(.success, string) } window.beginSheet(passwordWindow!) return passwordWindow! } @objc class func success_openWindow(window: NSWindow, type: KMPasswordInputWindowType = .open, url: URL, callback: @escaping (String)->Void) { let passwordWindow = KMPasswordInputWindow.createWindow() passwordWindow?.documentURL = url passwordWindow?.type = type passwordWindow?.itemClick = { pwdWin, index, string in if let sheetParent = pwdWin.sheetParent { sheetParent.endSheet(pwdWin) } if index == 1 { /// 关闭 return } /// 解密成功 callback(string) } window.beginSheet(passwordWindow!) } @objc class func openWindow(window: NSWindow, url: URL, needOwner: Bool, callback: @escaping (KMPasswordInputWindowResult, String?)->Void) { let passwordWindow = KMPasswordInputWindow.createWindow() passwordWindow?.documentURL = url let document = CPDFDocument(url: url) if (document?.isLocked != nil && document!.isLocked) { passwordWindow?.type = .open } else if (document?.isEncrypted != nil && document!.isEncrypted) { passwordWindow?.type = .owner } else { passwordWindow?.type = .open } passwordWindow?.itemClick = { pwdWin, index, string in let type = pwdWin.type if let sheetParent = pwdWin.sheetParent { sheetParent.endSheet(pwdWin) } if index == 1 { /// 关闭 callback(.cancel, "") return } /// 解密成功 if (type == .owner) { // 解除的是权限密码 callback(.success, string) return } // 解除的是开启密码 if (needOwner == false) { // 不需要解除权限密码 callback(.success, string) return } if (document == nil) { callback(.success, string) return } document?.unlock(withPassword: string) if (document?.permissionsStatus == .owner) { // 用户是使用的权限密码解密 callback(.success, string) return } if (document!.allowsCopying == true && document!.allowsPrinting == true) { // 文件没有权限限制 callback(.success, string) return } // 需要解除权限密码 KMPasswordInputWindow.openWindow(window: window, type: .owner, url: url) { result, password in if (result == .cancel) { callback(.cancel, "") return } callback(.success, password) } } window.beginSheet(passwordWindow!) } class func saveDocument(_ document: CPDFDocument) -> Bool { let toPath = document.documentURL.path let tempFilePath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.applicationSupportDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last?.stringByAppendingPathComponent(Bundle.main.bundleIdentifier!).stringByAppendingPathComponent("\((document.documentURL.lastPathComponent))") if (FileManager.default.fileExists(atPath: tempFilePath!)) { /// 清空数据 try?FileManager.default.removeItem(atPath: tempFilePath!) } var result: Bool = document.write(to: URL(fileURLWithPath: tempFilePath!)) if (result == false) { return false } try?FileManager.default.removeItem(atPath: toPath) result = ((try?FileManager.default.moveItem(atPath: tempFilePath!, toPath: toPath)) != nil) /// 清空数据 try?FileManager.default.removeItem(atPath: tempFilePath!) return result } class func saveDocumentForRemovePassword(_ document: CPDFDocument) -> Bool { let toPath = document.documentURL.path let tempFilePath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.applicationSupportDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last?.stringByAppendingPathComponent(Bundle.main.bundleIdentifier!).stringByAppendingPathComponent("\((document.documentURL.lastPathComponent))") if (FileManager.default.fileExists(atPath: tempFilePath!)) { /// 清空数据 try?FileManager.default.removeItem(atPath: tempFilePath!) } var result: Bool = document.writeDecrypt(to: URL(fileURLWithPath: tempFilePath!)) if (result == false) { return false } try?FileManager.default.removeItem(atPath: toPath) result = ((try?FileManager.default.moveItem(atPath: tempFilePath!, toPath: toPath)) != nil) /// 清空数据 try?FileManager.default.removeItem(atPath: tempFilePath!) return result } } extension NSOpenPanel { /** * 打开 NSOpenPanel 窗口(如果文档存在开启密码或者权限密码,则会弹密码输入框) * @param window 弹出 NSOpenPanel 的窗口 [可选] [默认为 主窗口] * @param needOwner 是否需要限制权限密码(如果存在权限密码,会在解锁后再弹权限密码弹窗(目前未实现)) [可选] [默认为 false] * @param callback 回调 * *  默认弹开启密码输入框,needOwner = true 弹权限密码输入框 */ class func km_secure_openPanel(window: NSWindow = NSApp.mainWindow!, needOwner: Bool = false, callback:@escaping (URL?, KMPasswordInputWindowResult? , String?)->Void) { let panel = NSOpenPanel() panel.allowedFileTypes = ["pdf"] panel.beginSheetModal(for: window) { response in if (response == .cancel) { callback(nil, nil, nil) return } let document = CPDFDocument(url: panel.url) if ((document?.isLocked)! == false) { if (document?.isEncrypted == false) { callback(panel.url, nil, nil) return } if (!needOwner) { callback(panel.url, nil, nil) return } KMPasswordInputWindow.openWindow(window: window, type: .owner, url: panel.url!) { result, password in if (result == .cancel) { callback(panel.url, .cancel , nil) return } callback(panel.url, .success , password) } return } /// 已加锁(开启密码) KMPasswordInputWindow.openWindow(window: window, url: panel.url!) { result, password in if (result == .cancel) { callback(panel.url, .cancel, nil) return } if (!needOwner) { callback(panel.url, .success, password) return } /// 用户输入的是权限密码 if (KMPasswordInputWindow.permissionsStatus == .owner) { callback(panel.url, .success ,password) return } /// 用户输入的是开启密码 (无法判断是否还有权限未解密) /// 还有权限密码未解锁 // KMPasswordInputWindow.openWindow(window: window, type: .owner, url: panel.url!) { result, password in // if (result == .cancel) { // callback(panel.url, .cancel , nil) // return // } // // callback(panel.url, .success , password) // } callback(panel.url, .success ,password) } } } /** * 打开 NSOpenPanel 窗口(如果文档存在开启密码或者权限密码,则会弹密码输入框) * @param window 弹出 NSOpenPanel 的窗口 [可选] [默认为 主窗口] * @param needOwner 是否需要限制权限密码(如果存在权限密码,会在解锁后再弹权限密码弹窗(目前未实现)) [可选] [默认为 false] * @param callback 回调 * *  默认弹开启密码输入框,needOwner = true 弹权限密码输入框 *  只返回成功的结果, 用户关闭的操作都未回调(如果有需要回调的需求可以使用 km_secure_openPanel 方法) */ class func km_secure_openPanel_success(window: NSWindow = NSApp.mainWindow!, needOwner: Bool = false, callback:@escaping (URL, String?)->Void) { let panel = NSOpenPanel() panel.allowedFileTypes = ["pdf"] panel.beginSheetModal(for: window) { response in if (response == .cancel) { return } let document = CPDFDocument(url: panel.url) if ((document?.isLocked)! == false) { if (document?.isEncrypted == false) { callback(panel.url!, nil) return } if (!needOwner) { callback(panel.url!, nil) return } KMPasswordInputWindow.openWindow(window: window, type: .owner, url: panel.url!) { result, password in if (result == .cancel) { return } callback(panel.url!, password) } return } /// 已加锁(开启密码) KMPasswordInputWindow.openWindow(window: window, url: panel.url!) { result, password in if (result == .cancel) { return } if (!needOwner) { callback(panel.url!, password) return } /// 用户输入的是权限密码 if (KMPasswordInputWindow.permissionsStatus == .owner) { callback(panel.url!, password) return } callback(panel.url!, password) } } } }