未验证 提交 0cd5f797 编写于 作者: B Benjamin Pasero 提交者: GitHub

Merge pull request #109368 from microsoft/ben/login-dialog

Use a custom dialog for proxy authentication
......@@ -211,6 +211,10 @@ export class Button extends Disposable {
focus(): void {
this._element.focus();
}
hasFocus(): boolean {
return this._element === document.activeElement;
}
}
export class ButtonGroup extends Disposable {
......
......@@ -45,6 +45,6 @@
}
/* hide check when unchecked */
.monaco-custom-checkbox.monaco-simple-checkbox.unchecked:not(.checked)::before {
visibility: hidden;;
.monaco-custom-checkbox.monaco-simple-checkbox:not(.checked)::before {
visibility: hidden;
}
......@@ -108,7 +108,9 @@ export class Checkbox extends Widget {
if (this._opts.actionClassName) {
classes.push(this._opts.actionClassName);
}
classes.push(this._checked ? 'checked' : 'unchecked');
if (this._checked) {
classes.push('checked');
}
this.domNode = document.createElement('div');
this.domNode.title = this._opts.title;
......@@ -154,12 +156,9 @@ export class Checkbox extends Widget {
set checked(newIsChecked: boolean) {
this._checked = newIsChecked;
this.domNode.setAttribute('aria-checked', String(this._checked));
if (this._checked) {
this.domNode.classList.add('checked');
} else {
this.domNode.classList.remove('checked');
}
this.domNode.classList.toggle('checked', this._checked);
this.applyStyles();
}
......@@ -230,6 +229,14 @@ export class SimpleCheckbox extends Widget {
this.applyStyles();
}
focus(): void {
this.domNode.focus();
}
hasFocus(): boolean {
return this.domNode === document.activeElement;
}
style(styles: ISimpleCheckboxStyles): void {
this.styles = styles;
......
......@@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/** Dialog: Modal Block */
.monaco-dialog-modal-block {
position: fixed;
......@@ -46,7 +47,6 @@
margin-left: 4px;
}
/** Dialog: Message Row */
.monaco-dialog-box .dialog-message-row {
display: flex;
......@@ -100,6 +100,7 @@
outline-style: solid;
}
/** Dialog: Checkbox */
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-checkbox-row {
padding: 15px 0px 0px;
display: flex;
......@@ -112,6 +113,16 @@
-ms-user-select: none;
}
/** Dialog: Input */
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-input {
padding: 15px 0px 0px;
display: flex;
}
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-input .monaco-inputbox {
flex: 1;
}
/** Dialog: Buttons Row */
.monaco-dialog-box > .dialog-buttons-row {
display: flex;
......
......@@ -18,34 +18,46 @@ import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { isMacintosh, isLinux } from 'vs/base/common/platform';
import { SimpleCheckbox, ISimpleCheckboxStyles } from 'vs/base/browser/ui/checkbox/checkbox';
import { Codicon, registerIcon } from 'vs/base/common/codicons';
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
export interface IDialogInputOptions {
readonly placeholder?: string;
readonly type?: 'text' | 'password';
readonly value?: string;
}
export interface IDialogOptions {
cancelId?: number;
detail?: string;
checkboxLabel?: string;
checkboxChecked?: boolean;
type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending';
keyEventProcessor?: (event: StandardKeyboardEvent) => void;
readonly cancelId?: number;
readonly detail?: string;
readonly checkboxLabel?: string;
readonly checkboxChecked?: boolean;
readonly type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending';
readonly inputs?: IDialogInputOptions[];
readonly keyEventProcessor?: (event: StandardKeyboardEvent) => void;
}
export interface IDialogResult {
button: number;
checkboxChecked?: boolean;
readonly button: number;
readonly checkboxChecked?: boolean;
readonly values?: string[];
}
export interface IDialogStyles extends IButtonStyles, ISimpleCheckboxStyles {
dialogForeground?: Color;
dialogBackground?: Color;
dialogShadow?: Color;
dialogBorder?: Color;
errorIconForeground?: Color;
warningIconForeground?: Color;
infoIconForeground?: Color;
readonly dialogForeground?: Color;
readonly dialogBackground?: Color;
readonly dialogShadow?: Color;
readonly dialogBorder?: Color;
readonly errorIconForeground?: Color;
readonly warningIconForeground?: Color;
readonly infoIconForeground?: Color;
readonly inputBackground?: Color;
readonly inputForeground?: Color;
readonly inputBorder?: Color;
}
interface ButtonMapEntry {
label: string;
index: number;
readonly label: string;
readonly index: number;
}
const dialogErrorIcon = registerIcon('dialog-error', Codicon.error);
......@@ -54,30 +66,30 @@ const dialogInfoIcon = registerIcon('dialog-info', Codicon.info);
const dialogCloseIcon = registerIcon('dialog-close', Codicon.close);
export class Dialog extends Disposable {
private element: HTMLElement | undefined;
private shadowElement: HTMLElement | undefined;
private modal: HTMLElement | undefined;
private buttonsContainer: HTMLElement | undefined;
private messageDetailElement: HTMLElement | undefined;
private iconElement: HTMLElement | undefined;
private checkbox: SimpleCheckbox | undefined;
private toolbarContainer: HTMLElement | undefined;
private readonly element: HTMLElement;
private readonly shadowElement: HTMLElement;
private modalElement: HTMLElement | undefined;
private readonly buttonsContainer: HTMLElement;
private readonly messageDetailElement: HTMLElement;
private readonly iconElement: HTMLElement;
private readonly checkbox: SimpleCheckbox | undefined;
private readonly toolbarContainer: HTMLElement;
private buttonGroup: ButtonGroup | undefined;
private styles: IDialogStyles | undefined;
private focusToReturn: HTMLElement | undefined;
private checkboxHasFocus: boolean = false;
private buttons: string[];
private readonly inputs: InputBox[];
private readonly buttons: string[];
constructor(private container: HTMLElement, private message: string, buttons: string[], private options: IDialogOptions) {
super();
this.modal = this.container.appendChild($(`.monaco-dialog-modal-block${options.type === 'pending' ? '.dimmed' : ''}`));
this.shadowElement = this.modal.appendChild($('.dialog-shadow'));
this.modalElement = this.container.appendChild($(`.monaco-dialog-modal-block${options.type === 'pending' ? '.dimmed' : ''}`));
this.shadowElement = this.modalElement.appendChild($('.dialog-shadow'));
this.element = this.shadowElement.appendChild($('.monaco-dialog-box'));
this.element.setAttribute('role', 'dialog');
hide(this.element);
// If no button is provided, default to OK
this.buttons = buttons.length ? buttons : [nls.localize('ok', "OK")];
this.buttons = buttons.length ? buttons : [nls.localize('ok', "OK")]; // If no button is provided, default to OK
const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row'));
this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons'));
......@@ -94,6 +106,25 @@ export class Dialog extends Disposable {
this.messageDetailElement = messageContainer.appendChild($('.dialog-message-detail'));
this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message;
if (this.options.inputs) {
this.inputs = this.options.inputs.map(input => {
const inputRowElement = messageContainer.appendChild($('.dialog-message-input'));
const inputBox = this._register(new InputBox(inputRowElement, undefined, {
placeholder: input.placeholder,
type: input.type ?? 'text',
}));
if (input.value) {
inputBox.value = input.value;
}
return inputBox;
});
} else {
this.inputs = [];
}
if (this.options.checkboxLabel) {
const checkboxRowElement = messageContainer.appendChild($('.dialog-checkbox-row'));
......@@ -133,75 +164,112 @@ export class Dialog extends Disposable {
}
updateMessage(message: string): void {
if (this.messageDetailElement) {
this.messageDetailElement.innerText = message;
}
this.messageDetailElement.innerText = message;
}
async show(): Promise<IDialogResult> {
this.focusToReturn = document.activeElement as HTMLElement;
return new Promise<IDialogResult>((resolve) => {
if (!this.element || !this.buttonsContainer || !this.iconElement || !this.toolbarContainer) {
resolve({ button: 0 });
return;
}
clearNode(this.buttonsContainer);
let focusedButton = 0;
const buttonGroup = this.buttonGroup = new ButtonGroup(this.buttonsContainer, this.buttons.length, { title: true });
const buttonGroup = this.buttonGroup = this._register(new ButtonGroup(this.buttonsContainer, this.buttons.length, { title: true }));
const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId);
// Set focused button to UI index
buttonMap.forEach((value, index) => {
if (value.index === 0) {
focusedButton = index;
}
});
// Handle button clicks
buttonGroup.buttons.forEach((button, index) => {
button.label = mnemonicButtonLabel(buttonMap[index].label, true);
this._register(button.onDidClick(e => {
EventHelper.stop(e);
resolve({ button: buttonMap[index].index, checkboxChecked: this.checkbox ? this.checkbox.checked : undefined });
resolve({
button: buttonMap[index].index,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
});
}));
});
// Handle keyboard events gloably: Tab, Arrow-Left/Right
this._register(domEvent(window, 'keydown', true)((e: KeyboardEvent) => {
const evt = new StandardKeyboardEvent(e);
if (evt.equals(KeyCode.Enter) || evt.equals(KeyCode.Space)) {
return;
if (evt.equals(KeyCode.Enter)) {
// Enter in input field should OK the dialog
if (this.inputs.some(input => input.hasFocus())) {
EventHelper.stop(e);
resolve({
button: buttonMap.find(button => button.index !== this.options.cancelId)?.index ?? 0,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
});
}
return; // leave default handling
}
if (evt.equals(KeyCode.Space)) {
return; // leave default handling
}
let eventHandled = false;
if (evt.equals(KeyMod.Shift | KeyCode.Tab) || evt.equals(KeyCode.LeftArrow)) {
if (!this.checkboxHasFocus && focusedButton === 0) {
if (this.checkbox) {
this.checkbox.domNode.focus();
// Focus: Next / Previous
if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow) || evt.equals(KeyMod.Shift | KeyCode.Tab) || evt.equals(KeyCode.LeftArrow)) {
// Build a list of focusable elements in their visual order
const focusableElements: { focus: () => void }[] = [];
let focusedIndex = -1;
for (const input of this.inputs) {
focusableElements.push(input);
if (input.hasFocus()) {
focusedIndex = focusableElements.length - 1;
}
this.checkboxHasFocus = true;
} else {
focusedButton = (this.checkboxHasFocus ? 0 : focusedButton) + buttonGroup.buttons.length - 1;
focusedButton = focusedButton % buttonGroup.buttons.length;
buttonGroup.buttons[focusedButton].focus();
this.checkboxHasFocus = false;
}
eventHandled = true;
} else if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) {
if (!this.checkboxHasFocus && focusedButton === buttonGroup.buttons.length - 1) {
if (this.checkbox) {
this.checkbox.domNode.focus();
if (this.checkbox) {
focusableElements.push(this.checkbox);
if (this.checkbox.hasFocus()) {
focusedIndex = focusableElements.length - 1;
}
}
if (this.buttonGroup) {
for (const button of this.buttonGroup.buttons) {
focusableElements.push(button);
if (button.hasFocus()) {
focusedIndex = focusableElements.length - 1;
}
}
}
// Focus next element (with wrapping)
if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) {
if (focusedIndex === -1) {
focusedIndex = 0; // default to focus first element if none have focus
}
this.checkboxHasFocus = true;
} else {
focusedButton = this.checkboxHasFocus ? 0 : focusedButton + 1;
focusedButton = focusedButton % buttonGroup.buttons.length;
buttonGroup.buttons[focusedButton].focus();
this.checkboxHasFocus = false;
const newFocusedIndex = (focusedIndex + 1) % focusableElements.length;
focusableElements[newFocusedIndex].focus();
}
// Focus previous element (with wrapping)
else {
if (focusedIndex === -1) {
focusedIndex = focusableElements.length; // default to focus last element if none have focus
}
let newFocusedIndex = focusedIndex - 1;
if (newFocusedIndex === -1) {
newFocusedIndex = focusableElements.length - 1;
}
focusableElements[newFocusedIndex].focus();
}
eventHandled = true;
}
......@@ -217,10 +285,14 @@ export class Dialog extends Disposable {
const evt = new StandardKeyboardEvent(e);
if (evt.equals(KeyCode.Escape)) {
resolve({ button: this.options.cancelId || 0, checkboxChecked: this.checkbox ? this.checkbox.checked : undefined });
resolve({
button: this.options.cancelId || 0,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
});
}
}));
// Detect focus out
this._register(domEvent(this.element, 'focusout', false)((e: FocusEvent) => {
if (!!e.relatedTarget && !!this.element) {
if (!isAncestor(e.relatedTarget as HTMLElement, this.element)) {
......@@ -254,12 +326,14 @@ export class Dialog extends Disposable {
break;
}
const actionBar = new ActionBar(this.toolbarContainer, {});
const actionBar = this._register(new ActionBar(this.toolbarContainer, {}));
const action = new Action('dialog.close', nls.localize('dialogClose', "Close Dialog"), dialogCloseIcon.classNames, true, () => {
resolve({ button: this.options.cancelId || 0, checkboxChecked: this.checkbox ? this.checkbox.checked : undefined });
return Promise.resolve();
});
const action = this._register(new Action('dialog.close', nls.localize('dialogClose', "Close Dialog"), dialogCloseIcon.classNames, true, async () => {
resolve({
button: this.options.cancelId || 0,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
});
}));
actionBar.push(action, { icon: true, label: false, });
......@@ -268,8 +342,17 @@ export class Dialog extends Disposable {
this.element.setAttribute('aria-label', this.getAriaLabel());
show(this.element);
// Focus first element
buttonGroup.buttons[focusedButton].focus();
// Focus first element (input or button)
if (this.inputs.length > 0) {
this.inputs[0].focus();
this.inputs[0].select();
} else {
buttonMap.forEach((value, index) => {
if (value.index === 0) {
buttonGroup.buttons[index].focus();
}
});
}
});
}
......@@ -282,60 +365,59 @@ export class Dialog extends Disposable {
const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : '';
const border = style.dialogBorder ? `1px solid ${style.dialogBorder}` : '';
if (this.shadowElement) {
this.shadowElement.style.boxShadow = shadowColor;
}
this.shadowElement.style.boxShadow = shadowColor;
if (this.element) {
this.element.style.color = fgColor?.toString() ?? '';
this.element.style.backgroundColor = bgColor?.toString() ?? '';
this.element.style.border = border;
this.element.style.color = fgColor?.toString() ?? '';
this.element.style.backgroundColor = bgColor?.toString() ?? '';
this.element.style.border = border;
if (this.buttonGroup) {
this.buttonGroup.buttons.forEach(button => button.style(style));
}
if (this.buttonGroup) {
this.buttonGroup.buttons.forEach(button => button.style(style));
}
if (this.checkbox) {
this.checkbox.style(style);
}
if (this.checkbox) {
this.checkbox.style(style);
}
if (this.messageDetailElement && fgColor && bgColor) {
const messageDetailColor = fgColor.transparent(.9);
this.messageDetailElement.style.color = messageDetailColor.makeOpaque(bgColor).toString();
}
if (fgColor && bgColor) {
const messageDetailColor = fgColor.transparent(.9);
this.messageDetailElement.style.color = messageDetailColor.makeOpaque(bgColor).toString();
}
if (this.iconElement) {
let color;
switch (this.options.type) {
case 'error':
color = style.errorIconForeground;
break;
case 'warning':
color = style.warningIconForeground;
break;
default:
color = style.infoIconForeground;
break;
}
if (color) {
this.iconElement.style.color = color.toString();
}
}
let color;
switch (this.options.type) {
case 'error':
color = style.errorIconForeground;
break;
case 'warning':
color = style.warningIconForeground;
break;
default:
color = style.infoIconForeground;
break;
}
if (color) {
this.iconElement.style.color = color.toString();
}
for (const input of this.inputs) {
input.style(style);
}
}
}
style(style: IDialogStyles): void {
this.styles = style;
this.applyStyles();
}
dispose(): void {
super.dispose();
if (this.modal) {
this.modal.remove();
this.modal = undefined;
if (this.modalElement) {
this.modalElement.remove();
this.modalElement = undefined;
}
if (this.focusToReturn && isAncestor(this.focusToReturn, document.body)) {
......@@ -346,9 +428,10 @@ export class Dialog extends Disposable {
private rearrangeButtons(buttons: Array<string>, cancelId: number | undefined): ButtonMapEntry[] {
const buttonMap: ButtonMapEntry[] = [];
// Maps each button to its current label and old index so that when we move them around it's not a problem
buttons.forEach((button, index) => {
buttonMap.push({ label: button, index: index });
buttonMap.push({ label: button, index });
});
// macOS/linux: reverse button order
......
......@@ -157,3 +157,14 @@ export interface CrashReporterStartOptions {
*/
globalExtra?: Record<string, string>;
}
/**
* Additional information around a `app.on('login')` event.
*/
export interface AuthInfo {
isProxy: boolean;
scheme: string;
host: string;
port: number;
realm: string;
}
......@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { app, ipcMain as ipc, systemPreferences, shell, Event, contentTracing, protocol, IpcMainEvent, BrowserWindow, dialog, session } from 'electron';
import { app, ipcMain as ipc, systemPreferences, shell, contentTracing, protocol, IpcMainEvent, BrowserWindow, dialog, session } from 'electron';
import { IProcessEnvironment, isWindows, isMacintosh } from 'vs/base/common/platform';
import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService';
import { IWindowOpenable } from 'vs/platform/windows/common/windows';
......@@ -34,6 +34,7 @@ import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProper
import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import product from 'vs/platform/product/common/product';
import { ProxyAuthHandler } from 'vs/code/electron-main/auth';
import { ProxyAuthHandler2 } from 'vs/code/electron-main/auth2';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { URI } from 'vs/base/common/uri';
......@@ -114,14 +115,16 @@ export class CodeApplication extends Disposable {
// Contextmenu via IPC support
registerContextMenuListener();
app.on('accessibility-support-changed', (event: Event, accessibilitySupportEnabled: boolean) => {
// Accessibility change event
app.on('accessibility-support-changed', (event, accessibilitySupportEnabled) => {
if (this.windowsMainService) {
this.windowsMainService.sendToAll('vscode:accessibilitySupportChanged', accessibilitySupportEnabled);
}
});
app.on('activate', (event: Event, hasVisibleWindows: boolean) => {
this.logService.trace('App#activate');
// macOS dock activate
app.on('activate', (event, hasVisibleWindows) => {
this.logService.trace('app#activate');
// Mac only event: open new window when we get activated
if (!hasVisibleWindows && this.windowsMainService) {
......@@ -134,7 +137,7 @@ export class CodeApplication extends Disposable {
// !!! DO NOT CHANGE without consulting the documentation !!!
//
app.on('remote-require', (event, sender, module) => {
this.logService.trace('App#on(remote-require): prevented');
this.logService.trace('app#on(remote-require): prevented');
event.preventDefault();
});
......@@ -164,8 +167,8 @@ export class CodeApplication extends Disposable {
event.preventDefault();
});
app.on('web-contents-created', (_event: Event, contents) => {
contents.on('will-attach-webview', (event: Event, webPreferences, params) => {
app.on('web-contents-created', (event, contents) => {
contents.on('will-attach-webview', (event, webPreferences, params) => {
const isValidWebviewSource = (source: string | undefined): boolean => {
if (!source) {
......@@ -207,7 +210,7 @@ export class CodeApplication extends Disposable {
event.preventDefault();
});
contents.on('new-window', (event: Event, url: string) => {
contents.on('new-window', (event, url) => {
event.preventDefault(); // prevent code that wants to open links
shell.openExternal(url);
......@@ -226,8 +229,8 @@ export class CodeApplication extends Disposable {
let macOpenFileURIs: IWindowOpenable[] = [];
let runningTimeout: NodeJS.Timeout | null = null;
app.on('open-file', (event: Event, path: string) => {
this.logService.trace('App#open-file: ', path);
app.on('open-file', (event, path) => {
this.logService.trace('app#open-file: ', path);
event.preventDefault();
// Keep in array because more might come!
......@@ -386,7 +389,11 @@ export class CodeApplication extends Disposable {
}
// Setup Auth Handler
this._register(new ProxyAuthHandler());
if (this.configurationService.getValue('window.enableExperimentalProxyLoginDialog') !== true) {
this._register(new ProxyAuthHandler());
} else {
this._register(appInstantiationService.createInstance(ProxyAuthHandler2));
}
// Open Windows
const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient));
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { app, AuthInfo, WebContents, Event as ElectronEvent } from 'electron';
import { ILogService } from 'vs/platform/log/common/log';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService';
import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService';
import { generateUuid } from 'vs/base/common/uuid';
type LoginEvent = {
event: ElectronEvent;
webContents: WebContents;
authInfo: AuthInfo;
cb: (username: string, password: string) => void;
};
type Credentials = {
username: string;
password: string;
};
export class ProxyAuthHandler2 extends Disposable {
private static PROXY_CREDENTIALS_SERVICE_KEY = 'vscode.proxy-credentials';
private pendingProxyHandler = false;
private proxyDialogShown = false;
private storedProxyCredentialsUsed = false;
constructor(
@ILogService private readonly logService: ILogService,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
@IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService
) {
super();
this.registerListeners();
}
private registerListeners(): void {
const onLogin = Event.fromNodeEventEmitter<LoginEvent>(app, 'login', (event, webContents, req, authInfo, cb) => ({ event, webContents, req, authInfo, cb }));
this._register(onLogin(this.onLogin, this));
}
private onLogin(event: LoginEvent): void {
if (!event.authInfo.isProxy) {
return; // only for proxy
}
if (this.pendingProxyHandler) {
this.logService.trace('auth#onLogin (proxy) - exit - pending proxy handling found');
return; // never more than once at the same time
}
if (this.proxyDialogShown) {
this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown');
return; // only one dialog per session at max
}
this.handleOnLogin(event);
}
private async handleOnLogin({ event, webContents, authInfo, cb }: LoginEvent): Promise<void> {
this.logService.trace('auth#handleOnLogin (proxy) - enter');
this.pendingProxyHandler = true;
try {
const credentials = await this.resolveProxyCredentials(event, webContents, authInfo);
if (credentials) {
this.logService.trace('auth#handleOnLogin (proxy) - got credentials');
cb(credentials.username, credentials.password);
} else {
this.logService.trace('auth#handleOnLogin (proxy) - did not get credentials');
}
} finally {
this.logService.trace('auth#handleOnLogin (proxy) - exit');
this.pendingProxyHandler = false;
}
}
private async resolveProxyCredentials(event: ElectronEvent, webContents: WebContents, authInfo: AuthInfo): Promise<Credentials | undefined> {
this.logService.trace('auth#resolveProxyCredentials - enter');
// Signal we handle this on our own
event.preventDefault();
// Find any previously stored credentials
let username: string | undefined = undefined;
let password: string | undefined = undefined;
try {
const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, 'account');
if (encryptedSerializedProxyCredentials) {
const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials));
if (credentials.username && credentials.password) {
username = credentials.username;
password = credentials.password;
}
}
} catch (error) {
this.logService.error(error); // handle errors by asking user for login via dialog
}
// Reply with stored credentials unless we used them already.
// In that case we need to show a login dialog again because
// they seem invalid.
if (!this.storedProxyCredentialsUsed && username && password) {
this.logService.trace('auth#resolveProxyCredentials (proxy) - exit - found stored credentials to use');
this.storedProxyCredentialsUsed = true;
return { username, password };
}
// Find suitable window to show dialog
const window = this.windowsMainService.getWindowByWebContents(webContents) || this.windowsMainService.getLastActiveWindow();
if (!window) {
this.logService.trace('auth#resolveProxyCredentials (proxy) - exit - no opened window found to show dialog in');
return undefined; // unexpected
}
this.logService.trace(`auth#resolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`);
// Open proxy dialog
const payload = { authInfo, username, password, replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` };
window.sendWhenReady('vscode:openProxyAuthenticationDialog', payload);
this.proxyDialogShown = true;
// Handle reply
return new Promise(resolve => {
const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean }) => {
if (channel === payload.replyChannel) {
this.logService.trace(`auth#resolveProxyCredentials - exit - received credentials from window ${window.id}`);
webContents.off('ipc-message', proxyAuthResponseHandler);
const credentials: Credentials = { username: reply.username, password: reply.password };
// Update stored credentials based on `remember` flag
try {
if (reply.remember) {
const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials));
await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, 'account', encryptedSerializedCredentials);
} else {
await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, 'account');
}
} catch (error) {
this.logService.error(error); // handle gracefully
}
resolve({ username: credentials.username, password: credentials.password });
}
};
webContents.on('ipc-message', proxyAuthResponseHandler);
});
}
}
......@@ -27,7 +27,7 @@ import { CommandsRegistry, ICommandEvent, ICommandHandler, ICommandService } fro
import { IConfigurationChangeEvent, IConfigurationData, IConfigurationOverrides, IConfigurationService, IConfigurationModel, IConfigurationValue, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { Configuration, ConfigurationModel, DefaultConfigurationModel, ConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels';
import { IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IShowResult } from 'vs/platform/dialogs/common/dialogs';
import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IInputResult, IShowResult } from 'vs/platform/dialogs/common/dialogs';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { AbstractKeybindingService } from 'vs/platform/keybinding/common/abstractKeybindingService';
import { IKeybindingEvent, IKeyboardEvent, KeybindingSource, KeybindingsSchemaContribution } from 'vs/platform/keybinding/common/keybinding';
......@@ -207,6 +207,10 @@ export class SimpleDialogService implements IDialogService {
return Promise.resolve({ choice: 0 });
}
public input(): Promise<IInputResult> {
return Promise.resolve({ choice: 0 }); // unsupported
}
public about(): Promise<void> {
return Promise.resolve(undefined);
}
......
......@@ -17,6 +17,11 @@ export interface FileFilter {
export type DialogType = 'none' | 'info' | 'error' | 'question' | 'warning';
export interface ICheckbox {
label: string;
checked?: boolean;
}
export interface IConfirmation {
title?: string;
type?: DialogType;
......@@ -24,10 +29,7 @@ export interface IConfirmation {
detail?: string;
primaryButton?: string;
secondaryButton?: string;
checkbox?: {
label: string;
checked?: boolean;
};
checkbox?: ICheckbox;
}
export interface IConfirmationResult {
......@@ -61,6 +63,15 @@ export interface IShowResult {
checkboxChecked?: boolean;
}
export interface IInputResult extends IShowResult {
/**
* Values for the input fields as provided by the user
* or `undefined` if none.
*/
values?: string[];
}
export interface IPickAndOpenOptions {
forceNewWindow?: boolean;
defaultUri?: URI;
......@@ -146,10 +157,13 @@ export const IDialogService = createDecorator<IDialogService>('dialogService');
export interface IDialogOptions {
cancelId?: number;
detail?: string;
checkbox?: {
label: string;
checked?: boolean;
};
checkbox?: ICheckbox;
}
export interface IInput {
placeholder?: string;
type?: 'text' | 'password'
value?: string;
}
/**
......@@ -176,6 +190,16 @@ export interface IDialogService {
*/
show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<IShowResult>;
/**
* Present a modal dialog to the user asking for input.
*
* @returns A promise with the selected choice index. If the user refused to choose,
* then a promise with index of `cancelId` option is returned. If there is no such
* option then promise with index `0` is returned. In addition, the values for the
* inputs are returned as well.
*/
input(type: DialogType, message: string, buttons: string[], inputs: IInput[], options?: IDialogOptions): Promise<IInputResult>;
/**
* Present the about dialog to the user.
*/
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import Severity from 'vs/base/common/severity';
import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions, IShowResult } from 'vs/platform/dialogs/common/dialogs';
import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions, IShowResult, IInputResult } from 'vs/platform/dialogs/common/dialogs';
export class TestDialogService implements IDialogService {
......@@ -12,5 +12,6 @@ export class TestDialogService implements IDialogService {
confirm(_confirmation: IConfirmation): Promise<IConfirmationResult> { return Promise.resolve({ confirmed: false }); }
show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise<IShowResult> { return Promise.resolve({ choice: 0 }); }
input(): Promise<IInputResult> { { return Promise.resolve({ choice: 0, values: [] }); } }
about(): Promise<void> { return Promise.resolve(); }
}
......@@ -360,6 +360,9 @@ export interface IDialogStyleOverrides extends IButtonStyleOverrides {
errorIconForeground?: ColorIdentifier;
warningIconForeground?: ColorIdentifier;
infoIconForeground?: ColorIdentifier;
inputBackground?: ColorIdentifier;
inputForeground?: ColorIdentifier;
inputBorder?: ColorIdentifier;
}
export const defaultDialogStyles = <IDialogStyleOverrides>{
......@@ -376,7 +379,10 @@ export const defaultDialogStyles = <IDialogStyleOverrides>{
checkboxForeground: simpleCheckboxForeground,
errorIconForeground: problemsErrorIconForeground,
warningIconForeground: problemsWarningIconForeground,
infoIconForeground: problemsInfoIconForeground
infoIconForeground: problemsInfoIconForeground,
inputBackground: inputBackground,
inputForeground: inputForeground,
inputBorder: inputBorder
};
......
......@@ -116,6 +116,7 @@ export interface IWindowSettings {
enableMenuBarMnemonics: boolean;
closeWhenEmpty: boolean;
clickThroughInactive: boolean;
enableExperimentalProxyLoginDialog: boolean;
}
export function getTitleBarStyle(configurationService: IConfigurationService, environment: IEnvironmentService, isExtensionDevelopment = environment.isExtensionDevelopment): 'native' | 'custom' {
......
......@@ -12,7 +12,7 @@ import { IProcessEnvironment } from 'vs/base/common/platform';
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { URI } from 'vs/base/common/uri';
import { Rectangle, BrowserWindow } from 'electron';
import { Rectangle, BrowserWindow, WebContents } from 'electron';
import { IDisposable } from 'vs/base/common/lifecycle';
export interface IWindowState {
......@@ -112,6 +112,7 @@ export interface IWindowsMainService {
getLastActiveWindow(): ICodeWindow | undefined;
getWindowById(windowId: number): ICodeWindow | undefined;
getWindowByWebContents(webContents: WebContents): ICodeWindow | undefined;
getWindows(): ICodeWindow[];
getWindowCount(): number;
}
......
......@@ -14,7 +14,7 @@ import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/e
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { IStateService } from 'vs/platform/state/node/state';
import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window';
import { screen, BrowserWindow, MessageBoxOptions, Display, app } from 'electron';
import { screen, BrowserWindow, MessageBoxOptions, Display, app, WebContents } from 'electron';
import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILogService } from 'vs/platform/log/common/log';
......@@ -1664,6 +1664,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
return arrays.firstOrDefault(res);
}
getWindowByWebContents(webContents: WebContents): ICodeWindow | undefined {
const win = BrowserWindow.fromWebContents(webContents);
if (win) {
return this.getWindowById(win.id);
}
return undefined;
}
getWindows(): ICodeWindow[] {
return WindowsMainService.WINDOWS;
}
......
......@@ -36,6 +36,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo
private updateMode: string | undefined;
private debugConsoleWordWrap: boolean | undefined;
private accessibilitySupport: 'on' | 'off' | 'auto' | undefined;
private enableExperimentalProxyLoginDialog: boolean | undefined;
constructor(
@IHostService private readonly hostService: IHostService,
......@@ -97,6 +98,12 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo
changed = true;
}
}
// Experimental proxy login dialog
if (typeof config.window?.enableExperimentalProxyLoginDialog === 'boolean' && config.window.enableExperimentalProxyLoginDialog !== this.enableExperimentalProxyLoginDialog) {
this.enableExperimentalProxyLoginDialog = config.window.enableExperimentalProxyLoginDialog;
changed = true;
}
}
// Notify only when changed and we are the focused window (avoids notification spam across windows)
......
......@@ -292,6 +292,12 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema';
'scope': ConfigurationScope.APPLICATION,
'description': nls.localize('window.clickThroughInactive', "If enabled, clicking on an inactive window will both activate the window and trigger the element under the mouse if it is clickable. If disabled, clicking anywhere on an inactive window will activate it only and a second click is required on the element."),
'included': isMacintosh
},
'window.enableExperimentalProxyLoginDialog': {
'type': 'boolean',
'default': false,
'scope': ConfigurationScope.APPLICATION,
'description': nls.localize('window.enableExperimentalProxyLoginDialog', "Enables a new login dialog for proxy authentication. Requires a restart to take effect."),
}
}
});
......
......@@ -43,7 +43,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { MenubarControl } from '../browser/parts/titlebar/menubarControl';
import { ILabelService } from 'vs/platform/label/common/label';
import { IUpdateService } from 'vs/platform/update/common/update';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IPreferencesService } from '../services/preferences/common/preferences';
import { IMenubarData, IMenubarMenu, IMenubarKeybinding, IMenubarMenuItemSubmenu, IMenubarMenuItemAction, MenubarMenuItem } from 'vs/platform/menubar/common/menubar';
import { IMenubarService } from 'vs/platform/menubar/electron-sandbox/menubar';
......@@ -63,9 +63,13 @@ import { clearAllFontInfos } from 'vs/editor/browser/config/configuration';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IAddressProvider, IAddress } from 'vs/platform/remote/common/remoteAgentConnection';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { AuthInfo } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes';
export class NativeWindow extends Disposable {
private static REMEMBER_PROXY_CREDENTIALS_KEY = 'window.rememberProxyCredentials';
private touchBarMenu: IMenu | undefined;
private readonly touchBarDisposables = this._register(new DisposableStore());
private lastInstalledTouchedBar: ICommandAction[][] | undefined;
......@@ -107,7 +111,9 @@ export class NativeWindow extends Disposable {
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
@IProductService private readonly productService: IProductService,
@IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService
@IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@IDialogService private readonly dialogService: IDialogService,
@IStorageService private readonly storageService: IStorageService
) {
super();
......@@ -199,7 +205,44 @@ export class NativeWindow extends Disposable {
setFullscreen(false);
});
// accessibility support changed event
// Proxy Login Dialog
ipcRenderer.on('vscode:openProxyAuthenticationDialog', async (event: unknown, payload: { authInfo: AuthInfo, username?: string, password?: string, replyChannel: string }) => {
const rememberCredentials = this.storageService.getBoolean(NativeWindow.REMEMBER_PROXY_CREDENTIALS_KEY, StorageScope.GLOBAL);
const result = await this.dialogService.input('question', nls.localize('proxyAuthRequired', "Proxy Authentication Required"),
[
nls.localize({ key: 'loginButton', comment: ['&& denotes a mnemonic'] }, "&&Log In"),
nls.localize({ key: 'cancelButton', comment: ['&& denotes a mnemonic'] }, "&&Cancel")
],
[
{ placeholder: nls.localize('username', "Username"), value: payload.username },
{ placeholder: nls.localize('password', "Password"), type: 'password', value: payload.password }
],
{
cancelId: 1,
detail: nls.localize('proxyDetail', "The proxy {0} requires a userame and password.", `${payload.authInfo.host}:${payload.authInfo.port}`),
checkbox: {
label: nls.localize('rememberCredentials', "Remember my credentials"),
checked: rememberCredentials
}
});
if (result.choice !== 0 || !result.values) {
return; // dialog canceled
}
// Update state based on checkbox
if (result.checkboxChecked) {
this.storageService.store(NativeWindow.REMEMBER_PROXY_CREDENTIALS_KEY, true, StorageScope.GLOBAL);
} else {
this.storageService.remove(NativeWindow.REMEMBER_PROXY_CREDENTIALS_KEY, StorageScope.GLOBAL);
}
// Reply back to main side with credentials
const [username, password] = result.values;
ipcRenderer.send(payload.replyChannel, { username, password, remember: !!result.checkboxChecked });
});
// Accessibility support changed event
ipcRenderer.on('vscode:accessibilitySupportChanged', (event: unknown, accessibilitySupportEnabled: boolean) => {
this.accessibilityService.setAccessibilitySupport(accessibilitySupportEnabled ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled);
});
......
......@@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IDialogService, IDialogOptions, IConfirmation, IConfirmationResult, DialogType, IShowResult } from 'vs/platform/dialogs/common/dialogs';
import { IDialogService, IDialogOptions, IConfirmation, IConfirmationResult, DialogType, IShowResult, IInputResult, ICheckbox, IInput } from 'vs/platform/dialogs/common/dialogs';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { ILogService } from 'vs/platform/log/common/log';
import Severity from 'vs/base/common/severity';
import { Dialog } from 'vs/base/browser/ui/dialog/dialog';
import { Dialog, IDialogResult } from 'vs/base/browser/ui/dialog/dialog';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachDialogStyler } from 'vs/platform/theme/common/styler';
import { DisposableStore } from 'vs/base/common/lifecycle';
......@@ -24,7 +24,14 @@ export class DialogService implements IDialogService {
declare readonly _serviceBrand: undefined;
private allowableCommands = ['copy', 'cut', 'editor.action.clipboardCopyAction', 'editor.action.clipboardCutAction'];
private static readonly ALLOWABLE_COMMANDS = [
'copy',
'cut',
'editor.action.selectAll',
'editor.action.clipboardCopyAction',
'editor.action.clipboardCutAction',
'editor.action.clipboardPasteAction'
];
constructor(
@ILogService private readonly logService: ILogService,
......@@ -51,32 +58,7 @@ export class DialogService implements IDialogService {
buttons.push(nls.localize('cancelButton', "Cancel"));
}
const dialogDisposables = new DisposableStore();
const dialog = new Dialog(
this.layoutService.container,
confirmation.message,
buttons,
{
detail: confirmation.detail,
cancelId: 1,
type: confirmation.type,
keyEventProcessor: (event: StandardKeyboardEvent) => {
const resolved = this.keybindingService.softDispatch(event, this.layoutService.container);
if (resolved && resolved.commandId) {
if (this.allowableCommands.indexOf(resolved.commandId) === -1) {
EventHelper.stop(event, true);
}
}
},
checkboxChecked: confirmation.checkbox ? confirmation.checkbox.checked : undefined,
checkboxLabel: confirmation.checkbox ? confirmation.checkbox.label : undefined
});
dialogDisposables.add(dialog);
dialogDisposables.add(attachDialogStyler(dialog, this.themeService));
const result = await dialog.show();
dialogDisposables.dispose();
const result = await this.doShow(confirmation.type, confirmation.message, buttons, confirmation.detail, 1, confirmation.checkbox);
return { confirmed: result.button === 0, checkboxChecked: result.checkboxChecked };
}
......@@ -88,25 +70,35 @@ export class DialogService implements IDialogService {
async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<IShowResult> {
this.logService.trace('DialogService#show', message);
const result = await this.doShow(this.getDialogType(severity), message, buttons, options?.detail, options?.cancelId, options?.checkbox);
return {
choice: result.button,
checkboxChecked: result.checkboxChecked
};
}
private async doShow(type: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending' | undefined, message: string, buttons: string[], detail?: string, cancelId?: number, checkbox?: ICheckbox, inputs?: IInput[]): Promise<IDialogResult> {
const dialogDisposables = new DisposableStore();
const dialog = new Dialog(
this.layoutService.container,
message,
buttons,
{
detail: options ? options.detail : undefined,
cancelId: options ? options.cancelId : undefined,
type: this.getDialogType(severity),
detail,
cancelId,
type,
keyEventProcessor: (event: StandardKeyboardEvent) => {
const resolved = this.keybindingService.softDispatch(event, this.layoutService.container);
if (resolved && resolved.commandId) {
if (this.allowableCommands.indexOf(resolved.commandId) === -1) {
if (DialogService.ALLOWABLE_COMMANDS.indexOf(resolved.commandId) === -1) {
EventHelper.stop(event, true);
}
}
},
checkboxLabel: options && options.checkbox ? options.checkbox.label : undefined,
checkboxChecked: options && options.checkbox ? options.checkbox.checked : undefined
checkboxLabel: checkbox?.label,
checkboxChecked: checkbox?.checked,
inputs
});
dialogDisposables.add(dialog);
......@@ -115,9 +107,18 @@ export class DialogService implements IDialogService {
const result = await dialog.show();
dialogDisposables.dispose();
return result;
}
async input(type: DialogType, message: string, buttons: string[], inputs: IInput[], options?: IDialogOptions): Promise<IInputResult> {
this.logService.trace('DialogService#input', message);
const result = await this.doShow(type, message, buttons, options?.detail, options?.cancelId, options?.checkbox, inputs);
return {
choice: result.button,
checkboxChecked: result.checkboxChecked
checkboxChecked: result.checkboxChecked,
values: result.values
};
}
......
......@@ -7,7 +7,7 @@ import * as nls from 'vs/nls';
import Severity from 'vs/base/common/severity';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IDialogService, IConfirmation, IConfirmationResult, IDialogOptions, IShowResult } from 'vs/platform/dialogs/common/dialogs';
import { IDialogService, IConfirmation, IConfirmationResult, IDialogOptions, IShowResult, IInputResult, DialogType, IInput } from 'vs/platform/dialogs/common/dialogs';
import { DialogService as HTMLDialogService } from 'vs/workbench/services/dialogs/browser/dialogService';
import { ILogService } from 'vs/platform/log/common/log';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
......@@ -70,7 +70,7 @@ export class DialogService implements IDialogService {
return this.nativeImpl.confirm(confirmation);
}
show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions | undefined): Promise<IShowResult> {
show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<IShowResult> {
if (this.useCustomDialog) {
return this.customImpl.show(severity, message, buttons, options);
}
......@@ -78,6 +78,10 @@ export class DialogService implements IDialogService {
return this.nativeImpl.show(severity, message, buttons, options);
}
input(type: DialogType, message: string, buttons: string[], inputs: IInput[], options?: IDialogOptions): Promise<IInputResult> {
return this.customImpl.input(type, message, buttons, inputs, options);
}
about(): Promise<void> {
return this.nativeImpl.about();
}
......@@ -205,6 +209,10 @@ class NativeDialogService implements IDialogService {
return { options, buttonIndexMap };
}
input(): never {
throw new Error('Unsupported'); // we have no native API for password dialogs in Electron
}
async about(): Promise<void> {
let version = this.productService.version;
if (this.productService.target) {
......
......@@ -25,6 +25,10 @@ const emptyDialogService = new class implements IDialogService {
about(): never {
throw new Error('not implemented');
}
input(): never {
throw new Error('not implemented');
}
};
const emptyCommandService: ICommandService = {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册