dazscript-framework 0.2.5 → 0.3.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.
@@ -0,0 +1,332 @@
1
+ import { debug } from '@dsf/common/log'
2
+ import { CustomAction } from '@dsf/core/custom-action'
3
+ import { BasicDialog } from '@dsf/dialog/basic-dialog'
4
+ import { PopupMenuBuilder, PopupMenuItem } from '@dsf/dialog/builders/popup-menu-builder'
5
+ import { addToToolbar, applyCustomActionTargets, cleanupEmptyToolbar, clearToolbar, getInstalledCustomActionState, removeCustomActionTargets } from '@dsf/helpers/custom-action-helper'
6
+ import { checkAll, getDataItem } from '@dsf/helpers/list-view-helper'
7
+ import { normalizeShortcut, setActionShortcut } from '@dsf/helpers/action-helper'
8
+ import { progress } from '@dsf/helpers/progress-helper'
9
+ import { Observable } from '@dsf/lib/observable'
10
+ import CustomSet from '@dsf/lib/set'
11
+ import { TreeNode } from '@dsf/lib/tree-node'
12
+ import { promptKeyboardShortcut } from '@dsf/shared/set-keyboard-shortcut'
13
+
14
+ type InstallerEntry = {
15
+ action: CustomAction
16
+ supportsMenu: boolean
17
+ supportsToolbar: boolean
18
+ selected: boolean
19
+ installedMenu: boolean
20
+ installedToolbar: boolean
21
+ installedShortcut: string
22
+ defaultShortcut: string
23
+ effectiveShortcut: string
24
+ isShortcutOverridden: boolean
25
+ installedActionName: string
26
+ }
27
+
28
+ type SetupDialogOptions = {
29
+ settingsPath: string
30
+ bundleName?: string
31
+ }
32
+
33
+ const OVERRIDE_MARKER = '[ovr]'
34
+
35
+ const toKey = (action: CustomAction): string => String(action.filePath ?? action.text ?? '')
36
+
37
+ const toTreeNode = (entry: InstallerEntry): TreeNode<InstallerEntry> =>
38
+ new TreeNode(String(entry.action.text), toKey(entry.action), entry)
39
+
40
+ const getDisplayedToolbar = (entry: InstallerEntry): string => String(entry.action.toolbar ?? '')
41
+
42
+ const getSetupDialogOptions = (options: string | SetupDialogOptions): SetupDialogOptions =>
43
+ typeof options === 'string'
44
+ ? { settingsPath: options }
45
+ : options
46
+
47
+ const getDisplayedShortcut = (entry: InstallerEntry): string => {
48
+ if (!entry.isShortcutOverridden) return entry.effectiveShortcut
49
+ return entry.effectiveShortcut
50
+ ? `${entry.effectiveShortcut} ${OVERRIDE_MARKER}`
51
+ : OVERRIDE_MARKER
52
+ }
53
+
54
+ const updateOverrideState = (entry: InstallerEntry) => {
55
+ entry.isShortcutOverridden = Boolean(entry.installedActionName) &&
56
+ normalizeShortcut(entry.installedShortcut) !== normalizeShortcut(entry.defaultShortcut)
57
+ }
58
+
59
+ const buildEntries = (actions: CustomAction[]): InstallerEntry[] => {
60
+ return actions
61
+ .filter((action) => Boolean(action.menuPath) || Boolean(action.toolbar))
62
+ .map((action) => {
63
+ const installed = getInstalledCustomActionState(action)
64
+ const supportsMenu = Boolean(action.menuPath)
65
+ const supportsToolbar = Boolean(action.toolbar)
66
+ const hasInstalledTargets = installed.installedMenu || installed.installedToolbar
67
+ const defaultShortcut = normalizeShortcut(String(action.shortcut ?? ''))
68
+ const installedShortcut = normalizeShortcut(String(installed.customAction?.shortcut ?? ''))
69
+ const entry: InstallerEntry = {
70
+ action: { ...action },
71
+ supportsMenu,
72
+ supportsToolbar,
73
+ selected: hasInstalledTargets,
74
+ installedMenu: installed.installedMenu,
75
+ installedToolbar: installed.installedToolbar,
76
+ installedShortcut,
77
+ defaultShortcut,
78
+ effectiveShortcut: installed.customAction?.name ? installedShortcut : defaultShortcut,
79
+ isShortcutOverridden: false,
80
+ installedActionName: String(installed.customAction?.name ?? '')
81
+ }
82
+
83
+ updateOverrideState(entry)
84
+ return entry
85
+ })
86
+ }
87
+
88
+ class InstallerSelectionDialog extends BasicDialog {
89
+ private readonly keywords$ = new Observable('')
90
+ private readonly items$: Observable<TreeNode<InstallerEntry>[]>
91
+ private readonly refreshListEvent$ = new Observable<void>()
92
+ private listView: DzListView | null = null
93
+
94
+ constructor(private readonly entries: InstallerEntry[], bundleName?: string) {
95
+ super(bundleName ? `${bundleName} Setup` : 'Setup Scripts', 'dsfSetupScripts')
96
+ this.items$ = new Observable(entries.map(toTreeNode))
97
+ }
98
+
99
+ protected build(): void {
100
+ this.builder.options({ resizable: true, width: 1100, height: 700 })
101
+ this.dialog.setAcceptButtonText('Apply')
102
+ this.dialog.setCancelButtonText('Cancel')
103
+
104
+ const add = this.add
105
+ add.label('Choose which scripts to install, then use the columns to review their shortcut, menu, and toolbar targets.')
106
+ .wordWrap()
107
+ .build()
108
+ add.group('Search').horizontal().build(() => {
109
+ add.edit()
110
+ .value(this.keywords$)
111
+ .focus()
112
+ .placeholder('Search actions...')
113
+ .toolTip('Search by action name, description, path, shortcut, or available targets.')
114
+ add.button('Select All').clicked(() => this.setVisibleSelections(true))
115
+ add.button('Deselect All').clicked(() => this.setVisibleSelections(false))
116
+ })
117
+
118
+ add.group('Scripts')
119
+ .style({ flat: true })
120
+ .build(() => {
121
+ add.list.view<InstallerEntry, InstallerEntry>()
122
+ .sorting(true)
123
+ .sortOnBuild(true)
124
+ .refresh(this.refreshListEvent$)
125
+ .row((item, parent, id) => {
126
+ const listItem = new DzCheckListItem(parent, DzCheckListItem.CheckBox, id)
127
+ listItem.on = item.value.selected
128
+ listItem.setText(0, '')
129
+ return listItem
130
+ })
131
+ .contextMenu((listView, item) => this.buildContextMenu(listView, item))
132
+ .items(this.items$)
133
+ .columns(['Install', 'Action', 'Shortcut', 'Description', 'Menu', 'Toolbar'], (index, width) => {
134
+ if (index === 0) return Math.max(width, 70)
135
+ if (index === 1) return Math.max(width * 1.6, 220)
136
+ if (index === 2) return Math.max(width, 140)
137
+ if (index === 3) return Math.max(width * 1.8, 240)
138
+ if (index === 4) return Math.max(width * 2.5, 320)
139
+ if (index === 5) return Math.max(width * 1.2, 140)
140
+ return width
141
+ })
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)
151
+ .filter({
152
+ keywords: this.keywords$,
153
+ field: (listItem) => [
154
+ listItem.text(1),
155
+ listItem.text(2),
156
+ listItem.text(3),
157
+ listItem.text(4),
158
+ listItem.text(5),
159
+ ].join(' ')
160
+ })
161
+ .build((listView) => {
162
+ this.listView = listView
163
+ listView.allColumnsShowFocus = true
164
+ })
165
+ })
166
+ }
167
+
168
+ getSelections(): InstallerEntry[] {
169
+ this.syncSelectionsFromListView()
170
+ return this.entries
171
+ }
172
+
173
+ private buildContextMenu(listView: DzListView, listItem: DzListViewItem | null): DzPopupMenu {
174
+ const entry = listItem ? getDataItem<InstallerEntry>(listItem) : null
175
+ const canSetShortcut = Boolean(entry)
176
+ const canResetShortcut = Boolean(entry?.installedActionName && entry?.isShortcutOverridden)
177
+
178
+ const items = [
179
+ {
180
+ text: 'Set Shortcut',
181
+ activated: () => {
182
+ if (!entry) return
183
+ this.setShortcut(entry)
184
+ }
185
+ },
186
+ {
187
+ text: 'Reset Default Shortcut',
188
+ activated: () => {
189
+ if (!entry) return
190
+ this.resetDefaultShortcut(entry)
191
+ }
192
+ },
193
+ {},
194
+ { text: 'Check All', activated: () => this.checkVisible(listView, true) },
195
+ { text: 'Uncheck All', activated: () => this.checkVisible(listView, false) }
196
+ ] as PopupMenuItem[]
197
+
198
+ return new PopupMenuBuilder(this.builder.context).items(...items).build((menu) => {
199
+ menu.setItemEnabled(0, canSetShortcut)
200
+ menu.setItemEnabled(1, canResetShortcut)
201
+ })
202
+ }
203
+
204
+ private setShortcut(entry: InstallerEntry) {
205
+ const actionName = entry.installedActionName || toKey(entry.action)
206
+ const actionLabel = String(entry.action.text ?? entry.action.filePath ?? actionName)
207
+ const shortcut = promptKeyboardShortcut(actionLabel, actionName, entry.effectiveShortcut)
208
+ if (shortcut == null) return
209
+
210
+ if (entry.installedActionName) {
211
+ setActionShortcut(entry.installedActionName, shortcut)
212
+ entry.installedShortcut = normalizeShortcut(shortcut)
213
+ entry.effectiveShortcut = entry.installedShortcut
214
+ updateOverrideState(entry)
215
+ } else {
216
+ entry.effectiveShortcut = normalizeShortcut(shortcut)
217
+ }
218
+
219
+ this.refreshListEvent$.trigger()
220
+ }
221
+
222
+ private resetDefaultShortcut(entry: InstallerEntry) {
223
+ if (!entry.installedActionName || !entry.isShortcutOverridden) return
224
+
225
+ setActionShortcut(entry.installedActionName, entry.defaultShortcut)
226
+ entry.installedShortcut = normalizeShortcut(entry.defaultShortcut)
227
+ entry.effectiveShortcut = entry.installedShortcut
228
+ updateOverrideState(entry)
229
+ this.refreshListEvent$.trigger()
230
+ }
231
+
232
+ private checkVisible(listView: DzListView, onOff: boolean) {
233
+ if (!this.keywords$.value?.trim()) {
234
+ checkAll(listView, onOff)
235
+ }
236
+
237
+ listView.getItems(DzListView.All).forEach((item) => {
238
+ if (!item.visible || !(item as any).inherits('DzCheckListItem')) return
239
+
240
+ const checkbox = item as DzCheckListItem
241
+ checkbox.on = onOff
242
+
243
+ const data = getDataItem<InstallerEntry>(item)
244
+ if (!data) return
245
+ data.selected = onOff
246
+ })
247
+ }
248
+
249
+ private setVisibleSelections(onOff: boolean) {
250
+ if (!this.listView) return
251
+ this.checkVisible(this.listView, onOff)
252
+ }
253
+
254
+ private syncSelectionsFromListView() {
255
+ if (!this.listView) return
256
+
257
+ const selectionByKey: Record<string, boolean> = {}
258
+
259
+ this.listView.getItems(DzListView.All).forEach((item) => {
260
+ if (!(item as any).inherits('DzCheckListItem')) return
261
+
262
+ const data = getDataItem<InstallerEntry>(item)
263
+ if (!data) return
264
+
265
+ const checkbox = item as DzCheckListItem
266
+ const selected = !(checkbox.state === 0 && !checkbox.on)
267
+ selectionByKey[toKey(data.action)] = selected
268
+ })
269
+
270
+ this.entries.forEach((entry) => {
271
+ const key = toKey(entry.action)
272
+ if (Object.prototype.hasOwnProperty.call(selectionByKey, key)) {
273
+ entry.selected = selectionByKey[key]
274
+ }
275
+ })
276
+ }
277
+ }
278
+
279
+ const runDialog = (actions: CustomAction[], options: SetupDialogOptions): InstallerEntry[] | null => {
280
+ const entries = buildEntries(actions)
281
+ const dialog = new InstallerSelectionDialog(entries, options.bundleName)
282
+ return dialog.ok() ? dialog.getSelections() : null
283
+ }
284
+
285
+ export const showSetupCustomActionsDialog = (actions: CustomAction[], options: string | SetupDialogOptions) => {
286
+ const settings = getSetupDialogOptions(options)
287
+ const selections = runDialog(actions, settings)
288
+ if (!selections) return
289
+
290
+ debug(`[Setup] applying ${selections.filter(selection => selection.selected).length}/${selections.length} selected actions`)
291
+
292
+ const removedToolbarNames = new CustomSet<string>()
293
+
294
+ progress('Setting Up Scripts', selections, (selection) => {
295
+ debug(`[Setup] ${selection.selected ? 'install' : 'remove'} "${selection.action.text}"`)
296
+
297
+ if (selection.selected) {
298
+ applyCustomActionTargets({
299
+ ...selection.action,
300
+ shortcut: selection.effectiveShortcut
301
+ }, {
302
+ menu: selection.supportsMenu,
303
+ toolbar: selection.supportsToolbar
304
+ })
305
+ return
306
+ }
307
+
308
+ if (selection.supportsToolbar && selection.action.toolbar) {
309
+ removedToolbarNames.add(selection.action.toolbar)
310
+ }
311
+
312
+ removeCustomActionTargets(selection.action, {
313
+ menu: selection.supportsMenu,
314
+ toolbar: selection.supportsToolbar
315
+ })
316
+ })
317
+
318
+ removedToolbarNames.forEach((toolbarName) => {
319
+ clearToolbar(toolbarName)
320
+
321
+ const stillSelected = selections.filter(s => s.selected && s.supportsToolbar && s.action.toolbar === toolbarName)
322
+ stillSelected.forEach((s) => addToToolbar({
323
+ ...s.action,
324
+ shortcut: s.effectiveShortcut
325
+ }))
326
+ debug(`[Setup] Toolbar ${toolbarName} rebuilt with ${stillSelected.length} remaining actions`)
327
+
328
+ if (stillSelected.length === 0) {
329
+ cleanupEmptyToolbar(toolbarName)
330
+ }
331
+ })
332
+ }
@@ -1,6 +1,6 @@
1
1
  import { BasicDialog } from '@dsf/dialog/basic-dialog';
