Fix space filter visual and close verification
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>APPL</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>0.0.60</string> |
| | | <string>0.0.61</string> |
| | | <key>CFBundleVersion</key> |
| | | <string>20260612.1713</string> |
| | | <string>20260612.1926</string> |
| | | <key>LSMinimumSystemVersion</key> |
| | | <string>26.0</string> |
| | | <key>NSHighResolutionCapable</key> |
| | |
| | | let isActive = isHovered || isFocused || isLocked |
| | | let isAssociated = (isAppAssociated || isWindowAssociated) && !isActive |
| | | |
| | | segment.containerLayer.borderWidth = isLocked |
| | | ? 6 |
| | | : (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1 |
| | | segment.containerLayer.borderColor = (isActive |
| | | segment.containerLayer.borderWidth = (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1 |
| | | segment.containerLayer.borderColor = (isLocked |
| | | ? NSColor.white.withAlphaComponent(0.86) |
| | | : isActive |
| | | ? spaceLaneOccupiedColor.withAlphaComponent(isLocked ? 0.92 : 0.62) |
| | | : isAssociated |
| | | ? spaceLaneOccupiedColor.withAlphaComponent(0.50) |
| | |
| | | ? NSColor.labelColor.withAlphaComponent(0.34) |
| | | : NSColor.separatorColor.withAlphaComponent(0.34) |
| | | ).cgColor |
| | | if !hasWindows { |
| | | if isLocked { |
| | | applySpaceLaneSolidFill( |
| | | to: segment.containerLayer, |
| | | color: spaceLaneLockedFillColor |
| | | ) |
| | | } else if !hasWindows { |
| | | applySpaceLaneSolidFill(to: segment.containerLayer, color: .clear) |
| | | } else if isActive { |
| | | applySpaceLaneSolidFill( |
| | |
| | | segment.containerLayer.transform = (isActive || isAssociated) |
| | | ? CATransform3DMakeScale(isActive ? 1.03 : 1.015, isActive ? 1.03 : 1.015, 1) |
| | | : CATransform3DIdentity |
| | | segment.containerLayer.shadowPath = hasWindows |
| | | segment.containerLayer.shadowPath = (hasWindows || isLocked) |
| | | ? CGPath( |
| | | roundedRect: segment.containerLayer.bounds, |
| | | cornerWidth: segment.containerLayer.cornerRadius, |
| | |
| | | transform: nil |
| | | ) |
| | | : nil |
| | | |
| | | applySpaceLaneContentVisualState(segment, isLocked: isLocked) |
| | | } |
| | | |
| | | private func applySpaceLaneContentVisualState(_ segment: SpaceLaneSegmentLayers, isLocked: Bool) { |
| | | let primaryTextColor: NSColor = isLocked ? .white : .labelColor |
| | | let secondaryTextColor: NSColor = isLocked |
| | | ? .white |
| | | : .secondaryLabelColor |
| | | |
| | | segment.labelLayer.foregroundColor = primaryTextColor.cgColor |
| | | segment.countLayer.foregroundColor = (isLocked ? NSColor.white : .secondaryLabelColor).cgColor |
| | | segment.appLayer.foregroundColor = (segment.space.type == .fullscreen |
| | | ? primaryTextColor |
| | | : secondaryTextColor |
| | | ).cgColor |
| | | |
| | | for block in segment.windowBlocks { |
| | | block.backgroundColor = (isLocked |
| | | ? NSColor.white |
| | | : spaceLaneOccupiedColor.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.58) |
| | | ).cgColor |
| | | } |
| | | |
| | | (segment.fullscreenMarkerLayer as? CAShapeLayer)?.strokeColor = (isLocked |
| | | ? NSColor.white |
| | | : NSColor.secondaryLabelColor.withAlphaComponent(segment.space.isCurrent ? 0.42 : 0.28) |
| | | ).cgColor |
| | | } |
| | | |
| | | private func shouldScrollSpaceLane(for event: NSEvent) -> Bool { |
| | |
| | | return NSColor(calibratedRed: 0, green: 122 / 255, blue: 1, alpha: 1) |
| | | } |
| | | |
| | | private var spaceLaneLockedFillColor: NSColor { |
| | | if effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua { |
| | | return NSColor(calibratedRed: 58 / 255, green: 148 / 255, blue: 1, alpha: 0.86) |
| | | } |
| | | |
| | | return NSColor(calibratedRed: 90 / 255, green: 171 / 255, blue: 1, alpha: 0.82) |
| | | } |
| | | |
| | | private func spaceLaneIdleOccupiedGradientColors() -> [NSColor] { |
| | | if effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua { |
| | | let blue = NSColor(calibratedRed: 36 / 255, green: 54 / 255, blue: 72 / 255, alpha: 1) |
| | |
| | | "windowBlockCount": segment.windowBlocks.count, |
| | | "windowBlockColors": segment.windowBlocks.map { colorDictionary(from: $0.backgroundColor) }, |
| | | "windowBlockFrames": segment.windowBlocks.map { dictionary(from: $0.frame) }, |
| | | "labelColor": colorDictionary(from: segment.labelLayer.foregroundColor), |
| | | "countColor": colorDictionary(from: segment.countLayer.foregroundColor), |
| | | "appColor": colorDictionary(from: segment.appLayer.foregroundColor), |
| | | "fullscreenMarkerStrokeColor": colorDictionary( |
| | | from: (segment.fullscreenMarkerLayer as? CAShapeLayer)?.strokeColor |
| | | ), |
| | | "backgroundKind": spaceLaneFillKind(for: segment.containerLayer), |
| | | "backgroundColor": colorDictionary(from: segment.containerLayer.backgroundColor), |
| | | "gradientColors": gradientColorDictionaries(from: segment.containerLayer), |
| | |
| | | "result": Self.closeResultString(result) |
| | | ]) |
| | | if result == .requested { |
| | | suppressCloseTarget(request) |
| | | DevelopmentDiagnostics.log("quickSwitch.close.request.refreshAfterClose", [ |
| | | "targetKind": request.kindDescription, |
| | | "appGroupIndex": request.appGroupIndex, |
| | | "appName": request.appName, |
| | | "windowID": request.windowID |
| | | "windowID": request.windowID, |
| | | "optimisticRemove": false |
| | | ]) |
| | | pendingCloseVerification = PendingCloseVerification( |
| | | request: request, |
| | |
| | | require(report.get("lastCloseTargetKind") == "window", "confirmed window close must record window target") |
| | | require(report.get("lastCloseWindowID") is not None, "confirmed window close must record target window") |
| | | require(report.get("lastCloseResult") == "requested", "debug window close service must return requested") |
| | | require(report.get("lastCloseWindowID") not in visible_window_ids, "confirmed window close must optimistically remove the stale card from Waterfall") |
| | | require(report.get("suppressedCloseTargetCount") == 1, "confirmed window close must expose one suppressed close target") |
| | | require(report.get("lastCloseWindowID") in visible_window_ids, "requested-but-still-present window close must keep the card visible") |
| | | require(report.get("suppressedCloseTargetCount") == 0, "requested-but-still-present window close must not suppress the card") |
| | | require(root.get("closeConfirmationVisible") is False, "confirmed window close must hide confirmation") |
| | | require(root.get("closeFeedbackVisible") is True, "confirmed window close must show failure feedback when target remains") |
| | | require(root.get("closeFeedbackKind") == "failure", "confirmed window close feedback must be failure") |
| | |
| | | "lastCloseTargetKind": report.get("lastCloseTargetKind"), |
| | | "lastCloseResult": report.get("lastCloseResult"), |
| | | "lastDismissReason": report.get("lastDismissReason"), |
| | | "stillVisible": report.get("lastCloseWindowID") in visible_window_ids, |
| | | "suppressedCloseTargetCount": report.get("suppressedCloseTargetCount"), |
| | | "closeFeedbackKind": root.get("closeFeedbackKind"), |
| | | "closeFeedbackMessage": root.get("closeFeedbackMessage") |
| | | }, indent=2, ensure_ascii=False)) |
| | |
| | | require(report.get("quickSwitchVisible") is True, "opt-out confirmed close must keep Quick Switch visible") |
| | | require(report.get("lastCloseTargetKind") == "window", "opt-out confirmed close must record window target") |
| | | require(report.get("lastCloseResult") == "requested", "opt-out confirmed close must request close") |
| | | require(report.get("lastCloseWindowID") not in visible_window_ids, "opt-out confirmed close must optimistically remove the stale card") |
| | | require(report.get("suppressedCloseTargetCount") == 1, "opt-out confirmed close must expose one suppressed close target") |
| | | require(report.get("lastCloseWindowID") in visible_window_ids, "opt-out requested-but-still-present close must keep the card visible") |
| | | require(report.get("suppressedCloseTargetCount") == 0, "opt-out requested-but-still-present close must not suppress the card") |
| | | require(root.get("closeConfirmationVisible") is False, "opt-out confirmed close must hide confirmation") |
| | | require(root.get("pendingCloseOptOut") is False, "opt-out confirmed close must clear temporary checkbox state") |
| | | require(root.get("closeFeedbackKind") == "failure", "opt-out confirmed close must still verify close result") |
| | |
| | | "case": "cardOptOutConfirmed", |
| | | "lastCloseTargetKind": report.get("lastCloseTargetKind"), |
| | | "lastCloseResult": report.get("lastCloseResult"), |
| | | "stillVisible": report.get("lastCloseWindowID") in visible_window_ids, |
| | | "suppressedCloseTargetCount": report.get("suppressedCloseTargetCount"), |
| | | "pendingCloseOptOut": root.get("pendingCloseOptOut"), |
| | | "closeConfirmationVisible": root.get("closeConfirmationVisible") |
| | | }, indent=2, ensure_ascii=False)) |
| | |
| | | require(report.get("quickSwitchVisible") is True, "second close after opt-out must keep Quick Switch visible") |
| | | require(report.get("lastCloseTargetKind") == "window", "second close after opt-out must directly request window close") |
| | | require(report.get("lastCloseResult") == "requested", "second close after opt-out must call close service") |
| | | require(report.get("lastCloseWindowID") not in visible_window_ids, "second close after opt-out must optimistically remove the stale card") |
| | | require(report.get("suppressedCloseTargetCount") == 1, "second close after opt-out must expose one suppressed close target") |
| | | require(report.get("lastCloseWindowID") in visible_window_ids, "second requested-but-still-present close must keep the card visible") |
| | | require(report.get("suppressedCloseTargetCount") == 0, "second requested-but-still-present close must not suppress the card") |
| | | require(root.get("pendingCloseTargetKind") is None, "second close after opt-out must not create pending close target") |
| | | require(root.get("closeConfirmationVisible") is False, "second close after opt-out must not show confirmation") |
| | | require(root.get("closeFeedbackKind") == "failure", "second close after opt-out must still verify close result") |
| | |
| | | "case": "cardOptOutSecondClose", |
| | | "lastCloseTargetKind": report.get("lastCloseTargetKind"), |
| | | "lastCloseResult": report.get("lastCloseResult"), |
| | | "stillVisible": report.get("lastCloseWindowID") in visible_window_ids, |
| | | "suppressedCloseTargetCount": report.get("suppressedCloseTargetCount"), |
| | | "pendingCloseTargetKind": root.get("pendingCloseTargetKind"), |
| | | "closeConfirmationVisible": root.get("closeConfirmationVisible") |
| | | }, indent=2, ensure_ascii=False)) |
| | |
| | | require(report.get("lastCloseTargetKind") == "app", "confirmed App close must record app target") |
| | | require(report.get("lastCloseAppGroupIndex") == 1, "confirmed App close must record target app group") |
| | | require(report.get("lastCloseResult") == "requested", "debug App close service must return requested") |
| | | require(report.get("suppressedCloseTargetCount") == 1, "confirmed App close must expose one suppressed close target") |
| | | require(1 not in [column.get("appGroupIndex") for column in root.get("waterfallColumns", [])], "confirmed App close must optimistically remove the target column") |
| | | require(report.get("suppressedCloseTargetCount") == 0, "requested-but-still-present App close must not suppress the column") |
| | | require(1 in [column.get("appGroupIndex") for column in root.get("waterfallColumns", [])], "requested-but-still-present App close must keep the target column visible") |
| | | require(root.get("closeConfirmationVisible") is False, "confirmed App close must hide confirmation") |
| | | require(root.get("closeFeedbackVisible") is True, "confirmed App close must show failure feedback when target remains") |
| | | require(root.get("closeFeedbackKind") == "failure", "confirmed App close feedback must be failure") |
| | |
| | | "lastCloseTargetKind": report.get("lastCloseTargetKind"), |
| | | "lastCloseResult": report.get("lastCloseResult"), |
| | | "lastDismissReason": report.get("lastDismissReason"), |
| | | "targetColumnStillVisible": 1 in [column.get("appGroupIndex") for column in root.get("waterfallColumns", [])], |
| | | "suppressedCloseTargetCount": report.get("suppressedCloseTargetCount"), |
| | | "closeFeedbackKind": root.get("closeFeedbackKind"), |
| | | "closeFeedbackMessage": root.get("closeFeedbackMessage") |
| | | }, indent=2, ensure_ascii=False)) |
| | |
| | | print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr) |
| | | sys.exit(1) |
| | | |
| | | def is_locked_fill(color): |
| | | return ( |
| | | color.get("blue", 0) >= 0.85 |
| | | and color.get("green", 0) >= 0.55 |
| | | and color.get("red", 1) <= 0.58 |
| | | and color.get("alpha", 0) >= 0.70 |
| | | ) |
| | | |
| | | def is_white(color): |
| | | return ( |
| | | color.get("red", 0) >= 0.94 |
| | | and color.get("green", 0) >= 0.94 |
| | | and color.get("blue", 0) >= 0.94 |
| | | and color.get("alpha", 0) >= 0.88 |
| | | ) |
| | | |
| | | root = report.get("rootView", {}) |
| | | items = root.get("appShelfItems", []) |
| | | columns = root.get("waterfallColumns", []) |
| | |
| | | require(segment_by_id[4].get("windowCount") == 0, "B1 must remain visible as empty Space") |
| | | require(segment_by_id[5].get("windowCount") == 1, "B2 fullscreen Space must remain visible") |
| | | require("locked" in segment_by_id[1].get("visualStates", []), "locked Space must expose locked visual state") |
| | | require(segment_by_id[1].get("borderWidth", 0) >= 5.5, "locked Space must use a heavy blue border") |
| | | require(segment_by_id[1].get("borderWidth", 99) <= 2.0, "locked Space must not use the old heavy border") |
| | | require(segment_by_id[1].get("backgroundKind") == "solid", "locked Space must use a solid light-blue fill") |
| | | require(is_locked_fill(segment_by_id[1].get("backgroundColor", {})), "locked Space fill must be clean light blue") |
| | | require(is_white(segment_by_id[1].get("labelColor", {})), "locked Space label text must be pure white") |
| | | require(is_white(segment_by_id[1].get("countColor", {})), "locked Space count text must be pure white") |
| | | require(is_white(segment_by_id[1].get("appColor", {})), "locked Space app text must be pure white") |
| | | require(all(is_white(color) for color in segment_by_id[1].get("windowBlockColors", [])), "locked Space window blocks must be pure white") |
| | | lock_shadow = segment_by_id[1].get("shadowColor", {}) |
| | | require(lock_shadow.get("red", 0) >= 0.90 and lock_shadow.get("green", 0) >= 0.90 and lock_shadow.get("blue", 0) >= 0.90, "locked Space must use a white glow") |
| | | require(segment_by_id[1].get("shadowOpacity", 0) >= 0.60, "locked Space glow must be visibly stronger than hover") |
| | |
| | | "cardWindowIDs": [card.get("windowID") for card in cards], |
| | | "lockedSpace": root.get("spaceFilterLockedSpaceID"), |
| | | "lockedBorderWidth": segment_by_id[1].get("borderWidth"), |
| | | "lockedBackgroundColor": segment_by_id[1].get("backgroundColor"), |
| | | "lockedLabelColor": segment_by_id[1].get("labelColor"), |
| | | "lockedWindowBlockColors": segment_by_id[1].get("windowBlockColors"), |
| | | "lockedShadowOpacity": segment_by_id[1].get("shadowOpacity"), |
| | | "lockedShadowRadius": segment_by_id[1].get("shadowRadius"), |
| | | "waterfallLeftBlankWidth": left_blank_width, |
| | |
| | | print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr) |
| | | sys.exit(1) |
| | | |
| | | def is_locked_fill(color): |
| | | return ( |
| | | color.get("blue", 0) >= 0.85 |
| | | and color.get("green", 0) >= 0.55 |
| | | and color.get("red", 1) <= 0.58 |
| | | and color.get("alpha", 0) >= 0.70 |
| | | ) |
| | | |
| | | def is_white(color): |
| | | return ( |
| | | color.get("red", 0) >= 0.94 |
| | | and color.get("green", 0) >= 0.94 |
| | | and color.get("blue", 0) >= 0.94 |
| | | and color.get("alpha", 0) >= 0.88 |
| | | ) |
| | | |
| | | root = report.get("rootView", {}) |
| | | segments = root.get("spaceLaneSegments", []) |
| | | segment_by_id = {segment.get("spaceID"): segment for segment in segments} |
| | |
| | | require(root.get("waterfallColumnCount") == 0, "root Waterfall must be empty") |
| | | require(root.get("spaceLaneLabels") == ["A1", "A2", "A3", "B1", "B2"], "Space Lane must stay global while empty Space is locked") |
| | | require("locked" in segment_by_id[3].get("visualStates", []), "empty locked Space must expose locked visual state") |
| | | require(segment_by_id[3].get("borderWidth", 0) >= 5.5, "empty locked Space must still use a heavy locked border") |
| | | require(segment_by_id[3].get("borderWidth", 99) <= 2.0, "empty locked Space must not use the old heavy border") |
| | | require(segment_by_id[3].get("backgroundKind") == "solid", "empty locked Space must use the same light-blue fill") |
| | | require(is_locked_fill(segment_by_id[3].get("backgroundColor", {})), "empty locked Space fill must be clean light blue") |
| | | require(is_white(segment_by_id[3].get("labelColor", {})), "empty locked Space label text must be pure white") |
| | | require(is_white(segment_by_id[3].get("countColor", {})), "empty locked Space count text must be pure white") |
| | | require(is_white(segment_by_id[3].get("appColor", {})), "empty locked Space app text must be pure white") |
| | | empty_shadow = segment_by_id[3].get("shadowColor", {}) |
| | | require(empty_shadow.get("red", 0) >= 0.90 and empty_shadow.get("green", 0) >= 0.90 and empty_shadow.get("blue", 0) >= 0.90, "empty locked Space must use white glow") |
| | | require(segment_by_id[3].get("shadowOpacity", 0) >= 0.60, "empty locked Space glow must be visible") |
| | | require(segment_by_id[3].get("backgroundKind") == "clear", "empty locked Space must not use gray/gradient fill") |
| | | require(segment_by_id[4].get("backgroundKind") == "clear", "empty sibling Space must not inherit gray/gradient fill") |
| | | |
| | | print(json.dumps({ |
| | |
| | | "lockedSpace": report.get("spaceFilterLockedSpaceID"), |
| | | "appCount": report.get("appCount"), |
| | | "columnCount": report.get("columnCount"), |
| | | "a3BorderWidth": segment_by_id[3].get("borderWidth"), |
| | | "a3BackgroundColor": segment_by_id[3].get("backgroundColor"), |
| | | "a3LabelColor": segment_by_id[3].get("labelColor"), |
| | | "a3BackgroundKind": segment_by_id[3].get("backgroundKind"), |
| | | "b1BackgroundKind": segment_by_id[4].get("backgroundKind") |
| | | }, indent=2, ensure_ascii=False)) |