From 6f0ede789115d393e1236d98776e91fb52c90c79 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 12 Jun 2026 14:52:37 +0800
Subject: [PATCH] Add Quick Switch settings entry

---
 C3.tools/round1-view-mode-entry-qa.sh                        |  130 ++++++++++++
 C1.source/Sources/Aligner/QuickSwitchRootView.swift          |  115 +++++++++++
 C1.source/Resources/Aligner-Info.plist                       |    4 
 C1.source/Sources/Aligner/AlignerApplicationDelegate.swift   |  176 ++++++++++++++++-
 C1.source/Sources/Aligner/QuickSwitchSessionController.swift |    4 
 C1.source/Sources/Aligner/SettingsWindowController.swift     |  138 +++++++++++++
 6 files changed, 551 insertions(+), 16 deletions(-)

diff --git a/C1.source/Resources/Aligner-Info.plist b/C1.source/Resources/Aligner-Info.plist
index 9baef85..bdf5cf0 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.58</string>
+    <string>0.0.59</string>
 	<key>CFBundleVersion</key>
-    <string>20260612.1249</string>
+    <string>20260612.1439</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 ad68641..455f099 100644
--- a/C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
+++ b/C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
@@ -20,10 +20,11 @@
     private var round1TriggerProbe: CGEventTapTriggerProbe?
     private var quickSwitchSessionController: QuickSwitchSessionController?
     private var quickSwitchTriggerService: CGEventTapTriggerService?
+    private var settingsWindowController: SettingsWindowController?
     private var isRunningQuickSwitchLifecycle = false
     private var statusItem: NSStatusItem?
-    private var verticalWaterfallViewModeMenuItem: NSMenuItem?
-    private var horizontalWaterfallViewModeMenuItem: NSMenuItem?
+    private var verticalWaterfallViewModeMenuItems: [NSMenuItem] = []
+    private var horizontalWaterfallViewModeMenuItems: [NSMenuItem] = []
 
     func applicationDidFinishLaunching(_ notification: Notification) {
         let bundlePathSummary = DevelopmentDiagnostics.pathSummary(Bundle.main.bundlePath)
@@ -47,6 +48,7 @@
             return
         }
 
