Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions packages/code-studio/src/main/AppMainContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
ContextActions,
GLOBAL_SHORTCUTS,
NAVIGATION_SHORTCUTS,
Popper,
type ContextAction,
Button,
Expand All @@ -39,6 +40,10 @@ import {
setDashboardData as setDashboardDataAction,
setDashboardPluginData as setDashboardPluginDataAction,
updateDashboardData as updateDashboardDataAction,
emitCycleToNextStack,
emitCycleToPreviousStack,
emitCycleToNextTab,
emitCycleToPreviousTab,
} from '@deephaven/dashboard';
import {
ConsolePlugin,
Expand Down Expand Up @@ -235,6 +240,34 @@ export class AppMainContainer extends Component<
shortcut: IRIS_GRID_SHORTCUTS.TABLE.CLEAR_ALL_FILTERS,
isGlobal: true,
},
{
action: () => {
this.sendCycleStackForward();
},
shortcut: NAVIGATION_SHORTCUTS.CYCLE_TO_NEXT_STACK,
isGlobal: true,
},
{
action: () => {
this.sendCycleStackBackward();
},
shortcut: NAVIGATION_SHORTCUTS.CYCLE_TO_PREVIOUS_STACK,
isGlobal: true,
},
{
action: () => {
this.sendCycleTabForward();
},
shortcut: NAVIGATION_SHORTCUTS.CYCLE_TO_NEXT_TAB,
isGlobal: true,
},
{
action: () => {
this.sendCycleTabBackward();
},
shortcut: NAVIGATION_SHORTCUTS.CYCLE_TO_PREVIOUS_TAB,
isGlobal: true,
},
{
action: () => {
this.sendReopenLast();
Expand Down Expand Up @@ -411,6 +444,22 @@ export class AppMainContainer extends Component<
this.emitLayoutEvent(InputFilterEvent.CLEAR_ALL_FILTERS);
}

sendCycleStackForward(): void {
Comment thread
gzh2003 marked this conversation as resolved.
emitCycleToNextStack(this.getActiveEventHub());
}

sendCycleStackBackward(): void {
emitCycleToPreviousStack(this.getActiveEventHub());
}

sendCycleTabForward(): void {
emitCycleToNextTab(this.getActiveEventHub());
}

sendCycleTabBackward(): void {
emitCycleToPreviousTab(this.getActiveEventHub());
}

sendReopenLast(): void {
this.emitLayoutEvent(PanelEvent.REOPEN_LAST);
}
Expand Down
35 changes: 35 additions & 0 deletions packages/components/src/shortcuts/NavigationShortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import ShortcutRegistry from './ShortcutRegistry';
import { MODIFIER, KEY } from './Shortcut';

const NAVIGATION_SHORTCUTS = {
CYCLE_TO_NEXT_STACK: ShortcutRegistry.createAndAdd({
id: 'NAVIGATION.CYCLE_TO_NEXT_STACK',
name: 'Cycle To Next Stack',
shortcut: [MODIFIER.CTRL, KEY.SINGLE_QUOTE],
macShortcut: [MODIFIER.CMD, KEY.SINGLE_QUOTE],
isEditable: true,
}),
CYCLE_TO_PREVIOUS_STACK: ShortcutRegistry.createAndAdd({
id: 'NAVIGATION.CYCLE_TO_PREVIOUS_STACK',
name: 'Cycle To Previous Stack',
shortcut: [MODIFIER.CTRL, KEY.SEMICOLON],
macShortcut: [MODIFIER.CMD, KEY.SEMICOLON],
isEditable: true,
}),
Comment thread
gzh2003 marked this conversation as resolved.
CYCLE_TO_NEXT_TAB: ShortcutRegistry.createAndAdd({
id: 'NAVIGATION.CYCLE_TO_NEXT_TAB',
name: 'Cycle To Next Tab',
shortcut: [MODIFIER.CTRL, MODIFIER.SHIFT, KEY.DOUBLE_QUOTE],
macShortcut: [MODIFIER.CMD, MODIFIER.SHIFT, KEY.SINGLE_QUOTE],
isEditable: true,
}),
CYCLE_TO_PREVIOUS_TAB: ShortcutRegistry.createAndAdd({
id: 'NAVIGATION.CYCLE_TO_PREVIOUS_TAB',
name: 'Cycle To Previous TAB',
shortcut: [MODIFIER.CTRL, MODIFIER.SHIFT, KEY.COLON],
macShortcut: [MODIFIER.CMD, MODIFIER.SHIFT, KEY.SEMICOLON],
isEditable: true,
}),
};

export default NAVIGATION_SHORTCUTS;
1 change: 1 addition & 0 deletions packages/components/src/shortcuts/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as GLOBAL_SHORTCUTS } from './GlobalShortcuts';
export { default as NAVIGATION_SHORTCUTS } from './NavigationShortcuts';
export { default as Shortcut } from './Shortcut';
export * from './Shortcut';
export { default as ShortcutRegistry } from './ShortcutRegistry';
34 changes: 34 additions & 0 deletions packages/dashboard/src/NavigationEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { makeEventFunctions } from '@deephaven/golden-layout';

