Skip to content

Commit b62bc44

Browse files
author
nwoodfine
committed
Add custom color palette library with iCloud sync
Adds user-defined color palettes in a text format with support for all color formats (hex, rgb, hsl, hsb, lab, oklch). Palettes appear as swatch bars below color history and sync via iCloud Key-Value Store. - PaletteParser: INI-style [Name] headers with comma-separated colors - NSColor.fromColorString(): Reverse parsing for all supported formats - PaletteSyncManager: Bidirectional iCloud KVS sync with loop prevention - PaletteEditor: Monospaced text input with validation feedback - Up to 5 palettes, 20 colors each
1 parent 4dbbe64 commit b62bc44

14 files changed

Lines changed: 772 additions & 10 deletions

Pika.xcodeproj/project.pbxproj

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
1C7412E01A6EF6E6D516CEB6 /* ColorHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB487C20A8FA16610A2DE6C4 /* ColorHistory.swift */; };
1011
220D5E9428DB154300B6285E /* AppModeButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220D5E9328DB154300B6285E /* AppModeButtons.swift */; };
1112
220D5E9828DB158400B6285E /* AppModeToggleGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220D5E9728DB158400B6285E /* AppModeToggleGroup.swift */; };
1213
221600F925A62E5B00B8B7D9 /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 221600F825A62E5B00B8B7D9 /* IconImage.swift */; };
@@ -16,8 +17,22 @@
1617
22D28DB72862377F00FC7DD4 /* OverflowContentViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D28DB62862377F00FC7DD4 /* OverflowContentViewModifier.swift */; };
1718
22EF1D9B25B7AA18001102FA /* Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22EF1D9A25B7AA18001102FA /* Sequence.swift */; };
1819
22FE80B325BA0F820063759E /* KeyboardShortcutItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22FE80B225BA0F820063759E /* KeyboardShortcutItem.swift */; };
20+
3FB457874C3F741FC84DADD6 /* ColorHistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1804CBF1E38631A06B89C401 /* ColorHistoryManager.swift */; };
21+
68267E79E2A1817195C7C16F /* ColorHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB487C20A8FA16610A2DE6C4 /* ColorHistory.swift */; };
1922
C49A11482DB394F500EE7E80 /* APCACompliance.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49A11472DB394F500EE7E80 /* APCACompliance.swift */; };
2023
C49A11492DB394F500EE7E80 /* APCACompliance.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49A11472DB394F500EE7E80 /* APCACompliance.swift */; };
24+
B1C2D3E4F5061728394A5B6C /* ColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5C /* ColorPalette.swift */; };
25+
C1D2E3F4051627384A5B6C7D /* ColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5C /* ColorPalette.swift */; };
26+
B1C2D3E4F5061728394A5B6D /* PaletteSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5D /* PaletteSyncManager.swift */; };
27+
C1D2E3F4051627384A5B6C7E /* PaletteSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5D /* PaletteSyncManager.swift */; };
28+
B1C2D3E4F5061728394A5B6E /* ColorPaletteBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5E /* ColorPaletteBar.swift */; };
29+
C1D2E3F4051627384A5B6C7F /* ColorPaletteBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5E /* ColorPaletteBar.swift */; };
30+
B1C2D3E4F5061728394A5B6F /* ColorPalettes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5F /* ColorPalettes.swift */; };
31+
C1D2E3F4051627384A5B6C80 /* ColorPalettes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5F /* ColorPalettes.swift */; };
32+
B1C2D3E4F5061728394A5B70 /* PaletteEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B60 /* PaletteEditor.swift */; };
33+
C1D2E3F4051627384A5B6C81 /* PaletteEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B60 /* PaletteEditor.swift */; };
34+
D1E2F3A4B5C6D7E8F9A0B1C1 /* SwatchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E2F3A4B5C6D7E8F9A0B1C0 /* SwatchBar.swift */; };
35+
D1E2F3A4B5C6D7E8F9A0B1C2 /* SwatchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E2F3A4B5C6D7E8F9A0B1C0 /* SwatchBar.swift */; };
2136
EA0C525025AA729300AFF716 /* Visualisation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0C524F25AA729300AFF716 /* Visualisation.swift */; };
2237
EA0C526025AB5A2B00AFF716 /* NavigationMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0C525F25AB5A2B00AFF716 /* NavigationMenuItems.swift */; };
2338
EA0C526425AB5D1700AFF716 /* PikaWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0C526325AB5D1700AFF716 /* PikaWindow.swift */; };
@@ -129,14 +144,10 @@
129144
F8ABAC5D2EAAD0F0008CD152 /* ColorPickOverlayWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8ABAC5C2EAAD0F0008CD152 /* ColorPickOverlayWindow.swift */; };
130145
F8ABAC5E2EAAD0F0008CD152 /* ColorPickOverlayWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8ABAC5C2EAAD0F0008CD152 /* ColorPickOverlayWindow.swift */; };
131146
FC6B02943A97C369AEDA0D64 /* ColorHistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1804CBF1E38631A06B89C401 /* ColorHistoryManager.swift */; };
132-
68267E79E2A1817195C7C16F /* ColorHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB487C20A8FA16610A2DE6C4 /* ColorHistory.swift */; };
133-
3FB457874C3F741FC84DADD6 /* ColorHistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1804CBF1E38631A06B89C401 /* ColorHistoryManager.swift */; };
134-
1C7412E01A6EF6E6D516CEB6 /* ColorHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB487C20A8FA16610A2DE6C4 /* ColorHistory.swift */; };
135-
D1E2F3A4B5C6D7E8F9A0B1C1 /* SwatchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E2F3A4B5C6D7E8F9A0B1C0 /* SwatchBar.swift */; };
136-
D1E2F3A4B5C6D7E8F9A0B1C2 /* SwatchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E2F3A4B5C6D7E8F9A0B1C0 /* SwatchBar.swift */; };
137147
/* End PBXBuildFile section */
138148

