Skip to content

Commit

Permalink
Add a preference to copy color after picking (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus authored May 15, 2021
1 parent 167d9b3 commit 2c32d93
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 37 deletions.
14 changes: 14 additions & 0 deletions Color Picker/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,19 @@ final class AppState: ObservableObject {
}
}

private func copyColorIfNeeded() {
switch Defaults[.colorFormatToCopyAfterPicking] {
case .none:
break
case .hex:
colorPanel.hexColorString.copyToPasteboard()
case .hsl:
colorPanel.hslColorString.copyToPasteboard()
case .rgb:
colorPanel.rgbColorString.copyToPasteboard()
}
}

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

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

Expand Down
21 changes: 21 additions & 0 deletions Color Picker/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import KeyboardShortcuts
extension Defaults.Keys {
static let showInMenuBar = Key<Bool>("showInMenuBar", default: false)
static let showColorSamplerOnOpen = Key<Bool>("showColorSamplerOnOpen", default: false)
static let colorFormatToCopyAfterPicking = Key<CopyColorFormat>("colorFormatToCopyAfterPicking", default: .none)
static let stayOnTop = Key<Bool>("stayOnTop", default: true)
static let uppercaseHexColor = Key<Bool>("uppercaseHexColor", default: false)
static let legacyColorSyntax = Key<Bool>("legacyColorSyntax", default: false)
Expand All @@ -13,3 +14,23 @@ extension Defaults.Keys {
extension KeyboardShortcuts.Name {
static let toggleWindow = Self("toggleWindow")
}

enum CopyColorFormat: String, Codable, CaseIterable {
case none
case hex
case hsl
case rgb

var title: String {
switch self {
case .none:
return "None"
case .hex:
return "Hex"
case .hsl:
return "HSL"
case .rgb:
return "RGB"
}
}
}
18 changes: 17 additions & 1 deletion Color Picker/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ private struct ShowInMenuBarSetting: View {
}
}

private struct CopyColorFormatSetting: View {
@Default(.colorFormatToCopyAfterPicking) private var colorFormat

var body: some View {
EnumPicker("Copy color after picking:", enumBinding: $colorFormat) { element, _ in
Text(element.title)
if element == .none {
Divider()
}
}
}
}

private struct KeyboardShortcutSetting: View {
@Default(.showInMenuBar) private var showInMenuBar

Expand Down Expand Up @@ -51,15 +64,18 @@ struct SettingsView: View {
var body: some View {
Form {
VStack(alignment: .leading) {
ShowInMenuBarSetting()
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)
CopyColorFormatSetting()
Divider()
.padding(.vertical)
KeyboardShortcutSetting()
Expand Down
185 changes: 149 additions & 36 deletions Color Picker/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ extension NSColor {

/// - Important: Ensure you use a compatible color space, otherwise it will just be black.
var hsla: HSLA {
let hsba = self.hsba
let hsba = hsba

var saturation = hsba.saturation * hsba.brightness
var lightness = (2.0 - hsba.saturation) * hsba.brightness
Expand Down Expand Up @@ -648,25 +648,25 @@ extension NSColor {

return string
case .hsl:
let hsla = self.hsla
let hsla = hsla
let hue = Int((hsla.hue * 360).rounded())
let saturation = Int((hsla.saturation * 100).rounded())
let lightness = Int((hsla.lightness * 100).rounded())
return String(format: "hsl(%ddeg %d%% %d%%)", hue, saturation, lightness)
case .rgb:
let rgba = self.rgba
let rgba = rgba
let red = Int((rgba.red * 0xFF).rounded())
let green = Int((rgba.green * 0xFF).rounded())
let blue = Int((rgba.blue * 0xFF).rounded())
return String(format: "rgb(%d %d %d)", red, green, blue)
case .hslLegacy:
let hsla = self.hsla
let hsla = hsla
let hue = Int((hsla.hue * 360).rounded())
let saturation = Int((hsla.saturation * 100).rounded())
let lightness = Int((hsla.lightness * 100).rounded())
return String(format: "hsl(%d, %d%%, %d%%)", hue, saturation, lightness)
case .rgbLegacy:
let rgba = self.rgba
let rgba = rgba
let red = Int((rgba.red * 0xFF).rounded())
let green = Int((rgba.green * 0xFF).rounded())
let blue = Int((rgba.blue * 0xFF).rounded())
Expand Down Expand Up @@ -732,37 +732,6 @@ extension DispatchQueue {
}


extension Binding where Value: Equatable {
/**
Get notified when the binding value changes to a different one.
Can be useful to manually update non-reactive properties.
```
Toggle(
"Foo",
isOn: $foo.onChange {
bar.isEnabled = $0
}
)
```
*/
func onChange(_ action: @escaping (Value) -> Void) -> Self {
.init(
get: { wrappedValue },
set: {
let oldValue = wrappedValue
wrappedValue = $0
let newValue = wrappedValue
if newValue != oldValue {
action(newValue)
}
}
)
}
}


extension Defaults {
final class Observable<Value: Codable>: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
Expand Down Expand Up @@ -1574,3 +1543,147 @@ extension NSPasteboard {
}
}
}


extension Binding where Value: CaseIterable & Equatable {
/**
```
enum Priority: String, CaseIterable {
case no
case low
case medium
case high
}
// …
Picker("Priority", selection: $priority.caseIndex) {
ForEach(Priority.allCases.indices) { priorityIndex in
Text(
Priority.allCases[priorityIndex].rawValue.capitalized
)
.tag(priorityIndex)
}
}
```
*/
var caseIndex: Binding<Value.AllCases.Index> {
.init(
get: { Value.allCases.firstIndex(of: wrappedValue)! },
set: {
wrappedValue = Value.allCases[$0]
}
)
}
}


/**
Useful in SwiftUI:
```
ForEach(persons.indexed(), id: \.1.id) { index, person in
// …
}
```
*/
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)

let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }

func index(after index: Index) -> Index {
base.index(after: index)
}

func index(before index: Index) -> Index {
base.index(before: index)
}

func index(_ index: Index, offsetBy distance: Int) -> Index {
base.index(index, offsetBy: distance)
}

subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}

extension RandomAccessCollection {
/**
Returns a sequence with a tuple of both the index and the element.
- Important: Use this instead of `.enumerated()`. See: https://khanlou.com/2017/03/you-probably-don%27t-want-enumerated/
*/
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}


/**
Create a `Picker` from an enum.
- Note: The enum must conform to `CaseIterable`.
```
enum EventIndicatorsInCalendar: String, Codable, CaseIterable {
case none
case one
case maxThree
var title: String {
switch self {
case .none:
return "None"
case .one:
return "Single Gray Dot"
case .maxThree:
return "Up To Three Colored Dots"
}
}
}
struct ContentView: View {
@Default(.indicateEventsInCalendar) private var indicator
var body: some View {
EnumPicker(
"Foo",
enumCase: $indicator
) { element, isSelected in
Text(element.title)
}
}
}
```
*/
struct EnumPicker<Enum, Label, Content>: View where Enum: CaseIterable & Equatable, Enum.AllCases.Index: Hashable, Label: View, Content: View {
let enumBinding: Binding<Enum>
let label: Label
@ViewBuilder let content: (Enum, Bool) -> Content

var body: some View {
Picker(selection: enumBinding.caseIndex, label: label) {
ForEach(Array(Enum.allCases).indexed(), id: \.0) { index, element in
content(element, element == enumBinding.wrappedValue)
.tag(index)
}
}
}
}

extension EnumPicker where Label == Text {
init<S>(
_ title: S,
enumBinding: Binding<Enum>,
@ViewBuilder content: @escaping (Enum, Bool) -> Content
) where S: StringProtocol {
self.enumBinding = enumBinding
self.label = Text(title)
self.content = content
}
}

0 comments on commit 2c32d93

Please sign in to comment.