Skip to content

Commit be73f5e

Browse files
authored
Fix context menu position when zoomed (microsoft#205276)
* Add codefeature toggle * Fix button size * Fix context menu position when zoomed Also set toggle disabled color
1 parent b4aab58 commit be73f5e

File tree

4 files changed

+214
-18
lines changed

4 files changed

+214
-18
lines changed

src/vs/base/common/network.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ export namespace Schemas {
116116
* Scheme used for special rendering of settings in the release notes
117117
*/
118118
export const codeSetting = 'code-setting';
119+
120+
/**
121+
* Scheme used for special rendering of features in the release notes
122+
*/
123+
export const codeFeature = 'code-feature';
119124
}
120125

121126
export function matchesScheme(target: URI | string, scheme: string): boolean {

src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
1414
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
1515
import { IAction } from 'vs/base/common/actions';
1616

17-
const codeSettingRegex = /^<span codesetting="([^\s"\:]+)(?::([^\s"]+))?">/;
17+
const codeSettingRegex = /^<span (codesetting|codefeature)="([^\s"\:]+)(?::([^\s"]+))?">/;
1818

1919
export class SimpleSettingRenderer {
2020
private _defaultSettings: DefaultSettings;
2121
private _updatedSettings = new Map<string, any>(); // setting ID to user's original setting value
2222
private _encounteredSettings = new Map<string, ISetting>(); // setting ID to setting
23+
private _featuredSettings = new Map<string, any>(); // setting ID to feature value
2324

2425
constructor(
2526
@IConfigurationService private readonly _configurationService: IConfigurationService,
@@ -29,12 +30,20 @@ export class SimpleSettingRenderer {
2930
this._defaultSettings = new DefaultSettings([], ConfigurationTarget.USER);
3031
}
3132

33+
get featuredSettingStates(): Map<string, boolean> {
34+
const result = new Map<string, boolean>();
35+
for (const [settingId, value] of this._featuredSettings) {
36+
result.set(settingId, this._configurationService.getValue(settingId) === value);
37+
}
38+
return result;
39+
}
40+
3241
getHtmlRenderer(): (html: string) => string {
3342
return (html): string => {
3443
const match = codeSettingRegex.exec(html);
35-
if (match && match.length === 3) {
36-
const settingId = match[1];
37-
const rendered = this.render(settingId, match[2]);
44+
if (match && match.length === 4) {
45+
const settingId = match[2];
46+
const rendered = this.render(settingId, match[3], match[1] === 'codefeature');
3847
if (rendered) {
3948
html = html.replace(codeSettingRegex, rendered);
4049
}
@@ -47,6 +56,10 @@ export class SimpleSettingRenderer {
4756
return `${Schemas.codeSetting}://${settingId}${value ? `/${value}` : ''}`;
4857
}
4958

59+
featureToUriString(settingId: string, value?: any): string {
60+
return `${Schemas.codeFeature}://${settingId}${value ? `/${value}` : ''}`;
61+
}
62+
5063
private settingsGroups: ISettingsGroup[] | undefined = undefined;
5164
private getSetting(settingId: string): ISetting | undefined {
5265
if (!this.settingsGroups) {
@@ -69,7 +82,7 @@ export class SimpleSettingRenderer {
6982
}
7083

7184
parseValue(settingId: string, value: string): any {
72-
if (value === 'undefined') {
85+
if (value === 'undefined' || value === '') {
7386
return undefined;
7487
}
7588
const setting = this.getSetting(settingId);
@@ -88,13 +101,16 @@ export class SimpleSettingRenderer {
88101
}
89102
}
90103

91-
private render(settingId: string, newValue: string): string | undefined {
104+
private render(settingId: string, newValue: string, asFeature: boolean): string | undefined {
92105
const setting = this.getSetting(settingId);
93106
if (!setting) {
94107
return '';
95108
}
96-
97-
return this.renderSetting(setting, newValue);
109+
if (asFeature) {
110+
return this.renderFeature(setting, newValue);
111+
} else {
112+
return this.renderSetting(setting, newValue);
113+
}
98114
}
99115

100116
private viewInSettingsMessage(settingId: string, alreadyDisplayed: boolean) {
@@ -149,7 +165,16 @@ export class SimpleSettingRenderer {
149165
private renderSetting(setting: ISetting, newValue: string | undefined): string | undefined {
150166
const href = this.settingToUriString(setting.key, newValue);
151167
const title = nls.localize('changeSettingTitle', "Try feature");
152-
return `<span><a href="${href}" class="codesetting" title="${title}" aria-rol="button"><svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M9.1 4.4L8.6 2H7.4l-.5 2.4-.7.3-2-1.3-.9.8 1.3 2-.2.7-2.4.5v1.2l2.4.5.3.8-1.3 2 .8.8 2-1.3.8.3.4 2.3h1.2l.5-2.4.8-.3 2 1.3.8-.8-1.3-2 .3-.8 2.3-.4V7.4l-2.4-.5-.3-.8 1.3-2-.8-.8-2 1.3-.7-.2zM9.4 1l.5 2.4L12 2.1l2 2-1.4 2.1 2.4.4v2.8l-2.4.5L14 12l-2 2-2.1-1.4-.5 2.4H6.6l-.5-2.4L4 13.9l-2-2 1.4-2.1L1 9.4V6.6l2.4-.5L2.1 4l2-2 2.1 1.4.4-2.4h2.8zm.6 7c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zM8 9c.6 0 1-.4 1-1s-.4-1-1-1-1 .4-1 1 .4 1 1 1z"/></svg></a>`;
168+
return `<span><a href="${href}" class="codesetting" title="${title}" aria-role="button"><svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M9.1 4.4L8.6 2H7.4l-.5 2.4-.7.3-2-1.3-.9.8 1.3 2-.2.7-2.4.5v1.2l2.4.5.3.8-1.3 2 .8.8 2-1.3.8.3.4 2.3h1.2l.5-2.4.8-.3 2 1.3.8-.8-1.3-2 .3-.8 2.3-.4V7.4l-2.4-.5-.3-.8 1.3-2-.8-.8-2 1.3-.7-.2zM9.4 1l.5 2.4L12 2.1l2 2-1.4 2.1 2.4.4v2.8l-2.4.5L14 12l-2 2-2.1-1.4-.5 2.4H6.6l-.5-2.4L4 13.9l-2-2 1.4-2.1L1 9.4V6.6l2.4-.5L2.1 4l2-2 2.1 1.4.4-2.4h2.8zm.6 7c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zM8 9c.6 0 1-.4 1-1s-.4-1-1-1-1 .4-1 1 .4 1 1 1z"/></svg></a>`;
169+
}
170+
171+
private renderFeature(setting: ISetting, newValue: string): string | undefined {
172+
const href = this.featureToUriString(setting.key, newValue);
173+
const parsedValue = this.parseValue(setting.key, newValue);
174+
const isChecked = this._configurationService.getValue(setting.key) === parsedValue;
175+
this._featuredSettings.set(setting.key, parsedValue);
176+
const title = nls.localize('changeFeatureTitle', "Toggle feature with setting {0}", setting.key);
177+
return `<span><div class="codefeature-container"><input id="${setting.key}" class="hiddenCheck" type="checkbox" ${isChecked ? 'checked' : ''}><span class="codefeature"><a href="${href}" class="toggle" title="${title}" role="checkbox" aria-checked="${isChecked ? 'true' : 'false'}"></a></span><span class="title"></span></div>`;
153178
}
154179

155180
private getSettingMessage(setting: ISetting, newValue: boolean | string | number): string | undefined {
@@ -185,7 +210,7 @@ export class SimpleSettingRenderer {
185210
const newSettingValue = this.parseValue(uri.authority, uri.path.substring(1));
186211
const currentSettingValue = this._configurationService.inspect(settingId).userValue;
187212

188-
if (newSettingValue && newSettingValue === currentSettingValue && this._updatedSettings.has(settingId)) {
213+
if ((newSettingValue !== undefined) && newSettingValue === currentSettingValue && this._updatedSettings.has(settingId)) {
189214
const restoreMessage = this.restorePreviousSettingMessage(settingId);
190215
actions.push({
191216
class: undefined,
@@ -197,7 +222,7 @@ export class SimpleSettingRenderer {
197222
return this.restoreSetting(settingId);
198223
}
199224
});
200-
} else if (newSettingValue) {
225+
} else if (newSettingValue !== undefined) {
201226
const setting = this.getSetting(settingId);
202227
const trySettingMessage = setting ? this.getSettingMessage(setting, newSettingValue) : undefined;
203228

@@ -230,7 +255,7 @@ export class SimpleSettingRenderer {
230255
return actions;
231256
}
232257

233-
async updateSetting(uri: URI, x: number, y: number) {
258+
private showContextMenu(uri: URI, x: number, y: number) {
234259
const actions = this.getActions(uri);
235260
if (!actions) {
236261
return;
@@ -244,4 +269,27 @@ export class SimpleSettingRenderer {
244269
},
245270
});
246271
}
272+
273+
private async setFeatureState(uri: URI) {
274+
const settingId = uri.authority;
275+
const newSettingValue = this.parseValue(uri.authority, uri.path.substring(1));
276+
let valueToSetSetting: any;
277+
if (this._updatedSettings.has(settingId)) {
278+
valueToSetSetting = this._updatedSettings.get(settingId);
279+
this._updatedSettings.delete(settingId);
280+
} else if (newSettingValue !== this._configurationService.getValue(settingId)) {
281+
valueToSetSetting = newSettingValue;
282+
} else {
283+
valueToSetSetting = undefined;
284+
}
285+
await this._configurationService.updateValue(settingId, valueToSetSetting, ConfigurationTarget.USER);
286+
}
287+
288+
async updateSetting(uri: URI, x: number, y: number) {
289+
if (uri.scheme === Schemas.codeSetting) {
290+
return this.showContextMenu(uri, x, y);
291+
} else if (uri.scheme === Schemas.codeFeature) {
292+
return this.setFeatureState(uri);
293+
}
294+
}
247295
}

src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ suite('Markdown Setting Renderer Test', () => {
7373
const htmlNoValue = '<span codesetting="example.booleanSetting">';
7474
const renderedHtmlNoValue = htmlRenderer(htmlNoValue);
7575
assert.strictEqual(renderedHtmlNoValue,
76-
`<span><a href="code-setting://example.booleanSetting" class="codesetting" title="Try feature" aria-rol="button"><svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M9.1 4.4L8.6 2H7.4l-.5 2.4-.7.3-2-1.3-.9.8 1.3 2-.2.7-2.4.5v1.2l2.4.5.3.8-1.3 2 .8.8 2-1.3.8.3.4 2.3h1.2l.5-2.4.8-.3 2 1.3.8-.8-1.3-2 .3-.8 2.3-.4V7.4l-2.4-.5-.3-.8 1.3-2-.8-.8-2 1.3-.7-.2zM9.4 1l.5 2.4L12 2.1l2 2-1.4 2.1 2.4.4v2.8l-2.4.5L14 12l-2 2-2.1-1.4-.5 2.4H6.6l-.5-2.4L4 13.9l-2-2 1.4-2.1L1 9.4V6.6l2.4-.5L2.1 4l2-2 2.1 1.4.4-2.4h2.8zm.6 7c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zM8 9c.6 0 1-.4 1-1s-.4-1-1-1-1 .4-1 1 .4 1 1 1z"/></svg></a>`);
76+
`<span><a href="code-setting://example.booleanSetting" class="codesetting" title="Try feature" aria-role="button"><svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M9.1 4.4L8.6 2H7.4l-.5 2.4-.7.3-2-1.3-.9.8 1.3 2-.2.7-2.4.5v1.2l2.4.5.3.8-1.3 2 .8.8 2-1.3.8.3.4 2.3h1.2l.5-2.4.8-.3 2 1.3.8-.8-1.3-2 .3-.8 2.3-.4V7.4l-2.4-.5-.3-.8 1.3-2-.8-.8-2 1.3-.7-.2zM9.4 1l.5 2.4L12 2.1l2 2-1.4 2.1 2.4.4v2.8l-2.4.5L14 12l-2 2-2.1-1.4-.5 2.4H6.6l-.5-2.4L4 13.9l-2-2 1.4-2.1L1 9.4V6.6l2.4-.5L2.1 4l2-2 2.1 1.4.4-2.4h2.8zm.6 7c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zM8 9c.6 0 1-.4 1-1s-.4-1-1-1-1 .4-1 1 .4 1 1 1z"/></svg></a>`);
7777
});
7878

7979
test('actions with no value', () => {

src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts

Lines changed: 148 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ export class ReleaseNotesManager {
117117
} else if (e.message.type === 'scroll') {
118118
this.scrollPosition = e.message.value.scrollPosition;
119119
} else if (e.message.type === 'clickSetting') {
120-
this._simpleSettingRenderer.updateSetting(URI.parse(e.message.value.uri), e.message.value.x, e.message.value.y);
120+
const x = this._currentReleaseNotes?.webview.container.offsetLeft + e.message.value.x;
121+
const y = this._currentReleaseNotes?.webview.container.offsetTop + e.message.value.y;
122+
this._simpleSettingRenderer.updateSetting(URI.parse(e.message.value.uri), x, y);
121123
}
122124
}));
123125

@@ -221,7 +223,7 @@ export class ReleaseNotesManager {
221223
}
222224

223225
private async onDidClickLink(uri: URI) {
224-
if (uri.scheme === Schemas.codeSetting) {
226+
if (uri.scheme === Schemas.codeSetting || uri.scheme === Schemas.codeFeature) {
225227
// handled in receive message
226228
} else {
227229
this.addGAParameters(uri, 'ReleaseNotes')
@@ -260,7 +262,7 @@ export class ReleaseNotesManager {
260262
color: var(--vscode-button-foreground);
261263
background-color: var(--vscode-button-background);
262264
width: fit-content;
263-
padding: 1px 1px 1px 1px;
265+
padding: 1px 1px 0px 1px;
264266
font-size: 12px;
265267
overflow: hidden;
266268
text-overflow: ellipsis;
@@ -298,6 +300,123 @@ export class ReleaseNotesManager {
298300
user-select: none;
299301
-webkit-user-select: none;
300302
}
303+
304+
.codefeature-container {
305+
display: flex;
306+
}
307+
308+
.codefeature {
309+
position: relative;
310+
display: inline-block;
311+
width: 46px;
312+
height: 24px;
313+
}
314+
315+
.codefeature-container input {
316+
display: none;
317+
}
318+
319+
.toggle {
320+
position: absolute;
321+
cursor: pointer;
322+
top: 0;
323+
left: 0;
324+
right: 0;
325+
bottom: 0;
326+
background-color: var(--vscode-button-background);
327+
transition: .4s;
328+
border-radius: 24px;
329+
}
330+
331+
.toggle:before {
332+
position: absolute;
333+
content: "";
334+
height: 16px;
335+
width: 16px;
336+
left: 4px;
337+
bottom: 4px;
338+
background-color: var(--vscode-editor-foreground);
339+
transition: .4s;
340+
border-radius: 50%;
341+
}
342+
343+
input:checked+.codefeature > .toggle:before {
344+
transform: translateX(22px);
345+
}
346+
347+
.codefeature-container:has(input) .title {
348+
line-height: 30px;
349+
padding-left: 4px;
350+
font-weight: bold;
351+
}
352+
353+
.codefeature-container:has(input:checked) .title:after {
354+
content: "${nls.localize('disableFeature', "Disable this feature")}";
355+
}
356+
.codefeature-container:has(input:not(:checked)) .title:after {
357+
content: "${nls.localize('enableFeature', "Enable this feature")}";
358+
}
359+
360+
.codefeature-container {
361+
display: flex;
362+
}
363+
364+
.codefeature {
365+
position: relative;
366+
display: inline-block;
367+
width: 58px;
368+
height: 30px;
369+
}
370+
371+
.codefeature-container input {
372+
display: none;
373+
}
374+
375+
.toggle {
376+
position: absolute;
377+
cursor: pointer;
378+
top: 0;
379+
left: 0;
380+
right: 0;
381+
bottom: 0;
382+
background-color: var(--vscode-disabledForeground);
383+
transition: .4s;
384+
border-radius: 30px;
385+
}
386+
387+
.toggle:before {
388+
position: absolute;
389+
content: "";
390+
height: 22px;
391+
width: 22px;
392+
left: 4px;
393+
bottom: 4px;
394+
background-color: var(--vscode-editor-foreground);
395+
transition: .4s;
396+
border-radius: 50%;
397+
}
398+
399+
input:checked+.codefeature > .toggle:before {
400+
transform: translateX(26px);
401+
}
402+
403+
input:checked+.codefeature > .toggle {
404+
background-color: var(--vscode-button-background);
405+
}
406+
407+
.codefeature-container:has(input) .title {
408+
line-height: 30px;
409+
padding-left: 4px;
410+
font-weight: bold;
411+
}
412+
413+
.codefeature-container:has(input:checked) .title:after {
414+
content: "${nls.localize('disableFeature', "Disable this feature")}";
415+
}
416+
.codefeature-container:has(input:not(:checked)) .title:after {
417+
content: "${nls.localize('enableFeature', "Enable this feature")}";
418+
}
419+
301420
header { display: flex; align-items: center; padding-top: 1em; }
302421
</style>
303422
</head>
@@ -332,6 +451,13 @@ export class ReleaseNotesManager {
332451
input.checked = event.data.value;
333452
} else if (event.data.type === 'setScroll') {
334453
window.scrollTo(event.data.value.scrollPosition.x, event.data.value.scrollPosition.y);
454+
} else if (event.data.type === 'setFeaturedSettings') {
455+
for (const [settingId, value] of event.data.value) {
456+
const setting = document.getElementById(settingId);
457+
if (setting instanceof HTMLInputElement) {
458+
setting.checked = value;
459+
}
460+
}
335461
}
336462
});
337463
@@ -349,8 +475,14 @@ export class ReleaseNotesManager {
349475
350476
window.addEventListener('click', event => {
351477
const href = event.target.href ?? event.target.parentElement.href ?? event.target.parentElement.parentElement?.href;
352-
if (href && href.startsWith('${Schemas.codeSetting}')) {
353-
vscode.postMessage({ type: 'clickSetting', value: { uri: href, x: event.screenX, y: event.screenY }});
478+
if (href && (href.startsWith('${Schemas.codeSetting}') || href.startsWith('${Schemas.codeFeature}'))) {
479+
vscode.postMessage({ type: 'clickSetting', value: { uri: href, x: event.clientX, y: event.clientY }});
480+
if (href.startsWith('${Schemas.codeFeature}')) {
481+
const featureInput = event.target.parentElement.previousSibling;
482+
if (featureInput instanceof HTMLInputElement) {
483+
featureInput.checked = !featureInput.checked;
484+
}
485+
}
354486
}
355487
});
356488
@@ -371,6 +503,7 @@ export class ReleaseNotesManager {
371503
private onDidChangeActiveWebviewEditor(input: WebviewInput | undefined): void {
372504
if (input && input === this._currentReleaseNotes) {
373505
this.updateCheckboxWebview();
506+
this.updateFeaturedSettingsWebview();
374507
}
375508
}
376509

@@ -382,4 +515,14 @@ export class ReleaseNotesManager {
382515
});
383516
}
384517
}
518+
519+
private updateFeaturedSettingsWebview() {
520+
if (this._currentReleaseNotes) {
521+
this._currentReleaseNotes.webview.postMessage({
522+
type: 'setFeaturedSettings',
523+
value: this._simpleSettingRenderer.featuredSettingStates
524+
});
525+
}
526+
}
385527
}
528+

0 commit comments

Comments
 (0)