chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,591 @@
|
||||
import AppKit
|
||||
import CodexBarCore
|
||||
|
||||
extension StatusItemController {
|
||||
func beginMenuTrackingSession(for menu: NSMenu) {
|
||||
if menu.supermenu != nil, !self.isHostedSubviewMenu(menu) {
|
||||
self.advanceMenuInteraction(for: self.rootMenu(for: menu))
|
||||
}
|
||||
let menuID = ObjectIdentifier(menu)
|
||||
let generation = self.menuSession.beginTrackingSession(menuID)
|
||||
(menu as? StatusItemMenu)?.menuInteractionGeneration = generation
|
||||
}
|
||||
|
||||
func endMenuTrackingSession(for menu: NSMenu) {
|
||||
(menu as? StatusItemMenu)?.menuInteractionGeneration = nil
|
||||
self.menuSession.endTrackingSession(ObjectIdentifier(menu))
|
||||
}
|
||||
|
||||
private func rootMenu(for menu: NSMenu) -> NSMenu {
|
||||
var root = menu
|
||||
while let parent = root.supermenu {
|
||||
root = parent
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
private static let defaultClosedMenuPreparationDelay: Duration = .milliseconds(350)
|
||||
|
||||
var isMenuRefreshEnabled: Bool {
|
||||
#if DEBUG
|
||||
if let menuRefreshEnabledOverrideForTesting {
|
||||
return menuRefreshEnabledOverrideForTesting
|
||||
}
|
||||
#endif
|
||||
return self.menuRefreshEnabledForController
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static var closedMenuPreparationDelayForTesting: Duration = defaultClosedMenuPreparationDelay
|
||||
static func setClosedMenuPreparationDelayForTesting(_ delay: Duration) {
|
||||
self.closedMenuPreparationDelayForTesting = delay
|
||||
}
|
||||
|
||||
static func resetClosedMenuPreparationDelayForTesting() {
|
||||
self.closedMenuPreparationDelayForTesting = self.defaultClosedMenuPreparationDelay
|
||||
}
|
||||
#endif
|
||||
|
||||
private static var closedMenuPreparationDelay: Duration {
|
||||
#if DEBUG
|
||||
closedMenuPreparationDelayForTesting
|
||||
#else
|
||||
defaultClosedMenuPreparationDelay
|
||||
#endif
|
||||
}
|
||||
|
||||
func invalidateMenus(
|
||||
refreshOpenMenus: Bool = false,
|
||||
deferOpenParentMenuRebuild: Bool = false,
|
||||
allowStaleContentDuringDataRefresh: Bool = false)
|
||||
{
|
||||
#if DEBUG
|
||||
guard !self.isReleasedForTesting else { return }
|
||||
#endif
|
||||
let preservesMergedSwitcherContentCaches = self.preservesMergedSwitcherContentCachesDuringInvalidation
|
||||
self.menuSession.invalidate(
|
||||
allowsStaleContent: allowStaleContentDuringDataRefresh,
|
||||
requiresRebuild: !preservesMergedSwitcherContentCaches)
|
||||
if !preservesMergedSwitcherContentCaches {
|
||||
self.clearMergedSwitcherContentCaches()
|
||||
}
|
||||
self.pruneVersionScopedMenuCardHeightCache()
|
||||
guard self.isMenuRefreshEnabled else { return }
|
||||
if !self.openMenus.isEmpty {
|
||||
guard refreshOpenMenus else { return }
|
||||
self.refreshOpenMenusAllowingParentRebuild(
|
||||
deferParentRebuildDuringTracking: deferOpenParentMenuRebuild)
|
||||
self.scheduleOpenMenuInvalidationRetry(
|
||||
deferParentRebuildDuringTracking: deferOpenParentMenuRebuild)
|
||||
return
|
||||
}
|
||||
if allowStaleContentDuringDataRefresh {
|
||||
if !self.cancelNonRequiredClosedMenuPreparation() {
|
||||
self.prepareAttachedClosedMenusIfNeeded()
|
||||
}
|
||||
return
|
||||
}
|
||||
self.prepareAttachedClosedMenusIfNeeded()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func cancelNonRequiredClosedMenuPreparation() -> Bool {
|
||||
let menus = self.attachedMenusForClosedPreparation()
|
||||
let menuIDs = menus.map(ObjectIdentifier.init)
|
||||
guard !self.menuSession.hasRequiredClosedPreparation(for: menuIDs) else { return false }
|
||||
self.cancelAllClosedMenuRebuilds()
|
||||
for menuID in menuIDs {
|
||||
self.menuSession.clearNextOpenDeferral(menuID)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func prepareAttachedClosedMenusIfNeeded() {
|
||||
guard self.isMenuRefreshEnabled else { return }
|
||||
guard self.openMenus.isEmpty else { return }
|
||||
guard !self.isMenuDataRefreshInFlight else { return }
|
||||
let menus = self.attachedMenusForClosedPreparation()
|
||||
let preparationPlan = self.menuSession.closedPreparationPlan(
|
||||
for: menus.lazy.map(ObjectIdentifier.init))
|
||||
guard preparationPlan != .none else { return }
|
||||
for menu in menus {
|
||||
let key = ObjectIdentifier(menu)
|
||||
switch preparationPlan {
|
||||
case .none:
|
||||
return
|
||||
case .nonDeferred:
|
||||
guard !self.menuSession.isDeferredUntilNextOpen(key) else { continue }
|
||||
case let .required(requiredVersion):
|
||||
self.menuSession.clearNextOpenDeferral(key)
|
||||
guard self.menuSession.isRenderedVersion(key, olderThan: requiredVersion) else { continue }
|
||||
}
|
||||
// Pre-warming the merged menu while it is closed runs a full main-thread populateMenu
|
||||
// (incl. SwiftUI hosting-view layout) that menuWillOpen redoes synchronously on display
|
||||
// anyway. In Merge Icons mode it is the only attached menu, so this just relocates that
|
||||
// work into a background freeze on every store tick (#1274). Defer it until next open.
|
||||
if menu === self.mergedMenu {
|
||||
self.menuSession.deferUntilNextOpen(key)
|
||||
continue
|
||||
}
|
||||
self.rebuildClosedMenuIfNeeded(menu)
|
||||
}
|
||||
}
|
||||
|
||||
var isMenuDataRefreshInFlight: Bool {
|
||||
self.store.isRefreshing ||
|
||||
!self.manualRefreshTasks.isEmpty ||
|
||||
!self.store.refreshingProviders.isEmpty ||
|
||||
UsageProvider.allCases.contains { self.store.isTokenRefreshInFlight(for: $0) }
|
||||
}
|
||||
|
||||
func removeMenuTrackingState(_ key: ObjectIdentifier) {
|
||||
self.menuProviders.removeValue(forKey: key)
|
||||
self.menuSession.removeMenu(key)
|
||||
self.menuReadinessSignatures.removeValue(forKey: key)
|
||||
self.menuIdentitySignatures.removeValue(forKey: key)
|
||||
}
|
||||
|
||||
func cancelMenuWork(_ key: ObjectIdentifier) {
|
||||
self.menuRefreshTasks.removeValue(forKey: key)?.cancel()
|
||||
self.closedMenuRebuildTasks.removeValue(forKey: key)?.cancel()
|
||||
self.closedMenuRebuildRequests.cancel(for: key)
|
||||
self.openMenuRebuildTasks.removeValue(forKey: key)?.cancel()
|
||||
self.openMenuRebuildRequests.cancel(for: key)
|
||||
self.openMenuRebuildsClosingHostedSubviewMenus.remove(key)
|
||||
self.pendingMenuBaselineResyncs.remove(key)
|
||||
self.cancelManualRefreshViewportRestore(for: key)
|
||||
}
|
||||
|
||||
func clearMenuHighlight(_ key: ObjectIdentifier) {
|
||||
if let highlightedView = self.highlightedMenuItems.removeValue(forKey: key)?.view {
|
||||
(highlightedView as? MenuCardHighlighting)?.setHighlighted(false)
|
||||
}
|
||||
self.nativeHighlightDeferredMenuRebuilds.removeValue(forKey: key)
|
||||
}
|
||||
|
||||
func removeMenuLifecycleState(_ key: ObjectIdentifier) {
|
||||
self.openMenus.removeValue(forKey: key)
|
||||
self.cancelMenuWork(key)
|
||||
self.clearMenuHighlight(key)
|
||||
self.removeMenuTrackingState(key)
|
||||
}
|
||||
|
||||
func handleClosedPersistentMenuNeedingRefresh(_ menu: NSMenu) {
|
||||
if menu === self.mergedMenu {
|
||||
// Closing the merged menu is on the user's dismiss path. Leave stale content attached and let
|
||||
// menuWillOpen rebuild it, while other closed-menu invalidations can still prepare in the background.
|
||||
self.menuSession.deferUntilNextOpen(ObjectIdentifier(menu))
|
||||
} else {
|
||||
self.rebuildClosedMenuIfNeeded(menu)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshMenuForOpenIfNeeded(_ menu: NSMenu, provider: UsageProvider?) {
|
||||
self.menuSession.clearNextOpenDeferral(ObjectIdentifier(menu))
|
||||
guard self.menuNeedsRefresh(menu) else { return }
|
||||
if self.canPreserveStaleMenuContentForInstantOpen(menu) {
|
||||
#if DEBUG
|
||||
self.menuLogger.debug(
|
||||
"menu open kept existing content for instant render",
|
||||
metadata: [
|
||||
"items": "\(menu.items.count)",
|
||||
"provider": provider?.rawValue ?? "nil",
|
||||
"storeRefreshing": self.store.isRefreshing ? "1" : "0",
|
||||
])
|
||||
#endif
|
||||
if self.isMenuRefreshEnabled, !self.isMenuDataRefreshInFlight {
|
||||
self.scheduleOpenMenuRebuildIfStillVisible(
|
||||
menu,
|
||||
provider: provider,
|
||||
resyncReadinessBaselineAfterRebuild: self.openMenus.isEmpty)
|
||||
}
|
||||
return
|
||||
}
|
||||
self.populateMenu(menu, provider: provider)
|
||||
self.markMenuFresh(menu)
|
||||
}
|
||||
|
||||
private func canPreserveStaleMenuContentForInstantOpen(_ menu: NSMenu) -> Bool {
|
||||
guard !menu.items.isEmpty else { return false }
|
||||
let key = ObjectIdentifier(menu)
|
||||
return self.menuSession.canPreserveStaleContent(for: key) &&
|
||||
self.menuIdentitySignatures[key] == self.menuIdentitySignature(
|
||||
for: self.renderedProviders(for: menu))
|
||||
}
|
||||
|
||||
private func attachedMenusForClosedPreparation() -> [NSMenu] {
|
||||
var menus: [NSMenu] = []
|
||||
var seen = Set<ObjectIdentifier>()
|
||||
|
||||
func append(_ menu: NSMenu?) {
|
||||
guard let menu else { return }
|
||||
let key = ObjectIdentifier(menu)
|
||||
guard seen.insert(key).inserted else { return }
|
||||
menus.append(menu)
|
||||
}
|
||||
|
||||
append(self.statusItem.menu)
|
||||
append(self.mergedMenu)
|
||||
append(self.fallbackMenu)
|
||||
for item in self.statusItems.values {
|
||||
append(item.menu)
|
||||
}
|
||||
for menu in self.providerMenus.values {
|
||||
append(menu)
|
||||
}
|
||||
return menus
|
||||
}
|
||||
|
||||
func renderedMenuWidth(for menu: NSMenu) -> CGFloat {
|
||||
let menuKey = ObjectIdentifier(menu)
|
||||
let trackedWindowWidth: CGFloat? = if self.openMenus[menuKey] != nil {
|
||||
menu.items.lazy.compactMap { item -> CGFloat? in
|
||||
guard let window = item.view?.window else { return nil }
|
||||
let contentWidth = window.contentLayoutRect.width
|
||||
return contentWidth > 0 ? contentWidth : window.frame.width
|
||||
}.first
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
return Self.resolvedRenderedMenuWidth(
|
||||
menuWidth: menu.size.width,
|
||||
trackedWindowWidth: trackedWindowWidth)
|
||||
}
|
||||
|
||||
static func resolvedRenderedMenuWidth(
|
||||
menuWidth: CGFloat,
|
||||
trackedWindowWidth: CGFloat?) -> CGFloat
|
||||
{
|
||||
max(
|
||||
ceil(menuWidth),
|
||||
ceil(trackedWindowWidth ?? 0),
|
||||
menuCardBaseWidth)
|
||||
}
|
||||
|
||||
func rebuildClosedMenuIfNeeded(_ menu: NSMenu) {
|
||||
guard !self.hasPreparedForAppShutdown else { return }
|
||||
guard !self.isMenuDataRefreshInFlight else { return }
|
||||
let key = ObjectIdentifier(menu)
|
||||
let provider = self.menuProvider(for: menu)
|
||||
let rebuildToken = self.closedMenuRebuildRequests.replaceRequest(for: key)
|
||||
self.closedMenuRebuildTasks[key]?.cancel()
|
||||
self.closedMenuRebuildTasks[key] = Task { @MainActor [weak self, weak menu] in
|
||||
let delay = Self.closedMenuPreparationDelay
|
||||
if delay > .zero {
|
||||
try? await Task.sleep(for: delay)
|
||||
}
|
||||
guard !Task.isCancelled else { return }
|
||||
await Task.yield()
|
||||
guard !Task.isCancelled else { return }
|
||||
guard let self else { return }
|
||||
defer {
|
||||
if self.closedMenuRebuildRequests.finish(rebuildToken, for: key) {
|
||||
self.closedMenuRebuildTasks.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
guard let menu else { return }
|
||||
guard self.closedMenuRebuildRequests.isCurrent(rebuildToken, for: key) else { return }
|
||||
guard !self.hasPreparedForAppShutdown else { return }
|
||||
guard !self.isMenuDataRefreshInFlight else { return }
|
||||
// A delayed prewarm for one menu must never populate while another menu is tracking.
|
||||
guard self.openMenus.isEmpty else { return }
|
||||
guard self.menuNeedsRefresh(menu) else { return }
|
||||
self.populateMenu(menu, provider: provider)
|
||||
self.markMenuFresh(menu)
|
||||
#if DEBUG
|
||||
if self.lastLoggedClosedMenuRebuildVersion != self.menuSession.contentVersion {
|
||||
self.lastLoggedClosedMenuRebuildVersion = self.menuSession.contentVersion
|
||||
self.menuLogger.debug(
|
||||
"closed menu rebuild completed",
|
||||
metadata: [
|
||||
"items": "\(menu.items.count)",
|
||||
"provider": provider?.rawValue ?? "nil",
|
||||
])
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func cancelClosedMenuRebuild(_ menu: NSMenu) {
|
||||
let key = ObjectIdentifier(menu)
|
||||
self.closedMenuRebuildTasks.removeValue(forKey: key)?.cancel()
|
||||
self.closedMenuRebuildRequests.cancel(for: key)
|
||||
}
|
||||
|
||||
func cancelAllClosedMenuRebuilds() {
|
||||
for task in self.closedMenuRebuildTasks.values {
|
||||
task.cancel()
|
||||
}
|
||||
self.closedMenuRebuildTasks.removeAll(keepingCapacity: false)
|
||||
self.closedMenuRebuildRequests.cancelAll()
|
||||
}
|
||||
|
||||
func menuNeedsRefresh(_ menu: NSMenu) -> Bool {
|
||||
self.menuSession.needsRefresh(ObjectIdentifier(menu))
|
||||
}
|
||||
|
||||
func markMenuFresh(_ menu: NSMenu) {
|
||||
let key = ObjectIdentifier(menu)
|
||||
self.menuSession.markFresh(key)
|
||||
self.menuReadinessSignatures[key] = self.menuAdjunctReadinessSignature()
|
||||
self.menuIdentitySignatures[key] = self.menuIdentitySignature(
|
||||
for: self.renderedProviders(for: menu))
|
||||
}
|
||||
|
||||
private func menuIdentitySignature(for providers: [UsageProvider]) -> String {
|
||||
var parts: [String] = []
|
||||
for target in providers {
|
||||
parts.append(target.rawValue)
|
||||
parts.append(self.providerIdentitySignature(self.store.snapshot(for: target)?.identity(for: target)))
|
||||
|
||||
if target != .codex, self.store.metadata(for: target).usesAccountFallback {
|
||||
let account = self.store.accountInfo(for: target)
|
||||
parts.append(Self.menuIdentityField(account.email))
|
||||
parts.append(Self.menuIdentityField(account.plan))
|
||||
}
|
||||
|
||||
for accountSnapshot in self.store.accountSnapshots[target] ?? [] {
|
||||
parts.append(accountSnapshot.account.id.uuidString)
|
||||
parts.append(Self.menuIdentityField(accountSnapshot.account.label))
|
||||
parts.append(self.providerIdentitySignature(accountSnapshot.snapshot?.identity(for: target)))
|
||||
}
|
||||
|
||||
if target == .codex {
|
||||
parts.append(Self.menuIdentityField(self.account.email))
|
||||
parts.append(Self.menuIdentityField(self.account.plan))
|
||||
for account in self.settings.codexVisibleAccountProjectionForMenuDisplay?.visibleAccounts ?? [] {
|
||||
parts.append(Self.menuIdentityField(account.id))
|
||||
parts.append(Self.menuIdentityField(account.email))
|
||||
parts.append(Self.menuIdentityField(account.workspaceLabel))
|
||||
parts.append(account.isActive ? "active" : "inactive")
|
||||
parts.append(account.isLive ? "live" : "stored")
|
||||
}
|
||||
for accountSnapshot in self.store.codexAccountSnapshots {
|
||||
parts.append(Self.menuIdentityField(accountSnapshot.id))
|
||||
parts.append(self.providerIdentitySignature(accountSnapshot.snapshot?.identity(for: target)))
|
||||
}
|
||||
}
|
||||
|
||||
if target == .kilo {
|
||||
for scopeSnapshot in self.store.kiloScopeSnapshots {
|
||||
parts.append(Self.menuIdentityField(scopeSnapshot.id))
|
||||
parts.append(self.providerIdentitySignature(scopeSnapshot.snapshot?.identity(for: target)))
|
||||
}
|
||||
}
|
||||
|
||||
if target == .claude {
|
||||
parts.append(Self.menuIdentityField(self.store.claudeSwapLastError ?? ""))
|
||||
for accountSnapshot in self.store.claudeSwapAccountSnapshots {
|
||||
parts.append(Self.menuIdentityField(accountSnapshot.id.opaqueID))
|
||||
parts.append(accountSnapshot.isActive ? "active" : "inactive")
|
||||
parts.append(self.providerIdentitySignature(accountSnapshot.snapshot?.identity(for: target)))
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts.joined(separator: "|")
|
||||
}
|
||||
|
||||
private func providerIdentitySignature(_ identity: ProviderIdentitySnapshot?) -> String {
|
||||
[
|
||||
identity?.providerID?.rawValue ?? "",
|
||||
Self.menuIdentityField(identity?.accountEmail),
|
||||
Self.menuIdentityField(identity?.accountOrganization),
|
||||
Self.menuIdentityField(identity?.loginMethod),
|
||||
].joined(separator: ":")
|
||||
}
|
||||
|
||||
private static func menuIdentityField(_ value: String?) -> String {
|
||||
let value = value ?? ""
|
||||
return "\(value.utf8.count):\(value)"
|
||||
}
|
||||
|
||||
func hasOpenHostedSubviewMenu() -> Bool {
|
||||
self.openMenus.values.contains { self.isHostedSubviewMenu($0) }
|
||||
}
|
||||
|
||||
func hasOpenNonHostedChildMenu() -> Bool {
|
||||
self.openMenus.values.contains { $0.supermenu != nil && !self.isHostedSubviewMenu($0) }
|
||||
}
|
||||
|
||||
func refreshOpenMenuIfStillVisible(_ menu: NSMenu, provider: UsageProvider?) {
|
||||
let key = ObjectIdentifier(menu)
|
||||
guard self.openMenus[key] != nil else { return }
|
||||
if self.isHostedSubviewMenu(menu) {
|
||||
self.scheduleOpenMenuRebuildIfStillVisible(menu, provider: provider)
|
||||
return
|
||||
}
|
||||
self.invalidateMenus(
|
||||
refreshOpenMenus: true,
|
||||
deferOpenParentMenuRebuild: true,
|
||||
allowStaleContentDuringDataRefresh: true)
|
||||
}
|
||||
|
||||
func rebuildOpenMenuIfStillVisible(_ menu: NSMenu, provider: UsageProvider?) {
|
||||
let key = ObjectIdentifier(menu)
|
||||
guard self.openMenus[key] != nil else { return }
|
||||
let isHostedSubviewMenu = self.isHostedSubviewMenu(menu)
|
||||
guard isHostedSubviewMenu || !self.hasOpenHostedSubviewMenu() else { return }
|
||||
guard !self.isNativeMenuItemHighlighted(in: menu) else {
|
||||
self.nativeHighlightDeferredMenuRebuilds[key] = NativeHighlightDeferredMenuRebuild(provider: provider)
|
||||
return
|
||||
}
|
||||
self.nativeHighlightDeferredMenuRebuilds.removeValue(forKey: key)
|
||||
if isHostedSubviewMenu {
|
||||
self.refreshHostedSubviewMenu(menu)
|
||||
} else {
|
||||
self.populateMenu(menu, provider: provider)
|
||||
self.markMenuFresh(menu)
|
||||
self.menuSession.clearParentRebuildDeferral(key)
|
||||
self.applyIcon(phase: nil)
|
||||
self.scheduleDeferredManualRefreshViewportRestoreAfterRebuild(for: menu)
|
||||
}
|
||||
#if DEBUG
|
||||
self._test_openMenuRebuildObserver?(menu)
|
||||
#endif
|
||||
}
|
||||
|
||||
func isNativeMenuItemHighlighted(in menu: NSMenu) -> Bool {
|
||||
let key = ObjectIdentifier(menu)
|
||||
guard let item = self.highlightedMenuItems[key], item.menu === menu else { return false }
|
||||
return item.isEnabled && item.view == nil
|
||||
}
|
||||
|
||||
func resumeMenuRebuildDeferredForNativeHighlightIfNeeded(_ menu: NSMenu) {
|
||||
let key = ObjectIdentifier(menu)
|
||||
guard let deferredRebuild = self.nativeHighlightDeferredMenuRebuilds[key] else { return }
|
||||
guard self.openMenus[key] === menu else {
|
||||
self.nativeHighlightDeferredMenuRebuilds.removeValue(forKey: key)
|
||||
self.pendingMenuBaselineResyncs.remove(key)
|
||||
return
|
||||
}
|
||||
let isHostedSubviewMenu = self.isHostedSubviewMenu(menu)
|
||||
guard isHostedSubviewMenu || !self.hasOpenHostedSubviewMenu() else { return }
|
||||
self.nativeHighlightDeferredMenuRebuilds.removeValue(forKey: key)
|
||||
self.scheduleOpenMenuRebuildIfStillVisible(
|
||||
menu,
|
||||
provider: deferredRebuild.provider)
|
||||
}
|
||||
|
||||
func refreshOpenMenusIfNeeded() {
|
||||
guard self.isMenuRefreshEnabled else { return }
|
||||
guard !self.openMenus.isEmpty else { return }
|
||||
self.refreshOpenMenusIfNeeded(allowsParentRebuild: false)
|
||||
}
|
||||
|
||||
func refreshOpenMenusForStructureChange() {
|
||||
self.refreshOpenMenusAllowingParentRebuild()
|
||||
}
|
||||
|
||||
func refreshOpenMenusAfterHostedSubviewClose() {
|
||||
guard self.isMenuRefreshEnabled else { return }
|
||||
guard !self.openMenus.isEmpty else { return }
|
||||
if self.isMenuDataRefreshInFlight {
|
||||
self.parentMenuRebuildPendingAfterHostedSubviewClose = true
|
||||
return
|
||||
}
|
||||
self.parentMenuRebuildPendingAfterHostedSubviewClose = false
|
||||
self.refreshOpenMenusIfNeeded(allowsParentRebuild: true)
|
||||
self.resumeParentMenuRebuildsDeferredForNativeHighlightAfterHostedSubviewClose()
|
||||
}
|
||||
|
||||
private func resumeParentMenuRebuildsDeferredForNativeHighlightAfterHostedSubviewClose() {
|
||||
guard !self.hasOpenHostedSubviewMenu() else { return }
|
||||
let deferredParents = self.openMenus.values.filter { menu in
|
||||
let key = ObjectIdentifier(menu)
|
||||
return !self.isHostedSubviewMenu(menu) &&
|
||||
self.nativeHighlightDeferredMenuRebuilds[key] != nil
|
||||
}
|
||||
// Schedule the saved explicit request after the generic dirty-menu pass, even when the native
|
||||
// highlight is still active. The scheduled rebuild will defer again, preserving its provider.
|
||||
for menu in deferredParents {
|
||||
self.resumeMenuRebuildDeferredForNativeHighlightIfNeeded(menu)
|
||||
}
|
||||
}
|
||||
|
||||
func completeParentMenuRebuildAfterHostedSubviewCloseIfNeeded() {
|
||||
guard self.parentMenuRebuildPendingAfterHostedSubviewClose else { return }
|
||||
guard !self.isMenuDataRefreshInFlight else { return }
|
||||
guard !self.hasOpenHostedSubviewMenu() else { return }
|
||||
self.refreshOpenMenusAfterHostedSubviewClose()
|
||||
}
|
||||
|
||||
func refreshOpenMenusAllowingParentRebuild(deferParentRebuildDuringTracking: Bool = false) {
|
||||
guard self.isMenuRefreshEnabled else { return }
|
||||
guard !self.openMenus.isEmpty else { return }
|
||||
self.refreshOpenMenusIfNeeded(
|
||||
allowsParentRebuild: true,
|
||||
deferParentRebuildDuringTracking: deferParentRebuildDuringTracking)
|
||||
}
|
||||
|
||||
func scheduleOpenMenuInvalidationRetry(deferParentRebuildDuringTracking: Bool = false) {
|
||||
self.openMenuInvalidationRetryTask?.cancel()
|
||||
self.openMenuInvalidationRetryTask = Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
await Task.yield()
|
||||
guard !Task.isCancelled else { return }
|
||||
#if DEBUG
|
||||
self.onOpenMenuInvalidationRetryForTesting?()
|
||||
#endif
|
||||
self.refreshOpenMenusAllowingParentRebuild(
|
||||
deferParentRebuildDuringTracking: deferParentRebuildDuringTracking)
|
||||
self.openMenuInvalidationRetryTask = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshOpenMenusIfNeeded(
|
||||
allowsParentRebuild: Bool,
|
||||
deferParentRebuildDuringTracking: Bool = false,
|
||||
respectsParentRebuildDeferral: Bool = false)
|
||||
{
|
||||
var orphanedKeys: [ObjectIdentifier] = []
|
||||
let hasOpenHostedSubviewMenu = self.hasOpenHostedSubviewMenu()
|
||||
for (key, menu) in self.openMenus {
|
||||
guard key == ObjectIdentifier(menu) else {
|
||||
orphanedKeys.append(key)
|
||||
continue
|
||||
}
|
||||
self.refreshOpenMenuIfNeeded(
|
||||
menu,
|
||||
allowsParentRebuild: allowsParentRebuild,
|
||||
deferParentRebuildDuringTracking: deferParentRebuildDuringTracking,
|
||||
respectsParentRebuildDeferral: respectsParentRebuildDeferral,
|
||||
hasOpenHostedSubviewMenu: hasOpenHostedSubviewMenu)
|
||||
}
|
||||
self.removeOrphanedOpenMenuEntries(orphanedKeys)
|
||||
}
|
||||
|
||||
private func refreshOpenMenuIfNeeded(
|
||||
_ menu: NSMenu,
|
||||
allowsParentRebuild: Bool,
|
||||
deferParentRebuildDuringTracking: Bool,
|
||||
respectsParentRebuildDeferral: Bool,
|
||||
hasOpenHostedSubviewMenu: Bool)
|
||||
{
|
||||
if self.isHostedSubviewMenu(menu) {
|
||||
self.scheduleOpenMenuRebuildIfStillVisible(menu, provider: self.menuProvider(for: menu))
|
||||
return
|
||||
}
|
||||
guard allowsParentRebuild else { return }
|
||||
guard self.menuNeedsRefresh(menu) else { return }
|
||||
let key = ObjectIdentifier(menu)
|
||||
|
||||
if deferParentRebuildDuringTracking {
|
||||
self.menuSession.deferParentRebuild(key)
|
||||
return
|
||||
}
|
||||
if respectsParentRebuildDeferral, self.menuSession.isParentRebuildDeferred(key) {
|
||||
return
|
||||
}
|
||||
self.menuSession.clearParentRebuildDeferral(key)
|
||||
guard !hasOpenHostedSubviewMenu else { return }
|
||||
|
||||
let provider = self.menuProvider(for: menu)
|
||||
self.scheduleOpenMenuRebuildIfStillVisible(menu, provider: provider)
|
||||
}
|
||||
|
||||
private func removeOrphanedOpenMenuEntries(_ keys: [ObjectIdentifier]) {
|
||||
for key in keys {
|
||||
self.removeMenuLifecycleState(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user