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 Pinned Tabs #10817

Merged
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
66 changes: 62 additions & 4 deletions packages/core/src/browser/common-frontend-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import { SelectionService } from '../common/selection-service';
import { MessageService } from '../common/message-service';
import { OpenerService, open } from '../browser/opener-service';
import { ApplicationShell } from './shell/application-shell';
import { SHELL_TABBAR_CONTEXT_CLOSE, SHELL_TABBAR_CONTEXT_COPY, SHELL_TABBAR_CONTEXT_SPLIT } from './shell/tab-bars';
import { SHELL_TABBAR_CONTEXT_CLOSE, SHELL_TABBAR_CONTEXT_COPY, SHELL_TABBAR_CONTEXT_PIN, SHELL_TABBAR_CONTEXT_SPLIT } 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 @@ -59,6 +59,7 @@ import { ConfirmDialog, confirmExit, Dialog } from './dialogs';
import { WindowService } from './window/window-service';
import { FrontendApplicationConfigProvider } from './frontend-application-config-provider';
import { DecorationStyle } from './decoration-style';
import { isPinned, Title, togglePinned, Widget } from './widgets';

export namespace CommonMenus {

Expand Down Expand Up @@ -241,6 +242,16 @@ export namespace CommonCommands {
category: VIEW_CATEGORY,
label: 'Toggle Status Bar Visibility'
});
export const PIN_TAB = Command.toDefaultLocalizedCommand({
id: 'workbench.action.pinEditor',
category: VIEW_CATEGORY,
label: 'Pin Editor'
});
export const UNPIN_TAB = Command.toDefaultLocalizedCommand({
id: 'workbench.action.unpinEditor',
category: VIEW_CATEGORY,
label: 'Unpin Editor'
});
export const TOGGLE_MAXIMIZED = Command.toLocalizedCommand({
id: 'core.toggleMaximized',
category: VIEW_CATEGORY,
Expand Down Expand Up @@ -372,6 +383,8 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
@inject(WindowService)
protected readonly windowService: WindowService;

protected pinnedKey: ContextKey<boolean>;

async configure(app: FrontendApplication): Promise<void> {
const configDirUri = await this.environments.getConfigDirUri();
// Global settings
Expand All @@ -385,6 +398,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);
this.updatePinnedKey();
this.shell.onDidChangeActiveWidget(() => this.updatePinnedKey());

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

Expand Down Expand Up @@ -427,6 +444,13 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
}
}

