| C1.source/Resources/Aligner-Info.plist | ●●●●● patch | view | raw | blame | history | |
| C1.source/Sources/Aligner/AlignerApplicationDelegate.swift | ●●●●● patch | view | raw | blame | history | |
| C1.source/Sources/Aligner/QuickSwitchRootView.swift | ●●●●● patch | view | raw | blame | history | |
| C1.source/Sources/Aligner/QuickSwitchSessionController.swift | ●●●●● patch | view | raw | blame | history | |
| C3.tools/round1-view-mode-entry-qa.sh | ●●●●● patch | view | raw | blame | history |
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> 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" 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( 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 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"