Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.DS_Store
build/
xcuserdata/

# https://github.com/jordanbaird/Ice/pull/795
DerivedData/

78 changes: 78 additions & 0 deletions Ice/Settings/SettingsManagers/GeneralSettingsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Ice
//

import AppKit
import Combine
import Foundation

Expand All @@ -27,6 +28,17 @@ final class GeneralSettingsManager: ObservableObject {
/// in a separate bar below the menu bar.
@Published var useIceBar = false

/// A Boolean value that indicates whether Ice Bar should be
/// automatically enabled on built-in displays.
@Published var autoEnableIceBarOnBuiltInDisplay = false

/// The detection mode for automatic Ice Bar enabling.
@Published var iceBarAutoEnableMode: IceBarAutoEnableMode = .screenWidth

/// The screen width threshold (in pixels) below which Ice Bar is enabled.
/// Ice Bar will be enabled when screen width < threshold.
@Published var iceBarDisplayWidthThreshold: Double = 3000

/// The location where the Ice Bar appears.
@Published var iceBarLocation: IceBarLocation = .dynamic

Expand Down Expand Up @@ -78,12 +90,20 @@ final class GeneralSettingsManager: ObservableObject {
func performSetup() {
loadInitialState()
configureCancellables()
observeScreenChanges()
}

private func loadInitialState() {
Defaults.ifPresent(key: .showIceIcon, assign: &showIceIcon)
Defaults.ifPresent(key: .customIceIconIsTemplate, assign: &customIceIconIsTemplate)
Defaults.ifPresent(key: .useIceBar, assign: &useIceBar)
Defaults.ifPresent(key: .autoEnableIceBarOnBuiltInDisplay, assign: &autoEnableIceBarOnBuiltInDisplay)
Defaults.ifPresent(key: .iceBarAutoEnableMode) { rawValue in
if let mode = IceBarAutoEnableMode(rawValue: rawValue) {
iceBarAutoEnableMode = mode
}
}
Defaults.ifPresent(key: .iceBarDisplayWidthThreshold, assign: &iceBarDisplayWidthThreshold)
Defaults.ifPresent(key: .showOnClick, assign: &showOnClick)
Defaults.ifPresent(key: .showOnHover, assign: &showOnHover)
Defaults.ifPresent(key: .showOnScroll, assign: &showOnScroll)
Expand Down Expand Up @@ -156,6 +176,32 @@ final class GeneralSettingsManager: ObservableObject {
}
.store(in: &c)

$autoEnableIceBarOnBuiltInDisplay
.receive(on: DispatchQueue.main)
.sink { [weak self] autoEnable in
Defaults.set(autoEnable, forKey: .autoEnableIceBarOnBuiltInDisplay)
if autoEnable {
self?.updateIceBarForCurrentDisplay()
}
}
.store(in: &c)

$iceBarAutoEnableMode
.receive(on: DispatchQueue.main)
.sink { [weak self] mode in
Defaults.set(mode.rawValue, forKey: .iceBarAutoEnableMode)
self?.updateIceBarForCurrentDisplay()
}
.store(in: &c)

$iceBarDisplayWidthThreshold
.receive(on: DispatchQueue.main)
.sink { [weak self] threshold in
Defaults.set(threshold, forKey: .iceBarDisplayWidthThreshold)
self?.updateIceBarForCurrentDisplay()
}
.store(in: &c)

$iceBarLocation
.receive(on: DispatchQueue.main)
.sink { location in
Expand Down Expand Up @@ -215,6 +261,38 @@ final class GeneralSettingsManager: ObservableObject {

cancellables = c
}

/// Updates the Ice Bar setting based on the current display configuration.
private func updateIceBarForCurrentDisplay() {
guard autoEnableIceBarOnBuiltInDisplay else {
return
}

// Get the main screen (where the menu bar is)
guard let mainScreen = NSScreen.main else {
return
}

switch iceBarAutoEnableMode {
case .screenWidth:
// Enable Ice Bar if screen width is less than threshold
let screenWidth = mainScreen.frame.width
useIceBar = screenWidth < iceBarDisplayWidthThreshold
case .screensWithNotch:
// Enable Ice Bar only on screens with a notch
useIceBar = mainScreen.hasNotch
}
}

/// Sets up an observer for screen configuration changes.
func observeScreenChanges() {
NotificationCenter.default.publisher(for: NSApplication.didChangeScreenParametersNotification)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateIceBarForCurrentDisplay()
}
.store(in: &cancellables)
}
}

// MARK: GeneralSettingsManager: BindingExposable
Expand Down
41 changes: 41 additions & 0 deletions Ice/Settings/SettingsPanes/GeneralSettingsPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ struct GeneralSettingsPane: View {
@ViewBuilder
private var iceBarOptions: some View {
useIceBar
autoEnableIceBarToggle
if manager.autoEnableIceBarOnBuiltInDisplay {
onlyOnScreensWithNotchToggle
if manager.iceBarAutoEnableMode != .screensWithNotch {
widthThresholdInput
}
}
if manager.useIceBar {
iceBarLocationPicker
}
Expand All @@ -185,6 +192,40 @@ struct GeneralSettingsPane: View {
private var useIceBar: some View {
Toggle("Use Ice Bar", isOn: manager.bindings.useIceBar)
.annotation("Show hidden menu bar items in a separate bar below the menu bar")
.disabled(manager.autoEnableIceBarOnBuiltInDisplay)
}

@ViewBuilder
private var autoEnableIceBarToggle: some View {
Toggle(isOn: manager.bindings.autoEnableIceBarOnBuiltInDisplay) {
HStack {
Text("Auto-enable Ice Bar")
BetaBadge()
}
}
.annotation("Automatically enable or disable Ice Bar based on display")
}

@ViewBuilder
private var onlyOnScreensWithNotchToggle: some View {
Toggle("Only on screens with a notch", isOn: Binding(
get: { manager.iceBarAutoEnableMode == .screensWithNotch },
set: { manager.iceBarAutoEnableMode = $0 ? .screensWithNotch : .screenWidth }
))
.annotation("Enable Ice Bar only on screens with a notch")
}

@ViewBuilder
private var widthThresholdInput: some View {
HStack {
Text("Width threshold:")
TextField("", value: manager.bindings.iceBarDisplayWidthThreshold, format: .number.grouping(.never))
.textFieldStyle(.roundedBorder)
.frame(width: 80)
Text("pixels")
.foregroundStyle(.secondary)
}
.annotation("Ice Bar will be enabled when screen width is less than this value")
}

@ViewBuilder
Expand Down
26 changes: 26 additions & 0 deletions Ice/UI/IceBar/IceBarAutoEnableMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// IceBarAutoEnableMode.swift
// Ice
//
// See: https://github.com/jordanbaird/Ice/pull/795

import SwiftUI

/// Detection modes for automatic Ice Bar enabling.
enum IceBarAutoEnableMode: Int, CaseIterable, Identifiable {
/// Enable Ice Bar when screen width is below a threshold.
case screenWidth = 0

/// Enable Ice Bar only on screens with a notch.
case screensWithNotch = 1

var id: Int { rawValue }

/// Localized string key representation.
var localized: LocalizedStringKey {
switch self {
case .screenWidth: "Screen width threshold"
case .screensWithNotch: "Screens with a notch"
}
}
}
3 changes: 3 additions & 0 deletions Ice/Utilities/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ extension Defaults {

case iceBarLocation = "IceBarLocation"
case iceBarPinnedLocation = "IceBarPinnedLocation"
case autoEnableIceBarOnBuiltInDisplay = "AutoEnableIceBarOnBuiltInDisplay"
case iceBarAutoEnableMode = "IceBarAutoEnableMode"
case iceBarDisplayWidthThreshold = "IceBarDisplayWidthThreshold"

// MARK: Migration

Expand Down
5 changes: 5 additions & 0 deletions Ice/Utilities/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,11 @@ extension NSScreen {
)
}

/// A Boolean value that indicates whether the screen is a built-in display (laptop screen).
var isBuiltIn: Bool {
CGDisplayIsBuiltin(displayID) != 0
}

/// Returns the height of the menu bar on this screen.
func getMenuBarHeight() -> CGFloat? {
let menuBarWindow = WindowInfo.getMenuBarWindow(for: displayID)
Expand Down