Skip to content

Commit 4cffff9

Browse files
author
Benjamin Pasero
committed
1 parent 931c220 commit 4cffff9

26 files changed

Lines changed: 194 additions & 105 deletions

File tree

src/vs/editor/standalone/browser/simpleServices.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe
3535
import { OS } from 'vs/base/common/platform';
3636
import { Range } from 'vs/editor/common/core/range';
3737
import { ITextModel } from 'vs/editor/common/model';
38-
import { INotificationService, INotification, INotificationHandle, NoOpNotification, IPromptChoice } from 'vs/platform/notification/common/notification';
38+
import { INotificationService, INotification, INotificationHandle, NoOpNotification, IPromptChoice, IPromptOptions } from 'vs/platform/notification/common/notification';
3939
import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions } from 'vs/platform/dialogs/common/dialogs';
4040
import { IPosition, Position as Pos } from 'vs/editor/common/core/position';
4141
import { isEditorConfigurationKey, isDiffEditorConfigurationKey } from 'vs/editor/common/config/commonEditorConfig';
@@ -212,7 +212,7 @@ export class SimpleNotificationService implements INotificationService {
212212
return SimpleNotificationService.NO_OP;
213213
}
214214

215-
public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle {
215+
public prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle {
216216
return SimpleNotificationService.NO_OP;
217217
}
218218
}

src/vs/platform/integrity/node/integrityServiceImpl.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ export class IntegrityServiceImpl implements IIntegrityService {
9696
isSecondary: true,
9797
run: () => this._storage.set({ dontShowPrompt: true, commit: product.commit })
9898
}
99-
]
99+
],
100+
{ sticky: true }
100101
);
101102
}
102103

src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe
1616
import { OS } from 'vs/base/common/platform';
1717
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
1818
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
19-
import { INotificationService, NoOpNotification, INotification, IPromptChoice } from 'vs/platform/notification/common/notification';
19+
import { INotificationService, NoOpNotification, INotification, IPromptChoice, IPromptOptions } from 'vs/platform/notification/common/notification';
2020

2121
function createContext(ctx: any) {
2222
return {
@@ -140,7 +140,7 @@ suite('AbstractKeybindingService', () => {
140140
showMessageCalls.push({ sev: Severity.Error, message });
141141
return new NoOpNotification();
142142
},
143-
prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void) {
143+
prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions) {
144144
throw new Error('not implemented');
145145
}
146146
};

src/vs/platform/notification/common/notification.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ export interface INotification {
4545
* this usecase and much easier to use!
4646
*/
4747
actions?: INotificationActions;
48+
49+
/**
50+
* Sticky notifications are not automatically removed after a certain timeout. By
51+
* default, notifications with primary actions and severity error are always sticky.
52+
*/
53+
sticky?: boolean;
4854
}
4955

5056
export interface INotificationActions {
@@ -147,6 +153,22 @@ export interface IPromptChoice {
147153
run: () => void;
148154
}
149155

156+
export interface IPromptOptions {
157+
158+
/**
159+
* Sticky prompts are not automatically removed after a certain timeout.
160+
*
161+
* Note: Prompts of severity ERROR are always sticky.
162+
*/
163+
sticky?: boolean;
164+
165+
/**
166+
* Will be called if the user closed the notification without picking
167+
* any of the provided choices.
168+
*/
169+
onCancel?: () => void;
170+
}
171+
150172
/**
151173
* A service to bring up notifications and non-modal prompts.
152174
*
@@ -195,7 +217,7 @@ export interface INotificationService {
195217
*
196218
* @returns a handle on the notification to e.g. hide it or update message, buttons, etc.
197219
*/
198-
prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle;
220+
prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle;
199221
}
200222

201223
export class NoOpNotification implements INotificationHandle {

src/vs/platform/notification/test/common/testNotificationService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { INotificationService, INotificationHandle, NoOpNotification, Severity, INotification, IPromptChoice } from 'vs/platform/notification/common/notification';
6+
import { INotificationService, INotificationHandle, NoOpNotification, Severity, INotification, IPromptChoice, IPromptOptions } from 'vs/platform/notification/common/notification';
77

88
export class TestNotificationService implements INotificationService {
99

@@ -27,7 +27,7 @@ export class TestNotificationService implements INotificationService {
2727
return TestNotificationService.NO_OP;
2828
}
2929

30-
public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle {
30+
public prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle {
3131
return TestNotificationService.NO_OP;
3232
}
3333
}

src/vs/workbench/browser/parts/editor/editorStatus.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,8 @@ export class EditorStatus implements IStatusbarItem {
512512
run: () => {
513513
this.configurationService.updateValue('editor.accessibilitySupport', 'off', ConfigurationTarget.USER);
514514
}
515-
}]
515+
}],
516+
{ sticky: true }
516517
);
517518

