diff --git a/PixelPicker.xcodeproj/project.pbxproj b/PixelPicker.xcodeproj/project.pbxproj index 4e11a95..77dcaff 100644 --- a/PixelPicker.xcodeproj/project.pbxproj +++ b/PixelPicker.xcodeproj/project.pbxproj @@ -332,18 +332,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - BA74C14220AA409D00306B27 /* NSColor.swift in Sources */, + BA74C13320A6F16C00306B27 /* ShowAndHideCursor.m in Sources */, BA71817220AE2DB400619700 /* PPMenuShortcutView.m in Sources */, - 15492103207B53AE00E45BBC /* Util.swift in Sources */, - BA74C0F820A692D100306B27 /* PPOverlayController.swift in Sources */, - BA74C14E20ADA33E00306B27 /* PPColor.swift in Sources */, + B3B4C2C11E25894B009F8E4E /* AppDelegate.swift in Sources */, BA22232920B04D660001069C /* AppDelegate+Menu.swift in Sources */, + BA74C14E20ADA33E00306B27 /* PPColor.swift in Sources */, + 15FC8CC8204E2E2C000B5E1E /* PPState.swift in Sources */, BA74C13120A6E95900306B27 /* PPOverlayPanel.swift in Sources */, - B3B4C2C11E25894B009F8E4E /* AppDelegate.swift in Sources */, - BA74C14420AA609500306B27 /* PPOverlayWrapper.swift in Sources */, - BA74C13320A6F16C00306B27 /* ShowAndHideCursor.m in Sources */, BA74C14020AA390400306B27 /* PPOverlayPreview.swift in Sources */, - 15FC8CC8204E2E2C000B5E1E /* PPState.swift in Sources */, + BA74C14420AA609500306B27 /* PPOverlayWrapper.swift in Sources */, + BA74C0F820A692D100306B27 /* PPOverlayController.swift in Sources */, + BA74C14220AA409D00306B27 /* NSColor.swift in Sources */, + 15492103207B53AE00E45BBC /* Util.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/PixelPicker/AppDelegate+Menu.swift b/PixelPicker/AppDelegate+Menu.swift index 6c3fb4a..6bfb160 100644 --- a/PixelPicker/AppDelegate+Menu.swift +++ b/PixelPicker/AppDelegate+Menu.swift @@ -20,10 +20,43 @@ let concentrationModifiers: [(String, NSEvent.ModifierFlags)] = [ ] extension AppDelegate: NSMenuDelegate { - // Unregister/register the activating shortcut when the menu is opened/closed - // so it can't be called when setting a new shortcut. - func menuWillOpen(_ menu: NSMenu) { unregisterActivatingShortcut() } - func menuDidClose(_ menu: NSMenu) { registerActivatingShortcut() } + // Unregister the activating shortcut when the menu is opened/closed so it can't be called when + // setting a new shortcut. Also start a run loop observer so we know when the modifierFlags have + // changed (used to dynamically update the menu). + func menuWillOpen(_ menu: NSMenu) { + unregisterActivatingShortcut() + if runLoopObserver == nil { + let activites = CFRunLoopActivity.beforeWaiting.rawValue + runLoopObserver = CFRunLoopObserverCreateWithHandler(nil, activites, true, 0, { [unowned self] (_, _) in + self.updateMenuItems() + }) + CFRunLoopAddObserver(CFRunLoopGetCurrent(), runLoopObserver, CFRunLoopMode.commonModes) + } + } + + // Re-register the activating shortcut, and remove the run loop observer. + func menuDidClose(_ menu: NSMenu) { + registerActivatingShortcut() + if (runLoopObserver != nil) { + CFRunLoopObserverInvalidate(runLoopObserver) + runLoopObserver = nil + } + } + + // Updates the titles of the recently picked colors - if the `option` key is pressed, then + // the colors will be in the format they were *when* they were picked, otherwise they'll be + // in the currently chosen format. + private func updateMenuItems() { + // Update recent picks list with correct titles. + let alternate = NSEvent.modifierFlags.contains(.option) + for item in contextMenu.items { + if let pickedColor = item.representedObject as? PPPickedColor { + item.title = alternate + ? pickedColor.asString + : PPState.shared.chosenFormat.asString(withColor: pickedColor.color) + } + } + } // TODO: look into only updating the menu rather than rebuilding it each time. // Might not be worth it - it doesn't seem expensive to build it every time it's opened... @@ -40,39 +73,45 @@ extension AppDelegate: NSMenuDelegate { buildConcentrationMenu() buildFloatPrecisionSlider() buildShortcutMenuItem() - - let launchAtLoginItem = contextMenu.addItem(withTitle: "Launch \(APP_NAME) at Login", action: #selector(launchAtLogin(_:)), keyEquivalent: "") - launchAtLoginItem.state = LaunchAtLogin.isEnabled ? .on : .off + buildLaunchAtLoginItem() + contextMenu.addItem(.separator()) contextMenu.addItem(withTitle: "About", action: #selector(showAboutPanel), keyEquivalent: "") contextMenu.addItem(withTitle: "Quit \(APP_NAME)", action: #selector(quitApplication), keyEquivalent: "") } + private func buildLaunchAtLoginItem() { + let item = contextMenu.addItem(withTitle: "Launch \(APP_NAME) at Login", action: #selector(launchAtLogin(_:)), keyEquivalent: "") + item.state = LaunchAtLogin.isEnabled ? .on : .off + } + @objc private func launchAtLogin(_ sender: NSMenuItem) { LaunchAtLogin.isEnabled = !LaunchAtLogin.isEnabled } // Show the user's recent picks in the menu. private func buildRecentPicks() { - // Recent picks. - // TODO: it would be nice to hold the `option` key and have the recent picks show in the - // currently chosen format, and when clicked copy in that format. - // https://stackoverflow.com/q/11208632/5552584 if PPState.shared.recentPicks.count > 0 { contextMenu.addItem(.separator()) contextMenu.addItem(withTitle: "Recently Picked", action: nil, keyEquivalent: "") + let format = PPState.shared.chosenFormat for pickedColor in PPState.shared.recentPicks { - // TODO: copy to clipboard when clicked, if alt then in current format - let item = contextMenu.addItem(withTitle: pickedColor.asString, action: #selector(copyRecentPick(_:)), keyEquivalent: "") + let item = contextMenu.addItem(withTitle: format.asString(withColor: pickedColor.color), action: #selector(copyRecentPick(_:)), keyEquivalent: "") item.representedObject = pickedColor item.image = circleImage(withSize: 12, color: pickedColor.color) } } } - + + // Copies the recently picked color (associated with the menu item) to the clipboard. + // If the `option` key is pressed, then it copies the color in the same format it was + // when it was picked (otherwise, it copies it in the currently chosen format). @objc private func copyRecentPick(_ sender: NSMenuItem) { if let pickedColor = sender.representedObject as? PPPickedColor { - copyToPasteboard(stringValue: pickedColor.asString) + let value = NSEvent.modifierFlags.contains(.option) + ? pickedColor.asString + : PPState.shared.chosenFormat.asString(withColor: pickedColor.color) + copyToPasteboard(stringValue: value) } } @@ -116,13 +155,9 @@ extension AppDelegate: NSMenuDelegate { // Update state. PPState.shared.floatPrecision = newValue - + // Update recent picks list with new precision. - for item in contextMenu.items { - if let pickedColor = item.representedObject as? PPPickedColor { - item.title = pickedColor.asString - } - } + updateMenuItems() } // Build a submenu with each case in the PPColor enum. diff --git a/PixelPicker/AppDelegate.swift b/PixelPicker/AppDelegate.swift index cf7ee9b..37334d0 100644 --- a/PixelPicker/AppDelegate.swift +++ b/PixelPicker/AppDelegate.swift @@ -13,10 +13,14 @@ let ICON = setupMenuBarIcon(NSImage(named: NSImage.Name(rawValue: "icon"))) // This controller manages the pixel picker itself. @IBOutlet weak var overlayController: PPOverlayController! - - var contextMenu: NSMenu = NSMenu() + + // The actual menu bar item. var menuBarItem: NSStatusItem! = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) - + // The menu that drops down from the menu bar item. + var contextMenu: NSMenu = NSMenu() + // When the menu bar is opened, we observe the run loop for changes in modifierFlags. + var runLoopObserver: CFRunLoopObserver? = nil + // Setup logging and load state. func applicationWillFinishLaunching(_ notification: Notification) { let minimumSeverity: LogSeverity = PPState.shared.defaults.bool(forKey: "debugMode") ? .debug : .info @@ -37,7 +41,7 @@ let ICON = setupMenuBarIcon(NSImage(named: NSImage.Name(rawValue: "icon"))) menuBarItem.image = ICON menuBarItem.action = #selector(onMenuClick) menuBarItem.sendAction(on: [.leftMouseUp, .rightMouseUp]) - + registerActivatingShortcut() Log.info?.message("Sucessfully launched.") @@ -46,13 +50,13 @@ let ICON = setupMenuBarIcon(NSImage(named: NSImage.Name(rawValue: "icon"))) func applicationWillTerminate(_ aNotification: Notification) { PPState.shared.saveToDisk() } - + func registerActivatingShortcut() { if let shortcut = PPState.shared.activatingShortcut { MASShortcutMonitor.shared().register(shortcut, withAction: showPicker) } } - + func unregisterActivatingShortcut() { MASShortcutMonitor.shared().unregisterShortcut(PPState.shared.activatingShortcut) } @@ -61,7 +65,7 @@ let ICON = setupMenuBarIcon(NSImage(named: NSImage.Name(rawValue: "icon"))) let leftClickToggles = PPState.shared.defaults.bool(forKey: "leftClickActivates") let pickerEvent: NSEvent.EventType = leftClickToggles ? .leftMouseUp : .rightMouseUp let dropdownEvent: NSEvent.EventType = leftClickToggles ? .rightMouseUp : .leftMouseUp - + let event = NSApp.currentEvent! if event.type == dropdownEvent { rebuildContextMenu() @@ -70,7 +74,7 @@ let ICON = setupMenuBarIcon(NSImage(named: NSImage.Name(rawValue: "icon"))) showPicker() } } - + @objc func showPicker() { overlayController.showPicker() }