Skip to content

Commit 4dbbe64

Browse files
author
nwoodfine
committed
Add persistent color history bar
Addresses #139 Adds a color history bar below the footer showing recently picked colors. Tapping a swatch applies it as the foreground and copies to clipboard. - ColorHistoryManager: MRU list with immediate and debounced recording - SwatchBar: Reusable swatch component (shared with future palette feature) - Refactored system color picker to use togglePicker() directly - Dynamic window resizing when history appears/disappears
1 parent 737dba2 commit 4dbbe64

19 files changed

Lines changed: 338 additions & 46 deletions

Pika.xcodeproj/project.pbxproj

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@
128128
F8ABAC5B2EAAD0DF008CD152 /* ColorPickOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8ABAC592EAAD0DF008CD152 /* ColorPickOverlay.swift */; };
129129
F8ABAC5D2EAAD0F0008CD152 /* ColorPickOverlayWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8ABAC5C2EAAD0F0008CD152 /* ColorPickOverlayWindow.swift */; };
130130
F8ABAC5E2EAAD0F0008CD152 /* ColorPickOverlayWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8ABAC5C2EAAD0F0008CD152 /* ColorPickOverlayWindow.swift */; };
131+
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 */; };
131137
/* End PBXBuildFile section */
132138

133139
/* Begin PBXFileReference section */
@@ -211,6 +217,9 @@
211217
EAF100CC25C785C4006E1EC3 /* TouchBarVisual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBarVisual.swift; sourceTree = "<group>"; };
212218
F8ABAC592EAAD0DF008CD152 /* ColorPickOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickOverlay.swift; sourceTree = "<group>"; };
213219
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>"; };
214223
/* End PBXFileReference section */
215224

216225
/* Begin PBXFrameworksBuildPhase section */
@@ -344,6 +353,7 @@
344353
isa = PBXGroup;
345354
children = (
346355
EA7B199B25FBA0E600E06D9D /* ClosestVector.swift */,
356+
1804CBF1E38631A06B89C401 /* ColorHistoryManager.swift */,
347357
F8ABAC5C2EAAD0F0008CD152 /* ColorPickOverlayWindow.swift */,
348358
EACA8A44260501210064035C /* Exporter.swift */,
349359
EAD0B6F1259CF29300FA2F67 /* Eyedroppers.swift */,
@@ -362,6 +372,7 @@
362372
220D5E9728DB158400B6285E /* AppModeToggleGroup.swift */,
363373
EAD0B6F5259CF29300FA2F67 /* AppVersion.swift */,
364374
22903B0228294F49004BB9F0 /* ColorExampleRow.swift */,
375+
CB487C20A8FA16610A2DE6C4 /* ColorHistory.swift */,
365376
EA635DE025B4FC580014D91A /* ColorPickers.swift */,
366377
F8ABAC592EAAD0DF008CD152 /* ColorPickOverlay.swift */,
367378
EABAEADF284D50D1000716AE /* ComplianceButtons.swift */,
@@ -379,6 +390,7 @@
379390
EA0C525F25AB5A2B00AFF716 /* NavigationMenuItems.swift */,
380391
EAD0B6F4259CF29300FA2F67 /* PreferencesView.swift */,
381392
EA72BB8725A53750008205E7 /* SplashView.swift */,
393+
D1E2F3A4B5C6D7E8F9A0B1C0 /* SwatchBar.swift */,
382394
EA635DF025B5A6D80014D91A /* Toast.swift */,
383395
EAF100CC25C785C4006E1EC3 /* TouchBarVisual.swift */,
384396
226FD60F25A940F90021A67F /* VisualEffect.swift */,
@@ -657,6 +669,9 @@
657669
EAD0B713259CFD2000FA2F67 /* Defaults.swift in Sources */,
658670
226FD61025A940F90021A67F /* VisualEffect.swift in Sources */,
659671
EAD0B6FE259CF29C00FA2F67 /* Constants.swift in Sources */,
672+
FC6B02943A97C369AEDA0D64 /* ColorHistoryManager.swift in Sources */,
673+
68267E79E2A1817195C7C16F /* ColorHistory.swift in Sources */,
674+
D1E2F3A4B5C6D7E8F9A0B1C1 /* SwatchBar.swift in Sources */,
660675
);
661676
runOnlyForDeploymentPostprocessing = 0;
662677
};
@@ -717,6 +732,9 @@
717732
EAE23DD52D032A38005BB270 /* Defaults.swift in Sources */,
718733
EAE23DD62D032A38005BB270 /* VisualEffect.swift in Sources */,
719734
EAE23DD72D032A38005BB270 /* Constants.swift in Sources */,
735+
3FB457874C3F741FC84DADD6 /* ColorHistoryManager.swift in Sources */,
736+
1C7412E01A6EF6E6D516CEB6 /* ColorHistory.swift in Sources */,
737+
D1E2F3A4B5C6D7E8F9A0B1C2 /* SwatchBar.swift in Sources */,
720738
);
721739
runOnlyForDeploymentPostprocessing = 0;
722740
};

