From 4d4294f8c375a06ad87e12a2c73a98a19eb7ed7e Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 14 Jun 2026 18:46:39 +0800
Subject: [PATCH] Add Quick Switch view mode toggle
---
C3.tools/round1-view-mode-entry-qa.sh | 125 ++++++++++-----
C1.source/Sources/Aligner/QuickSwitchRootView.swift | 274 ++++++++++++++++++++++++---------
C1.source/Resources/Aligner-Info.plist | 4
C1.source/Sources/Aligner/AlignerApplicationDelegate.swift | 21 ++
C1.source/Sources/Aligner/QuickSwitchSessionController.swift | 6
5 files changed, 302 insertions(+), 128 deletions(-)
diff --git a/C1.source/Resources/Aligner-Info.plist b/C1.source/Resources/Aligner-Info.plist
index 5c260c2..f2b9d20 100644
--- a/C1.source/Resources/Aligner-Info.plist
+++ b/C1.source/Resources/Aligner-Info.plist
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
- <string>0.0.67</string>
+ <string>0.0.68</string>
<key>CFBundleVersion</key>
- <string>20260614.1559</string>
+ <string>20260614.1827</string>
<key>LSMinimumSystemVersion</key>
<string>26.0</string>
<key>NSHighResolutionCapable</key>
diff --git a/C1.source/Sources/Aligner/AlignerApplicationDelegate.swift b/C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
index ec6e5bf..4ada6fd 100644
--- a/C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
+++ b/C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
@@ -494,8 +494,8 @@
controller.onSnapshotUpdated = { [weak self] in
self?.writeQuickSwitchReportIfNeeded()
}
- controller.onOpenSettings = { [weak self] in
- self?.showSettingsFromQuickSwitch()
+ controller.onToggleWaterfallViewMode = { [weak self] in
+ self?.toggleQuickSwitchWaterfallViewModeFromQuickSwitch()
}
quickSwitchSessionController = controller
}
@@ -573,8 +573,8 @@
controller.onSnapshotUpdated = { [weak self] in
self?.writeQuickSwitchReportIfNeeded()
}
- controller.onOpenSettings = { [weak self] in
- self?.showSettingsFromQuickSwitch()
+ controller.onToggleWaterfallViewMode = { [weak self] in
+ self?.toggleQuickSwitchWaterfallViewModeFromQuickSwitch()
}
quickSwitchSessionController = controller
}
@@ -749,6 +749,19 @@
])
}
+ private func toggleQuickSwitchWaterfallViewModeFromQuickSwitch() {
+ let currentMode = quickSwitchWaterfallViewMode()
+ let nextMode: QuickSwitchWaterfallViewMode = currentMode == .horizontalMasonry
+ ? .verticalColumns
+ : .horizontalMasonry
+ DevelopmentDiagnostics.log("app.quickSwitch.waterfallViewModeToggle", [
+ "currentMode": currentMode.rawValue,
+ "nextMode": nextMode.rawValue,
+ "quickSwitchVisible": quickSwitchSessionController?.isVisible ?? false
+ ])
+ setQuickSwitchWaterfallViewMode(nextMode)
+ }
+
private func appVersionBuild() -> (version: String, build: String) {
let info = Bundle.main.infoDictionary ?? [:]
let version = info["CFBundleShortVersionString"] as? String ?? "Unknown"
diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index 40611ae..6675a87 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -24,9 +24,8 @@
private lazy var closeConfirmationLayers = makeCloseConfirmationLayers()
private let closeFeedbackLayer = CALayer()
private let closeFeedbackTextLayer = CATextLayer()
- private let settingsButtonLayer = CALayer()
- private let settingsButtonIconLayer = CALayer()
- private let settingsButtonFallbackTextLayer = CATextLayer()
+ private let viewModeToggleButtonLayer = CALayer()
+ private let viewModeToggleIconLayer = CALayer()
private let appIconProvider: any AppIconProviderProtocol = WorkspaceAppIconProvider()
private let skeletonThumbnailProvider = NativeSkeletonThumbnailProvider(size: NSSize(width: 160, height: 96))
private var screenshotSourcesByWindowID: [UInt32: ScreenshotResolutionSource] = [:]
@@ -52,7 +51,7 @@
private var closeConfirmationRequired = true
private var closeFeedback: CloseFeedback?
private var closeFeedbackHideTask: Task<Void, Never>?
- private var isSettingsButtonHovered = false
+ private var isViewModeToggleButtonHovered = false
private var hoverTarget: HoverTarget = .none
private var hoverTargetSource: HoverTargetSource = .event
private var hoveredSpaceLaneID: UInt64?
@@ -95,7 +94,7 @@
var onCommitSelection: ((QuickSwitchSelection, QuickSwitchCommitSource) -> Void)?
var onSpaceLaneClick: ((UInt64, Int) -> Void)?
var onBackgroundClick: (() -> Bool)?
- var onOpenSettings: (() -> Void)?
+ var onToggleWaterfallViewMode: (() -> Void)?
var onRequestClose: ((QuickSwitchCloseRequest) -> Void)?
var onCloseConfirmationDisabled: (() -> Void)?
@@ -485,12 +484,12 @@
override func mouseMoved(with event: NSEvent) {
guard debugHoveredAppGroupIndex == nil else { return }
- let settingsHovered = settingsButtonContains(windowLocation: event.locationInWindow)
- if isSettingsButtonHovered != settingsHovered {
- isSettingsButtonHovered = settingsHovered
+ let toggleHovered = viewModeToggleButtonContains(windowLocation: event.locationInWindow)
+ if isViewModeToggleButtonHovered != toggleHovered {
+ isViewModeToggleButtonHovered = toggleHovered
needsLayout = true
}
- if settingsHovered {
+ if toggleHovered {
hoveredCloseTarget = nil
setHoverTarget(.none)
return
@@ -506,7 +505,7 @@
override func mouseExited(with event: NSEvent) {
guard debugHoveredAppGroupIndex == nil else { return }
- isSettingsButtonHovered = false
+ isViewModeToggleButtonHovered = false
hoveredCloseTarget = nil
setHoverTarget(.none)
}
@@ -525,8 +524,8 @@
return
}
- if settingsButtonContains(windowLocation: event.locationInWindow) {
- clickSettingsButton(source: "mouse")
+ if viewModeToggleButtonContains(windowLocation: event.locationInWindow) {
+ clickViewModeToggleButton(source: "mouse")
return
}
@@ -1081,9 +1080,14 @@
"activeSpaceFocusID": activeSpaceFocusID ?? NSNull(),
"spaceFilterLockedSpaceID": currentViewModel?.lockedSpaceID ?? NSNull(),
"spaceFilterActive": currentViewModel?.lockedSpaceID != nil,
- "settingsButtonVisible": !settingsButtonLayer.isHidden,
- "settingsButtonHovered": isSettingsButtonHovered,
- "settingsButtonFrame": dictionary(from: settingsButtonLayer.frame),
+ "viewModeToggleButtonVisible": !viewModeToggleButtonLayer.isHidden,
+ "viewModeToggleButtonHovered": isViewModeToggleButtonHovered,
+ "viewModeToggleButtonFrame": dictionary(from: viewModeToggleButtonLayer.frame),
+ "viewModeToggleCurrentMode": waterfallViewMode.rawValue,
+ "viewModeToggleTargetMode": nextWaterfallViewMode.rawValue,
+ "viewModeToggleIconKind": viewModeToggleIconKind,
+ "viewModeToggleAccessibilityLabel": viewModeToggleAccessibilityLabel,
+ "settingsButtonVisible": false,
"spaceFocusAppGroupIndexes": spaceFocusedAppGroupIndexes(),
"appHoverAssociatedSpaceIDs": appHoverAssociatedSpaceIDs.sorted(),
"appHoverAssociatedSpaceLabels": appHoverAssociatedSpaceLabels,
@@ -1326,9 +1330,9 @@
case "click-background":
recordMouseCommand("click-background")
_ = onBackgroundClick?()
- case "click-settings":
- recordMouseCommand("click-settings")
- clickSettingsButton(source: "debug")
+ case "click-view-toggle", "click-settings":
+ recordMouseCommand("click-view-toggle")
+ clickViewModeToggleButton(source: "debug")
case "hover-card" where parts.count == 3:
guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
recordMouseCommand("invalid:\(command)")
@@ -2281,25 +2285,15 @@
closeFeedbackLayer.addSublayer(closeFeedbackTextLayer)
glassLayer.addSublayer(closeFeedbackLayer)
- settingsButtonLayer.cornerRadius = 10
- settingsButtonLayer.masksToBounds = false
- settingsButtonLayer.borderWidth = 1
- settingsButtonLayer.shadowColor = NSColor.black.cgColor
- settingsButtonLayer.shadowOffset = CGSize(width: 0, height: -2)
- settingsButtonLayer.zPosition = 170
- settingsButtonIconLayer.contentsGravity = .resizeAspect
- settingsButtonIconLayer.contentsScale = backingScaleFactor
- settingsButtonIconLayer.contents = settingsIconImage()
- settingsButtonFallbackTextLayer.contentsScale = backingScaleFactor
- settingsButtonFallbackTextLayer.string = "⚙"
- settingsButtonFallbackTextLayer.font = NSFont.systemFont(ofSize: 18, weight: .regular)
- settingsButtonFallbackTextLayer.fontSize = 18
- settingsButtonFallbackTextLayer.alignmentMode = .center
- settingsButtonFallbackTextLayer.foregroundColor = NSColor.secondaryLabelColor.cgColor
- settingsButtonFallbackTextLayer.isHidden = settingsButtonIconLayer.contents != nil
- settingsButtonLayer.addSublayer(settingsButtonIconLayer)
- settingsButtonLayer.addSublayer(settingsButtonFallbackTextLayer)
- glassLayer.addSublayer(settingsButtonLayer)
+ viewModeToggleButtonLayer.cornerRadius = 10
+ viewModeToggleButtonLayer.masksToBounds = false
+ viewModeToggleButtonLayer.borderWidth = 1
+ viewModeToggleButtonLayer.shadowColor = NSColor.black.cgColor
+ viewModeToggleButtonLayer.shadowOffset = CGSize(width: 0, height: -2)
+ viewModeToggleButtonLayer.zPosition = 170
+ viewModeToggleIconLayer.contentsScale = backingScaleFactor
+ viewModeToggleButtonLayer.addSublayer(viewModeToggleIconLayer)
+ glassLayer.addSublayer(viewModeToggleButtonLayer)
}
private func rebuildAppShelfItems() {
@@ -2650,12 +2644,12 @@
layoutWaterfallColumns()
layoutCloseConfirmation()
layoutCloseFeedback()
- layoutSettingsButton()
+ layoutViewModeToggleButton()
CATransaction.commit()
}
- private func layoutSettingsButton() {
+ private func layoutViewModeToggleButton() {
let size: CGFloat = 34
let topMargin = topSafeAreaInset + RootLayoutMetrics.topContentMargin
let x = max(
@@ -2666,24 +2660,139 @@
RootLayoutMetrics.bottomMargin,
glassLayer.bounds.height - topMargin - size
)
- settingsButtonLayer.frame = CGRect(x: x, y: y, width: size, height: size)
- settingsButtonLayer.backgroundColor = NSColor.windowBackgroundColor
- .withAlphaComponent(isSettingsButtonHovered ? 0.78 : 0.54)
+ viewModeToggleButtonLayer.frame = CGRect(x: x, y: y, width: size, height: size)
+ viewModeToggleButtonLayer.backgroundColor = NSColor.windowBackgroundColor
+ .withAlphaComponent(isViewModeToggleButtonHovered ? 0.78 : 0.54)
.cgColor
- settingsButtonLayer.borderColor = NSColor.separatorColor
- .withAlphaComponent(isSettingsButtonHovered ? 0.42 : 0.26)
+ viewModeToggleButtonLayer.borderColor = NSColor.separatorColor
+ .withAlphaComponent(isViewModeToggleButtonHovered ? 0.42 : 0.26)
.cgColor
- settingsButtonLayer.shadowOpacity = isSettingsButtonHovered ? 0.14 : 0.08
- settingsButtonLayer.shadowRadius = isSettingsButtonHovered ? 9 : 6
- settingsButtonIconLayer.contentsScale = backingScaleFactor
- settingsButtonFallbackTextLayer.contentsScale = backingScaleFactor
- let iconInset: CGFloat = 8
- settingsButtonIconLayer.frame = settingsButtonLayer.bounds.insetBy(dx: iconInset, dy: iconInset)
- settingsButtonFallbackTextLayer.frame = settingsButtonLayer.bounds.insetBy(dx: 0, dy: 6)
- settingsButtonFallbackTextLayer.foregroundColor = (isSettingsButtonHovered
- ? NSColor.labelColor
+ viewModeToggleButtonLayer.shadowOpacity = isViewModeToggleButtonHovered ? 0.14 : 0.08
+ viewModeToggleButtonLayer.shadowRadius = isViewModeToggleButtonHovered ? 9 : 6
+ viewModeToggleIconLayer.contentsScale = backingScaleFactor
+ let iconInset: CGFloat = 7
+ viewModeToggleIconLayer.frame = viewModeToggleButtonLayer.bounds.insetBy(dx: iconInset, dy: iconInset)
+ layoutViewModeToggleIcon()
+ }
+
+ private func layoutViewModeToggleIcon() {
+ CATransaction.begin()
+ CATransaction.setDisableActions(true)
+ viewModeToggleIconLayer.sublayers?.forEach { $0.removeFromSuperlayer() }
+
+ let bounds = viewModeToggleIconLayer.bounds
+ guard bounds.width > 0, bounds.height > 0 else {
+ CATransaction.commit()
+ return
+ }
+
+ let activeStroke = (isViewModeToggleButtonHovered
+ ? NSColor.controlAccentColor
: NSColor.secondaryLabelColor
- ).cgColor
+ ).withAlphaComponent(isViewModeToggleButtonHovered ? 0.88 : 0.82)
+ let quietStroke = (isViewModeToggleButtonHovered
+ ? NSColor.controlAccentColor
+ : NSColor.secondaryLabelColor
+ ).withAlphaComponent(isViewModeToggleButtonHovered ? 0.34 : 0.24)
+ let subtleFill = (isViewModeToggleButtonHovered
+ ? NSColor.controlAccentColor
+ : NSColor.labelColor
+ ).withAlphaComponent(isViewModeToggleButtonHovered ? 0.06 : 0.025)
+
+ addViewModeToggleRoundedRect(
+ bounds.insetBy(dx: 0.65, dy: 0.65),
+ cornerRadius: 4.8,
+ strokeColor: quietStroke,
+ fillColor: NSColor.clear,
+ lineWidth: 1.05
+ )
+
+ switch nextWaterfallViewMode {
+ case .verticalColumns:
+ addVerticalColumnsToggleGlyph(
+ in: bounds.insetBy(dx: 4.15, dy: 3.25),
+ strokeColor: activeStroke,
+ fillColor: subtleFill
+ )
+ case .horizontalMasonry:
+ addHorizontalMasonryToggleGlyph(
+ in: bounds.insetBy(dx: 3.95, dy: 3.95),
+ strokeColor: activeStroke,
+ fillColor: subtleFill
+ )
+ }
+
+ CATransaction.commit()
+ }
+
+ private func addVerticalColumnsToggleGlyph(
+ in rect: CGRect,
+ strokeColor: NSColor,
+ fillColor: NSColor
+ ) {
+ let columnWidth: CGFloat = 3.35
+ let gap = max(1.7, (rect.width - columnWidth * 3) / 2)
+ for index in 0..<3 {
+ let x = rect.minX + CGFloat(index) * (columnWidth + gap)
+ let columnRect = CGRect(x: x, y: rect.minY, width: columnWidth, height: rect.height)
+ addViewModeToggleRoundedRect(
+ columnRect,
+ cornerRadius: 1.6,
+ strokeColor: strokeColor,
+ fillColor: fillColor,
+ lineWidth: 1.22
+ )
+ }
+ }
+
+ private func addHorizontalMasonryToggleGlyph(
+ in rect: CGRect,
+ strokeColor: NSColor,
+ fillColor: NSColor
+ ) {
+ let cellSize: CGFloat = 3.65
+ let gapX = max(1.65, (rect.width - cellSize * 3) / 2)
+ let gapY = max(1.65, (rect.height - cellSize * 3) / 2)
+ for row in 0..<3 {
+ for column in 0..<3 {
+ let cellRect = CGRect(
+ x: rect.minX + CGFloat(column) * (cellSize + gapX),
+ y: rect.minY + CGFloat(row) * (cellSize + gapY),
+ width: cellSize,
+ height: cellSize
+ )
+ addViewModeToggleRoundedRect(
+ cellRect,
+ cornerRadius: 1.15,
+ strokeColor: strokeColor,
+ fillColor: fillColor,
+ lineWidth: 1.18
+ )
+ }
+ }
+ }
+
+ private func addViewModeToggleRoundedRect(
+ _ rect: CGRect,
+ cornerRadius: CGFloat,
+ strokeColor: NSColor,
+ fillColor: NSColor,
+ lineWidth: CGFloat
+ ) {
+ let layer = CAShapeLayer()
+ layer.contentsScale = backingScaleFactor
+ layer.frame = viewModeToggleIconLayer.bounds
+ layer.path = CGPath(
+ roundedRect: rect,
+ cornerWidth: cornerRadius,
+ cornerHeight: cornerRadius,
+ transform: nil
+ )
+ layer.fillColor = fillColor.cgColor
+ layer.strokeColor = strokeColor.cgColor
+ layer.lineWidth = lineWidth
+ layer.lineJoin = .round
+ viewModeToggleIconLayer.addSublayer(layer)
}
private var topSafeAreaInset: CGFloat {
@@ -3035,24 +3144,26 @@
spaceLaneSegments.first { $0.space.id == spaceID }
}
- private func settingsButtonContains(windowLocation: CGPoint) -> Bool {
+ private func viewModeToggleButtonContains(windowLocation: CGPoint) -> Bool {
let locationInView = convert(windowLocation, from: nil)
let locationInGlass = CGPoint(
x: locationInView.x - glassLayer.frame.minX,
y: locationInView.y - glassLayer.frame.minY
)
- return settingsButtonLayer.frame.contains(locationInGlass)
+ return viewModeToggleButtonLayer.frame.contains(locationInGlass)
}
- private func clickSettingsButton(source: String) {
- DevelopmentDiagnostics.log("quickSwitch.view.clickSettings", [
- "source": source
+ private func clickViewModeToggleButton(source: String) {
+ DevelopmentDiagnostics.log("quickSwitch.view.clickWaterfallViewModeToggle", [
+ "source": source,
+ "currentMode": waterfallViewMode.rawValue,
+ "targetMode": nextWaterfallViewMode.rawValue
])
- lastMouseCommand = "click-settings"
- if !mouseCommandsApplied.contains("click-settings") {
- mouseCommandsApplied.append("click-settings")
+ lastMouseCommand = "click-view-toggle"
+ if !mouseCommandsApplied.contains("click-view-toggle") {
+ mouseCommandsApplied.append("click-view-toggle")
}
- onOpenSettings?()
+ onToggleWaterfallViewMode?()
}
private func appShelfItem(at windowLocation: CGPoint) -> AppShelfItemLayers? {
@@ -4448,6 +4559,28 @@
waterfallViewMode == .horizontalMasonry
}
+ private var nextWaterfallViewMode: QuickSwitchWaterfallViewMode {
+ isHorizontalMasonryMode ? .verticalColumns : .horizontalMasonry
+ }
+
+ private var viewModeToggleIconKind: String {
+ switch nextWaterfallViewMode {
+ case .verticalColumns:
+ return "verticalColumnsTarget"
+ case .horizontalMasonry:
+ return "horizontalMasonryTarget"
+ }
+ }
+
+ private var viewModeToggleAccessibilityLabel: String {
+ switch nextWaterfallViewMode {
+ case .verticalColumns:
+ return "切换到纵栏瀑布"
+ case .horizontalMasonry:
+ return "切换到横栏瀑布"
+ }
+ }
+
private var waterfallMaxScrollOffset: CGFloat {
guard !isHorizontalMasonryMode else { return 0 }
return max(0, waterfallContentWidth - waterfallLayer.bounds.width)
@@ -4565,21 +4698,6 @@
shapeLayer.lineCap = .round
shapeLayer.lineJoin = .round
return shapeLayer
- }
-
- private func settingsIconImage() -> CGImage? {
- let names = [
- NSImage.Name("NSPreferencesGeneral"),
- NSImage.Name("NSAdvanced")
- ]
- for name in names {
- guard let image = NSImage(named: name) else { continue }
- var rect = CGRect(origin: .zero, size: image.size)
- if let cgImage = image.cgImage(forProposedRect: &rect, context: nil, hints: nil) {
- return cgImage
- }
- }
- return nil
}
private func makeTextLayer(
diff --git a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
index 7fa4f39..daff31e 100644
--- a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -86,7 +86,7 @@
private var lifecycleResidualOverlayWindows = 0
private var lifecycleResidualVisibleOverlayWindows = 0
var onSnapshotUpdated: (() -> Void)?
- var onOpenSettings: (() -> Void)?
+ var onToggleWaterfallViewMode: (() -> Void)?
private struct PendingCloseVerification {
let request: QuickSwitchCloseRequest
@@ -213,8 +213,8 @@
view.onBackgroundClick = { [weak self] in
self?.handleBackgroundClick() ?? false
}
- view.onOpenSettings = { [weak self] in
- self?.onOpenSettings?()
+ view.onToggleWaterfallViewMode = { [weak self] in
+ self?.onToggleWaterfallViewMode?()
}
view.setSuppressEventInput(!debugMouseSequence.isEmpty || !debugKeySequence.isEmpty)
view.onRequestClose = { [weak self] request in
diff --git a/C3.tools/round1-view-mode-entry-qa.sh b/C3.tools/round1-view-mode-entry-qa.sh
index cb53ff4..1dfb68f 100755
--- a/C3.tools/round1-view-mode-entry-qa.sh
+++ b/C3.tools/round1-view-mode-entry-qa.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# Round01.1 view-mode entry QA. It verifies that the installed-app path exposes
-# a discoverable status-menu entry for switching Quick Switch Waterfall view
-# modes and reports visible About version/build metadata.
+# menu entries plus the in-panel Quick Switch view-mode toggle for switching
+# Waterfall view modes, and reports visible About version/build metadata.
set -euo pipefail
@@ -12,7 +12,8 @@
APP="$BUILD_CURRENT_APP"
REPORT_VERTICAL="$BUILD_REPORT_ROOT/round01-view-mode-entry-vertical-report.json"
REPORT_HORIZONTAL="$BUILD_REPORT_ROOT/round01-view-mode-entry-horizontal-report.json"
-REPORT_SETTINGS="$BUILD_REPORT_ROOT/round01-view-mode-entry-settings-report.json"
+REPORT_TOGGLE_TO_HORIZONTAL="$BUILD_REPORT_ROOT/round01-view-mode-entry-toggle-to-horizontal-report.json"
+REPORT_TOGGLE_TO_VERTICAL="$BUILD_REPORT_ROOT/round01-view-mode-entry-toggle-to-vertical-report.json"
DOMAIN="com.ar.Aligner"
WATERFALL_MODE_KEY="com.ar.Aligner.preferences.quickSwitch.waterfallViewMode"
REPORT_WAIT="${ALIGNER_ROUND1_VIEW_MODE_ENTRY_REPORT_WAIT:-6.0}"
@@ -135,6 +136,9 @@
main_menu = report.get("mainMenu", {})
settings_window = report.get("settingsWindow", {})
root = report.get("rootView", {})
+expected_target_mode = "verticalColumns" if expected_mode == "horizontalMasonry" else "horizontalMasonry"
+expected_icon_kind = "verticalColumnsTarget" if expected_target_mode == "verticalColumns" else "horizontalMasonryTarget"
+expected_accessibility_label = "切换到纵栏瀑布" if expected_target_mode == "verticalColumns" else "切换到横栏瀑布"
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
require(report.get("quickSwitchVisible") is True, "Quick Switch must be visible during entry QA")
@@ -154,10 +158,15 @@
require(main_menu.get("settingsKeyEquivalent") == ",", "Settings must use Cmd+, key equivalent")
require(main_menu.get("hasQuickSwitchViewSubmenuInAppMenu") is True, "app menu must expose Quick Switch View submenu")
require(main_menu.get("hasQuickSwitchViewSubmenuInViewMenu") is True, "View menu must expose Quick Switch View submenu")
-require(root.get("settingsButtonVisible") is True, "Quick Switch must expose a top-right Settings button")
-settings_frame = root.get("settingsButtonFrame", {})
-require(settings_frame.get("width", 0) >= 30 and settings_frame.get("height", 0) >= 30, "Settings button must have tappable size")
-require(settings_frame.get("x", 0) + settings_frame.get("width", 0) > root.get("bounds", {}).get("width", 0) * 0.90, "Settings button must sit near the top-right edge")
+require(root.get("settingsButtonVisible") is False, "Quick Switch must not expose the old top-right Settings button")
+require(root.get("viewModeToggleButtonVisible") is True, "Quick Switch must expose a top-right view-mode toggle button")
+toggle_frame = root.get("viewModeToggleButtonFrame", {})
+require(toggle_frame.get("width", 0) >= 30 and toggle_frame.get("height", 0) >= 30, "view-mode toggle button must have tappable size")
+require(toggle_frame.get("x", 0) + toggle_frame.get("width", 0) > root.get("bounds", {}).get("width", 0) * 0.90, "view-mode toggle button must sit near the top-right edge")
+require(root.get("viewModeToggleCurrentMode") == expected_mode, "view-mode toggle must report the current mode")
+require(root.get("viewModeToggleTargetMode") == expected_target_mode, "view-mode toggle must report the next target mode")
+require(root.get("viewModeToggleIconKind") == expected_icon_kind, "view-mode toggle icon kind must represent the target mode")
+require(root.get("viewModeToggleAccessibilityLabel") == expected_accessibility_label, "view-mode toggle label must name the target mode")
require(settings_window.get("visible") is False, "Settings window should not be open in passive view-mode entry cases")
require(report.get("aboutVersion") == app_version, "About version must match app Info.plist")
require(report.get("aboutBuild") == app_build, "About build must match app Info.plist")
@@ -170,21 +179,25 @@
"aboutBuild": report.get("aboutBuild"),
"statusMenu": status_menu,
"mainMenu": main_menu,
- "settingsButtonFrame": settings_frame
+ "viewModeToggleButtonFrame": toggle_frame,
+ "viewModeToggleTargetMode": root.get("viewModeToggleTargetMode"),
+ "viewModeToggleIconKind": root.get("viewModeToggleIconKind")
}, indent=2, ensure_ascii=False))
PY
}
-wait_for_settings_report() {
+wait_for_toggle_report() {
local report="$1"
+ local expected_mode="$2"
- /usr/bin/python3 - "$report" "$REPORT_WAIT" <<'PY'
+ /usr/bin/python3 - "$report" "$expected_mode" "$REPORT_WAIT" <<'PY'
import json
import sys
import time
path = sys.argv[1]
-timeout = float(sys.argv[2])
+expected_mode = sys.argv[2]
+timeout = float(sys.argv[3])
deadline = time.monotonic() + timeout
last_report = None
@@ -193,7 +206,14 @@
with open(path, "r", encoding="utf-8") as file:
report = json.load(file)
last_report = report
- if report.get("settingsWindow", {}).get("visible") is True:
+ root = report.get("rootView", {})
+ if (
+ report.get("snapshotLoaded") is True
+ and report.get("quickSwitchVisible") is True
+ and report.get("resolvedWaterfallViewMode") == expected_mode
+ and root.get("waterfallViewMode") == expected_mode
+ and root.get("lastMouseCommand") == "click-view-toggle"
+ ):
sys.exit(0)
except FileNotFoundError:
pass
@@ -203,23 +223,31 @@
if last_report is not None:
print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr)
-print(f"settings report did not become visible within {timeout:.1f}s", file=sys.stderr)
+print(f"view-mode toggle report did not reach {expected_mode} within {timeout:.1f}s", file=sys.stderr)
sys.exit(1)
PY
}
-assert_settings_report() {
+assert_toggle_report() {
local report="$1"
- local app_version="$2"
- local app_build="$3"
+ local initial_mode="$2"
+ local expected_mode="$3"
+ local expected_vertical_state="$4"
+ local expected_horizontal_state="$5"
+ local app_version="$6"
+ local app_build="$7"
- /usr/bin/python3 - "$report" "$app_version" "$app_build" <<'PY'
+ /usr/bin/python3 - "$report" "$initial_mode" "$expected_mode" "$expected_vertical_state" "$expected_horizontal_state" "$app_version" "$app_build" <<'PY'
import json
import sys
path = sys.argv[1]
-app_version = sys.argv[2]
-app_build = sys.argv[3]
+initial_mode = sys.argv[2]
+expected_mode = sys.argv[3]
+expected_vertical_state = sys.argv[4]
+expected_horizontal_state = sys.argv[5]
+app_version = sys.argv[6]
+app_build = sys.argv[7]
with open(path, "r", encoding="utf-8") as file:
report = json.load(file)
@@ -231,30 +259,40 @@
sys.exit(1)
root = report.get("rootView", {})
+status_menu = report.get("statusMenu", {})
main_menu = report.get("mainMenu", {})
settings_window = report.get("settingsWindow", {})
+expected_icon_kind = "verticalColumnsTarget" if initial_mode == "verticalColumns" else "horizontalMasonryTarget"
+expected_label = "切换到纵栏瀑布" if initial_mode == "verticalColumns" else "切换到横栏瀑布"
-require(report.get("snapshotLoaded") is True, "settings case must load the snapshot before clicking the gear")
-require(report.get("quickSwitchVisible") is False, "clicking the Settings gear must close Quick Switch")
-require(root.get("lastMouseCommand") == "click-settings", "debug mouse sequence must click the Settings gear")
-require(root.get("settingsButtonVisible") is True, "Quick Switch root must report the Settings gear")
+require(report.get("snapshotLoaded") is True, "toggle case must load the snapshot before clicking the view-mode toggle")
+require(report.get("quickSwitchVisible") is True, "clicking the view-mode toggle must keep Quick Switch visible")
+require(report.get("preferenceWaterfallViewMode") == expected_mode, "preference Waterfall mode must switch after toggle")
+require(report.get("resolvedWaterfallViewMode") == expected_mode, "resolved Waterfall mode must switch after toggle")
+require(root.get("waterfallViewMode") == expected_mode, "root view Waterfall mode must switch after toggle")
+require(root.get("lastMouseCommand") == "click-view-toggle", "debug mouse sequence must click the view-mode toggle")
+require(root.get("settingsButtonVisible") is False, "old Settings button must stay hidden")
+require(root.get("viewModeToggleButtonVisible") is True, "Quick Switch root must report the view-mode toggle")
+require(root.get("viewModeToggleCurrentMode") == expected_mode, "toggle current mode must update after click")
+require(root.get("viewModeToggleTargetMode") == initial_mode, "toggle target mode must flip back to the initial mode after click")
+require(root.get("viewModeToggleIconKind") == expected_icon_kind, "toggle icon must represent the next target mode after click")
+require(root.get("viewModeToggleAccessibilityLabel") == expected_label, "toggle label must represent the next target mode after click")
+require(status_menu.get("verticalColumnsState") == expected_vertical_state, "Vertical Columns menu state must update after toggle")
+require(status_menu.get("horizontalWaterfallState") == expected_horizontal_state, "Horizontal Waterfall menu state must update after toggle")
require(main_menu.get("hasSettingsItem") is True, "main menu must expose Settings")
require(main_menu.get("settingsKeyEquivalent") == ",", "Settings must use Cmd+, key equivalent")
-require(settings_window.get("exists") is True, "Settings window controller must be created")
-require(settings_window.get("visible") is True, "Settings window must be visible after clicking gear")
-require(settings_window.get("selectedTabLabel") == "视图", "Settings must open on the View tab")
-require(settings_window.get("selectedTabIdentifier") == "view", "Settings View tab identifier must be stable")
-require(settings_window.get("hasViewTab") is True, "Settings window must expose View tab")
-require(settings_window.get("usesSwiftUI") is False, "Settings window must not use SwiftUI")
-require(settings_window.get("waterfallViewMode") == "verticalColumns", "Settings must read the current Waterfall view mode")
+require(settings_window.get("visible") is False, "view-mode toggle must not open Settings")
require(report.get("aboutVersion") == app_version, "About version must match app Info.plist")
require(report.get("aboutBuild") == app_build, "About build must match app Info.plist")
print(json.dumps({
- "case": "settings-entry",
+ "case": "view-mode-toggle",
+ "initialMode": initial_mode,
+ "expectedMode": expected_mode,
"quickSwitchVisible": report.get("quickSwitchVisible"),
"lastMouseCommand": root.get("lastMouseCommand"),
- "settingsWindow": settings_window,
+ "viewModeToggleIconKind": root.get("viewModeToggleIconKind"),
+ "statusMenu": status_menu,
"mainMenu": main_menu
}, indent=2, ensure_ascii=False))
PY
@@ -288,14 +326,18 @@
stop_current_aligner
}
-run_settings_case() {
- local report="$1"
- local app_version="$2"
- local app_build="$3"
+run_toggle_case() {
+ local initial_mode="$1"
+ local expected_mode="$2"
+ local report="$3"
+ local vertical_state="$4"
+ local horizontal_state="$5"
+ local app_version="$6"
+ local app_build="$7"
stop_current_aligner
rm -f "$report"
- /usr/bin/defaults write "$DOMAIN" "$WATERFALL_MODE_KEY" -string "verticalColumns"
+ /usr/bin/defaults write "$DOMAIN" "$WATERFALL_MODE_KEY" -string "$initial_mode"
"$APP/Contents/MacOS/Aligner" \
--round0-skip-permissions \
@@ -303,12 +345,12 @@
--round01-fixture-app-count=4 \
--round01-fixture-windows-per-app=2 \
--round01-disable-screenshot-refresh \
- --round01-debug-mouse-sequence="click-settings" \
+ --round01-debug-mouse-sequence="click-view-toggle" \
--round01-quick-switch-report="$report" \
--round01-quick-switch-quit-after=1.2 &
- wait_for_settings_report "$report"
- assert_settings_report "$report" "$app_version" "$app_build"
+ wait_for_toggle_report "$report" "$expected_mode"
+ assert_toggle_report "$report" "$initial_mode" "$expected_mode" "$vertical_state" "$horizontal_state" "$app_version" "$app_build"
wait "$!" || true
stop_current_aligner
}
@@ -325,6 +367,7 @@
run_case "verticalColumns" "$REPORT_VERTICAL" "on" "off" "$APP_VERSION" "$APP_BUILD"
run_case "horizontalMasonry" "$REPORT_HORIZONTAL" "off" "on" "$APP_VERSION" "$APP_BUILD"
-run_settings_case "$REPORT_SETTINGS" "$APP_VERSION" "$APP_BUILD"
+run_toggle_case "verticalColumns" "horizontalMasonry" "$REPORT_TOGGLE_TO_HORIZONTAL" "off" "on" "$APP_VERSION" "$APP_BUILD"
+run_toggle_case "horizontalMasonry" "verticalColumns" "$REPORT_TOGGLE_TO_VERTICAL" "on" "off" "$APP_VERSION" "$APP_BUILD"
echo "Round01.1 view-mode entry QA passed"
--
Gitblit v1.9.3