const NavigationEvent = Object.freeze({
Comment thread
gzh2003 marked this conversation as resolved.
CYCLE_TO_NEXT_STACK: 'NavigationEvent.CYCLE_TO_NEXT_STACK',
CYCLE_TO_PREVIOUS_STACK: 'NavigationEvent.CYCLE_TO_PREVIOUS_STACK',
CYCLE_TO_NEXT_TAB: 'NavigationEvent.CYCLE_TO_NEXT_TAB',
CYCLE_TO_PREVIOUS_TAB: 'NavigationEvent.CYCLE_TO_PREVIOUS_TAB',
});

export const {
listen: listenForCycleToNextStack,
emit: emitCycleToNextStack,
useListener: useCycleToNextStackListener,
} = makeEventFunctions(NavigationEvent.CYCLE_TO_NEXT_STACK);

export const {
listen: listenForCycleToPreviousStack,
emit: emitCycleToPreviousStack,
useListener: useCycleToPreviousStackListener,
} = makeEventFunctions(NavigationEvent.CYCLE_TO_PREVIOUS_STACK);

export const {
listen: listenForCycleToNextTab,
emit: emitCycleToNextTab,
useListener: useCycleToNextTabListener,
} = makeEventFunctions(NavigationEvent.CYCLE_TO_NEXT_TAB);

export const {
listen: listenForCycleToPreviousTab,
emit: emitCycleToPreviousTab,
useListener: useCycleToPreviousTabListener,
} = makeEventFunctions(NavigationEvent.CYCLE_TO_PREVIOUS_TAB);

