dazscript-framework 0.3.2 → 1.0.2

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.
@@ -4,6 +4,7 @@ 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 CustomSet from '@dsf/lib/set'
7
8
  import { getMenu } from './menu-helper'
8
9
  import { getScriptPath } from './script-helper'
9
10
 
@@ -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
@@ -2,14 +2,15 @@ import { debug } from '@dsf/common/log'
2
2
  import { CustomAction } from '@dsf/core/custom-action'
3
3
  import { BasicDialog } from '@dsf/dialog/basic-dialog'
4
4
  import { PopupMenuBuilder, PopupMenuItem } from '@dsf/dialog/builders/popup-menu-builder'
5
+ import { findAction, findActionsForShortcut, getActionShortcut, isCustomAction, normalizeShortcut, setActionShortcut } from '@dsf/helpers/action-helper'
5
6
  import { addToToolbar, applyCustomActionTargets, cleanupEmptyToolbar, clearToolbar, getInstalledCustomActionState, removeCustomActionTargets } from '@dsf/helpers/custom-action-helper'
6
7
  import { checkAll, getDataItem } from '@dsf/helpers/list-view-helper'
7
- import { normalizeShortcut, setActionShortcut } from '@dsf/helpers/action-helper'
8
8
  import { progress } from '@dsf/helpers/progress-helper'
9
9
  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, saveToFile } from './file-helper'
13
14
 
