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 ece1d0e..4e88061 100644 --- a/Color Picker/ColorPickerScreen.swift +++ b/Color Picker/ColorPickerScreen.swift @@ -127,6 +127,138 @@ 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 + let colorPanel: NSColorPanel + let textFieldFontSize: Double + let inputColorType: ColorFormat + + var body: some View { + HStack { + // TODO: When I use `TextField`, add the copy button using `.safeAreaInset()`. + 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 + } + } + + var hexColor = $0 + + if hexColor.hasPrefix("##") { + hexColor = hexColor.dropFirst().toString + inputColorText = hexColor + } + + if + isTextFieldFocused, + !appState.isPreventingUpdate, + let newColor = NSColor(hexString: inputColorText.trimmingCharacters(in: .whitespaces)) + { + colorPanel.color = newColor + } + + if NSColor(hexString: inputColorText.trimmingCharacters(in: .whitespaces)) != nil { + textColor = .primary + } else { + textColor = .red + } + + if !appState.isPreventingUpdate { + appState.updateColorsFromPanel(excludeHex: true, preventUpdate: true, color: colorPanel.color) + } + case .hsl: + if + isTextFieldFocused, + !appState.isPreventingUpdate, + let newColor = NSColor(cssHSLString: inputColorText.trimmingCharacters(in: .whitespaces)) + { + colorPanel.color = newColor + } + + if NSColor(cssHSLString: inputColorText.trimmingCharacters(in: .whitespaces)) != nil { + textColor = .primary + } else { + textColor = .red + } + + if !appState.isPreventingUpdate { + appState.updateColorsFromPanel(excludeHSL: true, preventUpdate: true, color: colorPanel.color) + } + case .rgb: + if + isTextFieldFocused, + !appState.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 !appState.isPreventingUpdate { + appState.updateColorsFromPanel(excludeRGB: true, preventUpdate: true, color: colorPanel.color) + } + case .lch: + if + isTextFieldFocused, + !appState.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 !appState.isPreventingUpdate { + appState.updateColorsFromPanel(excludeLCH: true, preventUpdate: true, color: colorPanel.color) + } + } + } + Button("Copy \(inputColorType.rawValue)", systemImage: "doc.on.doc.fill") { + switch inputColorType { + case .hex: + appState.colorPanel.color.hexColorString.copyToPasteboard() + case .hsl: + appState.colorPanel.color.hslColorString.copyToPasteboard() + case .rgb: + appState.colorPanel.color.rgbColorString.copyToPasteboard() + case .lch: + appState.colorPanel.color.lchColorString.copyToPasteboard() + } + } + .labelStyle(.iconOnly) + .symbolRenderingMode(.hierarchical) + .buttonStyle(.borderless) + .contentShape(.rectangle) + .keyboardShortcut(inputColorType.keyboardShortcut, modifiers: [.shift, .command]) + } + } +} + struct ColorPickerScreen: View { @EnvironmentObject private var appState: AppState @Default(.uppercaseHexColor) private var uppercaseHexColor @@ -134,15 +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 let colorPanel: NSColorPanel @@ -155,222 +282,83 @@ 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 - ) - .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 - ) - .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( - text: $rgbColor, - placeholder: "RGB", - font: .monospacedSystemFont(ofSize: textFieldFontSize, weight: .regular), - isFocused: $isTextFieldFocusedRGB - ) - .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 - ) - .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() if shownColorFormats.contains(.hex) { - hexColorView + ColorInputView( + inputColorText: $appState.hexColor, + isTextFieldFocused: $isTextFieldFocusedHex, + colorPanel: colorPanel, + textFieldFontSize: textFieldFontSize, + inputColorType: .hex + ) } if shownColorFormats.contains(.hsl) { - hslColorView + ColorInputView( + inputColorText: $appState.hslColor, + isTextFieldFocused: $isTextFieldFocusedHSL, + colorPanel: colorPanel, + textFieldFontSize: textFieldFontSize, + inputColorType: .hsl + ) } if shownColorFormats.contains(.rgb) { - rgbColorView + ColorInputView( + inputColorText: $appState.rgbColor, + isTextFieldFocused: $isTextFieldFocusedRGB, + colorPanel: colorPanel, + textFieldFontSize: textFieldFontSize, + inputColorType: .rgb + ) } if shownColorFormats.contains(.lch) { - lchColorView + ColorInputView( + inputColorText: $appState.lchColor, + isTextFieldFocused: $isTextFieldFocusedLCH, + colorPanel: colorPanel, + textFieldFontSize: textFieldFontSize, + inputColorType: .lch + ) } } .padding(9) // 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) + return + } + appState.updateColorsFromPanel(preventUpdate: true, color: colorPanel.color) } } +} - // 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 - } - } - } +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 { 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