// // KMMainDocument.swift // PDF Master // // Created by wanjun on 2022/12/6. // import Cocoa typealias KMMainDocumentCloudUploadHanddler = (@escaping(Bool, String)->()) -> () @objcMembers class KMMainDocument: CTTabContents { var mainViewController: KMMainViewController? var homeWindowController: KMHomeWindowController? var homeViewController: KMHomeViewController? var isNewCreated: Bool = false var closedByUserGestureFlag: Bool = false // 标记 closedByUserGesture 这个状态需要延后存储(如果需要) var cloud: Bool = false var cloudUploadHanddler: KMMainDocumentCloudUploadHanddler? var isUnlockFromKeychain: Bool = false private var _saveAsing = false private var _saveToURL: URL? var saveToURL: URL? { get { return self._saveToURL } } weak var watermarkSaveDelegate: AnyObject? private var _trackEvents = IndexSet() override func save(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, delegate: Any?, didSave didSaveSelector: Selector?, contextInfo: UnsafeMutableRawPointer?) { if (self.isNewCreated) { // if let data = self.mainViewController, !data.isPDFDocumentEdited && !data.needSave && !self.isDocumentEdited { self._km_save(to: url, ofType: typeName, for: saveOperation, delegate: delegate, didSave: didSaveSelector, contextInfo: contextInfo) return // } } if (!self.needSaveWatermark()) { self._km_save(to: url, ofType: typeName, for: saveOperation, delegate: delegate, didSave: didSaveSelector, contextInfo: contextInfo) return } var openAccessoryView = self.watermarkSaveDelegate != nil if (openAccessoryView) { if let _browser = self.watermarkSaveDelegate as? KMBrowser, _browser.isCloseAllTabViewItem { openAccessoryView = false } } self._km_saveForWatermark(openAccessoryView: openAccessoryView) { [unowned self] in self.trackEvents() } callback: { [unowned self] needSave, params in if (needSave) { self._km_save(to: url, ofType: typeName, for: saveOperation, delegate: delegate, didSave: didSaveSelector, contextInfo: contextInfo) } else { // 水印保存 if (self.watermarkSaveDelegate == nil) { if let data = params.first as? KMResult, data == .cancel { if let shouldClose = params.last as? Bool, shouldClose { DispatchQueue.main.async { self.mainViewController?.browserWindowController?.browser.windowDidBeginToClose() } } } else { DispatchQueue.main.async { self.mainViewController?.browserWindowController?.browser.windowDidBeginToClose() } } return } if let data = params.first as? KMResult, data == .cancel { if var shouldClose = params.last as? Bool { if let _browser = self.watermarkSaveDelegate as? KMBrowser, _browser.isCloseAllTabViewItem { shouldClose = true } (self.watermarkSaveDelegate as? KMBrowser)?.document(self, shouldClose: shouldClose, contextInfo: nil) } } else { (self.watermarkSaveDelegate as? KMBrowser)?.document(self, shouldClose: true, contextInfo: nil) } self.watermarkSaveDelegate = nil } } } override func makeWindowControllers() { // Returns the storyboard that contains your document window. if ((self.fileURL?.path) != nil) { if !self.fileURL!.path.isPDFValid() { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = NSLocalizedString("An error occurred while opening this document. The file is damaged and could not be repaired.", comment: "") alert.runModal() return } } let mainWindow = NSApp.mainWindow var currentWindowController: KMBrowserWindowController? if mainWindow != nil { let windowController = mainWindow!.windowController if windowController is KMBrowserWindowController { currentWindowController = (windowController as! KMBrowserWindowController) } else { for window in NSApp.windows { let windowController = window.windowController if windowController is KMBrowserWindowController { currentWindowController = (windowController as! KMBrowserWindowController) break } } } } else { for window in NSApp.windows { let windowController = window.windowController if windowController is KMBrowserWindowController { currentWindowController = (windowController as! KMBrowserWindowController) break } } } if (currentWindowController == nil) && (self.fileURL != nil) { let browser = KMBrowser.init() as KMBrowser browser.addHomeTabContents() browser.windowController = KMBrowserWindowController.init(browser: browser) currentWindowController = browser.windowController as? KMBrowserWindowController } if currentWindowController?.browser == nil && (self.fileURL != nil) { let browser: KMBrowser = KMBrowser.init() browser.windowController = KMBrowserWindowController.init(browser: browser) browser.addHomeTabContents() currentWindowController = browser.windowController as? KMBrowserWindowController browser.windowController.showWindow(self) } mainViewController = KMMainViewController.init() mainViewController?.myDocument = self if ((self.fileURL?.path) != nil) { let pdfDocument = CPDFDocument.init(url: URL(fileURLWithPath: self.fileURL!.path)) mainViewController?.document = pdfDocument } self.view = mainViewController?.view if currentWindowController != nil { if currentWindowController?.browser != nil { // currentWindowController?.browser.add(self, at: Int32()-1, inForeground: true) // self.addWindowController(currentWindowController!) // mainViewController.browserWindowController = currentWindowController let activeBrowser = (currentWindowController?.browser.activeTabContents())! as CTTabContents let activeIndex = Int((currentWindowController?.browser.activeTabIndex())!) self.addWindowController(currentWindowController!) self.mainViewController?.browserWindowController = currentWindowController let ishome = activeBrowser.isHome as Bool let isfirstTab = (activeIndex == 0) if ishome && !isfirstTab { // 替换 document currentWindowController?.browser.replaceTabContents(at: Int32(activeIndex), with: self) // 刷新标签 currentWindowController?.browser.updateTabState(at: Int32(activeIndex)) // 刷新 home icon if let tabStripController = currentWindowController?.tabStripController { if let view = tabStripController.view(at: UInt(activeIndex)) as? CTTabView { view.controller().isHome = self.isHome view.controller().isNewTab = self.isNewTab view.controller().updateUI() } } } else { currentWindowController?.browser.add(self, at: Int32()-1, inForeground: true) } } } } override func showWindows() { super.showWindows() self.setDataFromTmpData() } override func windowControllerDidLoadNib(_ aController: NSWindowController) { super.windowControllerDidLoadNib(aController) self.setDataFromTmpData() } override func save(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType) async throws { do { try await super.save(to: url, ofType: typeName, for: saveOperation) } catch let outError { Swift.print(outError) } } override func write(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, originalContentsURL absoluteOriginalContentsURL: URL?) throws { try self._km_write(to: url, ofType: typeName, for: saveOperation, originalContentsURL: absoluteOriginalContentsURL) } override func canClose(withDelegate delegate: Any, shouldClose shouldCloseSelector: Selector?, contextInfo: UnsafeMutableRawPointer?) { let isPrompt = KMPreferenceManager.shared.closeFileIsPrompt() if (isPrompt) { super.canClose(withDelegate: delegate, shouldClose: shouldCloseSelector, contextInfo: contextInfo) return } if (self.isNewCreated) { self.save(nil) } else if (self.isDocumentEdited) { self.save(nil) } else if (mainViewController != nil) { if self.mainViewController!.isPDFDocumentEdited || self.mainViewController!.needSave { self.save(nil) } } super.canClose(withDelegate: delegate, shouldClose: shouldCloseSelector, contextInfo: contextInfo) } override func saveAs(_ sender: Any?) { if (!self.needSaveWatermark()) { self._km_saveAs(sender) return } self._km_saveForWatermark { [unowned self] needSave, _ in if (needSave) { self._km_saveAs(sender) } } } override func runModalSavePanel(for saveOperation: NSDocument.SaveOperationType, delegate: Any?, didSave didSaveSelector: Selector?, contextInfo: UnsafeMutableRawPointer?) { if (self.isNewCreated) { // if let data = self.mainViewController, !data.isPDFDocumentEdited && !data.needSave && !self.isDocumentEdited { self._km_runModalSavePanel(for: saveOperation, delegate: delegate, didSave: didSaveSelector, contextInfo: contextInfo) return // } } if (!self.needSaveWatermark()) { self._km_runModalSavePanel(for: saveOperation, delegate: delegate, didSave: didSaveSelector, contextInfo: contextInfo) return } self._km_saveForWatermark { [unowned self] needSave, _ in if (needSave) { self._km_runModalSavePanel(for: saveOperation, delegate: delegate, didSave: didSaveSelector, contextInfo: contextInfo) } } } override func save(_ sender: Any?) { if (!self.needSaveWatermark()) { self._km_save(sender) return } self._km_saveForWatermark { [unowned self] in self.trackEvents() } callback: { [unowned self] needSave, _ in if (needSave) { self._km_save(sender) } } } func saveForWatermark() { if (!self.needSaveWatermark()) { self._km_save(nil) return } self._km_saveForWatermark { [unowned self] in self.trackEvents() } callback: { [unowned self] needSave, params in if (needSave) { self._km_save(nil) } else { // 水印保存 if (self.watermarkSaveDelegate == nil) { if let data = params.first as? KMResult, data == .cancel { if let shouldClose = params.last as? Bool, shouldClose { DispatchQueue.main.async { self.mainViewController?.browserWindowController?.browser.windowDidBeginToClose() } } } else { DispatchQueue.main.async { self.mainViewController?.browserWindowController?.browser.windowDidBeginToClose() } } return } if let data = params.first as? KMResult, data == .cancel { if var shouldClose = params.last as? Bool { if let _browser = self.watermarkSaveDelegate as? KMBrowser, _browser.isCloseAllTabViewItem { shouldClose = true } (self.watermarkSaveDelegate as? KMBrowser)?.document(self, shouldClose: shouldClose, contextInfo: nil) } } else { (self.watermarkSaveDelegate as? KMBrowser)?.document(self, shouldClose: true, contextInfo: nil) } self.watermarkSaveDelegate = nil } } } override func read(from absoluteURL: URL, ofType typeName: String) throws { do { try super.read(from: absoluteURL, ofType: typeName) updateChangeCount(.changeCleared) } catch let outError { Swift.print(outError) } } override func read(from data: Data, ofType typeName: String) throws { // Insert code here to read your document from the given data of the specified type, throwing an error in case of failure. // Alternatively, you could remove this method and override read(from:ofType:) instead. If you do, you should also override isEntireFileLoaded to return false if the contents are lazily loaded. let pdfDocument = CPDFDocument.init(data: data) if pdfDocument == nil { throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil) } } // MARK: Autosaving override func close() { if self.isActive { if browser != nil { var activeIndex = 0 let dex = browser.index(of: self) if dex == browser.tabCount() - 1 { activeIndex = Int(browser.tabCount()-2) } else { activeIndex = Int(dex + 1) } let activeContents = browser.tabContents(at: Int32(activeIndex)) activeContents?.addWindowController(browser.windowController) } } super.close() } // MARK: init override init() { super.init() // Add your subclass-specific initialization here. NotificationCenter.default.addObserver(self, selector: #selector(pdfChangedNotification(_:)), name: NSNotification.Name.init(rawValue: "CPDFListViewAnnotationsAttributeHasChangeNotification"), object: nil) // NotificationCenter.default.addObserver(self, selector: #selector(pdfChangedNotification(_:)), name: NSNotification.Name.init(rawValue: "CPDFViewDocumentChangedNotification"), object: nil) // NotificationCenter.default.addObserver(self, selector: #selector(pdfChangedNotification(_:)), name: NSNotification.Name.init(rawValue: "CPDFViewPageChangedNotification"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(pdfChangedNotification(_:)), name: NSNotification.Name.init(rawValue: "CPDFListViewDidAddAnnotationNotification"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(pdfChangedNotification(_:)), name: NSNotification.Name.init(rawValue: "CPDFListViewDidRemoveAnnotationNotification"), object: nil) } override init?(baseTabContents baseContents: CTTabContents?) { super.init(baseTabContents: baseContents) if isHome { homeViewController = KMHomeViewController.init() homeViewController?.myDocument = self self.view = homeViewController?.view } } // MARK: Handling User Actions override var title: String? { get { if isHome { if (self.isNewTab) { return NSLocalizedString("New Tab", comment: "") } else { return NSLocalizedString("Home", comment: "") } } else { return fileURL?.lastPathComponent } } set { super.title = newValue } } func needSaveWatermark() -> Bool { if let need = self.mainViewController?.saveWatermarkFlag { return need } return false } // MARK: Private Methods func pdfChangedNotification(_ notification: Notification) -> Void { if !isHome { let mainViewController = mainViewController var document: CPDFDocument! let dic = notification.object as? NSDictionary if dic?["object"] is CPDFAnnotation { let annotation : CPDFAnnotation = dic?["object"] as? CPDFAnnotation ?? CPDFAnnotation() document = annotation.page.document } else if dic?["object"] is CPDFListView { let pdflistView = notification.object as? CPDFListView document = pdflistView?.document } if mainViewController != nil { if document == mainViewController!.document { updateChangeCount(.changeDone) } } } } override func updateChangeCount(_ change: NSDocument.ChangeType) { let mainWindow = NSApp.mainWindow var currentWindowController: KMBrowserWindowController? if mainWindow != nil { let windowController = mainWindow!.windowController if windowController is KMBrowserWindowController { currentWindowController = (windowController as! KMBrowserWindowController) } else { for window in NSApp.windows { let windowController = window.windowController if windowController is KMBrowserWindowController { currentWindowController = (windowController as! KMBrowserWindowController) break } } } } else { for window in NSApp.windows { let windowController = window.windowController if windowController is KMBrowserWindowController { currentWindowController = (windowController as! KMBrowserWindowController) break } } } if currentWindowController != nil { if currentWindowController?.browser != nil { let activeBrowser = (currentWindowController?.browser.activeTabContents())! as CTTabContents let activeIndex = Int((currentWindowController?.browser.activeTabIndex())!) if self == activeBrowser { super.updateChangeCount(change) return } } } super.updateChangeCount(.changeCleared) } func uploadToCloud(_ callback: (@escaping(Bool, String)->())) { guard let handdler = self.cloudUploadHanddler else { return } handdler(callback) } func isPDFDocument() -> Bool { return true } func setDataFromTmpData() { guard let _document = self.mainViewController?.document else { return } // self.tryToUnlockDocument(document!) if (_document.permissionsStatus != .owner) { var password: NSString? = nil let fileId = self.fileId(for: _document) if (fileId.isEmpty) { return } self.getPassword(&password, fileId: fileId) if (password != nil) { self.isUnlockFromKeychain = true // document.unlock(withPassword: password! as String) self.mainViewController?.password = password as String? } } } func tryToUnlockDocument(_ document: CPDFDocument) { if (document.permissionsStatus != .owner) { var password: NSString? = nil let fileId = self.fileId(for: document) if (fileId.isEmpty) { return } self.getPassword(&password, fileId: fileId) if (password != nil) { self.isUnlockFromKeychain = true document.unlock(withPassword: password! as String) } } } func km_updateChangeCount(_ change: NSDocument.ChangeType) { super.updateChangeCount(change) } func trackEvents() { km_synchronized(self) { for i in self._trackEvents { if let type = KMSubscribeWaterMarkType(rawValue: i) { KMTools.trackEvent(type: type) } } } self.clearTrackEvents() } func recordTrackEvent(type: KMSubscribeWaterMarkType) { if (type == .none) { return } km_synchronized(self) { self._trackEvents.insert(type.rawValue) } } func clearTrackEvents() { km_synchronized(self) { self._trackEvents.removeAll() } } // MARK: - Private Methods private func _km_write(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, originalContentsURL absoluteOriginalContentsURL: URL?) throws { var success = true if !self.isHome { if mainViewController != nil { if mainViewController?.document != nil { self.mainViewController?.commitEditingIfNeed() // if mainViewController!.document!.isEncrypted { // success = mainViewController!.document!.write(to: url) // } else { if (mainViewController!.needSave) { if let options = self.mainViewController?.secureOptions, !options.isEmpty { success = self.mainViewController!.document!.write(to: url, withOptions: options) } else if let flag = self.mainViewController?.removeSecureFlag, flag { success = self.mainViewController!.document!.writeDecrypt(to: url) } else { success = mainViewController!.document!.write(to: url) } } else { success = mainViewController!.document!.write(to: url) } // } self.mainViewController?.needSave = false self.mainViewController?.clearSecureOptions() self.mainViewController?.clearRemoveSecureFlag() } } } else { success = false } if (success && self._saveAsing) { if let tabView = self.browser?.windowController?.tabStripController?.activeTabView() as? CTTabView { tabView.controller()?.title = url.lastPathComponent } self._saveAsing = false } if success && isNewCreated && NSDocument.SaveOperationType.saveAsOperation == saveOperation { isNewCreated = false } } private func _km_saveForWatermark(openAccessoryView: Bool = true, subscribeDidClick: (()->Void)? = nil, callback:@escaping (_ needSave: Bool, _ param: Any...)->Void) { Task { @MainActor in if await (KMLightMemberManager.manager.canPayFunction() == false) { let _ = KMSubscribeWaterMarkWindowController.show(window: NSApp.mainWindow!, isContinue:false, type: .save) { if let _callback = subscribeDidClick { _callback() } } completion: { isSubscribeSuccess, isWaterMarkExport, isClose in if (isClose) { callback(false, KMResult.cancel, false) return } if (isSubscribeSuccess) { callback(true) return } if (isWaterMarkExport) { guard let _document = self.mainViewController?.document else { callback(false, KMResult.failure) return } // 提交文本编辑的内容 self.mainViewController?.commitEditingIfNeed() DispatchQueue.main.async { NSPanel.savePanel(NSApp.mainWindow!, openAccessoryView, panel:{ panel in if (!self.isNewCreated) { panel.directoryURL = _document.documentURL.deletingLastPathComponent() } panel.nameFieldStringValue = _document.documentURL.lastPathComponent }) { response, url, isOpen in if (response == .cancel) { callback(false, KMResult.cancel, true) return } guard let _url = KMTools.saveWatermarkDocument(document: _document, to: url!, secureOptions: self.mainViewController?.secureOptions, removePWD: self.mainViewController!.removeSecureFlag) else { callback(false, KMResult.failure) return } callback(false, KMResult.success) if (isOpen) { NSDocumentController.shared.km_safe_openDocument(withContentsOf: _url, display: true) { _, _, _ in } } else { NSWorkspace.shared.activateFileViewerSelecting([_url]) } } } return } callback(false, KMResult.cancel, false) } return } callback(true) } } private func _km_save(_ sender: Any?) { super.save(sender) } private func _km_save(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, delegate: Any?, didSave didSaveSelector: Selector?, contextInfo: UnsafeMutableRawPointer?) { self._saveToURL = url super.save(to: url, ofType: typeName, for: saveOperation, delegate: delegate, didSave: didSaveSelector, contextInfo: contextInfo) } private func _km_saveAs(_ sender: Any?) { super.saveAs(sender) self._saveAsing = true } private func _km_runModalSavePanel(for saveOperation: NSDocument.SaveOperationType, delegate: Any?, didSave didSaveSelector: Selector?, contextInfo: UnsafeMutableRawPointer?) { super.runModalSavePanel(for: saveOperation, delegate: delegate, didSave: didSaveSelector, contextInfo: contextInfo) } } extension KMMainDocument { override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { if (menuItem.action == #selector(save(_ :))) { if (self.isHome) { return false } if (self.isDocumentEdited) { return self.isDocumentEdited } guard let mainVC = self.mainViewController else { return false } return mainVC.isPDFDocumentEdited || mainVC.needSave } else if (menuItem.action == #selector(saveAs(_ :))) { return !self.isHome } return super.validateMenuItem(menuItem) } } extension NSDocument { @objc class func isDamage(url: URL) -> Bool { // 文件路径是否存在 if (FileManager.default.fileExists(atPath: url.path) == false) { return true } /// PDF 格式文件 if (url.pathExtension.lowercased() == "pdf") { let document = PDFDocument(url: url) if (document == nil) { return true } if (document!.isLocked) { // 加锁文件不在这里判断 return false } if (document!.pageCount <= 0) { return true } return false } // 支持的图片格式 let imageExts = ["jpg","cur","bmp","jpeg","gif","png","tiff","tif","ico","icns","tga","psd","eps","hdr","jp2","jpc","pict","sgi","heic"] let isImage = imageExts.contains(url.pathExtension.lowercased()) if (isImage == false) { // 其他格式目前返回没损坏,后续再补充(如果有需求) return false } // 图片格式 let image = NSImage(contentsOf: url) let data = image?.tiffRepresentation if (data == nil) { return true } let imageRep = NSBitmapImageRep(data: data!) imageRep!.size = image!.size var imageData: NSData? if (url.pathExtension.lowercased() == "png") { imageData = imageRep?.representation(using: .png, properties: [:]) as NSData? } else { imageData = imageRep?.representation(using: .jpeg, properties: [:]) as NSData? } if (imageData == nil) { return true } let path = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).last?.stringByAppendingPathComponent(Bundle.main.bundleIdentifier!) if (FileManager.default.fileExists(atPath: path!) == false) { try?FileManager.default.createDirectory(atPath: path!, withIntermediateDirectories: false) } var tagString: String = "" let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyMMddHHmmss" tagString.append(dateFormatter.string(from: Date())) tagString = tagString.appendingFormat("%04d", arc4random()%10000) let filePath = path?.appending("/\(tagString).png") if (imageData!.write(toFile: filePath!, atomically: true) == false) { return true } // 删除临时图片 try?FileManager.default.removeItem(atPath: filePath!) return false } @objc class func isDamage(url: URL, needAlertIfDamage need: Bool) -> Bool { let result = self.isDamage(url: url) if (result == false) { return false } if (need == false) { return true } let alert = NSAlert() alert.messageText = NSLocalizedString("An error occurred while opening this document. The file is damaged and could not be repaired.", comment: "") alert.runModal() return true } } // MARK: - // MARK: 保存密码 extension NSDocument { func savePasswordInKeychain(_ password: String, _ document: CPDFDocument) { if (document.isLocked || password.isEmpty) { return } let fileId = self.fileId(for: document) if (fileId.isEmpty) { return } // let status: SKPasswordStatus = let label = "PDF Master: \(self.displayName!)" SKKeychain.setPassword(password, item: nil, forService: self.passwordServiceName(), account: fileId, label: label, comment: self.fileURL?.path) } func getPassword(_ password: AutoreleasingUnsafeMutablePointer, fileId: String) { let status = SKKeychain.getPassword(password, item: nil, forService: self.passwordServiceName(), account: fileId) // if (status == .found) { // } } fileprivate func fileId(for document: CPDFDocument) -> String { return "\(document.documentURL.path.hash)" } private func passwordServiceName() -> String { return "PDF Master password" } }