-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Fix workspace color picker context menu blinking #2566
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ed3c36f
62d1189
3175f5e
0837d1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9887,6 +9887,13 @@ private final class SidebarTabItemSettingsStore: ObservableObject { | |
| } | ||
| } | ||
|
|
||
| private struct SidebarTabItemPresentationSnapshot: Equatable { | ||
| let tabId: UUID | ||
| let unreadCount: Int | ||
| let latestNotificationText: String? | ||
| let showsModifierShortcutHints: Bool | ||
| } | ||
|
|
||
| struct VerticalTabsSidebar: View { | ||
| @ObservedObject var updateViewModel: UpdateViewModel | ||
| let onSendFeedback: () -> Void | ||
|
|
@@ -9902,6 +9909,7 @@ struct VerticalTabsSidebar: View { | |
| @ObservedObject private var keyboardShortcutSettingsObserver = KeyboardShortcutSettingsObserver.shared | ||
| @State private var draggedTabId: UUID? | ||
| @State private var dropIndicator: SidebarDropIndicator? | ||
| @State private var frozenTabItemPresentation: SidebarTabItemPresentationSnapshot? | ||
| @AppStorage(WorkspacePresentationModeSettings.modeKey) | ||
| private var workspacePresentationMode = WorkspacePresentationModeSettings.defaultMode.rawValue | ||
|
|
||
|
|
@@ -9967,6 +9975,26 @@ struct VerticalTabsSidebar: View { | |
| let allRemoteContextMenuTargetsDisconnected = usesSelectedContextMenuTargets | ||
| ? allSelectedRemoteContextMenuTargetsDisconnected | ||
| : (tab.isRemoteWorkspace && tab.remoteConnectionState == .disconnected) | ||
| let liveUnreadCount = notificationStore.unreadCount(forTabId: tab.id) | ||
| let liveLatestNotificationText: String? = { | ||
| guard showsSidebarNotificationMessage, | ||
| let notification = notificationStore.latestNotification(forTabId: tab.id) else { | ||
| return nil | ||
| } | ||
| let text = notification.body.isEmpty ? notification.title : notification.body | ||
| let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| return trimmed.isEmpty ? nil : trimmed | ||
| }() | ||
| let liveShowsModifierShortcutHints = modifierKeyMonitor.isModifierPressed | ||
| let livePresentation = SidebarTabItemPresentationSnapshot( | ||
| tabId: tab.id, | ||
| unreadCount: liveUnreadCount, | ||
| latestNotificationText: liveLatestNotificationText, | ||
| showsModifierShortcutHints: liveShowsModifierShortcutHints | ||
| ) | ||
| let frozenPresentation = frozenTabItemPresentation?.tabId == tab.id | ||
| ? frozenTabItemPresentation | ||
| : nil | ||
| TabItemView( | ||
| tabManager: tabManager, | ||
| notificationStore: notificationStore, | ||
|
|
@@ -9980,29 +10008,23 @@ struct VerticalTabsSidebar: View { | |
| workspaceShortcutModifierSymbol: workspaceNumberShortcut.numberedDigitHintPrefix, | ||
| canCloseWorkspace: canCloseWorkspace, | ||
| accessibilityWorkspaceCount: workspaceCount, | ||
| unreadCount: notificationStore.unreadCount(forTabId: tab.id), | ||
| latestNotificationText: { | ||
| guard showsSidebarNotificationMessage, | ||
| let notification = notificationStore.latestNotification(forTabId: tab.id) else { | ||
| return nil | ||
| } | ||
| let text = notification.body.isEmpty ? notification.title : notification.body | ||
| let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| return trimmed.isEmpty ? nil : trimmed | ||
| }(), | ||
| unreadCount: frozenPresentation?.unreadCount ?? liveUnreadCount, | ||
| latestNotificationText: frozenPresentation?.latestNotificationText ?? liveLatestNotificationText, | ||
| rowSpacing: tabRowSpacing, | ||
| setSelectionToTabs: { selection = .tabs }, | ||
| selectedTabIds: $selectedTabIds, | ||
| lastSidebarSelectionIndex: $lastSidebarSelectionIndex, | ||
| showsModifierShortcutHints: modifierKeyMonitor.isModifierPressed, | ||
| showsModifierShortcutHints: frozenPresentation?.showsModifierShortcutHints ?? liveShowsModifierShortcutHints, | ||
| dragAutoScrollController: dragAutoScrollController, | ||
| draggedTabId: $draggedTabId, | ||
| dropIndicator: $dropIndicator, | ||
| contextMenuWorkspaceIds: contextMenuWorkspaceIds, | ||
| remoteContextMenuWorkspaceIds: remoteContextMenuWorkspaceIds, | ||
| allRemoteContextMenuTargetsConnecting: allRemoteContextMenuTargetsConnecting, | ||
| allRemoteContextMenuTargetsDisconnected: allRemoteContextMenuTargetsDisconnected, | ||
| settings: tabItemSettings | ||
| settings: tabItemSettings, | ||
| livePresentation: livePresentation, | ||
| frozenPresentation: $frozenTabItemPresentation | ||
| ) | ||
| .equatable() | ||
| } | ||
|
|
@@ -10103,6 +10125,11 @@ struct VerticalTabsSidebar: View { | |
| dragAutoScrollController.stop() | ||
| dropIndicator = nil | ||
| } | ||
| .onChange(of: tabs.map(\.id)) { tabIds in | ||
| guard let frozenTabItemPresentation, | ||
| !tabIds.contains(frozenTabItemPresentation.tabId) else { return } | ||
| self.frozenTabItemPresentation = nil | ||
| } | ||
| .onReceive(NotificationCenter.default.publisher(for: SidebarDragLifecycleNotification.requestClear)) { notification in | ||
| guard draggedTabId != nil else { return } | ||
| let reason = SidebarDragLifecycleNotification.reason(from: notification) | ||
|
|
@@ -12377,6 +12404,11 @@ enum SidebarTrailingAccessoryWidthPolicy { | |
| // and bridge only sidebar-visible workspace changes into local state. | ||
| // Do NOT add @EnvironmentObject or new @Binding without updating ==. | ||
| // Do NOT remove .equatable() from the ForEach call site in VerticalTabsSidebar. | ||
| private final class SidebarTabItemContextMenuState: ObservableObject { | ||
| var isVisible = false | ||
| var hasDeferredWorkspaceObservationInvalidation = false | ||
| } | ||
|
|
||
| private struct TabItemView: View, Equatable { | ||
| private static let workspaceObservationCoalesceInterval: RunLoop.SchedulerTimeType.Stride = .milliseconds(40) | ||
|
|
||
|
|
@@ -12429,7 +12461,10 @@ private struct TabItemView: View, Equatable { | |
| let allRemoteContextMenuTargetsConnecting: Bool | ||
| let allRemoteContextMenuTargetsDisconnected: Bool | ||
| let settings: SidebarTabItemSettingsSnapshot | ||
| let livePresentation: SidebarTabItemPresentationSnapshot | ||
| @Binding var frozenPresentation: SidebarTabItemPresentationSnapshot? | ||
| @State private var workspaceObservationGeneration: UInt64 = 0 | ||
| @StateObject private var contextMenuState = SidebarTabItemContextMenuState() | ||
| @State private var isHovering = false | ||
| @State private var rowHeight: CGFloat = 1 | ||
|
|
||
|
|
@@ -13027,7 +13062,7 @@ private struct TabItemView: View, Equatable { | |
| "desc=\"\(debugCommandPaletteTextPreview(description))\"" | ||
| ) | ||
| #endif | ||
| workspaceObservationGeneration &+= 1 | ||
| scheduleWorkspaceObservationInvalidation() | ||
| } | ||
| .onReceive( | ||
| tab.sidebarObservationPublisher | ||
|
|
@@ -13047,7 +13082,7 @@ private struct TabItemView: View, Equatable { | |
| "desc=\"\(debugCommandPaletteTextPreview(description))\"" | ||
| ) | ||
| #endif | ||
| workspaceObservationGeneration &+= 1 | ||
| scheduleWorkspaceObservationInvalidation() | ||
| } | ||
| .onDrag { | ||
| #if DEBUG | ||
|
|
@@ -13078,6 +13113,7 @@ private struct TabItemView: View, Equatable { | |
| updateSelection() | ||
| } | ||
| .onHover { hovering in | ||
| guard !contextMenuState.isVisible else { return } | ||
| isHovering = hovering | ||
| } | ||
| .accessibilityElement(children: .combine) | ||
|
|
@@ -13089,7 +13125,37 @@ private struct TabItemView: View, Equatable { | |
| .accessibilityAction(named: Text(moveDownActionText)) { | ||
| moveBy(1) | ||
| } | ||
| .contextMenu { workspaceContextMenu } | ||
| .contextMenu { | ||
| workspaceContextMenu | ||
| .onAppear { | ||
| contextMenuState.isVisible = true | ||
| contextMenuState.hasDeferredWorkspaceObservationInvalidation = false | ||
| frozenPresentation = livePresentation | ||
| } | ||
| .onDisappear { | ||
| contextMenuState.isVisible = false | ||
| frozenPresentation = nil | ||
| if isHovering { | ||
| isHovering = false | ||
| } | ||
| flushDeferredWorkspaceObservationInvalidation() | ||
| } | ||
| } | ||
|
Comment on lines
+13128
to
+13143
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In that stuck state, every subsequent call to Consider adding a safety reset — for example, observing a SwiftUI
Comment on lines
+13128
to
+13143
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In practice this works on recent macOS releases, but it may be worth wrapping the content in an explicit .contextMenu {
Group {
workspaceContextMenu
}
.onAppear {
contextMenuVisible = true
}
.onDisappear {
contextMenuVisible = false
flushDeferredWorkspaceObservationInvalidation()
}
}This makes the attachment point unambiguous across SwiftUI versions. Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| } | ||
|
|
||
| private func scheduleWorkspaceObservationInvalidation() { | ||
| // Keep the context menu stable while background workspace telemetry keeps arriving. | ||
| if contextMenuState.isVisible { | ||
| contextMenuState.hasDeferredWorkspaceObservationInvalidation = true | ||
| return | ||
| } | ||
| workspaceObservationGeneration &+= 1 | ||
| } | ||
|
|
||
| private func flushDeferredWorkspaceObservationInvalidation() { | ||
| guard contextMenuState.hasDeferredWorkspaceObservationInvalidation else { return } | ||
| contextMenuState.hasDeferredWorkspaceObservationInvalidation = false | ||
| workspaceObservationGeneration &+= 1 | ||
| } | ||
|
|
||
| private func contextMenuLabel(multi: String, single: String, isMulti: Bool) -> String { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also clear the frozen snapshot when any frozen target disappears.
Right now this only checks the anchor
tabId. If the menu was opened on a multi-selection and one ofcontextMenuWorkspaceIds/remoteContextMenuWorkspaceIdsis removed while the menu is open, the frozen menu can keep stale target IDs and stale plural actions until dismiss.Suggested fix
🤖 Prompt for AI Agents