dazscript-framework 1.0.8 → 1.0.9
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 +43 -0
- package/package.json +1 -1
- package/src/dialog/builders/dialog-header-builder.ts +72 -0
- package/src/dialog/builders/widgets-builder.ts +5 -0
- package/src/helpers/custom-action-installer-helper.ts +40 -6
- package/src/scripts/install-generator.test.ts +70 -0
package/README.md
CHANGED
|
@@ -299,6 +299,16 @@ Custom action icons are selected from `action(...)` metadata and sibling image f
|
|
|
299
299
|
|
|
300
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
301
|
|
|
302
|
+
Setup dialog header assets are optional and are discovered beside `src/Setup.dsa.ts`:
|
|
303
|
+
|
|
304
|
+
1. `src/Setup.header.png`
|
|
305
|
+
2. `src/Setup.png`
|
|
306
|
+
3. `src/Setup.dsa.png` legacy fallback
|
|
307
|
+
|
|
308
|
+
Header text can be placed in `src/Setup.header.html`, `src/Setup.header.md`, or `src/Setup.header.txt`, with `src/Setup.html`, `src/Setup.md`, and `src/Setup.txt` as script-named fallbacks. The installer generator embeds that text into `Setup.dsa.ts`, so Daz Studio does not need to read the text file at setup time. The setup dialog renders the header body with `DzTextBrowser` rich text support; no Markdown conversion is performed. The image remains a deployed PNG asset and is resolved relative to the generated setup script at runtime.
|
|
309
|
+
|
|
310
|
+
The same layout is available to custom dialogs through `add.header({ imagePath, html, text, height, imageWidth }).build()`. Use `html` for rich text, or `text` for escaped plain text.
|
|
311
|
+
|
|
302
312
|
This replaces the older `Install.dsa.ts` / `Uninstall.dsa.ts` pattern. The installer generator removes those legacy files if they exist.
|
|
303
313
|
|
|
304
314
|
### Setup Keyboard Shortcuts
|
|
@@ -276,6 +276,48 @@ function toPosix(filePath) {
|
|
|
276
276
|
return filePath.replace(/\\/g, '/');
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
function resolveSetupHeaderImage(workdir) {
|
|
280
|
+
const candidates = [
|
|
281
|
+
path.join(workdir, 'src', 'Setup.header.png'),
|
|
282
|
+
path.join(workdir, 'src', 'Setup.png'),
|
|
283
|
+
path.join(workdir, 'src', 'Setup.dsa.png'),
|
|
284
|
+
];
|
|
285
|
+
const match = candidates.find((candidate) => fs.existsSync(candidate));
|
|
286
|
+
return match ? `./${path.basename(match)}` : undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function resolveSetupHeaderTextFile(workdir) {
|
|
290
|
+
const candidates = [
|
|
291
|
+
path.join(workdir, 'src', 'Setup.header.html'),
|
|
292
|
+
path.join(workdir, 'src', 'Setup.header.md'),
|
|
293
|
+
path.join(workdir, 'src', 'Setup.header.txt'),
|
|
294
|
+
path.join(workdir, 'src', 'Setup.html'),
|
|
295
|
+
path.join(workdir, 'src', 'Setup.md'),
|
|
296
|
+
path.join(workdir, 'src', 'Setup.txt'),
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function loadSetupHeader(workdir) {
|
|
303
|
+
const header = {};
|
|
304
|
+
const imagePath = resolveSetupHeaderImage(workdir);
|
|
305
|
+
const textFile = resolveSetupHeaderTextFile(workdir);
|
|
306
|
+
|
|
307
|
+
if (imagePath) {
|
|
308
|
+
header.headerImagePath = imagePath;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (textFile) {
|
|
312
|
+
const text = fs.readFileSync(textFile, 'utf8').trim();
|
|
313
|
+
if (text) {
|
|
314
|
+
header.headerText = text;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return header;
|
|
319
|
+
}
|
|
320
|
+
|
|
279
321
|
function resolveShortcutFile(workdir, config) {
|
|
280
322
|
const configuredPath =
|
|
281
323
|
config.keyboardShortcutsPath ||
|
|
@@ -340,6 +382,7 @@ function generateInstallerFiles(workdir, options) {
|
|
|
340
382
|
settingsPath,
|
|
341
383
|
bundleName,
|
|
342
384
|
shortcutBackupPath: `${appDataPath}/Installer/keyboard-shortcuts-backup.json`,
|
|
385
|
+
...loadSetupHeader(workdir),
|
|
343
386
|
};
|
|
344
387
|
if (shortcutData.shortcuts && shortcutData.shortcuts.length > 0) {
|
|
345
388
|
setupOptions.shortcuts = shortcutData.shortcuts;
|
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import LayoutBuilder from './layout-builder'
|
|
2
|
+
import { createWidget } from './widget-builder'
|
|
3
|
+
import { WidgetBuilderContext } from './widgets-builder'
|
|
4
|
+
|
|
5
|
+
export type DialogHeaderOptions = {
|
|
6
|
+
image?: Pixmap
|
|
7
|
+
imagePath?: string
|
|
8
|
+
imageWidth?: number
|
|
9
|
+
height?: number
|
|
10
|
+
html?: string
|
|
11
|
+
text?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const escapeHtml = (value: string): string =>
|
|
15
|
+
value
|
|
16
|
+
.replace(/&/g, '&')
|
|
17
|
+
.replace(/</g, '<')
|
|
18
|
+
.replace(/>/g, '>')
|
|
19
|
+
.replace(/"/g, '"')
|
|
20
|
+
.replace(/'/g, ''')
|
|
21
|
+
.replace(/\r?\n/g, '<br>')
|
|
22
|
+
|
|
23
|
+
export class DialogHeaderBuilder {
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly context: WidgetBuilderContext,
|
|
26
|
+
private readonly options: DialogHeaderOptions
|
|
27
|
+
) { }
|
|
28
|
+
|
|
29
|
+
build(): DzHBoxLayout {
|
|
30
|
+
const height = this.options.height ?? 96
|
|
31
|
+
const imageWidth = this.options.imageWidth ?? height
|
|
32
|
+
const html = this.options.html ?? (
|
|
33
|
+
typeof this.options.text === 'string'
|
|
34
|
+
? escapeHtml(this.options.text)
|
|
35
|
+
: ''
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return LayoutBuilder
|
|
39
|
+
.create(this.context)
|
|
40
|
+
.direction('horizontal')
|
|
41
|
+
.build(() => {
|
|
42
|
+
this.buildImage(height, imageWidth)
|
|
43
|
+
this.buildText(html, height)
|
|
44
|
+
}) as DzHBoxLayout
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private buildImage(height: number, imageWidth: number): void {
|
|
48
|
+
const pixmap = this.options.image ?? (
|
|
49
|
+
this.options.imagePath ? new Pixmap(this.options.imagePath) : null
|
|
50
|
+
)
|
|
51
|
+
if (!pixmap) return
|
|
52
|
+
|
|
53
|
+
createWidget(this.context).build(DzLabel, (label) => {
|
|
54
|
+
label.pixmap = pixmap
|
|
55
|
+
label.scaledContents = true
|
|
56
|
+
label.setFixedWidth(imageWidth)
|
|
57
|
+
label.setFixedHeight(height)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private buildText(html: string, height: number): void {
|
|
62
|
+
if (!html) return
|
|
63
|
+
|
|
64
|
+
createWidget(this.context).build(DzTextBrowser, (textBrowser) => {
|
|
65
|
+
textBrowser.html = html
|
|
66
|
+
textBrowser.readOnly = true
|
|
67
|
+
textBrowser.lineWrapMode = DzTextEdit.WidgetWidth
|
|
68
|
+
textBrowser.wordWrapMode = DzTextEdit.WordWrap
|
|
69
|
+
textBrowser.setFixedHeight(height)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -4,6 +4,7 @@ import CheckBoxBuilder from './checkbox-builder'
|
|
|
4
4
|
import ColorPickerBuilder from './color-picker-builder'
|
|
5
5
|
import { ComboBoxBuilder } from './combo-box-builder'
|
|
6
6
|
import { ComboEditBuilder } from './combo-edit-builder'
|
|
7
|
+
import { DialogHeaderBuilder, DialogHeaderOptions } from './dialog-header-builder'
|
|
7
8
|
import GroupBoxBuilder from './groupbox-builder'
|
|
8
9
|
import LabelBuilder from './label-builder'
|
|
9
10
|
import LayoutBuilder, { LayoutOrientation } from './layout-builder'
|
|
@@ -63,6 +64,10 @@ export class WidgetsBuilder {
|
|
|
63
64
|
return new LabelBuilder(this.context).text(text)
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
header(options: DialogHeaderOptions): DialogHeaderBuilder {
|
|
68
|
+
return new DialogHeaderBuilder(this.context, options)
|
|
69
|
+
}
|
|
70
|
+
|
|
66
71
|
button(text?: string): ButtonBuilder {
|
|
67
72
|
return new ButtonBuilder(this.context).text(text)
|
|
68
73
|
}
|
|
@@ -13,6 +13,7 @@ import { promptKeyboardShortcut } from '@dsf/shared/set-keyboard-shortcut'
|
|
|
13
13
|
import { readFromFile, saveToFile } from './file-helper'
|
|
14
14
|
import { getCanonicalInstallerEntry, toActionKey } from './custom-action-installer-entries'
|
|
15
15
|
import { canResetSetupShortcut, getDisplayedSetupShortcut, resetSetupShortcut, setSetupShortcut, updateShortcutOverrideState } from './custom-action-installer-shortcuts'
|
|
16
|
+
import { getScriptPath } from './script-helper'
|
|
16
17
|
|
|
17
18
|
type InstallerEntry = {
|
|
18
19
|
action: CustomAction
|
|
@@ -31,6 +32,10 @@ type InstallerEntry = {
|
|
|
31
32
|
type SetupDialogOptions = {
|
|
32
33
|
settingsPath: string
|
|
33
34
|
bundleName?: string
|
|
35
|
+
headerImagePath?: string
|
|
36
|
+
headerImageWidth?: number
|
|
37
|
+
headerHeight?: number
|
|
38
|
+
headerText?: string
|
|
34
39
|
shortcuts?: ActionAccelerator[]
|
|
35
40
|
shortcutsSourcePath?: string
|
|
36
41
|
shortcutBackupPath?: string
|
|
@@ -96,6 +101,17 @@ const getSetupDialogOptions = (options: string | SetupDialogOptions): SetupDialo
|
|
|
96
101
|
const getShortcutBackupPath = (options: SetupDialogOptions): string =>
|
|
97
102
|
`${App.getAppDataPath()}/${options.shortcutBackupPath ?? `${options.settingsPath}/keyboard-shortcuts-backup.json`}`
|
|
98
103
|
|
|
104
|
+
const hasSetupHeader = (options: SetupDialogOptions): boolean =>
|
|
105
|
+
Boolean(options.headerImagePath || options.headerText)
|
|
106
|
+
|
|
107
|
+
const resolveSetupAssetPath = (filePath: string | null | undefined): string => {
|
|
108
|
+
const value = String(filePath ?? '').replace(/\\/g, '/')
|
|
109
|
+
if (!value) return ''
|
|
110
|
+
if (value.indexOf(':') >= 0 || value.charAt(0) === '/') return value
|
|
111
|
+
|
|
112
|
+
return `${getScriptPath()}/${value.replace(/^\.\//, '')}`
|
|
113
|
+
}
|
|
114
|
+
|
|
99
115
|
const buildEntries = (actions: CustomAction[]): InstallerEntry[] => {
|
|
100
116
|
return actions
|
|
101
117
|
.filter((action) => Boolean(action.menuPath) || Boolean(action.toolbar))
|
|
@@ -180,9 +196,9 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
180
196
|
constructor(
|
|
181
197
|
private readonly entries: InstallerEntry[],
|
|
182
198
|
private readonly shortcutEntries: ShortcutEntry[],
|
|
183
|
-
|
|
199
|
+
private readonly options: SetupDialogOptions
|
|
184
200
|
) {
|
|
185
|
-
super(bundleName ? `${bundleName} Setup` : 'Setup Scripts', 'dsfSetupScripts')
|
|
201
|
+
super(options.bundleName ? `${options.bundleName} Setup` : 'Setup Scripts', 'dsfSetupScripts')
|
|
186
202
|
this.items$ = new Observable(entries.map(toTreeNode))
|
|
187
203
|
this.shortcutItems$ = new Observable(shortcutEntries.map(toShortcutTreeNode))
|
|
188
204
|
}
|
|
@@ -214,9 +230,13 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
214
230
|
private buildScriptsTab(): void {
|
|
215
231
|
const add = this.add
|
|
216
232
|
|
|
217
|
-
|
|
218
|
-
.
|
|
219
|
-
|
|
233
|
+
if (hasSetupHeader(this.options)) {
|
|
234
|
+
this.buildHeader()
|
|
235
|
+
} else {
|
|
236
|
+
add.label('Choose which scripts to install, then use the columns to review their shortcut, menu, and toolbar targets.')
|
|
237
|
+
.wordWrap()
|
|
238
|
+
.build()
|
|
239
|
+
}
|
|
220
240
|
add.group('Search').horizontal().build(() => {
|
|
221
241
|
add.edit()
|
|
222
242
|
.value(this.keywords$)
|
|
@@ -285,6 +305,20 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
285
305
|
})
|
|
286
306
|
}
|
|
287
307
|
|
|
308
|
+
private buildHeader(): void {
|
|
309
|
+
const add = this.add
|
|
310
|
+
const height = this.options.headerHeight ?? 96
|
|
311
|
+
const imageWidth = this.options.headerImageWidth ?? height
|
|
312
|
+
const headerText = String(this.options.headerText ?? 'Choose which scripts to install, then use the columns to review their shortcut, menu, and toolbar targets.')
|
|
313
|
+
|
|
314
|
+
add.header({
|
|
315
|
+
imagePath: resolveSetupAssetPath(this.options.headerImagePath),
|
|
316
|
+
imageWidth,
|
|
317
|
+
height,
|
|
318
|
+
html: headerText
|
|
319
|
+
}).build()
|
|
320
|
+
}
|
|
321
|
+
|
|
288
322
|
private buildShortcutsTab(): void {
|
|
289
323
|
const add = this.add
|
|
290
324
|
|
|
@@ -559,7 +593,7 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
559
593
|
const runDialog = (actions: CustomAction[], options: SetupDialogOptions): SetupSelection | null => {
|
|
560
594
|
const entries = buildEntries(actions)
|
|
561
595
|
const shortcutEntries = buildShortcutEntries(options.shortcuts)
|
|
562
|
-
const dialog = new InstallerSelectionDialog(entries, shortcutEntries, options
|
|
596
|
+
const dialog = new InstallerSelectionDialog(entries, shortcutEntries, options)
|
|
563
597
|
return dialog.ok() ? dialog.getSelections() : null
|
|
564
598
|
}
|
|
565
599
|
|
|
@@ -29,6 +29,10 @@ const writePng = (projectDir: string, fileName: string): void => {
|
|
|
29
29
|
fs.writeFileSync(path.join(projectDir, 'src', fileName), 'png')
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
const writeText = (projectDir: string, fileName: string, content: string): void => {
|
|
33
|
+
fs.writeFileSync(path.join(projectDir, 'src', fileName), content)
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
const generateSetup = (projectDir: string): string => {
|
|
33
37
|
const previousCwd = process.cwd()
|
|
34
38
|
process.chdir(projectDir)
|
|
@@ -103,3 +107,69 @@ describe('install generator action icons', () => {
|
|
|
103
107
|
expect(setup).not.toContain('"icon": "custom-icon.dsa.png"')
|
|
104
108
|
})
|
|
105
109
|
})
|
|
110
|
+
|
|
111
|
+
describe('install generator setup header', () => {
|
|
112
|
+
it('uses the explicit setup header image before the generic setup image', () => {
|
|
113
|
+
const projectDir = makeProject()
|
|
114
|
+
writeScript(projectDir, 'render-tools')
|
|
115
|
+
writePng(projectDir, 'Setup.header.png')
|
|
116
|
+
writePng(projectDir, 'Setup.png')
|
|
117
|
+
|
|
118
|
+
const setup = generateSetup(projectDir)
|
|
119
|
+
|
|
120
|
+
expect(setup).toContain('"headerImagePath":"./Setup.header.png"')
|
|
121
|
+
expect(setup).not.toContain('"headerImagePath":"./Setup.png"')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('falls back to the generic setup image when no explicit header image exists', () => {
|
|
125
|
+
const projectDir = makeProject()
|
|
126
|
+
writeScript(projectDir, 'render-tools')
|
|
127
|
+
writePng(projectDir, 'Setup.png')
|
|
128
|
+
|
|
129
|
+
const setup = generateSetup(projectDir)
|
|
130
|
+
|
|
131
|
+
expect(setup).toContain('"headerImagePath":"./Setup.png"')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('falls back to the setup script image when no header image exists', () => {
|
|
135
|
+
const projectDir = makeProject()
|
|
136
|
+
writeScript(projectDir, 'render-tools')
|
|
137
|
+
writePng(projectDir, 'Setup.dsa.png')
|
|
138
|
+
|
|
139
|
+
const setup = generateSetup(projectDir)
|
|
140
|
+
|
|
141
|
+
expect(setup).toContain('"headerImagePath":"./Setup.dsa.png"')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('embeds setup header markdown text into generated setup options', () => {
|
|
145
|
+
const projectDir = makeProject()
|
|
146
|
+
writeScript(projectDir, 'render-tools')
|
|
147
|
+
writeText(projectDir, 'Setup.header.md', 'Header line\n\nSecond line\n')
|
|
148
|
+
|
|
149
|
+
const setup = generateSetup(projectDir)
|
|
150
|
+
|
|
151
|
+
expect(setup).toContain('"headerText":"Header line\\n\\nSecond line"')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('prefers setup header html over markdown text', () => {
|
|
155
|
+
const projectDir = makeProject()
|
|
156
|
+
writeScript(projectDir, 'render-tools')
|
|
157
|
+
writeText(projectDir, 'Setup.header.html', '<h2>HTML Header</h2>')
|
|
158
|
+
writeText(projectDir, 'Setup.header.md', '# Markdown Header')
|
|
159
|
+
|
|
160
|
+
const setup = generateSetup(projectDir)
|
|
161
|
+
|
|
162
|
+
expect(setup).toContain('"headerText":"<h2>HTML Header</h2>"')
|
|
163
|
+
expect(setup).not.toContain('# Markdown Header')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('falls back to setup script markdown when no header text file exists', () => {
|
|
167
|
+
const projectDir = makeProject()
|
|
168
|
+
writeScript(projectDir, 'render-tools')
|
|
169
|
+
writeText(projectDir, 'Setup.md', 'Setup markdown fallback')
|
|
170
|
+
|
|
171
|
+
const setup = generateSetup(projectDir)
|
|
172
|
+
|
|
173
|
+
expect(setup).toContain('"headerText":"Setup markdown fallback"')
|
|
174
|
+
})
|
|
175
|
+
})
|