From 30018013c6f2d11a08ada8d03325fbf5075a7009 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 16 Apr 2019 13:06:16 +0000 Subject: [PATCH] Work in progress --- src/vs/base/browser/ui/list/listWidget.ts | 2 +- .../browser/parts/quickinput/quickInput.ts | 15 ++ .../parts/quickinput/quickInputList.ts | 6 + .../dialogs/browser/fileDialogService.ts | 50 ++++-- .../dialogs/browser/remoteFileDialog.ts | 148 ++++++++++-------- 5 files changed, 143 insertions(+), 78 deletions(-) diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 5babdbf0d72..63c4d076ae9 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -610,7 +610,7 @@ export class MouseController implements IDisposable { this.list.setFocus([focus], e.browserEvent); - if (!isMouseRightClick(e.browserEvent)) { + if (!isMouseRightClick(e.browserEvent) && !e.browserEvent.ctrlKey) { this.list.setSelection([focus], e.browserEvent); if (this.openController.shouldOpen(e.browserEvent)) { diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.ts b/src/vs/workbench/browser/parts/quickinput/quickInput.ts index 2b4af5a46a1..ad44c508dfc 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInput.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.ts @@ -1105,6 +1105,21 @@ export class QuickInputService extends Component implements IQuickInputService { this.ui.inputBox.setAttribute('aria-activedescendant', this.ui.list.getActiveDescendant() || ''); } })); + this._register(list.onCtrlClickElement(() => { + if (this.controller instanceof QuickPick) { + if (!this.controller.canSelectMany) { + this.controller.canSelectMany = true; + } else { + // Defer to avoid the list reacting to the selection change + setTimeout(() => { + console.log('SETTING!!!!!'); + if (this.controller instanceof QuickPick) { + this.controller.canSelectMany = false; + } + }, 0); + } + } + })); const focusTracker = dom.trackFocus(container); this._register(focusTracker); diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputList.ts b/src/vs/workbench/browser/parts/quickinput/quickInputList.ts index 78c08462c11..751fe43a4fd 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputList.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInputList.ts @@ -234,6 +234,8 @@ export class QuickInputList { onButtonTriggered = this._onButtonTriggered.event; private _onLeave = new Emitter(); onLeave: Event = this._onLeave.event; + private _onCtrlClickElement = new Emitter(); + onCtrlClickElement = this._onCtrlClickElement.event; private _fireCheckedEvents = true; private elementDisposables: IDisposable[] = []; private disposables: IDisposable[] = []; @@ -287,6 +289,10 @@ export class QuickInputList { // Works around / fixes #64350. e.browserEvent.preventDefault(); } + if (e.browserEvent.ctrlKey) { + this.list.setSelection([]); + this._onCtrlClickElement.fire(); + } })); this.disposables.push(dom.addDisposableListener(this.container, dom.EventType.CLICK, e => { if (e.x || e.y) { // Avoid 'click' triggered by 'space' on checkbox. diff --git a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts index 4d50a0b8ff0..6484950496b 100644 --- a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts @@ -103,12 +103,21 @@ export class FileDialogService implements IFileDialogService { if (this.shouldUseSimplified(schema)) { const title = nls.localize('openFileOrFolder.title', 'Open File Or Folder'); const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well - return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uri => { - if (uri) { - return (this.fileService.resolve(uri)).then(stat => { - const toOpen: IURIToOpen = stat.isDirectory ? { folderUri: uri } : { fileUri: uri }; - return this.windowService.openWindow([toOpen], { forceNewWindow: options.forceNewWindow }); + return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uris => { + if (uris) { + const promises: Promise[] = []; + uris.forEach(uri => { + promises.push(this.fileService.resolve(uri).then(stat => { + return stat.isDirectory ? { folderUri: uri } : { fileUri: uri }; + })); }); + return Promise.all(promises).then(files => { + return this.windowService.openWindow(files, { forceNewWindow: options.forceNewWindow }); + }); + // return (this.fileService.resolve(uri)).then(stat => { + // const toOpen: IURIToOpen = stat.isDirectory ? { folderUri: uri } : { fileUri: uri }; + // return this.windowService.openWindow([toOpen], { forceNewWindow: options.forceNewWindow }); + // }); } return undefined; }); @@ -127,9 +136,12 @@ export class FileDialogService implements IFileDialogService { if (this.shouldUseSimplified(schema)) { const title = nls.localize('openFile.title', 'Open File'); const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well - return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uri => { - if (uri) { - return this.windowService.openWindow([{ fileUri: uri }], { forceNewWindow: options.forceNewWindow }); + return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uris => { + if (uris) { + const files: IURIToOpen[] = uris.map(uri => { + return { fileUri: uri }; + }); + return this.windowService.openWindow(files, { forceNewWindow: options.forceNewWindow }); } return undefined; }); @@ -148,9 +160,12 @@ export class FileDialogService implements IFileDialogService { if (this.shouldUseSimplified(schema)) { const title = nls.localize('openFolder.title', 'Open Folder'); const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well - return this.pickRemoteResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uri => { - if (uri) { - return this.windowService.openWindow([{ folderUri: uri }], { forceNewWindow: options.forceNewWindow }); + return this.pickRemoteResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uris => { + if (uris) { + const folders: IURIToOpen[] = uris.map(uri => { + return { folderUri: uri }; + }); + return this.windowService.openWindow(folders, { forceNewWindow: options.forceNewWindow }); } return undefined; }); @@ -170,9 +185,12 @@ export class FileDialogService implements IFileDialogService { const title = nls.localize('openWorkspace.title', 'Open Workspace'); const filters: FileFilter[] = [{ name: nls.localize('filterName.workspace', 'Workspace'), extensions: [WORKSPACE_EXTENSION] }]; const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well - return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems }).then(uri => { - if (uri) { - return this.windowService.openWindow([{ workspaceUri: uri }], { forceNewWindow: options.forceNewWindow }); + return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems }).then(uris => { + if (uris) { + const workspaces: IURIToOpen[] = uris.map(uri => { + return { workspaceUri: uri }; + }); + return this.windowService.openWindow(workspaces, { forceNewWindow: options.forceNewWindow }); } return undefined; }); @@ -215,7 +233,7 @@ export class FileDialogService implements IFileDialogService { options.availableFileSystems = [schema]; // by default only allow loading in the own file system } return this.pickRemoteResource(options).then(uri => { - return uri ? [uri] : undefined; + return uri; }); } @@ -246,7 +264,7 @@ export class FileDialogService implements IFileDialogService { return this.windowService.showOpenDialog(newOptions).then(result => result ? result.map(URI.file) : undefined); } - private pickRemoteResource(options: IOpenDialogOptions): Promise { + private pickRemoteResource(options: IOpenDialogOptions): Promise { const remoteFileDialog = this.instantiationService.createInstance(RemoteFileDialog); return remoteFileDialog.showOpenDialog(options); } diff --git a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts index 36152cd4c4f..3a0c00864b6 100644 --- a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts @@ -78,7 +78,7 @@ export class RemoteFileDialog { this.contextKey = RemoteFileDialogContext.bindTo(contextKeyService); } - public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { + public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { this.scheme = this.getScheme(options.defaultUri, options.availableFileSystems); this.userHome = await this.getUserHome(); const newOptions = await this.getOptions(options); @@ -103,7 +103,7 @@ export class RemoteFileDialog { return new Promise((resolve) => { this.pickResource(true).then(folderUri => { - resolve(folderUri); + resolve(folderUri ? folderUri[0] : undefined); }); }); } @@ -145,7 +145,7 @@ export class RemoteFileDialog { return URI.from({ scheme: this.scheme, path: this.environmentService.userHome }); } - private async pickResource(isSave: boolean = false): Promise { + private async pickResource(isSave: boolean = false): Promise { this.allowFolderSelection = !!this.options.canSelectFolders; this.allowFileSelection = !!this.options.canSelectFiles; this.hidden = false; @@ -175,7 +175,7 @@ export class RemoteFileDialog { } } - return new Promise(async (resolve) => { + return new Promise(async (resolve) => { this.filePickBox = this.quickInputService.createQuickPick(); this.filePickBox.matchOnLabel = false; this.filePickBox.autoFocusOnList = false; @@ -198,13 +198,13 @@ export class RemoteFileDialog { this.currentFolder = homedir; this.userEnteredPathSegment = ''; this.autoCompletePathSegment = ''; - + // this.filePickBox.canSelectMany = true; this.filePickBox.title = this.options.title; this.filePickBox.value = this.pathFromUri(this.currentFolder, true); this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length]; this.filePickBox.items = []; - function doResolve(dialog: RemoteFileDialog, uri: URI | undefined) { + function doResolve(dialog: RemoteFileDialog, uri: URI[] | undefined) { resolve(uri); dialog.contextKey.set(false); dialog.filePickBox.dispose(); @@ -223,11 +223,11 @@ export class RemoteFileDialog { this.options.defaultUri = undefined; if (this.requiresTrailing) { return this.fileDialogService.showSaveDialog(this.options).then(result => { - doResolve(this, result); + doResolve(this, result ? [result] : undefined); }); } else { return this.fileDialogService.showOpenDialog(this.options).then(result => { - doResolve(this, result ? result[0] : undefined); + doResolve(this, result); }); } }); @@ -254,11 +254,18 @@ export class RemoteFileDialog { this.filePickBox.onDidChangeActive(i => { isAcceptHandled = false; // update input box to match the first selected item - if ((i.length === 1) && this.isChangeFromUser()) { + if (this.isChangeFromUser()) { this.filePickBox.validationMessage = undefined; - this.setAutoComplete(this.constructFullUserPath(), this.userEnteredPathSegment, i[0], true); + if (i.length === 1) { + this.setAutoComplete(this.constructFullUserPath(), this.userEnteredPathSegment, i[0], true); + } else { + this.setAutoComplete(this.constructFullUserPath(), '', undefined); + } } }); + this.filePickBox.onDidChangeSelection(i => { + this.setAutoComplete(this.constructFullUserPath(), '', undefined); + }); this.filePickBox.onDidChangeValue(async value => { // onDidChangeValue can also be triggered by the auto complete, so if it looks like the auto complete, don't do anything @@ -314,8 +321,8 @@ export class RemoteFileDialog { return this.pathAppend(this.currentFolder, this.userEnteredPathSegment); } - private async onDidAccept(): Promise { - let resolveValue: URI | undefined; + private async onDidAccept(): Promise { + let resolveValue: URI[] | undefined; let navigateValue: URI | undefined; const trimmedPickBoxValue = ((this.filePickBox.value.length > 1) && this.endsWithSlash(this.filePickBox.value)) ? this.filePickBox.value.substr(0, this.filePickBox.value.length - 1) : this.filePickBox.value; const inputUri = this.remoteUriFrom(trimmedPickBoxValue); @@ -330,11 +337,16 @@ export class RemoteFileDialog { } // Find resolve value - if (this.filePickBox.activeItems.length === 0) { + if (this.filePickBox.selectedItems && this.filePickBox.selectedItems.length > 0) { + resolveValue = []; + this.filePickBox.selectedItems.forEach(selectedItem => { + resolveValue!.push(selectedItem.uri); + }); + } else if (this.filePickBox.activeItems.length === 0) { if (!this.requiresTrailing && resources.isEqual(this.currentFolder, inputUri, true)) { - resolveValue = inputUri; + resolveValue = [inputUri]; } else if (statDirname && statDirname.isDirectory) { - resolveValue = inputUri; + resolveValue = [inputUri]; } else if (stat && stat.isDirectory) { navigateValue = inputUri; } @@ -342,7 +354,7 @@ export class RemoteFileDialog { const item = this.filePickBox.selectedItems[0]; if (item) { if (!item.isFolder) { - resolveValue = item.uri; + resolveValue = [item.uri]; } else { navigateValue = item.uri; } @@ -350,7 +362,9 @@ export class RemoteFileDialog { } if (resolveValue) { - resolveValue = this.addPostfix(resolveValue); + if (resolveValue.length === 1) { + resolveValue[0] = this.addPostfix(resolveValue[0]); + } if (await this.validate(resolveValue)) { return Promise.resolve(resolveValue); } @@ -434,8 +448,14 @@ export class RemoteFileDialog { } } - private setAutoComplete(startingValue: string, startingBasename: string, quickPickItem: FileQuickPickItem, force: boolean = false): boolean { - if (this.filePickBox.busy) { + private setAutoComplete(startingValue: string, startingBasename: string, quickPickItem: FileQuickPickItem | undefined, force: boolean = false): boolean { + if (!quickPickItem) { + this.autoCompletePathSegment = ''; + this.userEnteredPathSegment = ''; + this.filePickBox.valueSelection = [startingValue.length, this.filePickBox.value.length]; + this.insertText(startingValue, ''); + return false; + } else if (this.filePickBox.busy) { // We're in the middle of something else. Doing an auto complete now can result jumbled or incorrect autocompletes. this.userEnteredPathSegment = startingBasename; this.autoCompletePathSegment = ''; @@ -536,49 +556,55 @@ export class RemoteFileDialog { }); } - private async validate(uri: URI): Promise { - let stat: IFileStat | undefined; - let statDirname: IFileStat | undefined; - try { - statDirname = await this.fileService.resolve(resources.dirname(uri)); - stat = await this.fileService.resolve(uri); - } catch (e) { - // do nothing - } - - if (this.requiresTrailing) { // save - if (stat && stat.isDirectory) { - // Can't do this - this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateFolder', 'The folder already exists. Please use a new file name.'); - return Promise.resolve(false); - } else if (stat && !this.shouldOverwriteFile) { - // Replacing a file. - this.shouldOverwriteFile = true; - // Show a yes/no prompt - const message = nls.localize('remoteFileDialog.validateExisting', '{0} already exists. Are you sure you want to overwrite it?', resources.basename(uri)); - return this.yesNoPrompt(message); - } else if (!this.isValidBaseName(resources.basename(uri))) { - // Filename not allowed - this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateBadFilename', 'Please enter a valid file name.'); - return Promise.resolve(false); - } else if (!statDirname || !statDirname.isDirectory) { - // Folder to save in doesn't exist - this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateNonexistentDir', 'Please enter a path that exists.'); - return Promise.resolve(false); + private async validate(uri: URI[]): Promise { + for (let i = 0; i < uri.length; i++) { + let stat: IFileStat | undefined; + let statDirname: IFileStat | undefined; + try { + statDirname = await this.fileService.resolve(resources.dirname(uri[i])); + stat = await this.fileService.resolve(uri[i]); + } catch (e) { + // do nothing } - } else { // open - if (!stat) { - // File or folder doesn't exist - this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateNonexistentDir', 'Please enter a path that exists.'); - return Promise.resolve(false); - } else if (stat.isDirectory && !this.allowFolderSelection) { - // Folder selected when folder selection not permitted - this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateFileOnly', 'Please select a file.'); - return Promise.resolve(false); - } else if (!stat.isDirectory && !this.allowFileSelection) { - // File selected when file selection not permitted - this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateFolderOnly', 'Please select a folder.'); - return Promise.resolve(false); + + if (this.requiresTrailing) { // save + if (uri.length > 1) { + // The uri array can only have one value in it. Something has gone very wrong otherwise. + this.filePickBox.validationMessage = nls.localize('remoteFileDialog.tooMany', 'Can only Save As one item.'); + return Promise.resolve(false); + } else if (stat && stat.isDirectory) { + // Can't do this + this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateFolder', 'The folder already exists. Please use a new file name.'); + return Promise.resolve(false); + } else if (stat && !this.shouldOverwriteFile) { + // Replacing a file. + this.shouldOverwriteFile = true; + // Show a yes/no prompt + const message = nls.localize('remoteFileDialog.validateExisting', '{0} already exists. Are you sure you want to overwrite it?', resources.basename(uri[i])); + return this.yesNoPrompt(message); + } else if (!this.isValidBaseName(resources.basename(uri[i]))) { + // Filename not allowed + this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateBadFilename', 'Please enter a valid file name.'); + return Promise.resolve(false); + } else if (!statDirname || !statDirname.isDirectory) { + // Folder to save in doesn't exist + this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateNonexistentDir', 'Please enter a path that exists.'); + return Promise.resolve(false); + } + } else { // open + if (!stat) { + // File or folder doesn't exist + this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateNonexistentDir', 'Please enter a path that exists.'); + return Promise.resolve(false); + } else if (stat.isDirectory && !this.allowFolderSelection) { + // Folder selected when folder selection not permitted + this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateFileOnly', 'Please select a file.'); + return Promise.resolve(false); + } else if (!stat.isDirectory && !this.allowFileSelection) { + // File selected when file selection not permitted + this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateFolderOnly', 'Please select a folder.'); + return Promise.resolve(false); + } } } return Promise.resolve(true); -- GitLab