// // KMBackgroundPropertyController.swift // PDF Reader Pro // // Created by Niehaoyu on 2024/11/6. // import Cocoa import KMComponentLibrary @objc protocol KMBackgroundPropertyControllerDelegate: AnyObject { //水印数据有更新 @objc optional func backgroundPropertyControllerDidUpdate(_ controller: KMBackgroundPropertyController) //切换到模板界面 @objc optional func backgroundPropertyControllerDidChangetoTemplate(_ controller: KMBackgroundPropertyController) //成功保存到模板 @objc optional func backgroundPropertyControllerSaveTemplateSuccess(_ controller: KMBackgroundPropertyController, _ data: KMBackgroundModel) //取消修改模板信息 @objc optional func backgroundPropertyControllerCancelTemplateEdit(_ controller: KMBackgroundPropertyController) @objc optional func backgroundPropertyControllerFinishTemplateEdit(_ controller: KMBackgroundPropertyController) } class KMBackgroundPropertyController: NSViewController { @IBOutlet var contendView: NSView! @IBOutlet var leftTopButton: ComponentButton! @IBOutlet var titleLabel: NSTextField! @IBOutlet var templateButton: ComponentButton! @IBOutlet var typeTabsBGView: NSView! @IBOutlet var typeTabs: ComponentTabs! @IBOutlet var infoContendView: NSView! @IBOutlet var infoContendTopConst: NSLayoutConstraint! //Text @IBOutlet var textBGView: NSView! @IBOutlet var colorLabel: NSTextField! @IBOutlet var fontColorGroup: ComponentCColorGroup! //File @IBOutlet var fileBGView: NSView! @IBOutlet var fileInputView: ComponentInput! @IBOutlet var fileInputAddonView: ComponentInputAddon! //Appearance @IBOutlet var appearanceBGView: NSView! @IBOutlet var appearanceLabel: NSTextField! @IBOutlet var appearance_RotateSelect: ComponentSelect! @IBOutlet var appearance_OpacitySelect: ComponentSelect! @IBOutlet var appearanceScaleCheckbox: ComponentCheckBox! @IBOutlet var appearanceScaleSelect: ComponentSelect! @IBOutlet var appearanceBGTopConst: NSLayoutConstraint! //Position @IBOutlet var positionBGView: NSView! @IBOutlet var positionLabel: NSTextField! @IBOutlet var positionItemView: ComponentCPosition! @IBOutlet var positionXInput: ComponentInputNumber! @IBOutlet var positionYInput: ComponentInputNumber! //Save @IBOutlet var saveTemplateBGView: NSView! @IBOutlet var saveButton: ComponentButton! private var textTabProperty = ComponentTabsProperty(tabsType: .underline_Fill, state: .normal, showIcon: false, title: KMLocalizedString("Color")) private var fileTabProperty = ComponentTabsProperty(tabsType: .underline_Fill, state: .normal, showIcon: false, title: KMLocalizedString("File")) weak open var delegate: KMBackgroundPropertyControllerDelegate? var originalDataDict: Dictionary? var backgroundData: KMBackgroundModel = KMBackgroundModel() var editSubType: KMPDFEditSubModeType = .none //MARK: - func override func viewDidLoad() { super.viewDidLoad() // Do view setup here. setupProperty() } func setupProperty() { contendView.wantsLayer = true contendView.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") leftTopButton.properties = ComponentButtonProperty(type: .text_gray, size: .xxs, onlyIcon: true, icon: NSImage(named: "watermark_arrowLeft"), keepPressState: false) leftTopButton.setTarget(self, action: #selector(leftTopButtonClicked(_:))) templateButton.properties = ComponentButtonProperty(type: .text_gray, size: .xxs, onlyIcon: true, icon: NSImage(named: "icon_wm_template"), keepPressState: false) templateButton.setTarget(self, action: #selector(templateButtonClicked(_:))) typeTabs.updateItemProperty([textTabProperty, fileTabProperty]) typeTabs.delegate = self //Text let colorAProperty = ComponentCColorProperty(colorType: .color, state: .normal, isCustom: false, color: KMBackgroundModel.defaultColors()[0]) let colorBProperty = ComponentCColorProperty(colorType: .color, state: .normal, isCustom: false, color: KMBackgroundModel.defaultColors()[1]) let colorCProperty = ComponentCColorProperty(colorType: .color, state: .normal, isCustom: false, color: KMBackgroundModel.defaultColors()[2]) let colorDProperty = ComponentCColorProperty(colorType: .color, state: .normal, isCustom: false, color: KMBackgroundModel.defaultColors()[3]) let colorEProperty = ComponentCColorProperty(colorType: .color, state: .normal, isCustom: true, color: KMBackgroundModel.defaultColors()[4]) fontColorGroup.setUpWithColorPropertys([colorAProperty, colorBProperty, colorCProperty, colorDProperty], customItemProperty: colorEProperty) fontColorGroup.delegate = self //File fileInputView.properties = ComponentInputProperty(size: .s, state:.pressed , placeholder: "Select Source File...", text: "", creatable: false) fileInputView.properties.propertyInfo.cornerRadius_topLeft = 0 fileInputView.properties.propertyInfo.cornerRadius_topRight = 0 fileInputView.properties.propertyInfo.cornerRadius_bottomLeft = 0 fileInputView.properties.propertyInfo.cornerRadius_bottomRight = 0 fileInputView.reloadData() fileInputAddonView.properties = ComponentInputAddonProperty(size: .s, state: .normal, addOnBefore: false, onlyRead: false, addonType: .imageWithColor, iconImage: NSImage(named: "icon_folder")) fileInputAddonView.setTarget(self, action: #selector(chooseURLAction(_ :))) //Appearance appearanceLabel.stringValue = KMLocalizedString("Appearance") appearanceLabel.textColor = ComponentLibrary.shared.getComponentColorFromKey("colorText/2") appearanceLabel.font = ComponentLibrary.shared.getFontFromKey("mac/body-s-medium") appearance_RotateSelect.properties = ComponentSelectProperties(size: .s, state: .normal, creatable: true, text: "0°", textUnit: "°", regexString: "0123456789-") var rotateItems: [ComponentMenuitemProperty] = [] for string in ["0°", "45°", "-45°", "90°", "-90°"] { let item = ComponentMenuitemProperty(type: .normal, text: string) rotateItems.append(item) } appearance_RotateSelect.updateMenuItemsArr(rotateItems) appearance_RotateSelect.delegate = self appearance_OpacitySelect.properties = ComponentSelectProperties(size: .s, state: .normal, creatable: true, text: "100%", textUnit: "%", regexString: "0123456789") var opacityItems: [ComponentMenuitemProperty] = [] for string in ["25%", "50%", "75%", "100%"] { let item = ComponentMenuitemProperty(type: .normal, text: string) opacityItems.append(item) } appearance_OpacitySelect.updateMenuItemsArr(opacityItems) appearance_OpacitySelect.delegate = self appearanceScaleCheckbox.properties = ComponentCheckBoxProperty(size: .s, text: KMLocalizedString("Scale relative to target page"), checkboxType: .normal) appearanceScaleCheckbox.setTarget(self, action: #selector(checkBoxClicked(_:))) appearanceScaleSelect.properties = ComponentSelectProperties(size: .s, state: .normal, creatable: true, text: "100%", textUnit: "%", regexString: "0123456789") var appearanceScaleItems: [ComponentMenuitemProperty] = [] for string in ["25%", "50%", "75%", "100%", "125%", "150%","200%"] { let item = ComponentMenuitemProperty(type: .normal, text: string) appearanceScaleItems.append(item) } appearanceScaleSelect.updateMenuItemsArr(appearanceScaleItems) appearanceScaleSelect.delegate = self //Position positionLabel.stringValue = KMLocalizedString("Position (mm)") positionLabel.textColor = ComponentLibrary.shared.getComponentColorFromKey("colorText/2") positionLabel.font = ComponentLibrary.shared.getFontFromKey("mac/body-s-medium") positionItemView.properties = ComponentCPositionProperty(rowCount: 3, columnCount: 3, dash: false, selRow: 1, selColumn: 1) positionItemView.delegate = self positionXInput.properties = ComponentInputNumberProperty(alignment: .left, size: .s, minSize: -1000, maxSize: 1000, text: "0") positionXInput.delegate = self positionYInput.properties = ComponentInputNumberProperty(alignment: .left, size: .s, minSize: -1000, maxSize: 1000, text: "0") positionYInput.delegate = self //Save saveTemplateBGView.wantsLayer = true saveTemplateBGView.layer?.backgroundColor = ComponentLibrary.shared.getComponentColorFromKey("colorBg/layout-middle").cgColor saveButton.properties = ComponentButtonProperty(type: .default_tertiary, size: .m, showLeftIcon: true, buttonText: KMLocalizedString("Save as Template"), icon: NSImage(named: "icon_wm_template_save"), keepPressState: false) saveButton.setTarget(self, action: #selector(saveButtonClicked(_:))) } func reloadData() { titleLabel.stringValue = KMLocalizedString("Add Background") leftTopButton.isHidden = true saveTemplateBGView.isHidden = false templateButton.properties.icon = NSImage(named: "icon_wm_template") templateButton.properties.isDisabled = false infoContendTopConst.constant = 56 typeTabsBGView.isHidden = false if editSubType == .edit { infoContendTopConst.constant = 8 typeTabsBGView.isHidden = true titleLabel.stringValue = KMLocalizedString("Edit Background") leftTopButton.isHidden = false saveTemplateBGView.isHidden = true templateButton.properties.icon = NSImage(named: "edit_save") if backgroundData.type == .image { if backgroundData.imagePath == nil { templateButton.properties.isDisabled = true } } } templateButton.reloadData() fileBGView.isHidden = true textBGView.isHidden = true textTabProperty.state = .normal fileTabProperty.state = .normal if backgroundData.type == .color { textBGView.isHidden = false appearanceBGTopConst.constant = 88 textTabProperty.state = .pressed } else if backgroundData.type == .image { fileBGView.isHidden = false appearanceBGTopConst.constant = 48 fileTabProperty.state = .pressed } typeTabs.refreshItems() //Text fontColorGroup.currentColor = backgroundData.color fontColorGroup.refreshUI() //File fileInputView.properties.text = backgroundData.imagePath ?? "" fileInputView.reloadData() //Appearance appearance_RotateSelect.properties.text = String(format: "%.0f", backgroundData.rotation) + "°" appearance_RotateSelect.reloadData() if backgroundData.opacity > 1 { backgroundData.opacity = 1 } appearance_OpacitySelect.properties.text = String(format: "%.0f", backgroundData.opacity*100) + "%" appearance_OpacitySelect.reloadData() appearanceScaleCheckbox.properties.checkboxType = backgroundData.isScale ? .selected : .normal appearanceScaleCheckbox.reloadData() appearanceScaleSelect.properties.isDisabled = backgroundData.isScale ? false : true appearanceScaleSelect.properties.text = String(format: "%.0f", backgroundData.scale*100) + "%" appearanceScaleSelect.reloadData() //Position if backgroundData.verticalMode == 2 { positionItemView.properties.selRow = 0 } else if backgroundData.verticalMode == 1 { positionItemView.properties.selRow = 1 } else if backgroundData.verticalMode == 0 { positionItemView.properties.selRow = 2 } if backgroundData.horizontalMode == 0 { positionItemView.properties.selColumn = 0 } else if backgroundData.horizontalMode == 1 { positionItemView.properties.selColumn = 1 } else if backgroundData.horizontalMode == 2 { positionItemView.properties.selColumn = 2 } positionItemView.reloadData() positionXInput.properties.text = String(format: "%.0f", backgroundData.horizontalSpace) positionXInput.reloadData() positionYInput.properties.text = String(format: "%.0f", backgroundData.verticalSpace) positionYInput.reloadData() if backgroundData.type == .color { saveButton.properties.isDisabled = false } else if backgroundData.type == .image { if backgroundData.imagePath != nil { saveButton.properties.isDisabled = false } else { saveButton.properties.isDisabled = true } } saveButton.reloadData() } //MARK: - Action @objc func leftTopButtonClicked(_ sender: ComponentButton) { if sender == leftTopButton { var isChanged = false if let dict = self.originalDataDict { isChanged = KMBackgroundManager.compareIsChangedModel(backgroundData, withDict: dict as NSDictionary) } if isChanged == true { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = KMLocalizedString("Save template changes?") alert.informativeText = KMLocalizedString("Cancel and they will not be saved.") alert.addButton(withTitle: KMLocalizedString("Save")) alert.addButton(withTitle: KMLocalizedString("Cancel")) alert.beginSheetModal(for: NSApp.mainWindow!) { (response) in if response == NSApplication.ModalResponse.alertFirstButtonReturn { let _ = KMBackgroundManager.defaultManager.updateTemplate(model: self.backgroundData) self.delegate?.backgroundPropertyControllerFinishTemplateEdit?(self) } else { if let dict = self.originalDataDict { KMBackgroundManager.defaultManager.updateModel(self.backgroundData, withDict: dict as NSDictionary) let _ = KMBackgroundManager.defaultManager.updateTemplate(model: self.backgroundData) } self.delegate?.backgroundPropertyControllerFinishTemplateEdit?(self) } } } else { delegate?.backgroundPropertyControllerCancelTemplateEdit?(self) } } } @objc func templateButtonClicked(_ sender: ComponentButton) { if editSubType == .add { delegate?.backgroundPropertyControllerDidChangetoTemplate?(self) } else if editSubType == .edit { let _ = KMBackgroundManager.defaultManager.updateTemplate(model: self.backgroundData) delegate?.backgroundPropertyControllerFinishTemplateEdit?(self) } } @objc func checkBoxClicked(_ sender: ComponentCheckBox) { if sender == appearanceScaleCheckbox { backgroundData.isScale = appearanceScaleCheckbox.properties.checkboxType == .selected ? true : false } reloadData() delegate?.backgroundPropertyControllerDidUpdate?(self) } @objc func saveButtonClicked(_ sender: ComponentButton) { let saveWindow: KMWatermarkSaveWindow = KMWatermarkSaveWindow(windowNibName: "KMWatermarkSaveWindow") if backgroundData.type == .color { saveWindow.nameValue = backgroundData.name } else if backgroundData.type == .image { saveWindow.nameValue = backgroundData.imagePath?.lastPathComponent ?? "" } saveWindow.saveHandler = {[weak self] string in guard let weakSelf = self else { return } DispatchQueue.main.async { weakSelf.backgroundData.name = string ?? "" if KMBackgroundManager.defaultManager.addTemplate(model: weakSelf.backgroundData) { weakSelf.delegate?.backgroundPropertyControllerSaveTemplateSuccess?(weakSelf, weakSelf.backgroundData) } } } saveWindow.own_beginSheetModal(for: view.window) { string in } } @objc func chooseURLAction(_ sender: NSView) { let openPanel = NSOpenPanel() openPanel.canChooseDirectories = false openPanel.canChooseFiles = true openPanel.allowsMultipleSelection = false openPanel.allowedFileTypes = ["jpg", "jpeg", "png", "pdf"] openPanel.beginSheetModal(for: self.view.window!) { (result) in if result == NSApplication.ModalResponse.OK { guard let url = openPanel.url else { return } let filePath = url.path let outFolder = KMBackgroundManager.defaultManager.kBackgroundImageFolder let fileName = filePath.getLastComponentDeleteExtension if filePath.extension.lowercased() == ".pdf" { let pdf = CPDFDocument(url: url) guard !pdf!.isEncrypted else { return } if let image = self.generateThumbnail(for: url) { if let outFolderPath = outFolder?.stringByAppendingPathComponent(fileName + ".png") { try?image.pngData()?.write(to: URL(fileURLWithPath: outFolderPath)) if FileManager.default.fileExists(atPath: outFolderPath) { self.backgroundData.imagePath = outFolderPath self.delegate?.backgroundPropertyControllerDidUpdate?(self) self.reloadData() } } } } else { let image = NSImage(contentsOfFile: url.path) if image == nil || NSImage.isDamageImage(image, imagePath: url.path) { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = "The file \"\(url.lastPathComponent)\" could not be opened." alert.informativeText = "It may be damaged or use a file format that PDF Reader Pro doesn’t recognize." alert.addButton(withTitle: "Cancel") alert.beginSheetModal(for: NSApp.mainWindow!) { (response) in if response == NSApplication.ModalResponse.alertFirstButtonReturn { // Cancel button clicked } } return } if let resultPath = outFolder?.stringByAppendingPathComponent(fileName + ".png") { try?FileManager.default.copyItem(atPath: url.path, toPath: resultPath) self.backgroundData.imagePath = resultPath } self.delegate?.backgroundPropertyControllerDidUpdate?(self) self.reloadData() } } } } func generateThumbnail(for pdfURL: URL) -> NSImage? { // 创建PDF文档 guard let pdfDocument = CGPDFDocument(pdfURL as CFURL), let pdfPage = pdfDocument.page(at: 1) else { print("打开PDF文件失败") return nil } // 获取页面尺寸 let pageRect = pdfPage.getBoxRect(.mediaBox) // 创建图形上下文 let size = NSSize(width: 200, height: 300) let bitsPerComponent = 8 let bytesPerPixel = 4 let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: bitsPerComponent, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo) // 绘制PDF页面 context?.saveGState() context?.translateBy(x: 0, y: size.height) context?.scaleBy(x: 1.0, y: -1.0) // 反转坐标系 context?.scaleBy(x: size.width / pageRect.width, y: size.height / pageRect.height) // 缩放 context?.drawPDFPage(pdfPage) context?.restoreGState() // 创建NSImage if let cgImage = context?.makeImage() { return NSImage(cgImage: cgImage, size: NSSize(width: size.width, height: size.height)) } else { return nil } } //MARK: - Mouse override func mouseDown(with event: NSEvent) { super.mouseDown(with: event) view.window?.makeFirstResponder(nil) } } //MARK: - ComponentTabsDelegate extension KMBackgroundPropertyController: ComponentTabsDelegate { func componentTabsDidSelected(_ view: ComponentTabs, _ property: ComponentTabsProperty) { if property == textTabProperty { backgroundData.type = .color } else if property == fileTabProperty { backgroundData.type = .image } reloadData() delegate?.backgroundPropertyControllerDidUpdate?(self) } } //MARK: - ComponentCColorDelegate extension KMBackgroundPropertyController: ComponentCColorDelegate { func componentCColorDidChooseColor(_ view: NSView, _ color: NSColor?) { if view == fontColorGroup { backgroundData.color = color ?? NSColor.clear } reloadData() delegate?.backgroundPropertyControllerDidUpdate?(self) } } //MARK: - ComponentSelectDelegate extension KMBackgroundPropertyController: ComponentSelectDelegate { func componentSelectDidSelect(view: ComponentSelect?, menuItemProperty: ComponentMenuitemProperty?) { if view == appearance_OpacitySelect { if let text = menuItemProperty?.text { let result = text.stringByDeleteCharString("%") backgroundData.opacity = result.stringToCGFloat()/100 } } else if view == appearance_RotateSelect { if let text = menuItemProperty?.text { let result = text.stringByDeleteCharString("°") backgroundData.rotation = result.stringToCGFloat() } } else if view == appearanceScaleSelect { if let text = menuItemProperty?.text { let result = text.stringByDeleteCharString("%") backgroundData.scale = result.stringToCGFloat()/100 } } reloadData() delegate?.backgroundPropertyControllerDidUpdate?(self) } func componentSelectTextDidEndEditing(_ view: ComponentSelect) { if view == appearance_OpacitySelect { if let text = appearance_OpacitySelect.properties.text { let result = text.stringByDeleteCharString("%") backgroundData.opacity = result.stringToCGFloat()/100 } } else if view == appearance_RotateSelect { if let text = appearance_RotateSelect.properties.text { let result = text.stringByDeleteCharString("°") backgroundData.rotation = result.stringToCGFloat() } } else if view == appearanceScaleSelect { if let text = appearanceScaleSelect.properties.text { let result = text.stringByDeleteCharString("%") backgroundData.scale = min(10, result.stringToCGFloat()/100) } } reloadData() delegate?.backgroundPropertyControllerDidUpdate?(self) } } //MARK: - ComponentCPositionDelegate extension KMBackgroundPropertyController: ComponentCPositionDelegate { func componentCPositionDidChoose(_ view: NSView, _ row: Int, _ column: Int) { if view == positionItemView { if row == 0 { backgroundData.verticalMode = 2 } else if row == 1 { backgroundData.verticalMode = 1 } else if row == 2 { backgroundData.verticalMode = 0 } if column == 0 { backgroundData.horizontalMode = 0 } else if column == 1 { backgroundData.horizontalMode = 1 } else if column == 2 { backgroundData.horizontalMode = 2 } } reloadData() delegate?.backgroundPropertyControllerDidUpdate?(self) } } //MARK: - ComponentInputNumberDelegate extension KMBackgroundPropertyController: ComponentInputNumberDelegate { func componentInputNumberDidValueChanged(inputNumber: ComponentInputNumber?) { if inputNumber == positionXInput { if let text = inputNumber?.properties.text { backgroundData.horizontalSpace = text.stringToCGFloat() } } else if inputNumber == positionYInput { if let text = inputNumber?.properties.text { backgroundData.verticalSpace = text.stringToCGFloat() } } reloadData() delegate?.backgroundPropertyControllerDidUpdate?(self) } }