139149
/* Begin PBXFileReference section */
150+
1804CBF1E38631A06B89C401 /* ColorHistoryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorHistoryManager.swift; sourceTree = "<group>"; };
140151
220D5E9328DB154300B6285E /* AppModeButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppModeButtons.swift; sourceTree = "<group>"; };
141152
220D5E9728DB158400B6285E /* AppModeToggleGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppModeToggleGroup.swift; sourceTree = "<group>"; };
142153
221600F825A62E5B00B8B7D9 /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = "<group>"; };
@@ -158,6 +169,13 @@
158169
C1BF64202C1AE53C004D33DD /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
159170
C1BF64212C1AE53C004D33DD /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Main.strings"; sourceTree = "<group>"; };
160171
C49A11472DB394F500EE7E80 /* APCACompliance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APCACompliance.swift; sourceTree = "<group>"; };
172+
CB487C20A8FA16610A2DE6C4 /* ColorHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorHistory.swift; sourceTree = "<group>"; };
173+
A1B2C3D4E5F60718293A4B5C /* ColorPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPalette.swift; sourceTree = "<group>"; };
174+
A1B2C3D4E5F60718293A4B5D /* PaletteSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteSyncManager.swift; sourceTree = "<group>"; };
175+
A1B2C3D4E5F60718293A4B5E /* ColorPaletteBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPaletteBar.swift; sourceTree = "<group>"; };
176+
A1B2C3D4E5F60718293A4B5F /* ColorPalettes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPalettes.swift; sourceTree = "<group>"; };
177+
A1B2C3D4E5F60718293A4B60 /* PaletteEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteEditor.swift; sourceTree = "<group>"; };
178+
D1E2F3A4B5C6D7E8F9A0B1C0 /* SwatchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwatchBar.swift; sourceTree = "<group>"; };
161179
EA0C524F25AA729300AFF716 /* Visualisation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Visualisation.swift; sourceTree = "<group>"; };
162180
EA0C525F25AB5A2B00AFF716 /* NavigationMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationMenuItems.swift; sourceTree = "<group>"; };
163181
EA0C526325AB5D1700AFF716 /* PikaWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PikaWindow.swift; sourceTree = "<group>"; };
@@ -217,9 +235,6 @@
217235
EAF100CC25C785C4006E1EC3 /* TouchBarVisual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBarVisual.swift; sourceTree = "<group>"; };
218236
F8ABAC592EAAD0DF008CD152 /* ColorPickOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickOverlay.swift; sourceTree = "<group>"; };
219237
F8ABAC5C2EAAD0F0008CD152 /* ColorPickOverlayWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickOverlayWindow.swift; sourceTree = "<group>"; };
220-
1804CBF1E38631A06B89C401 /* ColorHistoryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorHistoryManager.swift; sourceTree = "<group>"; };
221-
CB487C20A8FA16610A2DE6C4 /* ColorHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorHistory.swift; sourceTree = "<group>"; };
222-
D1E2F3A4B5C6D7E8F9A0B1C0 /* SwatchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwatchBar.swift; sourceTree = "<group>"; };
223238
/* End PBXFileReference section */
224239

