import AppKit import UniformTypeIdentifiers // NIEUW: Voeg ConflictType enum definitie toe enum ConflictType { case renameOnly case saveToFolder // TODO: case moveToApplications // Als dit later nodig is } // NIEUW: Custom TextFieldCell voor verticale aanpassing van tekst class VerticallyAdjustedTextFieldCell: NSTextFieldCell { var verticalOffset: CGFloat = 0 // didSet is hier mogelijk niet effectief genoeg, we forceren de update in drawingRect // { // didSet { // self.controlView?.needsDisplay = true // } // } override func drawingRect(forBounds rect: NSRect) -> NSRect { var drawingRect = super.drawingRect(forBounds: rect) // Een positieve offset verplaatst de tekst naar beneden drawingRect.origin.y = drawingRect.origin.y + verticalOffset if let controlView = self.controlView { controlView.setNeedsDisplay(controlView.bounds) // Correcte aanroep met bounds } return drawingRect } } // NIEUW: Custom Button klasse met icoon en subtiele tekst class IconTextButton: NSButton { convenience init(sfSymbolName: String, title: String, target: AnyObject?, action: Selector?) { self.init() self.target = target self.action = action let symbolConfig = NSImage.SymbolConfiguration(pointSize: 12, weight: .regular) if let iconImage = NSImage(systemSymbolName: sfSymbolName, accessibilityDescription: title)?.withSymbolConfiguration(symbolConfig) { self.image = iconImage } let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .center let attributes: [NSAttributedString.Key: Any] = [ .foregroundColor: NSColor(white: 0.90, alpha: 0.85), .font: NSFont.systemFont(ofSize: 10, weight: .medium), .paragraphStyle: paragraphStyle ] self.attributedTitle = NSAttributedString(string: title, attributes: attributes) self.setButtonType(.momentaryPushIn) self.isBordered = false self.imagePosition = .imageLeading self.imageHugsTitle = true self.alignment = .center self.wantsLayer = true self.layer?.backgroundColor = NSColor.clear.cgColor self.layer?.cornerRadius = 5 (self.cell as? NSButtonCell)?.setAccessibilityLabel(title) } private var trackingArea: NSTrackingArea? override func updateTrackingAreas() { super.updateTrackingAreas() if let existingTrackingArea = self.trackingArea { self.removeTrackingArea(existingTrackingArea) } let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways, .inVisibleRect] trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil) if let ta = trackingArea { self.addTrackingArea(ta) } } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) NSAnimationContext.runAnimationGroup({ context in context.duration = 0.15 context.allowsImplicitAnimation = true self.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.15).cgColor }, completionHandler: nil) } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) NSAnimationContext.runAnimationGroup({ context in context.duration = 0.2 context.allowsImplicitAnimation = true self.layer?.backgroundColor = NSColor.clear.cgColor }, completionHandler: nil) } override var intrinsicContentSize: NSSize { var size = super.intrinsicContentSize size.width += 16 // Extra padding: 8pt left, 8pt right size.height = 24 return size } } // Custom Panel for Rename Input class RenamePromptPanel: NSPanel, NSTextFieldDelegate { var textField: NSTextField! var saveNameButton: IconTextButton! var saveToFolderButton: IconTextButton! var cancelButton: IconTextButton! var enteredName: String? { textField.stringValue } let textFieldTag = 101 var onAction: ((NSApplication.ModalResponse) -> Void)? var animationStartFrame: NSRect? // private var keepKeyTimer: Timer? // Uitgecommentarieerd weak var actionHandlerDelegate: RenameActionHandlerDelegate? override var acceptsFirstResponder: Bool { return true } override var canBecomeKey: Bool { true } override var canBecomeMain: Bool { true } override func keyDown(with event: NSEvent) { if event.keyCode == 53 { // ESC key closeRenamePanelAndCleanup(response: .cancel) return } super.keyDown(with: event) } private func closeRenamePanelAndCleanup(response: NSApplication.ModalResponse) { print("๐Ÿงผ DEBUG: closeRenamePanelAndCleanup called with response: \(response)") // keepKeyTimer?.invalidate() // Uitgecommentarieerd // keepKeyTimer = nil // Uitgecommentarieerd NSAnimationContext.runAnimationGroup({ context in context.duration = 0.4 context.timingFunction = CAMediaTimingFunction(name: .easeOut) self.animator().alphaValue = 0 if let startFrame = self.animationStartFrame { let endFrame = NSRect(x: startFrame.midX - 25, y: startFrame.midY - 12.5, width: 50, height: 25) self.animator().setFrame(endFrame, display: true) } }, completionHandler: { self.orderOut(nil) self.alphaValue = 1 self.actionHandlerDelegate?.enableGridMonitoring() self.actionHandlerDelegate?.hideGrid() self.onAction?(response) }) } override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { super.init(contentRect: contentRect, styleMask: [.borderless, .utilityWindow, .hudWindow, .nonactivatingPanel], backing: backingStoreType, defer: flag) self.isFloatingPanel = true self.level = .floating + 3 // Teruggezet self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] self.isOpaque = false self.backgroundColor = NSColor.clear self.hasShadow = false self.animationBehavior = .utilityWindow self.becomesKeyOnlyIfNeeded = true self.hidesOnDeactivate = false let visualEffectView = NSVisualEffectView() visualEffectView.blendingMode = .behindWindow visualEffectView.material = .hudWindow visualEffectView.state = .active visualEffectView.wantsLayer = true visualEffectView.layer?.cornerRadius = 12.0 visualEffectView.layer?.masksToBounds = true visualEffectView.translatesAutoresizingMaskIntoConstraints = false self.contentView = visualEffectView let horizontalPadding: CGFloat = 10 let verticalPadding: CGFloat = 15 let textFieldHeight: CGFloat = 19 let spacingBelowTextField: CGFloat = 12 let buttonSpacing: CGFloat = 8 let buttonHeight: CGFloat = 24 textField = NSTextField() textField.tag = textFieldTag textField.delegate = self textField.translatesAutoresizingMaskIntoConstraints = false visualEffectView.addSubview(textField) textFieldStyling() saveNameButton = IconTextButton(sfSymbolName: "checkmark.circle", title: " Save", target: self, action: #selector(buttonClicked(_:))) saveNameButton.tag = NSApplication.ModalResponse.OK.rawValue saveNameButton.translatesAutoresizingMaskIntoConstraints = false visualEffectView.addSubview(saveNameButton) saveToFolderButton = IconTextButton(sfSymbolName: "folder.badge.plus", title: " Save", target: self, action: #selector(buttonClicked(_:))) saveToFolderButton.tag = NSApplication.ModalResponse.continue.rawValue saveToFolderButton.translatesAutoresizingMaskIntoConstraints = false visualEffectView.addSubview(saveToFolderButton) cancelButton = IconTextButton(sfSymbolName: "xmark.circle", title: " Cancel", target: self, action: #selector(buttonClicked(_:))) cancelButton.tag = NSApplication.ModalResponse.cancel.rawValue cancelButton.translatesAutoresizingMaskIntoConstraints = false visualEffectView.addSubview(cancelButton) let buttons = [saveNameButton!, saveToFolderButton!, cancelButton!] let buttonStackView = NSStackView(views: buttons) buttonStackView.orientation = .horizontal buttonStackView.alignment = .centerY buttonStackView.spacing = buttonSpacing buttonStackView.distribution = .fillProportionally buttonStackView.translatesAutoresizingMaskIntoConstraints = false visualEffectView.addSubview(buttonStackView) let minPanelWidth: CGFloat = 280 let calculatedPanelHeight = verticalPadding + textFieldHeight + spacingBelowTextField + buttonHeight + verticalPadding self.setContentSize(NSSize(width: minPanelWidth, height: calculatedPanelHeight)) NSLayoutConstraint.activate([ visualEffectView.leadingAnchor.constraint(equalTo: (self.contentView!).leadingAnchor), visualEffectView.trailingAnchor.constraint(equalTo: (self.contentView!).trailingAnchor), visualEffectView.topAnchor.constraint(equalTo: (self.contentView!).topAnchor), visualEffectView.bottomAnchor.constraint(equalTo: (self.contentView!).bottomAnchor), textField.topAnchor.constraint(equalTo: visualEffectView.topAnchor, constant: verticalPadding), textField.leadingAnchor.constraint(equalTo: visualEffectView.leadingAnchor, constant: horizontalPadding), textField.trailingAnchor.constraint(equalTo: visualEffectView.trailingAnchor, constant: -horizontalPadding), textField.heightAnchor.constraint(equalToConstant: textFieldHeight), buttonStackView.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: spacingBelowTextField), buttonStackView.centerXAnchor.constraint(equalTo: visualEffectView.centerXAnchor), buttonStackView.heightAnchor.constraint(equalToConstant: buttonHeight), buttonStackView.leadingAnchor.constraint(greaterThanOrEqualTo: visualEffectView.leadingAnchor, constant: horizontalPadding), buttonStackView.trailingAnchor.constraint(lessThanOrEqualTo: visualEffectView.trailingAnchor, constant: -horizontalPadding), buttonStackView.bottomAnchor.constraint(equalTo: visualEffectView.bottomAnchor, constant: -verticalPadding) ]) let stackViewWidthConstraint = buttonStackView.widthAnchor.constraint(lessThanOrEqualToConstant: minPanelWidth - 2 * horizontalPadding) stackViewWidthConstraint.priority = .defaultHigh stackViewWidthConstraint.isActive = true // keepKeyTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(ensureKeyWindow), userInfo: nil, repeats: true) // Uitgecommentarieerd self.initialFirstResponder = textField } private func textFieldStyling() { textField.font = NSFont.systemFont(ofSize: 13) textField.textColor = NSColor(white: 0.95, alpha: 1.0) textField.isEditable = true textField.isSelectable = true textField.isBordered = false textField.isBezeled = false textField.backgroundColor = NSColor.clear textField.focusRingType = .none textField.wantsLayer = true textField.layer?.cornerRadius = 6.0 textField.layer?.masksToBounds = true textField.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.25).cgColor textField.layer?.borderColor = NSColor.gray.withAlphaComponent(0.4).cgColor textField.layer?.borderWidth = 0.5 let cell = textField.cell as? NSTextFieldCell cell?.usesSingleLineMode = true cell?.wraps = false cell?.isScrollable = true cell?.isEditable = true cell?.isSelectable = true // Set alignment AFTER configuring the cell textField.alignment = .center if let standardCell = textField.cell as? NSTextFieldCell { // Controleer of het een NSTextFieldCell is standardCell.alignment = .center } } @objc func ensureKeyWindow() { if !self.isKeyWindow { self.makeKeyAndOrderFront(nil) DispatchQueue.main.async { _ = self.makeFirstResponder(self.textField) } } } @objc func buttonClicked(_ sender: NSButton) { let response = NSApplication.ModalResponse(rawValue: sender.tag) print("๐Ÿ” DEBUG: RenamePromptPanel button clicked with tag: \(sender.tag), mapped to response: \(response)") closeRenamePanelAndCleanup(response: response) } func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { if commandSelector == #selector(insertNewline(_:)) { if let okButton = saveNameButton { buttonClicked(okButton) } return true } return false } } protocol RenameActionHandlerDelegate: AnyObject { func getScreenshotFolder() -> String? func renameActionHandler(_ handler: RenameActionHandler, didRenameFileFrom oldURL: URL, to newURL: URL) func findFilenameLabel(in window: NSWindow?) -> NSTextField? func setTempFileURL(_ url: URL?) func getActivePreviewWindow() -> NSWindow? func closePreviewWithAnimation(immediate: Bool, preserveTempFile: Bool) func getGridWindowFrame() -> NSRect? func hideGrid() func disableGridMonitoring() func enableGridMonitoring() } class RenameActionHandler { weak var delegate: RenameActionHandlerDelegate? private var renamePanel: RenamePromptPanel? init(delegate: RenameActionHandlerDelegate) { self.delegate = delegate } func isRenamePanelActive() -> Bool { return renamePanel?.isVisible == true } func closeRenamePanelAndCleanup() { if let panel = self.renamePanel, panel.isVisible { print("โ„น๏ธ Closing existing rename panel due to new action.") if let panelCancelButton = panel.cancelButton { panel.buttonClicked(panelCancelButton) } else { print("โš ๏ธ Could not find cancel button in rename panel to simulate click.") self.renamePanel = nil } } } private func sanitize(name: String) -> String { var characterSet = CharacterSet.alphanumerics characterSet.insert(charactersIn: "-_") let sanitized = name.trimmingCharacters(in: .whitespacesAndNewlines) .components(separatedBy: characterSet.inverted).joined() return sanitized.isEmpty ? "screenshot" : sanitized } func promptAndRename(originalURL: URL, completion: @escaping (NSApplication.ModalResponse) -> Void) { print("๐Ÿ” DEBUG: promptAndRename called with URL: \(originalURL)") print("๐Ÿ” DEBUG: Current working directory: \(FileManager.default.currentDirectoryPath)") print("๐Ÿ” DEBUG: Original URL exists: \(FileManager.default.fileExists(atPath: originalURL.path))") if let existingPanel = self.renamePanel, existingPanel.isVisible { print("โ„น๏ธ Rename panel already visible. Bringing to front.") existingPanel.makeKeyAndOrderFront(nil) completion(.cancel) return } guard let handlerDelegate = self.delegate else { print("โŒ RenameActionHandler: handlerDelegate (for self) not set.") completion(.cancel) return } let tempFilename = originalURL.deletingPathExtension().lastPathComponent let gridFrame = handlerDelegate.getGridWindowFrame() let panel = RenamePromptPanel(animationStartFrame: gridFrame) panel.actionHandlerDelegate = handlerDelegate panel.textField.stringValue = tempFilename self.renamePanel = panel panel.onAction = { [weak self] response in guard let self = self else { print("๐Ÿ” DEBUG: self is nil in onAction callback") completion(.cancel) return } print("๐Ÿ” DEBUG: RenamePanel onAction called with response: \(response)") let enteredName = self.renamePanel?.enteredName ?? "" print("๐Ÿ” DEBUG: enteredName: '\(enteredName)'") var actionResponse = response if response == .OK { print("๐Ÿ” DEBUG: Processing Save Name Only action") _ = self.performRename(originalURL: originalURL, newNameInput: enteredName) } else if response == .continue { print("๐Ÿ” DEBUG: Processing Save to Folder action") if let renamedURL = self.performRename(originalURL: originalURL, newNameInput: enteredName) { self.saveToDesignatedFolder(fileURL: renamedURL, desiredName: enteredName) } else { print("Rename failed or was cancelled, cannot proceed to Save to Folder.") actionResponse = .cancel } } else { print("๐Ÿ” DEBUG: Processing Cancel action or unknown response: \(response)") } print("๐Ÿ” DEBUG: Calling completion handler with actionResponse: \(actionResponse)") completion(actionResponse) if response != .OK && response != .continue { self.renamePanel = nil } } let previewFrame = handlerDelegate.getActivePreviewWindow()?.frame let panelSize = panel.frame.size var finalFrame = panel.frame if let currentGridFrame = gridFrame { let spacing: CGFloat = 20 finalFrame.origin.x = currentGridFrame.origin.x - panelSize.width - spacing finalFrame.origin.y = currentGridFrame.origin.y + (currentGridFrame.height - panelSize.height) / 2 finalFrame.size = panelSize let targetScreenForGrid = NSScreen.screens.first { $0.frame.intersects(currentGridFrame) } ?? NSScreen.main ?? NSScreen.screens.first! let screenVisibleFrame = targetScreenForGrid.visibleFrame if finalFrame.origin.x < screenVisibleFrame.origin.x { finalFrame.origin.x = currentGridFrame.maxX + spacing } if finalFrame.origin.y < screenVisibleFrame.origin.y { finalFrame.origin.y = screenVisibleFrame.origin.y + 50 } if finalFrame.maxY > screenVisibleFrame.maxY { finalFrame.origin.y = screenVisibleFrame.maxY - panelSize.height - 50 } } else { let previewScreen = handlerDelegate.getActivePreviewWindow()?.screen let mouseScreen = NSScreen.screenWithMouse() let targetScreenForFallback = previewScreen ?? mouseScreen ?? NSScreen.main ?? NSScreen.screens.first! let screenVisibleFrame = targetScreenForFallback.visibleFrame finalFrame.origin.x = screenVisibleFrame.origin.x + (screenVisibleFrame.width - panelSize.width) / 2 finalFrame.origin.y = screenVisibleFrame.origin.y + (screenVisibleFrame.height - panelSize.height) / 2 finalFrame.size = panelSize } panel.alphaValue = 0.0 let startFrameForAnimation: NSRect if let currentGridFrame = gridFrame { startFrameForAnimation = NSRect(x: currentGridFrame.midX - panelSize.width / 2, y: currentGridFrame.midY - panelSize.height / 2, width: panelSize.width, height: panelSize.height) } else if let previewFrame = previewFrame { startFrameForAnimation = NSRect(x: previewFrame.midX - panelSize.width / 2, y: previewFrame.midY - panelSize.height / 2, width: panelSize.width, height: panelSize.height) } else { startFrameForAnimation = finalFrame.offsetBy(dx: 0, dy: 50) } panel.setFrame(startFrameForAnimation, display: false) panel.animationStartFrame = startFrameForAnimation panel.orderFront(nil) handlerDelegate.disableGridMonitoring() NSAnimationContext.runAnimationGroup({ context in context.duration = 0.3 context.timingFunction = CAMediaTimingFunction(name: .easeOut) panel.animator().alphaValue = 1.0 panel.animator().setFrame(finalFrame, display: true) }, completionHandler: { print("โœจ Panel animation complete. Current panel alpha: \(panel.alphaValue), isVisible: \(panel.isVisible), frame: \(panel.frame)") panel.makeKey() DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { if panel.isVisible { if panel.alphaValue < 0.1 { print("โš ๏ธ Panel is visible but alpha is very low. Might be a ghost panel.") } } else { print("โš ๏ธ Panel disappeared unexpectedly after animation.") self.renamePanel = nil completion(.cancel) } } DispatchQueue.main.async { if let field = panel.textField { if panel.firstResponder != field { if panel.makeFirstResponder(field) { field.currentEditor()?.selectedRange = NSRange(location: 0, length: field.stringValue.count) print("โŒจ๏ธ DEBUG: TextField is first responder, text selected.") } else { print("โš ๏ธ DEBUG: Failed to make TextField first responder.") } } else { print("โŒจ๏ธ DEBUG: TextField was already first responder.") field.currentEditor()?.selectedRange = NSRange(location: 0, length: field.stringValue.count) } } } }) } private func performRename(originalURL: URL, newNameInput: String) -> URL? { guard let handlerDelegateRef = delegate else { return nil } let currentNameWithoutExt = originalURL.deletingPathExtension().lastPathComponent let sanitizedNewName = self.sanitize(name: newNameInput) if newNameInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || sanitizedNewName == currentNameWithoutExt.replacingOccurrences(of: "screenshot_", with: "").replacingOccurrences(of: ".", with: "_") { print("โ„น๏ธ Name not changed or empty, using original: \(originalURL.lastPathComponent)") handlerDelegateRef.setTempFileURL(originalURL) DispatchQueue.main.async { if let label = handlerDelegateRef.findFilenameLabel(in: handlerDelegateRef.getActivePreviewWindow()) { label.stringValue = originalURL.lastPathComponent label.toolTip = originalURL.lastPathComponent } } return originalURL } let fileManager = FileManager.default let directory = originalURL.deletingLastPathComponent() let newURL = directory.appendingPathComponent(sanitizedNewName).appendingPathExtension(originalURL.pathExtension.isEmpty ? "png" : originalURL.pathExtension) if fileManager.fileExists(atPath: newURL.path) { print("โš ๏ธ Rename failed: File already exists at \(newURL.path)") DispatchQueue.main.async { let conflictAlert = NSAlert() conflictAlert.messageText = "File Exists" conflictAlert.informativeText = "A file named \"\(newURL.lastPathComponent)\" already exists. Please use a different name." conflictAlert.addButton(withTitle: "OK") if let panelWindow = self.renamePanel { conflictAlert.beginSheetModal(for: panelWindow, completionHandler: nil) } else { conflictAlert.runModal() } } return nil } do { try fileManager.moveItem(at: originalURL, to: newURL) print("โœ… Temp File renamed from \(originalURL.lastPathComponent) to \(newURL.lastPathComponent)") handlerDelegateRef.setTempFileURL(newURL) DispatchQueue.main.async { if let label = handlerDelegateRef.findFilenameLabel(in: handlerDelegateRef.getActivePreviewWindow()) { label.stringValue = newURL.lastPathComponent label.toolTip = newURL.lastPathComponent } } return newURL } catch { print("โŒ Error renaming temp file: \(error)") DispatchQueue.main.async { let errorAlert = NSAlert(error: error) if let panelWindow = self.renamePanel { errorAlert.beginSheetModal(for: panelWindow, completionHandler: nil) } else { errorAlert.runModal() } } return nil } } private func saveToDesignatedFolder(fileURL: URL, desiredName: String) { guard let handlerDelegateRef = delegate else { return } guard let destinationFolderStr = handlerDelegateRef.getScreenshotFolder(), !destinationFolderStr.isEmpty else { DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "Folder Not Set" alert.informativeText = "Please set a default save folder in Settings." alert.addButton(withTitle: "OK") if let hostWindow = handlerDelegateRef.getActivePreviewWindow() ?? self.renamePanel { alert.beginSheetModal(for: hostWindow, completionHandler: nil) } else { alert.runModal() } } return } let finalFilename = fileURL.lastPathComponent let fileManager = FileManager.default let destinationFolderURL = URL(fileURLWithPath: destinationFolderStr) let destinationURL = destinationFolderURL.appendingPathComponent(finalFilename) if fileManager.fileExists(atPath: destinationURL.path) { print("โš ๏ธ Save failed: File already exists at \(destinationURL.path)") DispatchQueue.main.async { let conflictAlert = NSAlert() conflictAlert.messageText = "File Exists in Destination" conflictAlert.informativeText = "A file named \"\(destinationURL.lastPathComponent)\" already exists in the destination folder \"\(destinationFolderURL.lastPathComponent)\"." conflictAlert.addButton(withTitle: "OK") if let hostWindow = handlerDelegateRef.getActivePreviewWindow() ?? self.renamePanel { conflictAlert.beginSheetModal(for: hostWindow, completionHandler: nil) } else { conflictAlert.runModal() } } return } do { try fileManager.moveItem(at: fileURL, to: destinationURL) print("โœ… Screenshot moved to: \(destinationURL.path)") handlerDelegateRef.setTempFileURL(nil) DispatchQueue.main.async { if !SettingsManager.shared.closeAfterSave { print("RenameActionHandler: closeAfterSave is OFF, expliciet sluiten van preview na Save to Folder.") handlerDelegateRef.closePreviewWithAnimation(immediate: false, preserveTempFile: false) } } } catch { print("โŒ Error moving file to destination folder: \(error)") DispatchQueue.main.async { let errorAlert = NSAlert(error: error) if let hostWindow = handlerDelegateRef.getActivePreviewWindow() ?? self.renamePanel { errorAlert.beginSheetModal(for: hostWindow, completionHandler: nil) } else { errorAlert.runModal() } } } } private func handleExistingFileConflict(for type: ConflictType, originalURL: URL, destinationURL: URL, destinationFolderURL: URL, completion: @escaping (NSApplication.ModalResponse) -> Void) { let conflictAlert = NSAlert() conflictAlert.messageText = "File Exists" let fileName = destinationURL.lastPathComponent let folderName = destinationFolderURL.lastPathComponent if type == .renameOnly { conflictAlert.informativeText = "A file named \"\(fileName)\" already exists. Please use a different name." let okButton = conflictAlert.addButton(withTitle: "OK") okButton.tag = NSApplication.ModalResponse.OK.rawValue } else { conflictAlert.informativeText = "A file named \"\(fileName)\" already exists in the destination folder \"\(folderName)\"." let overwriteButton = conflictAlert.addButton(withTitle: "Overwrite") overwriteButton.tag = NSApplication.ModalResponse.OK.rawValue let saveNewNameButton = conflictAlert.addButton(withTitle: "Save with New Name") saveNewNameButton.tag = NSApplication.ModalResponse.continue.rawValue let cancelButton = conflictAlert.addButton(withTitle: "Cancel") cancelButton.tag = NSApplication.ModalResponse.cancel.rawValue } var response = conflictAlert.runModal() if type == .renameOnly && response == .OK { response = .cancel } completion(response) } private func showRenameErrorAlert(message: String, informativeText: String, onOK: (() -> Void)? = nil) { let alert = NSAlert() alert.messageText = message alert.informativeText = informativeText let okButton = alert.addButton(withTitle: "OK") okButton.tag = NSApplication.ModalResponse.OK.rawValue alert.beginSheetModal(for: self.renamePanel ?? NSApplication.shared.keyWindow ?? NSWindow()) { response in if response == .OK { onOK?() } } } } extension RenamePromptPanel { convenience init(animationStartFrame: NSRect?) { let initialWidth: CGFloat = 280 let initialHeight: CGFloat = 130 var initialRect = NSRect(x: 0, y: 0, width: initialWidth, height: initialHeight) if let startFrame = animationStartFrame, let mainScreen = NSScreen.main { let screenFrame = mainScreen.visibleFrame initialRect.origin.x = startFrame.midX - initialWidth / 2 initialRect.origin.y = startFrame.midY - initialHeight / 2 if initialRect.maxX > screenFrame.maxX { initialRect.origin.x = screenFrame.maxX - initialWidth } if initialRect.minX < screenFrame.minX { initialRect.origin.x = screenFrame.minX } if initialRect.maxY > screenFrame.maxY { initialRect.origin.y = screenFrame.maxY - initialHeight } if initialRect.minY < screenFrame.minY { initialRect.origin.y = screenFrame.minY } } else if let mainScreen = NSScreen.main { initialRect.origin.x = (mainScreen.visibleFrame.width - initialWidth) / 2 + mainScreen.visibleFrame.origin.x initialRect.origin.y = (mainScreen.visibleFrame.height - initialHeight) / 2 + mainScreen.visibleFrame.origin.y } self.init(contentRect: initialRect, styleMask: [.borderless, .utilityWindow, .hudWindow, .nonactivatingPanel], backing: .buffered, defer: false) self.animationStartFrame = animationStartFrame } }