Skip to content

Commit

Permalink
Show palettes in the menu bar menu
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed Oct 21, 2023
1 parent 69a9714 commit 88ab759
Show file tree
Hide file tree
Showing 11 changed files with 579 additions and 37 deletions.
9 changes: 9 additions & 0 deletions Color Picker/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 49 additions & 2 deletions Color Picker/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -136,6 +138,7 @@ final class AppState: ObservableObject {
requestReview()

if Defaults[.showInMenuBar] {
SSApp.isDockIconVisible = false
colorPanel.close()
} else {
colorPanel.makeKeyAndOrderFront(nil)
Expand Down Expand Up @@ -186,6 +189,10 @@ final class AppState: ObservableObject {
if Defaults[.copyColorAfterPicking] {
color.stringRepresentation.copyToPasteboard()
}

if NSEvent.modifiers == .shift {
pickColor()
}
}
}

Expand All @@ -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 ?? "<Unnamed>")
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
}
}
}
}
71 changes: 66 additions & 5 deletions Color Picker/ColorPickerScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
}
}
}
21 changes: 21 additions & 0 deletions Color Picker/Constants.swift
Original file line number Diff line number Diff line change
@@ -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: [])

Expand All @@ -18,6 +22,7 @@ extension Defaults.Keys {
static let largerText = Key<Bool>("largerText", default: false)
static let copyColorAfterPicking = Key<Bool>("copyColorAfterPicking", default: false)
static let showAccessibilityColorName = Key<Bool>("showAccessibilityColorName", default: false)
static let stickyPaletteName = Key<String?>("stickyPaletteName")
}

extension KeyboardShortcuts.Name {
Expand Down Expand Up @@ -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
}
}
}
2 changes: 1 addition & 1 deletion Color Picker/Events.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extension AppState {
}

SSApp.isDockIconVisible = !$0.newValue
NSApp.activate(ignoringOtherApps: true)
SSApp.forceActivate()

if !$0.newValue {
LaunchAtLogin.isEnabled = false
Expand Down
2 changes: 0 additions & 2 deletions Color Picker/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>
<true/>
<key>MDItemKeywords</key>
<string>color,picker,system,pick,colour,colors,colours,sampler</string>
</dict>
Expand Down
27 changes: 25 additions & 2 deletions Color Picker/SettingsScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ struct SettingsScreen: View {
}
}

#Preview {
SettingsScreen()
}

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

Expand Down Expand Up @@ -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()
}
}
}
Expand Down Expand Up @@ -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")
}
}
}
Loading

0 comments on commit 88ab759

Please sign in to comment.