From 2430f978c4bea20459efa03446a11dd42fcd4bd7 Mon Sep 17 00:00:00 2001 From: W1W1-M Date: Wed, 17 Aug 2022 14:20:12 +0200 Subject: [PATCH 1/9] Add ColorChecker class. Add colorInputs ObservedObject for color strings & checking methods. Add mark or checkmark image for feedback on valid text input. Fix SwiftLint warnings. --- Color Picker.xcodeproj/project.pbxproj | 4 +++ Color Picker/ColorChecker.swift | 45 ++++++++++++++++++++++++++ Color Picker/ColorPickerScreen.swift | 33 ++++++++++--------- 3 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 Color Picker/ColorChecker.swift diff --git a/Color Picker.xcodeproj/project.pbxproj b/Color Picker.xcodeproj/project.pbxproj index f7fcb63..b718e78 100644 --- a/Color Picker.xcodeproj/project.pbxproj +++ b/Color Picker.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 3DCF4CEE28AC2C0400CE25D5 /* ColorChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCF4CED28AC2C0400CE25D5 /* ColorChecker.swift */; }; E32CF5572793FACD002DDBC0 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E32CF5562793FACD002DDBC0 /* Defaults */; }; E38D432F263AAE8500701B82 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38D432E263AAE8500701B82 /* SettingsScreen.swift */; }; E38D4331263AAEA900701B82 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38D4330263AAEA900701B82 /* Constants.swift */; }; @@ -58,6 +59,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3DCF4CED28AC2C0400CE25D5 /* ColorChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorChecker.swift; sourceTree = ""; }; E38D432E263AAE8500701B82 /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; E38D4330263AAEA900701B82 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; E38D4332263AB24E00701B82 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; @@ -160,6 +162,7 @@ E3A3B12025904E7D001B4D0C /* Assets.xcassets */, E3E7D7A827218959009D71F4 /* Intents.intentdefinition */, E322B6DB26416E1100ABB691 /* Other */, + 3DCF4CED28AC2C0400CE25D5 /* ColorChecker.swift */, ); path = "Color Picker"; sourceTree = ""; @@ -341,6 +344,7 @@ E390D734263B3C71005FCB34 /* AppState.swift in Sources */, E38D4331263AAEA900701B82 /* Constants.swift in Sources */, E3DFA8C92662514800D2623E /* Events.swift in Sources */, + 3DCF4CEE28AC2C0400CE25D5 /* ColorChecker.swift in Sources */, E390D736263C6ACD005FCB34 /* ColorPanel.swift in Sources */, E3E7D7A927218959009D71F4 /* Intents.intentdefinition in Sources */, E38D4335263AEE3700701B82 /* ColorPickerScreen.swift in Sources */, diff --git a/Color Picker/ColorChecker.swift b/Color Picker/ColorChecker.swift new file mode 100644 index 0000000..46ea27b --- /dev/null +++ b/Color Picker/ColorChecker.swift @@ -0,0 +1,45 @@ +// +// ColorChecker.swift +// Color Picker +// +// Created by William Mead on 16/08/2022. +// + +import Foundation + +/// Color checker +final class ColorChecker: ObservableObject, Identifiable { + // MARK: - Properties + internal let id: UUID + @Published var hexColorValid: Bool + @Published var hexColor: String + // MARK: - Init & deinit + init() { + print("ColorChecker init ...") + self.id = UUID() + self.hexColor = "ffffff" + self.hexColorValid = true + } + deinit { + print("... deinit ColorChecker") + } + // MARK: - Methods + func checkHexColorTextInput() { + if hexColor.contains(where: { _ in + hexColor.rangeOfCharacter(from: CharacterSet(charactersIn: "abcdefABCDEF0123456789").inverted) != nil + }) { + hexColorValid = false + hexColor = hexColor.trimmingCharacters(in: CharacterSet(charactersIn: "abcdeABCDE0123456789").inverted) + } + if hexColor.count > 6 { + hexColorValid = false + hexColor = String(hexColor.prefix(6)) + } + if hexColor.count < 6 { + hexColorValid = false + } + if hexColor.count == 6 { + hexColorValid = true + } + } +} diff --git a/Color Picker/ColorPickerScreen.swift b/Color Picker/ColorPickerScreen.swift index ece1d0e..ffb7199 100644 --- a/Color Picker/ColorPickerScreen.swift +++ b/Color Picker/ColorPickerScreen.swift @@ -134,6 +134,7 @@ struct ColorPickerScreen: View { @Default(.legacyColorSyntax) private var legacyColorSyntax @Default(.shownColorFormats) private var shownColorFormats @Default(.largerText) private var largerText + @ObservedObject private var colorInputs = ColorChecker() @State private var hexColor = "" @State private var hslColor = "" @State private var rgbColor = "" @@ -156,23 +157,26 @@ struct ColorPickerScreen: View { private var textFieldFontSize: Double { largerText ? 16 : 0 } private var hexColorView: some View { - HStack { + HStack { + Image(systemName: colorInputs.hexColorValid ? "checkmark.circle" : "xmark.circle") + .symbolRenderingMode(.hierarchical) + .foregroundStyle(colorInputs.hexColorValid ? .green : .red) + .imageScale(.large) // TODO: When I use `TextField`, add the copy button using `.safeAreaInset()`. NativeTextField( - text: $hexColor, - placeholder: "Hex", - font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), - isFocused: $isTextFieldFocusedHex - ) + text: $colorInputs.hexColor, + placeholder: "Hex", + font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), + isFocused: $isTextFieldFocusedHex + ) .controlSize(.large) - .onChange(of: hexColor) { + .onChange(of: colorInputs.hexColor) { + colorInputs.checkHexColorTextInput() var hexColor = $0 - if hexColor.hasPrefix("##") { hexColor = hexColor.dropFirst().toString self.hexColor = hexColor } - if isTextFieldFocusedHex, !isPreventingUpdate, @@ -180,21 +184,20 @@ struct ColorPickerScreen: View { { colorPanel.color = newColor } - if !isPreventingUpdate { updateColorsFromPanel(excludeHex: true, preventUpdate: true) } } - Button("Copy Hex", systemImage: "doc.on.doc.fill") { - appState.colorPanel.color.hexColorString.copyToPasteboard() - } + Button("Copy Hex", systemImage: "doc.on.doc.fill") { + appState.colorPanel.color.hexColorString.copyToPasteboard() + } .labelStyle(.iconOnly) .symbolRenderingMode(.hierarchical) .buttonStyle(.borderless) .contentShape(.rectangle) .keyboardShortcut("h", modifiers: [.shift, .command]) - } - } + } + } private var hslColorView: some View { HStack { From 2d1aafbca2733d44bec2221e27337db150ab7fee Mon Sep 17 00:00:00 2001 From: W1W1-M Date: Thu, 18 Aug 2022 13:54:22 +0200 Subject: [PATCH 2/9] Delete ColorChecker class & ObservedObject. Delete SF symbol for color text input validation. Keep SwiftLint warning fixes. --- Color Picker.xcodeproj/project.pbxproj | 4 --- Color Picker/ColorChecker.swift | 45 -------------------------- Color Picker/ColorPickerScreen.swift | 13 +++----- 3 files changed, 5 insertions(+), 57 deletions(-) delete mode 100644 Color Picker/ColorChecker.swift diff --git a/Color Picker.xcodeproj/project.pbxproj b/Color Picker.xcodeproj/project.pbxproj index b718e78..f7fcb63 100644 --- a/Color Picker.xcodeproj/project.pbxproj +++ b/Color Picker.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 3DCF4CEE28AC2C0400CE25D5 /* ColorChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCF4CED28AC2C0400CE25D5 /* ColorChecker.swift */; }; E32CF5572793FACD002DDBC0 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E32CF5562793FACD002DDBC0 /* Defaults */; }; E38D432F263AAE8500701B82 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38D432E263AAE8500701B82 /* SettingsScreen.swift */; }; E38D4331263AAEA900701B82 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38D4330263AAEA900701B82 /* Constants.swift */; }; @@ -59,7 +58,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 3DCF4CED28AC2C0400CE25D5 /* ColorChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorChecker.swift; sourceTree = ""; }; E38D432E263AAE8500701B82 /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; E38D4330263AAEA900701B82 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; E38D4332263AB24E00701B82 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; @@ -162,7 +160,6 @@ E3A3B12025904E7D001B4D0C /* Assets.xcassets */, E3E7D7A827218959009D71F4 /* Intents.intentdefinition */, E322B6DB26416E1100ABB691 /* Other */, - 3DCF4CED28AC2C0400CE25D5 /* ColorChecker.swift */, ); path = "Color Picker"; sourceTree = ""; @@ -344,7 +341,6 @@ E390D734263B3C71005FCB34 /* AppState.swift in Sources */, E38D4331263AAEA900701B82 /* Constants.swift in Sources */, E3DFA8C92662514800D2623E /* Events.swift in Sources */, - 3DCF4CEE28AC2C0400CE25D5 /* ColorChecker.swift in Sources */, E390D736263C6ACD005FCB34 /* ColorPanel.swift in Sources */, E3E7D7A927218959009D71F4 /* Intents.intentdefinition in Sources */, E38D4335263AEE3700701B82 /* ColorPickerScreen.swift in Sources */, diff --git a/Color Picker/ColorChecker.swift b/Color Picker/ColorChecker.swift deleted file mode 100644 index 46ea27b..0000000 --- a/Color Picker/ColorChecker.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// ColorChecker.swift -// Color Picker -// -// Created by William Mead on 16/08/2022. -// - -import Foundation - -/// Color checker -final class ColorChecker: ObservableObject, Identifiable { - // MARK: - Properties - internal let id: UUID - @Published var hexColorValid: Bool - @Published var hexColor: String - // MARK: - Init & deinit - init() { - print("ColorChecker init ...") - self.id = UUID() - self.hexColor = "ffffff" - self.hexColorValid = true - } - deinit { - print("... deinit ColorChecker") - } - // MARK: - Methods - func checkHexColorTextInput() { - if hexColor.contains(where: { _ in - hexColor.rangeOfCharacter(from: CharacterSet(charactersIn: "abcdefABCDEF0123456789").inverted) != nil - }) { - hexColorValid = false - hexColor = hexColor.trimmingCharacters(in: CharacterSet(charactersIn: "abcdeABCDE0123456789").inverted) - } - if hexColor.count > 6 { - hexColorValid = false - hexColor = String(hexColor.prefix(6)) - } - if hexColor.count < 6 { - hexColorValid = false - } - if hexColor.count == 6 { - hexColorValid = true - } - } -} diff --git a/Color Picker/ColorPickerScreen.swift b/Color Picker/ColorPickerScreen.swift index ffb7199..0075183 100644 --- a/Color Picker/ColorPickerScreen.swift +++ b/Color Picker/ColorPickerScreen.swift @@ -134,7 +134,6 @@ struct ColorPickerScreen: View { @Default(.legacyColorSyntax) private var legacyColorSyntax @Default(.shownColorFormats) private var shownColorFormats @Default(.largerText) private var largerText - @ObservedObject private var colorInputs = ColorChecker() @State private var hexColor = "" @State private var hslColor = "" @State private var rgbColor = "" @@ -158,25 +157,22 @@ struct ColorPickerScreen: View { private var hexColorView: some View { HStack { - Image(systemName: colorInputs.hexColorValid ? "checkmark.circle" : "xmark.circle") - .symbolRenderingMode(.hierarchical) - .foregroundStyle(colorInputs.hexColorValid ? .green : .red) - .imageScale(.large) // TODO: When I use `TextField`, add the copy button using `.safeAreaInset()`. NativeTextField( - text: $colorInputs.hexColor, + text: $hexColor, placeholder: "Hex", font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), isFocused: $isTextFieldFocusedHex ) .controlSize(.large) - .onChange(of: colorInputs.hexColor) { - colorInputs.checkHexColorTextInput() + .onChange(of: hexColor) { var hexColor = $0 + if hexColor.hasPrefix("##") { hexColor = hexColor.dropFirst().toString self.hexColor = hexColor } + if isTextFieldFocusedHex, !isPreventingUpdate, @@ -184,6 +180,7 @@ struct ColorPickerScreen: View { { colorPanel.color = newColor } + if !isPreventingUpdate { updateColorsFromPanel(excludeHex: true, preventUpdate: true) } From 59b5e489e0d66d8c72c7404f5d3bf9b408798a4c Mon Sep 17 00:00:00 2001 From: W1W1-M Date: Thu, 18 Aug 2022 22:18:45 +0200 Subject: [PATCH 3/9] Add ColorInputView. Setup ColorInputView for hex color inputs. Add condition to change text color if color code is invalid. Add condition to limit character input. Add textColor to NativeTextField for use in updateNSView method. Update other ColorViews to use textColor. --- Color Picker/ColorPickerScreen.swift | 122 +++++++++++++++++++++++++-- Color Picker/Utilities.swift | 2 + 2 files changed, 119 insertions(+), 5 deletions(-) diff --git a/Color Picker/ColorPickerScreen.swift b/Color Picker/ColorPickerScreen.swift index 0075183..eb1d25d 100644 --- a/Color Picker/ColorPickerScreen.swift +++ b/Color Picker/ColorPickerScreen.swift @@ -127,6 +127,106 @@ private struct BarView: View { } } +struct ColorInputView: View { + @EnvironmentObject private var appState: AppState + @State private var textColor: Color = .primary + @Binding var inputColorText: String + @Binding var isTextFieldFocused: Bool + @Binding var isPreventingUpdate: Bool + let colorPanel: NSColorPanel + let textFieldFontSize: Double + let inputColorType: colorType + enum colorType: String, CaseIterable { + case hex = "Hex" + case hsl = "HSL" + case rgb = "RGB" + case lch = "LCH" + } + var colorKeyboardShortcut: KeyEquivalent { + switch inputColorType { + case .hex: + return KeyEquivalent("h") + case .hsl: + return KeyEquivalent("s") + case .rgb: + return KeyEquivalent("r") + case .lch: + return KeyEquivalent("l") + } + } + var body: some View { + HStack { + NativeTextField( + text: $inputColorText, + placeholder: inputColorType.rawValue, + font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), + isFocused: $isTextFieldFocused, + textColor: textColor + ) + .controlSize(.large) + .onChange(of: inputColorText) { + switch inputColorType { + case .hex: + if inputColorText.count > 6 { + if inputColorText.prefix(1) == "#" { + inputColorText = inputColorText.prefix(7).toString + } else { + inputColorText = inputColorText.prefix(6).toString + } + } + + // inputColorText = inputColorText.filter { "#abcdefABCDEF0123456789".contains($0) } // Hex text input filter + + var hexColor = $0 + + if hexColor.hasPrefix("##") { + hexColor = hexColor.dropFirst().toString + inputColorText = hexColor + } + + if + isTextFieldFocused, + !isPreventingUpdate, + let newColor = NSColor(hexString: hexColor.trimmingCharacters(in: .whitespaces)) + { + colorPanel.color = newColor + textColor = .primary + } else { + textColor = .red + } + + if !isPreventingUpdate { + // updateColorsFromPanel(excludeHex: true, preventUpdate: true) + } + case .hsl: + return + case .rgb: + return + case .lch: + return + } + } + Button("Copy \(inputColorType.rawValue)", systemImage: "doc.on.doc.fill") { + switch inputColorType { + case .hex: + appState.colorPanel.color.hexColorString.copyToPasteboard() + case .hsl: + return + case .rgb: + return + case .lch: + return + } + } + .labelStyle(.iconOnly) + .symbolRenderingMode(.hierarchical) + .buttonStyle(.borderless) + .contentShape(.rectangle) + .keyboardShortcut(colorKeyboardShortcut, modifiers: [.shift, .command]) // TODO: need to pass kb shortcut somehow + } + } +} + struct ColorPickerScreen: View { @EnvironmentObject private var appState: AppState @Default(.uppercaseHexColor) private var uppercaseHexColor @@ -143,6 +243,7 @@ struct ColorPickerScreen: View { @State private var isTextFieldFocusedRGB = false @State private var isTextFieldFocusedLCH = false @State private var isPreventingUpdate = false + @State private var textColor = Color.primary let colorPanel: NSColorPanel @@ -162,7 +263,8 @@ struct ColorPickerScreen: View { text: $hexColor, placeholder: "Hex", font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), - isFocused: $isTextFieldFocusedHex + isFocused: $isTextFieldFocusedHex, + textColor: textColor ) .controlSize(.large) .onChange(of: hexColor) { @@ -202,7 +304,8 @@ struct ColorPickerScreen: View { text: $hslColor, placeholder: "HSL", font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), - isFocused: $isTextFieldFocusedHSL + isFocused: $isTextFieldFocusedHSL, + textColor: textColor ) .controlSize(.large) .onChange(of: hslColor) { @@ -235,7 +338,8 @@ struct ColorPickerScreen: View { text: $rgbColor, placeholder: "RGB", font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), - isFocused: $isTextFieldFocusedRGB + isFocused: $isTextFieldFocusedRGB, + textColor: textColor ) .controlSize(.large) .onChange(of: rgbColor) { @@ -268,7 +372,8 @@ struct ColorPickerScreen: View { text: $lchColor, placeholder: "LCH", font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), - isFocused: $isTextFieldFocusedLCH + isFocused: $isTextFieldFocusedLCH, + textColor: textColor ) .controlSize(.large) .onChange(of: lchColor) { @@ -299,7 +404,14 @@ struct ColorPickerScreen: View { VStack { BarView() if shownColorFormats.contains(.hex) { - hexColorView + ColorInputView( + inputColorText: $hexColor, + isTextFieldFocused: $isTextFieldFocusedHex, + isPreventingUpdate: $isPreventingUpdate, + colorPanel: colorPanel, + textFieldFontSize: textFieldFontSize, + inputColorType: .hex + ) } if shownColorFormats.contains(.hsl) { hslColorView diff --git a/Color Picker/Utilities.swift b/Color Picker/Utilities.swift index 4276cb6..c7ef65b 100644 --- a/Color Picker/Utilities.swift +++ b/Color Picker/Utilities.swift @@ -1021,6 +1021,7 @@ struct NativeTextField: NSViewRepresentable { var isFirstResponder = false @Binding var isFocused: Bool // Note: This is only readable. var isSingleLine = true + var textColor: Color final class InternalTextField: NSTextField { private var globalEventMonitor: GlobalEventMonitor? @@ -1143,6 +1144,7 @@ struct NativeTextField: NSViewRepresentable { nsView.bezelStyle = .roundedBezel nsView.stringValue = text nsView.placeholderString = placeholder + nsView.textColor = NSColor(textColor) if let font = font { nsView.font = font From dbf5bed4baceb8bd8ea3d5840d3ae6e23a4e96d0 Mon Sep 17 00:00:00 2001 From: W1W1-M Date: Fri, 19 Aug 2022 14:38:36 +0200 Subject: [PATCH 4/9] Move hsl logic to ColorInputView. Delete hexColorView & hslColorView. Add hsl ColorInputView to ColorPickerScreen. Update comments. --- Color Picker/ColorPickerScreen.swift | 109 +++++++-------------------- 1 file changed, 27 insertions(+), 82 deletions(-) diff --git a/Color Picker/ColorPickerScreen.swift b/Color Picker/ColorPickerScreen.swift index eb1d25d..144b447 100644 --- a/Color Picker/ColorPickerScreen.swift +++ b/Color Picker/ColorPickerScreen.swift @@ -156,6 +156,7 @@ struct ColorInputView: View { } var body: some View { HStack { + // TODO: When I use `TextField`, add the copy button using `.safeAreaInset()`. NativeTextField( text: $inputColorText, placeholder: inputColorType.rawValue, @@ -187,7 +188,7 @@ struct ColorInputView: View { if isTextFieldFocused, !isPreventingUpdate, - let newColor = NSColor(hexString: hexColor.trimmingCharacters(in: .whitespaces)) + let newColor = NSColor(hexString: inputColorText.trimmingCharacters(in: .whitespaces)) { colorPanel.color = newColor textColor = .primary @@ -196,10 +197,23 @@ struct ColorInputView: View { } if !isPreventingUpdate { - // updateColorsFromPanel(excludeHex: true, preventUpdate: true) + // updateColorsFromPanel(excludeHex: true, preventUpdate: true) // TODO: Method needs refactoring to work here } case .hsl: - return + if + isTextFieldFocused, + !isPreventingUpdate, + let newColor = NSColor(cssHSLString: inputColorText.trimmingCharacters(in: .whitespaces)) + { + colorPanel.color = newColor + textColor = .primary + } else { + textColor = .red + } + + if !isPreventingUpdate { + // updateColorsFromPanel(excludeHSL: true, preventUpdate: true) + } case .rgb: return case .lch: @@ -211,7 +225,7 @@ struct ColorInputView: View { case .hex: appState.colorPanel.color.hexColorString.copyToPasteboard() case .hsl: - return + appState.colorPanel.color.hslColorString.copyToPasteboard() case .rgb: return case .lch: @@ -222,7 +236,7 @@ struct ColorInputView: View { .symbolRenderingMode(.hierarchical) .buttonStyle(.borderless) .contentShape(.rectangle) - .keyboardShortcut(colorKeyboardShortcut, modifiers: [.shift, .command]) // TODO: need to pass kb shortcut somehow + .keyboardShortcut(colorKeyboardShortcut, modifiers: [.shift, .command]) } } } @@ -256,82 +270,6 @@ struct ColorPickerScreen: View { private var textFieldFontSize: Double { largerText ? 16 : 0 } - private var hexColorView: some View { - HStack { - // TODO: When I use `TextField`, add the copy button using `.safeAreaInset()`. - NativeTextField( - text: $hexColor, - placeholder: "Hex", - font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), - isFocused: $isTextFieldFocusedHex, - textColor: textColor - ) - .controlSize(.large) - .onChange(of: hexColor) { - var hexColor = $0 - - if hexColor.hasPrefix("##") { - hexColor = hexColor.dropFirst().toString - self.hexColor = hexColor - } - - if - isTextFieldFocusedHex, - !isPreventingUpdate, - let newColor = NSColor(hexString: hexColor.trimmingCharacters(in: .whitespaces)) - { - colorPanel.color = newColor - } - - if !isPreventingUpdate { - updateColorsFromPanel(excludeHex: true, preventUpdate: true) - } - } - Button("Copy Hex", systemImage: "doc.on.doc.fill") { - appState.colorPanel.color.hexColorString.copyToPasteboard() - } - .labelStyle(.iconOnly) - .symbolRenderingMode(.hierarchical) - .buttonStyle(.borderless) - .contentShape(.rectangle) - .keyboardShortcut("h", modifiers: [.shift, .command]) - } - } - - private var hslColorView: some View { - HStack { - NativeTextField( - text: $hslColor, - placeholder: "HSL", - font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), - isFocused: $isTextFieldFocusedHSL, - textColor: textColor - ) - .controlSize(.large) - .onChange(of: hslColor) { - if - isTextFieldFocusedHSL, - !isPreventingUpdate, - let newColor = NSColor(cssHSLString: $0.trimmingCharacters(in: .whitespaces)) - { - colorPanel.color = newColor - } - - if !isPreventingUpdate { - updateColorsFromPanel(excludeHSL: true, preventUpdate: true) - } - } - Button("Copy HSL", systemImage: "doc.on.doc.fill") { - hslColor.copyToPasteboard() - } - .labelStyle(.iconOnly) - .symbolRenderingMode(.hierarchical) - .buttonStyle(.borderless) - .contentShape(.rectangle) - .keyboardShortcut("s", modifiers: [.shift, .command]) - } - } - private var rgbColorView: some View { HStack { NativeTextField( @@ -414,7 +352,14 @@ struct ColorPickerScreen: View { ) } if shownColorFormats.contains(.hsl) { - hslColorView + ColorInputView( + inputColorText: $hslColor, + isTextFieldFocused: $isTextFieldFocusedHSL, + isPreventingUpdate: $isPreventingUpdate, + colorPanel: colorPanel, + textFieldFontSize: textFieldFontSize, + inputColorType: .hsl + ) } if shownColorFormats.contains(.rgb) { rgbColorView From 99f45b2d10314ff4513462da5428a30c3dd30b3d Mon Sep 17 00:00:00 2001 From: W1W1-M Date: Fri, 19 Aug 2022 14:51:38 +0200 Subject: [PATCH 5/9] Isolate textColor logic --- Color Picker/ColorPickerScreen.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Color Picker/ColorPickerScreen.swift b/Color Picker/ColorPickerScreen.swift index 144b447..3841b79 100644 --- a/Color Picker/ColorPickerScreen.swift +++ b/Color Picker/ColorPickerScreen.swift @@ -191,6 +191,9 @@ struct ColorInputView: View { let newColor = NSColor(hexString: inputColorText.trimmingCharacters(in: .whitespaces)) { colorPanel.color = newColor + } + + if NSColor(hexString: inputColorText.trimmingCharacters(in: .whitespaces)) != nil { textColor = .primary } else { textColor = .red @@ -206,6 +209,9 @@ struct ColorInputView: View { let newColor = NSColor(cssHSLString: inputColorText.trimmingCharacters(in: .whitespaces)) { colorPanel.color = newColor + } + + if NSColor(cssHSLString: inputColorText.trimmingCharacters(in: .whitespaces)) != nil { textColor = .primary } else { textColor = .red From 26509e537562c88164dbc339a2f0b98ce18c7618 Mon Sep 17 00:00:00 2001 From: W1W1-M Date: Fri, 19 Aug 2022 17:16:38 +0200 Subject: [PATCH 6/9] Update inputColorType to ColorFormat type. Delete colorType enum. --- Color Picker/ColorPickerScreen.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Color Picker/ColorPickerScreen.swift b/Color Picker/ColorPickerScreen.swift index 3841b79..d55d9f3 100644 --- a/Color Picker/ColorPickerScreen.swift +++ b/Color Picker/ColorPickerScreen.swift @@ -135,13 +135,7 @@ struct ColorInputView: View { @Binding var isPreventingUpdate: Bool let colorPanel: NSColorPanel let textFieldFontSize: Double - let inputColorType: colorType - enum colorType: String, CaseIterable { - case hex = "Hex" - case hsl = "HSL" - case rgb = "RGB" - case lch = "LCH" - } + let inputColorType: ColorFormat var colorKeyboardShortcut: KeyEquivalent { switch inputColorType { case .hex: From 1cf5e650c4cf0b61dbfde8e33963c28f78fd09f1 Mon Sep 17 00:00:00 2001 From: W1W1-M Date: Fri, 19 Aug 2022 22:33:34 +0200 Subject: [PATCH 7/9] Move colorKeyboardShortcut to ColorFormat extension. Delete inputColorText filter. --- Color Picker/ColorPickerScreen.swift | 32 +++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Color Picker/ColorPickerScreen.swift b/Color Picker/ColorPickerScreen.swift index d55d9f3..ef85f46 100644 --- a/Color Picker/ColorPickerScreen.swift +++ b/Color Picker/ColorPickerScreen.swift @@ -136,18 +136,7 @@ struct ColorInputView: View { let colorPanel: NSColorPanel let textFieldFontSize: Double let inputColorType: ColorFormat - var colorKeyboardShortcut: KeyEquivalent { - switch inputColorType { - case .hex: - return KeyEquivalent("h") - case .hsl: - return KeyEquivalent("s") - case .rgb: - return KeyEquivalent("r") - case .lch: - return KeyEquivalent("l") - } - } + var body: some View { HStack { // TODO: When I use `TextField`, add the copy button using `.safeAreaInset()`. @@ -170,8 +159,6 @@ struct ColorInputView: View { } } - // inputColorText = inputColorText.filter { "#abcdefABCDEF0123456789".contains($0) } // Hex text input filter - var hexColor = $0 if hexColor.hasPrefix("##") { @@ -236,7 +223,7 @@ struct ColorInputView: View { .symbolRenderingMode(.hierarchical) .buttonStyle(.borderless) .contentShape(.rectangle) - .keyboardShortcut(colorKeyboardShortcut, modifiers: [.shift, .command]) + .keyboardShortcut(inputColorType.keyboardShortcut, modifiers: [.shift, .command]) } } } @@ -430,6 +417,21 @@ struct ColorPickerScreen: View { } } +extension ColorFormat { + fileprivate var keyboardShortcut: KeyEquivalent { + switch self { + case .hex: + return KeyEquivalent("h") + case .hsl: + return KeyEquivalent("s") + case .rgb: + return KeyEquivalent("r") + case .lch: + return KeyEquivalent("l") + } + } +} + struct ColorPickerScreen_Previews: PreviewProvider { static var previews: some View { ColorPickerScreen(colorPanel: .shared) From 32d5826235385a38bc2fdf4cf130ff551e79efa6 Mon Sep 17 00:00:00 2001 From: W1W1-M Date: Fri, 19 Aug 2022 22:45:55 +0200 Subject: [PATCH 8/9] Move rgb & lch logic to ColorInputView. Delete rgbColorView & lchColorView. --- Color Picker/ColorPickerScreen.swift | 126 +++++++++++---------------- 1 file changed, 52 insertions(+), 74 deletions(-) diff --git a/Color Picker/ColorPickerScreen.swift b/Color Picker/ColorPickerScreen.swift index ef85f46..fbd22d6 100644 --- a/Color Picker/ColorPickerScreen.swift +++ b/Color Picker/ColorPickerScreen.swift @@ -202,9 +202,41 @@ struct ColorInputView: View { // updateColorsFromPanel(excludeHSL: true, preventUpdate: true) } case .rgb: - return + if + isTextFieldFocused, + !isPreventingUpdate, + let newColor = NSColor(cssRGBString: inputColorText.trimmingCharacters(in: .whitespaces)) + { + colorPanel.color = newColor + } + + if NSColor(cssRGBString: inputColorText.trimmingCharacters(in: .whitespaces)) != nil { + textColor = .primary + } else { + textColor = .red + } + + if !isPreventingUpdate { + // updateColorsFromPanel(excludeRGB: true, preventUpdate: true) + } case .lch: - return + if + isTextFieldFocused, + !isPreventingUpdate, + let newColor = NSColor(cssLCHString: inputColorText.trimmingCharacters(in: .whitespaces)) + { + colorPanel.color = newColor + } + + if NSColor(cssLCHString: inputColorText.trimmingCharacters(in: .whitespaces)) != nil { + textColor = .primary + } else { + textColor = .red + } + + if !isPreventingUpdate { + // updateColorsFromPanel(excludeLCH: true, preventUpdate: true) + } } } Button("Copy \(inputColorType.rawValue)", systemImage: "doc.on.doc.fill") { @@ -214,9 +246,9 @@ struct ColorInputView: View { case .hsl: appState.colorPanel.color.hslColorString.copyToPasteboard() case .rgb: - return + appState.colorPanel.color.rgbColorString.copyToPasteboard() case .lch: - return + appState.colorPanel.color.lchColorString.copyToPasteboard() } } .labelStyle(.iconOnly) @@ -257,74 +289,6 @@ struct ColorPickerScreen: View { private var textFieldFontSize: Double { largerText ? 16 : 0 } - private var rgbColorView: some View { - HStack { - NativeTextField( - text: $rgbColor, - placeholder: "RGB", - font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), - isFocused: $isTextFieldFocusedRGB, - textColor: textColor - ) - .controlSize(.large) - .onChange(of: rgbColor) { - if - isTextFieldFocusedRGB, - !isPreventingUpdate, - let newColor = NSColor(cssRGBString: $0.trimmingCharacters(in: .whitespaces)) - { - colorPanel.color = newColor - } - - if !isPreventingUpdate { - updateColorsFromPanel(excludeRGB: true, preventUpdate: true) - } - } - Button("Copy RGB", systemImage: "doc.on.doc.fill") { - rgbColor.copyToPasteboard() - } - .labelStyle(.iconOnly) - .symbolRenderingMode(.hierarchical) - .buttonStyle(.borderless) - .contentShape(.rectangle) - .keyboardShortcut("r", modifiers: [.shift, .command]) - } - } - - private var lchColorView: some View { - HStack { - NativeTextField( - text: $lchColor, - placeholder: "LCH", - font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), - isFocused: $isTextFieldFocusedLCH, - textColor: textColor - ) - .controlSize(.large) - .onChange(of: lchColor) { - if - isTextFieldFocusedLCH, - !isPreventingUpdate, - let newColor = NSColor(cssLCHString: $0.trimmingCharacters(in: .whitespaces)) - { - colorPanel.color = newColor - } - - if !isPreventingUpdate { - updateColorsFromPanel(excludeLCH: true, preventUpdate: true) - } - } - Button("Copy LCH", systemImage: "doc.on.doc.fill") { - lchColor.copyToPasteboard() - } - .labelStyle(.iconOnly) - .symbolRenderingMode(.hierarchical) - .buttonStyle(.borderless) - .contentShape(.rectangle) - .keyboardShortcut("l", modifiers: [.shift, .command]) - } - } - var body: some View { VStack { BarView() @@ -349,10 +313,24 @@ struct ColorPickerScreen: View { ) } if shownColorFormats.contains(.rgb) { - rgbColorView + ColorInputView( + inputColorText: $rgbColor, + isTextFieldFocused: $isTextFieldFocusedRGB, + isPreventingUpdate: $isPreventingUpdate, + colorPanel: colorPanel, + textFieldFontSize: textFieldFontSize, + inputColorType: .rgb + ) } if shownColorFormats.contains(.lch) { - lchColorView + ColorInputView( + inputColorText: $lchColor, + isTextFieldFocused: $isTextFieldFocusedLCH, + isPreventingUpdate: $isPreventingUpdate, + colorPanel: colorPanel, + textFieldFontSize: textFieldFontSize, + inputColorType: .lch + ) } } .padding(9) From 98eef5730def40d3d0ed8356eaaa7aa1815963f2 Mon Sep 17 00:00:00 2001 From: W1W1-M Date: Wed, 24 Aug 2022 23:32:15 +0200 Subject: [PATCH 9/9] Move isPreventingUpdate bool & color strings to AppState as @Published properties. Move updateColorsFromPanel method to AppState. Refactor ColorInputView & ColorPickerScreen to use @Published properties from AppState. Delete unused textColor variable. Update comments. --- Color Picker/AppState.swift | 46 ++++++++++++++ Color Picker/ColorPickerScreen.swift | 95 +++++++--------------------- 2 files changed, 69 insertions(+), 72 deletions(-) diff --git a/Color Picker/AppState.swift b/Color Picker/AppState.swift index 1017ec1..20bc2eb 100644 --- a/Color Picker/AppState.swift +++ b/Color Picker/AppState.swift @@ -5,6 +5,8 @@ import Sentry @MainActor final class AppState: ObservableObject { + // MARK: - Properties + static let shared = AppState() var cancellables = Set() @@ -128,6 +130,14 @@ final class AppState: ObservableObject { } } + @Published var hexColor = "" + @Published var hslColor = "" + @Published var rgbColor = "" + @Published var lchColor = "" + @Published var isPreventingUpdate = false + + // MARK: - Methods + init() { setUpConfig() @@ -224,4 +234,40 @@ final class AppState: ObservableObject { func handleAppReopen() { handleMenuBarIcon() } + + // TODO: Find a better way to handle this. + func updateColorsFromPanel( + excludeHex: Bool = false, + excludeHSL: Bool = false, + excludeRGB: Bool = false, + excludeLCH: Bool = false, + preventUpdate: Bool = false, + color: NSColor + ) { + if preventUpdate { + isPreventingUpdate = true + } + + if !excludeHex { + hexColor = color.hexColorString + } + + if !excludeHSL { + hslColor = color.hslColorString + } + + if !excludeRGB { + rgbColor = color.rgbColorString + } + + if !excludeLCH { + lchColor = color.lchColorString + } + + if preventUpdate { + DispatchQueue.main.async { + self.isPreventingUpdate = false + } + } + } } diff --git a/Color Picker/ColorPickerScreen.swift b/Color Picker/ColorPickerScreen.swift index fbd22d6..4e88061 100644 --- a/Color Picker/ColorPickerScreen.swift +++ b/Color Picker/ColorPickerScreen.swift @@ -132,7 +132,6 @@ struct ColorInputView: View { @State private var textColor: Color = .primary @Binding var inputColorText: String @Binding var isTextFieldFocused: Bool - @Binding var isPreventingUpdate: Bool let colorPanel: NSColorPanel let textFieldFontSize: Double let inputColorType: ColorFormat @@ -168,7 +167,7 @@ struct ColorInputView: View { if isTextFieldFocused, - !isPreventingUpdate, + !appState.isPreventingUpdate, let newColor = NSColor(hexString: inputColorText.trimmingCharacters(in: .whitespaces)) { colorPanel.color = newColor @@ -180,13 +179,13 @@ struct ColorInputView: View { textColor = .red } - if !isPreventingUpdate { - // updateColorsFromPanel(excludeHex: true, preventUpdate: true) // TODO: Method needs refactoring to work here + if !appState.isPreventingUpdate { + appState.updateColorsFromPanel(excludeHex: true, preventUpdate: true, color: colorPanel.color) } case .hsl: if isTextFieldFocused, - !isPreventingUpdate, + !appState.isPreventingUpdate, let newColor = NSColor(cssHSLString: inputColorText.trimmingCharacters(in: .whitespaces)) { colorPanel.color = newColor @@ -198,13 +197,13 @@ struct ColorInputView: View { textColor = .red } - if !isPreventingUpdate { - // updateColorsFromPanel(excludeHSL: true, preventUpdate: true) + if !appState.isPreventingUpdate { + appState.updateColorsFromPanel(excludeHSL: true, preventUpdate: true, color: colorPanel.color) } case .rgb: if isTextFieldFocused, - !isPreventingUpdate, + !appState.isPreventingUpdate, let newColor = NSColor(cssRGBString: inputColorText.trimmingCharacters(in: .whitespaces)) { colorPanel.color = newColor @@ -216,13 +215,13 @@ struct ColorInputView: View { textColor = .red } - if !isPreventingUpdate { - // updateColorsFromPanel(excludeRGB: true, preventUpdate: true) + if !appState.isPreventingUpdate { + appState.updateColorsFromPanel(excludeRGB: true, preventUpdate: true, color: colorPanel.color) } case .lch: if isTextFieldFocused, - !isPreventingUpdate, + !appState.isPreventingUpdate, let newColor = NSColor(cssLCHString: inputColorText.trimmingCharacters(in: .whitespaces)) { colorPanel.color = newColor @@ -234,8 +233,8 @@ struct ColorInputView: View { textColor = .red } - if !isPreventingUpdate { - // updateColorsFromPanel(excludeLCH: true, preventUpdate: true) + if !appState.isPreventingUpdate { + appState.updateColorsFromPanel(excludeLCH: true, preventUpdate: true, color: colorPanel.color) } } } @@ -267,16 +266,10 @@ struct ColorPickerScreen: View { @Default(.legacyColorSyntax) private var legacyColorSyntax @Default(.shownColorFormats) private var shownColorFormats @Default(.largerText) private var largerText - @State private var hexColor = "" - @State private var hslColor = "" - @State private var rgbColor = "" - @State private var lchColor = "" @State private var isTextFieldFocusedHex = false @State private var isTextFieldFocusedHSL = false @State private var isTextFieldFocusedRGB = false @State private var isTextFieldFocusedLCH = false - @State private var isPreventingUpdate = false - @State private var textColor = Color.primary let colorPanel: NSColorPanel @@ -294,9 +287,8 @@ struct ColorPickerScreen: View { BarView() if shownColorFormats.contains(.hex) { ColorInputView( - inputColorText: $hexColor, + inputColorText: $appState.hexColor, isTextFieldFocused: $isTextFieldFocusedHex, - isPreventingUpdate: $isPreventingUpdate, colorPanel: colorPanel, textFieldFontSize: textFieldFontSize, inputColorType: .hex @@ -304,9 +296,8 @@ struct ColorPickerScreen: View { } if shownColorFormats.contains(.hsl) { ColorInputView( - inputColorText: $hslColor, + inputColorText: $appState.hslColor, isTextFieldFocused: $isTextFieldFocusedHSL, - isPreventingUpdate: $isPreventingUpdate, colorPanel: colorPanel, textFieldFontSize: textFieldFontSize, inputColorType: .hsl @@ -314,9 +305,8 @@ struct ColorPickerScreen: View { } if shownColorFormats.contains(.rgb) { ColorInputView( - inputColorText: $rgbColor, + inputColorText: $appState.rgbColor, isTextFieldFocused: $isTextFieldFocusedRGB, - isPreventingUpdate: $isPreventingUpdate, colorPanel: colorPanel, textFieldFontSize: textFieldFontSize, inputColorType: .rgb @@ -324,9 +314,8 @@ struct ColorPickerScreen: View { } if shownColorFormats.contains(.lch) { ColorInputView( - inputColorText: $lchColor, + inputColorText: $appState.lchColor, isTextFieldFocused: $isTextFieldFocusedLCH, - isPreventingUpdate: $isPreventingUpdate, colorPanel: colorPanel, textFieldFontSize: textFieldFontSize, inputColorType: .lch @@ -337,61 +326,23 @@ struct ColorPickerScreen: View { // 244 makes `HSL` always fit in the text field. .frame(minWidth: 244, maxWidth: .infinity) .onAppear { - updateColorsFromPanel() + appState.updateColorsFromPanel(color: colorPanel.color) } .onChange(of: uppercaseHexColor) { _ in - updateColorsFromPanel() + appState.updateColorsFromPanel(color: colorPanel.color) } .onChange(of: hashPrefixInHexColor) { _ in - updateColorsFromPanel() + appState.updateColorsFromPanel(color: colorPanel.color) } .onChange(of: legacyColorSyntax) { _ in - updateColorsFromPanel() + appState.updateColorsFromPanel(color: colorPanel.color) } .onReceive(colorPanel.colorDidChangePublisher) { guard !isAnyTextFieldFocused else { - return - } - - updateColorsFromPanel(preventUpdate: true) - } - } - - // TODO: Find a better way to handle this. - private func updateColorsFromPanel( - excludeHex: Bool = false, - excludeHSL: Bool = false, - excludeRGB: Bool = false, - excludeLCH: Bool = false, - preventUpdate: Bool = false - ) { - if preventUpdate { - isPreventingUpdate = true - } - - let color = colorPanel.color - - if !excludeHex { - hexColor = color.hexColorString - } - - if !excludeHSL { - hslColor = color.hslColorString - } - - if !excludeRGB { - rgbColor = color.rgbColorString - } - - if !excludeLCH { - lchColor = color.lchColorString - } - - if preventUpdate { - DispatchQueue.main.async { - isPreventingUpdate = false + return + } + appState.updateColorsFromPanel(preventUpdate: true, color: colorPanel.color) } - } } }