Path: blob/main/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';6import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';7import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';8import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';9import { INotificationViewItem, isNotificationViewItem, NotificationsModel } from '../../../common/notifications.js';10import { MenuRegistry, MenuId } from '../../../../platform/actions/common/actions.js';11import { localize, localize2 } from '../../../../nls.js';12import { IListService, WorkbenchList } from '../../../../platform/list/browser/listService.js';13import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';14import { NotificationFocusedContext, NotificationsCenterVisibleContext, NotificationsToastsVisibleContext } from '../../../common/contextkeys.js';15import { INotificationService, INotificationSourceFilter, NotificationsFilter } from '../../../../platform/notification/common/notification.js';16import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';17import { ActionRunner, IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../base/common/actions.js';18import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';19import { DisposableStore } from '../../../../base/common/lifecycle.js';20import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';2122// Center23export const SHOW_NOTIFICATIONS_CENTER = 'notifications.showList';24export const HIDE_NOTIFICATIONS_CENTER = 'notifications.hideList';25const TOGGLE_NOTIFICATIONS_CENTER = 'notifications.toggleList';2627// Toasts28export const HIDE_NOTIFICATION_TOAST = 'notifications.hideToasts';29const FOCUS_NOTIFICATION_TOAST = 'notifications.focusToasts';30const FOCUS_NEXT_NOTIFICATION_TOAST = 'notifications.focusNextToast';31const FOCUS_PREVIOUS_NOTIFICATION_TOAST = 'notifications.focusPreviousToast';32const FOCUS_FIRST_NOTIFICATION_TOAST = 'notifications.focusFirstToast';33const FOCUS_LAST_NOTIFICATION_TOAST = 'notifications.focusLastToast';3435// Notification36export const COLLAPSE_NOTIFICATION = 'notification.collapse';37export const EXPAND_NOTIFICATION = 'notification.expand';38export const ACCEPT_PRIMARY_ACTION_NOTIFICATION = 'notification.acceptPrimaryAction';39const TOGGLE_NOTIFICATION = 'notification.toggle';40export const CLEAR_NOTIFICATION = 'notification.clear';41export const CLEAR_ALL_NOTIFICATIONS = 'notifications.clearAll';42export const TOGGLE_DO_NOT_DISTURB_MODE = 'notifications.toggleDoNotDisturbMode';43export const TOGGLE_DO_NOT_DISTURB_MODE_BY_SOURCE = 'notifications.toggleDoNotDisturbModeBySource';4445export interface INotificationsCenterController {46readonly isVisible: boolean;4748show(): void;49hide(): void;5051clearAll(): void;52}5354export interface INotificationsToastController {55focus(): void;56focusNext(): void;57focusPrevious(): void;58focusFirst(): void;59focusLast(): void;6061hide(): void;62}6364export function getNotificationFromContext(listService: IListService, context?: unknown): INotificationViewItem | undefined {65if (isNotificationViewItem(context)) {66return context;67}6869const list = listService.lastFocusedList;70if (list instanceof WorkbenchList) {71let element = list.getFocusedElements()[0];72if (!isNotificationViewItem(element)) {73if (list.isDOMFocused()) {74// the notification list might have received focus75// via keyboard and might not have a focused element.76// in that case just return the first element77// https://github.com/microsoft/vscode/issues/19170578element = list.element(0);79}80}8182if (isNotificationViewItem(element)) {83return element;84}85}8687return undefined;88}8990export function registerNotificationCommands(center: INotificationsCenterController, toasts: INotificationsToastController, model: NotificationsModel): void {9192// Show Notifications Cneter93KeybindingsRegistry.registerCommandAndKeybindingRule({94id: SHOW_NOTIFICATIONS_CENTER,95weight: KeybindingWeight.WorkbenchContrib,96primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyN),97handler: () => {98toasts.hide();99center.show();100}101});102103// Hide Notifications Center104KeybindingsRegistry.registerCommandAndKeybindingRule({105id: HIDE_NOTIFICATIONS_CENTER,106weight: KeybindingWeight.WorkbenchContrib + 50,107when: NotificationsCenterVisibleContext,108primary: KeyCode.Escape,109handler: () => center.hide()110});111112// Toggle Notifications Center113CommandsRegistry.registerCommand(TOGGLE_NOTIFICATIONS_CENTER, () => {114if (center.isVisible) {115center.hide();116} else {117toasts.hide();118center.show();119}120});121122// Clear Notification123KeybindingsRegistry.registerCommandAndKeybindingRule({124id: CLEAR_NOTIFICATION,125weight: KeybindingWeight.WorkbenchContrib,126when: NotificationFocusedContext,127primary: KeyCode.Delete,128mac: {129primary: KeyMod.CtrlCmd | KeyCode.Backspace130},131handler: (accessor, args?) => {132const accessibilitySignalService = accessor.get(IAccessibilitySignalService);133const notification = getNotificationFromContext(accessor.get(IListService), args);134if (notification && !notification.hasProgress) {135notification.close();136accessibilitySignalService.playSignal(AccessibilitySignal.clear);137}138}139});140141// Expand Notification142KeybindingsRegistry.registerCommandAndKeybindingRule({143id: EXPAND_NOTIFICATION,144weight: KeybindingWeight.WorkbenchContrib,145when: NotificationFocusedContext,146primary: KeyCode.RightArrow,147handler: (accessor, args?) => {148const notification = getNotificationFromContext(accessor.get(IListService), args);149notification?.expand();150}151});152153// Accept Primary Action154KeybindingsRegistry.registerCommandAndKeybindingRule({155id: ACCEPT_PRIMARY_ACTION_NOTIFICATION,156weight: KeybindingWeight.WorkbenchContrib,157when: ContextKeyExpr.or(NotificationFocusedContext, NotificationsToastsVisibleContext),158primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA,159handler: (accessor) => {160const actionRunner = accessor.get(IInstantiationService).createInstance(NotificationActionRunner);161const notification = getNotificationFromContext(accessor.get(IListService)) || model.notifications.at(0);162if (!notification) {163return;164}165const primaryAction = notification.actions?.primary ? notification.actions.primary.at(0) : undefined;166if (!primaryAction) {167return;168}169actionRunner.run(primaryAction, notification);170notification.close();171actionRunner.dispose();172}173});174175// Collapse Notification176KeybindingsRegistry.registerCommandAndKeybindingRule({177id: COLLAPSE_NOTIFICATION,178weight: KeybindingWeight.WorkbenchContrib,179when: NotificationFocusedContext,180primary: KeyCode.LeftArrow,181handler: (accessor, args?) => {182const notification = getNotificationFromContext(accessor.get(IListService), args);183notification?.collapse();184}185});186187// Toggle Notification188KeybindingsRegistry.registerCommandAndKeybindingRule({189id: TOGGLE_NOTIFICATION,190weight: KeybindingWeight.WorkbenchContrib,191when: NotificationFocusedContext,192primary: KeyCode.Space,193secondary: [KeyCode.Enter],194handler: accessor => {195const notification = getNotificationFromContext(accessor.get(IListService));196notification?.toggle();197}198});199200// Hide Toasts201CommandsRegistry.registerCommand(HIDE_NOTIFICATION_TOAST, accessor => {202toasts.hide();203});204205KeybindingsRegistry.registerKeybindingRule({206id: HIDE_NOTIFICATION_TOAST,207weight: KeybindingWeight.WorkbenchContrib - 50, // lower when not focused (e.g. let editor suggest win over this command)208when: NotificationsToastsVisibleContext,209primary: KeyCode.Escape210});211212KeybindingsRegistry.registerKeybindingRule({213id: HIDE_NOTIFICATION_TOAST,214weight: KeybindingWeight.WorkbenchContrib + 100, // higher when focused215when: ContextKeyExpr.and(NotificationsToastsVisibleContext, NotificationFocusedContext),216primary: KeyCode.Escape217});218219// Focus Toasts220CommandsRegistry.registerCommand(FOCUS_NOTIFICATION_TOAST, () => toasts.focus());221222// Focus Next Toast223KeybindingsRegistry.registerCommandAndKeybindingRule({224id: FOCUS_NEXT_NOTIFICATION_TOAST,225weight: KeybindingWeight.WorkbenchContrib,226when: ContextKeyExpr.and(NotificationFocusedContext, NotificationsToastsVisibleContext),227primary: KeyCode.DownArrow,228handler: () => {229toasts.focusNext();230}231});232233// Focus Previous Toast234KeybindingsRegistry.registerCommandAndKeybindingRule({235id: FOCUS_PREVIOUS_NOTIFICATION_TOAST,236weight: KeybindingWeight.WorkbenchContrib,237when: ContextKeyExpr.and(NotificationFocusedContext, NotificationsToastsVisibleContext),238primary: KeyCode.UpArrow,239handler: () => {240toasts.focusPrevious();241}242});243244// Focus First Toast245KeybindingsRegistry.registerCommandAndKeybindingRule({246id: FOCUS_FIRST_NOTIFICATION_TOAST,247weight: KeybindingWeight.WorkbenchContrib,248when: ContextKeyExpr.and(NotificationFocusedContext, NotificationsToastsVisibleContext),249primary: KeyCode.PageUp,250secondary: [KeyCode.Home],251handler: () => {252toasts.focusFirst();253}254});255256// Focus Last Toast257KeybindingsRegistry.registerCommandAndKeybindingRule({258id: FOCUS_LAST_NOTIFICATION_TOAST,259weight: KeybindingWeight.WorkbenchContrib,260when: ContextKeyExpr.and(NotificationFocusedContext, NotificationsToastsVisibleContext),261primary: KeyCode.PageDown,262secondary: [KeyCode.End],263handler: () => {264toasts.focusLast();265}266});267268// Clear All Notifications269CommandsRegistry.registerCommand(CLEAR_ALL_NOTIFICATIONS, () => center.clearAll());270271// Toggle Do Not Disturb Mode272CommandsRegistry.registerCommand(TOGGLE_DO_NOT_DISTURB_MODE, accessor => {273const notificationService = accessor.get(INotificationService);274275notificationService.setFilter(notificationService.getFilter() === NotificationsFilter.ERROR ? NotificationsFilter.OFF : NotificationsFilter.ERROR);276});277278// Configure Do Not Disturb by Source279CommandsRegistry.registerCommand(TOGGLE_DO_NOT_DISTURB_MODE_BY_SOURCE, accessor => {280const notificationService = accessor.get(INotificationService);281const quickInputService = accessor.get(IQuickInputService);282283const sortedFilters = notificationService.getFilters().sort((a, b) => a.label.localeCompare(b.label));284285const disposables = new DisposableStore();286const picker = disposables.add(quickInputService.createQuickPick<IQuickPickItem & INotificationSourceFilter>());287288picker.items = sortedFilters.map(source => ({289id: source.id,290label: source.label,291tooltip: `${source.label} (${source.id})`,292filter: source.filter293}));294295picker.canSelectMany = true;296picker.placeholder = localize('selectSources', "Select sources to enable all notifications from");297picker.selectedItems = picker.items.filter(item => item.filter === NotificationsFilter.OFF);298299picker.show();300301disposables.add(picker.onDidAccept(async () => {302for (const item of picker.items) {303notificationService.setFilter({304id: item.id,305label: item.label,306filter: picker.selectedItems.includes(item) ? NotificationsFilter.OFF : NotificationsFilter.ERROR307});308}309310picker.hide();311}));312313disposables.add(picker.onDidHide(() => disposables.dispose()));314});315316// Commands for Command Palette317const category = localize2('notifications', 'Notifications');318MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: SHOW_NOTIFICATIONS_CENTER, title: localize2('showNotifications', 'Show Notifications'), category } });319MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: HIDE_NOTIFICATIONS_CENTER, title: localize2('hideNotifications', 'Hide Notifications'), category }, when: NotificationsCenterVisibleContext });320MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: CLEAR_ALL_NOTIFICATIONS, title: localize2('clearAllNotifications', 'Clear All Notifications'), category } });321MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: ACCEPT_PRIMARY_ACTION_NOTIFICATION, title: localize2('acceptNotificationPrimaryAction', 'Accept Notification Primary Action'), category } });322MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: TOGGLE_DO_NOT_DISTURB_MODE, title: localize2('toggleDoNotDisturbMode', 'Toggle Do Not Disturb Mode'), category } });323MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: TOGGLE_DO_NOT_DISTURB_MODE_BY_SOURCE, title: localize2('toggleDoNotDisturbModeBySource', 'Toggle Do Not Disturb Mode By Source...'), category } });324MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: FOCUS_NOTIFICATION_TOAST, title: localize2('focusNotificationToasts', 'Focus Notification Toast'), category }, when: NotificationsToastsVisibleContext });325}326327328export class NotificationActionRunner extends ActionRunner {329330constructor(331@ITelemetryService private readonly telemetryService: ITelemetryService,332@INotificationService private readonly notificationService: INotificationService333) {334super();335}336337protected override async runAction(action: IAction, context: unknown): Promise<void> {338this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: action.id, from: 'message' });339340// Run and make sure to notify on any error again341try {342await super.runAction(action, context);343} catch (error) {344this.notificationService.error(error);345}346}347}348349350