@@ -8,6 +8,12 @@ import type {
88} from '@deephaven/golden-layout' ;
99import Log from '@deephaven/log' ;
1010import PanelEvent from './PanelEvent' ;
11+ import {
12+ listenForCycleToNextStack ,
13+ listenForCycleToPreviousStack ,
14+ listenForCycleToNextTab ,
15+ listenForCycleToPreviousTab ,
16+ } from './NavigationEvent' ;
1117import LayoutUtils , { isReactComponentConfig } from './layout/LayoutUtils' ;
1218import {
1319 isWrappedComponent ,
@@ -18,6 +24,11 @@ import {
1824
1925const log = Log . module ( 'PanelManager' ) ;
2026
27+ enum CycleDirection {
28+ Next ,
29+ Previous ,
30+ }
31+
2132export type PanelHydraterFunction = (
2233 name : string ,
2334 props : PanelProps
@@ -64,6 +75,8 @@ class PanelManager {
6475
6576 openedMap : OpenedPanelMap ;
6677
78+ navigationEventListenerRemovers : ( ( ) => void ) [ ] ;
79+
6780 /**
6881 * @param layout The GoldenLayout object to attach to
6982 * @param hydrateComponent Function to hydrate a panel from a dehydrated state
@@ -88,6 +101,11 @@ class PanelManager {
88101 this . handleUnmount = this . handleUnmount . bind ( this ) ;
89102 this . handleReopen = this . handleReopen . bind ( this ) ;
90103 this . handleReopenLast = this . handleReopenLast . bind ( this ) ;
104+ this . handleCycleToNextStack = this . handleCycleToNextStack . bind ( this ) ;
105+ this . handleCycleToPreviousStack =
106+ this . handleCycleToPreviousStack . bind ( this ) ;
107+ this . handleCycleToNextTab = this . handleCycleToNextTab . bind ( this ) ;
108+ this . handleCycleToPreviousTab = this . handleCycleToPreviousTab . bind ( this ) ;
91109 this . handleDeleted = this . handleDeleted . bind ( this ) ;
92110 this . handleClosed = this . handleClosed . bind ( this ) ;
93111 this . handleControlClose = this . handleControlClose . bind ( this ) ;
@@ -103,6 +121,9 @@ class PanelManager {
103121 // Closed panels are stored in their dehydrated state
104122 this . closed = [ ...closed ] ;
105123
124+ // Store the navigation event listener removers
125+ this . navigationEventListenerRemovers = [ ] ;
126+
106127 this . startListening ( ) ;
107128 }
108129
@@ -117,6 +138,19 @@ class PanelManager {
117138 eventHub . on ( PanelEvent . CLOSED , this . handleClosed ) ;
118139 eventHub . on ( PanelEvent . CLOSE , this . handleControlClose ) ;
119140 // PanelEvent.OPEN should be listened to by plugins to open a panel
141+
142+ this . navigationEventListenerRemovers . push (
143+ listenForCycleToNextStack ( eventHub , this . handleCycleToNextStack )
144+ ) ;
145+ this . navigationEventListenerRemovers . push (
146+ listenForCycleToPreviousStack ( eventHub , this . handleCycleToPreviousStack )
147+ ) ;
148+ this . navigationEventListenerRemovers . push (
149+ listenForCycleToNextTab ( eventHub , this . handleCycleToNextTab )
150+ ) ;
151+ this . navigationEventListenerRemovers . push (
152+ listenForCycleToPreviousTab ( eventHub , this . handleCycleToPreviousTab )
153+ ) ;
120154 }
121155
122156 stopListening ( ) : void {
@@ -129,6 +163,9 @@ class PanelManager {
129163 eventHub . off ( PanelEvent . DELETE , this . handleDeleted ) ;
130164 eventHub . off ( PanelEvent . CLOSED , this . handleClosed ) ;
131165 eventHub . off ( PanelEvent . CLOSE , this . handleControlClose ) ;
166+
167+ this . navigationEventListenerRemovers . forEach ( remover => remover ( ) ) ;
168+ this . navigationEventListenerRemovers = [ ] ;
132169 }
133170
134171 getClosedPanelConfigsOfType ( typeString : string ) : ClosedPanels {
@@ -271,6 +308,79 @@ class PanelManager {
271308 this . sendUpdate ( ) ;
272309 }
273310
311+ cycleStack ( direction : CycleDirection ) : void {
312+ const allStacks = LayoutUtils . getAllStackContainers ( this . layout ) ;
313+ if ( allStacks . length <= 1 ) {
314+ return ;
315+ }
316+
317+ const focusedIndex = LayoutUtils . getFocusedStackIndex ( allStacks ) ;
318+
319+ // If no stack is focused, activate the first stack's content item
320+ if ( focusedIndex === - 1 ) {
321+ const targetStack = allStacks [ 0 ] ;
322+ const activeContentIndex = targetStack . config . activeItemIndex ;
323+ const activeContentItem =
324+ activeContentIndex != null
325+ ? targetStack . contentItems [ activeContentIndex ]
326+ : targetStack . contentItems [ 0 ] ;
327+
328+ targetStack . setActiveContentItem ( activeContentItem , true ) ;
329+ return ;
330+ }
331+
332+ const targetIndex =
333+ direction === CycleDirection . Next
334+ ? ( focusedIndex + 1 ) % allStacks . length
335+ : ( focusedIndex - 1 + allStacks . length ) % allStacks . length ;
336+ const targetStack = allStacks [ targetIndex ] ;
337+
338+ const activeContentIndex = targetStack . config . activeItemIndex ;
339+ const activeContentItem =
340+ activeContentIndex != null
341+ ? targetStack . contentItems [ activeContentIndex ]
342+ : targetStack . contentItems [ 0 ] ;
343+
344+ targetStack . setActiveContentItem ( activeContentItem , true ) ;
345+ }
346+
347+ handleCycleToNextStack ( ) : void {
348+ this . cycleStack ( CycleDirection . Next ) ;
349+ }
350+
351+ handleCycleToPreviousStack ( ) : void {
352+ this . cycleStack ( CycleDirection . Previous ) ;
353+ }
354+
355+ cycleTab ( direction : CycleDirection ) : void {
356+ const focusedStack = LayoutUtils . getFocusedStack ( this . layout ) ;
357+ if ( focusedStack === undefined ) {
358+ return ;
359+ }
360+ const { contentItems } = focusedStack ;
361+
362+ if ( contentItems . length <= 1 ) {
363+ return ;
364+ }
365+
366+ const activeItemIndex = focusedStack . config . activeItemIndex ?? 0 ;
367+ const targetIndex =
368+ direction === CycleDirection . Next
369+ ? ( activeItemIndex + 1 ) % contentItems . length
370+ : ( activeItemIndex - 1 + contentItems . length ) % contentItems . length ;
371+
372+ const targetContentItem = contentItems [ targetIndex ] ;
373+ focusedStack . setActiveContentItem ( targetContentItem , true ) ;
374+ }
375+
376+ handleCycleToNextTab ( ) : void {
377+ this . cycleTab ( CycleDirection . Next ) ;
378+ }
379+
380+ handleCycleToPreviousTab ( ) : void {
381+ this . cycleTab ( CycleDirection . Previous ) ;
382+ }
383+
274384 /**
275385 *
276386 * @param panelConfig The config to hydrate and load
0 commit comments