Freeze 8.0.2 usage tips banner
| | |
| | | - Keeps the most recent in-process `AppLibrarySnapshot` so newly-created overlay views can render immediately while background refresh catches up. |
| | | |
| | | - AppGrid collection renderer: `src/Apptag/AppGridCollectionView.swift` |
| | | - AppKit `NSCollectionView` implementation for grouped app layout, drag/drop, app ordering, bubbles, and usage tips overlay. |
| | | - AppKit `NSCollectionView` implementation for grouped app layout, drag/drop, app ordering, bubbles, and the native split teaching-banner usage tips overlay. |
| | | - Should not own full-screen AppGrid theme gradients; it receives theme only for derived glass readability. |
| | | |
| | | - AppGrid theme model: `src/Apptag/AppGridTheme.swift` |
| | |
| | | - `src/Scripts/macos14_build_metadata_qa.sh` |
| | | - Verifies build metadata and deployment target. |
| | | - `src/Scripts/usage_tips_qa.sh` |
| | | - Verifies usage tips overlay and 29-language coverage. |
| | | - Verifies native usage tips overlay, split teaching-banner layout, theme-aware accent/readability, click-through protection, close action, ordered-list details, and 29-language coverage. |
| | | - `src/Scripts/tag_navigation_hover_scroll_qa.sh` |
| | | - Verifies tag hover scroll semantics remain guarded. |
| | | - `src/Scripts/tag_double_click_preferences_qa.sh` |
| | |
| | | - The default theme preserves the original light AppGrid background. |
| | | - Legacy users with `useDarkAppGrid=true` must land on the `deepBlue` theme. |
| | | - Tag navigation single-click must keep immediate scroll behavior. Hover must keep guarded auto-scroll. Long-press must keep tag reorder behavior. Double-click may open Preferences on the Tags tab but must not replace those existing behaviors. |
| | | - Usage tips must remain a native AppKit overlay, not SwiftUI. The bottom teaching banner must consume its own click region so it never opens underlying apps; only arrow hit regions change pages. |
| | | - Usage tips title/detail text must remain readable across the default, dark, and bright App Grid themes; all visible usage-tip copy must stay complete across 29 localization files. |
| | | |
| | | Last updated: 2026-06-24, tag double-click opens Tags settings. |
| | | Last updated: 2026-06-25, usage tips teaching banner visual optimization. |
| | |
| | | } |
| | | |
| | | enum AppGridUsageTipsMetrics { |
| | | static let barHeight: CGFloat = 136 |
| | | static let reservedHeight: CGFloat = 176 |
| | | static let barHeight: CGFloat = 154 |
| | | static let reservedHeight: CGFloat = 194 |
| | | static let bottomMargin: CGFloat = 20 |
| | | static let horizontalInset: CGFloat = 24 |
| | | static let minWidth: CGFloat = 640 |
| | |
| | | let onGroupActivate: (String) -> Void |
| | | let onScrollActivity: () -> Void |
| | | let onDragModeChange: (Bool) -> Void |
| | | let onHideUsageTips: () -> Void |
| | | let onUsageTipsHoverChange: (Bool) -> Void |
| | | |
| | | func makeCoordinator() -> Coordinator { |
| | |
| | | onGroupActivate: onGroupActivate, |
| | | onScrollActivity: onScrollActivity, |
| | | onDragModeChange: onDragModeChange, |
| | | onHideUsageTips: onHideUsageTips, |
| | | onUsageTipIndexChange: { selectedUsageTipIndexBinding.wrappedValue = $0 }, |
| | | onUsageTipsHoverChange: onUsageTipsHoverChange |
| | | ) |
| | |
| | | var onGroupActivate: (String) -> Void = { _ in } |
| | | var onScrollActivity: () -> Void = {} |
| | | var onDragModeChange: (Bool) -> Void = { _ in } |
| | | var onHideUsageTips: () -> Void = {} |
| | | var onUsageTipIndexChange: (Int) -> Void = { _ in } |
| | | var onUsageTipsHoverChange: (Bool) -> Void = { _ in } |
| | | |
| | |
| | | onGroupActivate: @escaping (String) -> Void, |
| | | onScrollActivity: @escaping () -> Void, |
| | | onDragModeChange: @escaping (Bool) -> Void, |
| | | onHideUsageTips: @escaping () -> Void, |
| | | onUsageTipIndexChange: @escaping (Int) -> Void, |
| | | onUsageTipsHoverChange: @escaping (Bool) -> Void |
| | | ) { |
| | |
| | | self.onGroupActivate = onGroupActivate |
| | | self.onScrollActivity = onScrollActivity |
| | | self.onDragModeChange = onDragModeChange |
| | | self.onHideUsageTips = onHideUsageTips |
| | | self.onUsageTipIndexChange = onUsageTipIndexChange |
| | | self.onUsageTipsHoverChange = onUsageTipsHoverChange |
| | | if !self.usageTipsVisible { |
| | |
| | | weak var coordinator: AppGridCollectionView.Coordinator? |
| | | |
| | | private let backgroundView = NSVisualEffectView() |
| | | private let titlePanelView = NSView() |
| | | private let iconView = AppGridDecorativeImageView() |
| | | private let titleLabel = NSTextField(labelWithString: "") |
| | | private let detailScrollView = NSScrollView() |
| | | private let detailLabel = NSTextField(labelWithString: "") |
| | | private let closeButton = AppGridUsageTipIconButton(systemImage: "xmark") |
| | | private let previousButton = AppGridUsageTipIconButton(systemImage: "chevron.left") |
| | | private let nextButton = AppGridUsageTipIconButton(systemImage: "chevron.right") |
| | | private let dotsView = AppGridUsageTipDotsView() |
| | |
| | | private var visualWidth: CGFloat = AppGridUsageTipsMetrics.minWidth |
| | | private var visualHeight: CGFloat = AppGridUsageTipsMetrics.barHeight |
| | | private var visualBottomMargin: CGFloat = AppGridUsageTipsMetrics.bottomMargin |
| | | private var currentDetailText = "" |
| | | |
| | | private let titleFont = NSFont.systemFont(ofSize: 24, weight: .semibold) |
| | | private let detailFont = NSFont.systemFont(ofSize: 24, weight: .regular) |
| | | private let titleFont = NSFont.systemFont(ofSize: 25, weight: .bold) |
| | | private let detailFont = NSFont.systemFont(ofSize: 24, weight: .medium) |
| | | |
| | | override var isFlipped: Bool { true } |
| | | override var acceptsFirstResponder: Bool { true } |
| | |
| | | let visualFrame = currentVisualFrame() |
| | | backgroundView.frame = visualFrame |
| | | |
| | | let paddingLeft: CGFloat = 22 |
| | | let paddingRight: CGFloat = 16 |
| | | let iconSize: CGFloat = 24 |
| | | let iconTextGap: CGFloat = 10 |
| | | let controlZoneWidth: CGFloat = 176 |
| | | let buttonGap: CGFloat = 6 |
| | | let buttonSize: CGFloat = 48 |
| | | let buttonDotsGap: CGFloat = 6 |
| | | let outerPaddingX: CGFloat = 20 |
| | | let outerPaddingY: CGFloat = 19 |
| | | let titlePanelWidth = min( |
| | | max(350, visualFrame.width * 0.24), |
| | | min(460, visualFrame.width * 0.36) |
| | | ) |
| | | let titlePanelHeight = max(1, min(116, visualFrame.height - outerPaddingY * 2)) |
| | | let titlePanelFrame = NSRect( |
| | | x: visualFrame.minX + outerPaddingX, |
| | | y: visualFrame.midY - titlePanelHeight / 2, |
| | | width: titlePanelWidth, |
| | | height: titlePanelHeight |
| | | ) |
| | | titlePanelView.frame = titlePanelFrame |
| | | |
| | | let iconSize: CGFloat = 26 |
| | | let controlZoneWidth: CGFloat = min(148, max(128, visualFrame.width * 0.08)) |
| | | let buttonGap: CGFloat = 12 |
| | | let buttonSize: CGFloat = 52 |
| | | let buttonDotsGap: CGFloat = 10 |
| | | let dotsHeight: CGFloat = 10 |
| | | let dotsWidth = dotsView.preferredWidth |
| | | |
| | | let titleX = visualFrame.minX + paddingLeft + iconSize + iconTextGap |
| | | let titleIconGap: CGFloat = 20 |
| | | let titleSidePadding: CGFloat = 28 |
| | | let titleX = titlePanelFrame.minX + titleSidePadding + iconSize + titleIconGap |
| | | let titleWidth = max(1, titlePanelFrame.maxX - titleX - titleSidePadding) |
| | | let titleHeight: CGFloat = 68 |
| | | let titleY = titlePanelFrame.midY - titleHeight / 2 |
| | | let detailGap = visualFrame.width < 980 ? CGFloat(30) : CGFloat(52) |
| | | let controlGap = visualFrame.width < 980 ? CGFloat(24) : CGFloat(36) |
| | | let detailX = titlePanelFrame.maxX + detailGap |
| | | let controlsMaxX = visualFrame.maxX - 54 |
| | | let textRight = controlsMaxX - controlZoneWidth - controlGap |
| | | let detailHeight: CGFloat = 88 |
| | | let detailY = visualFrame.minY + max(0, (visualFrame.height - detailHeight) / 2) |
| | | |
| | | let buttonsWidth = buttonSize * 2 + buttonGap |
| | | let buttonsGroupMinX = visualFrame.maxX - paddingRight - buttonsWidth |
| | | let textRight = visualFrame.maxX - controlZoneWidth |
| | | let availableTextWidth = max(1, textRight - titleX) |
| | | let titleWidth = availableTextWidth |
| | | let titleHeight: CGFloat = 32 |
| | | let detailHeight: CGFloat = 68 |
| | | let lineGap: CGFloat = 8 |
| | | let textBlockHeight = titleHeight + lineGap + detailHeight |
| | | let textBlockY = visualFrame.minY + max(0, (visualFrame.height - textBlockHeight) / 2) |
| | | let buttonsGroupMinX = controlsMaxX - buttonsWidth |
| | | |
| | | iconView.frame = NSRect( |
| | | x: visualFrame.minX + paddingLeft, |
| | | y: visualFrame.midY - iconSize / 2, |
| | | x: titlePanelFrame.minX + titleSidePadding, |
| | | y: titlePanelFrame.midY - iconSize / 2, |
| | | width: iconSize, |
| | | height: iconSize |
| | | ) |
| | | |
| | | titleLabel.frame = NSRect( |
| | | x: titleX, |
| | | y: textBlockY, |
| | | y: titleY, |
| | | width: titleWidth, |
| | | height: titleHeight |
| | | ) |
| | | |
| | | closeButton.frame = NSRect( |
| | | x: visualFrame.maxX - 40, |
| | | y: visualFrame.minY + 10, |
| | | width: 30, |
| | | height: 30 |
| | | ) |
| | | |
| | | let controlGroupHeight = buttonSize + buttonDotsGap + dotsHeight |
| | |
| | | height: dotsHeight |
| | | ) |
| | | |
| | | let detailX = titleX |
| | | let detailRight = textRight |
| | | detailScrollView.frame = NSRect( |
| | | x: detailX, |
| | | y: titleLabel.frame.maxY + lineGap, |
| | | width: max(1, detailRight - detailX), |
| | | y: detailY, |
| | | width: max(1, textRight - detailX), |
| | | height: detailHeight |
| | | ) |
| | | layoutDetailLabel() |
| | |
| | | let safeIndex = min(max(0, coordinator.selectedUsageTipIndex), coordinator.usageTips.count - 1) |
| | | let tip = coordinator.usageTips[safeIndex] |
| | | titleLabel.stringValue = tr(tip.titleKey) |
| | | detailLabel.stringValue = formattedTipDetail(tr(tip.detailKey)) |
| | | updateTipIcon(id: tip.id) |
| | | currentDetailText = formattedTipDetail(tr(tip.detailKey)) |
| | | closeButton.buttonAccessibilityLabel = tr("usageTips.close") |
| | | previousButton.buttonAccessibilityLabel = tr("usageTips.previous") |
| | | nextButton.buttonAccessibilityLabel = tr("usageTips.next") |
| | | dotsView.configure(count: coordinator.usageTips.count, selectedIndex: safeIndex) |
| | |
| | | } |
| | | |
| | | func preferredWidth(maxAvailableWidth: CGFloat) -> CGFloat { |
| | | let padding: CGFloat = 22 + 16 |
| | | let fixedWidth: CGFloat = 24 + 10 + 24 + 48 + 6 + 48 |
| | | let titleWidth = ceil(titleLabel.attributedStringValue.size().width) |
| | | let detailWidth = measuredDetailLineWidth() |
| | | let contentWidth = padding + fixedWidth + max(titleWidth, detailWidth) |
| | | let preferredWidth = max(AppGridUsageTipsMetrics.minWidth, contentWidth) |
| | | return min(maxAvailableWidth, preferredWidth) |
| | | max(1, maxAvailableWidth) |
| | | } |
| | | |
| | | private func setup() { |
| | |
| | | backgroundView.layer?.borderWidth = 1 |
| | | addSubview(backgroundView) |
| | | |
| | | let iconConfig = NSImage.SymbolConfiguration(pointSize: 24, weight: .semibold) |
| | | titlePanelView.wantsLayer = true |
| | | titlePanelView.layer?.cornerRadius = 18 |
| | | titlePanelView.layer?.masksToBounds = true |
| | | titlePanelView.layer?.borderWidth = 1 |
| | | addSubview(titlePanelView) |
| | | |
| | | let iconConfig = NSImage.SymbolConfiguration(pointSize: 26, weight: .bold) |
| | | iconView.image = NSImage( |
| | | systemSymbolName: "lightbulb.fill", |
| | | accessibilityDescription: nil |
| | |
| | | iconView.imageScaling = .scaleProportionallyDown |
| | | addSubview(iconView) |
| | | |
| | | configureLabel(titleLabel, font: titleFont, lineBreakMode: .byClipping) |
| | | configureLabel(titleLabel, font: titleFont, lineBreakMode: .byWordWrapping) |
| | | configureDetailLabel() |
| | | |
| | | detailScrollView.drawsBackground = false |
| | |
| | | detailScrollView.borderType = .noBorder |
| | | detailScrollView.documentView = detailLabel |
| | | |
| | | closeButton.action = { [weak self] in self?.hideUsageTips() } |
| | | previousButton.action = { [weak self] in self?.selectUsageTip(offset: -1) } |
| | | nextButton.action = { [weak self] in self?.selectUsageTip(offset: 1) } |
| | | |
| | | addSubview(titleLabel) |
| | | addSubview(detailScrollView) |
| | | addSubview(dotsView) |
| | | addSubview(closeButton) |
| | | addSubview(previousButton) |
| | | addSubview(nextButton) |
| | | |
| | |
| | | label.drawsBackground = false |
| | | label.isBordered = false |
| | | label.lineBreakMode = lineBreakMode |
| | | label.maximumNumberOfLines = 1 |
| | | label.alignment = .center |
| | | label.maximumNumberOfLines = 2 |
| | | label.alignment = .left |
| | | label.font = font |
| | | } |
| | | |
| | | private func configureDetailLabel() { |
| | | detailLabel.cell = NSTextFieldCell(textCell: "") |
| | | detailLabel.cell = AppGridCenteredMultilineTextFieldCell(textCell: "") |
| | | detailLabel.isEditable = false |
| | | detailLabel.isSelectable = false |
| | | detailLabel.drawsBackground = false |
| | |
| | | applyCoordinatorState() |
| | | } |
| | | |
| | | private func hideUsageTips() { |
| | | Diagnostics.log("usageTips.hud.close") |
| | | clearHoverState() |
| | | isHidden = true |
| | | coordinator?.onHideUsageTips() |
| | | } |
| | | |
| | | private func routeButtonClickIfNeeded(_ event: NSEvent) -> Bool { |
| | | let point = usageTipsPoint(for: event) |
| | | let hitOutset: CGFloat = 24 |
| | | let closeHitOutset: CGFloat = 8 |
| | | if closeButton.frame.insetBy(dx: -closeHitOutset, dy: -closeHitOutset).contains(point) { |
| | | closeButton.performPressFeedback() |
| | | hideUsageTips() |
| | | return true |
| | | } |
| | | let hitOutset: CGFloat = 6 |
| | | if nextButton.frame.insetBy(dx: -hitOutset, dy: -hitOutset).contains(point) { |
| | | Diagnostics.log("usageTips.hud.routedNext") |
| | | selectUsageTip(offset: 1) |
| | |
| | | detailLabel.frame = NSRect(x: 0, y: 0, width: width, height: detailScrollView.bounds.height) |
| | | } |
| | | |
| | | private func measuredDetailLineWidth() -> CGFloat { |
| | | let attributes: [NSAttributedString.Key: Any] = [.font: detailFont] |
| | | let lineWidths = detailLabel.stringValue |
| | | .components(separatedBy: .newlines) |
| | | .map { NSAttributedString(string: $0, attributes: attributes).size().width } |
| | | return ceil(lineWidths.max() ?? detailLabel.attributedStringValue.size().width) |
| | | } |
| | | |
| | | private func currentVisualFrame() -> NSRect { |
| | | let width = min(bounds.width, visualWidth) |
| | | let height = min(bounds.height, visualHeight) |
| | |
| | | } |
| | | |
| | | private func formattedTipDetail(_ text: String) -> String { |
| | | text |
| | | let normalized = text |
| | | .replacingOccurrences(of: "\\s*(?:-->|->|>|→|>|,)\\s*", with: "\n", options: .regularExpression) |
| | | .replacingOccurrences(of: "[ \\t]{2,}", with: " ", options: .regularExpression) |
| | | .replacingOccurrences(of: "\\n{2,}", with: "\n", options: .regularExpression) |
| | | .trimmingCharacters(in: .whitespacesAndNewlines) |
| | | return orderedTipDetail(normalized) |
| | | } |
| | | |
| | | private func orderedTipDetail(_ text: String) -> String { |
| | | let lines = text |
| | | .components(separatedBy: .newlines) |
| | | .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } |
| | | .filter { !$0.isEmpty } |
| | | guard lines.count > 1 else { return text } |
| | | return lines |
| | | .enumerated() |
| | | .map { "\($0.offset + 1). \($0.element)" } |
| | | .joined(separator: "\n") |
| | | } |
| | | |
| | | private func updateTipIcon(id: Int) { |
| | | let symbolName: String |
| | | switch id { |
| | | case 1: |
| | | symbolName = "tag.fill" |
| | | case 2: |
| | | symbolName = "checkmark.seal.fill" |
| | | case 3: |
| | | symbolName = "arrow.up.and.down.and.arrow.left.and.right" |
| | | case 4: |
| | | symbolName = "doc.on.doc.fill" |
| | | case 5: |
| | | symbolName = "minus.circle.fill" |
| | | case 6: |
| | | symbolName = "arrow.up.arrow.down.circle.fill" |
| | | case 7: |
| | | symbolName = "square.grid.2x2.fill" |
| | | case 8: |
| | | symbolName = "note.text" |
| | | default: |
| | | symbolName = "lightbulb.fill" |
| | | } |
| | | let iconConfig = NSImage.SymbolConfiguration(pointSize: 26, weight: .bold) |
| | | iconView.image = NSImage( |
| | | systemSymbolName: symbolName, |
| | | accessibilityDescription: nil |
| | | )?.withSymbolConfiguration(iconConfig) |
| | | ?? NSImage( |
| | | systemSymbolName: "lightbulb.fill", |
| | | accessibilityDescription: nil |
| | | )?.withSymbolConfiguration(iconConfig) |
| | | } |
| | | |
| | | private func updateColors() { |
| | | backgroundView.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.35).cgColor |
| | | iconView.contentTintColor = .systemYellow |
| | | titleLabel.textColor = .secondaryLabelColor |
| | | detailLabel.textColor = .labelColor |
| | | previousButton.tintColor = .labelColor |
| | | nextButton.tintColor = .labelColor |
| | | dotsView.needsDisplay = true |
| | | let usesDarkGlass = coordinator?.appGridTheme.usesDarkGlass == true |
| | | |
| | | backgroundView.material = usesDarkGlass ? .underWindowBackground : .popover |
| | | backgroundView.appearance = usesDarkGlass ? NSAppearance(named: .darkAqua) : nil |
| | | backgroundView.layer?.backgroundColor = ( |
| | | usesDarkGlass |
| | | ? NSColor.black.withAlphaComponent(0.46) |
| | | : NSColor.white.withAlphaComponent(0.54) |
| | | ).cgColor |
| | | backgroundView.layer?.borderColor = ( |
| | | usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.22) |
| | | : NSColor.black.withAlphaComponent(0.18) |
| | | ).cgColor |
| | | |
| | | titlePanelView.layer?.backgroundColor = ( |
| | | usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.08) |
| | | : NSColor.white.withAlphaComponent(0.42) |
| | | ).cgColor |
| | | titlePanelView.layer?.borderColor = ( |
| | | usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.12) |
| | | : NSColor.black.withAlphaComponent(0.08) |
| | | ).cgColor |
| | | |
| | | let theme = coordinator?.appGridTheme ?? .fallback |
| | | let accentColor = usageTipAccentColor(for: theme) |
| | | iconView.contentTintColor = accentColor |
| | | titleLabel.textColor = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.92) |
| | | : accentColor.withAlphaComponent(0.94) |
| | | detailLabel.textColor = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.95) |
| | | : NSColor(calibratedRed: 0.08, green: 0.10, blue: 0.13, alpha: 0.94) |
| | | applyDetailTextStyle() |
| | | |
| | | let buttonTint = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.90) |
| | | : NSColor(calibratedWhite: 0.08, alpha: 0.92) |
| | | let buttonHoverTint = usesDarkGlass |
| | | ? NSColor.white |
| | | : NSColor(calibratedWhite: 0.04, alpha: 1.00) |
| | | let buttonPressedTint = usesDarkGlass |
| | | ? NSColor.white |
| | | : NSColor.black |
| | | let buttonSurface = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.10) |
| | | : NSColor.white.withAlphaComponent(0.38) |
| | | let buttonHoverSurface = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.20) |
| | | : NSColor.white.withAlphaComponent(0.70) |
| | | let buttonPressedSurface = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.28) |
| | | : NSColor.white.withAlphaComponent(0.86) |
| | | let buttonBorder = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.14) |
| | | : NSColor.black.withAlphaComponent(0.10) |
| | | [previousButton, nextButton].forEach { button in |
| | | button.tintColor = buttonTint |
| | | button.hoveredTintColor = buttonHoverTint |
| | | button.pressedTintColor = buttonPressedTint |
| | | button.surfaceColor = buttonSurface |
| | | button.hoveredSurfaceColor = buttonHoverSurface |
| | | button.pressedSurfaceColor = buttonPressedSurface |
| | | button.borderColor = buttonBorder |
| | | } |
| | | |
| | | closeButton.tintColor = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.42) |
| | | : NSColor.black.withAlphaComponent(0.34) |
| | | closeButton.hoveredTintColor = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.86) |
| | | : NSColor.black.withAlphaComponent(0.78) |
| | | closeButton.pressedTintColor = usesDarkGlass ? .white : .black |
| | | closeButton.surfaceColor = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.04) |
| | | : NSColor.white.withAlphaComponent(0.08) |
| | | closeButton.hoveredSurfaceColor = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.14) |
| | | : NSColor.white.withAlphaComponent(0.62) |
| | | closeButton.pressedSurfaceColor = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.22) |
| | | : NSColor.white.withAlphaComponent(0.82) |
| | | closeButton.borderColor = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.08) |
| | | : NSColor.black.withAlphaComponent(0.08) |
| | | |
| | | dotsView.selectedColor = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.88) |
| | | : NSColor.labelColor.withAlphaComponent(0.74) |
| | | dotsView.idleColor = usesDarkGlass |
| | | ? NSColor.white.withAlphaComponent(0.28) |
| | | : NSColor.labelColor.withAlphaComponent(0.24) |
| | | |
| | | layer?.shadowOpacity = usesDarkGlass ? 0.28 : 0.14 |
| | | layer?.shadowRadius = usesDarkGlass ? 18 : 12 |
| | | needsDisplay = true |
| | | } |
| | | |
| | | private func usageTipAccentColor(for theme: AppGridTheme) -> NSColor { |
| | | switch theme { |
| | | case .defaultLight: |
| | | return NSColor(calibratedRed: 0.34, green: 0.40, blue: 0.48, alpha: 1.00) |
| | | case .deepBlue: |
| | | return NSColor(calibratedRed: 0.42, green: 0.80, blue: 1.00, alpha: 1.00) |
| | | case .black: |
| | | return NSColor(calibratedRed: 0.36, green: 0.74, blue: 1.00, alpha: 1.00) |
| | | case .pink: |
| | | return NSColor(calibratedRed: 0.70, green: 0.18, blue: 0.45, alpha: 1.00) |
| | | case .purple: |
| | | return NSColor(calibratedRed: 0.40, green: 0.22, blue: 0.70, alpha: 1.00) |
| | | case .green: |
| | | return NSColor(calibratedRed: 0.08, green: 0.42, blue: 0.22, alpha: 1.00) |
| | | case .blue: |
| | | return NSColor(calibratedRed: 0.06, green: 0.34, blue: 0.74, alpha: 1.00) |
| | | case .colorful: |
| | | return NSColor(calibratedRed: 0.06, green: 0.44, blue: 0.78, alpha: 1.00) |
| | | } |
| | | } |
| | | |
| | | private func applyDetailTextStyle() { |
| | | let paragraphStyle = NSMutableParagraphStyle() |
| | | paragraphStyle.alignment = .left |
| | | paragraphStyle.lineBreakMode = .byWordWrapping |
| | | paragraphStyle.minimumLineHeight = 31 |
| | | paragraphStyle.maximumLineHeight = 34 |
| | | paragraphStyle.lineSpacing = 6 |
| | | |
| | | detailLabel.attributedStringValue = NSAttributedString( |
| | | string: currentDetailText, |
| | | attributes: [ |
| | | .font: detailFont, |
| | | .foregroundColor: detailLabel.textColor ?? NSColor.labelColor, |
| | | .paragraphStyle: paragraphStyle |
| | | ] |
| | | ) |
| | | } |
| | | } |
| | | |
| | |
| | | let textHeight = cellSize(forBounds: rect).height |
| | | drawingRect.origin.y = rect.origin.y + max(0, (rect.height - textHeight) / 2) |
| | | drawingRect.size.height = min(rect.height, textHeight + 2) |
| | | return drawingRect |
| | | } |
| | | } |
| | | |
| | | private final class AppGridCenteredMultilineTextFieldCell: NSTextFieldCell { |
| | | override func drawingRect(forBounds rect: NSRect) -> NSRect { |
| | | var drawingRect = super.drawingRect(forBounds: rect) |
| | | let textHeight = attributedStringValue.boundingRect( |
| | | with: NSSize(width: max(1, rect.width), height: .greatestFiniteMagnitude), |
| | | options: [.usesLineFragmentOrigin, .usesFontLeading] |
| | | ).height |
| | | let centeredHeight = min(rect.height, ceil(textHeight) + 4) |
| | | drawingRect.origin.y = rect.origin.y + max(0, (rect.height - centeredHeight) / 2) |
| | | drawingRect.size.height = centeredHeight |
| | | return drawingRect |
| | | } |
| | | } |
| | |
| | | } |
| | | var tintColor: NSColor = .labelColor { |
| | | didSet { |
| | | imageView.contentTintColor = tintColor |
| | | updateIconTint() |
| | | } |
| | | } |
| | | var hoveredTintColor: NSColor? { |
| | | didSet { updateIconTint() } |
| | | } |
| | | var pressedTintColor: NSColor? { |
| | | didSet { updateIconTint() } |
| | | } |
| | | var surfaceColor: NSColor = .clear { |
| | | didSet { needsDisplay = true } |
| | | } |
| | | var hoveredSurfaceColor: NSColor = NSColor.labelColor.withAlphaComponent(0.08) { |
| | | didSet { needsDisplay = true } |
| | | } |
| | | var pressedSurfaceColor: NSColor = NSColor.labelColor.withAlphaComponent(0.14) { |
| | | didSet { needsDisplay = true } |
| | | } |
| | | var borderColor: NSColor = .clear { |
| | | didSet { needsDisplay = true } |
| | | } |
| | | |
| | | private let imageView = AppGridDecorativeImageView() |
| | |
| | | return true |
| | | } |
| | | |
| | | func performPressFeedback() { |
| | | isPressed = true |
| | | needsDisplay = true |
| | | updateIconTint() |
| | | DispatchQueue.main.asyncAfter(deadline: .now() + 0.10) { [weak self] in |
| | | self?.isPressed = false |
| | | self?.needsDisplay = true |
| | | self?.updateIconTint() |
| | | } |
| | | } |
| | | |
| | | override func layout() { |
| | | super.layout() |
| | | let size: CGFloat = 26 |
| | | let size = min(CGFloat(26), max(14, min(bounds.width, bounds.height) * 0.62)) |
| | | imageView.frame = NSRect( |
| | | x: (bounds.width - size) / 2, |
| | | y: (bounds.height - size) / 2, |
| | |
| | | override func mouseEntered(with event: NSEvent) { |
| | | isHovered = true |
| | | needsDisplay = true |
| | | updateIconTint() |
| | | } |
| | | |
| | | override func mouseExited(with event: NSEvent) { |
| | | isHovered = false |
| | | isPressed = false |
| | | needsDisplay = true |
| | | updateIconTint() |
| | | } |
| | | |
| | | override func mouseDown(with event: NSEvent) { |
| | | Diagnostics.log("usageTips.button.mouseDown", [ |
| | | "label": buttonAccessibilityLabel |
| | | ]) |
| | | isPressed = true |
| | | needsDisplay = true |
| | | performPressFeedback() |
| | | action?() |
| | | DispatchQueue.main.asyncAfter(deadline: .now() + 0.10) { [weak self] in |
| | | self?.isPressed = false |
| | | self?.needsDisplay = true |
| | | } |
| | | } |
| | | |
| | | override func draw(_ dirtyRect: NSRect) { |
| | | guard isHovered || isPressed else { return } |
| | | tintColor.withAlphaComponent(isPressed ? 0.14 : 0.08).setFill() |
| | | NSBezierPath(roundedRect: bounds.insetBy(dx: 1, dy: 1), xRadius: 6, yRadius: 6).fill() |
| | | let path = NSBezierPath(roundedRect: bounds.insetBy(dx: 1, dy: 1), xRadius: 8, yRadius: 8) |
| | | let fill = isPressed ? pressedSurfaceColor : isHovered ? hoveredSurfaceColor : surfaceColor |
| | | fill.setFill() |
| | | path.fill() |
| | | borderColor.setStroke() |
| | | path.lineWidth = 1 |
| | | path.stroke() |
| | | } |
| | | |
| | | private func setup(systemImage: String) { |
| | |
| | | addSubview(imageView) |
| | | setAccessibilityRole(.button) |
| | | } |
| | | |
| | | private func updateIconTint() { |
| | | if isPressed, let pressedTintColor { |
| | | imageView.contentTintColor = pressedTintColor |
| | | } else if isHovered, let hoveredTintColor { |
| | | imageView.contentTintColor = hoveredTintColor |
| | | } else { |
| | | imageView.contentTintColor = tintColor |
| | | } |
| | | } |
| | | } |
| | | |
| | | private final class AppGridUsageTipDotsView: NSView { |
| | | private var count = 0 |
| | | private var selectedIndex = 0 |
| | | var selectedColor: NSColor = .labelColor { |
| | | didSet { needsDisplay = true } |
| | | } |
| | | var idleColor: NSColor = NSColor.labelColor.withAlphaComponent(0.28) { |
| | | didSet { needsDisplay = true } |
| | | } |
| | | |
| | | override var isFlipped: Bool { true } |
| | | |
| | |
| | | for index in 0..<count { |
| | | let selected = index == selectedIndex |
| | | let dotSize: CGFloat = selected ? 6 : 5 |
| | | let color = NSColor.labelColor.withAlphaComponent(selected ? 0.85 : 0.28) |
| | | let color = selected ? selectedColor : idleColor |
| | | color.setFill() |
| | | NSBezierPath( |
| | | ovalIn: NSRect( |
| | |
| | | }, |
| | | onScrollActivity: handleAppGridScrollActivity, |
| | | onDragModeChange: { setAppDragMode($0) }, |
| | | onHideUsageTips: { hideUsageTips = true }, |
| | | onUsageTipsHoverChange: handleUsageTipsHoverChange |
| | | ) |
| | | } |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>APPL</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>8.0.1</string> |
| | | <string>8.0.2</string> |
| | | <key>CFBundleVersion</key> |
| | | <string>20260624.2021</string> |
| | | <string>20260624.2341</string> |
| | | <key>LSApplicationCategoryType</key> |
| | | <string>public.app-category.utilities</string> |
| | | <key>LSMinimumSystemVersion</key> |
| | |
| | | "settings.hideUsageTipsDesc": "إذا شغّلته، تختفي التلميحات العايمة أسفل شبكة التطبيقات.", |
| | | "usageTips.previous": "التلميحة السابقة", |
| | | "usageTips.next": "التلميحة التالية", |
| | | "usageTips.tip1.title": "تلميحة 1 - إضافة وسوم", |
| | | "usageTips.tip1.detail": "افتح الإعدادات, عدّل الوسوم.", |
| | | "usageTips.close": "إغلاق التلميحات", |
| | | "usageTips.tip1.title": "تلميحة 1 - تحرير الوسوم", |
| | | "usageTips.tip1.detail": "افتح الإعدادات وانتقل إلى الوسوم للتحرير, انقر نقرًا مزدوجًا على وسم في قائمة الوسوم لفتح تحرير الوسوم بسرعة.", |
| | | "usageTips.tip2.title": "تلميحة 2 - وسم التطبيقات", |
| | | "usageTips.tip2.detail": "اضغط مطوّل على التطبيق, اسحبه على وسم.", |
| | | "usageTips.tip3.title": "تلميحة 3 - نقل التطبيقات", |
| | |
| | | "settings.hideUsageTipsDesc": "عند التفعيل، يتم إخفاء النصائح العائمة أسفل شبكة التطبيقات.", |
| | | "usageTips.previous": "النصيحة السابقة", |
| | | "usageTips.next": "النصيحة التالية", |
| | | "usageTips.tip1.title": "النصيحة 1 - إضافة وسوم", |
| | | "usageTips.tip1.detail": "افتح الإعدادات, حرر الوسوم.", |
| | | "usageTips.close": "إغلاق التلميحات", |
| | | "usageTips.tip1.title": "النصيحة 1 - تحرير الوسوم", |
| | | "usageTips.tip1.detail": "افتح الإعدادات وانتقل إلى الوسوم للتحرير, انقر نقرًا مزدوجًا على وسم في قائمة الوسوم لفتح تحرير الوسوم بسرعة.", |
| | | "usageTips.tip2.title": "النصيحة 2 - وسم التطبيقات", |
| | | "usageTips.tip2.detail": "اضغط مطولاً على تطبيق, اسحبه إلى وسم.", |
| | | "usageTips.tip3.title": "النصيحة 3 - نقل التطبيقات", |
| | |
| | | "settings.hideUsageTipsDesc": "Když je zapnuto, plovoucí tipy dole v mřížce aplikací se skryjí.", |
| | | "usageTips.previous": "Předchozí tip", |
| | | "usageTips.next": "Další tip", |
| | | "usageTips.tip1.title": "Tip 1 - Přidat tagy", |
| | | "usageTips.tip1.detail": "Otevřete Nastavení, upravte tagy.", |
| | | "usageTips.close": "Zavřít tipy", |
| | | "usageTips.tip1.title": "Tip 1 - Upravit tagy", |
| | | "usageTips.tip1.detail": "Otevřete Nastavení a přepněte na Tagy pro úpravy, Dvojklikem na tag v seznamu rychle otevřete úpravy tagů.", |
| | | "usageTips.tip2.title": "Tip 2 - Označit aplikace", |
| | | "usageTips.tip2.detail": "Podržte aplikaci, přetáhněte ji na tag.", |
| | | "usageTips.tip3.title": "Tip 3 - Přesunout aplikace", |
| | |
| | | "settings.hideUsageTipsDesc": "Når det er slået til, skjules de flydende tips nederst i appgitteret.", |
| | | "usageTips.previous": "Forrige tip", |
| | | "usageTips.next": "Næste tip", |
| | | "usageTips.tip1.title": "Tip 1 - Tilføj tags", |
| | | "usageTips.tip1.detail": "Åbn Indstillinger, rediger tags.", |
| | | "usageTips.close": "Luk tips", |
| | | "usageTips.tip1.title": "Tip 1 - Rediger tags", |
| | | "usageTips.tip1.detail": "Åbn Indstillinger og skift til Tags for at redigere, Dobbeltklik på et tag i taglisten for hurtigt at åbne tagredigering.", |
| | | "usageTips.tip2.title": "Tip 2 - Tag apps", |
| | | "usageTips.tip2.detail": "Hold en app nede, slip den på et tag.", |
| | | "usageTips.tip3.title": "Tip 3 - Flyt apps", |
| | |
| | | "settings.hideUsageTipsDesc": "Wenn aktiviert, werden die schwebenden Tipps unten im App-Raster ausgeblendet.", |
| | | "usageTips.previous": "Vorheriger Tipp", |
| | | "usageTips.next": "Nächster Tipp", |
| | | "usageTips.tip1.title": "Tipp 1 - Tags hinzufügen", |
| | | "usageTips.tip1.detail": "Einstellungen öffnen, Tags bearbeiten.", |
| | | "usageTips.close": "Tipps schließen", |
| | | "usageTips.tip1.title": "Tipp 1 - Tags bearbeiten", |
| | | "usageTips.tip1.detail": "Öffnen Sie Einstellungen und wechseln Sie zu Tags zum Bearbeiten, Doppelklicken Sie in der Tagliste auf einen Tag zum schnellen Bearbeiten.", |
| | | "usageTips.tip2.title": "Tipp 2 - Apps taggen", |
| | | "usageTips.tip2.detail": "App gedrückt halten, auf einen Tag ziehen.", |
| | | "usageTips.tip3.title": "Tipp 3 - Apps verschieben", |
| | |
| | | "settings.hideUsageTipsDesc": "When enabled, the floating tips at the bottom of the app grid are hidden.", |
| | | "usageTips.previous": "Previous tip", |
| | | "usageTips.next": "Next tip", |
| | | "usageTips.tip1.title": "Tip 1 - Add tags", |
| | | "usageTips.tip1.detail": "Open Settings, edit tags.", |
| | | "usageTips.close": "Close tips", |
| | | "usageTips.tip1.title": "Tip 1 - Edit tags", |
| | | "usageTips.tip1.detail": "Open Settings and switch to Tags to edit, Double-click a tag in the tag list to open tag editing quickly.", |
| | | "usageTips.tip2.title": "Tip 2 - Tag apps", |
| | | "usageTips.tip2.detail": "Long-press an app, then drag it onto a tag.", |
| | | "usageTips.tip3.title": "Tip 3 - Move apps", |
| | |
| | | "settings.hideUsageTipsDesc": "Si está activado, se ocultan los consejos flotantes de la parte inferior de la cuadrícula de apps.", |
| | | "usageTips.previous": "Consejo anterior", |
| | | "usageTips.next": "Consejo siguiente", |
| | | "usageTips.tip1.title": "Consejo 1 - Añadir etiquetas", |
| | | "usageTips.tip1.detail": "Abra Ajustes, edite etiquetas.", |
| | | "usageTips.close": "Cerrar consejos", |
| | | "usageTips.tip1.title": "Consejo 1 - Editar etiquetas", |
| | | "usageTips.tip1.detail": "Abra Ajustes y cambie a Etiquetas para editar, Haga doble clic en una etiqueta de la lista para abrir rápido la edición.", |
| | | "usageTips.tip2.title": "Consejo 2 - Etiquetar apps", |
| | | "usageTips.tip2.detail": "Mantén pulsada una app, arrástrala a una etiqueta.", |
| | | "usageTips.tip3.title": "Consejo 3 - Mover apps", |
| | |
| | | "settings.hideUsageTipsDesc": "Si cette option est activée, les astuces flottantes en bas de la grille des apps sont masquées.", |
| | | "usageTips.previous": "Astuce précédente", |
| | | "usageTips.next": "Astuce suivante", |
| | | "usageTips.tip1.title": "Astuce 1 - Ajouter des tags", |
| | | "usageTips.tip1.detail": "Ouvrez Réglages, modifiez les tags.", |
| | | "usageTips.close": "Fermer les astuces", |
| | | "usageTips.tip1.title": "Astuce 1 - Modifier les tags", |
| | | "usageTips.tip1.detail": "Ouvrez Réglages et passez à Tags pour modifier, Double-cliquez sur un tag dans la liste pour ouvrir vite l’édition.", |
| | | "usageTips.tip2.title": "Astuce 2 - Taguer des apps", |
| | | "usageTips.tip2.detail": "Appuyez longuement sur une app, déposez-la sur un tag.", |
| | | "usageTips.tip3.title": "Astuce 3 - Déplacer des apps", |
| | |
| | | "settings.hideUsageTipsDesc": "Jika aktif, tips mengambang di bagian bawah grid app akan disembunyikan.", |
| | | "usageTips.previous": "Tips sebelumnya", |
| | | "usageTips.next": "Tips berikutnya", |
| | | "usageTips.tip1.title": "Tips 1 - Tambah tag", |
| | | "usageTips.tip1.detail": "Buka Pengaturan, edit tag.", |
| | | "usageTips.close": "Tutup tips", |
| | | "usageTips.tip1.title": "Tips 1 - Edit tag", |
| | | "usageTips.tip1.detail": "Buka Pengaturan lalu pindah ke Tag untuk mengedit, Klik dua kali tag di daftar tag untuk cepat membuka editor tag.", |
| | | "usageTips.tip2.title": "Tips 2 - Beri tag app", |
| | | "usageTips.tip2.detail": "Tekan lama app, jatuhkan ke tag.", |
| | | "usageTips.tip3.title": "Tips 3 - Pindahkan app", |
| | |
| | | "settings.hideUsageTipsDesc": "Se attivo, i suggerimenti flottanti in basso nella griglia delle app vengono nascosti.", |
| | | "usageTips.previous": "Suggerimento precedente", |
| | | "usageTips.next": "Suggerimento successivo", |
| | | "usageTips.tip1.title": "Suggerimento 1 - Aggiungere tag", |
| | | "usageTips.tip1.detail": "Apri Impostazioni, modifica i tag.", |
| | | "usageTips.close": "Chiudi suggerimenti", |
| | | "usageTips.tip1.title": "Suggerimento 1 - Modificare i tag", |
| | | "usageTips.tip1.detail": "Apri Impostazioni e passa a Tag per modificare, Fai doppio clic su un tag nell’elenco per aprire rapidamente la modifica.", |
| | | "usageTips.tip2.title": "Suggerimento 2 - Taggare app", |
| | | "usageTips.tip2.detail": "Tieni premuta un’app, rilasciala su un tag.", |
| | | "usageTips.tip3.title": "Suggerimento 3 - Spostare app", |
| | |
| | | "settings.hideUsageTipsDesc": "オンにすると、アプリグリッド下部のフローティングヒントを表示しません。", |
| | | "usageTips.previous": "前のヒント", |
| | | "usageTips.next": "次のヒント", |
| | | "usageTips.tip1.title": "ヒント1 - タグを追加", |
| | | "usageTips.tip1.detail": "設定を開く, タグを編集します。", |
| | | "usageTips.close": "ヒントを閉じる", |
| | | "usageTips.tip1.title": "ヒント1 - タグを編集", |
| | | "usageTips.tip1.detail": "設定を開いて「タグ」に切り替えて編集, タグ一覧でタグをダブルクリックすると編集画面をすぐ開けます。", |
| | | "usageTips.tip2.title": "ヒント2 - アプリにタグ付け", |
| | | "usageTips.tip2.detail": "アプリを長押し, タグ上にドロップします。", |
| | | "usageTips.tip3.title": "ヒント3 - アプリを移動", |
| | |
| | | "settings.hideUsageTipsDesc": "켜면 앱 그리드 하단의 플로팅 사용 팁이 표시되지 않습니다.", |
| | | "usageTips.previous": "이전 팁", |
| | | "usageTips.next": "다음 팁", |
| | | "usageTips.tip1.title": "팁 1 - 태그 추가", |
| | | "usageTips.tip1.detail": "설정 열기, 태그 편집.", |
| | | "usageTips.close": "팁 닫기", |
| | | "usageTips.tip1.title": "팁 1 - 태그 편집", |
| | | "usageTips.tip1.detail": "설정을 열고 태그로 전환해 편집, 태그 목록에서 태그를 두 번 클릭하면 태그 편집으로 빠르게 이동합니다.", |
| | | "usageTips.tip2.title": "팁 2 - 앱에 태그 지정", |
| | | "usageTips.tip2.detail": "앱을 길게 누르기, 태그 위에 놓기.", |
| | | "usageTips.tip3.title": "팁 3 - 앱 이동", |
| | |
| | | "settings.hideUsageTipsDesc": "Apabila dihidupkan, petua terapung di bahagian bawah grid app akan disembunyikan.", |
| | | "usageTips.previous": "Petua sebelumnya", |
| | | "usageTips.next": "Petua seterusnya", |
| | | "usageTips.tip1.title": "Petua 1 - Tambah tag", |
| | | "usageTips.tip1.detail": "Buka Tetapan, edit tag.", |
| | | "usageTips.close": "Tutup petua", |
| | | "usageTips.tip1.title": "Petua 1 - Edit tag", |
| | | "usageTips.tip1.detail": "Buka Tetapan dan tukar ke Tag untuk mengedit, Dwiklik tag dalam senarai tag untuk cepat membuka editor tag.", |
| | | "usageTips.tip2.title": "Petua 2 - Tag app", |
| | | "usageTips.tip2.detail": "Tekan lama app, lepaskan pada tag.", |
| | | "usageTips.tip3.title": "Petua 3 - Alih app", |
| | |
| | | "settings.hideUsageTipsDesc": "Når dette er slått på, skjules de flytende tipsene nederst i app-rutenettet.", |
| | | "usageTips.previous": "Forrige tips", |
| | | "usageTips.next": "Neste tips", |
| | | "usageTips.tip1.title": "Tips 1 - Legg til tagger", |
| | | "usageTips.tip1.detail": "Åpne Innstillinger, rediger tagger.", |
| | | "usageTips.close": "Lukk tips", |
| | | "usageTips.tip1.title": "Tips 1 - Rediger tagger", |
| | | "usageTips.tip1.detail": "Åpne Innstillinger og bytt til Tagger for å redigere, Dobbeltklikk en tagg i tagglisten for raskt å åpne redigering.", |
| | | "usageTips.tip2.title": "Tips 2 - Tagg apper", |
| | | "usageTips.tip2.detail": "Hold en app inne, slipp den på en tagg.", |
| | | "usageTips.tip3.title": "Tips 3 - Flytt apper", |
| | |
| | | "settings.hideUsageTipsDesc": "Als dit is ingeschakeld, worden de zwevende tips onderaan het app-raster verborgen.", |
| | | "usageTips.previous": "Vorige tip", |
| | | "usageTips.next": "Volgende tip", |
| | | "usageTips.tip1.title": "Tip 1 - Tags toevoegen", |
| | | "usageTips.tip1.detail": "Open Instellingen, bewerk tags.", |
| | | "usageTips.close": "Tips sluiten", |
| | | "usageTips.tip1.title": "Tip 1 - Tags bewerken", |
| | | "usageTips.tip1.detail": "Open Instellingen en ga naar Tags om te bewerken, Dubbelklik op een tag in de lijst om tagbewerking snel te openen.", |
| | | "usageTips.tip2.title": "Tip 2 - Apps taggen", |
| | | "usageTips.tip2.detail": "Houd een app ingedrukt, sleep naar een tag.", |
| | | "usageTips.tip3.title": "Tip 3 - Apps verplaatsen", |
| | |
| | | "settings.hideUsageTipsDesc": "Når dette er slått på, vert dei flytande tipsa nedst i apprutenettet gøymde.", |
| | | "usageTips.previous": "Forrige tips", |
| | | "usageTips.next": "Neste tips", |
| | | "usageTips.tip1.title": "Tips 1 - Legg til tagger", |
| | | "usageTips.tip1.detail": "Opna Innstillingar, rediger taggar.", |
| | | "usageTips.close": "Lukk tips", |
| | | "usageTips.tip1.title": "Tips 1 - Rediger taggar", |
| | | "usageTips.tip1.detail": "Opna Innstillingar og byt til Taggar for å redigere, Dobbeltklikk ein tagg i tagglista for raskt å opne redigering.", |
| | | "usageTips.tip2.title": "Tips 2 - Tagg apper", |
| | | "usageTips.tip2.detail": "Hold ein app inne, slepp han på ein tagg.", |
| | | "usageTips.tip3.title": "Tips 3 - Flytt apper", |
| | |
| | | "settings.hideUsageTipsDesc": "Når dette er slått på, skjules de flytende tipsene nederst i app-rutenettet.", |
| | | "usageTips.previous": "Forrige tips", |
| | | "usageTips.next": "Neste tips", |
| | | "usageTips.tip1.title": "Tips 1 - Legg til tagger", |
| | | "usageTips.tip1.detail": "Åpne Innstillinger, rediger tagger.", |
| | | "usageTips.close": "Lukk tips", |
| | | "usageTips.tip1.title": "Tips 1 - Rediger tagger", |
| | | "usageTips.tip1.detail": "Åpne Innstillinger og bytt til Tagger for å redigere, Dobbeltklikk en tagg i tagglisten for raskt å åpne redigering.", |
| | | "usageTips.tip2.title": "Tips 2 - Tagg apper", |
| | | "usageTips.tip2.detail": "Hold en app inne, slipp den på en tagg.", |
| | | "usageTips.tip3.title": "Tips 3 - Flytt apper", |
| | |
| | | "settings.hideUsageTipsDesc": "Po włączeniu pływające wskazówki u dołu siatki aplikacji będą ukryte.", |
| | | "usageTips.previous": "Poprzednia wskazówka", |
| | | "usageTips.next": "Następna wskazówka", |
| | | "usageTips.tip1.title": "Wskazówka 1 - Dodaj tagi", |
| | | "usageTips.tip1.detail": "Otwórz Ustawienia, edytuj tagi.", |
| | | "usageTips.close": "Zamknij wskazówki", |
| | | "usageTips.tip1.title": "Wskazówka 1 - Edytuj tagi", |
| | | "usageTips.tip1.detail": "Otwórz Ustawienia i przejdź do Tagów aby edytować, Kliknij dwukrotnie tag na liście aby szybko otworzyć edycję.", |
| | | "usageTips.tip2.title": "Wskazówka 2 - Otaguj aplikacje", |
| | | "usageTips.tip2.detail": "Przytrzymaj aplikację, przeciągnij ją na tag.", |
| | | "usageTips.tip3.title": "Wskazówka 3 - Przenieś aplikacje", |
| | |
| | | "settings.hideUsageTipsDesc": "Quando ativado, as dicas flutuantes na parte inferior da grade de apps ficam ocultas.", |
| | | "usageTips.previous": "Dica anterior", |
| | | "usageTips.next": "Próxima dica", |
| | | "usageTips.tip1.title": "Dica 1 - Adicionar tags", |
| | | "usageTips.tip1.detail": "Abra Ajustes, edite tags.", |
| | | "usageTips.close": "Fechar dicas", |
| | | "usageTips.tip1.title": "Dica 1 - Editar tags", |
| | | "usageTips.tip1.detail": "Abra Ajustes e mude para Tags para editar, Clique duas vezes em uma tag na lista para abrir a edição rapidamente.", |
| | | "usageTips.tip2.title": "Dica 2 - Marcar apps", |
| | | "usageTips.tip2.detail": "Pressione e segure um app, solte sobre uma tag.", |
| | | "usageTips.tip3.title": "Dica 3 - Mover apps", |
| | |
| | | "settings.hideUsageTipsDesc": "Când este activat, sfaturile plutitoare din partea de jos a grilei de aplicații sunt ascunse.", |
| | | "usageTips.previous": "Sfatul anterior", |
| | | "usageTips.next": "Sfatul următor", |
| | | "usageTips.tip1.title": "Sfat 1 - Adaugă etichete", |
| | | "usageTips.tip1.detail": "Deschide Setări, editează etichetele.", |
| | | "usageTips.close": "Închide sfaturile", |
| | | "usageTips.tip1.title": "Sfat 1 - Editează etichete", |
| | | "usageTips.tip1.detail": "Deschide Setări și comută la Etichete pentru editare, Dublu clic pe o etichetă din listă pentru a deschide rapid editarea.", |
| | | "usageTips.tip2.title": "Sfat 2 - Etichetează aplicații", |
| | | "usageTips.tip2.detail": "Ține apăsată o aplicație, plaseaz-o pe o etichetă.", |
| | | "usageTips.tip3.title": "Sfat 3 - Mută aplicații", |
| | |
| | | "settings.hideUsageTipsDesc": "Если включено, плавающие советы внизу сетки приложений будут скрыты.", |
| | | "usageTips.previous": "Предыдущий совет", |
| | | "usageTips.next": "Следующий совет", |
| | | "usageTips.tip1.title": "Совет 1 — Добавить теги", |
| | | "usageTips.tip1.detail": "Откройте Настройки, редактируйте теги.", |
| | | "usageTips.close": "Закрыть советы", |
| | | "usageTips.tip1.title": "Совет 1 — Редактировать теги", |
| | | "usageTips.tip1.detail": "Откройте Настройки и перейдите к Тегам для редактирования, Дважды щёлкните тег в списке чтобы быстро открыть редактирование.", |
| | | "usageTips.tip2.title": "Совет 2 — Назначить теги", |
| | | "usageTips.tip2.detail": "Удерживайте приложение, перетащите на тег.", |
| | | "usageTips.tip3.title": "Совет 3 — Переместить приложения", |
| | |
| | | "settings.hideUsageTipsDesc": "Када је укључено, плутајући савети на дну мреже апликација су сакривени.", |
| | | "usageTips.previous": "Претходни савет", |
| | | "usageTips.next": "Следећи савет", |
| | | "usageTips.tip1.title": "Савет 1 - Додај ознаке", |
| | | "usageTips.tip1.detail": "Отворите Подешавања, уредите ознаке.", |
| | | "usageTips.close": "Затвори савете", |
| | | "usageTips.tip1.title": "Савет 1 - Уреди ознаке", |
| | | "usageTips.tip1.detail": "Отворите Подешавања и пређите на Ознаке за уређивање, Двапут кликните ознаку у листи за брзо отварање уређивања.", |
| | | "usageTips.tip2.title": "Савет 2 - Означи апликације", |
| | | "usageTips.tip2.detail": "Држите апликацију, превуците је на ознаку.", |
| | | "usageTips.tip3.title": "Савет 3 - Премести апликације", |
| | |
| | | "settings.hideUsageTipsDesc": "När detta är aktiverat döljs de flytande tipsen längst ned i apprutnätet.", |
| | | "usageTips.previous": "Föregående tips", |
| | | "usageTips.next": "Nästa tips", |
| | | "usageTips.tip1.title": "Tips 1 - Lägg till taggar", |
| | | "usageTips.tip1.detail": "Öppna Inställningar, redigera taggar.", |
| | | "usageTips.close": "Stäng tips", |
| | | "usageTips.tip1.title": "Tips 1 - Redigera taggar", |
| | | "usageTips.tip1.detail": "Öppna Inställningar och växla till Taggar för att redigera, Dubbelklicka på en tagg i tagglistan för att snabbt öppna redigering.", |
| | | "usageTips.tip2.title": "Tips 2 - Tagga appar", |
| | | "usageTips.tip2.detail": "Håll ned en app, släpp den på en tagg.", |
| | | "usageTips.tip3.title": "Tips 3 - Flytta appar", |
| | |
| | | "settings.hideUsageTipsDesc": "เมื่อเปิดใช้ เคล็ดลับแบบลอยที่ด้านล่างของกริดแอปจะถูกซ่อน", |
| | | "usageTips.previous": "เคล็ดลับก่อนหน้า", |
| | | "usageTips.next": "เคล็ดลับถัดไป", |
| | | "usageTips.tip1.title": "เคล็ดลับ 1 - เพิ่มแท็ก", |
| | | "usageTips.tip1.detail": "เปิดการตั้งค่า, แก้ไขแท็ก", |
| | | "usageTips.close": "ปิดเคล็ดลับ", |
| | | "usageTips.tip1.title": "เคล็ดลับ 1 - แก้ไขแท็ก", |
| | | "usageTips.tip1.detail": "เปิดการตั้งค่าแล้วไปที่แท็กเพื่อแก้ไข, ดับเบิลคลิกแท็กในรายการแท็กเพื่อเปิดการแก้ไขแท็กอย่างรวดเร็ว", |
| | | "usageTips.tip2.title": "เคล็ดลับ 2 - ติดแท็กแอป", |
| | | "usageTips.tip2.detail": "กดแอปค้างไว้, วางบนแท็ก", |
| | | "usageTips.tip3.title": "เคล็ดลับ 3 - ย้ายแอป", |
| | |
| | | "settings.hideUsageTipsDesc": "Etkinleştirildiğinde uygulama ızgarasının altındaki yüzen ipuçları gizlenir.", |
| | | "usageTips.previous": "Önceki ipucu", |
| | | "usageTips.next": "Sonraki ipucu", |
| | | "usageTips.tip1.title": "İpucu 1 - Etiket ekle", |
| | | "usageTips.tip1.detail": "Ayarları açın, etiketleri düzenleyin.", |
| | | "usageTips.close": "İpuçlarını kapat", |
| | | "usageTips.tip1.title": "İpucu 1 - Etiketleri düzenle", |
| | | "usageTips.tip1.detail": "Ayarları açın ve düzenlemek için Etiketler’e geçin, Etiket listesindeki bir etikete çift tıklayarak düzenlemeyi hızlı açın.", |
| | | "usageTips.tip2.title": "İpucu 2 - Uygulamaları etiketle", |
| | | "usageTips.tip2.detail": "Bir uygulamaya uzun basın, etikete bırakın.", |
| | | "usageTips.tip3.title": "İpucu 3 - Uygulamaları taşı", |
| | |
| | | "settings.hideUsageTipsDesc": "Якщо ввімкнено, плаваючі поради внизу сітки програм буде приховано.", |
| | | "usageTips.previous": "Попередня порада", |
| | | "usageTips.next": "Наступна порада", |
| | | "usageTips.tip1.title": "Порада 1 — Додати теги", |
| | | "usageTips.tip1.detail": "Відкрийте Параметри, редагуйте теги.", |
| | | "usageTips.close": "Закрити поради", |
| | | "usageTips.tip1.title": "Порада 1 — Редагувати теги", |
| | | "usageTips.tip1.detail": "Відкрийте Параметри й перейдіть до Тегів для редагування, Двічі клацніть тег у списку щоб швидко відкрити редагування.", |
| | | "usageTips.tip2.title": "Порада 2 — Позначити програми", |
| | | "usageTips.tip2.detail": "Утримуйте програму, перетягніть її на тег.", |
| | | "usageTips.tip3.title": "Порада 3 — Перемістити програми", |
| | |
| | | "settings.hideUsageTipsDesc": "Khi bật, các mẹo nổi ở cuối lưới ứng dụng sẽ bị ẩn.", |
| | | "usageTips.previous": "Mẹo trước", |
| | | "usageTips.next": "Mẹo tiếp theo", |
| | | "usageTips.tip1.title": "Mẹo 1 - Thêm thẻ", |
| | | "usageTips.tip1.detail": "Mở Cài đặt, chỉnh sửa thẻ.", |
| | | "usageTips.close": "Đóng mẹo", |
| | | "usageTips.tip1.title": "Mẹo 1 - Sửa thẻ", |
| | | "usageTips.tip1.detail": "Mở Cài đặt và chuyển sang Thẻ để chỉnh sửa, Nhấp đúp một thẻ trong danh sách để mở nhanh phần chỉnh sửa.", |
| | | "usageTips.tip2.title": "Mẹo 2 - Gắn thẻ ứng dụng", |
| | | "usageTips.tip2.detail": "Nhấn giữ ứng dụng, thả lên thẻ.", |
| | | "usageTips.tip3.title": "Mẹo 3 - Di chuyển ứng dụng", |
| | |
| | | "settings.hideUsageTipsDesc": "开启后,应用网格底部的悬浮使用技巧将不再显示。", |
| | | "usageTips.previous": "上一条技巧", |
| | | "usageTips.next": "下一条技巧", |
| | | "usageTips.tip1.title": "技巧1-添加标签", |
| | | "usageTips.tip1.detail": "在设置 > 切换到“标签”即可编辑", |
| | | "usageTips.close": "关闭使用技巧", |
| | | "usageTips.tip1.title": "技巧1-编辑标签", |
| | | "usageTips.tip1.detail": "打开设置,切换到“标签”即可编辑\n在标签列表,双击标签即可快速进入标签编辑页面", |
| | | "usageTips.tip2.title": "技巧2-打标签", |
| | | "usageTips.tip2.detail": "长按应用图标启动拖动 > 拖动到标签上是打标签", |
| | | "usageTips.tip3.title": "技巧3-移动应用", |
| | |
| | | "settings.hideUsageTipsDesc": "開啟後,應用程式網格底部的浮動使用技巧將不再顯示。", |
| | | "usageTips.previous": "上一則技巧", |
| | | "usageTips.next": "下一則技巧", |
| | | "usageTips.tip1.title": "技巧1-新增標籤", |
| | | "usageTips.tip1.detail": "在設定 > 切換到「標籤」即可編輯", |
| | | "usageTips.close": "關閉使用技巧", |
| | | "usageTips.tip1.title": "技巧1-編輯標籤", |
| | | "usageTips.tip1.detail": "打開設定,切換到「標籤」即可編輯\n在標籤列表,雙擊標籤即可快速進入標籤編輯頁面", |
| | | "usageTips.tip2.title": "技巧2-套用標籤", |
| | | "usageTips.tip2.detail": "長按應用程式圖示開始拖曳 > 拖到標籤上即可套用標籤", |
| | | "usageTips.tip3.title": "技巧3-移動應用", |
| | |
| | | # TagLauncher Changelog |
| | | |
| | | ## [8.0.2] — 2026-06-24 |
| | | |
| | | - 重设计 App Grid 底部使用技巧:改为原生 AppKit 分区式教学横幅,底部横向占满可用宽度,左侧为技巧编号与标题,中央为两行动作说明,右侧为上一条/下一条控制与页点 |
| | | - 使用技巧横幅接入主题玻璃 token:默认、粉色、紫色、绿色、蓝色、炫彩等亮色主题使用浅玻璃配色;深蓝和黑色主题使用深色玻璃材质与白色文字,保证可读性 |
| | | - 优化使用技巧横幅标题区:标题字号加大并加粗,标题和图标按当前主题使用高对比 accent;每条技巧使用不同语义图标,正文多段说明显示为编号步骤 |
| | | - 更新第 1 条使用技巧为“编辑标签”:说明可在设置页切换到“标签”编辑,也可在标签列表双击标签快速进入标签编辑页;同步补齐 29 个语种文案 |
| | | - 新增低视觉权重关闭按钮:默认半透明,hover 后更清晰;点击后持久关闭使用技巧,与设置页“隐藏使用技巧”一致 |
| | | - 保持使用技巧区域的全宽事件拦截:点击横幅和其底部事件区域不会穿透到底层 App Grid,也不会误触发应用气泡或打开应用;只有命中箭头区域才切换技巧 |
| | | - 补齐 29 个语种的 `usageTips.close` 文案,并更新 `Scripts/usage_tips_qa.sh`,覆盖分区布局、亮/暗主题配色、关闭动作、事件拦截、macOS 14 兼容和 29 语种文案完整性 |
| | | - 版本号更新为 `8.0.2`,Build 更新为 `20260624.2341` |
| | | |
| | | ## [8.0.1] — 2026-06-24 |
| | | |
| | | - 新增 App Grid 主题系统:设置页新增“主题”页签,可在默认、深蓝、黑色、粉色、紫色、绿色、蓝色、炫彩 8 个主题之间切换 |
| | |
| | | - [x] 移除旧“启用深色视图”设置入口和本地化文案。 |
| | | - [x] 新增主题设置 QA 脚本。 |
| | | - [x] 更新 changelog、TODO 和工作日志。 |
| | | - [x] 使用技巧横幅重设计:按分区式教学横幅落地,亮色主题用浅玻璃,深蓝/黑色主题用深玻璃和白字。 |
| | | - [x] 使用技巧新增弱视觉关闭按钮,hover 后清晰,关闭后持久写入 `hideUsageTips`。 |
| | | - [x] 使用技巧 QA 增加分区布局、主题配色、关闭动作、全宽事件拦截和 29 语种 `usageTips.close` 检查。 |
| | | - [x] 使用技巧标题区视觉优化:标题放大加粗、按主题使用高对比 accent、按技巧类型切换语义 icon。 |
| | | - [x] 使用技巧正文多段说明改为编号步骤;第 1 条改为“编辑标签”,并补充双击标签快速进入标签编辑页。 |
| | | - [x] 同步更新第 1 条使用技巧 29 个语种文案,并把标题字号、语义 icon、编号步骤和双击标签说明纳入 `usage_tips_qa.sh`。 |
| | | - [ ] 用户视觉体验验收。 |
| | | - [ ] 通过验收后再决定是否冻结、打包、创建正式 release tag。 |
| | | |
| | |
| | | - `src/build/TagLauncher-8.0.0-build20260624.1815.dmg` |
| | | - SHA256: |
| | | - `373355c87055680cb13e873bc5909abd84817d9ac2eaa98cc1ec2748be64bc80` |
| | | |
| | | ### 2026-06-24 23:41 使用技巧横幅重设计 |
| | | |
| | | - 用户确认最终方案: |
| | | - 布局选“2 分区式教学横幅”。 |
| | | - 亮色主题使用浅玻璃配色。 |
| | | - 深色/黑色主题使用同样布局,但切换到深色玻璃材质和白色文字。 |
| | | - 角色分工结论: |
| | | - 架构:继续使用原生 AppKit `AppGridUsageTipsNSView`,不引入 SwiftUI;tips 跟随 `renderedAppGridTheme`,编辑模式仍继承默认浅色主题 override。 |
| | | - 代码审核:保留全宽透明 shield 和 local mouse monitor,避免点击穿透到底层 AppGrid;箭头只在命中区域触发翻页。 |
| | | - QA:更新 `usage_tips_qa.sh`,覆盖分区式布局、主题配色、关闭按钮、全宽事件拦截、29 语种文案和 macOS 14 兼容。 |
| | | - 本轮实现: |
| | | - 使用技巧横幅底部横向占满可用宽度。 |
| | | - 左侧独立标题面板显示灯泡图标、技巧编号和标题。 |
| | | - 中央正文区域展示两行动作说明,保留长文本横向滚动能力。 |
| | | - 右侧固定上一条/下一条按钮,页点移动到箭头下方。 |
| | | - 右上角新增弱视觉关闭按钮;默认半透明,hover/press 更清晰;点击后写入 `hideUsageTips`。 |
| | | - `updateColors()` 按 `AppGridTheme.usesDarkGlass` 切换浅玻璃/深玻璃 token。 |
| | | - 29 个语种新增 `usageTips.close`。 |
| | | - QA: |
| | | - `bash Scripts/usage_tips_qa.sh`:PASS。 |
| | | - `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS。 |
| | | - `git diff --check`:PASS。 |
| | | - `bash build.sh`:PASS,生成 `src/build/TagLauncher.app`。 |
| | | - `bash Scripts/macos14_build_metadata_qa.sh`:PASS,`LSMinimumSystemVersion=14.0`,`minos=14.0`,`arches=arm64`。 |
| | | - `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`:PASS。 |
| | | - 构建产物版本:`8.0.2 (20260625.1322)`。 |
| | | - `bash Scripts/theme_settings_qa.sh`:PASS。 |
| | | - `bash Scripts/tag_navigation_hover_scroll_qa.sh`:PASS。 |
| | | - `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS。 |
| | | - `APP_BUILD=20260624.2331 bash build.sh`:PASS。 |
| | | |
| | | ### 2026-06-25 使用技巧横幅 26.0625-2 视觉优化 |
| | | |
| | | - 需求来源:`canvas/26.06-UI调整.excalidraw` frame `26.0625-2`。 |
| | | - 用户标注问题: |
| | | - 标题区空间充裕但排版仍显拥挤。 |
| | | - 标题字号偏小,需要适当放大并加粗,同时不能撑破 29 语种矩形空间。 |
| | | - 标题字色需要按主题优化,必须保持清晰可读。 |
| | | - 标题前 icon 不应固定为单一灯泡,需要按技巧类型变化。 |
| | | - 正文超过一行时应采用 ordered list 样式。 |
| | | - 第 1 条标题改为“编辑标签”,正文补充“打开设置切换到标签编辑”和“在标签列表双击标签快速进入标签编辑页”。 |
| | | - 以上文案修改覆盖 29 个语种。 |
| | | - 本轮实现: |
| | | - `AppGridUsageTipsNSView` 标题字号从 22 semibold 调整为 25 bold。 |
| | | - 左侧标题面板宽度从 300-380pt 扩展为 350-460pt,降低长语种标题换行/挤压风险。 |
| | | - 标题和 icon 改为按 `AppGridTheme` 取高对比 accent;深蓝/黑色主题继续使用白字优先。 |
| | | - 按 tip id 切换语义 SF Symbol:编辑标签、套用标签、移动、复制、移除、排序、备注等不再共用灯泡 icon。 |
| | | - 正文 formatter 保留分隔符转换规则,并在多段内容时自动渲染为 `1.` / `2.` 编号步骤。 |
| | | - 第 1 条使用技巧 29 个语种 title/detail 已同步更新。 |
| | | - `Scripts/usage_tips_qa.sh` 增加标题字号、标题面板宽度、语义 icon、主题 accent、ordered list 和第 1 条双击标签说明检查。 |
| | | - QA: |
| | | - `bash Scripts/usage_tips_qa.sh`:PASS。 |
| | | - `bash Scripts/macos14_build_metadata_qa.sh`:PASS,`LSMinimumSystemVersion=14.0`,`minos=14.0`,`arches=arm64`。 |
| | | - `bash Scripts/appgrid_startup_loading_qa.sh`:PASS。 |
| | | - `bash Scripts/app_ordering_data_qa.sh`:PASS。 |
| | | - `bash Scripts/apple_default_note_policy_qa.sh`:PASS。 |
| | | - `bash Scripts/apple_default_apps_resource_qa.sh`:PASS。 |
| | | - `bash Scripts/quick_search_app_name_qa.sh`:SKIP,本机没有 `/Applications/贝锐向日葵被控.app` fixture。 |
| | | - `bash Scripts/window_logic_qa.sh`:未作为本轮通过项;脚本在 Dock tile 去重检查处失败,退出后无 TagLauncher 进程残留,判断为 GUI/Dock 可访问性环境或既有 QA 基础设施问题,和本轮使用技巧横幅代码路径无直接交集。 |
| | | - 版本推进到:`8.0.2 / 20260624.2341`。 |
| | |
| | | APPTAG_APP_SWIFT="$ROOT_DIR/Apptag/ApptagApp.swift" |
| | | PREFERENCES_VIEW_SWIFT="$ROOT_DIR/Apptag/PreferencesView.swift" |
| | | APP_DEFAULTS_SWIFT="$ROOT_DIR/Apptag/AppDefaults.swift" |
| | | APP_GRID_THEME_SWIFT="$ROOT_DIR/Apptag/AppGridTheme.swift" |
| | | LOCALIZATION_DIR="$ROOT_DIR/Apptag/Localization" |
| | | |
| | | fail() { |
| | |
| | | [[ -f "$APPTAG_APP_SWIFT" ]] || fail "missing ApptagApp.swift" |
| | | [[ -f "$PREFERENCES_VIEW_SWIFT" ]] || fail "missing PreferencesView.swift" |
| | | [[ -f "$APP_DEFAULTS_SWIFT" ]] || fail "missing AppDefaults.swift" |
| | | [[ -f "$APP_GRID_THEME_SWIFT" ]] || fail "missing AppGridTheme.swift" |
| | | [[ -d "$LOCALIZATION_DIR" ]] || fail "missing Localization directory" |
| | | |
| | | python3 - "$CONTENT_VIEW_SWIFT" "$APP_GRID_SWIFT" "$APPTAG_APP_SWIFT" "$PREFERENCES_VIEW_SWIFT" "$APP_DEFAULTS_SWIFT" "$LOCALIZATION_DIR" <<'PY' |
| | | python3 - "$CONTENT_VIEW_SWIFT" "$APP_GRID_SWIFT" "$APPTAG_APP_SWIFT" "$PREFERENCES_VIEW_SWIFT" "$APP_DEFAULTS_SWIFT" "$APP_GRID_THEME_SWIFT" "$LOCALIZATION_DIR" <<'PY' |
| | | import json |
| | | import pathlib |
| | | import re |
| | |
| | | apptag_app = pathlib.Path(sys.argv[3]).read_text(encoding="utf-8") |
| | | preferences = pathlib.Path(sys.argv[4]).read_text(encoding="utf-8") |
| | | defaults = pathlib.Path(sys.argv[5]).read_text(encoding="utf-8") |
| | | localization_dir = pathlib.Path(sys.argv[6]) |
| | | |
| | | required_keys = [ |
| | | "settings.hideUsageTips", |
| | | "settings.hideUsageTipsDesc", |
| | | "usageTips.previous", |
| | | "usageTips.next", |
| | | ] |
| | | for index in range(1, 9): |
| | | required_keys.append(f"usageTips.tip{index}.title") |
| | | required_keys.append(f"usageTips.tip{index}.detail") |
| | | theme = pathlib.Path(sys.argv[6]).read_text(encoding="utf-8") |
| | | localization_dir = pathlib.Path(sys.argv[7]) |
| | | |
| | | |
| | | def fail(message: str) -> None: |
| | |
| | | fail(message) |
| | | |
| | | |
| | | required_keys = [ |
| | | "settings.hideUsageTips", |
| | | "settings.hideUsageTipsDesc", |
| | | "usageTips.previous", |
| | | "usageTips.next", |
| | | "usageTips.close", |
| | | ] |
| | | for index in range(1, 9): |
| | | required_keys.append(f"usageTips.tip{index}.title") |
| | | required_keys.append(f"usageTips.tip{index}.detail") |
| | | |
| | | require( |
| | | r"static\s+let\s+hideUsageTips\s*=\s*false", |
| | | defaults, |
| | |
| | | defaults, |
| | | "AppDefaults.register must register hideUsageTips", |
| | | ) |
| | | require( |
| | | r'@AppStorage\("hideUsageTips"\)\s+private\s+var\s+hideUsageTips\s*=\s*AppDefaults\.hideUsageTips', |
| | | content_view, |
| | | "ContentView must observe hideUsageTips with AppStorage", |
| | | ) |
| | | require( |
| | | r'@AppStorage\("hideUsageTips"\)\s+private\s+var\s+hideUsageTips\s*=\s*AppDefaults\.hideUsageTips', |
| | | preferences, |
| | | "PreferencesView must persist hideUsageTips with AppStorage", |
| | | ) |
| | | for source_name, source in { |
| | | "ContentView.swift": content_view, |
| | | "PreferencesView.swift": preferences, |
| | | }.items(): |
| | | require( |
| | | r'@AppStorage\("hideUsageTips"\)\s+private\s+var\s+hideUsageTips\s*=\s*AppDefaults\.hideUsageTips', |
| | | source, |
| | | f"{source_name} must persist hideUsageTips with AppStorage", |
| | | ) |
| | | |
| | | require( |
| | | r'Toggle\s*\(\s*tr\("settings\.hideUsageTips"\)\s*,\s*isOn\s*:\s*\$hideUsageTips\s*\)', |
| | | preferences, |
| | | "General settings must expose a Hide usage tips toggle", |
| | | ) |
| | | |
| | | require( |
| | | r"\bprivate\s+final\s+class\s+AppGridUsageTipsNSView\s*:\s*NSView\b", |
| | | app_grid, |
| | | "Usage tips must be implemented as a native AppKit NSView", |
| | | ) |
| | | if re.search(r"AppGridUsageTipsBar\s*:\s*View|usageTipNavigationButton|Text\s*\(\s*tr\s*\(\s*tip\.", content_view): |
| | | fail("usage tips must not be rendered by SwiftUI in ContentView") |
| | | |
| | | require( |
| | | r"enum\s+AppGridUsageTipsMetrics\s*\{(?P<body>.*?)static\s+let\s+barHeight\s*:\s*CGFloat\s*=\s*136(?P<body2>.*?)static\s+let\s+reservedHeight\s*:\s*CGFloat\s*=\s*176", |
| | | r"enum\s+AppGridUsageTipsMetrics\s*\{(?P<body>.*?)static\s+let\s+barHeight\s*:\s*CGFloat\s*=\s*154(?P<body2>.*?)static\s+let\s+reservedHeight\s*:\s*CGFloat\s*=\s*194", |
| | | app_grid, |
| | | "Usage tips overlay must reserve a design-reviewed multi-line bottom space for the native HUD bar", |
| | | ) |
| | | require( |
| | | r"NSVisualEffectView\s*\(", |
| | | app_grid, |
| | | "Usage tips must use a native macOS visual effect surface instead of split black/white blocks", |
| | | ) |
| | | require( |
| | | r'systemSymbolName\s*:\s*"lightbulb\.fill"', |
| | | app_grid, |
| | | "Usage tips HUD must include a compact native guidance icon", |
| | | ) |
| | | require( |
| | | r"preferredWidth\s*\(\s*maxAvailableWidth\s*:\s*CGFloat\s*\)", |
| | | app_grid, |
| | | "Usage tips HUD must be content-adaptive instead of stretched across the full AppGrid", |
| | | ) |
| | | if re.search(r"static\s+let\s+maxWidth\s*:\s*CGFloat", app_grid): |
| | | fail("Usage tips HUD must not use a hard maximum width that truncates localized titles") |
| | | require( |
| | | r"let\s+titleWidth\s*=\s*ceil\s*\(\s*titleLabel\.attributedStringValue\.size\(\)\.width\s*\)", |
| | | app_grid, |
| | | "Usage tips layout must measure the localized title width instead of using a fixed title column", |
| | | ) |
| | | require( |
| | | r"configureLabel\s*\(\s*titleLabel\s*,\s*font\s*:\s*titleFont\s*,\s*lineBreakMode\s*:\s*\.byClipping\s*\)", |
| | | app_grid, |
| | | "Usage tip titles must not use tail truncation ellipses", |
| | | ) |
| | | require( |
| | | r"let\s+titleWidth\s*=\s*availableTextWidth", |
| | | app_grid, |
| | | "Usage tip title label must span the text column so centered titles are visually centered", |
| | | ) |
| | | require( |
| | | r"configureLabel\s*\(_\s+label\s*:\s*NSTextField,\s*font\s*:\s*NSFont,\s*lineBreakMode\s*:\s*NSLineBreakMode\s*\)\s*\{(?P<body>.*?)label\.alignment\s*=\s*\.center", |
| | | app_grid, |
| | | "Usage tip title labels must be horizontally centered", |
| | | ) |
| | | if re.search(r"configureLabel\s*\(\s*titleLabel\s*,\s*font\s*:\s*titleFont\s*,\s*lineBreakMode\s*:\s*\.byTruncatingTail\s*\)", app_grid): |
| | | fail("Usage tip titles must not be configured with byTruncatingTail") |
| | | require( |
| | | r"relayoutUsageTipsAfterContentChange\s*\(\s*\)", |
| | | app_grid, |
| | | "Usage tips must ask the host to recompute width after localized text changes", |
| | | ) |
| | | require( |
| | | r"controlZoneWidth\s*:\s*CGFloat\s*=\s*176", |
| | | app_grid, |
| | | "Usage tips layout must reserve a fixed right-side control zone", |
| | | ) |
| | | require( |
| | | r"availableTextWidth\s*=\s*max\s*\(\s*1\s*,\s*textRight\s*-\s*titleX\s*\)", |
| | | app_grid, |
| | | "Usage tips layout must allocate text from actual available AppGrid width", |
| | | ) |
| | | require( |
| | | r"dotsView\.frame\s*=\s*NSRect\s*\((?P<body>.*?)x\s*:\s*buttonsGroupMinX\s*\+\s*\(buttonsWidth\s*-\s*dotsWidth\)\s*/\s*2(?P<body2>.*?)y\s*:\s*previousButton\.frame\.maxY\s*\+\s*buttonDotsGap", |
| | | app_grid, |
| | | "Usage tips page dots must sit centered below the previous/next arrow buttons", |
| | | ) |
| | | require( |
| | | r"detailRight\s*=\s*textRight", |
| | | app_grid, |
| | | "Usage tips detail text must stop before the fixed right-side arrow control zone", |
| | | ) |
| | | require( |
| | | r"configureDetailLabel\s*\(\s*\)(?P<body>.*?)detailLabel\.lineBreakMode\s*=\s*\.byWordWrapping", |
| | | app_grid, |
| | | "Usage tip detail text must wrap naturally inside the reserved text column", |
| | | ) |
| | | require( |
| | | r"detailLabel\.maximumNumberOfLines\s*=\s*2", |
| | | app_grid, |
| | | "Usage tip detail text must use the design-reviewed two-line body layout", |
| | | ) |
| | | require( |
| | | r"return\s+min\s*\(\s*maxAvailableWidth\s*,\s*preferredWidth\s*\)", |
| | | app_grid, |
| | | "Usage tips preferred width must expand up to available AppGrid width for long localizations", |
| | | ) |
| | | require( |
| | | r"detailScrollView\.hasHorizontalScroller\s*=\s*true", |
| | | app_grid, |
| | | "Long localized tip text must be horizontally scrollable in the native AppKit bar", |
| | | ) |
| | | if len(re.findall(r"NSFont\.systemFont\s*\(\s*ofSize\s*:\s*24\s*,\s*weight\s*:\s*\.(?:semibold|regular)\s*\)", app_grid)) < 2: |
| | | fail("Usage tip title/detail fonts must use readable 24pt native HUD typography") |
| | | require( |
| | | r"override\s+func\s+scrollWheel\s*\(\s*with\s+event\s*:\s*NSEvent\s*\)\s*\{\s*detailScrollView\.scrollWheel\s*\(\s*with\s*:\s*event\s*\)", |
| | | app_grid, |
| | | "Usage tips must route wheel events to the native horizontal text scroller", |
| | | "usage tips must reserve the new design-reviewed full-width teaching banner height", |
| | | ) |
| | | require( |
| | | r"bottomContentPadding\s*:\s*shouldShowUsageTips\s*\?\s*AppGridUsageTipsMetrics\.reservedHeight\s*:\s*0", |
| | |
| | | require( |
| | | r"usageTipsVisible\s*:\s*shouldShowUsageTips", |
| | | content_view, |
| | | "ContentView must pass visibility into the native AppKit usage tips bar", |
| | | "ContentView must pass usage tip visibility into AppGrid", |
| | | ) |
| | | require( |
| | | r"selectedUsageTipIndex\s*:\s*\$selectedUsageTipIndex", |
| | | content_view, |
| | | "ContentView must bind usage tip selection to the native AppKit bar", |
| | | "ContentView must bind usage tip selection to AppGrid", |
| | | ) |
| | | require( |
| | | r"override\s+func\s+hitTest\s*\(\s*_\s+point\s*:\s*NSPoint\s*\)\s*->\s*NSView\?\s*\{(?P<body>.*?)bounds\.contains\s*\(\s*point\s*\)", |
| | | r"onHideUsageTips\s*:\s*\{\s*hideUsageTips\s*=\s*true\s*\}", |
| | | content_view, |
| | | "closing the native usage tips banner must persist hideUsageTips", |
| | | ) |
| | | require( |
| | | r"let\s+onHideUsageTips\s*:\s*\(\)\s*->\s*Void", |
| | | app_grid, |
| | | "Native usage tips bar must own hit testing so clicks do not fall through to AppGrid", |
| | | "AppGridCollectionView must accept an onHideUsageTips callback", |
| | | ) |
| | | require( |
| | | r"override\s+func\s+hitTest\s*\(\s*_\s+point\s*:\s*NSPoint\s*\)\s*->\s*NSView\?\s*\{(?P<body>.*?)usageTipsView\.frame\.contains\s*\(\s*point\s*\)", |
| | | r"closeButton\.action\s*=\s*\{\s*\[weak\s+self\]\s+in\s+self\?\.hideUsageTips\(\)\s*\}", |
| | | app_grid, |
| | | "AppGrid host must prioritize the native usage tips hit-test region above the collection view", |
| | | "close button must call the native hideUsageTips handler", |
| | | ) |
| | | require( |
| | | r"coordinator\?\.onHideUsageTips\(\)", |
| | | app_grid, |
| | | "native hideUsageTips must call back to persisted SwiftUI state", |
| | | ) |
| | | |
| | | require( |
| | | r"private\s+let\s+titlePanelView\s*=\s*NSView\(\)", |
| | | app_grid, |
| | | "usage tips must use a separated title panel", |
| | | ) |
| | | require( |
| | | r"private\s+let\s+titleFont\s*=\s*NSFont\.systemFont\s*\(\s*ofSize\s*:\s*25\s*,\s*weight\s*:\s*\.bold\s*\)", |
| | | app_grid, |
| | | "usage tips title must use the design-reviewed larger bold title font", |
| | | ) |
| | | require( |
| | | r"titlePanelWidth\s*=\s*min\s*\((?P<body>.*?)max\s*\(\s*350\s*,\s*visualFrame\.width\s*\*\s*0\.24\s*\)(?P<body2>.*?)min\s*\(\s*460\s*,\s*visualFrame\.width\s*\*\s*0\.36\s*\)", |
| | | app_grid, |
| | | "usage tips title panel must be wide enough for 29-language titles", |
| | | ) |
| | | require( |
| | | r"private\s+let\s+closeButton\s*=\s*AppGridUsageTipIconButton\s*\(\s*systemImage\s*:\s*\"xmark\"\s*\)", |
| | | app_grid, |
| | | "usage tips must provide a low-emphasis native close button", |
| | | ) |
| | | require( |
| | | r"controlZoneWidth\s*:\s*CGFloat\s*=\s*min\s*\(\s*148\s*,\s*max\s*\(\s*128\s*,\s*visualFrame\.width\s*\*\s*0\.08\s*\)\s*\)", |
| | | app_grid, |
| | | "usage tips must reserve a stable 128-148pt right-side control zone", |
| | | ) |
| | | require( |
| | | r"func\s+preferredWidth\s*\(\s*maxAvailableWidth\s*:\s*CGFloat\s*\)\s*->\s*CGFloat\s*\{\s*max\s*\(\s*1\s*,\s*maxAvailableWidth\s*\)\s*\}", |
| | | app_grid, |
| | | "usage tips visual banner must occupy the available bottom width", |
| | | ) |
| | | require( |
| | | r"dotsView\.frame\s*=\s*NSRect\s*\((?P<body>.*?)x\s*:\s*buttonsGroupMinX\s*\+\s*\(buttonsWidth\s*-\s*dotsWidth\)\s*/\s*2(?P<body2>.*?)y\s*:\s*previousButton\.frame\.maxY\s*\+\s*buttonDotsGap", |
| | | app_grid, |
| | | "page dots must be centered below the previous/next arrows", |
| | | ) |
| | | require( |
| | | r"detailScrollView\.frame\s*=\s*NSRect\s*\((?P<body>.*?)x\s*:\s*detailX(?P<body2>.*?)width\s*:\s*max\s*\(\s*1\s*,\s*textRight\s*-\s*detailX\s*\)", |
| | | app_grid, |
| | | "detail text must live in the middle reading area and stop before controls", |
| | | ) |
| | | require( |
| | | r"detailLabel\.lineBreakMode\s*=\s*\.byWordWrapping", |
| | | app_grid, |
| | | "detail text must wrap inside the two-line teaching banner", |
| | | ) |
| | | require( |
| | | r"detailLabel\.maximumNumberOfLines\s*=\s*2", |
| | | app_grid, |
| | | "detail text must keep the design-reviewed two-line layout", |
| | | ) |
| | | require( |
| | | r"detailLabel\.cell\s*=\s*AppGridCenteredMultilineTextFieldCell\s*\(\s*textCell\s*:\s*\"\"\s*\)", |
| | | app_grid, |
| | | "detail text must use a vertically centered multiline cell", |
| | | ) |
| | | require( |
| | | r"paragraphStyle\.minimumLineHeight\s*=\s*31(?P<body>.*?)paragraphStyle\.maximumLineHeight\s*=\s*34(?P<body2>.*?)paragraphStyle\.lineSpacing\s*=\s*6", |
| | | app_grid, |
| | | "detail text must use the design-reviewed line height and spacing", |
| | | ) |
| | | require( |
| | | r"detailScrollView\.hasHorizontalScroller\s*=\s*true", |
| | | app_grid, |
| | | "long localized tip text must remain horizontally scrollable", |
| | | ) |
| | | require( |
| | | r"formattedTipDetail\s*\(_\s+text\s*:\s*String\s*\)", |
| | | app_grid, |
| | | "usage tips detail text must normalize separators for display", |
| | | ) |
| | | require( |
| | | r"orderedTipDetail\s*\(_\s+text\s*:\s*String\s*\)(?P<body>.*?)map\s*\{\s*\"\\\(\$0\.offset\s*\+\s*1\)\.\s*\\\(\$0\.element\)\"\s*\}", |
| | | app_grid, |
| | | "multi-line usage tips detail must be rendered as an ordered list", |
| | | ) |
| | | require( |
| | | r"replacingOccurrences\s*\(\s*of\s*:\s*\"\\\\s\*\(\?:-->\|->\|>\|→\|>\|,\)\\\\s\*\"\s*,\s*with\s*:\s*\"\\n\"", |
| | | app_grid, |
| | | "usage tips detail text must turn separators into hard line breaks", |
| | | ) |
| | | require( |
| | | r"updateTipIcon\s*\(\s*id\s*:\s*tip\.id\s*\)", |
| | | app_grid, |
| | | "usage tips must update the leading icon for each tip", |
| | | ) |
| | | require( |
| | | r"private\s+func\s+updateTipIcon\s*\(\s*id\s*:\s*Int\s*\)(?P<body>.*?)case\s+1:(?P<body2>.*?)tag\.fill(?P<body3>.*?)case\s+8:(?P<body4>.*?)note\.text", |
| | | app_grid, |
| | | "usage tips must vary the leading icon by tip type", |
| | | ) |
| | | |
| | | require( |
| | | r"private\s+func\s+updateColors\(\)(?P<body>.*?)coordinator\?\.appGridTheme\.usesDarkGlass\s*==\s*true", |
| | | app_grid, |
| | | "usage tips colors must be driven by the current AppGrid theme", |
| | | ) |
| | | for token in [ |
| | | r"backgroundView\.material\s*=\s*usesDarkGlass\s*\?\s*\.underWindowBackground\s*:\s*\.popover", |
| | | r"backgroundView\.appearance\s*=\s*usesDarkGlass\s*\?\s*NSAppearance\s*\(\s*named\s*:\s*\.darkAqua\s*\)\s*:\s*nil", |
| | | r"titlePanelView\.layer\?\.backgroundColor", |
| | | r"usageTipAccentColor\s*\(\s*for\s*:\s*theme\s*\)", |
| | | r"titleLabel\.textColor\s*=\s*usesDarkGlass", |
| | | r"detailLabel\.textColor\s*=\s*usesDarkGlass", |
| | | r"closeButton\.hoveredTintColor\s*=\s*usesDarkGlass", |
| | | r"dotsView\.selectedColor\s*=\s*usesDarkGlass", |
| | | ]: |
| | | require(token, app_grid, f"usage tips theme token missing: {token}") |
| | | require( |
| | | r"private\s+func\s+usageTipAccentColor\s*\(\s*for\s+theme\s*:\s*AppGridTheme\s*\)\s*->\s*NSColor(?P<body>.*?)case\s+\.pink:(?P<body2>.*?)case\s+\.colorful:", |
| | | app_grid, |
| | | "usage tips title color must provide per-theme readable accent colors", |
| | | ) |
| | | |
| | | require( |
| | | r"var\s+usesDarkGlass\s*:\s*Bool\s*\{(?P<body>.*?)case\s+\.deepBlue,\s*\.black:(?P<body2>.*?)return\s+true", |
| | | theme, |
| | | "only dark/deep-black themes should use the dark glass palette", |
| | | ) |
| | | |
| | | for token in [ |
| | | r"usageTipsEventRegion\s*\(\s*\)\s*->\s*NSRect", |
| | | app_grid, |
| | | "AppGrid host must reserve a full bottom event region for tips so clicks cannot pass through", |
| | | ) |
| | | require( |
| | | r"private\s+final\s+class\s+AppGridUsageTipsShieldView\s*:\s*NSView\b", |
| | | app_grid, |
| | | "AppGrid must use a native transparent shield view for the full bottom tips event region", |
| | | ) |
| | | require( |
| | | r"usageTipsShieldView\.frame\s*=\s*usageTipsEventRegion\s*\(\s*\)", |
| | | app_grid, |
| | | "Usage tips shield must cover the full bottom event region, not just the visible HUD", |
| | | ) |
| | | require( |
| | | r"usageTipsView\.frame\s*=\s*usageTipsEventRegion\s*\(\s*\)", |
| | | app_grid, |
| | | "Usage tips HUD hit-test view itself must cover the full bottom event region", |
| | | ) |
| | | require( |
| | | r"configureVisualLayout\s*\(\s*width\s*:\s*CGFloat\s*,\s*height\s*:\s*CGFloat\s*,\s*bottomMargin\s*:\s*CGFloat\s*\)", |
| | | app_grid, |
| | | "Usage tips must separate full hit-test coverage from the centered visual HUD frame", |
| | | ) |
| | | require( |
| | | r"currentVisualFrame\s*\(\s*\)\s*->\s*NSRect", |
| | | app_grid, |
| | | "Usage tips must compute a centered visual frame inside the full bottom hit-test region", |
| | | ) |
| | | require( |
| | | r"addSubview\s*\(\s*usageTipsShieldView\s*,\s*positioned\s*:\s*\.above\s*,\s*relativeTo\s*:\s*scrollView\s*\)", |
| | | app_grid, |
| | | "Usage tips shield must sit above the AppGrid collection scroll view", |
| | | ) |
| | | require( |
| | | r"addSubview\s*\(\s*usageTipsView\s*,\s*positioned\s*:\s*\.above\s*,\s*relativeTo\s*:\s*usageTipsShieldView\s*\)", |
| | | app_grid, |
| | | "Visible usage tips HUD must sit above the transparent event shield", |
| | | ) |
| | | require( |
| | | r"claimUsageTipsEventRegion\s*\(\s*\)", |
| | | app_grid, |
| | | "AppGrid host must claim bottom tips events and suppress lower bubbles", |
| | | ) |
| | | require( |
| | | r"handleUsageTipsMouseDown\s*\(\s*_\s+event\s*:\s*NSEvent\s*\)\s*->\s*Bool", |
| | | app_grid, |
| | | "AppGrid host must expose a native AppKit event router for usage tips clicks", |
| | | ) |
| | | ]: |
| | | require(token, app_grid, f"usage tips click-through protection missing: {token}") |
| | | require( |
| | | r"addLocalMonitorForEvents\s*\(\s*matching\s*:\s*\[(?P<body>.*?)\.leftMouseDown(?P<body2>.*?)\.leftMouseUp(?P<body3>.*?)\.rightMouseDown(?P<body4>.*?)\.otherMouseUp", |
| | | app_grid, |
| | | "AppGrid host must intercept usage tips mouseDown/mouseUp events before NSCollectionView app items receive them", |
| | | ) |
| | | require( |
| | | r"\.leftMouseUp(?P<body>.*?)\.rightMouseUp(?P<body2>.*?)\.otherMouseUp", |
| | | app_grid, |
| | | "AppGrid host must also swallow mouseUp events in the usage tips region so app icons below cannot open on release", |
| | | ) |
| | | require( |
| | | r"triggerButtons\s*:\s*event\.type\s*==\s*\.leftMouseDown", |
| | | app_grid, |
| | | "Usage tips monitor must only trigger paging on left mouseDown, not on mouseUp or right/other clicks", |
| | | "usage tips monitor must only trigger page changes on left mouseDown", |
| | | ) |
| | | require( |
| | | r"usageTipsHostPoint\s*\(\s*for\s+event\s*:\s*NSEvent\s*\)\s*->\s*NSPoint", |
| | | r"func\s+handleMouseEventFromHost\s*\(\s*_\s+event\s*:\s*NSEvent\s*,\s*triggerButtons\s*:\s*Bool\s*\)\s*->\s*Bool\s*\{(?P<body>.*?)if\s+triggerButtons,\s*routeButtonClickIfNeeded\s*\(\s*event\s*\)(?P<body2>.*?)claimInteractionFocus\(\)(?P<body3>.*?)return\s+true", |
| | | app_grid, |
| | | "Usage tips mouse monitor must resolve the event position through the AppGrid host", |
| | | "host-routed tips clicks must consume the event and only trigger buttons on hit regions", |
| | | ) |
| | | require( |
| | | r"window\.convertPoint\s*\(\s*fromScreen\s*:\s*NSEvent\.mouseLocation\s*\)", |
| | | app_grid, |
| | | "Usage tips hit testing must fall back to current screen mouse location when event.window is unreliable", |
| | | ) |
| | | require( |
| | | r"self\.handleUsageTipsMouseEvent\s*\(\s*event\s*,(?P<body>.*?)triggerButtons\s*:\s*event\.type\s*==\s*\.leftMouseDown(?P<body2>.*?)return\s+nil", |
| | | app_grid, |
| | | "Usage tips mouse monitor must swallow handled bottom-tip events so app icons below cannot open", |
| | | ) |
| | | require( |
| | | r"removeUsageTipsMouseMonitor\s*\(\s*\)", |
| | | app_grid, |
| | | "Usage tips mouse monitor must be removed when the AppGrid host leaves its window", |
| | | ) |
| | | require( |
| | | r"func\s+handleMouseEventFromHost\s*\(\s*_\s+event\s*:\s*NSEvent\s*,\s*triggerButtons\s*:\s*Bool\s*\)\s*->\s*Bool\s*\{(?P<body>.*?)if\s+triggerButtons\s*,\s*routeButtonClickIfNeeded\s*\(\s*event\s*\)(?P<body2>.*?)claimInteractionFocus\s*\(\s*\)(?P<body3>.*?)return\s+true", |
| | | app_grid, |
| | | "Usage tips host-routed clicks must only trigger page changes on arrow hit regions and otherwise only consume the event", |
| | | ) |
| | | if re.search(r"handleMouseEventFromHost\s*\(\s*_\s+event\s*:\s*NSEvent\s*,\s*triggerButtons\s*:\s*Bool\s*\)\s*->\s*Bool\s*\{(?P<body>.*?)target\.mouseDown", app_grid, re.S): |
| | | fail("Usage tips host-routed clicks must not redispatch arbitrary mouseDown events that can pierce to app icons") |
| | | require( |
| | | r"override\s+func\s+mouseDown\s*\(\s*with\s+event\s*:\s*NSEvent\s*\)", |
| | | app_grid, |
| | | "Native usage tips bar must swallow mouseDown events instead of letting clicks close the grid", |
| | | ) |
| | | require( |
| | | if re.search(r"handleMouseEventFromHost\s*\(\s*_\s+event\s*:\s*NSEvent,\s*triggerButtons\s*:\s*Bool\s*\).*?target\.mouseDown", app_grid, re.S): |
| | | fail("usage tips host-routed clicks must not redispatch arbitrary mouseDown events") |
| | | |
| | | for token in [ |
| | | r"private\s+final\s+class\s+AppGridUsageTipIconButton\s*:\s*NSView\b", |
| | | app_grid, |
| | | "Usage tip navigation controls must be native AppKit hit-testable views", |
| | | ) |
| | | require( |
| | | r"private\s+final\s+class\s+AppGridDecorativeImageView\s*:\s*NSImageView\b(?P<body>.*?)override\s+func\s+hitTest\s*\(\s*_\s+point\s*:\s*NSPoint\s*\)\s*->\s*NSView\?\s*\{\s*nil\s*\}", |
| | | app_grid, |
| | | "Usage tip icon images must not steal hit testing from their parent AppKit controls", |
| | | ) |
| | | require( |
| | | r"private\s+final\s+class\s+AppGridUsageTipIconButton\s*:\s*NSView\b(?P<body>.*?)acceptsFirstMouse\s*\(\s*for\s+event\s*:\s*NSEvent\?\s*\)\s*->\s*Bool\s*\{\s*true\s*\}", |
| | | app_grid, |
| | | "Usage tip navigation controls must accept first mouse clicks in the floating overlay", |
| | | ) |
| | | require( |
| | | r"override\s+func\s+acceptsFirstMouse\s*\(\s*for\s+event\s*:\s*NSEvent\?\s*\)\s*->\s*Bool\s*\{\s*true\s*\}", |
| | | r"override\s+func\s+isAccessibilityElement\s*\(\s*\)\s*->\s*Bool\s*\{\s*true\s*\}", |
| | | app_grid, |
| | | "Usage tip navigation controls must be exposed as accessibility elements", |
| | | ) |
| | | require( |
| | | r"override\s+func\s+accessibilityRole\s*\(\s*\)\s*->\s*NSAccessibility\.Role\?\s*\{\s*\.button\s*\}", |
| | | app_grid, |
| | | "Usage tip navigation controls must expose the AX button role", |
| | | ) |
| | | require( |
| | | r"override\s+func\s+accessibilityPerformPress\s*\(\s*\)\s*->\s*Bool\s*\{(?P<body>.*?)action\?\(\)(?P<body2>.*?)return\s+true", |
| | | app_grid, |
| | | "Usage tip navigation controls must support AX press actions", |
| | | ) |
| | | require( |
| | | r"override\s+var\s+mouseDownCanMoveWindow\s*:\s*Bool\s*\{\s*false\s*\}", |
| | | app_grid, |
| | | "Usage tips hit-test views must not let mouseDown move or dismiss the overlay window", |
| | | ) |
| | | require( |
| | | r"func\s+selectUsageTip\s*\(\s*offset\s*:\s*Int\s*\)", |
| | | app_grid, |
| | | "Usage tip navigation must update the selected tip index from AppKit", |
| | | ) |
| | | r"var\s+hoveredTintColor\s*:\s*NSColor\?", |
| | | r"var\s+pressedTintColor\s*:\s*NSColor\?", |
| | | r"func\s+performPressFeedback\(\)", |
| | | r"private\s+func\s+updateIconTint\(\)", |
| | | ]: |
| | | require(token, app_grid, f"native usage tip control behavior missing: {token}") |
| | | require( |
| | | r"previousButton\.action\s*=\s*\{\s*\[weak\s+self\]\s+in\s+self\?\.selectUsageTip\s*\(\s*offset\s*:\s*-1\s*\)\s*\}", |
| | | app_grid, |
| | | "Previous usage tip control must call the native selection handler", |
| | | "previous usage tip control must page backward", |
| | | ) |
| | | require( |
| | | r"nextButton\.action\s*=\s*\{\s*\[weak\s+self\]\s+in\s+self\?\.selectUsageTip\s*\(\s*offset\s*:\s*1\s*\)\s*\}", |
| | | app_grid, |
| | | "Next usage tip control must call the native selection handler", |
| | | "next usage tip control must page forward", |
| | | ) |
| | | require( |
| | | r"override\s+func\s+mouseDown\s*\(\s*with\s+event\s*:\s*NSEvent\s*\)\s*\{(?P<body>.*?)action\?\(\)", |
| | | r"routeButtonClickIfNeeded\s*\(_\s+event\s*:\s*NSEvent\s*\)(?P<body>.*?)nextButton\.frame\.insetBy(?P<body2>.*?)previousButton\.frame\.insetBy", |
| | | app_grid, |
| | | "Native usage tip icon buttons must invoke their action directly on mouseDown", |
| | | "usage tip arrows must only respond inside their hit regions", |
| | | ) |
| | | require( |
| | | r"onUsageTipIndexChange\s*\(\s*nextIndex\s*\)", |
| | | r"override\s+func\s+keyDown\s*\(\s*with\s+event\s*:\s*NSEvent\s*\)(?P<body>.*?)kVK_LeftArrow(?P<body2>.*?)kVK_RightArrow", |
| | | app_grid, |
| | | "Native usage tip selection must publish the new index back to SwiftUI state", |
| | | ) |
| | | require( |
| | | r"override\s+func\s+keyDown\s*\(\s*with\s+event\s*:\s*NSEvent\s*\)\s*\{(?P<body>.*?)kVK_LeftArrow(?P<body2>.*?)kVK_RightArrow", |
| | | app_grid, |
| | | "Focused native usage tips HUD must support left/right arrow key switching", |
| | | ) |
| | | require( |
| | | r"formattedTipDetail\s*\(_\s+text\s*:\s*String\s*\)", |
| | | app_grid, |
| | | "Usage tips detail text must normalize separators for display", |
| | | ) |
| | | require( |
| | | r"replacingOccurrences\s*\(\s*of\s*:\s*\"\\\\s\*\(\?:-->\|->\|>\|→\|>\|,\)\\\\s\*\"\s*,\s*with\s*:\s*\"\\n\"", |
| | | app_grid, |
| | | "Usage tips detail text must turn navigation markers and commas into hard line breaks", |
| | | ) |
| | | require( |
| | | r"setUsageTipsBubbleDisabled\s*\(\s*inside\s*\)", |
| | | app_grid, |
| | | "Hovering usage tips must suppress lower AppGrid bubbles", |
| | | ) |
| | | require( |
| | | r"onUsageTipsHoverChange\s*\(\s*inside\s*\)", |
| | | app_grid, |
| | | "Hovering usage tips must notify ContentView to clear already visible SwiftUI bubbles", |
| | | ) |
| | | require( |
| | | r"shouldSwallowUsageTipsBackdropClick\s*\(\s*at\s+location\s*:\s*NSPoint\s*\)\s*->\s*Bool", |
| | | apptag_app, |
| | | "DismissibleHostingView must not treat bottom usage tips clicks as backdrop taps", |
| | | ) |
| | | require( |
| | | r"routeUsageTipsMouseDownIfNeeded\s*\(\s*event\s*\)", |
| | | apptag_app, |
| | | "DismissibleHostingView must forward bottom usage tips clicks to native AppKit controls before swallowing", |
| | | ) |
| | | require( |
| | | r"findAppGridCollectionHost\s*\(\s*in\s+view\s*:\s*NSView\s*\)\s*->\s*AppGridCollectionHostView\?", |
| | | apptag_app, |
| | | "DismissibleHostingView must locate the native AppGrid host for usage tips event forwarding", |
| | | ) |
| | | require( |
| | | r"AppGridUsageTipsMetrics\.reservedHeight", |
| | | apptag_app, |
| | | "Backdrop usage tips guard must match the native AppGrid reserved height", |
| | | ) |
| | | require( |
| | | r'UserDefaults\.standard\.bool\s*\(\s*forKey\s*:\s*"hideUsageTips"\s*\)', |
| | | apptag_app, |
| | | "Backdrop usage tips guard must respect the hide usage tips setting", |
| | | ) |
| | | require( |
| | | r"private\s+func\s+handleUsageTipsHoverChange\s*\(\s*_\s+hovering\s*:\s*Bool\s*\)", |
| | | content_view, |
| | | "ContentView must clear existing app bubbles while the native usage tips bar is hovered", |
| | | ) |
| | | if re.search(r"AppGridUsageTipsBar\s*:\s*View|usageTipNavigationButton|ScrollView\s*\(\s*\.horizontal\s*,\s*showsIndicators\s*:\s*textHovered", content_view): |
| | | fail("usage tips must not be implemented with SwiftUI views in ContentView") |
| | | if re.search(r"Text\s*\(\s*tr\s*\(\s*tip\.(?:titleKey|detailKey)", content_view): |
| | | fail("usage tip title/detail rendering must not use SwiftUI Text") |
| | | require( |
| | | r"let\s+bottomContentPadding\s*:\s*CGFloat", |
| | | app_grid, |
| | | "AppGridCollectionView must accept bottom content padding", |
| | | ) |
| | | if '"bottom=\\(Int(bottomContentPadding.rounded()))"' not in app_grid: |
| | | fail("AppGrid layout signature must include bottom content padding") |
| | | require( |
| | | r"\+\s*max\s*\(\s*0\s*,\s*bottomContentPadding\s*\)", |
| | | app_grid, |
| | | "AppGrid layout plans must add bottom content padding to content height", |
| | | "focused native usage tips HUD must support left/right arrow keys", |
| | | ) |
| | | |
| | | tip_ids = [int(value) for value in re.findall(r"AppGridUsageTip\s*\(\s*id\s*:\s*(\d+)", content_view)] |
| | | if tip_ids != list(range(1, 9)): |
| | | fail(f"usage tip IDs must be exactly 1 through 8, got {tip_ids}") |
| | | if "usageTips.tip0" in content_view: |
| | | fail("usage tips must not expose a zero-based tip number") |
| | | for token in [ |
| | | r"shouldSwallowUsageTipsBackdropClick\s*\(\s*at\s+location\s*:\s*NSPoint\s*\)\s*->\s*Bool", |
| | | r"routeUsageTipsMouseDownIfNeeded\s*\(\s*event\s*\)", |
| | | r"findAppGridCollectionHost\s*\(\s*in\s+view\s*:\s*NSView\s*\)\s*->\s*AppGridCollectionHostView\?", |
| | | r"AppGridUsageTipsMetrics\.reservedHeight", |
| | | r'UserDefaults\.standard\.bool\s*\(\s*forKey\s*:\s*"hideUsageTips"\s*\)', |
| | | ]: |
| | | require(token, apptag_app, f"App window backdrop usage-tips protection missing: {token}") |
| | | |
| | | for source_name, source in { |
| | | "ContentView.swift": content_view, |
| | |
| | | fail(f"{source_name} must not rely on macOS 15/26 availability for usage tips") |
| | | if "scrollClipDisabled" in source: |
| | | fail(f"{source_name} must avoid newer scrollClipDisabled behavior for macOS 14 compatibility") |
| | | |
| | | tip_ids = [int(value) for value in re.findall(r"AppGridUsageTip\s*\(\s*id\s*:\s*(\d+)", content_view)] |
| | | if tip_ids != list(range(1, 9)): |
| | | fail(f"usage tip IDs must be exactly 1 through 8, got {tip_ids}") |
| | | if "usageTips.tip0" in content_view: |
| | | fail("usage tips must not expose a zero-based tip number") |
| | | |
| | | json_files = sorted(localization_dir.glob("*.json")) |
| | | if len(json_files) != 29: |
| | |
| | | title = data[f"usageTips.tip{tip_index}.title"] |
| | | if re.search(r"\s*[::]\s*$", title): |
| | | fail(f"{path.name} usageTips.tip{tip_index}.title must not end with a colon") |
| | | tip1_title = data["usageTips.tip1.title"] |
| | | tip1_detail = data["usageTips.tip1.detail"] |
| | | if path.stem == "en": |
| | | if tip1_title != "Tip 1 - Edit tags": |
| | | fail("en.json usageTips.tip1.title must describe editing tags") |
| | | if "Double-click" not in tip1_detail or "tag list" not in tip1_detail: |
| | | fail("en.json usageTips.tip1.detail must mention double-clicking a tag in the tag list") |
| | | if path.stem == "zh-Hans": |
| | | if tip1_title != "技巧1-编辑标签": |
| | | fail("zh-Hans.json usageTips.tip1.title must be 编辑标签") |
| | | if "双击标签" not in tip1_detail or "标签列表" not in tip1_detail: |
| | | fail("zh-Hans.json usageTips.tip1.detail must mention double-clicking in the tag list") |
| | | if path.stem == "zh-Hant": |
| | | if tip1_title != "技巧1-編輯標籤": |
| | | fail("zh-Hant.json usageTips.tip1.title must be 編輯標籤") |
| | | if "雙擊標籤" not in tip1_detail or "標籤列表" not in tip1_detail: |
| | | fail("zh-Hant.json usageTips.tip1.detail must mention double-clicking in the tag list") |
| | | if path.stem not in {"zh-Hans", "zh-Hant"}: |
| | | for tip_index in range(1, 9): |
| | | detail = data[f"usageTips.tip{tip_index}.detail"] |
| | |
| | | if not first or not second: |
| | | fail(f"{path.name} usageTips.tip{tip_index}.detail must have text on both sides of the comma line break") |
| | | |
| | | print("PASS usage tips QA: overlay, hide setting, macOS 14-safe implementation, and 29-language localizations are present") |
| | | print("PASS usage tips QA: native split teaching banner, theme-aware glass, click protection, close action, and 29-language localizations are present") |
| | | PY |