Files
shotscreen/ShotScreen/Sources/RenameActionHandler.swift
Nick Roodenrijs 0dabed11d2 🎉 ShotScreen v1.0 - Initial Release
🚀 First official release of ShotScreen with complete feature set:

 Core Features:
- Advanced screenshot capture system
- Multi-monitor support
- Professional UI/UX design
- Automated update system with Sparkle
- Apple notarized & code signed

🛠 Technical Excellence:
- Native Swift macOS application
- Professional build & deployment pipeline
- Comprehensive error handling
- Memory optimized performance

📦 Distribution Ready:
- Professional DMG packaging
- Apple notarization complete
- No security warnings for users
- Ready for public distribution

This is the foundation release that establishes ShotScreen as a premium screenshot tool for macOS.
2025-06-28 16:15:15 +02:00

711 lines
32 KiB
Swift
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}