Skip to content

Commit

Permalink
Add a customizable global keyboard shortcut for picking color
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed May 29, 2021
1 parent e035c6d commit 34ada45
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 41 deletions.
32 changes: 25 additions & 7 deletions Color Picker/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,24 @@ final class AppState: ObservableObject {
)

DispatchQueue.main.async { [self] in
// Make the invisible native SwitUI window not block access to the desktop.
NSApp.windows.first?.ignoresMouseEvents = true
didLaunch()
}
}

setUpEvents()
showWelcomeScreenIfNeeded()
private func didLaunch() {
// Make the invisible native SwitUI window not block access to the desktop.
NSApp.windows.first?.ignoresMouseEvents = true

// We hide the “View” menu as there's a macOS bug where it sometimes enables even though it doesn't work and then causes a crash when clicked.
NSApp.mainMenu?.item(withTitle: "View")?.isHidden = true
}
// We hide the “View” menu as there's a macOS bug where it sometimes enables even though it doesn't work and then causes a crash when clicked.
NSApp.mainMenu?.item(withTitle: "View")?.isHidden = true

setUpEvents()
showWelcomeScreenIfNeeded()
requestReview()

#if DEBUG
// SSApp.showSettingsWindow()
#endif
}

private func setUpEvents() {
Expand Down Expand Up @@ -116,6 +125,10 @@ final class AppState: ObservableObject {
}
.storeForever()

KeyboardShortcuts.onKeyUp(for: .pickColor) { [weak self] in
self?.pickColor()
}

KeyboardShortcuts.onKeyUp(for: .toggleWindow) { [weak self] in
self?.colorPanel.toggle()
}
Expand All @@ -136,6 +149,10 @@ final class AppState: ObservableObject {
}
}

private func requestReview() {
SSApp.requestReviewAfterBeingCalledThisManyTimes([10, 200, 1000])
}

func pickColor() {
NSColorSampler().show { [weak self] in
guard
Expand All @@ -147,6 +164,7 @@ final class AppState: ObservableObject {

self.colorPanel.color = color
self.copyColorIfNeeded()
self.requestReview()
}
}

Expand Down
1 change: 1 addition & 0 deletions Color Picker/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ extension Defaults.Keys {
}

extension KeyboardShortcuts.Name {
static let pickColor = Self("pickColor")
static let toggleWindow = Self("toggleWindow")
}

Expand Down
110 changes: 76 additions & 34 deletions Color Picker/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,54 +37,96 @@ private struct CopyColorFormatSetting: View {
}
}

