Prepare 7.9.2 build 20260617.2028
6 files modified
1 files added
| | |
| | | } |
| | | } |
| | | |
| | | 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) |
| | |
| | | overlayGeneration expectedOverlayGeneration: Int? = nil |
| | | ) { |
| | | if NSApp.activationPolicy() != .regular { |
| | | NSApp.setActivationPolicy(.regular) |
| | | setLauncherActivationPolicy(.regular) |
| | | } |
| | | |
| | | if isOverlayVisible && !NSApp.presentationOptions.contains(.hideDock) { |
| | |
| | | ? .regular |
| | | : (showDock ? .regular : .accessory)))) |
| | | if NSApp.activationPolicy() != desiredPolicy { |
| | | NSApp.setActivationPolicy(desiredPolicy) |
| | | setLauncherActivationPolicy(desiredPolicy) |
| | | } |
| | | |
| | | let desiredPresentation: NSApplication.PresentationOptions = isOverlayVisible ? [.hideDock] : [] |
| | |
| | | } |
| | | |
| | | 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 |
| | | } |
| | |
| | | } |
| | | |
| | | func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { |
| | | if textView.hasMarkedText() { |
| | | return false |
| | | } |
| | | if commandSelector == #selector(NSResponder.insertNewline(_:)) { |
| | | onSubmit?() |
| | | return true |
| | |
| | | var bubbleDraftNote = "" |
| | | var tagNavigationHoveredGroupName: String? = nil |
| | | var tagNavigationAppDropTargetName: String? = nil |
| | | var tagNavigationLastHoverScrollID: String? = nil |
| | | var tagNavigationLastHoverScrollAt: Date? = nil |
| | | } |
| | | |
| | | private struct EditActionFeedback: Identifiable { |
| | |
| | | ?? (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) |
| | |
| | | } |
| | | |
| | | private func handleTagNavigationHover(_ id: String, active: Bool) { |
| | | if appGridInteraction.appDragModeActive { |
| | | if appGridInteraction.appDragModeActive || tagNavDragModeActive { |
| | | if !active, appGridInteraction.tagNavigationHoveredGroupName == id { |
| | | appGridInteraction.tagNavigationHoveredGroupName = nil |
| | | } |
| | |
| | | |
| | | appGridInteraction.tagNavigationHoveredGroupName = id |
| | | fillColorlessContainer(id) |
| | | scheduleTagNavigationHoverScroll(id) |
| | | } |
| | | |
| | | private func handleTagNavigationAppDropHover(_ id: String, active: Bool) { |
| | |
| | | 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 } |
| | |
| | | <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> |
| | |
| | | # 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 左侧标签栏和主体区域可能不显示该标签的问题;空标签现在会按当前视图显示最小容器,避免用户以为创建失败 |
| | |
| | | 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" |
| | | |
| | |
| | | [[ -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) ;; |
| | |
| | | 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 \ |
| New file |
| | |
| | | #!/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 |
| | |
| | | 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')}" |
| | |
| | | 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. |
| | |
| | | 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 \ |