Skip to content

Commit

Permalink
Merge pull request microsoft#201328 from microsoft/merogge/alert-cue
Browse files Browse the repository at this point in the history
add `alert` so braille users can detect events
  • Loading branch information
meganrogge authored Jan 3, 2024
2 parents 40c0f93 + f39061c commit d8a75ea
Show file tree
Hide file tree
Showing 14 changed files with 208 additions and 60 deletions.
5 changes: 2 additions & 3 deletions src/vs/editor/standalone/browser/standaloneServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1076,9 +1076,8 @@ class StandaloneAudioService implements IAudioCueService {
class StandaloneAccessibleNotificationService implements IAccessibleNotificationService {
_serviceBrand: undefined;

notify(event: AccessibleNotificationEvent, userGesture?: boolean | undefined): void {
// NOOP
}
notify(event: AccessibleNotificationEvent, userGesture?: boolean | undefined): void { }
notifyLineChanges(event: AccessibleNotificationEvent[]): void { }
}

export interface IEditorOverrideServices {
Expand Down
19 changes: 17 additions & 2 deletions src/vs/platform/accessibility/common/accessibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,26 @@ export const IAccessibleNotificationService = createDecorator<IAccessibleNotific
*/
export interface IAccessibleNotificationService {
readonly _serviceBrand: undefined;
notify(event: AccessibleNotificationEvent, userGesture?: boolean): void;
notify(event: AccessibleNotificationEvent, userGesture?: boolean, forceSound?: boolean, allowManyInParallel?: boolean): void;
notifyLineChanges(event: AccessibleNotificationEvent[]): void;
}

export const enum AccessibleNotificationEvent {
Clear = 'clear',
Save = 'save',
Format = 'format'
Format = 'format',
Breakpoint = 'breakpoint',
Error = 'error',
Warning = 'warning',
Folded = 'folded',
TerminalQuickFix = 'terminalQuickFix',
TerminalBell = 'terminalBell',
TerminalCommandFailed = 'terminalCommandFailed',
TaskCompleted = 'taskCompleted',
TaskFailed = 'taskFailed',
ChatRequestSent = 'chatRequestSent',
NotebookCellCompleted = 'notebookCellCompleted',
NotebookCellFailed = 'notebookCellFailed',
OnDebugBreak = 'onDebugBreak',
NoInlayHints = 'noInlayHints'
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,22 @@ export const enum AccessibilityVerbositySettingId {
}

export const enum AccessibilityAlertSettingId {
Clear = 'accessibility.alert.clear',
Save = 'accessibility.alert.save',
Format = 'accessibility.alert.format'
Format = 'accessibility.alert.format',
Breakpoint = 'accessibility.alert.breakpoint',
Error = 'accessibility.alert.error',
Warning = 'accessibility.alert.warning',
FoldedArea = 'accessibility.alert.foldedArea',
TerminalQuickFix = 'accessibility.alert.terminalQuickFix',
TerminalBell = 'accessibility.alert.terminalBell',
TerminalCommandFailed = 'accessibility.alert.terminalCommandFailed',
TaskCompleted = 'accessibility.alert.taskCompleted',
TaskFailed = 'accessibility.alert.taskFailed',
ChatRequestSent = 'accessibility.alert.chatRequestSent',
NotebookCellCompleted = 'accessibility.alert.notebookCellCompleted',
NotebookCellFailed = 'accessibility.alert.notebookCellFailed',
OnDebugBreak = 'accessibility.alert.onDebugBreak'
}

export const enum AccessibleViewProviderId {
Expand Down Expand Up @@ -131,7 +145,7 @@ const configuration: IConfigurationNode = {
...baseProperty
},
[AccessibilityAlertSettingId.Save]: {
'markdownDescription': localize('alert.save', "When in screen reader mode, alerts when a file is saved. Note that this will be ignored when {0} is enabled.", '`#audioCues.save#`'),
'markdownDescription': localize('alert.save', "Alerts when a file is saved. Also see {0}.", '`#audioCues.save#`'),
'type': 'string',
'enum': ['userGesture', 'always', 'never'],
'default': 'always',
Expand All @@ -142,8 +156,14 @@ const configuration: IConfigurationNode = {
],
tags: ['accessibility']
},
[AccessibilityAlertSettingId.Clear]: {
'markdownDescription': localize('alert.clear', "Alerts when a feature is cleared (for example, the terminal, Debug Console, or Output channel). Also see {0}.", '`#audioCues.clear#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.Format]: {
'markdownDescription': localize('alert.format', "When in screen reader mode, alerts when a file or notebook cell is formatted. Note that this will be ignored when {0} is enabled.", '`#audioCues.format#`'),
'markdownDescription': localize('alert.format', "Alerts when a file or notebook cell is formatted. Also see {0}.", '`#audioCues.format#`'),
'type': 'string',
'enum': ['userGesture', 'always', 'never'],
'default': 'always',
Expand All @@ -154,6 +174,84 @@ const configuration: IConfigurationNode = {
],
tags: ['accessibility']
},
[AccessibilityAlertSettingId.Breakpoint]: {
'markdownDescription': localize('alert.breakpoint', "Alerts when the active line has a breakpoint. Also see {0}.", '`#audioCues.breakpoint#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.Error]: {
'markdownDescription': localize('alert.error', "Alerts when the active line has an error. Also see {0}.", '`#audioCues.error#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.Warning]: {
'markdownDescription': localize('alert.warning', "Alerts when the active line has a warning. Also see {0}.", '`#audioCues.warning#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.FoldedArea]: {
'markdownDescription': localize('alert.foldedArea', "Alerts when the active line has a folded area that can be unfolded. Also see {0}.", '`#audioCues.foldedArea#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.TerminalQuickFix]: {
'markdownDescription': localize('alert.terminalQuickFix', "Alerts when there is an available terminal quick fix. Also see {0}.", '`#audioCues.terminalQuickFix#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.TerminalBell]: {
'markdownDescription': localize('alert.terminalBell', "Alerts when the terminal bell is activated. Also see {0}.", '`#audioCues.terminalBell#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.TerminalCommandFailed]: {
'markdownDescription': localize('alert.terminalCommandFailed', "Alerts when a terminal command fails (non-zero exit code). Also see {0}.", '`#audioCues.terminalCommandFailed#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.TaskFailed]: {
'markdownDescription': localize('alert.taskFailed', "Alerts when a task fails (non-zero exit code). Also see {0}.", '`#audioCues.taskFailed#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.TaskCompleted]: {
'markdownDescription': localize('alert.taskCompleted', "Alerts when a task completes successfully (zero exit code). Also see {0}.", '`#audioCues.taskCompleted#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.ChatRequestSent]: {
'markdownDescription': localize('alert.chatRequestSent', "Alerts when a chat request is sent. Also see {0}.", '`#audioCues.chatRequestSent#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.NotebookCellCompleted]: {
'markdownDescription': localize('alert.notebookCellCompleted', "Alerts when a notebook cell completes successfully. Also see {0}.", '`#audioCues.notebookCellCompleted#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.NotebookCellFailed]: {
'markdownDescription': localize('alert.notebookCellFailed', "Alerts when a notebook cell fails. Also see {0}.", '`#audioCues.notebookCellFailed#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityAlertSettingId.OnDebugBreak]: {
'markdownDescription': localize('alert.onDebugBreak', "Alerts when the debugger breaks. Also see {0}.", '`#audioCues.onDebugBreak#`'),
'type': 'boolean',
'default': true,
tags: ['accessibility']
},
[AccessibilityVoiceSettingId.SpeechTimeout]: {
'markdownDescription': localize('voice.speechTimeout', "The duration in milliseconds that voice speech recognition remains active after you stop speaking. For example in a chat session, the transcribed text is submitted automatically after the timeout is met. Set to `0` to disable this feature."),
'type': 'number',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,73 @@ import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/wo

export class AccessibleNotificationService extends Disposable implements IAccessibleNotificationService {
declare readonly _serviceBrand: undefined;
private _events: Map<AccessibleNotificationEvent, { audioCue: AudioCue; alertMessage: string; alertSetting?: string }> = new Map();
private _events: Map<AccessibleNotificationEvent, {
audioCue: AudioCue;
alertMessage: string;
alertSetting?: string;
}> = new Map();
constructor(
@IAudioCueService private readonly _audioCueService: IAudioCueService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
@IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService,
@ILogService private readonly _logService: ILogService) {
super();
this._events.set(AccessibleNotificationEvent.Clear, { audioCue: AudioCue.clear, alertMessage: localize('cleared', "Cleared") });
this._events.set(AccessibleNotificationEvent.Clear, { audioCue: AudioCue.clear, alertMessage: localize('cleared', "Cleared"), alertSetting: AccessibilityAlertSettingId.Clear });
this._events.set(AccessibleNotificationEvent.Save, { audioCue: AudioCue.save, alertMessage: localize('saved', "Saved"), alertSetting: AccessibilityAlertSettingId.Save });
this._events.set(AccessibleNotificationEvent.Format, { audioCue: AudioCue.format, alertMessage: localize('formatted', "Formatted"), alertSetting: AccessibilityAlertSettingId.Format });
this._events.set(AccessibleNotificationEvent.Breakpoint, { audioCue: AudioCue.break, alertMessage: localize('breakpoint', "Breakpoint"), alertSetting: AccessibilityAlertSettingId.Breakpoint });
this._events.set(AccessibleNotificationEvent.Error, { audioCue: AudioCue.error, alertMessage: localize('error', "Error"), alertSetting: AccessibilityAlertSettingId.Error });
this._events.set(AccessibleNotificationEvent.Warning, { audioCue: AudioCue.warning, alertMessage: localize('warning', "Warning"), alertSetting: AccessibilityAlertSettingId.Warning });
this._events.set(AccessibleNotificationEvent.Folded, { audioCue: AudioCue.foldedArea, alertMessage: localize('foldedArea', "Folded Area"), alertSetting: AccessibilityAlertSettingId.FoldedArea });
this._events.set(AccessibleNotificationEvent.TerminalQuickFix, { audioCue: AudioCue.terminalQuickFix, alertMessage: localize('terminalQuickFix', "Quick Fix"), alertSetting: AccessibilityAlertSettingId.TerminalQuickFix });
this._events.set(AccessibleNotificationEvent.TerminalBell, { audioCue: AudioCue.terminalBell, alertMessage: localize('terminalBell', "Terminal Bell"), alertSetting: AccessibilityAlertSettingId.TerminalBell });
this._events.set(AccessibleNotificationEvent.TerminalCommandFailed, { audioCue: AudioCue.terminalCommandFailed, alertMessage: localize('terminalCommandFailed', "Terminal Command Failed"), alertSetting: AccessibilityAlertSettingId.TerminalCommandFailed });
this._events.set(AccessibleNotificationEvent.TaskFailed, { audioCue: AudioCue.taskFailed, alertMessage: localize('taskFailed', "Task Failed"), alertSetting: AccessibilityAlertSettingId.TaskFailed });
this._events.set(AccessibleNotificationEvent.TaskCompleted, { audioCue: AudioCue.taskCompleted, alertMessage: localize('taskCompleted', "Task Completed"), alertSetting: AccessibilityAlertSettingId.TaskCompleted });
this._events.set(AccessibleNotificationEvent.ChatRequestSent, { audioCue: AudioCue.chatRequestSent, alertMessage: localize('chatRequestSent', "Chat Request Sent"), alertSetting: AccessibilityAlertSettingId.ChatRequestSent });
this._events.set(AccessibleNotificationEvent.NotebookCellCompleted, { audioCue: AudioCue.notebookCellCompleted, alertMessage: localize('notebookCellCompleted', "Notebook Cell Completed"), alertSetting: AccessibilityAlertSettingId.NotebookCellCompleted });
this._events.set(AccessibleNotificationEvent.NotebookCellFailed, { audioCue: AudioCue.notebookCellFailed, alertMessage: localize('notebookCellFailed', "Notebook Cell Failed"), alertSetting: AccessibilityAlertSettingId.NotebookCellFailed });
this._events.set(AccessibleNotificationEvent.OnDebugBreak, { audioCue: AudioCue.onDebugBreak, alertMessage: localize('onDebugBreak', "On Debug Break"), alertSetting: AccessibilityAlertSettingId.OnDebugBreak });

this._register(this._workingCopyService.onDidSave((e) => this._notify(AccessibleNotificationEvent.Save, e.reason === SaveReason.EXPLICIT)));
this._register(this._workingCopyService.onDidSave((e) => this._notifyBasedOnUserGesture(AccessibleNotificationEvent.Save, e.reason === SaveReason.EXPLICIT)));
}

notify(event: AccessibleNotificationEvent, userGesture?: boolean): void {
notify(event: AccessibleNotificationEvent, userGesture?: boolean, forceSound?: boolean, allowManyInParallel?: boolean): void {
if (event === AccessibleNotificationEvent.Format) {
return this._notify(event, userGesture);
return this._notifyBasedOnUserGesture(AccessibleNotificationEvent.Format, userGesture);
}
const { audioCue, alertMessage } = this._events.get(event)!;
const audioCueValue = this._configurationService.getValue(audioCue.settingsKey);
if (audioCueValue === 'on' || audioCueValue === 'auto' && this._accessibilityService.isScreenReaderOptimized()) {
const { audioCue, alertMessage, alertSetting } = this._events.get(event)!;
const audioCueSetting = this._configurationService.getValue(audioCue.settingsKey);
if (audioCueSetting === 'on' || audioCueSetting === 'auto' && this._accessibilityService.isScreenReaderOptimized() || forceSound) {
this._logService.debug('AccessibleNotificationService playing sound: ', audioCue.name);
this._audioCueService.playAudioCue(audioCue);
} else {
this._audioCueService.playSound(audioCue.sound.getSound(), allowManyInParallel);
}

if (alertSetting && this._configurationService.getValue(alertSetting) === true) {
this._logService.debug('AccessibleNotificationService alerting: ', alertMessage);
this._accessibilityService.alert(alertMessage);
}
}

private _notify(event: AccessibleNotificationEvent, userGesture?: boolean): void {
/**
* Line feature contributions can use this to notify the user of changes to the line.
*/
notifyLineChanges(events: AccessibleNotificationEvent[]): void {
const audioCues = events.map(e => this._events.get(e)!.audioCue);
if (audioCues.length) {
this._logService.debug('AccessibleNotificationService playing sounds if enabled: ', events.map(e => this._events.get(e)!.audioCue.name).join(', '));
this._audioCueService.playAudioCues(audioCues);
}

const alerts = events.filter(e => this._configurationService.getValue(this._events.get(e)!.alertSetting!) === true).map(e => this._events.get(e)?.alertMessage);
if (alerts.length) {
this._logService.debug('AccessibleNotificationService alerting: ', alerts.join(', '));
this._accessibilityService.alert(alerts.join(', '));
}
}

private _notifyBasedOnUserGesture(event: AccessibleNotificationEvent, userGesture?: boolean): void {
const { audioCue, alertMessage, alertSetting } = this._events.get(event)!;
if (!alertSetting) {
return;
Expand All @@ -55,11 +91,6 @@ export class AccessibleNotificationService extends Disposable implements IAccess
this._logService.debug('AccessibleNotificationService playing sound: ', audioCue.name);
// Play sound bypasses the usual audio cue checks IE screen reader optimized, auto, etc.
this._audioCueService.playSound(audioCue.sound.getSound(), true);
return;
}
if (audioCueSetting !== 'never') {
// Never do both sound and alert
return;
}
const alertSettingValue: NotificationSetting = this._configurationService.getValue(alertSetting);
if (this._shouldNotify(alertSettingValue, userGesture)) {
Expand All @@ -79,4 +110,5 @@ export class TestAccessibleNotificationService extends Disposable implements IAc
declare readonly _serviceBrand: undefined;

notify(event: AccessibleNotificationEvent, userGesture?: boolean): void { }
notifyLineChanges(event: AccessibleNotificationEvent[]): void { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { autorunWithStore, observableFromEvent } from 'vs/base/common/observable';
import { AccessibleNotificationEvent, IAccessibleNotificationService } from 'vs/platform/accessibility/common/accessibility';
import { IAudioCueService, AudioCue, AudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IDebugService, IDebugSession } from 'vs/workbench/contrib/debug/common/debug';
Expand All @@ -15,7 +16,8 @@ export class AudioCueLineDebuggerContribution

constructor(
@IDebugService debugService: IDebugService,
@IAudioCueService private readonly audioCueService: AudioCueService,
@IAudioCueService audioCueService: AudioCueService,
@IAccessibleNotificationService private readonly accessibleNotificationService: IAccessibleNotificationService
) {
super();

Expand Down Expand Up @@ -60,7 +62,7 @@ export class AudioCueLineDebuggerContribution
const stoppedDetails = session.getStoppedDetails();
const BREAKPOINT_STOP_REASON = 'breakpoint';
if (stoppedDetails && stoppedDetails.reason === BREAKPOINT_STOP_REASON) {
this.audioCueService.playAudioCue(AudioCue.onDebugBreak);
this.accessibleNotificationService.notify(AccessibleNotificationEvent.OnDebugBreak);
}
});

Expand Down
Loading

0 comments on commit d8a75ea

Please sign in to comment.