diff --git a/apps/macos-sync/Sources/CairnquireSyncApp/App.swift b/apps/macos-sync/Sources/CairnquireSyncApp/App.swift index 3c887cf..3988516 100644 --- a/apps/macos-sync/Sources/CairnquireSyncApp/App.swift +++ b/apps/macos-sync/Sources/CairnquireSyncApp/App.swift @@ -81,21 +81,27 @@ class AppDelegate: NSObject, NSApplicationDelegate { func updateIcon(config: SyncConfig) { guard controller.uploadProgress == nil else { return } let iconName = config.iconName - let image = NSImage(systemSymbolName: iconName, accessibilityDescription: "Cairnquire") - ?? NSImage(systemSymbolName: "tray.circle.fill", accessibilityDescription: "Cairnquire") - dropButton.image = image + dropButton.image = makeMenuBarIcon(named: iconName, accessibilityDescription: "Cairnquire") + dropButton.setDropTargetActive(false) } private func updateIconForUpload(progress: Double?) { if progress != nil { // During upload: show tray.circle (outline) - let image = NSImage(systemSymbolName: "tray.circle", accessibilityDescription: "Uploading") - dropButton.image = image + dropButton.image = makeMenuBarIcon(named: "tray.circle", accessibilityDescription: "Uploading") + dropButton.setDropTargetActive(false) } else { // Upload complete: restore normal icon updateIcon(config: controller.config) } } + + private func makeMenuBarIcon(named iconName: String, accessibilityDescription: String) -> NSImage? { + let image = NSImage(systemSymbolName: iconName, accessibilityDescription: accessibilityDescription) + ?? NSImage(systemSymbolName: "tray.circle.fill", accessibilityDescription: accessibilityDescription) + image?.isTemplate = true + return image + } func bounceIcon() { guard let button = dropButton else { return } @@ -325,19 +331,32 @@ class DropStatusButton: NSButton { } private func setup() { + isBordered = false + bezelStyle = .regularSquare + imageScaling = .scaleProportionallyDown registerForDraggedTypes([.fileURL]) } + + func setDropTargetActive(_ isActive: Bool) { + isBordered = isActive + contentTintColor = isActive ? .systemBlue : nil + needsDisplay = true + } override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { - // Tint icon slightly to indicate drop zone is active - if let image = self.image { - self.image = image.tinted(with: .systemBlue) + let pasteboard = sender.draggingPasteboard + guard pasteboard.canReadObject(forClasses: [NSURL.self], options: nil) else { + setDropTargetActive(false) + return [] } + + // Show the button frame only while the menu bar icon is a valid drop target. + setDropTargetActive(true) return .copy } override func draggingExited(_ sender: NSDraggingInfo?) { - // Restore original icon + setDropTargetActive(false) if let appDelegate = appDelegate { appDelegate.updateIcon(config: appDelegate.controller.config) } @@ -347,9 +366,11 @@ class DropStatusButton: NSButton { let pasteboard = sender.draggingPasteboard guard let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: nil) as? [URL], !urls.isEmpty else { + setDropTargetActive(false) return false } + setDropTargetActive(false) appDelegate?.bounceIcon() Task { @MainActor in @@ -359,18 +380,3 @@ class DropStatusButton: NSButton { return true } } - -extension NSImage { - func tinted(with color: NSColor) -> NSImage { - let image = self.copy() as! NSImage - image.isTemplate = false - - let rect = NSRect(origin: .zero, size: image.size) - image.lockFocus() - color.set() - rect.fill(using: .sourceAtop) - image.unlockFocus() - - return image - } -}