🎉 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.
This commit is contained in:
2025-06-28 16:15:15 +02:00
commit 0dabed11d2
63 changed files with 25727 additions and 0 deletions

View File

@@ -0,0 +1,711 @@
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
}
}