225240
/* Begin PBXFrameworksBuildPhase section */
@@ -315,6 +330,7 @@
315330
EAD0B6D7259CED1D00FA2F67 /* Info.plist */,
316331
EAE2EB9B2D03697600FA9BC9 /* Info (Mac App Store).plist */,
317332
EA72BB8125A5334B008205E7 /* Metal */,
333+
D1E2F30415263748596A7B8C /* Models */,
318334
EAD0B6D8259CED1D00FA2F67 /* Pika.entitlements */,
319335
EAD0B6D1259CED1D00FA2F67 /* Preview Content */,
320336
EAD0B71A259D14C200FA2F67 /* Styles */,
@@ -358,6 +374,7 @@
358374
EACA8A44260501210064035C /* Exporter.swift */,
359375
EAD0B6F1259CF29300FA2F67 /* Eyedroppers.swift */,
360376
EA7B199525FBA08100E06D9D /* LoadColors.swift */,
377+
A1B2C3D4E5F60718293A4B5D /* PaletteSyncManager.swift */,
361378
EA0C526325AB5D1700AFF716 /* PikaWindow.swift */,
362379
22D28DB62862377F00FC7DD4 /* OverflowContentViewModifier.swift */,
363380
);
@@ -373,6 +390,8 @@
373390
EAD0B6F5259CF29300FA2F67 /* AppVersion.swift */,
374391
22903B0228294F49004BB9F0 /* ColorExampleRow.swift */,
375392
CB487C20A8FA16610A2DE6C4 /* ColorHistory.swift */,
393+
A1B2C3D4E5F60718293A4B5E /* ColorPaletteBar.swift */,
394+
A1B2C3D4E5F60718293A4B5F /* ColorPalettes.swift */,
376395
EA635DE025B4FC580014D91A /* ColorPickers.swift */,
377396
F8ABAC592EAAD0DF008CD152 /* ColorPickOverlay.swift */,
378397
EABAEADF284D50D1000716AE /* ComplianceButtons.swift */,
@@ -388,6 +407,7 @@
388407
EAA8AE1825B8EC070049299B /* KeyboardShortcutKey.swift */,
389408
EAD0B71B259D151400FA2F67 /* NavigationMenu.swift */,
390409
EA0C525F25AB5A2B00AFF716 /* NavigationMenuItems.swift */,
410+
A1B2C3D4E5F60718293A4B60 /* PaletteEditor.swift */,
391411
EAD0B6F4259CF29300FA2F67 /* PreferencesView.swift */,
392412
EA72BB8725A53750008205E7 /* SplashView.swift */,
393413
D1E2F3A4B5C6D7E8F9A0B1C0 /* SwatchBar.swift */,
@@ -399,6 +419,14 @@
399419
path = Views;
400420
sourceTree = "<group>";
401421
};
422+
D1E2F30415263748596A7B8C /* Models */ = {
423+
isa = PBXGroup;
424+
children = (
425+
A1B2C3D4E5F60718293A4B5C /* ColorPalette.swift */,
426+
);
427+
path = Models;
428+
sourceTree = "<group>";
429+
};
402430
EAD0B71A259D14C200FA2F67 /* Styles */ = {
403431
isa = PBXGroup;
404432
children = (
@@ -672,6 +700,11 @@
672700
FC6B02943A97C369AEDA0D64 /* ColorHistoryManager.swift in Sources */,
673701
68267E79E2A1817195C7C16F /* ColorHistory.swift in Sources */,
674702
D1E2F3A4B5C6D7E8F9A0B1C1 /* SwatchBar.swift in Sources */,
703+
B1C2D3E4F5061728394A5B6C /* ColorPalette.swift in Sources */,
704+
B1C2D3E4F5061728394A5B6D /* PaletteSyncManager.swift in Sources */,
705+
B1C2D3E4F5061728394A5B6E /* ColorPaletteBar.swift in Sources */,
706+
B1C2D3E4F5061728394A5B6F /* ColorPalettes.swift in Sources */,
707+
B1C2D3E4F5061728394A5B70 /* PaletteEditor.swift in Sources */,
675708
);
676709
runOnlyForDeploymentPostprocessing = 0;
677710
};
@@ -735,6 +768,11 @@
735768
3FB457874C3F741FC84DADD6 /* ColorHistoryManager.swift in Sources */,
736769
1C7412E01A6EF6E6D516CEB6 /* ColorHistory.swift in Sources */,
737770
D1E2F3A4B5C6D7E8F9A0B1C2 /* SwatchBar.swift in Sources */,
771+
C1D2E3F4051627384A5B6C7D /* ColorPalette.swift in Sources */,
772+
C1D2E3F4051627384A5B6C7E /* PaletteSyncManager.swift in Sources */,
773+
C1D2E3F4051627384A5B6C7F /* ColorPaletteBar.swift in Sources */,
774+
C1D2E3F4051627384A5B6C80 /* ColorPalettes.swift in Sources */,
775+
C1D2E3F4051627384A5B6C81 /* PaletteEditor.swift in Sources */,
738776
);
739777
runOnlyForDeploymentPostprocessing = 0;
740778
};

