Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add alert so braille users can detect events #201328

Merged
merged 7 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Member

@hediet hediet Jan 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this LineChanges?
I don't understand what notifyLineChanges([AccessibleNotificationEvent.Save]) would mean. Maybe that needs to be a different enum?

}

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