518519
once(this.screenReaderNotification.onDidClose)(() => {

src/vs/workbench/browser/parts/notifications/notificationsToasts.ts

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { localize } from 'vs/nls';
2121
import { Severity } from 'vs/platform/notification/common/notification';
2222
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
2323
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
24+
import { IWindowService } from 'vs/platform/windows/common/windows';
2425

2526
interface INotificationToast {
2627
item: INotificationViewItem;
@@ -52,6 +53,7 @@ export class NotificationsToasts extends Themable {
5253

5354
private notificationsToastsContainer: HTMLElement;
5455
private workbenchDimensions: Dimension;
56+
private windowHasFocus: boolean;
5557
private isNotificationsCenterVisible: boolean;
5658
private mapNotificationToToast: Map<INotificationViewItem, INotificationToast>;
5759
private notificationsToastsVisibleContextKey: IContextKey<boolean>;
@@ -64,13 +66,16 @@ export class NotificationsToasts extends Themable {
6466
@IThemeService themeService: IThemeService,
6567
@IEditorGroupsService private editorGroupService: IEditorGroupsService,
6668
@IContextKeyService contextKeyService: IContextKeyService,
67-
@ILifecycleService private lifecycleService: ILifecycleService
69+
@ILifecycleService private lifecycleService: ILifecycleService,
70+
@IWindowService private windowService: IWindowService
6871
) {
6972
super(themeService);
7073

7174
this.mapNotificationToToast = new Map<INotificationViewItem, INotificationToast>();
7275
this.notificationsToastsVisibleContextKey = NotificationsToastsVisibleContext.bindTo(contextKeyService);
7376

77+
this.windowService.isFocused().then(isFocused => this.windowHasFocus = isFocused);
78+
7479
this.registerListeners();
7580
}
7681

@@ -85,6 +90,9 @@ export class NotificationsToasts extends Themable {
8590
// Update toasts on notification changes
8691
this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e)));
8792
});
93+
94+
// Track window focus
95+
this.windowService.onDidChangeFocus(hasFocus => this.windowHasFocus = hasFocus);
8896
}
8997

9098
private onDidNotificationChange(e: INotificationChangeEvent): void {
@@ -177,31 +185,8 @@ export class NotificationsToasts extends Themable {
177185
this.removeToast(item);
178186
});
179187

180-
// Automatically hide collapsed notifications
181-
if (!item.expanded) {
182-
183-
// Track mouse over item
184-
let isMouseOverToast = false;
185-
itemDisposeables.push(addDisposableListener(notificationToastContainer, EventType.MOUSE_OVER, () => isMouseOverToast = true));
186-
itemDisposeables.push(addDisposableListener(notificationToastContainer, EventType.MOUSE_OUT, () => isMouseOverToast = false));
187-
188-
// Install Timers
189-
let timeoutHandle: any;
190-
const hideAfterTimeout = () => {
191-
timeoutHandle = setTimeout(() => {
192-
const showsProgress = item.hasProgress() && !item.progress.state.done;
193-
if (!notificationList.hasFocus() && !item.expanded && !isMouseOverToast && !showsProgress) {
194-
this.removeToast(item);
195-
} else {
196-
hideAfterTimeout(); // push out disposal if item has focus or is expanded
197-
}
198-
}, NotificationsToasts.PURGE_TIMEOUT[item.severity]);
199-
};
200-
201-
hideAfterTimeout();
202-
203-
itemDisposeables.push(toDisposable(() => clearTimeout(timeoutHandle)));
204-
}
188+
// Automatically purge non-sticky notifications
189+
this.purgeNotification(item, notificationToastContainer, notificationList, itemDisposeables);
205190

206191
// Theming
207192
this.updateStyles();
@@ -217,6 +202,35 @@ export class NotificationsToasts extends Themable {
217202
}));
218203
}
219204

