dazscript-framework 0.3.1 → 1.0.1

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.
Files changed (36) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +327 -267
  3. package/package.json +9 -6
  4. package/src/Setup.dsa.ts +37 -9
  5. package/src/dialog/builders/list-view-builder.ts +6 -1
  6. package/src/dialog/builders/slider-builder.ts +1 -1
  7. package/src/examples/01-hello-world.dsa.ts +8 -0
  8. package/src/examples/02-persistence-dialog.dsa.ts +21 -0
  9. package/src/examples/02-persistence-dialog.ts +68 -0
  10. package/src/examples/03-simple-dialog.dsa.ts +23 -0
  11. package/src/examples/03-simple-dialog.ts +47 -0
  12. package/src/examples/04-settings-dialog.dsa.ts +29 -0
  13. package/src/examples/04-settings-dialog.ts +83 -0
  14. package/src/examples/05-list-dialog.dsa.ts +53 -0
  15. package/src/examples/05-list-dialog.ts +88 -0
  16. package/src/examples/06-showcase-dialog.dsa.ts +87 -0
  17. package/src/examples/06-showcase-dialog.ts +518 -0
  18. package/src/helpers/action-helper.ts +2 -2
  19. package/src/helpers/custom-action-helper.ts +5 -4
  20. package/src/helpers/custom-action-installer-helper.ts +39 -10
  21. package/src/helpers/file-helper.ts +1 -1
  22. package/src/helpers/list-view-helper.ts +1 -1
  23. package/src/helpers/message-box-helper.ts +2 -2
  24. package/src/helpers/node-helper.ts +14 -14
  25. package/src/helpers/pane-helper.ts +5 -5
  26. package/src/helpers/scene-helper.ts +9 -9
  27. package/src/helpers/viewport-helper.ts +2 -2
  28. package/src/lib/observable.test.ts +416 -0
  29. package/src/lib/observable.ts +24 -18
  30. package/src/lib/tree-node.test.ts +21 -0
  31. package/tsconfig.json +34 -109
  32. package/webpack.config.js +1 -0
  33. package/src/samples/hello-world.dsa.ts +0 -8
  34. package/src/samples/sample-dialog.dsa.ts +0 -47
  35. package/src/samples/sample-dialog.ts +0 -49
  36. /package/src/{samples → examples}/config.ts +0 -0