protected updatePinnedKey(): void {
const activeTab = this.shell.findTabBar();
const pinningTarget = activeTab && this.shell.findTitle(activeTab);
const value = pinningTarget && isPinned(pinningTarget);
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 @@ -636,6 +660,16 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
label: nls.localizeByDefault('Toggle Menu Bar'),
order: '0'
});
registry.registerMenuAction(SHELL_TABBAR_CONTEXT_PIN, {
commandId: CommonCommands.PIN_TAB.id,
label: nls.localizeByDefault('Pin'),
order: '7'
});
registry.registerMenuAction(SHELL_TABBAR_CONTEXT_PIN, {
commandId: CommonCommands.UNPIN_TAB.id,
label: nls.localizeByDefault('Unpin'),
order: '8'
});
registry.registerMenuAction(CommonMenus.HELP, {
commandId: CommonCommands.ABOUT_COMMAND.id,
label: CommonCommands.ABOUT_COMMAND.label,
Expand Down Expand Up @@ -877,16 +911,30 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
commandRegistry.registerCommand(CommonCommands.SELECT_ICON_THEME, {
execute: () => this.selectIconTheme()
});

commandRegistry.registerCommand(CommonCommands.PIN_TAB, new CurrentWidgetCommandAdapter(this.shell, {
isEnabled: title => Boolean(title && !isPinned(title)),
execute: title => this.togglePinned(title),
}));
commandRegistry.registerCommand(CommonCommands.UNPIN_TAB, new CurrentWidgetCommandAdapter(this.shell, {
isEnabled: title => Boolean(title && isPinned(title)),
execute: title => this.togglePinned(title),
}));
commandRegistry.registerCommand(CommonCommands.CONFIGURE_DISPLAY_LANGUAGE, {
execute: () => this.configureDisplayLanguage()
});
}

private isElectron(): boolean {
protected isElectron(): boolean {
return environment.electron.is();
}

protected togglePinned(title?: Title<Widget>): void {
if (title) {
togglePinned(title);
this.updatePinnedKey();
}
}

registerKeybindings(registry: KeybindingRegistry): void {
if (supportCut) {
registry.registerKeybinding({
Expand Down Expand Up @@ -996,6 +1044,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: '!activeEditorIsPinned'
},
{
command: CommonCommands.UNPIN_TAB.id,
keybinding: 'ctrlcmd+k shift+enter',
Copy link
Member

Choose a reason for hiding this comment

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

I noticed that pinning does work using the specified keyboard shortcut. However, unpinning does not work for some reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It turns out this was also a consequence of the 'editor' vs. 'widget' question. An earlier version of the PR had either had two context keys or only activeWidgetIsPinned and it was switched to editor to agree with VSCode, but the keybindings still referred to widget.

when: 'activeEditorIsPinned'
}
);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/browser/frontend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
const tabBarDecoratorService = container.get(TabBarDecoratorService);
const iconThemeService = container.get(IconThemeService);
const selectionService = container.get(SelectionService);
return new TabBarRenderer(contextMenuRenderer, tabBarDecoratorService, iconThemeService, selectionService);
const commandService = container.get<CommandService>(CommandService);
return new TabBarRenderer(contextMenuRenderer, tabBarDecoratorService, iconThemeService, selectionService, commandService);
});

bindContributionProvider(bind, TabBarDecorator);
Expand Down
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 @@ -596,8 +596,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 @@ -607,6 +609,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;
}

/**
* 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 @@ -639,7 +663,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 @@ -663,13 +687,31 @@ export class ApplicationShell extends Widget {
} else {
this.collapseBottomPanel();
}
const widgets = toArray(this.bottomPanel.widgets());
if (bottomPanel.pinned && bottomPanel.pinned.length === widgets.length) {
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) {
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 @@ -1954,6 +1996,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 @@ -1967,6 +2010,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 @@ -272,7 +272,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 @@ -288,14 +289,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 (pinned) {
widget.title.className += ' theia-mod-pinned';
widget.title.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 @@ -725,6 +730,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
19 changes: 17 additions & 2 deletions packages/core/src/browser/shell/tab-bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import PerfectScrollbar from 'perfect-scrollbar';
import { TabBar, Title, Widget } from '@phosphor/widgets';
import { VirtualElement, h, VirtualDOM, ElementInlineStyle } from '@phosphor/virtualdom';
import { Disposable, DisposableCollection, MenuPath, notEmpty, SelectionService } from '../../common';
import { Disposable, DisposableCollection, MenuPath, notEmpty, SelectionService, CommandService } from '../../common';
import { ContextMenuRenderer } from '../context-menu-renderer';
import { Signal, Slot } from '@phosphor/signaling';
import { Message, MessageLoop } from '@phosphor/messaging';
Expand All @@ -31,6 +31,7 @@ import { IconThemeService } from '../icon-theme-service';
import { BreadcrumbsRenderer, BreadcrumbsRendererFactory } from '../breadcrumbs/breadcrumbs-renderer';
import { NavigatableWidget } from '../navigatable-types';
import { IDragEvent } from '@phosphor/dragdrop';
import { PINNED_CLASS } from '../widgets/widget';

/** The class name added to hidden content nodes, which are required to render vertical side bars. */
const HIDDEN_CONTENT_CLASS = 'theia-TabBar-hidden-content';
Expand Down Expand Up @@ -87,6 +88,7 @@ export class TabBarRenderer extends TabBar.Renderer {
protected readonly decoratorService?: TabBarDecoratorService,
protected readonly iconThemeService?: IconThemeService,
protected readonly selectionService?: SelectionService,
protected readonly commandService?: CommandService
) {
super();
if (this.decoratorService) {
Expand Down Expand Up @@ -162,7 +164,10 @@ export class TabBarRenderer extends TabBar.Renderer {
this.renderLabel(data, isInSidePanel),
this.renderBadge(data, isInSidePanel)
),
this.renderCloseIcon(data)
h.div({
className: 'p-TabBar-tabCloseIcon action-item',
onclick: this.handleCloseClickEvent
})
);
}

Expand Down Expand Up @@ -467,6 +472,16 @@ export class TabBarRenderer extends TabBar.Renderer {
}
};

