dazscript-framework 1.0.1 → 1.0.3

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 CHANGED
@@ -241,6 +241,7 @@ This scans all `.dsa.ts` entry files, reads each top-level `action(...)` call, a
241
241
  The generated setup dialog:
242
242
 
243
243
  - Shows an install checkbox per action with columns for Action, Shortcut, Description, Menu, and Toolbar
244
+ - Adds a `Keyboard Shortcuts` tab when a project defines shortcut JSON
244
245
  - Includes a search box that filters across all columns
245
246
  - Supports Select All / Deselect All on the visible rows
246
247
  - Lets the user right-click to set or reset a shortcut (overrides shown with `[ovr]`)
@@ -251,9 +252,44 @@ Applying the dialog:
251
252
  - Checked rows are installed or updated
252
253
  - Unchecked rows are removed from their menu and toolbar targets
253
254
  - Affected toolbars are rebuilt; empty framework-created toolbars are removed
255
+ - Selected keyboard shortcut rows are applied after actions are installed
254
256
 
255
257
  This replaces the older `Install.dsa.ts` / `Uninstall.dsa.ts` pattern. The installer generator removes those legacy files if they exist.
256
258
 
259
+ ### Setup Keyboard Shortcuts
260
+
261
+ Projects can define keyboard shortcuts for both framework custom actions and built-in Daz Studio actions. The installer generator looks for shortcut JSON in this order:
262
+
263
+ - `keyboardShortcutsPath`, `shortcutsPath`, or `actionAcceleratorsPath` in `dazscript.config.ts`
264
+ - `src/keyboard-shortcuts.json`
265
+ - `src/action-accelerators.json`
266
+ - `keyboard-shortcuts.json`
267
+ - `action-accelerators.json`
268
+
269
+ The JSON can be an array or an object containing `actions`, `shortcuts`, or `accelerators`. Each entry can use the Action Accelerator Finder style fields:
270
+
271
+ ```json
272
+ [
273
+ {
274
+ "name": "DzRenderAction",
275
+ "text": "Render",
276
+ "shortcut": "CTRL+R"
277
+ }
278
+ ]
279
+ ```
280
+
281
+ Accepted shortcut fields are `shortcut`, `accelerator`, or `key`. Accepted action-name fields are `name` or `action`.
282
+
283
+ At build time the JSON is embedded into generated `Setup.dsa.ts`; Daz Studio does not need to read the original JSON file at setup time. During setup, the `Keyboard Shortcuts` tab shows the action label, current shortcut, new shortcut, action type, and conflicts. The user chooses which shortcut rows to apply.
284
+
285
+ Before changing a non-custom Daz Studio action shortcut, setup writes the original value to:
286
+
287
+ ```text
288
+ App.getAppDataPath()/<appDataPath>/Installer/keyboard-shortcuts-backup.json
289
+ ```
290
+
291
+ Shortcut setup is part of the generated setup dialog. The generator does not create a separate shortcut-only uninstall script.
292
+
257
293
  ---
258
294
 
259
295
  ### Action-Level Bundles
@@ -21,14 +21,11 @@ function stringOrDefault(str, defaultValue) {
21
21
  return str !== undefined && str !== null && str !== '' ? str : defaultValue;
22
22
  }
23
23
 
24
- function generateInstallerTemplate(data, settingsPath, bundleName) {
24
+ function generateInstallerTemplate(data, options) {
25
25
  return `
26
26
  import { showSetupCustomActionsDialog as setup } from '@dsf/helpers/custom-action-installer-helper';
27
27
 
28
- setup(${data}, ${JSON.stringify({
29
- settingsPath,
30
- bundleName,
31
- })});
28
+ setup(${data}, ${JSON.stringify(options)});
32
29
  `;
33
30
  }
34
31
 
@@ -151,7 +148,7 @@ function findActionEntryFiles(workdir, options) {
151
148
  });
152
149
  }
153
150
 