+        configureMainMenu()
         configureStatusItem()
         if !launchOptions.skipPermissions {
             showPermissionsIfNeeded()
@@ -96,8 +98,9 @@
         let menu = NSMenu()
         menu.delegate = self
         menu.addItem(menuItem(title: "About Aligner...", action: #selector(showAbout), keyEquivalent: ""))
+        menu.addItem(menuItem(title: "Settings...", action: #selector(showSettingsFromMenu), keyEquivalent: ""))
         menu.addItem(.separator())
-        menu.addItem(menuItem(title: "Check Permissions...", action: #selector(showPermissions), keyEquivalent: ","))
+        menu.addItem(menuItem(title: "Check Permissions...", action: #selector(showPermissions), keyEquivalent: ""))
         menu.addItem(.separator())
         menu.addItem(menuItem(title: "Show Quick Switch", action: #selector(showQuickSwitchFromMenu), keyEquivalent: ""))
         menu.addItem(menuItem(title: "Hide Quick Switch", action: #selector(hideQuickSwitchFromMenu), keyEquivalent: ""))
@@ -114,8 +117,70 @@
         DevelopmentDiagnostics.log("app.statusItem.configure.end", [
             "hasButton": item.button != nil,
             "hasAboutItem": true,
-            "hasWaterfallViewModeItems": verticalWaterfallViewModeMenuItem != nil
-                && horizontalWaterfallViewModeMenuItem != nil
+            "hasSettingsItem": true,
+            "hasWaterfallViewModeItems": !verticalWaterfallViewModeMenuItems.isEmpty
+                && !horizontalWaterfallViewModeMenuItems.isEmpty
+        ])
+    }
+
+    private func configureMainMenu() {
+        DevelopmentDiagnostics.log("app.mainMenu.configure.start")
+
+        let mainMenu = NSMenu(title: "Main Menu")
+        let appMenuItem = NSMenuItem(title: "Aligner", action: nil, keyEquivalent: "")
+        let appMenu = NSMenu(title: "Aligner")
+        appMenu.delegate = self
+        appMenuItem.submenu = appMenu
+        appMenu.addItem(menuItem(title: "About Aligner...", action: #selector(showAbout), keyEquivalent: ""))
+        appMenu.addItem(.separator())
+        appMenu.addItem(menuItem(title: "Settings...", action: #selector(showSettingsFromMenu), keyEquivalent: ","))
+        appMenu.addItem(.separator())
+        appMenu.addItem(menuItem(title: "Check Permissions...", action: #selector(showPermissions), keyEquivalent: ""))
+        appMenu.addItem(menuItem(title: "Show Quick Switch", action: #selector(showQuickSwitchFromMenu), keyEquivalent: ""))
+        appMenu.addItem(menuItem(title: "Hide Quick Switch", action: #selector(hideQuickSwitchFromMenu), keyEquivalent: ""))
+        appMenu.addItem(quickSwitchViewModeMenuItem())
+        appMenu.addItem(.separator())
+        appMenu.addItem(menuItem(title: "Quit Aligner", action: #selector(quit), keyEquivalent: "q"))
+        mainMenu.addItem(appMenuItem)
+
+        let editMenuItem = NSMenuItem(title: "Edit", action: nil, keyEquivalent: "")
+        let editMenu = NSMenu(title: "Edit")
+        editMenuItem.submenu = editMenu
+        editMenu.addItem(responderMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z"))
+        editMenu.addItem(responderMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "Z"))
+        editMenu.addItem(.separator())
+        editMenu.addItem(responderMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"))
+        editMenu.addItem(responderMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"))
+        editMenu.addItem(responderMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"))
+        editMenu.addItem(responderMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"))
+        mainMenu.addItem(editMenuItem)
+
+        let viewMenuItem = NSMenuItem(title: "View", action: nil, keyEquivalent: "")
+        let viewMenu = NSMenu(title: "View")
+        viewMenu.delegate = self
+        viewMenuItem.submenu = viewMenu
+        viewMenu.addItem(quickSwitchViewModeMenuItem())
+        mainMenu.addItem(viewMenuItem)
+
+        let windowMenuItem = NSMenuItem(title: "Window", action: nil, keyEquivalent: "")
+        let windowMenu = NSMenu(title: "Window")
+        windowMenuItem.submenu = windowMenu
+        windowMenu.addItem(responderMenuItem(title: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m"))
+        windowMenu.addItem(responderMenuItem(title: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: ""))
+        windowMenu.addItem(.separator())
+        windowMenu.addItem(responderMenuItem(title: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: ""))
+        mainMenu.addItem(windowMenuItem)
+        NSApp.windowsMenu = windowMenu
+
+        let helpMenuItem = NSMenuItem(title: "Help", action: nil, keyEquivalent: "")
+        helpMenuItem.submenu = NSMenu(title: "Help")
+        mainMenu.addItem(helpMenuItem)
+
+        NSApp.mainMenu = mainMenu
+        updateWaterfallViewModeMenuState()
+        DevelopmentDiagnostics.log("app.mainMenu.configure.end", [
+            "hasSettingsItem": appMenu.item(withTitle: "Settings...") != nil,
+            "hasQuickSwitchViewSubmenu": appMenu.item(withTitle: "Quick Switch View")?.submenu != nil
         ])
     }
 
@@ -127,6 +192,10 @@
         let item = NSMenuItem(title: title, action: action, keyEquivalent: keyEquivalent)
         item.target = self
         return item
+    }
+
+    private func responderMenuItem(title: String, action: Selector?, keyEquivalent: String) -> NSMenuItem {
+        NSMenuItem(title: title, action: action, keyEquivalent: keyEquivalent)
     }
 
     private func quickSwitchViewModeMenuItem() -> NSMenuItem {
@@ -144,8 +213,8 @@
         submenu.addItem(verticalItem)
         submenu.addItem(horizontalItem)
 
-        verticalWaterfallViewModeMenuItem = verticalItem
-        horizontalWaterfallViewModeMenuItem = horizontalItem
+        verticalWaterfallViewModeMenuItems.append(verticalItem)
+        horizontalWaterfallViewModeMenuItems.append(horizontalItem)
 
         let rootItem = NSMenuItem(title: "Quick Switch View", action: nil, keyEquivalent: "")
         rootItem.submenu = submenu
@@ -154,8 +223,12 @@
 
     private func updateWaterfallViewModeMenuState() {
         let mode = quickSwitchWaterfallViewMode()
-        verticalWaterfallViewModeMenuItem?.state = mode == .verticalColumns ? .on : .off
-        horizontalWaterfallViewModeMenuItem?.state = mode == .horizontalMasonry ? .on : .off
+        for item in verticalWaterfallViewModeMenuItems {
+            item.state = mode == .verticalColumns ? .on : .off
+        }
+        for item in horizontalWaterfallViewModeMenuItems {
+            item.state = mode == .horizontalMasonry ? .on : .off
+        }
     }
 
     private func showPermissionsIfNeeded() {
@@ -421,6 +494,9 @@
             controller.onSnapshotUpdated = { [weak self] in
                 self?.writeQuickSwitchReportIfNeeded()
             }
+            controller.onOpenSettings = { [weak self] in
+                self?.showSettingsFromQuickSwitch()
+            }
             quickSwitchSessionController = controller
         }
 
@@ -497,6 +573,9 @@
             controller.onSnapshotUpdated = { [weak self] in
                 self?.writeQuickSwitchReportIfNeeded()
             }
+            controller.onOpenSettings = { [weak self] in
+                self?.showSettingsFromQuickSwitch()
+            }
             quickSwitchSessionController = controller
         }
 
@@ -531,6 +610,8 @@
         report["aboutVersion"] = versionBuild.version
         report["aboutBuild"] = versionBuild.build
         report["statusMenu"] = statusMenuReportDictionary()
+        report["mainMenu"] = mainMenuReportDictionary()
+        report["settingsWindow"] = settingsWindowReportDictionary()
 
         do {
             let data = try JSONSerialization.data(withJSONObject: report, options: [.prettyPrinted, .sortedKeys])
@@ -652,6 +733,7 @@
         preferenceStore.write(updatedPreferences)
         quickSwitchSessionController?.setWaterfallViewMode(mode)
         updateWaterfallViewModeMenuState()
+        settingsWindowController?.refreshWaterfallViewMode()
         writeQuickSwitchReportIfNeeded()
         DevelopmentDiagnostics.log("app.quickSwitch.waterfallViewModePreferenceChanged", [
             "mode": mode.rawValue,
@@ -668,13 +750,44 @@
 
     private func statusMenuReportDictionary() -> [String: Any] {
         updateWaterfallViewModeMenuState()
+        let verticalItem = verticalWaterfallViewModeMenuItems.first
+        let horizontalItem = horizontalWaterfallViewModeMenuItems.first
         return [
             "hasAboutItem": statusItem?.menu?.item(withTitle: "About Aligner...") != nil,
+            "hasSettingsItem": statusItem?.menu?.item(withTitle: "Settings...") != nil,
             "hasQuickSwitchViewSubmenu": statusItem?.menu?.item(withTitle: "Quick Switch View")?.submenu != nil,
-            "hasVerticalColumnsItem": verticalWaterfallViewModeMenuItem != nil,
-            "hasHorizontalWaterfallItem": horizontalWaterfallViewModeMenuItem != nil,
-            "verticalColumnsState": verticalWaterfallViewModeMenuItem?.state == .on ? "on" : "off",
-            "horizontalWaterfallState": horizontalWaterfallViewModeMenuItem?.state == .on ? "on" : "off"
+            "hasVerticalColumnsItem": verticalItem != nil,
+            "hasHorizontalWaterfallItem": horizontalItem != nil,
+            "verticalColumnsState": verticalItem?.state == .on ? "on" : "off",
+            "horizontalWaterfallState": horizontalItem?.state == .on ? "on" : "off"
+        ]
+    }
+
+    private func mainMenuReportDictionary() -> [String: Any] {
+        updateWaterfallViewModeMenuState()
+        let appMenu = NSApp.mainMenu?.items.first?.submenu
+        let viewMenu = NSApp.mainMenu?.item(withTitle: "View")?.submenu
+        return [
+            "hasMenu": NSApp.mainMenu != nil,
+            "hasAppMenu": appMenu != nil,
+            "hasAboutItem": appMenu?.item(withTitle: "About Aligner...") != nil,
+            "hasSettingsItem": appMenu?.item(withTitle: "Settings...") != nil,
+            "settingsKeyEquivalent": appMenu?.item(withTitle: "Settings...")?.keyEquivalent ?? "",
+            "hasQuickSwitchViewSubmenuInAppMenu": appMenu?.item(withTitle: "Quick Switch View")?.submenu != nil,
+            "hasQuickSwitchViewSubmenuInViewMenu": viewMenu?.item(withTitle: "Quick Switch View")?.submenu != nil
+        ]
+    }
+
+    private func settingsWindowReportDictionary() -> [String: Any] {
+        settingsWindowController?.reportDictionary() ?? [
+            "exists": false,
+            "visible": false,
+            "title": "",
+            "selectedTabLabel": "",
+            "selectedTabIdentifier": "",
+            "waterfallViewMode": quickSwitchWaterfallViewMode().rawValue,
+            "hasViewTab": false,
+            "usesSwiftUI": false
         ]
     }
 
@@ -696,6 +809,43 @@
         alert.runModal()
     }
 
+    @objc private func showSettingsFromMenu() {
+        openSettingsWindow(source: "menu")
+    }
+
+    private func showSettingsFromQuickSwitch() {
+        openSettingsWindow(source: "quickSwitch")
+    }
+
+    private func openSettingsWindow(source: String) {
+        DevelopmentDiagnostics.log("app.settings.show", [
+            "source": source,
+            "quickSwitchVisible": quickSwitchSessionController?.isVisible ?? false,
+            "mode": quickSwitchWaterfallViewMode().rawValue
+        ])
+        hideQuickSwitchSession(reason: .userClosed)
+        NSApp.setActivationPolicy(.regular)
+
+        if settingsWindowController == nil {
+            settingsWindowController = SettingsWindowController(
+                modeProvider: { [weak self] in
+                    self?.quickSwitchWaterfallViewMode() ?? .verticalColumns
+                },
+                onWaterfallViewModeChanged: { [weak self] mode in
+                    self?.setQuickSwitchWaterfallViewMode(mode)
+                }
+            )
+        }
+
+        settingsWindowController?.showWindow(nil)
+        NSApp.activate(ignoringOtherApps: true)
+        writeQuickSwitchReportIfNeeded()
+        DevelopmentDiagnostics.log("app.settings.windowShown", [
+            "windowVisible": settingsWindowController?.window?.isVisible ?? false,
+            "activationPolicy": NSApp.activationPolicy().rawValue
+        ])
+    }
+
     @objc private func useVerticalWaterfallViewMode() {
         setQuickSwitchWaterfallViewMode(.verticalColumns)
     }
diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index e001bc4..45ea62f 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -24,6 +24,9 @@
     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 appIconProvider: any AppIconProviderProtocol = WorkspaceAppIconProvider()
     private let skeletonThumbnailProvider = NativeSkeletonThumbnailProvider(size: NSSize(width: 160, height: 96))
     private var screenshotSourcesByWindowID: [UInt32: ScreenshotResolutionSource] = [:]
@@ -49,6 +52,7 @@
     private var closeConfirmationRequired = true
     private var closeFeedback: CloseFeedback?
     private var closeFeedbackHideTask: Task<Void, Never>?
+    private var isSettingsButtonHovered = false
     private var hoverTarget: HoverTarget = .none
     private var hoverTargetSource: HoverTargetSource = .event
     private var hoveredSpaceLaneID: UInt64?
@@ -91,6 +95,7 @@
     var onCommitSelection: ((QuickSwitchSelection, QuickSwitchCommitSource) -> Void)?
     var onSpaceLaneClick: ((UInt64, Int) -> Void)?
     var onBackgroundClick: (() -> Bool)?
+    var onOpenSettings: (() -> Void)?
     var onRequestClose: ((QuickSwitchCloseRequest) -> Void)?
     var onCloseConfirmationDisabled: (() -> Void)?
 
@@ -481,6 +486,17 @@
     override func mouseMoved(with event: NSEvent) {
         guard debugHoveredAppGroupIndex == nil else { return }
 
+        let settingsHovered = settingsButtonContains(windowLocation: event.locationInWindow)
+        if isSettingsButtonHovered != settingsHovered {
+            isSettingsButtonHovered = settingsHovered
+            needsLayout = true
+        }
+        if settingsHovered {
+            hoveredCloseTarget = nil
+            setHoverTarget(.none)
+            return
+        }
+
         let closeTarget = closeButtonTarget(at: event.locationInWindow)
         if hoveredCloseTarget != closeTarget {
             hoveredCloseTarget = closeTarget
@@ -491,6 +507,7 @@
 
     override func mouseExited(with event: NSEvent) {
         guard debugHoveredAppGroupIndex == nil else { return }
+        isSettingsButtonHovered = false
         hoveredCloseTarget = nil
         setHoverTarget(.none)
     }
@@ -506,6 +523,11 @@
         }
 
         if handleCloseConfirmationMouseDown(at: event.locationInWindow) {
+            return
+        }
+
+        if settingsButtonContains(windowLocation: event.locationInWindow) {
+            clickSettingsButton(source: "mouse")
             return
         }
 
@@ -1060,6 +1082,9 @@
             "activeSpaceFocusID": activeSpaceFocusID ?? NSNull(),
             "spaceFilterLockedSpaceID": currentViewModel?.lockedSpaceID ?? NSNull(),
             "spaceFilterActive": currentViewModel?.lockedSpaceID != nil,
+            "settingsButtonVisible": !settingsButtonLayer.isHidden,
+            "settingsButtonHovered": isSettingsButtonHovered,
+            "settingsButtonFrame": dictionary(from: settingsButtonLayer.frame),
             "spaceFocusAppGroupIndexes": spaceFocusedAppGroupIndexes(),
             "appHoverAssociatedSpaceIDs": appHoverAssociatedSpaceIDs.sorted(),
             "appHoverAssociatedSpaceLabels": appHoverAssociatedSpaceLabels,
@@ -1302,6 +1327,9 @@
         case "click-background":
             recordMouseCommand("click-background")
             _ = onBackgroundClick?()
+        case "click-settings":
+            recordMouseCommand("click-settings")
+            clickSettingsButton(source: "debug")
         case "hover-card" where parts.count == 3:
             guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
                 recordMouseCommand("invalid:\(command)")
@@ -2244,6 +2272,26 @@
         closeFeedbackTextLayer.truncationMode = .end
         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)
     }
 
     private func rebuildAppShelfItems() {
@@ -2581,8 +2629,40 @@
         layoutWaterfallColumns()
         layoutCloseConfirmation()
         layoutCloseFeedback()
+        layoutSettingsButton()
 
         CATransaction.commit()
+    }
+
+    private func layoutSettingsButton() {
+        let size: CGFloat = 34
+        let topMargin = topSafeAreaInset + RootLayoutMetrics.topContentMargin
+        let x = max(
+            RootLayoutMetrics.horizontalMargin,
+            glassLayer.bounds.width - RootLayoutMetrics.horizontalMargin - size
+        )
+        let y = max(
+            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)
+            .cgColor
+        settingsButtonLayer.borderColor = NSColor.separatorColor
+            .withAlphaComponent(isSettingsButtonHovered ? 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
+            : NSColor.secondaryLabelColor
+        ).cgColor
     }
 
     private var topSafeAreaInset: CGFloat {
@@ -2896,6 +2976,26 @@
 
     private func spaceLaneSegment(spaceID: UInt64) -> SpaceLaneSegmentLayers? {
         spaceLaneSegments.first { $0.space.id == spaceID }
+    }
+
+    private func settingsButtonContains(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)
+    }
+
+    private func clickSettingsButton(source: String) {
+        DevelopmentDiagnostics.log("quickSwitch.view.clickSettings", [
+            "source": source
+        ])
+        lastMouseCommand = "click-settings"
+        if !mouseCommandsApplied.contains("click-settings") {
+            mouseCommandsApplied.append("click-settings")
+        }
+        onOpenSettings?()
     }
 
     private func appShelfItem(at windowLocation: CGPoint) -> AppShelfItemLayers? {
@@ -4381,6 +4481,21 @@
         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(
         string: String,
         fontSize: CGFloat,
diff --git a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
index 5a406f5..13457a2 100644
--- a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -86,6 +86,7 @@
     private var lifecycleResidualOverlayWindows = 0
     private var lifecycleResidualVisibleOverlayWindows = 0
     var onSnapshotUpdated: (() -> Void)?
+    var onOpenSettings: (() -> Void)?
 
     private struct PendingCloseVerification {
         let request: QuickSwitchCloseRequest
@@ -212,6 +213,9 @@
         view.onBackgroundClick = { [weak self] in
             self?.handleBackgroundClick() ?? false
         }
+        view.onOpenSettings = { [weak self] in
+            self?.onOpenSettings?()
+        }
         view.setSuppressEventInput(!debugMouseSequence.isEmpty || !debugKeySequence.isEmpty)
         view.onRequestClose = { [weak self] request in
             self?.requestClose(request)
diff --git a/C1.source/Sources/Aligner/SettingsWindowController.swift b/C1.source/Sources/Aligner/SettingsWindowController.swift
new file mode 100644
index 0000000..7274c9e
--- /dev/null
+++ b/C1.source/Sources/Aligner/SettingsWindowController.swift
@@ -0,0 +1,138 @@
+import AppKit
+import AlignerCore
+
+@MainActor
+final class SettingsWindowController: NSWindowController {
+    private let modeProvider: () -> QuickSwitchWaterfallViewMode
+    private let onWaterfallViewModeChanged: (QuickSwitchWaterfallViewMode) -> Void
+    private let tabView = NSTabView()
+    private let verticalColumnsButton = NSButton(
+        radioButtonWithTitle: "纵栏瀑布",
+        target: nil,
+        action: nil
+    )
+    private let horizontalMasonryButton = NSButton(
+        radioButtonWithTitle: "横栏瀑布",
+        target: nil,
+        action: nil
+    )
+
+    init(
+        modeProvider: @escaping () -> QuickSwitchWaterfallViewMode,
+        onWaterfallViewModeChanged: @escaping (QuickSwitchWaterfallViewMode) -> Void
+    ) {
+        self.modeProvider = modeProvider
+        self.onWaterfallViewModeChanged = onWaterfallViewModeChanged
+
+        let window = NSWindow(
+            contentRect: NSRect(x: 0, y: 0, width: 520, height: 340),
+            styleMask: [.titled, .closable, .miniaturizable],
+            backing: .buffered,
+            defer: false
+        )
+        window.title = "Aligner 设置"
+        window.isReleasedWhenClosed = false
+        window.center()
+
+        super.init(window: window)
+        window.contentView = makeContentView()
+        refreshWaterfallViewMode()
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) {
+        nil
+    }
+
+    override func showWindow(_ sender: Any?) {
+        refreshWaterfallViewMode()
+        tabView.selectTabViewItem(withIdentifier: "view")
+        super.showWindow(sender)
+        window?.makeKeyAndOrderFront(sender)
+    }
+
+    func refreshWaterfallViewMode() {
+        let mode = modeProvider()
+        verticalColumnsButton.state = mode == .verticalColumns ? .on : .off
+        horizontalMasonryButton.state = mode == .horizontalMasonry ? .on : .off
+    }
+
+    func reportDictionary() -> [String: Any] {
+        [
+            "exists": true,
+            "visible": window?.isVisible ?? false,
+            "title": window?.title ?? "",
+            "selectedTabLabel": tabView.selectedTabViewItem?.label ?? "",
+            "selectedTabIdentifier": tabView.selectedTabViewItem?.identifier as? String ?? "",
+            "waterfallViewMode": modeProvider().rawValue,
+            "hasViewTab": tabView.tabViewItems.contains { $0.identifier as? String == "view" },
+            "usesSwiftUI": false
+        ]
+    }
+
+    private func makeContentView() -> NSView {
+        tabView.translatesAutoresizingMaskIntoConstraints = false
+        tabView.tabViewType = .topTabsBezelBorder
+        tabView.addTabViewItem(makeViewTab())
+
+        let root = NSView()
+        root.addSubview(tabView)
+        NSLayoutConstraint.activate([
+            tabView.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 20),
+            tabView.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -20),
+            tabView.topAnchor.constraint(equalTo: root.topAnchor, constant: 18),
+            tabView.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -20)
+        ])
+        return root
+    }
+
+    private func makeViewTab() -> NSTabViewItem {
+        let tab = NSTabViewItem(identifier: "view")
+        tab.label = "视图"
+        tab.view = makeViewSettingsView()
+        return tab
+    }
+
+    private func makeViewSettingsView() -> NSView {
+        let title = NSTextField(labelWithString: "Quick Switch 视图")
+        title.font = .systemFont(ofSize: 18, weight: .semibold)
+
+        let subtitle = NSTextField(wrappingLabelWithString: "切换后立即生效。")
+        subtitle.textColor = .secondaryLabelColor
+
+        verticalColumnsButton.target = self
+        verticalColumnsButton.action = #selector(selectVerticalColumns)
+        horizontalMasonryButton.target = self
+        horizontalMasonryButton.action = #selector(selectHorizontalMasonry)
+
+        let modeStack = NSStackView(views: [verticalColumnsButton, horizontalMasonryButton])
+        modeStack.orientation = .vertical
+        modeStack.alignment = .leading
+        modeStack.spacing = 10
+
+        let stack = NSStackView(views: [title, subtitle, modeStack])
+        stack.orientation = .vertical
+        stack.alignment = .leading
+        stack.spacing = 14
+        stack.translatesAutoresizingMaskIntoConstraints = false
+
+        let view = NSView()
+        view.addSubview(stack)
+        NSLayoutConstraint.activate([
+            stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 22),
+            stack.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -22),
+            stack.topAnchor.constraint(equalTo: view.topAnchor, constant: 24)
+        ])
+        return view
+    }
+
+    @objc private func selectVerticalColumns() {
+        onWaterfallViewModeChanged(.verticalColumns)
+        refreshWaterfallViewMode()
+    }
+
+    @objc private func selectHorizontalMasonry() {
+        onWaterfallViewModeChanged(.horizontalMasonry)
+        refreshWaterfallViewMode()
+    }
+}
diff --git a/C3.tools/round1-view-mode-entry-qa.sh b/C3.tools/round1-view-mode-entry-qa.sh
index 87491ee..cb53ff4 100755
--- a/C3.tools/round1-view-mode-entry-qa.sh
+++ b/C3.tools/round1-view-mode-entry-qa.sh
@@ -12,6 +12,7 @@
 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"
 DOMAIN="com.ar.Aligner"
 WATERFALL_MODE_KEY="com.ar.Aligner.preferences.quickSwitch.waterfallViewMode"
 REPORT_WAIT="${ALIGNER_ROUND1_VIEW_MODE_ENTRY_REPORT_WAIT:-6.0}"
@@ -131,6 +132,8 @@
         sys.exit(1)
 
 status_menu = report.get("statusMenu", {})
+main_menu = report.get("mainMenu", {})
+settings_window = report.get("settingsWindow", {})
 root = report.get("rootView", {})
 
 require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
@@ -139,11 +142,23 @@
 require(report.get("resolvedWaterfallViewMode") == expected_mode, "resolved Waterfall mode must match expected mode")
 require(root.get("waterfallViewMode") == expected_mode, "root view Waterfall mode must match expected mode")
 require(status_menu.get("hasAboutItem") is True, "status menu must expose About Aligner")
+require(status_menu.get("hasSettingsItem") is True, "status menu must expose Settings")
 require(status_menu.get("hasQuickSwitchViewSubmenu") is True, "status menu must expose Quick Switch View submenu")
 require(status_menu.get("hasVerticalColumnsItem") is True, "status menu must expose Vertical Columns item")
 require(status_menu.get("hasHorizontalWaterfallItem") is True, "status menu must expose Horizontal Waterfall item")
 require(status_menu.get("verticalColumnsState") == expected_vertical_state, "Vertical Columns check state must match expected mode")
 require(status_menu.get("horizontalWaterfallState") == expected_horizontal_state, "Horizontal Waterfall check state must match expected mode")
+require(main_menu.get("hasMenu") is True, "main menu must exist")
+require(main_menu.get("hasAppMenu") is True, "main menu must expose app menu")
+require(main_menu.get("hasSettingsItem") is True, "main menu must expose Settings")
+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(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")
 require(re.match(r"^[0-9]+\.[0-9]+\.[0-9]+$", app_version) is not None, "version must be MAJOR.MINOR.PATCH")
@@ -153,7 +168,94 @@
     "mode": expected_mode,
     "aboutVersion": report.get("aboutVersion"),
     "aboutBuild": report.get("aboutBuild"),
-    "statusMenu": status_menu
+    "statusMenu": status_menu,
+    "mainMenu": main_menu,
+    "settingsButtonFrame": settings_frame
+}, indent=2, ensure_ascii=False))
+PY
+}
+
+wait_for_settings_report() {
+  local report="$1"
+
+  /usr/bin/python3 - "$report" "$REPORT_WAIT" <<'PY'
+import json
+import sys
+import time
+
+path = sys.argv[1]
+timeout = float(sys.argv[2])
+deadline = time.monotonic() + timeout
+last_report = None
+
+while time.monotonic() < deadline:
+    try:
+        with open(path, "r", encoding="utf-8") as file:
+            report = json.load(file)
+        last_report = report
+        if report.get("settingsWindow", {}).get("visible") is True:
+            sys.exit(0)
+    except FileNotFoundError:
+        pass
+    except json.JSONDecodeError:
+        pass
+    time.sleep(0.2)
+
+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)
+sys.exit(1)
+PY
+}
+
+assert_settings_report() {
+  local report="$1"
+  local app_version="$2"
+  local app_build="$3"
+
+  /usr/bin/python3 - "$report" "$app_version" "$app_build" <<'PY'
+import json
+import sys
+
+path = sys.argv[1]
+app_version = sys.argv[2]
+app_build = sys.argv[3]
+
+with open(path, "r", encoding="utf-8") as file:
+    report = json.load(file)
+
+def require(condition, message):
+    if not condition:
+        print(message, file=sys.stderr)
+        print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
+        sys.exit(1)
+
+root = report.get("rootView", {})
+main_menu = report.get("mainMenu", {})
+settings_window = report.get("settingsWindow", {})
+
+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(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(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",
+    "quickSwitchVisible": report.get("quickSwitchVisible"),
+    "lastMouseCommand": root.get("lastMouseCommand"),
+    "settingsWindow": settings_window,
+    "mainMenu": main_menu
 }, indent=2, ensure_ascii=False))
 PY
 }
@@ -186,6 +288,31 @@
   stop_current_aligner
 }
 
+run_settings_case() {
+  local report="$1"
+  local app_version="$2"
+  local app_build="$3"
+
+  stop_current_aligner
+  rm -f "$report"
+  /usr/bin/defaults write "$DOMAIN" "$WATERFALL_MODE_KEY" -string "verticalColumns"
+
+  "$APP/Contents/MacOS/Aligner" \
+    --round0-skip-permissions \
+    --round01-open-quick-switch \
+    --round01-fixture-app-count=4 \
+    --round01-fixture-windows-per-app=2 \
+    --round01-disable-screenshot-refresh \
+    --round01-debug-mouse-sequence="click-settings" \
+    --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 "$!" || true
+  stop_current_aligner
+}
+
 [ -x "$APP/Contents/MacOS/Aligner" ] || fail "missing executable app at $APP"
 INFO_PLIST="$APP/Contents/Info.plist"
 APP_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$INFO_PLIST" 2>/dev/null)" \
@@ -198,5 +325,6 @@
 
 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"
 
 echo "Round01.1 view-mode entry QA passed"

--
Gitblit v1.9.3