@@ -0,0 +1,518 @@
1
+ import { BasicDialog } from '@dsf/dialog/basic-dialog'
2
+ import { PopupMenuItem } from '@dsf/dialog/builders/popup-menu-builder'
3
+ import { getDataItem } from '@dsf/helpers/list-view-helper'
4
+ import { AppSettings } from '@dsf/lib/settings'
5
+ import { Observable } from '@dsf/lib/observable'
6
+ import { TreeNode } from '@dsf/lib/tree-node'
7
+ import { config } from './config'
8
+
9
+ // Types
10
+
11
+ export interface SceneObject {
12
+ id: number
13
+ name: string
14
+ type: 'Figure' | 'Prop' | 'Light' | 'Camera' | 'Group'
15
+ visible: boolean
16
+ locked: boolean
17
+ }
18
+
19
+ // Settings (all persisted via DzAppSettings)
20
+
21
+ export class ShowcaseDialogSettings extends AppSettings {
22
+ constructor() {
23
+ super(`${config.author}/06-ShowcaseDialog`)
24
+ }
25
+
26
+ // Objects tab
27
+ activeTab$ = this.bindInt('activeTab$', 0)
28
+ flatList$ = this.bindBoolean('flatList$', false)
29
+ showIcons$ = this.bindBoolean('showIcons$', true)
30
+ showVisibleOnly$ = this.bindBoolean('showVisibleOnly$', false)
31
+ splitterState$ = this.bindString('splitterState$', '')
32
+ filterType$ = this.bindString('filterType$', 'All')
33
+
34
+ // Settings tab - Render
35
+ quality$ = this.bindString('quality$', 'Standard')
36
+ samples$ = this.bindInt('samples$', 64)
37
+ scale$ = this.bindFloat('scale$', 1.0)
38
+
39
+ // Settings tab - Export
40
+ exportFormat$ = this.bindString('exportFormat$', 'JSON')
41
+ outputPath$ = this.bindString('outputPath$', '')
42
+ autoSave$ = this.bindBoolean('autoSave$', false)
43
+
44
+ // Settings tab - Behaviour
45
+ notifications$ = this.bindBoolean('notifications$', true)
46
+ logLevel$ = this.bindString('logLevel$', 'Info')
47
+
48
+ // About tab
49
+ showFilters$ = this.bindBoolean('showFilters$', false)
50
+ }
51
+
52
+ // Model
53
+
54
+ export class ShowcaseDialogModel {
55
+ readonly settings = new ShowcaseDialogSettings()
56
+
57
+ objects$ = new Observable<TreeNode<SceneObject>[]>([])
58
+ selectedObject$ = new Observable<SceneObject>()
59
+ refreshObjects$ = new Observable<void>()
60
+ keywords$ = new Observable('')
61
+ logEntries$ = new Observable<string[]>([])
62
+ sceneStats$ = new Observable('')
63
+ selectedSummary$ = new Observable('No object selected')
64
+
65
+ // Detail panel fields - updated externally when selectedObject$ changes
66
+ detailName$ = new Observable('')
67
+ detailType$ = new Observable('')
68
+ detailVisible$ = new Observable(false)
69
+ detailLocked$ = new Observable(false)
70
+ }
71
+
72
+ // Dialog
73
+
74
+ export class ShowcaseDialog extends BasicDialog {
75
+ private sceneListView: DzListView | null = null
76
+
77
+ constructor(private readonly model: ShowcaseDialogModel) {
78
+ super('06 Showcase Dialog', `${config.author}/06-ShowcaseDialog`)
79
+ }
80
+
81
+ protected build(): void {
82
+ let add = this.add
83
+ let settings = this.model.settings
84
+
85
+ this.builder.options({ resizable: true, width: 780, height: 640 })
86
+ this.dialog.setAcceptButtonText('Apply')
87
+ this.dialog.setCancelButtonText('Close')
88
+
89
+ this.connectInteractions()
90
+
91
+ // Active tab index persists across runs via bindInt.
92
+ add.tab('Objects').bind(settings.activeTab$).build(() => {
93
+ this.buildObjectsTab()
94
+ })
95
+ add.tab('Settings').build(() => {
96
+ this.buildSettingsTab()
97
+ })
98
+ add.tab('About').build(() => {
99
+ this.buildAboutTab()
100
+ })
101
+
102
+ this.updateSceneStats()
103
+ }
104
+
105
+ // Tab 1: Objects
106
+
107
+ private buildObjectsTab(): void {
108
+ let add = this.add
109
+ let model = this.model
110
+ let settings = model.settings
111
+
112
+ // Toolbar row
113
+ add.group('Search').horizontal().build((layout) => {
114
+ layout.spacing = 4
115
+ add.edit()
116
+ .value(model.keywords$)
117
+ .placeholder('Filter objects...')
118
+ .focus()
119
+ add.label('Type:')
120
+ add.combo()
121
+ .items(['All', 'Figure', 'Prop', 'Light', 'Camera', 'Group'])
122
+ .selected(settings.filterType$)
123
+ add.button('Refresh')
124
+ .clicked(() => model.refreshObjects$.trigger())
125
+ .toolTip('Reload the object list')
126
+ add.button('Apply Checked Rows')
127
+ .clicked(() => this.applyCheckedRows())
128
+ .toolTip('Copies the list checkbox states back to the scene objects.')
129
+ })
130
+
131
+ add.horizontal((layout) => {
132
+ layout.spacing = 6
133
+ add.checkbox('Flat List').value(settings.flatList$)
134
+ add.checkbox('Show Icons').value(settings.showIcons$)
135
+ add.checkbox('Visible Only').value(settings.showVisibleOnly$)
136
+ const statsLabel = add.label(model.sceneStats$.value).build()
137
+ model.sceneStats$.connect(text => { statsLabel.text = text })
138
+ })
139
+
140
+ // Splitter - position persisted via bindString
141
+ add.splitter()
142
+ .state(settings.splitterState$)
143
+ .strech(2, 1)
144
+ .items(
145
+ // Left pane: tree list with filter + context menu
146
+ add.group().style({ flat: true }).build(() => {
147
+ add.list.view<SceneObject, SceneObject>()
148
+ .row((item, parent, id) => {
149
+ let obj = item.value as SceneObject
150
+ let listItem = new DzCheckListItem(parent, item.isLeaf ? DzCheckListItem.CheckBox : DzCheckListItem.CheckBoxController, id)
151
+ listItem.selectable = true
152
+ listItem.on = obj.visible
153
+ return listItem
154
+ })
155
+ .expanded(true)
156
+ .flat(settings.flatList$)
157
+ .refresh(model.refreshObjects$)
158
+ .items(model.objects$)
159
+ .columns(['Visible', 'Name', 'Type', 'Locked'])
160
+ .text(item => [
161
+ '',
162
+ this.getDisplayName(item.value),
163
+ item.value?.type ?? '',
164
+ item.value?.locked ? 'Yes' : 'No',
165
+ ])
166
+ .data(item => item.value)
167
+ .selected(model.selectedObject$)
168
+ .filter({
169
+ keywords: model.keywords$,
170
+ field: li => li.text(0),
171
+ selectOnFilter: true,
172
+ filters: li => {
173
+ let obj = getDataItem<SceneObject>(li)
174
+ if (!obj) return true
175
+ let type = settings.filterType$.value
176
+ let matchesType = type === 'All' || obj.type === type
177
+ let matchesVisibility = !settings.showVisibleOnly$.value || obj.visible
178
+ return matchesType && matchesVisibility
179
+ },
180
+ })
181
+ .contextMenu((_, li) => {
182
+ let obj = getDataItem<SceneObject>(li)
183
+ let label = obj?.name ?? 'item'
184
+ let items: PopupMenuItem[] = [
185
+ {
186
+ text: `Select "${label}"`,
187
+ activated: () => { /* select in viewport */ },
188
+ },
189
+ {
190
+ text: obj?.visible ? 'Hide' : 'Show',
191
+ activated: () => {
192
+ if (obj) obj.visible = !obj.visible
193
+ model.refreshObjects$.trigger()
194
+ },
195
+ },
196
+ { text: '' }, // separator
197
+ {
198
+ text: 'Duplicate',
199
+ activated: () => { /* duplicate */ },
200
+ },
201
+ {
202
+ text: 'Delete',
203
+ activated: () => { /* delete */ },
204
+ },
205
+ ]
206
+ return add.contextMenu().items(...items).build()
207
+ })
208
+ .build((listView) => {
209
+ this.sceneListView = listView
210
+ listView.allColumnsShowFocus = true
211
+ })
212
+ }),
213
+
214
+ // Right pane: detail panel for selected object
215
+ add.group('Details').build(() => {
216
+ add.horizontal((layout) => {
217
+ layout.spacing = 4
218
+ add.label('Name:').minWidth(45)
219
+ add.edit().readOnly().value(model.detailName$)
220
+ })
221
+ add.horizontal((layout) => {
222
+ layout.spacing = 4
223
+ add.label('Type:').minWidth(45)
224
+ add.edit().readOnly().value(model.detailType$)
225
+ })
226
+ add.group('Flags').horizontal().build(() => {
227
+ add.checkbox('Visible').value(model.detailVisible$)
228
+ add.checkbox('Locked').value(model.detailLocked$)
229
+ })
230
+ const summaryLabel = add.label(model.selectedSummary$.value).wordWrap().build()
231
+ model.selectedSummary$.connect(text => { summaryLabel.text = text })
232
+ add.group('Scene Node').build(() => {
233
+ // NodeSelection lets the user pick any node from the scene.
234
+ add.nodeSelection().build()
235
+ })
236
+ })
237
+ )
238
+ .build()
239
+ }
240
+
241
+ // Tab 2: Settings
242
+
243
+ private buildSettingsTab(): void {
244
+ let add = this.add
245
+ let settings = this.model.settings
246
+
247
+ add.horizontal((layout) => {
248
+ layout.spacing = 8
249
+
250
+ // Left column
251
+ add.vertical(() => {
252
+ add.group('Display').build(() => {
253
+ add.checkbox('Flat List').value(settings.flatList$)
254
+ add.checkbox('Show Icons').value(settings.showIcons$)
255
+ add.checkbox('Visible Only').value(settings.showVisibleOnly$)
256
+ add.horizontal((layout) => {
257
+ layout.spacing = 4
258
+ add.label('Object Type:').minWidth(80)
259
+ add.combo()
260
+ .items(['All', 'Figure', 'Prop', 'Light', 'Camera', 'Group'])
261
+ .selected(settings.filterType$)
262
+ })
263
+ })
264
+
265
+ // Radio buttons - initialize from persisted quality value;
266
+ // toggled() writes back when the user picks a different option.
267
+ add.group('Render Quality').build(() => {
268
+ add.radio('Draft')
269
+ .value(settings.quality$.value === 'Draft')
270
+ .toggled(v => {
271
+ if (!v) return
272
+ settings.quality$.value = 'Draft'
273
+ settings.samples$.value = 16
274
+ settings.scale$.value = 0.5
275
+ })
276
+ add.radio('Standard')
277
+ .value(settings.quality$.value === 'Standard')
278
+ .toggled(v => {
279
+ if (!v) return
280
+ settings.quality$.value = 'Standard'
281
+ settings.samples$.value = 64
282
+ settings.scale$.value = 1.0
283
+ })
284
+ add.radio('High')
285
+ .value(settings.quality$.value === 'High')
286
+ .toggled(v => {
287
+ if (!v) return
288
+ settings.quality$.value = 'High'
289
+ settings.samples$.value = 256
290
+ settings.scale$.value = 2.0
291
+ })
292
+ })
293
+ })
294
+
295
+ // Right column
296
+ add.vertical(() => {
297
+ add.group('Render').build(() => {
298
+ add.horizontal((layout) => {
299
+ layout.spacing = 4
300
+ add.label('Samples:').minWidth(55)
301
+ add.slider('integer')
302
+ .value(settings.samples$)
303
+ .min(1).max(512)
304
+ .build()
305
+ })
306
+ add.horizontal((layout) => {
307
+ layout.spacing = 4
308
+ add.label('Scale:').minWidth(55)
309
+ add.slider('float')
310
+ .value(settings.scale$)
311
+ .min(0.1).max(5.0)
312
+ .build()
313
+ })
314
+ })
315
+
316
+ add.group('Export').build(() => {
317
+ add.horizontal((layout) => {
318
+ layout.spacing = 4
319
+ add.label('Output:').minWidth(55)
320
+ add.edit()
321
+ .value(settings.outputPath$)
322
+ .placeholder('Default output path...')
323
+ })
324
+ add.horizontal((layout) => {
325
+ layout.spacing = 4
326
+ add.label('Format:').minWidth(55)
327
+ // ComboBox - two-way bind to persisted string
328
+ add.combo()
329
+ .items(['JSON', 'XML', 'CSV'])
330
+ .selected(settings.exportFormat$)
331
+ })
332
+ add.checkbox('Auto Save').value(settings.autoSave$)
333
+ add.label('Auto Save enables notifications and switches the log level to Debug.').wordWrap().build()
334
+ })
335
+ })
336
+ })
337
+
338
+ // Color pickers - read the final color from the widget on accept
339
+ add.group('Colors').horizontal().build(() => {
340
+ add.vertical(() => {
341
+ add.label('Background:').build()
342
+ add.color().build()
343
+ })
344
+ add.vertical(() => {
345
+ add.label('Foreground:').build()
346
+ add.color().build()
347
+ })
348
+ })
349
+
350
+ add.group('Behaviour').horizontal().build(() => {
351
+ add.checkbox('Notifications').value(settings.notifications$)
352
+ add.horizontal((layout) => {
353
+ layout.spacing = 4
354
+ add.label('Log Level:')
355
+ // ComboEdit: user can pick from list OR type a custom level
356
+ add.comboEdit()
357
+ .items(['Debug', 'Info', 'Warning', 'Error'])
358
+ .changed(settings.logLevel$)
359
+ })
360
+ })
361
+ }
362
+
363
+ // Tab 3: About
364
+
365
+ private buildAboutTab(): void {
366
+ let add = this.add
367
+ let model = this.model
368
+ let settings = model.settings
369
+
370
+ add.group('About This Example').build(() => {
371
+ add.label([
372
+ 'DAZScript Framework - Showcase Dialog (Example 06)',
373
+ '',
374
+ 'Widgets demonstrated in this dialog:',
375
+ ' - Tabs with persisted active-tab index (bindInt)',
376
+ ' - Splitter with persisted position (bindString)',
377
+ ' - list.view - columns, keyword filter, custom filters,',
378
+ ' checkbox rows, context menu, refresh trigger, flat/tree toggle,',
379
+ ' expanded, data binding, selection binding',
380
+ ' - list.box - scrollable list with multi-select',
381
+ ' - GroupBox - vertical / horizontal / flat style / visible toggle',
382
+ ' - Label - wordWrap, minWidth, alignment',
383
+ ' - LineEdit - value binding, placeholder, readOnly',
384
+ ' - CheckBox, RadioButton - Observable two-way binding',
385
+ ' - Button - clicked, toggle, toolTip',
386
+ ' - Slider - integer and float with min/max',
387
+ ' - ColorPicker (DzColorWgt)',
388
+ ' - ComboBox - items + selected binding',
389
+ ' - ComboEdit - items + changed/edited binding',
390
+ ' - NodeSelection combo (scene node picker)',
391
+ ' - All settings persisted via AppSettings (DzAppSettings)',
392
+ ].join('\n')).wordWrap(true).build()
393
+ })
394
+
395
+ // Log list - populated externally; demonstrates Observable<string[]> binding
396
+ add.group('Recent Log').build(() => {
397
+ add.list.box()
398
+ .items(model.logEntries$)
399
+ .mode('single')
400
+ .build()
401
+ })
402
+
403
+ // Conditional filter row - group visible when showFilters$ is true
404
+ add.group('Optional Filters').horizontal()
405
+ .visible(settings.showFilters$)
406
+ .build(() => {
407
+ add.label('Tag:').minWidth(30)
408
+ add.comboEdit()
409
+ .items(['render', 'test', 'draft', 'final'])
410
+ .build()
411
+ })
412
+
413
+ add.horizontal((layout) => {
414
+ layout.spacing = 4
415
+ add.checkbox('Show Filters').value(settings.showFilters$)
416
+ })
417
+ }
418
+
419
+ private connectInteractions(): void {
420
+ let model = this.model
421
+ let settings = model.settings
422
+
423
+ settings.filterType$.connect(() => model.refreshObjects$.trigger())
424
+ settings.showVisibleOnly$.connect(() => model.refreshObjects$.trigger())
425
+ settings.showIcons$.connect(() => model.refreshObjects$.trigger())
426
+ settings.flatList$.connect(() => model.refreshObjects$.trigger())
427
+
428
+ settings.autoSave$.connect((enabled) => {
429
+ if (!enabled) return
430
+ settings.notifications$.value = true
431
+ settings.logLevel$.value = 'Debug'
432
+ this.addLog('Auto Save enabled; notifications and Debug logging selected.')
433
+ })
434
+
435
+ model.objects$.connect(() => this.updateSceneStats())
436
+ model.refreshObjects$.connect(() => this.updateSceneStats())
437
+
438
+ model.selectedObject$.connect(obj => {
439
+ model.selectedSummary$.value = obj
440
+ ? `${obj.name}: ${obj.type}, ${obj.visible ? 'visible' : 'hidden'}, ${obj.locked ? 'locked' : 'unlocked'}`
441
+ : 'No object selected'
442
+ })
443
+
444
+ model.detailVisible$.connect((visible) => {
445
+ let obj = model.selectedObject$.value
446
+ if (!obj || obj.visible === visible) return
447
+ obj.visible = visible
448
+ this.addLog(`${obj.name} visibility changed from the detail checkbox.`)
449
+ model.refreshObjects$.trigger()
450
+ })
451
+
452
+ model.detailLocked$.connect((locked) => {
453
+ let obj = model.selectedObject$.value
454
+ if (!obj || obj.locked === locked) return
455
+ obj.locked = locked
456
+ this.addLog(`${obj.name} lock state changed from the detail checkbox.`)
457
+ model.refreshObjects$.trigger()
458
+ })
459
+ }
460
+
461
+ private getDisplayName(obj: SceneObject | null): string {
462
+ if (!obj) return ''
463
+ if (!this.model.settings.showIcons$.value) return obj.name
464
+
465
+ const prefixByType: Record<SceneObject['type'], string> = {
466
+ Figure: '[F]',
467
+ Prop: '[P]',
468
+ Light: '[L]',
469
+ Camera: '[C]',
470
+ Group: '[G]',
471
+ }
472
+
473
+ return `${prefixByType[obj.type]} ${obj.name}`
474
+ }
475
+
476
+ private addLog(message: string): void {
477
+ let entries = this.model.logEntries$.value.slice()
478
+ entries.push(`[Info] ${message}`)
479
+ if (entries.length > 8) entries.shift()
480
+ this.model.logEntries$.value = entries
481
+ }
482
+
483
+ private updateSceneStats(): void {
484
+ let objects = this.getAllObjects()
485
+ let visible = objects.filter(obj => obj.visible).length
486
+ let locked = objects.filter(obj => obj.locked).length
487
+ this.model.sceneStats$.value = `${visible}/${objects.length} visible, ${locked} locked`
488
+ }
489
+
490
+ private getAllObjects(): SceneObject[] {
491
+ let objects: SceneObject[] = []
492
+ this.model.objects$.value.forEach(root => {
493
+ root.forEach(node => {
494
+ if (node.value) objects.push(node.value)
495
+ })
496
+ })
497
+ return objects
498
+ }
499
+
500
+ private applyCheckedRows(): void {
501
+ if (!this.sceneListView) return
502
+
503
+ this.sceneListView.getItems(DzListView.All).forEach(item => {
504
+ if (!(item as any).inherits('DzCheckListItem')) return
505
+
506
+ let obj = getDataItem<SceneObject>(item)
507
+ if (!obj) return
508
+
509
+ let checkbox = item as DzCheckListItem
510
+ obj.visible = checkbox.state === 0 && !checkbox.on
511
+ ? false
512
+ : checkbox.on
513
+ })
514
+
515
+ this.addLog('List checkbox states applied to scene objects.')
516
+ this.model.refreshObjects$.trigger()
517
+ }
518
+ }
@@ -195,7 +195,7 @@ export const clearActionShorcut = (name: string) => {
195
195
  }
