diff --git a/Ice.xcodeproj/project.pbxproj b/Ice.xcodeproj/project.pbxproj index 771b3969..77743875 100644 --- a/Ice.xcodeproj/project.pbxproj +++ b/Ice.xcodeproj/project.pbxproj @@ -107,7 +107,6 @@ 7133ED5E2A853FCF000A7E1B /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7133ED5D2A853FCF000A7E1B /* Constants.swift */; }; 7133ED652A85811C000A7E1B /* NSApplication+windowWithIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7133ED642A85811C000A7E1B /* NSApplication+windowWithIdentifier.swift */; }; 7133ED6A2A85870E000A7E1B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7133ED692A85870E000A7E1B /* SettingsView.swift */; }; - 7133ED712A85AE6A000A7E1B /* SettingsNavigationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7133ED702A85AE6A000A7E1B /* SettingsNavigationItem.swift */; }; 7150A7AD2AA4265F0045EA68 /* KeyCombination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7150A7AC2AA4265F0045EA68 /* KeyCombination.swift */; }; 7150A7AF2AA426C60045EA68 /* HotkeyRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7150A7AE2AA426C60045EA68 /* HotkeyRegistry.swift */; }; 7150A7B12AA427F80045EA68 /* KeyCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7150A7B02AA427F80045EA68 /* KeyCode.swift */; }; @@ -128,6 +127,11 @@ 71C400322AAD76A8006FDB1C /* ReadWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C400312AAD76A8006FDB1C /* ReadWindow.swift */; }; 71D2B02E2AAFDD5C0002B6C8 /* Bundle+versionString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D2B02D2AAFDD5C0002B6C8 /* Bundle+versionString.swift */; }; 71D36C4F2A88FE4900D89CD5 /* SettingsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D36C4E2A88FE4900D89CD5 /* SettingsWindow.swift */; }; + 71F4280F2C3B5C870025706C /* SettingsNavigationIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F4280E2C3B5C870025706C /* SettingsNavigationIdentifier.swift */; }; + 71F428112C3B5CE90025706C /* NavigationIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F428102C3B5CE90025706C /* NavigationIdentifier.swift */; }; + 71F428132C3B5E030025706C /* IconResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F428122C3B5E030025706C /* IconResource.swift */; }; + 71F428152C3B5F3D0025706C /* AppNavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F428142C3B5F3D0025706C /* AppNavigationState.swift */; }; + 71F428172C3B60070025706C /* AppNavigationIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F428162C3B60070025706C /* AppNavigationIdentifier.swift */; }; 71FEA2542A8D701B0048341A /* LocalEventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FEA2532A8D701B0048341A /* LocalEventMonitor.swift */; }; /* End PBXBuildFile section */ @@ -230,7 +234,6 @@ 7133ED5D2A853FCF000A7E1B /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 7133ED642A85811C000A7E1B /* NSApplication+windowWithIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+windowWithIdentifier.swift"; sourceTree = ""; }; 7133ED692A85870E000A7E1B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 7133ED702A85AE6A000A7E1B /* SettingsNavigationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNavigationItem.swift; sourceTree = ""; }; 7150A7AC2AA4265F0045EA68 /* KeyCombination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCombination.swift; sourceTree = ""; }; 7150A7AE2AA426C60045EA68 /* HotkeyRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyRegistry.swift; sourceTree = ""; }; 7150A7B02AA427F80045EA68 /* KeyCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCode.swift; sourceTree = ""; }; @@ -253,6 +256,11 @@ 71C400312AAD76A8006FDB1C /* ReadWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadWindow.swift; sourceTree = ""; }; 71D2B02D2AAFDD5C0002B6C8 /* Bundle+versionString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+versionString.swift"; sourceTree = ""; }; 71D36C4E2A88FE4900D89CD5 /* SettingsWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindow.swift; sourceTree = ""; }; + 71F4280E2C3B5C870025706C /* SettingsNavigationIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNavigationIdentifier.swift; sourceTree = ""; }; + 71F428102C3B5CE90025706C /* NavigationIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationIdentifier.swift; sourceTree = ""; }; + 71F428122C3B5E030025706C /* IconResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconResource.swift; sourceTree = ""; }; + 71F428142C3B5F3D0025706C /* AppNavigationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNavigationState.swift; sourceTree = ""; }; + 71F428162C3B60070025706C /* AppNavigationIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNavigationIdentifier.swift; sourceTree = ""; }; 71FEA2532A8D701B0048341A /* LocalEventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalEventMonitor.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -595,7 +603,6 @@ 7133ED662A858230000A7E1B /* Settings */ = { isa = PBXGroup; children = ( - 7133ED702A85AE6A000A7E1B /* SettingsNavigationItem.swift */, 7133ED692A85870E000A7E1B /* SettingsView.swift */, 71D36C4E2A88FE4900D89CD5 /* SettingsWindow.swift */, 17B331D12B991D180084EBB0 /* SettingsManagers */, @@ -662,6 +669,7 @@ 17C261E72B5AEB7A0076F129 /* Main */, 7166834E2A768190006ABF84 /* MenuBar */, 179F13BC2B91E7F700EC6B52 /* MenuBarAppearance */, + 71F4280B2C3B5BF00025706C /* Navigation */, 1787C42A2B16ADF2002F50DF /* Permissions */, 7133ED662A858230000A7E1B /* Settings */, 71D36C522A89443800D89CD5 /* UI */, @@ -683,6 +691,7 @@ 17DFF4BF2AD8DBC500B5177A /* CodableColor.swift */, 7133ED5D2A853FCF000A7E1B /* Constants.swift */, 17928F192AC5DF9C0016C615 /* Defaults.swift */, + 71F428122C3B5E030025706C /* IconResource.swift */, 17F998452C16C78000D75EC0 /* Injection.swift */, 17C303772BA62CD20079755B /* MigrationManager.swift */, 17F998312C15C91400D75EC0 /* MouseCursor.swift */, @@ -767,6 +776,25 @@ path = UI; sourceTree = ""; }; + 71F4280B2C3B5BF00025706C /* Navigation */ = { + isa = PBXGroup; + children = ( + 71F428142C3B5F3D0025706C /* AppNavigationState.swift */, + 71F428182C3B605B0025706C /* NavigationIdentifiers */, + ); + path = Navigation; + sourceTree = ""; + }; + 71F428182C3B605B0025706C /* NavigationIdentifiers */ = { + isa = PBXGroup; + children = ( + 71F428162C3B60070025706C /* AppNavigationIdentifier.swift */, + 71F428102C3B5CE90025706C /* NavigationIdentifier.swift */, + 71F4280E2C3B5C870025706C /* SettingsNavigationIdentifier.swift */, + ); + path = NavigationIdentifiers; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -894,6 +922,7 @@ 17F9983E2C16562300D75EC0 /* LayoutBarItemView.swift in Sources */, 17F998322C15C91400D75EC0 /* MouseCursor.swift in Sources */, 171C6FC92C0659340097A5C8 /* Task+timeout.swift in Sources */, + 71F428132C3B5E030025706C /* IconResource.swift in Sources */, 17F71BB22B87E37800905CBA /* RehideStrategy.swift in Sources */, 17F9984F2C16CF8600D75EC0 /* LayoutBarPaddingView.swift in Sources */, 17B380F32ADCBC8A0002C9C3 /* OnKeyDown.swift in Sources */, @@ -901,6 +930,7 @@ 17543EB12C2C7A100052711E /* RunLoopLocalEventMonitor.swift in Sources */, 17BE3DD02C1A45B3008B98EF /* IceBar.swift in Sources */, 17C303782BA62CD20079755B /* MigrationManager.swift in Sources */, + 71F4280F2C3B5C870025706C /* SettingsNavigationIdentifier.swift in Sources */, 17CDCACD2C2E6F6A000B1CFF /* Deprecated.swift in Sources */, 17CC22B52B8A55E7001A0582 /* WindowInfo.swift in Sources */, 17CC22B02B8A0CA6001A0582 /* HotkeysSettingsPane.swift in Sources */, @@ -931,11 +961,13 @@ 179AC2FC2C1146EF0051E7B0 /* RemoveSidebarToggle.swift in Sources */, 17B7F32B2B264C1800CDCF49 /* MenuBarAppearanceManager.swift in Sources */, 17F9984D2C16CD8E00D75EC0 /* MenuBarItemsSettingsPane.swift in Sources */, + 71F428152C3B5F3D0025706C /* AppNavigationState.swift in Sources */, 17D1AC8D2B97BB5900726180 /* MenuBarItem.swift in Sources */, 71553E372C378BF50083F5BE /* Notifications.swift in Sources */, 173C248C2B8E821C0096F7A1 /* UpdatesSettingsPane.swift in Sources */, 171C6F9C2C0356BC0097A5C8 /* GeneralSettingsPane.swift in Sources */, 7150A7B12AA427F80045EA68 /* KeyCode.swift in Sources */, + 71F428172C3B60070025706C /* AppNavigationIdentifier.swift in Sources */, 71829E0A2C2FDCC200503604 /* NSScreen+getMenuBarHeight.swift in Sources */, 17B331D02B9916E70084EBB0 /* SettingsManager.swift in Sources */, 17B331D32B991D290084EBB0 /* GeneralSettingsManager.swift in Sources */, @@ -963,10 +995,10 @@ 718152C32C34E595005564AA /* NSScreen+screenWithMouse.swift in Sources */, 17F998532C16EFCD00D75EC0 /* CGImage+averageColor.swift in Sources */, 71287C602C3189AC0028706E /* IceBarColorManager.swift in Sources */, + 71F428112C3B5CE90025706C /* NavigationIdentifier.swift in Sources */, 17DFF4C02AD8DBC500B5177A /* CodableColor.swift in Sources */, 17CC22BC2B8CDE6F001A0582 /* EventManager.swift in Sources */, 1737E3E52BA3C4AA009F0EFA /* HotkeyAction.swift in Sources */, - 7133ED712A85AE6A000A7E1B /* SettingsNavigationItem.swift in Sources */, 17EE69682C277B4E00F778A7 /* Publisher+mapToVoid.swift in Sources */, 17EE69652C27777C00F778A7 /* MenuBarItemImageCache.swift in Sources */, 174AA5D62B71D97100E3FE74 /* MenuBarOverlayPanel.swift in Sources */, diff --git a/Ice/Main/AppState.swift b/Ice/Main/AppState.swift index 174503e1..dd0e62b8 100644 --- a/Ice/Main/AppState.swift +++ b/Ice/Main/AppState.swift @@ -39,6 +39,9 @@ final class AppState: ObservableObject { /// Manager for app updates. let updatesManager = UpdatesManager() + /// Model for app-wide navigation. + let navigationState = AppNavigationState() + /// The app's delegate. private(set) weak var appDelegate: AppDelegate? @@ -101,6 +104,15 @@ final class AppState: ObservableObject { } .store(in: &c) + if let settingsWindow { + settingsWindow.publisher(for: \.isVisible) + .receive(on: DispatchQueue.main) + .sink { [weak self] isVisible in + self?.navigationState.appNavigationIdentifier = if isVisible { .settings } else { .idle } + } + .store(in: &c) + } + menuBarManager.objectWillChange .sink { [weak self] in self?.objectWillChange.send() diff --git a/Ice/Navigation/AppNavigationState.swift b/Ice/Navigation/AppNavigationState.swift new file mode 100644 index 00000000..8249f725 --- /dev/null +++ b/Ice/Navigation/AppNavigationState.swift @@ -0,0 +1,13 @@ +// +// AppNavigationState.swift +// Ice +// + +import Combine + +/// The model for app-wide navigation. +@MainActor +final class AppNavigationState: ObservableObject { + @Published var appNavigationIdentifier: AppNavigationIdentifier = .idle + @Published var settingsNavigationIdentifier: SettingsNavigationIdentifier = .general +} diff --git a/Ice/Navigation/NavigationIdentifiers/AppNavigationIdentifier.swift b/Ice/Navigation/NavigationIdentifiers/AppNavigationIdentifier.swift new file mode 100644 index 00000000..ede119a3 --- /dev/null +++ b/Ice/Navigation/NavigationIdentifiers/AppNavigationIdentifier.swift @@ -0,0 +1,10 @@ +// +// AppNavigationIdentifier.swift +// Ice +// + +/// An identifier used for app-wide navigation. +enum AppNavigationIdentifier: String, NavigationIdentifier { + case idle + case settings +} diff --git a/Ice/Navigation/NavigationIdentifiers/NavigationIdentifier.swift b/Ice/Navigation/NavigationIdentifiers/NavigationIdentifier.swift new file mode 100644 index 00000000..3d8f87ff --- /dev/null +++ b/Ice/Navigation/NavigationIdentifiers/NavigationIdentifier.swift @@ -0,0 +1,20 @@ +// +// NavigationIdentifier.swift +// Ice +// + +import SwiftUI + +/// A type that represents an identifier used for navigation in a user interface. +protocol NavigationIdentifier: CaseIterable, Hashable, Identifiable, RawRepresentable { + /// A localized description of the identifier that can be presented to the user. + var localized: LocalizedStringKey { get } +} + +extension NavigationIdentifier where ID == Int { + var id: Int { hashValue } +} + +extension NavigationIdentifier where RawValue == String { + var localized: LocalizedStringKey { LocalizedStringKey(rawValue) } +} diff --git a/Ice/Navigation/NavigationIdentifiers/SettingsNavigationIdentifier.swift b/Ice/Navigation/NavigationIdentifiers/SettingsNavigationIdentifier.swift new file mode 100644 index 00000000..027da58d --- /dev/null +++ b/Ice/Navigation/NavigationIdentifiers/SettingsNavigationIdentifier.swift @@ -0,0 +1,15 @@ +// +// SettingsNavigationIdentifier.swift +// Ice +// + +/// An identifier used for navigation in the settings interface. +enum SettingsNavigationIdentifier: String, NavigationIdentifier { + case general = "General" + case menuBarItems = "Menu Bar Items" + case menuBarAppearance = "Menu Bar Appearance" + case hotkeys = "Hotkeys" + case advanced = "Advanced" + case updates = "Updates" + case about = "About" +} diff --git a/Ice/Settings/SettingsNavigationItem.swift b/Ice/Settings/SettingsNavigationItem.swift deleted file mode 100644 index d8af8b07..00000000 --- a/Ice/Settings/SettingsNavigationItem.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// SettingsNavigationItem.swift -// Ice -// - -import SwiftUI - -struct SettingsNavigationItem: Hashable, Identifiable { - let name: Name - let icon: IconResource - var id: Int { name.hashValue } -} - -extension SettingsNavigationItem { - enum Name: String { - case general = "General" - case menuBarItems = "Menu Bar Items" - case menuBarAppearance = "Menu Bar Appearance" - case hotkeys = "Hotkeys" - case advanced = "Advanced" - case updates = "Updates" - case about = "About" - - var localized: LocalizedStringKey { - LocalizedStringKey(rawValue) - } - } -} - -extension SettingsNavigationItem { - enum IconResource: Hashable { - case systemSymbol(_ name: String) - case assetCatalog(_ resource: ImageResource) - - var view: some View { - image.resizable().aspectRatio(contentMode: .fit) - } - - private var image: Image { - switch self { - case .systemSymbol(let name): - Image(systemName: name) - case .assetCatalog(let resource): - Image(resource) - } - } - } -} diff --git a/Ice/Settings/SettingsView.swift b/Ice/Settings/SettingsView.swift index 627deeb1..cb1ccc66 100644 --- a/Ice/Settings/SettingsView.swift +++ b/Ice/Settings/SettingsView.swift @@ -6,38 +6,7 @@ import SwiftUI struct SettingsView: View { - private static let items: [SettingsNavigationItem] = [ - SettingsNavigationItem( - name: .general, - icon: .systemSymbol("gearshape") - ), - SettingsNavigationItem( - name: .menuBarItems, - icon: .systemSymbol("menubar.rectangle") - ), - SettingsNavigationItem( - name: .menuBarAppearance, - icon: .systemSymbol("paintpalette") - ), - SettingsNavigationItem( - name: .hotkeys, - icon: .systemSymbol("keyboard") - ), - SettingsNavigationItem( - name: .advanced, - icon: .systemSymbol("gearshape.2") - ), - SettingsNavigationItem( - name: .updates, - icon: .systemSymbol("arrow.triangle.2.circlepath.circle") - ), - SettingsNavigationItem( - name: .about, - icon: .assetCatalog(.iceCubeStroke) - ), - ] - - @State private var selection = Self.items[0] + @EnvironmentObject var navigationState: AppNavigationState var body: some View { NavigationSplitView { @@ -45,15 +14,15 @@ struct SettingsView: View { } detail: { detailView } - .navigationTitle(selection.name.localized) + .navigationTitle(navigationState.settingsNavigationIdentifier.localized) } @ViewBuilder private var sidebar: some View { - List(selection: $selection) { + List(selection: $navigationState.settingsNavigationIdentifier) { Section { - ForEach(Self.items, id: \.self) { item in - sidebarItem(item: item) + ForEach(SettingsNavigationIdentifier.allCases, id: \.self) { identifier in + sidebarItem(for: identifier) } } header: { HStack { @@ -76,7 +45,7 @@ struct SettingsView: View { @ViewBuilder private var detailView: some View { - switch selection.name { + switch navigationState.settingsNavigationIdentifier { case .general: GeneralSettingsPane() case .menuBarItems: @@ -95,20 +64,27 @@ struct SettingsView: View { } @ViewBuilder - private func sidebarItem(item: SettingsNavigationItem) -> some View { + private func sidebarItem(for identifier: SettingsNavigationIdentifier) -> some View { Label { - Text(item.name.localized) + Text(identifier.localized) .font(.title3) .padding(.leading, 2) } icon: { - item.icon.view + icon(for: identifier).view .foregroundStyle(.primary) } .frame(height: 30) } -} -#Preview { - SettingsView() - .environmentObject(AppState()) + private func icon(for identifier: SettingsNavigationIdentifier) -> IconResource { + switch identifier { + case .general: .systemSymbol("gearshape") + case .menuBarItems: .systemSymbol("menubar.rectangle") + case .menuBarAppearance: .systemSymbol("paintpalette") + case .hotkeys: .systemSymbol("keyboard") + case .advanced: .systemSymbol("gearshape.2") + case .updates: .systemSymbol("arrow.triangle.2.circlepath.circle") + case .about: .assetCatalog(.iceCubeStroke) + } + } } diff --git a/Ice/Settings/SettingsWindow.swift b/Ice/Settings/SettingsWindow.swift index 45c461be..7b5efc8b 100644 --- a/Ice/Settings/SettingsWindow.swift +++ b/Ice/Settings/SettingsWindow.swift @@ -15,6 +15,7 @@ struct SettingsWindow: Scene { .frame(minWidth: 825, minHeight: 500) .onAppear(perform: onAppear) .environmentObject(appState) + .environmentObject(appState.navigationState) } .commandsRemoved() .windowResizability(.contentSize) diff --git a/Ice/Utilities/IconResource.swift b/Ice/Utilities/IconResource.swift new file mode 100644 index 00000000..24964bf0 --- /dev/null +++ b/Ice/Utilities/IconResource.swift @@ -0,0 +1,31 @@ +// +// IconResource.swift +// Ice +// + +import SwiftUI + +/// A type that produces a view representing an icon. +enum IconResource: Hashable { + /// A resource derived from a system symbol. + case systemSymbol(_ name: String) + + /// A resource derived from an asset catalog. + case assetCatalog(_ resource: ImageResource) + + /// The view produced by the resource. + var view: some View { + image + .resizable() + .aspectRatio(contentMode: .fit) + } + + private var image: Image { + switch self { + case .systemSymbol(let name): + Image(systemName: name) + case .assetCatalog(let resource): + Image(resource) + } + } +}