diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index ac7d83815a7e5def5621a2f9ef1f47d734e2bcc7..fa44d0667f33a71aa71b8b9a73d7f5b1b0d3ed87 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -26,7 +26,7 @@ export function forEach(array: T[], callback: (element: T, remove: Function) } } -export function equals(one: T[], other: T[], itemEquals: (a: T, b: T) => boolean): boolean { +export function equals(one: T[], other: T[], itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { if (one.length !== other.length) { return false; } diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index f173f88fd72fc9460a5dd70ca1ddccdf7ba7e5d9..f6e1ea325592b4ba79dee234829624901f7c1f08 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -124,7 +124,7 @@ export interface IEditorInput extends IEventEmitter { /** * Returns the display description of this input. */ - getDescription(): string; + getDescription(verbose?: boolean): string; /** * Returns if this input is dirty or not. diff --git a/src/vs/workbench/browser/actionBarRegistry.ts b/src/vs/workbench/browser/actionBarRegistry.ts index fe1b8754507fbd9a196b5da6848ba4d27d361775..87d66ae48039354279a9a5e8ba80d8997c4d4dac 100644 --- a/src/vs/workbench/browser/actionBarRegistry.ts +++ b/src/vs/workbench/browser/actionBarRegistry.ts @@ -182,6 +182,9 @@ export class ContributableActionProvider implements IActionProvider { // Helper function used in parts to massage actions before showing in action areas export function prepareActions(actions: IAction[]): IAction[] { + if (!actions.length) { + return actions; + } // Patch order if not provided for (let l = 0; l < actions.length; l++) { diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 85b00969334f7b48cddc6de4aa9e43a609dc88fe..48aac9da453a00cb2957174b853353f4f5e8cbca 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -23,7 +23,7 @@ import {Part} from 'vs/workbench/browser/part'; import {IEditorRegistry, Extensions as EditorExtensions, BaseEditor, EditorDescriptor} from 'vs/workbench/browser/parts/editor/baseEditor'; import {EditorInput, EditorOptions, TextEditorOptions, ConfirmResult, EditorInputEvent} from 'vs/workbench/common/editor'; import {BaseTextEditor} from 'vs/workbench/browser/parts/editor/textEditor'; -import {SideBySideEditorControl, Rochade, ISideBySideEditorControl, ProgressState, ITitleAreaState} from 'vs/workbench/browser/parts/editor/sideBySideEditorControl'; +import {SideBySideEditorControl, Rochade, ISideBySideEditorControl, ProgressState} from 'vs/workbench/browser/parts/editor/sideBySideEditorControl'; import {WorkbenchProgressService} from 'vs/workbench/services/progress/browser/progressService'; import {GroupArrangement} from 'vs/workbench/services/editor/common/editorService'; import {IEditorPart} from 'vs/workbench/services/editor/browser/editorService'; @@ -140,13 +140,9 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService private onEditorDirty(identifier: EditorIdentifier): void { const position = this.stacks.positionOfGroup(identifier.group); - const group = identifier.group; // we pin every editor that becomes dirty - this.pinEditor(position, identifier.editor, false /* we update the UI right after */); - - // Update UI - this.sideBySideControl.updateTitleArea({ position, preview: group.previewEditor, editorCount: group.count }); + this.pinEditor(position, identifier.editor); } private onEditorDisposed(identifier: EditorIdentifier): void { @@ -230,9 +226,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService const pinned = (options && (options.pinned || typeof options.index === 'number')) || input.isDirty(); group.openEditor(input, { active: true, pinned, index: options && options.index }); - // Set the title early enough - this.sideBySideControl.setTitleLabel(position, input, group.isPinned(input), this.stacks.isActive(group)); - // Progress Monitor & Ref Counting this.editorOpenToken[position]++; const editorOpenToken = this.editorOpenToken[position]; @@ -416,13 +409,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService this._onEditorsChanged.fire(); } - // Update Title Area - if (inputChanged) { - this.doRecreateEditorTitleArea(); // full title update - } else { - this.sideBySideControl.updateTitleArea({ position, preview: group.previewEditor, editorCount: group.count }); // little update for position - } - timerEvent.stop(); // Fullfill promise with Editor that is being used @@ -487,7 +473,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService this.doCloseActiveEditor(group, focusNext); } - // Closing inactive editor is just a model and title update + // Closing inactive editor is just a model update else { this.doCloseInactiveEditor(group, input); } @@ -514,9 +500,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Closing inactive editor is just a model update group.closeEditor(input); - - // Update UI - this.sideBySideControl.updateTitleArea({ position: this.stacks.positionOfGroup(group), preview: group.previewEditor, editorCount: group.count }); } private doCloseGroup(group: EditorGroup): void { @@ -565,9 +548,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Rochade as needed this.rochade(rochade); - // Clear Title Area for Position - this.sideBySideControl.clearTitleArea(position); - // Emit Editor move event if (rochade !== Rochade.NONE) { this._onEditorsMoved.fire(); @@ -634,9 +614,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Update stacks model: close non active editors supporting the direction group.closeEditors(group.activeEditor, direction); - - // Update UI - this.sideBySideControl.updateTitleArea({ position: this.stacks.positionOfGroup(group), preview: group.previewEditor, editorCount: group.count }); } // Finally: we are asked to close editors around a non-active editor @@ -729,9 +706,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Restore focus this.focusGroup(this.stacks.positionOfGroup(fromGroup)); - // Update all title areas - this.doRecreateEditorTitleArea(); - // Events this._onEditorsMoved.fire(); } @@ -768,9 +742,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Update stacks model group.moveEditor(input, toIndex); group.pin(input); - - // Update UI - this.doRecreateEditorTitleArea(); } private doMoveEditorAcrossGroups(input: EditorInput, from: EditorGroup, to: EditorGroup, index?: number): void { @@ -797,7 +768,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService this.sideBySideControl = this.instantiationService.createInstance(SideBySideEditorControl, contentArea); this.toUnbind.push(this.sideBySideControl.onGroupFocusChanged(() => this.onGroupFocusChanged())); - this.toUnbind.push(this.sideBySideControl.onEditorTitleDoubleclick((position) => this.onEditorTitleDoubleclick(position))); // get settings this.memento = this.getMemento(this.storageService, MementoScope.WORKSPACE); @@ -818,16 +788,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService if (activeEditor) { this._onEditorsChanged.fire(); } - - // Update Title Area - this.doRecreateEditorTitleArea(); - } - - private onEditorTitleDoubleclick(position: Position): void { - const group = this.stacks.groupAt(position); - if (group) { - this.pinEditor(position, group.activeEditor); - } } public replaceEditors(editors: { toReplace: EditorInput, replaceWith: EditorInput, options?: EditorOptions }[]): TPromise { @@ -1031,7 +991,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService } } - public pinEditor(position: Position, input: EditorInput, updateTitleArea = true): void { + public pinEditor(position: Position, input: EditorInput): void { const group = this.stacks.groupAt(position); if (group) { if (group.isPinned(input)) { @@ -1040,11 +1000,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Update stacks model group.pin(input); - - // Update UI - if (updateTitleArea) { - this.sideBySideControl.updateTitleArea({ position, preview: group.previewEditor, editorCount: group.count }); - } } } @@ -1079,25 +1034,10 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Update stacks model group.unpin(input); - - // Update UI - this.sideBySideControl.updateTitleArea({ position, preview: group.previewEditor, editorCount: group.count }); }); } } - private doRecreateEditorTitleArea(): void { - const titleAreaState: ITitleAreaState[] = this.stacks.groups.map((group, index) => { - return { - position: this.stacks.positionOfGroup(group), - preview: group.previewEditor, - editorCount: group.count - }; - }); - - this.sideBySideControl.recreateTitleArea(titleAreaState); - } - public layout(dimension: Dimension): Dimension[] { // Pass to super diff --git a/src/vs/workbench/browser/parts/editor/media/notabstitle.css b/src/vs/workbench/browser/parts/editor/media/notabstitle.css index 265455ddbeb8019cd0e50bfc21e33ee09c6c8bfe..c563ce2f1e6a3674a0d0dc665e58722db1fe72ec 100644 --- a/src/vs/workbench/browser/parts/editor/media/notabstitle.css +++ b/src/vs/workbench/browser/parts/editor/media/notabstitle.css @@ -57,7 +57,7 @@ color: #333333; } -.vs .monaco-workbench > .part.editor > .content .inactive .title .title-label a { +.vs .monaco-workbench > .part.editor > .content > .one-editor-container .title.inactive .title-label a { color: rgba(51, 51, 51, 0.5); } @@ -65,7 +65,7 @@ color: white; } -.vs-dark .monaco-workbench > .part.editor > .content .inactive .title .title-label a { +.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-container .title.inactive .title-label a { color: rgba(255, 255, 255, 0.5); } @@ -91,7 +91,7 @@ flex: initial; } -.monaco-workbench > .part.editor > .content .inactive .title .title-actions { +.monaco-workbench > .part.editor > .content > .one-editor-container .title.inactive .title-actions { opacity: 0.5; } diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts new file mode 100644 index 0000000000000000000000000000000000000000..eca7e5a497d80e81508482bd4be88c80c169099f --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -0,0 +1,237 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./media/notabstitle'; +import {prepareActions} from 'vs/workbench/browser/actionBarRegistry'; +import errors = require('vs/base/common/errors'); +import arrays = require('vs/base/common/arrays'); +import {Builder, $} from 'vs/base/browser/builder'; +import {IEditorGroup} from 'vs/workbench/common/editor'; +import DOM = require('vs/base/browser/dom'); +import {ToolBar} from 'vs/base/browser/ui/toolbar/toolbar'; +import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; +import {IContextMenuService} from 'vs/platform/contextview/browser/contextView'; +import {IEditorGroupService} from 'vs/workbench/services/group/common/groupService'; +import {IMessageService} from 'vs/platform/message/common/message'; +import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry'; +import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; +import {IKeybindingService} from 'vs/platform/keybinding/common/keybindingService'; +import {TitleControl} from 'vs/workbench/browser/parts/editor/titleControl'; + +export class NoTabsTitleControl extends TitleControl { + private titleContainer: Builder; + private titleLabel: Builder; + private titleDecoration: Builder; + private titleDescription: Builder; + + private groupActionsToolbar: ToolBar; + private editorActionsToolbar: ToolBar; + + private currentPrimaryEditorActionIds: string[]; + private currentSecondaryEditorActionIds: string[]; + private currentPrimaryGroupActionIds: string[]; + private currentSecondaryGroupActionIds: string[]; + + constructor( + @IContextMenuService contextMenuService: IContextMenuService, + @IInstantiationService instantiationService: IInstantiationService, + @IWorkbenchEditorService editorService: IWorkbenchEditorService, + @IEditorGroupService editorGroupService: IEditorGroupService, + @IKeybindingService keybindingService: IKeybindingService, + @ITelemetryService telemetryService: ITelemetryService, + @IMessageService messageService: IMessageService + ) { + super(contextMenuService, instantiationService, editorService, editorGroupService, keybindingService, telemetryService, messageService); + + this.currentPrimaryEditorActionIds = []; + this.currentSecondaryEditorActionIds = []; + this.currentPrimaryGroupActionIds = []; + this.currentSecondaryGroupActionIds = []; + } + + public setContext(group: IEditorGroup): void { + super.setContext(group); + + this.editorActionsToolbar.context = { group }; + this.groupActionsToolbar.context = { group }; + } + + public create(parent: Builder): void { + this.titleContainer = $(parent); + + // Pin on double click + parent.on(DOM.EventType.DBLCLICK, (e: MouseEvent) => { + DOM.EventHelper.stop(e); + + this.onTitleDoubleClick(); + }); + + // Detect mouse click + parent.on(DOM.EventType.MOUSE_UP, (e: MouseEvent) => { + DOM.EventHelper.stop(e, false); + + this.onTitleClick(e); + }); + + // Left Title Decoration + parent.div({ + 'class': 'title-decoration' + }, (div) => { + this.titleDecoration = div; + }); + + // Left Title Label & Description + parent.div({ + 'class': 'title-label' + }, (div) => { + + // Label + this.titleLabel = $(div).a(); + + // Description + this.titleDescription = $(div).span(); + }); + + // Right Actions Container + parent.div({ + 'class': 'title-actions' + }, (div) => { + + // Editor actions + this.editorActionsToolbar = this.doCreateToolbar(div); + + // Group actions + this.groupActionsToolbar = this.doCreateToolbar(div); + this.groupActionsToolbar.getContainer().addClass('editor-group-toolbar'); + }); + } + + private onTitleDoubleClick(): void { + if (!this.context) { + return; + } + + const group = this.context; + const position = this.stacks.positionOfGroup(group); + + this.editorGroupService.pinEditor(position, group.activeEditor); + } + + private onTitleClick(e: MouseEvent): void { + if (!this.context) { + return; + } + + const group = this.context; + const position = this.stacks.positionOfGroup(group); + + // Close editor on middle mouse click + if (e.button === 1 /* Middle Button */) { + this.editorService.closeEditor(position, group.activeEditor).done(null, errors.onUnexpectedError); + } + + // Focus editor group unless click on toolbar + else if (this.stacks.groups.length === 1 && !this.targetInToolbar(e.target || e.srcElement)) { + this.editorGroupService.focusGroup(position); + } + } + + private targetInToolbar(target: HTMLElement): boolean { + return DOM.isAncestor(target, this.editorActionsToolbar.getContainer().getHTMLElement()) || DOM.isAncestor(target, this.groupActionsToolbar.getContainer().getHTMLElement()); + } + + protected redraw(): void { + if (!this.context) { + return; + } + + const group = this.context; + const editor = group.activeEditor; + if (!editor) { + this.editorActionsToolbar.setActions([], [])(); + this.groupActionsToolbar.setActions([], [])(); + + this.currentPrimaryEditorActionIds = []; + this.currentSecondaryEditorActionIds = []; + this.currentPrimaryGroupActionIds = []; + this.currentSecondaryGroupActionIds = []; + + return; // return early if we are being closed + } + + const isPinned = group.isPinned(group.activeEditor); + const isActive = this.stacks.isActive(group); + + // Pinned state + if (isPinned) { + this.titleContainer.addClass('pinned'); + } else { + this.titleContainer.removeClass('pinned'); + } + + // Activity state + if (isActive) { + this.titleContainer.removeClass('inactive'); + } else { + this.titleContainer.addClass('inactive'); + } + + // Editor Title + let name = editor.getName() || ''; + let description = isActive ? (editor.getDescription() || '') : ''; + let verboseDescription = editor.getDescription(true) || ''; + if (description === verboseDescription) { + verboseDescription = ''; // dont repeat what is already shown + } + + this.titleLabel.safeInnerHtml(name); + this.titleLabel.title(verboseDescription); + + this.titleDescription.safeInnerHtml(description); + this.titleDescription.title(verboseDescription); + + // Editor Decoration + if (editor.isDirty()) { + this.titleDecoration.addClass('dirty'); + } else { + this.titleDecoration.removeClass('dirty'); + } + + // Update Editor Actions Toolbar + const editorActions = this.getEditorActions(group); + const primaryEditorActions = prepareActions(editorActions.primary); + const secondaryEditorActions = prepareActions(editorActions.secondary); + const primaryEditorActionIds = primaryEditorActions.map(a => a.id); + const secondaryEditorActionIds = secondaryEditorActions.map(a => a.id); + + if (!arrays.equals(primaryEditorActionIds, this.currentPrimaryEditorActionIds) || !arrays.equals(secondaryEditorActionIds, this.currentSecondaryEditorActionIds)) { + this.editorActionsToolbar.setActions(primaryEditorActions, secondaryEditorActions)(); + this.editorActionsToolbar.addPrimaryAction(this.closeEditorAction)(); + this.currentPrimaryEditorActionIds = primaryEditorActionIds; + this.currentSecondaryEditorActionIds = secondaryEditorActionIds; + } + + // Update Group Actions Toolbar + const groupActions = this.getGroupActions(group); + const primaryGroupActionIds = groupActions.primary.map(a => a.id); + const secondaryGroupActionIds = groupActions.secondary.map(a => a.id); + + if (!arrays.equals(primaryGroupActionIds, this.currentPrimaryGroupActionIds) || !arrays.equals(secondaryGroupActionIds, this.currentSecondaryGroupActionIds)) { + this.groupActionsToolbar.setActions(groupActions.primary, groupActions.secondary)(); + this.currentPrimaryGroupActionIds = primaryGroupActionIds; + this.currentSecondaryGroupActionIds = secondaryGroupActionIds; + } + } + + public dispose(): void { + super.dispose(); + + // Toolbars + this.groupActionsToolbar.dispose(); + this.editorActionsToolbar.dispose(); + } +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts index 7591a3561dfb08c96a30707833748c633e57901b..af5916fa1fab2c5f45d869db0b17cd90f24cef17 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts @@ -6,38 +6,29 @@ 'use strict'; import 'vs/css!./media/sidebyside'; -import 'vs/css!./media/notabstitle'; -import nls = require('vs/nls'); -import {Registry} from 'vs/platform/platform'; -import {Scope, IActionBarRegistry, Extensions, prepareActions} from 'vs/workbench/browser/actionBarRegistry'; -import {IAction, Action} from 'vs/base/common/actions'; import arrays = require('vs/base/common/arrays'); import Event, {Emitter} from 'vs/base/common/event'; import {StandardMouseEvent} from 'vs/base/browser/mouseEvent'; -import errors = require('vs/base/common/errors'); import {isWindows} from 'vs/base/common/platform'; import types = require('vs/base/common/types'); import {Dimension, Builder, $} from 'vs/base/browser/builder'; import {Sash, ISashEvent, IVerticalSashLayoutProvider} from 'vs/base/browser/ui/sash/sash'; import {ProgressBar} from 'vs/base/browser/ui/progressbar/progressbar'; -import {BaseEditor, IEditorInputActionContext} from 'vs/workbench/browser/parts/editor/baseEditor'; -import {EditorInput} from 'vs/workbench/common/editor'; -import {EventType as BaseEventType} from 'vs/base/common/events'; +import {BaseEditor} from 'vs/workbench/browser/parts/editor/baseEditor'; import DOM = require('vs/base/browser/dom'); -import {IActionItem, ActionsOrientation, Separator} from 'vs/base/browser/ui/actionbar/actionbar'; -import {ToolBar} from 'vs/base/browser/ui/toolbar/toolbar'; import {IWorkbenchEditorService, GroupArrangement} from 'vs/workbench/services/editor/common/editorService'; import {IContextMenuService} from 'vs/platform/contextview/browser/contextView'; import {Position, POSITIONS} from 'vs/platform/editor/common/editor'; import {IEditorGroupService} from 'vs/workbench/services/group/common/groupService'; import {IEventService} from 'vs/platform/event/common/event'; -import {IMessageService, Severity} from 'vs/platform/message/common/message'; -import {QuickOpenAction} from 'vs/workbench/browser/quickopen'; +import {IMessageService} from 'vs/platform/message/common/message'; import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; import {IKeybindingService} from 'vs/platform/keybinding/common/keybindingService'; -import {ShowEditorsInLeftGroupAction, ShowAllEditorsAction, ShowEditorsInCenterGroupAction, ShowEditorsInRightGroupAction, CloseEditorsInGroupAction, MoveGroupLeftAction, MoveGroupRightAction, SplitEditorAction, CloseEditorAction} from 'vs/workbench/browser/parts/editor/editorActions'; import {IDisposable, dispose} from 'vs/base/common/lifecycle'; +import {NoTabsTitleControl} from 'vs/workbench/browser/parts/editor/noTabsTitleControl'; +import {IEditorStacksModel} from 'vs/workbench/common/editor'; +import {ITitleAreaControl} from 'vs/workbench/browser/parts/editor/titleControl'; export enum Rochade { NONE, @@ -52,21 +43,9 @@ export enum ProgressState { STOP } -interface IEditorActions { - primary: IAction[]; - secondary: IAction[]; -} - -export interface ITitleAreaState { - editorCount: number; - position: number; - preview: EditorInput; -} - export interface ISideBySideEditorControl { onGroupFocusChanged: Event; - onEditorTitleDoubleclick: Event; show(editor: BaseEditor, container: Builder, position: Position, preserveActive: boolean, widthRatios?: number[]): void; hide(editor: BaseEditor, container: Builder, position: Position, layoutAndRochade: boolean): Rochade; @@ -86,11 +65,6 @@ export interface ISideBySideEditorControl { layout(dimension: Dimension): void; layout(position: Position): void; - recreateTitleArea(states: ITitleAreaState[]): void; - updateTitleArea(state: ITitleAreaState): void; - clearTitleArea(position: Position): void; - setTitleLabel(position: Position, input: EditorInput, isPinned: boolean, isActive: boolean): void; - arrangeGroups(arrangement: GroupArrangement): void; getWidthRatios(): number[]; @@ -106,6 +80,8 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti private static EDITOR_TITLE_HEIGHT = 35; private static SNAP_TO_MINIMIZED_THRESHOLD = 50; + private stacks: IEditorStacksModel; + private parent: Builder; private dimension: Dimension; private dragging: boolean; @@ -115,24 +91,9 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti private containerInitialRatios: number[]; private titleContainer: Builder[]; - private titleLabel: Builder[]; - private titleDecoration: Builder[]; - private titleDescription: Builder[]; + private titleAreaControl: ITitleAreaControl[]; private progressBar: ProgressBar[]; - private groupActionsToolbar: ToolBar[]; - private editorActionsToolbar: ToolBar[]; - - private mapActionsToEditors: { [editorId: string]: IEditorActions; }[]; - - private closeEditorActions: CloseEditorAction[]; - private showEditorsOfGroup: QuickOpenAction[]; - private moveGroupLeftActions: MoveGroupLeftAction[]; - private moveGroupRightActions: MoveGroupRightAction[]; - private closeEditorsInGroupActions: CloseEditorsInGroupAction[]; - private splitEditorAction: SplitEditorAction; - private showAllEditorsAction: ShowAllEditorsAction; - private leftSash: Sash; private startLeftContainerWidth: number; @@ -148,7 +109,6 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti private visibleEditorFocusTrackers: DOM.IFocusTracker[]; private _onGroupFocusChanged: Emitter; - private _onEditorTitleDoubleclick: Emitter; private toDispose: IDisposable[]; @@ -163,6 +123,8 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti @IKeybindingService private keybindingService: IKeybindingService, @IInstantiationService private instantiationService: IInstantiationService ) { + this.stacks = editorGroupService.getStacksModel(); + this.parent = parent; this.dimension = new Dimension(0, 0); @@ -170,69 +132,36 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti this.containerWidth = []; this.titleContainer = []; - this.titleLabel = []; - this.titleDecoration = []; - this.titleDescription = []; - this.groupActionsToolbar = []; - this.editorActionsToolbar = []; + this.titleAreaControl = []; + this.progressBar = []; this.visibleEditors = []; this.visibleEditorContainers = []; this.visibleEditorFocusTrackers = []; - this.mapActionsToEditors = arrays.fill(POSITIONS.length, () => Object.create(null)); - this._onGroupFocusChanged = new Emitter(); - this._onEditorTitleDoubleclick = new Emitter(); this.toDispose = []; - this.initActions(); this.create(this.parent); + this.registerListeners(); } - private initActions(): void { - - // Close - this.closeEditorActions = POSITIONS.map((position) => this.instantiationService.createInstance(CloseEditorAction, CloseEditorAction.ID, nls.localize('close', "Close"))); - - // Show All Editors - this.showAllEditorsAction = this.instantiationService.createInstance(ShowAllEditorsAction, ShowAllEditorsAction.ID, nls.localize('showEditors', "Show Editors")); + private registerListeners(): void { + this.toDispose.push(this.stacks.onModelChanged(() => this.onStacksChanged())); + } - // Show Editors of Group - this.showEditorsOfGroup = POSITIONS.map((position) => { - switch (position) { - case Position.LEFT: - return this.instantiationService.createInstance(ShowEditorsInLeftGroupAction, ShowEditorsInLeftGroupAction.ID, nls.localize('showEditors', "Show Editors")); - case Position.CENTER: - return this.instantiationService.createInstance(ShowEditorsInCenterGroupAction, ShowEditorsInCenterGroupAction.ID, nls.localize('showEditors', "Show Editors")); - default: - return this.instantiationService.createInstance(ShowEditorsInRightGroupAction, ShowEditorsInRightGroupAction.ID, nls.localize('showEditors', "Show Editors")); - } + private onStacksChanged(): void { + POSITIONS.forEach(position => { + this.titleAreaControl[position].setContext(this.stacks.groupAt(position)); }); - - // Split - this.splitEditorAction = this.instantiationService.createInstance(SplitEditorAction, SplitEditorAction.ID, SplitEditorAction.LABEL); - - // Move Group Left - this.moveGroupLeftActions = POSITIONS.map((position) => this.instantiationService.createInstance(MoveGroupLeftAction, MoveGroupLeftAction.ID, nls.localize('moveLeft', "Move Left"))); - - // Move Group Right - this.moveGroupRightActions = POSITIONS.map((position) => this.instantiationService.createInstance(MoveGroupRightAction, MoveGroupRightAction.ID, nls.localize('moveRight', "Move Right"))); - - // Close All Editors in Group - this.closeEditorsInGroupActions = POSITIONS.map((position) => this.instantiationService.createInstance(CloseEditorsInGroupAction, CloseEditorsInGroupAction.ID, nls.localize('closeAll', "Close All"))); } public get onGroupFocusChanged(): Event { return this._onGroupFocusChanged.event; } - public get onEditorTitleDoubleclick(): Event { - return this._onEditorTitleDoubleclick.event; - } - public show(editor: BaseEditor, container: Builder, position: Position, preserveActive: boolean, widthRatios?: number[]): void { let visibleEditorCount = this.getVisibleEditorCount(); @@ -401,7 +330,7 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti if (!types.isUndefinedOrNull(this.lastActivePosition) && this.containerWidth[this.lastActivePosition] === SideBySideEditorControl.MIN_EDITOR_WIDTH) { let candidate: Position = null; let currentWidth = SideBySideEditorControl.MIN_EDITOR_WIDTH; - POSITIONS.forEach((position) => { + POSITIONS.forEach(position => { // Skip current active position and check if the editor is larger than min width if (position !== this.lastActivePosition) { @@ -464,7 +393,6 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti if (position === Position.LEFT) { this.rochade(Position.CENTER, Position.LEFT); result = Rochade.CENTER_TO_LEFT; - this.clearTitleArea(Position.CENTER); // center closes so clear title } this.layoutContainers(); @@ -483,7 +411,6 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti if (position === Position.CENTER) { this.rochade(Position.RIGHT, Position.CENTER); result = Rochade.RIGHT_TO_CENTER; - this.clearTitleArea(Position.RIGHT); // right closes so clear title } // Move RIGHT to CENTER and CENTER to LEFT ([x]|[]|[] -> [ ]|[ ]) @@ -491,7 +418,6 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti this.rochade(Position.CENTER, Position.LEFT); this.rochade(Position.RIGHT, Position.CENTER); result = Rochade.CENTER_AND_RIGHT_TO_LEFT; - this.clearTitleArea(Position.RIGHT); // right closes so clear title } this.layoutContainers(); @@ -541,19 +467,8 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti } private doSetActive(editor: BaseEditor, newActive: Position): void { - let oldActive = this.lastActivePosition; this.lastActivePosition = newActive; this.lastActiveEditor = editor; - - if (!types.isUndefinedOrNull(oldActive)) { - this.containers[oldActive].addClass('inactive'); - this.containers[oldActive].removeClass('active'); - } - - if (!types.isUndefinedOrNull(newActive)) { - this.containers[newActive].removeClass('inactive'); - this.containers[newActive].addClass('active'); - } } private clearPosition(position: Position): void { @@ -589,10 +504,6 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti this.visibleEditors[to] = editor; this.visibleEditors[from] = null; - let actions = this.mapActionsToEditors[from]; - this.mapActionsToEditors[to] = actions; - this.mapActionsToEditors[from] = Object.create(null); - // Update last active position if (this.lastActivePosition === from) { this.doSetActive(this.lastActiveEditor, to); @@ -681,7 +592,6 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti arrays.move(this.visibleEditors, from, to); arrays.move(this.visibleEditorFocusTrackers, from, to); arrays.move(this.containerWidth, from, to); - arrays.move(this.mapActionsToEditors, from, to); // Layout if (!this.leftSash.isHidden()) { @@ -709,7 +619,7 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti // Minimize Others if (arrangement === GroupArrangement.MINIMIZE_OTHERS) { - POSITIONS.forEach((position) => { + POSITIONS.forEach(position => { if (this.visibleEditors[position]) { if (position !== this.lastActivePosition) { this.containerWidth[position] = SideBySideEditorControl.MIN_EDITOR_WIDTH; @@ -723,7 +633,7 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti // Even Widths else if (arrangement === GroupArrangement.EVEN_WIDTH) { - POSITIONS.forEach((position) => { + POSITIONS.forEach(position => { if (this.visibleEditors[position]) { this.containerWidth[position] = availableWidth / visibleEditors; } @@ -739,7 +649,7 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti if (this.dimension) { let fullWidth = this.dimension.width; - POSITIONS.forEach((position) => { + POSITIONS.forEach(position => { if (this.visibleEditors[position]) { ratio.push(this.containerWidth[position] / fullWidth); } @@ -785,13 +695,17 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti this.containers[Position.RIGHT] = $(parent).div({ class: 'one-editor-container editor-right monaco-editor-background' }); // Title containers - POSITIONS.forEach((position) => { + POSITIONS.forEach(position => { this.titleContainer[position] = $(this.containers[position]).div({ 'class': 'title' }); - this.fillTitleArea($(this.titleContainer[position]), position); + this.hookTitleDragListener(position); + + this.titleAreaControl[position] = this.instantiationService.createInstance(NoTabsTitleControl); + this.titleAreaControl[position].create($(this.titleContainer[position])); + this.titleAreaControl[position].setContext(this.stacks.groupAt(position)); }); // Progress Bars per position - POSITIONS.forEach((position) => { + POSITIONS.forEach(position => { this.progressBar[position] = new ProgressBar($(this.containers[position])); this.progressBar[position].getContainer().hide(); }); @@ -802,24 +716,17 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti } } - private fillTitleArea(parent: Builder, position: Position): void { + private hookTitleDragListener(position: Position): void { let wasDragged = false; - // Detect double click and emit - parent.on(DOM.EventType.DBLCLICK, (e: MouseEvent) => { - DOM.EventHelper.stop(e); - - this._onEditorTitleDoubleclick.fire(position); - }); - // Allow to reorder positions by dragging the title - parent.on(DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => { + this.titleContainer[position].on(DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => { // Reset flag wasDragged = false; // Return early if there is only one editor active or the user clicked into the toolbar - if (this.getVisibleEditorCount() <= 1 || this.targetInToolbar(e.target || e.srcElement, position)) { + if (this.getVisibleEditorCount() <= 1 || !!DOM.findParentWithClass((e.target || e.srcElement), 'monaco-action-bar', 'one-editor-container')) { return; } @@ -998,139 +905,6 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti $window.off('mousemove'); }); }); - - // Close editor on middle mouse click - parent.on(DOM.EventType.MOUSE_UP, (e: MouseEvent) => { - DOM.EventHelper.stop(e, false); - - // Close editor on middle mouse click - if (e.button === 1 /* Middle Button */) { - this.editorService.closeEditor(position, this.visibleEditors[position].input).done(null, errors.onUnexpectedError); - } - - // Focus editor group unless click on toolbar - else if (this.getVisibleEditorCount() === 1 && !this.targetInToolbar(e.target || e.srcElement, position)) { - this.editorGroupService.focusGroup(position); - } - }); - - // Left Title Decoration - parent.div({ - 'class': 'title-decoration' - }, (div) => { - this.titleDecoration[position] = div; - }); - - // Left Title Label - parent.div({ - 'class': 'title-label' - }, (div) => { - - // Label - this.titleLabel[position] = $(div).a(); - - // Subtle Description - this.titleDescription[position] = $(div).span(); - }); - - // Right Actions Container - parent.div({ - 'class': 'title-actions' - }, (div) => { - const group = this.editorGroupService.getStacksModel().groupAt(position); - - // Editor actions - this.editorActionsToolbar[position] = this.doCreateToolbar(div, position); - this.editorActionsToolbar[position].context = { group }; - this.editorActionsToolbar[position].setActions([this.closeEditorActions[position]])(); - - // Group actions - this.groupActionsToolbar[position] = this.doCreateToolbar(div, position); - this.groupActionsToolbar[position].context = { group }; - this.groupActionsToolbar[position].getContainer().addClass('editor-group-toolbar'); - this.groupActionsToolbar[position].setActions(this.getPrimaryGroupActions(position), this.getSecondaryGroupActions(position))(); - }); - } - - private getPrimaryGroupActions(position: Position, groupCount?: number, canSplit?: boolean, isOverflowing?: boolean): Action[] { - const actions: Action[] = []; - - if (isOverflowing) { - let actionPosition = position; - if (groupCount === 2 && position === Position.CENTER) { - actionPosition = Position.RIGHT; // with 2 groups, CENTER === RIGHT - } - - let overflowAction: Action; - if (groupCount === 1) { - overflowAction = this.showAllEditorsAction; - } else { - overflowAction = this.showEditorsOfGroup[actionPosition]; - } - - overflowAction.class = 'show-group-editors-action'; - overflowAction.enabled = true; - actions.push(overflowAction); - } - - if (canSplit) { - actions.push(this.splitEditorAction); - } - - return actions; - } - - private getSecondaryGroupActions(position: Position): Action[] { - - // Make sure enablement is good - this.moveGroupLeftActions[Position.LEFT].enabled = false; - this.moveGroupRightActions[Position.LEFT].enabled = this.getVisibleEditorCount() > 1; - this.moveGroupRightActions[Position.CENTER].enabled = this.getVisibleEditorCount() > 2; - this.moveGroupRightActions[Position.RIGHT].enabled = false; - - // Return actions - return [ - this.moveGroupLeftActions[position], - this.moveGroupRightActions[position], - new Separator(), - this.closeEditorsInGroupActions[position] - ]; - } - - private targetInToolbar(target: HTMLElement, position: Position): boolean { - return DOM.isAncestor(target, this.editorActionsToolbar[position].getContainer().getHTMLElement()) || DOM.isAncestor(target, this.groupActionsToolbar[position].getContainer().getHTMLElement()); - } - - private doCreateToolbar(container: Builder, position: Position): ToolBar { - const toolbar = new ToolBar(container.getHTMLElement(), this.contextMenuService, { - actionItemProvider: (action: Action) => this.actionItemProvider(action, position), - orientation: ActionsOrientation.HORIZONTAL, - ariaLabel: nls.localize('araLabelEditorActions', "Editor actions"), - getKeyBinding: (action) => { - const opts = this.keybindingService.lookupKeybindings(action.id); - if (opts.length > 0) { - return opts[0]; // only take the first one - } - - return null; - } - }); - - // Action Run Handling - this.toDispose.push(toolbar.actionRunner.addListener2(BaseEventType.RUN, (e: any) => { - - // Check for Error - if (e.error && !errors.isPromiseCanceledError(e.error)) { - this.messageService.show(Severity.Error, e.error); - } - - // Log in telemetry - if (this.telemetryService) { - this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'editorPart' }); - } - })); - - return toolbar; } private findMoveTarget(position: Position, diffX: number): Position { @@ -1199,189 +973,6 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti return null; } - private actionItemProvider(action: Action, position: Position): IActionItem { - let actionItem: IActionItem; - - // Check Active Editor - let editor = this.visibleEditors[position]; - if (editor) { - actionItem = editor.getActionItem(action); - } - - // Check Registry - if (!actionItem) { - let actionBarRegistry = Registry.as(Extensions.Actionbar); - actionItem = actionBarRegistry.getActionItemForContext(Scope.EDITOR, { input: editor && editor.input, editor: editor, position: position }, action); - } - - return actionItem; - } - - public updateTitleArea(state: ITitleAreaState): void { - let editor = this.visibleEditors[state.position]; - let input = editor ? editor.input : null; - - if (input && editor) { - - // Dirty - if (input.isDirty()) { - this.titleDecoration[state.position].addClass('dirty'); - } else { - this.titleDecoration[state.position].removeClass('dirty'); - } - - // Pinned - const isPinned = !input.matches(state.preview); - if (isPinned) { - this.titleContainer[state.position].addClass('pinned'); - } else { - this.titleContainer[state.position].removeClass('pinned'); - } - - // Overflow - const isOverflowing = state.editorCount > 1; - const actions = [this.showEditorsOfGroup[state.position], this.showAllEditorsAction]; - if (!isOverflowing) { - actions.forEach(a => a.enabled = false); - } else { - actions.forEach(a => a.enabled = true); - } - } - } - - public recreateTitleArea(states: ITitleAreaState[]): void { - const activePosition = this.lastActivePosition; - - states.forEach(state => { - const group = this.editorGroupService.getStacksModel().groupAt(state.position); - this.groupActionsToolbar[state.position].context = { group }; - this.editorActionsToolbar[state.position].context = { group }; - - let editor = this.visibleEditors[state.position]; - let input = editor ? editor.input : null; - - if (input && editor) { - this.doUpdateEditorTitleArea(editor, input, state.position, !input.matches(state.preview), activePosition === state.position, state.editorCount > 1, states.length); - } - }); - } - - private doUpdateEditorTitleArea(editor: BaseEditor, input: EditorInput, position: Position, isPinned: boolean, isActive: boolean, isOverflowing: boolean, groupCount: number): void { - let primaryActions: IAction[] = []; - let secondaryActions: IAction[] = []; - - // Handle toolbar only if side is active - if (isActive) { - - // Handle Editor Actions - let editorActions = this.mapActionsToEditors[position][editor.getId()]; - if (!editorActions) { - editorActions = this.getEditorActionsForContext(editor, editor, position); - this.mapActionsToEditors[position][editor.getId()] = editorActions; - } - - primaryActions.push(...editorActions.primary); - secondaryActions.push(...editorActions.secondary); - - // Handle Editor Input Actions - let editorInputActions = this.getEditorActionsForContext({ input: input, editor: editor, position: position }, editor, position); - - primaryActions.push(...editorInputActions.primary); - secondaryActions.push(...editorInputActions.secondary); - } - - // Apply to title in side by side control - this.setTitle(position, input, prepareActions(primaryActions), prepareActions(secondaryActions), isPinned, isActive, isOverflowing, groupCount); - } - - private getEditorActionsForContext(context: BaseEditor, editor: BaseEditor, position: Position): IEditorActions; - private getEditorActionsForContext(context: IEditorInputActionContext, editor: BaseEditor, position: Position): IEditorActions; - private getEditorActionsForContext(context: any, editor: BaseEditor, position: Position): IEditorActions { - let primaryActions: IAction[] = []; - let secondaryActions: IAction[] = []; - - // From Editor - if (context instanceof BaseEditor) { - primaryActions.push(...(context).getActions()); - secondaryActions.push(...(context).getSecondaryActions()); - } - - // From Contributions - let actionBarRegistry = Registry.as(Extensions.Actionbar); - primaryActions.push(...actionBarRegistry.getActionBarActionsForContext(Scope.EDITOR, context)); - secondaryActions.push(...actionBarRegistry.getSecondaryActionBarActionsForContext(Scope.EDITOR, context)); - - return { - primary: primaryActions, - secondary: secondaryActions - }; - } - - private setTitle(position: Position, input: EditorInput, primaryActions: IAction[], secondaryActions: IAction[], isPinned: boolean, isActive: boolean, isOverflowing: boolean, groupCount: number): void { - - // Editor Title (Status + Label + Description) - this.setTitleLabel(position, input, isPinned, isActive); - - // Update Editor Actions Toolbar - this.editorActionsToolbar[position].setActions(primaryActions, secondaryActions)(); - this.editorActionsToolbar[position].addPrimaryAction(this.closeEditorActions[position])(); - - // Update Group Actions Toolbar - const canSplit = isActive && this.getVisibleEditorCount() < 3 && this.lastActiveEditor.supportsSplitEditor(); - const primaryGroupActions = this.getPrimaryGroupActions(position, groupCount, canSplit, isOverflowing); - this.groupActionsToolbar[position].setActions(primaryGroupActions, this.getSecondaryGroupActions(position))(); - } - - public setTitleLabel(position: Position, input: EditorInput, isPinned: boolean, isActive: boolean): void { - - // Pinned state - if (isPinned) { - this.titleContainer[position].addClass('pinned'); - } else { - this.titleContainer[position].removeClass('pinned'); - } - - // Activity state - if (isActive) { - this.containers[position].removeClass('inactive'); - this.containers[position].addClass('active'); - } else { - this.containers[position].addClass('inactive'); - this.containers[position].removeClass('active'); - } - - // Editor Title (Status + Label + Description) - let name = input.getName() || ''; - let description = isActive ? (input.getDescription() || '') : ''; - let verboseDescription = input.getDescription(true) || ''; - if (description === verboseDescription) { - verboseDescription = ''; // dont repeat what is already shown - } - - this.titleLabel[position].safeInnerHtml(name); - this.titleLabel[position].title(verboseDescription); - - this.titleDescription[position].safeInnerHtml(description); - this.titleDescription[position].title(verboseDescription); - - if (input.isDirty()) { - this.titleDecoration[position].addClass('dirty'); - } else { - this.titleDecoration[position].removeClass('dirty'); - } - } - - public clearTitleArea(position: Position): void { - - // Editor Title - this.titleDecoration[position].removeClass('dirty'); - this.titleLabel[position].safeInnerHtml(''); - this.titleDescription[position].safeInnerHtml(''); - - // Toolbar - this.editorActionsToolbar[position].setActions([], [])(); - } - private centerSash(a: Position, b: Position): void { let sumWidth = this.containerWidth[a] + this.containerWidth[b]; let meanWidth = sumWidth / 2; @@ -1585,7 +1176,7 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti let totalWidth = 0; // Set preferred dimensions based on ratio to previous dimenions - POSITIONS.forEach((position) => { + POSITIONS.forEach(position => { if (this.visibleEditors[position]) { // Keep minimized editors in tact by not letting them grow if we have width to give @@ -1634,7 +1225,7 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti // We have width to take else if (overflow > 0) { - POSITIONS.forEach((position) => { + POSITIONS.forEach(position => { let maxCompensation = this.containerWidth[position] - SideBySideEditorControl.MIN_EDITOR_WIDTH; if (maxCompensation >= overflow) { this.containerWidth[position] -= overflow; @@ -1659,7 +1250,7 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti private layoutContainers(): void { // Layout containers - POSITIONS.forEach((position) => { + POSITIONS.forEach(position => { this.containers[position].size(this.containerWidth[position], this.dimension.height); }); @@ -1671,7 +1262,7 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti } // Visibility - POSITIONS.forEach((position) => { + POSITIONS.forEach(position => { if (this.visibleEditors[position] && this.containers[position].isHidden()) { this.containers[position].show(); } else if (!this.visibleEditors[position] && !this.containers[position].isHidden()) { @@ -1680,7 +1271,7 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti }); // Layout active editors - POSITIONS.forEach((position) => { + POSITIONS.forEach(position => { this.layoutEditor(position); }); } @@ -1714,28 +1305,18 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti dispose(this.toDispose); // Positions - POSITIONS.forEach((position) => { + POSITIONS.forEach(position => { this.clearPosition(position); }); - // Toolbars - this.groupActionsToolbar.forEach((toolbar) => { - toolbar.dispose(); - }); - this.editorActionsToolbar.forEach((toolbar) => { - toolbar.dispose(); - }); + // Title Area Control + this.titleAreaControl.forEach(c => c.dispose()); // Progress bars this.progressBar.forEach((bar) => { bar.dispose(); }); - // Actions - [this.splitEditorAction, this.showAllEditorsAction, ...this.showEditorsOfGroup, ...this.closeEditorActions, ...this.moveGroupLeftActions, ...this.moveGroupRightActions, ...this.closeEditorsInGroupActions].forEach((action) => { - action.dispose(); - }); - // Sash this.leftSash.dispose(); this.rightSash.dispose(); @@ -1751,6 +1332,5 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti this.visibleEditorContainers = null; this._onGroupFocusChanged.dispose(); - this._onEditorTitleDoubleclick.dispose(); } } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts new file mode 100644 index 0000000000000000000000000000000000000000..05a58d8e1b24cb39c4e74440e8b147fda17796cb --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -0,0 +1,293 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import nls = require('vs/nls'); +import {Registry} from 'vs/platform/platform'; +import {Scope, IActionBarRegistry, Extensions} from 'vs/workbench/browser/actionBarRegistry'; +import {IAction, Action} from 'vs/base/common/actions'; +import errors = require('vs/base/common/errors'); +import {Builder} from 'vs/base/browser/builder'; +import {BaseEditor, IEditorInputActionContext} from 'vs/workbench/browser/parts/editor/baseEditor'; +import {RunOnceScheduler} from 'vs/base/common/async'; +import {IEditorStacksModel, IEditorGroup} from 'vs/workbench/common/editor'; +import {EventType as BaseEventType} from 'vs/base/common/events'; +import {IActionItem, ActionsOrientation, Separator} from 'vs/base/browser/ui/actionbar/actionbar'; +import {ToolBar} from 'vs/base/browser/ui/toolbar/toolbar'; +import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; +import {IContextMenuService} from 'vs/platform/contextview/browser/contextView'; +import {Position} from 'vs/platform/editor/common/editor'; +import {IEditorGroupService} from 'vs/workbench/services/group/common/groupService'; +import {IMessageService, Severity} from 'vs/platform/message/common/message'; +import {QuickOpenAction} from 'vs/workbench/browser/quickopen'; +import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry'; +import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; +import {IKeybindingService} from 'vs/platform/keybinding/common/keybindingService'; +import {ShowEditorsInLeftGroupAction, ShowAllEditorsAction, ShowEditorsInCenterGroupAction, ShowEditorsInRightGroupAction, CloseEditorsInGroupAction, MoveGroupLeftAction, MoveGroupRightAction, SplitEditorAction, CloseEditorAction} from 'vs/workbench/browser/parts/editor/editorActions'; +import {IDisposable, dispose} from 'vs/base/common/lifecycle'; + +export interface IToolbarActions { + primary: IAction[]; + secondary: IAction[]; +} + +export interface ITitleAreaControl { + setContext(group: IEditorGroup): void; + create(parent: Builder): void; + dispose(): void; +} + +export abstract class TitleControl { + protected stacks: IEditorStacksModel; + protected context: IEditorGroup; + + protected closeEditorAction: CloseEditorAction; + protected showEditorsOfLeftGroup: QuickOpenAction; + protected showEditorsOfCenterGroup: QuickOpenAction; + protected showEditorsOfRightGroup: QuickOpenAction; + protected moveGroupLeftAction: MoveGroupLeftAction; + protected moveGroupRightAction: MoveGroupRightAction; + protected closeEditorsInGroupAction: CloseEditorsInGroupAction; + protected splitEditorAction: SplitEditorAction; + protected showAllEditorsAction: ShowAllEditorsAction; + + private mapActionsToEditors: { [editorId: string]: IToolbarActions; }; + + private scheduler: RunOnceScheduler; + protected toDispose: IDisposable[]; + + constructor( + @IContextMenuService protected contextMenuService: IContextMenuService, + @IInstantiationService protected instantiationService: IInstantiationService, + @IWorkbenchEditorService protected editorService: IWorkbenchEditorService, + @IEditorGroupService protected editorGroupService: IEditorGroupService, + @IKeybindingService protected keybindingService: IKeybindingService, + @ITelemetryService protected telemetryService: ITelemetryService, + @IMessageService protected messageService: IMessageService + ) { + this.toDispose = []; + this.stacks = editorGroupService.getStacksModel(); + this.mapActionsToEditors = Object.create(null); + + this.scheduler = new RunOnceScheduler(() => this.redraw(), 0); + this.toDispose.push(this.scheduler); + + this.initActions(); + } + + public setContext(group: IEditorGroup): void { + this.context = group; + + this.scheduler.schedule(); + } + + protected abstract redraw(); + + private initActions(): void { + this.closeEditorAction = this.instantiationService.createInstance(CloseEditorAction, CloseEditorAction.ID, nls.localize('close', "Close")); + this.showAllEditorsAction = this.instantiationService.createInstance(ShowAllEditorsAction, ShowAllEditorsAction.ID, nls.localize('showEditors', "Show Editors")); + this.splitEditorAction = this.instantiationService.createInstance(SplitEditorAction, SplitEditorAction.ID, SplitEditorAction.LABEL); + this.moveGroupLeftAction = this.instantiationService.createInstance(MoveGroupLeftAction, MoveGroupLeftAction.ID, nls.localize('moveLeft', "Move Left")); + this.moveGroupRightAction = this.instantiationService.createInstance(MoveGroupRightAction, MoveGroupRightAction.ID, nls.localize('moveRight', "Move Right")); + this.closeEditorsInGroupAction = this.instantiationService.createInstance(CloseEditorsInGroupAction, CloseEditorsInGroupAction.ID, nls.localize('closeAll', "Close All")); + this.showEditorsOfLeftGroup = this.instantiationService.createInstance(ShowEditorsInLeftGroupAction, ShowEditorsInLeftGroupAction.ID, nls.localize('showEditors', "Show Editors")); + this.showEditorsOfCenterGroup = this.instantiationService.createInstance(ShowEditorsInCenterGroupAction, ShowEditorsInCenterGroupAction.ID, nls.localize('showEditors', "Show Editors")); + this.showEditorsOfRightGroup = this.instantiationService.createInstance(ShowEditorsInRightGroupAction, ShowEditorsInRightGroupAction.ID, nls.localize('showEditors', "Show Editors")); + + [this.showEditorsOfLeftGroup, this.showEditorsOfCenterGroup, this.showEditorsOfRightGroup, this.showAllEditorsAction].forEach(a => a.class = 'show-group-editors-action'); + } + + protected doCreateToolbar(container: Builder): ToolBar { + const toolbar = new ToolBar(container.getHTMLElement(), this.contextMenuService, { + actionItemProvider: (action: Action) => this.actionItemProvider(action), + orientation: ActionsOrientation.HORIZONTAL, + ariaLabel: nls.localize('araLabelEditorActions', "Editor actions"), + getKeyBinding: (action) => { + const opts = this.keybindingService.lookupKeybindings(action.id); + if (opts.length > 0) { + return opts[0]; // only take the first one + } + + return null; + } + }); + + // Action Run Handling + this.toDispose.push(toolbar.actionRunner.addListener2(BaseEventType.RUN, (e: any) => { + + // Check for Error + if (e.error && !errors.isPromiseCanceledError(e.error)) { + this.messageService.show(Severity.Error, e.error); + } + + // Log in telemetry + if (this.telemetryService) { + this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'editorPart' }); + } + })); + + return toolbar; + } + + protected actionItemProvider(action: Action): IActionItem { + if (!this.context) { + return null; + } + + const group = this.context; + const position = this.stacks.positionOfGroup(group); + const editor = this.editorService.getVisibleEditors()[position]; + + let actionItem: IActionItem; + + // Check Active Editor + if (editor instanceof BaseEditor) { + actionItem = editor.getActionItem(action); + } + + // Check Registry + if (!actionItem) { + let actionBarRegistry = Registry.as(Extensions.Actionbar); + actionItem = actionBarRegistry.getActionItemForContext(Scope.EDITOR, { input: editor && editor.input, editor, position }, action); + } + + return actionItem; + } + + protected getEditorActions(group: IEditorGroup): IToolbarActions { + const position = this.stacks.positionOfGroup(group); + const isActive = this.stacks.isActive(group); + const primary: IAction[] = []; + const secondary: IAction[] = []; + const editor = this.editorService.getVisibleEditors()[position]; + + if (isActive && editor instanceof BaseEditor) { + let editorActions = this.mapActionsToEditors[editor.getId()]; + if (!editorActions) { + editorActions = this.getEditorActionsForContext(editor); + this.mapActionsToEditors[editor.getId()] = editorActions; + } + + primary.push(...editorActions.primary); + secondary.push(...editorActions.secondary); + + // Handle Editor Input Actions + let editorInputActions = this.getEditorActionsForContext({ input: editor.input, editor, position: editor.position }); + + primary.push(...editorInputActions.primary); + secondary.push(...editorInputActions.secondary); + } + + return { primary, secondary }; + } + + protected getEditorActionsForContext(context: BaseEditor): IToolbarActions; + protected getEditorActionsForContext(context: IEditorInputActionContext): IToolbarActions; + protected getEditorActionsForContext(context: any): IToolbarActions { + let primaryActions: IAction[] = []; + let secondaryActions: IAction[] = []; + + // From Editor + if (context instanceof BaseEditor) { + primaryActions.push(...(context).getActions()); + secondaryActions.push(...(context).getSecondaryActions()); + } + + // From Contributions + let actionBarRegistry = Registry.as(Extensions.Actionbar); + primaryActions.push(...actionBarRegistry.getActionBarActionsForContext(Scope.EDITOR, context)); + secondaryActions.push(...actionBarRegistry.getSecondaryActionBarActionsForContext(Scope.EDITOR, context)); + + return { + primary: primaryActions, + secondary: secondaryActions + }; + } + + protected getGroupActions(group: IEditorGroup): IToolbarActions { + const position = this.stacks.positionOfGroup(group); + const editor = this.editorService.getVisibleEditors()[position]; + const primary: IAction[] = []; + + const isOverflowing = group.count > 1; + const groupCount = this.stacks.groups.length; + + // Overflow + if (isOverflowing) { + let overflowAction: Action; + + if (groupCount === 1) { + overflowAction = this.showAllEditorsAction; + } else { + switch (this.stacks.positionOfGroup(group)) { + case Position.LEFT: + overflowAction = this.showEditorsOfLeftGroup; + break; + + case Position.CENTER: + overflowAction = (groupCount === 2) ? this.showEditorsOfRightGroup : this.showEditorsOfCenterGroup; + break; + + case Position.RIGHT: + overflowAction = this.showEditorsOfRightGroup; + break; + } + } + + primary.push(overflowAction); + } + + // Splitting + if (editor && editor instanceof BaseEditor && editor.supportsSplitEditor()) { + primary.push(this.splitEditorAction); + } + + // Make sure enablement is good + switch (this.stacks.positionOfGroup(group)) { + case Position.LEFT: + this.moveGroupLeftAction.enabled = false; + this.moveGroupRightAction.enabled = this.stacks.groups.length > 1; + break; + + case Position.CENTER: + this.moveGroupRightAction.enabled = this.stacks.groups.length > 2; + break; + + case Position.RIGHT: + this.moveGroupRightAction.enabled = false; + break; + } + + // Return actions + const secondary = [ + this.moveGroupLeftAction, + this.moveGroupRightAction, + new Separator(), + this.closeEditorsInGroupAction + ]; + + return { primary, secondary }; + } + + public dispose(): void { + dispose(this.toDispose); + + // Actions + [ + this.splitEditorAction, + this.showAllEditorsAction, + this.showEditorsOfLeftGroup, + this.showEditorsOfCenterGroup, + this.showEditorsOfRightGroup, + this.closeEditorAction, + this.moveGroupLeftAction, + this.moveGroupRightAction, + this.closeEditorsInGroupAction + ].forEach((action) => { + action.dispose(); + }); + } +} \ No newline at end of file