18 Commits
Author SHA1 Message Date
acheronfail 5c7e1b61d7 version 1.1.0 2018-05-25 11:39:33 +10:00
acheronfail 57da81bc29 feat: support for colorspaces; improvements to color extraction;
This commit adds support for color spaces - now the app should display the *exact* colors of the pixels on your screen. If you'd like to use other color spaces, those can be chosen from within the app's menu.
This also improves the calculation logic for retrieving the correct values from an NSColor (before the fence-post error was returning *very very* slightly incorrect values). Since colors on macOS are represented as Float values, there's only so much precision we can have.
We also now use a custom bitmap context to extract the color from the CGImage, this is to ensure the color we extract is in the correct color space.

Fixes #12
2018-05-25 11:37:27 +10:00
acheronfail 86c4b29ed5 fix: sometimes hex values would be shorter than 6 characters 2018-05-23 22:06:48 +10:00
acheronfail 88f3465f27 fix: preview crosshair now wraps the center pixel better 2018-05-22 19:37:50 +10:00
acheronfail 27d4ca0d3b version 1.0.2 2018-05-22 19:04:50 +10:00
acheronfail 2802953130 updated demo image 2018-05-22 19:04:29 +10:00
acheronfail 5087a973cf reversed the order of recent picks in the menu 2018-05-22 19:02:07 +10:00
acheronfail a6e85ec913 update banner 2018-05-22 18:42:20 +10:00
acheronfail 27b3b9768c updated icon 2018-05-22 15:32:35 +10:00
acheronfail c26eb28536 version 1.0.1 2018-05-22 15:13:53 +10:00
acheronfail 9330644f9e improvements to cursor tracking and handling - fixes #9 2018-05-22 15:13:53 +10:00
acheronfail e672053594 add configurable magnification levels - fixes #6 2018-05-22 15:13:53 +10:00
acheronfail ef3ddda8e6 dynamically update the menu when option is pressed - fixes #3 2018-05-21 13:43:08 +10:00
acheronfail b734282b0d xcode project updates 2018-05-20 21:34:54 +10:00
acheronfail a343b50916 refactor screen bounds checking 2018-05-20 17:26:57 +10:00
acheronfail 40866ce9f8 minor change in screen bounds checking 2018-05-20 17:25:32 +10:00
acheronfail ffeedffda9 update README 2018-05-20 17:13:31 +10:00
acheronfail 46159dd5d4 removed useless framework file 2018-05-20 17:12:08 +10:00
54 changed files with 425 additions and 496 deletions
+11 -11
View File
@@ -80,7 +80,7 @@
B3B4C2C71E25894B009F8E4E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BA22232620AFEC1B0001069C /* ApplicationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ApplicationServices.framework; path = System/Library/Frameworks/ApplicationServices.framework; sourceTree = SDKROOT; };
BA22232820B04D660001069C /* AppDelegate+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Menu.swift"; sourceTree = "<group>"; };
BA22232A20B0E3B50001069C /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = "/Users/cosmotherly/dev/pixel-picker/README.md"; sourceTree = "<absolute>"; };
BA22232A20B0E3B50001069C /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; };
BA71817120AE2DB400619700 /* PPMenuShortcutView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PPMenuShortcutView.m; sourceTree = "<group>"; };
BA71817320AE2DCC00619700 /* PPMenuShortcutView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PPMenuShortcutView.h; sourceTree = "<group>"; };
BA74C0F620A692D100306B27 /* PPOverlayController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPOverlayController.swift; sourceTree = "<group>"; };
@@ -332,18 +332,18 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BA74C14220AA409D00306B27 /* NSColor.swift in Sources */,
BA71817220AE2DB400619700 /* PPMenuShortcutView.m in Sources */,
15492103207B53AE00E45BBC /* Util.swift in Sources */,
BA74C0F820A692D100306B27 /* PPOverlayController.swift in Sources */,
BA74C14E20ADA33E00306B27 /* PPColor.swift in Sources */,
BA22232920B04D660001069C /* AppDelegate+Menu.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 */,
BA71817220AE2DB400619700 /* PPMenuShortcutView.m 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 */,
BA74C14020AA390400306B27 /* PPOverlayPreview.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;
};
+115 -40
View File
@@ -20,62 +20,145 @@ 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...
func rebuildContextMenu() {
contextMenu.removeAllItems()
let pickItem = contextMenu.addItem(withTitle: "Pick a pixel!", action: #selector(showPicker), keyEquivalent: "")
pickItem.image = ICON
buildRecentPicks()
contextMenu.addItem(.separator())
buildColorSpaceItem()
buildColorFormatsMenu()
buildMagnificationItem()
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: "")
}
// A menu that allows choosing what color space the picker will use.
private func buildColorSpaceItem() {
let submenu = NSMenu()
let defaultItem = submenu.addItem(withTitle: "Default (infer from screen)", action: #selector(setColorSpace(_:)), keyEquivalent: "")
defaultItem.state = PPState.shared.colorSpace == nil ? .on : .off
submenu.addItem(.separator())
for (title, name) in PPColor.colorSpaceNames {
let item = submenu.addItem(withTitle: title, action: #selector(setColorSpace(_:)), keyEquivalent: "")
item.representedObject = name
item.state = PPState.shared.colorSpace == name ? .on : .off
}
let item = contextMenu.addItem(withTitle: "Color Space", action: nil, keyEquivalent: "")
item.submenu = submenu
}
// If the selected color space is nil, then the preview will just infer the color space from
// the screen the picker is currently on.
@objc private func setColorSpace(_ sender: NSMenuItem) {
PPState.shared.colorSpace = sender.representedObject as? String
}
// A menu which allows the magnification level of the picker to be adjusted.
private func buildMagnificationItem() {
let submenu = NSMenu()
for i in stride(from: 4, through: 24, by: 2) {
let item = submenu.addItem(withTitle: "\(i)x", action: #selector(selectMagnification(_:)), keyEquivalent: "")
item.representedObject = i
item.state = PPState.shared.magnificationLevel == i ? .on : .off
}
let item = contextMenu.addItem(withTitle: "Magnification", action: nil, keyEquivalent: "")
item.submenu = submenu
}
@objc private func selectMagnification(_ sender: NSMenuItem) {
if let level = sender.representedObject as? Int {
PPState.shared.magnificationLevel = level
}
}
// Simple launch app at login menu item.
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: "")
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 format = PPState.shared.chosenFormat
for pickedColor in PPState.shared.recentPicks.reversed() {
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)
}
}
// Simply creates a circle NSImage with the given size and color.
private func circleImage(withSize size: CGFloat, color: NSColor) -> NSImage {
let image = NSImage(size: NSSize(width: size, height: size))
@@ -85,7 +168,7 @@ extension AppDelegate: NSMenuDelegate {
image.unlockFocus()
return image
}
// A slider to change the float precision.
private func buildFloatPrecisionSlider() {
contextMenu.addItem(withTitle: "Float Precision (\(PPState.shared.floatPrecision))", action: nil, keyEquivalent: "")
@@ -104,27 +187,23 @@ extension AppDelegate: NSMenuDelegate {
item.view!.autoresizingMask = .width
item.view!.addSubview(slider)
}
// Called when the slider is updated.
@objc private func sliderUpdate(_ sender: NSSlider) {
let newValue = UInt(sender.intValue)
// Update slider title.
if let item = contextMenu.item(withTitle: "Float Precision (\(PPState.shared.floatPrecision))") {
item.title = "Float Precision (\(newValue))"
}
// 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.
// TODO: with Swift 4.2, we shouldn't need to resort to the hacky "iterateEnum" approach.
private func buildColorFormatsMenu() {
@@ -138,16 +217,14 @@ extension AppDelegate: NSMenuDelegate {
let item = contextMenu.addItem(withTitle: "Color Format", action: nil, keyEquivalent: "")
item.submenu = submenu
}
// Set the selected format as the default.
@objc private func selectFormat(_ sender: NSMenuItem) {
if let format = sender.representedObject as? PPColor {
PPState.shared.chosenFormat = format
sender.menu?.items.forEach({ $0.state = .off })
sender.state = .on
}
}
// Builds and adds the concentration modifier submenu.
private func buildConcentrationMenu() {
let submenu = NSMenu()
@@ -160,16 +237,14 @@ extension AppDelegate: NSMenuDelegate {
let item = contextMenu.addItem(withTitle: "Concentration Modifier", action: nil, keyEquivalent: "")
item.submenu = submenu
}
// Set the chosen modifier to toggle "concentrationMode".
@objc private func selectModifier(_ sender: NSMenuItem) {
if let modifier = sender.representedObject as? NSEvent.ModifierFlags {
PPState.shared.concentrationModeModifier = modifier
sender.menu?.items.forEach({ $0.state = .off })
sender.state = .on
}
}
// Builds and adds the MASShortcutView to be used in the menu.
// Uses a custom view to handle events correctly (since it's inside a NSMenu).
private func buildShortcutMenuItem() {
@@ -178,7 +253,7 @@ extension AppDelegate: NSMenuDelegate {
let shortcutView = MASShortcutView()
shortcutView.shortcutValue = PPState.shared.activatingShortcut
shortcutView.shortcutValueChange = { PPState.shared.activatingShortcut = $0?.shortcutValue }
let item = contextMenu.addItem(withTitle: "Shortcut", action: nil, keyEquivalent: "")
item.view = PPMenuShortcutView(shortcut: shortcutView)
}
+18 -8
View File
@@ -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,22 +41,28 @@ let ICON = setupMenuBarIcon(NSImage(named: NSImage.Name(rawValue: "icon")))
menuBarItem.image = ICON
menuBarItem.action = #selector(onMenuClick)
menuBarItem.sendAction(on: [.leftMouseUp, .rightMouseUp])
registerActivatingShortcut()
// Set the CGEventSource.localEventsSuppressionInterval to a small interval (default: 250ms)
// otherwise there'll be a delay when we re-associate the mouse input with the mouse cursor
// (in the picker) that makes it feel laggy (the suppression interval controls how long
// hardware events are suppressed after functions like CGWarpMouseCursorPosition are used.
CGEventSource(stateID: CGEventSourceStateID.combinedSessionState)?.localEventsSuppressionInterval = 0.05
Log.info?.message("Sucessfully launched.")
}
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 +71,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 +80,7 @@ let ICON = setupMenuBarIcon(NSImage(named: NSImage.Name(rawValue: "icon")))
showPicker()
}
}
@objc func showPicker() {
overlayController.showPicker()
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 B

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 54 KiB

+3 -2
View File
@@ -3,7 +3,7 @@
}
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red230\green230\blue230;}
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\csgray\c92143;}
\paperw12240\paperh15840\margl1440\margr1440\vieww12800\viewh10340\viewkind0
\margl1440\margr1440\vieww12800\viewh10340\viewkind0
\deftab720
\pard\pardeftab720\qc\partightenfactor0
@@ -27,8 +27,9 @@
\pard\pardeftab720\qc\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/SwiftyJSON/SwiftyJSON"}}{\fldrslt \cf2 SwiftyJSON}}\
\
\pard\pardeftab720\qc\partightenfactor0
\b\fs36 Experimental Overrides
\b\fs36 \cf2 Experimental Overrides
\b0 \
\pard\pardeftab720\qc\partightenfactor0
+2 -2
View File
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<string>1.1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>4</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
+10
View File
@@ -26,6 +26,16 @@ extension NSColor {
return nil
}
// Creates an NSImage with the given size and color.
func image(withSize size: NSSize) -> NSImage {
let image = NSImage(size: size)
image.lockFocus()
self.set()
NSRect(origin: NSPoint.zero, size: size).fill()
image.unlockFocus()
return image
}
// Returns either black or white, whichever will contrast the most with the given color.
// See https://stackoverflow.com/a/3943023/5552584
func bestContrastingColor() -> NSColor {
@@ -15,29 +15,28 @@ class PPOverlayController: NSWindowController {
// Outlets relating to the picker's info box.
@IBOutlet weak var infoPanel: PPOverlayPanel!
@IBOutlet weak var infoBox: NSBox!
@IBOutlet weak var infoWrapper: NSView!
@IBOutlet weak var infoFormatField: NSTextField!
@IBOutlet weak var infoDetailField: NSTextField!
// The current magnification level of the preview.
// TODO: make this somewhat configurable?
var magnification: CGFloat = 8.0
// This mode increases the picker's size, increases the magnification and also
// slows down mouseMove events to make it easier to pick the right pixel.
// This mode increases the picker's size, increases the magnification and also slows down move
// events to make it easier to pick the right pixel. We dissociate the mouse (input) from the
// mouse cursor while the concentrationMode is active. This is so we can slow it down.
var concentrationMode: Bool = false {
didSet {
if isEnabled {
panelSize = concentrationMode ? 300 : 150
panelSize = concentrationMode ? PPOverlayController.panelSizeLarge : PPOverlayController.panelSizeNormal
overlayPanel.activate(withSize: panelSize, infoPanel: infoPanel)
wrapper.layer?.cornerRadius = PPState.shared.paschaModeEnabled ? 0 : panelSize / 2
CGAssociateMouseAndMouseCursorPosition(boolean_t(truncating: concentrationMode ? 0 : 1))
}
}
}
// The size of the pixel picker.
// TODO: use constants rather than hard-coded values.
private var panelSize: CGFloat = 150
// The current and different sizes of the pixel picker.
private static let panelSizeNormal: CGFloat = 150
private static let panelSizeLarge: CGFloat = 300
private var panelSize: CGFloat = PPOverlayController.panelSizeNormal
// The app that was last active before the picker was activated. We keep track
// of this in order to fully restore first responder status after picking a pixel.
@@ -50,23 +49,24 @@ class PPOverlayController: NSWindowController {
private var lastHighlightedColor: NSColor = NSColor.black
// Whether or not the picker should be actively updating its preview.
// We dissociate the mouse (input) from the mouse cursor while the picker is active.
// This is so we can slow down the mouse movement during concentration mode.
private var isEnabled: Bool = false {
didSet {
if isEnabled {
lastMouseLocation = NSEvent.mouseLocation
startMonitoringEvents()
CGAssociateMouseAndMouseCursorPosition(boolean_t(truncating: 0))
} else {
stopMonitoringEvents()
concentrationMode = false
CGAssociateMouseAndMouseCursorPosition(boolean_t(truncating: 1))
}
}
}
override func awakeFromNib() {
// Setup the info window.
infoWrapper.wantsLayer = true
infoWrapper.layer?.cornerRadius = 8
infoWrapper.layer?.backgroundColor = NSColor.black.cgColor
// For some reason if we don't listen for events this way we miss the escape key.
NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) {
if self.isEnabled { self.keyDown(with: $0) }
@@ -118,24 +118,30 @@ class PPOverlayController: NSWindowController {
// slow it down to make it easier to pick the correct pixel.
override func mouseMoved(with event: NSEvent) {
if isEnabled {
let speed: CGFloat = concentrationMode ? 0.1 : 0.5
let currentMouseLocation = NSEvent.mouseLocation
var nextMouseLocation = currentMouseLocation
let x = lastMouseLocation.x + (event.deltaX * speed)
let y = lastMouseLocation.y - (event.deltaY * speed)
// Slow down tracking speed when concentration mode is active.
if concentrationMode {
let speed: CGFloat = concentrationMode ? 0.1 : 0.5
let x = lastMouseLocation.x + (event.deltaX * speed)
let y = lastMouseLocation.y - (event.deltaY * speed)
// Ensure the picker doesn't travel off screen.
var nextMouseLocation = NSPoint(x: x, y: y)
for screen in NSScreen.screens {
let outsideCoordinate = coordinateOutsideRect(nextMouseLocation, screen.frame)
if NSMouseInRect(currentMouseLocation, screen.frame, false) && outsideCoordinate != .none {
if outsideCoordinate == .x { nextMouseLocation.x = currentMouseLocation.x }
if outsideCoordinate == .y { nextMouseLocation.y = currentMouseLocation.y }
if outsideCoordinate == .both { nextMouseLocation = currentMouseLocation }
// Ensure the picker doesn't travel off screen.
nextMouseLocation = NSPoint(x: x, y: y)
for screen in NSScreen.screens {
let outlier = Coordinate.isOutsideRect(nextMouseLocation, screen.frame)
if NSMouseInRect(currentMouseLocation, screen.frame, false) && outlier != .none {
if outlier == .x { nextMouseLocation.x = currentMouseLocation.x }
if outlier == .y { nextMouseLocation.y = currentMouseLocation.y }
if outlier == .both { nextMouseLocation = currentMouseLocation }
}
}
// Since we've disassociated the mouse input with the cursor, we manually move it.
CGWarpMouseCursorPosition(convertToCGCoordinateSystem(nextMouseLocation))
}
CGWarpMouseCursorPosition(convertToCGCoordinateSystem(nextMouseLocation))
updatePreview(aroundPoint: nextMouseLocation)
lastMouseLocation = nextMouseLocation
}
@@ -209,7 +215,7 @@ class PPOverlayController: NSWindowController {
// Updates the info panel with the correct colors and text.
func updateInfoPanel(_ color: NSColor, _ contrastingColor: NSColor) {
infoBox.fillColor = color.alphaComponent == 0 ? NSColor.black : color
infoWrapper.layer?.backgroundColor = color.cgColor
infoFormatField.textColor = contrastingColor
infoDetailField.textColor = contrastingColor
infoFormatField.stringValue = PPState.shared.chosenFormat.rawValue
@@ -232,9 +238,13 @@ class PPOverlayController: NSWindowController {
overlayPanel.activate(withSize: panelSize, infoPanel: infoPanel)
}
// Center the mouse in the picker window.
overlayPanel.setFrameOrigin(NSPoint(x: point.x - (panelSize / 2), y: point.y - (panelSize / 2)))
let normalisedPoint = NSPoint(x: round(point.x * 2) / 2, y: round(point.y * 2) / 2)
if let screenShot = getScreenShot(aroundPoint: normalisedPoint) {
// Calculate a zoomed rect which will crop the screenshot we took.
let magnification = CGFloat(PPState.shared.magnificationLevel)
let zoomReciprocal: CGFloat = 1.0 / (concentrationMode ? magnification * 2.5 : magnification)
let currentSize = CGFloat(screenShot.width) + 1
let origin = floor(currentSize * ((1 - zoomReciprocal) / 2))
@@ -243,28 +253,115 @@ class PPOverlayController: NSWindowController {
// Ensure preview size is an odd number (so there's a middle pixel).
let zoomedSize = floor(ensureOdd(currentSize * zoomReciprocal))
let middle = zoomedSize / 2
let croppedRect = CGRect(x: x, y: y, width: zoomedSize, height: zoomedSize)
let zoomedImage: CGImage = screenShot.cropping(to: croppedRect)!
let pixelSize = panelSize / zoomedSize
let middlePosition = zoomedSize / 2
// Extract the middle pixel color from the zoomed image.
let bitmap = NSBitmapImageRep(cgImage: zoomedImage)
bitmap.colorSpaceName = .calibratedRGB
if let colorAtPixel = bitmap.colorAt(x: Int(middle), y: Int(middle)) {
let contrastingColor = colorAtPixel.bestContrastingColor()
// Convert the preview to the correct color space, and update the overlay.
preview.layer?.contents = NSImage(cgImage: zoomedImage, size: NSSize(width: panelSize, height: panelSize))
preview.updateCrosshair(panelSize / zoomedSize, middle, contrastingColor.cgColor)
wrapper.update(contrastingColor.cgColor)
updateInfoPanel(colorAtPixel, contrastingColor)
// Save color under pixel (used when copied).
lastHighlightedColor = colorAtPixel
// User has chosen a custom color space.
if let colorSpace = getChosenColorSpace(point) {
if colorSpace == zoomedImage.colorSpace {
return updatePreview(zoomedImage, pixelSize, middlePosition)
} else if let image = zoomedImage.copy(colorSpace: colorSpace) {
return updatePreview(image, pixelSize, middlePosition)
}
}
// No custom color space chosen, so use the screen's one.
if let colorSpace = getScreenColorSpace(point) {
if colorSpace == zoomedImage.colorSpace {
return updatePreview(zoomedImage, pixelSize, middlePosition)
} else if let image = zoomedImage.copy(colorSpace: colorSpace) {
return updatePreview(image, pixelSize, middlePosition)
}
}
// If that also didn't work, then just return the image in its default color space.
return updatePreview(zoomedImage, pixelSize, middlePosition)
}
}
private func updatePreview(_ image: CGImage, _ pixelSize: CGFloat, _ middlePosition: CGFloat) {
// Extract the middle pixel color from the prepared image.
let colorAtPixel = image.colorAt(x: Int(middlePosition), y: Int(middlePosition))
let contrastingColor = colorAtPixel.bestContrastingColor()
preview.layer?.contents = image
preview.updateCrosshair(pixelSize, middlePosition, contrastingColor.cgColor)
wrapper.update(contrastingColor.cgColor)
updateInfoPanel(colorAtPixel, contrastingColor)
// Save color under pixel (used when copied).
lastHighlightedColor = colorAtPixel
}
private func getScreenColorSpace(_ point: NSPoint) -> CGColorSpace? {
return getScreenFromPoint(point)?.colorSpace?.cgColorSpace
}
// Gets the colorspace in which to display the preview and retreive color values.
private func getChosenColorSpace(_ point: NSPoint) -> CGColorSpace? {
if let name = PPState.shared.colorSpace, let colorSpace = CGColorSpace(name: name as CFString) {
return colorSpace
}
// Center the mouse in the picker window.
overlayPanel.setFrameOrigin(NSPoint(x: normalisedPoint.x - (panelSize / 2), y: normalisedPoint.y - (panelSize / 2)))
return nil
}
}
extension CGImage {
// In order to get the color at a given pixel from a CGImage, we need to convert the CGImage's
// data to a bitmap and draw it. We do so via a CGContext, and then manually extract the data at
// the desired pixel.
func colorAt(x: Int, y: Int) -> NSColor {
assert(0 <= x && x < self.width)
assert(0 <= y && y < self.height)
let bitmapBytesPerRow = width * 4
let bitmapByteCount = bitmapBytesPerRow * Int(self.height)
// Allocate memory for image data. This is the destination in memory where any drawing to
// the bitmap context will be rendered.
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
let bitmapData = malloc(bitmapByteCount)
// Since we manually allocate memeory for the data, we must ensure that the same memory is
// freed after we've used it.
defer { free(bitmapData) }
// Create the bitmap context.
let context = CGContext(
data: bitmapData,
width: self.width,
height: self.height,
bitsPerComponent: 8,
bytesPerRow: bitmapBytesPerRow,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: bitmapInfo.rawValue
)
// Extract the pixel data from the right offset.
if context != nil {
// First, we draw the image data onto the context we created.
let rect = CGRect(x: 0, y: 0, width: self.width, height: self.height)
context!.draw(self, in: rect)
// Then we extract the data at the right spot.
let data = context!.data!
let offset = 4 * (y * width + x)
let a = CGFloat(data.load(fromByteOffset: offset, as: UInt8.self)) / 255.0
let r = CGFloat(data.load(fromByteOffset: offset + 1, as: UInt8.self)) / 255.0
let g = CGFloat(data.load(fromByteOffset: offset + 2, as: UInt8.self)) / 255.0
let b = CGFloat(data.load(fromByteOffset: offset + 3, as: UInt8.self)) / 255.0
return NSColor(red: r, green: g, blue: b, alpha: a)
}
// Creating the context failed, so return a default color instead.
// Hopefully, this should never happen.
Log.error?.message("Failed to create CGContext!")
return NSColor.black
}
}
@@ -3,7 +3,6 @@
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14109"/>
<capability name="box content view" minToolsVersion="7.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -21,10 +20,10 @@
</customObject>
<customObject id="ECQ-9M-Pkg" customClass="PPOverlayController" customModule="PixelPicker" customModuleProvider="target">
<connections>
<outlet property="infoBox" destination="rpe-Xh-gaO" id="BYW-qP-HLd"/>
<outlet property="infoDetailField" destination="Re2-GH-fyd" id="K03-75-cf9"/>
<outlet property="infoFormatField" destination="1fa-UE-yMM" id="Nan-fO-kbu"/>
<outlet property="infoPanel" destination="OMO-LB-w57" id="GGj-Y6-LfU"/>
<outlet property="infoWrapper" destination="HED-rR-FDP" id="21H-Tc-c4W"/>
<outlet property="overlayPanel" destination="gZA-A3-IZ5" id="DUd-MU-oD4"/>
<outlet property="preview" destination="oj3-ml-2qo" id="xLc-pu-U90"/>
<outlet property="window" destination="gZA-A3-IZ5" id="GLv-sO-WxM"/>
@@ -35,7 +34,7 @@
<windowStyleMask key="styleMask" utility="YES" nonactivatingPanel="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="132" width="101" height="101"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1178"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1028"/>
<view key="contentView" id="acW-WA-gq2" customClass="PPOverlayWrapper" customModule="PixelPicker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="101" height="101"/>
<autoresizingMask key="autoresizingMask"/>
@@ -56,50 +55,35 @@
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" oneShot="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="OMO-LB-w57" customClass="PPOverlayPanel" customModule="PixelPicker" customModuleProvider="target">
<windowStyleMask key="styleMask" utility="YES" nonactivatingPanel="YES"/>
<rect key="contentRect" x="926" y="538" width="100" height="50"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1178"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1028"/>
<view key="contentView" id="HED-rR-FDP">
<rect key="frame" x="0.0" y="0.0" width="100" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<box boxType="custom" borderType="none" cornerRadius="4" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="rpe-Xh-gaO">
<rect key="frame" x="0.0" y="0.0" width="100" height="50"/>
<view key="contentView" id="g1f-dN-SYl">
<rect key="frame" x="0.0" y="0.0" width="100" height="50"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1fa-UE-yMM">
<rect key="frame" x="-2" y="28" width="104" height="17"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Label" id="RkG-Wy-Shq">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Re2-GH-fyd">
<rect key="frame" x="-2" y="5" width="104" height="14"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Label" id="IYd-1Y-7cd">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="1fa-UE-yMM" firstAttribute="top" secondItem="g1f-dN-SYl" secondAttribute="top" constant="5" id="OMX-Uf-gxc"/>
<constraint firstAttribute="trailing" secondItem="1fa-UE-yMM" secondAttribute="trailing" id="YWA-wF-wWD"/>
<constraint firstItem="Re2-GH-fyd" firstAttribute="leading" secondItem="g1f-dN-SYl" secondAttribute="leading" id="def-Ev-ONx"/>
<constraint firstItem="1fa-UE-yMM" firstAttribute="leading" secondItem="g1f-dN-SYl" secondAttribute="leading" id="j0J-F5-ipt"/>
<constraint firstAttribute="bottom" secondItem="Re2-GH-fyd" secondAttribute="bottom" constant="5" id="ktF-UW-gVI"/>
<constraint firstAttribute="trailing" secondItem="Re2-GH-fyd" secondAttribute="trailing" id="t9s-4R-bFb"/>
</constraints>
</view>
</box>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1fa-UE-yMM">
<rect key="frame" x="-2" y="25" width="104" height="17"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Label" id="RkG-Wy-Shq">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Re2-GH-fyd">
<rect key="frame" x="-2" y="8" width="104" height="14"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Label" id="IYd-1Y-7cd">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="rpe-Xh-gaO" firstAttribute="leading" secondItem="HED-rR-FDP" secondAttribute="leading" id="Buc-en-Way"/>
<constraint firstItem="rpe-Xh-gaO" firstAttribute="top" secondItem="HED-rR-FDP" secondAttribute="top" id="NrK-e2-suw"/>
<constraint firstAttribute="bottom" secondItem="rpe-Xh-gaO" secondAttribute="bottom" id="WSu-Ro-a4K"/>
<constraint firstAttribute="trailing" secondItem="rpe-Xh-gaO" secondAttribute="trailing" id="myM-UI-QfA"/>
<constraint firstItem="1fa-UE-yMM" firstAttribute="leading" secondItem="HED-rR-FDP" secondAttribute="leading" id="23z-pW-nAx"/>
<constraint firstItem="1fa-UE-yMM" firstAttribute="top" secondItem="HED-rR-FDP" secondAttribute="top" constant="8" id="G1j-j5-iWY"/>
<constraint firstItem="Re2-GH-fyd" firstAttribute="leading" secondItem="HED-rR-FDP" secondAttribute="leading" id="aZf-QY-Lpd"/>
<constraint firstAttribute="bottom" secondItem="Re2-GH-fyd" secondAttribute="bottom" constant="8" id="amm-jV-oWa"/>
<constraint firstAttribute="trailing" secondItem="1fa-UE-yMM" secondAttribute="trailing" id="flM-Te-2qj"/>
<constraint firstAttribute="trailing" secondItem="Re2-GH-fyd" secondAttribute="trailing" id="jWO-5f-uxW"/>
</constraints>
</view>
<point key="canvasLocation" x="236" y="230"/>
@@ -13,28 +13,27 @@ class PPOverlayPreview: NSView, CALayerDelegate {
override var wantsUpdateLayer: Bool {
get { return true }
}
override func awakeFromNib() {
// Make layers contents resize to fill, and disable antialiasing.
wantsLayer = true
layer?.magnificationFilter = kCAFilterNearest
layer?.contentsGravity = kCAGravityResizeAspectFill
layer?.delegate = self
// Prepare crosshair shape layers.
crosshair.strokeColor = NSColor.black.cgColor
crosshair.fillColor = nil
layer?.addSublayer(crosshair)
}
// Update the crosshair with the correct color, position and size.
func updateCrosshair(_ pixelSize: CGFloat, _ middle: CGFloat, _ color: CGColor) {
let pos: CGFloat = (pixelSize * middle) - (pixelSize / 2)
let pixelRect = NSMakeRect(pos, pos, pixelSize, pixelSize)
let outerRect = pixelRect.insetBy(dx: -1, dy: -1)
crosshair.path = CGPath(rect: outerRect, transform: nil)
crosshair.path = CGPath(rect: pixelRect, transform: nil)
crosshair.strokeColor = color
setNeedsDisplay(outerRect)
setNeedsDisplay(pixelRect)
}
}
+57 -44
View File
@@ -5,42 +5,30 @@
import SwiftyJSON
// This allows us to iterate over the raw values of an enum.
// TODO: use `CaseIterable` when Swift 4.2 comes out
func iterateEnum<T: Hashable>(_: T.Type) -> AnyIterator<T> {
var i = 0
return AnyIterator {
let next = withUnsafeBytes(of: &i) { $0.load(as: T.self) }
if next.hashValue != i { return nil }
i += 1
return next
}
}
// Each time the user picks a pixel we save it as the format and the color.
struct PPPickedColor {
let color: NSColor
let format: PPColor
init(color: NSColor, format: PPColor) {
self.color = color
self.format = format
}
init?(fromJSON json: JSON) {
guard
let formatString = json["format"].string,
let format = PPColor(rawValue: formatString),
let color = NSColor.deserialize(fromJson: json["color"])
else { return nil }
self.init(color: color, format: format)
}
var asString: String {
return format.asString(withColor: color)
}
var asJSON: JSON {
return [
"color": NSColor.serialize(self.color),
@@ -91,10 +79,10 @@ enum PPColor: String {
let f = self.insertFloatPrecisionFormatter
let c = colorInCorrectColorSpace(passedColor)
switch self {
case .genericHex: return self.formatAsHex(c, "%2X")
case .genericHex: return self.formatAsHex(c, "%06x")
case .generic8Bit: return self.formatAs8Bit(c, "%u, %u, %u")
case .genericDecimal: return self.formatAsDecimal(c, f("%f, %f, %f"), .rgb)
case .cssHex: return self.formatAsHex(c, "#%2X")
case .cssHex: return self.formatAsHex(c, "#%06x")
case .cssRgb: return self.formatAs8Bit(c, "rgb(%u, %u, %u)")
case .cssRgba: return self.formatAs8Bit(c, "rgba(%u, %u, %u, 1)")
case .cssHsl: return self.formatAsHSL(c, "hsl(%u, %u%%, %u%%)")
@@ -119,8 +107,8 @@ enum PPColor: String {
case .javaRgba: return self.formatAs8Bit(c, "new Color(%u, %u, %u, 255)")
case .androidRgb: return self.formatAs8Bit(c, "Color.rgb(%u, %u, %u)")
case .androidArgb: return self.formatAs8Bit(c, "Color.argb(255, %u, %u, %u)")
case .androidXmlRgb: return self.formatAsHex(c, "<color name=\"color_name\">#%2X</color>")
case .androidXmlArgb: return self.formatAsHex(c, "<color name=\"color_name\">#ff%2X</color>")
case .androidXmlRgb: return self.formatAsHex(c, "<color name=\"color_name\">#%06x</color>")
case .androidXmlArgb: return self.formatAsHex(c, "<color name=\"color_name\">#ff%06x</color>")
case .cgColorRgb: return self.formatAsDecimal(c, f("CGColorCreateGenericRGB(%f, %f, %f, 1.000)"), .rgb)
case .openGlRgb: return self.formatAsDecimal(c, f("glColor3f(%f, %f, %f)"), .rgb)
case .openGlRgba: return self.formatAsDecimal(c, f("glColor4f(%f, %f, %f, 1.000)"), .rgb)
@@ -136,7 +124,7 @@ enum PPColor: String {
case .androidXmlRgb: fallthrough
case .androidXmlArgb: fallthrough
case .cgColorRgb: fallthrough
case .genericHex: return self.formatAsHex(color, "%2X")
case .genericHex: return self.formatAsHex(color, "%06x")
// CSS HSL is a unique format.
case .cssHsl: fallthrough
case .cssHsla: return self.formatAsHSL(color, "%u, %u%%, %u%%")
@@ -170,17 +158,17 @@ enum PPColor: String {
case .objCUIColorHsb: return self.formatAsDecimal(color, self.insertFloatPrecisionFormatter("%f, %f, %f"), .rgb)
}
}
// Returns the PPColor that sits after this one.
func next() -> PPColor {
return next(withArray: iterateEnum(PPColor.self).map({ $0 }))
}
// Same as next() but backwards.
func previous() -> PPColor {
return next(withArray: iterateEnum(PPColor.self).reversed())
}
// Finds the next element after this element in the given array.
// This method should only be passed the result of iterateEnum(PPColor.self).
private func next(withArray array: [PPColor]) -> PPColor {
@@ -192,26 +180,26 @@ enum PPColor: String {
return array.first!
}
// A tiny enum that describes which components should be used when formatting.
private enum Components {
case rgb
case hsb
}
// Replaces "%f" with "%.3f" (if "3" is the current precision level).
private func insertFloatPrecisionFormatter(_ input: String) -> String {
return input.replacingOccurrences(of: "%f", with: String(format: "%%.%uf", PPState.shared.floatPrecision))
}
// Formats the colors as a hex value, eg: "D3504E".
private func formatAsHex(_ color: NSColor, _ template: String) -> String {
let a = Int(color.redComponent * 255) << 16
let b = Int(color.greenComponent * 255) << 8
let c = Int(color.blueComponent * 255) << 0
return String(format: template, a | b | c)
let r = min(Int(color.redComponent * 0x100), 0x0ff) << 16
let g = min(Int(color.greenComponent * 0x100), 0x0ff) << 8
let b = min(Int(color.blueComponent * 0x100), 0x0ff) << 0
return String(format: template, (r | g | b))
}
// Formats the color as decimal values, eg: "0.145, 0.361, 0.722".
private func formatAsDecimal(_ color: NSColor, _ template: String, _ cmp: Components) -> String {
let a = cmp == .rgb ? color.redComponent : color.hueComponent
@@ -219,23 +207,23 @@ enum PPColor: String {
let c = cmp == .rgb ? color.blueComponent : color.brightnessComponent
return String(format: template, a, b, c)
}
// Formats the color as 8-bit values, eg: "158, 198, 117".
private func formatAs8Bit(_ color: NSColor, _ template: String) -> String {
let a = Int(color.redComponent * 255)
let b = Int(color.greenComponent * 255)
let c = Int(color.blueComponent * 255)
return String(format: template, a, b, c)
let r = min(Int(color.redComponent * 0x100), 0x0ff)
let g = min(Int(color.greenComponent * 0x100), 0x0ff)
let b = min(Int(color.blueComponent * 0x100), 0x0ff)
return String(format: template, r, g, b)
}
// Special formatter since CSS uses a unique style here, eg: "35, 79%, 47%".
private func formatAsHSL(_ color: NSColor, _ template: String) -> String {
let a = Int(color.hueComponent * 360)
let b = Int(color.saturationComponent * 100)
let c = Int(color.brightnessComponent * 100)
return String(format: template, a, b, c)
let h = min(Int(color.hueComponent * 360), 359)
let s = Int(color.saturationComponent * 100)
let b = Int(color.brightnessComponent * 100)
return String(format: template, h, s, b)
}
// Returns a new color in the correct colorspace for the format.
private func colorInCorrectColorSpace(_ color: NSColor) -> NSColor {
switch self {
@@ -253,4 +241,29 @@ enum PPColor: String {
return color
}
}
// Available CGColorSpaces that can be chosen.
// NOTE: the commented out color spaces don't work at all on my screens, they may work for other
// screens, but here they're left out since they're probably not common/necessary. If you're
// reading this and you want to use them, make an issue on GitHub and then we can test them.
static let colorSpaceNames = [
// ("Generic CMYK", CGColorSpace.genericCMYK as String),
("Generic XYZ", CGColorSpace.genericXYZ as String),
("Generic RGB Linear", CGColorSpace.genericRGBLinear as String),
// ("Generic Gray Gamma 2.2", CGColorSpace.genericGrayGamma2_2 as String),
("ACESCG Linear", CGColorSpace.acescgLinear as String),
("Adobe RGB 1998", CGColorSpace.adobeRGB1998 as String),
("DCIP3", CGColorSpace.dcip3 as String),
("Display P3", CGColorSpace.displayP3 as String),
// ("Linear Gray", CGColorSpace.linearGray as String),
("Linear sRGB", CGColorSpace.linearSRGB as String),
// ("Extended Linear Gray", CGColorSpace.extendedLinearGray as String),
("Extended Linear sRGB", CGColorSpace.extendedLinearSRGB as String),
// ("Extended Gray", CGColorSpace.extendedGray as String),
("Extended sRGB", CGColorSpace.extendedSRGB as String),
("ITUR 2020", CGColorSpace.itur_2020 as String),
("ITUR 709", CGColorSpace.itur_709 as String),
("ROMM RGB", CGColorSpace.rommrgb as String),
("sRGB", CGColorSpace.sRGB as String)
]
}
+18 -1
View File
@@ -24,17 +24,24 @@ import CleanroomLogger
let defaults: UserDefaults = UserDefaults.standard
// Whether the picker should be square or not.
// TODO: implement shortcut
var paschaModeEnabled: Bool = false
// The shortcut that activates the pixel picker.
var activatingShortcut: MASShortcut?
// Magnification level of the picker.
var magnificationLevel: Int = 8
// Hold this down to enter concentration mode.
var concentrationModeModifier: NSEvent.ModifierFlags = .control
// The currently chosen format.
var chosenFormat: PPColor = .genericHex
// The name of the chosen color space. If this is nil, then the picker will attempt to get the
// color space of the screen the picker is currently on (and if that fails, fall back to the
// default color space of the screenshot).
var colorSpace: String? = nil
// How precise floats should be when copied.
var floatPrecision: UInt = 3
@@ -98,8 +105,16 @@ import CleanroomLogger
activatingShortcut = shortcut
}
}
case "colorSpace":
// Check for the presence of the color space in our list. If it's found, then we
// can use it (safeguard against bogus input).
if let _ = PPColor.colorSpaceNames.index(where: { $0.1 == value.string }) {
colorSpace = value.string
}
case "chosenFormat":
chosenFormat = PPColor(rawValue: value.stringValue) ?? .genericHex
case "magnificationLevel":
magnificationLevel = value.int ?? 8
case "floatPrecision":
let n = value.uInt ?? 3
floatPrecision = (n > 0 && n < PPState.maxFloatPrecision) ? n : 3
@@ -137,6 +152,8 @@ import CleanroomLogger
"paschaModeEnabled": paschaModeEnabled,
"concentrationModeModifier": concentrationModeModifier.rawValue,
"activatingShortcut": shortcutData,
"magnificationLevel": magnificationLevel,
"colorSpace": colorSpace ?? "",
"chosenFormat": chosenFormat.rawValue,
"floatPrecision": floatPrecision,
"recentPicks": recentPicks.map({ $0.asJSON })
+20 -8
View File
@@ -8,6 +8,18 @@ import CleanroomLogger
let APP_NAME = Bundle.main.infoDictionary![kCFBundleNameKey as String] as! String
let APPLE_INTERFACE_STYLE = "AppleInterfaceStyle"
// This allows us to iterate over the raw values of an enum.
// TODO: use `CaseIterable` when Swift 4.2 comes out
func iterateEnum<T: Hashable>(_: T.Type) -> AnyIterator<T> {
var i = 0
return AnyIterator {
let next = withUnsafeBytes(of: &i) { $0.load(as: T.self) }
if next.hashValue != i { return nil }
i += 1
return next
}
}
// Copies the given string to the clipboard.
func copyToPasteboard(stringValue value: String) {
NSPasteboard.general.declareTypes([.string], owner: nil)
@@ -42,14 +54,14 @@ func getScreenFromPoint(_ point: NSPoint) -> NSScreen? {
// Returns which coordinates are outside, if any.
enum Coordinate {
case x, y, both, none
}
func coordinateOutsideRect(_ point: NSPoint, _ rect: NSRect) -> Coordinate {
let x = point.x <= rect.origin.x || point.x >= (rect.origin.x + rect.width)
let y = point.y <= rect.origin.y || point.y >= (rect.origin.y + rect.height)
if x && y { return .both }
if x { return .x }
if y { return .y }
return .none
static func isOutsideRect(_ point: NSPoint, _ rect: NSRect) -> Coordinate {
let x = point.x < rect.origin.x || point.x > (rect.origin.x + rect.width)
let y = point.y < rect.origin.y || point.y > (rect.origin.y + rect.height)
if x && y { return .both }
if x { return .x }
if y { return .y }
return .none
}
}
// A simple helper to run animations with the same context configration.
-4
View File
@@ -15,11 +15,7 @@ PixelPicker is like Digital Color Meter, but lives in your menu bar and lets you
## Installation
Coming soon.
<!--
Simply download the dmg from the [releases](https://github.com/acheronfail/pixel-picker/releases) tab and drag "Pixel Picker.app" into your `/Applications` folder.
-->
## Usage
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 B

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 54 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 54 KiB

-1
View File
@@ -1 +0,0 @@
Versions/Current/Headers
-1
View File
@@ -1 +0,0 @@
Versions/Current/Modules
-1
View File
@@ -1 +0,0 @@
Versions/Current/Resources
-1
View File
@@ -1 +0,0 @@
Versions/Current/SwiftyJSON
@@ -1,189 +0,0 @@
// Generated by Apple Swift version 4.1 effective-3.3 (swiftlang-902.0.48 clang-902.0.37.1)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
#if !defined(__has_include)
# define __has_include(x) 0
#endif
#if !defined(__has_attribute)
# define __has_attribute(x) 0
#endif
#if !defined(__has_feature)
# define __has_feature(x) 0
#endif
#if !defined(__has_warning)
# define __has_warning(x) 0
#endif
#if __has_include(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
#pragma clang diagnostic ignored "-Wauto-import"
#include <objc/NSObject.h>
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#if !defined(SWIFT_TYPEDEFS)
# define SWIFT_TYPEDEFS 1
# if __has_include(<uchar.h>)
# include <uchar.h>
# elif !defined(__cplusplus)
typedef uint_least16_t char16_t;
typedef uint_least32_t char32_t;
# endif
typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
#endif
#if !defined(SWIFT_PASTE)
# define SWIFT_PASTE_HELPER(x, y) x##y
# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
#endif
#if !defined(SWIFT_METATYPE)
# define SWIFT_METATYPE(X) Class
#endif
#if !defined(SWIFT_CLASS_PROPERTY)
# if __has_feature(objc_class_property)
# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
# else
# define SWIFT_CLASS_PROPERTY(...)
# endif
#endif
#if __has_attribute(objc_runtime_name)
# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
#else
# define SWIFT_RUNTIME_NAME(X)
#endif
#if __has_attribute(swift_name)
# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
#else
# define SWIFT_COMPILE_NAME(X)
#endif
#if __has_attribute(objc_method_family)
# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
#else
# define SWIFT_METHOD_FAMILY(X)
#endif
#if __has_attribute(noescape)
# define SWIFT_NOESCAPE __attribute__((noescape))
#else
# define SWIFT_NOESCAPE
#endif
#if __has_attribute(warn_unused_result)
# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
#else
# define SWIFT_WARN_UNUSED_RESULT
#endif
#if __has_attribute(noreturn)
# define SWIFT_NORETURN __attribute__((noreturn))
#else
# define SWIFT_NORETURN
#endif
#if !defined(SWIFT_CLASS_EXTRA)
# define SWIFT_CLASS_EXTRA
#endif
#if !defined(SWIFT_PROTOCOL_EXTRA)
# define SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_ENUM_EXTRA)
# define SWIFT_ENUM_EXTRA
#endif
#if !defined(SWIFT_CLASS)
# if __has_attribute(objc_subclassing_restricted)
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# else
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# endif
#endif
#if !defined(SWIFT_PROTOCOL)
# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_EXTENSION)
# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
#endif
#if !defined(OBJC_DESIGNATED_INITIALIZER)
# if __has_attribute(objc_designated_initializer)
# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
# else
# define OBJC_DESIGNATED_INITIALIZER
# endif
#endif
#if !defined(SWIFT_ENUM_ATTR)
# if defined(__has_attribute) && __has_attribute(enum_extensibility)
# define SWIFT_ENUM_ATTR __attribute__((enum_extensibility(open)))
# else
# define SWIFT_ENUM_ATTR
# endif
#endif
#if !defined(SWIFT_ENUM)
# define SWIFT_ENUM(_type, _name) enum _name : _type _name; enum SWIFT_ENUM_ATTR SWIFT_ENUM_EXTRA _name : _type
# if __has_feature(generalized_swift_name)
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR SWIFT_ENUM_EXTRA _name : _type
# else
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME) SWIFT_ENUM(_type, _name)
# endif
#endif
#if !defined(SWIFT_UNAVAILABLE)
# define SWIFT_UNAVAILABLE __attribute__((unavailable))
#endif
#if !defined(SWIFT_UNAVAILABLE_MSG)
# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
#endif
#if !defined(SWIFT_AVAILABILITY)
# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
#endif
#if !defined(SWIFT_DEPRECATED)
# define SWIFT_DEPRECATED __attribute__((deprecated))
#endif
#if !defined(SWIFT_DEPRECATED_MSG)
# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
#endif
#if __has_feature(attribute_diagnose_if_objc)
# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
#else
# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
#endif
#if __has_feature(modules)
@import Foundation;
#endif
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
#pragma clang diagnostic ignored "-Wduplicate-method-arg"
#if __has_warning("-Wpragma-clang-attribute")
# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
#endif
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma clang diagnostic ignored "-Wnullability"
#if __has_attribute(external_source_symbol)
# pragma push_macro("any")
# undef any
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="SwiftyJSON",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
# pragma pop_macro("any")
#endif
#if __has_attribute(external_source_symbol)
# pragma clang attribute pop
#endif
#pragma clang diagnostic pop
@@ -1,31 +0,0 @@
// SwiftyJSON.h
//
// Copyright (c) 2014 - 2017 Ruoyu Fu, Pinglin Tang
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
@import Foundation;
//! Project version number for SwiftyJSON.
FOUNDATION_EXPORT double SwiftyJSONVersionNumber;
//! Project version string for SwiftyJSON.
FOUNDATION_EXPORT const unsigned char SwiftyJSONVersionString[];
@@ -1,11 +0,0 @@
framework module SwiftyJSON {
umbrella header "SwiftyJSON.h"
export *
module * { export * }
}
module SwiftyJSON.Swift {
header "SwiftyJSON-Swift.h"
requires objc
}
@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>17E202</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>SwiftyJSON</string>
<key>CFBundleIdentifier</key>
<string>com.swiftyjson.SwiftyJSON</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>SwiftyJSON</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>9E501</string>
<key>DTPlatformVersion</key>
<string>GM</string>
<key>DTSDKBuild</key>
<string>17E189</string>
<key>DTSDKName</key>
<string>macosx10.13</string>
<key>DTXcode</key>
<string>0931</string>
<key>DTXcodeBuild</key>
<string>9E501</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
</dict>
</plist>
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
A