private struct KeyboardShortcutSetting: View {
private struct GeneralSettings: View {
var body: some View {
VStack(alignment: .leading) {
LaunchAtLogin.Toggle()
ShowInMenuBarSetting()
Defaults.Toggle("Stay on top", key: .stayOnTop)
.help("Make the color picker window stay on top of all other windows.")
Defaults.Toggle("Uppercase Hex color", key: .uppercaseHexColor)
Defaults.Toggle("Use legacy syntax for HSL and RGB", key: .legacyColorSyntax)
.help("Use the legacy “hsl(198, 28%, 50%)” syntax instead of the modern “hsl(198deg 28% 50%)” syntax. This setting is meant for users that need to support older browsers. All modern browsers support the modern syntax.")
}
.padding()
.padding()
.frame(width: 380)
.windowLevel(.floating + 1) // Ensure it's always above the color picker.
}
}

private struct ShortcutsSettings: View {
@Default(.showInMenuBar) private var showInMenuBar
private let maxWidth: CGFloat = 100

var body: some View {
HStack(alignment: .firstTextBaseline) {
Text("Toggle Window:")
.respectDisabled()
KeyboardShortcuts.Recorder(for: .toggleWindow)
VStack {
HStack(alignment: .firstTextBaseline) {
Text("Pick color:")
.respectDisabled()
.frame(width: maxWidth, alignment: .trailing)
KeyboardShortcuts.Recorder(for: .pickColor)
}
.accessibilityElement(children: .combine)
.padding(.bottom, 8)
HStack(alignment: .firstTextBaseline) {
Text("Toggle window:")
.respectDisabled()
.frame(width: maxWidth, alignment: .trailing)
KeyboardShortcuts.Recorder(for: .toggleWindow)
}
.accessibilityElement(children: .combine)
.disabled(!showInMenuBar)
.overlay(
showInMenuBar
? nil
: Text("Requires “Show in menu bar” to be enabled.")
.font(.system(size: 10))
.foregroundColor(.secondary)
.offset(y: 20),
alignment: .bottom
)
.padding(.bottom, showInMenuBar ? 0 : 20)
}
.disabled(!showInMenuBar)
.overlay(
showInMenuBar
? nil
: Text("Requires “Show in menu bar” to be enabled.")
.font(.system(size: 10))
.foregroundColor(.secondary)
.offset(y: 20),
alignment: .bottomLeading
)
.padding(.bottom, showInMenuBar ? 0 : 20)
.padding()
.padding()
.padding(.vertical)
.offset(x: -10)
}
}

struct SettingsView: View {
private struct AdvancedSettings: View {
var body: some View {
Form {
VStack(alignment: .leading) {
VStack(alignment: .leading) {
LaunchAtLogin.Toggle()
ShowInMenuBarSetting()
Defaults.Toggle("Stay on top", key: .stayOnTop)
.help("Make the color picker window stay on top of all other windows.")
Defaults.Toggle("Show color sampler when opening window", key: .showColorSamplerOnOpen)
.help("Show the color picker loupe when the color picker window is shown.")
Defaults.Toggle("Uppercase Hex color", key: .uppercaseHexColor)
Defaults.Toggle("Use legacy syntax for HSL and RGB", key: .legacyColorSyntax)
.help("Use the legacy “hsl(198, 28%, 50%)” syntax instead of the modern “hsl(198deg 28% 50%)” syntax. This setting is meant for users that need to support older browsers. All modern browsers support the modern syntax.")
Divider()
.padding(.vertical)
}
.padding()
.padding(.horizontal)
Divider()
VStack(alignment: .leading) {
CopyColorFormatSetting()
Divider()
.padding(.vertical)
KeyboardShortcutSetting()
}
.padding()
.padding(.horizontal)
}
.padding()
.padding()
.frame(width: 380)
.windowLevel(.floating + 1) // Ensure it's always above the color picker.
.padding(.vertical)
}
}

struct SettingsView: View {
var body: some View {
TabView {
GeneralSettings()
.settingsTabItem(.general)
ShortcutsSettings()
.settingsTabItem(.shortcuts)
AdvancedSettings()
.settingsTabItem(.advanced)
}
.frame(width: 400)
.windowLevel(.modalPanel)
}
}

Expand Down
64 changes: 64 additions & 0 deletions Color Picker/Utilities.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import SwiftUI
import Combine
import Carbon
import StoreKit
import Defaults
import Regex

Expand Down Expand Up @@ -2048,3 +2049,66 @@ extension Colors {
)
}
}


enum SettingsTabType {
case general
case advanced
case shortcuts

fileprivate var label: some View {
switch self {
case .general:
return Label("General", systemImage: "gearshape")
case .advanced:
return Label("Advanced", systemImage: "gearshape.2")
case .shortcuts:
return Label("Shortcuts", systemImage: "command")
}
}
}

extension View {
/// Make the view a settings tab of the given type.
func settingsTabItem(_ type: SettingsTabType) -> some View {
tabItem { type.label }
}
}


extension Numeric {
mutating func increment(by value: Self = 1) -> Self {
self += value
return self
}

mutating func decrement(by value: Self = 1) -> Self {
self -= value
return self
}

func incremented(by value: Self = 1) -> Self {
self + value
}

func decremented(by value: Self = 1) -> Self {
self - value
}
}


extension SSApp {
private static let key = Defaults.Key("SSApp_requestReview", default: 0)

/// Requests a review only after this method has been called the given amount of times.
static func requestReviewAfterBeingCalledThisManyTimes(_ counts: [Int]) {
guard
!SSApp.isFirstLaunch,
counts.contains(Defaults[key].increment())
else {
return
}

SKStoreReviewController.requestReview()
}
}

0 comments on commit 34ada45

Please sign in to comment.