Refine macOS menu bar drop target icon

This commit is contained in:
2026-06-09 16:41:10 -04:00
parent 632621b226
commit 1e939f307d

View File

@@ -81,22 +81,28 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func updateIcon(config: SyncConfig) { func updateIcon(config: SyncConfig) {
guard controller.uploadProgress == nil else { return } guard controller.uploadProgress == nil else { return }
let iconName = config.iconName let iconName = config.iconName
let image = NSImage(systemSymbolName: iconName, accessibilityDescription: "Cairnquire") dropButton.image = makeMenuBarIcon(named: iconName, accessibilityDescription: "Cairnquire")
?? NSImage(systemSymbolName: "tray.circle.fill", accessibilityDescription: "Cairnquire") dropButton.setDropTargetActive(false)
dropButton.image = image
} }
private func updateIconForUpload(progress: Double?) { private func updateIconForUpload(progress: Double?) {
if progress != nil { if progress != nil {
// During upload: show tray.circle (outline) // During upload: show tray.circle (outline)
let image = NSImage(systemSymbolName: "tray.circle", accessibilityDescription: "Uploading") dropButton.image = makeMenuBarIcon(named: "tray.circle", accessibilityDescription: "Uploading")
dropButton.image = image dropButton.setDropTargetActive(false)
} else { } else {
// Upload complete: restore normal icon // Upload complete: restore normal icon
updateIcon(config: controller.config) 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() { func bounceIcon() {
guard let button = dropButton else { return } guard let button = dropButton else { return }
@@ -325,19 +331,32 @@ class DropStatusButton: NSButton {
} }
private func setup() { private func setup() {
isBordered = false
bezelStyle = .regularSquare
imageScaling = .scaleProportionallyDown
registerForDraggedTypes([.fileURL]) registerForDraggedTypes([.fileURL])
} }
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { func setDropTargetActive(_ isActive: Bool) {
// Tint icon slightly to indicate drop zone is active isBordered = isActive
if let image = self.image { contentTintColor = isActive ? .systemBlue : nil
self.image = image.tinted(with: .systemBlue) needsDisplay = true
} }
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
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 return .copy
} }
override func draggingExited(_ sender: NSDraggingInfo?) { override func draggingExited(_ sender: NSDraggingInfo?) {
// Restore original icon setDropTargetActive(false)
if let appDelegate = appDelegate { if let appDelegate = appDelegate {
appDelegate.updateIcon(config: appDelegate.controller.config) appDelegate.updateIcon(config: appDelegate.controller.config)
} }
@@ -347,9 +366,11 @@ class DropStatusButton: NSButton {
let pasteboard = sender.draggingPasteboard let pasteboard = sender.draggingPasteboard
guard let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: nil) as? [URL], guard let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: nil) as? [URL],
!urls.isEmpty else { !urls.isEmpty else {
setDropTargetActive(false)
return false return false
} }
setDropTargetActive(false)
appDelegate?.bounceIcon() appDelegate?.bounceIcon()
Task { @MainActor in Task { @MainActor in
@@ -359,18 +380,3 @@ class DropStatusButton: NSButton {
return true 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
}
}