Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ddfe5a0f3 | ||
|
|
0e60e12364 | ||
|
|
06e20d5594 |
@@ -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,10 +80,11 @@ extension AppDelegate: NSMenuDelegate {
|
||||
buildRecentPicks()
|
||||
|
||||
contextMenu.addItem(.separator())
|
||||
buildShowGridMenu()
|
||||
buildColorSpaceItem()
|
||||
buildColorFormatsMenu()
|
||||
buildMagnificationItem()
|
||||
buildConcentrationMenu()
|
||||
buildMagnificationMenu()
|
||||
buildFocusModeModifierMenu()
|
||||
buildFloatPrecisionSlider()
|
||||
buildShortcutMenuItem()
|
||||
buildLaunchAtLoginItem()
|
||||
@@ -82,6 +94,25 @@ extension AppDelegate: NSMenuDelegate {
|
||||
contextMenu.addItem(withTitle: "Quit \(APP_NAME)", action: #selector(quitApplication), keyEquivalent: "")
|
||||
}
|
||||
|
||||
// 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()
|
||||
@@ -106,7 +137,7 @@ extension AppDelegate: NSMenuDelegate {
|
||||
}
|
||||
|
||||
// A menu which allows the magnification level of the picker to be adjusted.
|
||||
private func buildMagnificationItem() {
|
||||
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: "")
|
||||
@@ -225,23 +256,23 @@ extension AppDelegate: NSMenuDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
PPState.shared.focusModeModifier = modifier
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.1.0</string>
|
||||
<string>1.2.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>4</string>
|
||||
<string>5</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@@ -21,14 +21,14 @@ class PPOverlayController: NSWindowController {
|
||||
|
||||
// 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 ? PPOverlayController.panelSizeLarge : PPOverlayController.panelSizeNormal
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class PPOverlayController: NSWindowController {
|
||||
startMonitoringEvents()
|
||||
} else {
|
||||
stopMonitoringEvents()
|
||||
concentrationMode = false
|
||||
focusMode = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,24 +106,24 @@ class PPOverlayController: NSWindowController {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -245,7 +245,7 @@ class PPOverlayController: NSWindowController {
|
||||
|
||||
// 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)
|
||||
@@ -256,43 +256,49 @@ class PPOverlayController: NSWindowController {
|
||||
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
|
||||
|
||||
// Convert the preview to the correct color space, and update the overlay.
|
||||
|
||||
// User has chosen a custom color space.
|
||||
if let colorSpace = getChosenColorSpace(point) {
|
||||
if colorSpace == zoomedImage.colorSpace {
|
||||
return updatePreview(zoomedImage, pixelSize, middlePosition)
|
||||
return updatePreview(zoomedImage, pixelSize, zoomedSize)
|
||||
} else if let image = zoomedImage.copy(colorSpace: colorSpace) {
|
||||
return updatePreview(image, pixelSize, middlePosition)
|
||||
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, middlePosition)
|
||||
return updatePreview(zoomedImage, pixelSize, zoomedSize)
|
||||
} else if let image = zoomedImage.copy(colorSpace: colorSpace) {
|
||||
return updatePreview(image, pixelSize, middlePosition)
|
||||
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, middlePosition)
|
||||
return updatePreview(zoomedImage, pixelSize, zoomedSize)
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePreview(_ image: CGImage, _ pixelSize: CGFloat, _ middlePosition: CGFloat) {
|
||||
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()
|
||||
|
||||
preview.layer?.contents = image
|
||||
preview.updateCrosshair(pixelSize, middlePosition, contrastingColor.cgColor)
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -25,6 +28,10 @@ class PPOverlayPreview: NSView, CALayerDelegate {
|
||||
crosshair.strokeColor = NSColor.black.cgColor
|
||||
crosshair.fillColor = nil
|
||||
layer?.addSublayer(crosshair)
|
||||
|
||||
grid.strokeColor = NSColor.gray.cgColor
|
||||
grid.fillColor = nil
|
||||
layer?.addSublayer(grid)
|
||||
}
|
||||
|
||||
// Update the crosshair with the correct color, position and size.
|
||||
@@ -36,4 +43,49 @@ class PPOverlayPreview: NSView, CALayerDelegate {
|
||||
crosshair.strokeColor = color
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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 .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()
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,12 @@ 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
|
||||
@@ -65,7 +68,7 @@ import CleanroomLogger
|
||||
|
||||
func resetState() {
|
||||
paschaModeEnabled = false
|
||||
concentrationModeModifier = .control
|
||||
focusModeModifier = .control
|
||||
activatingShortcut = nil
|
||||
chosenFormat = .genericHex
|
||||
floatPrecision = 3
|
||||
@@ -96,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)
|
||||
@@ -150,7 +153,7 @@ import CleanroomLogger
|
||||
|
||||
let json: JSON = [
|
||||
"paschaModeEnabled": paschaModeEnabled,
|
||||
"concentrationModeModifier": concentrationModeModifier.rawValue,
|
||||
"focusModeModifier": focusModeModifier.rawValue,
|
||||
"activatingShortcut": shortcutData,
|
||||
"magnificationLevel": magnificationLevel,
|
||||
"colorSpace": colorSpace ?? "",
|
||||
|
||||
@@ -26,7 +26,7 @@ After you've installed PixelPicker, it will live in your menu bar. You have many
|
||||
* 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!
|
||||
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.
|
||||
|
||||
@@ -36,10 +36,14 @@ 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**
|
||||
* How should the color be copied when you select it? Examples include CSS Hex: `#aabbcc`, or Java RGB: `new Color(158, 198, 117)`, etc.
|
||||
* **Focus Modifier**
|
||||
* Choose which modifier activates focus mode. When focus mode is active, the picker zooms closer in, and your mouse moves slower to help you pick the right pixel.
|
||||
* **Color Space**
|
||||
* Computer screens are complex things, and have various different 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).
|
||||
* **Show Grid**
|
||||
* Whether or not to show a grid around the pixels in the picker's preview.
|
||||
* **Float Precision**
|
||||
* This controls how many digits will come after the decimal point when using float-based color formats.
|
||||
* **Picker Shortcut**
|
||||
|
||||
Reference in New Issue
Block a user