Pika/AppDelegate.swift

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,50 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
1818
var aboutWindow: NSWindow!
1919
var preferencesWindow: NSWindow!
2020
var eyedroppers: Eyedroppers!
21+
let colorHistoryManager = ColorHistoryManager()
2122

2223
var undoManager = UndoManager()
24+
private var hadColorHistory = false
25+
26+
override init() {
27+
super.init()
28+
eyedroppers = Eyedroppers()
29+
eyedroppers.foreground.undoManager = undoManager
30+
eyedroppers.background.undoManager = undoManager
31+
eyedroppers.foreground.colorHistoryManager = colorHistoryManager
32+
eyedroppers.background.colorHistoryManager = colorHistoryManager
33+
}
2334

2435
var pikaTouchBarController: PikaTouchBarController!
2536
var splashTouchBarController: SplashTouchBarController!
2637
var aboutTouchBarController: SplashTouchBarController!
2738

2839
let notificationCenter = NotificationCenter.default
2940

41+
private func idealWindowContentHeight() -> CGFloat {
42+
SwatchLayout.totalHeight(
43+
base: 230,
44+
hasHistory: hadColorHistory,
45+
paletteCount: 0
46+
)
47+
}
48+
49+
func updateWindowSize(animate: Bool) {
50+
guard pikaWindow != nil else { return }
51+
let contentHeight = idealWindowContentHeight()
52+
let targetContent = NSRect(x: 0, y: 0, width: CGFloat(pikaWindow.frame.width), height: contentHeight)
53+
let targetFrame = pikaWindow.frameRect(forContentRect: targetContent)
54+
// Pin the top edge of the window.
55+
let currentFrame = pikaWindow.frame
56+
let newFrame = NSRect(
57+
x: currentFrame.origin.x,
58+
y: currentFrame.origin.y + currentFrame.size.height - targetFrame.height,
59+
width: currentFrame.size.width,
60+
height: targetFrame.height
61+
)
62+
pikaWindow.setFrame(newFrame, display: true, animate: animate && pikaWindow.isVisible)
63+
}
64+
3065
func setupAppMode() {
3166
var currentMode = Defaults[.appMode] == .regular
3267
? NSApplication.ActivationPolicy.regular
@@ -41,7 +76,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
4176
NSApp.setActivationPolicy(newMode)
4277
NSApp.activate(ignoringOtherApps: true)
4378
if change.newValue == .regular {
44-
DispatchQueue.main.asyncAfter(deadline: .now()) {
79+
DispatchQueue.main.async {
4580
NSApp.unhide(self)
4681

4782
if let window = NSApp.windows.first {
@@ -99,11 +134,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
99134
setupAppMode()
100135
setupStatusBar()
101136

102-
// Set up eyedroppers
103-
eyedroppers = Eyedroppers()
104-
eyedroppers.foreground.undoManager = undoManager
105-
eyedroppers.background.undoManager = undoManager
106-
107137
// Define content view
108138
let contentView = ContentView()
109139
.environmentObject(eyedroppers)
@@ -112,13 +142,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
112142
maxWidth: 650,
113143
minHeight: 230,
114144
idealHeight: 230,
115-
maxHeight: 400,
145+
maxHeight: 550,
116146
alignment: .center)
117147

118148
pikaWindow = PikaWindow.createPrimaryWindow()
119149
pikaWindow.contentView = NSHostingView(rootView: contentView)
120150
pikaTouchBarController = PikaTouchBarController(window: pikaWindow)
121151

152+
hadColorHistory = !Defaults[.colorHistory].isEmpty
153+
updateWindowSize(animate: false)
154+
155+
Defaults.observe(.colorHistory) { [weak self] _ in
156+
DispatchQueue.main.async {
157+
guard let self = self else { return }
158+
let hasHistory = !Defaults[.colorHistory].isEmpty
159+
guard hasHistory != self.hadColorHistory else { return }
160+
self.hadColorHistory = hasHistory
161+
self.updateWindowSize(animate: true)
162+
}
163+
}.tieToLifetime(of: self)
164+
122165
// Define global keyboard shortcuts
123166
KeyboardShortcuts.onKeyUp(for: .togglePika) { [] in
124167
if Defaults[.viewedSplash] {
@@ -157,25 +200,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
157200
// swiftlint:disable function_body_length
158201
@objc func handleGetURLEvent(_ event: NSAppleEventDescriptor, withReplyEvent _: NSAppleEventDescriptor) {
159202
if let urlString = event.forKeyword(AEKeyword(keyDirectObject))?.stringValue {
160-
let url = URL(string: urlString)
161-
guard url != nil, let scheme = url!.scheme, let action = url!.host else {
162-
// some error
203+
guard let url = URL(string: urlString),
204+
let scheme = url.scheme,
205+
let action = url.host
206+
else {
163207
return
164208
}
165209

166-
var list = url!.pathComponents.dropFirst()
210+
var list = url.pathComponents.dropFirst()
167211
let task = list.popFirst()
168212
let colorFormat = list.popFirst()
169213

170214
if scheme.caseInsensitiveCompare("pika") == .orderedSame {
171-
if colorFormat != nil {
172-
if let format = ColorFormat.withLabel(colorFormat!) {
173-
Defaults[.colorFormat] = format
174-
}
215+
if let colorFormat = colorFormat,
216+
let format = ColorFormat.withLabel(colorFormat)
217+
{
218+
Defaults[.colorFormat] = format
175219
}
176220

177221
if action == "format" {
178-
if let format = ColorFormat.withLabel(task!) {
222+
if let task = task, let format = ColorFormat.withLabel(task) {
179223
Defaults[.colorFormat] = format
180224
}
181225
}
@@ -385,14 +429,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
385429
}
386430

387431
@IBAction func triggerSystemPickerForeground(_: Any) {
432+
eyedroppers.foreground.togglePicker()
388433
notificationCenter.post(name: Notification.Name(PikaConstants.ncTriggerSystemPickerForeground), object: self)
389434
}
390435

391436
@IBAction func triggerSystemPickerBackground(_: Any) {
437+
eyedroppers.background.togglePicker()
392438
notificationCenter.post(name: Notification.Name(PikaConstants.ncTriggerSystemPickerBackground), object: self)
393439
}
394440

395441
@IBAction func triggerSwap(_: Any) {
442+
swap(&eyedroppers.foreground.color, &eyedroppers.background.color)
396443
notificationCenter.post(name: Notification.Name(PikaConstants.ncTriggerSwap), object: self)
397444
}
398445

Pika/Assets/de.lproj/Localizable.strings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,6 @@
351351

352352
/* Duration: */
353353
"preferences.overlay.duration" = "Dauer";
354+
355+
/* Color History */
356+
"color.history" = "Color History";

Pika/Assets/en.lproj/Localizable.strings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,6 @@
351351

352352
/* Duration: */
353353
"preferences.overlay.duration" = "Duration";
354+
355+
/* Color History */
356+
"color.history" = "Color History";

Pika/Assets/es.lproj/Localizable.strings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,6 @@
351351

352352
/* Duration: */
353353
"preferences.overlay.duration" = "Duración";
354+
355+
/* Color History */
356+
"color.history" = "Color History";

Pika/Assets/fr.lproj/Localizable.strings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,6 @@
351351

352352
/* Duration: */
353353
"preferences.overlay.duration" = "Durée";
354+
355+
/* Color History */
356+
"color.history" = "Color History";

Pika/Assets/hr.lproj/Localizable.strings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,6 @@
351351

352352
/* Duration: */
353353
"preferences.overlay.duration" = "Trajanje";
354+
355+
/* Color History */
356+
"color.history" = "Color History";

Pika/Assets/pl.lproj/Localizable.strings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,6 @@
351351

352352
/* Duration: */
353353
"preferences.overlay.duration" = "Czas trwania";
354+
355+
/* Color History */
356+
"color.history" = "Color History";
112 Bytes
Binary file not shown.
112 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)