From aba608b433a9dd7e218239947c2c983741fc7fc7 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 17 Jun 2026 20:42:14 +0800
Subject: [PATCH] Prepare 7.9.2 build 20260617.2028
---
src/build.sh | 79 ++++++++++++
src/CHANGELOG.md | 11 +
src/Apptag/Info.plist | 4
src/Apptag/ContentView.swift | 34 +++++
src/Scripts/tag_navigation_hover_scroll_qa.sh | 74 ++++++++++++
src/Scripts/macos14_availability_typecheck_qa.sh | 78 ++++++++++++
src/Apptag/ApptagApp.swift | 48 +++++++
7 files changed, 318 insertions(+), 10 deletions(-)
diff --git a/src/Apptag/ApptagApp.swift b/src/Apptag/ApptagApp.swift
index e4efd6d..f0df77a 100644
--- a/src/Apptag/ApptagApp.swift
+++ b/src/Apptag/ApptagApp.swift
@@ -342,11 +342,53 @@
}
}
+ private func foregroundWindowForActivationPolicyRestore() -> NSWindow? {
+ if let settingsWindow, settingsWindow.isVisible {
+ return settingsWindow
+ }
+ if let overlayWindow, overlayWindow.isVisible {
+ return overlayWindow
+ }
+ return NSApp.keyWindow
+ }
+
+ private func setLauncherActivationPolicy(_ desiredPolicy: NSApplication.ActivationPolicy) {
+ guard NSApp.activationPolicy() != desiredPolicy else { return }
+
+ let restoreWindow = desiredPolicy == .accessory && NSApp.isActive
+ ? foregroundWindowForActivationPolicyRestore()
+ : nil
+
+ // Deactivate before switching regular -> accessory; otherwise Dock can
+ // keep a stale running tile until Dock itself restarts.
+ if desiredPolicy == .accessory && NSApp.isActive {
+ NSApp.deactivate()
+ }
+
+ NSApp.setActivationPolicy(desiredPolicy)
+
+ guard desiredPolicy == .accessory,
+ let restoreWindow,
+ restoreWindow.isVisible
+ else { return }
+
+ DispatchQueue.main.async { [weak self, weak restoreWindow] in
+ guard let self,
+ let restoreWindow,
+ restoreWindow.isVisible,
+ self.requiresForegroundOwnership
+ else { return }
+ NSApp.activate(ignoringOtherApps: true)
+ restoreWindow.makeKeyAndOrderFront(nil)
+ restoreWindow.orderFrontRegardless()
+ }
+ }
+
private func beginLauncherForegroundOwnership(activate: Bool = true, keyWindow: NSWindow? = nil) {
let showDock = UserDefaults.standard.bool(forKey: Self.showDockIconKey)
let desiredPolicy: NSApplication.ActivationPolicy = showDock ? .regular : .accessory
if NSApp.activationPolicy() != desiredPolicy {
- NSApp.setActivationPolicy(desiredPolicy)
+ setLauncherActivationPolicy(desiredPolicy)
}
if activate, showDock {
claimLauncherForeground(keyWindow: keyWindow)
@@ -363,7 +405,7 @@
overlayGeneration expectedOverlayGeneration: Int? = nil
) {
if NSApp.activationPolicy() != .regular {
- NSApp.setActivationPolicy(.regular)
+ setLauncherActivationPolicy(.regular)
}
if isOverlayVisible && !NSApp.presentationOptions.contains(.hideDock) {
@@ -425,7 +467,7 @@
? .regular
: (showDock ? .regular : .accessory))))
if NSApp.activationPolicy() != desiredPolicy {
- NSApp.setActivationPolicy(desiredPolicy)
+ setLauncherActivationPolicy(desiredPolicy)
}
let desiredPresentation: NSApplication.PresentationOptions = isOverlayVisible ? [.hideDock] : []
diff --git a/src/Apptag/ContentView.swift b/src/Apptag/ContentView.swift
index ed6f932..745a918 100644
--- a/src/Apptag/ContentView.swift
+++ b/src/Apptag/ContentView.swift
@@ -82,6 +82,10 @@
}
func updateNSView(_ container: TextFieldContainer, context: Context) {
+ if let editor = container.textField.currentEditor() as? NSTextView,
+ editor.hasMarkedText() {
+ return
+ }
if container.textField.stringValue != text {
container.textField.stringValue = text
}
@@ -106,6 +110,9 @@
}
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
+ if textView.hasMarkedText() {
+ return false
+ }
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
onSubmit?()
return true
@@ -319,6 +326,8 @@
var bubbleDraftNote = ""
var tagNavigationHoveredGroupName: String? = nil
var tagNavigationAppDropTargetName: String? = nil
+ var tagNavigationLastHoverScrollID: String? = nil
+ var tagNavigationLastHoverScrollAt: Date? = nil
}
private struct EditActionFeedback: Identifiable {
@@ -436,6 +445,8 @@
?? (isColorlessContainerMode ? filledColorlessContainer : nil)
}
private let rightSidebarFloatingClearance: CGFloat = 44
+ private let tagNavigationHoverScrollDelay: TimeInterval = 0.14
+ private let tagNavigationHoverScrollInterval: TimeInterval = 0.22
private var floatingButtonSurfaceColor: Color {
colorScheme == .dark
? Color.white.opacity(0.10)
@@ -2040,7 +2051,7 @@
}
private func handleTagNavigationHover(_ id: String, active: Bool) {
- if appGridInteraction.appDragModeActive {
+ if appGridInteraction.appDragModeActive || tagNavDragModeActive {
if !active, appGridInteraction.tagNavigationHoveredGroupName == id {
appGridInteraction.tagNavigationHoveredGroupName = nil
}
@@ -2056,6 +2067,7 @@
appGridInteraction.tagNavigationHoveredGroupName = id
fillColorlessContainer(id)
+ scheduleTagNavigationHoverScroll(id)
}
private func handleTagNavigationAppDropHover(_ id: String, active: Bool) {
@@ -2076,6 +2088,26 @@
appGridInteraction.tagNavigationAppDropTargetName = id
}
+ private func scheduleTagNavigationHoverScroll(_ id: String) {
+ DispatchQueue.main.asyncAfter(deadline: .now() + tagNavigationHoverScrollDelay) {
+ guard appGridInteraction.tagNavigationHoveredGroupName == id else { return }
+ guard !appGridInteraction.appDragModeActive && !tagNavDragModeActive else { return }
+ scrollToTagFromHover(id)
+ }
+ }
+
+ private func scrollToTagFromHover(_ id: String) {
+ let now = Date()
+ if appGridInteraction.tagNavigationLastHoverScrollID == id,
+ let lastScrollAt = appGridInteraction.tagNavigationLastHoverScrollAt,
+ now.timeIntervalSince(lastScrollAt) < tagNavigationHoverScrollInterval {
+ return
+ }
+ appGridInteraction.tagNavigationLastHoverScrollID = id
+ appGridInteraction.tagNavigationLastHoverScrollAt = now
+ scrollTo(id)
+ }
+
private func fillColorlessContainer(_ id: String) {
guard isColorlessContainerMode else { return }
guard filledColorlessContainer != id else { return }
diff --git a/src/Apptag/Info.plist b/src/Apptag/Info.plist
index 4cf0480..8faf870 100644
--- a/src/Apptag/Info.plist
+++ b/src/Apptag/Info.plist
@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
- <string>7.9.1</string>
+ <string>7.9.2</string>
<key>CFBundleVersion</key>
- <string>20260614.1454</string>
+ <string>20260617.2028</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 44d76aa..c8782d5 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -1,5 +1,16 @@
# TagLauncher Changelog
+## [7.9.2] — 2026-06-17
+
+- 恢复 App Grid 标签列表 hover 自动滚动:鼠标停到某个标签时会自动滚动到对应容器,点击标签仍保持立即滚动
+- 为 hover 自动滚动增加短延迟和同标签节流,避免快速扫过标签时重新引入容器抖动
+- 保留拖拽保护:拖动应用或拖动标签排序时,标签 hover 不会触发自动滚动,避免干扰打标签和排序操作
+- 修复标签输入偶发只能输入英文的问题;中文输入法组合字期间不再被 UI 状态刷新打断
+- 修复关闭“在 Dock 中显示”后 Dock 图标可能残留的问题,切换到菜单栏模式时会更稳定地刷新 macOS activation policy
+- 加固构建脚本和 macOS 14 typecheck QA 的 SDK 选择逻辑,避免本机 Swift / Xcode SDK 不匹配时无法生成包
+- 新增 `tag_navigation_hover_scroll_qa.sh`,把“hover 标签必须触发受控滚动”纳入回归门禁
+- 版本号更新为 `7.9.2`,Build 更新为 `20260617.2028`
+
## [7.9.1] — 2026-06-13
- 修复新建空标签后 App Grid 左侧标签栏和主体区域可能不显示该标签的问题;空标签现在会按当前视图显示最小容器,避免用户以为创建失败
diff --git a/src/Scripts/macos14_availability_typecheck_qa.sh b/src/Scripts/macos14_availability_typecheck_qa.sh
index a756e9c..89d00b0 100755
--- a/src/Scripts/macos14_availability_typecheck_qa.sh
+++ b/src/Scripts/macos14_availability_typecheck_qa.sh
@@ -5,13 +5,14 @@
SWIFT_DIR="$ROOT_DIR/Apptag"
MACOS_DEPLOYMENT_TARGET="${MACOS_DEPLOYMENT_TARGET:-14.0}"
TARGET_ARCHES_RAW="${TARGET_ARCHES:-arm64}"
+SWIFTC="${SWIFTC:-swiftc}"
if [[ ! "$MACOS_DEPLOYMENT_TARGET" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
echo "FAIL: invalid MACOS_DEPLOYMENT_TARGET: $MACOS_DEPLOYMENT_TARGET" >&2
exit 1
fi
-SDK_PATH="$(xcrun --sdk macosx --show-sdk-path)"
+SDK_PATH="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true)"
MODULE_CACHE_DIR="${MODULE_CACHE_DIR:-${TMPDIR:-/tmp}/taglauncher-macos14-typecheck-cache}"
mkdir -p "$MODULE_CACHE_DIR"
@@ -30,6 +31,76 @@
[[ -n "$arch" ]] && TARGET_ARCHES+=("$arch")
done < <(tr ',' '\n' <<<"$TARGET_ARCHES_RAW" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
+select_macos_sdk() {
+ local target="$1"
+ local explicit_sdk="${SDK_PATH_OVERRIDE:-${SDKROOT:-}}"
+ if [[ -n "$explicit_sdk" ]]; then
+ if [[ ! -d "$explicit_sdk" ]]; then
+ echo "FAIL: explicit SDK path does not exist: $explicit_sdk" >&2
+ return 1
+ fi
+ echo "$explicit_sdk"
+ return 0
+ fi
+
+ local default_sdk="$SDK_PATH"
+ local sdk_candidates
+ sdk_candidates="$(python3 - "$default_sdk" <<'PY'
+import glob
+import os
+import re
+import sys
+
+seen = set()
+candidates = []
+
+def add(path):
+ if not path:
+ return
+ real = os.path.realpath(path)
+ if os.path.isdir(path) and real not in seen:
+ seen.add(real)
+ candidates.append(path)
+
+if len(sys.argv) > 1:
+ add(sys.argv[1])
+
+for path in glob.glob("/Library/Developer/CommandLineTools/SDKs/MacOSX*.sdk"):
+ add(path)
+
+for path in glob.glob("/Applications/Xcode*.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX*.sdk"):
+ add(path)
+
+def version_key(path):
+ name = os.path.basename(path)
+ match = re.search(r"MacOSX(\d+(?:\.\d+)*)\.sdk$", name)
+ if not match:
+ return ()
+ return tuple(int(part) for part in match.group(1).split("."))
+
+default_real = os.path.realpath(sys.argv[1]) if len(sys.argv) > 1 else ""
+rest = [p for p in candidates if os.path.realpath(p) != default_real]
+rest.sort(key=version_key, reverse=True)
+ordered = [p for p in candidates if os.path.realpath(p) == default_real] + rest
+for path in ordered:
+ print(path)
+PY
+)"
+
+ while IFS= read -r sdk; do
+ [[ -n "$sdk" ]] || continue
+ if printf 'import Swift\n' | "$SWIFTC" -typecheck -sdk "$sdk" -target "$target" - >/dev/null 2>&1; then
+ echo "$sdk"
+ return 0
+ fi
+ done <<< "$sdk_candidates"
+
+ echo "FAIL: no installed macOS SDK is compatible with current swiftc for target $target" >&2
+ echo "swiftc: $("$SWIFTC" --version | head -n 1)" >&2
+ echo "default SDK: $default_sdk" >&2
+ return 1
+}
+
for arch in "${TARGET_ARCHES[@]}"; do
case "$arch" in
arm64|x86_64) ;;
@@ -40,8 +111,11 @@
esac
target="${arch}-apple-macosx${MACOS_DEPLOYMENT_TARGET}"
+ SDK_PATH="$(select_macos_sdk "$target")"
echo "==> Typechecking for $target"
- swiftc -typecheck \
+ echo "==> Using SDK: $SDK_PATH"
+ echo "==> Using Swift compiler: $SWIFTC"
+ "$SWIFTC" -typecheck \
-framework AppKit \
-framework SwiftUI \
-framework Carbon \
diff --git a/src/Scripts/tag_navigation_hover_scroll_qa.sh b/src/Scripts/tag_navigation_hover_scroll_qa.sh
new file mode 100755
index 0000000..b275908
--- /dev/null
+++ b/src/Scripts/tag_navigation_hover_scroll_qa.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+CONTENT_VIEW_SWIFT="$ROOT_DIR/Apptag/ContentView.swift"
+
+fail() {
+ printf 'FAIL: %s\n' "$*" >&2
+ exit 1
+}
+
+[[ -f "$CONTENT_VIEW_SWIFT" ]] || fail "missing ContentView.swift"
+
+python3 - "$CONTENT_VIEW_SWIFT" <<'PY'
+import pathlib
+import re
+import sys
+
+content = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")
+
+
+def fail(message: str) -> None:
+ print(f"FAIL: {message}", file=sys.stderr)
+ sys.exit(1)
+
+
+def require(pattern: str, source: str, message: str) -> None:
+ if not re.search(pattern, source, re.S):
+ fail(message)
+
+
+require(
+ r"var\s+tagNavigationLastHoverScrollID\s*:\s*String\?\s*=\s*nil",
+ content,
+ "hover scroll must remember the last scrolled tag id",
+)
+require(
+ r"var\s+tagNavigationLastHoverScrollAt\s*:\s*Date\?\s*=\s*nil",
+ content,
+ "hover scroll must remember the last scroll time",
+)
+require(
+ r"private\s+let\s+tagNavigationHoverScrollDelay\s*:\s*TimeInterval\s*=\s*0\.14",
+ content,
+ "hover scroll must use the reviewed short hover-intent delay",
+)
+require(
+ r"private\s+let\s+tagNavigationHoverScrollInterval\s*:\s*TimeInterval\s*=\s*0\.22",
+ content,
+ "hover scroll must keep throttling repeated scrolls for the same tag",
+)
+require(
+ r"private\s+func\s+activateTagNavigation\(_\s+id:\s+String\)\s*\{(?P<body>.*?)scrollTo\(id\)(?P<body2>.*?)\n\s*\}",
+ content,
+ "clicking a tag must still scroll immediately",
+)
+require(
+ r"private\s+func\s+handleTagNavigationHover\(_\s+id:\s+String,\s+active:\s+Bool\)\s*\{(?P<body>.*?)appGridInteraction\.appDragModeActive\s*\|\|\s*tagNavDragModeActive(?P<body2>.*?)scheduleTagNavigationHoverScroll\(id\)",
+ content,
+ "hovering a tag must schedule controlled scrolling and must be disabled during drag states",
+)
+require(
+ r"private\s+func\s+scheduleTagNavigationHoverScroll\(_\s+id:\s+String\)\s*\{(?P<body>.*?)DispatchQueue\.main\.asyncAfter\(deadline:\s*\.now\(\)\s*\+\s*tagNavigationHoverScrollDelay\)(?P<body2>.*?)tagNavigationHoveredGroupName\s*==\s*id(?P<body3>.*?)!appGridInteraction\.appDragModeActive\s*&&\s*!tagNavDragModeActive(?P<body4>.*?)scrollToTagFromHover\(id\)",
+ content,
+ "scheduled hover scroll must only fire if the same tag is still hovered and no drag state is active",
+)
+require(
+ r"private\s+func\s+scrollToTagFromHover\(_\s+id:\s+String\)\s*\{(?P<body>.*?)tagNavigationLastHoverScrollID\s*==\s*id(?P<body2>.*?)timeIntervalSince\(lastScrollAt\)\s*<\s*tagNavigationHoverScrollInterval(?P<body3>.*?)scrollTo\(id\)",
+ content,
+ "hover scrolling must be throttled and eventually call scrollTo(id)",
+)
+
+print("PASS: tag navigation hover scroll semantics are guarded")
+PY
diff --git a/src/build.sh b/src/build.sh
index e91bfd2..7cd66d7 100644
--- a/src/build.sh
+++ b/src/build.sh
@@ -12,10 +12,83 @@
INFO_PLIST="$SWIFT_DIR/Info.plist"
MODULE_CACHE_DIR="${MODULE_CACHE_DIR:-$BUILD_DIR/ModuleCache}"
-SDK_PATH=$(xcrun --sdk macosx --show-sdk-path)
MACOS_DEPLOYMENT_TARGET="${MACOS_DEPLOYMENT_TARGET:-14.0}"
TARGET_ARCH="${TARGET_ARCH:-arm64}"
TARGET="${TARGET_ARCH}-apple-macosx${MACOS_DEPLOYMENT_TARGET}"
+SWIFTC="${SWIFTC:-swiftc}"
+
+select_macos_sdk() {
+ local target="$1"
+ local explicit_sdk="${SDK_PATH:-${SDKROOT:-}}"
+ if [ -n "$explicit_sdk" ]; then
+ if [ ! -d "$explicit_sdk" ]; then
+ echo "❌ Explicit SDK path does not exist: $explicit_sdk" >&2
+ return 1
+ fi
+ echo "$explicit_sdk"
+ return 0
+ fi
+
+ local default_sdk
+ default_sdk="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true)"
+ local sdk_candidates
+ sdk_candidates="$(python3 - "$default_sdk" <<'PY'
+import glob
+import os
+import re
+import sys
+
+seen = set()
+candidates = []
+
+def add(path):
+ if not path:
+ return
+ real = os.path.realpath(path)
+ if os.path.isdir(path) and real not in seen:
+ seen.add(real)
+ candidates.append(path)
+
+if len(sys.argv) > 1:
+ add(sys.argv[1])
+
+for path in glob.glob("/Library/Developer/CommandLineTools/SDKs/MacOSX*.sdk"):
+ add(path)
+
+for path in glob.glob("/Applications/Xcode*.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX*.sdk"):
+ add(path)
+
+def version_key(path):
+ name = os.path.basename(path)
+ match = re.search(r"MacOSX(\d+(?:\.\d+)*)\.sdk$", name)
+ if not match:
+ return ()
+ return tuple(int(part) for part in match.group(1).split("."))
+
+default_real = os.path.realpath(sys.argv[1]) if len(sys.argv) > 1 else ""
+rest = [p for p in candidates if os.path.realpath(p) != default_real]
+rest.sort(key=version_key, reverse=True)
+ordered = [p for p in candidates if os.path.realpath(p) == default_real] + rest
+for path in ordered:
+ print(path)
+PY
+)"
+
+ while IFS= read -r sdk; do
+ [ -n "$sdk" ] || continue
+ if printf 'import Swift\n' | "$SWIFTC" -typecheck -sdk "$sdk" -target "$target" - >/dev/null 2>&1; then
+ echo "$sdk"
+ return 0
+ fi
+ done <<< "$sdk_candidates"
+
+ echo "❌ No installed macOS SDK is compatible with current swiftc for target $target" >&2
+ echo " swiftc: $("$SWIFTC" --version | head -n 1)" >&2
+ echo " default SDK: $default_sdk" >&2
+ return 1
+}
+
+SDK_PATH="$(select_macos_sdk "$TARGET")"
APP_VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$INFO_PLIST")
APP_BUILD="${APP_BUILD:-$(date '+%Y%m%d.%H%M')}"
@@ -26,6 +99,8 @@
fi
echo "==> Packaging $APP_NAME $APP_VERSION ($APP_BUILD)"
+echo "==> Using SDK: $SDK_PATH"
+echo "==> Using Swift compiler: $SWIFTC"
# --- Optional: App Store / Sandbox signing ---
# Set CODESIGN_IDENTITY to your "Apple Distribution" or "Mac Developer" cert name.
@@ -58,7 +133,7 @@
while IFS= read -r file; do
SWIFT_FILES+=("$file")
done < <(find "$SWIFT_DIR" -name '*.swift' -print | sort)
-swiftc \
+"$SWIFTC" \
-o "$MACOS_DIR/$APP_NAME" \
-framework AppKit \
-framework SwiftUI \
--
Gitblit v1.9.3