@theia/preferences 1.75.0 → 1.76.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/preference-tree-model.d.ts +38 -1
- package/lib/browser/preference-tree-model.d.ts.map +1 -1
- package/lib/browser/preference-tree-model.js +136 -3
- package/lib/browser/preference-tree-model.js.map +1 -1
- package/lib/browser/preference-tree-model.spec.d.ts +2 -0
- package/lib/browser/preference-tree-model.spec.d.ts.map +1 -0
- package/lib/browser/preference-tree-model.spec.js +320 -0
- package/lib/browser/preference-tree-model.spec.js.map +1 -0
- package/lib/browser/views/preference-editor-widget.d.ts +29 -7
- package/lib/browser/views/preference-editor-widget.d.ts.map +1 -1
- package/lib/browser/views/preference-editor-widget.js +119 -15
- package/lib/browser/views/preference-editor-widget.js.map +1 -1
- package/lib/browser/views/preference-widget.d.ts +1 -2
- package/lib/browser/views/preference-widget.d.ts.map +1 -1
- package/lib/browser/views/preference-widget.js +0 -2
- package/lib/browser/views/preference-widget.js.map +1 -1
- package/package.json +8 -8
- package/src/browser/preference-tree-model.spec.ts +362 -0
- package/src/browser/preference-tree-model.ts +144 -3
- package/src/browser/style/index.css +6 -0
- package/src/browser/views/preference-editor-widget.ts +124 -23
- package/src/browser/views/preference-widget.tsx +1 -4
|
@@ -34,6 +34,9 @@ import { Preference } from './util/preference-types';
|
|
|
34
34
|
import { Event } from '@theia/core/lib/common';
|
|
35
35
|
import { COMMONLY_USED_SECTION_PREFIX } from './util/preference-layout';
|
|
36
36
|
|
|
37
|
+
/** Node id of the 'Commonly Used' group, see `PreferenceTreeGenerator`. */
|
|
38
|
+
const COMMONLY_USED_NODE_ID = `${COMMONLY_USED_SECTION_PREFIX}@${COMMONLY_USED_SECTION_PREFIX}`;
|
|
39
|
+
|
|
37
40
|
export interface PreferenceTreeNodeProps extends NodeProps {
|
|
38
41
|
visibleChildren: number;
|
|
39
42
|
isExpansible?: boolean;
|
|
@@ -46,6 +49,7 @@ export enum PreferenceFilterChangeSource {
|
|
|
46
49
|
Schema,
|
|
47
50
|
Search,
|
|
48
51
|
Scope,
|
|
52
|
+
Category,
|
|
49
53
|
}
|
|
50
54
|
export interface PreferenceFilterChangeEvent {
|
|
51
55
|
source: PreferenceFilterChangeSource
|
|
@@ -71,6 +75,14 @@ export class PreferenceTreeModel extends TreeModelImpl {
|
|
|
71
75
|
protected _currentRows: Map<string, PreferenceTreeNodeRow> = new Map();
|
|
72
76
|
protected _totalVisibleLeaves = 0;
|
|
73
77
|
private _suppressSelection = false;
|
|
78
|
+
private preservingCategoryFilter = false;
|
|
79
|
+
protected _categoryFilterId: string | undefined;
|
|
80
|
+
protected _initialSelectionApplied = false;
|
|
81
|
+
protected lastSelectionId: string | undefined;
|
|
82
|
+
|
|
83
|
+
get categoryFilterId(): string | undefined {
|
|
84
|
+
return this._categoryFilterId;
|
|
85
|
+
}
|
|
74
86
|
|
|
75
87
|
get currentRows(): Readonly<Map<string, PreferenceTreeNodeRow>> {
|
|
76
88
|
return this._currentRows;
|
|
@@ -104,23 +116,44 @@ export class PreferenceTreeModel extends TreeModelImpl {
|
|
|
104
116
|
protected async doInit(): Promise<void> {
|
|
105
117
|
super.init();
|
|
106
118
|
this.toDispose.pushAll([
|
|
119
|
+
this.onSelectionChanged(selectionEvent => {
|
|
120
|
+
this.lastSelectionId = selectionEvent[0]?.id;
|
|
121
|
+
if (this.preservingCategoryFilter) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const node = selectionEvent[0];
|
|
125
|
+
const newId = node ? this.categoryIdForSelection(node) : undefined;
|
|
126
|
+
if (newId !== this._categoryFilterId) {
|
|
127
|
+
this._categoryFilterId = newId;
|
|
128
|
+
this.updateFilteredRows(PreferenceFilterChangeSource.Category);
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
107
131
|
this.treeGenerator.onSchemaChanged(newTree => this.handleNewSchema(newTree)),
|
|
108
132
|
this.scopeTracker.onScopeChanged(scopeDetails => {
|
|
109
133
|
this._currentScope = scopeDetails.scope;
|
|
110
134
|
this.updateFilteredRows(PreferenceFilterChangeSource.Scope);
|
|
135
|
+
this.ensureSelectedCategoryHasVisibleSettings();
|
|
111
136
|
}),
|
|
112
137
|
this.filterInput.onFilterChanged(newSearchTerm => {
|
|
113
138
|
this.lastSearchedTags = Array.from(newSearchTerm.matchAll(/@tag:([^\s]+)/g)).map(match => match[0].slice(5));
|
|
114
139
|
const newSearchTermWithoutTags = newSearchTerm.replace(/@tag:[^\s]+/g, '');
|
|
115
140
|
this.lastSearchedLiteral = newSearchTermWithoutTags;
|
|
116
141
|
this.lastSearchedFuzzy = newSearchTermWithoutTags.replace(/\s/g, '');
|
|
142
|
+
const wasFiltered = this._isFiltered;
|
|
117
143
|
this._isFiltered = newSearchTerm.length > 2;
|
|
118
|
-
if (this.
|
|
144
|
+
if (this._isFiltered) {
|
|
145
|
+
// Search results span all categories: drop the category filter and the
|
|
146
|
+
// now-stale selection so the tree matches the shown results.
|
|
147
|
+
this._categoryFilterId = undefined;
|
|
148
|
+
this.clearSelection();
|
|
119
149
|
this.expandAll();
|
|
120
150
|
} else if (CompositeTreeNode.is(this.root)) {
|
|
121
151
|
const root = this.root;
|
|
122
152
|
// Avoid intermediate selection events while collapsing.
|
|
123
153
|
this.withSuppressedSelection(() => this.collapseAll(root));
|
|
154
|
+
if (wasFiltered) {
|
|
155
|
+
this.selectDefaultCategory();
|
|
156
|
+
}
|
|
124
157
|
}
|
|
125
158
|
this.updateFilteredRows(PreferenceFilterChangeSource.Search);
|
|
126
159
|
}),
|
|
@@ -135,10 +168,91 @@ export class PreferenceTreeModel extends TreeModelImpl {
|
|
|
135
168
|
|
|
136
169
|
private handleNewSchema(newRoot: CompositeTreeNode): void {
|
|
137
170
|
this.root = newRoot;
|
|
171
|
+
if (this._categoryFilterId && !this.getNode(this._categoryFilterId)) {
|
|
172
|
+
this._categoryFilterId = undefined;
|
|
173
|
+
}
|
|
138
174
|
if (this.isFiltered) {
|
|
139
175
|
this.expandAll();
|
|
140
176
|
}
|
|
141
177
|
this.updateFilteredRows(PreferenceFilterChangeSource.Schema);
|
|
178
|
+
this.restoreSelectionInNewTree();
|
|
179
|
+
this.ensureSelectedCategoryHasVisibleSettings();
|
|
180
|
+
this.applyInitialSelection();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Re-applies the selection after the tree has been rebuilt with new node instances,
|
|
185
|
+
* e.g. when plugins contribute preferences after startup. The selection state resolves
|
|
186
|
+
* nodes by id, but the rebuilt node lacks the `selected` flag that drives the tree
|
|
187
|
+
* highlight. If the selected node is gone from the new schema, falls back to the
|
|
188
|
+
* default category.
|
|
189
|
+
*/
|
|
190
|
+
protected restoreSelectionInNewTree(): void {
|
|
191
|
+
if (!this.lastSelectionId) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const node = this.getNode(this.lastSelectionId);
|
|
195
|
+
if (node && SelectableTreeNode.is(node)) {
|
|
196
|
+
if (!node.selected) {
|
|
197
|
+
this.selectPreservingCategoryFilter(node);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
this.clearSelection();
|
|
201
|
+
this._initialSelectionApplied = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
protected applyInitialSelection(): void {
|
|
206
|
+
if (this._initialSelectionApplied) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (this.selectedNodes.length > 0) {
|
|
210
|
+
// Something is already selected, e.g. by the user before the schema change.
|
|
211
|
+
this._initialSelectionApplied = true;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (this._isFiltered) {
|
|
215
|
+
// A search (e.g. restored on startup) is active; the default selection is
|
|
216
|
+
// applied when it is cleared instead.
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (this.selectDefaultCategory()) {
|
|
220
|
+
this._initialSelectionApplied = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Selects the 'Commonly Used' category, the default detail page of the settings editor.
|
|
226
|
+
* @returns `true` if the category was found and selected.
|
|
227
|
+
*/
|
|
228
|
+
protected selectDefaultCategory(): boolean {
|
|
229
|
+
const commonlyUsed = this.getNode(COMMONLY_USED_NODE_ID);
|
|
230
|
+
if (commonlyUsed && SelectableTreeNode.is(commonlyUsed)) {
|
|
231
|
+
this.selectNode(commonlyUsed);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Falls back to the default category when the selected category has no settings left to
|
|
239
|
+
* show, e.g. after switching to a scope in which none of its settings are valid. Otherwise
|
|
240
|
+
* the editor would show an empty page for a category the tree no longer lists. Clears the
|
|
241
|
+
* selection instead if the default category has no settings to show either.
|
|
242
|
+
*/
|
|
243
|
+
protected ensureSelectedCategoryHasVisibleSettings(): void {
|
|
244
|
+
if (!this._categoryFilterId || this.hasVisibleSettings(this._categoryFilterId)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (this.hasVisibleSettings(COMMONLY_USED_NODE_ID)) {
|
|
248
|
+
this.selectDefaultCategory();
|
|
249
|
+
} else {
|
|
250
|
+
this.clearSelection();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
protected hasVisibleSettings(categoryId: string): boolean {
|
|
255
|
+
return !!this._currentRows.get(categoryId)?.visibleChildren;
|
|
142
256
|
}
|
|
143
257
|
|
|
144
258
|
protected updateRows(): void {
|
|
@@ -266,15 +380,42 @@ export class PreferenceTreeModel extends TreeModelImpl {
|
|
|
266
380
|
}
|
|
267
381
|
|
|
268
382
|
/**
|
|
383
|
+
* Returns the id of the nearest composite (category) ancestor of `node`,
|
|
384
|
+
* inclusive of `node` itself. Returns `undefined` if no category ancestor is found.
|
|
385
|
+
*/
|
|
386
|
+
protected categoryIdForSelection(node: TreeNode): string | undefined {
|
|
387
|
+
let current: TreeNode | undefined = node;
|
|
388
|
+
while (current) {
|
|
389
|
+
if (Preference.TreeNode.is(current) && Preference.CompositeTreeNode.is(current)) {
|
|
390
|
+
return current.id;
|
|
391
|
+
}
|
|
392
|
+
current = current.parent;
|
|
393
|
+
}
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Selects `node` to mirror the editor's scroll position in the tree. Unlike an explicit
|
|
399
|
+
* selection, this does not change the category filter: scrolling must never narrow the
|
|
400
|
+
* settings editor, only an explicit selection may.
|
|
269
401
|
* @returns true if selection changed, false otherwise
|
|
270
402
|
*/
|
|
271
403
|
selectIfNotSelected(node: SelectableTreeNode): boolean {
|
|
272
404
|
const currentlySelected = this.selectedNodes[0];
|
|
273
405
|
if (!node.selected || node !== currentlySelected) {
|
|
274
|
-
node
|
|
275
|
-
this.selectNode(node);
|
|
406
|
+
this.selectPreservingCategoryFilter(node);
|
|
276
407
|
return true;
|
|
277
408
|
}
|
|
278
409
|
return false;
|
|
279
410
|
}
|
|
411
|
+
|
|
412
|
+
protected selectPreservingCategoryFilter(node: SelectableTreeNode): void {
|
|
413
|
+
this.preservingCategoryFilter = true;
|
|
414
|
+
try {
|
|
415
|
+
node.selected = true;
|
|
416
|
+
this.selectNode(node);
|
|
417
|
+
} finally {
|
|
418
|
+
this.preservingCategoryFilter = false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
280
421
|
}
|
|
@@ -219,6 +219,12 @@
|
|
|
219
219
|
font-size: var(--theia-ui-font-size2);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
.theia-settings-container .theia-settings-pinned-category-header {
|
|
223
|
+
position: sticky;
|
|
224
|
+
z-index: 5;
|
|
225
|
+
background-color: var(--theia-editor-background);
|
|
226
|
+
}
|
|
227
|
+
|
|
222
228
|
.theia-settings-container .settings-section>li {
|
|
223
229
|
list-style-type: none;
|
|
224
230
|
margin: var(--theia-ui-padding) 0px;
|
|
@@ -21,7 +21,6 @@ import * as deepEqual from 'fast-deep-equal';
|
|
|
21
21
|
import {
|
|
22
22
|
CompositeTreeNode,
|
|
23
23
|
SelectableTreeNode,
|
|
24
|
-
StatefulWidget,
|
|
25
24
|
TopDownTreeIterator,
|
|
26
25
|
ExpandableTreeNode,
|
|
27
26
|
} from '@theia/core/lib/browser';
|
|
@@ -37,12 +36,8 @@ import { PreferencesScopeTabBar } from './preference-scope-tabbar-widget';
|
|
|
37
36
|
import { PreferenceNodeRendererCreatorRegistry } from './components/preference-node-renderer-creator';
|
|
38
37
|
import { COMMONLY_USED_SECTION_PREFIX } from '../util/preference-layout';
|
|
39
38
|
|
|
40
|
-
export interface PreferencesEditorState {
|
|
41
|
-
firstVisibleChildID: string,
|
|
42
|
-
}
|
|
43
|
-
|
|
44
39
|
@injectable()
|
|
45
|
-
export class PreferencesEditorWidget extends BaseWidget
|
|
40
|
+
export class PreferencesEditorWidget extends BaseWidget {
|
|
46
41
|
static readonly ID = 'settings.editor';
|
|
47
42
|
static readonly LABEL = nls.localizeByDefault('Settings Editor');
|
|
48
43
|
|
|
@@ -138,6 +133,8 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
|
|
|
138
133
|
|
|
139
134
|
if (e.source === PreferenceFilterChangeSource.Search) {
|
|
140
135
|
this.handleSearchChange(isFiltered, leavesAreVisible);
|
|
136
|
+
} else if (e.source === PreferenceFilterChangeSource.Category) {
|
|
137
|
+
this.handleCategoryChange(isFiltered, leavesAreVisible);
|
|
141
138
|
} else if (e.source === PreferenceFilterChangeSource.Scope) {
|
|
142
139
|
this.handleScopeChange(isFiltered);
|
|
143
140
|
} else if (e.source === PreferenceFilterChangeSource.Schema) {
|
|
@@ -146,10 +143,14 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
|
|
|
146
143
|
unreachable(e.source, 'Not all PreferenceFilterChangeSource enum variants handled.');
|
|
147
144
|
}
|
|
148
145
|
|
|
149
|
-
|
|
146
|
+
// Only sync the tree to a row that is still shown. A row hidden by the change, e.g. because
|
|
147
|
+
// it is not valid in the new scope or lies outside the selected category, must not pull the
|
|
148
|
+
// tree selection away from the page that is shown.
|
|
149
|
+
if (scrollTarget && this.isRendererVisible(scrollTarget)) {
|
|
150
150
|
this.showInTree(scrollTarget);
|
|
151
151
|
}
|
|
152
152
|
this.resetScroll(scrollTarget);
|
|
153
|
+
this.pinCategoryHeaders();
|
|
153
154
|
|
|
154
155
|
if (e.source === PreferenceFilterChangeSource.Search) {
|
|
155
156
|
// Reset focus if search context changes.
|
|
@@ -158,6 +159,11 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
protected getScrollTarget(source: PreferenceFilterChangeSource): string | undefined {
|
|
162
|
+
if (source === PreferenceFilterChangeSource.Category) {
|
|
163
|
+
// A newly selected category always opens at its top. Scrolling back to the last focused
|
|
164
|
+
// control would re-select its (previous) category in the tree and desync tree and editor.
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
161
167
|
if (source !== PreferenceFilterChangeSource.Search) {
|
|
162
168
|
return this.firstVisibleChildID;
|
|
163
169
|
}
|
|
@@ -167,6 +173,11 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
|
|
|
167
173
|
return undefined;
|
|
168
174
|
}
|
|
169
175
|
|
|
176
|
+
protected isRendererVisible(nodeId: string): boolean {
|
|
177
|
+
const { id, collection } = this.analyzeIDAndGetRendererGroup(nodeId);
|
|
178
|
+
return !!collection.get(id)?.visible;
|
|
179
|
+
}
|
|
180
|
+
|
|
170
181
|
protected isRendererInViewport(nodeId: string): boolean {
|
|
171
182
|
const { id, collection } = this.analyzeIDAndGetRendererGroup(nodeId);
|
|
172
183
|
const renderer = collection.get(id);
|
|
@@ -243,6 +254,14 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
|
|
|
243
254
|
}
|
|
244
255
|
}
|
|
245
256
|
|
|
257
|
+
protected handleCategoryChange(isFiltered: boolean, leavesAreVisible: boolean): void {
|
|
258
|
+
if (leavesAreVisible) {
|
|
259
|
+
for (const [, renderer] of this.allRenderers()) {
|
|
260
|
+
this.hideIfFailsFilters(renderer, isFiltered);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
246
265
|
protected areLeavesVisible(): boolean {
|
|
247
266
|
const leavesAreVisible = this.model.totalVisibleLeaves > 0;
|
|
248
267
|
this.node.classList.toggle('no-results', !leavesAreVisible);
|
|
@@ -266,12 +285,30 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
|
|
|
266
285
|
}
|
|
267
286
|
}
|
|
268
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Returns true if a category filter is active and `node` is outside the visible slice:
|
|
290
|
+
* not the selected category itself, not a descendant of it, and not one of its
|
|
291
|
+
* composite ancestors (whose headers we keep visible above the selected category).
|
|
292
|
+
* Keeps the left tree intact (filtering is applied here, not in the model).
|
|
293
|
+
*/
|
|
294
|
+
protected isOutsideSelectedCategory(node: Preference.TreeNode): boolean {
|
|
295
|
+
const categoryId = this.model.categoryFilterId;
|
|
296
|
+
if (!categoryId || node.id === categoryId) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
const category = this.model.getNode(categoryId);
|
|
300
|
+
if (!CompositeTreeNode.is(category) || CompositeTreeNode.isAncestor(category, node)) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
return !(CompositeTreeNode.is(node) && CompositeTreeNode.isAncestor(node, category));
|
|
304
|
+
}
|
|
305
|
+
|
|
269
306
|
/**
|
|
270
307
|
* @returns true if the renderer is hidden, false otherwise.
|
|
271
308
|
*/
|
|
272
309
|
protected hideIfFailsFilters(renderer: GeneralPreferenceNodeRenderer, isFiltered: boolean): boolean {
|
|
273
310
|
const row = this.model.currentRows.get(renderer.nodeId);
|
|
274
|
-
if (!row || (CompositeTreeNode.is(row.node) && (isFiltered || row.visibleChildren === 0))) {
|
|
311
|
+
if (!row || (CompositeTreeNode.is(row.node) && (isFiltered || row.visibleChildren === 0)) || this.isOutsideSelectedCategory(row.node)) {
|
|
275
312
|
renderer.hide();
|
|
276
313
|
return true;
|
|
277
314
|
} else {
|
|
@@ -280,6 +317,69 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
|
|
|
280
317
|
}
|
|
281
318
|
}
|
|
282
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Pins category header rows to the top of the scroll container via `position: sticky`.
|
|
322
|
+
* The headers of the selected category and its ancestors are stacked on top of each
|
|
323
|
+
* other; the headers of its subsections all share the single slot below that stack, so
|
|
324
|
+
* the subsection currently scrolled into view replaces the previous one instead of the
|
|
325
|
+
* headers piling up. Clears the styles when no category filter is active.
|
|
326
|
+
*/
|
|
327
|
+
protected pinCategoryHeaders(): void {
|
|
328
|
+
requestAnimationFrame(() => {
|
|
329
|
+
const categoryId = this.model.categoryFilterId;
|
|
330
|
+
const stackedHeaders = this.getStackedHeaderRenderers();
|
|
331
|
+
// Read all heights before writing any styles, so that the layout is computed only once.
|
|
332
|
+
const heights = stackedHeaders.map(renderer => renderer.node.offsetHeight);
|
|
333
|
+
const stackedTops = new Map<string, number>();
|
|
334
|
+
let stackHeight = 0;
|
|
335
|
+
stackedHeaders.forEach((renderer, index) => {
|
|
336
|
+
stackedTops.set(renderer.nodeId, stackHeight);
|
|
337
|
+
stackHeight += heights[index];
|
|
338
|
+
});
|
|
339
|
+
for (const [, renderer] of this.allRenderers()) {
|
|
340
|
+
if (!CompositeTreeNode.is(this.model.getNode(renderer.nodeId))) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const wrapper = renderer.node;
|
|
344
|
+
if (categoryId && renderer.visible) {
|
|
345
|
+
wrapper.classList.add('theia-settings-pinned-category-header');
|
|
346
|
+
wrapper.style.top = `${stackedTops.get(renderer.nodeId) ?? stackHeight}px`;
|
|
347
|
+
} else {
|
|
348
|
+
wrapper.classList.remove('theia-settings-pinned-category-header');
|
|
349
|
+
wrapper.style.top = '';
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @returns the visible header renderers of the selected category and its ancestors,
|
|
357
|
+
* outermost first, i.e. in document order. Empty if no category filter is active.
|
|
358
|
+
*/
|
|
359
|
+
protected getStackedHeaderRenderers(): GeneralPreferenceNodeRenderer[] {
|
|
360
|
+
const renderers: GeneralPreferenceNodeRenderer[] = [];
|
|
361
|
+
const categoryId = this.model.categoryFilterId;
|
|
362
|
+
if (!categoryId) {
|
|
363
|
+
return renderers;
|
|
364
|
+
}
|
|
365
|
+
for (let node = this.model.getNode(categoryId); node && Preference.TreeNode.is(node); node = node.parent) {
|
|
366
|
+
const { id, collection } = this.analyzeIDAndGetRendererGroup(node.id);
|
|
367
|
+
const renderer = collection.get(id);
|
|
368
|
+
if (renderer?.visible) {
|
|
369
|
+
renderers.unshift(renderer);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return renderers;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @returns the total height of the pinned header stack, i.e. the space at the top of the
|
|
377
|
+
* scroll container that is covered by the headers of the selected category and its ancestors.
|
|
378
|
+
*/
|
|
379
|
+
protected getStackedHeaderHeight(stackedHeaders: GeneralPreferenceNodeRenderer[] = this.getStackedHeaderRenderers()): number {
|
|
380
|
+
return stackedHeaders.reduce((height, renderer) => height + renderer.node.offsetHeight, 0);
|
|
381
|
+
}
|
|
382
|
+
|
|
283
383
|
protected resetScroll(nodeIDToScrollTo?: string): void {
|
|
284
384
|
if (this.scrollBar) { // Absent on widget creation
|
|
285
385
|
this.doResetScroll(nodeIDToScrollTo);
|
|
@@ -300,7 +400,8 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
|
|
|
300
400
|
const { id, collection } = this.analyzeIDAndGetRendererGroup(nodeIDToScrollTo);
|
|
301
401
|
const renderer = collection.get(id);
|
|
302
402
|
if (renderer?.visible) {
|
|
303
|
-
|
|
403
|
+
// Place the target right below the pinned category headers, not behind them.
|
|
404
|
+
this.scrollContainer.scrollTo(0, renderer.node.offsetTop - this.getStackedHeaderHeight());
|
|
304
405
|
return;
|
|
305
406
|
}
|
|
306
407
|
}
|
|
@@ -324,10 +425,21 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
|
|
|
324
425
|
onScroll = throttle(this.doOnScroll.bind(this), 50);
|
|
325
426
|
|
|
326
427
|
protected findFirstVisibleChildID(): string | undefined {
|
|
327
|
-
|
|
428
|
+
// The pinned headers of the selected category and its ancestors always sit at the top of
|
|
429
|
+
// the scroll container. Skip them and look for the first row right below the stack,
|
|
430
|
+
// otherwise the outermost ancestor would always be reported and the tree selection would
|
|
431
|
+
// jump from the selected sub-category to its parent.
|
|
432
|
+
const stackedHeaders = this.getStackedHeaderRenderers();
|
|
433
|
+
const stackBottom = this.scrollContainer.scrollTop + this.getStackedHeaderHeight(stackedHeaders);
|
|
328
434
|
for (const [, renderer] of this.allRenderers()) {
|
|
329
|
-
const { offsetTop, offsetHeight } = renderer.node;
|
|
330
|
-
|
|
435
|
+
const { offsetTop, offsetHeight, offsetParent } = renderer.node;
|
|
436
|
+
// Skip hidden renderers (display:none): they report offsetTop/offsetHeight 0
|
|
437
|
+
// and would otherwise match scrollTop===0, hijacking the selection back to
|
|
438
|
+
// the first hidden category.
|
|
439
|
+
if (!offsetParent || stackedHeaders.includes(renderer)) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (Math.abs(offsetTop - stackBottom) <= offsetHeight / 2) {
|
|
331
443
|
return renderer.nodeId;
|
|
332
444
|
}
|
|
333
445
|
}
|
|
@@ -408,15 +520,4 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
|
|
|
408
520
|
protected override getScrollContainer(): HTMLElement {
|
|
409
521
|
return this.scrollContainer;
|
|
410
522
|
}
|
|
411
|
-
|
|
412
|
-
storeState(): PreferencesEditorState {
|
|
413
|
-
return {
|
|
414
|
-
firstVisibleChildID: this.firstVisibleChildID,
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
restoreState(oldState: PreferencesEditorState): void {
|
|
419
|
-
this.firstVisibleChildID = oldState.firstVisibleChildID;
|
|
420
|
-
this.resetScroll(this.firstVisibleChildID);
|
|
421
|
-
}
|
|
422
523
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { postConstruct, injectable, inject } from '@theia/core/shared/inversify';
|
|
18
18
|
import { Panel, Widget, Message, StatefulWidget, codicon } from '@theia/core/lib/browser';
|
|
19
|
-
import {
|
|
19
|
+
import { PreferencesEditorWidget } from './preference-editor-widget';
|
|
20
20
|
import { PreferencesTreeWidget } from './preference-tree-widget';
|
|
21
21
|
import { PreferencesSearchbarState, PreferencesSearchbarWidget } from './preference-searchbar-widget';
|
|
22
22
|
import { PreferencesScopeTabBar, PreferencesScopeTabBarState } from './preference-scope-tabbar-widget';
|
|
@@ -27,7 +27,6 @@ import { PreferenceScope } from '@theia/core';
|
|
|
27
27
|
|
|
28
28
|
interface PreferencesWidgetState {
|
|
29
29
|
scopeTabBarState: PreferencesScopeTabBarState,
|
|
30
|
-
editorState: PreferencesEditorState,
|
|
31
30
|
searchbarWidgetState: PreferencesSearchbarState,
|
|
32
31
|
}
|
|
33
32
|
|
|
@@ -106,14 +105,12 @@ export class PreferencesWidget extends Panel implements StatefulWidget {
|
|
|
106
105
|
storeState(): PreferencesWidgetState {
|
|
107
106
|
return {
|
|
108
107
|
scopeTabBarState: this.tabBarWidget.storeState(),
|
|
109
|
-
editorState: this.editorWidget.storeState(),
|
|
110
108
|
searchbarWidgetState: this.searchbarWidget.storeState(),
|
|
111
109
|
};
|
|
112
110
|
}
|
|
113
111
|
|
|
114
112
|
restoreState(state: PreferencesWidgetState): void {
|
|
115
113
|
this.tabBarWidget.restoreState(state.scopeTabBarState);
|
|
116
|
-
this.editorWidget.restoreState(state.editorState);
|
|
117
114
|
this.searchbarWidget.restoreState(state.searchbarWidgetState);
|
|
118
115
|
}
|
|
119
116
|
}
|