2
2
  import { debug } from '@dsf/common/log';
3
- import { findAction, findActionsForShortcut, setActionShortcut } from '@dsf/helpers/action-helper';
3
+ import { findAction, findActionsForShortcut, getActionShortcut, normalizeShortcut, setActionShortcut } from '@dsf/helpers/action-helper';
4
4
  import { contains } from '@dsf/helpers/array-helper';
5
5
  import { confirm } from '@dsf/helpers/message-box-helper';
6
6
  import { Observable } from '@dsf/lib/observable';
@@ -69,6 +69,8 @@ class KeyboardShortcutDialog extends BasicDialog {
69
69
  this.dialog.setAcceptButtonEnabled(Boolean(text))
70
70
  })
71
71
 
72
+ this.loadInitialShortcut(model.shortcut.value)
73
+
72
74
  add.group('Assign Keyboard Shortcut').build((layout) => {
73
75
  layout.spacing = 5
74
76
  add.edit().value(model.actionLabel).readOnly()
@@ -117,17 +119,32 @@ class KeyboardShortcutDialog extends BasicDialog {
117
119
 
118
120
  return `Already assigned to: ${conflicts.map(action => action.text || action.name).join(', ')}`
119
121
  }
122
+
123
+ private loadInitialShortcut(shortcut: string): void {
124
+ const normalized = normalizeShortcut(shortcut)
125
+ if (!normalized) return
126
+
127
+ const parts = normalized.split('+').map(part => part.trim()).filter(Boolean)
128
+ const key = parts.pop() ?? ''
129
+
130
+ this.model.control.value = parts.indexOf('Ctrl') >= 0
131
+ this.model.alt.value = parts.indexOf('Alt') >= 0
132
+ this.model.shift.value = parts.indexOf('Shift') >= 0
133
+ this.model.windows.value = parts.indexOf('Win') >= 0
134
+ this.key.value = key
135
+ }
120
136
  }
121
137
 
122
- export const setKeyboardShortcut = (actionLabel: string, actionName: string) => {
138
+ export const promptKeyboardShortcut = (actionLabel: string, actionName: string, initialShortcut?: string): string | null => {
123
139
  let model = new KeyboardShortcutModel()
124
140
  model.actionLabel = actionLabel
125
141
  model.actionName = actionName
142
+ model.shortcut.value = normalizeShortcut(initialShortcut ?? getActionShortcut(actionName) ?? '')
126
143
 
127
144
  let dialog = new KeyboardShortcutDialog(model)
128
145
  let result = dialog.run()
129
146
 
130
- if (!result || !model.shortcut) return
147
+ if (!result || !model.shortcut) return null
131
148
 
132
149
  const conflicts = findActionsForShortcut(model.shortcut.value)
133
150
  .filter(action => action && action.name !== actionName)
@@ -139,8 +156,15 @@ export const setKeyboardShortcut = (actionLabel: string, actionName: string) =>
139
156
  if (conflicts.length > 0) {
140
157
  const conflictText = conflicts.map(action => action.text || action.name).join(', ')
141
158
  const response = confirm(`"${model.shortcut.value}" is already assigned to: ${conflictText}\n\nReplace it?`)
142
- if (!response.ok) return
159
+ if (!response.ok) return null
143
160
  }
144
161
 
145
- setActionShortcut(actionName, model.shortcut.value)
162
+ return model.shortcut.value
163
+ }
164
+
165
+ export const setKeyboardShortcut = (actionLabel: string, actionName: string, initialShortcut?: string) => {
166
+ const shortcut = promptKeyboardShortcut(actionLabel, actionName, initialShortcut)
167
+ if (!shortcut) return
168
+
169
+ setActionShortcut(actionName, shortcut)
146
170
  }
package/webpack.config.js CHANGED
@@ -1,7 +1,8 @@
1
1
  const path = require('path');
2
2
  const glob = require('glob');
3
3
  const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
4
- const { createActionLaunchers, validateAppDataPath } = require('./dist/scripts/launchers');
4
+ const { createActionLaunchers } = require('./dist/scripts/launchers');
5
+ const { validateAppDataPath } = require('./dist/scripts/app-data-path');
5
6
  const { loadConfig } = require('./dist/scripts/config-loader');
6
7
 
7
8
  class ActionLauncherPlugin {
@@ -1,19 +0,0 @@
1
-
2
- import { uninstallCustomActions as uninstall } from '@dsf/helpers/custom-action-helper';
3
-
4
- uninstall([
5
- {
6
- "name": null,
7
- "text": "Hello World",
8
- "filePath": "samples/hello-world.dsa",
9
- "menuPath": "/DazScriptFramework/samples",
10
- "description": "hello-world"
11
- },
12
- {
13
- "name": null,
14
- "text": "Sample Dialog",
15
- "filePath": "samples/sample-dialog.dsa",
16
- "menuPath": "/DazScriptFramework/samples",
17
- "description": "sample-dialog"
18
- }
19
- ]);