14
15
  type InstallerEntry = {
15
16
  action: CustomAction
@@ -28,6 +29,45 @@ type InstallerEntry = {
28
29
  type SetupDialogOptions = {
29
30
  settingsPath: string
30
31
  bundleName?: string
32
+ shortcuts?: ActionAccelerator[]
33
+ shortcutsSourcePath?: string
34
+ shortcutBackupPath?: string
35
+ }
36
+
37
+ type ActionAccelerator = {
38
+ name?: string
39
+ action?: string
40
+ text?: string
41
+ label?: string
42
+ shortcut?: string
43
+ accelerator?: string
44
+ key?: string
45
+ }
46
+
47
+ type ShortcutEntry = {
48
+ selected: boolean
49
+ name: string
50
+ label: string
51
+ currentShortcut: string
52
+ newShortcut: string
53
+ conflictText: string
54
+ exists: boolean
55
+ isCustom: boolean
56
+ }
57
+
58
+ type ShortcutBackupEntry = {
59
+ name: string
60
+ shortcut: string
61
+ }
62
+
63
+ type ShortcutBackupFile = {
64
+ version: number
65
+ shortcuts: ShortcutBackupEntry[]
66
+ }
67
+
68
+ type SetupSelection = {
69
+ actions: InstallerEntry[]
70
+ shortcuts: ShortcutEntry[]
31
71
  }
32
72
 
33
73
  const OVERRIDE_MARKER = '[ovr]'
@@ -37,6 +77,15 @@ const toKey = (action: CustomAction): string => String(action.filePath ?? action
37
77
  const toTreeNode = (entry: InstallerEntry): TreeNode<InstallerEntry> =>
38
78
  new TreeNode(String(entry.action.text), toKey(entry.action), entry)
39
79
 
80
+ const toShortcutTreeNode = (entry: ShortcutEntry): TreeNode<ShortcutEntry> =>
81
+ new TreeNode(entry.label, entry.name, entry)
82
+
83
+ const getEntry = (item: TreeNode<InstallerEntry>): InstallerEntry =>
84
+ item.value as InstallerEntry
85
+
86
+ const getShortcutEntry = (item: TreeNode<ShortcutEntry>): ShortcutEntry =>
87
+ item.value as ShortcutEntry
88
+
40
89
  const getDisplayedToolbar = (entry: InstallerEntry): string => String(entry.action.toolbar ?? '')
41
90
 
42
91
  const getSetupDialogOptions = (options: string | SetupDialogOptions): SetupDialogOptions =>
@@ -44,6 +93,9 @@ const getSetupDialogOptions = (options: string | SetupDialogOptions): SetupDialo
44
93
  ? { settingsPath: options }
45
94
  : options
46
95
 
96
+ const getShortcutBackupPath = (options: SetupDialogOptions): string =>
97
+ `${App.getAppDataPath()}/${options.shortcutBackupPath ?? `${options.settingsPath}/keyboard-shortcuts-backup.json`}`
98
+
47
99
  const getDisplayedShortcut = (entry: InstallerEntry): string => {
48
100
  if (!entry.isShortcutOverridden) return entry.effectiveShortcut
49
101
  return entry.effectiveShortcut
@@ -85,15 +137,66 @@ const buildEntries = (actions: CustomAction[]): InstallerEntry[] => {
85
137
  })
86
138
  }
87
139
 
140
+ const normalizeAccelerator = (accelerator: ActionAccelerator): { name: string, label: string, shortcut: string } | null => {
141
+ const name = String(accelerator.name ?? accelerator.action ?? '').trim()
142
+ const shortcut = normalizeShortcut(String(accelerator.shortcut ?? accelerator.accelerator ?? accelerator.key ?? '').trim())
143
+ if (!name || !shortcut) return null
144
+
145
+ return {
146
+ name,
147
+ label: String(accelerator.text ?? accelerator.label ?? name),
148
+ shortcut
149
+ }
150
+ }
151
+
152
+ const getShortcutConflictText = (name: string, shortcut: string): string => {
153
+ if (!shortcut) return ''
154
+
155
+ const conflicts = findActionsForShortcut(shortcut)
156
+ .filter(action => action && action.name !== name)
157
+
158
+ return conflicts.map(action => String(action.text || action.name)).join(', ')
159
+ }
160
+
161
+ const buildShortcutEntries = (accelerators: ActionAccelerator[] = []): ShortcutEntry[] => {
162
+ return accelerators
163
+ .map(normalizeAccelerator)
164
+ .filter((accelerator): accelerator is { name: string, label: string, shortcut: string } => accelerator !== null)
165
+ .map((accelerator) => {
166
+ const action = findAction(accelerator.name)
167
+ const currentShortcut = getActionShortcut(accelerator.name)
168
+ const newShortcut = normalizeShortcut(accelerator.shortcut)
169
+
170
+ return {
171
+ selected: true,
172
+ name: accelerator.name,
173
+ label: action?.text ? String(action.text) : accelerator.label,
174
+ currentShortcut,
175
+ newShortcut,
176
+ conflictText: getShortcutConflictText(accelerator.name, newShortcut),
177
+ exists: action !== null,
178
+ isCustom: action ? isCustomAction(action) : false
179
+ }
180
+ })
181
+ }
182
+
88
183
  class InstallerSelectionDialog extends BasicDialog {
89
184
  private readonly keywords$ = new Observable('')
90
185
  private readonly items$: Observable<TreeNode<InstallerEntry>[]>
186
+ private readonly shortcutKeywords$ = new Observable('')
187
+ private readonly shortcutItems$: Observable<TreeNode<ShortcutEntry>[]>
91
188
  private readonly refreshListEvent$ = new Observable<void>()
92
189
  private listView: DzListView | null = null
190
+ private shortcutListView: DzListView | null = null
93
191
 
94
- constructor(private readonly entries: InstallerEntry[], bundleName?: string) {
192
+ constructor(
193
+ private readonly entries: InstallerEntry[],
194
+ private readonly shortcutEntries: ShortcutEntry[],
195
+ bundleName?: string
196
+ ) {
95
197
  super(bundleName ? `${bundleName} Setup` : 'Setup Scripts', 'dsfSetupScripts')
96
198
  this.items$ = new Observable(entries.map(toTreeNode))
199
+ this.shortcutItems$ = new Observable(shortcutEntries.map(toShortcutTreeNode))
97
200
  }
98
201
 
99
202
  protected build(): void {
@@ -101,7 +204,28 @@ class InstallerSelectionDialog extends BasicDialog {
101
204
  this.dialog.setAcceptButtonText('Apply')
102
205
  this.dialog.setCancelButtonText('Cancel')
103
206
 
207
+ if (this.shortcutEntries.length > 0) {
208
+ const add = this.add
209
+ add.tab('Scripts').build(() => this.buildScriptsTab())
210
+ add.tab('Keyboard Shortcuts').build(() => this.buildShortcutsTab())
211
+ return
212
+ }
213
+
214
+ this.buildScriptsTab()
215
+ }
216
+
217
+ getSelections(): SetupSelection {
218
+ this.syncSelectionsFromListView()
219
+ this.syncShortcutSelectionsFromListView()
220
+ return {
221
+ actions: this.entries,
222
+ shortcuts: this.shortcutEntries
223
+ }
224
+ }
225
+
226
+ private buildScriptsTab(): void {
104
227
  const add = this.add
228
+
105
229
  add.label('Choose which scripts to install, then use the columns to review their shortcut, menu, and toolbar targets.')
106
230
  .wordWrap()
107
231
  .build()
@@ -123,8 +247,9 @@ class InstallerSelectionDialog extends BasicDialog {
123
247
  .sortOnBuild(true)
124
248
  .refresh(this.refreshListEvent$)
125
249
  .row((item, parent, id) => {
250
+ const entry = getEntry(item)
126
251
  const listItem = new DzCheckListItem(parent, DzCheckListItem.CheckBox, id)
127
- listItem.on = item.value.selected
252
+ listItem.on = entry.selected
128
253
  listItem.setText(0, '')
129
254
  return listItem
130
255
  })
@@ -139,15 +264,18 @@ class InstallerSelectionDialog extends BasicDialog {
139
264
  if (index === 5) return Math.max(width * 1.2, 140)
140
265
  return width
141
266
  })
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)
267
+ .text((item) => {
268
+ const entry = getEntry(item)
269
+ return [
270
+ '',
271
+ String(entry.action.text ?? ''),
272
+ getDisplayedShortcut(entry),
273
+ String(entry.action.description ?? ''),
274
+ String(entry.action.menuPath ?? ''),
275
+ getDisplayedToolbar(entry),
276
+ ]
277
+ })
278
+ .data((item) => getEntry(item))
151
279
  .filter({
152
280
  keywords: this.keywords$,
153
281
  field: (listItem) => [
@@ -165,9 +293,71 @@ class InstallerSelectionDialog extends BasicDialog {
165
293
  })
166
294
  }
167
295
 
168
- getSelections(): InstallerEntry[] {
169
- this.syncSelectionsFromListView()
170
- return this.entries
296
+ private buildShortcutsTab(): void {
297
+ const add = this.add
298
+
299
+ add.label('Choose which keyboard shortcuts to apply. Current and new shortcut values are shown side by side.')
300
+ .wordWrap()
301
+ .build()
302
+ add.group('Search').horizontal().build(() => {
303
+ add.edit()
304
+ .value(this.shortcutKeywords$)
305
+ .placeholder('Search shortcuts...')
306
+ .toolTip('Search by action name, current shortcut, new shortcut, status, or conflicts.')
307
+ add.button('Select All').clicked(() => this.setVisibleShortcutSelections(true))
308
+ add.button('Deselect All').clicked(() => this.setVisibleShortcutSelections(false))
309
+ })
310
+
311
+ add.group('Keyboard Shortcuts')
312
+ .style({ flat: true })
313
+ .build(() => {
314
+ add.list.view<ShortcutEntry, ShortcutEntry>()
315
+ .sorting(true)
316
+ .sortOnBuild(true)
317
+ .row((item, parent, id) => {
318
+ const entry = getShortcutEntry(item)
319
+ const listItem = new DzCheckListItem(parent, DzCheckListItem.CheckBox, id)
320
+ listItem.on = entry.selected
321
+ listItem.setText(0, '')
322
+ return listItem
323
+ })
324
+ .items(this.shortcutItems$)
325
+ .columns(['Apply', 'Action', 'Current', 'New', 'Status', 'Conflicts'], (index, width) => {
326
+ if (index === 0) return Math.max(width, 70)
327
+ if (index === 1) return Math.max(width * 2, 260)
328
+ if (index === 2) return Math.max(width, 130)
329
+ if (index === 3) return Math.max(width, 130)
330
+ if (index === 4) return Math.max(width, 120)
331
+ if (index === 5) return Math.max(width * 2, 260)
332
+ return width
333
+ })
334
+ .text((item) => {
335
+ const entry = getShortcutEntry(item)
336
+ return [
337
+ '',
338
+ entry.label,
339
+ entry.currentShortcut || '(none)',
340
+ entry.newShortcut || '(none)',
341
+ entry.exists ? (entry.isCustom ? 'Custom Action' : 'DAZ Action') : 'Not currently found',
342
+ entry.conflictText || ''
343
+ ]
344
+ })
345
+ .data((item) => getShortcutEntry(item))
346
+ .filter({
347
+ keywords: this.shortcutKeywords$,
348
+ field: (listItem) => [
349
+ listItem.text(1),
350
+ listItem.text(2),
351
+ listItem.text(3),
352
+ listItem.text(4),
353
+ listItem.text(5),
354
+ ].join(' ')
355
+ })
356
+ .build((listView) => {
357
+ this.shortcutListView = listView
358
+ listView.allColumnsShowFocus = true
359
+ })
360
+ })
171
361
  }
172
362
 
173
363
  private buildContextMenu(listView: DzListView, listItem: DzListViewItem | null): DzPopupMenu {
@@ -274,24 +464,106 @@ class InstallerSelectionDialog extends BasicDialog {
274
464
  }
275
465
  })
276
466
  }
467
+
468
+ private setVisibleShortcutSelections(onOff: boolean) {
469
+ if (!this.shortcutListView) return
470
+ this.checkShortcutVisible(this.shortcutListView, onOff)
471
+ }
472
+
473
+ private checkShortcutVisible(listView: DzListView, onOff: boolean) {
474
+ if (!this.shortcutKeywords$.value?.trim()) {
475
+ checkAll(listView, onOff)
476
+ }
477
+
478
+ listView.getItems(DzListView.All).forEach((item) => {
479
+ if (!item.visible || !(item as any).inherits('DzCheckListItem')) return
480
+
481
+ const checkbox = item as DzCheckListItem
482
+ checkbox.on = onOff
483
+
484
+ const data = getDataItem<ShortcutEntry>(item)
485
+ if (!data) return
486
+ data.selected = onOff
487
+ })
488
+ }
489
+
490
+ private syncShortcutSelectionsFromListView() {
491
+ if (!this.shortcutListView) return
492
+
493
+ const selectionByName: Record<string, boolean> = {}
494
+
495
+ this.shortcutListView.getItems(DzListView.All).forEach((item) => {
496
+ if (!(item as any).inherits('DzCheckListItem')) return
497
+
498
+ const data = getDataItem<ShortcutEntry>(item)
499
+ if (!data) return
500
+
501
+ const checkbox = item as DzCheckListItem
502
+ const selected = !(checkbox.state === 0 && !checkbox.on)
503
+ selectionByName[data.name] = selected
504
+ })
505
+
506
+ this.shortcutEntries.forEach((entry) => {
507
+ if (Object.prototype.hasOwnProperty.call(selectionByName, entry.name)) {
508
+ entry.selected = selectionByName[entry.name]
509
+ }
510
+ })
511
+ }
277
512
  }
278
513
 
279
- const runDialog = (actions: CustomAction[], options: SetupDialogOptions): InstallerEntry[] | null => {
514
+ const runDialog = (actions: CustomAction[], options: SetupDialogOptions): SetupSelection | null => {
280
515
  const entries = buildEntries(actions)
281
- const dialog = new InstallerSelectionDialog(entries, options.bundleName)
516
+ const shortcutEntries = buildShortcutEntries(options.shortcuts)
517
+ const dialog = new InstallerSelectionDialog(entries, shortcutEntries, options.bundleName)
282
518
  return dialog.ok() ? dialog.getSelections() : null
283
519
  }
284
520
 
521
+ const readShortcutBackup = (backupPath: string): ShortcutBackupFile => {
522
+ const backup = readFromFile<ShortcutBackupFile>(backupPath)
523
+ return backup ?? { version: 1, shortcuts: [] }
524
+ }
525
+
526
+ const saveShortcutBackup = (backupPath: string, backup: ShortcutBackupFile): void => {
527
+ saveToFile(backupPath, JSON.stringify(backup, null, 2))
528
+ }
529
+
530
+ const backupCurrentShortcut = (entry: ShortcutEntry, backupPath: string): void => {
531
+ const action = findAction(entry.name)
532
+ if (!action || isCustomAction(action)) return
533
+
534
+ const backup = readShortcutBackup(backupPath)
535
+ const alreadyBackedUp = backup.shortcuts.some(shortcut => shortcut.name === entry.name)
536
+ if (alreadyBackedUp) return
537
+
538
+ backup.shortcuts.push({
539
+ name: entry.name,
540
+ shortcut: getActionShortcut(entry.name)
541
+ })
542
+ saveShortcutBackup(backupPath, backup)
543
+ }
544
+
545
+ const applyKeyboardShortcuts = (shortcuts: ShortcutEntry[], options: SetupDialogOptions): void => {
546
+ const selected = shortcuts.filter(shortcut => shortcut.selected)
547
+ if (selected.length === 0) return
548
+
549
+ const backupPath = getShortcutBackupPath(options)
550
+
551
+ progress('Applying Keyboard Shortcuts', selected, (shortcut) => {
552
+ backupCurrentShortcut(shortcut, backupPath)
553
+ setActionShortcut(shortcut.name, shortcut.newShortcut)
554
+ })
555
+ }
556
+
285
557
  export const showSetupCustomActionsDialog = (actions: CustomAction[], options: string | SetupDialogOptions) => {
286
558
  const settings = getSetupDialogOptions(options)
287
559
  const selections = runDialog(actions, settings)
288
560
  if (!selections) return
289
561
 
290
- debug(`[Setup] applying ${selections.filter(selection => selection.selected).length}/${selections.length} selected actions`)
562
+ debug(`[Setup] applying ${selections.actions.filter(selection => selection.selected).length}/${selections.actions.length} selected actions`)
291
563
 
292
564
  const removedToolbarNames = new CustomSet<string>()
293
565
 
294
- progress('Setting Up Scripts', selections, (selection) => {
566
+ progress('Setting Up Scripts', selections.actions, (selection) => {
295
567
  debug(`[Setup] ${selection.selected ? 'install' : 'remove'} "${selection.action.text}"`)
296
568
 
297
569
  if (selection.selected) {
@@ -318,7 +590,7 @@ export const showSetupCustomActionsDialog = (actions: CustomAction[], options: s
318
590
  removedToolbarNames.forEach((toolbarName) => {
319
591
  clearToolbar(toolbarName)
320
592
 
321
- const stillSelected = selections.filter(s => s.selected && s.supportsToolbar && s.action.toolbar === toolbarName)
593
+ const stillSelected = selections.actions.filter(s => s.selected && s.supportsToolbar && s.action.toolbar === toolbarName)
322
594
  stillSelected.forEach((s) => addToToolbar({
323
595
  ...s.action,
324
596
  shortcut: s.effectiveShortcut
@@ -329,4 +601,18 @@ export const showSetupCustomActionsDialog = (actions: CustomAction[], options: s
329
601
  cleanupEmptyToolbar(toolbarName)
330
602
  }
331
603
  })
604
+
605
+ applyKeyboardShortcuts(selections.shortcuts, settings)
606
+ }
607
+
608
+ export const restoreSetupKeyboardShortcuts = (options: string | SetupDialogOptions) => {
609
+ const settings = getSetupDialogOptions(options)
610
+ const backupPath = getShortcutBackupPath(settings)
611
+ const backup = readFromFile<ShortcutBackupFile>(backupPath)
612
+ if (!backup || !backup.shortcuts || backup.shortcuts.length === 0) return
613
+
614
+ progress('Restoring Keyboard Shortcuts', backup.shortcuts, (shortcut) => {
615
+ if (!shortcut.name) return
616
+ setActionShortcut(shortcut.name, shortcut.shortcut ?? '')
617
+ })
332
618
  }