diff --git a/.gitignore b/.gitignore index 4609f84..4e8c87d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,9 +27,7 @@ playground.xcworkspace # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata # hence it is not needed unless you have added a package configuration file to your project -# Build outputs -build/ - +# .swiftpm .build/ diff --git a/Magic Switch/AppDelegate/AppDelegate.swift b/Magic Switch/AppDelegate/AppDelegate.swift index 0ccce87..5875271 100644 --- a/Magic Switch/AppDelegate/AppDelegate.swift +++ b/Magic Switch/AppDelegate/AppDelegate.swift @@ -222,7 +222,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, .sink { [weak self] _ in self?.refreshStatusBarIcon() } - peripheralsObserver = bluetoothStore.$peripherals + peripheralsObserver = bluetoothStore.$peripherals.map { _ in () } + .merge(with: bluetoothStore.$connectionStates.map { _ in () }) .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.refreshStatusBarIcon() @@ -357,11 +358,44 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, dropdownContentView?.updateFrameToFit() } + /// The idle glyph, template-tinted like every other state the icon shows. + private static let statusBarIdleIcon: NSImage? = { + let icon = NSImage(named: "StatusBarIcon") + icon?.size = NSSize(width: 24, height: 24) + icon?.isTemplate = true + return icon + }() + + /// The idle glyph knocked out of a filled rounded square — the Control + /// Center "engaged" look — shown while peripherals are connected to this + /// Mac. Composited from the same asset so the two states can't drift. + private static let statusBarConnectedIcon: NSImage? = { + guard let glyph = NSImage(named: "StatusBarIcon") else { return nil } + let icon = NSImage(size: NSSize(width: 24, height: 24), flipped: false) { rect in + NSBezierPath(roundedRect: rect.insetBy(dx: 2, dy: 4), xRadius: 5, yRadius: 5).fill() + glyph.draw( + in: rect.insetBy(dx: 3, dy: 3), from: .zero, operation: .destinationOut, fraction: 1) + return true + } + icon.isTemplate = true + return icon + }() + + /// Peripheral presence for the idle icon/tooltip. Only trusts the store + /// once Bluetooth is up: before the first `.poweredOn`, `connectionStates` + /// is still empty and would misreport connected peripherals as away. + private func idlePeripheralPresence() -> BluetoothPeripheralStore.PeripheralPresence { + guard BluetoothManager.shared.state == .poweredOn else { return .none } + return bluetoothStore.peripheralPresence + } + /// Updates the menu-bar icon based on transfer state (highest priority), /// then Pairing + Bluetooth state. Transfer state shows arrow icons so /// the user can tell at a glance that peripherals are moving, and in /// which direction. When the app cannot function (unpaired, Bluetooth - /// off, etc.) we show a triangle exclamation mark instead. + /// off, etc.) we show a triangle exclamation mark instead. When idle, + /// the icon gains a filled background while peripherals are connected + /// to this Mac. private func refreshStatusBarIcon() { guard let button = statusItem?.button else { return } @@ -372,9 +406,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, accessibilityDescription: "Sending peripherals to the other Mac") img?.isTemplate = true button.image = img - button.appearsDisabled = false - button.alphaValue = 1.0 - button.contentTintColor = nil button.toolTip = "Sending peripherals to the other Mac…" button.setAccessibilityLabel(button.toolTip ?? "") return @@ -384,9 +415,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, accessibilityDescription: "Receiving peripherals from the other Mac") img?.isTemplate = true button.image = img - button.appearsDisabled = false - button.alphaValue = 1.0 - button.contentTintColor = nil button.toolTip = "Receiving peripherals from the other Mac…" button.setAccessibilityLabel(button.toolTip ?? "") return @@ -405,17 +433,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, accessibilityDescription: "Magic Switch needs attention") image?.isTemplate = true button.image = image - button.appearsDisabled = false - button.alphaValue = 1.0 - button.contentTintColor = nil button.toolTip = statusBarTooltip() - } else if let normal = NSImage(named: "StatusBarIcon") { - normal.size = NSSize(width: 24, height: 24) - normal.isTemplate = true - button.image = normal - button.appearsDisabled = false - button.alphaValue = 1.0 - button.contentTintColor = bluetoothStore.isAnyPeripheralConnected ? nil : .tertiaryLabelColor + } else { + let connected = idlePeripheralPresence() == .connectedHere + if let image = connected ? Self.statusBarConnectedIcon : Self.statusBarIdleIcon { + button.image = image + } button.toolTip = statusBarTooltip() } button.setAccessibilityLabel(statusBarTooltip()) @@ -502,13 +525,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, self?.beginTransferHold(.sending) } // Settle signal for held transfers: any per-peripheral state flip - // re-evaluates whether the last transitioning row just resolved and - // updates the menu bar icon. + // re-evaluates whether the last transitioning row just resolved. transferSettleObserver = bluetoothStore.$connectionStates .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.maybeEndHeldTransfer() - self?.refreshStatusBarIcon() } } @@ -526,10 +547,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, case .resetting: return "Magic Switch: Bluetooth is resetting." case .poweredOn, .unknown: - if bluetoothStore.isAnyPeripheralConnected { - return "Magic Switch (Connected)" - } else { - return "Magic Switch (Disconnected)" + switch idlePeripheralPresence() { + case .connectedHere: + return "Magic Switch — peripherals connected to this Mac" + case .away: + return "Magic Switch — no peripherals connected to this Mac" + case .none: + return "Magic Switch" } @unknown default: return "Magic Switch" @@ -951,9 +975,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, ) flash?.isTemplate = true button.image = flash - button.appearsDisabled = false - button.alphaValue = 1.0 - button.contentTintColor = nil pingFlashTimer?.cancel() let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main) timer.schedule(deadline: .now() + 3.0) diff --git a/Magic Switch/Model/Store/BluetoothPeripheralStore.swift b/Magic Switch/Model/Store/BluetoothPeripheralStore.swift index 31e6886..d02de28 100644 --- a/Magic Switch/Model/Store/BluetoothPeripheralStore.swift +++ b/Magic Switch/Model/Store/BluetoothPeripheralStore.swift @@ -319,12 +319,20 @@ final class BluetoothPeripheralStore: NSObject, ObservableObject, BluetoothPerip } } - /// True if at least one registered peripheral is currently connected to this Mac. - /// Reads `connectionStates`; main-thread only. - var isAnyPeripheralConnected: Bool { - peripherals.contains { peripheral in + enum PeripheralPresence { + case none + case connectedHere + case away + } + + /// Whether any registered peripheral is connected to this Mac, none are, + /// or none are registered at all. Reads `connectionStates`; main-thread only. + var peripheralPresence: PeripheralPresence { + guard !peripherals.isEmpty else { return .none } + let anyConnected = peripherals.contains { peripheral in connectionState(for: peripheral.id) == .connected } + return anyConnected ? .connectedHere : .away } /// Resolved display type for `peripheral`: the user's manual override if set, diff --git a/Magic Switch/View/Settings/BluetoothPeripheralSettingsView.swift b/Magic Switch/View/Settings/BluetoothPeripheralSettingsView.swift index 259765b..1a7cb06 100644 --- a/Magic Switch/View/Settings/BluetoothPeripheralSettingsView.swift +++ b/Magic Switch/View/Settings/BluetoothPeripheralSettingsView.swift @@ -335,8 +335,6 @@ private struct PeripheralRowView: View { // MARK: - Preview -#if canImport(PreviewsMacros) #Preview { BluetoothPeripheralSettingsView() } -#endif diff --git a/Magic Switch/View/Settings/NetworkDeviceManagementView.swift b/Magic Switch/View/Settings/NetworkDeviceManagementView.swift index 24bdf58..f647ecb 100644 --- a/Magic Switch/View/Settings/NetworkDeviceManagementView.swift +++ b/Magic Switch/View/Settings/NetworkDeviceManagementView.swift @@ -636,8 +636,6 @@ private struct NetworkDeviceListView: View { // MARK: - Preview -#if canImport(PreviewsMacros) #Preview { NetworkDeviceManagementView() } -#endif diff --git a/Magic Switch/View/Settings/OtherSettingsView.swift b/Magic Switch/View/Settings/OtherSettingsView.swift index 41a8ced..d2ddbc7 100644 --- a/Magic Switch/View/Settings/OtherSettingsView.swift +++ b/Magic Switch/View/Settings/OtherSettingsView.swift @@ -385,8 +385,6 @@ private struct SettingsRowView: View { // MARK: - Preview -#if canImport(PreviewsMacros) #Preview { OtherSettingsView() } -#endif diff --git a/Magic Switch/View/Settings/PairingSettingsView.swift b/Magic Switch/View/Settings/PairingSettingsView.swift index 3aaa8d7..2b200ae 100644 --- a/Magic Switch/View/Settings/PairingSettingsView.swift +++ b/Magic Switch/View/Settings/PairingSettingsView.swift @@ -362,8 +362,6 @@ private struct EnterCodeSheet: View { // MARK: - Preview -#if canImport(PreviewsMacros) #Preview { PairingSettingsView() } -#endif diff --git a/Magic Switch/View/Settings/SettingsView.swift b/Magic Switch/View/Settings/SettingsView.swift index b477fcf..53e2070 100644 --- a/Magic Switch/View/Settings/SettingsView.swift +++ b/Magic Switch/View/Settings/SettingsView.swift @@ -63,8 +63,6 @@ struct SettingsView: View { // MARK: - Preview -#if canImport(PreviewsMacros) #Preview { SettingsView() } -#endif diff --git a/README.md b/README.md index 4c818e1..5aa004f 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ On the **Macs** tab, find the other Mac under **Your Other Mac** and click its * | Menu → a peripheral | Switch just that one peripheral. Checkmark = currently on this Mac, with its battery level | | Menu → Settings | Open the Settings window | -The menu-bar icon also signals state: a **warning triangle** means Magic Switch needs attention (not paired, or Bluetooth off/denied) — hover for the reason; **up/down arrows** flash briefly while peripherals are moving between Macs; when idle, the icon is **highlighted** when peripherals are connected to this Mac and **grayed out** when disconnected (the dropdown is pictured at the top of this README). +The menu-bar icon also signals state: a **warning triangle** means Magic Switch needs attention (not paired, or Bluetooth off/denied) — hover for the reason; **up/down arrows** flash briefly while peripherals are moving between Macs; when idle, the icon gains a **filled background** while peripherals are connected to this Mac (the dropdown is pictured at the top of this README). ### Trigger switches from anything (URL scheme)