Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cc77460f3 | ||
|
|
ec78ffb864 | ||
|
|
9ce676c51c | ||
|
|
1ad62f4bf8 | ||
|
|
fee08c000b | ||
|
|
e087df3b93 | ||
|
|
a7d112a8ba |
@@ -256,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;
|
||||
};
|
||||
};
|
||||
@@ -389,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;
|
||||
@@ -417,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;
|
||||
@@ -559,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;
|
||||
};
|
||||
@@ -585,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;
|
||||
};
|
||||
|
||||
@@ -236,10 +236,9 @@ extension AppDelegate: NSMenuDelegate {
|
||||
}
|
||||
|
||||
// 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 }
|
||||
@@ -282,6 +281,7 @@ extension AppDelegate: NSMenuDelegate {
|
||||
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 }
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2.1</string>
|
||||
<string>1.2.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>6</string>
|
||||
<string>8</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@@ -11,10 +11,10 @@ extension NSBezierPath {
|
||||
var points = [CGPoint](repeating: .zero, count: 3)
|
||||
for i in 0 ..< self.elementCount {
|
||||
switch self.element(at: i, associatedPoints: &points) {
|
||||
case .moveToBezierPathElement: path.move(to: points[0])
|
||||
case .lineToBezierPathElement: path.addLine(to: points[0])
|
||||
case .curveToBezierPathElement: path.addCurve(to: points[2], control1: points[0], control2: points[1])
|
||||
case .closePathBezierPathElement: path.closeSubpath()
|
||||
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
|
||||
|
||||
@@ -90,19 +90,28 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,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 }
|
||||
|
||||
@@ -20,8 +20,8 @@ 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.
|
||||
|
||||
@@ -39,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"
|
||||
@@ -161,16 +161,15 @@ enum PPColor: String {
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -18,14 +18,14 @@ void LogWarning(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.
|
||||
// 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.
|
||||
// Associates a value for the given key on the given connection.
|
||||
CGError CGSSetConnectionProperty(CGSConnectionID cid, CGSConnectionID targetCID, CFStringRef key, CFTypeRef value);
|
||||
|
||||
/// Both of these methods get the default connection for this process.
|
||||
// Gets the default connection for this process. `CGSMainConnectionID` is just a more modern name.
|
||||
CGSConnectionID _CGSDefaultConnection(void);
|
||||
CGSConnectionID CGSMainConnectionID(void);
|
||||
|
||||
|
||||
+1
-13
@@ -8,18 +8,6 @@ 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)
|
||||
@@ -68,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 {}
|
||||
|
||||
@@ -27,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.
|
||||
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!
|
||||
|
||||
**ProTip:** 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user