Skip to content

Commit

Permalink
New UI (jordanbaird#346)
Browse files Browse the repository at this point in the history
* Begin implementing new UI

* Update IcePicker.swift

* Migrate General settings pane to new UI

* Remove old General settings pane

* Fix custom image size in picker

* Implement IceMenu and fix menus in General settings pane

* Implement IceForm

* Implement more of the UI

* More UI rewrites

* Rename Annotation.swift to AnnotationView.swift

* Reworks

* Update IceMenu

* Update IceMenu

* Tweaks

* Remove rectangle mask

* More IceMenu tweaks

* More IceMenu tweaks

* Update GeneralSettingsPane.swift

* Rework IceSlider

* Update .swiftlint.yml

* Update IceSection.swift

* Tweaks

* Misc changes

* Rework SettingsView sidebar

* Fix key repeat in SectionedList

* Fix content margins in SectionedList

* Update SettingsView.swift

* Fix launch behavior on macOS 15

* Update SettingsWindow.swift

* More macOS 15 tweaks

* Misc tweaks

* Fix launch behavior on macOS 14

* Use IceSection in PermissionsView
  • Loading branch information
jordanbaird authored Sep 11, 2024
1 parent 0e2ccc6 commit 9bf6762
Show file tree
Hide file tree
Showing 29 changed files with 1,192 additions and 433 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ disabled_rules:
- file_length
- function_body_length
- function_parameter_count
- generic_type_name
- identifier_name
- large_tuple
- line_length
Expand Down
64 changes: 54 additions & 10 deletions Ice.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions Ice/Hotkeys/HotkeyRecorder/HotkeyRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@ struct HotkeyRecorder<Label: View>: View {
}

var body: some View {
LabeledContent {
IceLabeledContent {
HStack(spacing: 1) {
leadingSegment
trailingSegment
}
.frame(width: 130, height: 22)
.alignmentGuide(.firstTextBaseline) { dimension in
dimension[VerticalAlignment.center]
}
} label: {
label
.alignmentGuide(.firstTextBaseline) { dimension in
dimension[VerticalAlignment.center]
}
}
.alert(
"Hotkey is reserved by macOS",
Expand Down Expand Up @@ -128,9 +134,8 @@ private struct HotkeyRecorderSegmentButtonStyle: PrimitiveButtonStyle {
}

func makeBody(configuration: Configuration) -> some View {
UnevenRoundedRectangle(cornerRadii: radii)
.foregroundStyle(Color.primary) // explicitly specify `Color.primary`
.opacity(isHighlighted || isPressed ? 0.2 : 0.1)
UnevenRoundedRectangle(cornerRadii: radii, style: .circular)
.fill(isHighlighted || isPressed ? .tertiary : .quaternary)
.overlay {
configuration.label
.lineLimit(1)
Expand Down
26 changes: 12 additions & 14 deletions Ice/Main/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import OSLog
import SwiftUI

@MainActor
class AppDelegate: NSObject, NSApplicationDelegate {
final class AppDelegate: NSObject, NSApplicationDelegate {
private weak var appState: AppState?

// MARK: NSApplicationDelegate Methods
Expand All @@ -34,25 +34,23 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return
}

// Assign and close the various windows.
let windowAssignments: KeyValuePairs = [
Constants.settingsWindowID: appState.assignSettingsWindow,
Constants.permissionsWindowID: appState.assignPermissionsWindow,
]
for (identifier, assign) in windowAssignments {
if let window = NSApp.window(withIdentifier: identifier) {
assign(window)
window.close()
}
}

// Hide the main menu to make more space in the menu bar.
if let mainMenu = NSApp.mainMenu {
for item in mainMenu.items {
item.isHidden = true
}
}

// On macOS 15, the windows handle their own closure. If on macOS 14,
// close them here.
//
// NOTE: The windows might not close when running from Xcode, but it
// does work when running standalone.
if #unavailable(macOS 15.0) {
appState.settingsWindow?.close()
appState.permissionsWindow?.close()
}

if !appState.isPreview {
// If we have the required permissions, set up the shared app state.
// Otherwise, open the permissions window.
Expand Down Expand Up @@ -95,7 +93,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let appState,
let settingsWindow = appState.settingsWindow
else {
Logger.appDelegate.warning("Failed to open settings window")
Logger.appDelegate.error("Failed to open settings window")
return
}
// Small delay makes this more reliable.
Expand Down
74 changes: 28 additions & 46 deletions Ice/Main/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ final class AppState: ObservableObject {
/// A Boolean value that indicates whether the active space is fullscreen.
@Published private(set) var isActiveSpaceFullscreen = Bridging.isSpaceFullscreen(Bridging.activeSpaceID)

private var cancellables = Set<AnyCancellable>()

/// Manager for events received by the app.
private(set) lazy var eventManager = EventManager(appState: self)

Expand All @@ -33,9 +31,6 @@ final class AppState: ObservableObject {
/// Global cache for menu bar item images.
private(set) lazy var imageCache = MenuBarItemImageCache(appState: self)

/// The app's hotkey registry.
nonisolated let hotkeyRegistry = HotkeyRegistry()

/// Manager for menu bar item spacing.
let spacingManager = MenuBarItemSpacingManager()

Expand All @@ -45,26 +40,17 @@ final class AppState: ObservableObject {
/// Model for app-wide navigation.
let navigationState = AppNavigationState()

/// The app's hotkey registry.
nonisolated let hotkeyRegistry = HotkeyRegistry()

/// The app's delegate.
private(set) weak var appDelegate: AppDelegate?

/// The window that contains the settings interface.
private(set) weak var settingsWindow: NSWindow?

/// The window that contains the permissions interface.
private(set) weak var permissionsWindow: NSWindow?

/// A Boolean value that indicates whether the "ShowOnHover" feature is prevented.
private(set) var isShowOnHoverPrevented = false

/// A Boolean value that indicates whether the application can set the
/// cursor in the background.
///
/// The default value of this property is `false`.
var setsCursorInBackground: Bool {
get { Bridging.getConnectionProperty(forKey: "SetsCursorInBackground") as? Bool ?? false }
set { Bridging.setConnectionProperty(newValue, forKey: "SetsCursorInBackground") }
}
/// Storage for internal observers.
private var cancellables = Set<AnyCancellable>()

/// A Boolean value that indicates whether the app is running as a SwiftUI preview.
let isPreview: Bool = {
Expand All @@ -77,8 +63,21 @@ final class AppState: ObservableObject {
#endif
}()

init() {
MigrationManager(appState: self).migrateAll()
/// The window that contains the settings interface.
var settingsWindow: NSWindow? {
NSApp.window(withIdentifier: Constants.settingsWindowID)
}

/// The window that contains the permissions interface.
var permissionsWindow: NSWindow? {
NSApp.window(withIdentifier: Constants.permissionsWindowID)
}

/// A Boolean value that indicates whether the application can set the cursor
/// in the background.
var setsCursorInBackground: Bool {
get { Bridging.getConnectionProperty(forKey: "SetsCursorInBackground") as? Bool ?? false }
set { Bridging.setConnectionProperty(newValue, forKey: "SetsCursorInBackground") }
}

private func configureCancellables() {
Expand All @@ -88,12 +87,12 @@ final class AppState: ObservableObject {
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.activeSpaceDidChangeNotification)
.mapToVoid(),
// frontmost application change can indicate a space change from one display to
// another, which gets ignored by `NSWorkspace.activeSpaceDidChangeNotification`
// Frontmost application change can indicate a space change from one display to
// another, which gets ignored by NSWorkspace.activeSpaceDidChangeNotification.
NSWorkspace.shared
.publisher(for: \.frontmostApplication)
.mapToVoid(),
// clicking into a fullscreen space from another space is also ignored
// Clicking into a fullscreen space from another space is also ignored.
UniversalEventMonitor
.publisher(for: .leftMouseDown)
.delay(for: 0.1, scheduler: DispatchQueue.main)
Expand Down Expand Up @@ -127,6 +126,8 @@ final class AppState: ObservableObject {
navigationState.isSettingsPresented = isVisible
}
.store(in: &c)
} else {
Logger.appState.warning("No settings window!")
}

Publishers.Merge(
Expand Down Expand Up @@ -176,7 +177,6 @@ final class AppState: ObservableObject {
settingsManager.performSetup()
itemManager.performSetup()
imageCache.performSetup()
permissionsWindow?.close()
}

/// Assigns the app delegate to the app state.
Expand All @@ -188,28 +188,10 @@ final class AppState: ObservableObject {
self.appDelegate = appDelegate
}

/// Assigns the settings window to the app state.
func assignSettingsWindow(_ settingsWindow: NSWindow) {
guard self.settingsWindow == nil else {
Logger.appState.warning("Multiple attempts made to assign settings window")
return
}
self.settingsWindow = settingsWindow
}

/// Assigns the permissions window to the app state.
func assignPermissionsWindow(_ permissionsWindow: NSWindow) {
guard self.permissionsWindow == nil else {
Logger.appState.warning("Multiple attempts made to assign permissions window")
return
}
self.permissionsWindow = permissionsWindow
}

/// Activates the app and sets its activation policy to the given value.
func activate(withPolicy policy: NSApplication.ActivationPolicy) {
// store whether the app has previously activated inside an internal
// context to keep it isolated
// Store whether the app has previously activated inside an internal
// context to keep it isolated.
enum Context {
static let hasActivated = ObjectAssociation<Bool>()
}
Expand All @@ -228,7 +210,7 @@ final class AppState: ObservableObject {
} else {
Context.hasActivated[self] = true
Logger.appState.debug("First time activating app, so going through Dock")
// hack to make sure the app properly activates for the first time
// Hack to make sure the app properly activates for the first time.
NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dock").first?.activate()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
activate()
Expand Down
15 changes: 3 additions & 12 deletions Ice/Main/IceApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,16 @@ import SwiftUI
struct IceApp: App {
@NSApplicationDelegateAdaptor var appDelegate: AppDelegate
@ObservedObject var appState = AppState()
@Environment(\.openWindow) private var openWindow

init() {
NSSplitViewItem.swizzle()
IceBarPanel.swizzle()
// Occurs before AppDelegate.applicationWillFinishLaunching(_:).
MigrationManager(appState: appState).migrateAll()
appDelegate.assignAppState(appState)
}

var body: some Scene {
SettingsWindow(appState: appState, onAppear: {
// Open the permissions window no matter what, so that we can
// reference it. We'll close it in AppDelegate if permissions
// have already been granted.
openWindow(id: Constants.permissionsWindowID)
})
PermissionsWindow(appState: appState, onContinue: {
appState.performSetup()
openWindow(id: Constants.settingsWindowID)
})
SettingsWindow(appState: appState)
PermissionsWindow(appState: appState)
}
}
28 changes: 23 additions & 5 deletions Ice/Permissions/PermissionsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ struct PermissionsView: View {
@EnvironmentObject var permissionsManager: PermissionsManager
@Environment(\.openWindow) private var openWindow

let onContinue: () -> Void

var body: some View {
VStack(spacing: 0) {
headerView
Expand All @@ -24,6 +22,21 @@ struct PermissionsView: View {
}
.padding(.horizontal)
.fixedSize()
.readWindow { window in
guard let window else {
return
}
window.styleMask.remove([.closable, .miniaturizable])
if let contentView = window.contentView {
with(contentView.safeAreaInsets) { insets in
insets.bottom = -insets.bottom
insets.left = -insets.left
insets.right = -insets.right
insets.top = -insets.top
contentView.additionalSafeAreaInsets = insets
}
}
}
}

@ViewBuilder
Expand All @@ -43,7 +56,7 @@ struct PermissionsView: View {

@ViewBuilder
private var explanationView: some View {
GroupBox {
IceSection {
VStack {
Text("Ice needs permission to manage the menu bar.")
Text("Absolutely no personal information is collected or stored.")
Expand Down Expand Up @@ -86,7 +99,12 @@ struct PermissionsView: View {
@ViewBuilder
private var continueButton: some View {
Button {
onContinue()
guard let appState = permissionsManager.appState else {
return
}
appState.performSetup()
appState.permissionsWindow?.close()
appState.appDelegate?.openSettingsWindow()
} label: {
Text("Continue")
.frame(maxWidth: .infinity)
Expand All @@ -96,7 +114,7 @@ struct PermissionsView: View {

@ViewBuilder
private func permissionBox(_ permission: Permission) -> some View {
GroupBox {
IceSection {
VStack(spacing: 10) {
Text(permission.title)
.font(.title)
Expand Down
Loading

0 comments on commit 9bf6762

Please sign in to comment.