From fd4ac233a520282ce7277195a34e09cbbf16c028 Mon Sep 17 00:00:00 2001 From: hzl <1803573449@qq.com> Date: Tue, 4 Aug 2026 19:35:54 +0800 Subject: [PATCH 1/2] feat: add keyboard page navigation --- Resources/en.lproj/Localizable.strings | 1 + Resources/zh-Hans.lproj/Localizable.strings | 1 + Sources/Views/PageIndicator.swift | 27 +++++++------ Sources/Window/IslandWindowController.swift | 43 +++++++++++++++++---- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/Resources/en.lproj/Localizable.strings b/Resources/en.lproj/Localizable.strings index 1e9d5952..c2d3cf19 100644 --- a/Resources/en.lproj/Localizable.strings +++ b/Resources/en.lproj/Localizable.strings @@ -101,6 +101,7 @@ "Ring" = "Ring"; "scanning local logs…" = "scanning local logs…"; "Settings" = "Settings"; +"Switch to %@ (⌘%d)" = "Switch to %@ (⌘%d)"; "Show on" = "Show on"; "Sparkline" = "Sparkline"; "Spacing" = "Spacing"; diff --git a/Resources/zh-Hans.lproj/Localizable.strings b/Resources/zh-Hans.lproj/Localizable.strings index e729569a..d96e12d3 100644 --- a/Resources/zh-Hans.lproj/Localizable.strings +++ b/Resources/zh-Hans.lproj/Localizable.strings @@ -101,6 +101,7 @@ "Ring" = "环形"; "scanning local logs…" = "正在扫描本地日志…"; "Settings" = "设置"; +"Switch to %@ (⌘%d)" = "切换到%@(⌘%d)"; "Show on" = "显示在"; "Sparkline" = "折线"; "Spacing" = "间距"; diff --git a/Sources/Views/PageIndicator.swift b/Sources/Views/PageIndicator.swift index 88a42f5e..5e0d01fe 100644 --- a/Sources/Views/PageIndicator.swift +++ b/Sources/Views/PageIndicator.swift @@ -2,31 +2,36 @@ import SwiftUI /// Page indicator that mirrors the active screen. Sits in the /// expanded panel footer between the style chip and the live-status group. -/// Each dot is tappable so regular-mouse users (no trackpad swipe, no -/// horizontal wheel) have a click-to-page affordance. +/// Each dot sits inside a 24pt button so regular-mouse users do not need +/// pixel-precise aim. The visible dots stay compact and quiet. struct PageIndicator: View { @ObservedObject var model: IslandModel @ObservedObject private var screenPref = ScreenPref.shared var body: some View { - HStack(spacing: 5) { + HStack(spacing: 0) { ForEach(ScreenPref.Screen.allCases, id: \.self) { screen in dot(for: screen) } } + .padding(.horizontal, 2) + .contentShape(Rectangle()) .animation(.strongEaseOut, value: screenPref.screen) } private func dot(for screen: ScreenPref.Screen) -> some View { let isActive = screenPref.screen == screen - return Circle() - .fill(.white.opacity(isActive ? 0.78 : 0.22)) - .frame(width: 5, height: 5) - // Visual stays 5pt; hit area expands ~6pt outward so the dot - // is reachable without pixel-precise aim. - .contentShape(Rectangle().inset(by: -6)) - .onTapGesture { model.showScreen(screen) } - .accessibilityElement() + return Button { + model.showScreen(screen) + } label: { + Circle() + .fill(.white.opacity(isActive ? 0.82 : 0.25)) + .frame(width: isActive ? 8 : 7, height: isActive ? 8 : 7) + .frame(width: 24, height: 24) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .help(L10n.tr("Switch to %@ (⌘%d)", screen.pageLabel, screen.pageIndex + 1)) .accessibilityLabel(accessibilityLabel(for: screen)) .accessibilityAddTraits(.isButton) .accessibilityAddTraits(isActive ? .isSelected : []) diff --git a/Sources/Window/IslandWindowController.swift b/Sources/Window/IslandWindowController.swift index 55d5b35c..789c869a 100644 --- a/Sources/Window/IslandWindowController.swift +++ b/Sources/Window/IslandWindowController.swift @@ -136,13 +136,8 @@ final class IslandWindowController { if inside { NSApp.activate(ignoringOtherApps: true) window.makeKey() - cmdQMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in - if event.modifierFlags.contains(.command), - event.charactersIgnoringModifiers == "q" { - NSApp.terminate(nil) - return nil - } - return event + cmdQMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in + self?.handleKeyDown(event) ?? event } } else { if let m = cmdQMonitor { NSEvent.removeMonitor(m) } @@ -151,6 +146,40 @@ final class IslandWindowController { } } + private func handleKeyDown(_ event: NSEvent) -> NSEvent? { + guard window.isKeyWindow else { return event } + + let modifiers = event.modifierFlags + .intersection(.deviceIndependentFlagsMask) + .subtracting([.capsLock]) + if modifiers == .command, event.charactersIgnoringModifiers == "q" { + NSApp.terminate(nil) + return nil + } + + guard model.state == .expanded else { return event } + + if modifiers == .command, + let character = event.charactersIgnoringModifiers, + let index = ["1", "2", "3"].firstIndex(of: character) { + model.showScreen(ScreenPref.Screen.allCases[index]) + return nil + } + + let navigationModifiers = modifiers.subtracting([.function, .numericPad, .capsLock]) + guard navigationModifiers.isEmpty else { return event } + switch event.keyCode { + case 123: + model.rewindScreen() + return nil + case 124: + model.advanceScreen() + return nil + default: + return event + } + } + @MainActor private static func targetScreen() -> NSScreen? { DisplayInfo.currentTarget()?.screen From 30cc3960d2f8a0663180dbe238f7d710babd86a3 Mon Sep 17 00:00:00 2001 From: ericjypark Date: Tue, 18 Aug 2026 16:00:44 -0500 Subject: [PATCH 2/2] fix(views): keep the page indicator's original 5pt dot visuals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The keyboard-navigation change also grew the dots to 7–8pt inside 24pt button slots, roughly doubling the indicator's footprint in the footer. Restore the original geometry, spacing, and opacities; keep the Button wrapper and the ⌘-shortcut tooltip. --- Sources/Views/PageIndicator.swift | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/Sources/Views/PageIndicator.swift b/Sources/Views/PageIndicator.swift index 5e0d01fe..35bbe62e 100644 --- a/Sources/Views/PageIndicator.swift +++ b/Sources/Views/PageIndicator.swift @@ -2,20 +2,18 @@ import SwiftUI /// Page indicator that mirrors the active screen. Sits in the /// expanded panel footer between the style chip and the live-status group. -/// Each dot sits inside a 24pt button so regular-mouse users do not need -/// pixel-precise aim. The visible dots stay compact and quiet. +/// Each dot is tappable so regular-mouse users (no trackpad swipe, no +/// horizontal wheel) have a click-to-page affordance. struct PageIndicator: View { @ObservedObject var model: IslandModel @ObservedObject private var screenPref = ScreenPref.shared var body: some View { - HStack(spacing: 0) { + HStack(spacing: 5) { ForEach(ScreenPref.Screen.allCases, id: \.self) { screen in dot(for: screen) } } - .padding(.horizontal, 2) - .contentShape(Rectangle()) .animation(.strongEaseOut, value: screenPref.screen) } @@ -25,16 +23,17 @@ struct PageIndicator: View { model.showScreen(screen) } label: { Circle() - .fill(.white.opacity(isActive ? 0.82 : 0.25)) - .frame(width: isActive ? 8 : 7, height: isActive ? 8 : 7) - .frame(width: 24, height: 24) - .contentShape(Rectangle()) + .fill(.white.opacity(isActive ? 0.78 : 0.22)) + .frame(width: 5, height: 5) + // Visual stays 5pt; hit area expands ~6pt outward so the dot + // is reachable without pixel-precise aim. + .contentShape(Rectangle().inset(by: -6)) } - .buttonStyle(.plain) - .help(L10n.tr("Switch to %@ (⌘%d)", screen.pageLabel, screen.pageIndex + 1)) - .accessibilityLabel(accessibilityLabel(for: screen)) - .accessibilityAddTraits(.isButton) - .accessibilityAddTraits(isActive ? .isSelected : []) + .buttonStyle(.plain) + .help(L10n.tr("Switch to %@ (⌘%d)", screen.pageLabel, screen.pageIndex + 1)) + .accessibilityLabel(accessibilityLabel(for: screen)) + .accessibilityAddTraits(.isButton) + .accessibilityAddTraits(isActive ? .isSelected : []) } private func accessibilityLabel(for screen: ScreenPref.Screen) -> String {