Skip to content
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

Add ability to pin tabs #9614

Closed
wants to merge 10 commits into from
79 changes: 78 additions & 1 deletion packages/core/src/browser/common-frontend-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { SHELL_TABBAR_CONTEXT_MENU } from './shell/tab-bars';
import { AboutDialog } from './about-dialog';
import * as browser from './browser';
import URI from '../common/uri';
import { ContextKeyService } from './context-key-service';
import { ContextKey, ContextKeyService } from './context-key-service';
import { OS, isOSX, isWindows } from '../common/os';
import { ResourceContextKey } from './resource-context-key';
import { UriSelection } from '../common/selection';
Expand Down Expand Up @@ -215,6 +215,16 @@ export namespace CommonCommands {
category: VIEW_CATEGORY,
label: 'Toggle Bottom Panel'
};
export const PIN_TAB: Command = {
id: 'workbench.action.pinEditor',
category: VIEW_CATEGORY,
label: 'Pin Tab'
};
export const UNPIN_TAB: Command = {
id: 'workbench.action.unpinEditor',
category: VIEW_CATEGORY,
label: 'Unpin Tab'
};
export const TOGGLE_MAXIMIZED: Command = {
id: 'core.toggleMaximized',
category: VIEW_CATEGORY,
Expand Down Expand Up @@ -281,6 +291,8 @@ export const supportPaste = browser.isNative || (!browser.isChrome && document.q

export const RECENT_COMMANDS_STORAGE_KEY = 'commands';

export const PINNED_CLASS = 'theia-mod-pinned';

@injectable()
export class CommonFrontendContribution implements FrontendApplicationContribution, MenuContribution, CommandContribution, KeybindingContribution, ColorContribution {

Expand Down Expand Up @@ -334,6 +346,8 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
@inject(AuthenticationService)
protected readonly authenticationService: AuthenticationService;

protected pinnedKey: ContextKey<boolean>;

async configure(app: FrontendApplication): Promise<void> {
const configDirUri = await this.environments.getConfigDirUri();
// Global settings
Expand All @@ -347,6 +361,10 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
this.contextKeyService.createKey<boolean>('isWindows', OS.type() === OS.Type.Windows);
this.contextKeyService.createKey<boolean>('isWeb', !this.isElectron());

this.pinnedKey = this.contextKeyService.createKey<boolean>('activeEditorIsPinned', false);
dsseng marked this conversation as resolved.
Show resolved Hide resolved
this.updatePinnedKey();
this.shell.activeChanged.connect(() => this.updatePinnedKey());

this.initResourceContextKeys();
this.registerCtrlWHandling();

Expand Down Expand Up @@ -394,6 +412,11 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
}
}

protected updatePinnedKey(): void {
const value = this.shell.activeWidget && this.shell.activeWidget.title.className.indexOf(PINNED_CLASS) >= 0;
this.pinnedKey.set(value);
}

protected updateThemePreference(preferenceName: 'workbench.colorTheme' | 'workbench.iconTheme'): void {
const inspect = this.preferenceService.inspect<string | null>(preferenceName);
const workspaceValue = inspect && inspect.workspaceValue;
Expand Down Expand Up @@ -533,6 +556,16 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
label: 'Toggle Maximized',
order: '5'
});
registry.registerMenuAction(SHELL_TABBAR_CONTEXT_MENU, {
commandId: CommonCommands.PIN_TAB.id,
label: 'Pin',
order: '7'
});
registry.registerMenuAction(SHELL_TABBAR_CONTEXT_MENU, {
commandId: CommonCommands.UNPIN_TAB.id,
label: 'Unpin',
order: '8'
});
registry.registerMenuAction(CommonMenus.HELP, {
commandId: CommonCommands.ABOUT_COMMAND.id,
label: 'About',
Expand Down Expand Up @@ -771,6 +804,21 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
commandRegistry.registerCommand(CommonCommands.SELECT_ICON_THEME, {
execute: () => this.selectIconTheme()
});

commandRegistry.registerCommand(CommonCommands.PIN_TAB, {
isEnabled: (event?: Event) => {
const currentTitle = (this.shell.findTargetedWidget(event)?.title || this.shell.currentTabBar?.currentTitle);
return !!currentTitle && currentTitle.closable && currentTitle.className.indexOf(PINNED_CLASS) === -1;
},
execute: (event?: Event) => this.togglePinned(event),
});
commandRegistry.registerCommand(CommonCommands.UNPIN_TAB, {
isEnabled: (event?: Event) => {
const currentTitle = (this.shell.findTargetedWidget(event)?.title || this.shell.currentTabBar?.currentTitle);
return !!currentTitle && currentTitle.className.indexOf(PINNED_CLASS) >= 0;
},
execute: (event?: Event) => this.togglePinned(event),
});
}

private findTabArea(event?: Event): ApplicationShell.Area | undefined {
Expand Down Expand Up @@ -834,6 +882,25 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
return environment.electron.is();
}

private togglePinned(event?: Event): void {
const tabBar = this.shell.findTabBar(event) || this.shell.currentTabBar;
if (!tabBar) {
return;
}
const currentTitle = this.shell.findTargetedWidget(event)?.title || tabBar.currentTitle;
if (!currentTitle) {
return;
}
currentTitle.closable = currentTitle.className.indexOf(PINNED_CLASS) >= 0;

currentTitle.className = currentTitle.className.replace(` ${PINNED_CLASS}`, '');
if (!currentTitle.closable) {
currentTitle.className += ` ${PINNED_CLASS}`;
}

this.updatePinnedKey();
}

registerKeybindings(registry: KeybindingRegistry): void {
if (supportCut) {
registry.registerKeybinding({
Expand Down Expand Up @@ -943,6 +1010,16 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
{
command: CommonCommands.SELECT_COLOR_THEME.id,
keybinding: 'ctrlcmd+k ctrlcmd+t'
},
{
command: CommonCommands.PIN_TAB.id,
keybinding: 'ctrlcmd+k shift+enter',
when: '!activeWidgetIsPinned'
},
{
command: CommonCommands.UNPIN_TAB.id,
keybinding: 'ctrlcmd+k shift+enter',
when: 'activeWidgetIsPinned'
}
);
}
Expand Down
32 changes: 17 additions & 15 deletions packages/core/src/browser/frontend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,21 +144,6 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
return container.get(TabBarToolbar);
});

bind(DockPanelRendererFactory).toFactory(context => () => {
const { container } = context;
const tabBarToolbarRegistry = container.get(TabBarToolbarRegistry);
const tabBarRendererFactory: () => TabBarRenderer = container.get(TabBarRendererFactory);
const tabBarToolbarFactory: () => TabBarToolbar = container.get(TabBarToolbarFactory);
return new DockPanelRenderer(tabBarRendererFactory, tabBarToolbarRegistry, tabBarToolbarFactory);
});
bind(DockPanelRenderer).toSelf();
bind(TabBarRendererFactory).toFactory(context => () => {
const contextMenuRenderer = context.container.get<ContextMenuRenderer>(ContextMenuRenderer);
const decoratorService = context.container.get<TabBarDecoratorService>(TabBarDecoratorService);
const iconThemeService = context.container.get<IconThemeService>(IconThemeService);
return new TabBarRenderer(contextMenuRenderer, decoratorService, iconThemeService);
});

bindContributionProvider(bind, TabBarDecorator);
bind(TabBarDecoratorService).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(TabBarDecoratorService);
Expand Down Expand Up @@ -347,5 +332,22 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo

bind(CredentialsService).to(CredentialsServiceImpl);

bind(TabBarRendererFactory).toFactory(context => () => {
const contextMenuRenderer = context.container.get<ContextMenuRenderer>(ContextMenuRenderer);
const decoratorService = context.container.get<TabBarDecoratorService>(TabBarDecoratorService);
const iconThemeService = context.container.get<IconThemeService>(IconThemeService);
const commandService = context.container.get<CommandService>(CommandService);
return new TabBarRenderer(contextMenuRenderer, decoratorService, iconThemeService, commandService);
});

bind(DockPanelRendererFactory).toFactory(context => () => {
const { container } = context;
const tabBarToolbarRegistry = container.get(TabBarToolbarRegistry);
const tabBarRendererFactory: () => TabBarRenderer = container.get(TabBarRendererFactory);
const tabBarToolbarFactory: () => TabBarToolbar = container.get(TabBarToolbarFactory);
return new DockPanelRenderer(tabBarRendererFactory, tabBarToolbarRegistry, tabBarToolbarFactory);
});
bind(DockPanelRenderer).toSelf();

bind(ContributionFilterRegistry).to(ContributionFilterRegistryImpl).inSingletonScope();
});
3 changes: 3 additions & 0 deletions packages/core/src/browser/icons/pinned-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/core/src/browser/icons/pinned-dirty-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/core/src/browser/icons/pinned-dirty-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/core/src/browser/icons/pinned-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 45 additions & 1 deletion packages/core/src/browser/shell/application-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,8 +564,10 @@ export class ApplicationShell extends Widget {
return {
version: applicationShellLayoutVersion,
mainPanel: this.mainPanel.saveLayout(),
mainPanelPinned: this.getPinnedMainWidgets(),
bottomPanel: {
config: this.bottomPanel.saveLayout(),
pinned: this.getPinnedBottomWidgets(),
size: this.bottomPanel.isVisible ? this.getBottomPanelSize() : this.bottomPanelState.lastPanelSize,
expanded: this.isExpanded('bottom')
},
Expand All @@ -575,6 +577,28 @@ export class ApplicationShell extends Widget {
};
}

// Get an array corresponding to main panel widgets' pinned state.
getPinnedMainWidgets(): boolean[] {
const pinned: boolean[] = [];

toArray(this.mainPanel.widgets()).forEach((a, i) => {
pinned[i] = a.title.className.indexOf('theia-mod-pinned') >= 0;
});

return pinned;
}

// Get an array corresponding to bottom panel widgets' pinned state.
getPinnedBottomWidgets(): boolean[] {
const pinned: boolean[] = [];

toArray(this.bottomPanel.widgets()).forEach((a, i) => {
pinned[i] = a.title.className.indexOf('theia-mod-pinned') >= 0;
});

return pinned;
}
Comment on lines +592 to +600
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure that this warrants its own function. Maybe getPinnedWidgetsForArea(area) that can handle both of them?


/**
* Compute the current height of the bottom panel. This implementation assumes that the container
* of the bottom panel is a `SplitPanel`.
Expand Down Expand Up @@ -607,7 +631,7 @@ export class ApplicationShell extends Widget {
* Apply a shell layout that has been previously created with `getLayoutData`.
*/
async setLayoutData(layoutData: ApplicationShell.LayoutData): Promise<void> {
const { mainPanel, bottomPanel, leftPanel, rightPanel, activeWidgetId } = layoutData;
const { mainPanel, mainPanelPinned, bottomPanel, leftPanel, rightPanel, activeWidgetId } = layoutData;
if (leftPanel) {
this.leftPanelHandler.setLayoutData(leftPanel);
this.registerWithFocusTracker(leftPanel);
Expand All @@ -631,13 +655,31 @@ export class ApplicationShell extends Widget {
} else {
this.collapseBottomPanel();
}
const widgets = toArray(this.bottomPanel.widgets());
if (bottomPanel.pinned && bottomPanel.pinned.length === widgets.length) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re: handling uncertainty of restoration.

widgets.forEach((a, i) => {
if (bottomPanel.pinned![i]) {
a.title.className += ' theia-mod-pinned';
a.title.closable = false;
}
});
}
this.refreshBottomPanelToggleButton();
}
// Proceed with the main panel once all others are set up
await this.bottomPanelState.pendingUpdate;
if (mainPanel) {
this.mainPanel.restoreLayout(mainPanel);
this.registerWithFocusTracker(mainPanel.main);
const widgets = toArray(this.mainPanel.widgets());
if (mainPanelPinned && mainPanelPinned.length === widgets.length) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above for a way to handle the uncertainty of whether the widgets restored will be the same as the widgets whose state was saved.

widgets.forEach((a, i) => {
if (mainPanelPinned[i]) {
a.title.className += ' theia-mod-pinned';
a.title.closable = false;
}
});
}
}
if (activeWidgetId) {
this.activateWidget(activeWidgetId);
Expand Down Expand Up @@ -1895,6 +1937,7 @@ export namespace ApplicationShell {
export interface LayoutData {
version?: string | ApplicationShellLayoutVersion,
mainPanel?: DockPanel.ILayoutConfig;
mainPanelPinned?: boolean[];
bottomPanel?: BottomPanelLayoutData;
leftPanel?: SidePanel.LayoutData;
rightPanel?: SidePanel.LayoutData;
Expand All @@ -1908,6 +1951,7 @@ export namespace ApplicationShell {
config?: DockPanel.ILayoutConfig;
size?: number;
expanded?: boolean;
pinned?: boolean[];
}

/**
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/browser/shell/side-panel-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ export class SidePanelHandler {
const items = toArray(map(this.tabBar.titles, title => <SidePanel.WidgetItem>{
widget: title.owner,
rank: SidePanelHandler.rankProperty.get(title.owner),
expanded: title === currentTitle
expanded: title === currentTitle,
pinned: title.className.indexOf('theia-mod-pinned') >= 0
}));
// eslint-disable-next-line no-null/no-null
const size = currentTitle !== null ? this.getPanelSize() : this.state.lastPanelSize;
Expand All @@ -269,14 +270,18 @@ export class SidePanelHandler {

let currentTitle: Title<Widget> | undefined;
if (layoutData.items) {
for (const { widget, rank, expanded } of layoutData.items) {
for (const { widget, rank, expanded, pinned } of layoutData.items) {
if (widget) {
if (rank) {
SidePanelHandler.rankProperty.set(widget, rank);
}
if (expanded) {
currentTitle = widget.title;
}
if (currentTitle && pinned) {
currentTitle.className += ' theia-mod-pinned';
currentTitle.closable = false;
}
// Add the widgets directly to the tab bar in the same order as they are stored
this.tabBar.addTab(widget.title);
}
Expand Down Expand Up @@ -676,6 +681,7 @@ export namespace SidePanel {
/** Can be undefined in case the widget could not be restored. */
widget?: Widget;
expanded?: boolean;
pinned?: boolean;
}

export interface State {
Expand Down
Loading