protected handleCloseClickEvent = (event: MouseEvent) => {
if (this.tabBar && event.currentTarget instanceof HTMLElement) {
const id = event.currentTarget.parentElement!.id;
const title = this.tabBar.titles.find(t => this.createTabId(t) === id);
if (title?.closable === false && title?.className.includes(PINNED_CLASS) && this.commandService) {
this.commandService.executeCommand('workbench.action.unpinEditor', event);
}
}
};

protected handleDblClickEvent = (event: MouseEvent) => {
if (this.tabBar && event.currentTarget instanceof HTMLElement) {
const id = event.currentTarget.id;
Expand Down
14 changes: 12 additions & 2 deletions packages/core/src/browser/style/tabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ body.theia-editor-highlightModifiedTabs
height: inherit;
}

.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable > .p-TabBar-tabCloseIcon {
.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable > .p-TabBar-tabCloseIcon,
.p-TabBar.theia-app-centers .p-TabBar-tab.theia-mod-pinned > .p-TabBar-tabCloseIcon {
padding: 2px;
margin-top: 2px;
margin-left: 4px;
Expand All @@ -258,7 +259,8 @@ body.theia-editor-highlightModifiedTabs
background-color: rgba(50%, 50%, 50%, 0.2);
}

.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable {
.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable,
.p-TabBar.theia-app-centers .p-TabBar-tab.theia-mod-pinned {
padding-right: 4px;
}

Expand All @@ -272,6 +274,14 @@ body.theia-editor-highlightModifiedTabs
content: "\ea71";
}

.p-TabBar.theia-app-centers .p-TabBar-tab.theia-mod-pinned > .p-TabBar-tabCloseIcon:before {
content: "\eba0";
}

.p-TabBar.theia-app-centers .p-TabBar-tab.theia-mod-pinned.theia-mod-dirty > .p-TabBar-tabCloseIcon:before {
content: "\ebb2";
}

.p-TabBar-tabIcon.no-icon {
display: none !important;
}
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/browser/view-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { interfaces, injectable, inject, postConstruct } from 'inversify';
import { IIterator, toArray, find, some, every, map, ArrayExt } from '@phosphor/algorithm';
import {
Widget, EXPANSION_TOGGLE_CLASS, COLLAPSED_CLASS, CODICON_TREE_ITEM_CLASSES, MessageLoop, Message, SplitPanel,
BaseWidget, addEventListener, SplitLayout, LayoutItem, PanelLayout, addKeyListener, waitForRevealed, UnsafeWidgetUtilities, DockPanel
BaseWidget, addEventListener, SplitLayout, LayoutItem, PanelLayout, addKeyListener, waitForRevealed, UnsafeWidgetUtilities, DockPanel, PINNED_CLASS
} from './widgets';
import { Event as CommonEvent, Emitter } from '../common/event';
import { Disposable, DisposableCollection } from '../common/disposable';
Expand Down Expand Up @@ -276,7 +276,9 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica
if (title.iconClass) {
this.title.iconClass = title.iconClass;
}
if (title.closeable !== undefined) {
if (this.title.className.includes(PINNED_CLASS)) {
this.title.closable &&= false;
} else if (title.closeable !== undefined) {
this.title.closable = title.closeable;
}
}
Expand Down
Loading