🎉 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:
711
ShotScreen/Sources/RenameActionHandler.swift
Executable file
711
ShotScreen/Sources/RenameActionHandler.swift
Executable 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user