Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cc77460f3 | ||
|
|
ec78ffb864 | ||
|
|
9ce676c51c | ||
|
|
1ad62f4bf8 | ||
|
|
fee08c000b | ||
|
|
e087df3b93 | ||
|
|
a7d112a8ba | ||
|
|
b4175bae13 | ||
|
|
a63fd85bc5 | ||
|
|
ff1063c8e2 | ||
|
|
1305e4366a | ||
|
|
4cad88d0cd | ||
|
|
58f3ef9ecd | ||
|
|
8b907ac7ed | ||
|
|
e5ce05dc89 | ||
|
|
42f88bcf53 | ||
|
|
4ddfe5a0f3 | ||
|
|
0e60e12364 | ||
|
|
06e20d5594 | ||
|
|
5c7e1b61d7 | ||
|
|
57da81bc29 | ||
|
|
86c4b29ed5 | ||
|
|
88f3465f27 | ||
|
|
27d4ca0d3b | ||
|
|
2802953130 | ||
|
|
5087a973cf | ||
|
|
a6e85ec913 | ||
|
|
27b3b9768c |
@@ -7,7 +7,9 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
15349DB820BAE3F70071CAA1 /* NSBezierPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15349DB720BAE3F70071CAA1 /* NSBezierPath.swift */; };
|
||||
15492103207B53AE00E45BBC /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15492102207B53AE00E45BBC /* Util.swift */; };
|
||||
15493F6720BE6E0F007B2BA5 /* CGImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15493F6620BE6E0F007B2BA5 /* CGImage.swift */; };
|
||||
155D2C03204F61220087478B /* Credits.rtfd in Resources */ = {isa = PBXBuildFile; fileRef = 155D2C02204F61220087478B /* Credits.rtfd */; };
|
||||
159F304E2079A111000BD387 /* AppDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159F304D2079A111000BD387 /* AppDelegateTests.swift */; };
|
||||
159F30522079A147000BD387 /* PPStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159F30512079A147000BD387 /* PPStateTests.swift */; };
|
||||
@@ -65,7 +67,9 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
15349DB720BAE3F70071CAA1 /* NSBezierPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSBezierPath.swift; sourceTree = "<group>"; };
|
||||
15492102207B53AE00E45BBC /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = "<group>"; };
|
||||
15493F6620BE6E0F007B2BA5 /* CGImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGImage.swift; sourceTree = "<group>"; };
|
||||
155D2C02204F61220087478B /* Credits.rtfd */ = {isa = PBXFileReference; lastKnownFileType = wrapper.rtfd; path = Credits.rtfd; sourceTree = "<group>"; };
|
||||
159F304D2079A111000BD387 /* AppDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateTests.swift; sourceTree = "<group>"; };
|
||||
159F30512079A147000BD387 /* PPStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPStateTests.swift; sourceTree = "<group>"; };
|
||||
@@ -171,6 +175,7 @@
|
||||
B3B4C2BF1E25894B009F8E4E /* PixelPicker */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
15493F6620BE6E0F007B2BA5 /* CGImage.swift */,
|
||||
BA74C13420A6F1BE00306B27 /* ShowAndHideCursor.h */,
|
||||
BA74C13220A6F16C00306B27 /* ShowAndHideCursor.m */,
|
||||
BA71817320AE2DCC00619700 /* PPMenuShortcutView.h */,
|
||||
@@ -180,6 +185,7 @@
|
||||
BA74C14D20ADA33E00306B27 /* PPColor.swift */,
|
||||
15FC8CC7204E2E2C000B5E1E /* PPState.swift */,
|
||||
159ED10B207EB2EF005489ED /* OverlayPanel */,
|
||||
15349DB720BAE3F70071CAA1 /* NSBezierPath.swift */,
|
||||
BA74C14120AA409D00306B27 /* NSColor.swift */,
|
||||
15492102207B53AE00E45BBC /* Util.swift */,
|
||||
B3B4C2C21E25894B009F8E4E /* Assets.xcassets */,
|
||||
@@ -250,13 +256,14 @@
|
||||
TargetAttributes = {
|
||||
15B4E86F207994AF0024FDE4 = {
|
||||
CreatedOnToolsVersion = 9.3;
|
||||
LastSwiftMigration = 1000;
|
||||
ProvisioningStyle = Automatic;
|
||||
TestTargetID = B3B4C2BC1E25894B009F8E4E;
|
||||
};
|
||||
B3B4C2BC1E25894B009F8E4E = {
|
||||
CreatedOnToolsVersion = 8.2.1;
|
||||
DevelopmentTeam = UNR5C68RPX;
|
||||
LastSwiftMigration = 0920;
|
||||
LastSwiftMigration = 1000;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
@@ -333,6 +340,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BA74C13320A6F16C00306B27 /* ShowAndHideCursor.m in Sources */,
|
||||
15349DB820BAE3F70071CAA1 /* NSBezierPath.swift in Sources */,
|
||||
BA71817220AE2DB400619700 /* PPMenuShortcutView.m in Sources */,
|
||||
B3B4C2C11E25894B009F8E4E /* AppDelegate.swift in Sources */,
|
||||
BA22232920B04D660001069C /* AppDelegate+Menu.swift in Sources */,
|
||||
@@ -342,6 +350,7 @@
|
||||
BA74C14020AA390400306B27 /* PPOverlayPreview.swift in Sources */,
|
||||
BA74C14420AA609500306B27 /* PPOverlayWrapper.swift in Sources */,
|
||||
BA74C0F820A692D100306B27 /* PPOverlayController.swift in Sources */,
|
||||
15493F6720BE6E0F007B2BA5 /* CGImage.swift in Sources */,
|
||||
BA74C14220AA409D00306B27 /* NSColor.swift in Sources */,
|
||||
15492103207B53AE00E45BBC /* Util.swift in Sources */,
|
||||
);
|
||||
@@ -381,7 +390,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.acheronfail.PixelPickerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PixelPicker.app/Contents/MacOS/PixelPicker";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -409,7 +418,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.acheronfail.PixelPickerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PixelPicker.app/Contents/MacOS/PixelPicker";
|
||||
};
|
||||
name = Release;
|
||||
@@ -551,8 +560,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "PixelPicker/Bridging-Header.h";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -577,8 +585,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "PixelPicker/Bridging-Header.h";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
@@ -10,8 +10,18 @@
|
||||
|
||||
import LaunchAtLogin
|
||||
|
||||
// The modifiers available to use to toggle "concentrationMode".
|
||||
let concentrationModifiers: [(String, NSEvent.ModifierFlags)] = [
|
||||
// The settings available for displaying a grid in the picker's preview.
|
||||
enum GridSetting {
|
||||
case never, always, inFocusMode
|
||||
static let withNames: [(String, GridSetting)] = [
|
||||
("Only in Focus Mode", .inFocusMode),
|
||||
("Always", .always),
|
||||
("Never", .never)
|
||||
]
|
||||
}
|
||||
|
||||
// The modifiers available to use to toggle "focusMode".
|
||||
let focusModifiers: [(String, NSEvent.ModifierFlags)] = [
|
||||
("fn Function", .function),
|
||||
("⌘ Command", .command),
|
||||
("⌃ Control", .control),
|
||||
@@ -58,8 +68,9 @@ extension AppDelegate: NSMenuDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// 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...
|
||||
// This rebuilds the context menu from scratch. For the sake of simplicity, we re-create the
|
||||
// menu from scratch each time. It's not an expensive operation, and is only called when the
|
||||
// user opens the menu.
|
||||
func rebuildContextMenu() {
|
||||
contextMenu.removeAllItems()
|
||||
|
||||
@@ -69,9 +80,11 @@ extension AppDelegate: NSMenuDelegate {
|
||||
buildRecentPicks()
|
||||
|
||||
contextMenu.addItem(.separator())
|
||||
buildShowGridMenu()
|
||||
buildColorSpaceItem()
|
||||
buildColorFormatsMenu()
|
||||
buildMagnificationItem()
|
||||
buildConcentrationMenu()
|
||||
buildMagnificationMenu()
|
||||
buildFocusModeModifierMenu()
|
||||
buildFloatPrecisionSlider()
|
||||
buildShortcutMenuItem()
|
||||
buildLaunchAtLoginItem()
|
||||
@@ -81,7 +94,50 @@ extension AppDelegate: NSMenuDelegate {
|
||||
contextMenu.addItem(withTitle: "Quit \(APP_NAME)", action: #selector(quitApplication), keyEquivalent: "")
|
||||
}
|
||||
|
||||
private func buildMagnificationItem() {
|
||||
// Choose whether to always draw a grid, never draw one, or only draw one when in focus mode.
|
||||
private func buildShowGridMenu() {
|
||||
let submenu = NSMenu()
|
||||
for (title, setting) in GridSetting.withNames {
|
||||
let item = submenu.addItem(withTitle: title, action: #selector(selectGridSetting(_:)), keyEquivalent: "")
|
||||
item.representedObject = setting
|
||||
item.state = PPState.shared.gridSetting == setting ? .on : .off
|
||||
}
|
||||
|
||||
let item = contextMenu.addItem(withTitle: "Show Grid", action: nil, keyEquivalent: "")
|
||||
item.submenu = submenu
|
||||
}
|
||||
|
||||
@objc private func selectGridSetting(_ sender: NSMenuItem) {
|
||||
if let setting = sender.representedObject as? GridSetting {
|
||||
PPState.shared.gridSetting = setting
|
||||
}
|
||||
}
|
||||
|
||||
// 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 buildMagnificationMenu() {
|
||||
let submenu = NSMenu()
|
||||
for i in stride(from: 4, through: 24, by: 2) {
|
||||
let item = submenu.addItem(withTitle: "\(i)x", action: #selector(selectMagnification(_:)), keyEquivalent: "")
|
||||
@@ -98,6 +154,7 @@ extension AppDelegate: NSMenuDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -113,7 +170,7 @@ extension AppDelegate: NSMenuDelegate {
|
||||
contextMenu.addItem(.separator())
|
||||
contextMenu.addItem(withTitle: "Recently Picked", action: nil, keyEquivalent: "")
|
||||
let format = PPState.shared.chosenFormat
|
||||
for pickedColor in PPState.shared.recentPicks {
|
||||
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)
|
||||
@@ -132,7 +189,7 @@ extension AppDelegate: NSMenuDelegate {
|
||||
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))
|
||||
@@ -142,7 +199,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: "")
|
||||
@@ -161,28 +218,27 @@ 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.
|
||||
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() {
|
||||
let submenu = NSMenu()
|
||||
for format in iterateEnum(PPColor.self) {
|
||||
for format in PPColor.allCases {
|
||||
let formatItem = submenu.addItem(withTitle: format.rawValue, action: #selector(selectFormat(_:)), keyEquivalent: "")
|
||||
formatItem.representedObject = format
|
||||
if PPState.shared.chosenFormat == format { formatItem.state = .on }
|
||||
@@ -191,47 +247,44 @@ 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() {
|
||||
|
||||
// Builds and adds the focus modifier submenu.
|
||||
private func buildFocusModeModifierMenu() {
|
||||
let submenu = NSMenu()
|
||||
for (name, modifier) in concentrationModifiers {
|
||||
for (name, modifier) in focusModifiers {
|
||||
let modifierItem = submenu.addItem(withTitle: name, action: #selector(selectModifier(_:)), keyEquivalent: "")
|
||||
modifierItem.representedObject = modifier
|
||||
if PPState.shared.concentrationModeModifier == modifier { modifierItem.state = .on }
|
||||
if PPState.shared.focusModeModifier == modifier { modifierItem.state = .on }
|
||||
}
|
||||
|
||||
let item = contextMenu.addItem(withTitle: "Concentration Modifier", action: nil, keyEquivalent: "")
|
||||
let item = contextMenu.addItem(withTitle: "Focus Mode Modifier", action: nil, keyEquivalent: "")
|
||||
item.submenu = submenu
|
||||
}
|
||||
|
||||
// Set the chosen modifier to toggle "concentrationMode".
|
||||
|
||||
// Set the chosen modifier to toggle "focusMode".
|
||||
@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
|
||||
PPState.shared.focusModeModifier = modifier
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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() {
|
||||
contextMenu.addItem(withTitle: "Picker Shortcut", action: nil, keyEquivalent: "")
|
||||
|
||||
let shortcutView = MASShortcutView()
|
||||
shortcutView.style = .flat
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import MASShortcut
|
||||
import CleanroomLogger
|
||||
|
||||
// The app's menu bar item.
|
||||
let ICON = setupMenuBarIcon(NSImage(named: NSImage.Name(rawValue: "icon")))
|
||||
let ICON = setupMenuBarIcon(NSImage(named: NSImage.Name(stringLiteral: "icon")))
|
||||
|
||||
@NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 994 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// CGImage.swift
|
||||
// PixelPicker
|
||||
//
|
||||
|
||||
import CleanroomLogger
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.1</string>
|
||||
<string>1.2.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2</string>
|
||||
<string>8</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// NSBezierPath.swift
|
||||
// PixelPicker
|
||||
//
|
||||
|
||||
// Extend NSBezierPath to allow easy conversion to a CGPath.
|
||||
// https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CocoaDrawingGuide/Paths/Paths.html#//apple_ref/doc/uid/TP40003290-CH206-SW2
|
||||
extension NSBezierPath {
|
||||
var cgPath: CGPath {
|
||||
let path = CGMutablePath()
|
||||
var points = [CGPoint](repeating: .zero, count: 3)
|
||||
for i in 0 ..< self.elementCount {
|
||||
switch self.element(at: i, associatedPoints: &points) {
|
||||
case .moveTo: path.move(to: points[0])
|
||||
case .lineTo: path.addLine(to: points[0])
|
||||
case .curveTo: path.addCurve(to: points[2], control1: points[0], control2: points[1])
|
||||
case .closePath: path.closeSubpath()
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
}
|
||||
@@ -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,27 +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!
|
||||
|
||||
// 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 {
|
||||
// mouse cursor while the focusMode is active. This is so we can slow it down.
|
||||
var focusMode: Bool = false {
|
||||
didSet {
|
||||
if isEnabled {
|
||||
panelSize = concentrationMode ? 300 : 150
|
||||
panelSize = focusMode ? 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))
|
||||
CGAssociateMouseAndMouseCursorPosition(boolean_t(truncating: focusMode ? 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.
|
||||
@@ -55,12 +56,17 @@ class PPOverlayController: NSWindowController {
|
||||
startMonitoringEvents()
|
||||
} else {
|
||||
stopMonitoringEvents()
|
||||
concentrationMode = false
|
||||
focusMode = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) }
|
||||
@@ -84,40 +90,49 @@ class PPOverlayController: NSWindowController {
|
||||
// When the user clicks, animate the cursor out, save the picked color and copy it.
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
hidePicker(animate: true)
|
||||
|
||||
let pickedColor = PPPickedColor(color: lastHighlightedColor, format: PPState.shared.chosenFormat)
|
||||
PPState.shared.addRecentPick(pickedColor)
|
||||
copyToPasteboard(stringValue: pickedColor.asString)
|
||||
pickColor()
|
||||
}
|
||||
|
||||
// Close picker immediately when the user presses the escape key.
|
||||
// Listen to key events.
|
||||
override func keyDown(with event: NSEvent) {
|
||||
if event.keyCode == kVK_Escape { hidePicker(animate: false) }
|
||||
if event.keyCode == kVK_Space {
|
||||
let format = PPState.shared.chosenFormat
|
||||
PPState.shared.chosenFormat = event.modifierFlags.contains(.shift) ? format.previous() : format.next()
|
||||
switch Int(event.keyCode) {
|
||||
// Close immediately on escape.
|
||||
case kVK_Escape:
|
||||
hidePicker(animate: false)
|
||||
// Act as if the user clicked on space.
|
||||
case kVK_Space:
|
||||
hidePicker(animate: true)
|
||||
pickColor()
|
||||
// Cycle between formats when the user presses the left or right arrow keys.
|
||||
case kVK_LeftArrow:
|
||||
PPState.shared.chosenFormat = PPState.shared.chosenFormat.previous()
|
||||
updateInfoPanel(lastHighlightedColor, lastHighlightedColor.bestContrastingColor())
|
||||
case kVK_RightArrow:
|
||||
PPState.shared.chosenFormat = PPState.shared.chosenFormat.next()
|
||||
updateInfoPanel(lastHighlightedColor, lastHighlightedColor.bestContrastingColor())
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Enable "concentrationMode" when correct modifier flag is changed.
|
||||
// Enable "focusMode" when correct modifier flag is changed.
|
||||
override func flagsChanged(with event: NSEvent) {
|
||||
if isEnabled {
|
||||
concentrationMode = event.modifierFlags.contains(PPState.shared.concentrationModeModifier)
|
||||
focusMode = event.modifierFlags.contains(PPState.shared.focusModeModifier)
|
||||
updatePreview(aroundPoint: lastMouseLocation)
|
||||
}
|
||||
}
|
||||
|
||||
// Move the picker with the mouse, and if "concentrationMode" is active then
|
||||
// Move the picker with the mouse, and if "focusMode" is active then
|
||||
// slow it down to make it easier to pick the correct pixel.
|
||||
override func mouseMoved(with event: NSEvent) {
|
||||
if isEnabled {
|
||||
let currentMouseLocation = NSEvent.mouseLocation
|
||||
var nextMouseLocation = currentMouseLocation
|
||||
|
||||
// Slow down tracking speed when concentration mode is active.
|
||||
if concentrationMode {
|
||||
let speed: CGFloat = concentrationMode ? 0.1 : 0.5
|
||||
// Slow down tracking speed when focusMode is active.
|
||||
if focusMode {
|
||||
let speed: CGFloat = focusMode ? 0.1 : 0.5
|
||||
let x = lastMouseLocation.x + (event.deltaX * speed)
|
||||
let y = lastMouseLocation.y - (event.deltaY * speed)
|
||||
|
||||
@@ -141,6 +156,12 @@ class PPOverlayController: NSWindowController {
|
||||
}
|
||||
}
|
||||
|
||||
func pickColor() {
|
||||
let pickedColor = PPPickedColor(color: lastHighlightedColor, format: PPState.shared.chosenFormat)
|
||||
PPState.shared.addRecentPick(pickedColor)
|
||||
copyToPasteboard(stringValue: pickedColor.asString)
|
||||
}
|
||||
|
||||
// Show the picker, save the previously active application, and hide the cursor.
|
||||
func showPicker() {
|
||||
if overlayPanel.isVisible || infoPanel.isVisible { return }
|
||||
@@ -209,7 +230,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
|
||||
@@ -230,13 +251,20 @@ class PPOverlayController: NSWindowController {
|
||||
// Ensure the overlayPanel is always the key window.
|
||||
if !overlayPanel.isKeyWindow {
|
||||
overlayPanel.activate(withSize: panelSize, infoPanel: infoPanel)
|
||||
// Reset the cursor state to ensure it's hidden (the foremost application changes then
|
||||
// WindowServer resets our "SetsCursorInBackground" magic).
|
||||
ShowCursor()
|
||||
HideCursor()
|
||||
}
|
||||
|
||||
// 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 zoomReciprocal: CGFloat = 1.0 / (focusMode ? magnification * 2.5 : magnification)
|
||||
let currentSize = CGFloat(screenShot.width) + 1
|
||||
let origin = floor(currentSize * ((1 - zoomReciprocal) / 2))
|
||||
let x = origin + (isHalf(normalisedPoint.x) ? 1 : 0)
|
||||
@@ -244,28 +272,66 @@ 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
|
||||
|
||||
// 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, zoomedSize)
|
||||
} else if let image = zoomedImage.copy(colorSpace: colorSpace) {
|
||||
return updatePreview(image, pixelSize, zoomedSize)
|
||||
}
|
||||
}
|
||||
|
||||
// No custom color space chosen, so use the screen's one.
|
||||
if let colorSpace = getScreenColorSpace(point) {
|
||||
if colorSpace == zoomedImage.colorSpace {
|
||||
return updatePreview(zoomedImage, pixelSize, zoomedSize)
|
||||
} else if let image = zoomedImage.copy(colorSpace: colorSpace) {
|
||||
return updatePreview(image, pixelSize, zoomedSize)
|
||||
}
|
||||
}
|
||||
|
||||
// If that also didn't work, then just return the image in its default color space.
|
||||
return updatePreview(zoomedImage, pixelSize, zoomedSize)
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePreview(_ image: CGImage, _ pixelSize: CGFloat, _ numberOfPixels: CGFloat) {
|
||||
let middlePosition = numberOfPixels / 2
|
||||
|
||||
// Extract the middle pixel color from the prepared image.
|
||||
let colorAtPixel = image.colorAt(x: Int(middlePosition), y: Int(middlePosition))
|
||||
let contrastingColor = colorAtPixel.bestContrastingColor()
|
||||
|
||||
// Update picker border and info panel.
|
||||
wrapper.update(contrastingColor.cgColor)
|
||||
updateInfoPanel(colorAtPixel, contrastingColor)
|
||||
|
||||
// Update the picker preview.
|
||||
preview.layer?.contents = image
|
||||
preview.updateCrosshair(pixelSize, middlePosition, contrastingColor.cgColor)
|
||||
let showGrid = PPState.shared.gridSetting == .always || (focusMode && PPState.shared.gridSetting == .inFocusMode)
|
||||
preview.updateGrid(cellSize: pixelSize, numberOfCells: Int(numberOfPixels), shouldDisplay: showGrid)
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
//
|
||||
|
||||
// This class is in charge of rendering the pixel preview.
|
||||
//
|
||||
// TODO: draw grid b/w pixels when zoomed?
|
||||
class PPOverlayPreview: NSView, CALayerDelegate {
|
||||
// A grid that's optionally shown in the preview.
|
||||
private var grid: CAShapeLayer = CAShapeLayer()
|
||||
private var lastCellSize: CGFloat?
|
||||
private var lastNumberOfCells: Int?
|
||||
|
||||
// The small pixel-box in the centre of the picker.
|
||||
var crosshair: CAShapeLayer = CAShapeLayer()
|
||||
|
||||
@@ -17,10 +20,15 @@ class PPOverlayPreview: NSView, CALayerDelegate {
|
||||
override func awakeFromNib() {
|
||||
// Make layers contents resize to fill, and disable antialiasing.
|
||||
wantsLayer = true
|
||||
layer?.magnificationFilter = kCAFilterNearest
|
||||
layer?.contentsGravity = kCAGravityResizeAspectFill
|
||||
layer?.magnificationFilter = .nearest
|
||||
layer?.contentsGravity = .resizeAspectFill
|
||||
layer?.delegate = self
|
||||
|
||||
// Add the grid shape layers to the view.
|
||||
grid.strokeColor = NSColor.gray.cgColor
|
||||
grid.fillColor = nil
|
||||
layer?.addSublayer(grid)
|
||||
|
||||
// Prepare crosshair shape layers.
|
||||
crosshair.strokeColor = NSColor.black.cgColor
|
||||
crosshair.fillColor = nil
|
||||
@@ -31,10 +39,36 @@ class PPOverlayPreview: NSView, CALayerDelegate {
|
||||
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)
|
||||
}
|
||||
|
||||
// Redraws the grid in the preview.
|
||||
func updateGrid(cellSize size: CGFloat, numberOfCells n: Int, shouldDisplay: Bool) {
|
||||
// Disable implicit animations within this transaction.
|
||||
CATransaction.setDisableActions(true)
|
||||
grid.opacity = shouldDisplay ? 0.25 : 0
|
||||
|
||||
// Only redraw the grid if we need to display it and the dimensions have changed since last
|
||||
// the last time we drew it.
|
||||
guard shouldDisplay && (size != lastCellSize || n != lastNumberOfCells) else { return }
|
||||
lastCellSize = size
|
||||
lastNumberOfCells = n
|
||||
|
||||
let path = NSBezierPath()
|
||||
let start = CGFloat(0)
|
||||
let end = CGFloat(n) * size
|
||||
|
||||
for i in 1..<n {
|
||||
let pos = CGFloat(i) * size
|
||||
path.move(to: NSPoint(x: pos, y: start))
|
||||
path.line(to: NSPoint(x: pos, y: end))
|
||||
path.move(to: NSPoint(x: start, y: pos))
|
||||
path.line(to: NSPoint(x: end, y: pos))
|
||||
}
|
||||
|
||||
grid.path = path.cgPath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
@@ -51,7 +39,7 @@ struct PPPickedColor {
|
||||
|
||||
// This enum is responsible for each of the color formats PixelPicker supports,
|
||||
// as well as being responsible for formatting them as strings, etc.
|
||||
enum PPColor: String {
|
||||
enum PPColor: String, CaseIterable {
|
||||
case genericHex = "Generic Hex"
|
||||
case generic8Bit = "Generic 8-Bit"
|
||||
case genericDecimal = "Generic Decimal"
|
||||
@@ -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,19 +158,18 @@ 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 }))
|
||||
return next(withArray: PPColor.allCases)
|
||||
}
|
||||
|
||||
|
||||
// Same as next() but backwards.
|
||||
func previous() -> PPColor {
|
||||
return next(withArray: iterateEnum(PPColor.self).reversed())
|
||||
return next(withArray: PPColor.allCases.reversed())
|
||||
}
|
||||
|
||||
// Finds the next element after this element in the given array.
|
||||
// This method should only be passed the result of iterateEnum(PPColor.self).
|
||||
|
||||
// Finds the next PPColor after this one in the list of PPColors.
|
||||
private func next(withArray array: [PPColor]) -> PPColor {
|
||||
var found = false
|
||||
for x in array {
|
||||
@@ -192,26 +179,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 +206,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 +240,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)
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#import "PPMenuShortcutView.h"
|
||||
#import "PixelPicker-Swift.h"
|
||||
|
||||
// The trailing end of the cancel button in the MASShortcutView (within the NSMenuItem).
|
||||
CGFloat cancelBoundary = 15;
|
||||
|
||||
// In order to have a MASShortcutView inside our menubar dropdown menu, we need to
|
||||
// perform some hacks. Basically, we catch all the key and mouse events, and then
|
||||
// use those to manually control the shortcut view (the MASShortcutView doesn't work
|
||||
@@ -44,6 +47,21 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
-(void)drawRect:(NSRect)dirtyRect
|
||||
{
|
||||
[super drawRect:dirtyRect];
|
||||
|
||||
// Draw a border around the view.
|
||||
// Currently we don't use any of MASShortcut's custom drawing styles since they don't draw
|
||||
// nicely with macOS 10.14's new dark interface style.
|
||||
CGFloat xInset = 20;
|
||||
CGFloat yInset = 1;
|
||||
NSRect borderRect = NSMakeRect(self.frame.origin.x + xInset, self.frame.origin.y + yInset, self.frame.size.width - xInset - cancelBoundary, self.frame.size.height - (yInset * 2));
|
||||
NSBezierPath *border = [NSBezierPath bezierPathWithRoundedRect:borderRect xRadius:3 yRadius:3];
|
||||
[[[NSColor scrollBarColor] colorWithAlphaComponent:0.5] set];
|
||||
[border stroke];
|
||||
}
|
||||
|
||||
// This is called when the NSMenu is opened, so it's a good time to register our
|
||||
// intercepting event handlers to catch all keyboard events inside the NSMenu.
|
||||
- (void)viewDidMoveToWindow
|
||||
@@ -95,7 +113,7 @@
|
||||
// End of the MASShortcutView.
|
||||
CGFloat end = start + shortcutView.frame.size.width;
|
||||
// Start of the cancel button in the MASShortcutView.
|
||||
CGFloat cancelStart = end - 20;
|
||||
CGFloat cancelStart = end - cancelBoundary;
|
||||
|
||||
if (shortcutView.shortcutValue != nil) {
|
||||
if (point.x >= start && point.x <= cancelStart) {
|
||||
|
||||
@@ -24,7 +24,6 @@ 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.
|
||||
@@ -32,12 +31,20 @@ import CleanroomLogger
|
||||
|
||||
// Magnification level of the picker.
|
||||
var magnificationLevel: Int = 8
|
||||
|
||||
// When to draw a grid in the preview.
|
||||
var gridSetting: GridSetting = .inFocusMode
|
||||
|
||||
// Hold this down to enter concentration mode.
|
||||
var concentrationModeModifier: NSEvent.ModifierFlags = .control
|
||||
// Hold this down to enter focus mode.
|
||||
var focusModeModifier: 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
|
||||
@@ -61,7 +68,7 @@ import CleanroomLogger
|
||||
|
||||
func resetState() {
|
||||
paschaModeEnabled = false
|
||||
concentrationModeModifier = .control
|
||||
focusModeModifier = .control
|
||||
activatingShortcut = nil
|
||||
chosenFormat = .genericHex
|
||||
floatPrecision = 3
|
||||
@@ -92,8 +99,8 @@ import CleanroomLogger
|
||||
switch key {
|
||||
case "paschaModeEnabled":
|
||||
paschaModeEnabled = value.bool ?? false
|
||||
case "concentrationModeModifier":
|
||||
concentrationModeModifier = NSEvent.ModifierFlags(rawValue: value.uInt ?? NSEvent.ModifierFlags.control.rawValue)
|
||||
case "focusModeModifier":
|
||||
focusModeModifier = NSEvent.ModifierFlags(rawValue: value.uInt ?? NSEvent.ModifierFlags.control.rawValue)
|
||||
case "activatingShortcut":
|
||||
if let keyCode = value["keyCode"].uInt, let modifierFlags = value["modifierFlags"].uInt {
|
||||
let shortcut = MASShortcut(keyCode: keyCode, modifierFlags: modifierFlags)
|
||||
@@ -101,6 +108,12 @@ 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":
|
||||
@@ -140,9 +153,10 @@ import CleanroomLogger
|
||||
|
||||
let json: JSON = [
|
||||
"paschaModeEnabled": paschaModeEnabled,
|
||||
"concentrationModeModifier": concentrationModeModifier.rawValue,
|
||||
"focusModeModifier": focusModeModifier.rawValue,
|
||||
"activatingShortcut": shortcutData,
|
||||
"magnificationLevel": magnificationLevel,
|
||||
"colorSpace": colorSpace ?? "",
|
||||
"chosenFormat": chosenFormat.rawValue,
|
||||
"floatPrecision": floatPrecision,
|
||||
"recentPicks": recentPicks.map({ $0.asJSON })
|
||||
|
||||
@@ -15,10 +15,18 @@ void LogWarning(void);
|
||||
|
||||
// We use an undocumented API to hide the cursor even when the application *isn't* active.
|
||||
// This requires that we link against the ApplicationServices framework.
|
||||
// See https://stackoverflow.com/a/3939241/5552584
|
||||
// and https://web.archive.org/web/20150609013355/http://lists.apple.com:80/archives/carbon-dev/2006/Jan/msg00555.html
|
||||
// and http://doomlaser.com/cursorcerer-hide-your-cursor-at-will/
|
||||
void CGSSetConnectionProperty(int a, int b, CFStringRef c, CFBooleanRef d);
|
||||
int _CGSDefaultConnection(void);
|
||||
// See: https://stackoverflow.com/a/3939241/5552584
|
||||
// Also: https://github.com/asmagill/hammerspoon_asm.undocumented/blob/master/cursor/CGSConnection.h
|
||||
|
||||
// Every application is given a singular connection ID through which it can receieve and manipulate
|
||||
// values, state, notifications, events, etc. in the Window Server.
|
||||
typedef int CGSConnectionID;
|
||||
|
||||
// Associates a value for the given key on the given connection.
|
||||
CGError CGSSetConnectionProperty(CGSConnectionID cid, CGSConnectionID targetCID, CFStringRef key, CFTypeRef value);
|
||||
|
||||
// Gets the default connection for this process. `CGSMainConnectionID` is just a more modern name.
|
||||
CGSConnectionID _CGSDefaultConnection(void);
|
||||
CGSConnectionID CGSMainConnectionID(void);
|
||||
|
||||
#endif /* ShowAndHideCursor_h */
|
||||
|
||||
@@ -31,7 +31,8 @@ void HideCursor() {
|
||||
#endif
|
||||
|
||||
CFStringRef propertyString = CFStringCreateWithCString(NULL, "SetsCursorInBackground", kCFStringEncodingUTF8);
|
||||
CGSSetConnectionProperty(_CGSDefaultConnection(), _CGSDefaultConnection(), propertyString, kCFBooleanTrue);
|
||||
CGSConnectionID cid = _CGSDefaultConnection();
|
||||
CGSSetConnectionProperty(cid, cid, propertyString, kCFBooleanTrue);
|
||||
CFRelease(propertyString);
|
||||
|
||||
CGDisplayHideCursor(kCGDirectMainDisplay);
|
||||
|
||||
@@ -56,7 +56,7 @@ enum Coordinate {
|
||||
func runAnimation(_ f: (NSAnimationContext) -> Void, done: (() -> Void)?) {
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = 0.5
|
||||
context.timingFunction = .init(name: kCAMediaTimingFunctionEaseInEaseOut)
|
||||
context.timingFunction = CAMediaTimingFunction.init(name: .easeInEaseOut)
|
||||
context.allowsImplicitAnimation = true
|
||||
f(context)
|
||||
}, completionHandler: done)
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import PixelPicker
|
||||
|
||||
class AppDelegateTests: XCTestCase {}
|
||||
|
||||
@@ -4,4 +4,3 @@
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import PixelPicker
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import PixelPicker
|
||||
|
||||
class PPStateTests: XCTestCase {}
|
||||
|
||||
@@ -4,11 +4,13 @@ PixelPicker is like Digital Color Meter, but lives in your menu bar and lets you
|
||||
|
||||
### Features
|
||||
|
||||
* Pick *any* pixel on your screen with ease
|
||||
* Different zoom levels
|
||||
* Multiple color export formats
|
||||
* Multi-monitor support
|
||||
* Global keyboard shortcuts
|
||||
* 📸 Pick *any* pixel on your screen with ease
|
||||
* 🔎 Different magnification levels
|
||||
* ✏ Multiple color export formats
|
||||
* 🖥 Multi-monitor support
|
||||
* 🎨 Supports different color spaces
|
||||
* ⌨️ Global keyboard shortcut activation
|
||||
* 🌄 Customisable preview
|
||||
* ✨
|
||||
|
||||

|
||||
@@ -25,10 +27,10 @@ After you've installed PixelPicker, it will live in your menu bar. You have many
|
||||
* Select the "Pick a Pixel" item in the dropdown menu to activate the picker
|
||||
* Simply right-click the menu item and the picker will activate
|
||||
|
||||
Once you have the picker open, it should be quite straightforward! Move your mouse around the screen and click on the pixel you want.
|
||||
You can also hold down the <kbd>control (⌃)</kbd> key (this can be changed to another key) while the picker is open, and the picker will enter "Concentration Mode". This will zoom the pixel preview even further, and will slow down the picker's movement to make it easier to pick that hard-to-get pixel!
|
||||
Once you have the picker open, it should be quite straightforward! Move your mouse around the screen and click (or press <kbd>space</kbd> on the pixel you want.
|
||||
You can also hold down the <kbd>control (⌃)</kbd> key (this can be changed to another key) while the picker is open, and the picker will enter "Focus Mode". This will zoom the pixel preview even further, and will slow down the picker's movement to make it easier to pick that hard-to-get pixel!
|
||||
|
||||
Remember, you can press <kbd>space</kbd> (or <kbd>shift</kbd> + <kbd>space</kbd>) to cycle between color formats while the picker is open.
|
||||
**ProTip:** Remember, you can use the left or right arrow keys to cycle between color formats while the picker is open.
|
||||
|
||||
### Options
|
||||
|
||||
@@ -36,20 +38,26 @@ All of PixelPicker's options are in its menubar dropdown. Simply click on the me
|
||||
|
||||
PixelPicker provides some neat options:
|
||||
|
||||
* **Concentration Modifier**
|
||||
* Choose which modifier activates concentration mode. When concentration mode is active, the picker zooms closer in, and your mouse moves slower to help you pick the right pixel.
|
||||
* **Color Format**
|
||||
* 🔳 **Show Grid**
|
||||
* Whether or not to show a grid around the pixels in the picker's preview.
|
||||
* 🎨 **Color Space**
|
||||
* Computer screens are complex things, and have various color spaces. You can either leave this setting as its default (it will choose the default color space of your screen) or you can specify a specific color space and PixelPicker will try and use that instead (compatibility depends on your computer's screen/monitor).
|
||||
* ✏️ **Color Format**
|
||||
* How should the color be copied when you select it? Examples include CSS Hex: `#aabbcc`, or Java RGB: `new Color(158, 198, 117)`, etc.
|
||||
* **Float Precision**
|
||||
* 🔍 **Magnification**
|
||||
- How far in should the preview zoom/how big should each pixel be?
|
||||
* 🧐 **Focus Modifier**
|
||||
* Choose which modifier activates focus mode. When focus mode is active, the picker zooms even closer in, and your mouse moves slower to help you pick the right pixel.
|
||||
* 🔢 **Float Precision**
|
||||
* This controls how many digits will come after the decimal point when using float-based color formats.
|
||||
* **Picker Shortcut**
|
||||
* ⌨️ **Picker Shortcut**
|
||||
* The global keyboard shortcut to use that will activate Pixel Picker.
|
||||
* **Launch at Login**
|
||||
* 🚀 **Launch at Login**
|
||||
* If this is enabled then PixelPicker will be launched when you log into your computer.
|
||||
|
||||
#### Extra Overrides or Hidden Settings
|
||||
|
||||
PixelPicker has some experimental overrides that can be toggled via the Terminal (`defaults write ...`, etc). Look under the "Experimental Overrides" section at `PixelPicker` -> `About` for more information.
|
||||
In some releases PixelPicker might have some experimental overrides that can be toggled via the Terminal (`defaults write ...`, etc). Look under the "Experimental Overrides" section at `PixelPicker` -> `About` for more information.
|
||||
|
||||
## Questions/Concerns
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 333 KiB After Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 994 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 54 KiB |