in patched-vscode/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts [870:1182]
private registerTabListeners(tab: HTMLElement, tabIndex: number, tabsContainer: HTMLElement, tabsScrollbar: ScrollableElement): IDisposable {
const disposables = new DisposableStore();
const handleClickOrTouch = async (e: MouseEvent | GestureEvent, preserveFocus: boolean): Promise<void> => {
tab.blur(); // prevent flicker of focus outline on tab until editor got focus
if (isMouseEvent(e) && (e.button !== 0 /* middle/right mouse button */ || (isMacintosh && e.ctrlKey /* macOS context menu */))) {
if (e.button === 1) {
e.preventDefault(); // required to prevent auto-scrolling (https://github.com/microsoft/vscode/issues/16690)
}
return;
}
if (this.originatesFromTabActionBar(e)) {
return; // not when clicking on actions
}
// Open tabs editor
const editor = this.tabsModel.getEditorByIndex(tabIndex);
if (editor) {
if (e.shiftKey) {
let anchor: EditorInput;
if (this.lastSingleSelectSelectedEditor && this.tabsModel.isSelected(this.lastSingleSelectSelectedEditor)) {
// The last selected editor is the anchor
anchor = this.lastSingleSelectSelectedEditor;
} else {
// The active editor is the anchor
const activeEditor = assertIsDefined(this.groupView.activeEditor);
this.lastSingleSelectSelectedEditor = activeEditor;
anchor = activeEditor;
}
await this.selectEditorsBetween(editor, anchor);
} else if ((e.ctrlKey && !isMacintosh) || (e.metaKey && isMacintosh)) {
if (this.tabsModel.isSelected(editor)) {
await this.unselectEditor(editor);
} else {
await this.selectEditor(editor);
this.lastSingleSelectSelectedEditor = editor;
}
} else {
// Even if focus is preserved make sure to activate the group.
// If a new active editor is selected, keep the current selection on key
// down such that drag and drop can operate over the selection. The selection
// is removed on key up in this case.
const inactiveSelection = this.tabsModel.isSelected(editor) ? this.groupView.selectedEditors.filter(e => !e.matches(editor)) : [];
await this.groupView.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }, { inactiveSelection, focusTabControl: true });
}
}
};
const showContextMenu = (e: Event) => {
EventHelper.stop(e);
const editor = this.tabsModel.getEditorByIndex(tabIndex);
if (editor) {
this.onTabContextMenu(editor, e, tab);
}
};
// Open on Click / Touch
disposables.add(addDisposableListener(tab, EventType.MOUSE_DOWN, e => handleClickOrTouch(e, false)));
disposables.add(addDisposableListener(tab, TouchEventType.Tap, (e: GestureEvent) => handleClickOrTouch(e, true))); // Preserve focus on touch #125470
// Touch Scroll Support
disposables.add(addDisposableListener(tab, TouchEventType.Change, (e: GestureEvent) => {
tabsScrollbar.setScrollPosition({ scrollLeft: tabsScrollbar.getScrollPosition().scrollLeft - e.translationX });
}));
// Update selection & prevent flicker of focus outline on tab until editor got focus
disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, async e => {
EventHelper.stop(e);
tab.blur();
if (isMouseEvent(e) && (e.button !== 0 /* middle/right mouse button */ || (isMacintosh && e.ctrlKey /* macOS context menu */))) {
return;
}
if (this.originatesFromTabActionBar(e)) {
return; // not when clicking on actions
}
const isCtrlCmd = (e.ctrlKey && !isMacintosh) || (e.metaKey && isMacintosh);
if (!isCtrlCmd && !e.shiftKey && this.groupView.selectedEditors.length > 1) {
await this.unselectAllEditors();
}
}));
// Close on mouse middle click
disposables.add(addDisposableListener(tab, EventType.AUXCLICK, e => {
if (e.button === 1 /* Middle Button*/) {
EventHelper.stop(e, true /* for https://github.com/microsoft/vscode/issues/56715 */);
const editor = this.tabsModel.getEditorByIndex(tabIndex);
if (editor) {
if (preventEditorClose(this.tabsModel, editor, EditorCloseMethod.MOUSE, this.groupsView.partOptions)) {
return;
}
this.blockRevealActiveTabOnce();
this.closeEditorAction.run({ groupId: this.groupView.id, editorIndex: this.groupView.getIndexOfEditor(editor) });
}
}
}));
// Context menu on Shift+F10
disposables.add(addDisposableListener(tab, EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e);
if (event.shiftKey && event.keyCode === KeyCode.F10) {
showContextMenu(e);
}
}));
// Context menu on touch context menu gesture
disposables.add(addDisposableListener(tab, TouchEventType.Contextmenu, (e: GestureEvent) => {
showContextMenu(e);
}));
// Keyboard accessibility
disposables.add(addDisposableListener(tab, EventType.KEY_UP, e => {
const event = new StandardKeyboardEvent(e);
let handled = false;
// Run action on Enter/Space
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
handled = true;
const editor = this.tabsModel.getEditorByIndex(tabIndex);
if (editor) {
this.groupView.openEditor(editor);
}
}
// Navigate in editors
else if ([KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.Home, KeyCode.End].some(kb => event.equals(kb))) {
let editorIndex = this.toEditorIndex(tabIndex);
if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.UpArrow)) {
editorIndex = editorIndex - 1;
} else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.DownArrow)) {
editorIndex = editorIndex + 1;
} else if (event.equals(KeyCode.Home)) {
editorIndex = 0;
} else {
editorIndex = this.groupView.count - 1;
}
const target = this.groupView.getEditorByIndex(editorIndex);
if (target) {
handled = true;
this.groupView.openEditor(target, { preserveFocus: true }, { focusTabControl: true });
}
}
if (handled) {
EventHelper.stop(e, true);
}
// moving in the tabs container can have an impact on scrolling position, so we need to update the custom scrollbar
tabsScrollbar.setScrollPosition({
scrollLeft: tabsContainer.scrollLeft
});
}));
// Double click: either pin or toggle maximized
for (const eventType of [TouchEventType.Tap, EventType.DBLCLICK]) {
disposables.add(addDisposableListener(tab, eventType, (e: MouseEvent | GestureEvent) => {
if (eventType === EventType.DBLCLICK) {
EventHelper.stop(e);
} else if ((<GestureEvent>e).tapCount !== 2) {
return; // ignore single taps
}
const editor = this.tabsModel.getEditorByIndex(tabIndex);
if (editor && this.tabsModel.isPinned(editor)) {
switch (this.groupsView.partOptions.doubleClickTabToToggleEditorGroupSizes) {
case 'maximize':
this.groupsView.toggleMaximizeGroup(this.groupView);
break;
case 'expand':
this.groupsView.toggleExpandGroup(this.groupView);
break;
case 'off':
break;
}
} else {
this.groupView.pinEditor(editor);
}
}));
}
// Context menu
disposables.add(addDisposableListener(tab, EventType.CONTEXT_MENU, e => {
EventHelper.stop(e, true);
const editor = this.tabsModel.getEditorByIndex(tabIndex);
if (editor) {
this.onTabContextMenu(editor, e, tab);
}
}, true /* use capture to fix https://github.com/microsoft/vscode/issues/19145 */));
// Drag & Drop support
let lastDragEvent: DragEvent | undefined = undefined;
let isNewWindowOperation = false;
disposables.add(new DragAndDropObserver(tab, {
onDragStart: e => {
const editor = this.tabsModel.getEditorByIndex(tabIndex);
if (!editor) {
return;
}
isNewWindowOperation = this.isNewWindowOperation(e);
const selectedEditors = this.groupView.selectedEditors;
this.editorTransfer.setData(selectedEditors.map(e => new DraggedEditorIdentifier({ editor: e, groupId: this.groupView.id })), DraggedEditorIdentifier.prototype);
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'copyMove';
if (selectedEditors.length > 1) {
const label = `${editor.getName()} + ${selectedEditors.length - 1}`;
applyDragImage(e, label, 'monaco-editor-group-drag-image', this.getColor(listActiveSelectionBackground), this.getColor(listActiveSelectionForeground));
} else {
e.dataTransfer.setDragImage(tab, 0, 0); // top left corner of dragged tab set to cursor position to make room for drop-border feedback
}
}
// Apply some datatransfer types to allow for dragging the element outside of the application
this.doFillResourceDataTransfers([editor], e, isNewWindowOperation);
scheduleAtNextAnimationFrame(getWindow(this.parent), () => this.updateDropFeedback(tab, false, e, tabIndex));
},
onDrag: e => {
lastDragEvent = e;
},
onDragEnter: e => {
// Return if transfer is unsupported
if (!this.isSupportedDropTransfer(e)) {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'none';
}
return;
}
// Update the dropEffect to "copy" if there is no local data to be dragged because
// in that case we can only copy the data into and not move it from its source
if (!this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'copy';
}
}
this.updateDropFeedback(tab, true, e, tabIndex);
},
onDragOver: (e, dragDuration) => {
if (dragDuration >= MultiEditorTabsControl.DRAG_OVER_OPEN_TAB_THRESHOLD) {
const draggedOverTab = this.tabsModel.getEditorByIndex(tabIndex);
if (draggedOverTab && this.tabsModel.activeEditor !== draggedOverTab) {
this.groupView.openEditor(draggedOverTab, { preserveFocus: true });
}
}
this.updateDropFeedback(tab, true, e, tabIndex);
},
onDragEnd: async e => {
this.updateDropFeedback(tab, false, e, tabIndex);
const draggedEditors = this.editorTransfer.getData(DraggedEditorIdentifier.prototype);
this.editorTransfer.clearData(DraggedEditorIdentifier.prototype);
if (
!isNewWindowOperation ||
isWindowDraggedOver() ||
!draggedEditors ||
draggedEditors.length === 0
) {
return; // drag to open in new window is disabled
}
const auxiliaryEditorPart = await this.maybeCreateAuxiliaryEditorPartAt(e, tab);
if (!auxiliaryEditorPart) {
return;
}
const targetGroup = auxiliaryEditorPart.activeGroup;
const editors = draggedEditors.map(de => ({ editor: de.identifier.editor }));
if (this.isMoveOperation(lastDragEvent ?? e, targetGroup.id, draggedEditors[0].identifier.editor)) {
this.groupView.moveEditors(editors, targetGroup);
} else {
this.groupView.copyEditors(editors, targetGroup);
}
targetGroup.focus();
},
onDrop: e => {
this.updateDropFeedback(tab, false, e, tabIndex);
// compute the target index
let targetIndex = tabIndex;
if (this.getTabDragOverLocation(e, tab) === 'right') {
targetIndex++;
}
this.onDrop(e, targetIndex, tabsContainer);
}
}));
return disposables;
}