This commit is contained in:
Henning Dieterichs
2023-08-03 17:43:54 +02:00
committed by Henning Dieterichs
parent 4df2340a3c
commit e8b1e2bea8
9 changed files with 68 additions and 28 deletions

View File

@@ -142,9 +142,9 @@ class ViewModel extends Disposable {
/** @description play audio-cue for diff */
const currentViewItem = this.currentElement.read(reader);
if (currentViewItem?.type === LineType.Deleted) {
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted);
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { source: 'accessibleDiffViewer.currentElementChanged' });
} else if (currentViewItem?.type === LineType.Added) {
this._audioCueService.playAudioCue(AudioCue.diffLineInserted);
this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { source: 'accessibleDiffViewer.currentElementChanged' });
}
}));

View File

@@ -255,11 +255,11 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
if (e?.reason === CursorChangeReason.Explicit) {
const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.modifiedRange.contains(e.position.lineNumber));
if (diff?.lineRangeMapping.modifiedRange.isEmpty) {
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted);
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' });
} else if (diff?.lineRangeMapping.originalRange.isEmpty) {
this._audioCueService.playAudioCue(AudioCue.diffLineInserted);
this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' });
} else if (diff) {
this._audioCueService.playAudioCue(AudioCue.diffLineModified);
this._audioCueService.playAudioCue(AudioCue.diffLineModified, { source: 'diffEditor.cursorPositionChanged' });
}
}
}));
@@ -460,11 +460,11 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
this._goTo(diff);
if (diff.lineRangeMapping.modifiedRange.isEmpty) {
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted);
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { source: 'diffEditor.goToDiff' });
} else if (diff.lineRangeMapping.originalRange.isEmpty) {
this._audioCueService.playAudioCue(AudioCue.diffLineInserted);
this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { source: 'diffEditor.goToDiff' });
} else if (diff) {
this._audioCueService.playAudioCue(AudioCue.diffLineModified);
this._audioCueService.playAudioCue(AudioCue.diffLineModified, { source: 'diffEditor.goToDiff' });
}
}

View File

@@ -224,11 +224,11 @@ export class DiffNavigator extends Disposable implements IDiffNavigator {
}
const insertedOrModified = modifiedEditor.getLineDecorations(lineNumber).find(l => l.options.className === 'line-insert');
if (insertedOrModified) {
this._audioCueService.playAudioCue(AudioCue.diffLineModified, true);
this._audioCueService.playAudioCue(AudioCue.diffLineModified, { allowManyInParallel: true });
} else if (jumpToChange) {
// The modified editor does not include deleted lines, but when
// we are moved to the area where lines were deleted, play this cue
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, true);
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { allowManyInParallel: true });
} else {
return;
}

View File

@@ -325,9 +325,9 @@ export class DiffReview extends Disposable {
}
const element = !type ? current : type === 'next' ? current?.nextElementSibling : current?.previousElementSibling;
if (element?.classList.contains(DiffEditorLineClasses.Insert)) {
this._audioCueService.playAudioCue(AudioCue.diffLineInserted, true);
this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { allowManyInParallel: true });
} else if (element?.classList.contains(DiffEditorLineClasses.Delete)) {
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, true);
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { allowManyInParallel: true });
}
this.scrollbar.scanDomNode();
}

View File

