@gtkx/components 2.0.0-beta.12 → 2.0.0-beta.13
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/README.md +1 -1
- package/dist/column-view.d.ts.map +1 -1
- package/dist/column-view.js +7 -3
- package/dist/column-view.js.map +1 -1
- package/dist/combo-row.d.ts.map +1 -1
- package/dist/combo-row.js +3 -1
- package/dist/combo-row.js.map +1 -1
- package/dist/grid-view.d.ts.map +1 -1
- package/dist/grid-view.js +3 -2
- package/dist/grid-view.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +5 -2
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +86 -43
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/collection-index.d.ts +3 -5
- package/dist/internal/collection-index.d.ts.map +1 -1
- package/dist/internal/collection-index.js +3 -7
- package/dist/internal/collection-index.js.map +1 -1
- package/dist/internal/collection-model.d.ts +15 -4
- package/dist/internal/collection-model.d.ts.map +1 -1
- package/dist/internal/collection-model.js +154 -73
- package/dist/internal/collection-model.js.map +1 -1
- package/dist/internal/collection.d.ts +3 -3
- package/dist/internal/collection.d.ts.map +1 -1
- package/dist/internal/collection.js +11 -9
- package/dist/internal/collection.js.map +1 -1
- package/dist/internal/controlled-sync.d.ts +2 -1
- package/dist/internal/controlled-sync.d.ts.map +1 -1
- package/dist/internal/controlled-sync.js +6 -1
- package/dist/internal/controlled-sync.js.map +1 -1
- package/dist/internal/drop-down.d.ts +10 -4
- package/dist/internal/drop-down.d.ts.map +1 -1
- package/dist/internal/drop-down.js +60 -8
- package/dist/internal/drop-down.js.map +1 -1
- package/dist/internal/expansion.d.ts.map +1 -1
- package/dist/internal/expansion.js +46 -19
- package/dist/internal/expansion.js.map +1 -1
- package/dist/internal/first-header.d.ts +11 -0
- package/dist/internal/first-header.d.ts.map +1 -0
- package/dist/internal/first-header.js +21 -0
- package/dist/internal/first-header.js.map +1 -0
- package/dist/internal/level-splice.d.ts +27 -0
- package/dist/internal/level-splice.d.ts.map +1 -0
- package/dist/internal/level-splice.js +98 -0
- package/dist/internal/level-splice.js.map +1 -0
- package/dist/internal/selection.d.ts.map +1 -1
- package/dist/internal/selection.js +23 -16
- package/dist/internal/selection.js.map +1 -1
- package/dist/internal/tree-expansion.d.ts +3 -1
- package/dist/internal/tree-expansion.d.ts.map +1 -1
- package/dist/internal/tree-expansion.js +31 -2
- package/dist/internal/tree-expansion.js.map +1 -1
- package/dist/internal/tree-order.d.ts +1 -0
- package/dist/internal/tree-order.d.ts.map +1 -1
- package/dist/internal/tree-order.js +2 -1
- package/dist/internal/tree-order.js.map +1 -1
- package/dist/internal/use-collection.d.ts +1 -0
- package/dist/internal/use-collection.d.ts.map +1 -1
- package/dist/internal/use-collection.js +6 -2
- package/dist/internal/use-collection.js.map +1 -1
- package/dist/list-item-factory.d.ts +1 -1
- package/dist/list-item-factory.d.ts.map +1 -1
- package/dist/list-item-factory.js +2 -2
- package/dist/list-item-factory.js.map +1 -1
- package/dist/list-view.d.ts.map +1 -1
- package/dist/list-view.js +8 -3
- package/dist/list-view.js.map +1 -1
- package/dist/types.d.ts +37 -47
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/column-view.tsx +8 -4
- package/src/combo-row.tsx +4 -1
- package/src/grid-view.tsx +3 -2
- package/src/index.ts +0 -4
- package/src/internal/cells.tsx +141 -53
- package/src/internal/collection-index.ts +7 -14
- package/src/internal/collection-model.ts +211 -76
- package/src/internal/collection.ts +14 -13
- package/src/internal/controlled-sync.ts +8 -1
- package/src/internal/drop-down.tsx +111 -14
- package/src/internal/expansion.ts +52 -20
- package/src/internal/first-header.ts +23 -0
- package/src/internal/level-splice.ts +157 -0
- package/src/internal/selection.tsx +24 -19
- package/src/internal/tree-expansion.ts +52 -2
- package/src/internal/tree-order.ts +3 -1
- package/src/internal/use-collection.tsx +8 -2
- package/src/list-item-factory.tsx +2 -1
- package/src/list-view.tsx +10 -2
- package/src/types.ts +39 -51
- package/dist/tab-collection.d.ts +0 -9
- package/dist/tab-collection.d.ts.map +0 -1
- package/dist/tab-collection.js +0 -114
- package/dist/tab-collection.js.map +0 -1
- package/src/tab-collection.tsx +0 -124
|
@@ -7,16 +7,32 @@ import { registerClass } from "@gtkx/runtime";
|
|
|
7
7
|
import type { CollectionIndex, Level } from "./collection-index.js";
|
|
8
8
|
import type { TreeExpansion } from "./tree-expansion.js";
|
|
9
9
|
import { createCollectionIndex } from "./collection-index.js";
|
|
10
|
+
import type { LevelKeys, Relocate, Splice } from "./level-splice.js";
|
|
10
11
|
import { encodePart } from "./keys.js";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
findSplice,
|
|
14
|
+
hasSplice,
|
|
15
|
+
matchMoves,
|
|
16
|
+
NO_SPLICE,
|
|
17
|
+
relocateEntries,
|
|
18
|
+
relocateSlot,
|
|
19
|
+
searchableSplice,
|
|
20
|
+
} from "./level-splice.js";
|
|
21
|
+
import { adoptIndex, createTreeExpansion, pruneSlots, relocateSlots } from "./tree-expansion.js";
|
|
12
22
|
|
|
13
23
|
type LevelStore = Gio.ListModel & LazyLevelStore;
|
|
14
24
|
|
|
15
25
|
type SlotRef = {
|
|
26
|
+
key: string;
|
|
16
27
|
store: LazyLevelStore;
|
|
17
28
|
slot: number;
|
|
18
29
|
};
|
|
19
30
|
|
|
31
|
+
type SlotItem = {
|
|
32
|
+
ref: SlotRef;
|
|
33
|
+
object: Gtk.StringObject;
|
|
34
|
+
};
|
|
35
|
+
|
|
20
36
|
type SyncContext = {
|
|
21
37
|
index: CollectionIndex;
|
|
22
38
|
expansion: TreeExpansion;
|
|
@@ -27,7 +43,13 @@ type SlotRun = {
|
|
|
27
43
|
length: number;
|
|
28
44
|
};
|
|
29
45
|
|
|
46
|
+
type Serial = {
|
|
47
|
+
value: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
30
50
|
type ModelState = {
|
|
51
|
+
version: number;
|
|
52
|
+
listeners: Set<() => void>;
|
|
31
53
|
root: CollectionRootStore | null;
|
|
32
54
|
model: Gtk.FlattenListModel | null;
|
|
33
55
|
groupStores: Map<number, LevelStore>;
|
|
@@ -38,10 +60,13 @@ type ModelState = {
|
|
|
38
60
|
|
|
39
61
|
type CollectionModel = {
|
|
40
62
|
expansion: TreeExpansion;
|
|
63
|
+
subscribe: (onChange: () => void) => () => void;
|
|
64
|
+
getVersion: () => number;
|
|
65
|
+
isPending: (index: CollectionIndex) => boolean;
|
|
41
66
|
rowAt: (position: number) => Gtk.TreeListRow | null;
|
|
42
67
|
bind: (root: CollectionRootStore, model: Gtk.FlattenListModel) => void;
|
|
43
68
|
clear: () => void;
|
|
44
|
-
sync: (index: CollectionIndex) => void;
|
|
69
|
+
sync: (index: CollectionIndex, onSectionsFilled: () => void) => void;
|
|
45
70
|
};
|
|
46
71
|
|
|
47
72
|
type CollectionRootProps = { ref: Ref<CollectionRootStore> };
|
|
@@ -50,6 +75,13 @@ const STORE_CLASS_KEY = Symbol.for("gtkx.components.lazy-level-store");
|
|
|
50
75
|
const ROOT_CLASS_KEY = Symbol.for("gtkx.components.collection-root-store");
|
|
51
76
|
const SLOTS_KEY = Symbol.for("gtkx.components.lazy-level-store.slots");
|
|
52
77
|
const EMPTY_INDEX = createCollectionIndex(undefined, [], true);
|
|
78
|
+
const serial: Serial = { value: 0 };
|
|
79
|
+
|
|
80
|
+
function nextKey(): string {
|
|
81
|
+
serial.value += 1;
|
|
82
|
+
|
|
83
|
+
return String(serial.value);
|
|
84
|
+
}
|
|
53
85
|
|
|
54
86
|
function sharedSlots(): WeakMap<GObject.Object, SlotRef> {
|
|
55
87
|
const cached: unknown = Reflect.get(globalThis, SLOTS_KEY);
|
|
@@ -90,16 +122,20 @@ function registeredRootClass(): unknown {
|
|
|
90
122
|
return CollectionRootStore;
|
|
91
123
|
}
|
|
92
124
|
|
|
93
|
-
function
|
|
125
|
+
function anySlotRefFor(value: GObject.Object | null): SlotRef | null {
|
|
94
126
|
const item = value instanceof Gtk.TreeListRow ? value.getItem() : value;
|
|
95
127
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
128
|
+
return item === null ? null : (sharedSlots().get(item) ?? null);
|
|
129
|
+
}
|
|
99
130
|
|
|
100
|
-
|
|
131
|
+
function slotRefFor(value: GObject.Object | null): SlotRef | null {
|
|
132
|
+
const ref = anySlotRefFor(value);
|
|
101
133
|
|
|
102
|
-
return ref ===
|
|
134
|
+
return ref === null || ref.slot === -1 ? null : ref;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function levelStoreFor(value: GObject.Object | null): LazyLevelStore | null {
|
|
138
|
+
return anySlotRefFor(value)?.store ?? null;
|
|
103
139
|
}
|
|
104
140
|
|
|
105
141
|
function slotPathAt(store: LazyLevelStore, slot: number): string {
|
|
@@ -113,22 +149,68 @@ function newLevelStore(level: Level): LevelStore {
|
|
|
113
149
|
return store;
|
|
114
150
|
}
|
|
115
151
|
|
|
116
|
-
function
|
|
152
|
+
function itemKeys(previous: Level, next: Level): LevelKeys {
|
|
153
|
+
return {
|
|
154
|
+
previousLength: previous.items.length,
|
|
155
|
+
nextLength: next.items.length,
|
|
156
|
+
previousKey: (slot) => previous.items[slot]?.id ?? "",
|
|
157
|
+
nextKey: (slot) => next.items[slot]?.id ?? "",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function groupKey(index: CollectionIndex, group: number): string {
|
|
162
|
+
const level = index.groups[group];
|
|
163
|
+
|
|
164
|
+
return level?.section?.id ?? "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function groupKeys(previous: CollectionIndex, next: CollectionIndex): LevelKeys {
|
|
168
|
+
return {
|
|
169
|
+
previousLength: previous.groups.length,
|
|
170
|
+
nextLength: next.groups.length,
|
|
171
|
+
previousKey: (group) => groupKey(previous, group),
|
|
172
|
+
nextKey: (group) => groupKey(next, group),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isExpandableAt(level: Level, slot: number): boolean {
|
|
177
|
+
return level.expandableFlags[slot] ?? false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function collectFlips(previous: Level, next: Level, splice: Splice): Set<number> {
|
|
117
181
|
const flipped: Set<number> = new Set();
|
|
118
182
|
|
|
119
183
|
if (previous.expandableFlags.length === 0 && next.expandableFlags.length === 0) {
|
|
120
184
|
return flipped;
|
|
121
185
|
}
|
|
122
186
|
|
|
123
|
-
for (let slot = 0; slot <
|
|
124
|
-
if ((previous
|
|
187
|
+
for (let slot = 0; slot < splice.start; slot++) {
|
|
188
|
+
if (isExpandableAt(previous, slot) !== isExpandableAt(next, slot)) {
|
|
125
189
|
flipped.add(slot);
|
|
126
190
|
}
|
|
127
191
|
}
|
|
128
192
|
|
|
193
|
+
const shift = splice.added - splice.removed;
|
|
194
|
+
|
|
195
|
+
for (let slot = splice.start + splice.removed; slot < previous.items.length; slot++) {
|
|
196
|
+
if (isExpandableAt(previous, slot) !== isExpandableAt(next, slot + shift)) {
|
|
197
|
+
flipped.add(slot + shift);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
129
201
|
return flipped;
|
|
130
202
|
}
|
|
131
203
|
|
|
204
|
+
function detachStore(store: LevelStore): void {
|
|
205
|
+
for (const { ref } of store.slots.values()) {
|
|
206
|
+
ref.slot = -1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const child of store.childStores.values()) {
|
|
210
|
+
detachStore(child);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
132
214
|
function pruneFlips(context: SyncContext, store: LevelStore, flipped: Set<number>): void {
|
|
133
215
|
if (flipped.size === 0) {
|
|
134
216
|
return;
|
|
@@ -137,42 +219,45 @@ function pruneFlips(context: SyncContext, store: LevelStore, flipped: Set<number
|
|
|
137
219
|
pruneSlots(context.expansion, store.level.path, (slot) => flipped.has(slot));
|
|
138
220
|
|
|
139
221
|
for (const slot of flipped) {
|
|
140
|
-
store.childStores.
|
|
141
|
-
}
|
|
142
|
-
}
|
|
222
|
+
const child = store.childStores.get(slot);
|
|
143
223
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
continue;
|
|
224
|
+
if (child !== undefined) {
|
|
225
|
+
detachStore(child);
|
|
226
|
+
store.childStores.delete(slot);
|
|
148
227
|
}
|
|
149
|
-
|
|
150
|
-
ref.slot = -1;
|
|
151
|
-
store.refs.delete(slot);
|
|
152
|
-
store.objects.delete(slot);
|
|
153
|
-
store.childStores.delete(slot);
|
|
154
228
|
}
|
|
155
229
|
}
|
|
156
230
|
|
|
157
|
-
function
|
|
158
|
-
if (
|
|
231
|
+
function renewKeys(store: LevelStore, previous: Level, from: number, to: number): void {
|
|
232
|
+
if (from === to) {
|
|
159
233
|
return;
|
|
160
234
|
}
|
|
161
235
|
|
|
162
|
-
|
|
163
|
-
|
|
236
|
+
for (const [slot, { ref }] of store.slots) {
|
|
237
|
+
if (slot >= from && slot < to && previous.items[slot]?.id !== store.level.items[slot]?.id) {
|
|
238
|
+
ref.key = nextKey();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
164
241
|
}
|
|
165
242
|
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
243
|
+
function relocateStore(context: SyncContext, store: LevelStore, keys: LevelKeys, splice: Splice): void {
|
|
244
|
+
const moves = matchMoves(keys, splice, store.slots.keys());
|
|
245
|
+
const shift: Relocate = (slot) => relocateSlot(splice, slot);
|
|
169
246
|
|
|
170
|
-
|
|
171
|
-
|
|
247
|
+
store.slots = relocateEntries(
|
|
248
|
+
store.slots,
|
|
249
|
+
(slot) => relocateSlot(splice, slot, moves),
|
|
250
|
+
({ ref }) => {
|
|
251
|
+
ref.slot = -1;
|
|
252
|
+
},
|
|
253
|
+
);
|
|
172
254
|
|
|
173
|
-
|
|
174
|
-
|
|
255
|
+
for (const [slot, { ref }] of store.slots) {
|
|
256
|
+
ref.slot = slot;
|
|
175
257
|
}
|
|
258
|
+
|
|
259
|
+
store.childStores = relocateEntries(store.childStores, shift, detachStore);
|
|
260
|
+
relocateSlots(context.expansion, store.level.path, shift);
|
|
176
261
|
}
|
|
177
262
|
|
|
178
263
|
function extendRuns(runs: SlotRun[], slot: number): void {
|
|
@@ -205,20 +290,26 @@ function emitFlips(store: LevelStore, flipped: Set<number>): void {
|
|
|
205
290
|
|
|
206
291
|
function syncLevel(context: SyncContext, store: LevelStore, level: Level): void {
|
|
207
292
|
const previous = store.level;
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
const
|
|
293
|
+
const keys = itemKeys(previous, level);
|
|
294
|
+
const found = previous.items === level.items ? NO_SPLICE : findSplice(keys);
|
|
295
|
+
const splice = searchableSplice(found, found.added);
|
|
211
296
|
store.level = level;
|
|
297
|
+
renewKeys(store, previous, found.start, splice.start);
|
|
298
|
+
|
|
299
|
+
if (hasSplice(splice)) {
|
|
300
|
+
relocateStore(context, store, keys, splice);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const flipped = collectFlips(previous, level, splice);
|
|
212
304
|
pruneFlips(context, store, flipped);
|
|
213
|
-
|
|
214
|
-
|
|
305
|
+
|
|
306
|
+
if (hasSplice(splice)) {
|
|
307
|
+
store.itemsChanged(splice.start, splice.removed, splice.added);
|
|
308
|
+
}
|
|
309
|
+
|
|
215
310
|
emitFlips(store, flipped);
|
|
216
311
|
|
|
217
312
|
for (const [slot, child] of store.childStores) {
|
|
218
|
-
if (slot >= overlap || flipped.has(slot)) {
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
313
|
const childLevel = context.index.childLevel(level, slot);
|
|
223
314
|
|
|
224
315
|
if (childLevel !== undefined) {
|
|
@@ -230,7 +321,7 @@ function syncLevel(context: SyncContext, store: LevelStore, level: Level): void
|
|
|
230
321
|
function ensureChildStore(index: CollectionIndex, store: LazyLevelStore, slot: number): LevelStore | null {
|
|
231
322
|
const existing = store.childStores.get(slot) ?? null;
|
|
232
323
|
|
|
233
|
-
if (existing !== null || !(store.level
|
|
324
|
+
if (existing !== null || !isExpandableAt(store.level, slot)) {
|
|
234
325
|
return existing;
|
|
235
326
|
}
|
|
236
327
|
|
|
@@ -275,37 +366,51 @@ function rowAt(state: ModelState, position: number): Gtk.TreeListRow | null {
|
|
|
275
366
|
return item instanceof Gtk.TreeListRow ? item : null;
|
|
276
367
|
}
|
|
277
368
|
|
|
278
|
-
function
|
|
279
|
-
|
|
369
|
+
function dropGroup(state: ModelState, store: LevelStore): void {
|
|
370
|
+
detachStore(store);
|
|
371
|
+
state.trees.delete(store);
|
|
372
|
+
}
|
|
280
373
|
|
|
281
|
-
|
|
282
|
-
|
|
374
|
+
function replaceGroups(state: ModelState, before: number): void {
|
|
375
|
+
for (const store of state.groupStores.values()) {
|
|
376
|
+
dropGroup(state, store);
|
|
377
|
+
}
|
|
283
378
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
379
|
+
state.groupStores.clear();
|
|
380
|
+
state.expansion.expanded.clear();
|
|
381
|
+
state.expansion.slots.clear();
|
|
382
|
+
state.root?.itemsChanged(0, before, state.index.groups.length);
|
|
383
|
+
}
|
|
288
384
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
385
|
+
function addedRows(index: CollectionIndex, splice: Splice): number {
|
|
386
|
+
return index.groups
|
|
387
|
+
.slice(splice.start, splice.start + splice.added)
|
|
388
|
+
.reduce((rows, level) => rows + level.items.length, 0);
|
|
293
389
|
}
|
|
294
390
|
|
|
295
391
|
function syncRoot(state: ModelState, previous: CollectionIndex): void {
|
|
296
|
-
const before = previous.groups.length;
|
|
297
|
-
const after = state.index.groups.length;
|
|
298
|
-
|
|
299
392
|
if (previous.isTree !== state.index.isTree) {
|
|
300
|
-
state
|
|
393
|
+
replaceGroups(state, previous.groups.length);
|
|
301
394
|
|
|
302
395
|
return;
|
|
303
396
|
}
|
|
304
397
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
398
|
+
const keys = groupKeys(previous, state.index);
|
|
399
|
+
const found = findSplice(keys);
|
|
400
|
+
const splice = searchableSplice(found, addedRows(state.index, found));
|
|
401
|
+
|
|
402
|
+
if (!hasSplice(splice)) {
|
|
403
|
+
return;
|
|
308
404
|
}
|
|
405
|
+
|
|
406
|
+
const moves = matchMoves(keys, splice, state.groupStores.keys());
|
|
407
|
+
const relocate: Relocate = (group) => relocateSlot(splice, group, moves);
|
|
408
|
+
state.groupStores = relocateEntries(state.groupStores, relocate, (store) => {
|
|
409
|
+
dropGroup(state, store);
|
|
410
|
+
});
|
|
411
|
+
relocateSlots(state.expansion, "", relocate);
|
|
412
|
+
|
|
413
|
+
state.root?.itemsChanged(splice.start, splice.removed, splice.added);
|
|
309
414
|
}
|
|
310
415
|
|
|
311
416
|
function stepGroupStores(state: ModelState, context: SyncContext): void {
|
|
@@ -318,7 +423,17 @@ function stepGroupStores(state: ModelState, context: SyncContext): void {
|
|
|
318
423
|
}
|
|
319
424
|
}
|
|
320
425
|
|
|
321
|
-
function
|
|
426
|
+
function rowCount(index: CollectionIndex): number {
|
|
427
|
+
return index.groups.reduce((rows, level) => rows + level.items.length, 0);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function hasFilledSections(previous: CollectionIndex, index: CollectionIndex): boolean {
|
|
431
|
+
return (
|
|
432
|
+
rowCount(previous) === 0 && index.groups.some((level) => level.section !== undefined && level.items.length > 0)
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function syncModel(state: ModelState, index: CollectionIndex, onSectionsFilled: () => void): void {
|
|
322
437
|
if (state.root === null) {
|
|
323
438
|
return;
|
|
324
439
|
}
|
|
@@ -329,14 +444,22 @@ function syncModel(state: ModelState, index: CollectionIndex): void {
|
|
|
329
444
|
state.expansion.isSyncing = true;
|
|
330
445
|
|
|
331
446
|
try {
|
|
332
|
-
pruneGroups(state, previous);
|
|
333
447
|
syncRoot(state, previous);
|
|
334
448
|
stepGroupStores(state, context);
|
|
449
|
+
|
|
450
|
+
if (hasFilledSections(previous, index)) {
|
|
451
|
+
onSectionsFilled();
|
|
452
|
+
}
|
|
335
453
|
} finally {
|
|
336
454
|
state.expansion.isSyncing = false;
|
|
337
455
|
}
|
|
338
456
|
|
|
339
457
|
adoptIndex(state.expansion, index);
|
|
458
|
+
state.version += 1;
|
|
459
|
+
|
|
460
|
+
for (const listener of state.listeners) {
|
|
461
|
+
listener();
|
|
462
|
+
}
|
|
340
463
|
}
|
|
341
464
|
|
|
342
465
|
function clearModel(state: ModelState): void {
|
|
@@ -368,6 +491,8 @@ function clearModel(state: ModelState): void {
|
|
|
368
491
|
|
|
369
492
|
function createCollectionModel(): CollectionModel {
|
|
370
493
|
const state: ModelState = {
|
|
494
|
+
version: 0,
|
|
495
|
+
listeners: new Set(),
|
|
371
496
|
root: null,
|
|
372
497
|
model: null,
|
|
373
498
|
groupStores: new Map(),
|
|
@@ -378,6 +503,15 @@ function createCollectionModel(): CollectionModel {
|
|
|
378
503
|
|
|
379
504
|
return {
|
|
380
505
|
expansion: state.expansion,
|
|
506
|
+
subscribe: (onChange) => {
|
|
507
|
+
state.listeners.add(onChange);
|
|
508
|
+
|
|
509
|
+
return () => {
|
|
510
|
+
state.listeners.delete(onChange);
|
|
511
|
+
};
|
|
512
|
+
},
|
|
513
|
+
getVersion: () => state.version,
|
|
514
|
+
isPending: (index) => state.root !== null && state.index !== index,
|
|
381
515
|
rowAt: (position) => rowAt(state, position),
|
|
382
516
|
bind: (root, model) => {
|
|
383
517
|
root.state = state;
|
|
@@ -387,8 +521,8 @@ function createCollectionModel(): CollectionModel {
|
|
|
387
521
|
clear: () => {
|
|
388
522
|
clearModel(state);
|
|
389
523
|
},
|
|
390
|
-
sync: (index) => {
|
|
391
|
-
syncModel(state, index);
|
|
524
|
+
sync: (index, onSectionsFilled) => {
|
|
525
|
+
syncModel(state, index, onSectionsFilled);
|
|
392
526
|
},
|
|
393
527
|
};
|
|
394
528
|
}
|
|
@@ -426,19 +560,18 @@ class CollectionRootStore extends GObject.Object implements Gio.ListModelImpl {
|
|
|
426
560
|
|
|
427
561
|
class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
|
|
428
562
|
declare itemsChanged: Gio.ListModel["itemsChanged"];
|
|
563
|
+
key: string = nextKey();
|
|
429
564
|
level: Level = { path: "", items: [], expandableFlags: [] };
|
|
430
|
-
|
|
431
|
-
objects: Map<number, Gtk.StringObject> = new Map();
|
|
565
|
+
slots: Map<number, SlotItem> = new Map();
|
|
432
566
|
childStores: Map<number, LevelStore> = new Map();
|
|
433
567
|
|
|
434
568
|
private createItem(position: number): Gtk.StringObject {
|
|
435
|
-
const ref = { store: this, slot: position };
|
|
436
|
-
const
|
|
437
|
-
this.
|
|
438
|
-
|
|
439
|
-
sharedSlots().set(created, ref);
|
|
569
|
+
const ref = { key: nextKey(), store: this, slot: position };
|
|
570
|
+
const object = Gtk.StringObject.new("");
|
|
571
|
+
this.slots.set(position, { ref, object });
|
|
572
|
+
sharedSlots().set(object, ref);
|
|
440
573
|
|
|
441
|
-
return
|
|
574
|
+
return object;
|
|
442
575
|
}
|
|
443
576
|
|
|
444
577
|
vfuncGetItemType(): bigint {
|
|
@@ -454,7 +587,7 @@ class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
|
|
|
454
587
|
return null;
|
|
455
588
|
}
|
|
456
589
|
|
|
457
|
-
return this.
|
|
590
|
+
return this.slots.get(position)?.object ?? this.createItem(position);
|
|
458
591
|
}
|
|
459
592
|
}
|
|
460
593
|
|
|
@@ -466,9 +599,11 @@ const CollectionRoot: (props: CollectionRootProps) => ReactNode = createElementC
|
|
|
466
599
|
export {
|
|
467
600
|
CollectionRoot,
|
|
468
601
|
createCollectionModel,
|
|
602
|
+
levelStoreFor,
|
|
469
603
|
slotPathAt,
|
|
470
604
|
slotRefFor,
|
|
471
605
|
type CollectionModel,
|
|
472
606
|
type CollectionRootStore,
|
|
607
|
+
type LazyLevelStore,
|
|
473
608
|
type SlotRef,
|
|
474
609
|
};
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
2
|
import type { ListItem } from "../types.js";
|
|
3
|
-
import type { CollectionIndex
|
|
3
|
+
import type { CollectionIndex } from "./collection-index.js";
|
|
4
4
|
import type { CollectionModel, SlotRef } from "./collection-model.js";
|
|
5
5
|
import type { MatchedRows } from "./tree-order.js";
|
|
6
6
|
import { slotPathAt, slotRefFor } from "./collection-model.js";
|
|
7
|
+
import { orderFor } from "./tree-expansion.js";
|
|
7
8
|
import { findIds, findRows } from "./tree-order.js";
|
|
8
9
|
|
|
9
|
-
type Collection = Pick<CollectionModel, "expansion" | "rowAt"> & {
|
|
10
|
+
type Collection = Pick<CollectionModel, "expansion" | "rowAt" | "subscribe" | "getVersion"> & {
|
|
10
11
|
isTree: boolean;
|
|
11
12
|
itemAt: (ref: SlotRef) => ListItem | undefined;
|
|
12
|
-
sectionFor: (levelPath: string) => SectionIdentity | undefined;
|
|
13
13
|
idAt: (position: number) => string | null;
|
|
14
14
|
idsAt: (positions: number[]) => string[];
|
|
15
15
|
pathAt: (position: number) => string | null;
|
|
16
|
+
isPending: () => boolean;
|
|
16
17
|
positionFor: (id: string) => number;
|
|
17
18
|
rowsFor: (ids: string[]) => MatchedRows;
|
|
18
19
|
};
|
|
@@ -31,9 +32,7 @@ function rowsFor(gtkModel: CollectionModel, ids: string[]): MatchedRows {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function positionFor(gtkModel: CollectionModel, id: string): number {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return first;
|
|
35
|
+
return orderFor(gtkModel.expansion).positions.get(id) ?? -1;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
function refAt(model: Gtk.FlattenListModel | null, position: number): SlotRef | null {
|
|
@@ -44,14 +43,14 @@ function refAt(model: Gtk.FlattenListModel | null, position: number): SlotRef |
|
|
|
44
43
|
return slotRefFor(model?.getItem(position) ?? null);
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
function itemAt(
|
|
48
|
-
return
|
|
46
|
+
function itemAt(ref: SlotRef): ListItem | undefined {
|
|
47
|
+
return ref.store.level.items[ref.slot];
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
function idAt(model: Gtk.FlattenListModel | null,
|
|
50
|
+
function idAt(model: Gtk.FlattenListModel | null, position: number): string | null {
|
|
52
51
|
const ref = refAt(model, position);
|
|
53
52
|
|
|
54
|
-
return ref === null ? null : (itemAt(
|
|
53
|
+
return ref === null ? null : (itemAt(ref)?.id ?? null);
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
function pathAt(model: Gtk.FlattenListModel | null, position: number): string | null {
|
|
@@ -89,11 +88,13 @@ function createCollection(
|
|
|
89
88
|
expansion: gtkModel.expansion,
|
|
90
89
|
isTree: index.isTree,
|
|
91
90
|
rowAt: gtkModel.rowAt,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
subscribe: gtkModel.subscribe,
|
|
92
|
+
getVersion: gtkModel.getVersion,
|
|
93
|
+
itemAt,
|
|
94
|
+
idAt: (position) => idAt(model, position),
|
|
95
95
|
idsAt: (positions) => idsAt(gtkModel, positions),
|
|
96
96
|
pathAt: (position) => pathAt(model, position),
|
|
97
|
+
isPending: () => gtkModel.isPending(index),
|
|
97
98
|
positionFor: (id) => positionFor(gtkModel, id),
|
|
98
99
|
rowsFor: (ids) => rowsFor(gtkModel, ids),
|
|
99
100
|
};
|
|
@@ -8,6 +8,13 @@ type ControlledSyncOptions<T> = {
|
|
|
8
8
|
apply: (value: T) => void;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
function hasSameIds(reported: string[], requested: string[]): boolean {
|
|
12
|
+
const wanted = new Set(requested);
|
|
13
|
+
const actual = new Set(reported);
|
|
14
|
+
|
|
15
|
+
return actual.size === wanted.size && actual.isSubsetOf(wanted);
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
function useControlledSync<T>(options: ControlledSyncOptions<T>): () => void {
|
|
12
19
|
const { value, source, target } = options;
|
|
13
20
|
const [drift, setDrift] = useState(0);
|
|
@@ -27,4 +34,4 @@ function useControlledSync<T>(options: ControlledSyncOptions<T>): () => void {
|
|
|
27
34
|
return markDrift;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
export { useControlledSync };
|
|
37
|
+
export { hasSameIds, useControlledSync };
|