Skip to content

Commit

Permalink
Support tree view drag and drop. Fixes eclipse-theia#11778, Fixes ecl…
Browse files Browse the repository at this point in the history
…ipse-theia#11776

Adds support for contributing drag and drop controller in the tree view
plugin API

Contributed on behalf of STMicroelectronics

Signed-off-by: Thomas Mäder <[email protected]>
  • Loading branch information
tsmaeder committed Jan 12, 2023
1 parent 73f32f7 commit 6789039
Show file tree
Hide file tree
Showing 13 changed files with 625 additions and 78 deletions.
12 changes: 10 additions & 2 deletions packages/core/src/browser/frontend-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,18 @@ export class FrontendApplication {
document.body.addEventListener('wheel', preventNavigation, { passive: false });
}
// Prevent the default browser behavior when dragging and dropping files into the window.
window.addEventListener('dragover', event => {
document.addEventListener('dragenter', event => {
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'none';
}
event.preventDefault();
}, false);
window.addEventListener('drop', event => {
document.addEventListener('dragover', event => {
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'none';
} event.preventDefault();
}, false);
document.addEventListener('drop', event => {
event.preventDefault();
}, false);

Expand Down
65 changes: 65 additions & 0 deletions packages/core/src/browser/shell/application-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import { Deferred } from '../../common/promise-util';
import { SaveResourceService } from '../save-resource-service';
import { nls } from '../../common/nls';
import { SecondaryWindowHandler } from '../secondary-window-handler';
import URI from '../../common/uri';
import { OpenerService } from '../opener-service';

/** The class name added to ApplicationShell instances. */
const APPLICATION_SHELL_CLASS = 'theia-ApplicationShell';
Expand Down Expand Up @@ -190,10 +192,14 @@ export class ApplicationShell extends Widget {

private readonly tracker = new FocusTracker<Widget>();
private dragState?: WidgetDragState;
additionalDraggedUris: URI[] | undefined;

@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;

@inject(OpenerService)
protected readonly openerService: OpenerService;

protected readonly onDidAddWidgetEmitter = new Emitter<Widget>();
readonly onDidAddWidget = this.onDidAddWidgetEmitter.event;
protected fireDidAddWidget(widget: Widget): void {
Expand Down Expand Up @@ -498,9 +504,68 @@ export class ApplicationShell extends Widget {
dockPanel.id = MAIN_AREA_ID;
dockPanel.widgetAdded.connect((_, widget) => this.fireDidAddWidget(widget));
dockPanel.widgetRemoved.connect((_, widget) => this.fireDidRemoveWidget(widget));

const openUri = async (fileUri: URI) => {
try {
const opener = await this.openerService.getOpener(fileUri);
opener.open(fileUri);
} catch (e) {
console.info(`no opener found for '${fileUri}'`);
}
};

dockPanel.node.addEventListener('drop', event => {
if (event.dataTransfer) {
const uris = this.additionalDraggedUris || ApplicationShell.getDraggedEditorUris(event.dataTransfer);
if (uris.length > 0) {
uris.forEach(openUri);
} else if (event.dataTransfer.files?.length > 0) {
// the files were dragged from the outside the workspace
Array.from(event.dataTransfer.files).forEach(async file => {
if (file.path) {
const fileUri = URI.fromComponents({
scheme: 'file',
path: file.path,
authority: '',
query: '',
fragment: ''
});
openUri(fileUri);
}
});
}
}
});
const handler = (e: DragEvent) => {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'link';
e.preventDefault();
e.stopPropagation();
}
};
dockPanel.node.addEventListener('dragover', handler);
dockPanel.node.addEventListener('dragenter', handler);

return dockPanel;
}

addAdditionalDraggedEditorUris(uris: URI[]): void {
this.additionalDraggedUris = uris;
}

clearAdditionalDraggedEditorUris(): void {
this.additionalDraggedUris = undefined;
}

static getDraggedEditorUris(dataTransfer: DataTransfer): URI[] {
const data = dataTransfer.getData('theia-editor-dnd');
return data ? data.split('\n').map(entry => new URI(entry)) : [];
}

static setDraggedEditorUris(dataTransfer: DataTransfer, uris: URI[]): void {
dataTransfer.setData('theia-editor-dnd', uris.map(uri => uri.toString()).join('\r\n'));
}

/**
* Create the dock panel in the bottom shell area.
*/
Expand Down
19 changes: 12 additions & 7 deletions packages/filesystem/src/browser/file-tree/file-tree-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { FileUploadService } from '../file-upload-service';
import { DirNode, FileStatNode, FileStatNodeData } from './file-tree';
import { FileTreeModel } from './file-tree-model';
import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service';
import { ApplicationShell } from '@theia/core/lib/browser/shell';
import { FileStat, FileType } from '../../common/files';
import { isOSX } from '@theia/core';

Expand Down Expand Up @@ -119,14 +120,18 @@ export class FileTreeWidget extends CompressedTreeWidget {

protected handleDragStartEvent(node: TreeNode, event: React.DragEvent): void {
event.stopPropagation();
let selectedNodes;
if (this.model.selectedNodes.find(selected => TreeNode.equals(selected, node))) {
selectedNodes = [...this.model.selectedNodes];
} else {
selectedNodes = [node];
}
this.setSelectedTreeNodesAsData(event.dataTransfer, node, selectedNodes);
if (event.dataTransfer) {
let selectedNodes;
if (this.model.selectedNodes.find(selected => TreeNode.equals(selected, node))) {
selectedNodes = [...this.model.selectedNodes];
} else {
selectedNodes = [node];
}
this.setSelectedTreeNodesAsData(event.dataTransfer, node, selectedNodes);
const uris = selectedNodes.filter(n => FileStatNode.is(n)).map(n => (n as FileStatNode).fileStat.resource);
if (uris.length > 0) {
ApplicationShell.setDraggedEditorUris(event.dataTransfer, uris);
}
let label: string;
if (selectedNodes.length === 1) {
label = this.toNodeName(node);
Expand Down
39 changes: 2 additions & 37 deletions packages/navigator/src/browser/navigator-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,14 @@ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'
import { Message } from '@theia/core/shared/@phosphor/messaging';
import URI from '@theia/core/lib/common/uri';
import { CommandService } from '@theia/core/lib/common';
import { Key, TreeModel, SelectableTreeNode, OpenerService, ContextMenuRenderer, ExpandableTreeNode, TreeProps, TreeNode } from '@theia/core/lib/browser';
import { FileNode, DirNode } from '@theia/filesystem/lib/browser';
import { Key, TreeModel, ContextMenuRenderer, ExpandableTreeNode, TreeProps, TreeNode } from '@theia/core/lib/browser';
import { DirNode } from '@theia/filesystem/lib/browser';
import { WorkspaceService, WorkspaceCommands } from '@theia/workspace/lib/browser';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WorkspaceNode, WorkspaceRootNode } from './navigator-tree';
import { FileNavigatorModel } from './navigator-model';
import { isOSX, environment } from '@theia/core';
import * as React from '@theia/core/shared/react';
import { NavigatorContextKeyService } from './navigator-context-key-service';
import { FileNavigatorCommands } from './file-navigator-commands';
import { nls } from '@theia/core/lib/common/nls';
import { AbstractNavigatorTreeWidget } from './abstract-navigator-tree-widget';

Expand All @@ -38,10 +36,8 @@ export const CLASS = 'theia-Files';
@injectable()
export class FileNavigatorWidget extends AbstractNavigatorTreeWidget {

@inject(ApplicationShell) protected readonly shell: ApplicationShell;
@inject(CommandService) protected readonly commandService: CommandService;
@inject(NavigatorContextKeyService) protected readonly contextKeyService: NavigatorContextKeyService;
@inject(OpenerService) protected readonly openerService: OpenerService;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;

constructor(
Expand Down Expand Up @@ -97,36 +93,6 @@ export class FileNavigatorWidget extends AbstractNavigatorTreeWidget {
}
}

protected enableDndOnMainPanel(): void {
const mainPanelNode = this.shell.mainPanel.node;
this.addEventListener(mainPanelNode, 'drop', async ({ dataTransfer }) => {
const treeNodes = dataTransfer && this.getSelectedTreeNodesFromData(dataTransfer) || [];
if (treeNodes.length > 0) {
treeNodes.filter(FileNode.is).forEach(treeNode => {
if (!SelectableTreeNode.isSelected(treeNode)) {
this.model.toggleNode(treeNode);
}
});
this.commandService.executeCommand(FileNavigatorCommands.OPEN.id);
} else if (dataTransfer && dataTransfer.files?.length > 0) {
// the files were dragged from the outside the workspace
Array.from(dataTransfer.files).forEach(async file => {
const fileUri = new URI(file.path);
const opener = await this.openerService.getOpener(fileUri);
opener.open(fileUri);
});
}
});
const handler = (e: DragEvent) => {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'link';
e.preventDefault();
}
};
this.addEventListener(mainPanelNode, 'dragover', handler);
this.addEventListener(mainPanelNode, 'dragenter', handler);
}

override getContainerTreeNode(): TreeNode | undefined {
const root = this.model.root;
if (this.workspaceService.isMultiRootWorkspaceOpened) {
Expand All @@ -153,7 +119,6 @@ export class FileNavigatorWidget extends AbstractNavigatorTreeWidget {
super.onAfterAttach(msg);
this.addClipboardListener(this.node, 'copy', e => this.handleCopy(e));
this.addClipboardListener(this.node, 'paste', e => this.handlePaste(e));
this.enableDndOnMainPanel();
}

protected handleCopy(event: ClipboardEvent): void {
Expand Down
12 changes: 11 additions & 1 deletion packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,16 +726,26 @@ export interface TreeViewRevealOptions {
}

export interface TreeViewsMain {
$registerTreeDataProvider(treeViewId: string): void;
$registerTreeDataProvider(treeViewId: string, dragMimetypes: string[] | undefined, dropMimetypes: string[] | undefined): void;
$readDroppedFile(contentId: string): Promise<BinaryBuffer>;
$unregisterTreeDataProvider(treeViewId: string): void;
$refresh(treeViewId: string): Promise<void>;
$reveal(treeViewId: string, elementParentChain: string[], options: TreeViewRevealOptions): Promise<any>;
$setMessage(treeViewId: string, message: string): void;
$setTitle(treeViewId: string, title: string): void;
$setDescription(treeViewId: string, description: string): void;
}
export class DataTransferFileDTO {
constructor(readonly name: string, readonly contentId: string, readonly uri?: UriComponents) { }

static is(value: string | DataTransferFileDTO): value is DataTransferFileDTO {
return !(typeof value === 'string');
}
}

export interface TreeViewsExt {
$dragStarted(treeViewId: string, treeItemIds: string[], token: CancellationToken): Promise<UriComponents[] | undefined>;
$drop(treeViewId: string, treeItemId: string | undefined, dataTransferItems: [string, string | DataTransferFileDTO][], token: CancellationToken): Promise<void>;
$getChildren(treeViewId: string, treeItemId: string | undefined): Promise<TreeViewItem[] | undefined>;
$hasResolveTreeItem(treeViewId: string): Promise<boolean>;
$resolveTreeItem(treeViewId: string, treeItemId: string, token: CancellationToken): Promise<TreeViewItem | undefined>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { SelectionProviderCommandContribution } from './selection-provider-comma
import { ViewColumnService } from './view-column-service';
import { ViewContextKeyService } from './view/view-context-key-service';
import { PluginViewWidget, PluginViewWidgetIdentifier } from './view/plugin-view-widget';
import { TreeViewWidgetIdentifier, VIEW_ITEM_CONTEXT_MENU, PluginTree, TreeViewWidget, PluginTreeModel } from './view/tree-view-widget';
import { TreeViewWidgetOptions, VIEW_ITEM_CONTEXT_MENU, PluginTree, TreeViewWidget, PluginTreeModel } from './view/tree-view-widget';
import { RPCProtocol } from '../../common/rpc-protocol';
import { LanguagesMainFactory, OutputChannelRegistryFactory } from '../../common';
import { LanguagesMainImpl } from './languages-main';
Expand Down Expand Up @@ -80,6 +80,7 @@ import { bindTreeViewDecoratorUtilities, TreeViewDecoratorService } from './view
import { CodeEditorWidgetUtil } from './menus/vscode-theia-menu-mappings';
import { PluginMenuCommandAdapter } from './menus/plugin-menu-command-adapter';
import './theme-icon-override';
import { DnDFileContentStore } from './view/dnd-file-content-store';

export default new ContainerModule((bind, unbind, isBound, rebind) => {

Expand Down Expand Up @@ -143,9 +144,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bindTreeViewDecoratorUtilities(bind);
bind(PluginTreeViewNodeLabelProvider).toSelf().inSingletonScope();
bind(LabelProviderContribution).toService(PluginTreeViewNodeLabelProvider);
bind(DnDFileContentStore).toSelf().inSingletonScope();
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: PLUGIN_VIEW_DATA_FACTORY_ID,
createWidget: (identifier: TreeViewWidgetIdentifier) => {
createWidget: (identifier: TreeViewWidgetOptions) => {
const props = {
contextMenuPath: VIEW_ITEM_CONTEXT_MENU,
expandOnlyOnExpansionToggleClick: true,
Expand All @@ -161,7 +163,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
widget: TreeViewWidget,
decoratorService: TreeViewDecoratorService
});
child.bind(TreeViewWidgetIdentifier).toConstantValue(identifier);
child.bind(TreeViewWidgetOptions).toConstantValue(identifier);
return child.get(TreeWidget);
}
})).inSingletonScope();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// *****************************************************************************
// Copyright (C) 2022 ST Microelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************

import { injectable } from '@theia/core/shared/inversify';

@injectable()
export class DnDFileContentStore {
static id: number = 0;
private files: Map<string, File> = new Map();
addFile(f: File): string {
const id = (DnDFileContentStore.id++).toString();
this.files.set(id, f);
return id;
}

removeFile(id: string): boolean {
return this.files.delete(id);
}

getFile(id: string): File {
const file = this.files.get(id);
if (file) {
return file;
}

throw new Error(`File with id ${id} not found in dnd operation`);
}
}
Loading

0 comments on commit 6789039

Please sign in to comment.