diff --git a/Color Picker/App.swift b/Color Picker/App.swift index c8699d5..739075e 100644 --- a/Color Picker/App.swift +++ b/Color Picker/App.swift @@ -4,6 +4,15 @@ import SwiftUI NOTES: - The "com.apple.security.files.user-selected.read-only" entitlement is required by the "Open" menu in the "Color Palettes" pane. +TODO: + - test the action when not already running: + if NSApp.activationPolicy() == .prohibited { + SSApp.url.open() + } +- Show screenshot in App Store of the palette in menu bar. + +- Use `Color.Resolved` instead of `XColor` and `RGBA`. + TODO shortcut action ideas; - Convert color - Toggle color panel diff --git a/Color Picker/AppState.swift b/Color Picker/AppState.swift index e32e7d7..65e4889 100644 --- a/Color Picker/AppState.swift +++ b/Color Picker/AppState.swift @@ -61,17 +61,19 @@ final class AppState: ObservableObject { menu.addSeparator() if let colors = Defaults[.recentlyPickedColors].reversed().nilIfEmpty { - menu.addHeader("Recently Picked Colors") + menu.addHeader("Recently Picked") for color in colors { let menuItem = menu.addCallbackItem(color.stringRepresentation) { color.stringRepresentation.copyToPasteboard() } - menuItem.image = color.swatchImage + menuItem.image = color.swatchImage(size: 20) } } + addPalettes(menu) + menu.addSeparator() menu.addSettingsItem() @@ -136,6 +138,7 @@ final class AppState: ObservableObject { requestReview() if Defaults[.showInMenuBar] { + SSApp.isDockIconVisible = false colorPanel.close() } else { colorPanel.makeKeyAndOrderFront(nil) @@ -186,6 +189,10 @@ final class AppState: ObservableObject { if Defaults[.copyColorAfterPicking] { color.stringRepresentation.copyToPasteboard() } + + if NSEvent.modifiers == .shift { + pickColor() + } } } @@ -200,4 +207,44 @@ final class AppState: ObservableObject { func handleAppReopen() { handleMenuBarIcon() } + + private func addPalettes(_ menu: NSMenu) { + func createColorListMenu(menu: NSMenu, colorList: NSColorList) { + for (key, color) in colorList.keysAndColors { + let menuItem = menu.addCallbackItem(key) { + color.stringRepresentation.copyToPasteboard() + } + + // TODO: Cache the swatch image. + menuItem.image = color.swatchImage(size: Constants.swatchImageSize) + } + } + + if + let colorListName = Defaults[.stickyPaletteName], + let colorList = NSColorList(named: colorListName) + { + menu.addHeader(colorList.name ?? "") + createColorListMenu(menu: menu, colorList: colorList) + } + + guard let colorLists = NSColorList.all.withoutStickyPalette().nilIfEmpty else { + return + } + + menu.addHeader("Palettes") + + for colorList in colorLists { + guard let colorListName = colorList.name else { + continue + } + + menu.addItem(colorListName) + .withSubmenuLazy { + let menu = SSMenu() + createColorListMenu(menu: menu, colorList: colorList) + return menu + } + } + } } diff --git a/Color Picker/ColorPickerScreen.swift b/Color Picker/ColorPickerScreen.swift index f51062c..3cf7234 100644 --- a/Color Picker/ColorPickerScreen.swift +++ b/Color Picker/ColorPickerScreen.swift @@ -261,7 +261,6 @@ private struct BarView: View { @Environment(\.colorScheme) private var colorScheme @EnvironmentObject private var appState: AppState @StateObject private var pasteboardObserver = NSPasteboard.SimpleObservable(.general).stop() - @Default(.showInMenuBar) private var showInMenuBar var body: some View { HStack(spacing: 12) { @@ -287,7 +286,8 @@ private struct BarView: View { .keyboardShortcut("v", modifiers: [.shift, .command]) .disabled(NSColor.fromPasteboardGraceful(.general) == nil) RecentlyPickedColorsButton() - actionButton + PalettesButton() + ActionButton() Spacer() } // Cannot do this as the `Menu` buttons don't respect it. (macOS 13.2) @@ -307,8 +307,13 @@ private struct BarView: View { pasteboardObserver.stop() } } +} + +private struct ActionButton: View { + @EnvironmentObject private var appState: AppState + @Default(.showInMenuBar) private var showInMenuBar - private var actionButton: some View { + var body: some View { Menu { Button("Copy as HSB") { appState.colorPanel.color.hsbColorString.copyToPasteboard() @@ -354,9 +359,9 @@ private struct RecentlyPickedColorsButton: View { Label { Text(color.stringRepresentation) } icon: { - // We don't use SwiftUI here as it only supports showing an actual image. (macOS 12.0) + // We don't use SwiftUI here as it only supports showing an actual image. (macOS 14.0) // https://github.com/feedback-assistant/reports/issues/247 - Image(nsImage: color.swatchImage) + Image(nsImage: color.swatchImage(size: Constants.swatchImageSize)) } .labelStyle(.titleAndIcon) } @@ -379,3 +384,59 @@ private struct RecentlyPickedColorsButton: View { .help(recentlyPickedColors.isEmpty ? "No recently picked colors" : "Recently picked colors") } } + +private struct PalettesButton: View { + @EnvironmentObject private var appState: AppState + @StateObject private var updates = NotificationCenter.default.publisher(for: NSColorList.didChangeNotification).toListenOnlyObservableObject() + @Default(.stickyPaletteName) private var stickyPaletteName + + var body: some View { + let colorLists = NSColorList.all.withoutStickyPalette() + Menu { + if + let colorListName = stickyPaletteName, + let colorList = NSColorList(named: colorListName) + { + Section(colorListName) { + createColorList(colorList) + } + } + Section { + ForEach(colorLists, id: \.name) { colorList in + if let name = colorList.name { + Menu(name) { + createColorList(colorList) + } + } + } + } + } label: { + Image(systemName: "swatchpalette.fill") + .controlSize(.large) +// .padding(8) // Has no effect. (macOS 12.0.1) + .contentShape(.rectangle) + } + .menuIndicator(.hidden) + .padding(8) + .opacity(0.6) // Try to match the other buttons. + .disabled(colorLists.isEmpty) + .help(colorLists.isEmpty ? "No palettes" : "Palettes") + } + + private func createColorList(_ colorList: NSColorList) -> some View { + ForEach(Array(colorList.keysAndColors), id: \.key) { key, color in + Button { + appState.colorPanel.color = color + } label: { + Label { + Text(key) + } icon: { + // We don't use SwiftUI here as it only supports showing an actual image. (macOS 14.0) + // https://github.com/feedback-assistant/reports/issues/247 + Image(nsImage: color.swatchImage(size: Constants.swatchImageSize)) + } + .labelStyle(.titleAndIcon) + } + } + } +} diff --git a/Color Picker/Constants.swift b/Color Picker/Constants.swift index 5c47a41..db8bbcc 100644 --- a/Color Picker/Constants.swift +++ b/Color Picker/Constants.swift @@ -1,6 +1,10 @@ import Cocoa import KeyboardShortcuts +enum Constants { + static let swatchImageSize = 20.0 +} + extension Defaults.Keys { static let recentlyPickedColors = Key<[NSColor]>("recentlyPickedColors", default: []) @@ -18,6 +22,7 @@ extension Defaults.Keys { static let largerText = Key("largerText", default: false) static let copyColorAfterPicking = Key("copyColorAfterPicking", default: false) static let showAccessibilityColorName = Key("showAccessibilityColorName", default: false) + static let stickyPaletteName = Key("stickyPaletteName") } extension KeyboardShortcuts.Name { @@ -74,3 +79,19 @@ enum MenuBarItemClickAction: String, CaseIterable, Defaults.Serializable { } } } + +extension [NSColorList] { + func withoutStickyPalette() -> Self { + filter { + // Don't show sticky palette. + if + let colorListName = Defaults[.stickyPaletteName], + $0 == NSColorList(named: colorListName) + { + return false + } + + return true + } + } +} diff --git a/Color Picker/Events.swift b/Color Picker/Events.swift index c61833c..e28be13 100644 --- a/Color Picker/Events.swift +++ b/Color Picker/Events.swift @@ -13,7 +13,7 @@ extension AppState { } SSApp.isDockIconVisible = !$0.newValue - NSApp.activate(ignoringOtherApps: true) + SSApp.forceActivate() if !$0.newValue { LaunchAtLogin.isEnabled = false diff --git a/Color Picker/Info.plist b/Color Picker/Info.plist index 3751d46..6b76842 100644 --- a/Color Picker/Info.plist +++ b/Color Picker/Info.plist @@ -24,8 +24,6 @@ public.app-category.developer-tools LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) - LSUIElement - MDItemKeywords color,picker,system,pick,colour,colors,colours,sampler diff --git a/Color Picker/SettingsScreen.swift b/Color Picker/SettingsScreen.swift index f096555..eb9f9d6 100644 --- a/Color Picker/SettingsScreen.swift +++ b/Color Picker/SettingsScreen.swift @@ -22,6 +22,10 @@ struct SettingsScreen: View { } } +#Preview { + SettingsScreen() +} + private struct GeneralSettings: View { @Default(.showInMenuBar) private var showInMenuBar @@ -98,6 +102,7 @@ private struct AdvancedSettings: View { .help("Show the color picker loupe when the color picker window is shown.") Defaults.Toggle("Use larger text in text fields", key: .largerText) Defaults.Toggle("Show accessibility color name", key: .showAccessibilityColorName) + StickyPaletteSetting() } } } @@ -155,6 +160,24 @@ private struct ShownColorFormatsSetting: View { } } -#Preview { - SettingsScreen() +private struct StickyPaletteSetting: View { + @Default(.stickyPaletteName) private var stickyPalette + @Default(.showInMenuBar) private var showInMenuBar + + var body: some View { + Picker(selection: $stickyPalette) { + Text("None") + .tag(nil as String?) + Divider() + ForEach(NSColorList.all, id: \.self) { colorList in + if let name = colorList.name { + Text(name) + .tag(name as String?) + } + } + } label: { + Text("Sticky palette") + Text(showInMenuBar ? "Palette to show at the top-level of the menu bar menu and at the top of the palette menu in the color picker window" : "Palette to show at the top of the palette menu") + } + } } diff --git a/Color Picker/Utilities.swift b/Color Picker/Utilities.swift index e3c332a..6347c63 100644 --- a/Color Picker/Utilities.swift +++ b/Color Picker/Utilities.swift @@ -3,6 +3,7 @@ import Combine import Carbon import StoreKit import Defaults +import UniformTypeIdentifiers #if !APP_EXTENSION import Sentry @@ -75,13 +76,13 @@ extension NSColor { #endif extension NSColor { - var swatchImage: NSImage { + func swatchImage(size: Double) -> NSImage { .color( self, - size: CGSize(width: 16, height: 16), - borderWidth: 1, + size: CGSize(width: size, height: size), + borderWidth: (NSScreen.main?.backingScaleFactor ?? 2) > 1 ? 0.5 : 1, borderColor: (SSApp.isDarkMode ? NSColor.white : .black).withAlphaComponent(0.2), - cornerRadius: 4 + cornerRadius: 5 ) } } @@ -92,9 +93,9 @@ extension NSColor { */ -#if canImport(AppKit) +#if os(macOS) typealias XColor = NSColor -#elseif canImport(UIKit) +#else typealias XColor = UIColor #endif @@ -184,6 +185,28 @@ enum SSApp { } +extension SSApp { + // @MainActor + static func forceActivate() { + if #available(macOS 14, *) { + NSApp.yieldActivation(toApplicationWithBundleIdentifier: idString) + NSApp.activate() + } else { + NSApp.activate(ignoringOtherApps: true) + } + } + + @MainActor + static func activateIfAccessory() { + guard NSApp.activationPolicy() == .accessory else { + return + } + + forceActivate() + } +} + + extension SSApp { @MainActor static var swiftUIMainWindow: NSWindow? { @@ -200,9 +223,7 @@ extension SSApp { static func showSettingsWindow() { // Run in the next runloop so it doesn't conflict with SwiftUI if run at startup. DispatchQueue.main.async { - if NSApp.activationPolicy() == .accessory { - NSApp.activate(ignoringOtherApps: true) - } + SSApp.activateIfAccessory() if #available(macOS 14, *) { let menuItem = NSApp.mainMenu?.items.first?.submenu?.item(withTitle: "Settings…") @@ -239,6 +260,11 @@ extension SSApp { } +extension UTType { + static var adobeSwatchExchange: Self { .init(filenameExtension: "ase", conformingTo: .data)! } +} + + enum Device { static let osVersion: String = { let os = ProcessInfo.processInfo.operatingSystemVersion @@ -506,12 +532,12 @@ extension NSView { extension NSColor { var rgb: Colors.RGB { - #if canImport(AppKit) + #if os(macOS) guard let color = usingColorSpace(.extendedSRGB) else { assertionFailure("Unsupported color space") return .init(red: 0, green: 0, blue: 0, alpha: 0) } - #elseif canImport(UIKit) + #else let color = self #endif @@ -570,12 +596,12 @@ extension NSColor { } var hsb: HSB { - #if canImport(AppKit) + #if os(macOS) guard let color = usingColorSpace(.extendedSRGB) else { assertionFailure("Unsupported color space") return HSB(0, 0, 0, 0) } - #elseif canImport(UIKit) + #else let color = self #endif @@ -854,6 +880,7 @@ extension NSColor { ) } + // TODO: Parse alpha hex color. convenience init?(hexString: String, alpha: Double = 1) { var string = hexString @@ -874,6 +901,7 @@ extension NSColor { /** - Important: Don't forget to convert it to the correct color space first. + - Note: It respects the opacity of the color. ``` NSColor(hexString: "#fefefe")!.hex @@ -881,7 +909,7 @@ extension NSColor { ``` */ var hex: Int { - #if canImport(AppKit) + #if os(macOS) guard numberOfComponents == 4 else { assertionFailure() return 0x0 @@ -891,12 +919,14 @@ extension NSColor { let red = Int((redComponent * 0xFF).rounded()) let green = Int((greenComponent * 0xFF).rounded()) let blue = Int((blueComponent * 0xFF).rounded()) + let opacity = Int((alphaComponent * 0xFF).rounded()) - return red << 16 | green << 8 | blue + return opacity << 24 | red << 16 | green << 8 | blue } /** - Important: Don't forget to convert it to the correct color space first. + - Note: It includes the opacity of the color if not `1`. ``` NSColor(hexString: "#fefefe")!.hexString @@ -904,7 +934,11 @@ extension NSColor { ``` */ var hexString: String { - String(format: "#%06x", hex) + if alphaComponent < 1 { + String(format: "#%08x", hex) + } else { + String(format: "#%06x", hex & 0xFFFFFF) // Masking to remove the alpha portion for full opacity + } } } @@ -1363,15 +1397,180 @@ extension NSMenuItem { } +extension NSMenuItem { + /** + The menu is only created when it's enabled. + + ``` + menu.addItem("Foo") + .withSubmenu(createCalendarEventMenu(with: event)) + ``` + */ + @discardableResult + func withSubmenu(_ menu: @autoclosure () -> NSMenu) -> Self { + submenu = isEnabled ? menu() : NSMenu() + return self + } + + /** + The menu is only created when it's enabled. + + ``` + menu + .addItem("Foo") + .withSubmenu { menu in + + } + ``` + */ + @discardableResult + func withSubmenu(_ menuBuilder: (NSMenu) -> NSMenu) -> Self { + withSubmenu(menuBuilder(NSMenu())) + } + + /** + The menu is only created when it's enabled and it's created only when it's being shown. + + ``` + menu.addItem("Foo") + .withSubmenuLazy { [self] in + createCalendarEventMenu(with: event) + } + ``` + + - Note: You cannot use any events like `.onOpenClose` on the given menu as the menu is created lazily. + */ + @discardableResult + func withSubmenuLazy( + _ menu: @escaping () -> NSMenu, + onOpenClose: ((Bool) -> Void)? = nil + ) -> Self { + let emptyMenu = SSMenu() + submenu = emptyMenu + + if isEnabled { + emptyMenu.isOpenPublisher.sink { isOpen in + onOpenClose?(isOpen) + + guard + isOpen, + emptyMenu.items.isEmpty + else { + return + } + + let menu = menu() + let items = menu.items + menu.items.removeAll() + emptyMenu.items = items + } + .store(forTheLifetimeOf: self) + } + + return self + } +} + + +final class SSMenu: NSMenu, NSMenuDelegate { + private let isOpenSubject = CurrentValueSubject(false) + + private(set) var isOpen = false + + let isOpenPublisher: AnyPublisher + + + override init(title: String) { + self.isOpenPublisher = isOpenSubject.eraseToAnyPublisher() + super.init(title: title) + self.delegate = self + self.autoenablesItems = false + } + + @available(*, unavailable) + required init(coder decoder: NSCoder) { + fatalError() // swiftlint:disable:this fatal_error_message + } + + func menuWillOpen(_ menu: NSMenu) { + isOpen = true + isOpenSubject.send(true) + } + + func menuDidClose(_ menu: NSMenu) { + isOpen = false + isOpenSubject.send(false) + } +} + + +enum AssociationPolicy { + case assign + case retainNonatomic + case copyNonatomic + case retain + case copy + + var rawValue: objc_AssociationPolicy { + switch self { + case .assign: + .OBJC_ASSOCIATION_ASSIGN + case .retainNonatomic: + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + case .copyNonatomic: + .OBJC_ASSOCIATION_COPY_NONATOMIC + case .retain: + .OBJC_ASSOCIATION_RETAIN + case .copy: + .OBJC_ASSOCIATION_COPY + } + } +} + + +final class ObjectAssociation { + private let defaultValue: Value + private let policy: AssociationPolicy + + init(defaultValue: Value, policy: AssociationPolicy = .retainNonatomic) { + self.defaultValue = defaultValue + self.policy = policy + } + + subscript(index: AnyObject) -> Value { + get { + objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as? Value ?? defaultValue + } + set { + objc_setAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque(), newValue, policy.rawValue) + } + } +} + + +extension AnyCancellable { + private enum AssociatedKeys { + static let cancellables = ObjectAssociation>(defaultValue: []) + } + + /** + Stores this AnyCancellable for the lifetime of the given `object`. + */ + func store(forTheLifetimeOf object: AnyObject) { + store(in: &AssociatedKeys.cancellables[object]) + } +} + + extension NSWindow { func toggle() { - if isVisible, isKeyWindow { + if + isVisible, + isKeyWindow + { performClose(nil) } else { - if NSApp.activationPolicy() == .accessory { - NSApp.activate(ignoringOtherApps: true) - } - + SSApp.activateIfAccessory() makeKeyAndOrderFront(nil) } } @@ -1424,7 +1623,118 @@ extension CallbackMenuItem: NSMenuItemValidation { } +extension NSMenuItem { + convenience init( + _ title: String, + key: String = "", + keyModifiers: NSEvent.ModifierFlags? = nil, + isEnabled: Bool = true, + isChecked: Bool = false, + isHidden: Bool = false + ) { + self.init(title: title, action: nil, keyEquivalent: key) + self.isEnabled = isEnabled + self.isChecked = isChecked + self.isHidden = isHidden + + if let keyModifiers { + self.keyEquivalentModifierMask = keyModifiers + } + } + + convenience init( + _ attributedTitle: NSAttributedString, + key: String = "", + keyModifiers: NSEvent.ModifierFlags? = nil, + isEnabled: Bool = true, + isChecked: Bool = false, + isHidden: Bool = false + ) { + self.init( + "", + key: key, + keyModifiers: keyModifiers, + isEnabled: isEnabled, + isChecked: isChecked, + isHidden: isHidden + ) + self.attributedTitle = attributedTitle + } + + var isChecked: Bool { + get { state == .on } + set { + state = newValue ? .on : .off + } + } +} + + extension NSMenu { + @discardableResult + func add(_ menuItem: NSMenuItem) -> NSMenuItem { + addItem(menuItem) + return menuItem + } + + @discardableResult + func addDisabled(_ title: String) -> NSMenuItem { + let menuItem = NSMenuItem(title) + menuItem.isEnabled = false + addItem(menuItem) + return menuItem + } + + @discardableResult + func addDisabled(_ attributedTitle: NSAttributedString) -> NSMenuItem { + let menuItem = NSMenuItem(attributedTitle) + menuItem.isEnabled = false + addItem(menuItem) + return menuItem + } + + @discardableResult + func addItem( + _ title: String, + key: String = "", + keyModifiers: NSEvent.ModifierFlags? = nil, + isEnabled: Bool = true, + isChecked: Bool = false, + isHidden: Bool = false + ) -> NSMenuItem { + let menuItem = NSMenuItem( + title, + key: key, + keyModifiers: keyModifiers, + isEnabled: isEnabled, + isChecked: isChecked, + isHidden: isHidden + ) + addItem(menuItem) + return menuItem + } + + @discardableResult + func addItem( + _ attributedTitle: NSAttributedString, + key: String = "", + keyModifiers: NSEvent.ModifierFlags? = nil, + isEnabled: Bool = true, + isChecked: Bool = false, + isHidden: Bool = false + ) -> NSMenuItem { + let menuItem = NSMenuItem( + attributedTitle, + key: key, + keyModifiers: keyModifiers, + isEnabled: isEnabled, + isChecked: isChecked, + isHidden: isHidden + ) + addItem(menuItem) + return menuItem + } + @discardableResult func addCallbackItem( _ title: String, @@ -1465,7 +1775,12 @@ extension NSMenu { @discardableResult func addHeader(_ title: String, hasSeparatorAbove: Bool = true) -> NSMenuItem { - addHeader(title.toNSAttributedString, hasSeparatorAbove: hasSeparatorAbove) + // Doesn't work yet. +// if #available(macOS 14, *) { +// .sectionHeader(title: title) +// } else { + addHeader(title.toNSAttributedString, hasSeparatorAbove: hasSeparatorAbove) +// } } @discardableResult @@ -2939,7 +3254,7 @@ extension OperatingSystem { return false #else - return false + false #endif }() @@ -2954,9 +3269,64 @@ extension OperatingSystem { return false #else - return false + false #endif }() } typealias OS = OperatingSystem + + +extension Sequence where Element: Hashable { + func removingDuplicates() -> [Element] { + var seen = Set() + return filter { seen.insert($0).inserted } + } +} + +extension Sequence where Element: Equatable { + func removingDuplicates() -> [Element] { + reduce(into: []) { result, element in + if !result.contains(element) { + result.append(element) + } + } + } +} + + +extension NSColorList { + static var all: [NSColorList] { + availableColorLists + .filter { !$0.allKeys.isEmpty } + // `availableColorLists` returns duplicates after editing a palette, for example, adding a color to it. + .removingDuplicates() + } + + var colors: [NSColor] { + allKeys.compactMap { color(withKey: $0) } + } + + var keysAndColors: [NSColorList.Name: NSColor] { + .init(zip(allKeys, colors)) { first, _ in first } + } +} + + +final class ListenOnlyPublisherObservable: ObservableObject { + let objectWillChange = ObservableObjectPublisher() + private var cancellable: AnyCancellable? + + init(for publisher: some Publisher) { + self.cancellable = publisher.receive(on: DispatchQueue.main).sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] _ in + self?.objectWillChange.send() + } + ) + } +} + +extension Publisher { + func toListenOnlyObservableObject() -> ListenOnlyPublisherObservable { .init(for: self) } +} diff --git a/Color Picker/WelcomeScreen.swift b/Color Picker/WelcomeScreen.swift index e54ee05..384a037 100644 --- a/Color Picker/WelcomeScreen.swift +++ b/Color Picker/WelcomeScreen.swift @@ -10,7 +10,7 @@ extension AppState { Defaults[.showAccessibilityColorName] = true } - NSApp.activate(ignoringOtherApps: true) + SSApp.activateIfAccessory() NSAlert.showModal( title: "Welcome to Color Picker!", diff --git a/app-store-description.txt b/app-store-description.txt index 3ab2d57..b5ad5db 100644 --- a/app-store-description.txt +++ b/app-store-description.txt @@ -11,6 +11,7 @@ Pick colors from anywhere using the built-in color picker. - Recently picked colors - Shortcuts support - Hide menu bar icon +- Palettes ■ Tip diff --git a/readme.md b/readme.md index ca28e11..42b482c 100644 --- a/readme.md +++ b/readme.md @@ -40,11 +40,13 @@ A special version for users that cannot access the App Store. It won't receive a - Recently picked colors - Shortcuts support - Hide menu bar icon +- Palettes ## Tips - Press the Space key while using the color sampler to show the RGB values. The color sampler is a system component and it can unfortunately not show other kinds of color values. - Press the Option key when copying the Hex color to invert whether to include `#`. +- Press the Shift key while selecting a color using the color sampler to prevent it from disappearing after your selection. ## Keyboard shortcuts @@ -87,6 +89,16 @@ Note that the LCH color is [currently clamped to sRGB range](https://github.com/ That is because the default color space in the picker is [Display P3](https://en.wikipedia.org/wiki/DCI-P3), which is [part of CSS Color 4](https://drafts.csswg.org/css-color-4/#valdef-color-display-p3), but the color space used for the legacy CSS color formats is [sRGB](https://en.wikipedia.org/wiki/SRGB) (browsers are starting to [handle color spaces](https://css-tricks.com/the-expanding-gamut-of-color-on-the-web/) but they are not all there yet). +#### How do I use palettes? + +You can manage palettes by selecting the third tab in the window toolbar. + +The fastest way to add a color to a palette is to paste a Hex color value into the app and then click the `+` button in the palette. You can also drag and drop a color into the palette from anywhere. + +Palettes can be accessed both from the app and the menu bar icon (if enabled). You can even access them in other apps that use the system color picker. + +You can find palettes on [Coolors](https://coolors.co/palettes/trending). + #### How do I change the color space? Right-click the color wheel. You probably want to select “sRGB”.