@@ -1039,7 +1039,7 @@ class StandaloneContextMenuService extends ContextMenuService {
class StandaloneAudioService implements IAudioCueService {
_serviceBrand: undefined;
async playAudioCue(cue: AudioCue, allowManyInParallel?: boolean | undefined): Promise<void> {
async playAudioCue(cue: AudioCue, options: {}): Promise<void> {
}
async playAudioCues(cues: AudioCue[]): Promise<void> {

View File

@@ -11,13 +11,14 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { Event } from 'vs/base/common/event';
import { localize } from 'vs/nls';
import { observableFromEvent, derived } from 'vs/base/common/observable';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
export const IAudioCueService = createDecorator<IAudioCueService>('audioCue');
export interface IAudioCueService {
readonly _serviceBrand: undefined;
playAudioCue(cue: AudioCue, allowManyInParallel?: boolean): Promise<void>;
playAudioCues(cues: AudioCue[]): Promise<void>;
playAudioCue(cue: AudioCue, options?: IAudioCueOptions): Promise<void>;
playAudioCues(cues: (AudioCue | { cue: AudioCue; source: string })[]): Promise<void>;
isEnabled(cue: AudioCue): boolean;
onEnabledChanged(cue: AudioCue): Event<void>;
@@ -25,33 +26,72 @@ export interface IAudioCueService {
playAudioCueLoop(cue: AudioCue, milliseconds: number): IDisposable;
}
export interface IAudioCueOptions {
allowManyInParallel?: boolean;
source?: string;
}
export class AudioCueService extends Disposable implements IAudioCueService {
readonly _serviceBrand: undefined;
sounds: Map<string, HTMLAudioElement> = new Map();
private readonly sounds: Map<string, HTMLAudioElement> = new Map();
private readonly screenReaderAttached = observableFromEvent(
this.accessibilityService.onDidChangeScreenReaderOptimized,
() => /** @description accessibilityService.onDidChangeScreenReaderOptimized */ this.accessibilityService.isScreenReaderOptimized()
);
private readonly sentTelemetry = new Set<string>();
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IAccessibilityService private readonly accessibilityService: IAccessibilityService
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
) {
super();
}
public async playAudioCue(cue: AudioCue, allowManyInParallel = false): Promise<void> {
public async playAudioCue(cue: AudioCue, options: IAudioCueOptions = {}): Promise<void> {
if (this.isEnabled(cue)) {
await this.playSound(cue.sound.getSound(), allowManyInParallel);
this.sendAudioCueTelemetry(cue, options.source);
await this.playSound(cue.sound.getSound(), options.allowManyInParallel);
}
}
public async playAudioCues(cues: AudioCue[]): Promise<void> {
public async playAudioCues(cues: (AudioCue | { cue: AudioCue; source: string })[]): Promise<void> {
for (const cue of cues) {
this.sendAudioCueTelemetry('cue' in cue ? cue.cue : cue, 'source' in cue ? cue.source : undefined);
}
// Some audio cues might reuse sounds. Don't play the same sound twice.
const sounds = new Set(cues.filter(cue => this.isEnabled(cue)).map(cue => cue.sound.getSound()));
const sounds = new Set(cues.map(c => 'cue' in c ? c.cue : c).filter(cue => this.isEnabled(cue)).map(cue => cue.sound.getSound()));
await Promise.all(Array.from(sounds).map(sound => this.playSound(sound, true)));
}
private sendAudioCueTelemetry(cue: AudioCue, source: string | undefined): void {
const isScreenReaderOptimized = this.accessibilityService.isScreenReaderOptimized();
const key = cue.name + (source ? `::${source}` : '') + (isScreenReaderOptimized ? '{screenReaderOptimized}' : '');
// Only send once per user session
if (this.sentTelemetry.has(key) || this.getVolumeInPercent() === 0) {
return;
}
this.sentTelemetry.add(key);
this.telemetryService.publicLog2<{
audioCue: string;
source: string;
isScreenReaderOptimized: boolean;
}, {
owner: 'hediet';
audioCue: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The audio cue that was played.' };
source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source that triggered the audio cue (e.g. "diffEditorNavigation").' };
isScreenReaderOptimized: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is using a screen reader' };
comment: 'This data is collected to understand how audio cues are used and if more audio cues should be added.';
}>('audioCue.played', {
audioCue: cue.name,
source: source ?? '',
isScreenReaderOptimized,
});
}
private getVolumeInPercent(): number {
const volume = this.configurationService.getValue<number>('audioCues.volume');
@@ -92,7 +132,7 @@ export class AudioCueService extends Disposable implements IAudioCueService {
let playing = true;
const playSound = () => {
if (playing) {
this.playAudioCue(cue, true).finally(() => {
this.playAudioCue(cue, { allowManyInParallel: true }).finally(() => {
setTimeout(() => {
if (playing) {
playSound();

View File

@@ -30,7 +30,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi
}, CHAT_RESPONSE_PENDING_ALLOWANCE_MS));
}
acceptRequest(): void {
this._audioCueService.playAudioCue(AudioCue.chatRequestSent, true);
this._audioCueService.playAudioCue(AudioCue.chatRequestSent, { allowManyInParallel: true });
this._runOnceScheduler.schedule();
}
acceptResponse(response?: IChatResponseViewModel | string): void {
@@ -42,7 +42,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi
if (this._lastResponse === responseContent) {
return;
}
this._audioCueService.playAudioCue(AudioCue.chatResponseReceived, true);
this._audioCueService.playAudioCue(AudioCue.chatResponseReceived, { allowManyInParallel: true });
this._hasReceivedRequest = false;
if (!response) {
return;

View File

@@ -662,13 +662,13 @@ async function playAudioCueForChange(change: IChange, audioCueService: IAudioCue
const changeType = getChangeType(change);
switch (changeType) {
case ChangeType.Add:
audioCueService.playAudioCue(AudioCue.diffLineInserted, true);
audioCueService.playAudioCue(AudioCue.diffLineInserted, { allowManyInParallel: true, source: 'dirtyDiffDecoration' });
break;
case ChangeType.Delete:
audioCueService.playAudioCue(AudioCue.diffLineDeleted, true);
audioCueService.playAudioCue(AudioCue.diffLineDeleted, { allowManyInParallel: true, source: 'dirtyDiffDecoration' });
break;
case ChangeType.Modify:
audioCueService.playAudioCue(AudioCue.diffLineModified, true);
audioCueService.playAudioCue(AudioCue.diffLineModified, { allowManyInParallel: true, source: 'dirtyDiffDecoration' });
break;
}
}

View File

@@ -147,7 +147,7 @@ export class AccessibleBufferWidget extends TerminalAccessibleWidget {
return;
}
if (activeItem.exitCode) {
this._audioCueService.playAudioCue(AudioCue.error, true);
this._audioCueService.playAudioCue(AudioCue.error, { allowManyInParallel: true, source: 'accessibleBufferWidget' });
}
this.editorWidget.revealLine(activeItem.lineNumber, 0);
});