205+
private purgeNotification(item: INotificationViewItem, notificationToastContainer: HTMLElement, notificationList: NotificationsList, disposables: IDisposable[]): void {
206+
207+
// Track mouse over item
208+
let isMouseOverToast = false;
209+
disposables.push(addDisposableListener(notificationToastContainer, EventType.MOUSE_OVER, () => isMouseOverToast = true));
210+
disposables.push(addDisposableListener(notificationToastContainer, EventType.MOUSE_OUT, () => isMouseOverToast = false));
211+
212+
// Install Timers
213+
let timeoutHandle: any;
214+
const hideAfterTimeout = () => {
215+
timeoutHandle = setTimeout(() => {
216+
if (
217+
item.sticky || // never hide sticky notifications
218+
notificationList.hasFocus() || // never hide notifications with focus
219+
isMouseOverToast || // never hide notifications under mouse
220+
!this.windowHasFocus // never hide when window has no focus
221+
) {
222+
hideAfterTimeout();
223+
} else {
224+
this.removeToast(item);
225+
}
226+
}, NotificationsToasts.PURGE_TIMEOUT[item.severity]);
227+
};
228+
229+
hideAfterTimeout();
230+
231+
disposables.push(toDisposable(() => clearTimeout(timeoutHandle)));
232+
}
233+
220234
private removeToast(item: INotificationViewItem): void {
221235
const notificationToast = this.mapNotificationToToast.get(item);
222236
let focusGroup = false;

src/vs/workbench/common/notifications.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export class NotificationsModel extends Disposable implements INotificationsMode
177177

178178
export interface INotificationViewItem {
179179
readonly severity: Severity;
180+
readonly sticky: boolean;
180181
readonly message: INotificationMessage;
181182
readonly source: string;
182183
readonly actions: INotificationActions;
@@ -363,7 +364,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie
363364
actions = { primary: notification.message.actions };
364365
}
365366

366-
return new NotificationViewItem(severity, message, notification.source, actions);
367+
return new NotificationViewItem(severity, notification.sticky, message, notification.source, actions);
367368
}
368369

369370
private static parseNotificationMessage(input: NotificationMessage): INotificationMessage {
@@ -397,11 +398,10 @@ export class NotificationViewItem extends Disposable implements INotificationVie
397398
return matchString;
398399
});
399400

400-
401401
return { raw, value: message, links, original: input };
402402
}
403403

404-
private constructor(private _severity: Severity, private _message: INotificationMessage, private _source: string, actions?: INotificationActions) {
404+
private constructor(private _severity: Severity, private _sticky: boolean, private _message: INotificationMessage, private _source: string, actions?: INotificationActions) {
405405
super();
406406

407407
this.setActions(actions);
@@ -436,6 +436,23 @@ export class NotificationViewItem extends Disposable implements INotificationVie
436436
return this._severity;
437437
}
438438

439+
get sticky(): boolean {
440+
if (this._sticky) {
441+
return true; // explicitly sticky
442+
}
443+
444+
const hasPrimaryActions = Array.isArray(this._actions.primary) && this._actions.primary.length > 0;
445+
if (
446+
(hasPrimaryActions && this._severity === Severity.Error) || // notification errors with actions are sticky
447+
(!hasPrimaryActions && this._expanded) || // notifications that got expanded are sticky
448+
(this._progress && !this._progress.state.done) // notifications with running progress are sticky
449+
) {
450+
return true;
451+
}
452+
453+
return false; // not sticky
454+
}
455+
439456
hasProgress(): boolean {
440457
return !!this._progress;
441458
}

src/vs/workbench/parts/experiments/electron-browser/experimentalPrompt.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@ export class ExperimentalPrompts extends Disposable implements IWorkbenchContrib
8080
};
8181
});
8282

83-
this.notificationService.prompt(Severity.Info, actionProperties.promptText, choices, () => {
84-
logTelemetry();
85-
this.experimentService.markAsCompleted(experiment.id);
83+
this.notificationService.prompt(Severity.Info, actionProperties.promptText, choices, {
84+
onCancel: () => {
85+
logTelemetry();
86+
this.experimentService.markAsCompleted(experiment.id);
87+
}
8688
});
8789
}
8890

src/vs/workbench/parts/experiments/test/electron-browser/experimentalPrompts.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ suite('Experimental Prompts', () => {
9494
};
9595

9696
instantiationService.stub(INotificationService, {
97-
prompt: (a: Severity, b: string, c: IPromptChoice[], d) => {
97+
prompt: (a: Severity, b: string, c: IPromptChoice[], options) => {
9898
assert.equal(b, promptText);
9999
assert.equal(c.length, 2);
100100
c[0].run();
@@ -118,7 +118,7 @@ suite('Experimental Prompts', () => {
118118
};
119119

120120
instantiationService.stub(INotificationService, {
121-
prompt: (a: Severity, b: string, c: IPromptChoice[], d) => {
121+
prompt: (a: Severity, b: string, c: IPromptChoice[], options) => {
122122
assert.equal(b, promptText);
123123
assert.equal(c.length, 2);
124124
c[1].run();
@@ -142,10 +142,10 @@ suite('Experimental Prompts', () => {
142142
};
143143

144144
instantiationService.stub(INotificationService, {
145-
prompt: (a: Severity, b: string, c: IPromptChoice[], d) => {
145+
prompt: (a: Severity, b: string, c: IPromptChoice[], options) => {
146146
assert.equal(b, promptText);
147147
assert.equal(c.length, 2);
148-
d();
148+
options.onCancel();
149149
}
150150
});
151151

0 commit comments

Comments
 (0)