@@ -80,7 +80,8 @@ class ThemedSplitView: NSSplitView {
8080 }
8181}
8282
83- @MainActor class MainWindowController : NSWindowController , SplitContainerDelegate , NSSplitViewDelegate , NSWindowDelegate {
83+ @MainActor class MainWindowController : NSWindowController , SplitContainerDelegate , NSSplitViewDelegate , NSWindowDelegate
84+ {
8485 let appState = AppState ( )
8586
8687 let toolbar = ToolbarView ( frame: . zero)
@@ -190,6 +191,11 @@ class ThemedSplitView: NSSplitView {
190191 set { coordinator? . sidebarSectionOrder = newValue }
191192 }
192193
194+ func cancelPendingSidebarStateSave( ) {
195+ sidebarStateSaveTimer? . invalidate ( )
196+ sidebarStateSaveTimer = nil
197+ }
198+
193199 func performWhileIgnoringSidebarLayoutSettingsRefresh< T> ( _ body: ( ) -> T ) -> T {
194200 sidebarSettingsRefreshSuppressionDepth += 1
195201 defer { sidebarSettingsRefreshSuppressionDepth -= 1 }
@@ -466,108 +472,105 @@ class ThemedSplitView: NSSplitView {
466472 settingsObserver = NotificationCenter . default. addObserver (
467473 forName: . settingsChanged, object: nil , queue: . main
468474 ) { [ weak self] notification in
469- guard let self, self . window? . isVisible == true else { return }
470- let topic = ( notification. userInfo ? [ " topic " ] as? String ) . flatMap ( SettingsTopic . init ( rawValue: ) )
471-
472- // Broadcast settings/theme changes to socket subscribers
473- if let t = topic {
474- BooSocketServer . shared. emitSettingsChanged ( topic: t. rawValue)
475- }
476-
477- // Theme changes: refresh chrome colors, pane backgrounds, sidebar, status bar
478- if topic == nil || topic == . theme {
479- MainActor . assumeIsolated { AppStore . shared. refreshTheme ( ) }
480- let theme = AppSettings . shared. theme
481- BooSocketServer . shared. emitThemeChanged ( name: theme. name, isDark: theme. isDark)
482- self . window? . backgroundColor = theme. chromeBg
483- self . window? . appearance = NSAppearance ( named: theme. isDark ? . darkAqua : . aqua)
484- self . sidebarContainer. layer? . backgroundColor = theme. sidebarBg. cgColor
485- self . splitContainer. layer? . backgroundColor = theme. background. nsColor. cgColor
486- self . mainSplitView. needsDisplay = true
487- self . toolbar. needsDisplay = true
488- self . statusBar. needsDisplay = true
489- for (_, pv) in self . paneViews {
490- pv. layer? . backgroundColor = theme. background. nsColor. cgColor
491- pv. needsLayout = true
492- pv. needsDisplay = true
493- }
494- // Rebuild plugin sidebar to pick up new theme
495- if let ctx = MainActor . assumeIsolated ( { self . pluginRegistry. lastContext } ) {
496- self . cachedDetailViews. removeAll ( )
497- self . rebuildSidebarTabs ( context: ctx)
475+ let topicString = notification. userInfo ? [ " topic " ] as? String
476+ MainActor . assumeIsolated {
477+ guard let self, self . window? . isVisible == true else { return }
478+ let topic = topicString. flatMap ( SettingsTopic . init ( rawValue: ) )
479+
480+ // Broadcast settings/theme changes to socket subscribers
481+ if let t = topic {
482+ BooSocketServer . shared. emitSettingsChanged ( topic: t. rawValue)
498483 }
499- }
500-
501- // Status bar changes
502- if topic == nil || topic == . statusBar {
503- self . statusBar. needsDisplay = true
504- }
505484
506- // Layout changes: sidebar/workspace bar position, density
507- if topic == nil || topic == . layout {
508- if self . isIgnoringSidebarLayoutSettingsRefresh {
509- return
510- }
511- self . statusBarHeightConstraint? . constant = DensityMetrics . current. statusBarHeight
512- self . statusBar. needsDisplay = true
513- let newSidebarPos = AppSettings . shared. sidebarPosition
514- if newSidebarPos != self . currentSidebarPosition {
515- self . currentSidebarPosition = newSidebarPos
516- MainActor . assumeIsolated {
517- self . sidebarController. position = newSidebarPos
518- }
519- self . rebuildSidebarLayout ( )
520- }
521- let newWsBarPos = AppSettings . shared. workspaceBarPosition
522- if newWsBarPos != self . currentWorkspaceBarPosition {
523- self . currentWorkspaceBarPosition = newWsBarPos
524- self . rebuildWorkspaceBarLayout ( )
525- }
526- let newTabOverflow = AppSettings . shared. tabOverflowMode
527- if newTabOverflow != self . currentTabOverflowMode {
528- self . currentTabOverflowMode = newTabOverflow
485+ // Theme changes: refresh chrome colors, pane backgrounds, sidebar, status bar
486+ if topic == nil || topic == . theme {
487+ AppStore . shared. refreshTheme ( )
488+ let theme = AppSettings . shared. theme
489+ BooSocketServer . shared. emitThemeChanged ( name: theme. name, isDark: theme. isDark)
490+ self . window? . backgroundColor = theme. chromeBg
491+ self . window? . appearance = NSAppearance ( named: theme. isDark ? . darkAqua : . aqua)
492+ self . sidebarContainer. layer? . backgroundColor = theme. sidebarBg. cgColor
493+ self . splitContainer. layer? . backgroundColor = theme. background. nsColor. cgColor
494+ self . mainSplitView. needsDisplay = true
495+ self . toolbar. needsDisplay = true
496+ self . statusBar. needsDisplay = true
529497 for (_, pv) in self . paneViews {
498+ pv. layer? . backgroundColor = theme. background. nsColor. cgColor
530499 pv. needsLayout = true
531500 pv. needsDisplay = true
532501 }
502+ // Rebuild plugin sidebar to pick up new theme
503+ if let ctx = self . pluginRegistry. lastContext {
504+ self . cachedDetailViews. removeAll ( )
505+ self . rebuildSidebarTabs ( context: ctx)
506+ }
533507 }
534- // Tab bar position changed — swap constraints and re-install content views
535- self . applySidebarTabBarPositionConstraints ( )
536- if let ctx = MainActor . assumeIsolated ( { self . pluginRegistry. lastContext } ) {
537- self . cachedDetailViews. removeAll ( )
538- self . removeAllPluginContent ( )
539- self . rebuildSidebarTabs ( context: ctx)
508+
509+ // Status bar changes
510+ if topic == nil || topic == . statusBar {
511+ self . statusBar. needsDisplay = true
540512 }
541- MainActor . assumeIsolated {
513+
514+ // Layout changes: sidebar/workspace bar position, density
515+ if topic == nil || topic == . layout {
516+ if self . isIgnoringSidebarLayoutSettingsRefresh {
517+ return
518+ }
519+ self . statusBarHeightConstraint? . constant = DensityMetrics . current. statusBarHeight
520+ self . statusBar. needsDisplay = true
521+ let newSidebarPos = AppSettings . shared. sidebarPosition
522+ if newSidebarPos != self . currentSidebarPosition {
523+ self . currentSidebarPosition = newSidebarPos
524+ self . sidebarController. position = newSidebarPos
525+ self . rebuildSidebarLayout ( )
526+ }
527+ let newWsBarPos = AppSettings . shared. workspaceBarPosition
528+ if newWsBarPos != self . currentWorkspaceBarPosition {
529+ self . currentWorkspaceBarPosition = newWsBarPos
530+ self . rebuildWorkspaceBarLayout ( )
531+ }
532+ let newTabOverflow = AppSettings . shared. tabOverflowMode
533+ if newTabOverflow != self . currentTabOverflowMode {
534+ self . currentTabOverflowMode = newTabOverflow
535+ for (_, pv) in self . paneViews {
536+ pv. needsLayout = true
537+ pv. needsDisplay = true
538+ }
539+ }
540+ // Tab bar position changed — swap constraints and re-install content views
541+ self . applySidebarTabBarPositionConstraints ( )
542+ if let ctx = self . pluginRegistry. lastContext {
543+ self . cachedDetailViews. removeAll ( )
544+ self . removeAllPluginContent ( )
545+ self . rebuildSidebarTabs ( context: ctx)
546+ }
542547 self . sidebarController. persistLiveState ( )
543548 self . sidebarController. applyRestoredState ( self . sidebarController. resolveEffectiveSidebarState ( ) )
544549 }
545- }
546550
547- // Plugin changes: deactivate any now-disabled plugins and run a fresh cycle.
548- if topic == . plugins {
549- let disabled = AppSettings . shared. disabledPluginIDsSet
550- // Deactivate plugins that just got disabled
551- MainActor . assumeIsolated {
551+ // Plugin changes: deactivate any now-disabled plugins and run a fresh cycle.
552+ if topic == . plugins {
553+ let disabled = AppSettings . shared. disabledPluginIDsSet
554+ // Deactivate plugins that just got disabled
552555 for plugin in self . pluginRegistry. plugins where disabled. contains ( plugin. pluginID) {
553556 self . pluginRegistry. deactivatePlugin ( plugin. pluginID)
554557 self . cachedDetailViews. removeValue ( forKey: plugin. pluginID)
555558 }
556559 self . pluginRegistry. clearChangeDetection ( )
557560 }
558- }
559561
560- // Explorer/plugin/font changes: refresh sidebar
561- if topic == nil || topic == . explorer || topic == . sidebarFont || topic == . plugins {
562- if topic == . sidebarFont, let ctx = MainActor . assumeIsolated ( { self . pluginRegistry. lastContext } ) {
563- if self . sidebarVisible {
564- self . cachedDetailViews. removeAll ( )
565- self . rebuildSidebarTabs ( context: ctx)
562+ // Explorer/plugin/font changes: refresh sidebar
563+ if topic == nil || topic == . explorer || topic == . sidebarFont || topic == . plugins {
564+ if topic == . sidebarFont, let ctx = self . pluginRegistry. lastContext {
565+ if self . sidebarVisible {
566+ self . cachedDetailViews. removeAll ( )
567+ self . rebuildSidebarTabs ( context: ctx)
568+ }
569+ } else {
570+ self . runPluginCycle ( reason: . focusChanged)
566571 }
567- } else {
568- self . runPluginCycle ( reason: . focusChanged)
569572 }
570- }
573+ } // end MainActor.assumeIsolated
571574 }
572575
573576 ghosttyActionObserver = NotificationCenter . default. addObserver (
@@ -588,7 +591,7 @@ class ThemedSplitView: NSSplitView {
588591 guard let self,
589592 let url = notification. userInfo ? [ " url " ] as? URL
590593 else { return }
591- self . handleOpenTab ( . browser( url: url) )
594+ MainActor . assumeIsolated { self . handleOpenTab ( . browser( url: url) ) }
592595 }
593596 }
594597
@@ -680,7 +683,7 @@ class ThemedSplitView: NSSplitView {
680683 }
681684 self . splitRatioSaveTimer? . invalidate ( )
682685 self . splitRatioSaveTimer = Timer . scheduledTimer ( withTimeInterval: 0.5 , repeats: false ) { [ weak self] _ in
683- self ? . saveSession ( )
686+ MainActor . assumeIsolated { self ? . saveSession ( ) }
684687 }
685688 }
686689
@@ -1164,7 +1167,7 @@ class ThemedSplitView: NSSplitView {
11641167
11651168 sidebarStateSaveTimer? . invalidate ( )
11661169 sidebarStateSaveTimer = Timer . scheduledTimer ( withTimeInterval: 0.2 , repeats: false ) { [ weak self] _ in
1167- self ? . saveSession ( )
1170+ MainActor . assumeIsolated { self ? . saveSession ( ) }
11681171 }
11691172 }
11701173
0 commit comments