提交 e33ea819 编写于 作者: B Benjamin Pasero

grid - first cut DND support for editor groups

上级 c1c8a49e
......@@ -70,3 +70,15 @@ export const DataTransfers = {
*/
TEXT: 'text/plain'
};
export function applyDragImage(event: DragEvent, label: string, clazz: string): void {
const dragImage = document.createElement('div');
dragImage.className = clazz;
dragImage.textContent = label;
document.body.appendChild(dragImage);
event.dataTransfer.setDragImage(dragImage, -10, -10);
// Removes the element when the DND operation is done
setTimeout(() => document.body.removeChild(dragImage), 0);
}
\ No newline at end of file
......@@ -47,6 +47,14 @@ export class DraggedEditorIdentifier {
}
}
export class DraggedEditorGroupIdentifier {
constructor(private _identifier: GroupIdentifier) { }
get identifier(): GroupIdentifier {
return this._identifier;
}
}
export interface IDraggedEditor extends IDraggedResource {
backupResource?: URI;
viewState?: IEditorViewState;
......@@ -431,9 +439,11 @@ export class LocalSelectionTransfer<T> {
return proto && proto === this.proto;
}
clearData(): void {
this.proto = void 0;
this.data = void 0;
clearData(proto: T): void {
if (this.hasData(proto)) {
this.proto = void 0;
this.data = void 0;
}
}
getData(proto: T): T[] {
......
......@@ -7,7 +7,7 @@
import { GroupIdentifier, IWorkbenchEditorConfiguration, IWorkbenchEditorPartConfiguration, EditorOptions, TextEditorOptions } from 'vs/workbench/common/editor';
import { EditorGroup } from 'vs/workbench/common/editor/editorStacksModel';
import { INextEditorGroup, GroupDirection, IAddGroupOptions } from 'vs/workbench/services/group/common/nextEditorGroupsService';
import { INextEditorGroup, GroupDirection, IAddGroupOptions, IMergeGroupOptions } from 'vs/workbench/services/group/common/nextEditorGroupsService';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Dimension } from 'vs/base/browser/dom';
import { Event } from 'vs/base/common/event';
......@@ -73,7 +73,11 @@ export interface INextEditorGroupsAccessor {
getGroup(identifier: GroupIdentifier): INextEditorGroupView;
addGroup(location: INextEditorGroupView | GroupIdentifier, direction: GroupDirection, options?: IAddGroupOptions): INextEditorGroup;
activateGroup(identifier: INextEditorGroupView | GroupIdentifier): INextEditorGroupView;
addGroup(location: INextEditorGroupView | GroupIdentifier, direction: GroupDirection, options?: IAddGroupOptions): INextEditorGroupView;
moveGroup(group: INextEditorGroupView | GroupIdentifier, location: INextEditorGroupView | GroupIdentifier, direction: GroupDirection): INextEditorGroupView;
mergeGroup(group: INextEditorGroupView | GroupIdentifier, target: INextEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): INextEditorGroupView;
copyGroup(group: INextEditorGroupView | GroupIdentifier, location: INextEditorGroupView | GroupIdentifier, direction: GroupDirection): INextEditorGroupView;
}
export interface INextEditorGroupView extends IDisposable, ISerializableView, INextEditorGroup {
......
......@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/** Container */
/* Container */
.monaco-workbench > .part.editor > .content .editor-group-container {
height: 100%;
......@@ -29,7 +29,7 @@
outline-width: 0; /* no outline when editor part is empty */
}
/** Title */
/* Title */
.monaco-workbench > .part.editor > .content .editor-group-container > .title {
height: 35px;
......@@ -47,7 +47,7 @@
display: none;
}
/** Toolbar */
/* Toolbar */
.monaco-workbench > .part.editor > .content .editor-group-container > .editor-group-container-toolbar {
display: none;
......
......@@ -54,24 +54,11 @@
display: none;
}
/* Drag Cursor (TODO@grid this depends on the feature to drag an entire group to another location) */
.monaco-workbench > .part.editor > .content.multiple-groups .editor-group-container > .title,
.monaco-workbench > .part.editor > .content.multiple-groups .editor-group-container > .title.tabs .scrollbar .slider,
.monaco-workbench > .part.editor > .content.multiple-groups .editor-group-container > .title .monaco-icon-label::before,
.monaco-workbench > .part.editor > .content.multiple-groups .editor-group-container > .title .title-label a,
.monaco-workbench > .part.editor > .content.multiple-groups .editor-group-container > .title .title-label span {
/* Drag Cursor */
.monaco-workbench > .part.editor > .content.multiple-groups .editor-group-container > .title {
cursor: -webkit-grab;
}
#monaco-workbench-editor-move-overlay,
.monaco-workbench > .part.editor > .content.multiple-groups .editor-group-container.drag,
.monaco-workbench > .part.editor > .content.multiple-groups .editor-group-container.drag > .title,
.monaco-workbench > .part.editor > .content.multiple-groups .editor-group-container.drag > .title.tabs .scrollbar .slider,
.monaco-workbench > .part.editor > .content.multiple-groups .editor-group-container.drag > .title .monaco-icon-label::before,
.monaco-workbench > .part.editor > .content.multiple-groups .editor-group-container.drag > .title .title-label a,
.monaco-workbench > .part.editor > .content.multiple-groups .editor-group-container.drag > .title .title-label span {
cursor: -webkit-grabbing;
}
/* Actions */
......@@ -100,4 +87,14 @@
.vs-dark .monaco-workbench > .part.editor > .content .editor-group-container > .title .split-editor-vertical-action,
.hc-black .monaco-workbench > .part.editor > .content .editor-group-container > .title .split-editor-vertical-action {
background: url('split-editor-vertical-inverse.svg') center center no-repeat;
}
/* Drag and Drop Feedback */
.monaco-editor-group-drag-image {
display: inline-block;
padding: 1px 7px;
border-radius: 10px;
font-size: 12px;
position: absolute;
}
\ No newline at end of file
......@@ -6,7 +6,7 @@
'use strict';
import 'vs/css!./media/nextEditorDragAndDrop';
import { LocalSelectionTransfer, DraggedEditorIdentifier, DragCounter, ResourcesDropHandler } from 'vs/workbench/browser/dnd';
import { LocalSelectionTransfer, DraggedEditorIdentifier, DragCounter, ResourcesDropHandler, DraggedEditorGroupIdentifier } from 'vs/workbench/browser/dnd';
import { addDisposableListener, EventType, EventHelper, isAncestor, toggleClass, addClass } from 'vs/base/browser/dom';
import { INextEditorGroupsAccessor, EDITOR_TITLE_HEIGHT, INextEditorGroupView, getActiveTextEditorOptions } from 'vs/workbench/browser/parts/editor2/editor2';
import { EDITOR_DRAG_AND_DROP_BACKGROUND, Themable } from 'vs/workbench/common/theme';
......@@ -14,7 +14,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { IEditorIdentifier, EditorInput, EditorOptions } from 'vs/workbench/common/editor';
import { isMacintosh } from 'vs/base/common/platform';
import { GroupDirection, INextEditorGroup } from 'vs/workbench/services/group/common/nextEditorGroupsService';
import { GroupDirection, MergeGroupMode } from 'vs/workbench/services/group/common/nextEditorGroupsService';
import { toDisposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
......@@ -29,10 +29,12 @@ class DropOverlay extends Themable {
private splitDirection: GroupDirection;
private _disposed: boolean;
private readonly editorTransfer = LocalSelectionTransfer.getInstance<DraggedEditorIdentifier>();
private readonly groupTransfer = LocalSelectionTransfer.getInstance<DraggedEditorGroupIdentifier>();
constructor(
private accessor: INextEditorGroupsAccessor,
private groupView: INextEditorGroupView,
private transfer: LocalSelectionTransfer<DraggedEditorIdentifier>,
themeService: IThemeService,
private instantiationService: IInstantiationService
) {
......@@ -87,7 +89,7 @@ class DropOverlay extends Themable {
// Update the dropEffect, otherwise it would look like a "move" operation. but only if we are
// not dragging a tab actually because there we support both moving as well as copying
if (!this.transfer.hasData(DraggedEditorIdentifier.prototype)) {
if (!this.editorTransfer.hasData(DraggedEditorIdentifier.prototype) && !this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) {
e.dataTransfer.dropEffect = 'copy';
}
......@@ -108,7 +110,7 @@ class DropOverlay extends Themable {
// Dispose on drag end
this._register(addDisposableListener(this.container, EventType.DRAG_END, () => this.dispose()));
this._register(addDisposableListener(this.container, EventType.DRAG_LEAVE, (e: DragEvent) => this.dispose()));
this._register(addDisposableListener(this.container, EventType.DRAG_LEAVE, () => this.dispose()));
this._register(addDisposableListener(this.container, EventType.MOUSE_OVER, () => {
// Under some circumstances we have seen reports where the drop overlay is not being
......@@ -128,7 +130,7 @@ class DropOverlay extends Themable {
// Determine target group
const ensureTargetGroup = () => {
let targetGroup: INextEditorGroup;
let targetGroup: INextEditorGroupView;
if (typeof this.splitDirection === 'number') {
targetGroup = this.accessor.addGroup(this.groupView, this.splitDirection, { activate: true });
} else {
......@@ -138,9 +140,42 @@ class DropOverlay extends Themable {
return targetGroup;
};
// Check for transfer from title control
if (this.transfer.hasData(DraggedEditorIdentifier.prototype)) {
const draggedEditor = this.transfer.getData(DraggedEditorIdentifier.prototype)[0].identifier;
// Check for group transfer from title control
if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) {
const draggedEditorGroup = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype)[0].identifier;
// Return if the drop is a no-op
const sourceGroup = this.accessor.getGroup(draggedEditorGroup);
if (typeof this.splitDirection !== 'number' && sourceGroup === this.groupView) {
return;
}
// Split to new group
let targetGroup: INextEditorGroupView;
if (typeof this.splitDirection === 'number') {
if (this.isCopyOperation(event)) {
targetGroup = this.accessor.copyGroup(sourceGroup, this.groupView, this.splitDirection);
} else {
targetGroup = this.accessor.moveGroup(sourceGroup, this.groupView, this.splitDirection);
}
}
// Merge into existing group
else {
if (this.isCopyOperation(event)) {
targetGroup = this.accessor.mergeGroup(sourceGroup, this.groupView, { mode: MergeGroupMode.COPY_EDITORS });
} else {
targetGroup = this.accessor.mergeGroup(sourceGroup, this.groupView);
}
}
this.accessor.activateGroup(targetGroup);
this.groupTransfer.clearData(DraggedEditorGroupIdentifier.prototype);
}
// Check for editor transfer from title control
else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) {
const draggedEditor = this.editorTransfer.getData(DraggedEditorIdentifier.prototype)[0].identifier;
const targetGroup = ensureTargetGroup();
// Return if the drop is a no-op
......@@ -154,10 +189,13 @@ class DropOverlay extends Themable {
targetGroup.openEditor(draggedEditor.editor, options);
// Close in source group unless we copy
const copyEditor = this.shouldCopyEditor(draggedEditor, event);
const copyEditor = this.isCopyOperation(event, draggedEditor);
if (!copyEditor) {
sourceGroup.closeEditor(draggedEditor.editor);
}
this.accessor.activateGroup(targetGroup);
this.editorTransfer.clearData(DraggedEditorIdentifier.prototype);
}
// Check for URI transfer
......@@ -169,8 +207,8 @@ class DropOverlay extends Themable {
}
}
private shouldCopyEditor(draggedEditor: IEditorIdentifier, e: DragEvent) {
if (draggedEditor.editor instanceof EditorInput && !draggedEditor.editor.supportsSplitEditor()) {
private isCopyOperation(e: DragEvent, draggedEditor?: IEditorIdentifier): boolean {
if (draggedEditor && draggedEditor.editor instanceof EditorInput && !draggedEditor.editor.supportsSplitEditor()) {
return false;
}
......@@ -195,42 +233,32 @@ class DropOverlay extends Themable {
case topEdgeDistance:
if (topEdgeDistance < edgeHeightThreshold) {
splitDirection = GroupDirection.UP;
this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '50%' });
}
break;
case bottomEdgeDistance:
if (bottomEdgeDistance < edgeHeightThreshold) {
splitDirection = GroupDirection.DOWN;
this.doPositionOverlay({ top: '50%', left: '0', width: '100%', height: '50%' });
}
break;
case leftEdgeDistance:
if (leftEdgeDistance < edgeWidthThreshold) {
splitDirection = GroupDirection.LEFT;
this.doPositionOverlay({ top: '0', left: '0', width: '50%', height: '100%' });
}
break;
case rightEdgeDistance:
if (rightEdgeDistance < edgeWidthThreshold) {
splitDirection = GroupDirection.RIGHT;
this.doPositionOverlay({ top: '0', left: '50%', width: '50%', height: '100%' });
}
break;
}
// Position overlay according to location
switch (splitDirection) {
case GroupDirection.UP:
this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '50%' });
break;
case GroupDirection.DOWN:
this.doPositionOverlay({ top: '50%', left: '0', width: '100%', height: '50%' });
break;
case GroupDirection.LEFT:
this.doPositionOverlay({ top: '0', left: '0', width: '50%', height: '100%' });
break;
case GroupDirection.RIGHT:
this.doPositionOverlay({ top: '0', left: '50%', width: '50%', height: '100%' });
break;
default:
this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '100%' });
break;
// No split, position overlay over entire group
if (typeof splitDirection !== 'number') {
this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '100%' });
}
// Make sure the overlay is visible now
......@@ -273,9 +301,11 @@ export class NextEditorDragAndDrop extends Themable {
private _overlay: DropOverlay;
private transfer = LocalSelectionTransfer.getInstance<DraggedEditorIdentifier>();
private counter = new DragCounter(); // see https://github.com/Microsoft/vscode/issues/14470
private readonly editorTransfer = LocalSelectionTransfer.getInstance<DraggedEditorIdentifier>();
private readonly groupTransfer = LocalSelectionTransfer.getInstance<DraggedEditorGroupIdentifier>();
constructor(
private accessor: INextEditorGroupsAccessor,
private container: HTMLElement,
......@@ -297,13 +327,18 @@ export class NextEditorDragAndDrop extends Themable {
private registerListeners(): void {
this._register(addDisposableListener(this.container, EventType.DRAG_ENTER, e => this.onDragEnter(e)));
this._register(addDisposableListener(this.container, EventType.DRAG_LEAVE, e => this.onDragLeave()));
this._register(addDisposableListener(this.container, EventType.DRAG_LEAVE, () => this.onDragLeave()));
[this.container, window].forEach(node => this._register(addDisposableListener(node as HTMLElement, EventType.DRAG_END, () => this.onDragEnd())));
}
private onDragEnter(event: DragEvent): void {
if (!this.transfer.hasData(DraggedEditorIdentifier.prototype) && !event.dataTransfer.types.length) {
return; // invalid DND (see https://github.com/Microsoft/vscode/issues/25789)
if (
!this.editorTransfer.hasData(DraggedEditorIdentifier.prototype) &&
!this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype) &&
!event.dataTransfer.types.length // see https://github.com/Microsoft/vscode/issues/25789
) {
event.dataTransfer.dropEffect = 'none';
return; // unsupported transfer
}
// Signal DND start
......@@ -322,7 +357,7 @@ export class NextEditorDragAndDrop extends Themable {
if (!this.overlay) {
const groupView = this.findGroupView(target);
if (groupView) {
this._overlay = new DropOverlay(this.accessor, groupView, this.transfer, this.themeService, this.instantiationService);
this._overlay = new DropOverlay(this.accessor, groupView, this.themeService, this.instantiationService);
}
}
}
......
......@@ -204,11 +204,11 @@ export class NextEditorGroupView extends Themable implements INextEditorGroupVie
// Toolbar
const groupId = this._group.id;
const containerToolbar = new ActionBar(toolbarContainer, {
ariaLabel: localize('araLabelGroupActions', "Editor group actions"), actionRunner: new class extends ActionRunner {
ariaLabel: localize('araLabelGroupActions', "Editor group actions"), actionRunner: this._register(new class extends ActionRunner {
run(action: IAction) {
return action.run(groupId);
}
}
})
});
// Toolbar actions
......
......@@ -11,7 +11,7 @@ import { Part } from 'vs/workbench/browser/part';
import { Dimension, isAncestor, toggleClass, addClass, clearNode } from 'vs/base/browser/dom';
import { Event, Emitter, once } from 'vs/base/common/event';
import { contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { INextEditorGroupsService, GroupDirection, IAddGroupOptions, GroupsArrangement, GroupOrientation } from 'vs/workbench/services/group/common/nextEditorGroupsService';
import { INextEditorGroupsService, GroupDirection, IAddGroupOptions, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, ICopyEditorOptions } from 'vs/workbench/services/group/common/nextEditorGroupsService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, ISerializedNode } from 'vs/base/browser/ui/grid/grid';
import { GroupIdentifier, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
......@@ -193,6 +193,9 @@ export class NextEditorPart extends Part implements INextEditorGroupsService, IN
focusGroup(group: INextEditorGroupView | GroupIdentifier): INextEditorGroupView {
const groupView = this.assertGroupView(group);
// Activate and focus group
this.doSetGroupActive(groupView);
groupView.focus();
return groupView;
......@@ -370,7 +373,7 @@ export class NextEditorPart extends Part implements INextEditorGroupsService, IN
}
// Remove group with editors
return this.doRemoveGroupWithEditors(groupView);
this.doRemoveGroupWithEditors(groupView);
}
private doRemoveGroupWithEditors(groupView: INextEditorGroupView): void {
......@@ -385,7 +388,7 @@ export class NextEditorPart extends Part implements INextEditorGroupsService, IN
// Removing a group with editors should merge these editors into the
// last active group and then remove this group.
return this.mergeGroup(groupView, lastActiveGroup);
this.mergeGroup(groupView, lastActiveGroup);
}
private doRemoveEmptyGroup(groupView: INextEditorGroupView): void {
......@@ -405,7 +408,7 @@ export class NextEditorPart extends Part implements INextEditorGroupsService, IN
// 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) {
this._activeGroup.focus();
this.focusGroup(this._activeGroup);
}
// Update container
......@@ -419,20 +422,25 @@ export class NextEditorPart extends Part implements INextEditorGroupsService, IN
const groupView = this.assertGroupView(group);
const locationView = this.assertGroupView(location);
const groupHasFocus = isAncestor(document.activeElement, groupView.element);
// Target is same view: we first need to create the new group and then merge
// all editors of the group into it to preserve the view state.
if (groupView.id === locationView.id) {
throw new Error('Unable to move the same editor group into itself!');
const newGroup = this.doAddGroup(groupView, direction);
this.mergeGroup(groupView, newGroup, { mode: MergeGroupMode.MOVE_EDITORS_KEEP_GROUP });
}
const groupHasFocus = isAncestor(document.activeElement, groupView.element);
// Move is a remove + add
this.gridWidget.removeView(groupView, Sizing.Distribute);
this.gridWidget.addView(groupView, Sizing.Distribute, locationView, this.toGridViewDirection(direction));
// Target is different view: operation is a simple remove and add
else {
this.gridWidget.removeView(groupView, Sizing.Distribute);
this.gridWidget.addView(groupView, Sizing.Distribute, locationView, 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) {
groupView.focus();
this.focusGroup(groupView);
}
// Event
......@@ -445,10 +453,20 @@ export class NextEditorPart extends Part implements INextEditorGroupsService, IN
const groupView = this.assertGroupView(group);
const locationView = this.assertGroupView(location);
return this.doAddGroup(locationView, direction, groupView);
const groupHasFocus = isAncestor(document.activeElement, groupView.element);
// Copy the group view
const copiedGroupView = this.doAddGroup(locationView, direction, groupView);
// Restore focus if we had it
if (groupHasFocus) {
this.focusGroup(copiedGroupView);
}
return copiedGroupView;
}
mergeGroup(group: INextEditorGroupView | GroupIdentifier, target: INextEditorGroupView | GroupIdentifier): void {
mergeGroup(group: INextEditorGroupView | GroupIdentifier, target: INextEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): INextEditorGroupView {
const sourceView = this.assertGroupView(group);
const targetView = this.assertGroupView(target);
......@@ -456,13 +474,23 @@ export class NextEditorPart extends Part implements INextEditorGroupsService, IN
let index = targetView.count;
sourceView.editors.forEach(editor => {
const inactive = sourceView.activeEditor !== editor;
sourceView.moveEditor(editor, targetView, { index, inactive, preserveFocus: inactive });
const copyOptions: ICopyEditorOptions = { index, inactive, preserveFocus: inactive };
if (options && options.mode === MergeGroupMode.COPY_EDITORS) {
sourceView.copyEditor(editor, targetView, copyOptions);
} else {
sourceView.moveEditor(editor, targetView, copyOptions);
}
index++;
});
// Remove source
this.removeGroup(sourceView);
// Remove source (unless prevented)
if (!options || options.mode === MergeGroupMode.MOVE_EDITORS_REMOVE_GROUP) {
this.removeGroup(sourceView);
}
return targetView;
}
private assertGroupView(group: INextEditorGroupView | GroupIdentifier): INextEditorGroupView {
......@@ -689,6 +717,7 @@ export class NextEditorPart extends Part implements INextEditorGroupsService, IN
private updateContainer(): void {
toggleClass(this.container, 'empty', this.isEmpty());
toggleClass(this.container, 'multiple-groups', this.count > 1);
}
private isEmpty(): boolean {
......
......@@ -14,27 +14,27 @@ import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workb
import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch';
import { addDisposableListener, EventType, addClass, EventHelper, removeClass } from 'vs/base/browser/dom';
import { INextEditorPartOptions } from 'vs/workbench/browser/parts/editor2/editor2';
import { LocalSelectionTransfer, DraggedEditorGroupIdentifier, fillResourceDataTransfers } from 'vs/workbench/browser/dnd';
import { applyDragImage } from 'vs/base/browser/dnd';
import { localize } from 'vs/nls';
export class NextNoTabsTitleControl extends NextTitleControl {
private titleContainer: HTMLElement;
private editorLabel: ResourceLabel;
private lastRenderedEditor: IEditorInput;
private readonly groupTransfer = LocalSelectionTransfer.getInstance<DraggedEditorGroupIdentifier>();
protected create(parent: HTMLElement): void {
this.titleContainer = parent;
this.titleContainer.draggable = true;
//Container listeners
this.hookContainerListeners();
// Gesture Support
Gesture.addTarget(this.titleContainer);
// Pin on double click
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)));
// Detect touch
this._register(addDisposableListener(this.titleContainer, TouchEventType.Tap, (e: GestureEvent) => this.onTitleClick(e)));
// Editor Label
this.editorLabel = this._register(this.instantiationService.createInstance(ResourceLabel, this.titleContainer, void 0));
this._register(this.editorLabel.onClick(e => this.onTitleLabelClick(e)));
......@@ -46,6 +46,43 @@ export class NextNoTabsTitleControl extends NextTitleControl {
// Editor actions toolbar
this.createEditorActionsToolBar(actionsContainer);
}
private hookContainerListeners(): void {
// Drag start
this._register(addDisposableListener(this.titleContainer, EventType.DRAG_START, (e: DragEvent) => {
if (e.target !== this.titleContainer) {
return; // only if originating from tabs container
}
// Set editor group as transfer
this.groupTransfer.setData([new DraggedEditorGroupIdentifier(this.group.id)], DraggedEditorGroupIdentifier.prototype);
e.dataTransfer.effectAllowed = 'copyMove';
// Apply some datatransfer types to allow for dragging the element outside of the application
const resource = toResource(this.lastRenderedEditor, { supportSideBySide: true });
if (resource) {
this.instantiationService.invokeFunction(fillResourceDataTransfers, [resource], e);
}
// Drag Image
applyDragImage(e, this.group.count === 1 ? localize('oneEditor', "1 editor") : localize('multipleEditor', "{0} editors", this.group.count), 'monaco-editor-group-drag-image');
}));
// Drag end
this._register(addDisposableListener(this.titleContainer, EventType.DRAG_END, () => {
this.groupTransfer.clearData(DraggedEditorGroupIdentifier.prototype);
}));
// Pin on double click
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)));
// Detect touch
this._register(addDisposableListener(this.titleContainer, TouchEventType.Tap, (e: GestureEvent) => this.onTitleClick(e)));
// Context Menu
this._register(addDisposableListener(this.titleContainer, EventType.CONTEXT_MENU, (e: Event) => this.onContextMenu(this.group.activeEditor, e, this.titleContainer)));
......
......@@ -32,7 +32,7 @@ import { getOrSet } from 'vs/base/common/map';
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } 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 { ResourcesDropHandler, fillResourceDataTransfers, LocalSelectionTransfer, DraggedEditorIdentifier, DragCounter } from 'vs/workbench/browser/dnd';
import { ResourcesDropHandler, fillResourceDataTransfers, LocalSelectionTransfer, DraggedEditorIdentifier, DragCounter, DraggedEditorGroupIdentifier } from 'vs/workbench/browser/dnd';
import { Color } from 'vs/base/common/color';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
......@@ -41,6 +41,7 @@ import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/un
import { addClass, addDisposableListener, hasClass, EventType, EventHelper, removeClass, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode } from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { INextEditorGroupsAccessor, INextEditorPartOptions } from 'vs/workbench/browser/parts/editor2/editor2';
import { applyDragImage } from 'vs/base/browser/dnd';
interface IEditorInputLabel {
name: string;
......@@ -65,7 +66,8 @@ export class NextTabsTitleControl extends NextTitleControl {
private layoutScheduled: IDisposable;
private blockRevealActiveTab: boolean;
private transfer = LocalSelectionTransfer.getInstance<DraggedEditorIdentifier>();
private readonly editorTransfer = LocalSelectionTransfer.getInstance<DraggedEditorIdentifier>();
private readonly groupTransfer = LocalSelectionTransfer.getInstance<DraggedEditorGroupIdentifier>();
constructor(
parent: HTMLElement,
......@@ -92,10 +94,46 @@ export class NextTabsTitleControl extends NextTitleControl {
// Tabs Container
this.tabsContainer = document.createElement('div');
this.tabsContainer.setAttribute('role', 'tablist');
this.tabsContainer.draggable = true;
addClass(this.tabsContainer, 'tabs-container');
// Tabs Container listeners
this.hookContainerListeners();
// Scrollbar
this.createScrollbar();
// Editor Toolbar Container
this.editorToolbarContainer = document.createElement('div');
addClass(this.editorToolbarContainer, 'editor-actions');
this.titleContainer.appendChild(this.editorToolbarContainer);
// Editor Actions Toolbar
this.createEditorActionsToolBar(this.editorToolbarContainer);
}
private createScrollbar(): void {
// Custom Scrollbar
this.scrollbar = new ScrollableElement(this.tabsContainer, {
horizontal: ScrollbarVisibility.Auto,
vertical: ScrollbarVisibility.Hidden,
scrollYToX: true,
useShadows: false,
horizontalScrollbarSize: 3
});
this.scrollbar.onScroll(e => {
this.tabsContainer.scrollLeft = e.scrollLeft;
});
this.titleContainer.appendChild(this.scrollbar.getDomNode());
}
private hookContainerListeners(): void {
// Forward scrolling inside the container to our custom scrollbar
this._register(addDisposableListener(this.tabsContainer, EventType.SCROLL, e => {
this._register(addDisposableListener(this.tabsContainer, EventType.SCROLL, () => {
if (hasClass(this.tabsContainer, 'scroll')) {
this.scrollbar.setScrollPosition({
scrollLeft: this.tabsContainer.scrollLeft // during DND the container gets scrolled so we need to update the custom scrollbar
......@@ -105,8 +143,7 @@ export class NextTabsTitleControl extends NextTitleControl {
// New file when double clicking on tabs container (but not tabs)
this._register(addDisposableListener(this.tabsContainer, EventType.DBLCLICK, e => {
const target = e.target;
if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
if (e.target === this.tabsContainer) {
EventHelper.stop(e);
this.group.openEditor(this.untitledEditorService.createOrGet(), { pinned: true /* untitled is always pinned */, index: this.group.count /* always at the end */ });
......@@ -120,57 +157,69 @@ export class NextTabsTitleControl extends NextTitleControl {
}
}));
// Custom Scrollbar
this.scrollbar = new ScrollableElement(this.tabsContainer, {
horizontal: ScrollbarVisibility.Auto,
vertical: ScrollbarVisibility.Hidden,
scrollYToX: true,
useShadows: false,
horizontalScrollbarSize: 3
});
// Drag start
this._register(addDisposableListener(this.tabsContainer, EventType.DRAG_START, (e: DragEvent) => {
if (e.target !== this.tabsContainer) {
return; // only if originating from tabs container
}
this.scrollbar.onScroll(e => {
this.tabsContainer.scrollLeft = e.scrollLeft;
});
// Set editor group as transfer
this.groupTransfer.setData([new DraggedEditorGroupIdentifier(this.group.id)], DraggedEditorGroupIdentifier.prototype);
e.dataTransfer.effectAllowed = 'copyMove';
this.titleContainer.appendChild(this.scrollbar.getDomNode());
// Drag Image
applyDragImage(e, this.group.count === 1 ? localize('oneEditor', "1 editor") : localize('multipleEditor', "{0} editors", this.group.count), 'monaco-editor-group-drag-image');
}));
// Drag over
this._register(addDisposableListener(this.tabsContainer, EventType.DRAG_OVER, (e: DragEvent) => {
const draggedEditor = this.transfer.hasData(DraggedEditorIdentifier.prototype) ? this.transfer.getData(DraggedEditorIdentifier.prototype)[0].identifier : void 0;
// Drag enter
this._register(addDisposableListener(this.tabsContainer, EventType.DRAG_ENTER, (e: DragEvent) => {
// update the dropEffect, otherwise it would look like a "move" operation. but only if we are
// not dragging a tab actually because there we support both moving as well as copying
if (!draggedEditor) {
e.dataTransfer.dropEffect = 'copy';
// Always enable support to scroll while dragging
addClass(this.tabsContainer, 'scroll');
// Return if the target is not on the tabs container
if (e.target !== this.tabsContainer) {
return;
}
addClass(this.tabsContainer, 'scroll'); // enable support to scroll while dragging
// Return if transfer is unsupported
if (
this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype) ||
(
!this.editorTransfer.hasData(DraggedEditorIdentifier.prototype) &&
!e.dataTransfer.types.length // see https://github.com/Microsoft/vscode/issues/25789
)
) {
e.dataTransfer.dropEffect = 'none';
return;
}
const target = e.target;
if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
const draggedEditor = this.editorTransfer.hasData(DraggedEditorIdentifier.prototype) ? this.editorTransfer.getData(DraggedEditorIdentifier.prototype)[0].identifier : void 0;
const draggedEditorIsLastTab = draggedEditor && this.group.id === draggedEditor.group.id && this.group.getIndexOfEditor(draggedEditor.editor) === this.group.count - 1;
// Find out if the currently dragged editor is the last tab of this group and in that
// case we do not want to show any drop feedback because the drop would be a no-op
let draggedEditorIsLastTab = false;
if (draggedEditor && this.group.id === draggedEditor.group.id && this.group.getIndexOfEditor(draggedEditor.editor) === this.group.count - 1) {
draggedEditorIsLastTab = true;
}
// Return if dragged editor is last tab because then this is a no-op
if (draggedEditorIsLastTab) {
return;
}
if (!draggedEditorIsLastTab) {
this.updateDropFeedback(this.tabsContainer, true);
}
// Update drop effect for external drops as they can only be "copy"
if (!draggedEditor) {
e.dataTransfer.dropEffect = 'copy';
}
this.updateDropFeedback(this.tabsContainer, true);
}));
// Drag leave
this._register(addDisposableListener(this.tabsContainer, EventType.DRAG_LEAVE, (e: DragEvent) => {
this._register(addDisposableListener(this.tabsContainer, EventType.DRAG_LEAVE, () => {
this.updateDropFeedback(this.tabsContainer, false);
removeClass(this.tabsContainer, 'scroll');
}));
// Drag end
this._register(addDisposableListener(this.tabsContainer, EventType.DRAG_END, (e: DragEvent) => {
this._register(addDisposableListener(this.tabsContainer, EventType.DRAG_END, () => {
this.groupTransfer.clearData(DraggedEditorGroupIdentifier.prototype);
this.updateDropFeedback(this.tabsContainer, false);
removeClass(this.tabsContainer, 'scroll');
}));
......@@ -180,19 +229,10 @@ export class NextTabsTitleControl extends NextTitleControl {
this.updateDropFeedback(this.tabsContainer, false);
removeClass(this.tabsContainer, 'scroll');
const target = e.target;
if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
if (e.target === this.tabsContainer) {
this.onDrop(e, this.group.count);
}
}));
// Editor Toolbar Container
this.editorToolbarContainer = document.createElement('div');
addClass(this.editorToolbarContainer, 'editor-actions');
this.titleContainer.appendChild(this.editorToolbarContainer);
// Editor Actions Toolbar
this.createEditorActionsToolBar(this.editorToolbarContainer);
}
protected updateEditorActionsToolbar(): void {
......@@ -501,7 +541,7 @@ export class NextTabsTitleControl extends NextTitleControl {
// Drag start
disposables.push(addDisposableListener(tab, EventType.DRAG_START, (e: DragEvent) => {
const editor = this.group.getEditor(index);
this.transfer.setData([new DraggedEditorIdentifier({ editor, group: (<any>this.group /* TODO@grid should be GroupIdentifier or INextEditorGroup */).group })], DraggedEditorIdentifier.prototype);
this.editorTransfer.setData([new DraggedEditorIdentifier({ editor, group: (<any>this.group /* TODO@grid should be GroupIdentifier or INextEditorGroup */).group })], DraggedEditorIdentifier.prototype);
e.dataTransfer.effectAllowed = 'copyMove';
......@@ -526,25 +566,37 @@ export class NextTabsTitleControl extends NextTitleControl {
disposables.push(addDisposableListener(tab, EventType.DRAG_ENTER, (e: DragEvent) => {
counter.increment();
// Find out if the currently dragged editor is this tab and in that
// case we do not want to show any drop feedback
let draggedEditorIsTab = false;
const draggedEditor = this.transfer.hasData(DraggedEditorIdentifier.prototype) ? this.transfer.getData(DraggedEditorIdentifier.prototype)[0].identifier : void 0;
if (draggedEditor) {
if (draggedEditor.editor === this.group.getEditor(index) && draggedEditor.group.id === this.group.id) {
draggedEditorIsTab = true;
}
// Return if transfer is unsupported
if (
this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype) ||
(
!this.editorTransfer.hasData(DraggedEditorIdentifier.prototype) &&
!e.dataTransfer.types.length // see https://github.com/Microsoft/vscode/issues/25789
)
) {
e.dataTransfer.dropEffect = 'none';
return;
}
addClass(tab, 'dragged-over');
const draggedEditor = this.editorTransfer.hasData(DraggedEditorIdentifier.prototype) ? this.editorTransfer.getData(DraggedEditorIdentifier.prototype)[0].identifier : void 0;
const draggedEditorIsSameTab = draggedEditor && draggedEditor.editor === this.group.getEditor(index) && draggedEditor.group.id === this.group.id;
if (!draggedEditorIsTab) {
this.updateDropFeedback(tab, true, index);
// Return if dragged editor is the current tab dragged over
if (draggedEditorIsSameTab) {
return;
}
// Update drop effect for external drops as they can only be "copy"
if (!draggedEditor) {
e.dataTransfer.dropEffect = 'copy';
}
addClass(tab, 'dragged-over');
this.updateDropFeedback(tab, true, index);
}));
// Drag leave
disposables.push(addDisposableListener(tab, EventType.DRAG_LEAVE, (e: DragEvent) => {
disposables.push(addDisposableListener(tab, EventType.DRAG_LEAVE, () => {
counter.decrement();
if (!counter.value) {
......@@ -554,13 +606,13 @@ export class NextTabsTitleControl extends NextTitleControl {
}));
// Drag end
disposables.push(addDisposableListener(tab, EventType.DRAG_END, (e: DragEvent) => {
disposables.push(addDisposableListener(tab, EventType.DRAG_END, () => {
counter.reset();
removeClass(tab, 'dragged-over');
this.updateDropFeedback(tab, false, index);
this.transfer.clearData();
this.editorTransfer.clearData(DraggedEditorIdentifier.prototype);
}));
// Drop
......@@ -909,12 +961,12 @@ export class NextTabsTitleControl extends NextTitleControl {
this.blockRevealActiveTab = true;
}
private originatesFromTabActionBar(event: MouseEvent | GestureEvent): boolean {
private originatesFromTabActionBar(e: MouseEvent | GestureEvent): boolean {
let element: HTMLElement;
if (event instanceof MouseEvent) {
element = (event.target || event.srcElement) as HTMLElement;
if (e instanceof MouseEvent) {
element = (e.target || e.srcElement) as HTMLElement;
} else {
element = (event as GestureEvent).initialTarget as HTMLElement;
element = (e as GestureEvent).initialTarget as HTMLElement;
}
return !!findParentWithClass(element, 'monaco-action-bar', 'tab');
......@@ -927,7 +979,7 @@ export class NextTabsTitleControl extends NextTitleControl {
removeClass(this.tabsContainer, 'scroll');
// Local DND
const draggedEditor = this.transfer.hasData(DraggedEditorIdentifier.prototype) ? this.transfer.getData(DraggedEditorIdentifier.prototype)[0].identifier : void 0;
const draggedEditor = this.editorTransfer.hasData(DraggedEditorIdentifier.prototype) ? this.editorTransfer.getData(DraggedEditorIdentifier.prototype)[0].identifier : void 0;
if (draggedEditor) {
const sourceGroup = this.accessor.getGroup(draggedEditor.group.id) as INextEditorGroup;
......@@ -941,7 +993,7 @@ export class NextTabsTitleControl extends NextTitleControl {
sourceGroup.copyEditor(draggedEditor.editor, this.group, { index: targetIndex });
}
this.transfer.clearData();
this.editorTransfer.clearData(DraggedEditorIdentifier.prototype);
}
// External DND
......
......@@ -28,7 +28,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { createActionItem, fillInActions } from 'vs/platform/actions/browser/menuItemActionItem';
import { IMenuService, MenuId, IMenu, ExecuteCommandAction } from 'vs/platform/actions/common/actions';
import { ResourceContextKey } from 'vs/workbench/common/resources';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { Themable } from 'vs/workbench/common/theme';
import { isDiffEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
import { INotificationService } from 'vs/platform/notification/common/notification';
......@@ -37,6 +37,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten
import { INextEditorGroup } from 'vs/workbench/services/group/common/nextEditorGroupsService';
import { IEditorInput } from 'vs/platform/editor/common/editor';
import { INextEditorGroupsAccessor, INextEditorPartOptions } from 'vs/workbench/browser/parts/editor2/editor2';
import { listActiveSelectionBackground } from 'vs/platform/theme/common/colorRegistry';
export interface IToolbarActions {
primary: IAction[];
......@@ -307,3 +308,16 @@ export abstract class NextTitleControl extends Themable {
//#endregion
}
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
// Drag Feedback
const dragImageColor = theme.getColor(listActiveSelectionBackground);
if (dragImageColor) {
collector.addRule(`
.monaco-editor-group-drag-image {
background: ${dragImageColor};
}
`);
}
});
\ No newline at end of file
......@@ -588,7 +588,7 @@ class OpenEditorRenderer implements IRenderer<OpenEditor, IOpenEditorTemplateDat
}
}));
editorTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DRAG_END, () => {
this.transfer.clearData();
this.transfer.clearData(OpenEditor.prototype);
}));
return editorTemplate;
......
......@@ -44,12 +44,22 @@ export interface IMoveEditorOptions {
preserveFocus?: boolean;
}
export interface ICopyEditorOptions extends IMoveEditorOptions { }
export interface IAddGroupOptions {
activate?: boolean;
copyGroup?: boolean;
}
export interface ICopyEditorOptions extends IMoveEditorOptions { }
export enum MergeGroupMode {
COPY_EDITORS,
MOVE_EDITORS_REMOVE_GROUP,
MOVE_EDITORS_KEEP_GROUP
}
export interface IMergeGroupOptions {
mode?: MergeGroupMode;
}
export type ICloseEditorsFilter = {
except?: IEditorInput,
......@@ -169,13 +179,18 @@ export interface INextEditorGroupsService {
moveGroup(group: INextEditorGroup | GroupIdentifier, location: INextEditorGroup | GroupIdentifier, direction: GroupDirection): INextEditorGroup;
/**
* Merging a group will take any opened editor of that group and move them
* into the target. After that, the group will be removed.
* Merge the editors of a group into a target group. By default, all editors will
* move and the source group will close. This behaviour can be configured via the
* `IMergeGroupOptions` options.
*
* @param group the group to merge
* @param target the target group to merge into
* @param options controls how the merge should be performed. by default all editors
* will be moved over to the target and the source group will close. Configure to
* `MOVE_EDITORS_KEEP_GROUP` to prevent the source group from closing. Set to
* `COPY_EDITORS` to copy the editors into the target instead of moding them.
*/
mergeGroup(group: INextEditorGroup | GroupIdentifier, target: INextEditorGroup | GroupIdentifier): void;
mergeGroup(group: INextEditorGroup | GroupIdentifier, target: INextEditorGroup | GroupIdentifier, options?: IMergeGroupOptions): INextEditorGroup;
/**
* Copy a group to a new group in the editor area.
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册