Merge from master

This commit is contained in:
Raj Musuku
2019-02-21 17:56:04 -08:00
parent 5a146e34fa
commit 666ae11639
11482 changed files with 119352 additions and 255574 deletions

View File

@@ -3,27 +3,30 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/activityaction';
import * as nls from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch';
import { TPromise } from 'vs/base/common/winjs.base';
import { Action } from 'vs/base/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ViewletDescriptor } from 'vs/workbench/browser/viewlet';
import { IActivity, IGlobalActivity } from 'vs/workbench/common/activity';
import { dispose } from 'vs/base/common/lifecycle';
import { IViewletService, } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IPartService, Parts } from 'vs/workbench/services/part/common/partService';
import { IThemeService, ITheme, registerThemingParticipant, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ActivityAction, ActivityActionItem, ICompositeBarColors, ToggleCompositePinnedAction, ICompositeBar } from 'vs/workbench/browser/parts/compositebar/compositeBarActions';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch';
import { Action } from 'vs/base/common/actions';
import { KeyCode } from 'vs/base/common/keyCodes';
import { dispose } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { Registry } from 'vs/platform/registry/common/platform';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import URI from 'vs/base/common/uri';
import { activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { ActivityAction, ActivityActionItem, ICompositeBar, ICompositeBarColors, ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions';
import { ViewletDescriptor } from 'vs/workbench/browser/viewlet';
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
import { IActivity, IGlobalActivity } from 'vs/workbench/common/activity';
import { ACTIVITY_BAR_FOREGROUND } from 'vs/workbench/common/theme';
import { IActivityService } from 'vs/workbench/services/activity/common/activity';
import { IPartService, Parts } from 'vs/workbench/services/part/common/partService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
export class ViewletActivityAction extends ActivityAction {
@@ -40,15 +43,15 @@ export class ViewletActivityAction extends ActivityAction {
super(activity);
}
run(event: any): TPromise<any> {
run(event: any): Thenable<any> {
if (event instanceof MouseEvent && event.button === 2) {
return TPromise.as(false); // do not run on right click
return Promise.resolve(false); // do not run on right click
}
// prevent accident trigger on a doubleclick (to help nervous people)
const now = Date.now();
if (now > this.lastRun /* https://github.com/Microsoft/vscode/issues/25830 */ && now - this.lastRun < ViewletActivityAction.preventDoubleClickDelay) {
return TPromise.as(true);
return Promise.resolve(true);
}
this.lastRun = now;
@@ -58,7 +61,8 @@ export class ViewletActivityAction extends ActivityAction {
// Hide sidebar if selected viewlet already visible
if (sideBarVisible && activeViewlet && activeViewlet.getId() === this.activity.id) {
this.logAction('hide');
return this.partService.setSideBarHidden(true);
this.partService.setSideBarHidden(true);
return Promise.resolve(null);
}
this.logAction('show');
@@ -86,13 +90,14 @@ export class ToggleViewletAction extends Action {
super(_viewlet.id, _viewlet.name);
}
run(): TPromise<any> {
run(): Thenable<any> {
const sideBarVisible = this.partService.isVisible(Parts.SIDEBAR_PART);
const activeViewlet = this.viewletService.getActiveViewlet();
// Hide sidebar if selected viewlet already visible
if (sideBarVisible && activeViewlet && activeViewlet.getId() === this._viewlet.id) {
return this.partService.setSideBarHidden(true);
this.partService.setSideBarHidden(true);
return Promise.resolve(null);
}
return this.viewletService.openViewlet(this._viewlet.id, true);
@@ -110,7 +115,7 @@ export class GlobalActivityActionItem extends ActivityActionItem {
constructor(
action: GlobalActivityAction,
colors: ICompositeBarColors,
colors: (theme: ITheme) => ICompositeBarColors,
@IThemeService themeService: IThemeService,
@IContextMenuService protected contextMenuService: IContextMenuService
) {
@@ -122,28 +127,29 @@ export class GlobalActivityActionItem extends ActivityActionItem {
// Context menus are triggered on mouse down so that an item can be picked
// and executed with releasing the mouse over it
this.$container.on(DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
this._register(DOM.addDisposableListener(this.container, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
DOM.EventHelper.stop(e, true);
const event = new StandardMouseEvent(e);
this.showContextMenu({ x: event.posx, y: event.posy });
});
}));
this.$container.on(DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
DOM.EventHelper.stop(e, true);
this.showContextMenu(this.$container.getHTMLElement());
this.showContextMenu(this.container);
}
});
}));
this.$container.on(TouchEventType.Tap, (e: GestureEvent) => {
this._register(DOM.addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => {
DOM.EventHelper.stop(e, true);
const event = new StandardMouseEvent(e);
this.showContextMenu({ x: event.posx, y: event.posy });
});
}));
}
private showContextMenu(location: HTMLElement | { x: number, y: number }): void {
@@ -153,7 +159,7 @@ export class GlobalActivityActionItem extends ActivityActionItem {
this.contextMenuService.showContextMenu({
getAnchor: () => location,
getActions: () => TPromise.as(actions),
getActions: () => actions,
onHide: () => dispose(actions)
});
}
@@ -171,12 +177,10 @@ export class PlaceHolderViewletActivityAction extends ViewletActivityAction {
const iconClass = `.monaco-workbench > .activitybar .monaco-action-bar .action-label.${this.class}`; // Generate Placeholder CSS to show the icon in the activity bar
DOM.createCSSRule(iconClass, `-webkit-mask: url('${iconUrl || ''}') no-repeat 50% 50%`);
this.enabled = false;
}
setActivity(activity: IActivity): void {
this.activity = activity;
this.enabled = true;
}
}
@@ -184,18 +188,97 @@ export class PlaceHolderToggleCompositePinnedAction extends ToggleCompositePinne
constructor(id: string, compositeBar: ICompositeBar) {
super({ id, name: id, cssClass: void 0 }, compositeBar);
this.enabled = false;
}
setActivity(activity: IActivity): void {
this.label = activity.name;
this.enabled = true;
}
}
class SwitchSideBarViewAction extends Action {
constructor(
id: string,
name: string,
@IViewletService private viewletService: IViewletService,
@IActivityService private activityService: IActivityService
) {
super(id, name);
}
run(offset: number): Thenable<any> {
const pinnedViewletIds = this.activityService.getPinnedViewletIds();
const activeViewlet = this.viewletService.getActiveViewlet();
if (!activeViewlet) {
return Promise.resolve(null);
}
let targetViewletId: string;
for (let i = 0; i < pinnedViewletIds.length; i++) {
if (pinnedViewletIds[i] === activeViewlet.getId()) {
targetViewletId = pinnedViewletIds[(i + pinnedViewletIds.length + offset) % pinnedViewletIds.length];
break;
}
}
return this.viewletService.openViewlet(targetViewletId, true);
}
}
export class PreviousSideBarViewAction extends SwitchSideBarViewAction {
static readonly ID = 'workbench.action.previousSideBarView';
static LABEL = nls.localize('previousSideBarView', 'Previous Side Bar View');
constructor(
id: string,
name: string,
@IViewletService viewletService: IViewletService,
@IActivityService activityService: IActivityService
) {
super(id, name, viewletService, activityService);
}
run(): Thenable<any> {
return super.run(-1);
}
}
export class NextSideBarViewAction extends SwitchSideBarViewAction {
static readonly ID = 'workbench.action.nextSideBarView';
static LABEL = nls.localize('nextSideBarView', 'Next Side Bar View');
constructor(
id: string,
name: string,
@IViewletService viewletService: IViewletService,
@IActivityService activityService: IActivityService
) {
super(id, name, viewletService, activityService);
}
run(): Thenable<any> {
return super.run(1);
}
}
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(PreviousSideBarViewAction, PreviousSideBarViewAction.ID, PreviousSideBarViewAction.LABEL), 'View: Open Previous Side Bar View', nls.localize('view', "View"));
registry.registerWorkbenchAction(new SyncActionDescriptor(NextSideBarViewAction, NextSideBarViewAction.ID, NextSideBarViewAction.LABEL), 'View: Open Next Side Bar View', nls.localize('view', "View"));
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const activeForegroundColor = theme.getColor(ACTIVITY_BAR_FOREGROUND);
if (activeForegroundColor) {
collector.addRule(`
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item.active .action-label,
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item:focus .action-label,
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item:hover .action-label {
background-color: ${activeForegroundColor} !important;
}
`);
}
// Styling with Outline color (e.g. high contrast theme)
const outline = theme.getColor(activeContrastBorder);
if (outline) {
@@ -207,7 +290,6 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
left: 9px;
height: 32px;
width: 32px;
opacity: 0.6;
}
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item.active:before,
@@ -221,12 +303,6 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
outline: 1px dashed;
}
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item.active:before,
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item.checked:before,
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item:hover:before {
opacity: 1;
}
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item:focus:before {
border-left-color: ${outline};
}
@@ -246,21 +322,10 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const focusBorderColor = theme.getColor(focusBorder);
if (focusBorderColor) {
collector.addRule(`
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item.active .action-label,
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item.checked .action-label,
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item:focus .action-label,
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item:hover .action-label {
opacity: 1;
}
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item .action-label {
opacity: 0.6;
}
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item:focus:before {
border-left-color: ${focusBorderColor};
}
`);
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item:focus:before {
border-left-color: ${focusBorderColor};
}
`);
}
}
});
});

View File

@@ -3,12 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/activitybarpart';
import * as nls from 'vs/nls';
import { illegalArgument } from 'vs/base/common/errors';
import { $ } from 'vs/base/browser/builder';
import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { GlobalActivityExtensions, IGlobalActivityRegistry } from 'vs/workbench/common/activity';
import { Registry } from 'vs/platform/registry/common/platform';
@@ -20,43 +17,43 @@ import { IPartService, Parts, Position as SideBarPosition } from 'vs/workbench/s
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ToggleActivityBarVisibilityAction } from 'vs/workbench/browser/actions/toggleActivityBarVisibility';
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme';
import { IThemeService, registerThemingParticipant, ITheme } from 'vs/platform/theme/common/themeService';
import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND } from 'vs/workbench/common/theme';
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { CompositeBar } from 'vs/workbench/browser/parts/compositebar/compositeBar';
import { CompositeBar } from 'vs/workbench/browser/parts/compositeBar';
import { isMacintosh } from 'vs/base/common/platform';
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { scheduleAtNextAnimationFrame, Dimension } from 'vs/base/browser/dom';
import { scheduleAtNextAnimationFrame, Dimension, addClass } from 'vs/base/browser/dom';
import { Color } from 'vs/base/common/color';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import URI from 'vs/base/common/uri';
import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositebar/compositeBarActions';
import { URI } from 'vs/base/common/uri';
import { ToggleCompositePinnedAction, ICompositeBarColors } from 'vs/workbench/browser/parts/compositeBarActions';
import { ViewletDescriptor } from 'vs/workbench/browser/viewlet';
import { IViewsService, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer, TEST_VIEW_CONTAINER_ID, IViewDescriptorCollection } from 'vs/workbench/common/views';
import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IViewlet } from 'vs/workbench/common/viewlet';
interface IPlaceholderComposite {
const SCM_VIEWLET_ID = 'workbench.view.scm';
interface ICachedViewlet {
id: string;
iconUrl: URI;
views?: { when: string }[];
}
export class ActivitybarPart extends Part {
private static readonly ACTION_HEIGHT = 50;
private static readonly PINNED_VIEWLETS = 'workbench.activity.pinnedViewlets';
private static readonly PLACEHOLDER_VIEWLETS = 'workbench.activity.placeholderViewlets';
private static readonly COLORS = {
backgroundColor: ACTIVITY_BAR_FOREGROUND,
badgeBackground: ACTIVITY_BAR_BADGE_BACKGROUND,
badgeForeground: ACTIVITY_BAR_BADGE_FOREGROUND,
dragAndDropBackground: ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND
};
private static readonly CACHED_VIEWLETS = 'workbench.activity.placeholderViewlets';
private dimension: Dimension;
private globalActionBar: ActionBar;
private globalActivityIdToActions: { [globalActivityId: string]: GlobalActivityAction; } = Object.create(null);
private placeholderComposites: IPlaceholderComposite[] = [];
private cachedViewlets: ICachedViewlet[] = [];
private compositeBar: CompositeBar;
private compositeActions: { [compositeId: string]: { activityAction: ViewletActivityAction, pinnedAction: ToggleCompositePinnedAction } } = Object.create(null);
@@ -68,9 +65,11 @@ export class ActivitybarPart extends Part {
@IThemeService themeService: IThemeService,
@ILifecycleService private lifecycleService: ILifecycleService,
@IStorageService private storageService: IStorageService,
@IExtensionService private extensionService: IExtensionService
@IExtensionService private extensionService: IExtensionService,
@IViewsService private viewsService: IViewsService,
@IContextKeyService private contextKeyService: IContextKeyService
) {
super(id, { hasTitle: false }, themeService);
super(id, { hasTitle: false }, themeService, storageService);
this.compositeBar = this._register(this.instantiationService.createInstance(CompositeBar, {
icon: true,
@@ -84,28 +83,20 @@ export class ActivitybarPart extends Part {
getDefaultCompositeId: () => this.viewletService.getDefaultViewletId(),
hidePart: () => this.partService.setSideBarHidden(true),
compositeSize: 50,
colors: ActivitybarPart.COLORS,
colors: theme => this.getActivitybarItemColors(theme),
overflowActionSize: ActivitybarPart.ACTION_HEIGHT
}));
const previousState = this.storageService.get(ActivitybarPart.PLACEHOLDER_VIEWLETS, StorageScope.GLOBAL, void 0);
if (previousState) {
let parsedPreviousState = <IPlaceholderComposite[]>JSON.parse(previousState);
parsedPreviousState.forEach((s) => {
if (typeof s.iconUrl === 'object') {
s.iconUrl = URI.revive(s.iconUrl);
} else {
s.iconUrl = void 0;
}
});
this.placeholderComposites = parsedPreviousState;
} else {
this.placeholderComposites = this.compositeBar.getCompositesFromStorage().map(id => (<IPlaceholderComposite>{ id, iconUrl: void 0 }));
const previousState = this.storageService.get(ActivitybarPart.CACHED_VIEWLETS, StorageScope.GLOBAL, '[]');
this.cachedViewlets = (<ICachedViewlet[]>JSON.parse(previousState)).map(({ id, iconUrl, views }) => ({ id, views, iconUrl: typeof iconUrl === 'object' ? URI.revive(iconUrl) : void 0 }));
for (const cachedViewlet of this.cachedViewlets) {
if (this.shouldBeHidden(cachedViewlet.id, cachedViewlet)) {
this.compositeBar.hideComposite(cachedViewlet.id);
}
}
this.registerListeners();
this.updateCompositebar();
this.updatePlaceholderComposites();
this.onDidRegisterViewlets(viewletService.getAllViewlets());
}
// {{SQL CARBON EDIT}}
@@ -121,10 +112,10 @@ export class ActivitybarPart extends Part {
}
private registerListeners(): void {
this._register(this.viewletService.onDidViewletRegister(() => this.updateCompositebar()));
this._register(this.viewletService.onDidViewletRegister(viewlet => this.onDidRegisterViewlets([viewlet])));
// Activate viewlet action on opening of a viewlet
this._register(this.viewletService.onDidViewletOpen(viewlet => this.compositeBar.activateComposite(viewlet.getId())));
this._register(this.viewletService.onDidViewletOpen(viewlet => this.onDidViewletOpen(viewlet)));
// Deactivate viewlet action on close
this._register(this.viewletService.onDidViewletClose(viewlet => this.compositeBar.deactivateComposite(viewlet.getId())));
@@ -132,7 +123,7 @@ export class ActivitybarPart extends Part {
if (enabled) {
this.compositeBar.addComposite(this.viewletService.getViewlet(id));
} else {
this.removeComposite(id);
this.removeComposite(id, true);
}
}));
@@ -140,8 +131,36 @@ export class ActivitybarPart extends Part {
}
private onDidRegisterExtensions(): void {
this.removeNotExistingPlaceholderComposites();
this.updateCompositebar();
this.removeNotExistingComposites();
for (const viewlet of this.viewletService.getAllViewlets()) {
this.enableCompositeActions(viewlet);
const viewContainer = this.getViewContainer(viewlet.id);
if (viewContainer) {
const viewDescriptors = this.viewsService.getViewDescriptors(viewContainer);
this.onDidChangeActiveViews(viewlet, viewDescriptors);
viewDescriptors.onDidChangeActiveViews(() => this.onDidChangeActiveViews(viewlet, viewDescriptors));
}
}
}
private onDidChangeActiveViews(viewlet: ViewletDescriptor, viewDescriptors: IViewDescriptorCollection): void {
if (viewDescriptors.activeViewDescriptors.length) {
this.compositeBar.addComposite(viewlet);
} else {
this.removeComposite(viewlet.id, true);
}
}
private onDidViewletOpen(viewlet: IViewlet): void {
// Update the composite bar by adding
this.compositeBar.addComposite(this.viewletService.getViewlet(viewlet.getId()));
this.compositeBar.activateComposite(viewlet.getId());
const viewletDescriptor = this.viewletService.getViewlet(viewlet.getId());
const viewContainer = this.getViewContainer(viewletDescriptor.id);
if (viewContainer && this.viewsService.getViewDescriptors(viewContainer).activeViewDescriptors.length === 0) {
// Update the composite bar by hiding
this.removeComposite(viewletDescriptor.id, true);
}
}
showActivity(viewletOrActionId: string, badge: IBadge, clazz?: string, priority?: number): IDisposable {
@@ -168,14 +187,19 @@ export class ActivitybarPart extends Part {
}
createContentArea(parent: HTMLElement): HTMLElement {
const $el = $(parent);
const $result = $('.content').appendTo($el);
const content = document.createElement('div');
addClass(content, 'content');
parent.appendChild(content);
// Top Actionbar with action items for each viewlet action
this.compositeBar.create($result.getHTMLElement());
this.compositeBar.create(content);
// Top Actionbar with action items for each viewlet action
this.createGlobalActivityActionBar($('.global-activity').appendTo($result).getHTMLElement());
const globalActivities = document.createElement('div');
addClass(globalActivities, 'global-activity');
content.appendChild(globalActivities);
this.createGlobalActivityActionBar(globalActivities);
// TODO@Ben: workaround for https://github.com/Microsoft/vscode/issues/45700
// It looks like there are rendering glitches on macOS with Chrome 61 when
@@ -183,7 +207,7 @@ export class ActivitybarPart extends Part {
// The workaround is to promote the element onto its own drawing layer. We do
// this only after the workbench has loaded because otherwise there is ugly flicker.
if (isMacintosh) {
this.lifecycleService.when(LifecyclePhase.Running).then(() => {
this.lifecycleService.when(LifecyclePhase.Restored).then(() => {
scheduleAtNextAnimationFrame(() => { // another delay...
scheduleAtNextAnimationFrame(() => { // ...to prevent more flickering on startup
registerThemingParticipant((theme, collector) => {
@@ -198,26 +222,37 @@ export class ActivitybarPart extends Part {
});
}
return $result.getHTMLElement();
return content;
}
updateStyles(): void {
super.updateStyles();
// Part container
const container = $(this.getContainer());
const container = this.getContainer();
const background = this.getColor(ACTIVITY_BAR_BACKGROUND);
container.style('background-color', background);
container.style.backgroundColor = background;
const borderColor = this.getColor(ACTIVITY_BAR_BORDER) || this.getColor(contrastBorder);
const isPositionLeft = this.partService.getSideBarPosition() === SideBarPosition.LEFT;
container.style('box-sizing', borderColor && isPositionLeft ? 'border-box' : null);
container.style('border-right-width', borderColor && isPositionLeft ? '1px' : null);
container.style('border-right-style', borderColor && isPositionLeft ? 'solid' : null);
container.style('border-right-color', isPositionLeft ? borderColor : null);
container.style('border-left-width', borderColor && !isPositionLeft ? '1px' : null);
container.style('border-left-style', borderColor && !isPositionLeft ? 'solid' : null);
container.style('border-left-color', !isPositionLeft ? borderColor : null);
container.style.boxSizing = borderColor && isPositionLeft ? 'border-box' : null;
container.style.borderRightWidth = borderColor && isPositionLeft ? '1px' : null;
container.style.borderRightStyle = borderColor && isPositionLeft ? 'solid' : null;
container.style.borderRightColor = isPositionLeft ? borderColor : null;
container.style.borderLeftWidth = borderColor && !isPositionLeft ? '1px' : null;
container.style.borderLeftStyle = borderColor && !isPositionLeft ? 'solid' : null;
container.style.borderLeftColor = !isPositionLeft ? borderColor : null;
}
private getActivitybarItemColors(theme: ITheme): ICompositeBarColors {
return <ICompositeBarColors>{
activeForegroundColor: theme.getColor(ACTIVITY_BAR_FOREGROUND),
inactiveForegroundColor: theme.getColor(ACTIVITY_BAR_INACTIVE_FOREGROUND),
badgeBackground: theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND),
badgeForeground: theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND),
dragAndDropBackground: theme.getColor(ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND),
activeBackgroundColor: null, inactiveBackgroundColor: null, activeBorderBottomColor: null,
};
}
private createGlobalActivityActionBar(container: HTMLElement): void {
@@ -228,7 +263,7 @@ export class ActivitybarPart extends Part {
.map(a => new GlobalActivityAction(a));
this.globalActionBar = this._register(new ActionBar(container, {
actionItemProvider: a => this.instantiationService.createInstance(GlobalActivityActionItem, a, ActivitybarPart.COLORS),
actionItemProvider: a => this.instantiationService.createInstance(GlobalActivityActionItem, a, theme => this.getActivitybarItemColors(theme)),
orientation: ActionsOrientation.VERTICAL,
ariaLabel: nls.localize('globalActions', "Global Actions"),
animated: false
@@ -250,9 +285,9 @@ export class ActivitybarPart extends Part {
pinnedAction: new ToggleCompositePinnedAction(viewlet, this.compositeBar)
};
} else {
const placeHolderComposite = this.placeholderComposites.filter(c => c.id === compositeId)[0];
const cachedComposite = this.cachedViewlets.filter(c => c.id === compositeId)[0];
compositeActions = {
activityAction: this.instantiationService.createInstance(PlaceHolderViewletActivityAction, compositeId, placeHolderComposite.iconUrl),
activityAction: this.instantiationService.createInstance(PlaceHolderViewletActivityAction, compositeId, cachedComposite && cachedComposite.iconUrl),
pinnedAction: new PlaceHolderToggleCompositePinnedAction(compositeId, this.compositeBar)
};
}
@@ -263,45 +298,48 @@ export class ActivitybarPart extends Part {
return compositeActions;
}
private updateCompositebar(): void {
const viewlets = this.viewletService.getViewlets();
private onDidRegisterViewlets(viewlets: ViewletDescriptor[]): void {
for (const viewlet of viewlets) {
this.compositeBar.addComposite(viewlet);
// Pin it by default if it is new => it does not has a placeholder
if (this.placeholderComposites.every(c => c.id !== viewlet.id)) {
this.compositeBar.pin(viewlet.id);
}
this.enableCompositeActions(viewlet);
const cachedViewlet = this.cachedViewlets.filter(({ id }) => id === viewlet.id)[0];
const activeViewlet = this.viewletService.getActiveViewlet();
if (activeViewlet && activeViewlet.getId() === viewlet.id) {
this.compositeBar.pin(viewlet.id);
this.compositeBar.activateComposite(viewlet.id);
const isActive = activeViewlet && activeViewlet.getId() === viewlet.id;
if (isActive || !this.shouldBeHidden(viewlet.id, cachedViewlet)) {
this.compositeBar.addComposite(viewlet);
// Pin it by default if it is new
if (!cachedViewlet) {
this.compositeBar.pin(viewlet.id);
}
if (isActive) {
this.compositeBar.activateComposite(viewlet.id);
}
}
}
}
private updatePlaceholderComposites(): void {
const viewlets = this.viewletService.getViewlets();
for (const { id } of this.placeholderComposites) {
private shouldBeHidden(viewletId: string, cachedViewlet: ICachedViewlet): boolean {
return cachedViewlet && cachedViewlet.views && cachedViewlet.views.length
? cachedViewlet.views.every(({ when }) => when && !this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(when)))
: viewletId === TEST_VIEW_CONTAINER_ID /* Hide Test viewlet for the first time or it had no views registered before */;
}
private removeNotExistingComposites(): void {
const viewlets = this.viewletService.getAllViewlets();
for (const { id } of this.compositeBar.getComposites()) {
if (viewlets.every(viewlet => viewlet.id !== id)) {
this.compositeBar.addComposite({ id, name: id, order: void 0 });
this.removeComposite(id, false);
}
}
}
private removeNotExistingPlaceholderComposites(): void {
const viewlets = this.viewletService.getViewlets();
for (const { id } of this.placeholderComposites) {
if (viewlets.every(viewlet => viewlet.id !== id)) {
this.removeComposite(id);
}
private removeComposite(compositeId: string, hide: boolean): void {
if (hide) {
this.compositeBar.hideComposite(compositeId);
} else {
this.compositeBar.removeComposite(compositeId);
}
}
private removeComposite(compositeId: string): void {
this.compositeBar.removeComposite(compositeId);
const compositeActions = this.compositeActions[compositeId];
if (compositeActions) {
compositeActions.activityAction.dispose();
@@ -320,8 +358,12 @@ export class ActivitybarPart extends Part {
}
}
getPinned(): string[] {
return this.viewletService.getViewlets().map(v => v.id).filter(id => this.compositeBar.isPinned(id));
getPinnedViewletIds(): string[] {
const pinnedCompositeIds = this.compositeBar.getPinnedComposites().map(v => v.id);
return this.viewletService.getViewlets()
.filter(v => this.compositeBar.isPinned(v.id))
.sort((v1, v2) => pinnedCompositeIds.indexOf(v1.id) - pinnedCompositeIds.indexOf(v2.id))
.map(v => v.id);
}
layout(dimension: Dimension): Dimension[] {
@@ -344,10 +386,29 @@ export class ActivitybarPart extends Part {
return sizes;
}
shutdown(): void {
const state = this.viewletService.getViewlets().map(viewlet => ({ id: viewlet.id, iconUrl: viewlet.iconUrl }));
this.storageService.store(ActivitybarPart.PLACEHOLDER_VIEWLETS, JSON.stringify(state), StorageScope.GLOBAL);
protected saveState(): void {
const state: ICachedViewlet[] = [];
for (const { id, iconUrl } of this.viewletService.getAllViewlets()) {
const viewContainer = this.getViewContainer(id);
const views: { when: string }[] = [];
if (viewContainer) {
for (const { when } of this.viewsService.getViewDescriptors(viewContainer).allViewDescriptors) {
views.push({ when: when ? when.serialize() : void 0 });
}
}
state.push({ id, iconUrl, views });
}
this.storageService.store(ActivitybarPart.CACHED_VIEWLETS, JSON.stringify(state), StorageScope.GLOBAL);
super.shutdown();
super.saveState();
}
private getViewContainer(viewletId: string): ViewContainer | undefined {
// TODO: @Joao Remove this after moving SCM Viewlet to ViewContainerViewlet - https://github.com/Microsoft/vscode/issues/49054
if (viewletId === SCM_VIEWLET_ID) {
return null;
}
const viewContainerRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
return viewContainerRegistry.get(viewletId);
}
}

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32"><defs><style>.icon-canvas-transparent{fill:#f6f6f6;opacity:0;}.cls-1{fill:#fff;}</style></defs><title>Ellipsis_32x</title><g id="canvas"><path class="icon-canvas-transparent" d="M32,32H0V0H32Z" transform="translate(0 0)"/></g><g id="iconBg"><path class="cls-1" d="M9,16a3,3,0,1,1-3-3A3,3,0,0,1,9,16Zm7-3a3,3,0,1,0,3,3A3,3,0,0,0,16,13Zm10,0a3,3,0,1,0,3,3A3,3,0,0,0,26,13Z" transform="translate(0 0)"/></g></svg>
<svg fill="none" height="28" viewBox="0 0 28 28" width="28" xmlns="http://www.w3.org/2000/svg"><path d="m7.53846 13.7692c0 .5477-.16241 1.0831-.4667 1.5385-.30428.4554-.73678.8104-1.24279 1.02s-1.06281.2644-1.59999.1576c-.53718-.1069-1.03061-.3706-1.41789-.7579s-.65103-.8807-.75788-1.4179-.05201-1.094.15759-1.6c.20959-.506.56453-.9385 1.01993-1.2428s.9908-.4667 1.5385-.4667c.73445 0 1.43881.2918 1.95814.8111.51934.5193.81109 1.2237.81109 1.9581zm6.46154-2.7692c-.5477 0-1.0831.1624-1.5385.4667s-.8103.7368-1.0199 1.2428-.2645 1.0628-.1576 1.6c.1068.5372.3706 1.0306.7579 1.4179.3872.3873.8807.651 1.4179.7579.5371.1068 1.0939.052 1.5999-.1576s.9385-.5646 1.2428-1.02.4667-.9908.4667-1.5385c0-.7344-.2917-1.4388-.8111-1.9581-.5193-.5193-1.2237-.8111-1.9581-.8111zm9.2308 0c-.5477 0-1.0831.1624-1.5385.4667s-.8104.7368-1.02 1.2428-.2644 1.0628-.1576 1.6c.1069.5372.3706 1.0306.7579 1.4179s.8807.651 1.4179.7579c.5372.1068 1.094.052 1.6-.1576s.9385-.5646 1.2428-1.02.4667-.9908.4667-1.5385c0-.7344-.2918-1.4388-.8111-1.9581s-1.2237-.8111-1.9581-.8111z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 493 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import { Action, IAction } from 'vs/base/common/actions';
import { illegalArgument } from 'vs/base/common/errors';
@@ -14,27 +12,29 @@ import { IBadge } from 'vs/workbench/services/activity/common/activity';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ActionBar, ActionsOrientation, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { CompositeActionItem, CompositeOverflowActivityAction, ICompositeActivity, CompositeOverflowActivityActionItem, ActivityAction, ICompositeBar, ICompositeBarColors } from 'vs/workbench/browser/parts/compositebar/compositeBarActions';
import { TPromise } from 'vs/base/common/winjs.base';
import { CompositeActionItem, CompositeOverflowActivityAction, ICompositeActivity, CompositeOverflowActivityActionItem, ActivityAction, ICompositeBar, ICompositeBarColors, DraggedCompositeIdentifier } from 'vs/workbench/browser/parts/compositeBarActions';
import { Dimension, $, addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { Widget } from 'vs/base/browser/ui/widget';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { LocalSelectionTransfer } from 'vs/workbench/browser/dnd';
import { ITheme } from 'vs/platform/theme/common/themeService';
export interface ICompositeBarOptions {
icon: boolean;
storageId: string;
orientation: ActionsOrientation;
colors: ICompositeBarColors;
colors: (theme: ITheme) => ICompositeBarColors;
compositeSize: number;
overflowActionSize: number;
getActivityAction: (compositeId: string) => ActivityAction;
getCompositePinnedAction: (compositeId: string) => Action;
getOnCompositeClickAction: (compositeId: string) => Action;
getContextMenuActions: () => Action[];
openComposite: (compositeId: string) => TPromise<any>;
openComposite: (compositeId: string) => Thenable<any>;
getDefaultCompositeId: () => string;
hidePart: () => TPromise<any>;
hidePart: () => void;
}
export class CompositeBar extends Widget implements ICompositeBar {
@@ -46,26 +46,31 @@ export class CompositeBar extends Widget implements ICompositeBar {
private compositeOverflowActionItem: CompositeOverflowActivityActionItem;
private model: CompositeBarModel;
private storedState: ISerializedCompositeBarItem[];
private visibleComposites: string[];
private compositeSizeInBar: Map<string, number>;
private compositeTransfer: LocalSelectionTransfer<DraggedCompositeIdentifier>;
constructor(
private options: ICompositeBarOptions,
@IInstantiationService private instantiationService: IInstantiationService,
@IStorageService private storageService: IStorageService,
@IStorageService storageService: IStorageService,
@IContextMenuService private contextMenuService: IContextMenuService
) {
super();
this.model = new CompositeBarModel(options);
this.storedState = this.loadCompositeItemsFromStorage();
this.model = new CompositeBarModel(options, storageService);
this.visibleComposites = [];
this.compositeSizeInBar = new Map<string, number>();
this.compositeTransfer = LocalSelectionTransfer.getInstance<DraggedCompositeIdentifier>();
}
getCompositesFromStorage(): string[] {
return this.storedState.map(s => s.id);
getComposites(): ICompositeBarItem[] {
return [...this.model.items];
}
getPinnedComposites(): ICompositeBarItem[] {
return this.model.pinnedItems;
}
create(parent: HTMLElement): HTMLElement {
@@ -76,7 +81,7 @@ export class CompositeBar extends Widget implements ICompositeBar {
return this.compositeOverflowActionItem;
}
const item = this.model.findItem(action.id);
return item && this.instantiationService.createInstance(CompositeActionItem, action, item.pinnedAction, this.options.colors, this.options.icon, this);
return item && this.instantiationService.createInstance(CompositeActionItem, action, item.pinnedAction, () => this.getContextMenuActions(), this.options.colors, this.options.icon, this);
},
orientation: this.options.orientation,
ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"),
@@ -88,12 +93,13 @@ export class CompositeBar extends Widget implements ICompositeBar {
// Allow to drop at the end to move composites to the end
this._register(addDisposableListener(parent, EventType.DROP, (e: DragEvent) => {
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId) {
if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) {
EventHelper.stop(e, true);
CompositeActionItem.clearDraggedComposite();
const targetItem = this.model.items[this.model.items.length - 1];
const draggedCompositeId = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype)[0].id;
this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype);
const targetItem = this.model.visibleItems[this.model.visibleItems.length - 1];
if (targetItem && targetItem.id !== draggedCompositeId) {
this.move(draggedCompositeId, targetItem.id);
}
@@ -113,38 +119,17 @@ export class CompositeBar extends Widget implements ICompositeBar {
if (this.compositeSizeInBar.size === 0) {
// Compute size of each composite by getting the size from the css renderer
// Size is later used for overflow computation
this.computeSizes(this.model.items);
this.computeSizes(this.model.visibleItems);
}
this.updateCompositeSwitcher();
}
addComposite({ id, name, order }: { id: string; name: string, order: number }): void {
const state = this.storedState.filter(s => s.id === id)[0];
const pinned = state ? state.pinned : true;
let index = order >= 0 ? order : this.model.items.length;
if (state) {
// Find the index by looking its previous item
index = 0;
for (let i = this.storedState.indexOf(state) - 1; i >= 0; i--) {
const previousItemId = this.storedState[i].id;
const previousItemIndex = this.model.findIndex(previousItemId);
if (previousItemIndex !== -1) {
index = previousItemIndex + 1;
break;
}
}
}
// Add to the model
if (this.model.add(id, name, order, index)) {
if (this.model.add(id, name, order)) {
this.computeSizes([this.model.findItem(id)]);
if (pinned) {
this.pin(id);
} else {
this.updateCompositeSwitcher();
}
this.updateCompositeSwitcher();
}
}
@@ -161,6 +146,13 @@ export class CompositeBar extends Widget implements ICompositeBar {
}
}
hideComposite(id: string): void {
if (this.model.hide(id)) {
this.resetActiveComposite(id);
this.updateCompositeSwitcher();
}
}
activateComposite(id: string): void {
const previousActiveItem = this.model.activeItem;
if (this.model.activate(id)) {
@@ -200,8 +192,7 @@ export class CompositeBar extends Widget implements ICompositeBar {
this.updateCompositeSwitcher();
if (open) {
this.options.openComposite(compositeId)
.done(() => this.activateComposite(compositeId)); // Activate after opening
this.options.openComposite(compositeId).then(() => this.activateComposite(compositeId)); // Activate after opening
}
}
}
@@ -211,34 +202,38 @@ export class CompositeBar extends Widget implements ICompositeBar {
this.updateCompositeSwitcher();
const defaultCompositeId = this.options.getDefaultCompositeId();
this.resetActiveComposite(compositeId);
}
}
// Case: composite is not the active one or the active one is a different one
// Solv: we do nothing
if (!this.model.activeItem || this.model.activeItem.id !== compositeId) {
return;
}
private resetActiveComposite(compositeId: string) {
const defaultCompositeId = this.options.getDefaultCompositeId();
// Deactivate itself
this.deactivateComposite(compositeId);
// Case: composite is not the active one or the active one is a different one
// Solv: we do nothing
if (!this.model.activeItem || this.model.activeItem.id !== compositeId) {
return;
}
// Case: composite is not the default composite and default composite is still showing
// Solv: we open the default composite
if (defaultCompositeId !== compositeId && this.isPinned(defaultCompositeId)) {
this.options.openComposite(defaultCompositeId);
}
// Deactivate itself
this.deactivateComposite(compositeId);
// Case: we closed the last visible composite
// Solv: we hide the part
else if (this.visibleComposites.length === 1) {
this.options.hidePart();
}
// Case: composite is not the default composite and default composite is still showing
// Solv: we open the default composite
if (defaultCompositeId !== compositeId && this.isPinned(defaultCompositeId)) {
this.options.openComposite(defaultCompositeId);
}
// Case: we closed the default composite
// Solv: we open the next visible composite from top
else {
this.options.openComposite(this.visibleComposites.filter(cid => cid !== compositeId)[0]);
}
// Case: we closed the last visible composite
// Solv: we hide the part
else if (this.visibleComposites.length === 1) {
this.options.hidePart();
}
// Case: we closed the default composite
// Solv: we open the next visible composite from top
else {
this.options.openComposite(this.visibleComposites.filter(cid => cid !== compositeId)[0]);
}
}
@@ -282,7 +277,7 @@ export class CompositeBar extends Widget implements ICompositeBar {
return; // We have not been rendered yet so there is nothing to update.
}
let compositesToShow = this.model.items.filter(item =>
let compositesToShow = this.model.visibleItems.filter(item =>
item.pinned
|| (this.model.activeItem && this.model.activeItem.id === item.id) /* Show the active composite even if it is not pinned */
).map(item => item.id);
@@ -385,11 +380,11 @@ export class CompositeBar extends Widget implements ICompositeBar {
}
// Persist
this.saveCompositeItems();
this.model.saveState();
}
private getOverflowingComposites(): { id: string, name: string }[] {
let overflowingIds = this.model.items.filter(item => item.pinned).map(item => item.id);
let overflowingIds = this.model.visibleItems.filter(item => item.pinned).map(item => item.id);
// Show the active composite even if it is not pinned
if (this.model.activeItem && !this.model.activeItem.pinned) {
@@ -397,16 +392,23 @@ export class CompositeBar extends Widget implements ICompositeBar {
}
overflowingIds = overflowingIds.filter(compositeId => this.visibleComposites.indexOf(compositeId) === -1);
return this.model.items.filter(c => overflowingIds.indexOf(c.id) !== -1);
return this.model.visibleItems.filter(c => overflowingIds.indexOf(c.id) !== -1);
}
private showContextMenu(e: MouseEvent): void {
EventHelper.stop(e, true);
const event = new StandardMouseEvent(e);
const actions: IAction[] = this.model.items
this.contextMenuService.showContextMenu({
getAnchor: () => { return { x: event.posx, y: event.posy }; },
getActions: () => this.getContextMenuActions()
});
}
private getContextMenuActions(): IAction[] {
const actions: IAction[] = this.model.visibleItems
.map(({ id, name, activityAction }) => (<IAction>{
id,
label: name,
label: name || id,
checked: this.isPinned(id),
enabled: activityAction.enabled,
run: () => {
@@ -422,22 +424,7 @@ export class CompositeBar extends Widget implements ICompositeBar {
actions.push(new Separator());
actions.push(...otherActions);
}
this.contextMenuService.showContextMenu({
getAnchor: () => { return { x: event.posx, y: event.posy }; },
getActions: () => TPromise.as(actions),
});
}
private loadCompositeItemsFromStorage(): ISerializedCompositeBarItem[] {
const storedStates = <Array<string | ISerializedCompositeBarItem>>JSON.parse(this.storageService.get(this.options.storageId, StorageScope.GLOBAL, '[]'));
const compositeStates = <ISerializedCompositeBarItem[]>storedStates.map(c =>
typeof c === 'string' /* migration from pinned states to composites states */ ? { id: c, pinned: true } : c);
return compositeStates;
}
private saveCompositeItems(): void {
this.storedState = this.model.toJSON();
this.storageService.store(this.options.storageId, JSON.stringify(this.storedState), StorageScope.GLOBAL);
return actions;
}
}
@@ -445,6 +432,7 @@ interface ISerializedCompositeBarItem {
id: string;
pinned: boolean;
order: number;
visible: boolean;
}
interface ICompositeBarItem extends ISerializedCompositeBarItem {
@@ -456,15 +444,32 @@ interface ICompositeBarItem extends ISerializedCompositeBarItem {
class CompositeBarModel {
readonly items: ICompositeBarItem[] = [];
private readonly options: ICompositeBarOptions;
readonly items: ICompositeBarItem[];
activeItem: ICompositeBarItem;
constructor(private options: ICompositeBarOptions) { }
constructor(
options: ICompositeBarOptions,
private storageService: IStorageService,
) {
this.options = options;
this.items = this.loadItemStates();
}
private createCompositeBarItem(id: string, name: string, order: number, pinned: boolean): ICompositeBarItem {
get visibleItems(): ICompositeBarItem[] {
return this.items.filter(item => item.visible);
}
get pinnedItems(): ICompositeBarItem[] {
return this.items.filter(item => item.visible && item.pinned);
}
private createCompositeBarItem(id: string, name: string, order: number, pinned: boolean, visible: boolean): ICompositeBarItem {
const options = this.options;
return {
id, name, pinned, order, activity: [],
id, name, pinned, order, visible,
activity: [],
get activityAction() {
return options.getActivityAction(id);
},
@@ -474,20 +479,31 @@ class CompositeBarModel {
};
}
add(id: string, name: string, order: number, index: number): boolean {
add(id: string, name: string, order: number): boolean {
const item = this.findItem(id);
if (item) {
item.order = order;
let changed = false;
item.name = name;
return false;
if (!isUndefinedOrNull(order)) {
changed = item.order !== order;
item.order = order;
}
if (!item.visible) {
item.visible = true;
changed = true;
}
return changed;
} else {
if (index === void 0) {
index = 0;
const item = this.createCompositeBarItem(id, name, order, true, true);
if (isUndefinedOrNull(order)) {
this.items.push(item);
} else {
let index = 0;
while (index < this.items.length && this.items[index].order < order) {
index++;
}
this.items.splice(index, 0, item);
}
this.items.splice(index, 0, this.createCompositeBarItem(id, name, order, false));
return true;
}
}
@@ -502,6 +518,19 @@ class CompositeBarModel {
return false;
}
hide(id: string): boolean {
for (const item of this.items) {
if (item.id === id) {
if (item.visible) {
item.visible = false;
return true;
}
return false;
}
}
return false;
}
move(compositeId: string, toCompositeId: string): boolean {
const fromIndex = this.findIndex(compositeId);
@@ -610,7 +639,7 @@ class CompositeBarModel {
return this.items.filter(item => item.id === id)[0];
}
findIndex(id: string): number {
private findIndex(id: string): number {
for (let index = 0; index < this.items.length; index++) {
if (this.items[index].id === id) {
return index;
@@ -619,7 +648,16 @@ class CompositeBarModel {
return -1;
}
toJSON(): ISerializedCompositeBarItem[] {
return this.items.map(({ id, pinned, order }) => ({ id, pinned, order }));
private loadItemStates(): ICompositeBarItem[] {
const storedStates = <Array<string | ISerializedCompositeBarItem>>JSON.parse(this.storageService.get(this.options.storageId, StorageScope.GLOBAL, '[]'));
return <ICompositeBarItem[]>storedStates.map(c => {
const serialized: ISerializedCompositeBarItem = typeof c === 'string' /* migration from pinned states to composites states */ ? { id: c, pinned: true, order: void 0, visible: true } : c;
return this.createCompositeBarItem(serialized.id, void 0, serialized.order, serialized.pinned, isUndefinedOrNull(serialized.visible) ? true : serialized.visible);
});
}
saveState(): void {
const serialized = this.items.map(({ id, pinned, order, visible }) => ({ id, pinned, order, visible }));
this.storageService.store(this.options.storageId, JSON.stringify(serialized), StorageScope.GLOBAL);
}
}

View File

@@ -3,13 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import { Action } from 'vs/base/common/actions';
import { TPromise } from 'vs/base/common/winjs.base';
import * as dom from 'vs/base/browser/dom';
import { Builder, $ } from 'vs/base/browser/builder';
import { BaseActionItem, IBaseActionItemOptions, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { dispose, IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
@@ -22,6 +18,8 @@ import { DelayedDragHandler } from 'vs/base/browser/dnd';
import { IActivity } from 'vs/workbench/common/activity';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { Event, Emitter } from 'vs/base/common/event';
import { DragAndDropObserver, LocalSelectionTransfer } from 'vs/workbench/browser/dnd';
import { Color } from 'vs/base/common/color';
export interface ICompositeActivity {
badge: IBadge;
@@ -112,26 +110,30 @@ export class ActivityAction extends Action {
}
export interface ICompositeBarColors {
backgroundColor: string;
badgeBackground: string;
badgeForeground: string;
dragAndDropBackground: string;
activeBackgroundColor: Color;
inactiveBackgroundColor: Color;
activeBorderBottomColor: Color;
activeForegroundColor: Color;
inactiveForegroundColor: Color;
badgeBackground: Color;
badgeForeground: Color;
dragAndDropBackground: Color;
}
export interface IActivityActionItemOptions extends IBaseActionItemOptions {
icon?: boolean;
colors: ICompositeBarColors;
colors: (theme: ITheme) => ICompositeBarColors;
}
export class ActivityActionItem extends BaseActionItem {
protected $container: Builder;
protected $label: Builder;
protected $badge: Builder;
protected container: HTMLElement;
protected label: HTMLElement;
protected badge: HTMLElement;
protected options: IActivityActionItemOptions;
private $badgeContent: Builder;
private badgeContent: HTMLElement;
private badgeDisposable: IDisposable = Disposable.None;
private mouseUpTimeout: number;
private mouseUpTimeout: any;
constructor(
action: ActivityAction,
@@ -140,9 +142,9 @@ export class ActivityActionItem extends BaseActionItem {
) {
super(null, action, options);
this.themeService.onThemeChange(this.onThemeChange, this, this._callOnDispose);
action.onDidChangeActivity(this.updateActivity, this, this._callOnDispose);
action.onDidChangeBadge(this.updateBadge, this, this._callOnDispose);
this._register(this.themeService.onThemeChange(this.onThemeChange, this));
this._register(action.onDidChangeActivity(this.updateActivity, this));
this._register(action.onDidChangeBadge(this.updateBadge, this));
}
protected get activity(): IActivity {
@@ -151,60 +153,67 @@ export class ActivityActionItem extends BaseActionItem {
protected updateStyles(): void {
const theme = this.themeService.getTheme();
const colors = this.options.colors(theme);
// Label
if (this.$label && this.options.icon) {
const background = theme.getColor(this.options.colors.backgroundColor);
this.$label.style('background-color', background ? background.toString() : null);
if (this.label) {
if (this.options.icon) {
const foreground = this._action.checked ? colors.activeBackgroundColor || colors.activeForegroundColor : colors.inactiveBackgroundColor || colors.inactiveForegroundColor;
this.label.style.backgroundColor = foreground ? foreground.toString() : null;
} else {
const foreground = this._action.checked ? colors.activeForegroundColor : colors.inactiveForegroundColor;
const borderBottomColor = this._action.checked ? colors.activeBorderBottomColor : null;
this.label.style.color = foreground ? foreground.toString() : null;
this.label.style.borderBottomColor = borderBottomColor ? borderBottomColor.toString() : null;
}
}
// Badge
if (this.$badgeContent) {
const badgeForeground = theme.getColor(this.options.colors.badgeForeground);
const badgeBackground = theme.getColor(this.options.colors.badgeBackground);
if (this.badgeContent) {
const badgeForeground = colors.badgeForeground;
const badgeBackground = colors.badgeBackground;
const contrastBorderColor = theme.getColor(contrastBorder);
this.$badgeContent.style('color', badgeForeground ? badgeForeground.toString() : null);
this.$badgeContent.style('background-color', badgeBackground ? badgeBackground.toString() : null);
this.badgeContent.style.color = badgeForeground ? badgeForeground.toString() : null;
this.badgeContent.style.backgroundColor = badgeBackground ? badgeBackground.toString() : null;
this.$badgeContent.style('border-style', contrastBorderColor ? 'solid' : null);
this.$badgeContent.style('border-width', contrastBorderColor ? '1px' : null);
this.$badgeContent.style('border-color', contrastBorderColor ? contrastBorderColor.toString() : null);
this.badgeContent.style.borderStyle = contrastBorderColor ? 'solid' : null;
this.badgeContent.style.borderWidth = contrastBorderColor ? '1px' : null;
this.badgeContent.style.borderColor = contrastBorderColor ? contrastBorderColor.toString() : null;
}
}
render(container: HTMLElement): void {
super.render(container);
this.container = container;
// Make the container tab-able for keyboard navigation
this.$container = $(container).attr({
tabIndex: '0',
role: 'button'
});
this.container.tabIndex = 0;
this.container.setAttribute('role', this.options.icon ? 'button' : 'tab');
// Try hard to prevent keyboard only focus feedback when using mouse
this.$container.on(dom.EventType.MOUSE_DOWN, () => {
this.$container.addClass('clicked');
});
this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_DOWN, () => {
dom.addClass(this.container, 'clicked');
}));
this.$container.on(dom.EventType.MOUSE_UP, () => {
this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_UP, () => {
if (this.mouseUpTimeout) {
clearTimeout(this.mouseUpTimeout);
}
this.mouseUpTimeout = setTimeout(() => {
this.$container.removeClass('clicked');
dom.removeClass(this.container, 'clicked');
}, 800); // delayed to prevent focus feedback from showing on mouse up
});
}));
// Label
this.$label = $('a.action-label').appendTo(this.builder);
this.label = dom.append(this.element, dom.$('a.action-label'));
this.$badge = this.builder.clone().div({ 'class': 'badge' }, badge => {
this.$badgeContent = badge.div({ 'class': 'badge-content' });
});
this.$badge.hide();
// Badge
this.badge = dom.append(this.element, dom.$('.badge'));
this.badgeContent = dom.append(this.badge, dom.$('.badge-content'));
dom.hide(this.badge);
this.updateActivity();
this.updateStyles();
@@ -222,7 +231,7 @@ export class ActivityActionItem extends BaseActionItem {
protected updateBadge(): void {
const action = this.getAction();
if (!this.$badge || !this.$badgeContent || !(action instanceof ActivityAction)) {
if (!this.badge || !this.badgeContent || !(action instanceof ActivityAction)) {
return;
}
@@ -232,8 +241,8 @@ export class ActivityActionItem extends BaseActionItem {
this.badgeDisposable.dispose();
this.badgeDisposable = Disposable.None;
this.$badgeContent.empty();
this.$badge.hide();
dom.clearNode(this.badgeContent);
dom.hide(this.badge);
if (badge) {
@@ -241,35 +250,39 @@ export class ActivityActionItem extends BaseActionItem {
if (badge instanceof NumberBadge) {
if (badge.number) {
let number = badge.number.toString();
if (badge.number > 9999) {
number = nls.localize('largeNumberBadge', '10k+');
} else if (badge.number > 999) {
number = number.charAt(0) + 'k';
if (badge.number > 999) {
const noOfThousands = badge.number / 1000;
const floor = Math.floor(noOfThousands);
if (noOfThousands > floor) {
number = nls.localize('largeNumberBadge1', '{0}k+', floor);
} else {
number = nls.localize('largeNumberBadge2', '{0}k', noOfThousands);
}
}
this.$badgeContent.text(number);
this.$badge.show();
this.badgeContent.textContent = number;
dom.show(this.badge);
}
}
// Text
else if (badge instanceof TextBadge) {
this.$badgeContent.text(badge.text);
this.$badge.show();
this.badgeContent.textContent = badge.text;
dom.show(this.badge);
}
// Text
else if (badge instanceof IconBadge) {
this.$badge.show();
dom.show(this.badge);
}
// Progress
else if (badge instanceof ProgressBadge) {
this.$badge.show();
dom.show(this.badge);
}
if (clazz) {
this.$badge.addClass(clazz);
this.badgeDisposable = toDisposable(() => this.$badge.removeClass(clazz));
dom.addClasses(this.badge, clazz);
this.badgeDisposable = toDisposable(() => dom.removeClasses(this.badge, clazz));
}
}
@@ -287,20 +300,20 @@ export class ActivityActionItem extends BaseActionItem {
this.updateTitle(title);
}
private updateLabel(): void {
protected updateLabel(): void {
if (this.activity.cssClass) {
this.$label.addClass(this.activity.cssClass);
dom.addClasses(this.label, this.activity.cssClass);
}
if (!this.options.icon) {
this.$label.text(this.getAction().label);
this.label.textContent = this.getAction().label;
}
}
private updateTitle(title: string): void {
[this.$label, this.$badge, this.$container].forEach(b => {
if (b) {
b.attr('aria-label', title);
b.title(title);
[this.label, this.badge, this.container].forEach(element => {
if (element) {
element.setAttribute('aria-label', title);
element.title = title;
}
});
}
@@ -312,7 +325,7 @@ export class ActivityActionItem extends BaseActionItem {
clearTimeout(this.mouseUpTimeout);
}
this.$badge.destroy();
this.badge.remove();
}
}
@@ -328,10 +341,10 @@ export class CompositeOverflowActivityAction extends ActivityAction {
});
}
run(event: any): TPromise<any> {
run(event: any): Promise<any> {
this.showMenu();
return TPromise.as(true);
return Promise.resolve(true);
}
}
@@ -344,7 +357,7 @@ export class CompositeOverflowActivityActionItem extends ActivityActionItem {
private getActiveCompositeId: () => string,
private getBadge: (compositeId: string) => IBadge,
private getCompositeOpenAction: (compositeId: string) => Action,
colors: ICompositeBarColors,
colors: (theme: ITheme) => ICompositeBarColors,
@IContextMenuService private contextMenuService: IContextMenuService,
@IThemeService themeService: IThemeService
) {
@@ -359,8 +372,8 @@ export class CompositeOverflowActivityActionItem extends ActivityActionItem {
this.actions = this.getActions();
this.contextMenuService.showContextMenu({
getAnchor: () => this.builder.getHTMLElement(),
getActions: () => TPromise.as(this.actions),
getAnchor: () => this.element,
getActions: () => this.actions,
onHide: () => dispose(this.actions)
});
}
@@ -403,23 +416,32 @@ class ManageExtensionAction extends Action {
super('activitybar.manage.extension', nls.localize('manageExtension', "Manage Extension"));
}
run(id: string): TPromise<any> {
run(id: string): Promise<any> {
return this.commandService.executeCommand('_extensions.manage', id);
}
}
export class DraggedCompositeIdentifier {
constructor(private _compositeId: string) { }
get id(): string {
return this._compositeId;
}
}
export class CompositeActionItem extends ActivityActionItem {
private static manageExtensionAction: ManageExtensionAction;
private static draggedCompositeId: string;
private compositeActivity: IActivity;
private cssClass: string;
private compositeTransfer: LocalSelectionTransfer<DraggedCompositeIdentifier>;
constructor(
private compositeActivityAction: ActivityAction,
private toggleCompositePinnedAction: Action,
colors: ICompositeBarColors,
private contextMenuActionsProvider: () => Action[],
colors: (theme: ITheme) => ICompositeBarColors,
icon: boolean,
private compositeBar: ICompositeBar,
@IContextMenuService private contextMenuService: IContextMenuService,
@@ -430,12 +452,13 @@ export class CompositeActionItem extends ActivityActionItem {
super(compositeActivityAction, { draggable: true, colors, icon }, themeService);
this.cssClass = compositeActivityAction.class;
this.compositeTransfer = LocalSelectionTransfer.getInstance<DraggedCompositeIdentifier>();
if (!CompositeActionItem.manageExtensionAction) {
CompositeActionItem.manageExtensionAction = instantiationService.createInstance(ManageExtensionAction);
}
compositeActivityAction.onDidChangeActivity(() => { this.compositeActivity = null; this.updateActivity(); }, this, this._callOnDispose);
this._register(compositeActivityAction.onDidChangeActivity(() => { this.compositeActivity = null; this.updateActivity(); }, this));
}
protected get activity(): IActivity {
@@ -470,74 +493,67 @@ export class CompositeActionItem extends ActivityActionItem {
render(container: HTMLElement): void {
super.render(container);
this._updateChecked();
this._updateEnabled();
this.updateChecked();
this.updateEnabled();
this.$container.on('contextmenu', e => {
this._register(dom.addDisposableListener(this.container, dom.EventType.CONTEXT_MENU, e => {
dom.EventHelper.stop(e, true);
this.showContextMenu(container);
});
}));
// Allow to drag
this.$container.on(dom.EventType.DRAG_START, (e: DragEvent) => {
this._register(dom.addDisposableListener(this.container, dom.EventType.DRAG_START, (e: DragEvent) => {
e.dataTransfer.effectAllowed = 'move';
this.setDraggedComposite(this.activity.id);
// Registe as dragged to local transfer
this.compositeTransfer.setData([new DraggedCompositeIdentifier(this.activity.id)], DraggedCompositeIdentifier.prototype);
// Trigger the action even on drag start to prevent clicks from failing that started a drag
if (!this.getAction().checked) {
this.getAction().run();
}
});
}));
// Drag enter
let counter = 0; // see https://github.com/Microsoft/vscode/issues/14470
this.$container.on(dom.EventType.DRAG_ENTER, (e: DragEvent) => {
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId && draggedCompositeId !== this.activity.id) {
counter++;
this.updateFromDragging(container, true);
}
});
this._register(new DragAndDropObserver(this.container, {
onDragEnter: e => {
if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype) && this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype)[0].id !== this.activity.id) {
this.updateFromDragging(container, true);
}
},
// Drag leave
this.$container.on(dom.EventType.DRAG_LEAVE, (e: DragEvent) => {
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId) {
counter--;
if (counter === 0) {
onDragLeave: e => {
if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) {
this.updateFromDragging(container, false);
}
},
onDragEnd: e => {
if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) {
this.updateFromDragging(container, false);
this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype);
}
},
onDrop: e => {
dom.EventHelper.stop(e, true);
if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) {
const draggedCompositeId = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype)[0].id;
if (draggedCompositeId !== this.activity.id) {
this.updateFromDragging(container, false);
this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype);
this.compositeBar.move(draggedCompositeId, this.activity.id);
}
}
}
});
// Drag end
this.$container.on(dom.EventType.DRAG_END, (e: DragEvent) => {
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId) {
counter = 0;
this.updateFromDragging(container, false);
CompositeActionItem.clearDraggedComposite();
}
});
// Drop
this.$container.on(dom.EventType.DROP, (e: DragEvent) => {
dom.EventHelper.stop(e, true);
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId && draggedCompositeId !== this.activity.id) {
this.updateFromDragging(container, false);
CompositeActionItem.clearDraggedComposite();
this.compositeBar.move(draggedCompositeId, this.activity.id);
}
});
}));
// Activate on drag over to reveal targets
[this.$badge, this.$label].forEach(b => new DelayedDragHandler(b.getHTMLElement(), () => {
if (!CompositeActionItem.getDraggedCompositeId() && !this.getAction().checked) {
[this.badge, this.label].forEach(b => new DelayedDragHandler(b, () => {
if (!this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype) && !this.getAction().checked) {
this.getAction().run();
}
}));
@@ -547,23 +563,11 @@ export class CompositeActionItem extends ActivityActionItem {
private updateFromDragging(element: HTMLElement, isDragging: boolean): void {
const theme = this.themeService.getTheme();
const dragBackground = theme.getColor(this.options.colors.dragAndDropBackground);
const dragBackground = this.options.colors(theme).dragAndDropBackground;
element.style.backgroundColor = isDragging && dragBackground ? dragBackground.toString() : null;
}
static getDraggedCompositeId(): string {
return CompositeActionItem.draggedCompositeId;
}
private setDraggedComposite(compositeId: string): void {
CompositeActionItem.draggedCompositeId = compositeId;
}
static clearDraggedComposite(): void {
CompositeActionItem.draggedCompositeId = void 0;
}
private showContextMenu(container: HTMLElement): void {
const actions: Action[] = [this.toggleCompositePinnedAction];
if ((<any>this.compositeActivityAction.activity).extensionId) {
@@ -579,52 +583,65 @@ export class CompositeActionItem extends ActivityActionItem {
this.toggleCompositePinnedAction.label = nls.localize('keep', "Keep");
}
const otherActions = this.contextMenuActionsProvider();
if (otherActions.length) {
actions.push(new Separator());
actions.push(...otherActions);
}
const elementPosition = dom.getDomNodePagePosition(container);
const anchor = {
x: Math.floor(elementPosition.left + (elementPosition.width / 2)),
y: elementPosition.top + elementPosition.height
};
this.contextMenuService.showContextMenu({
getAnchor: () => container,
getAnchor: () => anchor,
getActionsContext: () => this.activity.id,
getActions: () => TPromise.as(actions)
getActions: () => actions
});
}
focus(): void {
this.$container.domFocus();
this.container.focus();
}
protected _updateClass(): void {
protected updateClass(): void {
if (this.cssClass) {
this.$label.removeClass(this.cssClass);
dom.removeClasses(this.label, this.cssClass);
}
this.cssClass = this.getAction().class;
if (this.cssClass) {
this.$label.addClass(this.cssClass);
dom.addClasses(this.label, this.cssClass);
}
}
protected _updateChecked(): void {
protected updateChecked(): void {
if (this.getAction().checked) {
this.$container.addClass('checked');
this.$container.attr('aria-label', nls.localize('compositeActive', "{0} active", this.$container.getHTMLElement().title));
dom.addClass(this.container, 'checked');
this.container.setAttribute('aria-label', nls.localize('compositeActive', "{0} active", this.container.title));
} else {
this.$container.removeClass('checked');
this.$container.attr('aria-label', this.$container.getHTMLElement().title);
dom.removeClass(this.container, 'checked');
this.container.setAttribute('aria-label', this.container.title);
}
this.updateStyles();
}
protected _updateEnabled(): void {
protected updateEnabled(): void {
if (this.getAction().enabled) {
this.builder.removeClass('disabled');
dom.removeClass(this.element, 'disabled');
} else {
this.builder.addClass('disabled');
dom.addClass(this.element, 'disabled');
}
}
dispose(): void {
super.dispose();
CompositeActionItem.clearDraggedComposite();
this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype);
this.$label.destroy();
this.label.remove();
}
}
@@ -639,7 +656,7 @@ export class ToggleCompositePinnedAction extends Action {
this.checked = this.activity && this.compositeBar.isPinned(this.activity.id);
}
run(context: string): TPromise<any> {
run(context: string): Promise<any> {
const id = this.activity ? this.activity.id : context;
if (this.compositeBar.isPinned(id)) {
@@ -648,6 +665,6 @@ export class ToggleCompositePinnedAction extends Action {
this.compositeBar.pin(id);
}
return TPromise.as(true);
return Promise.resolve(true);
}
}

View File

@@ -3,17 +3,12 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/compositepart';
import * as nls from 'vs/nls';
import { defaultGenerator } from 'vs/base/common/idGenerator';
import { TPromise } from 'vs/base/common/winjs.base';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Builder, $ } from 'vs/base/browser/builder';
import * as strings from 'vs/base/common/strings';
import { Emitter } from 'vs/base/common/event';
import * as types from 'vs/base/common/types';
import * as errors from 'vs/base/common/errors';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
@@ -35,7 +30,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachProgressBarStyler } from 'vs/platform/theme/common/styler';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { Dimension } from 'vs/base/browser/dom';
import { Dimension, append, $, addClass, hide, show, addClasses } from 'vs/base/browser/dom';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
export interface ICompositeTitleLabel {
@@ -52,13 +48,13 @@ export interface ICompositeTitleLabel {
export abstract class CompositePart<T extends Composite> extends Part {
protected _onDidCompositeOpen = this._register(new Emitter<IComposite>());
protected _onDidCompositeOpen = this._register(new Emitter<{ composite: IComposite, focus: boolean }>());
protected _onDidCompositeClose = this._register(new Emitter<IComposite>());
protected toolBar: ToolBar;
private instantiatedCompositeListeners: IDisposable[];
private mapCompositeToCompositeContainer: { [compositeId: string]: Builder; };
private mapCompositeToCompositeContainer: { [compositeId: string]: HTMLElement; };
private mapActionsBindingToComposite: { [compositeId: string]: () => void; };
private mapProgressServiceToComposite: { [compositeId: string]: IProgressService; };
private activeComposite: Composite;
@@ -88,7 +84,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
id: string,
options: IPartOptions
) {
super(id, options, themeService);
super(id, options, themeService, storageService);
this.instantiatedCompositeListeners = [];
this.mapCompositeToCompositeContainer = {};
@@ -99,7 +95,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
this.lastActiveCompositeId = storageService.get(activeCompositeSettingsKey, StorageScope.WORKSPACE, this.defaultCompositeId);
}
protected openComposite(id: string, focus?: boolean): TPromise<Composite> {
protected openComposite(id: string, focus?: boolean): Composite {
// Check if composite already visible and just focus in that case
if (this.activeComposite && this.activeComposite.getId() === id) {
if (focus) {
@@ -107,66 +103,57 @@ export abstract class CompositePart<T extends Composite> extends Part {
}
// Fullfill promise with composite that is being opened
return TPromise.as(this.activeComposite);
return this.activeComposite;
}
// Open
return this.doOpenComposite(id, focus);
}
private doOpenComposite(id: string, focus?: boolean): TPromise<Composite> {
private doOpenComposite(id: string, focus?: boolean): Composite {
// Use a generated token to avoid race conditions from long running promises
const currentCompositeOpenToken = defaultGenerator.nextId();
this.currentCompositeOpenToken = currentCompositeOpenToken;
// Hide current
let hidePromise: TPromise<Composite>;
if (this.activeComposite) {
hidePromise = this.hideActiveComposite();
} else {
hidePromise = TPromise.as(null);
this.hideActiveComposite();
}
return hidePromise.then(() => {
// Update Title
this.updateTitle(id);
// Update Title
this.updateTitle(id);
// Create composite
const composite = this.createComposite(id, true);
// Create composite
const composite = this.createComposite(id, true);
// Check if another composite opened meanwhile and return in that case
if ((this.currentCompositeOpenToken !== currentCompositeOpenToken) || (this.activeComposite && this.activeComposite.getId() !== composite.getId())) {
return undefined;
}
// Check if another composite opened meanwhile and return in that case
if ((this.currentCompositeOpenToken !== currentCompositeOpenToken) || (this.activeComposite && this.activeComposite.getId() !== composite.getId())) {
return TPromise.as(null);
}
// Check if composite already visible and just focus in that case
if (this.activeComposite && this.activeComposite.getId() === composite.getId()) {
if (focus) {
composite.focus();
}
// Fullfill promise with composite that is being opened
return TPromise.as(composite);
}
// Show Composite and Focus
return this.showComposite(composite).then(() => {
if (focus) {
composite.focus();
}
// Fullfill promise with composite that is being opened
return composite;
});
}).then(composite => {
if (composite) {
this._onDidCompositeOpen.fire(composite);
// Check if composite already visible and just focus in that case
if (this.activeComposite && this.activeComposite.getId() === composite.getId()) {
if (focus) {
composite.focus();
}
this._onDidCompositeOpen.fire({ composite, focus });
return composite;
});
}
// Show Composite and Focus
this.showComposite(composite);
if (focus) {
composite.focus();
}
// Return with the composite that is being opened
if (composite) {
this._onDidCompositeOpen.fire({ composite, focus });
}
return composite;
}
protected createComposite(id: string, isActive?: boolean): Composite {
@@ -199,7 +186,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
throw new Error(strings.format('Unable to find composite with id {0}', id));
}
protected showComposite(composite: Composite): TPromise<void> {
protected showComposite(composite: Composite): void {
// Remember Composite
this.activeComposite = composite;
@@ -215,105 +202,92 @@ export abstract class CompositePart<T extends Composite> extends Part {
// Remember
this.lastActiveCompositeId = this.activeComposite.getId();
let createCompositePromise: TPromise<void>;
// Composites created for the first time
let compositeContainer = this.mapCompositeToCompositeContainer[composite.getId()];
if (!compositeContainer) {
// Build Container off-DOM
compositeContainer = $().div({
'class': ['composite', this.compositeCSSClass],
id: composite.getId()
}, div => {
createCompositePromise = composite.create(div.getHTMLElement()).then(() => {
composite.updateStyles();
});
});
compositeContainer = $('.composite');
addClasses(compositeContainer, this.compositeCSSClass);
compositeContainer.id = composite.getId();
composite.create(compositeContainer);
composite.updateStyles();
// Remember composite container
this.mapCompositeToCompositeContainer[composite.getId()] = compositeContainer;
}
// Composite already exists but is hidden
else {
createCompositePromise = TPromise.as(null);
}
// Report progress for slow loading composites (but only if we did not create the composites before already)
const progressService = this.mapProgressServiceToComposite[composite.getId()];
if (progressService && !compositeContainer) {
this.mapProgressServiceToComposite[composite.getId()].showWhile(createCompositePromise, this.partService.isCreated() ? 800 : 3200 /* less ugly initial startup */);
this.mapProgressServiceToComposite[composite.getId()].showWhile(Promise.resolve(), this.partService.isRestored() ? 800 : 3200 /* less ugly initial startup */);
}
// Fill Content and Actions
return createCompositePromise.then(() => {
// Make sure that the user meanwhile did not open another composite or closed the part containing the composite
if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {
return void 0;
}
// Make sure that the user meanwhile did not open another composite or closed the part containing the composite
if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {
return void 0;
// Take Composite on-DOM and show
this.getContentArea().appendChild(compositeContainer);
show(compositeContainer);
// Setup action runner
this.toolBar.actionRunner = composite.getActionRunner();
// Update title with composite title if it differs from descriptor
const descriptor = this.registry.getComposite(composite.getId());
if (descriptor && descriptor.name !== composite.getTitle()) {
this.updateTitle(composite.getId(), composite.getTitle());
}
// Handle Composite Actions
let actionsBinding = this.mapActionsBindingToComposite[composite.getId()];
if (!actionsBinding) {
actionsBinding = this.collectCompositeActions(composite);
this.mapActionsBindingToComposite[composite.getId()] = actionsBinding;
}
actionsBinding();
if (this.telemetryActionsListener) {
this.telemetryActionsListener.dispose();
this.telemetryActionsListener = null;
}
// Action Run Handling
this.telemetryActionsListener = this.toolBar.actionRunner.onDidRun(e => {
// Check for Error
if (e.error && !errors.isPromiseCanceledError(e.error)) {
this.notificationService.error(e.error);
}
// Take Composite on-DOM and show
compositeContainer.build(this.getContentArea());
compositeContainer.show();
// Setup action runner
this.toolBar.actionRunner = composite.getActionRunner();
// Update title with composite title if it differs from descriptor
const descriptor = this.registry.getComposite(composite.getId());
if (descriptor && descriptor.name !== composite.getTitle()) {
this.updateTitle(composite.getId(), composite.getTitle());
// Log in telemetry
if (this.telemetryService) {
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: this.nameForTelemetry });
}
});
// Handle Composite Actions
let actionsBinding = this.mapActionsBindingToComposite[composite.getId()];
if (!actionsBinding) {
actionsBinding = this.collectCompositeActions(composite);
this.mapActionsBindingToComposite[composite.getId()] = actionsBinding;
}
actionsBinding();
// Indicate to composite that it is now visible
composite.setVisible(true);
if (this.telemetryActionsListener) {
this.telemetryActionsListener.dispose();
this.telemetryActionsListener = null;
}
// Make sure that the user meanwhile did not open another composite or closed the part containing the composite
if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {
return;
}
// Action Run Handling
this.telemetryActionsListener = this.toolBar.actionRunner.onDidRun(e => {
// Check for Error
if (e.error && !errors.isPromiseCanceledError(e.error)) {
this.notificationService.error(e.error);
}
// Log in telemetry
if (this.telemetryService) {
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: this.nameForTelemetry });
}
});
// Indicate to composite that it is now visible
return composite.setVisible(true).then(() => {
// Make sure that the user meanwhile did not open another composite or closed the part containing the composite
if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {
return;
}
// Make sure the composite is layed out
if (this.contentAreaSize) {
composite.layout(this.contentAreaSize);
}
});
}, error => this.onError(error));
// Make sure the composite is layed out
if (this.contentAreaSize) {
composite.layout(this.contentAreaSize);
}
}
protected onTitleAreaUpdate(compositeId: string): void {
@@ -375,9 +349,9 @@ export abstract class CompositePart<T extends Composite> extends Part {
return this.lastActiveCompositeId;
}
protected hideActiveComposite(): TPromise<Composite> {
protected hideActiveComposite(): Composite {
if (!this.activeComposite) {
return TPromise.as(null); // Nothing to do
return undefined; // Nothing to do
}
const composite = this.activeComposite;
@@ -386,65 +360,58 @@ export abstract class CompositePart<T extends Composite> extends Part {
const compositeContainer = this.mapCompositeToCompositeContainer[composite.getId()];
// Indicate to Composite
return composite.setVisible(false).then(() => {
composite.setVisible(false);
// Take Container Off-DOM and hide
compositeContainer.offDOM();
compositeContainer.hide();
// Take Container Off-DOM and hide
compositeContainer.remove();
hide(compositeContainer);
// Clear any running Progress
this.progressBar.stop().hide();
// Clear any running Progress
this.progressBar.stop().hide();
// Empty Actions
this.toolBar.setActions([])();
this._onDidCompositeClose.fire(composite);
// Empty Actions
this.toolBar.setActions([])();
this._onDidCompositeClose.fire(composite);
return composite;
});
return composite;
}
createTitleArea(parent: HTMLElement): HTMLElement {
// Title Area Container
const titleArea = $(parent).div({
'class': ['composite', 'title']
});
const titleArea = append(parent, $('.composite'));
addClass(titleArea, 'title');
// Left Title Label
this.titleLabel = this.createTitleLabel(titleArea.getHTMLElement());
this.titleLabel = this.createTitleLabel(titleArea);
// Right Actions Container
$(titleArea).div({
'class': 'title-actions'
}, div => {
const titleActionsContainer = append(titleArea, $('.title-actions'));
// Toolbar
this.toolBar = this._register(new ToolBar(div.getHTMLElement(), this.contextMenuService, {
actionItemProvider: action => this.actionItemProvider(action as Action),
orientation: ActionsOrientation.HORIZONTAL,
getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id)
}));
});
// Toolbar
this.toolBar = this._register(new ToolBar(titleActionsContainer, this.contextMenuService, {
actionItemProvider: action => this.actionItemProvider(action as Action),
orientation: ActionsOrientation.HORIZONTAL,
getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment()
}));
return titleArea.getHTMLElement();
return titleArea;
}
protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel {
let titleLabel: Builder;
$(parent).div({
'class': 'title-label'
}, div => {
titleLabel = div.element('h2');
});
const titleContainer = append(parent, $('.title-label'));
const titleLabel = append(titleContainer, $('h2'));
const $this = this;
return {
updateTitle: (id, title, keybinding) => {
titleLabel.safeInnerHtml(title);
titleLabel.title(keybinding ? nls.localize('titleTooltip', "{0} ({1})", title, keybinding) : title);
titleLabel.innerHTML = strings.escape(title);
titleLabel.title = keybinding ? nls.localize('titleTooltip', "{0} ({1})", title, keybinding) : title;
},
updateStyles: () => {
titleLabel.style('color', $this.getColor($this.titleForegroundColor));
titleLabel.style.color = $this.getColor($this.titleForegroundColor);
}
};
}
@@ -467,17 +434,13 @@ export abstract class CompositePart<T extends Composite> extends Part {
}
createContentArea(parent: HTMLElement): HTMLElement {
return $(parent).div({
'class': 'content'
}, div => {
this.progressBar = this._register(new ProgressBar(div.getHTMLElement()));
this._register(attachProgressBarStyler(this.progressBar, this.themeService));
this.progressBar.hide();
}).getHTMLElement();
}
const contentContainer = append(parent, $('.content'));
private onError(error: any): void {
this.notificationService.error(types.isString(error) ? new Error(error) : error);
this.progressBar = this._register(new ProgressBar(contentContainer));
this._register(attachProgressBarStyler(this.progressBar, this.themeService));
this.progressBar.hide();
return contentContainer;
}
getProgressIndicator(id: string): IProgressService {
@@ -492,6 +455,10 @@ export abstract class CompositePart<T extends Composite> extends Part {
return [];
}
protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment {
return AnchorAlignment.RIGHT;
}
layout(dimension: Dimension): Dimension[] {
// Pass to super
@@ -506,12 +473,6 @@ export abstract class CompositePart<T extends Composite> extends Part {
return sizes;
}
shutdown(): void {
this.instantiatedComposites.forEach(i => i.shutdown());
super.shutdown();
}
dispose(): void {
this.mapCompositeToCompositeContainer = null;
this.mapProgressServiceToComposite = null;

View File

@@ -2,18 +2,16 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { Panel } from 'vs/workbench/browser/panel';
import { EditorInput, EditorOptions, IEditor, GroupIdentifier, IEditorMemento } from 'vs/workbench/common/editor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { LRUCache } from 'vs/base/common/map';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { once, Event } from 'vs/base/common/event';
import { isEmptyObject } from 'vs/base/common/types';
import { DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor';
@@ -43,16 +41,17 @@ export abstract class BaseEditor extends Panel implements IEditor {
readonly onDidSizeConstraintsChange: Event<{ width: number; height: number; }> = Event.None;
protected _input: EditorInput;
protected _options: EditorOptions;
private _options: EditorOptions;
private _group: IEditorGroup;
constructor(
id: string,
telemetryService: ITelemetryService,
themeService: IThemeService
themeService: IThemeService,
storageService: IStorageService
) {
super(id, telemetryService, themeService);
super(id, telemetryService, themeService, storageService);
}
get input(): EditorInput {
@@ -82,7 +81,7 @@ export abstract class BaseEditor extends Panel implements IEditor {
this._input = input;
this._options = options;
return TPromise.wrap<void>(null);
return Promise.resolve();
}
/**
@@ -105,15 +104,11 @@ export abstract class BaseEditor extends Panel implements IEditor {
this._options = options;
}
create(parent: HTMLElement): void; // create is sync for editors
create(parent: HTMLElement): TPromise<void>;
create(parent: HTMLElement): TPromise<void> {
const res = super.create(parent);
create(parent: HTMLElement): void {
super.create(parent);
// Create Editor
this.createEditor(parent);
return res;
}
/**
@@ -121,15 +116,10 @@ export abstract class BaseEditor extends Panel implements IEditor {
*/
protected abstract createEditor(parent: HTMLElement): void;
setVisible(visible: boolean, group?: IEditorGroup): void; // setVisible is sync for editors
setVisible(visible: boolean, group?: IEditorGroup): TPromise<void>;
setVisible(visible: boolean, group?: IEditorGroup): TPromise<void> {
const promise = super.setVisible(visible);
setVisible(visible: boolean, group?: IEditorGroup): void {
super.setVisible(visible);
// Propagate to Editor
this.setEditorVisible(visible, group);
return promise;
}
/**
@@ -143,28 +133,28 @@ export abstract class BaseEditor extends Panel implements IEditor {
this._group = group;
}
protected getEditorMemento<T>(storageService: IStorageService, editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento<T> {
protected getEditorMemento<T>(editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento<T> {
const mementoKey = `${this.getId()}${key}`;
let editorMemento = BaseEditor.EDITOR_MEMENTOS.get(mementoKey);
if (!editorMemento) {
editorMemento = new EditorMemento(this.getId(), key, this.getMemento(storageService), limit, editorGroupService);
editorMemento = new EditorMemento(this.getId(), key, this.getMemento(StorageScope.WORKSPACE), limit, editorGroupService);
BaseEditor.EDITOR_MEMENTOS.set(mementoKey, editorMemento);
}
return editorMemento;
}
shutdown(): void {
protected saveState(): void {
// Shutdown all editor memento for this editor type
// Save all editor memento for this editor type
BaseEditor.EDITOR_MEMENTOS.forEach(editorMemento => {
if (editorMemento.id === this.getId()) {
editorMemento.shutdown();
editorMemento.saveState();
}
});
super.shutdown();
super.saveState();
}
dispose(): void {
@@ -195,9 +185,9 @@ export class EditorMemento<T> implements IEditorMemento<T> {
return this._id;
}
saveState(group: IEditorGroup, resource: URI, state: T): void;
saveState(group: IEditorGroup, editor: EditorInput, state: T): void;
saveState(group: IEditorGroup, resourceOrEditor: URI | EditorInput, state: T): void {
saveEditorState(group: IEditorGroup, resource: URI, state: T): void;
saveEditorState(group: IEditorGroup, editor: EditorInput, state: T): void;
saveEditorState(group: IEditorGroup, resourceOrEditor: URI | EditorInput, state: T): void {
const resource = this.doGetResource(resourceOrEditor);
if (!resource || !group) {
return; // we are not in a good state to save any state for a resource
@@ -216,14 +206,14 @@ export class EditorMemento<T> implements IEditorMemento<T> {
// Automatically clear when editor input gets disposed if any
if (resourceOrEditor instanceof EditorInput) {
once(resourceOrEditor.onDispose)(() => {
this.clearState(resource);
this.clearEditorState(resource);
});
}
}
loadState(group: IEditorGroup, resource: URI): T;
loadState(group: IEditorGroup, editor: EditorInput): T;
loadState(group: IEditorGroup, resourceOrEditor: URI | EditorInput): T {
loadEditorState(group: IEditorGroup, resource: URI): T;
loadEditorState(group: IEditorGroup, editor: EditorInput): T;
loadEditorState(group: IEditorGroup, resourceOrEditor: URI | EditorInput): T {
const resource = this.doGetResource(resourceOrEditor);
if (!resource || !group) {
return void 0; // we are not in a good state to load any state for a resource
@@ -239,13 +229,21 @@ export class EditorMemento<T> implements IEditorMemento<T> {
return void 0;
}
clearState(resource: URI): void;
clearState(editor: EditorInput): void;
clearState(resourceOrEditor: URI | EditorInput): void {
clearEditorState(resource: URI, group?: IEditorGroup): void;
clearEditorState(editor: EditorInput, group?: IEditorGroup): void;
clearEditorState(resourceOrEditor: URI | EditorInput, group?: IEditorGroup): void {
const resource = this.doGetResource(resourceOrEditor);
if (resource) {
const cache = this.doLoad();
cache.delete(resource.toString());
if (group) {
const resourceViewState = cache.get(resource.toString());
if (resourceViewState) {
delete resourceViewState[group.id];
}
} else {
cache.delete(resource.toString());
}
}
}
@@ -271,7 +269,7 @@ export class EditorMemento<T> implements IEditorMemento<T> {
return this.cache;
}
shutdown(): void {
saveState(): void {
const cache = this.doLoad();
// Cleanup once during shutdown
@@ -300,4 +298,4 @@ export class EditorMemento<T> implements IEditorMemento<T> {
});
});
}
}
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import { BINARY_DIFF_EDITOR_ID } from 'vs/workbench/common/editor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -12,6 +10,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/binaryEditor';
import { IStorageService } from 'vs/platform/storage/common/storage';
/**
* An implementation of editor for diffing binary files like images or videos.
@@ -23,9 +22,10 @@ export class BinaryResourceDiffEditor extends SideBySideEditor {
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService
) {
super(telemetryService, instantiationService, themeService);
super(telemetryService, instantiationService, themeService, storageService);
}
getMetadata(): string {

View File

@@ -3,12 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
import { Builder, $ } from 'vs/base/browser/builder';
import { EditorInput, EditorOptions } from 'vs/workbench/common/editor';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel';
@@ -17,13 +13,15 @@ import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableEle
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ResourceViewerContext, ResourceViewer } from 'vs/workbench/browser/parts/editor/resourceViewer';
import URI from 'vs/base/common/uri';
import { Dimension } from 'vs/base/browser/dom';
import { URI } from 'vs/base/common/uri';
import { Dimension, size, clearNode } from 'vs/base/browser/dom';
import { IFileService } from 'vs/platform/files/common/files';
import { CancellationToken } from 'vs/base/common/cancellation';
import { dispose } from 'vs/base/common/lifecycle';
import { IStorageService } from 'vs/platform/storage/common/storage';
export interface IOpenCallbacks {
openInternal: (input: EditorInput, options: EditorOptions) => void;
openInternal: (input: EditorInput, options: EditorOptions) => Thenable<void>;
openExternal: (uri: URI) => void;
}
@@ -35,9 +33,12 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor {
private readonly _onMetadataChanged: Emitter<void> = this._register(new Emitter<void>());
get onMetadataChanged(): Event<void> { return this._onMetadataChanged.event; }
private readonly _onDidOpenInPlace: Emitter<void> = this._register(new Emitter<void>());
get onDidOpenInPlace(): Event<void> { return this._onDidOpenInPlace.event; }
private callbacks: IOpenCallbacks;
private metadata: string;
private binaryContainer: Builder;
private binaryContainer: HTMLElement;
private scrollbar: DomScrollableElement;
private resourceViewerContext: ResourceViewerContext;
@@ -47,8 +48,9 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor {
telemetryService: ITelemetryService,
themeService: IThemeService,
@IFileService private readonly _fileService: IFileService,
@IStorageService storageService: IStorageService
) {
super(id, telemetryService, themeService);
super(id, telemetryService, themeService, storageService);
this.callbacks = callbacks;
}
@@ -60,14 +62,13 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor {
protected createEditor(parent: HTMLElement): void {
// Container for Binary
const binaryContainerElement = document.createElement('div');
binaryContainerElement.className = 'binary-container';
this.binaryContainer = $(binaryContainerElement);
this.binaryContainer.style('outline', 'none');
this.binaryContainer.tabindex(0); // enable focus support from the editor part (do not remove)
this.binaryContainer = document.createElement('div');
this.binaryContainer.className = 'binary-container';
this.binaryContainer.style.outline = 'none';
this.binaryContainer.tabIndex = 0; // enable focus support from the editor part (do not remove)
// Custom Scrollbars
this.scrollbar = this._register(new DomScrollableElement(binaryContainerElement, { horizontal: ScrollbarVisibility.Auto, vertical: ScrollbarVisibility.Auto }));
this.scrollbar = this._register(new DomScrollableElement(this.binaryContainer, { horizontal: ScrollbarVisibility.Auto, vertical: ScrollbarVisibility.Auto }));
parent.appendChild(this.scrollbar.getDomNode());
}
@@ -82,16 +83,16 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor {
// Assert Model instance
if (!(model instanceof BinaryEditorModel)) {
return TPromise.wrapError<void>(new Error('Unable to open file as binary'));
return Promise.reject(new Error('Unable to open file as binary'));
}
// Render Input
this.resourceViewerContext = ResourceViewer.show(
{ name: model.getName(), resource: model.getResource(), size: model.getSize(), etag: model.getETag(), mime: model.getMime() },
this._fileService,
this.binaryContainer.getHTMLElement(),
this.binaryContainer,
this.scrollbar,
resource => this.callbacks.openInternal(input, options),
resource => this.handleOpenInternalCallback(input, options),
resource => this.callbacks.openExternal(resource),
meta => this.handleMetadataChanged(meta)
);
@@ -101,6 +102,14 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor {
});
}
private handleOpenInternalCallback(input: EditorInput, options: EditorOptions) {
this.callbacks.openInternal(input, options).then(() => {
// Signal to listeners that the binary editor has been opened in-place
this._onDidOpenInPlace.fire();
});
}
private handleMetadataChanged(meta: string): void {
this.metadata = meta;
@@ -116,8 +125,9 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor {
// Clear Meta
this.handleMetadataChanged(null);
// Empty HTML Container
$(this.binaryContainer).empty();
// Clear Resource Viewer
clearNode(this.binaryContainer);
this.resourceViewerContext = dispose(this.resourceViewerContext);
super.clearInput();
}
@@ -125,21 +135,21 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor {
layout(dimension: Dimension): void {
// Pass on to Binary Container
this.binaryContainer.size(dimension.width, dimension.height);
size(this.binaryContainer, dimension.width, dimension.height);
this.scrollbar.scanDomNode();
if (this.resourceViewerContext) {
if (this.resourceViewerContext && this.resourceViewerContext.layout) {
this.resourceViewerContext.layout(dimension);
}
}
focus(): void {
this.binaryContainer.domFocus();
this.binaryContainer.focus();
}
dispose(): void {
this.binaryContainer.remove();
// Destroy Container
this.binaryContainer.destroy();
this.resourceViewerContext = dispose(this.resourceViewerContext);
super.dispose();
}

View File

@@ -3,18 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { BreadcrumbsWidget } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget';
import { IDisposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { GroupIdentifier } from 'vs/workbench/common/editor';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Emitter, Event } from 'vs/base/common/event';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
import * as glob from 'vs/base/common/glob';
import { IDisposable } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { IConfigurationOverrides, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Registry } from 'vs/platform/registry/common/platform';
import { GroupIdentifier } from 'vs/workbench/common/editor';
export const IBreadcrumbsService = createDecorator<IBreadcrumbsService>('IEditorBreadcrumbsService');
@@ -57,8 +56,10 @@ registerSingleton(IBreadcrumbsService, BreadcrumbsService);
export abstract class BreadcrumbsConfig<T> {
name: string;
value: T;
onDidChange: Event<T>;
onDidChange: Event<void>;
abstract getValue(overrides?: IConfigurationOverrides): T;
abstract updateValue(value: T, overrides?: IConfigurationOverrides): Thenable<void>;
abstract dispose(): void;
private constructor() {
@@ -69,30 +70,31 @@ export abstract class BreadcrumbsConfig<T> {
static UseQuickPick = BreadcrumbsConfig._stub<boolean>('breadcrumbs.useQuickPick');
static FilePath = BreadcrumbsConfig._stub<'on' | 'off' | 'last'>('breadcrumbs.filePath');
static SymbolPath = BreadcrumbsConfig._stub<'on' | 'off' | 'last'>('breadcrumbs.symbolPath');
static SymbolSortOrder = BreadcrumbsConfig._stub<'position' | 'name' | 'type'>('breadcrumbs.symbolSortOrder');
static FilterOnType = BreadcrumbsConfig._stub<boolean>('breadcrumbs.filterOnType');
static FileExcludes = BreadcrumbsConfig._stub<glob.IExpression>('files.exclude');
private static _stub<T>(name: string): { bindTo(service: IConfigurationService): BreadcrumbsConfig<T> } {
return {
bindTo(service) {
let value: T = service.getValue(name);
let onDidChange = new Emitter<T>();
let onDidChange = new Emitter<void>();
let listener = service.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(name)) {
value = service.getValue(name);
onDidChange.fire(value);
onDidChange.fire(undefined);
}
});
return {
name,
get value() {
return value;
},
set value(newValue: T) {
service.updateValue(name, newValue);
value = newValue;
},
onDidChange: onDidChange.event,
return new class implements BreadcrumbsConfig<T>{
readonly name = name;
readonly onDidChange = onDidChange.event;
getValue(overrides?: IConfigurationOverrides): T {
return service.getValue(name, overrides);
}
updateValue(newValue: T, overrides?: IConfigurationOverrides): Thenable<void> {
return service.updateValue(name, newValue, overrides);
}
dispose(): void {
listener.dispose();
onDidChange.dispose();
@@ -141,6 +143,22 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfigurat
localize('symbolpath.last', "Only show the current symbol in the breadcrumbs view."),
]
},
'breadcrumbs.symbolSortOrder': {
description: localize('symbolSortOrder', "Controls how symbols are sorted in the breadcrumbs outline view."),
type: 'string',
default: 'position',
enum: ['position', 'name', 'type'],
enumDescriptions: [
localize('symbolSortOrder.position', "Show symbol outline in file position order."),
localize('symbolSortOrder.name', "Show symbol outline in alphabetical order."),
localize('symbolSortOrder.type', "Show symbol outline in symbol type order."),
]
},
// 'breadcrumbs.filterOnType': {
// description: localize('filterOnType', "Controls whether the breadcrumb picker filters or highlights when typing."),
// type: 'boolean',
// default: false
// },
}
});

View File

@@ -3,43 +3,48 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as dom from 'vs/base/browser/dom';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget';
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { tail } from 'vs/base/common/arrays';
import { timeout } from 'vs/base/common/async';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { combinedDisposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { isEqual } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/breadcrumbscontrol';
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { Range } from 'vs/editor/common/core/range';
import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon';
import { symbolKindToCssClass } from 'vs/editor/common/modes';
import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { localize } from 'vs/nls';
import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { FileKind, IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { ColorIdentifier, ColorFunction } from 'vs/platform/theme/common/colorRegistry';
import { attachBreadcrumbsStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { FileLabel } from 'vs/workbench/browser/labels';
import { BreadcrumbsConfig, IBreadcrumbsService } from 'vs/workbench/browser/parts/editor/breadcrumbs';
import { BreadcrumbElement, EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel';
import { createBreadcrumbsPicker, BreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker';
import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView';
import { IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { BreadcrumbsPicker, createBreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker';
import { SideBySideEditorInput } from 'vs/workbench/common/editor';
import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { localize } from 'vs/nls';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { tail } from 'vs/base/common/arrays';
import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor';
class Item extends BreadcrumbsItem {
@@ -116,20 +121,22 @@ export interface IBreadcrumbsControlOptions {
showFileIcons: boolean;
showSymbolIcons: boolean;
showDecorationColors: boolean;
extraClasses: string[];
breadcrumbsBackground: ColorIdentifier | ColorFunction;
}
export class BreadcrumbsControl {
static HEIGHT = 25;
static HEIGHT = 22;
static readonly Payload_Reveal = {};
static readonly Payload_RevealAside = {};
static readonly Payload_Pick = {};
static CK_BreadcrumbsPossible = new RawContextKey('breadcrumbsPossible', false);
static CK_BreadcrumbsVisible = new RawContextKey('breadcrumbsVisible', false);
static CK_BreadcrumbsActive = new RawContextKey('breadcrumbsActive', false);
private readonly _ckBreadcrumbsPossible: IContextKey<boolean>;
private readonly _ckBreadcrumbsVisible: IContextKey<boolean>;
private readonly _ckBreadcrumbsActive: IContextKey<boolean>;
@@ -145,29 +152,31 @@ export class BreadcrumbsControl {
constructor(
container: HTMLElement,
private readonly _options: IBreadcrumbsControlOptions,
private readonly _editorGroup: EditorGroupView,
private readonly _editorGroup: IEditorGroupView,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IContextViewService private readonly _contextViewService: IContextViewService,
@IEditorService private readonly _editorService: IEditorService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IThemeService private readonly _themeService: IThemeService,
@IQuickOpenService private readonly _quickOpenService: IQuickOpenService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IFileService private readonly _fileService: IFileService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IBreadcrumbsService breadcrumbsService: IBreadcrumbsService,
) {
this.domNode = document.createElement('div');
dom.addClass(this.domNode, 'breadcrumbs-control');
dom.addClasses(this.domNode, ..._options.extraClasses);
dom.append(container, this.domNode);
this._widget = new BreadcrumbsWidget(this.domNode);
this._widget.onDidSelectItem(this._onSelectEvent, this, this._disposables);
this._widget.onDidFocusItem(this._onFocusEvent, this, this._disposables);
this._widget.onDidChangeFocus(this._updateCkBreadcrumbsActive, this, this._disposables);
this._disposables.push(attachBreadcrumbsStyler(this._widget, this._themeService));
this._disposables.push(attachBreadcrumbsStyler(this._widget, this._themeService, { breadcrumbsBackground: _options.breadcrumbsBackground }));
this._ckBreadcrumbsPossible = BreadcrumbsControl.CK_BreadcrumbsPossible.bindTo(this._contextKeyService);
this._ckBreadcrumbsVisible = BreadcrumbsControl.CK_BreadcrumbsVisible.bindTo(this._contextKeyService);
this._ckBreadcrumbsActive = BreadcrumbsControl.CK_BreadcrumbsActive.bindTo(this._contextKeyService);
@@ -179,6 +188,7 @@ export class BreadcrumbsControl {
dispose(): void {
this._disposables = dispose(this._disposables);
this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables);
this._ckBreadcrumbsPossible.reset();
this._ckBreadcrumbsVisible.reset();
this._ckBreadcrumbsActive.reset();
this._cfUseQuickPick.dispose();
@@ -201,12 +211,18 @@ export class BreadcrumbsControl {
}
update(): boolean {
const input = this._editorGroup.activeEditor;
this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables);
// honor diff editors and such
let input = this._editorGroup.activeEditor;
if (input instanceof SideBySideEditorInput) {
input = input.master;
}
if (!input || !input.getResource() || (input.getResource().scheme !== Schemas.untitled && !this._fileService.canHandleResource(input.getResource()))) {
// cleanup and return when there is no input or when
// we cannot handle this input
this._ckBreadcrumbsPossible.set(false);
if (!this.isHidden()) {
this.hide();
return true;
@@ -217,9 +233,10 @@ export class BreadcrumbsControl {
dom.toggleClass(this.domNode, 'hidden', false);
this._ckBreadcrumbsVisible.set(true);
this._ckBreadcrumbsPossible.set(true);
let control = this._editorGroup.activeControl.getControl() as ICodeEditor;
let model = new EditorBreadcrumbsModel(input.getResource(), isCodeEditor(control) ? control : undefined, this._workspaceService, this._configurationService);
let editor = this._getActiveCodeEditor();
let model = new EditorBreadcrumbsModel(input.getResource(), editor, this._workspaceService, this._configurationService);
dom.toggleClass(this.domNode, 'relative-path', model.isRelative());
let updateBreadcrumbs = () => {
@@ -243,6 +260,17 @@ export class BreadcrumbsControl {
return true;
}
private _getActiveCodeEditor(): ICodeEditor {
let control = this._editorGroup.activeControl.getControl();
let editor: ICodeEditor;
if (isCodeEditor(control)) {
editor = control as ICodeEditor;
} else if (isDiffEditor(control)) {
editor = control.getModifiedEditor();
}
return editor;
}
private _onFocusEvent(event: IBreadcrumbsItemEvent): void {
if (event.item && this._breadcrumbsPickerShowing) {
return this._widget.setSelection(event.item);
@@ -254,8 +282,15 @@ export class BreadcrumbsControl {
return;
}
this._editorGroup.focus();
const { element } = event.item as Item;
this._editorGroup.focus();
/* __GDPR__
"breadcrumbs/select" : {
"type": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this._telemetryService.publicLog('breadcrumbs/select', { type: element instanceof TreeElement ? 'symbol' : 'file' });
const group = this._getEditorGroup(event.payload);
if (group !== undefined) {
@@ -266,7 +301,7 @@ export class BreadcrumbsControl {
return;
}
if (this._cfUseQuickPick.value) {
if (this._cfUseQuickPick.getValue()) {
// using quick pick
this._widget.setFocused(undefined);
this._widget.setSelection(undefined);
@@ -276,41 +311,86 @@ export class BreadcrumbsControl {
// show picker
let picker: BreadcrumbsPicker;
let editor = this._getActiveCodeEditor();
let editorDecorations: string[] = [];
let editorViewState: ICodeEditorViewState;
this._contextViewService.showContextView({
render: (parent: HTMLElement) => {
picker = createBreadcrumbsPicker(this._instantiationService, parent, element);
let listener = picker.onDidPickElement(data => {
let selectListener = picker.onDidPickElement(data => {
if (data.target) {
editorViewState = undefined;
}
this._contextViewService.hideContextView(this);
this._revealInEditor(event, data.target, this._getEditorGroup(data.payload && data.payload.originalEvent));
this._revealInEditor(event, data.target, this._getEditorGroup(data.payload && data.payload.originalEvent), (data.payload && data.payload.originalEvent && data.payload.originalEvent.middleButton));
/* __GDPR__
"breadcrumbs/open" : {
"type": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this._telemetryService.publicLog('breadcrumbs/open', { type: !data ? 'nothing' : data.target instanceof TreeElement ? 'symbol' : 'file' });
});
let focusListener = picker.onDidFocusElement(data => {
if (!editor || !(data.target instanceof OutlineElement)) {
return;
}
if (!editorViewState) {
editorViewState = editor.saveViewState();
}
const { symbol } = data.target;
editor.revealRangeInCenter(symbol.range, ScrollType.Smooth);
editorDecorations = editor.deltaDecorations(editorDecorations, [{
range: symbol.range,
options: {
className: 'rangeHighlight',
isWholeLine: true
}
}]);
});
this._breadcrumbsPickerShowing = true;
this._updateCkBreadcrumbsActive();
return combinedDisposable([listener, picker]);
return combinedDisposable([selectListener, focusListener, picker]);
},
getAnchor() {
getAnchor: () => {
let maxInnerWidth = window.innerWidth - 8 /*a little less the full widget*/;
let maxHeight = Math.min(window.innerHeight * .7, 300);
let pickerHeight = 330;
let pickerWidth = Math.max(220, dom.getTotalWidth(event.node));
let pickerWidth = Math.min(maxInnerWidth, Math.max(240, maxInnerWidth / 4.17));
let pickerArrowSize = 8;
let pickerArrowOffset: number;
let data = dom.getDomNodePagePosition(event.node.firstChild as HTMLElement);
let y = data.top + data.height - pickerArrowSize;
let y = data.top + data.height + pickerArrowSize;
if (y + maxHeight >= window.innerHeight) {
maxHeight = window.innerHeight - y - 30 /* room for shadow and status bar*/;
}
let x = data.left;
if (x + pickerWidth >= window.innerWidth) {
x = window.innerWidth - pickerWidth;
if (x + pickerWidth >= maxInnerWidth) {
x = maxInnerWidth - pickerWidth;
}
if (event.payload instanceof StandardMouseEvent) {
pickerArrowOffset = event.payload.posx - x - pickerArrowSize;
let maxPickerArrowOffset = pickerWidth - 2 * pickerArrowSize;
pickerArrowOffset = event.payload.posx - x;
if (pickerArrowOffset > maxPickerArrowOffset) {
x = Math.min(maxInnerWidth - pickerWidth, x + pickerArrowOffset - maxPickerArrowOffset);
pickerArrowOffset = maxPickerArrowOffset;
}
} else {
pickerArrowOffset = (data.left + (data.width * .3)) - x;
}
picker.layout(pickerHeight, pickerWidth, pickerArrowSize, Math.max(0, pickerArrowOffset));
picker.setInput(element);
picker.setInput(element, maxHeight, pickerWidth, pickerArrowSize, Math.max(0, pickerArrowOffset));
return { x, y };
},
onHide: (data) => {
if (editor) {
editor.deltaDecorations(editorDecorations, []);
if (editorViewState) {
editor.restoreViewState(editorViewState);
}
}
this._breadcrumbsPickerShowing = false;
this._updateCkBreadcrumbsActive();
if (data === this) {
@@ -326,11 +406,11 @@ export class BreadcrumbsControl {
this._ckBreadcrumbsActive.set(value);
}
private _revealInEditor(event: IBreadcrumbsItemEvent, element: any, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): void {
private _revealInEditor(event: IBreadcrumbsItemEvent, element: any, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE, pinned: boolean = false): void {
if (element instanceof FileElement) {
if (element.kind === FileKind.FILE) {
// open file in editor
this._editorService.openEditor({ resource: element.uri }, group);
// open file in any editor
this._editorService.openEditor({ resource: element.uri, options: { pinned: pinned } }, group);
} else {
// show next picker
let items = this._widget.getItems();
@@ -340,12 +420,15 @@ export class BreadcrumbsControl {
}
} else if (element instanceof OutlineElement) {
// open symbol in editor
// open symbol in code editor
let model = OutlineModel.get(element);
this._editorService.openEditor({
this._codeEditorService.openCodeEditor({
resource: model.textModel.uri,
options: { selection: Range.collapseToStart(element.symbol.selectionRange) }
}, group);
options: {
selection: Range.collapseToStart(element.symbol.selectionRange),
revealInCenterIfOutsideViewport: true
}
}, this._getActiveCodeEditor(), group === SIDE_GROUP);
}
}
@@ -362,16 +445,12 @@ export class BreadcrumbsControl {
//#region commands
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: 'breadcrumbs.focusAndSelect',
title: localize('cmd.focus', "Focus Breadcrumbs")
}
});
// toggle command
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: 'breadcrumbs.toggle',
title: localize('cmd.toggle', "Toggle Breadcrumbs")
title: { value: localize('cmd.toggle', "Toggle Breadcrumbs"), original: 'Toggle Breadcrumbs' },
category: localize('cmd.category', "View")
}
});
MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
@@ -379,42 +458,73 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
order: 99,
command: {
id: 'breadcrumbs.toggle',
title: localize('cmd.toggle', "Toggle Breadcrumbs")
title: localize('miToggleBreadcrumbs', "Toggle &&Breadcrumbs"),
toggled: ContextKeyExpr.equals('config.breadcrumbs.enabled', true)
}
});
CommandsRegistry.registerCommand('breadcrumbs.toggle', accessor => {
let config = accessor.get(IConfigurationService);
let value = BreadcrumbsConfig.IsEnabled.bindTo(config).value;
BreadcrumbsConfig.IsEnabled.bindTo(config).value = !value;
let value = BreadcrumbsConfig.IsEnabled.bindTo(config).getValue();
BreadcrumbsConfig.IsEnabled.bindTo(config).updateValue(!value);
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'breadcrumbs.focus',
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_SEMICOLON,
when: BreadcrumbsControl.CK_BreadcrumbsVisible,
handler(accessor) {
const groups = accessor.get(IEditorGroupsService);
const breadcrumbs = accessor.get(IBreadcrumbsService);
const widget = breadcrumbs.getWidget(groups.activeGroup.id);
// focus/focus-and-select
function focusAndSelectHandler(accessor: ServicesAccessor, select: boolean): void {
// find widget and focus/select
const groups = accessor.get(IEditorGroupsService);
const breadcrumbs = accessor.get(IBreadcrumbsService);
const widget = breadcrumbs.getWidget(groups.activeGroup.id);
if (widget) {
const item = tail(widget.getItems());
widget.setFocused(item);
if (select) {
widget.setSelection(item, BreadcrumbsControl.Payload_Pick);
}
}
}
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: 'breadcrumbs.focusAndSelect',
title: { value: localize('cmd.focus', "Focus Breadcrumbs"), original: 'Focus Breadcrumbs' },
precondition: BreadcrumbsControl.CK_BreadcrumbsVisible
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'breadcrumbs.focusAndSelect',
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT,
when: BreadcrumbsControl.CK_BreadcrumbsVisible,
handler(accessor) {
const groups = accessor.get(IEditorGroupsService);
const breadcrumbs = accessor.get(IBreadcrumbsService);
const widget = breadcrumbs.getWidget(groups.activeGroup.id);
const item = tail(widget.getItems());
widget.setFocused(item);
widget.setSelection(item, BreadcrumbsControl.Payload_Pick);
when: BreadcrumbsControl.CK_BreadcrumbsPossible,
handler: accessor => focusAndSelectHandler(accessor, true)
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'breadcrumbs.focus',
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_SEMICOLON,
when: BreadcrumbsControl.CK_BreadcrumbsPossible,
handler: accessor => focusAndSelectHandler(accessor, false)
});
// this commands is only enabled when breadcrumbs are
// disabled which it then enables and focuses
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'breadcrumbs.toggleToOn',
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT,
when: ContextKeyExpr.not('config.breadcrumbs.enabled'),
handler: async accessor => {
const instant = accessor.get(IInstantiationService);
const config = accessor.get(IConfigurationService);
// check if enabled and iff not enable
const isEnabled = BreadcrumbsConfig.IsEnabled.bindTo(config);
if (!isEnabled.getValue()) {
await isEnabled.updateValue(true);
await timeout(50); // hacky - the widget might not be ready yet...
}
return instant.invokeFunction(focusAndSelectHandler, true);
}
});
// navigation
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'breadcrumbs.focusNext',
weight: KeybindingWeight.WorkbenchContrib,
@@ -492,10 +602,26 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
primary: KeyMod.CtrlCmd | KeyCode.Enter,
when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey),
handler(accessor) {
const groups = accessor.get(IEditorGroupsService);
const breadcrumbs = accessor.get(IBreadcrumbsService);
const widget = breadcrumbs.getWidget(groups.activeGroup.id);
widget.setSelection(widget.getFocused(), BreadcrumbsControl.Payload_RevealAside);
const editors = accessor.get(IEditorService);
const lists = accessor.get(IListService);
const element = <OutlineElement | IFileStat>lists.lastFocusedList.getFocus();
if (element instanceof OutlineElement) {
// open symbol in editor
return editors.openEditor({
resource: OutlineModel.get(element).textModel.uri,
options: { selection: Range.collapseToStart(element.symbol.selectionRange) }
}, SIDE_GROUP);
} else if (URI.isUri(element.resource)) {
// open file in editor
return editors.openEditor({
resource: element.resource,
}, SIDE_GROUP);
} else {
// ignore
return undefined;
}
}
});
//#endregion

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { equals } from 'vs/base/common/arrays';
import { TimeoutTimer } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
@@ -12,9 +10,8 @@ import { size } from 'vs/base/common/collections';
import { onUnexpectedError } from 'vs/base/common/errors';
import { debounceEvent, Emitter, Event } from 'vs/base/common/event';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import * as paths from 'vs/base/common/paths';
import { isEqual } from 'vs/base/common/resources';
import URI from 'vs/base/common/uri';
import { isEqual, dirname } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IPosition } from 'vs/editor/common/core/position';
import { DocumentSymbolProviderRegistry } from 'vs/editor/common/modes';
@@ -82,16 +79,16 @@ export class EditorBreadcrumbsModel {
let result: BreadcrumbElement[] = [];
// file path elements
if (this._cfgFilePath.value === 'on') {
if (this._cfgFilePath.getValue() === 'on') {
result = result.concat(this._fileInfo.path);
} else if (this._cfgFilePath.value === 'last' && this._fileInfo.path.length > 0) {
} else if (this._cfgFilePath.getValue() === 'last' && this._fileInfo.path.length > 0) {
result = result.concat(this._fileInfo.path.slice(-1));
}
// symbol path elements
if (this._cfgSymbolPath.value === 'on') {
if (this._cfgSymbolPath.getValue() === 'on') {
result = result.concat(this._outlineElements);
} else if (this._cfgSymbolPath.value === 'last' && this._outlineElements.length > 0) {
} else if (this._cfgSymbolPath.getValue() === 'last' && this._outlineElements.length > 0) {
result = result.concat(this._outlineElements.slice(-1));
}
@@ -117,7 +114,7 @@ export class EditorBreadcrumbsModel {
break;
}
info.path.unshift(new FileElement(uri, info.path.length === 0 ? FileKind.FILE : FileKind.FOLDER));
uri = uri.with({ path: paths.dirname(uri.path) });
uri = dirname(uri);
}
if (info.folder && workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
@@ -177,7 +174,7 @@ export class EditorBreadcrumbsModel {
this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition()));
this._outlineDisposables.push(this._editor.onDidChangeCursorPosition(_ => {
timeout.cancelAndSet(() => {
if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId()) {
if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId() && this._editor.getModel()) {
this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition()));
}
}, 150);

View File

@@ -3,37 +3,47 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { onDidChangeZoomLevel } from 'vs/base/browser/browser';
import * as dom from 'vs/base/browser/dom';
import { compareFileNames } from 'vs/base/common/comparers';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { createMatches, FuzzyScore, fuzzyScore } from 'vs/base/common/filters';
import * as glob from 'vs/base/common/glob';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { dirname, isEqual } from 'vs/base/common/resources';
import URI from 'vs/base/common/uri';
import { join } from 'vs/base/common/paths';
import { basename, dirname, isEqual } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { IDataSource, IRenderer, ISelectionEvent, ISorter, ITree } from 'vs/base/parts/tree/browser/tree';
import { IDataSource, IFilter, IRenderer, ISorter, ITree } from 'vs/base/parts/tree/browser/tree';
import 'vs/css!./media/breadcrumbscontrol';
import { OutlineElement, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { OutlineDataSource, OutlineItemComparator, OutlineRenderer } from 'vs/editor/contrib/documentSymbols/outlineTree';
import { OutlineDataSource, OutlineItemComparator, OutlineRenderer, OutlineItemCompareType } from 'vs/editor/contrib/documentSymbols/outlineTree';
import { localize } from 'vs/nls';
import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files';
import { IInstantiationService, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation';
import { HighlightingWorkbenchTree, IHighlightingTreeConfiguration, IHighlightingRenderer } from 'vs/platform/list/browser/listService';
import { IThemeService, DARK } from 'vs/platform/theme/common/themeService';
import { FileLabel } from 'vs/workbench/browser/labels';
import { BreadcrumbElement, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel';
import { onUnexpectedError } from 'vs/base/common/errors';
import { breadcrumbsPickerBackground } from 'vs/platform/theme/common/colorRegistry';
import { FuzzyScore, createMatches, fuzzyScore } from 'vs/base/common/filters';
import { IWorkspaceContextService, IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files';
import { IConstructorSignature1, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { HighlightingWorkbenchTree, IHighlighter, IHighlightingTreeConfiguration, IHighlightingTreeOptions } from 'vs/platform/list/browser/listService';
import { breadcrumbsPickerBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { FileLabel } from 'vs/workbench/browser/labels';
import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs';
import { BreadcrumbElement, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel';
import { IFileIconTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
export function createBreadcrumbsPicker(instantiationService: IInstantiationService, parent: HTMLElement, element: BreadcrumbElement): BreadcrumbsPicker {
let ctor: IConstructorSignature1<HTMLElement, BreadcrumbsPicker> = element instanceof FileElement ? BreadcrumbsFilePicker : BreadcrumbsOutlinePicker;
return instantiationService.createInstance(ctor, parent);
}
interface ILayoutInfo {
maxHeight: number;
width: number;
arrowSize: number;
arrowOffset: number;
inputHeight: number;
}
export abstract class BreadcrumbsPicker {
protected readonly _disposables = new Array<IDisposable>();
@@ -42,14 +52,20 @@ export abstract class BreadcrumbsPicker {
protected readonly _treeContainer: HTMLDivElement;
protected readonly _tree: HighlightingWorkbenchTree;
protected readonly _focus: dom.IFocusTracker;
protected readonly _symbolSortOrder: BreadcrumbsConfig<'position' | 'name' | 'type'>;
private _layoutInfo: ILayoutInfo;
private readonly _onDidPickElement = new Emitter<{ target: any, payload: any }>();
readonly onDidPickElement: Event<{ target: any, payload: any }> = this._onDidPickElement.event;
private readonly _onDidFocusElement = new Emitter<{ target: any, payload: any }>();
readonly onDidFocusElement: Event<{ target: any, payload: any }> = this._onDidFocusElement.event;
constructor(
parent: HTMLElement,
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
@IThemeService protected readonly _themeService: IThemeService,
@IWorkbenchThemeService protected readonly _themeService: IWorkbenchThemeService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
) {
this._domNode = document.createElement('div');
this._domNode.className = 'monaco-breadcrumbs-picker show-file-icons';
@@ -57,42 +73,71 @@ export abstract class BreadcrumbsPicker {
this._focus = dom.trackFocus(this._domNode);
this._focus.onDidBlur(_ => this._onDidPickElement.fire({ target: undefined, payload: undefined }), undefined, this._disposables);
this._disposables.push(onDidChangeZoomLevel(_ => this._onDidPickElement.fire({ target: undefined, payload: undefined })));
const theme = this._themeService.getTheme();
const color = theme.getColor(breadcrumbsPickerBackground);
this._arrow = document.createElement('div');
this._arrow.style.width = '0';
this._arrow.style.borderStyle = 'solid';
this._arrow.style.borderWidth = '8px';
this._arrow.className = 'arrow';
this._arrow.style.borderColor = `transparent transparent ${color.toString()}`;
this._domNode.appendChild(this._arrow);
this._treeContainer = document.createElement('div');
this._treeContainer.style.background = color.toString();
this._treeContainer.style.paddingTop = '2px';
this._treeContainer.style.boxShadow = `0px 5px 8px ${(theme.type === DARK ? color.darken(.6) : color.darken(.2))}`;
this._treeContainer.style.boxShadow = `0px 5px 8px ${this._themeService.getTheme().getColor(widgetShadow)}`;
this._domNode.appendChild(this._treeContainer);
const treeConifg = this._completeTreeConfiguration({ dataSource: undefined, renderer: undefined });
this._symbolSortOrder = BreadcrumbsConfig.SymbolSortOrder.bindTo(this._configurationService);
const filterConfig = BreadcrumbsConfig.FilterOnType.bindTo(this._configurationService);
this._disposables.push(filterConfig);
const treeConfig = this._completeTreeConfiguration({ dataSource: undefined, renderer: undefined, highlighter: undefined });
this._tree = this._instantiationService.createInstance(
HighlightingWorkbenchTree,
this._treeContainer,
treeConifg,
{ useShadows: false },
treeConfig,
<IHighlightingTreeOptions>{ useShadows: false, filterOnType: filterConfig.getValue(), showTwistie: false, twistiePixels: 12 },
{ placeholder: localize('placeholder', "Find") }
);
this._disposables.push(this._tree.onDidChangeSelection(e => {
if (e.payload !== this._tree) {
const target = this._getTargetFromSelectionEvent(e);
if (!target) {
return;
const target = this._getTargetFromEvent(e.selection[0], e.payload);
if (target) {
setTimeout(_ => {// need to debounce here because this disposes the tree and the tree doesn't like to be disposed on click
this._onDidPickElement.fire({ target, payload: e.payload });
}, 0);
}
setTimeout(_ => {// need to debounce here because this disposes the tree and the tree doesn't like to be disposed on click
this._onDidPickElement.fire({ target, payload: e.payload });
}, 0);
}
}));
this._disposables.push(this._tree.onDidChangeFocus(e => {
const target = this._getTargetFromEvent(e.focus, e.payload);
if (target) {
this._onDidFocusElement.fire({ target, payload: e.payload });
}
}));
this._disposables.push(this._tree.onDidStartFiltering(() => {
this._layoutInfo.inputHeight = 36;
this._layout();
}));
this._disposables.push(this._tree.onDidExpandItem(() => {
this._layout();
}));
this._disposables.push(this._tree.onDidCollapseItem(() => {
this._layout();
}));
// tree icon theme specials
dom.addClass(this._treeContainer, 'file-icon-themable-tree');
dom.addClass(this._treeContainer, 'show-file-icons');
const onFileIconThemeChange = (fileIconTheme: IFileIconTheme) => {
dom.toggleClass(this._treeContainer, 'align-icons-and-twisties', fileIconTheme.hasFileIcons && !fileIconTheme.hasFolderIcons);
dom.toggleClass(this._treeContainer, 'hide-arrows', fileIconTheme.hidesExplorerArrows === true);
};
this._disposables.push(_themeService.onDidFileIconThemeChange(onFileIconThemeChange));
onFileIconThemeChange(_themeService.getFileIconTheme());
this._domNode.focus();
}
@@ -102,14 +147,20 @@ export abstract class BreadcrumbsPicker {
this._onDidPickElement.dispose();
this._tree.dispose();
this._focus.dispose();
this._symbolSortOrder.dispose();
}
setInput(input: any): void {
setInput(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): void {
let actualInput = this._getInput(input);
this._tree.setInput(actualInput).then(() => {
this._layoutInfo = { maxHeight, width, arrowSize, arrowOffset, inputHeight: 0 };
this._layout();
// use proper selection, reveal
let selection = this._getInitialSelection(this._tree, input);
if (selection) {
this._tree.reveal(selection, .5).then(() => {
return this._tree.reveal(selection, .5).then(() => {
this._tree.setSelection([selection], this._tree);
this._tree.setFocus(selection);
this._tree.domFocus();
@@ -118,25 +169,37 @@ export abstract class BreadcrumbsPicker {
this._tree.focusFirst();
this._tree.setSelection([this._tree.getFocus()], this._tree);
this._tree.domFocus();
return Promise.resolve(null);
}
}, onUnexpectedError);
}
layout(height: number, width: number, arrowSize: number, arrowOffset: number) {
this._domNode.style.height = `${height}px`;
this._domNode.style.width = `${width}px`;
this._arrow.style.borderWidth = `${arrowSize}px`;
this._arrow.style.marginLeft = `${arrowOffset}px`;
private _layout(info: ILayoutInfo = this._layoutInfo): void {
this._treeContainer.style.height = `${height - 2 * arrowSize}px`;
this._treeContainer.style.width = `${width}px`;
let count = 0;
let nav = this._tree.getNavigator(undefined, false);
while (nav.next() && count < 13) { count += 1; }
let headerHeight = 2 * info.arrowSize;
let treeHeight = Math.min(info.maxHeight - headerHeight, count * 22);
let totalHeight = treeHeight + headerHeight;
this._domNode.style.height = `${totalHeight}px`;
this._domNode.style.width = `${info.width}px`;
this._arrow.style.top = `-${2 * info.arrowSize}px`;
this._arrow.style.borderWidth = `${info.arrowSize}px`;
this._arrow.style.marginLeft = `${info.arrowOffset}px`;
this._treeContainer.style.height = `${treeHeight}px`;
this._treeContainer.style.width = `${info.width}px`;
this._tree.layout();
this._layoutInfo = info;
}
protected abstract _getInput(input: BreadcrumbElement): any;
protected abstract _getInitialSelection(tree: ITree, input: BreadcrumbElement): any;
protected abstract _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration;
protected abstract _getTargetFromSelectionEvent(e: ISelectionEvent): any | undefined;
protected abstract _getTargetFromEvent(element: any, payload: any): any | undefined;
}
//#region - Files
@@ -167,7 +230,7 @@ export class FileDataSource implements IDataSource {
getChildren(tree: ITree, element: IWorkspace | IWorkspaceFolder | IFileStat | URI): TPromise<IWorkspaceFolder[] | IFileStat[]> {
if (IWorkspace.isIWorkspace(element)) {
return TPromise.as(element.folders).then(folders => {
return Promise.resolve(element.folders).then(folders => {
for (let child of folders) {
this._parents.set(element, child);
}
@@ -191,13 +254,80 @@ export class FileDataSource implements IDataSource {
}
getParent(tree: ITree, element: IWorkspace | URI | IWorkspaceFolder | IFileStat): TPromise<IWorkspaceFolder | IFileStat> {
return TPromise.as(this._parents.get(element));
return Promise.resolve(this._parents.get(element));
}
}
export class FileRenderer implements IRenderer, IHighlightingRenderer {
export class FileFilter implements IFilter {
private readonly _scores = new Map<string, FuzzyScore>();
private readonly _cachedExpressions = new Map<string, glob.ParsedExpression>();
private readonly _disposables: IDisposable[] = [];
constructor(
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
@IConfigurationService configService: IConfigurationService,
) {
const config = BreadcrumbsConfig.FileExcludes.bindTo(configService);
const update = () => {
_workspaceService.getWorkspace().folders.forEach(folder => {
const excludesConfig = config.getValue({ resource: folder.uri });
if (!excludesConfig) {
return;
}
// adjust patterns to be absolute in case they aren't
// free floating (**/)
const adjustedConfig: glob.IExpression = {};
for (const pattern in excludesConfig) {
if (typeof excludesConfig[pattern] !== 'boolean') {
continue;
}
let patternAbs = pattern.indexOf('**/') !== 0
? join(folder.uri.path, pattern)
: pattern;
adjustedConfig[patternAbs] = excludesConfig[pattern];
}
this._cachedExpressions.set(folder.uri.toString(), glob.parse(adjustedConfig));
});
};
update();
this._disposables.push(
config,
config.onDidChange(update),
_workspaceService.onDidChangeWorkspaceFolders(update)
);
}
dispose(): void {
dispose(this._disposables);
}
isVisible(tree: ITree, element: IWorkspaceFolder | IFileStat): boolean {
if (IWorkspaceFolder.isIWorkspaceFolder(element)) {
// not a file
return true;
}
const folder = this._workspaceService.getWorkspaceFolder(element.resource);
if (!folder || !this._cachedExpressions.has(folder.uri.toString())) {
// no folder or no filer
return true;
}
const expression = this._cachedExpressions.get(folder.uri.toString());
return !expression(element.resource.path, basename(element.resource));
}
}
export class FileHighlighter implements IHighlighter {
getHighlightsStorageKey(element: IFileStat | IWorkspaceFolder): string {
return IWorkspaceFolder.isIWorkspaceFolder(element) ? element.uri.toString() : element.resource.toString();
}
getHighlights(tree: ITree, element: IFileStat | IWorkspaceFolder, pattern: string): FuzzyScore {
return fuzzyScore(pattern, pattern.toLowerCase(), 0, element.name, element.name.toLowerCase(), 0, true);
}
}
export class FileRenderer implements IRenderer {
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@@ -231,29 +361,14 @@ export class FileRenderer implements IRenderer, IHighlightingRenderer {
fileKind,
hidePath: true,
fileDecorations: fileDecorations,
matches: createMatches((this._scores.get(resource.toString()) || [, []])[1])
matches: createMatches((tree as HighlightingWorkbenchTree).getHighlighterScore(element)),
extraClasses: ['picker-item']
});
}
disposeTemplate(tree: ITree, templateId: string, templateData: FileLabel): void {
templateData.dispose();
}
updateHighlights(tree: ITree, pattern: string): any {
let nav = tree.getNavigator(undefined, false);
let topScore: FuzzyScore;
let topElement: any;
while (nav.next()) {
let element = nav.current() as IFileStat | IWorkspaceFolder;
let score = fuzzyScore(pattern, element.name, undefined, true);
this._scores.set(IWorkspaceFolder.isIWorkspaceFolder(element) ? element.uri.toString() : element.resource.toString(), score);
if (!topScore || score && topScore[0] < score[0]) {
topScore = score;
topElement = element;
}
}
return topElement;
}
}
export class FileSorter implements ISorter {
@@ -278,10 +393,11 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker {
constructor(
parent: HTMLElement,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IWorkbenchThemeService themeService: IWorkbenchThemeService,
@IConfigurationService configService: IConfigurationService,
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
) {
super(parent, instantiationService, themeService);
super(parent, instantiationService, themeService, configService);
}
protected _getInput(input: BreadcrumbElement): any {
@@ -308,16 +424,20 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker {
protected _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration {
// todo@joh reuse explorer implementations?
const filter = this._instantiationService.createInstance(FileFilter);
this._disposables.push(filter);
config.dataSource = this._instantiationService.createInstance(FileDataSource);
config.renderer = this._instantiationService.createInstance(FileRenderer);
config.sorter = new FileSorter();
config.highlighter = new FileHighlighter();
config.filter = filter;
return config;
}
protected _getTargetFromSelectionEvent(e: ISelectionEvent): any | undefined {
let [first] = e.selection;
if (first && !IWorkspaceFolder.isIWorkspaceFolder(first) && !(first as IFileStat).isDirectory) {
return new FileElement((first as IFileStat).resource, FileKind.FILE);
protected _getTargetFromEvent(element: any, _payload: any): any | undefined {
if (element && !IWorkspaceFolder.isIWorkspaceFolder(element) && !(element as IFileStat).isDirectory) {
return new FileElement((element as IFileStat).resource, FileKind.FILE);
}
}
}
@@ -325,11 +445,10 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker {
//#region - Symbols
class HighlightingOutlineRenderer extends OutlineRenderer implements IHighlightingRenderer {
updateHighlights(tree: ITree, pattern: string): any {
let model = OutlineModel.get(tree.getInput());
return model.updateMatches(pattern);
class OutlineHighlighter implements IHighlighter {
getHighlights(tree: ITree, element: OutlineElement, pattern: string): FuzzyScore {
OutlineModel.get(element).updateMatches(pattern);
return element.score;
}
}
@@ -348,18 +467,30 @@ export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker {
protected _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration {
config.dataSource = this._instantiationService.createInstance(OutlineDataSource);
config.renderer = this._instantiationService.createInstance(HighlightingOutlineRenderer);
config.sorter = new OutlineItemComparator();
config.renderer = this._instantiationService.createInstance(OutlineRenderer);
config.sorter = new OutlineItemComparator(this._getOutlineItemComparator());
config.highlighter = new OutlineHighlighter();
return config;
}
protected _getTargetFromSelectionEvent(e: ISelectionEvent): any | undefined {
if (e.payload && e.payload.didClickOnTwistie) {
protected _getTargetFromEvent(element: any, payload: any): any | undefined {
if (payload && payload.didClickOnTwistie) {
return;
}
let [first] = e.selection;
if (first instanceof OutlineElement) {
return first;
if (element instanceof OutlineElement) {
return element;
}
}
private _getOutlineItemComparator(): OutlineItemCompareType {
switch (this._symbolSortOrder.getValue()) {
case 'name':
return OutlineItemCompareType.ByName;
case 'type':
return OutlineItemCompareType.ByKind;
case 'position':
default:
return OutlineItemCompareType.ByPosition;
}
}
}

View File

@@ -2,16 +2,16 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Registry } from 'vs/platform/registry/common/platform';
import * as nls from 'vs/nls';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { Action, IAction } from 'vs/base/common/actions';
import { IEditorQuickOpenEntry, IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen';
import { StatusbarItemDescriptor, StatusbarAlignment, IStatusbarRegistry, Extensions as StatusExtensions } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { StatusbarItemDescriptor, IStatusbarRegistry, Extensions as StatusExtensions } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar';
import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { EditorInput, IEditorInputFactory, SideBySideEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor';
import { EditorInput, IEditorInputFactory, SideBySideEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, TextCompareEditorActiveContext } from 'vs/workbench/common/editor';
import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor';
import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
@@ -24,7 +24,7 @@ import { BinaryResourceDiffEditor } from 'vs/workbench/browser/parts/editor/bina
import { ChangeEncodingAction, ChangeEOLAction, ChangeModeAction, EditorStatus } from 'vs/workbench/browser/parts/editor/editorStatus';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
import { Scope, IActionBarRegistry, Extensions as ActionBarExtensions, ActionBarContributor } from 'vs/workbench/browser/actions';
import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { SyncActionDescriptor, MenuRegistry, MenuId, IMenuItem } from 'vs/platform/actions/common/actions';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes';
import {
@@ -38,7 +38,7 @@ import {
SplitEditorUpAction, SplitEditorDownAction, MoveEditorToLeftGroupAction, MoveEditorToRightGroupAction, MoveEditorToAboveGroupAction, MoveEditorToBelowGroupAction, CloseAllEditorGroupsAction,
JoinAllGroupsAction, FocusLeftGroup, FocusAboveGroup, FocusRightGroup, FocusBelowGroup, EditorLayoutSingleAction, EditorLayoutTwoColumnsAction, EditorLayoutThreeColumnsAction, EditorLayoutTwoByTwoGridAction,
EditorLayoutTwoRowsAction, EditorLayoutThreeRowsAction, EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoRowsRightAction, NewEditorGroupLeftAction, NewEditorGroupRightAction,
NewEditorGroupAboveAction, NewEditorGroupBelowAction, SplitEditorOrthogonalAction
NewEditorGroupAboveAction, NewEditorGroupBelowAction, SplitEditorOrthogonalAction, CloseEditorInAllGroupsAction, NavigateToLastEditLocationAction
} from 'vs/workbench/browser/parts/editor/editorActions';
import * as editorCommands from 'vs/workbench/browser/parts/editor/editorCommands';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
@@ -48,6 +48,8 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { isMacintosh } from 'vs/base/common/platform';
import { AllEditorsPicker, ActiveEditorGroupPicker } from 'vs/workbench/browser/parts/editor/editorPicker';
import { Schemas } from 'vs/base/common/network';
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/parts/editor/editorWidgets';
// Register String Editor
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
@@ -217,6 +219,9 @@ class SideBySideEditorInputFactory implements IEditorInputFactory {
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(SideBySideEditorInput.ID, SideBySideEditorInputFactory);
// Register Editor Contributions
registerEditorContribution(OpenWorkspaceButtonContribution);
// Register Editor Status
const statusBar = Registry.as<IStatusbarRegistry>(StatusExtensions.Statusbar);
statusBar.registerStatusbarItem(new StatusbarItemDescriptor(EditorStatus, StatusbarAlignment.RIGHT, 100 /* towards the left of the right hand side */));
@@ -322,6 +327,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(CloseAllEditorsAction,
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseAllEditorGroupsAction, CloseAllEditorGroupsAction.ID, CloseAllEditorGroupsAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W) }), 'View: Close All Editor Groups', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseLeftEditorsInGroupAction, CloseLeftEditorsInGroupAction.ID, CloseLeftEditorsInGroupAction.LABEL), 'View: Close Editors in Group to the Left', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseEditorsInOtherGroupsAction, CloseEditorsInOtherGroupsAction.ID, CloseEditorsInOtherGroupsAction.LABEL), 'View: Close Editors in Other Groups', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseEditorInAllGroupsAction, CloseEditorInAllGroupsAction.ID, CloseEditorInAllGroupsAction.LABEL), 'View: Close Editor in All Groups', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(SplitEditorAction, SplitEditorAction.ID, SplitEditorAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.US_BACKSLASH }), 'View: Split Editor', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(SplitEditorOrthogonalAction, SplitEditorOrthogonalAction.ID, SplitEditorOrthogonalAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_BACKSLASH) }), 'View: Split Editor Orthogonal', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(SplitEditorLeftAction, SplitEditorLeftAction.ID, SplitEditorLeftAction.LABEL), 'View: Split Editor Left', category);
@@ -332,7 +338,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(JoinTwoGroupsAction, J
registry.registerWorkbenchAction(new SyncActionDescriptor(JoinAllGroupsAction, JoinAllGroupsAction.ID, JoinAllGroupsAction.LABEL), 'View: Join Editors of All Groups', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateBetweenGroupsAction, NavigateBetweenGroupsAction.ID, NavigateBetweenGroupsAction.LABEL), 'View: Navigate Between Editor Groups', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(ResetGroupSizesAction, ResetGroupSizesAction.ID, ResetGroupSizesAction.LABEL), 'View: Reset Editor Group Sizes', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(MaximizeGroupAction, MaximizeGroupAction.ID, MaximizeGroupAction.LABEL), 'View: Maximize Editor Group and Hide Sidebar', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(MaximizeGroupAction, MaximizeGroupAction.ID, MaximizeGroupAction.LABEL), 'View: Maximize Editor Group and Hide Side Bar', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(MinimizeOtherGroupsAction, MinimizeOtherGroupsAction.ID, MinimizeOtherGroupsAction.LABEL), 'View: Maximize Editor Group', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(MoveEditorLeftInGroupAction, MoveEditorLeftInGroupAction.ID, MoveEditorLeftInGroupAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.PageUp, mac: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.LeftArrow) } }), 'View: Move Editor Left', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(MoveEditorRightInGroupAction, MoveEditorRightInGroupAction.ID, MoveEditorRightInGroupAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.PageDown, mac: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.RightArrow) } }), 'View: Move Editor Right', category);
@@ -361,8 +367,9 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(NewEditorGroupLeftActi
registry.registerWorkbenchAction(new SyncActionDescriptor(NewEditorGroupRightAction, NewEditorGroupRightAction.ID, NewEditorGroupRightAction.LABEL), 'View: New Editor Group to the Right', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(NewEditorGroupAboveAction, NewEditorGroupAboveAction.ID, NewEditorGroupAboveAction.LABEL), 'View: New Editor Group Above', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(NewEditorGroupBelowAction, NewEditorGroupBelowAction.ID, NewEditorGroupBelowAction.LABEL), 'View: New Editor Group Below', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateForwardAction, NavigateForwardAction.ID, NavigateForwardAction.LABEL, { primary: null, win: { primary: KeyMod.Alt | KeyCode.RightArrow }, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.US_MINUS }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_MINUS } }), 'Go Forward');
registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateBackwardsAction, NavigateBackwardsAction.ID, NavigateBackwardsAction.LABEL, { primary: null, win: { primary: KeyMod.Alt | KeyCode.LeftArrow }, mac: { primary: KeyMod.WinCtrl | KeyCode.US_MINUS }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_MINUS } }), 'Go Back');
registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateForwardAction, NavigateForwardAction.ID, NavigateForwardAction.LABEL, { primary: 0, win: { primary: KeyMod.Alt | KeyCode.RightArrow }, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.US_MINUS }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_MINUS } }), 'Go Forward');
registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateBackwardsAction, NavigateBackwardsAction.ID, NavigateBackwardsAction.LABEL, { primary: 0, win: { primary: KeyMod.Alt | KeyCode.LeftArrow }, mac: { primary: KeyMod.WinCtrl | KeyCode.US_MINUS }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_MINUS } }), 'Go Back');
registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateToLastEditLocationAction, NavigateToLastEditLocationAction.ID, NavigateToLastEditLocationAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_Q) }), 'Go to Last Edit Location');
registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateLastAction, NavigateLastAction.ID, NavigateLastAction.LABEL), 'Go Last');
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenPreviousEditorFromHistoryAction, OpenPreviousEditorFromHistoryAction.ID, OpenPreviousEditorFromHistoryAction.LABEL), 'Open Previous Editor from History');
registry.registerWorkbenchAction(new SyncActionDescriptor(ClearEditorHistoryAction, ClearEditorHistoryAction.ID, ClearEditorHistoryAction.LABEL), 'Clear Editor History');
@@ -438,15 +445,15 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCo
MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.SPLIT_EDITOR_RIGHT, title: nls.localize('splitRight', "Split Right") }, group: '5_split', order: 40 });
// Editor Title Menu
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.TOGGLE_DIFF_INLINE_MODE, title: nls.localize('toggleInlineView', "Toggle Inline View") }, group: '1_diff', order: 10, when: ContextKeyExpr.has('isInDiffEditor') });
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.TOGGLE_DIFF_SIDE_BY_SIDE, title: nls.localize('toggleSideBySideView', "Toggle Side By Side View") }, group: '1_diff', order: 10, when: ContextKeyExpr.has('isInDiffEditor') });
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.SHOW_EDITORS_IN_GROUP, title: nls.localize('showOpenedEditors', "Show Opened Editors") }, group: '3_open', order: 10, when: ContextKeyExpr.has('config.workbench.editor.showTabs') });
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: nls.localize('closeAll', "Close All") }, group: '5_close', order: 10, when: ContextKeyExpr.has('config.workbench.editor.showTabs') });
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.CLOSE_SAVED_EDITORS_COMMAND_ID, title: nls.localize('closeAllSaved', "Close Saved") }, group: '5_close', order: 20, when: ContextKeyExpr.has('config.workbench.editor.showTabs') });
interface IEditorToolItem { id: string; title: string; iconDark: string; iconLight: string; }
function appendEditorToolItem(primary: IEditorToolItem, alternative: IEditorToolItem, when: ContextKeyExpr, order: number): void {
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
function appendEditorToolItem(primary: IEditorToolItem, when: ContextKeyExpr, order: number, alternative?: IEditorToolItem): void {
const item: IMenuItem = {
command: {
id: primary.id,
title: primary.title,
@@ -455,18 +462,23 @@ function appendEditorToolItem(primary: IEditorToolItem, alternative: IEditorTool
light: URI.parse(require.toUrl(`vs/workbench/browser/parts/editor/media/${primary.iconLight}`))
}
},
alt: {
group: 'navigation',
when,
order
};
if (alternative) {
item.alt = {
id: alternative.id,
title: alternative.title,
iconLocation: {
dark: URI.parse(require.toUrl(`vs/workbench/browser/parts/editor/media/${alternative.iconDark}`)),
light: URI.parse(require.toUrl(`vs/workbench/browser/parts/editor/media/${alternative.iconLight}`))
}
},
group: 'navigation',
when,
order
});
};
}
MenuRegistry.appendMenuItem(MenuId.EditorTitle, item);
}
// Editor Title Menu: Split Editor
@@ -476,14 +488,15 @@ appendEditorToolItem(
title: nls.localize('splitEditorRight', "Split Editor Right"),
iconDark: 'split-editor-horizontal-inverse.svg',
iconLight: 'split-editor-horizontal.svg'
}, {
},
ContextKeyExpr.not('splitEditorsVertically'),
100000, // towards the end
{
id: editorCommands.SPLIT_EDITOR_DOWN,
title: nls.localize('splitEditorDown', "Split Editor Down"),
iconDark: 'split-editor-vertical-inverse.svg',
iconLight: 'split-editor-vertical.svg'
},
ContextKeyExpr.not('splitEditorsVertically'),
100000 /* towards the end */
}
);
appendEditorToolItem(
@@ -492,14 +505,15 @@ appendEditorToolItem(
title: nls.localize('splitEditorDown', "Split Editor Down"),
iconDark: 'split-editor-vertical-inverse.svg',
iconLight: 'split-editor-vertical.svg'
}, {
},
ContextKeyExpr.has('splitEditorsVertically'),
100000, // towards the end
{
id: editorCommands.SPLIT_EDITOR_RIGHT,
title: nls.localize('splitEditorRight', "Split Editor Right"),
iconDark: 'split-editor-horizontal-inverse.svg',
iconLight: 'split-editor-horizontal.svg'
},
ContextKeyExpr.has('splitEditorsVertically'),
100000 // towards the end
}
);
// Editor Title Menu: Close Group (tabs disabled)
@@ -509,14 +523,15 @@ appendEditorToolItem(
title: nls.localize('close', "Close"),
iconDark: 'close-big-inverse-alt.svg',
iconLight: 'close-big-alt.svg'
}, {
},
ContextKeyExpr.and(ContextKeyExpr.not('config.workbench.editor.showTabs'), ContextKeyExpr.not('groupActiveEditorDirty')),
1000000, // towards the far end
{
id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID,
title: nls.localize('closeAll', "Close All"),
iconDark: 'closeall-editors-inverse.svg',
iconLight: 'closeall-editors.svg'
},
ContextKeyExpr.and(ContextKeyExpr.not('config.workbench.editor.showTabs'), ContextKeyExpr.not('groupActiveEditorDirty')),
1000000 // towards the end
}
);
appendEditorToolItem(
@@ -525,22 +540,71 @@ appendEditorToolItem(
title: nls.localize('close', "Close"),
iconDark: 'close-dirty-inverse-alt.svg',
iconLight: 'close-dirty-alt.svg'
}, {
},
ContextKeyExpr.and(ContextKeyExpr.not('config.workbench.editor.showTabs'), ContextKeyExpr.has('groupActiveEditorDirty')),
1000000, // towards the far end
{
id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID,
title: nls.localize('closeAll', "Close All"),
iconDark: 'closeall-editors-inverse.svg',
iconLight: 'closeall-editors.svg'
}
);
// Diff Editor Title Menu: Previous Change
appendEditorToolItem(
{
id: editorCommands.GOTO_PREVIOUS_CHANGE,
title: nls.localize('navigate.prev.label', "Previous Change"),
iconDark: 'previous-diff-inverse.svg',
iconLight: 'previous-diff.svg'
},
ContextKeyExpr.and(ContextKeyExpr.not('config.workbench.editor.showTabs'), ContextKeyExpr.has('groupActiveEditorDirty')),
1000000 // towards the end
TextCompareEditorActiveContext,
10
);
// Diff Editor Title Menu: Next Change
appendEditorToolItem(
{
id: editorCommands.GOTO_NEXT_CHANGE,
title: nls.localize('navigate.next.label', "Next Change"),
iconDark: 'next-diff-inverse.svg',
iconLight: 'next-diff.svg'
},
TextCompareEditorActiveContext,
11
);
// Diff Editor Title Menu: Toggle Ignore Trim Whitespace (Enabled)
appendEditorToolItem(
{
id: editorCommands.TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE,
title: nls.localize('ignoreTrimWhitespace.label', "Ignore Trim Whitespace"),
iconDark: 'paragraph-inverse.svg',
iconLight: 'paragraph.svg'
},
ContextKeyExpr.and(TextCompareEditorActiveContext, ContextKeyExpr.notEquals('config.diffEditor.ignoreTrimWhitespace', true)),
20
);
// Diff Editor Title Menu: Toggle Ignore Trim Whitespace (Disabled)
appendEditorToolItem(
{
id: editorCommands.TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE,
title: nls.localize('showTrimWhitespace.label', "Show Trim Whitespace"),
iconDark: 'paragraph-disabled-inverse.svg',
iconLight: 'paragraph-disabled.svg'
},
ContextKeyExpr.and(TextCompareEditorActiveContext, ContextKeyExpr.notEquals('config.diffEditor.ignoreTrimWhitespace', false)),
20
);
// Editor Commands for Command Palette
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.KEEP_EDITOR_COMMAND_ID, title: nls.localize('keepEditor', "Keep Editor"), category }, when: ContextKeyExpr.has('config.workbench.editor.enablePreview') });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: nls.localize('closeEditorsInGroup', "Close All Editors in Group"), category } });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_SAVED_EDITORS_COMMAND_ID, title: nls.localize('closeSavedEditors', "Close Saved Editors in Group"), category } });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, title: nls.localize('closeOtherEditors', "Close Other Editors in Group"), category } });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, title: nls.localize('closeRightEditors', "Close Editors to the Right in Group"), category } });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.KEEP_EDITOR_COMMAND_ID, title: { value: nls.localize('keepEditor', "Keep Editor"), original: 'View: Keep Editor' }, category }, when: ContextKeyExpr.has('config.workbench.editor.enablePreview') });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: { value: nls.localize('closeEditorsInGroup', "Close All Editors in Group"), original: 'View: Close All Editors in Group' }, category } });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_SAVED_EDITORS_COMMAND_ID, title: { value: nls.localize('closeSavedEditors', "Close Saved Editors in Group"), original: 'View: Close Saved Editors in Group' }, category } });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, title: { value: nls.localize('closeOtherEditors', "Close Other Editors in Group"), original: 'View: Close Other Editors in Group' }, category } });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, title: { value: nls.localize('closeRightEditors', "Close Editors to the Right in Group"), original: 'View: Close Editors to the Right in Group' }, category } });
// File menu
MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, {
@@ -676,3 +740,177 @@ MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, {
},
order: 9
});
// Main Menu Bar Contributions:
// Forward/Back
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: '1_fwd_back',
command: {
id: 'workbench.action.navigateBack',
title: nls.localize({ key: 'miBack', comment: ['&& denotes a mnemonic'] }, "&&Back"),
precondition: ContextKeyExpr.has('canNavigateBack')
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: '1_fwd_back',
command: {
id: 'workbench.action.navigateForward',
title: nls.localize({ key: 'miForward', comment: ['&& denotes a mnemonic'] }, "&&Forward"),
precondition: ContextKeyExpr.has('canNavigateForward')
},
order: 2
});
// Switch Editor
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchEditorMenu, {
group: '1_any',
command: {
id: 'workbench.action.nextEditor',
title: nls.localize({ key: 'miNextEditor', comment: ['&& denotes a mnemonic'] }, "&&Next Editor")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchEditorMenu, {
group: '1_any',
command: {
id: 'workbench.action.previousEditor',
title: nls.localize({ key: 'miPreviousEditor', comment: ['&& denotes a mnemonic'] }, "&&Previous Editor")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchEditorMenu, {
group: '2_used',
command: {
id: 'workbench.action.openNextRecentlyUsedEditorInGroup',
title: nls.localize({ key: 'miNextEditorInGroup', comment: ['&& denotes a mnemonic'] }, "&&Next Used Editor in Group")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchEditorMenu, {
group: '2_used',
command: {
id: 'workbench.action.openPreviousRecentlyUsedEditorInGroup',
title: nls.localize({ key: 'miPreviousEditorInGroup', comment: ['&& denotes a mnemonic'] }, "&&Previous Used Editor in Group")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: '2_switch',
title: nls.localize({ key: 'miSwitchEditor', comment: ['&& denotes a mnemonic'] }, "Switch &&Editor"),
submenu: MenuId.MenubarSwitchEditorMenu,
order: 1
});
// Switch Group
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '1_focus_index',
command: {
id: 'workbench.action.focusFirstEditorGroup',
title: nls.localize({ key: 'miFocusFirstGroup', comment: ['&& denotes a mnemonic'] }, "Group &&1")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '1_focus_index',
command: {
id: 'workbench.action.focusSecondEditorGroup',
title: nls.localize({ key: 'miFocusSecondGroup', comment: ['&& denotes a mnemonic'] }, "Group &&2")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '1_focus_index',
command: {
id: 'workbench.action.focusThirdEditorGroup',
title: nls.localize({ key: 'miFocusThirdGroup', comment: ['&& denotes a mnemonic'] }, "Group &&3")
},
order: 3
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '1_focus_index',
command: {
id: 'workbench.action.focusFourthEditorGroup',
title: nls.localize({ key: 'miFocusFourthGroup', comment: ['&& denotes a mnemonic'] }, "Group &&4")
},
order: 4
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '1_focus_index',
command: {
id: 'workbench.action.focusFifthEditorGroup',
title: nls.localize({ key: 'miFocusFifthGroup', comment: ['&& denotes a mnemonic'] }, "Group &&5")
},
order: 5
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '2_next_prev',
command: {
id: 'workbench.action.focusNextGroup',
title: nls.localize({ key: 'miNextGroup', comment: ['&& denotes a mnemonic'] }, "&&Next Group")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '2_next_prev',
command: {
id: 'workbench.action.focusPreviousGroup',
title: nls.localize({ key: 'miPreviousGroup', comment: ['&& denotes a mnemonic'] }, "&&Previous Group")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '3_directional',
command: {
id: 'workbench.action.focusLeftGroup',
title: nls.localize({ key: 'miFocusLeftGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Left")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '3_directional',
command: {
id: 'workbench.action.focusRightGroup',
title: nls.localize({ key: 'miFocusRightGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Right")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '3_directional',
command: {
id: 'workbench.action.focusAboveGroup',
title: nls.localize({ key: 'miFocusAboveGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Above")
},
order: 3
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '3_directional',
command: {
id: 'workbench.action.focusBelowGroup',
title: nls.localize({ key: 'miFocusBelowGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Below")
},
order: 4
});
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: '2_switch',
title: nls.localize({ key: 'miSwitchGroup', comment: ['&& denotes a mnemonic'] }, "Switch &&Group"),
submenu: MenuId.MenubarSwitchGroupMenu,
order: 2
});

View File

@@ -3,9 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { GroupIdentifier, IWorkbenchEditorConfiguration, IWorkbenchEditorPartConfiguration, EditorOptions, TextEditorOptions, IEditorInput, IEditorIdentifier, IEditorCloseEvent } from 'vs/workbench/common/editor';
import { EditorGroup } from 'vs/workbench/common/editor/editorGroup';
import { IEditorGroup, GroupDirection, IAddGroupOptions, IMergeGroupOptions, GroupsOrder, IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
@@ -30,6 +27,7 @@ export interface IEditorPartOptions extends IWorkbenchEditorPartConfiguration {
export const DEFAULT_EDITOR_PART_OPTIONS: IEditorPartOptions = {
showTabs: true,
highlightModifiedTabs: false,
tabCloseButton: 'right',
tabSizing: 'fit',
showIcons: true,
@@ -104,7 +102,7 @@ export interface IEditorGroupsAccessor {
export interface IEditorGroupView extends IDisposable, ISerializableView, IEditorGroup {
readonly group: EditorGroup;
readonly whenRestored: TPromise<void>;
readonly whenRestored: Thenable<void>;
readonly disposed: boolean;
readonly onDidFocus: Event<void>;
@@ -118,8 +116,6 @@ export interface IEditorGroupView extends IDisposable, ISerializableView, IEdito
setActive(isActive: boolean): void;
setLabel(label: string): void;
relayout(): void;
shutdown(): void;
}
export function getActiveTextEditorOptions(group: IEditorGroup, expectedActiveEditor?: IEditorInput, presetOptions?: EditorOptions): EditorOptions {
@@ -159,5 +155,5 @@ export interface EditorGroupsServiceImpl extends IEditorGroupsService {
/**
* A promise that resolves when groups have been restored.
*/
readonly whenRestored: TPromise<void>;
readonly whenRestored: Thenable<void>;
}

View File

@@ -2,9 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import * as nls from 'vs/nls';
import { Action } from 'vs/base/common/actions';
import { mixin } from 'vs/base/common/objects';
@@ -37,7 +35,7 @@ export class ExecuteCommandAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
return this.commandService.executeCommand(this.commandId, this.commandArgs);
}
}
@@ -71,10 +69,10 @@ export class BaseSplitEditorAction extends Action {
}));
}
run(context?: IEditorIdentifier): TPromise<any> {
run(context?: IEditorIdentifier): Promise<any> {
splitEditor(this.editorGroupService, this.direction, context);
return TPromise.as(true);
return Promise.resolve(true);
}
dispose(): void {
@@ -189,7 +187,7 @@ export class JoinTwoGroupsAction extends Action {
super(id, label);
}
run(context?: IEditorIdentifier): TPromise<any> {
run(context?: IEditorIdentifier): Promise<any> {
let sourceGroup: IEditorGroup;
if (context && typeof context.groupId === 'number') {
sourceGroup = this.editorGroupService.getGroup(context.groupId);
@@ -203,11 +201,11 @@ export class JoinTwoGroupsAction extends Action {
if (targetGroup && sourceGroup !== targetGroup) {
this.editorGroupService.mergeGroup(sourceGroup, targetGroup);
return TPromise.as(true);
return Promise.resolve(true);
}
}
return TPromise.as(true);
return Promise.resolve(true);
}
}
@@ -224,10 +222,10 @@ export class JoinAllGroupsAction extends Action {
super(id, label);
}
run(context?: IEditorIdentifier): TPromise<any> {
run(context?: IEditorIdentifier): Promise<any> {
mergeAllGroups(this.editorGroupService);
return TPromise.as(true);
return Promise.resolve(true);
}
}
@@ -244,11 +242,11 @@ export class NavigateBetweenGroupsAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
const nextGroup = this.editorGroupService.findGroup({ location: GroupLocation.NEXT }, this.editorGroupService.activeGroup, true);
nextGroup.focus();
return TPromise.as(true);
return Promise.resolve(true);
}
}
@@ -265,10 +263,10 @@ export class FocusActiveGroupAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
this.editorGroupService.activeGroup.focus();
return TPromise.as(true);
return Promise.resolve(true);
}
}
@@ -283,13 +281,13 @@ export abstract class BaseFocusGroupAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
const group = this.editorGroupService.findGroup(this.scope, this.editorGroupService.activeGroup, true);
if (group) {
group.focus();
}
return TPromise.as(true);
return Promise.resolve(true);
}
}
@@ -425,7 +423,7 @@ export class OpenToSideFromQuickOpenAction extends Action {
this.class = (preferredDirection === GroupDirection.RIGHT) ? 'quick-open-sidebyside-vertical' : 'quick-open-sidebyside-horizontal';
}
run(context: any): TPromise<any> {
run(context: any): Thenable<any> {
const entry = toEditorQuickOpenEntry(context);
if (entry) {
const input = entry.getInput();
@@ -439,7 +437,7 @@ export class OpenToSideFromQuickOpenAction extends Action {
return this.editorService.openEditor(resourceInput, SIDE_GROUP);
}
return TPromise.as(false);
return Promise.resolve(false);
}
}
@@ -474,7 +472,7 @@ export class CloseEditorAction extends Action {
super(id, label, 'close-editor-action');
}
run(context?: IEditorCommandsContext): TPromise<any> {
run(context?: IEditorCommandsContext): Promise<any> {
return this.commandService.executeCommand(CLOSE_EDITOR_COMMAND_ID, void 0, context);
}
}
@@ -492,7 +490,7 @@ export class CloseOneEditorAction extends Action {
super(id, label, 'close-editor-action');
}
run(context?: IEditorCommandsContext): TPromise<any> {
run(context?: IEditorCommandsContext): Thenable<any> {
let group: IEditorGroup;
let editorIndex: number;
if (context) {
@@ -520,7 +518,7 @@ export class CloseOneEditorAction extends Action {
return group.closeEditor(group.activeEditor);
}
return TPromise.as(false);
return Promise.resolve(false);
}
}
@@ -537,7 +535,7 @@ export class RevertAndCloseEditorAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
const activeControl = this.editorService.activeControl;
if (activeControl) {
const editor = activeControl.input;
@@ -553,7 +551,7 @@ export class RevertAndCloseEditorAction extends Action {
});
}
return TPromise.as(false);
return Promise.resolve(false);
}
}
@@ -571,13 +569,13 @@ export class CloseLeftEditorsInGroupAction extends Action {
super(id, label);
}
run(context?: IEditorIdentifier): TPromise<any> {
run(context?: IEditorIdentifier): Thenable<any> {
const { group, editor } = getTarget(this.editorService, this.editorGroupService, context);
if (group && editor) {
return group.closeEditors({ direction: CloseDirection.LEFT, except: editor });
}
return TPromise.as(false);
return Promise.resolve(false);
}
}
@@ -616,7 +614,7 @@ export abstract class BaseCloseAllAction extends Action {
return groupsToClose;
}
run(): TPromise<any> {
run(): Thenable<any> {
// Just close all if there are no or one dirty editor
if (this.textFileService.getDirty().length < 2) {
@@ -629,7 +627,7 @@ export abstract class BaseCloseAllAction extends Action {
return void 0;
}
let saveOrRevertPromise: TPromise<boolean>;
let saveOrRevertPromise: Thenable<boolean>;
if (confirm === ConfirmResult.DONT_SAVE) {
saveOrRevertPromise = this.textFileService.revertAll(null, { soft: true }).then(() => true);
} else {
@@ -646,7 +644,7 @@ export abstract class BaseCloseAllAction extends Action {
});
}
protected abstract doCloseAll(): TPromise<any>;
protected abstract doCloseAll(): Thenable<any>;
}
export class CloseAllEditorsAction extends BaseCloseAllAction {
@@ -663,8 +661,8 @@ export class CloseAllEditorsAction extends BaseCloseAllAction {
super(id, label, 'action-close-all-files', textFileService, editorGroupService);
}
protected doCloseAll(): TPromise<any> {
return TPromise.join(this.groupsToClose.map(g => g.closeAllEditors()));
protected doCloseAll(): Promise<any> {
return Promise.all(this.groupsToClose.map(g => g.closeAllEditors()));
}
}
@@ -682,8 +680,8 @@ export class CloseAllEditorGroupsAction extends BaseCloseAllAction {
super(id, label, void 0, textFileService, editorGroupService);
}
protected doCloseAll(): TPromise<any> {
return TPromise.join(this.groupsToClose.map(g => g.closeAllEditors())).then(() => {
protected doCloseAll(): Promise<any> {
return Promise.all(this.groupsToClose.map(g => g.closeAllEditors())).then(() => {
this.groupsToClose.forEach(group => this.editorGroupService.removeGroup(group));
});
}
@@ -702,11 +700,11 @@ export class CloseEditorsInOtherGroupsAction extends Action {
super(id, label);
}
run(context?: IEditorIdentifier): TPromise<any> {
run(context?: IEditorIdentifier): Thenable<any> {
const groupToSkip = context ? this.editorGroupService.getGroup(context.groupId) : this.editorGroupService.activeGroup;
return TPromise.join(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(g => {
return Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(g => {
if (g.id === groupToSkip.id) {
return TPromise.as(null);
return Promise.resolve(null);
}
return g.closeAllEditors();
@@ -714,6 +712,30 @@ export class CloseEditorsInOtherGroupsAction extends Action {
}
}
export class CloseEditorInAllGroupsAction extends Action {
static readonly ID = 'workbench.action.closeEditorInAllGroups';
static readonly LABEL = nls.localize('closeEditorInAllGroups', "Close Editor in All Groups");
constructor(
id: string,
label: string,
@IEditorGroupsService private editorGroupService: IEditorGroupsService,
@IEditorService private editorService: IEditorService
) {
super(id, label);
}
run(): Thenable<any> {
const activeEditor = this.editorService.activeEditor;
if (activeEditor) {
return Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(g => g.closeEditor(activeEditor)));
}
return Promise.resolve(null);
}
}
export class BaseMoveGroupAction extends Action {
constructor(
@@ -725,7 +747,7 @@ export class BaseMoveGroupAction extends Action {
super(id, label);
}
run(context?: IEditorIdentifier): TPromise<any> {
run(context?: IEditorIdentifier): Promise<any> {
let sourceGroup: IEditorGroup;
if (context && typeof context.groupId === 'number') {
sourceGroup = this.editorGroupService.getGroup(context.groupId);
@@ -738,7 +760,7 @@ export class BaseMoveGroupAction extends Action {
this.editorGroupService.moveGroup(sourceGroup, targetGroup, this.direction);
}
return TPromise.as(true);
return Promise.resolve(true);
}
private findTargetGroup(sourceGroup: IEditorGroup): IEditorGroup {
@@ -834,10 +856,10 @@ export class MinimizeOtherGroupsAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
this.editorGroupService.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS);
return TPromise.as(false);
return Promise.resolve(false);
}
}
@@ -850,10 +872,10 @@ export class ResetGroupSizesAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
this.editorGroupService.arrangeGroups(GroupsArrangement.EVEN);
return TPromise.as(false);
return Promise.resolve(false);
}
}
@@ -872,14 +894,13 @@ export class MaximizeGroupAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
if (this.editorService.activeEditor) {
this.editorGroupService.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS);
return this.partService.setSideBarHidden(true);
this.partService.setSideBarHidden(true);
}
return TPromise.as(false);
return Promise.resolve(false);
}
}
@@ -894,15 +915,15 @@ export abstract class BaseNavigateEditorAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
const result = this.navigate();
if (!result) {
return TPromise.as(false);
return Promise.resolve(false);
}
const { groupId, editor } = result;
if (!editor) {
return TPromise.as(false);
return Promise.resolve(false);
}
const group = this.editorGroupService.getGroup(groupId);
@@ -1081,10 +1102,10 @@ export class NavigateForwardAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
this.historyService.forward();
return TPromise.as(null);
return Promise.resolve(null);
}
}
@@ -1097,10 +1118,26 @@ export class NavigateBackwardsAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
this.historyService.back();
return TPromise.as(null);
return Promise.resolve(null);
}
}
export class NavigateToLastEditLocationAction extends Action {
static readonly ID = 'workbench.action.navigateToLastEditLocation';
static readonly LABEL = nls.localize('navigateToLastEditLocation', "Go to Last Edit Location");
constructor(id: string, label: string, @IHistoryService private historyService: IHistoryService) {
super(id, label);
}
run(): Thenable<any> {
this.historyService.openLastEditLocation();
return Promise.resolve(null);
}
}
@@ -1113,10 +1150,10 @@ export class NavigateLastAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
this.historyService.last();
return TPromise.as(null);
return Promise.resolve(null);
}
}
@@ -1133,10 +1170,10 @@ export class ReopenClosedEditorAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
this.historyService.reopenLastClosedEditor();
return TPromise.as(false);
return Promise.resolve(false);
}
}
@@ -1148,15 +1185,21 @@ export class ClearRecentFilesAction extends Action {
constructor(
id: string,
label: string,
@IWindowsService private windowsService: IWindowsService
@IWindowsService private windowsService: IWindowsService,
@IHistoryService private historyService: IHistoryService
) {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
// Clear global recently opened
this.windowsService.clearRecentlyOpened();
return TPromise.as(false);
// Clear workspace specific recently opened
this.historyService.clearRecentlyOpened();
return Promise.resolve(false);
}
}
@@ -1195,14 +1238,14 @@ export class BaseQuickOpenEditorInGroupAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
const keys = this.keybindingService.lookupKeybindings(this.id);
this.quickOpenService.show(NAVIGATE_IN_ACTIVE_GROUP_PREFIX, { quickNavigateConfiguration: { keybindings: keys } });
return TPromise.as(true);
return Promise.resolve(true);
}
}
@@ -1250,12 +1293,12 @@ export class OpenPreviousEditorFromHistoryAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
const keys = this.keybindingService.lookupKeybindings(this.id);
this.quickOpenService.show(null, { quickNavigateConfiguration: { keybindings: keys } });
return TPromise.as(true);
return Promise.resolve(true);
}
}
@@ -1268,10 +1311,10 @@ export class OpenNextRecentlyUsedEditorAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
this.historyService.forward(true);
return TPromise.as(null);
return Promise.resolve(null);
}
}
@@ -1284,10 +1327,10 @@ export class OpenPreviousRecentlyUsedEditorAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
this.historyService.back(true);
return TPromise.as(null);
return Promise.resolve(null);
}
}
@@ -1304,12 +1347,12 @@ export class ClearEditorHistoryAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
// Editor history
this.historyService.clear();
return TPromise.as(true);
return Promise.resolve(true);
}
}
@@ -1576,10 +1619,10 @@ export class BaseCreateEditorGroupAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
this.editorGroupService.addGroup(this.editorGroupService.activeGroup, this.direction, { activate: true });
return TPromise.as(true);
return Promise.resolve(true);
}
}

View File

@@ -12,17 +12,16 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor';
import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes';
import { TPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IListService } from 'vs/platform/list/browser/listService';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { distinct } from 'vs/base/common/arrays';
import { IEditorGroupsService, IEditorGroup, GroupDirection, GroupLocation, GroupsOrder, preferredSideBySideGroupDirection, EditorGroupLayout } from 'vs/workbench/services/group/common/editorGroupsService';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
export const CLOSE_SAVED_EDITORS_COMMAND_ID = 'workbench.action.closeUnmodifiedEditors';
export const CLOSE_EDITORS_IN_GROUP_COMMAND_ID = 'workbench.action.closeEditorsInGroup';
@@ -36,7 +35,11 @@ export const MOVE_ACTIVE_EDITOR_COMMAND_ID = 'moveActiveEditor';
export const LAYOUT_EDITOR_GROUPS_COMMAND_ID = 'layoutEditorGroups';
export const KEEP_EDITOR_COMMAND_ID = 'workbench.action.keepEditor';
export const SHOW_EDITORS_IN_GROUP = 'workbench.action.showEditorsInGroup';
export const TOGGLE_DIFF_INLINE_MODE = 'toggle.diff.editorMode';
export const TOGGLE_DIFF_SIDE_BY_SIDE = 'toggle.diff.renderSideBySide';
export const GOTO_NEXT_CHANGE = 'workbench.action.compareEditor.nextChange';
export const GOTO_PREVIOUS_CHANGE = 'workbench.action.compareEditor.previousChange';
export const TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE = 'toggle.diff.ignoreTrimWhitespace';
export const SPLIT_EDITOR_UP = 'workbench.action.splitEditorUp';
export const SPLIT_EDITOR_DOWN = 'workbench.action.splitEditorDown';
@@ -46,6 +49,8 @@ export const SPLIT_EDITOR_RIGHT = 'workbench.action.splitEditorRight';
export const NAVIGATE_ALL_EDITORS_GROUP_PREFIX = 'edt ';
export const NAVIGATE_IN_ACTIVE_GROUP_PREFIX = 'edt active ';
export const OPEN_EDITOR_AT_INDEX_COMMAND_ID = 'workbench.action.openEditorAtIndex';
export interface ActiveEditorMoveArguments {
to?: 'first' | 'last' | 'left' | 'right' | 'up' | 'down' | 'center' | 'position' | 'previous' | 'next';
by?: 'tab' | 'group';
@@ -77,7 +82,7 @@ function registerActiveEditorMoveCommand(): void {
id: MOVE_ACTIVE_EDITOR_COMMAND_ID,
weight: KeybindingWeight.WorkbenchContrib,
when: EditorContextKeys.editorTextFocus,
primary: null,
primary: 0,
handler: (accessor, args: any) => moveActiveEditor(args, accessor),
description: {
description: nls.localize('editorCommand.activeEditorMove.description', "Move the active editor by tabs or groups"),
@@ -221,18 +226,18 @@ export function mergeAllGroups(editorGroupService: IEditorGroupsService): void {
function registerDiffEditorCommands(): void {
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'workbench.action.compareEditor.nextChange',
id: GOTO_NEXT_CHANGE,
weight: KeybindingWeight.WorkbenchContrib,
when: TextCompareEditorVisibleContext,
primary: null,
primary: KeyMod.Alt | KeyCode.F5,
handler: accessor => navigateInDiffEditor(accessor, true)
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'workbench.action.compareEditor.previousChange',
id: GOTO_PREVIOUS_CHANGE,
weight: KeybindingWeight.WorkbenchContrib,
when: TextCompareEditorVisibleContext,
primary: null,
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F5,
handler: accessor => navigateInDiffEditor(accessor, false)
});
@@ -245,27 +250,66 @@ function registerDiffEditorCommands(): void {
}
}
function toggleDiffSideBySide(accessor: ServicesAccessor): void {
const configurationService = accessor.get(IConfigurationService);
const newValue = !configurationService.getValue<boolean>('diffEditor.renderSideBySide');
configurationService.updateValue('diffEditor.renderSideBySide', newValue, ConfigurationTarget.USER);
}
function toggleDiffIgnoreTrimWhitespace(accessor: ServicesAccessor): void {
const configurationService = accessor.get(IConfigurationService);
const newValue = !configurationService.getValue<boolean>('diffEditor.ignoreTrimWhitespace');
configurationService.updateValue('diffEditor.ignoreTrimWhitespace', newValue, ConfigurationTarget.USER);
}
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: TOGGLE_DIFF_INLINE_MODE,
id: TOGGLE_DIFF_SIDE_BY_SIDE,
weight: KeybindingWeight.WorkbenchContrib,
when: void 0,
primary: void 0,
handler: (accessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => {
const editorGroupService = accessor.get(IEditorGroupsService);
handler: accessor => toggleDiffSideBySide(accessor)
});
const { control } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context));
if (control instanceof TextDiffEditor) {
const widget = control.getControl();
const isInlineMode = !widget.renderSideBySide;
widget.updateOptions(<IDiffEditorOptions>{
renderSideBySide: isInlineMode
});
}
}
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: TOGGLE_DIFF_SIDE_BY_SIDE,
title: {
value: nls.localize('toggleInlineView', "Toggle Inline View"),
original: 'Compare: Toggle Inline View'
},
category: nls.localize('compare', "Compare")
},
when: ContextKeyExpr.has('textCompareEditorActive')
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE,
weight: KeybindingWeight.WorkbenchContrib,
when: void 0,
primary: void 0,
handler: accessor => toggleDiffIgnoreTrimWhitespace(accessor)
});
}
function registerOpenEditorAtIndexCommands(): void {
const openEditorAtIndex: ICommandHandler = (accessor: ServicesAccessor, editorIndex: number): void => {
const editorService = accessor.get(IEditorService);
const activeControl = editorService.activeControl;
if (activeControl) {
const editor = activeControl.group.getEditor(editorIndex);
if (editor) {
editorService.openEditor(editor);
}
}
};
// This command takes in the editor index number to open as an argument
CommandsRegistry.registerCommand({
id: OPEN_EDITOR_AT_INDEX_COMMAND_ID,
handler: openEditorAtIndex
});
// Keybindings to focus a specific index in the tab folder if tabs are enabled
for (let i = 0; i < 9; i++) {
@@ -273,24 +317,12 @@ function registerOpenEditorAtIndexCommands(): void {
const visibleIndex = i + 1;
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'workbench.action.openEditorAtIndex' + visibleIndex,
id: OPEN_EDITOR_AT_INDEX_COMMAND_ID + visibleIndex,
weight: KeybindingWeight.WorkbenchContrib,
when: void 0,
primary: KeyMod.Alt | toKeyCode(visibleIndex),
mac: { primary: KeyMod.WinCtrl | toKeyCode(visibleIndex) },
handler: accessor => {
const editorService = accessor.get(IEditorService);
const activeControl = editorService.activeControl;
if (activeControl) {
const editor = activeControl.group.getEditor(editorIndex);
if (editor) {
return editorService.openEditor(editor).then(() => void 0);
}
}
return void 0;
}
handler: accessor => openEditorAtIndex(accessor, editorIndex)
});
}
@@ -434,7 +466,7 @@ function registerCloseEditorCommands() {
contexts.push({ groupId: activeGroup.id }); // active group as fallback
}
return TPromise.join(distinct(contexts.map(c => c.groupId)).map(groupId =>
return Promise.all(distinct(contexts.map(c => c.groupId)).map(groupId =>
editorGroupService.getGroup(groupId).closeEditors({ savedOnly: true })
));
}
@@ -454,7 +486,7 @@ function registerCloseEditorCommands() {
distinctGroupIds.push(editorGroupService.activeGroup.id);
}
return TPromise.join(distinctGroupIds.map(groupId =>
return Promise.all(distinctGroupIds.map(groupId =>
editorGroupService.getGroup(groupId).closeAllEditors()
));
}
@@ -477,7 +509,7 @@ function registerCloseEditorCommands() {
const groupIds = distinct(contexts.map(context => context.groupId));
return TPromise.join(groupIds.map(groupId => {
return Promise.all(groupIds.map(groupId => {
const group = editorGroupService.getGroup(groupId);
const editors = contexts
.filter(context => context.groupId === groupId)
@@ -526,7 +558,7 @@ function registerCloseEditorCommands() {
const groupIds = distinct(contexts.map(context => context.groupId));
return TPromise.join(groupIds.map(groupId => {
return Promise.all(groupIds.map(groupId => {
const group = editorGroupService.getGroup(groupId);
const editors = contexts
.filter(context => context.groupId === groupId)
@@ -551,7 +583,7 @@ function registerCloseEditorCommands() {
return group.closeEditors({ direction: CloseDirection.RIGHT, except: editor });
}
return TPromise.as(false);
return Promise.resolve(false);
}
});
@@ -568,7 +600,7 @@ function registerCloseEditorCommands() {
return group.pinEditor(editor);
}
return TPromise.as(false);
return Promise.resolve(false);
}
});
@@ -647,7 +679,7 @@ export function getMultiSelectedEditorContexts(editorContext: IEditorCommandsCon
// First check for a focused list to return the selected items from
const list = listService.lastFocusedList;
if (list instanceof List && list.isDOMFocused()) {
if (list instanceof List && list.getHTMLElement() === document.activeElement) {
const elementToContext = (element: IEditorIdentifier | IEditorGroup) => {
if (isEditorGroup(element)) {
return { groupId: element.id, editorIndex: void 0 };

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { dispose, Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { EditorInput, EditorOptions } from 'vs/workbench/common/editor';
import { Dimension, show, hide, addClass } from 'vs/base/browser/dom';
@@ -15,7 +13,6 @@ import { IPartService } from 'vs/workbench/services/part/common/partService';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress';
import { toWinJsPromise } from 'vs/base/common/async';
import { IEditorGroupView, DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor';
import { Event, Emitter } from 'vs/base/common/event';
@@ -171,11 +168,11 @@ export class EditorControl extends Disposable {
// Show progress while setting input after a certain timeout. If the workbench is opening
// be more relaxed about progress showing by increasing the delay a little bit to reduce flicker.
const operation = this.editorOperation.start(this.partService.isCreated() ? 800 : 3200);
const operation = this.editorOperation.start(this.partService.isRestored() ? 800 : 3200);
// Call into editor control
const editorWillChange = !inputMatches;
return toWinJsPromise(control.setInput(editor, options, operation.token)).then(() => {
return TPromise.wrap(control.setInput(editor, options, operation.token)).then(() => {
// Focus (unless prevented or another operation is running)
if (operation.isCurrent()) {
@@ -233,15 +230,9 @@ export class EditorControl extends Disposable {
}
}
shutdown(): void {
// Forward to all editor controls
this.controls.forEach(editor => editor.shutdown());
}
dispose(): void {
this.activeControlDisposeables = dispose(this.activeControlDisposeables);
super.dispose();
}
}
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/editordroptarget';
import { LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, DragAndDropObserver } from 'vs/workbench/browser/dnd';
import { addDisposableListener, EventType, EventHelper, isAncestor, toggleClass, addClass, removeClass } from 'vs/base/browser/dom';
@@ -17,6 +15,7 @@ import { isMacintosh } from 'vs/base/common/platform';
import { GroupDirection, MergeGroupMode } from 'vs/workbench/services/group/common/editorGroupsService';
import { toDisposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { RunOnceScheduler } from 'vs/base/common/async';
interface IDropOperation {
splitDirection?: GroupDirection;
@@ -32,6 +31,8 @@ class DropOverlay extends Themable {
private currentDropOperation: IDropOperation;
private _disposed: boolean;
private cleanupOverlayScheduler: RunOnceScheduler;
private readonly editorTransfer = LocalSelectionTransfer.getInstance<DraggedEditorIdentifier>();
private readonly groupTransfer = LocalSelectionTransfer.getInstance<DraggedEditorGroupIdentifier>();
@@ -43,6 +44,8 @@ class DropOverlay extends Themable {
) {
super(themeService);
this.cleanupOverlayScheduler = this._register(new RunOnceScheduler(() => this.dispose(), 300));
this.create();
}
@@ -118,6 +121,11 @@ class DropOverlay extends Themable {
// Position overlay
this.positionOverlay(e.offsetX, e.offsetY, isDraggingGroup);
// Make sure to stop any running cleanup scheduler to remove the overlay
if (this.cleanupOverlayScheduler.isScheduled()) {
this.cleanupOverlayScheduler.cancel();
}
},
onDragLeave: e => this.dispose(),
@@ -144,9 +152,9 @@ class DropOverlay extends Themable {
// To protect against this issue we always destroy the overlay as soon as we detect a
// mouse event over it. The delay is used to guarantee we are not interfering with the
// actual DROP event that can also trigger a mouse over event.
setTimeout(() => {
this.dispose();
}, 300);
if (!this.cleanupOverlayScheduler.isScheduled()) {
this.cleanupOverlayScheduler.schedule();
}
}));
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/editorgroupview';
import { TPromise } from 'vs/base/common/winjs.base';
import { EditorGroup, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup';
@@ -26,7 +24,7 @@ import { IProgressService } from 'vs/platform/progress/common/progress';
import { ProgressService } from 'vs/workbench/services/progress/browser/progressService';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { localize } from 'vs/nls';
import { isPromiseCanceledError, isErrorWithActions, IErrorWithActions } from 'vs/base/common/errors';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { Severity, INotificationService, INotificationActions } from 'vs/platform/notification/common/notification';
import { toErrorMessage } from 'vs/base/common/errorMessage';
@@ -49,6 +47,9 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
// {{SQL CARBON EDIT}}
import { ICommandService } from 'vs/platform/commands/common/commands';
import { GlobalNewUntitledFileAction } from 'vs/workbench/parts/files/electron-browser/fileActions';
// {{SQL CARBON EDIT}} - End
import { isErrorWithActions, IErrorWithActions } from 'vs/base/common/errorsWithActions';
import { URI } from 'vs/base/common/uri';
export class EditorGroupView extends Themable implements IEditorGroupView {
@@ -99,7 +100,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
private active: boolean;
private dimension: Dimension;
private _whenRestored: TPromise<void>;
private _whenRestored: Thenable<void>;
private isRestored: boolean;
private scopedInstantiationService: IInstantiationService;
@@ -250,7 +251,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
if (this.isEmpty()) {
EventHelper.stop(e);
// {{SQL CARBON EDIT}}
this.commandService.executeCommand(GlobalNewUntitledFileAction.ID).done(undefined, err => this.notificationService.warn(err));
this.commandService.executeCommand(GlobalNewUntitledFileAction.ID).then(undefined, err => this.notificationService.warn(err));
}
}));
@@ -313,8 +314,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
// Show it
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => TPromise.as(actions),
getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
getActions: () => actions,
onHide: () => this.focus()
});
}
@@ -404,9 +404,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
}
}
private restoreEditors(from: IEditorGroupView | ISerializedEditorGroup): TPromise<void> {
private restoreEditors(from: IEditorGroupView | ISerializedEditorGroup): Thenable<void> {
if (this._group.count === 0) {
return TPromise.as(void 0); // nothing to show
return Promise.resolve(void 0); // nothing to show
}
// Determine editor options
@@ -421,11 +421,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
options.pinned = this._group.isPinned(activeEditor); // preserve pinned state
options.preserveFocus = true; // handle focus after editor is opened
const activeElement = document.activeElement;
// Show active editor
return this.doShowEditor(activeEditor, true, options).then(() => {
// Set focused now if this is the active group
if (this.accessor.activeGroup === this) {
// Set focused now if this is the active group and focus has
// not changed meanwhile. This prevents focus from being
// stolen accidentally on startup when the user already
// clicked somewhere.
if (this.accessor.activeGroup === this && activeElement === document.activeElement) {
this.focus();
}
});
@@ -604,7 +609,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
return this._disposed;
}
get whenRestored(): TPromise<void> {
get whenRestored(): Thenable<void> {
return this._whenRestored;
}
@@ -728,6 +733,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
openEditor(editor: EditorInput, options?: EditorOptions): TPromise<void> {
// Guard against invalid inputs
if (!editor) {
return TPromise.as(void 0);
}
// Editor opening event allows for prevention
const event = new EditorOpeningEvent(this._group.id, editor, options);
this._onWillOpenEditor.fire(event);
@@ -764,13 +774,21 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
this.accessor.activateGroup(this);
}
// Actually move the editor if a specific index is provided and we figure
// out that the editor is already opened at a different index. This
// ensures the right set of events are fired to the outside.
if (typeof openEditorOptions.index === 'number') {
const indexOfEditor = this._group.indexOf(editor);
if (indexOfEditor !== -1 && indexOfEditor !== openEditorOptions.index) {
this.doMoveEditorInsideGroup(editor, openEditorOptions);
}
}
// Update model
this._group.openEditor(editor, openEditorOptions);
// Show editor
const showEditorResult = this.doShowEditor(editor, openEditorOptions.active, options);
return showEditorResult;
return this.doShowEditor(editor, openEditorOptions.active, options);
}
private doShowEditor(editor: EditorInput, active: boolean, options?: EditorOptions): TPromise<void> {
@@ -846,7 +864,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
const startingIndex = this.getIndexOfEditor(editor) + 1;
// Open the other ones inactive
return TPromise.join(editors.map(({ editor, options }, index) => {
return Promise.all(editors.map(({ editor, options }, index) => {
const adjustedEditorOptions = options || new EditorOptions();
adjustedEditorOptions.inactive = true;
adjustedEditorOptions.pinned = true;
@@ -963,13 +981,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
this.doCloseInactiveEditor(editor);
}
// Forward to title control & breadcrumbs
// Forward to title control
this.titleAreaControl.closeEditor(editor);
}
private doCloseActiveEditor(focusNext = this.accessor.activeGroup === this, fromError?: boolean): void {
const editorToClose = this.activeEditor;
const editorHasFocus = isAncestor(document.activeElement, this.element);
const restoreFocus = this.shouldRestoreFocus(this.element);
// Optimization: if we are about to close the last editor in this group and settings
// are configured to close the group since it will be empty, we first set the last
@@ -983,7 +1001,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
const mostRecentlyActiveGroups = this.accessor.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
const nextActiveGroup = mostRecentlyActiveGroups[1]; // [0] will be the current one, so take [1]
if (nextActiveGroup) {
if (editorHasFocus) {
if (restoreFocus) {
nextActiveGroup.focus();
} else {
this.accessor.activateGroup(nextActiveGroup);
@@ -1020,7 +1038,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
this.editorControl.closeEditor(editorToClose);
// Restore focus to group container as needed unless group gets closed
if (editorHasFocus && !closeEmptyGroup) {
if (restoreFocus && !closeEmptyGroup) {
this.focus();
}
@@ -1034,6 +1052,17 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
}
}
private shouldRestoreFocus(target: Element): boolean {
const activeElement = document.activeElement;
if (activeElement === document.body) {
return true; // always restore focus if nothing is focused currently
}
// otherwise check for the active element being an ancestor of the target
return isAncestor(activeElement, target);
}
private doCloseInactiveEditor(editor: EditorInput) {
// Update model
@@ -1368,10 +1397,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
//#endregion
shutdown(): void {
this.editorControl.shutdown();
}
dispose(): void {
this._disposed = true;
@@ -1427,7 +1452,7 @@ registerThemingParticipant((theme, collector, environment) => {
const letterpress = `resources/letterpress${theme.type === 'dark' ? '-dark' : theme.type === 'hc' ? '-hc' : ''}.svg`;
collector.addRule(`
.monaco-workbench > .part.editor > .content .editor-group-container.empty .editor-group-letterpress {
background-image: url('${join(environment.appRoot, letterpress)}')
background-image: url('${URI.file(join(environment.appRoot, letterpress)).toString()}')
}
`);

View File

@@ -3,20 +3,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/workbench/browser/parts/editor/editor.contribution';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { Part } from 'vs/workbench/browser/part';
import { Dimension, isAncestor, toggleClass, addClass, $ } from 'vs/base/browser/dom';
import { Event, Emitter, once, Relay, anyEvent } from 'vs/base/common/event';
import { contrastBorder, editorBackground, registerColor } from 'vs/platform/theme/common/colorRegistry';
import { contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { GroupDirection, IAddGroupOptions, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, ICopyEditorOptions, GroupsOrder, GroupChangeKind, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument } from 'vs/workbench/services/group/common/editorGroupsService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, ISerializedNode, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } from 'vs/base/browser/ui/grid/grid';
import { Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } from 'vs/base/browser/ui/grid/grid';
import { GroupIdentifier, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
import { values } from 'vs/base/common/map';
import { EDITOR_GROUP_BORDER } from 'vs/workbench/common/theme';
import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme';
import { distinct } from 'vs/base/common/arrays';
import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptions, getEditorPartOptions, impactsEditorPartOptions, IEditorPartOptionsChangeEvent, EditorGroupsServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView';
@@ -24,15 +22,14 @@ import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/co
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { assign } from 'vs/base/common/objects';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { Scope } from 'vs/workbench/common/memento';
import { ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup';
import { TValueCallback, TPromise } from 'vs/base/common/winjs.base';
import { always } from 'vs/base/common/async';
import { EditorDropTarget } from 'vs/workbench/browser/parts/editor/editorDropTarget';
import { localize } from 'vs/nls';
import { Color } from 'vs/base/common/color';
import { CenteredViewLayout } from 'vs/base/browser/ui/centered/centeredViewLayout';
import { IView, orthogonal } from 'vs/base/browser/ui/grid/gridview';
import { onUnexpectedError } from 'vs/base/common/errors';
// {{SQL CARBON EDIT}}
import { convertEditorInput } from 'sql/parts/common/customInputConverter';
@@ -86,12 +83,6 @@ class GridWidgetView<T extends IView> implements IView {
}
}
export const EDITOR_PANE_BACKGROUND = registerColor('editorPane.background', {
dark: editorBackground,
light: editorBackground,
hc: editorBackground
}, localize('editorPaneBackground', "Background color of the editor pane visible on the left and right side of the centered editor layout."));
export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditorGroupsAccessor {
_serviceBrand: any;
@@ -128,7 +119,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
private dimension: Dimension;
private _preferredSize: Dimension;
private memento: object;
private workspaceMemento: object;
private globalMemento: object;
private _partOptions: IEditorPartOptions;
@@ -142,8 +133,8 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
private gridWidget: SerializableGrid<IEditorGroupView>;
private gridWidgetView: GridWidgetView<IEditorGroupView>;
private _whenRestored: TPromise<void>;
private whenRestoredComplete: TValueCallback<void>;
private _whenRestored: Thenable<void>;
private whenRestoredResolve: () => void;
constructor(
id: string,
@@ -151,19 +142,18 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
@IInstantiationService private instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IConfigurationService private configurationService: IConfigurationService,
@IStorageService private storageService: IStorageService
@IStorageService storageService: IStorageService
) {
super(id, { hasTitle: false }, themeService);
super(id, { hasTitle: false }, themeService, storageService);
this.gridWidgetView = new GridWidgetView<IEditorGroupView>();
this._partOptions = getEditorPartOptions(this.configurationService.getValue<IWorkbenchEditorConfiguration>());
this.memento = this.getMemento(this.storageService, Scope.WORKSPACE);
this.globalMemento = this.getMemento(this.storageService, Scope.GLOBAL);
this._whenRestored = new TPromise(resolve => {
this.whenRestoredComplete = resolve;
});
this.workspaceMemento = this.getMemento(StorageScope.WORKSPACE);
this.globalMemento = this.getMemento(StorageScope.GLOBAL);
this._whenRestored = new Promise(resolve => (this.whenRestoredResolve = resolve));
this.registerListeners();
}
@@ -236,7 +226,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
return this.gridWidget.orientation === Orientation.VERTICAL ? GroupOrientation.VERTICAL : GroupOrientation.HORIZONTAL;
}
get whenRestored(): TPromise<void> {
get whenRestored(): Thenable<void> {
return this._whenRestored;
}
@@ -376,7 +366,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
}
applyLayout(layout: EditorGroupLayout): void {
const gridHasFocus = isAncestor(document.activeElement, this.container);
const restoreFocus = this.shouldRestoreFocus(this.container);
// Determine how many groups we need overall
let layoutGroupsCount = 0;
@@ -429,19 +419,33 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
// Mark preferred size as changed
this.resetPreferredSize();
// Events for groupd that got added
// Events for groups that got added
this.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(groupView => {
if (currentGroupViews.indexOf(groupView) === -1) {
this._onDidAddGroup.fire(groupView);
}
});
// Update labels
this.updateGroupLabels();
// Restore focus as needed
if (gridHasFocus) {
if (restoreFocus) {
this._activeGroup.focus();
}
}
private shouldRestoreFocus(target: Element): boolean {
const activeElement = document.activeElement;
if (activeElement === document.body) {
return true; // always restore focus if nothing is focused currently
}
// otherwise check for the active element being an ancestor of the target
return isAncestor(activeElement, target);
}
private isTwoDimensionalGrid(): boolean {
const views = this.gridWidget.getViews();
if (isGridBranchNode(views)) {
@@ -485,6 +489,9 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
// Event
this._onDidAddGroup.fire(newGroupView);
// Update labels
this.updateGroupLabels();
return newGroupView;
}
@@ -622,7 +629,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
}
private doRemoveEmptyGroup(groupView: IEditorGroupView): void {
const gridHasFocus = isAncestor(document.activeElement, this.container);
const restoreFocus = this.shouldRestoreFocus(this.container);
// Activate next group if the removed one was active
if (this._activeGroup === groupView) {
@@ -637,16 +644,12 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
// Restore focus if we had it previously (we run this after gridWidget.removeView() is called
// because removing a view can mean to reparent it and thus focus would be removed otherwise)
if (gridHasFocus) {
if (restoreFocus) {
this._activeGroup.focus();
}
// Update labels: since our labels are created using the index of the
// group, removing a group might produce gaps. So we iterate over all
// groups and reassign the label based on the index.
this.getGroups(GroupsOrder.CREATION_TIME).forEach((group, index) => {
group.setLabel(this.getGroupLabel(index + 1));
});
// Update labels
this.updateGroupLabels();
// Update container
this.updateContainer();
@@ -658,10 +661,6 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
this._onDidRemoveGroup.fire(groupView);
}
private getGroupLabel(index: number): string {
return localize('groupLabel', "Group {0}", index);
}
moveGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView {
const sourceView = this.assertGroupView(group);
const targetView = this.assertGroupView(location);
@@ -670,15 +669,14 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
throw new Error('Cannot move group into its own');
}
const groupHasFocus = isAncestor(document.activeElement, sourceView.element);
const restoreFocus = this.shouldRestoreFocus(sourceView.element);
// Move is a simple remove and add of the same view
this.gridWidget.removeView(sourceView, Sizing.Distribute);
this.gridWidget.addView(sourceView, Sizing.Distribute, targetView, this.toGridViewDirection(direction));
// Move through grid widget API
this.gridWidget.moveView(sourceView, Sizing.Distribute, targetView, this.toGridViewDirection(direction));
// Restore focus if we had it previously (we run this after gridWidget.removeView() is called
// because removing a view can mean to reparent it and thus focus would be removed otherwise)
if (groupHasFocus) {
if (restoreFocus) {
sourceView.focus();
}
@@ -692,13 +690,13 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
const groupView = this.assertGroupView(group);
const locationView = this.assertGroupView(location);
const groupHasFocus = isAncestor(document.activeElement, groupView.element);
const restoreFocus = this.shouldRestoreFocus(groupView.element);
// Copy the group view
const copiedGroupView = this.doAddGroup(locationView, direction, groupView);
// Restore focus if we had it
if (groupHasFocus) {
if (restoreFocus) {
copiedGroupView.focus();
}
@@ -712,7 +710,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
// Move/Copy editors over into target
let index = (options && typeof options.index === 'number') ? options.index : targetView.count;
sourceView.editors.forEach(editor => {
const inactive = !sourceView.isActive(editor);
const inactive = !sourceView.isActive(editor) || this._activeGroup !== sourceView;
const copyOptions: ICopyEditorOptions = { index, inactive, preserveFocus: inactive };
if (options && options.mode === MergeGroupMode.COPY_EDITORS) {
@@ -802,6 +800,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
centerLayout(active: boolean): void {
this.centeredLayoutWidget.activate(active);
this._activeGroup.focus();
}
isLayoutCentered(): boolean {
@@ -825,27 +824,47 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
}
// Signal restored
always(TPromise.join(this.groups.map(group => group.whenRestored)), () => this.whenRestoredComplete(void 0));
always(Promise.all(this.groups.map(group => group.whenRestored)), () => this.whenRestoredResolve());
// Update container
this.updateContainer();
}
private doCreateGridControlWithPreviousState(): void {
const uiState = this.doGetPreviousState();
const uiState = this.workspaceMemento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY] as IEditorPartUIState;
if (uiState && uiState.serializedGrid) {
try {
// MRU
this.mostRecentActiveGroups = uiState.mostRecentActiveGroups;
// MRU
this.mostRecentActiveGroups = uiState.mostRecentActiveGroups;
// Grid Widget
this.doCreateGridControlWithState(uiState.serializedGrid, uiState.activeGroup);
// Grid Widget
this.doCreateGridControlWithState(uiState.serializedGrid, uiState.activeGroup);
// Ensure last active group has focus
this._activeGroup.focus();
// Ensure last active group has focus
this._activeGroup.focus();
} catch (error) {
this.handleGridRestoreError(error, uiState);
}
}
}
private handleGridRestoreError(error: Error, state: IEditorPartUIState): void {
// Log error
onUnexpectedError(new Error(`Error restoring editor grid widget: ${error} (with state: ${JSON.stringify(state)})`));
// Clear any state we have from the failing restore
if (this.gridWidget) {
this.doSetGridWidget();
}
this.groupViews.forEach(group => group.dispose());
this.groupViews.clear();
this._activeGroup = void 0;
this.mostRecentActiveGroups = [];
}
private doCreateGridControlWithState(serializedGrid: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[]): void {
// Determine group views to reuse if any
@@ -857,8 +876,9 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
}
// Create new
const groupViews: IEditorGroupView[] = [];
const gridWidget = SerializableGrid.deserialize(serializedGrid, {
fromJSON: (serializedEditorGroup: ISerializedEditorGroup) => {
fromJSON: (serializedEditorGroup: ISerializedEditorGroup | null) => {
let groupView: IEditorGroupView;
if (reuseGroupViews.length > 0) {
groupView = reuseGroupViews.shift();
@@ -866,6 +886,8 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
groupView = this.doCreateGroupView(serializedEditorGroup);
}
groupViews.push(groupView);
if (groupView.id === activeGroupId) {
this.doSetGroupActive(groupView);
}
@@ -874,6 +896,18 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
}
}, { styles: { separatorBorder: this.gridSeparatorBorder } });
// If the active group was not found when restoring the grid
// make sure to make at least one group active. We always need
// an active group.
if (!this._activeGroup) {
this.doSetGroupActive(groupViews[0]);
}
// Validate MRU group views matches grid widget state
if (this.mostRecentActiveGroups.some(groupId => !this.getGroup(groupId))) {
this.mostRecentActiveGroups = groupViews.map(group => group.id);
}
// Set it
this.doSetGridWidget(gridWidget);
}
@@ -893,127 +927,25 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
this.onDidSetGridWidget.fire();
}
private doGetPreviousState(): IEditorPartUIState {
const legacyState = this.doGetPreviousLegacyState();
if (legacyState) {
return legacyState; // TODO@ben remove after a while
}
return this.memento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY] as IEditorPartUIState;
}
private doGetPreviousLegacyState(): IEditorPartUIState {
const LEGACY_EDITOR_PART_UI_STATE_STORAGE_KEY = 'editorpart.uiState';
const LEGACY_STACKS_MODEL_STORAGE_KEY = 'editorStacks.model';
interface ILegacyEditorPartUIState {
ratio: number[];
groupOrientation: 'vertical' | 'horizontal';
}
interface ISerializedLegacyEditorStacksModel {
groups: ISerializedEditorGroup[];
active: number;
}
let legacyUIState: ISerializedLegacyEditorStacksModel;
const legacyUIStateRaw = this.storageService.get(LEGACY_STACKS_MODEL_STORAGE_KEY, StorageScope.WORKSPACE);
if (legacyUIStateRaw) {
try {
legacyUIState = JSON.parse(legacyUIStateRaw);
} catch (error) { /* ignore */ }
}
if (legacyUIState) {
this.storageService.remove(LEGACY_STACKS_MODEL_STORAGE_KEY, StorageScope.WORKSPACE);
}
const legacyPartState = this.memento[LEGACY_EDITOR_PART_UI_STATE_STORAGE_KEY] as ILegacyEditorPartUIState;
if (legacyPartState) {
delete this.memento[LEGACY_EDITOR_PART_UI_STATE_STORAGE_KEY];
}
if (legacyUIState && Array.isArray(legacyUIState.groups) && legacyUIState.groups.length > 0) {
const splitHorizontally = legacyPartState && legacyPartState.groupOrientation === 'horizontal';
const legacyState: IEditorPartUIState = Object.create(null);
const positionOneGroup = legacyUIState.groups[0];
const positionTwoGroup = legacyUIState.groups[1];
const positionThreeGroup = legacyUIState.groups[2];
legacyState.activeGroup = legacyUIState.active;
legacyState.mostRecentActiveGroups = [legacyUIState.active];
if (positionTwoGroup || positionThreeGroup) {
if (!positionThreeGroup) {
legacyState.mostRecentActiveGroups.push(legacyState.activeGroup === 0 ? 1 : 0);
} else {
if (legacyState.activeGroup === 0) {
legacyState.mostRecentActiveGroups.push(1, 2);
} else if (legacyState.activeGroup === 1) {
legacyState.mostRecentActiveGroups.push(0, 2);
} else {
legacyState.mostRecentActiveGroups.push(0, 1);
}
}
}
const toNode = function (group: ISerializedEditorGroup, size: number): ISerializedNode {
return {
data: group,
size,
type: 'leaf'
};
};
const baseSize = 1200; // just some number because layout() was not called yet, but we only need the proportions
// No split editor
if (!positionTwoGroup) {
legacyState.serializedGrid = {
width: baseSize,
height: baseSize,
orientation: splitHorizontally ? Orientation.VERTICAL : Orientation.HORIZONTAL,
root: toNode(positionOneGroup, baseSize)
};
}
// Split editor (2 or 3 columns)
else {
const children: ISerializedNode[] = [];
const size = positionThreeGroup ? baseSize / 3 : baseSize / 2;
children.push(toNode(positionOneGroup, size));
children.push(toNode(positionTwoGroup, size));
if (positionThreeGroup) {
children.push(toNode(positionThreeGroup, size));
}
legacyState.serializedGrid = {
width: baseSize,
height: baseSize,
orientation: splitHorizontally ? Orientation.VERTICAL : Orientation.HORIZONTAL,
root: {
data: children,
size: baseSize,
type: 'branch'
}
};
}
return legacyState;
}
return void 0;
}
private updateContainer(): void {
toggleClass(this.container, 'empty', this.isEmpty());
}
private updateGroupLabels(): void {
// Since our labels are created using the index of the
// group, adding/removing a group might produce gaps.
// So we iterate over all groups and reassign the label
// based on the index.
this.getGroups(GroupsOrder.GRID_APPEARANCE).forEach((group, index) => {
group.setLabel(this.getGroupLabel(index + 1));
});
}
private getGroupLabel(index: number): string {
return localize('groupLabel', "Group {0}", index);
}
private isEmpty(): boolean {
return this.groupViews.size === 1 && this._activeGroup.isEmpty();
}
@@ -1041,7 +973,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
//this.editorGroupsControl.refreshTitles();
}
shutdown(): void {
protected saveState(): void {
// Persist grid UI state
if (this.gridWidget) {
@@ -1052,19 +984,21 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor
};
if (this.isEmpty()) {
delete this.memento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY];
delete this.workspaceMemento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY];
} else {
this.memento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY] = uiState;
this.workspaceMemento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY] = uiState;
}
}
// Persist centered view state
this.globalMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY] = this.centeredLayoutWidget.state;
const centeredLayoutState = this.centeredLayoutWidget.state;
if (this.centeredLayoutWidget.isDefault(centeredLayoutState)) {
delete this.globalMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY];
} else {
this.globalMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY] = centeredLayoutState;
}
// Forward to all groups
this.groupViews.forEach(group => group.shutdown());
super.shutdown();
super.saveState();
}
dispose(): void {

View File

@@ -2,17 +2,15 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/editorpicker';
import { TPromise } from 'vs/base/common/winjs.base';
import * as nls from 'vs/nls';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { IAutoFocus, Mode, IEntryRunContext, IQuickNavigateConfiguration, IModel } from 'vs/base/parts/quickopen/common/quickOpen';
import { QuickOpenModel, QuickOpenEntry, QuickOpenEntryGroup, QuickOpenItemAccessor } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { IModeService } from 'vs/editor/common/services/modeService';
import { getIconClasses } from 'vs/workbench/browser/labels';
import { getIconClasses } from 'vs/editor/common/services/getIconClasses';
import { IModelService } from 'vs/editor/common/services/modelService';
import { QuickOpenHandler } from 'vs/workbench/browser/quickopen';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
@@ -20,6 +18,7 @@ import { IEditorGroupsService, IEditorGroup, EditorsOrder, GroupsOrder } from 'v
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { EditorInput, toResource } from 'vs/workbench/common/editor';
import { compareItemsByScore, scoreItem, ScorerCache, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer';
import { CancellationToken } from 'vs/base/common/cancellation';
export class EditorPickerEntry extends QuickOpenEntryGroup {
@@ -91,10 +90,10 @@ export abstract class BaseEditorPicker extends QuickOpenHandler {
this.scorerCache = Object.create(null);
}
getResults(searchValue: string): TPromise<QuickOpenModel> {
getResults(searchValue: string, token: CancellationToken): Thenable<QuickOpenModel> {
const editorEntries = this.getEditorEntries();
if (!editorEntries.length) {
return TPromise.as(null);
return Promise.resolve(null);
}
// Prepare search for scoring
@@ -117,7 +116,7 @@ export abstract class BaseEditorPicker extends QuickOpenHandler {
// Sorting
if (query.value) {
const groups = this.editorGroupService.getGroups(GroupsOrder.CREATION_TIME);
const groups = this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE);
entries.sort((e1, e2) => {
if (e1.group !== e2.group) {
return groups.indexOf(e1.group) - groups.indexOf(e2.group); // older groups first
@@ -139,7 +138,7 @@ export abstract class BaseEditorPicker extends QuickOpenHandler {
});
}
return TPromise.as(new QuickOpenModel(entries));
return Promise.resolve(new QuickOpenModel(entries));
}
onClose(canceled: boolean): void {
@@ -206,7 +205,7 @@ export class AllEditorsPicker extends BaseEditorPicker {
protected getEditorEntries(): EditorPickerEntry[] {
const entries: EditorPickerEntry[] = [];
this.editorGroupService.getGroups(GroupsOrder.CREATION_TIME).forEach(group => {
this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(group => {
group.editors.forEach(editor => {
entries.push(this.instantiationService.createInstance(EditorPickerEntry, editor, group));
});
@@ -232,4 +231,4 @@ export class AllEditorsPicker extends BaseEditorPicker {
return super.getAutoFocus(searchValue, context);
}
}
}

View File

@@ -3,22 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/editorstatus';
import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { $, append, runAtThisOrScheduleAtNextAnimationFrame, addDisposableListener, getDomNodePagePosition } from 'vs/base/browser/dom';
import { $, append, runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
import * as strings from 'vs/base/common/strings';
import * as paths from 'vs/base/common/paths';
import * as types from 'vs/base/common/types';
import uri from 'vs/base/common/uri';
import * as errors from 'vs/base/common/errors';
import { URI as uri } from 'vs/base/common/uri';
import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { Action } from 'vs/base/common/actions';
import { language, LANGUAGE_DEFAULT, AccessibilitySupport } from 'vs/base/common/platform';
import * as browser from 'vs/base/browser/browser';
import { IMode } from 'vs/editor/common/modes';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { IFileEditorInput, EncodingMode, IEncodingSupport, toResource, SideBySideEditorInput, IEditor as IBaseEditor, IEditorInput } from 'vs/workbench/common/editor';
import { IDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle';
@@ -31,11 +26,11 @@ import { IndentUsingSpaces, IndentUsingTabs, DetectIndentation, IndentationToSpa
import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/binaryEditor';
import { BinaryResourceDiffEditor } from 'vs/workbench/browser/parts/editor/binaryDiffEditor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IQuickOpenService, IPickOpenEntry, IFilePickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { SUPPORTED_ENCODINGS, IFileService, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
@@ -46,18 +41,16 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile
import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
import { IConfigurationChangedEvent, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { widgetShadow, editorWidgetBackground, foreground, darken, contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { deepClone } from 'vs/base/common/objects';
import { ICodeEditor, isCodeEditor, isDiffEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser';
import { Button } from 'vs/base/browser/ui/button/button';
import { Schemas } from 'vs/base/common/network';
import { IAnchor } from 'vs/base/browser/ui/contextview/contextview';
import { Themable } from 'vs/workbench/common/theme';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
import { getIconClasses } from 'vs/editor/common/services/getIconClasses';
import { timeout } from 'vs/base/common/async';
import { INotificationHandle, INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { once } from 'vs/base/common/event';
class SideBySideEditorEncodingSupport implements IEncodingSupport {
constructor(private master: IEncodingSupport, private details: IEncodingSupport) { }
@@ -294,7 +287,7 @@ export class EditorStatus implements IStatusbarItem {
private activeEditorListeners: IDisposable[];
private delayedRender: IDisposable;
private toRender: StateChange;
private screenReaderExplanation: ScreenReaderDetectedExplanation;
private screenReaderNotification: INotificationHandle;
constructor(
@IEditorService private editorService: IEditorService,
@@ -304,6 +297,7 @@ export class EditorStatus implements IStatusbarItem {
@IModeService private modeService: IModeService,
@ITextFileService private textFileService: ITextFileService,
@IWorkspaceConfigurationService private readonly configurationService: IWorkspaceConfigurationService,
@INotificationService private readonly notificationService: INotificationService
) {
this.toDispose = [];
this.activeEditorListeners = [];
@@ -494,28 +488,39 @@ export class EditorStatus implements IStatusbarItem {
private onModeClick(): void {
const action = this.instantiationService.createInstance(ChangeModeAction, ChangeModeAction.ID, ChangeModeAction.LABEL);
action.run().done(null, errors.onUnexpectedError);
action.run();
action.dispose();
}
private onIndentationClick(): void {
const action = this.instantiationService.createInstance(ChangeIndentationAction, ChangeIndentationAction.ID, ChangeIndentationAction.LABEL);
action.run().done(null, errors.onUnexpectedError);
action.run();
action.dispose();
}
private onScreenReaderModeClick(): void {
const showExplanation = !this.screenReaderExplanation || !this.screenReaderExplanation.visible;
if (!this.screenReaderNotification) {
this.screenReaderNotification = this.notificationService.prompt(
Severity.Info,
// {{SQL CARBON EDIT}}
nls.localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate Azure Data Studio?"),
[{
label: nls.localize('screenReaderDetectedExplanation.answerYes', "Yes"),
run: () => {
this.configurationService.updateValue('editor.accessibilitySupport', 'on', ConfigurationTarget.USER);
}
}, {
label: nls.localize('screenReaderDetectedExplanation.answerNo', "No"),
run: () => {
this.configurationService.updateValue('editor.accessibilitySupport', 'off', ConfigurationTarget.USER);
}
}],
{ sticky: true }
);
if (!this.screenReaderExplanation) {
this.screenReaderExplanation = this.instantiationService.createInstance(ScreenReaderDetectedExplanation);
this.toDispose.push(this.screenReaderExplanation);
}
if (showExplanation) {
this.screenReaderExplanation.show(this.screenRedearModeElement);
} else {
this.screenReaderExplanation.hide();
once(this.screenReaderNotification.onDidClose)(() => {
this.screenReaderNotification = null;
});
}
}
@@ -526,14 +531,14 @@ export class EditorStatus implements IStatusbarItem {
private onEOLClick(): void {
const action = this.instantiationService.createInstance(ChangeEOLAction, ChangeEOLAction.ID, ChangeEOLAction.LABEL);
action.run().done(null, errors.onUnexpectedError);
action.run();
action.dispose();
}
private onEncodingClick(): void {
const action = this.instantiationService.createInstance(ChangeEncodingAction, ChangeEncodingAction.ID, ChangeEncodingAction.LABEL);
action.run().done(null, errors.onUnexpectedError);
action.run();
action.dispose();
}
@@ -617,6 +622,10 @@ export class EditorStatus implements IStatusbarItem {
this.activeEditorListeners.push(editor.onMetadataChanged(metadata => {
this.onMetadataChange(activeControl);
}));
this.activeEditorListeners.push(editor.onDidOpenInPlace(() => {
this.updateStatusBar();
}));
});
}
}
@@ -689,8 +698,8 @@ export class EditorStatus implements IStatusbarItem {
screenReaderMode = (editorWidget.getConfiguration().accessibilitySupport === AccessibilitySupport.Enabled);
}
if (screenReaderMode === false && this.screenReaderExplanation && this.screenReaderExplanation.visible) {
this.screenReaderExplanation.hide();
if (screenReaderMode === false && this.screenReaderNotification) {
this.screenReaderNotification.close();
}
this.updateState({ screenReaderMode: screenReaderMode });
@@ -819,7 +828,7 @@ export class ShowLanguageExtensionsAction extends Action {
this.enabled = galleryService.isEnabled();
}
run(): TPromise<void> {
run(): Thenable<void> {
return this.commandService.executeCommand('workbench.extensions.action.showExtensionsForLanguage', this.fileExtension).then(() => void 0);
}
}
@@ -836,7 +845,7 @@ export class ChangeModeAction extends Action {
@IModelService private modelService: IModelService,
@IEditorService private editorService: IEditorService,
@IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService,
@IQuickOpenService private quickOpenService: IQuickOpenService,
@IQuickInputService private quickInputService: IQuickInputService,
@IPreferencesService private preferencesService: IPreferencesService,
@IInstantiationService private instantiationService: IInstantiationService,
@IUntitledEditorService private untitledEditorService: IUntitledEditorService
@@ -844,10 +853,10 @@ export class ChangeModeAction extends Action {
super(actionId, actionLabel);
}
run(): TPromise<any> {
run(): Thenable<any> {
const activeTextEditorWidget = getCodeEditor(this.editorService.activeTextEditorWidget);
if (!activeTextEditorWidget) {
return this.quickOpenService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
}
const textModel = activeTextEditorWidget.getModel();
@@ -868,7 +877,7 @@ export class ChangeModeAction extends Action {
// All languages are valid picks
const languages = this.modeService.getRegisteredLanguageNames();
const picks: IPickOpenEntry[] = languages.sort().map((lang, index) => {
const picks: QuickPickInput[] = languages.sort().map((lang, index) => {
let description: string;
if (currentModeId === lang) {
description = nls.localize('languageDescription', "({0}) - Configured Language", this.modeService.getModeIdForLanguageName(lang.toLowerCase()));
@@ -888,20 +897,20 @@ export class ChangeModeAction extends Action {
}
}
return <IFilePickOpenEntry>{
return <IQuickPickItem>{
label: lang,
resource: fakeResource,
iconClasses: getIconClasses(this.modelService, this.modeService, fakeResource),
description
};
});
if (hasLanguageSupport) {
picks[0].separator = { border: true, label: nls.localize('languagesPicks', "languages (identifier)") };
picks.unshift({ type: 'separator', label: nls.localize('languagesPicks', "languages (identifier)") });
}
// Offer action to configure via settings
let configureModeAssociations: IPickOpenEntry;
let configureModeSettings: IPickOpenEntry;
let configureModeAssociations: IQuickPickItem;
let configureModeSettings: IQuickPickItem;
let galleryAction: Action;
if (hasLanguageSupport) {
const ext = paths.extname(resource.fsPath) || paths.basename(resource.fsPath);
@@ -918,7 +927,7 @@ export class ChangeModeAction extends Action {
}
// Offer to "Auto Detect"
const autoDetectMode: IPickOpenEntry = {
const autoDetectMode: IQuickPickItem = {
label: nls.localize('autoDetect', "Auto Detect")
};
@@ -926,7 +935,7 @@ export class ChangeModeAction extends Action {
picks.unshift(autoDetectMode);
}
return this.quickOpenService.pick(picks, { placeHolder: nls.localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }).then(pick => {
return this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }).then(pick => {
if (!pick) {
return;
}
@@ -970,23 +979,22 @@ export class ChangeModeAction extends Action {
}
// Find mode
let mode: TPromise<IMode>;
let languageSelection: ILanguageSelection;
if (pick === autoDetectMode) {
mode = this.modeService.getOrCreateModeByFilenameOrFirstLine(toResource(activeEditor.input, { supportSideBySide: true }).fsPath, textModel.getLineContent(1));
// {{SQL CARBON EDIT}} - use activeEditor.input instead of activeEditor
languageSelection = this.modeService.createByFilepathOrFirstLine(toResource(activeEditor.input, { supportSideBySide: true }).fsPath, textModel.getLineContent(1));
} else {
mode = this.modeService.getOrCreateModeByLanguageName(pick.label);
languageSelection = this.modeService.createByLanguageName(pick.label);
}
// {{SQL CARBON EDIT}}
// Change mode
models.forEach(textModel => {
let self = this;
mode.then((modeValue) => {
QueryEditorService.sqlLanguageModeCheck(textModel, modeValue, activeEditor).then((newTextModel) => {
if (newTextModel) {
self.modelService.setMode(newTextModel, modeValue);
}
});
QueryEditorService.sqlLanguageModeCheck(textModel, languageSelection, activeEditor).then((newTextModel) => {
if (newTextModel) {
self.modelService.setMode(newTextModel, languageSelection);
}
});
});
});
@@ -995,21 +1003,21 @@ export class ChangeModeAction extends Action {
private configureFileAssociation(resource: uri): void {
const extension = paths.extname(resource.fsPath);
const basename = paths.basename(resource.fsPath);
const currentAssociation = this.modeService.getModeIdByFilenameOrFirstLine(basename);
const currentAssociation = this.modeService.getModeIdByFilepathOrFirstLine(basename);
const languages = this.modeService.getRegisteredLanguageNames();
const picks: IPickOpenEntry[] = languages.sort().map((lang, index) => {
const picks: IQuickPickItem[] = languages.sort().map((lang, index) => {
const id = this.modeService.getModeIdForLanguageName(lang.toLowerCase());
return <IPickOpenEntry>{
return <IQuickPickItem>{
id,
label: lang,
description: (id === currentAssociation) ? nls.localize('currentAssociation', "Current Association") : void 0
};
});
TPromise.timeout(50 /* quick open is sensitive to being opened so soon after another */).done(() => {
this.quickOpenService.pick(picks, { placeHolder: nls.localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || basename) }).done(language => {
setTimeout(() => {
this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || basename) }).then(language => {
if (language) {
const fileAssociationsConfig = this.configurationService.inspect(FILES_ASSOCIATIONS_CONFIG);
@@ -1037,11 +1045,11 @@ export class ChangeModeAction extends Action {
this.configurationService.updateValue(FILES_ASSOCIATIONS_CONFIG, currentAssociations, target);
}
});
});
}, 50 /* quick open is sensitive to being opened so soon after another */);
}
}
export interface IChangeEOLEntry extends IPickOpenEntry {
export interface IChangeEOLEntry extends IQuickPickItem {
eol: EndOfLineSequence;
}
@@ -1054,22 +1062,22 @@ class ChangeIndentationAction extends Action {
actionId: string,
actionLabel: string,
@IEditorService private editorService: IEditorService,
@IQuickOpenService private quickOpenService: IQuickOpenService
@IQuickInputService private quickInputService: IQuickInputService
) {
super(actionId, actionLabel);
}
run(): TPromise<any> {
run(): Thenable<any> {
const activeTextEditorWidget = getCodeEditor(this.editorService.activeTextEditorWidget);
if (!activeTextEditorWidget) {
return this.quickOpenService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
}
if (!isWritableCodeEditor(activeTextEditorWidget)) {
return this.quickOpenService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]);
return this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]);
}
const picks = [
const picks: QuickPickInput<IQuickPickItem & { run(): void }>[] = [
activeTextEditorWidget.getAction(IndentUsingSpaces.ID),
activeTextEditorWidget.getAction(IndentUsingTabs.ID),
activeTextEditorWidget.getAction(DetectIndentation.ID),
@@ -1088,10 +1096,10 @@ class ChangeIndentationAction extends Action {
};
});
(<IPickOpenEntry>picks[0]).separator = { label: nls.localize('indentView', "change view") };
(<IPickOpenEntry>picks[3]).separator = { label: nls.localize('indentConvert', "convert file"), border: true };
picks.splice(3, 0, { type: 'separator', label: nls.localize('indentConvert', "convert file") });
picks.unshift({ type: 'separator', label: nls.localize('indentView', "change view") });
return this.quickOpenService.pick(picks, { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }).then(action => action && action.run());
return this.quickInputService.pick(picks, { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }).then(action => action && action.run());
}
}
@@ -1104,19 +1112,19 @@ export class ChangeEOLAction extends Action {
actionId: string,
actionLabel: string,
@IEditorService private editorService: IEditorService,
@IQuickOpenService private quickOpenService: IQuickOpenService
@IQuickInputService private quickInputService: IQuickInputService
) {
super(actionId, actionLabel);
}
run(): TPromise<any> {
run(): Thenable<any> {
const activeTextEditorWidget = getCodeEditor(this.editorService.activeTextEditorWidget);
if (!activeTextEditorWidget) {
return this.quickOpenService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
}
if (!isWritableCodeEditor(activeTextEditorWidget)) {
return this.quickOpenService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]);
return this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]);
}
const textModel = activeTextEditorWidget.getModel();
@@ -1128,7 +1136,7 @@ export class ChangeEOLAction extends Action {
const selectedIndex = (textModel && textModel.getEOL() === '\n') ? 0 : 1;
return this.quickOpenService.pick(EOLOptions, { placeHolder: nls.localize('pickEndOfLine', "Select End of Line Sequence"), autoFocus: { autoFocusIndex: selectedIndex } }).then(eol => {
return this.quickInputService.pick(EOLOptions, { placeHolder: nls.localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] }).then(eol => {
if (eol) {
const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorWidget);
if (activeCodeEditor && isWritableCodeEditor(activeCodeEditor)) {
@@ -1149,28 +1157,26 @@ export class ChangeEncodingAction extends Action {
actionId: string,
actionLabel: string,
@IEditorService private editorService: IEditorService,
@IQuickOpenService private quickOpenService: IQuickOpenService,
@IQuickInputService private quickInputService: IQuickInputService,
@ITextResourceConfigurationService private textResourceConfigurationService: ITextResourceConfigurationService,
@IFileService private fileService: IFileService
) {
super(actionId, actionLabel);
}
run(): TPromise<any> {
run(): Thenable<any> {
if (!getCodeEditor(this.editorService.activeTextEditorWidget)) {
return this.quickOpenService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
}
let activeControl = this.editorService.activeControl;
let encodingSupport: IEncodingSupport = toEditorWithEncodingSupport(activeControl.input);
if (!encodingSupport) {
return this.quickOpenService.pick([{ label: nls.localize('noFileEditor', "No file active at this time") }]);
return this.quickInputService.pick([{ label: nls.localize('noFileEditor', "No file active at this time") }]);
}
let pickActionPromise: TPromise<IPickOpenEntry>;
let saveWithEncodingPick: IPickOpenEntry;
let reopenWithEncodingPick: IPickOpenEntry;
let saveWithEncodingPick: IQuickPickItem;
let reopenWithEncodingPick: IQuickPickItem;
if (language === LANGUAGE_DEFAULT) {
saveWithEncodingPick = { label: nls.localize('saveWithEncoding', "Save with Encoding") };
reopenWithEncodingPick = { label: nls.localize('reopenWithEncoding', "Reopen with Encoding") };
@@ -1179,12 +1185,13 @@ export class ChangeEncodingAction extends Action {
reopenWithEncodingPick = { label: nls.localize('reopenWithEncoding', "Reopen with Encoding"), detail: 'Reopen with Encoding' };
}
let pickActionPromise: Promise<IQuickPickItem>;
if (encodingSupport instanceof UntitledEditorInput) {
pickActionPromise = TPromise.as(saveWithEncodingPick);
pickActionPromise = Promise.resolve(saveWithEncodingPick);
} else if (!isWritableBaseEditor(activeControl)) {
pickActionPromise = TPromise.as(reopenWithEncodingPick);
pickActionPromise = Promise.resolve(reopenWithEncodingPick);
} else {
pickActionPromise = this.quickOpenService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true });
pickActionPromise = this.quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true });
}
return pickActionPromise.then(action => {
@@ -1194,10 +1201,10 @@ export class ChangeEncodingAction extends Action {
const resource = toResource(activeControl.input, { supportSideBySide: true });
return TPromise.timeout(50 /* quick open is sensitive to being opened so soon after another */)
return timeout(50 /* quick open is sensitive to being opened so soon after another */)
.then(() => {
if (!resource || !this.fileService.canHandleResource(resource)) {
return TPromise.as(null); // encoding detection only possible for resources the file service can handle
return Promise.resolve(null); // encoding detection only possible for resources the file service can handle
}
return this.fileService.resolveContent(resource, { autoGuessEncoding: true, acceptTextOnly: true }).then(content => content.encoding, err => null);
@@ -1211,7 +1218,7 @@ export class ChangeEncodingAction extends Action {
let aliasMatchIndex: number;
// All encodings are valid picks
const picks: IPickOpenEntry[] = Object.keys(SUPPORTED_ENCODINGS)
const picks: QuickPickInput[] = Object.keys(SUPPORTED_ENCODINGS)
.sort((k1, k2) => {
if (k1 === configuredEncoding) {
return -1;
@@ -1238,15 +1245,17 @@ export class ChangeEncodingAction extends Action {
return { id: key, label: SUPPORTED_ENCODINGS[key].labelLong, description: key };
});
const items = picks.slice() as IQuickPickItem[];
// If we have a guessed encoding, show it first unless it matches the configured encoding
if (guessedEncoding && configuredEncoding !== guessedEncoding && SUPPORTED_ENCODINGS[guessedEncoding]) {
picks[0].separator = { border: true };
picks.unshift({ type: 'separator' });
picks.unshift({ id: guessedEncoding, label: SUPPORTED_ENCODINGS[guessedEncoding].labelLong, description: nls.localize('guessedEncoding', "Guessed from content") });
}
return this.quickOpenService.pick(picks, {
return this.quickInputService.pick(picks, {
placeHolder: isReopenWithEncoding ? nls.localize('pickEncodingForReopen', "Select File Encoding to Reopen File") : nls.localize('pickEncodingForSave', "Select File Encoding to Save with"),
autoFocus: { autoFocusIndex: typeof directMatchIndex === 'number' ? directMatchIndex : typeof aliasMatchIndex === 'number' ? aliasMatchIndex : void 0 }
activeItem: items[typeof directMatchIndex === 'number' ? directMatchIndex : typeof aliasMatchIndex === 'number' ? aliasMatchIndex : -1]
}).then(encoding => {
if (encoding) {
activeControl = this.editorService.activeControl;
@@ -1260,149 +1269,3 @@ export class ChangeEncodingAction extends Action {
});
}
}
class ScreenReaderDetectedExplanation extends Themable {
private container: HTMLElement;
private hrElement: HTMLHRElement;
private _visible: boolean;
constructor(
@IThemeService themeService: IThemeService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IWorkspaceConfigurationService private readonly configurationService: IWorkspaceConfigurationService,
) {
super(themeService);
}
get visible(): boolean {
return this._visible;
}
protected updateStyles(): void {
if (this.container) {
const background = this.getColor(editorWidgetBackground);
this.container.style.backgroundColor = background ? background.toString() : null;
const widgetShadowColor = this.getColor(widgetShadow);
this.container.style.boxShadow = widgetShadowColor ? `0 0px 8px ${widgetShadowColor}` : null;
const contrastBorderColor = this.getColor(contrastBorder);
this.container.style.border = contrastBorderColor ? `1px solid ${contrastBorderColor}` : null;
const foregroundColor = this.getColor(foreground);
this.hrElement.style.backgroundColor = foregroundColor ? foregroundColor.toString() : null;
}
}
show(anchorElement: HTMLElement): void {
this._visible = true;
this.contextViewService.showContextView({
getAnchor: () => {
const res = getDomNodePagePosition(anchorElement);
return {
x: res.left,
y: res.top - 9, /* above the status bar */
width: res.width,
height: res.height
} as IAnchor;
},
render: (container) => {
return this.renderContents(container);
},
onDOMEvent: (e, activeElement) => { },
onHide: () => {
this._visible = false;
}
});
}
hide(): void {
this.contextViewService.hideContextView();
}
protected renderContents(parent: HTMLElement): IDisposable {
const toDispose: IDisposable[] = [];
this.container = $('div.screen-reader-detected-explanation', {
'aria-hidden': 'true'
});
const title = $('h2.title', {}, nls.localize('screenReaderDetectedExplanation.title', "Screen Reader Optimized"));
this.container.appendChild(title);
const closeBtn = $('div.cancel');
toDispose.push(addDisposableListener(closeBtn, 'click', () => {
this.contextViewService.hideContextView();
}));
toDispose.push(addDisposableListener(closeBtn, 'mouseover', () => {
const theme = this.themeService.getTheme();
let darkenFactor: number;
switch (theme.type) {
case 'light':
darkenFactor = 0.1;
break;
case 'dark':
darkenFactor = 0.2;
break;
}
if (darkenFactor) {
closeBtn.style.backgroundColor = this.getColor(editorWidgetBackground, (color, theme) => darken(color, darkenFactor)(theme));
}
}));
toDispose.push(addDisposableListener(closeBtn, 'mouseout', () => {
closeBtn.style.backgroundColor = null;
}));
this.container.appendChild(closeBtn);
// {{SQL CARBON EDIT}}
const question = $('p.question', {}, nls.localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate Azure Data Studio?"));
this.container.appendChild(question);
const buttonContainer = $('div.buttons');
this.container.appendChild(buttonContainer);
const yesBtn = new Button(buttonContainer);
yesBtn.label = nls.localize('screenReaderDetectedExplanation.answerYes', "Yes");
toDispose.push(attachButtonStyler(yesBtn, this.themeService));
toDispose.push(yesBtn.onDidClick(e => {
this.configurationService.updateValue('editor.accessibilitySupport', 'on', ConfigurationTarget.USER);
this.contextViewService.hideContextView();
}));
const noBtn = new Button(buttonContainer);
noBtn.label = nls.localize('screenReaderDetectedExplanation.answerNo', "No");
toDispose.push(attachButtonStyler(noBtn, this.themeService));
toDispose.push(noBtn.onDidClick(e => {
this.configurationService.updateValue('editor.accessibilitySupport', 'off', ConfigurationTarget.USER);
this.contextViewService.hideContextView();
}));
const clear = $('div');
clear.style.clear = 'both';
this.container.appendChild(clear);
const br = $('br');
this.container.appendChild(br);
this.hrElement = $('hr');
this.container.appendChild(this.hrElement);
// {{SQL CARBON EDIT}}
const explanation1 = $('p.body1', {}, nls.localize('screenReaderDetectedExplanation.body1', "Azure Data Studio is now optimized for usage with a screen reader."));
this.container.appendChild(explanation1);
const explanation2 = $('p.body2', {}, nls.localize('screenReaderDetectedExplanation.body2', "Some editor features will have different behaviour: e.g. word wrapping, folding, etc."));
this.container.appendChild(explanation2);
parent.appendChild(this.container);
this.updateStyles();
return {
dispose: () => dispose(toDispose)
};
}
}

View File

@@ -0,0 +1,179 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Widget } from 'vs/base/browser/ui/widget';
import { IOverlayWidget, ICodeEditor, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
import { Event, Emitter } from 'vs/base/common/event';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { $, append } from 'vs/base/browser/dom';
import { attachStylerCallback } from 'vs/platform/theme/common/styler';
import { buttonBackground, buttonForeground, editorBackground, editorForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { Schemas } from 'vs/base/common/network';
import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
import { extname } from 'vs/base/common/paths';
import { Disposable, dispose } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { isEqual } from 'vs/base/common/resources';
export class FloatingClickWidget extends Widget implements IOverlayWidget {
private _onClick: Emitter<void> = this._register(new Emitter<void>());
get onClick(): Event<void> { return this._onClick.event; }
private _domNode: HTMLElement;
constructor(
private editor: ICodeEditor,
private label: string,
keyBindingAction: string,
@IKeybindingService keybindingService: IKeybindingService,
@IThemeService private themeService: IThemeService
) {
super();
if (keyBindingAction) {
const keybinding = keybindingService.lookupKeybinding(keyBindingAction);
if (keybinding) {
this.label += ` (${keybinding.getLabel()})`;
}
}
}
getId(): string {
return 'editor.overlayWidget.floatingClickWidget';
}
getDomNode(): HTMLElement {
return this._domNode;
}
getPosition(): IOverlayWidgetPosition {
return {
preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER
};
}
render() {
this._domNode = $('.floating-click-widget');
this._register(attachStylerCallback(this.themeService, { buttonBackground, buttonForeground, editorBackground, editorForeground, contrastBorder }, colors => {
const backgroundColor = colors.buttonBackground ? colors.buttonBackground : colors.editorBackground;
if (backgroundColor) {
this._domNode.style.backgroundColor = backgroundColor.toString();
}
const foregroundColor = colors.buttonForeground ? colors.buttonForeground : colors.editorForeground;
if (foregroundColor) {
this._domNode.style.color = foregroundColor.toString();
}
const borderColor = colors.contrastBorder ? colors.contrastBorder.toString() : null;
this._domNode.style.borderWidth = borderColor ? '1px' : null;
this._domNode.style.borderStyle = borderColor ? 'solid' : null;
this._domNode.style.borderColor = borderColor;
}));
append(this._domNode, $('')).textContent = this.label;
this.onclick(this._domNode, e => this._onClick.fire());
this.editor.addOverlayWidget(this);
}
dispose(): void {
this.editor.removeOverlayWidget(this);
super.dispose();
}
}
export class OpenWorkspaceButtonContribution extends Disposable implements IEditorContribution {
static get(editor: ICodeEditor): OpenWorkspaceButtonContribution {
return editor.getContribution<OpenWorkspaceButtonContribution>(OpenWorkspaceButtonContribution.ID);
}
private static readonly ID = 'editor.contrib.openWorkspaceButton';
private openWorkspaceButton: FloatingClickWidget;
constructor(
private editor: ICodeEditor,
@IInstantiationService private instantiationService: IInstantiationService,
@IWindowService private windowService: IWindowService,
@IWorkspaceContextService private contextService: IWorkspaceContextService
) {
super();
this.update();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.editor.onDidChangeModel(e => this.update()));
}
getId(): string {
return OpenWorkspaceButtonContribution.ID;
}
private update(): void {
if (!this.shouldShowButton(this.editor)) {
this.disposeOpenWorkspaceWidgetRenderer();
return;
}
this.createOpenWorkspaceWidgetRenderer();
}
private shouldShowButton(editor: ICodeEditor): boolean {
const model = editor.getModel();
if (!model) {
return false; // we need a model
}
if (model.uri.scheme !== Schemas.file || extname(model.uri.fsPath) !== `.${WORKSPACE_EXTENSION}`) {
return false; // we need a local workspace file
}
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
const workspaceConfiguration = this.contextService.getWorkspace().configuration;
if (workspaceConfiguration && isEqual(workspaceConfiguration, model.uri)) {
return false; // already inside workspace
}
}
return true;
}
private createOpenWorkspaceWidgetRenderer(): void {
if (!this.openWorkspaceButton) {
this.openWorkspaceButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, localize('openWorkspace', "Open Workspace"), null);
this._register(this.openWorkspaceButton.onClick(() => {
const model = this.editor.getModel();
if (model) {
this.windowService.openWindow([model.uri]);
}
}));
this.openWorkspaceButton.render();
}
}
private disposeOpenWorkspaceWidgetRenderer(): void {
this.openWorkspaceButton = dispose(this.openWorkspaceButton);
}
dispose(): void {
this.disposeOpenWorkspaceWidgetRenderer();
super.dispose();
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 381 B

View File

@@ -7,10 +7,6 @@
display: none;
}
.monaco-workbench>.part.editor>.content .editor-group-container:not(.active) .breadcrumbs-control {
opacity: .8;
}
.monaco-workbench>.part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumb-item.selected .monaco-icon-label,
.monaco-workbench>.part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumb-item.focused .monaco-icon-label {
text-decoration-line: underline;
@@ -23,6 +19,17 @@
/* todo@joh move somewhere else */
.monaco-workbench .monaco-breadcrumbs-picker .arrow {
position: absolute;
width: 0;
border-style: solid;
}
.monaco-workbench .monaco-breadcrumbs-picker .picker-item {
line-height: 22px;
flex: 1;
}
.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree {
height: 100%;
overflow: hidden;

View File

@@ -44,15 +44,13 @@
.monaco-workbench > .part.editor > .content .editor-group-container > .title {
position: relative;
display: flex;
flex-wrap: nowrap;
box-sizing: border-box;
overflow: hidden;
justify-content: space-between;
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title.tabs {
flex-wrap: wrap;
.monaco-workbench > .part.editor > .content .editor-group-container > .title:not(.tabs) {
display: flex; /* when tabs are not shown, use flex layout */
flex-wrap: nowrap;
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title.title-border-bottom::after {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 381 B

View File

@@ -3,6 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Title Label */
.monaco-workbench > .part.editor > .content .editor-group-container > .title > .label-container {
display: flex;
justify-content: flex-start;
@@ -11,8 +13,6 @@
flex: auto;
}
/* Title Label */
.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label {
line-height: 35px;
overflow: hidden;
@@ -34,7 +34,7 @@
.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control {
flex: 1 50%;
overflow: hidden;
padding: 0 6px;
margin-left: .45em;
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item {
@@ -67,8 +67,8 @@
.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item.root_folder::after {
/* use dot separator for workspace folder */
content: '';
padding: 0 4px;
content: '\00a0•\00a0';
padding: 0px;
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child {

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#252526;opacity:0.5;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#c5c5c5;opacity:0.5;}</style></defs><title>Paragraph_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M13,1V4H12V16H6V9.973A4.5,4.5,0,0,1,6.5,1Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M12,2V3H11V15H10V3H8V15H7V8.95A3.588,3.588,0,0,1,6.5,9a3.5,3.5,0,0,1,0-7Z"/></g></svg>

After

Width:  |  Height:  |  Size: 576 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;opacity:0.5;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;opacity:0.5;}</style></defs><title>Paragraph_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M13,1V4H12V16H6V9.973A4.5,4.5,0,0,1,6.5,1Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M12,2V3H11V15H10V3H8V15H7V8.95A3.588,3.588,0,0,1,6.5,9a3.5,3.5,0,0,1,0-7Z"/></g></svg>

After

Width:  |  Height:  |  Size: 576 B

View File

@@ -5,11 +5,15 @@
/* Title Container */
.monaco-workbench > .part.editor > .content .editor-group-container > .title.tabs > .monaco-scrollable-element {
.monaco-workbench > .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container {
display: flex;
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container > .monaco-scrollable-element {
flex: 1;
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title.tabs > .monaco-scrollable-element .scrollbar {
.monaco-workbench > .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container > .monaco-scrollable-element .scrollbar {
z-index: 3; /* on top of tabs */
cursor: default;
}
@@ -92,28 +96,33 @@
display: none; /* hidden by default until a color is provided (see below) */
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container {
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container,
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container,
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container {
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 6; /* over possible title border */
pointer-events: none;
background-color: var(--tab-border-top-color);
width: 100%;
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container {
top: 0;
height: 1px;
background-color: var(--tab-border-top-color);
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container {
display: block;
position: absolute;
bottom: 0;
left: 0;
z-index: 6; /* over possible title border */
pointer-events: none;
background-color: var(--tab-border-bottom-color);
width: 100%;
height: 1px;
background-color: var(--tab-border-bottom-color);
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container {
top: 0;
height: 2px;
background-color: var(--tab-dirty-border-top-color);
}
/* Tab Label */
@@ -137,6 +146,10 @@
padding: 0;
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink:focus > .tab-label::after {
opacity: 0; /* when tab has the focus this shade breaks the tab border (fixes https://github.com/Microsoft/vscode/issues/57819) */
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label,
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label > .monaco-icon-label-description-container {
overflow: visible; /* fixes https://github.com/Microsoft/vscode/issues/20182 */
@@ -168,8 +181,9 @@
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.close-button-right.sizing-shrink > .tab-close,
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-right.sizing-shrink:hover > .tab-close {
overflow: visible; /* ...but still show the close button on hover and when dirty */
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-right.sizing-shrink:hover > .tab-close,
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-right.sizing-shrink > .tab-close:focus-within {
overflow: visible; /* ...but still show the close button on hover, focus and when dirty */
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off > .tab-close {
@@ -259,23 +273,28 @@
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control {
flex: 1 100%;
height: 25px;
height: 22px;
cursor: default;
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-icon-label {
height: 22px;
line-height: 22px;
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-icon-label::before {
height: 18px; /* tweak the icon size of the editor labels when icons are enabled */
height: 22px; /* tweak the icon size of the editor labels when icons are enabled */
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item {
max-width: 260px;
max-width: 80%;
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before {
min-width: 16px;
height: 22px;
}
.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child {
padding-right: 8px;
}
/* .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:not(:last-child):not(:hover):not(.focused):not(.file) {
min-width: 33px;
} */

View File

@@ -1,40 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.vs .monaco-workbench .textdiff-editor-action.next {
background: url('next-diff.svg') center center no-repeat;
}
.vs .monaco-workbench .textdiff-editor-action.previous {
background: url('previous-diff.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .textdiff-editor-action.next,
.hc-black .monaco-workbench .textdiff-editor-action.next {
background: url('next-diff-inverse.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .textdiff-editor-action.previous,
.hc-black .monaco-workbench .textdiff-editor-action.previous {
background: url('previous-diff-inverse.svg') center center no-repeat;
}
.vs .monaco-workbench .textdiff-editor-action.toggleIgnoreTrimWhitespace {
opacity: 1;
background: url('paragraph.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .textdiff-editor-action.toggleIgnoreTrimWhitespace,
.hc-black .monaco-workbench .textdiff-editor-action.toggleIgnoreTrimWhitespace {
opacity: 1;
background: url('paragraph-inverse.svg') center center no-repeat;
}
.vs .monaco-workbench .textdiff-editor-action.toggleIgnoreTrimWhitespace.is-checked {
opacity: 0.5;
}
.vs-dark .monaco-workbench .textdiff-editor-action.toggleIgnoreTrimWhitespace.is-checked,
.hc-black .monaco-workbench .textdiff-editor-action.toggleIgnoreTrimWhitespace.is-checked {
opacity: 0.5;
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/notabstitlecontrol';
import { toResource, Verbosity, IEditorInput } from 'vs/workbench/common/editor';
import { TitleControl, IToolbarActions } from 'vs/workbench/browser/parts/editor/titleControl';
@@ -15,11 +13,17 @@ import { addDisposableListener, EventType, addClass, EventHelper, removeClass, t
import { IEditorPartOptions, EDITOR_TITLE_HEIGHT } from 'vs/workbench/browser/parts/editor/editor';
import { IAction } from 'vs/base/common/actions';
import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
import { Color } from 'vs/base/common/color';
interface IRenderedEditorLabel {
editor: IEditorInput;
pinned: boolean;
}
export class NoTabsTitleControl extends TitleControl {
private titleContainer: HTMLElement;
private editorLabel: ResourceLabel;
private lastRenderedActiveEditor: IEditorInput;
private activeLabel: IRenderedEditorLabel = Object.create(null);
protected create(parent: HTMLElement): void {
this.titleContainer = parent;
@@ -40,7 +44,9 @@ export class NoTabsTitleControl extends TitleControl {
this._register(this.editorLabel.onClick(e => this.onTitleLabelClick(e)));
// Breadcrumbs
this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, extraClasses: ['no-tabs-breadcrumbs'] });
this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: () => Color.transparent });
toggleClass(this.titleContainer, 'breadcrumbs', Boolean(this.breadcrumbsControl));
this.toDispose.push({ dispose: () => removeClass(this.titleContainer, 'breadcrumbs') }); // import to remove because the container is a shared dom node
// Right Actions Container
const actionsContainer = document.createElement('div');
@@ -60,7 +66,7 @@ export class NoTabsTitleControl extends TitleControl {
this._register(addDisposableListener(this.titleContainer, EventType.DBLCLICK, (e: MouseEvent) => this.onTitleDoubleClick(e)));
// Detect mouse click
this._register(addDisposableListener(this.titleContainer, EventType.CLICK, (e: MouseEvent) => this.onTitleClick(e)));
this._register(addDisposableListener(this.titleContainer, EventType.MOUSE_UP, (e: MouseEvent) => this.onTitleClick(e)));
// Detect touch
this._register(addDisposableListener(this.titleContainer, TouchEventType.Tap, (e: GestureEvent) => this.onTitleClick(e)));
@@ -87,6 +93,8 @@ export class NoTabsTitleControl extends TitleControl {
// Close editor on middle mouse click
if (e instanceof MouseEvent && e.button === 1 /* Middle Button */) {
EventHelper.stop(e, true /* for https://github.com/Microsoft/vscode/issues/56715 */);
this.group.closeEditor(this.group.activeEditor);
}
}
@@ -96,7 +104,10 @@ export class NoTabsTitleControl extends TitleControl {
}
openEditor(editor: IEditorInput): void {
this.ifActiveEditorChanged(() => this.redraw());
const activeEditorChanged = this.ifActiveEditorChanged(() => this.redraw());
if (!activeEditorChanged) {
this.ifActiveEditorPropertiesChanged(() => this.redraw());
}
}
closeEditor(editor: IEditorInput): void {
@@ -147,16 +158,36 @@ export class NoTabsTitleControl extends TitleControl {
this.redraw();
}
private ifActiveEditorChanged(fn: () => void): void {
protected handleBreadcrumbsEnablementChange(): void {
toggleClass(this.titleContainer, 'breadcrumbs', Boolean(this.breadcrumbsControl));
this.redraw();
}
private ifActiveEditorChanged(fn: () => void): boolean {
if (
!this.lastRenderedActiveEditor && this.group.activeEditor || // active editor changed from null => editor
this.lastRenderedActiveEditor && !this.group.activeEditor || // active editor changed from editor => null
!this.group.isActive(this.lastRenderedActiveEditor) // active editor changed from editorA => editorB
!this.activeLabel.editor && this.group.activeEditor || // active editor changed from null => editor
this.activeLabel.editor && !this.group.activeEditor || // active editor changed from editor => null
!this.group.isActive(this.activeLabel.editor) // active editor changed from editorA => editorB
) {
fn();
return true;
}
return false;
}
private ifActiveEditorPropertiesChanged(fn: () => void): void {
if (!this.activeLabel.editor || !this.group.activeEditor) {
return; // need an active editor to check for properties changed
}
if (this.activeLabel.pinned !== this.group.isPinned(this.group.activeEditor)) {
fn(); // only run if pinned state has changed
}
}
private ifEditorIsActive(editor: IEditorInput, fn: () => void): void {
if (this.group.isActive(editor)) {
fn(); // only run if editor is current active
@@ -165,11 +196,12 @@ export class NoTabsTitleControl extends TitleControl {
private redraw(): void {
const editor = this.group.activeEditor;
this.lastRenderedActiveEditor = editor;
const isEditorPinned = this.group.isPinned(this.group.activeEditor);
const isGroupActive = this.accessor.activeGroup === this.group;
this.activeLabel = { editor, pinned: isEditorPinned };
// Update Breadcrumbs
if (this.breadcrumbsControl) {
if (isGroupActive) {
@@ -189,6 +221,7 @@ export class NoTabsTitleControl extends TitleControl {
// Otherwise render it
else {
// Dirty state
this.updateEditorDirty(editor);

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IRange } from 'vs/editor/common/core/range';
@@ -21,8 +21,8 @@ export interface IRangeHighlightDecoration {
export class RangeHighlightDecorations extends Disposable {
private rangeHighlightDecorationId: string = null;
private editor: ICodeEditor = null;
private rangeHighlightDecorationId: string | null = null;
private editor: ICodeEditor | null = null;
private editorDisposables: IDisposable[] = [];
private readonly _onHighlightRemoved: Emitter<void> = this._register(new Emitter<void>());

View File

@@ -6,20 +6,19 @@
import 'vs/css!./media/resourceviewer';
import * as nls from 'vs/nls';
import * as mimes from 'vs/base/common/mime';
import URI from 'vs/base/common/uri';
import { Builder, $ } from 'vs/base/browser/builder';
import { URI } from 'vs/base/common/uri';
import * as DOM from 'vs/base/browser/dom';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { LRUCache } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import { clamp } from 'vs/base/common/numbers';
import { Themable } from 'vs/workbench/common/theme';
import { IStatusbarItem, StatusbarItemDescriptor, IStatusbarRegistry, Extensions, StatusbarAlignment } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { IStatusbarItem, StatusbarItemDescriptor, IStatusbarRegistry, Extensions } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IDisposable, Disposable, combinedDisposable } from 'vs/base/common/lifecycle';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { Registry } from 'vs/platform/registry/common/platform';
import { TPromise } from 'vs/base/common/winjs.base';
import { Action } from 'vs/base/common/actions';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { memoize } from 'vs/base/common/decorators';
@@ -61,8 +60,8 @@ class BinarySize {
}
}
export interface ResourceViewerContext {
layout(dimension: DOM.Dimension): void;
export interface ResourceViewerContext extends IDisposable {
layout?(dimension: DOM.Dimension): void;
}
/**
@@ -81,10 +80,10 @@ export class ResourceViewer {
openInternalClb: (uri: URI) => void,
openExternalClb: (uri: URI) => void,
metadataClb: (meta: string) => void
): ResourceViewerContext | null {
): ResourceViewerContext {
// Ensure CSS class
$(container).setClass('monaco-resource-viewer');
container.className = 'monaco-resource-viewer';
// Images
if (ResourceViewer.isImageResource(descriptor)) {
@@ -93,21 +92,20 @@ export class ResourceViewer {
// Large Files
if (descriptor.size > ResourceViewer.MAX_OPEN_INTERNAL_SIZE) {
FileTooLargeFileView.create(container, descriptor, scrollbar, metadataClb);
return FileTooLargeFileView.create(container, descriptor, scrollbar, metadataClb);
}
// Seemingly Binary Files
else {
FileSeemsBinaryFileView.create(container, descriptor, scrollbar, openInternalClb, metadataClb);
return FileSeemsBinaryFileView.create(container, descriptor, scrollbar, openInternalClb, metadataClb);
}
return null;
}
private static isImageResource(descriptor: IResourceDescriptor) {
const mime = getMime(descriptor);
return mime.indexOf('image/') >= 0;
// Chrome does not support tiffs
return mime.indexOf('image/') >= 0 && mime !== 'image/tiff';
}
}
@@ -122,14 +120,12 @@ class ImageView {
scrollbar: DomScrollableElement,
openExternalClb: (uri: URI) => void,
metadataClb: (meta: string) => void
): ResourceViewerContext | null {
): ResourceViewerContext {
if (ImageView.shouldShowImageInline(descriptor)) {
return InlineImageView.create(container, descriptor, fileService, scrollbar, metadataClb);
}
LargeImageView.create(container, descriptor, openExternalClb);
return null;
return LargeImageView.create(container, descriptor, openExternalClb, metadataClb);
}
private static shouldShowImageInline(descriptor: IResourceDescriptor): boolean {
@@ -156,25 +152,29 @@ class LargeImageView {
static create(
container: HTMLElement,
descriptor: IResourceDescriptor,
openExternalClb: (uri: URI) => void
openExternalClb: (uri: URI) => void,
metadataClb: (meta: string) => void
) {
const size = BinarySize.formatSize(descriptor.size);
metadataClb(size);
const imageContainer = $(container)
.empty()
.p({
text: nls.localize('largeImageError', "The image is not displayed in the editor because it is too large ({0}).", size)
});
DOM.clearNode(container);
const disposables: IDisposable[] = [];
const label = document.createElement('p');
label.textContent = nls.localize('largeImageError', "The image is not displayed in the editor because it is too large ({0}).", size);
container.appendChild(label);
if (descriptor.resource.scheme !== Schemas.data) {
imageContainer.append($('a', {
role: 'button',
class: 'embedded-link',
text: nls.localize('resourceOpenExternalButton', "Open image using external program?")
}).on(DOM.EventType.CLICK, (e) => {
openExternalClb(descriptor.resource);
}));
const link = DOM.append(label, DOM.$('a.embedded-link'));
link.setAttribute('role', 'button');
link.textContent = nls.localize('resourceOpenExternalButton', "Open image using external program?");
disposables.push(DOM.addDisposableListener(link, DOM.EventType.CLICK, () => openExternalClb(descriptor.resource)));
}
return combinedDisposable(disposables);
}
}
@@ -186,18 +186,17 @@ class FileTooLargeFileView {
metadataClb: (meta: string) => void
) {
const size = BinarySize.formatSize(descriptor.size);
metadataClb(size);
$(container)
.empty()
.span({
text: nls.localize('nativeFileTooLargeError', "The file is not displayed in the editor because it is too large ({0}).", size)
});
DOM.clearNode(container);
if (metadataClb) {
metadataClb(size);
}
const label = document.createElement('span');
label.textContent = nls.localize('nativeFileTooLargeError', "The file is not displayed in the editor because it is too large ({0}).", size);
container.appendChild(label);
scrollbar.scanDomNode();
return Disposable.None;
}
}
@@ -209,27 +208,27 @@ class FileSeemsBinaryFileView {
openInternalClb: (uri: URI) => void,
metadataClb: (meta: string) => void
) {
const binaryContainer = $(container)
.empty()
.p({
text: nls.localize('nativeBinaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding.")
});
metadataClb(typeof descriptor.size === 'number' ? BinarySize.formatSize(descriptor.size) : '');
DOM.clearNode(container);
const disposables: IDisposable[] = [];
const label = document.createElement('p');
label.textContent = nls.localize('nativeBinaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding.");
container.appendChild(label);
if (descriptor.resource.scheme !== Schemas.data) {
binaryContainer.append($('a', {
role: 'button',
class: 'embedded-link',
text: nls.localize('openAsText', "Do you want to open it anyway?")
}).on(DOM.EventType.CLICK, (e) => {
openInternalClb(descriptor.resource);
}));
}
const link = DOM.append(label, DOM.$('a.embedded-link'));
link.setAttribute('role', 'button');
link.textContent = nls.localize('openAsText', "Do you want to open it anyway?");
if (metadataClb) {
metadataClb(BinarySize.formatSize(descriptor.size));
disposables.push(DOM.addDisposableListener(link, DOM.EventType.CLICK, () => openInternalClb(descriptor.resource)));
}
scrollbar.scanDomNode();
return combinedDisposable(disposables);
}
}
@@ -239,7 +238,7 @@ class ZoomStatusbarItem extends Themable implements IStatusbarItem {
static instance: ZoomStatusbarItem;
showTimeout: number;
showTimeout: any;
private statusBarItem: HTMLElement;
private onSelectScale?: (scale: Scale) => void;
@@ -276,16 +275,16 @@ class ZoomStatusbarItem extends Themable implements IStatusbarItem {
render(container: HTMLElement): IDisposable {
if (!this.statusBarItem && container) {
this.statusBarItem = $(container).a()
.addClass('.zoom-statusbar-item')
.on('click', () => {
this.contextMenuService.showContextMenu({
getAnchor: () => container,
getActions: () => TPromise.as(this.zoomActions)
});
})
.getHTMLElement();
this.statusBarItem = DOM.append(container, DOM.$('a.zoom-statusbar-item'));
this.statusBarItem.setAttribute('role', 'button');
this.statusBarItem.style.display = 'none';
DOM.addDisposableListener(this.statusBarItem, DOM.EventType.CLICK, () => {
this.contextMenuService.showContextMenu({
getAnchor: () => container,
getActions: () => this.zoomActions
});
});
}
return this;
@@ -304,7 +303,7 @@ class ZoomStatusbarItem extends Themable implements IStatusbarItem {
this.onSelectScale(scale);
}
return null;
return void 0;
}));
}
@@ -368,8 +367,11 @@ class InlineImageView {
scrollbar: DomScrollableElement,
metadataClb: (meta: string) => void
) {
const context = {
layout(dimension: DOM.Dimension) { }
const disposables: IDisposable[] = [];
const context: ResourceViewerContext = {
layout(dimension: DOM.Dimension) { },
dispose: () => combinedDisposable(disposables).dispose()
};
const cacheKey = descriptor.resource.toString();
@@ -379,41 +381,40 @@ class InlineImageView {
const initialState: ImageState = InlineImageView.imageStateCache.get(cacheKey) || { scale: 'fit', offsetX: 0, offsetY: 0 };
let scale = initialState.scale;
let img: Builder | null = null;
let imgElement: HTMLImageElement | null = null;
let image: HTMLImageElement | null = null;
function updateScale(newScale: Scale) {
if (!img || !imgElement.parentElement) {
if (!image || !image.parentElement) {
return;
}
if (newScale === 'fit') {
scale = 'fit';
img.addClass('scale-to-fit');
img.removeClass('pixelated');
img.style('min-width', 'auto');
img.style('width', 'auto');
DOM.addClass(image, 'scale-to-fit');
DOM.removeClass(image, 'pixelated');
image.style.minWidth = 'auto';
image.style.width = 'auto';
InlineImageView.imageStateCache.set(cacheKey, null);
} else {
const oldWidth = imgElement.width;
const oldHeight = imgElement.height;
const oldWidth = image.width;
const oldHeight = image.height;
scale = clamp(newScale, InlineImageView.MIN_SCALE, InlineImageView.MAX_SCALE);
if (scale >= InlineImageView.PIXELATION_THRESHOLD) {
img.addClass('pixelated');
DOM.addClass(image, 'pixelated');
} else {
img.removeClass('pixelated');
DOM.removeClass(image, 'pixelated');
}
const { scrollTop, scrollLeft } = imgElement.parentElement;
const dx = (scrollLeft + imgElement.parentElement.clientWidth / 2) / imgElement.parentElement.scrollWidth;
const dy = (scrollTop + imgElement.parentElement.clientHeight / 2) / imgElement.parentElement.scrollHeight;
const { scrollTop, scrollLeft } = image.parentElement;
const dx = (scrollLeft + image.parentElement.clientWidth / 2) / image.parentElement.scrollWidth;
const dy = (scrollTop + image.parentElement.clientHeight / 2) / image.parentElement.scrollHeight;
img.removeClass('scale-to-fit');
img.style('min-width', `${(imgElement.naturalWidth * scale)}px`);
img.style('width', `${(imgElement.naturalWidth * scale)}px`);
DOM.removeClass(image, 'scale-to-fit');
image.style.minWidth = `${(image.naturalWidth * scale)}px`;
image.style.width = `${(image.naturalWidth * scale)}px`;
const newWidth = imgElement.width;
const newWidth = image.width;
const scaleFactor = (newWidth - oldWidth) / oldWidth;
const newScrollLeft = ((oldWidth * scaleFactor * dx) + scrollLeft);
@@ -431,123 +432,131 @@ class InlineImageView {
}
function firstZoom() {
scale = imgElement.clientWidth / imgElement.naturalWidth;
scale = image.clientWidth / image.naturalWidth;
updateScale(scale);
}
$(container)
.on(DOM.EventType.KEY_DOWN, (e: KeyboardEvent, c) => {
if (!img) {
return;
}
ctrlPressed = e.ctrlKey;
altPressed = e.altKey;
disposables.push(DOM.addDisposableListener(window, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
if (!image) {
return;
}
ctrlPressed = e.ctrlKey;
altPressed = e.altKey;
if (platform.isMacintosh ? altPressed : ctrlPressed) {
c.removeClass('zoom-in').addClass('zoom-out');
}
})
.on(DOM.EventType.KEY_UP, (e: KeyboardEvent, c) => {
if (!img) {
return;
}
if (platform.isMacintosh ? altPressed : ctrlPressed) {
DOM.removeClass(container, 'zoom-in');
DOM.addClass(container, 'zoom-out');
}
}));
ctrlPressed = e.ctrlKey;
altPressed = e.altKey;
disposables.push(DOM.addDisposableListener(window, DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
if (!image) {
return;
}
if (!(platform.isMacintosh ? altPressed : ctrlPressed)) {
c.removeClass('zoom-out').addClass('zoom-in');
}
})
.on(DOM.EventType.CLICK, (e: MouseEvent) => {
if (!img) {
return;
}
ctrlPressed = e.ctrlKey;
altPressed = e.altKey;
if (e.button !== 0) {
return;
}
if (!(platform.isMacintosh ? altPressed : ctrlPressed)) {
DOM.removeClass(container, 'zoom-out');
DOM.addClass(container, 'zoom-in');
}
}));
// left click
if (scale === 'fit') {
firstZoom();
}
disposables.push(DOM.addDisposableListener(container, DOM.EventType.CLICK, (e: MouseEvent) => {
if (!image) {
return;
}
if (!(platform.isMacintosh ? altPressed : ctrlPressed)) { // zoom in
let i = 0;
for (; i < InlineImageView.zoomLevels.length; ++i) {
if (InlineImageView.zoomLevels[i] > scale) {
break;
}
if (e.button !== 0) {
return;
}
// left click
if (scale === 'fit') {
firstZoom();
}
if (!(platform.isMacintosh ? altPressed : ctrlPressed)) { // zoom in
let i = 0;
for (; i < InlineImageView.zoomLevels.length; ++i) {
if (InlineImageView.zoomLevels[i] > scale) {
break;
}
updateScale(InlineImageView.zoomLevels[i] || InlineImageView.MAX_SCALE);
} else {
let i = InlineImageView.zoomLevels.length - 1;
for (; i >= 0; --i) {
if (InlineImageView.zoomLevels[i] < scale) {
break;
}
}
updateScale(InlineImageView.zoomLevels[i] || InlineImageView.MAX_SCALE);
} else {
let i = InlineImageView.zoomLevels.length - 1;
for (; i >= 0; --i) {
if (InlineImageView.zoomLevels[i] < scale) {
break;
}
updateScale(InlineImageView.zoomLevels[i] || InlineImageView.MIN_SCALE);
}
})
.on(DOM.EventType.WHEEL, (e: WheelEvent) => {
if (!img) {
return;
}
updateScale(InlineImageView.zoomLevels[i] || InlineImageView.MIN_SCALE);
}
}));
const isScrollWhellKeyPressed = platform.isMacintosh ? altPressed : ctrlPressed;
if (!isScrollWhellKeyPressed && !e.ctrlKey) { // pinching is reported as scroll wheel + ctrl
return;
}
disposables.push(DOM.addDisposableListener(container, DOM.EventType.WHEEL, (e: WheelEvent) => {
if (!image) {
return;
}
e.preventDefault();
e.stopPropagation();
const isScrollWhellKeyPressed = platform.isMacintosh ? altPressed : ctrlPressed;
if (!isScrollWhellKeyPressed && !e.ctrlKey) { // pinching is reported as scroll wheel + ctrl
return;
}
if (scale === 'fit') {
firstZoom();
}
e.preventDefault();
e.stopPropagation();
let delta = e.deltaY < 0 ? 1 : -1;
if (scale === 'fit') {
firstZoom();
}
// Pinching should increase the scale
if (e.ctrlKey && !isScrollWhellKeyPressed) {
delta *= -1;
}
updateScale(scale as number * (1 - delta * InlineImageView.SCALE_PINCH_FACTOR));
})
.on(DOM.EventType.SCROLL, () => {
if (!imgElement || !imgElement.parentElement || scale === 'fit') {
return;
}
let delta = e.deltaY < 0 ? 1 : -1;
const entry = InlineImageView.imageStateCache.get(cacheKey);
if (entry) {
const { scrollTop, scrollLeft } = imgElement.parentElement;
InlineImageView.imageStateCache.set(cacheKey, { scale: entry.scale, offsetX: scrollLeft, offsetY: scrollTop });
}
});
// Pinching should increase the scale
if (e.ctrlKey && !isScrollWhellKeyPressed) {
delta *= -1;
}
updateScale(scale as number * (1 - delta * InlineImageView.SCALE_PINCH_FACTOR));
}));
$(container)
.empty()
.addClass('image', 'zoom-in')
.img({})
.style('visibility', 'hidden')
.addClass('scale-to-fit')
.on(DOM.EventType.LOAD, (e, i) => {
img = i;
imgElement = img.getHTMLElement() as HTMLImageElement;
metadataClb(nls.localize('imgMeta', '{0}x{1} {2}', imgElement.naturalWidth, imgElement.naturalHeight, BinarySize.formatSize(descriptor.size)));
scrollbar.scanDomNode();
img.style('visibility', 'visible');
updateScale(scale);
if (initialState.scale !== 'fit') {
scrollbar.setScrollPosition({
scrollLeft: initialState.offsetX,
scrollTop: initialState.offsetY,
});
}
});
disposables.push(DOM.addDisposableListener(container, DOM.EventType.SCROLL, () => {
if (!image || !image.parentElement || scale === 'fit') {
return;
}
const entry = InlineImageView.imageStateCache.get(cacheKey);
if (entry) {
const { scrollTop, scrollLeft } = image.parentElement;
InlineImageView.imageStateCache.set(cacheKey, { scale: entry.scale, offsetX: scrollLeft, offsetY: scrollTop });
}
}));
DOM.clearNode(container);
DOM.addClasses(container, 'image', 'zoom-in');
image = DOM.append(container, DOM.$('img.scale-to-fit'));
image.style.visibility = 'hidden';
disposables.push(DOM.addDisposableListener(image, DOM.EventType.LOAD, e => {
if (typeof descriptor.size === 'number') {
metadataClb(nls.localize('imgMeta', '{0}x{1} {2}', image.naturalWidth, image.naturalHeight, BinarySize.formatSize(descriptor.size)));
} else {
metadataClb(nls.localize('imgMetaNoSize', '{0}x{1}', image.naturalWidth, image.naturalHeight));
}
scrollbar.scanDomNode();
image.style.visibility = 'visible';
updateScale(scale);
if (initialState.scale !== 'fit') {
scrollbar.setScrollPosition({
scrollLeft: initialState.offsetX,
scrollTop: initialState.offsetY,
});
}
}));
InlineImageView.imageSrc(descriptor, fileService).then(dataUri => {
const imgs = container.getElementsByTagName('img');
@@ -559,9 +568,9 @@ class InlineImageView {
return context;
}
private static imageSrc(descriptor: IResourceDescriptor, fileService: IFileService): TPromise<string> {
private static imageSrc(descriptor: IResourceDescriptor, fileService: IFileService): Thenable<string> {
if (descriptor.resource.scheme === Schemas.data) {
return TPromise.as(descriptor.resource.toString(true /* skip encoding */));
return Promise.resolve(descriptor.resource.toString(true /* skip encoding */));
}
return fileService.resolveContent(descriptor.resource, { encoding: 'base64' }).then(data => {
@@ -575,7 +584,8 @@ class InlineImageView {
function getMime(descriptor: IResourceDescriptor) {
let mime = descriptor.mime;
if (!mime && descriptor.resource.scheme !== Schemas.data) {
mime = mimes.getMediaMime(descriptor.resource.toString());
mime = mimes.getMediaMime(descriptor.resource.path);
}
return mime || mimes.MIME_BINARY;
}

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TPromise } from 'vs/base/common/winjs.base';
import * as DOM from 'vs/base/browser/dom';
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorInput, EditorOptions, SideBySideEditorInput, IEditorControl, IEditor } from 'vs/workbench/common/editor';
@@ -17,6 +16,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService';
import { SplitView, Sizing, Orientation } from 'vs/base/browser/ui/splitview/splitview';
import { Event, Relay, anyEvent, mapEvent, Emitter } from 'vs/base/common/event';
import { IStorageService } from 'vs/platform/storage/common/storage';
export class SideBySideEditor extends BaseEditor {
@@ -59,9 +59,10 @@ export class SideBySideEditor extends BaseEditor {
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IInstantiationService private instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService
) {
super(SideBySideEditor.ID, telemetryService, themeService);
super(SideBySideEditor.ID, telemetryService, themeService, storageService);
}
protected createEditor(parent: HTMLElement): void {
@@ -107,9 +108,11 @@ export class SideBySideEditor extends BaseEditor {
if (this.masterEditor) {
this.masterEditor.setVisible(visible, group);
}
if (this.detailsEditor) {
this.detailsEditor.setVisible(visible, group);
}
super.setEditorVisible(visible, group);
}
@@ -117,10 +120,13 @@ export class SideBySideEditor extends BaseEditor {
if (this.masterEditor) {
this.masterEditor.clearInput();
}
if (this.detailsEditor) {
this.detailsEditor.clearInput();
}
this.disposeEditors();
super.clearInput();
}
@@ -139,6 +145,7 @@ export class SideBySideEditor extends BaseEditor {
if (this.masterEditor) {
return this.masterEditor.getControl();
}
return null;
}
@@ -159,7 +166,10 @@ export class SideBySideEditor extends BaseEditor {
return this.setNewInput(newInput, options, token);
}
return TPromise.join([this.detailsEditor.setInput(newInput.details, null, token), this.masterEditor.setInput(newInput.master, options, token)]).then(() => void 0);
return Promise.all([
this.detailsEditor.setInput(newInput.details, null, token),
this.masterEditor.setInput(newInput.master, options, token)]
).then(() => void 0);
}
private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions, token: CancellationToken): Thenable<void> {
@@ -179,7 +189,7 @@ export class SideBySideEditor extends BaseEditor {
return editor;
}
private onEditorsCreated(details: BaseEditor, master: BaseEditor, detailsInput: EditorInput, masterInput: EditorInput, options: EditorOptions, token: CancellationToken): TPromise<void> {
private onEditorsCreated(details: BaseEditor, master: BaseEditor, detailsInput: EditorInput, masterInput: EditorInput, options: EditorOptions, token: CancellationToken): Promise<void> {
this.detailsEditor = details;
this.masterEditor = master;
@@ -190,7 +200,7 @@ export class SideBySideEditor extends BaseEditor {
this.onDidCreateEditors.fire();
return TPromise.join([this.detailsEditor.setInput(detailsInput, null, token), this.masterEditor.setInput(masterInput, options, token)]).then(() => this.focus());
return Promise.all([this.detailsEditor.setInput(detailsInput, null, token), this.masterEditor.setInput(masterInput, options, token)]).then(() => this.focus());
}
updateStyles(): void {

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/tabstitlecontrol';
import { isMacintosh } from 'vs/base/common/platform';
import { shorten } from 'vs/base/common/labels';
@@ -26,11 +24,10 @@ import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecyc
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { getOrSet } from 'vs/base/common/map';
// {{SQL CARBON EDIT}} -- Display the editor's tab color
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService';
import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP } from 'vs/workbench/common/theme';
import { activeContrastBorder, contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER } from 'vs/workbench/common/theme';
import { activeContrastBorder, contrastBorder, editorBackground, breadcrumbsBackground } from 'vs/platform/theme/common/colorRegistry';
import { ResourcesDropHandler, fillResourceDataTransfers, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DragAndDropObserver } from 'vs/workbench/browser/dnd';
import { Color } from 'vs/base/common/color';
import { INotificationService } from 'vs/platform/notification/common/notification';
@@ -43,9 +40,7 @@ import { IEditorGroupsAccessor, IEditorPartOptions, IEditorGroupView } from 'vs/
import { CloseOneEditorAction } from 'vs/workbench/browser/parts/editor/editorActions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { BreadcrumbsControl } from 'vs/workbench/browser/parts/editor/breadcrumbsControl';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import * as QueryConstants from 'sql/parts/query/common/constants';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import { IFileService } from 'vs/platform/files/common/files';
// {{SQL CARBON EDIT}} -- Display the editor's tab color
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
@@ -53,6 +48,10 @@ import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/q
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { GlobalNewUntitledFileAction } from 'vs/workbench/parts/files/electron-browser/fileActions';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import * as QueryConstants from 'sql/parts/query/common/constants';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
// {{SQL CARBON EDIT}} -- End
interface IEditorInputLabel {
name: string;
@@ -67,7 +66,7 @@ export class TabsTitleControl extends TitleControl {
private titleContainer: HTMLElement;
private tabsContainer: HTMLElement;
private editorToolbarContainer: HTMLElement;
private scrollbar: ScrollableElement;
private tabsScrollbar: ScrollableElement;
private closeOneEditorAction: CloseOneEditorAction;
private tabLabelWidgets: ResourceLabel[] = [];
@@ -94,19 +93,26 @@ export class TabsTitleControl extends TitleControl {
@IThemeService themeService: IThemeService,
@IExtensionService extensionService: IExtensionService,
@IConfigurationService configurationService: IConfigurationService,
@IFileService fileService: IFileService,
// {{SQL CARBON EDIT}} -- Display the editor's tab color
@IWorkspaceConfigurationService private workspaceConfigurationService: IWorkspaceConfigurationService,
@ICommandService private commandService: ICommandService,
@IConnectionManagementService private connectionService: IConnectionManagementService,
@IQueryEditorService private queryEditorService: IQueryEditorService,
@IObjectExplorerService private objectExplorerService: IObjectExplorerService
@IObjectExplorerService private objectExplorerService: IObjectExplorerService,
// {{SQL CARBON EDIT}} -- End
) {
super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService, configurationService);
super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService, configurationService, fileService);
}
protected create(parent: HTMLElement): void {
this.titleContainer = parent;
// Tabs and Actions Container (are on a single row with flex side-by-side)
const tabsAndActionsContainer = document.createElement('div');
addClass(tabsAndActionsContainer, 'tabs-and-actions-container');
this.titleContainer.appendChild(tabsAndActionsContainer);
// Tabs Container
this.tabsContainer = document.createElement('div');
this.tabsContainer.setAttribute('role', 'tablist');
@@ -114,15 +120,16 @@ export class TabsTitleControl extends TitleControl {
addClass(this.tabsContainer, 'tabs-container');
// Tabs Container listeners
this.registerContainerListeners();
this.registerTabsContainerListeners();
// Scrollbar
this.createScrollbar();
// Tabs Scrollbar
this.tabsScrollbar = this.createTabsScrollbar(this.tabsContainer);
tabsAndActionsContainer.appendChild(this.tabsScrollbar.getDomNode());
// Editor Toolbar Container
this.editorToolbarContainer = document.createElement('div');
addClass(this.editorToolbarContainer, 'editor-actions');
this.titleContainer.appendChild(this.editorToolbarContainer);
tabsAndActionsContainer.appendChild(this.editorToolbarContainer);
// Editor Actions Toolbar
this.createEditorActionsToolBar(this.editorToolbarContainer);
@@ -130,17 +137,15 @@ export class TabsTitleControl extends TitleControl {
// Close Action
this.closeOneEditorAction = this._register(this.instantiationService.createInstance(CloseOneEditorAction, CloseOneEditorAction.ID, CloseOneEditorAction.LABEL));
// Breadcrumbs
// Breadcrumbs (are on a separate row below tabs and actions)
const breadcrumbsContainer = document.createElement('div');
addClass(breadcrumbsContainer, 'tabs-breadcrumbs');
this.titleContainer.appendChild(breadcrumbsContainer);
this.createBreadcrumbsControl(breadcrumbsContainer, { showFileIcons: true, showSymbolIcons: true, showDecorationColors: false, extraClasses: [] });
this.createBreadcrumbsControl(breadcrumbsContainer, { showFileIcons: true, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: breadcrumbsBackground });
}
private createScrollbar(): void {
// Custom Scrollbar
this.scrollbar = new ScrollableElement(this.tabsContainer, {
private createTabsScrollbar(scrollable: HTMLElement): ScrollableElement {
const tabsScrollbar = new ScrollableElement(scrollable, {
horizontal: ScrollbarVisibility.Auto,
vertical: ScrollbarVisibility.Hidden,
scrollYToX: true,
@@ -148,11 +153,11 @@ export class TabsTitleControl extends TitleControl {
horizontalScrollbarSize: 3
});
this.scrollbar.onScroll(e => {
this.tabsContainer.scrollLeft = e.scrollLeft;
tabsScrollbar.onScroll(e => {
scrollable.scrollLeft = e.scrollLeft;
});
this.titleContainer.appendChild(this.scrollbar.getDomNode());
return tabsScrollbar;
}
private updateBreadcrumbsControl(): void {
@@ -163,7 +168,12 @@ export class TabsTitleControl extends TitleControl {
}
}
private registerContainerListeners(): void {
protected handleBreadcrumbsEnablementChange(): void {
// relayout when breadcrumbs are enable/disabled
this.group.relayout();
}
private registerTabsContainerListeners(): void {
// Group dragging
this.enableGroupDragging(this.tabsContainer);
@@ -171,7 +181,7 @@ export class TabsTitleControl extends TitleControl {
// Forward scrolling inside the container to our custom scrollbar
this._register(addDisposableListener(this.tabsContainer, EventType.SCROLL, () => {
if (hasClass(this.tabsContainer, 'scroll')) {
this.scrollbar.setScrollPosition({
this.tabsScrollbar.setScrollPosition({
scrollLeft: this.tabsContainer.scrollLeft // during DND the container gets scrolled so we need to update the custom scrollbar
});
}
@@ -182,7 +192,7 @@ export class TabsTitleControl extends TitleControl {
if (e.target === this.tabsContainer) {
EventHelper.stop(e);
// {{SQL CARBON EDIT}}
this.commandService.executeCommand(GlobalNewUntitledFileAction.ID).done(undefined, err => this.notificationService.warn(err));
this.commandService.executeCommand(GlobalNewUntitledFileAction.ID).then(undefined, err => this.notificationService.warn(err));
}
}));
@@ -354,7 +364,7 @@ export class TabsTitleControl extends TitleControl {
// Activity has an impact on each tab
this.forEachTab((editor, index, tabContainer, tabLabelWidget, tabLabel) => {
this.redrawEditorActive(isGroupActive, editor, tabContainer, tabLabelWidget);
this.redrawEditorActiveAndDirty(isGroupActive, editor, tabContainer, tabLabelWidget);
});
// Activity has an impact on the toolbar, so we need to update and layout
@@ -377,7 +387,7 @@ export class TabsTitleControl extends TitleControl {
}
updateEditorDirty(editor: IEditorInput): void {
this.withTab(editor, tabContainer => this.redrawEditorDirty(editor, tabContainer));
this.withTab(editor, (tabContainer, tabLabelWidget) => this.redrawEditorActiveAndDirty(this.accessor.activeGroup === this.group, editor, tabContainer, tabLabelWidget));
}
updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void {
@@ -393,7 +403,8 @@ export class TabsTitleControl extends TitleControl {
oldOptions.tabCloseButton !== newOptions.tabCloseButton ||
oldOptions.tabSizing !== newOptions.tabSizing ||
oldOptions.showIcons !== newOptions.showIcons ||
oldOptions.iconTheme !== newOptions.iconTheme
oldOptions.iconTheme !== newOptions.iconTheme ||
oldOptions.highlightModifiedTabs !== newOptions.highlightModifiedTabs
) {
this.redraw();
}
@@ -493,7 +504,7 @@ export class TabsTitleControl extends TitleControl {
// Touch Scroll Support
disposables.push(addDisposableListener(tab, TouchEventType.Change, (e: GestureEvent) => {
this.scrollbar.setScrollPosition({ scrollLeft: this.scrollbar.getScrollPosition().scrollLeft - e.translationX });
this.tabsScrollbar.setScrollPosition({ scrollLeft: this.tabsScrollbar.getScrollPosition().scrollLeft - e.translationX });
}));
// Close on mouse middle click
@@ -502,7 +513,9 @@ export class TabsTitleControl extends TitleControl {
tab.blur();
if (e.button === 1 /* Middle Button*/ && !this.originatesFromTabActionBar(e)) {
if (e.button === 1 /* Middle Button*/) {
e.stopPropagation(); // for https://github.com/Microsoft/vscode/issues/56715
this.blockRevealActiveTabOnce();
this.closeOneEditorAction.run({ groupId: this.group.id, editorIndex: index });
}
@@ -558,7 +571,7 @@ export class TabsTitleControl extends TitleControl {
}
// moving in the tabs container can have an impact on scrolling position, so we need to update the custom scrollbar
this.scrollbar.setScrollPosition({
this.tabsScrollbar.setScrollPosition({
scrollLeft: this.tabsContainer.scrollLeft
});
}));
@@ -838,9 +851,7 @@ export class TabsTitleControl extends TitleControl {
this.redrawLabel(editor, tabContainer, tabLabelWidget, tabLabel);
// Borders / Outline
const borderLeftColor = (index !== 0) ? (this.getColor(TAB_BORDER) || this.getColor(contrastBorder)) : null;
const borderRightColor = (index === this.group.count - 1) ? (this.getColor(TAB_BORDER) || this.getColor(contrastBorder)) : null;
tabContainer.style.borderLeft = borderLeftColor ? `1px solid ${borderLeftColor}` : null;
const borderRightColor = (this.getColor(TAB_BORDER) || this.getColor(contrastBorder));
tabContainer.style.borderRight = borderRightColor ? `1px solid ${borderRightColor}` : null;
tabContainer.style.outlineColor = this.getColor(activeContrastBorder);
@@ -863,12 +874,8 @@ export class TabsTitleControl extends TitleControl {
removeClass(tabContainer, 'has-icon-theme');
}
// Active state
this.redrawEditorActive(this.accessor.activeGroup === this.group, editor, tabContainer, tabLabelWidget);
// Dirty State
this.redrawEditorDirty(editor, tabContainer);
// Active / dirty state
this.redrawEditorActiveAndDirty(this.accessor.activeGroup === this.group, editor, tabContainer, tabLabelWidget);
// {{SQL CARBON EDIT}} -- Display the editor's tab color
this.setEditorTabColor(editor, tabContainer, this.group.isActive(editor));
}
@@ -883,15 +890,22 @@ export class TabsTitleControl extends TitleControl {
tabContainer.title = title;
// Label
// {{SQL CARBON EDIT}} -- add title in options passed
tabLabelWidget.setLabel({ name, description, resource: toResource(editor, { supportSideBySide: true }) }, { extraClasses: ['tab-label'], italic: !this.group.isPinned(editor), title });
tabLabelWidget.setLabel({ name, description, resource: toResource(editor, { supportSideBySide: true }) }, { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor) });
// {{SQL CARBON EDIT}} -- Display the editor's tab color
const isTabActive = this.group.isActive(editor);
this.setEditorTabColor(editor, tabContainer, isTabActive);
}
private redrawEditorActive(isGroupActive: boolean, editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: ResourceLabel): void {
private redrawEditorActiveAndDirty(isGroupActive: boolean, editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: ResourceLabel): void {
const isTabActive = this.group.isActive(editor);
const hasModifiedBorderTop = this.doRedrawEditorDirty(isGroupActive, isTabActive, editor, tabContainer);
this.doRedrawEditorActive(isGroupActive, !hasModifiedBorderTop, editor, tabContainer, tabLabelWidget);
}
private doRedrawEditorActive(isGroupActive: boolean, allowBorderTop: boolean, editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: ResourceLabel): void {
// Tab is active
if (this.group.isActive(editor)) {
@@ -910,7 +924,7 @@ export class TabsTitleControl extends TitleControl {
tabContainer.style.removeProperty('--tab-border-bottom-color');
}
const activeTabBorderColorTop = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER_TOP : TAB_UNFOCUSED_ACTIVE_BORDER_TOP);
const activeTabBorderColorTop = allowBorderTop ? this.getColor(isGroupActive ? TAB_ACTIVE_BORDER_TOP : TAB_UNFOCUSED_ACTIVE_BORDER_TOP) : void 0;
if (activeTabBorderColorTop) {
addClass(tabContainer, 'tab-border-top');
tabContainer.style.setProperty('--tab-border-top-color', activeTabBorderColorTop.toString());
@@ -926,7 +940,7 @@ export class TabsTitleControl extends TitleControl {
// Tab is inactive
else {
// Containr
// Container
removeClass(tabContainer, 'active');
tabContainer.setAttribute('aria-selected', 'false');
tabContainer.style.backgroundColor = this.getColor(TAB_INACTIVE_BACKGROUND);
@@ -937,22 +951,57 @@ export class TabsTitleControl extends TitleControl {
}
}
private redrawEditorDirty(editor: IEditorInput, tabContainer: HTMLElement): void {
private doRedrawEditorDirty(isGroupActive: boolean, isTabActive: boolean, editor: IEditorInput, tabContainer: HTMLElement): boolean {
let hasModifiedBorderColor = false;
// Tab: dirty
if (editor.isDirty()) {
addClass(tabContainer, 'dirty');
} else {
removeClass(tabContainer, 'dirty');
// Highlight modified tabs with a border if configured
if (this.accessor.partOptions.highlightModifiedTabs) {
let modifiedBorderColor: string;
if (isGroupActive && isTabActive) {
modifiedBorderColor = this.getColor(TAB_ACTIVE_MODIFIED_BORDER);
} else if (isGroupActive && !isTabActive) {
modifiedBorderColor = this.getColor(TAB_INACTIVE_MODIFIED_BORDER);
} else if (!isGroupActive && isTabActive) {
modifiedBorderColor = this.getColor(TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER);
} else {
modifiedBorderColor = this.getColor(TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER);
}
if (modifiedBorderColor) {
hasModifiedBorderColor = true;
addClass(tabContainer, 'dirty-border-top');
tabContainer.style.setProperty('--tab-dirty-border-top-color', modifiedBorderColor);
}
} else {
removeClass(tabContainer, 'dirty-border-top');
tabContainer.style.removeProperty('--tab-dirty-border-top-color');
}
}
// Tab: not dirty
else {
removeClass(tabContainer, 'dirty');
removeClass(tabContainer, 'dirty-border-top');
tabContainer.style.removeProperty('--tab-dirty-border-top-color');
}
return hasModifiedBorderColor;
}
layout(dimension: Dimension): void {
this.dimension = dimension;
const activeTab = this.getTab(this.group.activeEditor);
if (!activeTab || !dimension) {
if (!activeTab || !this.dimension) {
return;
}
this.dimension = dimension;
// The layout of tabs can be an expensive operation because we access DOM properties
// that can result in the browser doing a full page layout to validate them. To buffer
// this a little bit we try at least to schedule this work on the next animation frame.
@@ -972,7 +1021,7 @@ export class TabsTitleControl extends TitleControl {
if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) {
this.breadcrumbsControl.layout({ width: dimension.width, height: BreadcrumbsControl.HEIGHT });
this.scrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`;
this.tabsScrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`;
}
const visibleContainerWidth = this.tabsContainer.offsetWidth;
@@ -987,7 +1036,7 @@ export class TabsTitleControl extends TitleControl {
}
// Update scrollbar
this.scrollbar.setScrollDimensions({
this.tabsScrollbar.setScrollDimensions({
width: visibleContainerWidth,
scrollWidth: totalContainerWidth
});
@@ -999,20 +1048,20 @@ export class TabsTitleControl extends TitleControl {
}
// Reveal the active one
const containerScrollPosX = this.scrollbar.getScrollPosition().scrollLeft;
const containerScrollPosX = this.tabsScrollbar.getScrollPosition().scrollLeft;
const activeTabFits = activeTabWidth <= visibleContainerWidth;
// Tab is overflowing to the right: Scroll minimally until the element is fully visible to the right
// Note: only try to do this if we actually have enough width to give to show the tab fully!
if (activeTabFits && containerScrollPosX + visibleContainerWidth < activeTabPosX + activeTabWidth) {
this.scrollbar.setScrollPosition({
this.tabsScrollbar.setScrollPosition({
scrollLeft: containerScrollPosX + ((activeTabPosX + activeTabWidth) /* right corner of tab */ - (containerScrollPosX + visibleContainerWidth) /* right corner of view port */)
});
}
// Tab is overlflowng to the left or does not fit: Scroll it into view to the left
else if (containerScrollPosX > activeTabPosX || !activeTabFits) {
this.scrollbar.setScrollPosition({
this.tabsScrollbar.setScrollPosition({
scrollLeft: activeTabPosX
});
}
@@ -1247,7 +1296,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const adjustedColorDrag = editorDragAndDropBackground.flatten(adjustedTabDragBackground);
collector.addRule(`
.monaco-workbench > .part.editor > .content.dragged-over .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.active):not(.dragged) > .tab-label::after,
.monaco-workbench > .part.editor > .content.dragged-over .editor-group-container > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.dragged) > .tab-label::after {
.monaco-workbench > .part.editor > .content.dragged-over .editor-group-container:not(.active) > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.dragged) > .tab-label::after {
background: linear-gradient(to left, ${adjustedColorDrag}, transparent) !important;
}
`);

View File

@@ -3,13 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/textdiffeditor';
import { TPromise } from 'vs/base/common/winjs.base';
import * as nls from 'vs/nls';
import * as objects from 'vs/base/common/objects';
import { Action, IAction } from 'vs/base/common/actions';
import * as types from 'vs/base/common/types';
import { IDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions';
@@ -28,15 +23,15 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ScrollType, IDiffEditorViewState, IDiffEditorModel } from 'vs/editor/common/editorCommon';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { once } from 'vs/base/common/event';
import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor';
import { IWindowService } from 'vs/platform/windows/common/windows';
/**
* The text editor that leverages the diff text editor for the editing experience.
@@ -47,31 +42,22 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor {
private diffNavigator: DiffNavigator;
private diffNavigatorDisposables: IDisposable[] = [];
private nextDiffAction: NavigateAction;
private previousDiffAction: NavigateAction;
private toggleIgnoreTrimWhitespaceAction: ToggleIgnoreTrimWhitespaceAction;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IInstantiationService instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService,
@ITextResourceConfigurationService configurationService: ITextResourceConfigurationService,
@IConfigurationService private readonly _actualConfigurationService: IConfigurationService,
@IEditorService editorService: IEditorService,
@IThemeService themeService: IThemeService,
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@ITextFileService textFileService: ITextFileService,
@IWindowService windowService: IWindowService
) {
super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService);
this._register(this._actualConfigurationService.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('diffEditor.ignoreTrimWhitespace')) {
this.updateIgnoreTrimWhitespaceAction();
}
}));
super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService, windowService);
}
protected getEditorMemento<T>(storageService: IStorageService, editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento<T> {
protected getEditorMemento<T>(editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento<T> {
return new EditorMemento(this.getId(), key, Object.create(null), limit, editorGroupService); // do not persist in storage as diff editors are never persisted
}
@@ -84,13 +70,6 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor {
}
createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IDiffEditor {
// Actions
this.nextDiffAction = new NavigateAction(this, true);
this.previousDiffAction = new NavigateAction(this, false);
this.toggleIgnoreTrimWhitespaceAction = new ToggleIgnoreTrimWhitespaceAction(this._actualConfigurationService);
this.updateIgnoreTrimWhitespaceAction();
return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration);
}
@@ -139,14 +118,6 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor {
});
this.diffNavigatorDisposables.push(this.diffNavigator);
this.diffNavigatorDisposables.push(this.diffNavigator.onDidUpdate(() => {
this.nextDiffAction.updateEnablement();
this.previousDiffAction.updateEnablement();
}));
// Enablement of actions
this.updateIgnoreTrimWhitespaceAction();
// Readonly flag
diffEditor.updateOptions({ readOnly: resolvedDiffEditorModel.isReadonly() });
}, error => {
@@ -157,7 +128,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor {
}
// Otherwise make sure the error bubbles up
return TPromise.wrapError(error);
return Promise.reject(error);
});
});
}
@@ -185,13 +156,6 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor {
return false;
}
private updateIgnoreTrimWhitespaceAction(): void {
const ignoreTrimWhitespace = this.configurationService.getValue<boolean>(this.getResource(), 'diffEditor.ignoreTrimWhitespace');
if (this.toggleIgnoreTrimWhitespaceAction) {
this.toggleIgnoreTrimWhitespaceAction.updateClassName(ignoreTrimWhitespace);
}
}
private openAsBinary(input: EditorInput, options: EditorOptions): boolean {
if (input instanceof DiffEditorInput) {
const originalInput = input.originalInput;
@@ -232,6 +196,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor {
const options: IDiffEditorOptions = super.getConfigurationOverrides();
options.readOnly = this.isReadOnly();
options.lineDecorationsWidth = '2ch';
return options;
}
@@ -289,14 +254,6 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor {
return this.diffNavigator;
}
getActions(): IAction[] {
return [
this.toggleIgnoreTrimWhitespaceAction,
this.previousDiffAction,
this.nextDiffAction
];
}
getControl(): IDiffEditor {
return super.getControl() as IDiffEditor;
}
@@ -380,59 +337,3 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor {
super.dispose();
}
}
class NavigateAction extends Action {
static ID_NEXT = 'workbench.action.compareEditor.nextChange';
static ID_PREV = 'workbench.action.compareEditor.previousChange';
private editor: TextDiffEditor;
private next: boolean;
constructor(editor: TextDiffEditor, next: boolean) {
super(next ? NavigateAction.ID_NEXT : NavigateAction.ID_PREV);
this.editor = editor;
this.next = next;
this.label = this.next ? nls.localize('navigate.next.label', "Next Change") : nls.localize('navigate.prev.label', "Previous Change");
this.class = this.next ? 'textdiff-editor-action next' : 'textdiff-editor-action previous';
this.enabled = false;
}
run(): TPromise<any> {
if (this.next) {
this.editor.getDiffNavigator().next();
} else {
this.editor.getDiffNavigator().previous();
}
return null;
}
updateEnablement(): void {
this.enabled = this.editor.getDiffNavigator().canNavigate();
}
}
class ToggleIgnoreTrimWhitespaceAction extends Action {
static ID = 'workbench.action.compareEditor.toggleIgnoreTrimWhitespace';
private _isChecked: boolean;
constructor(
@IConfigurationService private readonly _configurationService: IConfigurationService
) {
super(ToggleIgnoreTrimWhitespaceAction.ID);
this.label = nls.localize('toggleIgnoreTrimWhitespace.label', "Ignore Trim Whitespace");
}
updateClassName(ignoreTrimWhitespace: boolean): void {
this._isChecked = ignoreTrimWhitespace;
this.class = `textdiff-editor-action toggleIgnoreTrimWhitespace${this._isChecked ? ' is-checked' : ''}`;
}
run(): TPromise<any> {
this._configurationService.updateValue(`diffEditor.ignoreTrimWhitespace`, !this._isChecked);
return null;
}
}

View File

@@ -3,13 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import * as objects from 'vs/base/common/objects';
import * as types from 'vs/base/common/types';
import * as errors from 'vs/base/common/errors';
import * as DOM from 'vs/base/browser/dom';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { EditorInput, EditorOptions, IEditorMemento, ITextEditor } from 'vs/workbench/common/editor';
@@ -26,6 +23,7 @@ import { isDiffEditor, isCodeEditor, ICodeEditor, getCodeEditor } from 'vs/edito
import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWindowService } from 'vs/platform/windows/common/windows';
const TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState';
@@ -55,10 +53,11 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor {
@ITextFileService private readonly _textFileService: ITextFileService,
@IEditorService protected editorService: IEditorService,
@IEditorGroupsService protected editorGroupService: IEditorGroupsService,
@IWindowService private windowService: IWindowService
) {
super(id, telemetryService, themeService);
super(id, telemetryService, themeService, storageService);
this.editorMemento = this.getEditorMemento<IEditorViewState>(storageService, editorGroupService, TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100);
this.editorMemento = this.getEditorMemento<IEditorViewState>(editorGroupService, TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100);
this._register(this.configurationService.onDidChangeConfiguration(e => this.handleConfigurationChangeEvent(this.configurationService.getValue<IEditorConfiguration>(this.getResource()))));
}
@@ -75,7 +74,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor {
return this._textFileService;
}
private handleConfigurationChangeEvent(configuration?: IEditorConfiguration): void {
protected handleConfigurationChangeEvent(configuration?: IEditorConfiguration): void {
if (this.isVisible()) {
this.updateEditorConfiguration(configuration);
} else {
@@ -148,15 +147,17 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor {
}
this._register(this.editorService.onDidActiveEditorChange(() => this.onEditorFocusLost()));
this._register(DOM.addDisposableListener(window, DOM.EventType.BLUR, () => this.onWindowFocusLost()));
this._register(this.windowService.onDidChangeFocus(focused => this.onWindowFocusChange(focused)));
}
private onEditorFocusLost(): void {
this.maybeTriggerSaveAll(SaveReason.FOCUS_CHANGE);
}
private onWindowFocusLost(): void {
this.maybeTriggerSaveAll(SaveReason.WINDOW_CHANGE);
private onWindowFocusChange(focused: boolean): void {
if (!focused) {
this.maybeTriggerSaveAll(SaveReason.WINDOW_CHANGE);
}
}
private maybeTriggerSaveAll(reason: SaveReason): void {
@@ -169,7 +170,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor {
(reason === SaveReason.FOCUS_CHANGE && mode === AutoSaveMode.ON_FOCUS_CHANGE)
) {
if (this.textFileService.isDirty()) {
this.textFileService.saveAll(void 0, { reason }).done(null, errors.onUnexpectedError);
this.textFileService.saveAll(void 0, { reason });
}
}
}
@@ -232,7 +233,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor {
return;
}
this.editorMemento.saveState(this.group, resource, editorViewState);
this.editorMemento.saveEditorState(this.group, resource, editorViewState);
}
protected retrieveTextEditorViewState(resource: URI): IEditorViewState {
@@ -257,9 +258,9 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor {
/**
* Clears the text editor view state for the given resources.
*/
protected clearTextEditorViewState(resources: URI[]): void {
protected clearTextEditorViewState(resources: URI[], group?: IEditorGroup): void {
resources.forEach(resource => {
this.editorMemento.clearState(resource);
this.editorMemento.clearEditorState(resource, group);
});
}
@@ -267,7 +268,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor {
* Loads the text editor view state for the given resource and returns it.
*/
protected loadTextEditorViewState(resource: URI): IEditorViewState {
return this.editorMemento.loadState(this.group, resource);
return this.editorMemento.loadEditorState(this.group, resource);
}
private updateEditorConfiguration(configuration = this.configurationService.getValue<IEditorConfiguration>(this.getResource())): void {

View File

@@ -2,9 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import * as nls from 'vs/nls';
import * as types from 'vs/base/common/types';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
@@ -25,6 +23,7 @@ import { ScrollType } from 'vs/editor/common/editorCommon';
import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWindowService } from 'vs/platform/windows/common/windows';
/**
* An editor implementation that is capable of showing the contents of resource inputs. Uses
@@ -41,9 +40,10 @@ export class AbstractTextResourceEditor extends BaseTextEditor {
@IThemeService themeService: IThemeService,
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@ITextFileService textFileService: ITextFileService,
@IEditorService editorService: IEditorService
@IEditorService editorService: IEditorService,
@IWindowService windowService: IWindowService
) {
super(id, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService);
super(id, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService, windowService);
}
getTitle(): string {
@@ -70,7 +70,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor {
// Assert Model instance
if (!(resolvedModel instanceof BaseTextEditorModel)) {
return TPromise.wrapError<void>(new Error('Unable to open file as text'));
return Promise.reject(new Error('Unable to open file as text'));
}
// Set Editor Model
@@ -163,15 +163,14 @@ export class AbstractTextResourceEditor extends BaseTextEditor {
super.clearInput();
}
shutdown(): void {
protected saveState(): void {
// Save View State (only for untitled)
if (this.input instanceof UntitledEditorInput) {
this.saveTextResourceEditorViewState(this.input);
}
// Call Super
super.shutdown();
super.saveState();
}
private saveTextResourceEditorViewState(input: EditorInput): void {
@@ -210,8 +209,9 @@ export class TextResourceEditor extends AbstractTextResourceEditor {
@IThemeService themeService: IThemeService,
@ITextFileService textFileService: ITextFileService,
@IEditorService editorService: IEditorService,
@IEditorGroupsService editorGroupService: IEditorGroupsService
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@IWindowService windowService: IWindowService
) {
super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorGroupService, textFileService, editorService);
super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorGroupService, textFileService, editorService, windowService);
}
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { applyDragImage } from 'vs/base/browser/dnd';
import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
@@ -14,7 +12,6 @@ import { Action, IAction, IRunEvent } from 'vs/base/common/actions';
import * as arrays from 'vs/base/common/arrays';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { TPromise } from 'vs/base/common/winjs.base';
import 'vs/css!./media/titlecontrol';
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
import { localize } from 'vs/nls';
@@ -40,6 +37,8 @@ import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput
import { ResourceContextKey } from 'vs/workbench/common/resources';
import { Themable } from 'vs/workbench/common/theme';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { IFileService } from 'vs/platform/files/common/files';
export interface IToolbarActions {
primary: IAction[];
@@ -57,8 +56,6 @@ export abstract class TitleControl extends Themable {
private currentSecondaryEditorActionIds: string[] = [];
protected editorActionsToolbar: ToolBar;
private mapEditorToActions: Map<string, IToolbarActions> = new Map();
private resourceContext: ResourceContextKey;
private editorToolBarMenuDisposables: IDisposable[] = [];
@@ -79,11 +76,12 @@ export abstract class TitleControl extends Themable {
@IQuickOpenService protected quickOpenService: IQuickOpenService,
@IThemeService themeService: IThemeService,
@IExtensionService private extensionService: IExtensionService,
@IConfigurationService protected configurationService: IConfigurationService
@IConfigurationService protected configurationService: IConfigurationService,
@IFileService private readonly fileService: IFileService,
) {
super(themeService);
this.resourceContext = instantiationService.createInstance(ResourceContextKey);
this.resourceContext = this._register(instantiationService.createInstance(ResourceContextKey));
this.contextMenu = this._register(this.menuService.createMenu(MenuId.EditorTitleContext, this.contextKeyService));
this.create(parent);
@@ -98,22 +96,31 @@ export abstract class TitleControl extends Themable {
protected createBreadcrumbsControl(container: HTMLElement, options: IBreadcrumbsControlOptions): void {
const config = this._register(BreadcrumbsConfig.IsEnabled.bindTo(this.configurationService));
config.onDidChange(value => {
this._register(config.onDidChange(() => {
const value = config.getValue();
if (!value && this.breadcrumbsControl) {
this.breadcrumbsControl.dispose();
this.breadcrumbsControl = undefined;
this.group.relayout();
this.handleBreadcrumbsEnablementChange();
} else if (value && !this.breadcrumbsControl) {
this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group);
this.breadcrumbsControl.update();
this.group.relayout();
this.handleBreadcrumbsEnablementChange();
}
});
if (config.value) {
}));
if (config.getValue()) {
this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group);
}
this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(() => {
if (this.breadcrumbsControl && this.breadcrumbsControl.update()) {
this.handleBreadcrumbsEnablementChange();
}
}));
}
protected abstract handleBreadcrumbsEnablementChange(): void;
protected createEditorActionsToolBar(container: HTMLElement): void {
const context = { groupId: this.group.id } as IEditorCommandsContext;
@@ -122,7 +129,8 @@ export abstract class TitleControl extends Themable {
orientation: ActionsOrientation.HORIZONTAL,
ariaLabel: localize('araLabelEditorActions', "Editor actions"),
getKeyBinding: action => this.getKeybinding(action),
actionRunner: this._register(new EditorCommandsContextActionRunner(context))
actionRunner: this._register(new EditorCommandsContextActionRunner(context)),
anchorAlignmentProvider: () => AnchorAlignment.RIGHT
}));
// Context
@@ -215,17 +223,6 @@ export abstract class TitleControl extends Themable {
// Editor actions require the editor control to be there, so we retrieve it via service
const activeControl = this.group.activeControl;
if (activeControl instanceof BaseEditor) {
// Editor Control Actions
let editorActions = this.mapEditorToActions.get(activeControl.getId());
if (!editorActions) {
editorActions = { primary: activeControl.getActions(), secondary: activeControl.getSecondaryActions() };
this.mapEditorToActions.set(activeControl.getId(), editorActions);
}
primary.push(...editorActions.primary);
secondary.push(...editorActions.secondary);
// Contributed Actions
const codeEditor = getCodeEditor(activeControl.getControl());
const scopedContextKeyService = codeEditor && codeEditor.invokeWithinContext(accessor => accessor.get(IContextKeyService)) || this.contextKeyService;
const titleBarMenu = this.menuService.createMenu(MenuId.EditorTitle, scopedContextKeyService);
@@ -302,7 +299,7 @@ export abstract class TitleControl extends Themable {
// Show it
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => TPromise.as(actions),
getActions: () => actions,
getActionsContext: () => ({ groupId: this.group.id, editorIndex: this.group.getIndexOfEditor(editor) } as IEditorCommandsContext),
getKeyBinding: (action) => this.getKeybinding(action),
onHide: () => {
@@ -316,7 +313,7 @@ export abstract class TitleControl extends Themable {
});
}
protected getKeybinding(action: IAction): ResolvedKeybinding {
private getKeybinding(action: IAction): ResolvedKeybinding {
return this.keybindingService.lookupKeybinding(action.id);
}

View File

@@ -1,50 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-workbench .part.menubar {
display: flex;
flex-shrink: 1;
box-sizing: border-box;
height: 30px;
-webkit-app-region: no-drag;
overflow: hidden;
flex-wrap: wrap;
}
.monaco-workbench.fullscreen .part.menubar {
margin: 0px;
padding: 0px 5px;
}
.monaco-workbench .part.menubar > .menubar-menu-button {
align-items: center;
box-sizing: border-box;
padding: 0px 8px;
cursor: default;
-webkit-app-region: no-drag;
zoom: 1;
white-space: nowrap;
}
.monaco-workbench .part.menubar .menubar-menu-items-holder {
position: absolute;
left: 0px;
opacity: 1;
z-index: 2000;
}
.monaco-workbench .part.menubar .menubar-menu-items-holder.monaco-menu-container {
font-family: "Segoe WPC", "Segoe UI", ".SFNSDisplay-Light", "SFUIText-Light", "HelveticaNeue-Light", sans-serif, "Droid Sans Fallback";
outline: 0;
border: none;
}
.monaco-workbench .part.menubar .menubar-menu-items-holder.monaco-menu-container :focus {
outline: 0;
}
.hc-black .monaco-workbench .part.menubar .menubar-menu-items-holder.monaco-menu-container {
border: 2px solid #6FC3DF;
}

View File

@@ -1,402 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { isMacintosh } from 'vs/base/common/platform';
goMenuRegistration();
if (isMacintosh) {
windowMenuRegistration();
}
helpMenuRegistration();
// Menu registration
function goMenuRegistration() {
// Forward/Back
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: '1_fwd_back',
command: {
id: 'workbench.action.navigateBack',
title: nls.localize({ key: 'miBack', comment: ['&& denotes a mnemonic'] }, "&&Back")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: '1_fwd_back',
command: {
id: 'workbench.action.navigateForward',
title: nls.localize({ key: 'miForward', comment: ['&& denotes a mnemonic'] }, "&&Forward")
},
order: 2
});
// Switch Editor
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchEditorMenu, {
group: '1_any',
command: {
id: 'workbench.action.nextEditor',
title: nls.localize({ key: 'miNextEditor', comment: ['&& denotes a mnemonic'] }, "&&Next Editor")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchEditorMenu, {
group: '1_any',
command: {
id: 'workbench.action.previousEditor',
title: nls.localize({ key: 'miPreviousEditor', comment: ['&& denotes a mnemonic'] }, "&&Previous Editor")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchEditorMenu, {
group: '2_used',
command: {
id: 'workbench.action.openNextRecentlyUsedEditorInGroup',
title: nls.localize({ key: 'miNextEditorInGroup', comment: ['&& denotes a mnemonic'] }, "&&Next Used Editor in Group")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchEditorMenu, {
group: '2_used',
command: {
id: 'workbench.action.openPreviousRecentlyUsedEditorInGroup',
title: nls.localize({ key: 'miPreviousEditorInGroup', comment: ['&& denotes a mnemonic'] }, "&&Previous Used Editor in Group")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: '2_switch',
title: nls.localize({ key: 'miSwitchEditor', comment: ['&& denotes a mnemonic'] }, "Switch &&Editor"),
submenu: MenuId.MenubarSwitchEditorMenu,
order: 1
});
// Switch Group
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '1_focus_index',
command: {
id: 'workbench.action.focusFirstEditorGroup',
title: nls.localize({ key: 'miFocusFirstGroup', comment: ['&& denotes a mnemonic'] }, "Group &&1")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '1_focus_index',
command: {
id: 'workbench.action.focusSecondEditorGroup',
title: nls.localize({ key: 'miFocusSecondGroup', comment: ['&& denotes a mnemonic'] }, "Group &&2")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '1_focus_index',
command: {
id: 'workbench.action.focusThirdEditorGroup',
title: nls.localize({ key: 'miFocusThirdGroup', comment: ['&& denotes a mnemonic'] }, "Group &&3")
},
order: 3
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '1_focus_index',
command: {
id: 'workbench.action.focusFourthEditorGroup',
title: nls.localize({ key: 'miFocusFourthGroup', comment: ['&& denotes a mnemonic'] }, "Group &&4")
},
order: 4
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '1_focus_index',
command: {
id: 'workbench.action.focusFifthEditorGroup',
title: nls.localize({ key: 'miFocusFifthGroup', comment: ['&& denotes a mnemonic'] }, "Group &&5")
},
order: 5
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '2_next_prev',
command: {
id: 'workbench.action.focusNextGroup',
title: nls.localize({ key: 'miNextGroup', comment: ['&& denotes a mnemonic'] }, "&&Next Group")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '2_next_prev',
command: {
id: 'workbench.action.focusPreviousGroup',
title: nls.localize({ key: 'miPreviousGroup', comment: ['&& denotes a mnemonic'] }, "&&Previous Group")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '3_directional',
command: {
id: 'workbench.action.focusLeftGroup',
title: nls.localize({ key: 'miFocusLeftGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Left")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '3_directional',
command: {
id: 'workbench.action.focusRightGroup',
title: nls.localize({ key: 'miFocusRightGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Right")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '3_directional',
command: {
id: 'workbench.action.focusAboveGroup',
title: nls.localize({ key: 'miFocusAboveGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Above")
},
order: 3
});
MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, {
group: '3_directional',
command: {
id: 'workbench.action.focusBelowGroup',
title: nls.localize({ key: 'miFocusBelowGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Below")
},
order: 4
});
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: '2_switch',
title: nls.localize({ key: 'miSwitchGroup', comment: ['&& denotes a mnemonic'] }, "Switch &&Group"),
submenu: MenuId.MenubarSwitchGroupMenu,
order: 2
});
// Go to
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: 'z_go_to',
command: {
id: 'workbench.action.quickOpen',
title: nls.localize({ key: 'miGotoFile', comment: ['&& denotes a mnemonic'] }, "Go to &&File...")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: 'z_go_to',
command: {
id: 'workbench.action.gotoSymbol',
title: nls.localize({ key: 'miGotoSymbolInFile', comment: ['&& denotes a mnemonic'] }, "Go to &&Symbol in File...")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: 'z_go_to',
command: {
id: 'workbench.action.showAllSymbols',
title: nls.localize({ key: 'miGotoSymbolInWorkspace', comment: ['&& denotes a mnemonic'] }, "Go to Symbol in &&Workspace...")
},
order: 3
});
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: 'z_go_to',
command: {
id: 'editor.action.goToDeclaration',
title: nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition")
},
order: 4
});
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: 'z_go_to',
command: {
id: 'editor.action.goToTypeDefinition',
title: nls.localize({ key: 'miGotoTypeDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Type Definition")
},
order: 5
});
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: 'z_go_to',
command: {
id: 'editor.action.goToImplementation',
title: nls.localize({ key: 'miGotoImplementation', comment: ['&& denotes a mnemonic'] }, "Go to &&Implementation")
},
order: 6
});
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
group: 'z_go_to',
command: {
id: 'workbench.action.gotoLine',
title: nls.localize({ key: 'miGotoLine', comment: ['&& denotes a mnemonic'] }, "Go to &&Line...")
},
order: 7
});
}
function windowMenuRegistration() {
}
function helpMenuRegistration() {
// Welcome
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '1_welcome',
command: {
id: 'workbench.action.showWelcomePage',
title: nls.localize({ key: 'miWelcome', comment: ['&& denotes a mnemonic'] }, "&&Welcome")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '1_welcome',
command: {
id: 'workbench.action.showInteractivePlayground',
title: nls.localize({ key: 'miInteractivePlayground', comment: ['&& denotes a mnemonic'] }, "&&Interactive Playground")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '1_welcome',
command: {
id: 'workbench.action.openDocumentationUrl',
title: nls.localize({ key: 'miDocumentation', comment: ['&& denotes a mnemonic'] }, "&&Documentation")
},
order: 3
});
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '1_welcome',
command: {
id: 'update.showCurrentReleaseNotes',
title: nls.localize({ key: 'miReleaseNotes', comment: ['&& denotes a mnemonic'] }, "&&Release Notes")
},
order: 4
});
// Reference
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '2_reference',
command: {
id: 'workbench.action.keybindingsReference',
title: nls.localize({ key: 'miKeyboardShortcuts', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts Reference")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '2_reference',
command: {
id: 'workbench.action.openIntroductoryVideosUrl',
title: nls.localize({ key: 'miIntroductoryVideos', comment: ['&& denotes a mnemonic'] }, "Introductory &&Videos")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '2_reference',
command: {
id: 'workbench.action.openTipsAndTricksUrl',
title: nls.localize({ key: 'miTipsAndTricks', comment: ['&& denotes a mnemonic'] }, "&&Tips and Tricks")
},
order: 3
});
// Feedback
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '3_feedback',
command: {
id: 'workbench.action.openTwitterUrl',
title: nls.localize({ key: 'miTwitter', comment: ['&& denotes a mnemonic'] }, "&&Join us on Twitter")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '3_feedback',
command: {
id: 'workbench.action.openRequestFeatureUrl',
title: nls.localize({ key: 'miUserVoice', comment: ['&& denotes a mnemonic'] }, "&&Search Feature Requests")
},
order: 2
});
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '3_feedback',
command: {
id: 'workbench.action.openIssueReporter',
title: nls.localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue")
},
order: 3
});
// Legal
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '4_legal',
command: {
id: 'workbench.action.openLicenseUrl',
title: nls.localize({ key: 'miLicense', comment: ['&& denotes a mnemonic'] }, "View &&License")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '4_legal',
command: {
id: 'workbench.action.openPrivacyStatementUrl',
title: nls.localize({ key: 'miPrivacyStatement', comment: ['&& denotes a mnemonic'] }, "&&Privacy Statement")
},
order: 2
});
// Tools
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '5_tools',
command: {
id: 'workbench.action.toggleDevTools',
title: nls.localize({ key: 'miToggleDevTools', comment: ['&& denotes a mnemonic'] }, "&&Toggle Developer Tools")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: '5_tools',
command: {
id: 'workbench.action.openProcessExplorer',
title: nls.localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer")
},
order: 2
});
if (!isMacintosh) {
// About
MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, {
group: 'z_about',
command: {
id: 'workbench.action.showAboutDialog',
title: nls.localize({ key: 'miAbout', comment: ['&& denotes a mnemonic'] }, "&&About")
},
order: 1
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -104,7 +104,7 @@
.monaco-workbench .notifications-list-container .notification-list-item > .notification-list-item-details-row {
display: none;
align-items: center;
padding: 0 5px;
padding-left: 5px;
overflow: hidden; /* details row should never overflow */
}
@@ -131,7 +131,7 @@
.monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-buttons-container .monaco-button {
max-width: fit-content;
padding: 5px 10px;
margin-left: 10px;
margin: 4px 5px; /* allows button focus outline to be visible */
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -3,13 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/notificationsActions';
import { INotificationViewItem } from 'vs/workbench/common/notifications';
import { localize } from 'vs/nls';
import { Action, IAction, ActionRunner } from 'vs/base/common/actions';
import { TPromise } from 'vs/base/common/winjs.base';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { CLEAR_NOTIFICATION, EXPAND_NOTIFICATION, COLLAPSE_NOTIFICATION, CLEAR_ALL_NOTIFICATIONS, HIDE_NOTIFICATIONS_CENTER } from 'vs/workbench/browser/parts/notifications/notificationsCommands';
@@ -29,10 +26,10 @@ export class ClearNotificationAction extends Action {
super(id, label, 'clear-notification-action');
}
run(notification: INotificationViewItem): TPromise<any> {
run(notification: INotificationViewItem): Promise<any> {
this.commandService.executeCommand(CLEAR_NOTIFICATION, notification);
return TPromise.as(void 0);
return Promise.resolve(void 0);
}
}
@@ -49,10 +46,10 @@ export class ClearAllNotificationsAction extends Action {
super(id, label, 'clear-all-notifications-action');
}
run(notification: INotificationViewItem): TPromise<any> {
run(notification: INotificationViewItem): Promise<any> {
this.commandService.executeCommand(CLEAR_ALL_NOTIFICATIONS);
return TPromise.as(void 0);
return Promise.resolve(void 0);
}
}
@@ -69,10 +66,10 @@ export class HideNotificationsCenterAction extends Action {
super(id, label, 'hide-all-notifications-action');
}
run(notification: INotificationViewItem): TPromise<any> {
run(notification: INotificationViewItem): Promise<any> {
this.commandService.executeCommand(HIDE_NOTIFICATIONS_CENTER);
return TPromise.as(void 0);
return Promise.resolve(void 0);
}
}
@@ -89,10 +86,10 @@ export class ExpandNotificationAction extends Action {
super(id, label, 'expand-notification-action');
}
run(notification: INotificationViewItem): TPromise<any> {
run(notification: INotificationViewItem): Promise<any> {
this.commandService.executeCommand(EXPAND_NOTIFICATION, notification);
return TPromise.as(void 0);
return Promise.resolve(void 0);
}
}
@@ -109,10 +106,10 @@ export class CollapseNotificationAction extends Action {
super(id, label, 'collapse-notification-action');
}
run(notification: INotificationViewItem): TPromise<any> {
run(notification: INotificationViewItem): Promise<any> {
this.commandService.executeCommand(COLLAPSE_NOTIFICATION, notification);
return TPromise.as(void 0);
return Promise.resolve(void 0);
}
}
@@ -147,10 +144,10 @@ export class CopyNotificationMessageAction extends Action {
super(id, label);
}
run(notification: INotificationViewItem): TPromise<any> {
run(notification: INotificationViewItem): Promise<any> {
this.clipboardService.writeText(notification.message.raw);
return TPromise.as(void 0);
return Promise.resolve(void 0);
}
}
@@ -163,7 +160,7 @@ export class NotificationActionRunner extends ActionRunner {
super();
}
protected runAction(action: IAction, context: INotificationViewItem): TPromise<any> {
protected runAction(action: IAction, context: INotificationViewItem): Promise<any> {
/* __GDPR__
"workbenchActionExecuted" : {
@@ -174,8 +171,8 @@ export class NotificationActionRunner extends ActionRunner {
this.telemetryService.publicLog('workbenchActionExecuted', { id: action.id, from: 'message' });
// Run and make sure to notify on any error again
super.runAction(action, context).done(null, error => this.notificationService.error(error));
super.runAction(action, context).then(null, error => this.notificationService.error(error));
return TPromise.as(void 0);
return Promise.resolve(void 0);
}
}
}

View File

@@ -3,14 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { alert } from 'vs/base/browser/ui/aria/aria';
import { localize } from 'vs/nls';
import { INotificationViewItem, INotificationsModel, NotificationChangeType, INotificationChangeEvent } from 'vs/workbench/common/notifications';
import { INotificationViewItem, INotificationsModel, NotificationChangeType, INotificationChangeEvent, NotificationViewItemLabelKind } from 'vs/workbench/common/notifications';
import { Disposable } from 'vs/base/common/lifecycle';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { Severity } from 'vs/platform/notification/common/notification';
import { once } from 'vs/base/common/event';
export class NotificationsAlerts extends Disposable {
@@ -18,7 +17,7 @@ export class NotificationsAlerts extends Disposable {
super();
// Alert initial notifications if any
model.notifications.forEach(n => this.ariaAlert(n));
model.notifications.forEach(n => this.triggerAriaAlert(n));
this.registerListeners();
}
@@ -31,7 +30,7 @@ export class NotificationsAlerts extends Disposable {
if (e.kind === NotificationChangeType.ADD) {
// ARIA alert for screen readers
this.ariaAlert(e.item);
this.triggerAriaAlert(e.item);
// Always log errors to console with full details
if (e.item.severity === Severity.Error) {
@@ -44,7 +43,21 @@ export class NotificationsAlerts extends Disposable {
}
}
private ariaAlert(notifiation: INotificationViewItem): void {
private triggerAriaAlert(notifiation: INotificationViewItem): void {
// Trigger the alert again whenever the label changes
const listener = notifiation.onDidLabelChange(e => {
if (e.kind === NotificationViewItemLabelKind.MESSAGE) {
this.doTriggerAriaAlert(notifiation);
}
});
once(notifiation.onDidClose)(() => listener.dispose());
this.doTriggerAriaAlert(notifiation);
}
private doTriggerAriaAlert(notifiation: INotificationViewItem): void {
let alertText: string;
if (notifiation.severity === Severity.Error) {
alertText = localize('alertErrorMessage', "Error: {0}", notifiation.message.value);

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/notificationsCenter';
import 'vs/css!./media/notificationsActions';
import { Themable, NOTIFICATIONS_BORDER, NOTIFICATIONS_CENTER_HEADER_FOREGROUND, NOTIFICATIONS_CENTER_HEADER_BACKGROUND, NOTIFICATIONS_CENTER_BORDER } from 'vs/workbench/common/theme';
@@ -106,9 +104,9 @@ export class NotificationsCenter extends Themable {
private updateTitle(): void {
if (this.model.notifications.length === 0) {
this.notificationsCenterTitle.innerText = localize('notificationsEmpty', "No new notifications");
this.notificationsCenterTitle.textContent = localize('notificationsEmpty', "No new notifications");
} else {
this.notificationsCenterTitle.innerText = localize('notifications', "Notifications");
this.notificationsCenterTitle.textContent = localize('notifications', "Notifications");
}
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
@@ -226,7 +224,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl
// Commands for Command Palette
const category = localize('notifications', "Notifications");
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: SHOW_NOTIFICATIONS_CENTER, title: localize('showNotifications', "Show Notifications"), category }, when: NotificationsCenterVisibleContext.toNegated() });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: HIDE_NOTIFICATIONS_CENTER, title: localize('hideNotifications', "Hide Notifications"), category }, when: NotificationsCenterVisibleContext });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: CLEAR_ALL_NOTIFICATIONS, title: localize('clearAllNotifications', "Clear All Notifications"), category } });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: SHOW_NOTIFICATIONS_CENTER, title: { value: localize('showNotifications', "Show Notifications"), original: 'Notifications: Show Notifications' }, category }, when: NotificationsCenterVisibleContext.toNegated() });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: HIDE_NOTIFICATIONS_CENTER, title: { value: localize('hideNotifications', "Hide Notifications"), original: 'Notifications: Hide Notifications' }, category }, when: NotificationsCenterVisibleContext });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: CLEAR_ALL_NOTIFICATIONS, title: { value: localize('clearAllNotifications', "Clear All Notifications"), original: 'Notifications: Clear All Notifications' }, category } });
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/notificationsList';
import { addClass, isAncestor, trackFocus } from 'vs/base/browser/dom';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
@@ -18,7 +16,6 @@ import { NotificationsListDelegate, NotificationRenderer } from 'vs/workbench/br
import { NotificationActionRunner, CopyNotificationMessageAction } from 'vs/workbench/browser/parts/notifications/notificationsActions';
import { NotificationFocusedContext } from 'vs/workbench/browser/parts/notifications/notificationsCommands';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { TPromise } from 'vs/base/common/winjs.base';
export class NotificationsList extends Themable {
private listContainer: HTMLElement;
@@ -78,15 +75,22 @@ export class NotificationsList extends Themable {
this.listContainer,
new NotificationsListDelegate(this.listContainer),
[renderer],
this.options
{
...this.options,
setRowLineHeight: false
}
));
// Context menu to copy message
const copyAction = this._register(this.instantiationService.createInstance(CopyNotificationMessageAction, CopyNotificationMessageAction.ID, CopyNotificationMessageAction.LABEL));
this._register((this.list.onContextMenu(e => {
if (!e.element) {
return;
}
this.contextMenuService.showContextMenu({
getAnchor: () => e.anchor,
getActions: () => TPromise.as([copyAction]),
getActions: () => [copyAction],
getActionsContext: () => e.element,
actionRunner
});

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, INotificationViewItem } from 'vs/workbench/common/notifications';
import { IStatusbarService, StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/notificationsToasts';
import { INotificationsModel, NotificationChangeType, INotificationChangeEvent, INotificationViewItem, NotificationViewItemLabelKind } from 'vs/workbench/common/notifications';
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
@@ -23,6 +21,7 @@ import { localize } from 'vs/nls';
import { Severity } from 'vs/platform/notification/common/notification';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IWindowService } from 'vs/platform/windows/common/windows';
interface INotificationToast {
item: INotificationViewItem;
@@ -45,15 +44,16 @@ export class NotificationsToasts extends Themable {
private static PURGE_TIMEOUT: { [severity: number]: number } = (() => {
const intervals = Object.create(null);
intervals[Severity.Info] = 10000;
intervals[Severity.Warning] = 12000;
intervals[Severity.Error] = 15000;
intervals[Severity.Info] = 15000;
intervals[Severity.Warning] = 18000;
intervals[Severity.Error] = 20000;
return intervals;
})();
private notificationsToastsContainer: HTMLElement;
private workbenchDimensions: Dimension;
private windowHasFocus: boolean;
private isNotificationsCenterVisible: boolean;
private mapNotificationToToast: Map<INotificationViewItem, INotificationToast>;
private notificationsToastsVisibleContextKey: IContextKey<boolean>;
@@ -66,21 +66,33 @@ export class NotificationsToasts extends Themable {
@IThemeService themeService: IThemeService,
@IEditorGroupsService private editorGroupService: IEditorGroupsService,
@IContextKeyService contextKeyService: IContextKeyService,
@ILifecycleService private lifecycleService: ILifecycleService
@ILifecycleService private lifecycleService: ILifecycleService,
@IWindowService private windowService: IWindowService
) {
super(themeService);
this.mapNotificationToToast = new Map<INotificationViewItem, INotificationToast>();
this.notificationsToastsVisibleContextKey = NotificationsToastsVisibleContext.bindTo(contextKeyService);
// Show toast for initial notifications if any
model.notifications.forEach(notification => this.addToast(notification));
this.windowService.isFocused().then(isFocused => this.windowHasFocus = isFocused);
this.registerListeners();
}
private registerListeners(): void {
this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e)));
// Wait for the running phase to ensure we can draw notifications properly
this.lifecycleService.when(LifecyclePhase.Ready).then(() => {
// Show toast for initial notifications if any
this.model.notifications.forEach(notification => this.addToast(notification));
// Update toasts on notification changes
this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e)));
});
// Track window focus
this.windowService.onDidChangeFocus(hasFocus => this.windowHasFocus = hasFocus);
}
private onDidNotificationChange(e: INotificationChangeEvent): void {
@@ -97,6 +109,10 @@ export class NotificationsToasts extends Themable {
return; // do not show toasts while notification center is visibles
}
if (item.silent) {
return; // do not show toats for silenced notifications
}
// Lazily create toasts containers
if (!this.notificationsToastsContainer) {
this.notificationsToastsContainer = document.createElement('div');
@@ -173,31 +189,8 @@ export class NotificationsToasts extends Themable {
this.removeToast(item);
});
// Automatically hide collapsed notifications
if (!item.expanded) {
// Track mouse over item
let isMouseOverToast = false;
itemDisposeables.push(addDisposableListener(notificationToastContainer, EventType.MOUSE_OVER, () => isMouseOverToast = true));
itemDisposeables.push(addDisposableListener(notificationToastContainer, EventType.MOUSE_OUT, () => isMouseOverToast = false));
// Install Timers
let timeoutHandle: number;
const hideAfterTimeout = () => {
timeoutHandle = setTimeout(() => {
const showsProgress = item.hasProgress() && !item.progress.state.done;
if (!notificationList.hasFocus() && !item.expanded && !isMouseOverToast && !showsProgress) {
this.removeToast(item);
} else {
hideAfterTimeout(); // push out disposal if item has focus or is expanded
}
}, NotificationsToasts.PURGE_TIMEOUT[item.severity]);
};
hideAfterTimeout();
itemDisposeables.push(toDisposable(() => clearTimeout(timeoutHandle)));
}
// Automatically purge non-sticky notifications
this.purgeNotification(item, notificationToastContainer, notificationList, itemDisposeables);
// Theming
this.updateStyles();
@@ -205,16 +198,61 @@ export class NotificationsToasts extends Themable {
// Context Key
this.notificationsToastsVisibleContextKey.set(true);
// Animate In if we are in a running session (otherwise just show directly)
if (this.lifecycleService.phase >= LifecyclePhase.Running) {
addClass(notificationToast, 'notification-fade-in');
itemDisposeables.push(addDisposableListener(notificationToast, 'transitionend', () => {
removeClass(notificationToast, 'notification-fade-in');
addClass(notificationToast, 'notification-fade-in-done');
}));
} else {
// Animate in
addClass(notificationToast, 'notification-fade-in');
itemDisposeables.push(addDisposableListener(notificationToast, 'transitionend', () => {
removeClass(notificationToast, 'notification-fade-in');
addClass(notificationToast, 'notification-fade-in-done');
}
}));
}
private purgeNotification(item: INotificationViewItem, notificationToastContainer: HTMLElement, notificationList: NotificationsList, disposables: IDisposable[]): void {
// Track mouse over item
let isMouseOverToast = false;
disposables.push(addDisposableListener(notificationToastContainer, EventType.MOUSE_OVER, () => isMouseOverToast = true));
disposables.push(addDisposableListener(notificationToastContainer, EventType.MOUSE_OUT, () => isMouseOverToast = false));
// Install Timers to Purge Notification
let purgeTimeoutHandle: any;
let listener: IDisposable;
const hideAfterTimeout = () => {
purgeTimeoutHandle = setTimeout(() => {
// If the notification is sticky or prompting and the window does not have
// focus, we wait for the window to gain focus again before triggering
// the timeout again. This prevents an issue where focussing the window
// could immediately hide the notification because the timeout was triggered
// again.
if ((item.sticky || item.hasPrompt()) && !this.windowHasFocus) {
if (!listener) {
listener = this.windowService.onDidChangeFocus(focus => {
if (focus) {
hideAfterTimeout();
}
});
disposables.push(listener);
}
}
// Otherwise...
else if (
item.sticky || // never hide sticky notifications
notificationList.hasFocus() || // never hide notifications with focus
isMouseOverToast // never hide notifications under mouse
) {
hideAfterTimeout();
} else {
this.removeToast(item);
}
}, NotificationsToasts.PURGE_TIMEOUT[item.severity]);
};
hideAfterTimeout();
disposables.push(toDisposable(() => clearTimeout(purgeTimeoutHandle)));
}
private removeToast(item: INotificationViewItem): void {
@@ -481,4 +519,4 @@ export class NotificationsToasts extends Themable {
private isVisible(toast: INotificationToast): boolean {
return !!toast.container.parentElement;
}
}
}

View File

@@ -3,13 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
import { clearNode, addClass, removeClass, toggleClass, addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import URI from 'vs/base/common/uri';
import { onUnexpectedError } from 'vs/base/common/errors';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { ButtonGroup } from 'vs/base/browser/ui/button/button';
import { attachButtonStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler';
@@ -20,13 +17,13 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { DropdownMenuActionItem } from 'vs/base/browser/ui/dropdown/dropdown';
import { INotificationViewItem, NotificationViewItem, NotificationViewItemLabelKind, INotificationMessage } from 'vs/workbench/common/notifications';
import { INotificationViewItem, NotificationViewItem, NotificationViewItemLabelKind, INotificationMessage, ChoiceAction } from 'vs/workbench/common/notifications';
import { ClearNotificationAction, ExpandNotificationAction, CollapseNotificationAction, ConfigureNotificationAction } from 'vs/workbench/browser/parts/notifications/notificationsActions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { Severity } from 'vs/platform/notification/common/notification';
export class NotificationsListDelegate implements IVirtualDelegate<INotificationViewItem> {
export class NotificationsListDelegate implements IListVirtualDelegate<INotificationViewItem> {
private static readonly ROW_HEIGHT = 42;
private static readonly LINE_HEIGHT = 22;
@@ -179,7 +176,7 @@ class NotificationMessageRenderer {
}
}
export class NotificationRenderer implements IRenderer<INotificationViewItem, INotificationTemplateData> {
export class NotificationRenderer implements IListRenderer<INotificationViewItem, INotificationTemplateData> {
static readonly TEMPLATE_ID = 'notification';
@@ -371,7 +368,7 @@ export class NotificationTemplateRenderer {
private renderMessage(notification: INotificationViewItem): boolean {
clearNode(this.template.message);
this.template.message.appendChild(NotificationMessageRenderer.render(notification.message, {
callback: link => this.openerService.open(URI.parse(link)).then(void 0, onUnexpectedError),
callback: link => this.openerService.open(URI.parse(link)),
disposeables: this.inputDisposeables
}));
@@ -426,10 +423,10 @@ export class NotificationTemplateRenderer {
private renderSource(notification): void {
if (notification.expanded && notification.source) {
this.template.source.innerText = localize('notificationSource', "Source: {0}", notification.source);
this.template.source.textContent = localize('notificationSource', "Source: {0}", notification.source);
this.template.source.title = notification.source;
} else {
this.template.source.innerText = '';
this.template.source.textContent = '';
this.template.source.removeAttribute('title');
}
}
@@ -449,8 +446,10 @@ export class NotificationTemplateRenderer {
// Run action
this.actionRunner.run(action, notification);
// Hide notification
notification.close();
// Hide notification (unless explicitly prevented)
if (!(action instanceof ChoiceAction) || !action.keepOpen) {
notification.close();
}
}));
this.inputDisposeables.push(attachButtonStyler(button, this.themeService));

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#2d2d30;}.icon-canvas-transparent{opacity:0;}.icon-vs-fg{fill:#2b282e;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>DockBottom_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M16,1V15H0V1Z" style="display: none;"/><path class="icon-vs-fg" d="M14,3V9H2V3Z" style="display: none;"/><path class="icon-vs-bg" d="M1,2V14H15V2ZM14,9H2V3H14Z"/></svg>

Before

Width:  |  Height:  |  Size: 511 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-fg{fill:#f0eff1;}.icon-vs-bg{fill:#424242;}</style></defs><title>DockBottom_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M16,1V15H0V1Z" style="display: none;"/><path class="icon-vs-fg" d="M14,3V9H2V3Z" style="display: none;"/><path class="icon-vs-bg" d="M1,2V14H15V2ZM14,9H2V3H14Z"/></svg>

Before

Width:  |  Height:  |  Size: 511 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#2d2d30;}.icon-canvas-transparent{opacity:0;}.icon-vs-fg{fill:#2b282e;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>DockRight_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M16,1V15H0V1Z" style="display: none;"/><path class="icon-vs-fg" d="M10,3V13H2V3Z" style="display: none;"/><path class="icon-vs-bg" d="M1,2V14H15V2Zm9,11H2V3h8Z"/></svg>

Before

Width:  |  Height:  |  Size: 510 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-fg{fill:#f0eff1;}.icon-vs-bg{fill:#424242;}</style></defs><title>DockRight_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M16,1V15H0V1Z" style="display: none;"/><path class="icon-vs-fg" d="M10,3V13H2V3Z" style="display: none;"/><path class="icon-vs-bg" d="M1,2V14H15V2Zm9,11H2V3h8Z"/></svg>

Before

Width:  |  Height:  |  Size: 510 B

View File

@@ -147,24 +147,6 @@
background-image: url('right-inverse.svg');
}
.monaco-workbench .move-panel-to-right {
background: url('panel-right.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .move-panel-to-right,
.hc-black .monaco-workbench .move-panel-to-right {
background: url('panel-right-inverse.svg') center center no-repeat;
}
.monaco-workbench .move-panel-to-bottom {
background: url('panel-bottom.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .move-panel-to-bottom,
.hc-black .monaco-workbench .move-panel-to-bottom {
background: url('panel-bottom-inverse.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .hide-panel-action,
.hc-black .monaco-workbench .hide-panel-action {
background: url('close-inverse.svg') center center no-repeat;

View File

@@ -5,7 +5,6 @@
import 'vs/css!./media/panelpart';
import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { Action } from 'vs/base/common/actions';
@@ -14,7 +13,7 @@ import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/
import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/actions';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IPartService, Parts, Position } from 'vs/workbench/services/part/common/partService';
import { ActivityAction } from 'vs/workbench/browser/parts/compositebar/compositeBarActions';
import { ActivityAction } from 'vs/workbench/browser/parts/compositeBarActions';
import { IActivity } from 'vs/workbench/common/activity';
export class ClosePanelAction extends Action {
@@ -30,8 +29,9 @@ export class ClosePanelAction extends Action {
super(id, name, 'hide-panel-action');
}
run(): TPromise<any> {
return this.partService.setPanelHidden(true);
run(): Thenable<any> {
this.partService.setPanelHidden(true);
return Promise.resolve(null);
}
}
@@ -48,8 +48,9 @@ export class TogglePanelAction extends Action {
super(id, name, partService.isVisible(Parts.PANEL_PART) ? 'panel expanded' : 'panel');
}
run(): TPromise<any> {
return this.partService.setPanelHidden(this.partService.isVisible(Parts.PANEL_PART));
run(): Thenable<any> {
this.partService.setPanelHidden(this.partService.isVisible(Parts.PANEL_PART));
return Promise.resolve(null);
}
}
@@ -67,11 +68,12 @@ class FocusPanelAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
// Show panel
if (!this.partService.isVisible(Parts.PANEL_PART)) {
return this.partService.setPanelHidden(false);
this.partService.setPanelHidden(false);
return Promise.resolve(null);
}
// Focus into active panel
@@ -79,7 +81,8 @@ class FocusPanelAction extends Action {
if (panel) {
panel.focus();
}
return TPromise.as(true);
return Promise.resolve(null);
}
}
@@ -88,8 +91,8 @@ export class TogglePanelPositionAction extends Action {
static readonly ID = 'workbench.action.togglePanelPosition';
static readonly LABEL = nls.localize('toggledPanelPosition', "Toggle Panel Position");
private static readonly MOVE_TO_RIGHT_LABEL = nls.localize('moveToRight', "Move to Right");
private static readonly MOVE_TO_BOTTOM_LABEL = nls.localize('moveToBottom', "Move to Bottom");
private static readonly MOVE_TO_RIGHT_LABEL = nls.localize('moveToRight', "Move Panel Right");
private static readonly MOVE_TO_BOTTOM_LABEL = nls.localize('moveToBottom', "Move Panel to Bottom");
private toDispose: IDisposable[];
@@ -97,26 +100,32 @@ export class TogglePanelPositionAction extends Action {
id: string,
label: string,
@IPartService private partService: IPartService,
) {
super(id, label, partService.getPanelPosition() === Position.RIGHT ? 'move-panel-to-bottom' : 'move-panel-to-right');
this.toDispose = [];
const setClassAndLabel = () => {
const positionRight = this.partService.getPanelPosition() === Position.RIGHT;
this.class = positionRight ? 'move-panel-to-bottom' : 'move-panel-to-right';
this.label = positionRight ? TogglePanelPositionAction.MOVE_TO_BOTTOM_LABEL : TogglePanelPositionAction.MOVE_TO_RIGHT_LABEL;
};
this.toDispose.push(partService.onEditorLayout(() => setClassAndLabel()));
setClassAndLabel();
}
run(): TPromise<any> {
run(): Thenable<any> {
const position = this.partService.getPanelPosition();
return this.partService.setPanelPosition(position === Position.BOTTOM ? Position.RIGHT : Position.BOTTOM);
this.partService.setPanelPosition(position === Position.BOTTOM ? Position.RIGHT : Position.BOTTOM);
return Promise.resolve(null);
}
dispose(): void {
super.dispose();
this.toDispose = dispose(this.toDispose);
}
}
@@ -137,7 +146,9 @@ export class ToggleMaximizedPanelAction extends Action {
@IPartService private partService: IPartService
) {
super(id, label, partService.isPanelMaximized() ? 'minimize-panel-action' : 'maximize-panel-action');
this.toDispose = [];
this.toDispose.push(partService.onEditorLayout(() => {
const maximized = this.partService.isPanelMaximized();
this.class = maximized ? 'minimize-panel-action' : 'maximize-panel-action';
@@ -145,13 +156,18 @@ export class ToggleMaximizedPanelAction extends Action {
}));
}
run(): TPromise<any> {
return (!this.partService.isVisible(Parts.PANEL_PART) ? this.partService.setPanelHidden(false) : TPromise.as(null))
.then(() => this.partService.toggleMaximizedPanel());
run(): Thenable<any> {
if (!this.partService.isVisible(Parts.PANEL_PART)) {
this.partService.setPanelHidden(false);
}
this.partService.toggleMaximizedPanel();
return Promise.resolve(null);
}
dispose(): void {
super.dispose();
this.toDispose = dispose(this.toDispose);
}
}
@@ -165,8 +181,74 @@ export class PanelActivityAction extends ActivityAction {
super(activity);
}
run(event: any): TPromise<any> {
return this.panelService.openPanel(this.activity.id, true).then(() => this.activate());
run(event: any): Thenable<any> {
this.panelService.openPanel(this.activity.id, true);
this.activate();
return Promise.resolve(null);
}
}
export class SwitchPanelViewAction extends Action {
constructor(
id: string,
name: string,
@IPanelService private panelService: IPanelService
) {
super(id, name);
}
run(offset: number): Thenable<any> {
const pinnedPanels = this.panelService.getPinnedPanels();
const activePanel = this.panelService.getActivePanel();
if (!activePanel) {
return Promise.resolve(null);
}
let targetPanelId: string;
for (let i = 0; i < pinnedPanels.length; i++) {
if (pinnedPanels[i].id === activePanel.getId()) {
targetPanelId = pinnedPanels[(i + pinnedPanels.length + offset) % pinnedPanels.length].id;
break;
}
}
this.panelService.openPanel(targetPanelId, true);
return Promise.resolve(null);
}
}
export class PreviousPanelViewAction extends SwitchPanelViewAction {
static readonly ID = 'workbench.action.previousPanelView';
static LABEL = nls.localize('previousPanelView', 'Previous Panel View');
constructor(
id: string,
name: string,
@IPanelService panelService: IPanelService
) {
super(id, name, panelService);
}
run(): Thenable<any> {
return super.run(-1);
}
}
export class NextPanelViewAction extends SwitchPanelViewAction {
static readonly ID = 'workbench.action.nextPanelView';
static LABEL = nls.localize('nextPanelView', 'Next Panel View');
constructor(
id: string,
name: string,
@IPanelService panelService: IPanelService
) {
super(id, name, panelService);
}
public run(): Thenable<any> {
return super.run(1);
}
}
@@ -177,6 +259,8 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMaximizedP
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ClosePanelAction, ClosePanelAction.ID, ClosePanelAction.LABEL), 'View: Close Panel', nls.localize('view', "View"));
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(TogglePanelPositionAction, TogglePanelPositionAction.ID, TogglePanelPositionAction.LABEL), 'View: Toggle Panel Position', nls.localize('view', "View"));
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMaximizedPanelAction, ToggleMaximizedPanelAction.ID, undefined), 'View: Toggle Panel Position', nls.localize('view', "View"));
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(PreviousPanelViewAction, PreviousPanelViewAction.ID, PreviousPanelViewAction.LABEL), 'View: Open Previous Panel View', nls.localize('view', "View"));
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(NextPanelViewAction, NextPanelViewAction.ID, NextPanelViewAction.LABEL), 'View: Open Next Panel View', nls.localize('view', "View"));
MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, {
group: '2_workbench_layout',
@@ -186,3 +270,12 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, {
},
order: 5
});
MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, {
group: '2_workbench_layout',
command: {
id: TogglePanelPositionAction.ID,
title: TogglePanelPositionAction.LABEL
},
order: 3
});

View File

@@ -4,10 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/panelpart';
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction } from 'vs/base/common/actions';
import { Event } from 'vs/base/common/event';
import { $ } from 'vs/base/browser/builder';
import { Event, mapEvent } from 'vs/base/common/event';
import { Registry } from 'vs/platform/registry/common/platform';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { IPanel } from 'vs/workbench/common/panel';
@@ -24,17 +22,17 @@ import { ClosePanelAction, TogglePanelPositionAction, PanelActivityAction, Toggl
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme';
import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry';
import { CompositeBar } from 'vs/workbench/browser/parts/compositebar/compositeBar';
import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositebar/compositeBarActions';
import { CompositeBar } from 'vs/workbench/browser/parts/compositeBar';
import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions';
import { IBadge } from 'vs/workbench/services/activity/common/activity';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { Dimension } from 'vs/base/browser/dom';
import { Dimension, trackFocus } from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { IDisposable } from 'vs/base/common/lifecycle';
import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
const ActivePanleContextId = 'activePanel';
export const ActivePanelContext = new RawContextKey<string>(ActivePanleContextId, '');
export const ActivePanelContext = new RawContextKey<string>('activePanel', '');
export const PanelFocusContext = new RawContextKey<boolean>('panelFocus', false);
export class PanelPart extends CompositePart<Panel> implements IPanelService {
@@ -46,6 +44,7 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
_serviceBrand: any;
private activePanelContextKey: IContextKey<string>;
private panelFocusContextKey: IContextKey<boolean>;
private blockOpeningPanel: boolean;
private compositeBar: CompositeBar;
private compositeActions: { [compositeId: string]: { activityAction: PanelActivityAction, pinnedAction: ToggleCompositePinnedAction } } = Object.create(null);
@@ -86,21 +85,28 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
icon: false,
storageId: PanelPart.PINNED_PANELS,
orientation: ActionsOrientation.HORIZONTAL,
openComposite: (compositeId: string) => this.openPanel(compositeId, true),
openComposite: (compositeId: string) => Promise.resolve(this.openPanel(compositeId, true)),
getActivityAction: (compositeId: string) => this.getCompositeActions(compositeId).activityAction,
getCompositePinnedAction: (compositeId: string) => this.getCompositeActions(compositeId).pinnedAction,
getOnCompositeClickAction: (compositeId: string) => this.instantiationService.createInstance(PanelActivityAction, this.getPanel(compositeId)),
getContextMenuActions: () => [this.instantiationService.createInstance(TogglePanelAction, TogglePanelAction.ID, localize('hidePanel', "Hide Panel"))],
getContextMenuActions: () => [
this.instantiationService.createInstance(TogglePanelPositionAction, TogglePanelPositionAction.ID, TogglePanelPositionAction.LABEL),
this.instantiationService.createInstance(TogglePanelAction, TogglePanelAction.ID, localize('hidePanel', "Hide Panel"))
],
getDefaultCompositeId: () => Registry.as<PanelRegistry>(PanelExtensions.Panels).getDefaultPanelId(),
hidePart: () => this.partService.setPanelHidden(true),
compositeSize: 0,
overflowActionSize: 44,
colors: {
backgroundColor: PANEL_BACKGROUND,
badgeBackground,
badgeForeground,
dragAndDropBackground: PANEL_DRAG_AND_DROP_BACKGROUND
}
colors: theme => ({
activeBackgroundColor: theme.getColor(PANEL_BACKGROUND), // Background color for overflow action
inactiveBackgroundColor: theme.getColor(PANEL_BACKGROUND), // Background color for overflow action
activeBorderBottomColor: theme.getColor(PANEL_ACTIVE_TITLE_BORDER),
activeForegroundColor: theme.getColor(PANEL_ACTIVE_TITLE_FOREGROUND),
inactiveForegroundColor: theme.getColor(PANEL_INACTIVE_TITLE_FOREGROUND),
badgeBackground: theme.getColor(badgeBackground),
badgeForeground: theme.getColor(badgeForeground),
dragAndDropBackground: theme.getColor(PANEL_DRAG_AND_DROP_BACKGROUND)
})
}));
for (const panel of this.getPanels()) {
@@ -108,18 +114,32 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
}
this.activePanelContextKey = ActivePanelContext.bindTo(contextKeyService);
this.panelFocusContextKey = PanelFocusContext.bindTo(contextKeyService);
this.registerListeners();
}
create(parent: HTMLElement): void {
super.create(parent);
const focusTracker = trackFocus(parent);
focusTracker.onDidFocus(() => {
this.panelFocusContextKey.set(true);
});
focusTracker.onDidBlur(() => {
this.panelFocusContextKey.set(false);
});
}
private registerListeners(): void {
this._register(this.onDidPanelOpen(this._onDidPanelOpen, this));
this._register(this.onDidPanelOpen(({ panel }) => this._onDidPanelOpen(panel)));
this._register(this.onDidPanelClose(this._onDidPanelClose, this));
this._register(this.registry.onDidRegister(panelDescriptor => this.compositeBar.addComposite(panelDescriptor)));
// Activate panel action on opening of a panel
this._register(this.onDidPanelOpen(panel => {
this._register(this.onDidPanelOpen(({ panel }) => {
this.compositeBar.activateComposite(panel.getId());
this.layoutCompositeBar(); // Need to relayout composite bar since different panels have different action bar width
}));
@@ -140,8 +160,8 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
}
}
get onDidPanelOpen(): Event<IPanel> {
return this._onDidCompositeOpen.event;
get onDidPanelOpen(): Event<{ panel: IPanel, focus: boolean }> {
return mapEvent(this._onDidCompositeOpen.event, compositeOpen => ({ panel: compositeOpen.composite, focus: compositeOpen.focus }));
}
get onDidPanelClose(): Event<IPanel> {
@@ -151,31 +171,30 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
updateStyles(): void {
super.updateStyles();
const container = $(this.getContainer());
container.style('background-color', this.getColor(PANEL_BACKGROUND));
container.style('border-left-color', this.getColor(PANEL_BORDER) || this.getColor(contrastBorder));
const container = this.getContainer();
container.style.backgroundColor = this.getColor(PANEL_BACKGROUND);
container.style.borderLeftColor = this.getColor(PANEL_BORDER) || this.getColor(contrastBorder);
const title = $(this.getTitleArea());
title.style('border-top-color', this.getColor(PANEL_BORDER) || this.getColor(contrastBorder));
const title = this.getTitleArea();
title.style.borderTopColor = this.getColor(PANEL_BORDER) || this.getColor(contrastBorder);
}
openPanel(id: string, focus?: boolean): TPromise<Panel> {
openPanel(id: string, focus?: boolean): Panel {
if (this.blockOpeningPanel) {
return TPromise.as(null); // Workaround against a potential race condition
return null; // Workaround against a potential race condition
}
// First check if panel is hidden and show if so
let promise = TPromise.wrap(null);
if (!this.partService.isVisible(Parts.PANEL_PART)) {
try {
this.blockOpeningPanel = true;
promise = this.partService.setPanelHidden(false);
this.partService.setPanelHidden(false);
} finally {
this.blockOpeningPanel = false;
}
}
return promise.then(() => this.openComposite(id, focus));
return this.openComposite(id, focus);
}
showActivity(panelId: string, badge: IBadge, clazz?: string): IDisposable {
@@ -192,6 +211,13 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
.sort((v1, v2) => v1.order - v2.order);
}
getPinnedPanels(): PanelDescriptor[] {
const pinnedCompositeIds = this.compositeBar.getPinnedComposites().map(c => c.id);
return this.getPanels()
.filter(p => pinnedCompositeIds.indexOf(p.id) !== -1)
.sort((p1, p2) => pinnedCompositeIds.indexOf(p1.id) - pinnedCompositeIds.indexOf(p2.id));
}
setPanelEnablement(id: string, enabled: boolean): void {
const descriptor = Registry.as<PanelRegistry>(PanelExtensions.Panels).getPanels().filter(p => p.id === id).pop();
if (descriptor && descriptor.enabled !== enabled) {
@@ -207,7 +233,6 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
protected getActions(): IAction[] {
return [
this.instantiationService.createInstance(ToggleMaximizedPanelAction, ToggleMaximizedPanelAction.ID, ToggleMaximizedPanelAction.LABEL),
this.instantiationService.createInstance(TogglePanelPositionAction, TogglePanelPositionAction.ID, TogglePanelPositionAction.LABEL),
this.instantiationService.createInstance(ClosePanelAction, ClosePanelAction.ID, ClosePanelAction.LABEL)
];
}
@@ -220,8 +245,8 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
return this.getLastActiveCompositetId();
}
hideActivePanel(): TPromise<void> {
return this.hideActiveComposite().then(composite => void 0);
hideActivePanel(): void {
this.hideActiveComposite();
}
protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel {
@@ -282,7 +307,7 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
}
private removeComposite(compositeId: string): void {
this.compositeBar.removeComposite(compositeId);
this.compositeBar.hideComposite(compositeId);
const compositeActions = this.compositeActions[compositeId];
if (compositeActions) {
compositeActions.activityAction.dispose();
@@ -321,20 +346,9 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const titleActiveBorder = theme.getColor(PANEL_ACTIVE_TITLE_BORDER);
if (titleActive || titleActiveBorder) {
collector.addRule(`
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:hover .action-label,
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item.checked .action-label {
color: ${titleActive};
border-bottom-color: ${titleActiveBorder};
}
`);
}
// Title Inactive
const titleInactive = theme.getColor(PANEL_INACTIVE_TITLE_FOREGROUND);
if (titleInactive) {
collector.addRule(`
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label {
color: ${titleInactive};
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:hover .action-label {
color: ${titleActive} !important;
border-bottom-color: ${titleActiveBorder} !important;
}
`);
}
@@ -344,7 +358,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
if (focusBorderColor) {
collector.addRule(`
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:focus .action-label {
color: ${titleActive};
color: ${titleActive} !important;
border-bottom-color: ${focusBorderColor} !important;
border-bottom: 1px solid;
}

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { QuickPickManyToggle, BackAction } from 'vs/workbench/browser/parts/quickinput/quickInput';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
@@ -15,4 +14,4 @@ import { inQuickOpenContext } from 'vs/workbench/browser/parts/quickopen/quickop
KeybindingsRegistry.registerCommandAndKeybindingRule(QuickPickManyToggle);
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(BackAction, BackAction.ID, BackAction.LABEL, { primary: null, win: { primary: KeyMod.Alt | KeyCode.LeftArrow }, mac: { primary: KeyMod.WinCtrl | KeyCode.US_MINUS }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_MINUS } }, inQuickOpenContext, KeybindingWeight.WorkbenchContrib + 50), 'Back');
registry.registerWorkbenchAction(new SyncActionDescriptor(BackAction, BackAction.ID, BackAction.LABEL, { primary: 0, win: { primary: KeyMod.Alt | KeyCode.LeftArrow }, mac: { primary: KeyMod.WinCtrl | KeyCode.US_MINUS }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_MINUS } }, inQuickOpenContext, KeybindingWeight.WorkbenchContrib + 50), 'Back');

View File

@@ -98,6 +98,10 @@
margin: 0px 11px;
}
.quick-input-progress.monaco-progress-container {
position: relative;
}
.quick-input-progress.monaco-progress-container,
.quick-input-progress.monaco-progress-container .progress-bit {
height: 2px;
@@ -113,12 +117,22 @@
}
.quick-input-list .quick-input-list-entry {
box-sizing: border-box;
overflow: hidden;
display: flex;
height: 100%;
padding: 0 6px;
}
.quick-input-list .quick-input-list-entry.quick-input-list-separator-border {
border-top-width: 1px;
border-top-style: solid;
}
.quick-input-list .monaco-list-row:first-child .quick-input-list-entry.quick-input-list-separator-border {
border-top-style: none;
}
.quick-input-list .quick-input-list-label {
overflow: hidden;
display: flex;
@@ -164,8 +178,50 @@
.quick-input-list .quick-input-list-label-meta {
opacity: 0.7;
line-height: normal;
text-overflow: ellipsis;
overflow: hidden;
}
.quick-input-list .monaco-highlighted-label .highlight {
font-weight: bold;
}
}
.quick-input-list .quick-input-list-separator {
margin-right: 18px;
}
.quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-separator,
.quick-input-list .monaco-list-row.focused .quick-input-list-entry.has-actions .quick-input-list-separator {
margin-right: 0;
}
.quick-input-list .quick-input-list-entry-action-bar {
display: none;
flex: 0;
overflow: visible;
}
.quick-input-list .quick-input-list-entry-action-bar .action-label.icon {
margin: 0;
width: 19px;
height: 100%;
background-position: center;
background-repeat: no-repeat;
}
.quick-input-list .quick-input-list-entry-action-bar {
margin-top: 1px;
}
.quick-input-list .quick-input-list-entry-action-bar ul:first-child .action-label.icon {
margin-left: 2px;
}
.quick-input-list .quick-input-list-entry-action-bar ul:last-child .action-label.icon {
margin-right: 8px;
}
.quick-input-list .quick-input-list-entry:hover .quick-input-list-entry-action-bar,
.quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar {
display: flex;
}

View File

@@ -3,11 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./quickInput';
import { Component } from 'vs/workbench/common/component';
import { IQuickInputService, IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox } from 'vs/platform/quickinput/common/quickInput';
import { IQuickInputService, IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods } from 'vs/platform/quickinput/common/quickInput';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import * as dom from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@@ -15,7 +13,6 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
import { contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND } from 'vs/workbench/common/theme';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { TPromise } from 'vs/base/common/winjs.base';
import { CancellationToken } from 'vs/base/common/cancellation';
import { QuickInputList } from './quickInputList';
import { QuickInputBox } from './quickInputBox';
@@ -38,14 +35,20 @@ import { ICommandAndKeybindingRule, KeybindingWeight } from 'vs/platform/keybind
import { inQuickOpenContext } from 'vs/workbench/browser/parts/quickopen/quickopen';
import { ActionBar, ActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action } from 'vs/base/common/actions';
import URI from 'vs/base/common/uri';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { URI } from 'vs/base/common/uri';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { equals } from 'vs/base/common/arrays';
import { TimeoutTimer } from 'vs/base/common/async';
import { getIconClass } from 'vs/workbench/browser/parts/quickinput/quickInputUtils';
import { AccessibilitySupport } from 'vs/base/common/platform';
import * as browser from 'vs/base/browser/browser';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IStorageService } from 'vs/platform/storage/common/storage';
const $ = dom.$;
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
const backButton = {
iconPath: {
dark: URI.parse(require.toUrl('vs/workbench/browser/parts/quickinput/media/dark/arrow-left.svg')),
@@ -70,9 +73,13 @@ interface QuickInputUI {
onDidAccept: Event<void>;
onDidTriggerButton: Event<IQuickInputButton>;
ignoreFocusOut: boolean;
keyMods: Writeable<IKeyMods>;
isScreenReaderOptimized(): boolean;
show(controller: QuickInput): void;
setVisibilities(visibilities: Visibilities): void;
setComboboxAccessibility(enabled: boolean): void;
setEnabled(enabled: boolean): void;
setContextKey(contextKey?: string): void;
hide(): void;
}
@@ -94,6 +101,7 @@ class QuickInput implements IQuickInput {
private _totalSteps: number;
protected visible = false;
private _enabled = true;
private _contextKey: string;
private _busy = false;
private _ignoreFocusOut = false;
private _buttons: IQuickInputButton[] = [];
@@ -148,6 +156,15 @@ class QuickInput implements IQuickInput {
this.update();
}
get contextKey() {
return this._contextKey;
}
set contextKey(contextKey: string) {
this._contextKey = contextKey;
this.update();
}
get busy() {
return this._busy;
}
@@ -182,7 +199,7 @@ class QuickInput implements IQuickInput {
if (this.visible) {
return;
}
this.disposables.push(
this.visibleDisposables.push(
this.ui.onDidTriggerButton(button => {
if (this.buttons.indexOf(button) !== -1) {
this.onDidTriggerButtonEmitter.fire(button);
@@ -235,20 +252,21 @@ class QuickInput implements IQuickInput {
this.ui.leftActionBar.clear();
const leftButtons = this.buttons.filter(button => button === backButton);
this.ui.leftActionBar.push(leftButtons.map((button, index) => {
const action = new Action(`id-${index}`, '', getIconClass(button.iconPath), true, () => this.onDidTriggerButtonEmitter.fire(button));
const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, () => this.onDidTriggerButtonEmitter.fire(button));
action.tooltip = button.tooltip;
return action;
}), { icon: true, label: false });
this.ui.rightActionBar.clear();
const rightButtons = this.buttons.filter(button => button !== backButton);
this.ui.rightActionBar.push(rightButtons.map((button, index) => {
const action = new Action(`id-${index}`, '', getIconClass(button.iconPath), true, () => this.onDidTriggerButtonEmitter.fire(button));
const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, () => this.onDidTriggerButtonEmitter.fire(button));
action.tooltip = button.tooltip;
return action;
}), { icon: true, label: false });
}
this.ui.ignoreFocusOut = this.ignoreFocusOut;
this.ui.setEnabled(this.enabled);
this.ui.setContextKey(this.contextKey);
}
private getTitle() {
@@ -282,11 +300,13 @@ class QuickInput implements IQuickInput {
class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPick<T> {
private static INPUT_BOX_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results.");
private _value = '';
private _placeholder;
private onDidChangeValueEmitter = new Emitter<string>();
private onDidAcceptEmitter = new Emitter<string>();
private _items: T[] = [];
private _items: (T | IQuickPickSeparator)[] = [];
private itemsUpdated = false;
private _canSelectMany = false;
private _matchOnDescription = false;
@@ -299,7 +319,9 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
private selectedItemsUpdated = false;
private selectedItemsToConfirm: T[] = [];
private onDidChangeSelectionEmitter = new Emitter<T[]>();
private quickNavigate = false;
private onDidTriggerItemButtonEmitter = new Emitter<IQuickPickItemButtonEvent<T>>();
quickNavigate: IQuickNavigateConfiguration;
constructor(ui: QuickInputUI) {
super(ui);
@@ -308,6 +330,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.onDidAcceptEmitter,
this.onDidChangeActiveEmitter,
this.onDidChangeSelectionEmitter,
this.onDidTriggerItemButtonEmitter,
);
}
@@ -337,7 +360,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
return this._items;
}
set items(items: T[]) {
set items(items: (T | IQuickPickSeparator)[]) {
this._items = items;
this.itemsUpdated = true;
this.update();
@@ -392,8 +415,14 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.update();
}
get keyMods() {
return this.ui.keyMods;
}
onDidChangeSelection = this.onDidChangeSelectionEmitter.event;
onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event;
show() {
if (!this.visible) {
this.visibleDisposables.push(
@@ -403,7 +432,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
}
this._value = value;
this.ui.list.filter(this.ui.inputBox.value);
if (!this.canSelectMany) {
if (!this.ui.isScreenReaderOptimized() && !this.canSelectMany) {
this.ui.list.focus('First');
}
this.onDidChangeValueEmitter.fire(value);
@@ -467,6 +496,9 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
}),
this.ui.list.onDidChangeSelection(selectedItems => {
if (this.canSelectMany) {
if (selectedItems.length) {
this.ui.list.setSelectedElements([]);
}
return;
}
if (this.selectedItemsToConfirm !== this._selectedItems && equals(selectedItems, this._selectedItems, (a, b) => a === b)) {
@@ -474,7 +506,9 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
}
this._selectedItems = selectedItems as T[];
this.onDidChangeSelectionEmitter.fire(selectedItems as T[]);
this.onDidAcceptEmitter.fire();
if (selectedItems.length) {
this.onDidAcceptEmitter.fire();
}
}),
this.ui.list.onChangedCheckedElements(checkedItems => {
if (!this.canSelectMany) {
@@ -486,77 +520,24 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this._selectedItems = checkedItems as T[];
this.onDidChangeSelectionEmitter.fire(checkedItems as T[]);
}),
this.ui.list.onButtonTriggered(event => this.onDidTriggerItemButtonEmitter.fire(event as IQuickPickItemButtonEvent<T>)),
this.registerQuickNavigation()
);
}
super.show();
super.show(); // TODO: Why have show() bubble up while update() trickles down? (Could move setComboboxAccessibility() here.)
}
protected update() {
super.update();
if (!this.visible) {
return;
}
if (this.ui.inputBox.value !== this.value) {
this.ui.inputBox.value = this.value;
}
if (this.ui.inputBox.placeholder !== (this.placeholder || '')) {
this.ui.inputBox.placeholder = (this.placeholder || '');
}
if (this.itemsUpdated) {
this.itemsUpdated = false;
this.ui.list.setElements(this.items);
this.ui.list.filter(this.ui.inputBox.value);
this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked();
this.ui.visibleCount.setCount(this.ui.list.getVisibleCount());
this.ui.count.setCount(this.ui.list.getCheckedCount());
if (!this.canSelectMany) {
this.ui.list.focus('First');
private registerQuickNavigation() {
return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, e => {
if (this.canSelectMany || !this.quickNavigate) {
return;
}
}
if (this.ui.container.classList.contains('show-checkboxes') !== this.canSelectMany) {
if (this.canSelectMany) {
this.ui.list.clearFocus();
} else {
this.ui.list.focus('First');
}
}
if (this.activeItemsUpdated) {
this.activeItemsUpdated = false;
this.activeItemsToConfirm = this._activeItems;
this.ui.list.setFocusedElements(this.activeItems);
if (this.activeItemsToConfirm === this._activeItems) {
this.activeItemsToConfirm = null;
}
}
if (this.selectedItemsUpdated) {
this.selectedItemsUpdated = false;
this.selectedItemsToConfirm = this._selectedItems;
if (this.canSelectMany) {
this.ui.list.setCheckedElements(this.selectedItems);
} else {
this.ui.list.setSelectedElements(this.selectedItems);
}
if (this.selectedItemsToConfirm === this._selectedItems) {
this.selectedItemsToConfirm = null;
}
}
this.ui.list.matchOnDescription = this.matchOnDescription;
this.ui.list.matchOnDetail = this.matchOnDetail;
this.ui.setVisibilities(this.canSelectMany ? { title: !!this.title || !!this.step, checkAll: true, inputBox: true, visibleCount: true, count: true, ok: true, list: true } : { title: !!this.title || !!this.step, inputBox: true, visibleCount: true, list: true });
}
configureQuickNavigate(quickNavigate: IQuickNavigateConfiguration) {
if (this.canSelectMany || this.quickNavigate) {
return;
}
this.quickNavigate = true;
this.disposables.push(dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, (e: KeyboardEvent) => {
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e as KeyboardEvent);
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
const keyCode = keyboardEvent.keyCode;
// Select element when keys are pressed that signal it
const quickNavKeys = quickNavigate.keybindings;
const quickNavKeys = this.quickNavigate.keybindings;
const wasTriggerKeyPressed = keyCode === KeyCode.Enter || quickNavKeys.some(k => {
const [firstPart, chordPart] = k.getParts();
if (chordPart) {
@@ -591,7 +572,63 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
this.onDidAcceptEmitter.fire();
}
}));
});
}
protected update() {
super.update();
if (!this.visible) {
return;
}
if (this.ui.inputBox.value !== this.value) {
this.ui.inputBox.value = this.value;
}
if (this.ui.inputBox.placeholder !== (this.placeholder || '')) {
this.ui.inputBox.placeholder = (this.placeholder || '');
}
if (this.itemsUpdated) {
this.itemsUpdated = false;
this.ui.list.setElements(this.items);
this.ui.list.filter(this.ui.inputBox.value);
this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked();
this.ui.visibleCount.setCount(this.ui.list.getVisibleCount());
this.ui.count.setCount(this.ui.list.getCheckedCount());
if (!this.ui.isScreenReaderOptimized() && !this.canSelectMany) {
this.ui.list.focus('First');
}
}
if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) {
if (this.canSelectMany) {
this.ui.list.clearFocus();
} else if (!this.ui.isScreenReaderOptimized()) {
this.ui.list.focus('First');
}
}
if (this.activeItemsUpdated) {
this.activeItemsUpdated = false;
this.activeItemsToConfirm = this._activeItems;
this.ui.list.setFocusedElements(this.activeItems);
if (this.activeItemsToConfirm === this._activeItems) {
this.activeItemsToConfirm = null;
}
}
if (this.selectedItemsUpdated) {
this.selectedItemsUpdated = false;
this.selectedItemsToConfirm = this._selectedItems;
if (this.canSelectMany) {
this.ui.list.setCheckedElements(this.selectedItems);
} else {
this.ui.list.setSelectedElements(this.selectedItems);
}
if (this.selectedItemsToConfirm === this._selectedItems) {
this.selectedItemsToConfirm = null;
}
}
this.ui.list.matchOnDescription = this.matchOnDescription;
this.ui.list.matchOnDetail = this.matchOnDetail;
this.ui.setComboboxAccessibility(true);
this.ui.inputBox.setAttribute('aria-label', QuickPick.INPUT_BOX_ARIA_LABEL);
this.ui.setVisibilities(this.canSelectMany ? { title: !!this.title || !!this.step, checkAll: true, inputBox: true, visibleCount: true, count: true, ok: true, list: true } : { title: !!this.title || !!this.step, inputBox: true, visibleCount: true, list: true });
}
}
@@ -688,6 +725,7 @@ class InputBox extends QuickInput implements IInputBox {
}),
this.ui.onDidAccept(() => this.onDidAcceptEmitter.fire()),
);
this.valueSelectionUpdated = true;
}
super.show();
}
@@ -729,6 +767,7 @@ export class QuickInputService extends Component implements IQuickInputService {
private static readonly ID = 'workbench.component.quickinput';
private static readonly MAX_WIDTH = 600; // Max total width of quick open widget
private idPrefix = 'quickInput_'; // Constant since there is still only one.
private layoutDimensions: dom.Dimension;
private titleBar: HTMLElement;
private filterContainer: HTMLElement;
@@ -737,11 +776,14 @@ export class QuickInputService extends Component implements IQuickInputService {
private okContainer: HTMLElement;
private ok: Button;
private ui: QuickInputUI;
private comboboxAccessibility = false;
private enabled = true;
private inQuickOpenWidgets: Record<string, boolean> = {};
private inQuickOpenContext: IContextKey<boolean>;
private contexts: { [id: string]: IContextKey<boolean>; } = Object.create(null);
private onDidAcceptEmitter = this._register(new Emitter<void>());
private onDidTriggerButtonEmitter = this._register(new Emitter<IQuickInputButton>());
private keyMods: Writeable<IKeyMods> = { ctrlCmd: false, alt: false };
private controller: QuickInput;
@@ -753,13 +795,15 @@ export class QuickInputService extends Component implements IQuickInputService {
@IQuickOpenService private quickOpenService: IQuickOpenService,
@IEditorGroupsService private editorGroupService: IEditorGroupsService,
@IKeybindingService private keybindingService: IKeybindingService,
@IContextKeyService contextKeyService: IContextKeyService,
@IThemeService themeService: IThemeService
@IContextKeyService private contextKeyService: IContextKeyService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService
) {
super(QuickInputService.ID, themeService);
super(QuickInputService.ID, themeService, storageService);
this.inQuickOpenContext = new RawContextKey<boolean>('inQuickOpen', false).bindTo(contextKeyService);
this._register(this.quickOpenService.onShow(() => this.inQuickOpen('quickOpen', true)));
this._register(this.quickOpenService.onHide(() => this.inQuickOpen('quickOpen', false)));
this.registerKeyModsListeners();
}
private inQuickOpen(widget: 'quickInput' | 'quickOpen', open: boolean) {
@@ -779,13 +823,71 @@ export class QuickInputService extends Component implements IQuickInputService {
}
}
private setContextKey(id?: string) {
let key: IContextKey<boolean>;
if (id) {
key = this.contexts[id];
if (!key) {
key = new RawContextKey<boolean>(id, false)
.bindTo(this.contextKeyService);
this.contexts[id] = key;
}
}
if (key && key.get()) {
return; // already active context
}
this.resetContextKeys();
if (key) {
key.set(true);
}
}
private resetContextKeys() {
for (const key in this.contexts) {
if (this.contexts[key].get()) {
this.contexts[key].reset();
}
}
}
private registerKeyModsListeners() {
const workbench = this.partService.getWorkbenchElement();
this._register(dom.addDisposableListener(workbench, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
const event = new StandardKeyboardEvent(e);
switch (event.keyCode) {
case KeyCode.Ctrl:
case KeyCode.Meta:
this.keyMods.ctrlCmd = true;
break;
case KeyCode.Alt:
this.keyMods.alt = true;
break;
}
}));
this._register(dom.addDisposableListener(workbench, dom.EventType.KEY_UP, (e: KeyboardEvent) => {
const event = new StandardKeyboardEvent(e);
switch (event.keyCode) {
case KeyCode.Ctrl:
case KeyCode.Meta:
this.keyMods.ctrlCmd = false;
break;
case KeyCode.Alt:
this.keyMods.alt = false;
break;
}
}));
}
private create() {
if (this.ui) {
return;
}
const workbench = document.getElementById(this.partService.getWorkbenchElementId());
const container = dom.append(workbench, $('.quick-input-widget'));
const workbench = this.partService.getWorkbenchElement();
const container = dom.append(workbench, $('.quick-input-widget.show-file-icons'));
container.tabIndex = -1;
container.style.display = 'none';
@@ -816,9 +918,11 @@ export class QuickInputService extends Component implements IQuickInputService {
this.filterContainer = dom.append(headerContainer, $('.quick-input-filter'));
const inputBox = this._register(new QuickInputBox(this.filterContainer));
inputBox.setAttribute('aria-describedby', `${this.idPrefix}message`);
this.visibleCountContainer = dom.append(this.filterContainer, $('.quick-input-visible-count'));
this.visibleCountContainer.setAttribute('aria-live', 'polite');
this.visibleCountContainer.setAttribute('aria-atomic', 'true');
const visibleCount = new CountBadge(this.visibleCountContainer, { countFormat: localize({ key: 'quickInput.visibleCount', comment: ['This tells the user how many items are shown in a list of items to select from. The items can be anything. Currently not visible, but read by screen readers.'] }, "{0} Results") });
this.countContainer = dom.append(this.filterContainer, $('.quick-input-count'));
@@ -834,13 +938,13 @@ export class QuickInputService extends Component implements IQuickInputService {
this.onDidAcceptEmitter.fire();
}));
const message = dom.append(container, $('.quick-input-message'));
const message = dom.append(container, $(`#${this.idPrefix}message.quick-input-message`));
const progressBar = new ProgressBar(container);
dom.addClass(progressBar.getContainer(), 'quick-input-progress');
this._register(attachProgressBarStyler(progressBar, this.themeService));
const list = this._register(this.instantiationService.createInstance(QuickInputList, container));
const list = this._register(this.instantiationService.createInstance(QuickInputList, container, this.idPrefix + 'list'));
this._register(list.onChangedAllVisibleChecked(checked => {
checkAll.checked = checked;
}));
@@ -859,6 +963,11 @@ export class QuickInputService extends Component implements IQuickInputService {
}
}, 0);
}));
this._register(list.onDidChangeFocus(() => {
if (this.comboboxAccessibility) {
this.ui.inputBox.setAttribute('aria-activedescendant', this.ui.list.getActiveDescendant());
}
}));
const focusTracker = dom.trackFocus(container);
this._register(focusTracker);
@@ -919,21 +1028,33 @@ export class QuickInputService extends Component implements IQuickInputService {
onDidAccept: this.onDidAcceptEmitter.event,
onDidTriggerButton: this.onDidTriggerButtonEmitter.event,
ignoreFocusOut: false,
keyMods: this.keyMods,
isScreenReaderOptimized: () => this.isScreenReaderOptimized(),
show: controller => this.show(controller),
hide: () => this.hide(),
setVisibilities: visibilities => this.setVisibilities(visibilities),
setComboboxAccessibility: enabled => this.setComboboxAccessibility(enabled),
setEnabled: enabled => this.setEnabled(enabled),
setContextKey: contextKey => this.setContextKey(contextKey),
};
this.updateStyles();
}
pick<T extends IQuickPickItem, O extends IPickOptions<T>>(picks: TPromise<T[]>, options: O = <O>{}, token: CancellationToken = CancellationToken.None): TPromise<O extends { canPickMany: true } ? T[] : T> {
return new TPromise<O extends { canPickMany: true } ? T[] : T>((resolve, reject) => {
pick<T extends IQuickPickItem, O extends IPickOptions<T>>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options: O = <O>{}, token: CancellationToken = CancellationToken.None): Promise<O extends { canPickMany: true } ? T[] : T> {
return new Promise<O extends { canPickMany: true } ? T[] : T>((doResolve, reject) => {
let resolve = (result: any) => {
resolve = doResolve;
if (options.onKeyMods) {
options.onKeyMods(input.keyMods);
}
doResolve(result);
};
if (token.isCancellationRequested) {
resolve(undefined);
return;
}
const input = this.createQuickPick<T>();
let activeItem: T;
const disposables = [
input,
input.onDidAccept(() => {
@@ -963,6 +1084,22 @@ export class QuickInputService extends Component implements IQuickInputService {
}
}
}),
input.onDidTriggerItemButton(event => options.onDidTriggerItemButton && options.onDidTriggerItemButton({
...event,
removeItem: () => {
const index = input.items.indexOf(event.item);
if (index !== -1) {
const items = input.items.slice();
items.splice(index, 1);
input.items = items;
}
}
})),
input.onDidChangeValue(value => {
if (activeItem && !value && (input.activeItems.length !== 1 || input.activeItems[0] !== activeItem)) {
input.activeItems = [activeItem];
}
}),
token.onCancellationRequested(() => {
input.hide();
}),
@@ -976,38 +1113,45 @@ export class QuickInputService extends Component implements IQuickInputService {
input.ignoreFocusOut = options.ignoreFocusLost;
input.matchOnDescription = options.matchOnDescription;
input.matchOnDetail = options.matchOnDetail;
input.quickNavigate = options.quickNavigate;
input.contextKey = options.contextKey;
input.busy = true;
picks.then(items => {
input.busy = false;
input.items = items;
if (input.canSelectMany) {
input.selectedItems = items.filter(item => item.picked);
}
});
Promise.all([picks, options.activeItem])
.then(([items, _activeItem]) => {
activeItem = _activeItem;
input.busy = false;
input.items = items;
if (input.canSelectMany) {
input.selectedItems = items.filter(item => item.type !== 'separator' && item.picked) as T[];
}
if (activeItem) {
input.activeItems = [activeItem];
}
});
input.show();
picks.then(null, err => {
Promise.resolve(picks).then(null, err => {
reject(err);
input.hide();
});
});
}
input(options: IInputOptions = {}, token: CancellationToken = CancellationToken.None): TPromise<string> {
return new TPromise<string>((resolve, reject) => {
input(options: IInputOptions = {}, token: CancellationToken = CancellationToken.None): Promise<string> {
return new Promise<string>((resolve, reject) => {
if (token.isCancellationRequested) {
resolve(undefined);
return;
}
const input = this.createInputBox();
const validateInput = options.validateInput || (() => TPromise.as(undefined));
const validateInput = options.validateInput || (() => <Thenable<undefined>>Promise.resolve(undefined));
const onDidValueChange = debounceEvent(input.onDidChangeValue, (last, cur) => cur, 100);
let validationValue = options.value || '';
let validation = TPromise.wrap(validateInput(validationValue));
let validation = Promise.resolve(validateInput(validationValue));
const disposables = [
input,
onDidValueChange(value => {
if (value !== validationValue) {
validation = TPromise.wrap(validateInput(value));
validation = Promise.resolve(validateInput(value));
validationValue = value;
}
validation.then(result => {
@@ -1019,7 +1163,7 @@ export class QuickInputService extends Component implements IQuickInputService {
input.onDidAccept(() => {
const value = input.value;
if (value !== validationValue) {
validation = TPromise.wrap(validateInput(value));
validation = Promise.resolve(validateInput(value));
validationValue = value;
}
validation.then(result => {
@@ -1087,11 +1231,14 @@ export class QuickInputService extends Component implements IQuickInputService {
this.ui.list.matchOnDescription = false;
this.ui.list.matchOnDetail = false;
this.ui.ignoreFocusOut = false;
this.setComboboxAccessibility(false);
this.ui.inputBox.removeAttribute('aria-label');
const keybinding = this.keybindingService.lookupKeybinding(BackAction.ID);
backButton.tooltip = keybinding ? localize('quickInput.backWithKeybinding', "Back ({0})", keybinding.getLabel()) : localize('quickInput.back', "Back");
this.inQuickOpen('quickInput', true);
this.resetContextKeys();
this.ui.container.style.display = '';
this.updateLayout();
@@ -1111,6 +1258,29 @@ export class QuickInputService extends Component implements IQuickInputService {
this.updateLayout(); // TODO
}
private setComboboxAccessibility(enabled: boolean) {
if (enabled !== this.comboboxAccessibility) {
this.comboboxAccessibility = enabled;
if (this.comboboxAccessibility) {
this.ui.inputBox.setAttribute('role', 'combobox');
this.ui.inputBox.setAttribute('aria-haspopup', 'true');
this.ui.inputBox.setAttribute('aria-autocomplete', 'list');
this.ui.inputBox.setAttribute('aria-activedescendant', this.ui.list.getActiveDescendant());
} else {
this.ui.inputBox.removeAttribute('role');
this.ui.inputBox.removeAttribute('aria-haspopup');
this.ui.inputBox.removeAttribute('aria-autocomplete');
this.ui.inputBox.removeAttribute('aria-activedescendant');
}
}
}
private isScreenReaderOptimized() {
const detected = browser.getAccessibilitySupport() === AccessibilitySupport.Enabled;
const config = this.configurationService.getValue<IEditorOptions>('editor').accessibilitySupport;
return config === 'on' || (config === 'auto' && detected);
}
private setEnabled(enabled: boolean) {
if (enabled !== this.enabled) {
this.enabled = enabled;
@@ -1132,6 +1302,7 @@ export class QuickInputService extends Component implements IQuickInputService {
if (controller) {
this.controller = null;
this.inQuickOpen('quickInput', false);
this.resetContextKeys();
this.ui.container.style.display = 'none';
if (!focusLost) {
this.editorGroupService.activeGroup.focus();
@@ -1156,24 +1327,24 @@ export class QuickInputService extends Component implements IQuickInputService {
if (this.isDisplayed() && this.ui.list.isDisplayed()) {
this.ui.list.focus(next ? 'Next' : 'Previous');
if (quickNavigate && this.controller instanceof QuickPick) {
this.controller.configureQuickNavigate(quickNavigate);
this.controller.quickNavigate = quickNavigate;
}
}
}
accept() {
this.onDidAcceptEmitter.fire();
return TPromise.as(undefined);
return Promise.resolve(undefined);
}
back() {
this.onDidTriggerButtonEmitter.fire(this.backButton);
return TPromise.as(undefined);
return Promise.resolve(undefined);
}
cancel() {
this.hide();
return TPromise.as(undefined);
return Promise.resolve(undefined);
}
layout(dimension: dom.Dimension): void {
@@ -1201,16 +1372,16 @@ export class QuickInputService extends Component implements IQuickInputService {
if (this.ui) {
// TODO
const titleColor = { dark: 'rgba(255, 255, 255, 0.105)', light: 'rgba(0,0,0,.06)', hc: 'black' }[theme.type];
this.titleBar.style.backgroundColor = titleColor ? titleColor.toString() : undefined;
this.titleBar.style.backgroundColor = titleColor ? titleColor.toString() : null;
this.ui.inputBox.style(theme);
const sideBarBackground = theme.getColor(SIDE_BAR_BACKGROUND);
this.ui.container.style.backgroundColor = sideBarBackground ? sideBarBackground.toString() : undefined;
this.ui.container.style.backgroundColor = sideBarBackground ? sideBarBackground.toString() : null;
const sideBarForeground = theme.getColor(SIDE_BAR_FOREGROUND);
this.ui.container.style.color = sideBarForeground ? sideBarForeground.toString() : undefined;
this.ui.container.style.color = sideBarForeground ? sideBarForeground.toString() : null;
const contrastBorderColor = theme.getColor(contrastBorder);
this.ui.container.style.border = contrastBorderColor ? `1px solid ${contrastBorderColor}` : undefined;
this.ui.container.style.border = contrastBorderColor ? `1px solid ${contrastBorderColor}` : null;
const widgetShadowColor = theme.getColor(widgetShadow);
this.ui.container.style.boxShadow = widgetShadowColor ? `0 5px 8px ${widgetShadowColor}` : undefined;
this.ui.container.style.boxShadow = widgetShadowColor ? `0 5px 8px ${widgetShadowColor}` : null;
}
}
@@ -1219,25 +1390,6 @@ export class QuickInputService extends Component implements IQuickInputService {
}
}
const iconPathToClass = {};
const iconClassGenerator = new IdGenerator('quick-input-button-icon-');
function getIconClass(iconPath: { dark: URI; light?: URI; }) {
let iconClass: string;
const key = iconPath.dark.toString();
if (iconPathToClass[key]) {
iconClass = iconPathToClass[key];
} else {
iconClass = iconClassGenerator.nextId();
dom.createCSSRule(`.${iconClass}`, `background-image: url("${(iconPath.light || iconPath.dark).toString()}")`);
dom.createCSSRule(`.vs-dark .${iconClass}, .hc-black .${iconClass}`, `background-image: url("${iconPath.dark.toString()}")`);
iconPathToClass[key] = iconClass;
}
return iconClass;
}
export const QuickPickManyToggle: ICommandAndKeybindingRule = {
id: 'workbench.action.quickPickManyToggle',
weight: KeybindingWeight.WorkbenchContrib,
@@ -1258,8 +1410,8 @@ export class BackAction extends Action {
super(id, label);
}
public run(): TPromise<any> {
public run(): Promise<any> {
this.quickInputService.back();
return TPromise.as(null);
return Promise.resolve(null);
}
}

View File

@@ -3,13 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./quickInput';
import * as dom from 'vs/base/browser/dom';
import { InputBox, IRange, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { localize } from 'vs/nls';
import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorBorder } from 'vs/platform/theme/common/colorRegistry';
import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder } from 'vs/platform/theme/common/colorRegistry';
import { ITheme } from 'vs/platform/theme/common/themeService';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
@@ -17,8 +14,6 @@ import Severity from 'vs/base/common/severity';
const $ = dom.$;
const DEFAULT_INPUT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results.");
export class QuickInputBox {
private container: HTMLElement;
@@ -29,16 +24,8 @@ export class QuickInputBox {
private parent: HTMLElement
) {
this.container = dom.append(this.parent, $('.quick-input-box'));
this.inputBox = new InputBox(this.container, null, {
ariaLabel: DEFAULT_INPUT_ARIA_LABEL
});
this.inputBox = new InputBox(this.container, null);
this.disposables.push(this.inputBox);
// ARIA
const inputElement = this.inputBox.inputElement;
inputElement.setAttribute('role', 'combobox');
inputElement.setAttribute('aria-haspopup', 'false');
inputElement.setAttribute('aria-autocomplete', 'list');
}
onKeyDown = (handler: (event: StandardKeyboardEvent) => void): IDisposable => {
@@ -59,7 +46,7 @@ export class QuickInputBox {
this.inputBox.value = value;
}
select(range: IRange = null): void {
select(range: IRange | null = null): void {
this.inputBox.select(range);
}
@@ -87,6 +74,14 @@ export class QuickInputBox {
this.inputBox.setEnabled(enabled);
}
setAttribute(name: string, value: string) {
this.inputBox.inputElement.setAttribute(name, value);
}
removeAttribute(name: string) {
this.inputBox.inputElement.removeAttribute(name);
}
showDecoration(decoration: Severity): void {
if (decoration === Severity.Ignore) {
this.inputBox.hideMessage();
@@ -109,10 +104,13 @@ export class QuickInputBox {
inputBackground: theme.getColor(inputBackground),
inputBorder: theme.getColor(inputBorder),
inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground),
inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground),
inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder),
inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground),
inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground),
inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder),
inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground),
inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground),
inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder),
});
}

View File

@@ -3,15 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./quickInput';
import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
import * as dom from 'vs/base/browser/dom';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { IMatch } from 'vs/base/common/filters';
import { matchesFuzzyOcticonAware, parseOcticons } from 'vs/base/common/octicon';
import { compareAnything } from 'vs/base/common/comparers';
@@ -24,21 +22,32 @@ import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlighte
import { memoize } from 'vs/base/common/decorators';
import { range } from 'vs/base/common/arrays';
import * as platform from 'vs/base/common/platform';
import { listFocusBackground } from 'vs/platform/theme/common/colorRegistry';
import { listFocusBackground, pickerGroupBorder, pickerGroupForeground } from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action } from 'vs/base/common/actions';
import { getIconClass } from 'vs/workbench/browser/parts/quickinput/quickInputUtils';
import { IListOptions } from 'vs/base/browser/ui/list/listWidget';
const $ = dom.$;
interface IListElement {
index: number;
item: IQuickPickItem;
saneLabel: string;
saneDescription?: string;
saneDetail?: string;
checked: boolean;
separator: IQuickPickSeparator;
fireButtonTriggered: (event: IQuickPickItemButtonEvent<IQuickPickItem>) => void;
}
class ListElement implements IListElement {
index: number;
item: IQuickPickItem;
shouldAlwaysShow = false;
saneLabel: string;
saneDescription?: string;
saneDetail?: string;
hidden = false;
private _onChecked = new Emitter<boolean>();
onChecked = this._onChecked.event;
@@ -52,9 +61,11 @@ class ListElement implements IListElement {
this._onChecked.fire(value);
}
}
separator: IQuickPickSeparator;
labelHighlights?: IMatch[];
descriptionHighlights?: IMatch[];
detailHighlights?: IMatch[];
fireButtonTriggered: (event: IQuickPickItemButtonEvent<IQuickPickItem>) => void;
constructor(init: IListElement) {
assign(this, init);
@@ -62,15 +73,18 @@ class ListElement implements IListElement {
}
interface IListElementTemplateData {
entry: HTMLDivElement;
checkbox: HTMLInputElement;
label: IconLabel;
detail: HighlightedLabel;
separator: HTMLDivElement;
actionBar: ActionBar;
element: ListElement;
toDisposeElement: IDisposable[];
toDisposeTemplate: IDisposable[];
}
class ListElementRenderer implements IRenderer<ListElement, IListElementTemplateData> {
class ListElementRenderer implements IListRenderer<ListElement, IListElementTemplateData> {
static readonly ID = 'listelement';
@@ -83,10 +97,10 @@ class ListElementRenderer implements IRenderer<ListElement, IListElementTemplate
data.toDisposeElement = [];
data.toDisposeTemplate = [];
const entry = dom.append(container, $('.quick-input-list-entry'));
data.entry = dom.append(container, $('.quick-input-list-entry'));
// Checkbox
const label = dom.append(entry, $('label.quick-input-list-label'));
const label = dom.append(data.entry, $('label.quick-input-list-label'));
data.checkbox = <HTMLInputElement>dom.append(label, $('input.quick-input-list-checkbox'));
data.checkbox.type = 'checkbox';
data.toDisposeTemplate.push(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => {
@@ -103,7 +117,15 @@ class ListElementRenderer implements IRenderer<ListElement, IListElementTemplate
// Detail
const detailContainer = dom.append(row2, $('.quick-input-list-label-meta'));
data.detail = new HighlightedLabel(detailContainer);
data.detail = new HighlightedLabel(detailContainer, true);
// Separator
data.separator = dom.append(data.entry, $('.quick-input-list-separator'));
// Actions
data.actionBar = new ActionBar(data.entry);
data.actionBar.domNode.classList.add('quick-input-list-entry-action-bar');
data.toDisposeTemplate.push(data.actionBar);
return data;
}
@@ -119,16 +141,56 @@ class ListElementRenderer implements IRenderer<ListElement, IListElementTemplate
// Label
const options: IIconLabelValueOptions = Object.create(null);
options.matches = labelHighlights || [];
options.descriptionTitle = element.item.description;
options.descriptionTitle = element.saneDescription;
options.descriptionMatches = descriptionHighlights || [];
data.label.setValue(element.item.label, element.item.description, options);
options.extraClasses = element.item.iconClasses;
data.label.setValue(element.saneLabel, element.saneDescription, options);
// Meta
data.detail.set(element.item.detail, detailHighlights);
data.detail.set(element.saneDetail, detailHighlights);
// ARIA label
data.entry.setAttribute('aria-label', [element.saneLabel, element.saneDescription, element.saneDetail]
.map(s => s && parseOcticons(s).text)
.filter(s => !!s)
.join(', '));
// Separator
if (element.separator && element.separator.label) {
data.separator.textContent = element.separator.label;
data.separator.style.display = null;
} else {
data.separator.style.display = 'none';
}
if (element.separator) {
dom.addClass(data.entry, 'quick-input-list-separator-border');
} else {
dom.removeClass(data.entry, 'quick-input-list-separator-border');
}
// Actions
data.actionBar.clear();
const buttons = element.item.buttons;
if (buttons && buttons.length) {
data.actionBar.push(buttons.map((button, index) => {
const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, () => {
element.fireButtonTriggered({
button,
item: element.item
});
return null;
});
action.tooltip = button.tooltip;
return action;
}), { icon: true, label: false });
dom.addClass(data.entry, 'has-actions');
} else {
dom.removeClass(data.entry, 'has-actions');
}
}
disposeElement(): void {
// noop
disposeElement(element: ListElement, index: number, data: IListElementTemplateData): void {
data.toDisposeElement = dispose(data.toDisposeElement);
}
disposeTemplate(data: IListElementTemplateData): void {
@@ -137,10 +199,10 @@ class ListElementRenderer implements IRenderer<ListElement, IListElementTemplate
}
}
class ListElementDelegate implements IVirtualDelegate<ListElement> {
class ListElementDelegate implements IListVirtualDelegate<ListElement> {
getHeight(element: ListElement): number {
return element.item.detail ? 44 : 22;
return element.saneDetail ? 44 : 22;
}
getTemplateId(element: ListElement): string {
@@ -150,8 +212,10 @@ class ListElementDelegate implements IVirtualDelegate<ListElement> {
export class QuickInputList {
readonly id: string;
private container: HTMLElement;
private list: WorkbenchList<ListElement>;
private inputElements: (IQuickPickItem | IQuickPickSeparator)[];
private elements: ListElement[] = [];
private elementsToIndexes = new Map<IQuickPickItem, number>();
matchOnDescription = false;
@@ -164,6 +228,8 @@ export class QuickInputList {
onChangedVisibleCount: Event<number> = this._onChangedVisibleCount.event;
private _onChangedCheckedElements = new Emitter<IQuickPickItem[]>();
onChangedCheckedElements: Event<IQuickPickItem[]> = this._onChangedCheckedElements.event;
private _onButtonTriggered = new Emitter<IQuickPickItemButtonEvent<IQuickPickItem>>();
onButtonTriggered = this._onButtonTriggered.event;
private _onLeave = new Emitter<void>();
onLeave: Event<void> = this._onLeave.event;
private _fireCheckedEvents = true;
@@ -172,14 +238,19 @@ export class QuickInputList {
constructor(
private parent: HTMLElement,
id: string,
@IInstantiationService private instantiationService: IInstantiationService
) {
this.id = id;
this.container = dom.append(this.parent, $('.quick-input-list'));
const delegate = new ListElementDelegate();
this.list = this.instantiationService.createInstance(WorkbenchList, this.container, delegate, [new ListElementRenderer()], {
identityProvider: element => element.label,
identityProvider: { getId: element => element.saneLabel },
openController: { shouldOpen: () => false }, // Workaround #58124
setRowLineHeight: false,
multipleSelectionSupport: false
}) as WorkbenchList<ListElement>;
} as IListOptions<ListElement>) as WorkbenchList<ListElement>;
this.list.getHTMLElement().id = id;
this.disposables.push(this.list);
this.disposables.push(this.list.onKeyDown(e => {
const event = new StandardKeyboardEvent(e);
@@ -208,16 +279,17 @@ export class QuickInputList {
break;
}
}));
this.disposables.push(this.list.onMouseDown(e => {
if (e.browserEvent.button !== 2) {
// Works around / fixes #64350.
e.browserEvent.preventDefault();
}
}));
this.disposables.push(dom.addDisposableListener(this.container, dom.EventType.CLICK, e => {
if (e.x || e.y) { // Avoid 'click' triggered by 'space' on checkbox.
this._onLeave.fire();
}
}));
this.disposables.push(this.list.onSelectionChange(e => {
if (e.elements.length) {
this.list.setSelection([]);
}
}));
}
@memoize
@@ -284,21 +356,34 @@ export class QuickInputList {
}
}
setElements(elements: IQuickPickItem[]): void {
setElements(inputElements: (IQuickPickItem | IQuickPickSeparator)[]): void {
this.elementDisposables = dispose(this.elementDisposables);
this.elements = elements.map((item, index) => new ListElement({
index,
item,
checked: false
}));
const fireButtonTriggered = (event: IQuickPickItemButtonEvent<IQuickPickItem>) => this.fireButtonTriggered(event);
this.inputElements = inputElements;
this.elements = inputElements.reduce((result, item, index) => {
if (item.type !== 'separator') {
const previous = index && inputElements[index - 1];
result.push(new ListElement({
index,
item,
saneLabel: item.label && item.label.replace(/\r?\n/g, ' '),
saneDescription: item.description && item.description.replace(/\r?\n/g, ' '),
saneDetail: item.detail && item.detail.replace(/\r?\n/g, ' '),
checked: false,
separator: previous && previous.type === 'separator' ? previous : undefined,
fireButtonTriggered
}));
}
return result;
}, [] as ListElement[]);
this.elementDisposables.push(...this.elements.map(element => element.onChecked(() => this.fireCheckedEvents())));
this.elementsToIndexes = this.elements.reduce((map, element, index) => {
map.set(element.item, index);
return map;
}, new Map<IQuickPickItem, number>());
this.list.splice(0, this.list.length); // Clear focus and selection first, sending the events when the list is empty.
this.list.splice(0, this.list.length, this.elements);
this.list.setFocus([]);
this._onChangedVisibleCount.fire(this.elements.length);
}
@@ -313,6 +398,10 @@ export class QuickInputList {
.map(item => this.elementsToIndexes.get(item)));
}
getActiveDescendant() {
return this.list.getHTMLElement().getAttribute('aria-activedescendant');
}
getSelectedElements() {
return this.list.getSelectedElements()
.map(e => e.item);
@@ -387,17 +476,19 @@ export class QuickInputList {
element.descriptionHighlights = undefined;
element.detailHighlights = undefined;
element.hidden = false;
const previous = element.index && this.inputElements[element.index - 1];
element.separator = previous && previous.type === 'separator' ? previous : undefined;
});
}
// Filter by value (since we support octicons, use octicon aware fuzzy matching)
else {
this.elements.forEach(element => {
const labelHighlights = matchesFuzzyOcticonAware(query, parseOcticons(element.item.label));
const descriptionHighlights = this.matchOnDescription ? matchesFuzzyOcticonAware(query, parseOcticons(element.item.description || '')) : undefined;
const detailHighlights = this.matchOnDetail ? matchesFuzzyOcticonAware(query, parseOcticons(element.item.detail || '')) : undefined;
const labelHighlights = matchesFuzzyOcticonAware(query, parseOcticons(element.saneLabel));
const descriptionHighlights = this.matchOnDescription ? matchesFuzzyOcticonAware(query, parseOcticons(element.saneDescription || '')) : undefined;
const detailHighlights = this.matchOnDetail ? matchesFuzzyOcticonAware(query, parseOcticons(element.saneDetail || '')) : undefined;
if (element.shouldAlwaysShow || labelHighlights || descriptionHighlights || detailHighlights) {
if (labelHighlights || descriptionHighlights || detailHighlights) {
element.labelHighlights = labelHighlights;
element.descriptionHighlights = descriptionHighlights;
element.detailHighlights = detailHighlights;
@@ -406,21 +497,21 @@ export class QuickInputList {
element.labelHighlights = undefined;
element.descriptionHighlights = undefined;
element.detailHighlights = undefined;
element.hidden = true;
element.hidden = !element.item.alwaysShow;
}
element.separator = undefined;
});
}
const shownElements = this.elements.filter(element => !element.hidden);
// Sort by value
const normalizedSearchValue = query.toLowerCase();
shownElements.sort((a, b) => {
if (!query) {
return a.index - b.index; // restore natural order
}
return compareEntries(a, b, normalizedSearchValue);
});
if (query) {
const normalizedSearchValue = query.toLowerCase();
shownElements.sort((a, b) => {
return compareEntries(a, b, normalizedSearchValue);
});
}
this.elementsToIndexes = shownElements.reduce((map, element, index) => {
map.set(element.item, index);
@@ -468,6 +559,10 @@ export class QuickInputList {
this._onChangedCheckedElements.fire(this.getCheckedElements());
}
}
private fireButtonTriggered(event: IQuickPickItemButtonEvent<IQuickPickItem>) {
this._onButtonTriggered.fire(event);
}
}
function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: string): number {
@@ -482,7 +577,7 @@ function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: s
return 1;
}
return compareAnything(elementA.item.label, elementB.item.label, lookFor);
return compareAnything(elementA.saneLabel, elementB.saneLabel, lookFor);
}
registerThemingParticipant((theme, collector) => {
@@ -492,4 +587,12 @@ registerThemingParticipant((theme, collector) => {
collector.addRule(`.quick-input-list .monaco-list .monaco-list-row.focused { background-color: ${listInactiveFocusBackground}; }`);
collector.addRule(`.quick-input-list .monaco-list .monaco-list-row.focused:hover { background-color: ${listInactiveFocusBackground}; }`);
}
const pickerGroupBorderColor = theme.getColor(pickerGroupBorder);
if (pickerGroupBorderColor) {
collector.addRule(`.quick-input-list .quick-input-list-entry { border-top-color: ${pickerGroupBorderColor}; }`);
}
const pickerGroupForegroundColor = theme.getColor(pickerGroupForeground);
if (pickerGroupForegroundColor) {
collector.addRule(`.quick-input-list .quick-input-list-separator { color: ${pickerGroupForegroundColor}; }`);
}
});

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./quickInput';
import * as dom from 'vs/base/browser/dom';
import { URI } from 'vs/base/common/uri';
import { IdGenerator } from 'vs/base/common/idGenerator';
const iconPathToClass = {};
const iconClassGenerator = new IdGenerator('quick-input-button-icon-');
export function getIconClass(iconPath: { dark: URI; light?: URI; }) {
let iconClass: string;
const key = iconPath.dark.toString();
if (iconPathToClass[key]) {
iconClass = iconPathToClass[key];
} else {
iconClass = iconClassGenerator.nextId();
dom.createCSSRule(`.${iconClass}`, `background-image: url("${(iconPath.light || iconPath.dark).toString()}")`);
dom.createCSSRule(`.vs-dark .${iconClass}, .hc-black .${iconClass}`, `background-image: url("${iconPath.dark.toString()}")`);
iconPathToClass[key] = iconClass;
}
return iconClass;
}

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Registry } from 'vs/platform/registry/common/platform';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
@@ -31,7 +30,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'workbench.action.acceptSelectedQuickOpenItem',
weight: KeybindingWeight.WorkbenchContrib,
when: inQuickOpenContext,
primary: null,
primary: 0,
handler: accessor => {
const quickOpenService = accessor.get(IQuickOpenService);
quickOpenService.accept();
@@ -44,7 +43,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'workbench.action.focusQuickOpen',
weight: KeybindingWeight.WorkbenchContrib,
when: inQuickOpenContext,
primary: null,
primary: 0,
handler: accessor => {
const quickOpenService = accessor.get(IQuickOpenService);
quickOpenService.focus();
@@ -67,11 +66,11 @@ KeybindingsRegistry.registerKeybindingRule({
});
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: { id: QUICKOPEN_ACTION_ID, title: QUICKOPEN_ACION_LABEL }
command: { id: QUICKOPEN_ACTION_ID, title: { value: QUICKOPEN_ACION_LABEL, original: 'Go to File...' } }
});
registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenSelectNextAction, QuickOpenSelectNextAction.ID, QuickOpenSelectNextAction.LABEL, { primary: null, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_N } }, inQuickOpenContext, KeybindingWeight.WorkbenchContrib + 50), 'Select Next in Quick Open');
registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenSelectPreviousAction, QuickOpenSelectPreviousAction.ID, QuickOpenSelectPreviousAction.LABEL, { primary: null, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_P } }, inQuickOpenContext, KeybindingWeight.WorkbenchContrib + 50), 'Select Previous in Quick Open');
registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenSelectNextAction, QuickOpenSelectNextAction.ID, QuickOpenSelectNextAction.LABEL, { primary: 0, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_N } }, inQuickOpenContext, KeybindingWeight.WorkbenchContrib + 50), 'Select Next in Quick Open');
registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenSelectPreviousAction, QuickOpenSelectPreviousAction.ID, QuickOpenSelectPreviousAction.LABEL, { primary: 0, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_P } }, inQuickOpenContext, KeybindingWeight.WorkbenchContrib + 50), 'Select Previous in Quick Open');
registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenNavigateNextAction, QuickOpenNavigateNextAction.ID, QuickOpenNavigateNextAction.LABEL), 'Navigate Next in Quick Open');
registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenNavigatePreviousAction, QuickOpenNavigatePreviousAction.ID, QuickOpenNavigatePreviousAction.LABEL), 'Navigate Previous in Quick Open');
registry.registerWorkbenchAction(new SyncActionDescriptor(RemoveFromEditorHistoryAction, RemoveFromEditorHistoryAction.ID, RemoveFromEditorHistoryAction.LABEL), 'Remove From History');

View File

@@ -3,9 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import * as nls from 'vs/nls';
import { Action } from 'vs/base/common/actions';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
@@ -22,19 +19,19 @@ export const defaultQuickOpenContext = ContextKeyExpr.and(inQuickOpenContext, Co
export const QUICKOPEN_ACTION_ID = 'workbench.action.quickOpen';
export const QUICKOPEN_ACION_LABEL = nls.localize('quickOpen', "Go to File...");
CommandsRegistry.registerCommand(QUICKOPEN_ACTION_ID, function (accessor: ServicesAccessor, prefix: string = null) {
CommandsRegistry.registerCommand(QUICKOPEN_ACTION_ID, function (accessor: ServicesAccessor, prefix: string | null = null) {
const quickOpenService = accessor.get(IQuickOpenService);
return quickOpenService.show(typeof prefix === 'string' ? prefix : null).then(() => {
return quickOpenService.show(typeof prefix === 'string' ? prefix : undefined).then(() => {
return void 0;
});
});
export const QUICKOPEN_FOCUS_SECONDARY_ACTION_ID = 'workbench.action.quickOpenPreviousEditor';
CommandsRegistry.registerCommand(QUICKOPEN_FOCUS_SECONDARY_ACTION_ID, function (accessor: ServicesAccessor, prefix: string = null) {
CommandsRegistry.registerCommand(QUICKOPEN_FOCUS_SECONDARY_ACTION_ID, function (accessor: ServicesAccessor, prefix: string | null = null) {
const quickOpenService = accessor.get(IQuickOpenService);
return quickOpenService.show(null, { autoFocus: { autoFocusSecondEntry: true } }).then(() => {
return quickOpenService.show(undefined, { autoFocus: { autoFocusSecondEntry: true } }).then(() => {
return void 0;
});
});
@@ -53,14 +50,14 @@ export class BaseQuickOpenNavigateAction extends Action {
super(id, label);
}
run(event?: any): TPromise<any> {
run(event?: any): Thenable<any> {
const keys = this.keybindingService.lookupKeybindings(this.id);
const quickNavigate = this.quickNavigate ? { keybindings: keys } : void 0;
this.quickOpenService.navigate(this.next, quickNavigate);
this.quickInputService.navigate(this.next, quickNavigate);
return TPromise.as(true);
return Promise.resolve(true);
}
}
@@ -73,8 +70,8 @@ export function getQuickNavigateHandler(id: string, next?: boolean): ICommandHan
const keys = keybindingService.lookupKeybindings(id);
const quickNavigate = { keybindings: keys };
quickOpenService.navigate(next, quickNavigate);
quickInputService.navigate(next, quickNavigate);
quickOpenService.navigate(!!next, quickNavigate);
quickInputService.navigate(!!next, quickNavigate);
};
}
@@ -140,4 +137,4 @@ export class QuickOpenSelectPreviousAction extends BaseQuickOpenNavigateAction {
) {
super(id, label, false, false, quickOpenService, quickInputService, keybindingService);
}
}
}

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/sidebarpart';
import { TPromise } from 'vs/base/common/winjs.base';
import * as nls from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { Action } from 'vs/base/common/actions';
@@ -21,19 +20,24 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { Event, mapEvent } from 'vs/base/common/event';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { SIDE_BAR_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND, SIDE_BAR_BORDER } from 'vs/workbench/common/theme';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { Dimension, EventType } from 'vs/base/browser/dom';
import { $ } from 'vs/base/browser/builder';
import { Dimension, EventType, addDisposableListener, trackFocus } from 'vs/base/browser/dom';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
const SideBarFocusContextId = 'sideBarFocus';
export const SidebarFocusContext = new RawContextKey<boolean>(SideBarFocusContextId, false);
export class SidebarPart extends CompositePart<Viewlet> {
static readonly activeViewletSettingsKey = 'workbench.sidebar.activeviewletid';
private sideBarFocusContextKey: IContextKey<boolean>;
private blockOpeningViewlet: boolean;
constructor(
@@ -45,7 +49,8 @@ export class SidebarPart extends CompositePart<Viewlet> {
@IPartService partService: IPartService,
@IKeybindingService keybindingService: IKeybindingService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService
@IThemeService themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService,
) {
super(
notificationService,
@@ -65,19 +70,37 @@ export class SidebarPart extends CompositePart<Viewlet> {
id,
{ hasTitle: true, borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0 }
);
this.sideBarFocusContextKey = SidebarFocusContext.bindTo(contextKeyService);
}
get onDidViewletOpen(): Event<IViewlet> {
return this._onDidCompositeOpen.event as Event<IViewlet>;
return mapEvent(this._onDidCompositeOpen.event, compositeEvent => <IViewlet>compositeEvent.composite);
}
get onDidViewletClose(): Event<IViewlet> {
return this._onDidCompositeClose.event as Event<IViewlet>;
}
create(parent: HTMLElement): void {
super.create(parent);
const focusTracker = trackFocus(parent);
focusTracker.onDidFocus(() => {
this.sideBarFocusContextKey.set(true);
});
focusTracker.onDidBlur(() => {
this.sideBarFocusContextKey.set(false);
});
}
createTitleArea(parent: HTMLElement): HTMLElement {
const titleArea = super.createTitleArea(parent);
$(titleArea).on(EventType.CONTEXT_MENU, (e: MouseEvent) => this.onTitleAreaContextMenu(new StandardMouseEvent(e)), this.toDispose);
this._register(addDisposableListener(titleArea, EventType.CONTEXT_MENU, e => {
this.onTitleAreaContextMenu(new StandardMouseEvent(e));
}));
return titleArea;
}
@@ -86,38 +109,37 @@ export class SidebarPart extends CompositePart<Viewlet> {
super.updateStyles();
// Part container
const container = $(this.getContainer());
const container = this.getContainer();
container.style('background-color', this.getColor(SIDE_BAR_BACKGROUND));
container.style('color', this.getColor(SIDE_BAR_FOREGROUND));
container.style.backgroundColor = this.getColor(SIDE_BAR_BACKGROUND);
container.style.color = this.getColor(SIDE_BAR_FOREGROUND);
const borderColor = this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder);
const isPositionLeft = this.partService.getSideBarPosition() === SideBarPosition.LEFT;
container.style('border-right-width', borderColor && isPositionLeft ? '1px' : null);
container.style('border-right-style', borderColor && isPositionLeft ? 'solid' : null);
container.style('border-right-color', isPositionLeft ? borderColor : null);
container.style('border-left-width', borderColor && !isPositionLeft ? '1px' : null);
container.style('border-left-style', borderColor && !isPositionLeft ? 'solid' : null);
container.style('border-left-color', !isPositionLeft ? borderColor : null);
container.style.borderRightWidth = borderColor && isPositionLeft ? '1px' : null;
container.style.borderRightStyle = borderColor && isPositionLeft ? 'solid' : null;
container.style.borderRightColor = isPositionLeft ? borderColor : null;
container.style.borderLeftWidth = borderColor && !isPositionLeft ? '1px' : null;
container.style.borderLeftStyle = borderColor && !isPositionLeft ? 'solid' : null;
container.style.borderLeftColor = !isPositionLeft ? borderColor : null;
}
openViewlet(id: string, focus?: boolean): TPromise<Viewlet> {
openViewlet(id: string, focus?: boolean): Viewlet {
if (this.blockOpeningViewlet) {
return TPromise.as(null); // Workaround against a potential race condition
return null; // Workaround against a potential race condition
}
// First check if sidebar is hidden and show if so
let promise = TPromise.wrap<void>(null);
if (!this.partService.isVisible(Parts.SIDEBAR_PART)) {
try {
this.blockOpeningViewlet = true;
promise = this.partService.setSideBarHidden(false);
this.partService.setSideBarHidden(false);
} finally {
this.blockOpeningViewlet = false;
}
}
return promise.then(() => this.openComposite(id, focus)) as TPromise<Viewlet>;
return this.openComposite(id, focus) as Viewlet;
}
getActiveViewlet(): IViewlet {
@@ -128,8 +150,8 @@ export class SidebarPart extends CompositePart<Viewlet> {
return this.getLastActiveCompositetId();
}
hideActiveViewlet(): TPromise<void> {
return this.hideActiveComposite().then(composite => void 0);
hideActiveViewlet(): void {
this.hideActiveComposite();
}
layout(dimension: Dimension): Dimension[] {
@@ -140,6 +162,10 @@ export class SidebarPart extends CompositePart<Viewlet> {
return super.layout(dimension);
}
protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment {
return this.partService.getSideBarPosition() === SideBarPosition.LEFT ? AnchorAlignment.LEFT : AnchorAlignment.RIGHT;
}
private onTitleAreaContextMenu(event: StandardMouseEvent): void {
const activeViewlet = this.getActiveViewlet() as Viewlet;
if (activeViewlet) {
@@ -148,10 +174,9 @@ export class SidebarPart extends CompositePart<Viewlet> {
const anchor: { x: number, y: number } = { x: event.posx, y: event.posy };
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => TPromise.as(contextMenuActions),
getActions: () => contextMenuActions,
getActionItem: action => this.actionItemProvider(action as Action),
actionRunner: activeViewlet.getActionRunner(),
getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id)
actionRunner: activeViewlet.getActionRunner()
});
}
}
@@ -172,11 +197,11 @@ class FocusSideBarAction extends Action {
super(id, label);
}
run(): TPromise<any> {
run(): Thenable<any> {
// Show side bar
if (!this.partService.isVisible(Parts.SIDEBAR_PART)) {
return this.partService.setSideBarHidden(false);
return Promise.resolve(this.partService.setSideBarHidden(false));
}
// Focus into active viewlet
@@ -184,7 +209,8 @@ class FocusSideBarAction extends Action {
if (viewlet) {
viewlet.focus();
}
return TPromise.as(true);
return Promise.resolve(true);
}
}

View File

@@ -9,7 +9,6 @@
width: 100%;
height: 22px;
font-size: 12px;
padding: 0 10px;
}
.monaco-workbench > .part.statusbar > .statusbar-item {
@@ -46,6 +45,15 @@
margin-left: 5px;
}
/* adding padding to the most left status bar item */
.monaco-workbench > .part.statusbar > .statusbar-item.left:first-child, .monaco-workbench > .part.statusbar > .statusbar-item.right + .statusbar-item.left {
padding-left: 10px;
}
/* adding padding to the most right status bar item */
.monaco-workbench > .part.statusbar > .statusbar-item.right:first-child {
padding-right: 10px;
}
.monaco-workbench > .part.statusbar > .statusbar-item a {
cursor: pointer;
display: inline-block;

View File

@@ -2,11 +2,10 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Registry } from 'vs/platform/registry/common/platform';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as statusbarService from 'vs/platform/statusbar/common/statusbar';
import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar';
import { SyncDescriptor0, createSyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { IConstructorSignature0 } from 'vs/platform/instantiation/common/instantiation';
@@ -14,8 +13,6 @@ export interface IStatusbarItem {
render(element: HTMLElement): IDisposable;
}
export import StatusbarAlignment = statusbarService.StatusbarAlignment;
export class StatusbarItemDescriptor {
syncDescriptor: SyncDescriptor0<IStatusbarItem>;
alignment: StatusbarAlignment;

View File

@@ -3,23 +3,19 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/statusbarpart';
import * as nls from 'vs/nls';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { TPromise } from 'vs/base/common/winjs.base';
import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { $ } from 'vs/base/browser/builder';
import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel';
import { Registry } from 'vs/platform/registry/common/platform';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { Part } from 'vs/workbench/browser/part';
import { StatusbarAlignment, IStatusbarRegistry, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { IStatusbarRegistry, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IStatusbarService, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar';
import { StatusbarAlignment, IStatusbarService, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { Action } from 'vs/base/common/actions';
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
@@ -28,15 +24,16 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { isThemeColor } from 'vs/editor/common/editorCommon';
import { Color } from 'vs/base/common/color';
import { addClass, EventHelper, createStyleSheet } from 'vs/base/browser/dom';
import { addClass, EventHelper, createStyleSheet, addDisposableListener } from 'vs/base/browser/dom';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageService } from 'vs/platform/storage/common/storage';
export class StatusbarPart extends Part implements IStatusbarService {
_serviceBrand: any;
private static readonly PRIORITY_PROP = 'priority';
private static readonly ALIGNMENT_PROP = 'alignment';
private static readonly PRIORITY_PROP = 'statusbar-entry-priority';
private static readonly ALIGNMENT_PROP = 'statusbar-entry-alignment';
private statusItemsContainer: HTMLElement;
private statusMsgDispose: IDisposable;
@@ -47,9 +44,10 @@ export class StatusbarPart extends Part implements IStatusbarService {
id: string,
@IInstantiationService private instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IWorkspaceContextService private contextService: IWorkspaceContextService
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IStorageService storageService: IStorageService
) {
super(id, { hasTitle: false }, themeService);
super(id, { hasTitle: false }, themeService, storageService);
this.registerListeners();
}
@@ -71,7 +69,7 @@ export class StatusbarPart extends Part implements IStatusbarService {
let inserted = false;
for (let i = 0; i < neighbours.length; i++) {
const neighbour = neighbours[i];
const nPriority = $(neighbour).getProperty(StatusbarPart.PRIORITY_PROP);
const nPriority = Number(neighbour.getAttribute(StatusbarPart.PRIORITY_PROP));
if (
alignment === StatusbarAlignment.LEFT && nPriority < priority ||
alignment === StatusbarAlignment.RIGHT && nPriority > priority
@@ -87,7 +85,7 @@ export class StatusbarPart extends Part implements IStatusbarService {
}
return toDisposable(() => {
$(el).destroy();
el.remove();
if (toDispose) {
toDispose.dispose();
@@ -102,7 +100,7 @@ export class StatusbarPart extends Part implements IStatusbarService {
const children = container.children;
for (let i = 0; i < children.length; i++) {
const childElement = <HTMLElement>children.item(i);
if ($(childElement).getProperty(StatusbarPart.ALIGNMENT_PROP) === alignment) {
if (Number(childElement.getAttribute(StatusbarPart.ALIGNMENT_PROP)) === alignment) {
entries.push(childElement);
}
}
@@ -116,17 +114,29 @@ export class StatusbarPart extends Part implements IStatusbarService {
// Fill in initial items that were contributed from the registry
const registry = Registry.as<IStatusbarRegistry>(Extensions.Statusbar);
const leftDescriptors = registry.items.filter(d => d.alignment === StatusbarAlignment.LEFT).sort((a, b) => b.priority - a.priority);
const rightDescriptors = registry.items.filter(d => d.alignment === StatusbarAlignment.RIGHT).sort((a, b) => a.priority - b.priority);
const descriptors = registry.items.slice().sort((a, b) => {
if (a.alignment === b.alignment) {
if (a.alignment === StatusbarAlignment.LEFT) {
return b.priority - a.priority;
} else {
return a.priority - b.priority;
}
} else if (a.alignment === StatusbarAlignment.LEFT) {
return 1;
} else if (a.alignment === StatusbarAlignment.RIGHT) {
return -1;
} else {
return 0;
}
});
const descriptors = rightDescriptors.concat(leftDescriptors); // right first because they float
descriptors.forEach(descriptor => {
for (const descriptor of descriptors) {
const item = this.instantiationService.createInstance(descriptor.syncDescriptor);
const el = this.doCreateStatusItem(descriptor.alignment, descriptor.priority);
this._register(item.render(el));
this.statusItemsContainer.appendChild(el);
});
}
return this.statusItemsContainer;
}
@@ -134,22 +144,22 @@ export class StatusbarPart extends Part implements IStatusbarService {
protected updateStyles(): void {
super.updateStyles();
const container = $(this.getContainer());
const container = this.getContainer();
// Background colors
const backgroundColor = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BACKGROUND : STATUS_BAR_NO_FOLDER_BACKGROUND);
container.style('background-color', backgroundColor);
container.style('color', this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND));
container.style.backgroundColor = backgroundColor;
container.style.color = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND);
// Border color
const borderColor = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BORDER : STATUS_BAR_NO_FOLDER_BORDER) || this.getColor(contrastBorder);
container.style('border-top-width', borderColor ? '1px' : null);
container.style('border-top-style', borderColor ? 'solid' : null);
container.style('border-top-color', borderColor);
container.style.borderTopWidth = borderColor ? '1px' : null;
container.style.borderTopStyle = borderColor ? 'solid' : null;
container.style.borderTopColor = borderColor;
// Notification Beak
if (!this.styleElement) {
this.styleElement = createStyleSheet(container.getHTMLElement());
this.styleElement = createStyleSheet(container);
}
this.styleElement.innerHTML = `.monaco-workbench > .part.statusbar > .statusbar-item.has-beak:before { border-bottom-color: ${backgroundColor}; }`;
@@ -168,8 +178,8 @@ export class StatusbarPart extends Part implements IStatusbarService {
addClass(el, 'left');
}
$(el).setProperty(StatusbarPart.PRIORITY_PROP, priority);
$(el).setProperty(StatusbarPart.ALIGNMENT_PROP, alignment);
el.setAttribute(StatusbarPart.PRIORITY_PROP, String(priority));
el.setAttribute(StatusbarPart.ALIGNMENT_PROP, String(alignment));
return el;
}
@@ -185,7 +195,7 @@ export class StatusbarPart extends Part implements IStatusbarService {
statusDispose = this.addEntry({ text: message }, StatusbarAlignment.LEFT, -Number.MAX_VALUE /* far right on left hand side */);
showHandle = null;
}, delayBy);
let hideHandle: number;
let hideHandle: any;
// Dispose function takes care of timeouts and actual entry
const dispose = {
@@ -242,7 +252,7 @@ class StatusBarEntryItem implements IStatusbarItem {
if (this.entry.command) {
textContainer = document.createElement('a');
$(textContainer).on('click', () => this.executeCommand(this.entry.command, this.entry.arguments), toDispose);
toDispose.push(addDisposableListener(textContainer, 'click', () => this.executeCommand(this.entry.command, this.entry.arguments)));
} else {
textContainer = document.createElement('span');
}
@@ -252,7 +262,7 @@ class StatusBarEntryItem implements IStatusbarItem {
// Tooltip
if (this.entry.tooltip) {
$(textContainer).title(this.entry.tooltip);
textContainer.title = this.entry.tooltip;
}
// Color
@@ -263,23 +273,23 @@ class StatusBarEntryItem implements IStatusbarItem {
color = (this.themeService.getTheme().getColor(colorId) || Color.transparent).toString();
toDispose.push(this.themeService.onThemeChange(theme => {
let colorValue = (this.themeService.getTheme().getColor(colorId) || Color.transparent).toString();
$(textContainer).color(colorValue);
textContainer.style.color = colorValue;
}));
}
$(textContainer).color(color);
textContainer.style.color = color;
}
// Context Menu
if (this.entry.extensionId) {
$(textContainer).on('contextmenu', e => {
toDispose.push(addDisposableListener(textContainer, 'contextmenu', e => {
EventHelper.stop(e, true);
this.contextMenuService.showContextMenu({
getAnchor: () => el,
getActionsContext: () => this.entry.extensionId,
getActions: () => TPromise.as([manageExtensionAction])
getActions: () => [manageExtensionAction]
});
}, toDispose);
}));
}
el.appendChild(textContainer);
@@ -307,7 +317,7 @@ class StatusBarEntryItem implements IStatusbarItem {
}
*/
this.telemetryService.publicLog('workbenchActionExecuted', { id, from: 'status bar' });
this.commandService.executeCommand(id, ...args).done(undefined, err => this.notificationService.error(toErrorMessage(err)));
this.commandService.executeCommand(id, ...args).then(undefined, err => this.notificationService.error(toErrorMessage(err)));
}
}
@@ -319,7 +329,7 @@ class ManageExtensionAction extends Action {
super('statusbar.manage.extension', nls.localize('manageExtension', "Manage Extension"));
}
run(extensionId: string): TPromise<any> {
run(extensionId: string): Thenable<any> {
return this.commandService.executeCommand('_extensions.manage', extensionId);
}
}

View File

@@ -51,6 +51,11 @@
overflow: visible;
}
.monaco-workbench.windows > .part.titlebar > .window-title,
.monaco-workbench.linux > .part.titlebar > .window-title {
cursor: default;
}
.monaco-workbench.linux > .part.titlebar > .window-title {
font-size: inherit;
}
@@ -96,6 +101,7 @@
-webkit-app-region: no-drag;
height: 100%;
width: 138px;
margin-left: auto;
}
.monaco-workbench.fullscreen > .part.titlebar > .window-controls-container {

View File

@@ -0,0 +1,708 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IMenubarMenu, IMenubarMenuItemAction, IMenubarMenuItemSubmenu, IMenubarKeybinding, IMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar';
import { IMenuService, MenuId, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions';
import { registerThemingParticipant, ITheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService';
import { IWindowService, MenuBarVisibility, IWindowsService, getTitleBarStyle } from 'vs/platform/windows/common/windows';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IAction, Action } from 'vs/base/common/actions';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import * as DOM from 'vs/base/browser/dom';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { isMacintosh, isLinux } from 'vs/base/common/platform';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IRecentlyOpened } from 'vs/platform/history/common/history';
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { RunOnceScheduler } from 'vs/base/common/async';
import { MENUBAR_SELECTION_FOREGROUND, MENUBAR_SELECTION_BACKGROUND, MENUBAR_SELECTION_BORDER, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND } from 'vs/workbench/common/theme';
import { URI } from 'vs/base/common/uri';
import { ILabelService } from 'vs/platform/label/common/label';
import { IUpdateService, StateType } from 'vs/platform/update/common/update';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { MenuBar } from 'vs/base/browser/ui/menu/menubar';
import { SubmenuAction } from 'vs/base/browser/ui/menu/menu';
import { attachMenuStyler } from 'vs/platform/theme/common/styler';
export class MenubarControl extends Disposable {
private keys = [
'files.autoSave',
'window.menuBarVisibility',
'editor.multiCursorModifier',
'workbench.sideBar.location',
'workbench.statusBar.visible',
'workbench.activityBar.visible',
'window.enableMenuBarMnemonics',
'window.nativeTabs'
];
private topLevelMenus: {
'File': IMenu;
'Edit': IMenu;
'Selection': IMenu;
'View': IMenu;
'Go': IMenu;
'Debug': IMenu;
'Terminal': IMenu;
'Window'?: IMenu;
'Help': IMenu;
[index: string]: IMenu;
};
private topLevelTitles = {
'File': nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File"),
'Edit': nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit"),
'Selection': nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection"),
'View': nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View"),
'Go': nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go"),
'Debug': nls.localize({ key: 'mDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug"),
'Terminal': nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal"),
'Help': nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")
};
private menubar: MenuBar;
private menuUpdater: RunOnceScheduler;
private container: HTMLElement;
private recentlyOpened: IRecentlyOpened;
private _onVisibilityChange: Emitter<boolean>;
private _onFocusStateChange: Emitter<boolean>;
private static MAX_MENU_RECENT_ENTRIES = 10;
constructor(
@IThemeService private themeService: IThemeService,
@IMenubarService private menubarService: IMenubarService,
@IMenuService private menuService: IMenuService,
@IWindowService private windowService: IWindowService,
@IWindowsService private windowsService: IWindowsService,
@IContextKeyService private contextKeyService: IContextKeyService,
@IKeybindingService private keybindingService: IKeybindingService,
@IConfigurationService private configurationService: IConfigurationService,
@ILabelService private labelService: ILabelService,
@IUpdateService private updateService: IUpdateService,
@IStorageService private storageService: IStorageService,
@INotificationService private notificationService: INotificationService,
@IPreferencesService private preferencesService: IPreferencesService,
@IEnvironmentService private environmentService: IEnvironmentService
) {
super();
this.topLevelMenus = {
'File': this._register(this.menuService.createMenu(MenuId.MenubarFileMenu, this.contextKeyService)),
'Edit': this._register(this.menuService.createMenu(MenuId.MenubarEditMenu, this.contextKeyService)),
'Selection': this._register(this.menuService.createMenu(MenuId.MenubarSelectionMenu, this.contextKeyService)),
'View': this._register(this.menuService.createMenu(MenuId.MenubarViewMenu, this.contextKeyService)),
'Go': this._register(this.menuService.createMenu(MenuId.MenubarGoMenu, this.contextKeyService)),
'Debug': this._register(this.menuService.createMenu(MenuId.MenubarDebugMenu, this.contextKeyService)),
'Terminal': this._register(this.menuService.createMenu(MenuId.MenubarTerminalMenu, this.contextKeyService)),
'Help': this._register(this.menuService.createMenu(MenuId.MenubarHelpMenu, this.contextKeyService))
};
if (isMacintosh) {
this.topLevelMenus['Preferences'] = this._register(this.menuService.createMenu(MenuId.MenubarPreferencesMenu, this.contextKeyService));
}
this.menuUpdater = this._register(new RunOnceScheduler(() => this.doUpdateMenubar(false), 200));
if (isMacintosh || this.currentTitlebarStyleSetting !== 'custom') {
for (let topLevelMenuName of Object.keys(this.topLevelMenus)) {
this._register(this.topLevelMenus[topLevelMenuName].onDidChange(() => this.updateMenubar()));
}
this.doUpdateMenubar(true);
}
this._onVisibilityChange = this._register(new Emitter<boolean>());
this._onFocusStateChange = this._register(new Emitter<boolean>());
this.windowService.getRecentlyOpened().then((recentlyOpened) => {
this.recentlyOpened = recentlyOpened;
});
this.detectAndRecommendCustomTitlebar();
this.registerListeners();
}
private get currentEnableMenuBarMnemonics(): boolean {
let enableMenuBarMnemonics = this.configurationService.getValue<boolean>('window.enableMenuBarMnemonics');
if (typeof enableMenuBarMnemonics !== 'boolean') {
enableMenuBarMnemonics = true;
}
return enableMenuBarMnemonics;
}
private get currentSidebarPosition(): string {
return this.configurationService.getValue<string>('workbench.sideBar.location');
}
private get currentStatusBarVisibility(): boolean {
let setting = this.configurationService.getValue<boolean>('workbench.statusBar.visible');
if (typeof setting !== 'boolean') {
setting = true;
}
return setting;
}
private get currentActivityBarVisibility(): boolean {
let setting = this.configurationService.getValue<boolean>('workbench.activityBar.visible');
if (typeof setting !== 'boolean') {
setting = true;
}
return setting;
}
private get currentMenubarVisibility(): MenuBarVisibility {
return this.configurationService.getValue<MenuBarVisibility>('window.menuBarVisibility');
}
private get currentTitlebarStyleSetting(): string {
return getTitleBarStyle(this.configurationService, this.environmentService);
}
private onDidChangeWindowFocus(hasFocus: boolean): void {
if (this.container) {
if (hasFocus) {
DOM.removeClass(this.container, 'inactive');
} else {
DOM.addClass(this.container, 'inactive');
this.menubar.blur();
}
}
}
private onConfigurationUpdated(event: IConfigurationChangeEvent): void {
if (this.keys.some(key => event.affectsConfiguration(key))) {
this.updateMenubar();
}
if (event.affectsConfiguration('window.menuBarVisibility')) {
this.detectAndRecommendCustomTitlebar();
}
}
private onRecentlyOpenedChange(): void {
this.windowService.getRecentlyOpened().then(recentlyOpened => {
this.recentlyOpened = recentlyOpened;
this.updateMenubar();
});
}
private detectAndRecommendCustomTitlebar(): void {
if (!isLinux) {
return;
}
if (!this.storageService.getBoolean('menubar/electronFixRecommended', StorageScope.GLOBAL, false)) {
if (this.currentMenubarVisibility === 'hidden' || this.currentTitlebarStyleSetting === 'custom') {
// Issue will not arise for user, abort notification
return;
}
const message = nls.localize('menubar.electronFixRecommendation', "If you experience hard to read text in the menu bar, we recommend trying out the custom title bar.");
this.notificationService.prompt(Severity.Info, message, [
{
label: nls.localize('goToSetting', "Open Settings"),
run: () => {
return this.preferencesService.openGlobalSettings(undefined, { query: 'window.titleBarStyle' });
}
},
{
label: nls.localize('moreInfo', "More Info"),
run: () => {
window.open('https://go.microsoft.com/fwlink/?linkid=2038566');
}
},
{
label: nls.localize('neverShowAgain', "Don't Show Again"),
run: () => {
this.storageService.store('menubar/electronFixRecommended', true, StorageScope.GLOBAL);
}
}
]);
}
}
private registerListeners(): void {
// Update when config changes
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)));
// Listen to update service
this.updateService.onStateChange(() => this.updateMenubar());
// Listen for changes in recently opened menu
this._register(this.windowsService.onRecentlyOpenedChange(() => { this.onRecentlyOpenedChange(); }));
// Listen to keybindings change
this._register(this.keybindingService.onDidUpdateKeybindings(() => this.updateMenubar()));
// These listeners only apply when the custom menubar is being used
if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') {
// Listen for window focus changes
this._register(this.windowService.onDidChangeFocus(e => this.onDidChangeWindowFocus(e)));
this._register(this.windowService.onDidChangeMaximize(e => this.updateMenubar()));
this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, () => {
this.menubar.blur();
}));
}
}
private doUpdateMenubar(firstTime: boolean): void {
if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') {
this.setupCustomMenubar(firstTime);
} else {
// Send menus to main process to be rendered by Electron
const menubarData = { menus: {}, keybindings: {} };
if (this.getMenubarMenus(menubarData)) {
this.menubarService.updateMenubar(this.windowService.getCurrentWindowId(), menubarData);
}
}
}
private updateMenubar(): void {
this.menuUpdater.schedule();
}
private calculateActionLabel(action: IAction | IMenubarMenuItemAction): string {
let label = action.label;
switch (action.id) {
case 'workbench.action.toggleSidebarPosition':
if (this.currentSidebarPosition !== 'right') {
label = nls.localize({ key: 'miMoveSidebarRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Right");
} else {
label = nls.localize({ key: 'miMoveSidebarLeft', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left");
}
break;
case 'workbench.action.toggleStatusbarVisibility':
if (this.currentStatusBarVisibility) {
label = nls.localize({ key: 'miHideStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Hide Status Bar");
} else {
label = nls.localize({ key: 'miShowStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Show Status Bar");
}
break;
case 'workbench.action.toggleActivityBarVisibility':
if (this.currentActivityBarVisibility) {
label = nls.localize({ key: 'miHideActivityBar', comment: ['&& denotes a mnemonic'] }, "Hide &&Activity Bar");
} else {
label = nls.localize({ key: 'miShowActivityBar', comment: ['&& denotes a mnemonic'] }, "Show &&Activity Bar");
}
break;
default:
break;
}
return label;
}
private createOpenRecentMenuAction(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI, commandId: string, isFile: boolean): IAction {
let label: string;
let uri: URI;
if (isSingleFolderWorkspaceIdentifier(workspace) && !isFile) {
label = this.labelService.getWorkspaceLabel(workspace, { verbose: true });
uri = workspace;
} else if (isWorkspaceIdentifier(workspace)) {
label = this.labelService.getWorkspaceLabel(workspace, { verbose: true });
uri = URI.file(workspace.configPath);
} else {
uri = workspace;
label = this.labelService.getUriLabel(uri);
}
return new Action(commandId, label, undefined, undefined, (event) => {
const openInNewWindow = event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey)));
return this.windowService.openWindow([uri], {
forceNewWindow: openInNewWindow,
forceOpenWorkspaceAsFile: isFile
});
});
}
private getOpenRecentActions(): IAction[] {
if (!this.recentlyOpened) {
return [];
}
const { workspaces, files } = this.recentlyOpened;
const result: IAction[] = [];
if (workspaces.length > 0) {
for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < workspaces.length; i++) {
result.push(this.createOpenRecentMenuAction(workspaces[i], 'openRecentWorkspace', false));
}
result.push(new Separator());
}
if (files.length > 0) {
for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < files.length; i++) {
result.push(this.createOpenRecentMenuAction(files[i], 'openRecentFile', false));
}
result.push(new Separator());
}
return result;
}
private getUpdateAction(): IAction | null {
const state = this.updateService.state;
switch (state.type) {
case StateType.Uninitialized:
return null;
case StateType.Idle:
const windowId = this.windowService.getCurrentWindowId();
return new Action('update.check', nls.localize({ key: 'checkForUpdates', comment: ['&& denotes a mnemonic'] }, "Check for &&Updates..."), undefined, true, () =>
this.updateService.checkForUpdates({ windowId }));
case StateType.CheckingForUpdates:
return new Action('update.checking', nls.localize('checkingForUpdates', "Checking For Updates..."), undefined, false);
case StateType.AvailableForDownload:
return new Action('update.downloadNow', nls.localize({ key: 'download now', comment: ['&& denotes a mnemonic'] }, "D&&ownload Now"), null, true, () =>
this.updateService.downloadUpdate());
case StateType.Downloading:
return new Action('update.downloading', nls.localize('DownloadingUpdate', "Downloading Update..."), undefined, false);
case StateType.Downloaded:
return new Action('update.install', nls.localize({ key: 'installUpdate...', comment: ['&& denotes a mnemonic'] }, "Install &&Update..."), undefined, true, () =>
this.updateService.applyUpdate());
case StateType.Updating:
return new Action('update.updating', nls.localize('installingUpdate', "Installing Update..."), undefined, false);
case StateType.Ready:
return new Action('update.restart', nls.localize({ key: 'restartToUpdate', comment: ['&& denotes a mnemonic'] }, "Restart to &&Update..."), undefined, true, () =>
this.updateService.quitAndInstall());
}
}
private insertActionsBefore(nextAction: IAction, target: IAction[]): void {
switch (nextAction.id) {
case 'workbench.action.openRecent':
target.push(...this.getOpenRecentActions());
break;
case 'workbench.action.showAboutDialog':
if (!isMacintosh) {
const updateAction = this.getUpdateAction();
if (updateAction) {
target.push(updateAction);
target.push(new Separator());
}
}
break;
default:
break;
}
}
private setupCustomMenubar(firstTime: boolean): void {
if (firstTime) {
this.menubar = this._register(new MenuBar(
this.container, {
enableMnemonics: this.currentEnableMenuBarMnemonics,
visibility: this.currentMenubarVisibility,
getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id),
}
));
this._register(this.menubar.onFocusStateChange(e => this._onFocusStateChange.fire(e)));
this._register(this.menubar.onVisibilityChange(e => this._onVisibilityChange.fire(e)));
this._register(attachMenuStyler(this.menubar, this.themeService));
} else {
this.menubar.update({ enableMnemonics: this.currentEnableMenuBarMnemonics, visibility: this.currentMenubarVisibility, getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id) });
}
// Update the menu actions
const updateActions = (menu: IMenu, target: IAction[]) => {
target.splice(0);
let groups = menu.getActions();
for (let group of groups) {
const [, actions] = group;
for (let action of actions) {
this.insertActionsBefore(action, target);
if (action instanceof SubmenuItemAction) {
const submenu = this.menuService.createMenu(action.item.submenu, this.contextKeyService);
const submenuActions: SubmenuAction[] = [];
updateActions(submenu, submenuActions);
target.push(new SubmenuAction(action.label, submenuActions));
submenu.dispose();
} else {
action.label = this.calculateActionLabel(action);
target.push(action);
}
}
target.push(new Separator());
}
target.pop();
};
for (let title of Object.keys(this.topLevelMenus)) {
const menu = this.topLevelMenus[title];
if (firstTime) {
this._register(menu.onDidChange(() => {
const actions = [];
updateActions(menu, actions);
this.menubar.updateMenu({ actions: actions, label: this.topLevelTitles[title] });
}));
}
const actions = [];
updateActions(menu, actions);
if (!firstTime) {
this.menubar.updateMenu({ actions: actions, label: this.topLevelTitles[title] });
} else {
this.menubar.push({ actions: actions, label: this.topLevelTitles[title] });
}
}
}
private getMenubarKeybinding(id: string): IMenubarKeybinding {
const binding = this.keybindingService.lookupKeybinding(id);
if (!binding) {
return undefined;
}
// first try to resolve a native accelerator
const electronAccelerator = binding.getElectronAccelerator();
if (electronAccelerator) {
return { label: electronAccelerator };
}
// we need this fallback to support keybindings that cannot show in electron menus (e.g. chords)
const acceleratorLabel = binding.getLabel();
if (acceleratorLabel) {
return { label: acceleratorLabel, isNative: false };
}
return null;
}
private populateMenuItems(menu: IMenu, menuToPopulate: IMenubarMenu, keybindings: { [id: string]: IMenubarKeybinding }) {
let groups = menu.getActions();
for (let group of groups) {
const [, actions] = group;
actions.forEach(menuItem => {
if (menuItem instanceof SubmenuItemAction) {
const submenu = { items: [] };
const menuToDispose = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService);
this.populateMenuItems(menuToDispose, submenu, keybindings);
let menubarSubmenuItem: IMenubarMenuItemSubmenu = {
id: menuItem.id,
label: menuItem.label,
submenu: submenu
};
menuToPopulate.items.push(menubarSubmenuItem);
menuToDispose.dispose();
} else {
let menubarMenuItem: IMenubarMenuItemAction = {
id: menuItem.id,
label: menuItem.label
};
if (menuItem.checked) {
menubarMenuItem.checked = true;
}
if (!menuItem.enabled) {
menubarMenuItem.enabled = false;
}
menubarMenuItem.label = this.calculateActionLabel(menubarMenuItem);
keybindings[menuItem.id] = this.getMenubarKeybinding(menuItem.id);
menuToPopulate.items.push(menubarMenuItem);
}
});
menuToPopulate.items.push({ id: 'vscode.menubar.separator' });
}
if (menuToPopulate.items.length > 0) {
menuToPopulate.items.pop();
}
}
private getAdditionalKeybindings(): { [id: string]: IMenubarKeybinding } {
const keybindings = {};
if (isMacintosh) {
keybindings['workbench.action.quit'] = (this.getMenubarKeybinding('workbench.action.quit'));
}
return keybindings;
}
private getMenubarMenus(menubarData: IMenubarData): boolean {
if (!menubarData) {
return false;
}
menubarData.keybindings = this.getAdditionalKeybindings();
for (let topLevelMenuName of Object.keys(this.topLevelMenus)) {
const menu = this.topLevelMenus[topLevelMenuName];
let menubarMenu: IMenubarMenu = { items: [] };
this.populateMenuItems(menu, menubarMenu, menubarData.keybindings);
if (menubarMenu.items.length === 0) {
// Menus are incomplete
return false;
}
menubarData.menus[topLevelMenuName] = menubarMenu;
}
return true;
}
public get onVisibilityChange(): Event<boolean> {
return this._onVisibilityChange.event;
}
public get onFocusStateChange(): Event<boolean> {
return this._onFocusStateChange.event;
}
public layout(dimension: DOM.Dimension) {
if (this.container) {
this.container.style.height = `${dimension.height}px`;
}
if (this.menubar) {
this.menubar.update({ enableMnemonics: this.currentEnableMenuBarMnemonics, visibility: this.currentMenubarVisibility, getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id) });
}
}
public getMenubarItemsDimensions(): DOM.Dimension {
if (this.menubar) {
return new DOM.Dimension(this.menubar.getWidth(), this.menubar.getHeight());
}
return new DOM.Dimension(0, 0);
}
public create(parent: HTMLElement): HTMLElement {
this.container = parent;
// Build the menubar
if (this.container) {
if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') {
this.doUpdateMenubar(true);
}
}
return this.container;
}
}
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const menubarActiveWindowFgColor = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND);
if (menubarActiveWindowFgColor) {
collector.addRule(`
.monaco-workbench .menubar > .menubar-menu-button {
color: ${menubarActiveWindowFgColor};
}
.monaco-workbench .menubar .toolbar-toggle-more {
background-color: ${menubarActiveWindowFgColor}
}
`);
}
const menubarInactiveWindowFgColor = theme.getColor(TITLE_BAR_INACTIVE_FOREGROUND);
if (menubarInactiveWindowFgColor) {
collector.addRule(`
.monaco-workbench .menubar.inactive > .menubar-menu-button {
color: ${menubarInactiveWindowFgColor};
}
.monaco-workbench .menubar.inactive > .menubar-menu-button .toolbar-toggle-more {
background-color: ${menubarInactiveWindowFgColor}
}
`);
}
const menubarSelectedFgColor = theme.getColor(MENUBAR_SELECTION_FOREGROUND);
if (menubarSelectedFgColor) {
collector.addRule(`
.monaco-workbench .menubar > .menubar-menu-button.open,
.monaco-workbench .menubar > .menubar-menu-button:focus,
.monaco-workbench .menubar:not(:focus-within) > .menubar-menu-button:hover {
color: ${menubarSelectedFgColor};
}
.monaco-workbench .menubar > .menubar-menu-button.open .toolbar-toggle-more,
.monaco-workbench .menubar > .menubar-menu-button:focus .toolbar-toggle-more,
.monaco-workbench .menubar:not(:focus-within) > .menubar-menu-button:hover .toolbar-toggle-more {
background-color: ${menubarSelectedFgColor}
}
`);
}
const menubarSelectedBgColor = theme.getColor(MENUBAR_SELECTION_BACKGROUND);
if (menubarSelectedBgColor) {
collector.addRule(`
.monaco-workbench .menubar > .menubar-menu-button.open,
.monaco-workbench .menubar > .menubar-menu-button:focus,
.monaco-workbench .menubar:not(:focus-within) > .menubar-menu-button:hover {
background-color: ${menubarSelectedBgColor};
}
`);
}
const menubarSelectedBorderColor = theme.getColor(MENUBAR_SELECTION_BORDER);
if (menubarSelectedBorderColor) {
collector.addRule(`
.monaco-workbench .menubar > .menubar-menu-button:hover {
outline: dashed 1px;
}
.monaco-workbench .menubar > .menubar-menu-button.open,
.monaco-workbench .menubar > .menubar-menu-button:focus {
outline: solid 1px;
}
.monaco-workbench .menubar > .menubar-menu-button.open,
.monaco-workbench .menubar > .menubar-menu-button:focus,
.monaco-workbench .menubar > .menubar-menu-button:hover {
outline-offset: -1px;
outline-color: ${menubarSelectedBorderColor};
}
`);
}
});

View File

@@ -3,17 +3,12 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/titlebarpart';
import { TPromise } from 'vs/base/common/winjs.base';
import { Builder, $ } from 'vs/base/browser/builder';
import * as paths from 'vs/base/common/paths';
import { Part } from 'vs/workbench/browser/part';
import { ITitleService, ITitleProperties } from 'vs/workbench/services/title/common/titleService';
import { getZoomFactor } from 'vs/base/browser/browser';
import { IWindowService, IWindowsService, MenuBarVisibility } from 'vs/platform/windows/common/windows';
import * as errors from 'vs/base/common/errors';
import { IWindowService, IWindowsService, MenuBarVisibility, getTitleBarStyle } from 'vs/platform/windows/common/windows';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IAction, Action } from 'vs/base/common/actions';
@@ -27,14 +22,16 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER } from 'vs/workbench/common/theme';
import { isMacintosh, isWindows, isLinux } from 'vs/base/common/platform';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { Color } from 'vs/base/common/color';
import { trim } from 'vs/base/common/strings';
import { addDisposableListener, EventType, EventHelper, Dimension } from 'vs/base/browser/dom';
import { MenubarPart } from 'vs/workbench/browser/parts/menubar/menubarPart';
import { EventType, EventHelper, Dimension, isAncestor, hide, show, removeClass, addClass, append, $, addDisposableListener } from 'vs/base/browser/dom';
import { MenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { template, getBaseLabel } from 'vs/base/common/labels';
import { IUriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
import { ILabelService } from 'vs/platform/label/common/label';
import { Event } from 'vs/base/common/event';
import { IStorageService } from 'vs/platform/storage/common/storage';
export class TitlebarPart extends Part implements ITitleService {
@@ -46,27 +43,19 @@ export class TitlebarPart extends Part implements ITitleService {
private static readonly TITLE_DIRTY = '\u25cf ';
private static readonly TITLE_SEPARATOR = isMacintosh ? ' — ' : ' - '; // macOS uses special - separator
private titleContainer: Builder;
private title: Builder;
private dragRegion: Builder;
private windowControls: Builder;
private maxRestoreControl: Builder;
private appIcon: Builder;
private menubarPart: MenubarPart;
private menubar: Builder;
private resizer: Builder;
private titleContainer: HTMLElement;
private title: HTMLElement;
private dragRegion: HTMLElement;
private windowControls: HTMLElement;
private maxRestoreControl: HTMLElement;
private appIcon: HTMLElement;
private menubarPart: MenubarControl;
private menubar: HTMLElement;
private resizer: HTMLElement;
private pendingTitle: string;
private representedFileName: string;
private initialSizing: {
titleFontSize?: number;
titlebarHeight?: number;
controlsWidth?: number;
appIconSize?: number;
appIconWidth?: number;
} = Object.create(null);
private isInactive: boolean;
private properties: ITitleProperties;
@@ -83,9 +72,10 @@ export class TitlebarPart extends Part implements ITitleService {
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IInstantiationService private instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IUriDisplayService private uriDisplayService: IUriDisplayService
@ILabelService private labelService: ILabelService,
@IStorageService storageService: IStorageService
) {
super(id, { hasTitle: false }, themeService);
super(id, { hasTitle: false }, themeService, storageService);
this.properties = { isPure: true, isAdmin: false };
this.activeEditorListeners = [];
@@ -94,13 +84,13 @@ export class TitlebarPart extends Part implements ITitleService {
}
private registerListeners(): void {
this._register(addDisposableListener(window, EventType.BLUR, () => this.onBlur()));
this._register(addDisposableListener(window, EventType.FOCUS, () => this.onFocus()));
this._register(this.windowService.onDidChangeFocus(focused => focused ? this.onFocus() : this.onBlur()));
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e)));
this._register(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChange()));
this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.setTitle(this.getWindowTitle())));
this._register(this.contextService.onDidChangeWorkbenchState(() => this.setTitle(this.getWindowTitle())));
this._register(this.contextService.onDidChangeWorkspaceName(() => this.setTitle(this.getWindowTitle())));
this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.doUpdateTitle()));
this._register(this.contextService.onDidChangeWorkbenchState(() => this.doUpdateTitle()));
this._register(this.contextService.onDidChangeWorkspaceName(() => this.doUpdateTitle()));
this._register(this.labelService.onDidRegisterFormatter(() => this.doUpdateTitle()));
}
private onBlur(): void {
@@ -115,7 +105,7 @@ export class TitlebarPart extends Part implements ITitleService {
private onConfigurationChanged(event: IConfigurationChangeEvent): void {
if (event.affectsConfiguration('window.title')) {
this.setTitle(this.getWindowTitle());
this.doUpdateTitle();
}
}
@@ -123,17 +113,29 @@ export class TitlebarPart extends Part implements ITitleService {
if (isWindows || isLinux) {
// Hide title when toggling menu bar
if (this.configurationService.getValue<MenuBarVisibility>('window.menuBarVisibility') === 'toggle' && visible) {
this.title.style('visibility', 'hidden');
// Hack to fix issue #52522 with layered webkit-app-region elements appearing under cursor
this.dragRegion.hide();
this.dragRegion.showDelayed(50);
hide(this.dragRegion);
setTimeout(() => show(this.dragRegion), 50);
}
this.adjustTitleMarginToCenter();
}
}
private onMenubarFocusChanged(focused: boolean) {
if (isWindows || isLinux) {
if (focused) {
hide(this.dragRegion);
} else {
this.title.style('visibility', null);
show(this.dragRegion);
}
}
}
onMenubarVisibilityChange(): Event<boolean> {
return this.menubarPart.onVisibilityChange;
}
private onActiveEditorChange(): void {
// Dispose old listeners
@@ -141,18 +143,13 @@ export class TitlebarPart extends Part implements ITitleService {
this.activeEditorListeners = [];
// Calculate New Window Title
this.setTitle(this.getWindowTitle());
this.doUpdateTitle();
// Apply listener for dirty and label changes
const activeEditor = this.editorService.activeEditor;
if (activeEditor instanceof EditorInput) {
this.activeEditorListeners.push(activeEditor.onDidChangeDirty(() => {
this.setTitle(this.getWindowTitle());
}));
this.activeEditorListeners.push(activeEditor.onDidChangeLabel(() => {
this.setTitle(this.getWindowTitle());
}));
this.activeEditorListeners.push(activeEditor.onDidChangeDirty(() => this.doUpdateTitle()));
this.activeEditorListeners.push(activeEditor.onDidChangeLabel(() => this.doUpdateTitle()));
}
// Represented File Name
@@ -170,23 +167,41 @@ export class TitlebarPart extends Part implements ITitleService {
this.representedFileName = path;
}
private getWindowTitle(): string {
let title = this.doGetWindowTitle();
if (!trim(title)) {
title = this.environmentService.appNameLong;
private doUpdateTitle(): void {
const title = this.getWindowTitle();
// Always set the native window title to identify us properly to the OS
let nativeTitle = title;
if (!trim(nativeTitle)) {
nativeTitle = this.environmentService.appNameLong;
}
window.document.title = nativeTitle;
// Apply custom title if we can
if (this.title) {
this.title.innerText = title;
} else {
this.pendingTitle = title;
}
if (isWindows || isLinux) {
this.adjustTitleMarginToCenter();
}
}
private getWindowTitle(): string {
let title = this.doGetWindowTitle();
if (this.properties.isAdmin) {
title = `${title} ${TitlebarPart.NLS_USER_IS_ADMIN}`;
title = `${title || this.environmentService.appNameLong} ${TitlebarPart.NLS_USER_IS_ADMIN}`;
}
if (!this.properties.isPure) {
title = `${title} ${TitlebarPart.NLS_UNSUPPORTED}`;
title = `${title || this.environmentService.appNameLong} ${TitlebarPart.NLS_UNSUPPORTED}`;
}
// Extension Development Host gets a special title to identify itself
if (this.environmentService.isExtensionDevelopment) {
title = `${TitlebarPart.NLS_EXTENSION_HOST} - ${title}`;
title = `${TitlebarPart.NLS_EXTENSION_HOST} - ${title || this.environmentService.appNameLong}`;
}
return title;
@@ -200,7 +215,7 @@ export class TitlebarPart extends Part implements ITitleService {
this.properties.isAdmin = isAdmin;
this.properties.isPure = isPure;
this.setTitle(this.getWindowTitle());
this.doUpdateTitle();
}
}
@@ -238,10 +253,10 @@ export class TitlebarPart extends Part implements ITitleService {
const activeEditorShort = editor ? editor.getTitle(Verbosity.SHORT) : '';
const activeEditorMedium = editor ? editor.getTitle(Verbosity.MEDIUM) : activeEditorShort;
const activeEditorLong = editor ? editor.getTitle(Verbosity.LONG) : activeEditorMedium;
const rootName = workspace.name;
const rootPath = root ? this.uriDisplayService.getLabel(root) : '';
const rootName = this.labelService.getWorkspaceLabel(workspace);
const rootPath = root ? this.labelService.getUriLabel(root) : '';
const folderName = folder ? folder.name : '';
const folderPath = folder ? this.uriDisplayService.getLabel(folder.uri) : '';
const folderPath = folder ? this.labelService.getUriLabel(folder.uri) : '';
const dirty = editor && editor.isDirty() ? TitlebarPart.TITLE_DIRTY : '';
const appName = this.environmentService.appNameLong;
const separator = TitlebarPart.TITLE_SEPARATOR;
@@ -262,83 +277,94 @@ export class TitlebarPart extends Part implements ITitleService {
}
createContentArea(parent: HTMLElement): HTMLElement {
this.titleContainer = $(parent);
this.titleContainer = parent;
// Draggable region that we can manipulate for #52522
this.dragRegion = $(this.titleContainer).div({ class: 'titlebar-drag-region' });
this.dragRegion = append(this.titleContainer, $('div.titlebar-drag-region'));
// App Icon (Windows/Linux)
if (!isMacintosh) {
this.appIcon = $(this.titleContainer).div({ class: 'window-appicon' });
this.appIcon = append(this.titleContainer, $('div.window-appicon'));
}
// Menubar: the menubar part which is responsible for populating both the custom and native menubars
this.menubarPart = this.instantiationService.createInstance(MenubarPart, 'workbench.parts.menubar');
this.menubar = $(this.titleContainer).div({
'class': ['part', 'menubar'],
id: 'workbench.parts.menubar',
role: 'menubar'
});
this.menubarPart = this.instantiationService.createInstance(MenubarControl);
this.menubar = append(this.titleContainer, $('div.menubar'));
this.menubar.setAttribute('role', 'menubar');
this.menubarPart.create(this.menubar.getHTMLElement());
this.menubarPart.create(this.menubar);
if (!isMacintosh) {
this._register(this.menubarPart.onVisibilityChange(e => this.onMenubarVisibilityChanged(e)));
this._register(this.menubarPart.onFocusStateChange(e => this.onMenubarFocusChanged(e)));
}
// Title
this.title = $(this.titleContainer).div({ class: 'window-title' });
this.title = append(this.titleContainer, $('div.window-title'));
if (this.pendingTitle) {
this.title.text(this.pendingTitle);
this.title.innerText = this.pendingTitle;
} else {
this.setTitle(this.getWindowTitle());
this.doUpdateTitle();
}
// Maximize/Restore on doubleclick
if (isMacintosh) {
this.titleContainer.on(EventType.DBLCLICK, (e) => {
this._register(addDisposableListener(this.titleContainer, EventType.DBLCLICK, e => {
EventHelper.stop(e);
this.onTitleDoubleclick();
});
}));
}
// Context menu on title
this.title.on([EventType.CONTEXT_MENU, EventType.MOUSE_DOWN], (e: MouseEvent) => {
if (e.type === EventType.CONTEXT_MENU || e.metaKey) {
EventHelper.stop(e);
[EventType.CONTEXT_MENU, EventType.MOUSE_DOWN].forEach(event => {
this._register(addDisposableListener(this.title, event, e => {
if (e.type === EventType.CONTEXT_MENU || e.metaKey) {
EventHelper.stop(e);
this.onContextMenu(e);
}
this.onContextMenu(e);
}
}));
});
// Window Controls (Windows/Linux)
if (!isMacintosh) {
this.windowControls = $(this.titleContainer).div({ class: 'window-controls-container' });
this.windowControls = append(this.titleContainer, $('div.window-controls-container'));
// Minimize
$($(this.windowControls).div({ class: 'window-icon-bg' })).div({ class: 'window-icon window-minimize' }).on(EventType.CLICK, () => {
this.windowService.minimizeWindow().then(null, errors.onUnexpectedError);
});
const minimizeIconContainer = append(this.windowControls, $('div.window-icon-bg'));
const minimizeIcon = append(minimizeIconContainer, $('div.window-icon'));
addClass(minimizeIcon, 'window-minimize');
this._register(addDisposableListener(minimizeIcon, EventType.CLICK, e => {
this.windowService.minimizeWindow();
}));
// Restore
this.maxRestoreControl = $($(this.windowControls).div({ class: 'window-icon-bg' })).div({ class: 'window-icon window-max-restore' }).on(EventType.CLICK, () => {
const restoreIconContainer = append(this.windowControls, $('div.window-icon-bg'));
this.maxRestoreControl = append(restoreIconContainer, $('div.window-icon'));
addClass(this.maxRestoreControl, 'window-max-restore');
this._register(addDisposableListener(this.maxRestoreControl, EventType.CLICK, e => {
this.windowService.isMaximized().then((maximized) => {
if (maximized) {
return this.windowService.unmaximizeWindow();
}
return this.windowService.maximizeWindow();
}).then(null, errors.onUnexpectedError);
});
});
}));
// Close
$($(this.windowControls).div({ class: 'window-icon-bg window-close-bg' })).div({ class: 'window-icon window-close' }).on(EventType.CLICK, () => {
this.windowService.closeWindow().then(null, errors.onUnexpectedError);
});
const closeIconContainer = append(this.windowControls, $('div.window-icon-bg'));
addClass(closeIconContainer, 'window-close-bg');
const closeIcon = append(closeIconContainer, $('div.window-icon'));
addClass(closeIcon, 'window-close');
this._register(addDisposableListener(closeIcon, EventType.CLICK, e => {
this.windowService.closeWindow();
}));
// Resizer
this.resizer = $(this.titleContainer).div({ class: 'resizer' });
this.resizer = append(this.titleContainer, $('div.resizer'));
const isMaximized = this.windowService.getConfiguration().maximized ? true : false;
this.onDidChangeMaximized(isMaximized);
@@ -347,36 +373,44 @@ export class TitlebarPart extends Part implements ITitleService {
// Since the title area is used to drag the window, we do not want to steal focus from the
// currently active element. So we restore focus after a timeout back to where it was.
this.titleContainer.on([EventType.MOUSE_DOWN], () => {
this._register(addDisposableListener(this.titleContainer, EventType.MOUSE_DOWN, e => {
if (e.target && isAncestor(e.target as HTMLElement, this.menubar)) {
return;
}
const active = document.activeElement;
setTimeout(() => {
if (active instanceof HTMLElement) {
active.focus();
}
}, 0 /* need a timeout because we are in capture phase */);
}, void 0, true /* use capture to know the currently active element properly */);
}, true /* use capture to know the currently active element properly */));
return this.titleContainer.getHTMLElement();
this.updateStyles();
return this.titleContainer;
}
private onDidChangeMaximized(maximized: boolean) {
if (this.maxRestoreControl) {
if (maximized) {
this.maxRestoreControl.removeClass('window-maximize');
this.maxRestoreControl.addClass('window-unmaximize');
removeClass(this.maxRestoreControl, 'window-maximize');
addClass(this.maxRestoreControl, 'window-unmaximize');
} else {
this.maxRestoreControl.removeClass('window-unmaximize');
this.maxRestoreControl.addClass('window-maximize');
removeClass(this.maxRestoreControl, 'window-unmaximize');
addClass(this.maxRestoreControl, 'window-maximize');
}
}
if (this.resizer) {
if (maximized) {
this.resizer.hide();
hide(this.resizer);
} else {
this.resizer.show();
show(this.resizer);
}
}
this.adjustTitleMarginToCenter();
}
protected updateStyles(): void {
@@ -385,29 +419,29 @@ export class TitlebarPart extends Part implements ITitleService {
// Part container
if (this.titleContainer) {
if (this.isInactive) {
this.titleContainer.addClass('inactive');
addClass(this.titleContainer, 'inactive');
} else {
this.titleContainer.removeClass('inactive');
removeClass(this.titleContainer, 'inactive');
}
const titleBackground = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_BACKGROUND : TITLE_BAR_ACTIVE_BACKGROUND);
this.titleContainer.style('background-color', titleBackground);
this.titleContainer.style.backgroundColor = titleBackground;
if (Color.fromHex(titleBackground).isLighter()) {
this.titleContainer.addClass('light');
addClass(this.titleContainer, 'light');
} else {
this.titleContainer.removeClass('light');
removeClass(this.titleContainer, 'light');
}
const titleForeground = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_FOREGROUND : TITLE_BAR_ACTIVE_FOREGROUND);
this.titleContainer.style('color', titleForeground);
this.titleContainer.style.color = titleForeground;
const titleBorder = this.getColor(TITLE_BAR_BORDER);
this.titleContainer.style('border-bottom', titleBorder ? `1px solid ${titleBorder}` : null);
this.titleContainer.style.borderBottom = titleBorder ? `1px solid ${titleBorder}` : null;
}
}
private onTitleDoubleclick(): void {
this.windowService.onWindowTitleDoubleClick().then(null, errors.onUnexpectedError);
this.windowService.onWindowTitleDoubleClick();
}
private onContextMenu(e: MouseEvent): void {
@@ -421,7 +455,7 @@ export class TitlebarPart extends Part implements ITitleService {
if (actions.length) {
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => TPromise.as(actions),
getActions: () => actions,
onHide: () => actions.forEach(a => a.dispose())
});
}
@@ -456,97 +490,47 @@ export class TitlebarPart extends Part implements ITitleService {
return actions;
}
setTitle(title: string): void {
// Always set the native window title to identify us properly to the OS
window.document.title = title;
// Apply if we can
if (this.title) {
this.title.text(title);
} else {
this.pendingTitle = title;
}
}
private updateLayout(dimension: Dimension) {
// Store initital title sizing if we need to prevent zooming
if (typeof this.initialSizing.titleFontSize !== 'number') {
this.initialSizing.titleFontSize = parseInt(this.title.getComputedStyle().fontSize, 10);
}
if (typeof this.initialSizing.titlebarHeight !== 'number') {
this.initialSizing.titlebarHeight = parseInt(this.title.getComputedStyle().height, 10);
}
// Only prevent zooming behavior on macOS or when the menubar is not visible
if (isMacintosh || this.configurationService.getValue<MenuBarVisibility>('window.menuBarVisibility') === 'hidden') {
// To prevent zooming we need to adjust the font size with the zoom factor
const newHeight = this.initialSizing.titlebarHeight / getZoomFactor();
this.title.style({
fontSize: `${this.initialSizing.titleFontSize / getZoomFactor()}px`,
'line-height': `${newHeight}px`
});
// Windows/Linux specific layout
if (isWindows || isLinux) {
if (typeof this.initialSizing.controlsWidth !== 'number') {
this.initialSizing.controlsWidth = parseInt(this.windowControls.getComputedStyle().width, 10);
}
if (typeof this.initialSizing.appIconWidth !== 'number') {
this.initialSizing.appIconWidth = parseInt(this.appIcon.getComputedStyle().width, 10);
}
if (typeof this.initialSizing.appIconSize !== 'number') {
this.initialSizing.appIconSize = parseInt(this.appIcon.getComputedStyle().backgroundSize, 10);
}
const currentAppIconHeight = parseInt(this.appIcon.getComputedStyle().height, 10);
const newControlsWidth = this.initialSizing.controlsWidth / getZoomFactor();
const newAppIconWidth = this.initialSizing.appIconWidth / getZoomFactor();
const newAppIconSize = this.initialSizing.appIconSize / getZoomFactor();
// Adjust app icon mimic menubar
this.appIcon.style({
'width': `${newAppIconWidth}px`,
'background-size': `${newAppIconSize}px`,
'padding-top': `${(newHeight - currentAppIconHeight) / 2.0}px`,
'padding-bottom': `${(newHeight - currentAppIconHeight) / 2.0}px`
});
// Adjust windows controls
this.windowControls.style({
'width': `${newControlsWidth}px`
});
private adjustTitleMarginToCenter(): void {
setTimeout(() => {
// Cannot center
if (!isMacintosh &&
(this.appIcon.clientWidth + this.menubar.clientWidth + 10 > (this.titleContainer.clientWidth - this.title.clientWidth) / 2 ||
this.titleContainer.clientWidth - this.windowControls.clientWidth - 10 < (this.titleContainer.clientWidth + this.title.clientWidth) / 2)) {
this.title.style.position = null;
this.title.style.left = null;
this.title.style.transform = null;
} else {
this.title.style.position = 'absolute';
this.title.style.left = '50%';
this.title.style.transform = 'translate(-50%, 0)';
}
} else {
// We need to undo zoom prevention
this.title.style({
fontSize: null,
'line-height': null
});
this.appIcon.style({
'width': null,
'background-size': null,
'padding-top': null,
'padding-bottom': null
});
this.windowControls.style({
'width': null
});
}
if (this.menubarPart) {
const menubarDimension = new Dimension(undefined, dimension.height);
this.menubarPart.layout(menubarDimension);
}
}, 0); // delay so that we can get accurate information about widths
}
layout(dimension: Dimension): Dimension[] {
this.updateLayout(dimension);
if (getTitleBarStyle(this.configurationService, this.environmentService) === 'custom') {
// Only prevent zooming behavior on macOS or when the menubar is not visible
if (isMacintosh || this.configurationService.getValue<MenuBarVisibility>('window.menuBarVisibility') === 'hidden') {
this.title.style.zoom = `${1.0 / getZoomFactor()}`;
if (isWindows || isLinux) {
this.appIcon.style.zoom = `${1.0 / getZoomFactor()}`;
this.windowControls.style.zoom = `${1.0 / getZoomFactor()}`;
}
} else {
this.title.style.zoom = null;
if (isWindows || isLinux) {
this.appIcon.style.zoom = null;
this.windowControls.style.zoom = null;
}
}
this.adjustTitleMarginToCenter();
if (this.menubarPart) {
const menubarDimension = new Dimension(undefined, dimension.height);
this.menubarPart.layout(menubarDimension);
}
}
return super.layout(dimension);
}
@@ -558,7 +542,7 @@ class ShowItemInFolderAction extends Action {
super('showItemInFolder.action.id', label);
}
run(): TPromise<void> {
run(): Thenable<void> {
return this.windowsService.showItemInFolder(this.path);
}
}

View File

@@ -7,85 +7,93 @@ import 'vs/css!./media/views';
import { Event, Emitter } from 'vs/base/common/event';
import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction, IActionItem, ActionRunner } from 'vs/base/common/actions';
import { IAction, IActionItem, ActionRunner, Action } from 'vs/base/common/actions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { ContextAwareMenuItemActionItem, fillInActionBarActions, fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IViewsService, ITreeViewer, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ICustomViewDescriptor, ViewsRegistry, ViewContainer } from 'vs/workbench/common/views';
import { IViewsService, ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ICustomViewDescriptor, ViewsRegistry, ViewContainer, ITreeItemLabel } from 'vs/workbench/common/views';
import { IViewletViewOptions, FileIconThemableWorkbenchTree } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IProgressService2 } from 'vs/workbench/services/progress/common/progress';
import { IProgressService2 } from 'vs/platform/progress/common/progress';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import * as DOM from 'vs/base/browser/dom';
import * as errors from 'vs/base/common/errors';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IDataSource, ITree, IRenderer, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree';
import { ResourceLabel } from 'vs/workbench/browser/labels';
import { ActionBar, IActionItemProvider, ActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { basename } from 'vs/base/common/paths';
import { LIGHT, FileThemeIcon, FolderThemeIcon } from 'vs/platform/theme/common/themeService';
import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { FileKind } from 'vs/platform/files/common/files';
import { WorkbenchTreeController } from 'vs/platform/list/browser/listService';
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { localize } from 'vs/nls';
import { timeout } from 'vs/base/common/async';
import { CollapseAllAction } from 'vs/base/parts/tree/browser/treeDefaults';
import { editorFindMatchHighlight, editorFindMatchHighlightBorder, textLinkForeground, textCodeBlockBackground, focusBorder } from 'vs/platform/theme/common/colorRegistry';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { isString } from 'vs/base/common/types';
import { renderMarkdown, RenderOptions } from 'vs/base/browser/htmlContentRenderer';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRenderer';
import { ILabelService } from 'vs/platform/label/common/label';
import { dirname } from 'vs/base/common/resources';
export class CustomTreeViewPanel extends ViewletPanel {
private menus: TitleMenus;
private treeViewer: ITreeViewer;
private treeView: ITreeView;
constructor(
options: IViewletViewOptions,
@INotificationService private notificationService: INotificationService,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IInstantiationService private instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@IViewsService viewsService: IViewsService,
) {
super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService);
this.treeViewer = (<ICustomViewDescriptor>ViewsRegistry.getView(options.id)).treeViewer;
this.disposables.push(toDisposable(() => this.treeViewer.setVisibility(false)));
this.menus = this.instantiationService.createInstance(TitleMenus, this.id);
this.menus.onDidChangeTitle(() => this.updateActions(), this, this.disposables);
const { treeView } = (<ICustomViewDescriptor>ViewsRegistry.getView(options.id));
this.treeView = treeView;
this.treeView.onDidChangeActions(() => this.updateActions(), this, this.disposables);
this.disposables.push(toDisposable(() => this.treeView.setVisibility(false)));
this.updateTreeVisibility();
}
setVisible(visible: boolean): TPromise<void> {
return super.setVisible(visible).then(() => this.updateTreeVisibility());
setVisible(visible: boolean): void {
super.setVisible(visible);
this.updateTreeVisibility();
}
focus(): void {
super.focus();
this.treeViewer.focus();
this.treeView.focus();
}
renderBody(container: HTMLElement): void {
this.treeViewer.show(container);
this.treeView.show(container);
}
setExpanded(expanded: boolean): void {
this.treeViewer.setVisibility(this.isVisible() && expanded);
this.treeView.setVisibility(this.isVisible() && expanded);
super.setExpanded(expanded);
}
layoutBody(size: number): void {
this.treeViewer.layout(size);
this.treeView.layout(size);
}
getActions(): IAction[] {
return [...this.menus.getTitleActions()];
return [...this.treeView.getPrimaryActions()];
}
getSecondaryActions(): IAction[] {
return this.menus.getTitleSecondaryActions();
return [...this.treeView.getSecondaryActions()];
}
getActionItem(action: IAction): IActionItem {
@@ -93,11 +101,11 @@ export class CustomTreeViewPanel extends ViewletPanel {
}
getOptimalWidth(): number {
return this.treeViewer.getOptimalWidth();
return this.treeView.getOptimalWidth();
}
private updateTreeVisibility(): void {
this.treeViewer.setVisibility(this.isVisible() && this.isExpanded());
this.treeView.setVisibility(this.isVisible() && this.isExpanded());
}
dispose(): void {
@@ -163,26 +171,35 @@ class TitleMenus implements IDisposable {
}
class Root implements ITreeItem {
label = 'root';
label = { label: 'root' };
handle = '0';
parentHandle = null;
collapsibleState = TreeItemCollapsibleState.Expanded;
children = void 0;
}
export class CustomTreeViewer extends Disposable implements ITreeViewer {
const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data.");
export class CustomTreeView extends Disposable implements ITreeView {
private isVisible: boolean = false;
private activated: boolean = false;
private _hasIconForParentNode = false;
private _hasIconForLeafNode = false;
private _showCollapseAllAction = false;
private focused: boolean = false;
private domNode: HTMLElement;
private treeContainer: HTMLElement;
private _messageValue: string | IMarkdownString | undefined;
private messageElement: HTMLDivElement;
private tree: FileIconThemableWorkbenchTree;
private root: ITreeItem;
private elementsToRefresh: ITreeItem[] = [];
private menus: TitleMenus;
private _dataProvider: ITreeViewDataProvider;
private markdownRenderer: MarkdownRenderer;
private markdownResult: IMarkdownRenderResult;
private _onDidExpandItem: Emitter<ITreeItem> = this._register(new Emitter<ITreeItem>());
readonly onDidExpandItem: Event<ITreeItem> = this._onDidExpandItem.event;
@@ -196,6 +213,9 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
private _onDidChangeVisibility: Emitter<boolean> = this._register(new Emitter<boolean>());
readonly onDidChangeVisibility: Event<boolean> = this._onDidChangeVisibility.event;
private _onDidChangeActions: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeActions: Event<void> = this._onDidChangeActions.event;
constructor(
private id: string,
private container: ViewContainer,
@@ -203,10 +223,13 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
@IWorkbenchThemeService private themeService: IWorkbenchThemeService,
@IInstantiationService private instantiationService: IInstantiationService,
@ICommandService private commandService: ICommandService,
@IConfigurationService private configurationService: IConfigurationService
@IConfigurationService private configurationService: IConfigurationService,
@IProgressService2 private progressService: IProgressService2
) {
super();
this.root = new Root();
this.menus = this._register(instantiationService.createInstance(TitleMenus, this.id));
this._register(this.menus.onDidChangeTitle(() => this._onDidChangeActions.fire()));
this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/));
this._register(this.themeService.onThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/));
this._register(this.configurationService.onDidChangeConfiguration(e => {
@@ -214,8 +237,16 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
this.doRefresh([this.root]); /** soft refresh **/
}
}));
this.markdownRenderer = instantiationService.createInstance(MarkdownRenderer);
this._register(toDisposable(() => {
if (this.markdownResult) {
this.markdownResult.dispose();
}
}));
this.create();
}
private _dataProvider: ITreeViewDataProvider;
get dataProvider(): ITreeViewDataProvider {
return this._dataProvider;
}
@@ -223,9 +254,9 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
set dataProvider(dataProvider: ITreeViewDataProvider) {
if (dataProvider) {
this._dataProvider = new class implements ITreeViewDataProvider {
getChildren(node?: ITreeItem): TPromise<ITreeItem[]> {
getChildren(node?: ITreeItem): Promise<ITreeItem[]> {
if (node && node.children) {
return TPromise.as(node.children);
return Promise.resolve(node.children);
}
const promise = node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node);
return promise.then(children => {
@@ -234,10 +265,22 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
});
}
};
this.updateMessage();
this.refresh();
} else {
this._dataProvider = null;
this.updateMessage();
}
this.refresh();
}
private _message: string | IMarkdownString | undefined;
get message(): string | IMarkdownString | undefined {
return this._message;
}
set message(message: string | IMarkdownString | undefined) {
this._message = message;
this.updateMessage();
}
get hasIconForParentNode(): boolean {
@@ -252,6 +295,30 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
return this.isVisible;
}
get showCollapseAllAction(): boolean {
return this._showCollapseAllAction;
}
set showCollapseAllAction(showCollapseAllAction: boolean) {
if (this._showCollapseAllAction !== !!showCollapseAllAction) {
this._showCollapseAllAction = !!showCollapseAllAction;
this._onDidChangeActions.fire();
}
}
getPrimaryActions(): IAction[] {
if (this.showCollapseAllAction) {
const collapseAllAction = new Action('vs.tree.collapse', localize('collapse', "Collapse"), 'monaco-tree-action collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve());
return [...this.menus.getTitleActions(), collapseAllAction];
} else {
return this.menus.getTitleActions();
}
}
getSecondaryActions(): IAction[] {
return this.menus.getTitleSecondaryActions();
}
setVisibility(isVisible: boolean): void {
isVisible = !!isVisible;
if (this.isVisible === isVisible) {
@@ -286,27 +353,34 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
}
focus(): void {
if (this.tree) {
if (this.tree && this.root.children && this.root.children.length > 0) {
// Make sure the current selected element is revealed
const selectedElement = this.tree.getSelection()[0];
if (selectedElement) {
this.tree.reveal(selectedElement, 0.5).done(null, errors.onUnexpectedError);
this.tree.reveal(selectedElement, 0.5);
}
// Pass Focus to Viewer
this.tree.domFocus();
} else {
this.domNode.focus();
}
}
show(container: HTMLElement): void {
if (!this.tree) {
this.createTree();
}
DOM.append(container, this.treeContainer);
DOM.append(container, this.domNode);
}
private create() {
this.domNode = DOM.$('.tree-explorer-viewlet-tree-view');
this.messageElement = DOM.append(this.domNode, DOM.$('.message'));
this.treeContainer = DOM.append(this.domNode, DOM.$('.customview-tree'));
const focusTracker = this._register(DOM.trackFocus(this.domNode));
this._register(focusTracker.onDidFocus(() => this.focused = true));
this._register(focusTracker.onDidBlur(() => this.focused = false));
}
private createTree() {
this.treeContainer = DOM.$('.tree-explorer-viewlet-tree-view');
const actionItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuItemActionItem, action) : undefined;
const menus = this.instantiationService.createInstance(TreeMenus, this.id);
const dataSource = this.instantiationService.createInstance(TreeDataSource, this, this.container);
@@ -319,27 +393,72 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
this._register(this.tree.onDidExpandItem(e => this._onDidExpandItem.fire(e.item.getElement())));
this._register(this.tree.onDidCollapseItem(e => this._onDidCollapseItem.fire(e.item.getElement())));
this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.selection)));
this.tree.setInput(this.root);
this.tree.setInput(this.root).then(() => this.updateContentAreas());
}
private updateMessage(): void {
if (this._message) {
this.showMessage(this._message);
} else if (!this.dataProvider) {
this.showMessage(noDataProviderMessage);
} else {
this.hideMessage();
}
this.updateContentAreas();
}
private showMessage(message: string | IMarkdownString): void {
DOM.removeClass(this.messageElement, 'hide');
if (this._messageValue !== message) {
this.resetMessageElement();
this._messageValue = message;
if (isString(this._messageValue)) {
this.messageElement.textContent = this._messageValue;
} else {
this.markdownResult = this.markdownRenderer.render(this._messageValue);
DOM.append(this.messageElement, this.markdownResult.element);
}
this.layout(this._size);
}
}
private hideMessage(): void {
this.resetMessageElement();
DOM.addClass(this.messageElement, 'hide');
this.layout(this._size);
}
private resetMessageElement(): void {
if (this.markdownResult) {
this.markdownResult.dispose();
this.markdownResult = null;
}
DOM.clearNode(this.messageElement);
}
private _size: number;
layout(size: number) {
if (this.tree) {
this.treeContainer.style.height = size + 'px';
this.tree.layout(size);
if (size) {
this._size = size;
const treeSize = size - DOM.getTotalHeight(this.messageElement);
this.treeContainer.style.height = treeSize + 'px';
if (this.tree) {
this.tree.layout(treeSize);
}
}
}
getOptimalWidth(): number {
if (this.tree) {
const parentNode = this.tree.getHTMLElement();
const childNodes = [].slice.call(parentNode.querySelectorAll('.outline-item-label > a'));
const childNodes = ([] as Element[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a'));
return DOM.getLargestChildWidth(parentNode, childNodes);
}
return 0;
}
refresh(elements?: ITreeItem[]): TPromise<void> {
if (this.tree) {
refresh(elements?: ITreeItem[]): Promise<void> {
if (this.dataProvider && this.tree) {
elements = elements || [this.root];
for (const element of elements) {
element.children = null; // reset children
@@ -350,49 +469,75 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
this.elementsToRefresh.push(...elements);
}
}
return TPromise.as(null);
return Promise.resolve(null);
}
reveal(item: ITreeItem, parentChain: ITreeItem[], options?: { select?: boolean, focus?: boolean }): TPromise<void> {
if (this.tree && this.isVisible) {
options = options ? options : { select: false, focus: false };
const select = isUndefinedOrNull(options.select) ? false : options.select;
const focus = isUndefinedOrNull(options.focus) ? false : options.focus;
const root: Root = this.tree.getInput();
const promise = root.children ? TPromise.as(null) : this.refresh(); // Refresh if root is not populated
return promise.then(() => {
var result = TPromise.as(null);
parentChain.forEach((e) => {
result = result.then(() => this.tree.expand(e));
});
return result.then(() => this.tree.reveal(item))
.then(() => {
if (select) {
this.tree.setSelection([item], { source: 'api' });
}
if (focus) {
this.focus();
this.tree.setFocus(item);
}
});
});
expand(itemOrItems: ITreeItem | ITreeItem[]): Thenable<void> {
if (this.tree) {
itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
return this.tree.expandAll(itemOrItems);
}
return TPromise.as(null);
return Promise.arguments(null);
}
setSelection(items: ITreeItem[]): void {
if (this.tree) {
this.tree.setSelection(items, { source: 'api' });
}
}
setFocus(item: ITreeItem): void {
if (this.tree) {
this.focus();
this.tree.setFocus(item);
}
}
reveal(item: ITreeItem): Thenable<void> {
if (this.tree) {
return this.tree.reveal(item);
}
return Promise.arguments(null);
}
private activate() {
if (!this.activated) {
this.extensionService.activateByEvent(`onView:${this.id}`);
this.createTree();
this.progressService.withProgress({ location: this.container.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`))
.then(() => timeout(2000))
.then(() => {
this.updateMessage();
});
this.activated = true;
}
}
private doRefresh(elements: ITreeItem[]): TPromise<void> {
private refreshing: boolean = false;
private doRefresh(elements: ITreeItem[]): Promise<void> {
if (this.tree) {
return TPromise.join(elements.map(e => this.tree.refresh(e))).then(() => null);
this.refreshing = true;
return Promise.all(elements.map(e => this.tree.refresh(e)))
.then(() => {
this.refreshing = false;
this.updateContentAreas();
if (this.focused) {
this.focus();
}
});
}
return Promise.resolve(null);
}
private updateContentAreas(): void {
const isTreeEmpty = !this.root.children || this.root.children.length === 0;
// Hide tree container only when there is a message and tree is empty and not refreshing
if (this._messageValue && isTreeEmpty && !this.refreshing) {
DOM.addClass(this.treeContainer, 'hide');
this.domNode.setAttribute('tabindex', '0');
} else {
DOM.removeClass(this.treeContainer, 'hide');
this.domNode.removeAttribute('tabindex');
}
return TPromise.as(null);
}
private onSelection({ payload }: any): void {
@@ -417,7 +562,7 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
class TreeDataSource implements IDataSource {
constructor(
private treeView: ITreeViewer,
private treeView: ITreeView,
private container: ViewContainer,
@IProgressService2 private progressService: IProgressService2
) {
@@ -431,19 +576,19 @@ class TreeDataSource implements IDataSource {
return this.treeView.dataProvider && node.collapsibleState !== TreeItemCollapsibleState.None;
}
getChildren(tree: ITree, node: ITreeItem): TPromise<any[]> {
getChildren(tree: ITree, node: ITreeItem): Promise<any[]> {
if (this.treeView.dataProvider) {
return this.progressService.withProgress({ location: this.container }, () => this.treeView.dataProvider.getChildren(node));
return this.progressService.withProgress({ location: this.container.id }, () => this.treeView.dataProvider.getChildren(node));
}
return TPromise.as([]);
return Promise.resolve([]);
}
shouldAutoexpand(tree: ITree, node: ITreeItem): boolean {
return node.collapsibleState === TreeItemCollapsibleState.Expanded;
}
getParent(tree: ITree, node: any): TPromise<any> {
return TPromise.as(null);
getParent(tree: ITree, node: any): Promise<any> {
return Promise.resolve(null);
}
}
@@ -454,6 +599,31 @@ interface ITreeExplorerTemplateData {
aligner: Aligner;
}
// todo@joh,sandy make this proper and contributable from extensions
registerThemingParticipant((theme, collector) => {
const findMatchHighlightColor = theme.getColor(editorFindMatchHighlight);
if (findMatchHighlightColor) {
collector.addRule(`.file-icon-themable-tree .monaco-tree-row .content .monaco-highlighted-label .highlight { color: unset !important; background-color: ${findMatchHighlightColor}; }`);
}
const findMatchHighlightColorBorder = theme.getColor(editorFindMatchHighlightBorder);
if (findMatchHighlightColorBorder) {
collector.addRule(`.file-icon-themable-tree .monaco-tree-row .content .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${findMatchHighlightColorBorder}; box-sizing: border-box; }`);
}
const link = theme.getColor(textLinkForeground);
if (link) {
collector.addRule(`.tree-explorer-viewlet-tree-view > .message a { color: ${link}; }`);
}
const focustBorderColor = theme.getColor(focusBorder);
if (focustBorderColor) {
collector.addRule(`.tree-explorer-viewlet-tree-view > .message a:focus { outline: 1px solid ${focustBorderColor}; outline-offset: -1px; }`);
}
const codeBackground = theme.getColor(textCodeBlockBackground);
if (codeBackground) {
collector.addRule(`.tree-explorer-viewlet-tree-view > .message code { background-color: ${codeBackground}; }`);
}
});
class TreeRenderer implements IRenderer {
private static readonly ITEM_HEIGHT = 22;
@@ -466,6 +636,7 @@ class TreeRenderer implements IRenderer {
@IInstantiationService private instantiationService: IInstantiationService,
@IWorkbenchThemeService private themeService: IWorkbenchThemeService,
@IConfigurationService private configurationService: IConfigurationService,
@ILabelService private labelService: ILabelService
) {
}
@@ -481,7 +652,7 @@ class TreeRenderer implements IRenderer {
DOM.addClass(container, 'custom-view-tree-node-item');
const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon'));
const resourceLabel = this.instantiationService.createInstance(ResourceLabel, container, {});
const resourceLabel = this.instantiationService.createInstance(ResourceLabel, container, { supportHighlights: true, donotSupportOcticons: true });
DOM.addClass(resourceLabel.element, 'custom-view-tree-node-item-resourceLabel');
const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions'));
const actionBar = new ActionBar(actionsContainer, {
@@ -494,23 +665,27 @@ class TreeRenderer implements IRenderer {
renderElement(tree: ITree, node: ITreeItem, templateId: string, templateData: ITreeExplorerTemplateData): void {
const resource = node.resourceUri ? URI.revive(node.resourceUri) : null;
const label = node.label ? node.label : resource ? basename(resource.path) : '';
const treeItemLabel: ITreeItemLabel = node.label ? node.label : resource ? { label: basename(resource.path) } : void 0;
const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : void 0;
const label = treeItemLabel ? treeItemLabel.label : void 0;
const matches = treeItemLabel && treeItemLabel.highlights ? treeItemLabel.highlights.map(([start, end]) => ({ start, end })) : void 0;
const icon = this.themeService.getTheme().type === LIGHT ? node.icon : node.iconDark;
const iconUrl = icon ? URI.revive(icon) : null;
const title = node.tooltip ? node.tooltip : resource ? void 0 : label;
// reset
templateData.resourceLabel.clear();
templateData.actionBar.clear();
if ((resource || node.themeIcon) && !icon) {
if (resource || node.themeIcon) {
const fileDecorations = this.configurationService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations');
templateData.resourceLabel.setLabel({ name: label, resource: resource ? resource : URI.parse('_icon_resource') }, { fileKind: this.getFileKind(node), title, fileDecorations: fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'] });
templateData.resourceLabel.setLabel({ name: label, description, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), title, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches });
} else {
templateData.resourceLabel.setLabel({ name: label }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'] });
templateData.resourceLabel.setLabel({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches });
}
templateData.icon.style.backgroundImage = icon ? `url('${icon}')` : '';
DOM.toggleClass(templateData.icon, 'custom-view-tree-node-item-icon', !!icon);
templateData.icon.style.backgroundImage = iconUrl ? `url('${iconUrl.toString(true)}')` : '';
DOM.toggleClass(templateData.icon, 'custom-view-tree-node-item-icon', !!iconUrl);
templateData.actionBar.context = (<TreeViewItemHandleArg>{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle });
templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false });
@@ -621,9 +796,7 @@ class TreeController extends WorkbenchTreeController {
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => {
return TPromise.as(actions);
},
getActions: () => actions,
getActionItem: (action) => {
const keybinding = this._keybindingService.lookupKeybinding(action.id);
@@ -654,7 +827,7 @@ class MultipleSelectionActionRunner extends ActionRunner {
super();
}
runAction(action: IAction, context: any): TPromise<any> {
runAction(action: IAction, context: any): Thenable<any> {
if (action instanceof MenuItemAction) {
const selection = this.getSelectedResources();
const filteredSelection = selection.filter(s => s !== context);
@@ -706,3 +879,39 @@ class TreeMenus extends Disposable implements IDisposable {
return result;
}
}
class MarkdownRenderer {
constructor(
@IOpenerService private readonly _openerService: IOpenerService
) {
}
private getOptions(disposeables: IDisposable[]): RenderOptions {
return {
actionHandler: {
callback: (content) => {
let uri: URI | undefined;
try {
uri = URI.parse(content);
} catch {
// ignore
}
if (uri && this._openerService) {
this._openerService.open(uri).catch(onUnexpectedError);
}
},
disposeables
}
};
}
render(markdown: IMarkdownString): IMarkdownRenderResult {
let disposeables: IDisposable[] = [];
const element: HTMLElement = markdown ? renderMarkdown(markdown, this.getOptions(disposeables)) : document.createElement('span');
return {
element,
dispose: () => dispose(disposeables)
};
}
}

View File

@@ -3,6 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-panel-view .split-view-view:first-of-type > .panel > .panel-header {
border-top: none !important; /* less clutter: do not show any border for first views in a panel */
}
.monaco-panel-view .panel > .panel-header h3.title {
white-space: nowrap;
text-overflow: ellipsis;

View File

@@ -50,19 +50,51 @@
display: none;
}
.tree-explorer-viewlet-tree-view.file-icon-themable-tree .monaco-tree-row .content.align-icon-with-twisty::before {
.monaco-workbench .tree-explorer-viewlet-tree-view {
height: 100%;
}
.monaco-workbench .tree-explorer-viewlet-tree-view .message {
display: flex;
padding: 4px 12px 0px 18px;
user-select: text
}
.monaco-workbench .tree-explorer-viewlet-tree-view .message p {
margin-top: 0px;
margin-bottom: 0px;
padding-bottom: 4px;
}
.monaco-workbench .tree-explorer-viewlet-tree-view .message ul {
padding-left: 24px;
}
.monaco-workbench .tree-explorer-viewlet-tree-view .message.hide {
display: none;
}
.tree-explorer-viewlet-tree-view.file-icon-themable-tree .monaco-tree-row .content:not(.align-icon-with-twisty)::before {
.monaco-workbench .tree-explorer-viewlet-tree-view .customview-tree {
height: 100%;
}
.monaco-workbench .tree-explorer-viewlet-tree-view .customview-tree.hide {
display: none;
}
.customview-tree.file-icon-themable-tree .monaco-tree-row .content.align-icon-with-twisty::before {
display: none;
}
.customview-tree.file-icon-themable-tree .monaco-tree-row .content:not(.align-icon-with-twisty)::before {
display: inline-block;
}
.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row {
.customview-tree .monaco-tree .monaco-tree-row {
padding-right: 12px;
}
.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item {
.customview-tree .monaco-tree .monaco-tree-row .custom-view-tree-node-item {
display: flex;
height: 22px;
line-height: 22px;
@@ -72,13 +104,13 @@
flex-wrap: nowrap
}
.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel {
.customview-tree .monaco-tree .monaco-tree-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel {
flex: 1;
text-overflow: ellipsis;
overflow: hidden;
}
.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon {
.customview-tree .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon {
background-size: 16px;
background-position: left center;
background-repeat: no-repeat;
@@ -88,25 +120,25 @@
-webkit-font-smoothing: antialiased;
}
.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel .monaco-icon-label-description-container {
.customview-tree .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel .monaco-icon-label-description-container {
flex: 1;
}
.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel::after {
.customview-tree .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel::after {
padding-right: 0px;
}
.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions {
.customview-tree .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions {
display: none;
}
.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row:hover .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions,
.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row.selected .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions,
.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row.focused .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions {
.customview-tree .monaco-tree .monaco-tree-row:hover .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions,
.customview-tree .monaco-tree .monaco-tree-row.selected .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions,
.customview-tree .monaco-tree .monaco-tree-row.focused .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions {
display: block;
}
.tree-explorer-viewlet-tree-view .monaco-tree .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions .action-label {
.customview-tree .monaco-tree .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions .action-label {
width: 16px;
height: 100%;
background-position: 50% 50%;

View File

@@ -5,11 +5,10 @@
import 'vs/css!./media/panelviewlet';
import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { Event, Emitter, filterEvent } from 'vs/base/common/event';
import { ColorIdentifier, contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry';
import { attachStyler, IColorMapping } from 'vs/platform/theme/common/styler';
import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND } from 'vs/workbench/common/theme';
import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER } from 'vs/workbench/common/theme';
import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener } from 'vs/base/browser/dom';
import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
import { firstIndex } from 'vs/base/common/arrays';
@@ -28,12 +27,13 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IView } from 'vs/workbench/common/views';
import { IStorageService } from 'vs/platform/storage/common/storage';
export interface IPanelColors extends IColorMapping {
dropBackground?: ColorIdentifier;
headerForeground?: ColorIdentifier;
headerBackground?: ColorIdentifier;
headerHighContrastBorder?: ColorIdentifier;
headerBorder?: ColorIdentifier;
}
export interface IViewletPanelOptions extends IPanelOptions {
@@ -55,7 +55,7 @@ export abstract class ViewletPanel extends Panel implements IView {
protected _onDidChangeTitleArea = new Emitter<void>();
readonly onDidChangeTitleArea: Event<void> = this._onDidChangeTitleArea.event;
private _isVisible: boolean;
private _isVisible: boolean = true;
readonly id: string;
readonly title: string;
@@ -76,12 +76,10 @@ export abstract class ViewletPanel extends Panel implements IView {
this.actionRunner = options.actionRunner;
}
setVisible(visible: boolean): TPromise<void> {
setVisible(visible: boolean): void {
if (this._isVisible !== visible) {
this._isVisible = visible;
}
return TPromise.wrap(null);
}
isVisible(): boolean {
@@ -165,7 +163,8 @@ export abstract class ViewletPanel extends Panel implements IView {
return 0;
}
shutdown(): void {
saveState(): void {
// Subclasses to implement for saving state
}
}
@@ -199,20 +198,21 @@ export class PanelViewlet extends Viewlet {
constructor(
id: string,
private options: IViewsViewletOptions,
@IConfigurationService configurationService: IConfigurationService,
@IPartService partService: IPartService,
@IContextMenuService protected contextMenuService: IContextMenuService,
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService
) {
super(id, partService, telemetryService, themeService);
super(id, configurationService, partService, telemetryService, themeService, storageService);
}
create(parent: HTMLElement): TPromise<void> {
return super.create(parent).then(() => {
this.panelview = this._register(new PanelView(parent, this.options));
this._register(this.panelview.onDidDrop(({ from, to }) => this.movePanel(from as ViewletPanel, to as ViewletPanel)));
this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e))));
});
create(parent: HTMLElement): void {
super.create(parent);
this.panelview = this._register(new PanelView(parent, this.options));
this._register(this.panelview.onDidDrop(({ from, to }) => this.movePanel(from as ViewletPanel, to as ViewletPanel)));
this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e))));
}
private showContextMenu(event: StandardMouseEvent): void {
@@ -229,7 +229,7 @@ export class PanelViewlet extends Viewlet {
let anchor: { x: number, y: number } = { x: event.posx, y: event.posy };
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => TPromise.as(this.getContextMenuActions())
getActions: () => this.getContextMenuActions()
});
}
@@ -323,7 +323,7 @@ export class PanelViewlet extends Viewlet {
const panelStyler = attachStyler<IPanelColors>(this.themeService, {
headerForeground: SIDE_BAR_SECTION_HEADER_FOREGROUND,
headerBackground: SIDE_BAR_SECTION_HEADER_BACKGROUND,
headerHighContrastBorder: index === 0 ? null : contrastBorder,
headerBorder: SIDE_BAR_SECTION_HEADER_BORDER,
dropBackground: SIDE_BAR_DRAG_AND_DROP_BACKGROUND
}, panel);
const disposable = combinedDisposable([onDidFocus, onDidChangeTitleArea, panelStyler, onDidChange]);

View File

@@ -5,19 +5,20 @@
import 'vs/css!./media/views';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TPromise } from 'vs/base/common/winjs.base';
import { IViewsService, ViewsRegistry, IViewsViewlet, ViewContainer, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID, IView } from 'vs/workbench/common/views';
import { IViewsService, ViewsRegistry, IViewsViewlet, ViewContainer, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, IView, IViewDescriptorCollection } from 'vs/workbench/common/views';
import { Registry } from 'vs/platform/registry/common/platform';
import { ViewletRegistry, Extensions as ViewletExtensions } from 'vs/workbench/browser/viewlet';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IContextKeyService, IContextKeyChangeEvent, IReadableSet } from 'vs/platform/contextkey/common/contextkey';
import { IContextKeyService, IContextKeyChangeEvent, IReadableSet, IContextKey, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { Event, chain, filterEvent, Emitter } from 'vs/base/common/event';
import { sortedDiff, firstIndex, move } from 'vs/base/common/arrays';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { MenuId, MenuRegistry, ICommandAction } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { localize } from 'vs/nls';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { values } from 'vs/base/common/map';
function filterViewEvent(container: ViewContainer, event: Event<IViewDescriptor[]>): Event<IViewDescriptor[]> {
return chain(event)
@@ -63,20 +64,24 @@ interface IViewItem {
active: boolean;
}
class ViewDescriptorCollection extends Disposable {
class ViewDescriptorCollection extends Disposable implements IViewDescriptorCollection {
private contextKeys = new CounterSet<string>();
private items: IViewItem[] = [];
private _onDidChange = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
private _onDidChange: Emitter<{ added: IViewDescriptor[], removed: IViewDescriptor[] }> = this._register(new Emitter<{ added: IViewDescriptor[], removed: IViewDescriptor[] }>());
readonly onDidChangeActiveViews: Event<{ added: IViewDescriptor[], removed: IViewDescriptor[] }> = this._onDidChange.event;
get viewDescriptors(): IViewDescriptor[] {
get activeViewDescriptors(): IViewDescriptor[] {
return this.items
.filter(i => i.active)
.map(i => i.viewDescriptor);
}
get allViewDescriptors(): IViewDescriptor[] {
return this.items.map(i => i.viewDescriptor);
}
constructor(
container: ViewContainer,
@IContextKeyService private contextKeyService: IContextKeyService
@@ -95,7 +100,7 @@ class ViewDescriptorCollection extends Disposable {
}
private onViewsRegistered(viewDescriptors: IViewDescriptor[]): any {
let fireChangeEvent = false;
const added: IViewDescriptor[] = [];
for (const viewDescriptor of viewDescriptors) {
const item = {
@@ -112,17 +117,17 @@ class ViewDescriptorCollection extends Disposable {
}
if (item.active) {
fireChangeEvent = true;
added.push(viewDescriptor);
}
}
if (fireChangeEvent) {
this._onDidChange.fire();
if (added.length) {
this._onDidChange.fire({ added, removed: [] });
}
}
private onViewsDeregistered(viewDescriptors: IViewDescriptor[]): any {
let fireChangeEvent = false;
const removed: IViewDescriptor[] = [];
for (const viewDescriptor of viewDescriptors) {
const index = firstIndex(this.items, i => i.viewDescriptor.id === viewDescriptor.id);
@@ -141,30 +146,35 @@ class ViewDescriptorCollection extends Disposable {
}
if (item.active) {
fireChangeEvent = true;
removed.push(viewDescriptor);
}
}
if (fireChangeEvent) {
this._onDidChange.fire();
if (removed.length) {
this._onDidChange.fire({ added: [], removed });
}
}
private onContextChanged(event: IContextKeyChangeEvent): any {
let fireChangeEvent = false;
const removed: IViewDescriptor[] = [];
const added: IViewDescriptor[] = [];
for (const item of this.items) {
const active = this.isViewDescriptorActive(item.viewDescriptor);
if (item.active !== active) {
fireChangeEvent = true;
if (active) {
added.push(item.viewDescriptor);
} else {
removed.push(item.viewDescriptor);
}
}
item.active = active;
}
if (fireChangeEvent) {
this._onDidChange.fire();
if (added.length || removed.length) {
this._onDidChange.fire({ added, removed });
}
}
@@ -208,14 +218,14 @@ export class ContributableViewsModel extends Disposable {
constructor(
container: ViewContainer,
contextKeyService: IContextKeyService,
viewsService: IViewsService,
protected viewStates = new Map<string, IViewState>(),
) {
super();
const viewDescriptorCollection = this._register(new ViewDescriptorCollection(container, contextKeyService));
const viewDescriptorCollection = viewsService.getViewDescriptors(container);
this._register(viewDescriptorCollection.onDidChange(() => this.onDidChangeViewDescriptors(viewDescriptorCollection.viewDescriptors)));
this.onDidChangeViewDescriptors(viewDescriptorCollection.viewDescriptors);
this._register(viewDescriptorCollection.onDidChangeActiveViews(() => this.onDidChangeViewDescriptors(viewDescriptorCollection.activeViewDescriptors)));
this.onDidChangeViewDescriptors(viewDescriptorCollection.activeViewDescriptors);
}
isVisible(id: string): boolean {
@@ -407,14 +417,14 @@ export class PersistentContributableViewsModel extends ContributableViewsModel {
constructor(
container: ViewContainer,
viewletStateStorageId: string,
@IContextKeyService contextKeyService: IContextKeyService,
@IViewsService viewsService: IViewsService,
@IStorageService storageService: IStorageService,
@IWorkspaceContextService contextService: IWorkspaceContextService
) {
const hiddenViewsStorageId = `${viewletStateStorageId}.hidden`;
const viewStates = PersistentContributableViewsModel.loadViewsStates(viewletStateStorageId, hiddenViewsStorageId, storageService, contextService);
super(container, contextKeyService, viewStates);
super(container, viewsService, viewStates);
this.viewletStateStorageId = viewletStateStorageId;
this.hiddenViewsStorageId = hiddenViewsStorageId;
@@ -423,17 +433,26 @@ export class PersistentContributableViewsModel extends ContributableViewsModel {
this._register(this.onDidAdd(viewDescriptorRefs => this.saveVisibilityStates(viewDescriptorRefs.map(r => r.viewDescriptor))));
this._register(this.onDidRemove(viewDescriptorRefs => this.saveVisibilityStates(viewDescriptorRefs.map(r => r.viewDescriptor))));
this._register(this.storageService.onWillSaveState(() => this.saveViewsStates()));
}
saveViewsStates(): void {
private saveViewsStates(): void {
const storedViewsStates: { [id: string]: { collapsed: boolean, size: number, order: number } } = {};
let hasState = false;
for (const viewDescriptor of this.viewDescriptors) {
const viewState = this.viewStates.get(viewDescriptor.id);
if (viewState) {
storedViewsStates[viewDescriptor.id] = { collapsed: viewState.collapsed, size: viewState.size, order: viewState.order };
hasState = true;
}
}
this.storageService.store(this.viewletStateStorageId, JSON.stringify(storedViewsStates), this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? StorageScope.WORKSPACE : StorageScope.GLOBAL);
if (hasState) {
this.storageService.store(this.viewletStateStorageId, JSON.stringify(storedViewsStates), StorageScope.WORKSPACE);
} else {
this.storageService.remove(this.viewletStateStorageId, StorageScope.WORKSPACE);
}
}
private saveVisibilityStates(viewDescriptors: IViewDescriptor[]): void {
@@ -441,17 +460,17 @@ export class PersistentContributableViewsModel extends ContributableViewsModel {
for (const viewDescriptor of viewDescriptors) {
if (viewDescriptor.canToggleVisibility) {
const viewState = this.viewStates.get(viewDescriptor.id);
storedViewsVisibilityStates.push({ id: viewDescriptor.id, isHidden: viewState ? !viewState.visible : void 0 });
storedViewsVisibilityStates.set(viewDescriptor.id, { id: viewDescriptor.id, isHidden: viewState ? !viewState.visible : void 0 });
}
}
this.storageService.store(this.hiddenViewsStorageId, JSON.stringify(storedViewsVisibilityStates), StorageScope.GLOBAL);
this.storageService.store(this.hiddenViewsStorageId, JSON.stringify(values(storedViewsVisibilityStates)), StorageScope.GLOBAL);
}
private static loadViewsStates(viewletStateStorageId: string, hiddenViewsStorageId: string, storageService: IStorageService, contextService: IWorkspaceContextService): Map<string, IViewState> {
const viewStates = new Map<string, IViewState>();
const storedViewsStates = JSON.parse(storageService.get(viewletStateStorageId, contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? StorageScope.WORKSPACE : StorageScope.GLOBAL, '{}'));
const storedViewsStates = JSON.parse(storageService.get(viewletStateStorageId, StorageScope.WORKSPACE, '{}'));
const viewsVisibilityStates = PersistentContributableViewsModel.loadViewsVisibilityState(hiddenViewsStorageId, storageService, contextService);
for (const { id, isHidden } of viewsVisibilityStates) {
for (const { id, isHidden } of values(viewsVisibilityStates)) {
const viewState = storedViewsStates[id];
if (viewState) {
viewStates.set(id, <IViewState>{ ...viewState, ...{ visible: !isHidden } });
@@ -468,43 +487,62 @@ export class PersistentContributableViewsModel extends ContributableViewsModel {
return viewStates;
}
private static loadViewsVisibilityState(hiddenViewsStorageId: string, storageService: IStorageService, contextService: IWorkspaceContextService): { id: string, isHidden: boolean }[] {
private static loadViewsVisibilityState(hiddenViewsStorageId: string, storageService: IStorageService, contextService: IWorkspaceContextService): Map<string, { id: string, isHidden: boolean }> {
const storedVisibilityStates = <Array<string | { id: string, isHidden: boolean }>>JSON.parse(storageService.get(hiddenViewsStorageId, StorageScope.GLOBAL, '[]'));
return <{ id: string, isHidden: boolean }[]>storedVisibilityStates.map(c => typeof c === 'string' /* migration */ ? { id: c, isHidden: true } : c);
}
let hasDuplicates = false;
const storedViewsVisibilityStates = storedVisibilityStates.reduce((result, storedState) => {
if (typeof storedState === 'string' /* migration */) {
hasDuplicates = hasDuplicates || result.has(storedState);
result.set(storedState, { id: storedState, isHidden: true });
} else {
hasDuplicates = hasDuplicates || result.has(storedState.id);
result.set(storedState.id, storedState);
}
return result;
}, new Map<string, { id: string, isHidden: boolean }>());
dispose(): void {
this.saveViewsStates();
super.dispose();
if (hasDuplicates) {
storageService.store(hiddenViewsStorageId, JSON.stringify(values(storedViewsVisibilityStates)), StorageScope.GLOBAL);
}
return storedViewsVisibilityStates;
}
}
const SCM_VIEWLET_ID = 'workbench.view.scm';
export class ViewsService extends Disposable implements IViewsService {
_serviceBrand: any;
private readonly viewDescriptorCollections: Map<ViewContainer, IViewDescriptorCollection>;
private readonly activeViewContextKeys: Map<string, IContextKey<boolean>>;
constructor(
@IInstantiationService private instantiationService: IInstantiationService,
@ILifecycleService private lifecycleService: ILifecycleService,
@IViewletService private viewletService: IViewletService,
@IStorageService private storageService: IStorageService
@IContextKeyService private contextKeyService: IContextKeyService
) {
super();
this.viewDescriptorCollections = new Map<ViewContainer, IViewDescriptorCollection>();
this.activeViewContextKeys = new Map<string, IContextKey<boolean>>();
this.onDidRegisterViews(ViewsRegistry.getAllViews());
this._register(ViewsRegistry.onViewsRegistered(views => this.onDidRegisterViews(views)));
const viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
viewContainersRegistry.all.forEach(viewContainer => this.onDidRegisterViewContainer(viewContainer));
this._register(viewContainersRegistry.onDidRegister(viewContainer => this.onDidRegisterViewContainer(viewContainer)));
this._register(Registry.as<ViewletRegistry>(ViewletExtensions.Viewlets).onDidRegister(viewlet => this.viewletService.setViewletEnablement(viewlet.id, this.storageService.getBoolean(`viewservice.${viewlet.id}.enablement`, StorageScope.GLOBAL, viewlet.id !== TEST_VIEW_CONTAINER_ID))));
}
openView(id: string, focus: boolean): TPromise<IView> {
getViewDescriptors(container: ViewContainer): IViewDescriptorCollection {
return this.viewDescriptorCollections.get(container);
}
openView(id: string, focus: boolean): Thenable<IView> {
const viewDescriptor = ViewsRegistry.getView(id);
if (viewDescriptor) {
const viewletDescriptor = this.viewletService.getViewlet(viewDescriptor.container.id);
if (viewletDescriptor) {
return this.viewletService.openViewlet(viewletDescriptor.id)
return this.viewletService.openViewlet(viewletDescriptor.id, focus)
.then((viewlet: IViewsViewlet) => {
if (viewlet && viewlet.openView) {
return viewlet.openView(id, focus);
@@ -513,21 +551,62 @@ export class ViewsService extends Disposable implements IViewsService {
});
}
}
return TPromise.as(null);
return Promise.resolve(null);
}
private onDidRegisterViewContainer(viewContainer: ViewContainer): void {
// TODO: @Joao Remove this after moving SCM Viewlet to ViewContainerViewlet - https://github.com/Microsoft/vscode/issues/49054
if (viewContainer.id !== SCM_VIEWLET_ID) {
const viewDescriptorCollection = this._register(this.instantiationService.createInstance(ViewDescriptorCollection, viewContainer));
this._register(viewDescriptorCollection.onDidChange(() => this.updateViewletEnablement(viewContainer, viewDescriptorCollection)));
this.lifecycleService.when(LifecyclePhase.Eventually).then(() => this.updateViewletEnablement(viewContainer, viewDescriptorCollection));
const viewDescriptorCollection = this._register(new ViewDescriptorCollection(viewContainer, this.contextKeyService));
this.onDidChangeActiveViews({ added: viewDescriptorCollection.activeViewDescriptors, removed: [] });
this._register(viewDescriptorCollection.onDidChangeActiveViews(changed => this.onDidChangeActiveViews(changed)));
this.viewDescriptorCollections.set(viewContainer, viewDescriptorCollection);
}
private onDidChangeActiveViews({ added, removed }: { added: IViewDescriptor[], removed: IViewDescriptor[] }): void {
added.forEach(viewDescriptor => this.getOrCreateActiveViewContextKey(viewDescriptor).set(true));
removed.forEach(viewDescriptor => this.getOrCreateActiveViewContextKey(viewDescriptor).set(false));
}
private onDidRegisterViews(viewDescriptors: IViewDescriptor[]): void {
for (const viewDescriptor of viewDescriptors) {
const viewlet = this.viewletService.getViewlet(viewDescriptor.container.id);
const command: ICommandAction = {
id: viewDescriptor.focusCommand ? viewDescriptor.focusCommand.id : `${viewDescriptor.id}.focus`,
title: { original: `Focus on ${viewDescriptor.name} View`, value: localize('focus view', "Focus on {0} View", viewDescriptor.name) },
category: viewlet ? viewlet.name : localize('view category', "View"),
};
const when = ContextKeyExpr.has(`${viewDescriptor.id}.active`);
CommandsRegistry.registerCommand(command.id, () => this.openView(viewDescriptor.id, true).then(() => null));
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command,
when
});
if (viewDescriptor.focusCommand && viewDescriptor.focusCommand.keybindings) {
KeybindingsRegistry.registerKeybindingRule({
id: command.id,
when,
weight: KeybindingWeight.WorkbenchContrib,
primary: viewDescriptor.focusCommand.keybindings.primary,
secondary: viewDescriptor.focusCommand.keybindings.secondary,
linux: viewDescriptor.focusCommand.keybindings.linux,
mac: viewDescriptor.focusCommand.keybindings.mac,
win: viewDescriptor.focusCommand.keybindings.win
});
}
}
}
private updateViewletEnablement(viewContainer: ViewContainer, viewDescriptorCollection: ViewDescriptorCollection): void {
const enabled = viewDescriptorCollection.viewDescriptors.length > 0;
this.viewletService.setViewletEnablement(viewContainer.id, enabled);
this.storageService.store(`viewservice.${viewContainer.id}.enablement`, enabled, StorageScope.GLOBAL);
private getOrCreateActiveViewContextKey(viewDescriptor: IViewDescriptor): IContextKey<boolean> {
const activeContextKeyId = `${viewDescriptor.id}.active`;
let contextKey = this.activeViewContextKeys.get(activeContextKeyId);
if (!contextKey) {
contextKey = new RawContextKey(activeContextKeyId, false).bindTo(this.contextKeyService);
this.activeViewContextKeys.set(activeContextKeyId, contextKey);
}
return contextKey;
}
}
}

View File

@@ -3,10 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TPromise } from 'vs/base/common/winjs.base';
import * as errors from 'vs/base/common/errors';
import * as DOM from 'vs/base/browser/dom';
import { Scope } from 'vs/workbench/common/memento';
import { dispose, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IAction } from 'vs/base/common/actions';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
@@ -18,7 +15,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { PanelViewlet, ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
@@ -44,12 +41,11 @@ export abstract class TreeViewsViewletPanel extends ViewletPanel {
}
}
setVisible(visible: boolean): TPromise<void> {
setVisible(visible: boolean): void {
if (this.isVisible() !== visible) {
return super.setVisible(visible)
.then(() => this.updateTreeVisibility(this.tree, visible && this.isExpanded()));
super.setVisible(visible);
this.updateTreeVisibility(this.tree, visible && this.isExpanded());
}
return TPromise.wrap(null);
}
focus(): void {
@@ -89,7 +85,7 @@ export abstract class TreeViewsViewletPanel extends ViewletPanel {
// Make sure the current selected element is revealed
const selectedElement = this.tree.getSelection()[0];
if (selectedElement) {
this.tree.reveal(selectedElement, 0.5).done(null, errors.onUnexpectedError);
this.tree.reveal(selectedElement);
}
// Pass Focus to Viewer
@@ -105,12 +101,12 @@ export abstract class TreeViewsViewletPanel extends ViewletPanel {
}
export interface IViewletViewOptions extends IViewletPanelOptions {
viewletSettings: object;
viewletState: object;
}
export abstract class ViewContainerViewlet extends PanelViewlet implements IViewsViewlet {
private readonly viewletSettings: Object;
private readonly viewletState: object;
private didLayout = false;
private dimension: DOM.Dimension;
private areExtensionsReady: boolean = false;
@@ -124,6 +120,7 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView
id: string,
viewletStateStorageId: string,
showHeaderInTitleWhenSingleView: boolean,
@IConfigurationService configurationService: IConfigurationService,
@IPartService partService: IPartService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService protected storageService: IStorageService,
@@ -133,42 +130,41 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView
@IExtensionService protected extensionService: IExtensionService,
@IWorkspaceContextService protected contextService: IWorkspaceContextService
) {
super(id, { showHeaderInTitleWhenSingleView, dnd: new DefaultPanelDndController() }, partService, contextMenuService, telemetryService, themeService);
super(id, { showHeaderInTitleWhenSingleView, dnd: new DefaultPanelDndController() }, configurationService, partService, contextMenuService, telemetryService, themeService, storageService);
const container = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).get(id);
this.viewsModel = this._register(this.instantiationService.createInstance(PersistentContributableViewsModel, container, viewletStateStorageId));
this.viewletSettings = this.getMemento(storageService, Scope.WORKSPACE);
this.viewletState = this.getMemento(StorageScope.WORKSPACE);
this.visibleViewsStorageId = `${id}.numberOfVisibleViews`;
this.visibleViewsCountFromCache = this.storageService.getInteger(this.visibleViewsStorageId, this.contextService.getWorkbenchState() === WorkbenchState.EMPTY ? StorageScope.GLOBAL : StorageScope.WORKSPACE, 0);
this.visibleViewsCountFromCache = this.storageService.getInteger(this.visibleViewsStorageId, StorageScope.WORKSPACE, 1);
this._register(toDisposable(() => this.viewDisposables = dispose(this.viewDisposables)));
}
create(parent: HTMLElement): TPromise<void> {
return super.create(parent).then(() => {
this._register(this.onDidSashChange(() => this.saveViewSizes()));
this.viewsModel.onDidAdd(added => this.onDidAddViews(added));
this.viewsModel.onDidRemove(removed => this.onDidRemoveViews(removed));
const addedViews: IAddedViewDescriptorRef[] = this.viewsModel.visibleViewDescriptors.map((viewDescriptor, index) => {
const size = this.viewsModel.getSize(viewDescriptor.id);
const collapsed = this.viewsModel.isCollapsed(viewDescriptor.id);
return ({ viewDescriptor, index, size, collapsed });
});
if (addedViews.length) {
this.onDidAddViews(addedViews);
}
// Update headers after and title contributed views after available, since we read from cache in the beginning to know if the viewlet has single view or not. Ref #29609
this.extensionService.whenInstalledExtensionsRegistered().then(() => {
this.areExtensionsReady = true;
if (this.panels.length) {
this.updateTitleArea();
this.updateViewHeaders();
}
});
this.focus();
create(parent: HTMLElement): void {
super.create(parent);
this._register(this.onDidSashChange(() => this.saveViewSizes()));
this.viewsModel.onDidAdd(added => this.onDidAddViews(added));
this.viewsModel.onDidRemove(removed => this.onDidRemoveViews(removed));
const addedViews: IAddedViewDescriptorRef[] = this.viewsModel.visibleViewDescriptors.map((viewDescriptor, index) => {
const size = this.viewsModel.getSize(viewDescriptor.id);
const collapsed = this.viewsModel.isCollapsed(viewDescriptor.id);
return ({ viewDescriptor, index, size, collapsed });
});
if (addedViews.length) {
this.onDidAddViews(addedViews);
}
// Update headers after and title contributed views after available, since we read from cache in the beginning to know if the viewlet has single view or not. Ref #29609
this.extensionService.whenInstalledExtensionsRegistered().then(() => {
this.areExtensionsReady = true;
if (this.panels.length) {
this.updateTitleArea();
this.updateViewHeaders();
}
});
this.focus();
}
getContextMenuActions(): IAction[] {
@@ -191,14 +187,13 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView
return result;
}
setVisible(visible: boolean): TPromise<void> {
return super.setVisible(visible)
.then(() => TPromise.join(this.panels.filter(view => view.isVisible() !== visible)
.map((view) => view.setVisible(visible))))
.then(() => void 0);
setVisible(visible: boolean): void {
super.setVisible(visible);
this.panels.filter(view => view.isVisible() !== visible)
.map((view) => view.setVisible(visible));
}
openView(id: string, focus?: boolean): TPromise<IView> {
openView(id: string, focus?: boolean): IView {
if (focus) {
this.focus();
}
@@ -211,7 +206,7 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView
if (focus) {
view.focus();
}
return TPromise.as(view);
return view;
}
movePanel(from: ViewletPanel, to: ViewletPanel): void {
@@ -241,13 +236,6 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView
return optimalWidth + additionalMargin;
}
shutdown(): void {
this.panels.forEach((view) => view.shutdown());
this.storageService.store(this.visibleViewsStorageId, this.length, this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? StorageScope.WORKSPACE : StorageScope.GLOBAL);
this.viewsModel.saveViewsStates();
super.shutdown();
}
protected isSingleView(): boolean {
if (!super.isSingleView()) {
return false;
@@ -276,7 +264,7 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView
title: viewDescriptor.name,
actionRunner: this.getActionRunner(),
expanded: !collapsed,
viewletSettings: this.viewletSettings
viewletState: this.viewletState
});
panel.render();
panel.setVisible(true);
@@ -330,7 +318,7 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView
let anchor: { x: number, y: number } = { x: event.posx, y: event.posy };
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => TPromise.as(actions)
getActions: () => actions
});
}
@@ -377,12 +365,20 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView
private computeInitialSizes(): { [id: string]: number } {
let sizes = {};
if (this.dimension) {
const totalWeight = this.viewsModel.visibleViewDescriptors.reduce((totalWeight, { weight }) => totalWeight + (weight || 20), 0);
for (const viewDescriptor of this.viewsModel.visibleViewDescriptors) {
sizes[viewDescriptor.id] = this.dimension.height * (viewDescriptor.weight || 20) / 100;
sizes[viewDescriptor.id] = this.dimension.height * (viewDescriptor.weight || 20) / totalWeight;
}
}
return sizes;
}
protected saveState(): void {
this.panels.forEach((view) => view.saveState());
this.storageService.store(this.visibleViewsStorageId, this.length, StorageScope.WORKSPACE);
super.saveState();
}
}
export class FileIconThemableWorkbenchTree extends WorkbenchTree {