196
196
  }
197
197
 
198
- export const getActionPixmap = (action: string, icon: string, maxSize?: number): QPixmap | null => {
198
+ export const getActionPixmap = (action: string, icon: string, maxSize?: number): Pixmap | null => {
199
199
  try {
200
200
  if (!action) return null;
201
201
  let isCustom = isCustomAction(action)
@@ -204,7 +204,7 @@ export const getActionPixmap = (action: string, icon: string, maxSize?: number):
204
204
  let image: DzImageTexture = (App.getImageMgr() as any)['getImage(QString)'](icon)
205
205
  if (image) {
206
206
  let size = image.getOriginalImageSize();
207
- let pixmap: QPixmap
207
+ let pixmap: Pixmap
208
208
  // Only clamp if maxSize is provided and image is larger
209
209
  if (maxSize && (size.width > maxSize || size.height > maxSize)) {
210
210
  pixmap = image.getPreviewPixmap(Math.min(size.width, maxSize), Math.min(size.height, maxSize))
@@ -4,8 +4,9 @@ import { mainWindow } from '@dsf/core/global'
4
4
  import * as array from '@dsf/helpers/array-helper'
5
5
  import { keys } from '@dsf/helpers/object-helper'
6
6
  import { progress } from '@dsf/helpers/progress-helper'
7
- import { getScriptPath } from './script-helper'
7
+ import CustomSet from '@dsf/lib/set'
8
8
  import { getMenu } from './menu-helper'
9
+ import { getScriptPath } from './script-helper'
9
10
 
10
11
  const actionMgr = mainWindow.getActionMgr()
11
12
  const paneMgr = mainWindow.getPaneMgr()
@@ -298,7 +299,7 @@ export const cleanupEmptyToolbar = (toolbarName: string | null | undefined) => {
298
299
  debug(`Toolbar ${toolbarName} closed (setClosed=true)`)
299
300
  toolbar.clear()
300
301
  debug(`Toolbar ${toolbarName} cleared`)
301
- paneMgr.removeToolBar(toolbar)
302
+ paneMgr.removeToolBar(toolbarName)
302
303
  debug(`Toolbar ${toolbarName} removeToolBar called`)
303
304
  }
304
305
 
@@ -445,7 +446,7 @@ export const installCustomActions = (actions: CustomAction[]) => {
445
446
 
446
447
  export const uninstallCustomActions = (actions: CustomAction[]) => {
447
448
  debug(`Uninstalling Actions`)
448
- const toolbarNames = new Set<string>()
449
+ const toolbarNames = new CustomSet<string>()
449
450
 
450
451
  actions.forEach(action => {
451
452
  if (!action) return
@@ -461,7 +462,7 @@ export const uninstallCustomActions = (actions: CustomAction[]) => {
461
462
  }
462
463
  toolbar.clear()
463
464
  toolbar.setClosed(true)
464
- paneMgr.removeToolBar(toolbar)
465
+ paneMgr.removeToolBar(toolbarName)
465
466
  debug(`Toolbar ${toolbarName} cleared, closed, and removed after uninstall`)
466
467
  })
467
468
  }
@@ -10,6 +10,7 @@ import { Observable } from '@dsf/lib/observable'
10
10
  import CustomSet from '@dsf/lib/set'
11
11
  import { TreeNode } from '@dsf/lib/tree-node'
12
12
  import { promptKeyboardShortcut } from '@dsf/shared/set-keyboard-shortcut'
13
+ import { readFromFile } from './file-helper'
13
14
 
14
15
  type InstallerEntry = {
15
16
  action: CustomAction
@@ -28,6 +29,12 @@ type InstallerEntry = {
28
29
  type SetupDialogOptions = {
29
30
  settingsPath: string
30
31
  bundleName?: string
32
+ shortcutsPath?: string
33
+ }
34
+
35
+ type ActionAccelerator = {
36
+ name: string
37
+ shortcut: string
31
38
  }
32
39
 
33
40
  const OVERRIDE_MARKER = '[ovr]'
@@ -37,6 +44,9 @@ const toKey = (action: CustomAction): string => String(action.filePath ?? action
37
44
  const toTreeNode = (entry: InstallerEntry): TreeNode<InstallerEntry> =>
38
45
  new TreeNode(String(entry.action.text), toKey(entry.action), entry)
39
46
 
47
+ const getEntry = (item: TreeNode<InstallerEntry>): InstallerEntry =>
48
+ item.value as InstallerEntry
49
+
40
50
  const getDisplayedToolbar = (entry: InstallerEntry): string => String(entry.action.toolbar ?? '')
41
51
 
42
52
  const getSetupDialogOptions = (options: string | SetupDialogOptions): SetupDialogOptions =>
@@ -123,8 +133,9 @@ class InstallerSelectionDialog extends BasicDialog {
123
133
  .sortOnBuild(true)
124
134
  .refresh(this.refreshListEvent$)
125
135
  .row((item, parent, id) => {
136
+ const entry = getEntry(item)
126
137
  const listItem = new DzCheckListItem(parent, DzCheckListItem.CheckBox, id)
127
- listItem.on = item.value.selected
138
+ listItem.on = entry.selected
128
139
  listItem.setText(0, '')
129
140
  return listItem
130
141
  })
@@ -139,15 +150,18 @@ class InstallerSelectionDialog extends BasicDialog {
139
150
  if (index === 5) return Math.max(width * 1.2, 140)
140
151
  return width
141
152
  })
142
- .text((item) => [
143
- '',
144
- String(item.value.action.text ?? ''),
145
- getDisplayedShortcut(item.value),
146
- String(item.value.action.description ?? ''),
147
- String(item.value.action.menuPath ?? ''),
148
- getDisplayedToolbar(item.value),
149
- ])
150
- .data((item) => item.value)
153
+ .text((item) => {
154
+ const entry = getEntry(item)
155
+ return [
156
+ '',
157
+ String(entry.action.text ?? ''),
158
+ getDisplayedShortcut(entry),
159
+ String(entry.action.description ?? ''),
160
+ String(entry.action.menuPath ?? ''),
161
+ getDisplayedToolbar(entry),
162
+ ]
163
+ })
164
+ .data((item) => getEntry(item))
151
165
  .filter({
152
166
  keywords: this.keywords$,
153
167
  field: (listItem) => [
@@ -287,6 +301,21 @@ export const showSetupCustomActionsDialog = (actions: CustomAction[], options: s
287
301
  const selections = runDialog(actions, settings)
288
302
  if (!selections) return
289
303
 
304
+ if (settings.shortcutsPath) {
305
+ try {
306
+ const shortcuts = readFromFile<ActionAccelerator[]>(settings.shortcutsPath)
307
+ if (shortcuts) {
308
+ progress('Applying Keyboard Shortcuts', shortcuts, (shortcut) => {
309
+ if (shortcut.name && shortcut.shortcut) {
310
+ setActionShortcut(shortcut.name, shortcut.shortcut)
311
+ }
312
+ })
313
+ }
314
+ } catch (e) {
315
+ debug(`[Setup] Failed to apply shortcuts from ${settings.shortcutsPath}: ${e}`)
316
+ }
317
+ }
318
+
290
319
  debug(`[Setup] applying ${selections.filter(selection => selection.selected).length}/${selections.length} selected actions`)
291
320
 
292
321
  const removedToolbarNames = new CustomSet<string>()
@@ -80,7 +80,7 @@ export const deleteFile = (filePath: string): boolean => {
80
80
  try {
81
81
  var file = new DzFile(filePath)
82
82
  if (!file.exists()) return false
83
- file.remove(filePath)
83
+ file.remove(/*filePath*/)
84
84
  file.deleteLater()
85
85
  return true
86
86
  } catch (error) {
@@ -13,7 +13,7 @@ export const setDataItem = (listItem: DzListViewItem, data: any) => {
13
13
  }
14
14
 
15
15
  export const getDataItem = <T>(listItem: DzListViewItem): T | null => {
16
- return listItem?.getDataItem('data') ?? null
16
+ return (listItem?.getDataItem('data') ?? null) as T | null
17
17
  }
18
18
 
19
19
  export const filter = (listView: DzListView, filterOn: (viewItem: DzListViewItem) => string, keywords: string, options?: { selectOnFilter?: boolean, filters?: (viewItem: DzListViewItem) => boolean }) => {