Pika/AppDelegate.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
1919
var preferencesWindow: NSWindow!
2020
var eyedroppers: Eyedroppers!
2121
let colorHistoryManager = ColorHistoryManager()
22+
/// Retained for the app's lifetime to keep the iCloud KVS observer alive.
23+
let paletteSyncManager = PaletteSyncManager()
2224

2325
var undoManager = UndoManager()
26+
private var cachedPaletteCount = 0
2427
private var hadColorHistory = false
2528

2629
override init() {
@@ -42,7 +45,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
4245
SwatchLayout.totalHeight(
4346
base: 230,
4447
hasHistory: hadColorHistory,
45-
paletteCount: 0
48+
paletteCount: cachedPaletteCount
4649
)
4750
}
4851

@@ -149,9 +152,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
149152
pikaWindow.contentView = NSHostingView(rootView: contentView)
150153
pikaTouchBarController = PikaTouchBarController(window: pikaWindow)
151154

155+
cachedPaletteCount = PaletteParser.countSections(Defaults[.paletteText])
152156
hadColorHistory = !Defaults[.colorHistory].isEmpty
153157
updateWindowSize(animate: false)
154158

159+
Defaults.observe(.paletteText) { [weak self] _ in
160+
DispatchQueue.main.async {
161+
guard let self = self else { return }
162+
self.cachedPaletteCount = PaletteParser.countSections(Defaults[.paletteText])
163+
self.updateWindowSize(animate: true)
164+
}
165+
}.tieToLifetime(of: self)
166+
155167
Defaults.observe(.colorHistory) { [weak self] _ in
156168
DispatchQueue.main.async {
157169
guard let self = self else { return }

Pika/Assets/en.lproj/Localizable.strings

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,9 @@
354354

355355
/* Color History */
356356
"color.history" = "Color History";
357+
358+
/* Custom Color Palettes */
359+
"palette.title" = "Custom Color Palettes";
360+
361+
/* Palette sync description */
362+
"palette.sync" = "Synced across all devices running Pika via iCloud";

Pika/Constants/Constants.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ enum PikaText {
281281
comment: "Show color overlay after picking"
282282
)
283283
static let textDuration = NSLocalizedString("preferences.overlay.duration", comment: "Duration:")
284+
285+
// Color Palettes
286+
static let textPalettesTitle = NSLocalizedString("palette.title", comment: "Custom Color Palettes")
287+
static let textPalettesSync = NSLocalizedString("palette.sync", comment: "Palette sync description")
284288
}
285289

286290
// swiftlint:enable trailing_comma

Pika/Constants/Defaults.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,5 @@ extension Defaults.Keys {
6767
static let showColorOverlay = Key<Bool>("showColorOverlay", default: true)
6868
static let colorOverlayDuration = Key<Double>("colorOverlayDuration", default: 2.0)
6969
static let colorHistory = Key<[String]>("colorHistory", default: [])
70+
static let paletteText = Key<String>("paletteText", default: "")
7071
}

0 commit comments

Comments
 (0)