dazscript-framework 1.0.6 → 1.0.8
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 +10 -0
- package/dist/scripts/install-generator.js +52 -6
- package/package.json +1 -1
- package/src/helpers/action-helper.ts +5 -50
- package/src/helpers/custom-action-helper.ts +6 -0
- package/src/helpers/custom-action-installer-entries.test.ts +53 -0
- package/src/helpers/custom-action-installer-entries.ts +58 -0
- package/src/helpers/custom-action-installer-helper.ts +89 -38
- package/src/helpers/custom-action-installer-shortcuts.test.ts +95 -0
- package/src/helpers/custom-action-installer-shortcuts.ts +49 -0
- package/src/helpers/shortcut-helper.ts +48 -0
- package/src/scripts/install-generator.test.ts +105 -0
- package/src/shared/set-keyboard-shortcut-conflicts.test.ts +18 -0
- package/src/shared/set-keyboard-shortcut-conflicts.ts +14 -0
- package/src/shared/set-keyboard-shortcut.ts +3 -1
package/README.md
CHANGED
|
@@ -238,6 +238,7 @@ action({ text: 'My Script' }, MyScript);
|
|
|
238
238
|
| `toolbar` | Toolbar name the action should appear on |
|
|
239
239
|
| `group` | Grouping label for related actions in Daz Studio |
|
|
240
240
|
| `description` | Longer description for the action |
|
|
241
|
+
| `icon` | Image path used for the installed custom action. Overrides discovered icon files. |
|
|
241
242
|
| `bundle` | Generates a setup script beside the action. `true` → `Setup.dsa.ts`, a string → `Setup <name>.dsa.ts` |
|
|
242
243
|
|
|
243
244
|
---
|
|
@@ -289,6 +290,15 @@ Applying the dialog:
|
|
|
289
290
|
- Affected toolbars are rebuilt; empty framework-created toolbars are removed
|
|
290
291
|
- Selected keyboard shortcut rows are applied after actions are installed
|
|
291
292
|
|
|
293
|
+
Custom action icons are selected from `action(...)` metadata and sibling image files in this order:
|
|
294
|
+
|
|
295
|
+
1. Explicit `action({ icon: '...' })`
|
|
296
|
+
2. `scriptname.action.png`
|
|
297
|
+
3. `scriptname.png`
|
|
298
|
+
4. `scriptname.dsa.png` legacy fallback
|
|
299
|
+
|
|
300
|
+
`scriptname.action.png` is the installed custom action icon. Daz Studio uses the same action icon for menu and toolbar placements. `scriptname.png` is the preferred script/content icon fallback. `scriptname.dsa.png` is a legacy fallback kept for older projects and will be removed in a future breaking release.
|
|
301
|
+
|
|
292
302
|
This replaces the older `Install.dsa.ts` / `Uninstall.dsa.ts` pattern. The installer generator removes those legacy files if they exist.
|
|
293
303
|
|
|
294
304
|
### Setup Keyboard Shortcuts
|
|
@@ -21,6 +21,54 @@ function stringOrDefault(str, defaultValue) {
|
|
|
21
21
|
return str !== undefined && str !== null && str !== '' ? str : defaultValue;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function replaceEntrySourceSuffix(filePath, suffix) {
|
|
25
|
+
if (filePath.endsWith('.dsa.ts')) {
|
|
26
|
+
return `${filePath.slice(0, -'.dsa.ts'.length)}${suffix}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return filePath.replace(/\.ts$/, suffix);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function replaceEntryOutputSuffix(filePath, suffix) {
|
|
33
|
+
if (filePath.endsWith('.dsa')) {
|
|
34
|
+
return `${filePath.slice(0, -'.dsa'.length)}${suffix}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return `${filePath}${suffix}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getActionIconPath(filePath, decorator) {
|
|
41
|
+
if (decorator.icon) {
|
|
42
|
+
return decorator.icon;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const actionIcon = replaceEntrySourceSuffix(filePath, '.action.png');
|
|
46
|
+
const scriptIcon = replaceEntrySourceSuffix(filePath, '.png');
|
|
47
|
+
const legacyScriptIcon = filePath.replace('.ts', '.png');
|
|
48
|
+
|
|
49
|
+
if (!fs.existsSync(actionIcon) && !fs.existsSync(scriptIcon) && !fs.existsSync(legacyScriptIcon)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isBundleAction = decorator.bundle !== undefined;
|
|
54
|
+
|
|
55
|
+
if (fs.existsSync(actionIcon)) {
|
|
56
|
+
return isBundleAction
|
|
57
|
+
? replaceEntryOutputSuffix(path.parse(filePath).name, '.action.png')
|
|
58
|
+
: replaceEntryOutputSuffix(getPartialPath(filePath), '.action.png');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (fs.existsSync(scriptIcon)) {
|
|
62
|
+
return isBundleAction
|
|
63
|
+
? replaceEntryOutputSuffix(path.parse(filePath).name, '.png')
|
|
64
|
+
: replaceEntryOutputSuffix(getPartialPath(filePath), '.png');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return isBundleAction
|
|
68
|
+
? path.parse(filePath).name.replace('.dsa', '.dsa.png')
|
|
69
|
+
: `${getPartialPath(filePath)}.png`;
|
|
70
|
+
}
|
|
71
|
+
|
|
24
72
|
function generateInstallerTemplate(data, options) {
|
|
25
73
|
return `
|
|
26
74
|
import { showSetupCustomActionsDialog as setup } from '@dsf/helpers/custom-action-installer-helper';
|
|
@@ -178,12 +226,9 @@ function processScript(filePath, container, defaultMenuPath, setupOptions) {
|
|
|
178
226
|
toolbar: stringOrDefault(decorator.toolbar, undefined),
|
|
179
227
|
};
|
|
180
228
|
|
|
181
|
-
const icon = filePath
|
|
182
|
-
if (
|
|
183
|
-
script.icon =
|
|
184
|
-
decorator.bundle === undefined
|
|
185
|
-
? `${getPartialPath(filePath)}.png`
|
|
186
|
-
: fileInfo.name.replace('.dsa', '.dsa.png');
|
|
229
|
+
const icon = getActionIconPath(filePath, decorator);
|
|
230
|
+
if (icon) {
|
|
231
|
+
script.icon = icon;
|
|
187
232
|
}
|
|
188
233
|
|
|
189
234
|
script.text = stringOrDefault(decorator.text, script.text);
|
|
@@ -364,4 +409,5 @@ if (require.main === module) {
|
|
|
364
409
|
module.exports = {
|
|
365
410
|
generateInstallerFiles,
|
|
366
411
|
findActionEntryFiles,
|
|
412
|
+
getActionIconPath,
|
|
367
413
|
};
|
package/package.json
CHANGED
|
@@ -1,58 +1,11 @@
|
|
|
1
1
|
import { debug, warn } from '@dsf/common/log'
|
|
2
2
|
import { mainWindow } from '@dsf/core/global'
|
|
3
3
|
import { isGUID } from './string-helper'
|
|
4
|
+
export { normalizeShortcut } from './shortcut-helper'
|
|
5
|
+
import { normalizeShortcut } from './shortcut-helper'
|
|
4
6
|
|
|
5
7
|
const actionMgr = mainWindow.getActionMgr()
|
|
6
8
|
|
|
7
|
-
const shortcutTokenMap: { [key: string]: string } = {
|
|
8
|
-
'CTRL': 'Ctrl',
|
|
9
|
-
'CONTROL': 'Ctrl',
|
|
10
|
-
'SHIFT': 'Shift',
|
|
11
|
-
'ALT': 'Alt',
|
|
12
|
-
'OPTION': 'Alt',
|
|
13
|
-
'WIN': 'Win',
|
|
14
|
-
'WINDOWS': 'Win',
|
|
15
|
-
'CMD': 'Win',
|
|
16
|
-
'COMMAND': 'Win',
|
|
17
|
-
'SPACE': 'Space',
|
|
18
|
-
'HOME': 'Home',
|
|
19
|
-
'END': 'End',
|
|
20
|
-
'INS': 'Ins',
|
|
21
|
-
'INSERT': 'Ins',
|
|
22
|
-
'DEL': 'Del',
|
|
23
|
-
'DELETE': 'Del',
|
|
24
|
-
'TAB': 'Tab',
|
|
25
|
-
'BACKSPACE': 'Backspace',
|
|
26
|
-
'COMMA': 'Comma',
|
|
27
|
-
'PERIOD': 'Period',
|
|
28
|
-
'PLUS': 'Plus',
|
|
29
|
-
'MINUS': 'Minus',
|
|
30
|
-
'PGUP': 'PgUp',
|
|
31
|
-
'PAGEUP': 'PgUp',
|
|
32
|
-
'PGDOWN': 'PgDn',
|
|
33
|
-
'PGDN': 'PgDn',
|
|
34
|
-
'LEFT': 'Left',
|
|
35
|
-
'RIGHT': 'Right',
|
|
36
|
-
'UP': 'Up',
|
|
37
|
-
'DOWN': 'Down',
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const normalizeShortcut = (shortcut: string): string => {
|
|
41
|
-
if (!shortcut) return ''
|
|
42
|
-
|
|
43
|
-
return shortcut
|
|
44
|
-
.split('+')
|
|
45
|
-
.map(part => part.trim())
|
|
46
|
-
.filter(Boolean)
|
|
47
|
-
.map(part => {
|
|
48
|
-
if (part.length === 1) return part.toUpperCase()
|
|
49
|
-
|
|
50
|
-
const key = part.toUpperCase()
|
|
51
|
-
return shortcutTokenMap[key] || part
|
|
52
|
-
})
|
|
53
|
-
.join('+')
|
|
54
|
-
}
|
|
55
|
-
|
|
56
9
|
export const getActionShortcut = (name: string): string => {
|
|
57
10
|
const action = findAction(name)
|
|
58
11
|
if (!action) return ''
|
|
@@ -184,7 +137,7 @@ export const findActionsForShortcut = (shortcut: string): DzAction[] => {
|
|
|
184
137
|
return result
|
|
185
138
|
}
|
|
186
139
|
|
|
187
|
-
export const
|
|
140
|
+
export const clearActionShortcut = (name: string) => {
|
|
188
141
|
let action = findAction(name)
|
|
189
142
|
if (!action) return
|
|
190
143
|
if (isCustomAction(action)) {
|
|
@@ -195,6 +148,8 @@ export const clearActionShorcut = (name: string) => {
|
|
|
195
148
|
}
|
|
196
149
|
}
|
|
197
150
|
|
|
151
|
+
export const clearActionShorcut = clearActionShortcut
|
|
152
|
+
|
|
198
153
|
export const getActionPixmap = (action: string, icon: string, maxSize?: number): Pixmap | null => {
|
|
199
154
|
try {
|
|
200
155
|
if (!action) return null;
|
|
@@ -280,6 +280,12 @@ const removeUnderlyingCustomAction = (actionName: string | null | undefined) =>
|
|
|
280
280
|
const index = actionMgr.findCustomAction(actionName)
|
|
281
281
|
if (index < 0) return
|
|
282
282
|
|
|
283
|
+
const shortcut = String(actionMgr.getCustomActionShortcut(index) ?? '').trim()
|
|
284
|
+
if (shortcut) {
|
|
285
|
+
actionMgr.setCustomActionShortcut(index, '')
|
|
286
|
+
debug(`Action ${actionName} shortcut cleared before removal`)
|
|
287
|
+
}
|
|
288
|
+
|
|
283
289
|
actionMgr.removeCustomAction(index)
|
|
284
290
|
debug(`Action ${actionName} removed`)
|
|
285
291
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getCanonicalInstallerEntry, toActionKey } from './custom-action-installer-entries'
|
|
3
|
+
|
|
4
|
+
const entries = [
|
|
5
|
+
{ action: { text: 'Power Menu - Main Actions', filePath: './Power Menu Actions.dsa' }, effectiveShortcut: '' },
|
|
6
|
+
{ action: { text: 'Power Menu', filePath: './Power Menu.dsa' }, effectiveShortcut: '' },
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
describe('setup installer entries', () => {
|
|
10
|
+
it('builds an empty key for missing action data instead of throwing', () => {
|
|
11
|
+
expect(toActionKey(undefined)).toBe('')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('resolves row data back to the canonical installer entry', () => {
|
|
15
|
+
const rowCopy = {
|
|
16
|
+
action: { text: 'Power Menu', filePath: './Power Menu.dsa' },
|
|
17
|
+
effectiveShortcut: 'F12'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const canonical = getCanonicalInstallerEntry(entries, rowCopy)
|
|
21
|
+
|
|
22
|
+
expect(canonical).toBe(entries[1])
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('falls back to the displayed action text when row data is not an installer entry', () => {
|
|
26
|
+
const canonical = getCanonicalInstallerEntry(entries, { value: null }, 'Power Menu')
|
|
27
|
+
|
|
28
|
+
expect(canonical).toBe(entries[1])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('ignores row data with an invalid action property before falling back to displayed text', () => {
|
|
32
|
+
const canonical = getCanonicalInstallerEntry(entries, { action: true }, 'Power Menu')
|
|
33
|
+
|
|
34
|
+
expect(canonical).toBe(entries[1])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('prefers displayed row text over attached row data', () => {
|
|
38
|
+
const staleRowData = {
|
|
39
|
+
action: { text: 'Power Menu - Main Actions', filePath: './Power Menu Actions.dsa' },
|
|
40
|
+
effectiveShortcut: ''
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const canonical = getCanonicalInstallerEntry(entries, staleRowData, 'Power Menu')
|
|
44
|
+
|
|
45
|
+
expect(canonical).toBe(entries[1])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns null when neither row data nor displayed text identify an installer entry', () => {
|
|
49
|
+
const canonical = getCanonicalInstallerEntry(entries, { action: true }, 'Missing')
|
|
50
|
+
|
|
51
|
+
expect(canonical).toBeNull()
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CustomAction } from '@dsf/core/custom-action'
|
|
2
|
+
|
|
3
|
+
export type InstallerActionEntry = {
|
|
4
|
+
action: CustomAction
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const isActionLike = (value: any): value is CustomAction =>
|
|
8
|
+
Boolean(value && (typeof value.filePath !== 'undefined' || typeof value.text !== 'undefined'))
|
|
9
|
+
|
|
10
|
+
export const toActionKey = (action?: CustomAction | null): string =>
|
|
11
|
+
String(action?.filePath ?? action?.text ?? '')
|
|
12
|
+
|
|
13
|
+
const findByDisplayedText = <TEntry extends InstallerActionEntry>(
|
|
14
|
+
entries: TEntry[],
|
|
15
|
+
displayedText: string
|
|
16
|
+
): TEntry | null => {
|
|
17
|
+
for (let i = 0; i < entries.length; i++) {
|
|
18
|
+
const entry = entries[i]
|
|
19
|
+
if (String(entry.action.text ?? '') === displayedText) return entry
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const findByActionKey = <TEntry extends InstallerActionEntry>(
|
|
26
|
+
entries: TEntry[],
|
|
27
|
+
key: string
|
|
28
|
+
): TEntry | null => {
|
|
29
|
+
for (let i = 0; i < entries.length; i++) {
|
|
30
|
+
const entry = entries[i]
|
|
31
|
+
if (toActionKey(entry.action) === key) return entry
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const asInstallerActionEntry = <TEntry extends InstallerActionEntry>(value: any): TEntry | null => {
|
|
38
|
+
if (isActionLike(value?.action)) return value as TEntry
|
|
39
|
+
if (isActionLike(value?.value?.action)) return value.value as TEntry
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const getCanonicalInstallerEntry = <TEntry extends InstallerActionEntry>(
|
|
44
|
+
entries: TEntry[],
|
|
45
|
+
value: any,
|
|
46
|
+
displayedActionText?: string
|
|
47
|
+
): TEntry | null => {
|
|
48
|
+
const displayedText = String(displayedActionText ?? '')
|
|
49
|
+
const displayedMatch = findByDisplayedText(entries, displayedText)
|
|
50
|
+
if (displayedMatch) return displayedMatch
|
|
51
|
+
|
|
52
|
+
const candidate = asInstallerActionEntry<TEntry>(value)
|
|
53
|
+
|
|
54
|
+
if (!candidate || !isActionLike(candidate.action)) return null
|
|
55
|
+
|
|
56
|
+
const key = toActionKey(candidate.action)
|
|
57
|
+
return findByActionKey(entries, key) ?? candidate
|
|
58
|
+
}
|
|
@@ -11,6 +11,8 @@ 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
13
|
import { readFromFile, saveToFile } from './file-helper'
|
|
14
|
+
import { getCanonicalInstallerEntry, toActionKey } from './custom-action-installer-entries'
|
|
15
|
+
import { canResetSetupShortcut, getDisplayedSetupShortcut, resetSetupShortcut, setSetupShortcut, updateShortcutOverrideState } from './custom-action-installer-shortcuts'
|
|
14
16
|
|
|
15
17
|
type InstallerEntry = {
|
|
16
18
|
action: CustomAction
|
|
@@ -70,9 +72,7 @@ type SetupSelection = {
|
|
|
70
72
|
shortcuts: ShortcutEntry[]
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const toKey = (action: CustomAction): string => String(action.filePath ?? action.text ?? '')
|
|
75
|
+
const toKey = toActionKey
|
|
76
76
|
|
|
77
77
|
const toTreeNode = (entry: InstallerEntry): TreeNode<InstallerEntry> =>
|
|
78
78
|
new TreeNode(String(entry.action.text), toKey(entry.action), entry)
|
|
@@ -96,18 +96,6 @@ const getSetupDialogOptions = (options: string | SetupDialogOptions): SetupDialo
|
|
|
96
96
|
const getShortcutBackupPath = (options: SetupDialogOptions): string =>
|
|
97
97
|
`${App.getAppDataPath()}/${options.shortcutBackupPath ?? `${options.settingsPath}/keyboard-shortcuts-backup.json`}`
|
|
98
98
|
|
|
99
|
-
const getDisplayedShortcut = (entry: InstallerEntry): string => {
|
|
100
|
-
if (!entry.isShortcutOverridden) return entry.effectiveShortcut
|
|
101
|
-
return entry.effectiveShortcut
|
|
102
|
-
? `${entry.effectiveShortcut} ${OVERRIDE_MARKER}`
|
|
103
|
-
: OVERRIDE_MARKER
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const updateOverrideState = (entry: InstallerEntry) => {
|
|
107
|
-
entry.isShortcutOverridden = Boolean(entry.installedActionName) &&
|
|
108
|
-
normalizeShortcut(entry.installedShortcut) !== normalizeShortcut(entry.defaultShortcut)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
99
|
const buildEntries = (actions: CustomAction[]): InstallerEntry[] => {
|
|
112
100
|
return actions
|
|
113
101
|
.filter((action) => Boolean(action.menuPath) || Boolean(action.toolbar))
|
|
@@ -132,7 +120,7 @@ const buildEntries = (actions: CustomAction[]): InstallerEntry[] => {
|
|
|
132
120
|
installedActionName: String(installed.customAction?.name ?? '')
|
|
133
121
|
}
|
|
134
122
|
|
|
135
|
-
|
|
123
|
+
updateShortcutOverrideState(entry)
|
|
136
124
|
return entry
|
|
137
125
|
})
|
|
138
126
|
}
|
|
@@ -235,8 +223,7 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
235
223
|
.focus()
|
|
236
224
|
.placeholder('Search actions...')
|
|
237
225
|
.toolTip('Search by action name, description, path, shortcut, or available targets.')
|
|
238
|
-
add.button('
|
|
239
|
-
add.button('Deselect All').clicked(() => this.setVisibleSelections(false))
|
|
226
|
+
add.button('Clear').clicked(() => this.keywords$.value = '')
|
|
240
227
|
})
|
|
241
228
|
|
|
242
229
|
add.group('Scripts')
|
|
@@ -269,7 +256,7 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
269
256
|
return [
|
|
270
257
|
'',
|
|
271
258
|
String(entry.action.text ?? ''),
|
|
272
|
-
|
|
259
|
+
getDisplayedSetupShortcut(entry),
|
|
273
260
|
String(entry.action.description ?? ''),
|
|
274
261
|
String(entry.action.menuPath ?? ''),
|
|
275
262
|
getDisplayedToolbar(entry),
|
|
@@ -291,6 +278,11 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
291
278
|
listView.allColumnsShowFocus = true
|
|
292
279
|
})
|
|
293
280
|
})
|
|
281
|
+
add.group().horizontal().build((layout) => {
|
|
282
|
+
layout.addStretch()
|
|
283
|
+
add.button('Select All').clicked(() => this.setVisibleSelections(true))
|
|
284
|
+
add.button('Deselect All').clicked(() => this.setVisibleSelections(false))
|
|
285
|
+
})
|
|
294
286
|
}
|
|
295
287
|
|
|
296
288
|
private buildShortcutsTab(): void {
|
|
@@ -304,8 +296,7 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
304
296
|
.value(this.shortcutKeywords$)
|
|
305
297
|
.placeholder('Search shortcuts...')
|
|
306
298
|
.toolTip('Search by action name, current shortcut, new shortcut, status, or conflicts.')
|
|
307
|
-
add.button('
|
|
308
|
-
add.button('Deselect All').clicked(() => this.setVisibleShortcutSelections(false))
|
|
299
|
+
add.button('Clear').clicked(() => this.shortcutKeywords$.value = '')
|
|
309
300
|
})
|
|
310
301
|
|
|
311
302
|
add.group('Keyboard Shortcuts')
|
|
@@ -358,12 +349,20 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
358
349
|
listView.allColumnsShowFocus = true
|
|
359
350
|
})
|
|
360
351
|
})
|
|
352
|
+
add.group().horizontal().build((layout) => {
|
|
353
|
+
layout.addStretch()
|
|
354
|
+
add.button('Select All').clicked(() => this.setVisibleShortcutSelections(true))
|
|
355
|
+
add.button('Deselect All').clicked(() => this.setVisibleShortcutSelections(false))
|
|
356
|
+
})
|
|
361
357
|
}
|
|
362
358
|
|
|
363
359
|
private buildContextMenu(listView: DzListView, listItem: DzListViewItem | null): DzPopupMenu {
|
|
364
|
-
|
|
360
|
+
debug(`[Setup] context menu row input ${this.describeListItem('item', listItem)} ${this.describeListItem('selected', listView.selectedItem())} ${this.describeListItem('current', listView.currentItem())}`)
|
|
361
|
+
const contextItem = this.getContextListItem(listView, listItem)
|
|
362
|
+
const entry = contextItem ? this.getCanonicalEntry(getDataItem<InstallerEntry>(contextItem), contextItem) : null
|
|
363
|
+
debug(`[Setup] context menu row resolved item="${this.safeListItemText(contextItem, 1)}" entry="${entry?.action?.text ?? ''}"`)
|
|
365
364
|
const canSetShortcut = Boolean(entry)
|
|
366
|
-
const canResetShortcut = Boolean(entry
|
|
365
|
+
const canResetShortcut = Boolean(entry && canResetSetupShortcut(entry))
|
|
367
366
|
|
|
368
367
|
const items = [
|
|
369
368
|
{
|
|
@@ -391,31 +390,77 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
391
390
|
})
|
|
392
391
|
}
|
|
393
392
|
|
|
393
|
+
private getContextListItem(listView: DzListView, listItem: any): DzListViewItem | null {
|
|
394
|
+
if (this.isListViewItem(listItem)) return listItem
|
|
395
|
+
|
|
396
|
+
const selectedItem = listView.selectedItem()
|
|
397
|
+
if (this.isListViewItem(selectedItem)) return selectedItem
|
|
398
|
+
|
|
399
|
+
const currentItem = listView.currentItem()
|
|
400
|
+
if (this.isListViewItem(currentItem)) return currentItem
|
|
401
|
+
|
|
402
|
+
return null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private isListViewItem(value: any): value is DzListViewItem {
|
|
406
|
+
return Boolean(value && typeof value.text === 'function')
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private describeListItem(label: string, value: any): string {
|
|
410
|
+
if (!value) return `${label}=null`
|
|
411
|
+
|
|
412
|
+
const hasTextFunction = typeof value.text === 'function'
|
|
413
|
+
const text0 = this.safeListItemText(value, 0)
|
|
414
|
+
const text1 = this.safeListItemText(value, 1)
|
|
415
|
+
const text2 = this.safeListItemText(value, 2)
|
|
416
|
+
const hasDataFunction = typeof value.getDataItem === 'function'
|
|
417
|
+
let dataText = ''
|
|
418
|
+
|
|
419
|
+
if (hasDataFunction) {
|
|
420
|
+
const data = getDataItem<any>(value)
|
|
421
|
+
dataText = String(data?.action?.text ?? data?.value?.action?.text ?? '')
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return `${label} type=${typeof value} textFn=${hasTextFunction} text0="${text0}" text1="${text1}" text2="${text2}" dataFn=${hasDataFunction} data="${dataText}"`
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private safeListItemText(value: any, column: number): string {
|
|
428
|
+
try {
|
|
429
|
+
if (!value || typeof value.text !== 'function') return ''
|
|
430
|
+
return String(value.text(column) ?? '')
|
|
431
|
+
} catch (error) {
|
|
432
|
+
return `[error:${String(error)}]`
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private getCanonicalEntry(entry: InstallerEntry | null, listItem?: DzListViewItem | null): InstallerEntry | null {
|
|
437
|
+
return getCanonicalInstallerEntry(this.entries, entry, listItem?.text(1))
|
|
438
|
+
}
|
|
439
|
+
|
|
394
440
|
private setShortcut(entry: InstallerEntry) {
|
|
441
|
+
if (!entry?.action) {
|
|
442
|
+
debug('[Setup] shortcut set ignored because the selected row did not resolve to an installer entry')
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
395
446
|
const actionName = entry.installedActionName || toKey(entry.action)
|
|
396
447
|
const actionLabel = String(entry.action.text ?? entry.action.filePath ?? actionName)
|
|
397
448
|
const shortcut = promptKeyboardShortcut(actionLabel, actionName, entry.effectiveShortcut)
|
|
398
449
|
if (shortcut == null) return
|
|
399
450
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
entry.effectiveShortcut = entry.installedShortcut
|
|
404
|
-
updateOverrideState(entry)
|
|
405
|
-
} else {
|
|
406
|
-
entry.effectiveShortcut = normalizeShortcut(shortcut)
|
|
407
|
-
}
|
|
451
|
+
const previousShortcut = entry.effectiveShortcut
|
|
452
|
+
setSetupShortcut(entry, shortcut)
|
|
453
|
+
debug(`[Setup] shortcut queued action="${entry.action.text}" installed=${Boolean(entry.installedActionName)} previous="${previousShortcut}" next="${entry.effectiveShortcut}" custom=${entry.isShortcutOverridden}`)
|
|
408
454
|
|
|
409
455
|
this.refreshListEvent$.trigger()
|
|
410
456
|
}
|
|
411
457
|
|
|
412
458
|
private resetDefaultShortcut(entry: InstallerEntry) {
|
|
413
|
-
if (!entry
|
|
459
|
+
if (!canResetSetupShortcut(entry)) return
|
|
414
460
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
entry.
|
|
418
|
-
updateOverrideState(entry)
|
|
461
|
+
const previousShortcut = entry.effectiveShortcut
|
|
462
|
+
resetSetupShortcut(entry)
|
|
463
|
+
debug(`[Setup] shortcut reset queued action="${entry.action.text}" previous="${previousShortcut}" default="${entry.defaultShortcut}"`)
|
|
419
464
|
this.refreshListEvent$.trigger()
|
|
420
465
|
}
|
|
421
466
|
|
|
@@ -565,7 +610,7 @@ export const showSetupCustomActionsDialog = (actions: CustomAction[], options: s
|
|
|
565
610
|
const selectedToolbarActions: CustomAction[] = []
|
|
566
611
|
|
|
567
612
|
progress('Setting Up Scripts', selections.actions, (selection) => {
|
|
568
|
-
debug(`[Setup] ${selection.selected ? 'install' : 'remove'} "${selection.action.text}"`)
|
|
613
|
+
debug(`[Setup] ${selection.selected ? 'install' : 'remove'} "${selection.action.text}" shortcut="${selection.effectiveShortcut}" custom=${selection.isShortcutOverridden}`)
|
|
569
614
|
|
|
570
615
|
if (selection.supportsToolbar && selection.action.toolbar) {
|
|
571
616
|
touchedToolbarNames.add(selection.action.toolbar)
|
|
@@ -574,7 +619,7 @@ export const showSetupCustomActionsDialog = (actions: CustomAction[], options: s
|
|
|
574
619
|
if (selection.selected) {
|
|
575
620
|
const customAction = applyCustomActionTargets({
|
|
576
621
|
...selection.action,
|
|
577
|
-
shortcut:
|
|
622
|
+
shortcut: ''
|
|
578
623
|
}, {
|
|
579
624
|
menu: selection.supportsMenu,
|
|
580
625
|
toolbar: selection.supportsToolbar
|
|
@@ -582,6 +627,12 @@ export const showSetupCustomActionsDialog = (actions: CustomAction[], options: s
|
|
|
582
627
|
deferToolbar: true
|
|
583
628
|
})
|
|
584
629
|
|
|
630
|
+
if (customAction) {
|
|
631
|
+
setActionShortcut(String(customAction.name), selection.effectiveShortcut)
|
|
632
|
+
const appliedShortcut = getActionShortcut(String(customAction.name))
|
|
633
|
+
debug(`[Setup] applied shortcut "${appliedShortcut}" to "${selection.action.text}" after install`)
|
|
634
|
+
}
|
|
635
|
+
|
|
585
636
|
if (customAction && selection.supportsToolbar && selection.action.toolbar) {
|
|
586
637
|
selectedToolbarActions.push(customAction)
|
|
587
638
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
canResetSetupShortcut,
|
|
4
|
+
getDisplayedSetupShortcut,
|
|
5
|
+
resetSetupShortcut,
|
|
6
|
+
setSetupShortcut,
|
|
7
|
+
updateShortcutOverrideState
|
|
8
|
+
} from './custom-action-installer-shortcuts'
|
|
9
|
+
|
|
10
|
+
const shortcutState = (overrides: Partial<ReturnType<typeof baseState>> = {}) => ({
|
|
11
|
+
...baseState(),
|
|
12
|
+
...overrides
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const baseState = () => ({
|
|
16
|
+
installedActionName: '',
|
|
17
|
+
installedShortcut: '',
|
|
18
|
+
defaultShortcut: '',
|
|
19
|
+
effectiveShortcut: '',
|
|
20
|
+
isShortcutOverridden: false
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('setup shortcut state', () => {
|
|
24
|
+
it('stores pending shortcuts for actions that are not installed yet', () => {
|
|
25
|
+
const entry = shortcutState()
|
|
26
|
+
|
|
27
|
+
setSetupShortcut(entry, 'F12')
|
|
28
|
+
|
|
29
|
+
expect(entry.effectiveShortcut).toBe('F12')
|
|
30
|
+
expect(entry.installedShortcut).toBe('')
|
|
31
|
+
expect(entry.isShortcutOverridden).toBe(false)
|
|
32
|
+
expect(canResetSetupShortcut(entry)).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('marks installed shortcuts that differ from the default as custom', () => {
|
|
36
|
+
const entry = shortcutState({
|
|
37
|
+
installedActionName: 'installed-guid',
|
|
38
|
+
defaultShortcut: '',
|
|
39
|
+
installedShortcut: ''
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
setSetupShortcut(entry, 'F12')
|
|
43
|
+
|
|
44
|
+
expect(entry.installedShortcut).toBe('F12')
|
|
45
|
+
expect(entry.effectiveShortcut).toBe('F12')
|
|
46
|
+
expect(entry.isShortcutOverridden).toBe(true)
|
|
47
|
+
expect(getDisplayedSetupShortcut(entry)).toBe('F12 [custom]')
|
|
48
|
+
expect(canResetSetupShortcut(entry)).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('resets installed custom shortcuts to the default', () => {
|
|
52
|
+
const entry = shortcutState({
|
|
53
|
+
installedActionName: 'installed-guid',
|
|
54
|
+
defaultShortcut: 'Ctrl+P',
|
|
55
|
+
installedShortcut: 'F12',
|
|
56
|
+
effectiveShortcut: 'F12',
|
|
57
|
+
isShortcutOverridden: true
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
resetSetupShortcut(entry)
|
|
61
|
+
|
|
62
|
+
expect(entry.installedShortcut).toBe('Ctrl+P')
|
|
63
|
+
expect(entry.effectiveShortcut).toBe('Ctrl+P')
|
|
64
|
+
expect(entry.isShortcutOverridden).toBe(false)
|
|
65
|
+
expect(canResetSetupShortcut(entry)).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('resets pending shortcuts without inventing an installed shortcut', () => {
|
|
69
|
+
const entry = shortcutState({
|
|
70
|
+
defaultShortcut: '',
|
|
71
|
+
installedShortcut: '',
|
|
72
|
+
effectiveShortcut: 'F12'
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
resetSetupShortcut(entry)
|
|
76
|
+
|
|
77
|
+
expect(entry.installedShortcut).toBe('')
|
|
78
|
+
expect(entry.effectiveShortcut).toBe('')
|
|
79
|
+
expect(entry.isShortcutOverridden).toBe(false)
|
|
80
|
+
expect(canResetSetupShortcut(entry)).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('displays custom marker when installed shortcut was changed outside setup', () => {
|
|
84
|
+
const entry = shortcutState({
|
|
85
|
+
installedActionName: 'installed-guid',
|
|
86
|
+
defaultShortcut: '',
|
|
87
|
+
installedShortcut: 'F12',
|
|
88
|
+
effectiveShortcut: 'F12'
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
updateShortcutOverrideState(entry)
|
|
92
|
+
|
|
93
|
+
expect(getDisplayedSetupShortcut(entry)).toBe('F12 [custom]')
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { normalizeShortcut } from '@dsf/helpers/shortcut-helper'
|
|
2
|
+
|
|
3
|
+
export const CUSTOM_SHORTCUT_MARKER = '[custom]'
|
|
4
|
+
|
|
5
|
+
export type InstallerShortcutState = {
|
|
6
|
+
installedActionName: string
|
|
7
|
+
installedShortcut: string
|
|
8
|
+
defaultShortcut: string
|
|
9
|
+
effectiveShortcut: string
|
|
10
|
+
isShortcutOverridden: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const updateShortcutOverrideState = (entry: InstallerShortcutState): void => {
|
|
14
|
+
entry.isShortcutOverridden = Boolean(entry.installedActionName) &&
|
|
15
|
+
normalizeShortcut(entry.installedShortcut) !== normalizeShortcut(entry.defaultShortcut)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const getDisplayedSetupShortcut = (entry: InstallerShortcutState): string => {
|
|
19
|
+
if (!entry.isShortcutOverridden) return entry.effectiveShortcut
|
|
20
|
+
|
|
21
|
+
return entry.effectiveShortcut
|
|
22
|
+
? `${entry.effectiveShortcut} ${CUSTOM_SHORTCUT_MARKER}`
|
|
23
|
+
: CUSTOM_SHORTCUT_MARKER
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const canResetSetupShortcut = (entry: InstallerShortcutState): boolean =>
|
|
27
|
+
normalizeShortcut(entry.effectiveShortcut) !== normalizeShortcut(entry.defaultShortcut)
|
|
28
|
+
|
|
29
|
+
export const setSetupShortcut = (entry: InstallerShortcutState, shortcut: string): void => {
|
|
30
|
+
const normalizedShortcut = normalizeShortcut(shortcut)
|
|
31
|
+
|
|
32
|
+
if (entry.installedActionName) {
|
|
33
|
+
entry.installedShortcut = normalizedShortcut
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
entry.effectiveShortcut = normalizedShortcut
|
|
37
|
+
updateShortcutOverrideState(entry)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const resetSetupShortcut = (entry: InstallerShortcutState): void => {
|
|
41
|
+
const defaultShortcut = normalizeShortcut(entry.defaultShortcut)
|
|
42
|
+
|
|
43
|
+
if (entry.installedActionName) {
|
|
44
|
+
entry.installedShortcut = defaultShortcut
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
entry.effectiveShortcut = defaultShortcut
|
|
48
|
+
updateShortcutOverrideState(entry)
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const shortcutTokenMap: { [key: string]: string } = {
|
|
2
|
+
'CTRL': 'Ctrl',
|
|
3
|
+
'CONTROL': 'Ctrl',
|
|
4
|
+
'SHIFT': 'Shift',
|
|
5
|
+
'ALT': 'Alt',
|
|
6
|
+
'OPTION': 'Alt',
|
|
7
|
+
'WIN': 'Win',
|
|
8
|
+
'WINDOWS': 'Win',
|
|
9
|
+
'CMD': 'Win',
|
|
10
|
+
'COMMAND': 'Win',
|
|
11
|
+
'SPACE': 'Space',
|
|
12
|
+
'HOME': 'Home',
|
|
13
|
+
'END': 'End',
|
|
14
|
+
'INS': 'Ins',
|
|
15
|
+
'INSERT': 'Ins',
|
|
16
|
+
'DEL': 'Del',
|
|
17
|
+
'DELETE': 'Del',
|
|
18
|
+
'TAB': 'Tab',
|
|
19
|
+
'BACKSPACE': 'Backspace',
|
|
20
|
+
'COMMA': 'Comma',
|
|
21
|
+
'PERIOD': 'Period',
|
|
22
|
+
'PLUS': 'Plus',
|
|
23
|
+
'MINUS': 'Minus',
|
|
24
|
+
'PGUP': 'PgUp',
|
|
25
|
+
'PAGEUP': 'PgUp',
|
|
26
|
+
'PGDOWN': 'PgDn',
|
|
27
|
+
'PGDN': 'PgDn',
|
|
28
|
+
'LEFT': 'Left',
|
|
29
|
+
'RIGHT': 'Right',
|
|
30
|
+
'UP': 'Up',
|
|
31
|
+
'DOWN': 'Down',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const normalizeShortcut = (shortcut: string): string => {
|
|
35
|
+
if (!shortcut) return ''
|
|
36
|
+
|
|
37
|
+
return shortcut
|
|
38
|
+
.split('+')
|
|
39
|
+
.map(part => part.trim())
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.map(part => {
|
|
42
|
+
if (part.length === 1) return part.toUpperCase()
|
|
43
|
+
|
|
44
|
+
const key = part.toUpperCase()
|
|
45
|
+
return shortcutTokenMap[key] || part
|
|
46
|
+
})
|
|
47
|
+
.join('+')
|
|
48
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
const { generateInstallerFiles } = require('../../dist/scripts/install-generator')
|
|
7
|
+
|
|
8
|
+
const tempDirs: string[] = []
|
|
9
|
+
|
|
10
|
+
const makeProject = (): string => {
|
|
11
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsf-install-generator-'))
|
|
12
|
+
tempDirs.push(projectDir)
|
|
13
|
+
fs.mkdirSync(path.join(projectDir, 'src'), { recursive: true })
|
|
14
|
+
fs.writeFileSync(
|
|
15
|
+
path.join(projectDir, 'dazscript.config.cjs'),
|
|
16
|
+
"module.exports = { appDataPath: 'Test/ActionIcons' }\n"
|
|
17
|
+
)
|
|
18
|
+
return projectDir
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const writeScript = (projectDir: string, name: string, actionOptions: string = ''): void => {
|
|
22
|
+
fs.writeFileSync(
|
|
23
|
+
path.join(projectDir, 'src', `${name}.dsa.ts`),
|
|
24
|
+
`action({ text: '${name}'${actionOptions} }, function() {})\n`
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const writePng = (projectDir: string, fileName: string): void => {
|
|
29
|
+
fs.writeFileSync(path.join(projectDir, 'src', fileName), 'png')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const generateSetup = (projectDir: string): string => {
|
|
33
|
+
const previousCwd = process.cwd()
|
|
34
|
+
process.chdir(projectDir)
|
|
35
|
+
try {
|
|
36
|
+
generateInstallerFiles(projectDir, {
|
|
37
|
+
scriptsPath: './src',
|
|
38
|
+
defaultMenuPath: '/Test',
|
|
39
|
+
appDataPath: undefined,
|
|
40
|
+
})
|
|
41
|
+
} finally {
|
|
42
|
+
process.chdir(previousCwd)
|
|
43
|
+
}
|
|
44
|
+
return fs.readFileSync(path.join(projectDir, 'src', 'Setup.dsa.ts'), 'utf8')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
while (tempDirs.length > 0) {
|
|
49
|
+
const dir = tempDirs.pop()
|
|
50
|
+
if (dir) fs.rmSync(dir, { recursive: true, force: true })
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('install generator action icons', () => {
|
|
55
|
+
it('prefers the action icon convention over the script icon fallback', () => {
|
|
56
|
+
const projectDir = makeProject()
|
|
57
|
+
writeScript(projectDir, 'render-tools')
|
|
58
|
+
writePng(projectDir, 'render-tools.action.png')
|
|
59
|
+
writePng(projectDir, 'render-tools.png')
|
|
60
|
+
writePng(projectDir, 'render-tools.dsa.png')
|
|
61
|
+
|
|
62
|
+
const setup = generateSetup(projectDir)
|
|
63
|
+
|
|
64
|
+
expect(setup).toContain('"icon": "./render-tools.action.png"')
|
|
65
|
+
expect(setup).not.toContain('"icon": "./render-tools.png"')
|
|
66
|
+
expect(setup).not.toContain('"icon": "./render-tools.dsa.png"')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('falls back to the script icon when no action icon exists', () => {
|
|
70
|
+
const projectDir = makeProject()
|
|
71
|
+
writeScript(projectDir, 'power-menu')
|
|
72
|
+
writePng(projectDir, 'power-menu.png')
|
|
73
|
+
writePng(projectDir, 'power-menu.dsa.png')
|
|
74
|
+
|
|
75
|
+
const setup = generateSetup(projectDir)
|
|
76
|
+
|
|
77
|
+
expect(setup).toContain('"icon": "./power-menu.png"')
|
|
78
|
+
expect(setup).not.toContain('"icon": "./power-menu.dsa.png"')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('keeps the dsa-named script icon as a legacy fallback', () => {
|
|
82
|
+
const projectDir = makeProject()
|
|
83
|
+
writeScript(projectDir, 'legacy-icon')
|
|
84
|
+
writePng(projectDir, 'legacy-icon.dsa.png')
|
|
85
|
+
|
|
86
|
+
const setup = generateSetup(projectDir)
|
|
87
|
+
|
|
88
|
+
expect(setup).toContain('"icon": "./legacy-icon.dsa.png"')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('lets explicit action icon metadata override discovered icon files', () => {
|
|
92
|
+
const projectDir = makeProject()
|
|
93
|
+
writeScript(projectDir, 'custom-icon', ", icon: 'icons/custom-action.png'")
|
|
94
|
+
writePng(projectDir, 'custom-icon.action.png')
|
|
95
|
+
writePng(projectDir, 'custom-icon.png')
|
|
96
|
+
writePng(projectDir, 'custom-icon.dsa.png')
|
|
97
|
+
|
|
98
|
+
const setup = generateSetup(projectDir)
|
|
99
|
+
|
|
100
|
+
expect(setup).toContain('"icon": "icons/custom-action.png"')
|
|
101
|
+
expect(setup).not.toContain('"icon": "custom-icon.action.png"')
|
|
102
|
+
expect(setup).not.toContain('"icon": "custom-icon.png"')
|
|
103
|
+
expect(setup).not.toContain('"icon": "custom-icon.dsa.png"')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { clearShortcutConflicts } from './set-keyboard-shortcut-conflicts'
|
|
3
|
+
|
|
4
|
+
describe('clearShortcutConflicts', () => {
|
|
5
|
+
it('clears conflicting actions without clearing the target action', () => {
|
|
6
|
+
const cleared: string[] = []
|
|
7
|
+
|
|
8
|
+
clearShortcutConflicts('target-action', [
|
|
9
|
+
{ name: 'target-action', text: 'Target' } as DzAction,
|
|
10
|
+
{ name: 'old-owner', text: 'Old Owner' } as DzAction,
|
|
11
|
+
{ name: 'other-owner', text: 'Other Owner' } as DzAction
|
|
12
|
+
], (name) => {
|
|
13
|
+
cleared.push(name)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
expect(cleared).toEqual(['old-owner', 'other-owner'])
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ShortcutConflict = {
|
|
2
|
+
name: string
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export const clearShortcutConflicts = (
|
|
6
|
+
actionName: string,
|
|
7
|
+
conflicts: ShortcutConflict[],
|
|
8
|
+
clearShortcut: (name: string) => void
|
|
9
|
+
): void => {
|
|
10
|
+
conflicts.forEach((action) => {
|
|
11
|
+
if (!action || action.name === actionName) return
|
|
12
|
+
clearShortcut(action.name)
|
|
13
|
+
})
|
|
14
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { BasicDialog } from '@dsf/dialog/basic-dialog';
|
|
2
2
|
import { debug } from '@dsf/common/log';
|
|
3
|
-
import {
|
|
3
|
+
import { clearActionShortcut, 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';
|
|
7
|
+
import { clearShortcutConflicts } from './set-keyboard-shortcut-conflicts';
|
|
7
8
|
|
|
8
9
|
class KeyboardShortcutModel {
|
|
9
10
|
actionLabel: string
|
|
@@ -157,6 +158,7 @@ export const promptKeyboardShortcut = (actionLabel: string, actionName: string,
|
|
|
157
158
|
const conflictText = conflicts.map(action => action.text || action.name).join(', ')
|
|
158
159
|
const response = confirm(`"${model.shortcut.value}" is already assigned to: ${conflictText}\n\nReplace it?`)
|
|
159
160
|
if (!response.ok) return null
|
|
161
|
+
clearShortcutConflicts(actionName, conflicts, clearActionShortcut)
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
return model.shortcut.value
|