154
- function processScript(filePath, container, defaultMenuPath, settingsPath, bundleName) {
151
+ function processScript(filePath, container, defaultMenuPath, setupOptions) {
155
152
  const fileInfo = path.parse(filePath);
156
153
  const content = fs.readFileSync(filePath, 'utf-8').toString();
157
154
  const actionCall = findTopLevelActionCall(content, filePath);
@@ -213,8 +210,7 @@ function processScript(filePath, container, defaultMenuPath, settingsPath, bundl
213
210
 
214
211
  let bundleScriptContent = generateInstallerTemplate(
215
212
  JSON.stringify(container.scripts, null, 4),
216
- settingsPath,
217
- bundleName
213
+ setupOptions
218
214
  );
219
215
  let bundleScriptFilePath = path.join(
220
216
  path.parse(filePath).dir,
@@ -224,13 +220,66 @@ function processScript(filePath, container, defaultMenuPath, settingsPath, bundl
224
220
  }
225
221
  }
226
222
 
227
- function processScripts(paths, container, defaultMenuPath, settingsPath, bundleName) {
223
+ function processScripts(paths, container, defaultMenuPath, setupOptions) {
228
224
  paths.forEach((filePath) => {
229
225
  console.log(`Processing ${filePath}`);
230
- processScript(filePath, container, defaultMenuPath, settingsPath, bundleName);
226
+ processScript(filePath, container, defaultMenuPath, setupOptions);
231
227
  });
232
228
  }
233
229
 
230
+ function toPosix(filePath) {
231
+ return filePath.replace(/\\/g, '/');
232
+ }
233
+
234
+ function resolveShortcutFile(workdir, config) {
235
+ const configuredPath =
236
+ config.keyboardShortcutsPath ||
237
+ config.shortcutsPath ||
238
+ config.actionAcceleratorsPath;
239
+
240
+ const candidates = [];
241
+ if (configuredPath) {
242
+ candidates.push(path.resolve(workdir, configuredPath));
243
+ }
244
+
245
+ candidates.push(
246
+ path.join(workdir, 'src', 'keyboard-shortcuts.json'),
247
+ path.join(workdir, 'src', 'action-accelerators.json'),
248
+ path.join(workdir, 'keyboard-shortcuts.json'),
249
+ path.join(workdir, 'action-accelerators.json')
250
+ );
251
+
252
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null;
253
+ }
254
+
255
+ function normalizeShortcutData(raw) {
256
+ if (!raw) return [];
257
+ if (Array.isArray(raw)) return raw;
258
+ if (Array.isArray(raw.actions)) return raw.actions;
259
+ if (Array.isArray(raw.shortcuts)) return raw.shortcuts;
260
+ if (Array.isArray(raw.accelerators)) return raw.accelerators;
261
+ return [];
262
+ }
263
+
264
+ function loadShortcutData(workdir, config) {
265
+ const shortcutFile = resolveShortcutFile(workdir, config);
266
+ if (!shortcutFile) {
267
+ return { shortcuts: undefined, sourcePath: undefined };
268
+ }
269
+
270
+ const raw = JSON.parse(fs.readFileSync(shortcutFile, 'utf8'));
271
+ const shortcuts = normalizeShortcutData(raw);
272
+
273
+ if (shortcuts.length === 0) {
274
+ console.warn(`[dazscript] Keyboard shortcut file has no shortcut entries: ${shortcutFile}`);
275
+ }
276
+
277
+ return {
278
+ shortcuts,
279
+ sourcePath: toPosix(path.relative(workdir, shortcutFile)),
280
+ };
281
+ }
282
+
234
283
  function generateInstallerFiles(workdir, options) {
235
284
  const defaultMenuPath = options.defaultMenuPath.endsWith('/')
236
285
  ? options.defaultMenuPath
@@ -241,10 +290,20 @@ function generateInstallerFiles(workdir, options) {
241
290
  const bundleName = typeof config.bundleName === 'string' && config.bundleName.trim()
242
291
  ? config.bundleName.trim()
243
292
  : undefined;
293
+ const shortcutData = loadShortcutData(workdir, config);
294
+ const setupOptions = {
295
+ settingsPath,
296
+ bundleName,
297
+ shortcutBackupPath: `${appDataPath}/Installer/keyboard-shortcuts-backup.json`,
298
+ };
299
+ if (shortcutData.shortcuts && shortcutData.shortcuts.length > 0) {
300
+ setupOptions.shortcuts = shortcutData.shortcuts;
301
+ setupOptions.shortcutsSourcePath = shortcutData.sourcePath;
302
+ }
244
303
  const container = { scripts: [] };
245
304
  const matches = findActionEntryFiles(workdir, options);
246
305
 
247
- processScripts(matches, container, defaultMenuPath, settingsPath, bundleName);
306
+ processScripts(matches, container, defaultMenuPath, setupOptions);
248
307
 
249
308
  container.scripts = container.scripts.sort((a, b) => {
250
309
  const aKey = a.menuPath + a.filePath;
@@ -254,15 +313,17 @@ function generateInstallerFiles(workdir, options) {
254
313
 
255
314
  const installerScriptContent = generateInstallerTemplate(
256
315
  JSON.stringify(container.scripts, null, 4),
257
- settingsPath,
258
- bundleName
316
+ setupOptions
259
317
  );
260
318
  fs.writeFileSync(path.join(workdir, 'src', 'Setup.dsa.ts'), installerScriptContent);
261
319
 
262
320
  const installPath = path.join(workdir, 'src', 'Install.dsa.ts');
263
321
  const uninstallPath = path.join(workdir, 'src', 'Uninstall.dsa.ts');
264
322
  if (fs.existsSync(installPath)) fs.unlinkSync(installPath);
265
- if (fs.existsSync(uninstallPath)) fs.unlinkSync(uninstallPath);
323
+
324
+ if (fs.existsSync(uninstallPath)) {
325
+ fs.unlinkSync(uninstallPath);
326
+ }
266
327
  }
267
328
 
268
329
  if (require.main === module) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dazscript-framework",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "author": "Freddy Diaz",
5
5
  "license": "MPL-2.0",
6
6
  "description": "",
package/src/Setup.dsa.ts CHANGED
@@ -44,4 +44,4 @@ setup([
44
44
  "menuPath": "/DazScriptFramework/examples",
45
45
  "description": "06-showcase-dialog"
46
46
  }
47
- ], {"settingsPath":"DazScriptFramework/examples/Installer","bundleName":"Examples"});
47
+ ], {"settingsPath":"DazScriptFramework/examples/Installer","bundleName":"Examples","shortcutBackupPath":"DazScriptFramework/examples/Installer/keyboard-shortcuts-backup.json"});
@@ -78,6 +78,10 @@ export type CustomActionTargets = {
78
78
  toolbar: boolean
79
79
  }
80
80
 
81
+ type ApplyCustomActionOptions = {
82
+ deferToolbar?: boolean
83
+ }
84
+
81
85
  type CustomActionCandidate = {
82
86
  name: string
83
87
  text: string
@@ -377,13 +381,18 @@ export const getInstalledCustomActionState = (action: CustomAction, scriptsPath:
377
381
  }
378
382
  }
379
383
 
380
- export const applyCustomActionTargets = (action: CustomAction, targets: CustomActionTargets, scriptsPath: string = getScriptPath()) => {
384
+ export const applyCustomActionTargets = (
385
+ action: CustomAction,
386
+ targets: CustomActionTargets,
387
+ scriptsPath: string = getScriptPath(),
388
+ options: ApplyCustomActionOptions = {}
389
+ ): CustomAction | null => {
381
390
  const resolvedAction = resolveActionPaths(action, scriptsPath)
382
391
  const shouldInstall = targets.menu || targets.toolbar
383
392
 
384
393
  if (!shouldInstall) {
385
394
  removeCustomActionTargets(action, { menu: true, toolbar: true }, scriptsPath)
386
- return
395
+ return null
387
396
  }
388
397
 
389
398
  const customAction = createOrUpdateCustomAction(action, resolvedAction)
@@ -392,9 +401,11 @@ export const applyCustomActionTargets = (action: CustomAction, targets: CustomAc
392
401
  addToMenu(customAction)
393
402
  }
394
403
 
395
- if (targets.toolbar && action.toolbar) {
404
+ if (targets.toolbar && action.toolbar && !options.deferToolbar) {
396
405
  addToToolbar(customAction)
397
406
  }
407
+
408
+ return customAction
398
409
  }
399
410
 
400
411
  export const removeCustomActionTargets = (action: CustomAction, targets: CustomActionTargets, scriptsPath: string = getScriptPath()) => {
@@ -2,15 +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 } from './file-helper'
13
+ import { readFromFile, saveToFile } from './file-helper'
14
14
 
15
15
  type InstallerEntry = {
16
16
  action: CustomAction
@@ -29,14 +29,47 @@ type InstallerEntry = {
29
29
  type SetupDialogOptions = {
30
30
  settingsPath: string
31
31
  bundleName?: string
32
- shortcutsPath?: string
32
+ shortcuts?: ActionAccelerator[]
33
+ shortcutsSourcePath?: string
34
+ shortcutBackupPath?: string
33
35
  }
34
36
 
35
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 = {
36
59
  name: string
37
60
  shortcut: string
38
61
  }
39
62
 
63
+ type ShortcutBackupFile = {
64
+ version: number
65
+ shortcuts: ShortcutBackupEntry[]
66
+ }
67
+
68
+ type SetupSelection = {
69
+ actions: InstallerEntry[]
70
+ shortcuts: ShortcutEntry[]
71
+ }
72
+
40
73
  const OVERRIDE_MARKER = '[ovr]'
41
74
 
42
75
  const toKey = (action: CustomAction): string => String(action.filePath ?? action.text ?? '')
@@ -44,9 +77,15 @@ const toKey = (action: CustomAction): string => String(action.filePath ?? action
44
77
  const toTreeNode = (entry: InstallerEntry): TreeNode<InstallerEntry> =>
45
78
  new TreeNode(String(entry.action.text), toKey(entry.action), entry)
46
79
 
80
+ const toShortcutTreeNode = (entry: ShortcutEntry): TreeNode<ShortcutEntry> =>
81
+ new TreeNode(entry.label, entry.name, entry)
82
+
47
83
  const getEntry = (item: TreeNode<InstallerEntry>): InstallerEntry =>
48
84
  item.value as InstallerEntry
49
85
 
86
+ const getShortcutEntry = (item: TreeNode<ShortcutEntry>): ShortcutEntry =>
87
+ item.value as ShortcutEntry
88
+
50
89
  const getDisplayedToolbar = (entry: InstallerEntry): string => String(entry.action.toolbar ?? '')
51
90
 
52
91
  const getSetupDialogOptions = (options: string | SetupDialogOptions): SetupDialogOptions =>
@@ -54,6 +93,9 @@ const getSetupDialogOptions = (options: string | SetupDialogOptions): SetupDialo
54
93
  ? { settingsPath: options }
55
94
  : options
56
95
 
96
+ const getShortcutBackupPath = (options: SetupDialogOptions): string =>
97
+ `${App.getAppDataPath()}/${options.shortcutBackupPath ?? `${options.settingsPath}/keyboard-shortcuts-backup.json`}`
98
+
57
99
  const getDisplayedShortcut = (entry: InstallerEntry): string => {
58
100
  if (!entry.isShortcutOverridden) return entry.effectiveShortcut
59
101
  return entry.effectiveShortcut
@@ -95,15 +137,66 @@ const buildEntries = (actions: CustomAction[]): InstallerEntry[] => {
95
137
  })
96
138
  }
97
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
+
98
183
  class InstallerSelectionDialog extends BasicDialog {
99
184
  private readonly keywords$ = new Observable('')
100
185
  private readonly items$: Observable<TreeNode<InstallerEntry>[]>
186
+ private readonly shortcutKeywords$ = new Observable('')
187
+ private readonly shortcutItems$: Observable<TreeNode<ShortcutEntry>[]>
101
188
  private readonly refreshListEvent$ = new Observable<void>()
102
189
  private listView: DzListView | null = null
190
+ private shortcutListView: DzListView | null = null
103
191
 
104
- constructor(private readonly entries: InstallerEntry[], bundleName?: string) {
192
+ constructor(
193
+ private readonly entries: InstallerEntry[],
194
+ private readonly shortcutEntries: ShortcutEntry[],
195
+ bundleName?: string
196
+ ) {
105
197
  super(bundleName ? `${bundleName} Setup` : 'Setup Scripts', 'dsfSetupScripts')
106
198
  this.items$ = new Observable(entries.map(toTreeNode))
199
+ this.shortcutItems$ = new Observable(shortcutEntries.map(toShortcutTreeNode))
107
200
  }
108
201
 
109
202
  protected build(): void {
@@ -111,7 +204,28 @@ class InstallerSelectionDialog extends BasicDialog {
111
204
  this.dialog.setAcceptButtonText('Apply')
112
205
  this.dialog.setCancelButtonText('Cancel')
113
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 {
114
227
  const add = this.add
228
+
115
229
  add.label('Choose which scripts to install, then use the columns to review their shortcut, menu, and toolbar targets.')
116
230
  .wordWrap()
117
231
  .build()
@@ -179,9 +293,71 @@ class InstallerSelectionDialog extends BasicDialog {
179
293
  })
180
294
  }
181
295
 
182
- getSelections(): InstallerEntry[] {
183
- this.syncSelectionsFromListView()
184
- 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
+ })
185
361
  }
186
362
 
187
363
  private buildContextMenu(listView: DzListView, listItem: DzListViewItem | null): DzPopupMenu {
@@ -288,54 +464,128 @@ class InstallerSelectionDialog extends BasicDialog {
288
464
  }
289
465
  })
290
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
+ }
291
512
  }
292
513
 
293
- const runDialog = (actions: CustomAction[], options: SetupDialogOptions): InstallerEntry[] | null => {
514
+ const runDialog = (actions: CustomAction[], options: SetupDialogOptions): SetupSelection | null => {
294
515
  const entries = buildEntries(actions)
295
- const dialog = new InstallerSelectionDialog(entries, options.bundleName)
516
+ const shortcutEntries = buildShortcutEntries(options.shortcuts)
517
+ const dialog = new InstallerSelectionDialog(entries, shortcutEntries, options.bundleName)
296
518
  return dialog.ok() ? dialog.getSelections() : null
297
519
  }
298
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
+
299
557
  export const showSetupCustomActionsDialog = (actions: CustomAction[], options: string | SetupDialogOptions) => {
300
558
  const settings = getSetupDialogOptions(options)
301
559
  const selections = runDialog(actions, settings)
302
560
  if (!selections) return
303
561
 
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
-
319
- 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`)
320
563
 
321
- const removedToolbarNames = new CustomSet<string>()
564
+ const touchedToolbarNames = new CustomSet<string>()
565
+ const selectedToolbarActions: CustomAction[] = []
322
566
 
323
- progress('Setting Up Scripts', selections, (selection) => {
567
+ progress('Setting Up Scripts', selections.actions, (selection) => {
324
568
  debug(`[Setup] ${selection.selected ? 'install' : 'remove'} "${selection.action.text}"`)
325
569
 
570
+ if (selection.supportsToolbar && selection.action.toolbar) {
571
+ touchedToolbarNames.add(selection.action.toolbar)
572
+ }
573
+
326
574
  if (selection.selected) {
327
- applyCustomActionTargets({
575
+ const customAction = applyCustomActionTargets({
328
576
  ...selection.action,
329
577
  shortcut: selection.effectiveShortcut
330
578
  }, {
331
579
  menu: selection.supportsMenu,
332
580
  toolbar: selection.supportsToolbar
581
+ }, undefined, {
582
+ deferToolbar: true
333
583
  })
334
- return
335
- }
336
584
 
337
- if (selection.supportsToolbar && selection.action.toolbar) {
338
- removedToolbarNames.add(selection.action.toolbar)
585
+ if (customAction && selection.supportsToolbar && selection.action.toolbar) {
586
+ selectedToolbarActions.push(customAction)
587
+ }
588
+ return
339
589
  }
340
590
 
341
591
  removeCustomActionTargets(selection.action, {
@@ -344,18 +594,29 @@ export const showSetupCustomActionsDialog = (actions: CustomAction[], options: s
344
594
  })
345
595
  })
346
596
 
347
- removedToolbarNames.forEach((toolbarName) => {
597
+ touchedToolbarNames.forEach((toolbarName) => {
348
598
  clearToolbar(toolbarName)
349
599
 
350
- const stillSelected = selections.filter(s => s.selected && s.supportsToolbar && s.action.toolbar === toolbarName)
351
- stillSelected.forEach((s) => addToToolbar({
352
- ...s.action,
353
- shortcut: s.effectiveShortcut
354
- }))
600
+ const stillSelected = selectedToolbarActions.filter(action => action.toolbar === toolbarName)
601
+ stillSelected.forEach((action) => addToToolbar(action))
355
602
  debug(`[Setup] Toolbar ${toolbarName} rebuilt with ${stillSelected.length} remaining actions`)
356
603
 
357
604
  if (stillSelected.length === 0) {
358
605
  cleanupEmptyToolbar(toolbarName)
359
606
  }
360
607
  })
608
+
609
+ applyKeyboardShortcuts(selections.shortcuts, settings)
610
+ }
611
+
612
+ export const restoreSetupKeyboardShortcuts = (options: string | SetupDialogOptions) => {
613
+ const settings = getSetupDialogOptions(options)
614
+ const backupPath = getShortcutBackupPath(settings)
615
+ const backup = readFromFile<ShortcutBackupFile>(backupPath)
616
+ if (!backup || !backup.shortcuts || backup.shortcuts.length === 0) return
617
+
618
+ progress('Restoring Keyboard Shortcuts', backup.shortcuts, (shortcut) => {
619
+ if (!shortcut.name) return
620
+ setActionShortcut(shortcut.name, shortcut.shortcut ?? '')
621
+ })
361
622
  }
package/webpack.config.js CHANGED
@@ -75,6 +75,12 @@ module.exports = (env, argv) => {
75
75
  loader: 'ts-loader',
76
76
  options: {
77
77
  allowTsInNodeModules: true,
78
+ configFile: path.resolve(projectRoot, 'tsconfig.json'),
79
+ compilerOptions: {
80
+ strict: false,
81
+ strictNullChecks: false,
82
+ strictPropertyInitialization: false,
83
+ },
78
84
  ignoreDiagnostics: [5107],
79
85
  },
80
86
  },