export default NavigationEvent;
110 changes: 110 additions & 0 deletions packages/dashboard/src/PanelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import type {
} from '@deephaven/golden-layout';
import Log from '@deephaven/log';
import PanelEvent from './PanelEvent';
import {
listenForCycleToNextStack,
listenForCycleToPreviousStack,
listenForCycleToNextTab,
listenForCycleToPreviousTab,
} from './NavigationEvent';
import LayoutUtils, { isReactComponentConfig } from './layout/LayoutUtils';
import {
isWrappedComponent,
Expand All @@ -18,6 +24,11 @@ import {

const log = Log.module('PanelManager');

enum CycleDirection {
Next,
Previous,
}

export type PanelHydraterFunction = (
name: string,
props: PanelProps
Expand Down Expand Up @@ -64,6 +75,8 @@ class PanelManager {

openedMap: OpenedPanelMap;

navigationEventListenerRemovers: (() => void)[];

/**
* @param layout The GoldenLayout object to attach to
* @param hydrateComponent Function to hydrate a panel from a dehydrated state
Expand All @@ -88,6 +101,11 @@ class PanelManager {
this.handleUnmount = this.handleUnmount.bind(this);
this.handleReopen = this.handleReopen.bind(this);
this.handleReopenLast = this.handleReopenLast.bind(this);
this.handleCycleToNextStack = this.handleCycleToNextStack.bind(this);
this.handleCycleToPreviousStack =
this.handleCycleToPreviousStack.bind(this);
this.handleCycleToNextTab = this.handleCycleToNextTab.bind(this);
this.handleCycleToPreviousTab = this.handleCycleToPreviousTab.bind(this);
this.handleDeleted = this.handleDeleted.bind(this);
this.handleClosed = this.handleClosed.bind(this);
this.handleControlClose = this.handleControlClose.bind(this);
Expand All @@ -103,6 +121,9 @@ class PanelManager {
// Closed panels are stored in their dehydrated state
this.closed = [...closed];

// Store the navigation event listener removers
this.navigationEventListenerRemovers = [];

this.startListening();
}

Expand All @@ -117,6 +138,19 @@ class PanelManager {
eventHub.on(PanelEvent.CLOSED, this.handleClosed);
eventHub.on(PanelEvent.CLOSE, this.handleControlClose);
// PanelEvent.OPEN should be listened to by plugins to open a panel

this.navigationEventListenerRemovers.push(
listenForCycleToNextStack(eventHub, this.handleCycleToNextStack)
);
this.navigationEventListenerRemovers.push(
listenForCycleToPreviousStack(eventHub, this.handleCycleToPreviousStack)
);
this.navigationEventListenerRemovers.push(
listenForCycleToNextTab(eventHub, this.handleCycleToNextTab)
);
this.navigationEventListenerRemovers.push(
listenForCycleToPreviousTab(eventHub, this.handleCycleToPreviousTab)
);
}

stopListening(): void {
Expand All @@ -129,6 +163,9 @@ class PanelManager {
eventHub.off(PanelEvent.DELETE, this.handleDeleted);
eventHub.off(PanelEvent.CLOSED, this.handleClosed);
eventHub.off(PanelEvent.CLOSE, this.handleControlClose);

this.navigationEventListenerRemovers.forEach(remover => remover());
this.navigationEventListenerRemovers = [];
}

getClosedPanelConfigsOfType(typeString: string): ClosedPanels {
Expand Down Expand Up @@ -271,6 +308,79 @@ class PanelManager {
this.sendUpdate();
}

cycleStack(direction: CycleDirection): void {
const allStacks = LayoutUtils.getAllStackContainers(this.layout);
if (allStacks.length <= 1) {
return;
}

const focusedIndex = LayoutUtils.getFocusedStackIndex(allStacks);

// If no stack is focused, activate the first stack's content item
if (focusedIndex === -1) {
const targetStack = allStacks[0];
const activeContentIndex = targetStack.config.activeItemIndex;
const activeContentItem =
activeContentIndex != null
? targetStack.contentItems[activeContentIndex]
: targetStack.contentItems[0];

targetStack.setActiveContentItem(activeContentItem, true);
return;
}

const targetIndex =
direction === CycleDirection.Next
? (focusedIndex + 1) % allStacks.length
: (focusedIndex - 1 + allStacks.length) % allStacks.length;
const targetStack = allStacks[targetIndex];

const activeContentIndex = targetStack.config.activeItemIndex;
const activeContentItem =
activeContentIndex != null
? targetStack.contentItems[activeContentIndex]
: targetStack.contentItems[0];

targetStack.setActiveContentItem(activeContentItem, true);
}

handleCycleToNextStack(): void {
this.cycleStack(CycleDirection.Next);
}

handleCycleToPreviousStack(): void {
this.cycleStack(CycleDirection.Previous);
}

cycleTab(direction: CycleDirection): void {
const focusedStack = LayoutUtils.getFocusedStack(this.layout);
if (focusedStack === undefined) {
return;
}
const { contentItems } = focusedStack;

if (contentItems.length <= 1) {
return;
}

const activeItemIndex = focusedStack.config.activeItemIndex ?? 0;
const targetIndex =
direction === CycleDirection.Next
? (activeItemIndex + 1) % contentItems.length
: (activeItemIndex - 1 + contentItems.length) % contentItems.length;

const targetContentItem = contentItems[targetIndex];
focusedStack.setActiveContentItem(targetContentItem, true);
}

handleCycleToNextTab(): void {
this.cycleTab(CycleDirection.Next);
}

handleCycleToPreviousTab(): void {
this.cycleTab(CycleDirection.Previous);
}

/**
*
* @param panelConfig The config to hydrate and load
Expand Down
1 change: 1 addition & 0 deletions packages/dashboard/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export * from './layout';
export * from './redux';
export * from './PanelManager';
export * from './PanelEvent';
export * from './NavigationEvent';
export { default as PanelErrorBoundary } from './PanelErrorBoundary';
export { default as PanelManager } from './PanelManager';
37 changes: 37 additions & 0 deletions packages/dashboard/src/layout/LayoutUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,43 @@ class LayoutUtils {
return this.addStack(newParent, !columnPreferred);
}

/**
* Gets all stack containers in the layout
* @param layout GoldenLayout instance
* @returns The found stack containers
*/
static getAllStackContainers(layout: GoldenLayout): Stack[] {
// eslint-disable-next-line no-underscore-dangle
return layout._findAllStackContainers();
}

/**
* Get the index of the stack that is currently focused
* @param allStacks All the stacks
* @returns The focused stack's index or -1 if not found
*/
static getFocusedStackIndex(allStacks: Stack[]): number {
// NOTE: We target the 'lm_focusin' class because GoldenLayout automatically applies this class
// to tab elements when they receive focus. Until we enhance focus tracking in GoldenLayout, we
// will have to rely on this internal CSS class.
return allStacks.findIndex(stack =>
stack.header.tabs.some(tab =>
tab.element[0].classList.contains('lm_focusin')
)
);
}

/**
* Get the stack that is currently focused
* @param layout GoldenLayout instance
* @returns The focused stack or undefined if none found
*/
static getFocusedStack(layout: GoldenLayout): Stack | undefined {
const allStacks = LayoutUtils.getAllStackContainers(layout);
const focusedStackIndex = LayoutUtils.getFocusedStackIndex(allStacks);
return allStacks[focusedStackIndex];
}

/**
* Gets a stack by its ID
* @param item Golden layout content item to search for the stack
Expand Down
4 changes: 2 additions & 2 deletions packages/golden-layout/src/items/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ export default class Component extends AbstractContentItem {
AbstractContentItem.prototype._$hide.call(this);
}

_$show() {
_$show(forceFocus?: boolean) {
this.container.show();
if (this.container._config.isFocusOnShow) {
if (this.container._config.isFocusOnShow || forceFocus) {
// focus the shown container element on show
// preventScroll isn't supported in safari, but also doesn't matter for illumon when 100% window
this.container._contentElement[0].focus({ preventScroll: true });
Expand Down
Loading
Loading