dazscript-framework 0.3.2 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +361 -265
- package/dist/scripts/install-generator.js +85 -14
- package/package.json +7 -4
- package/src/Setup.dsa.ts +37 -9
- package/src/dialog/builders/list-view-builder.ts +5 -0
- package/src/examples/01-hello-world.dsa.ts +8 -0
- package/src/examples/02-persistence-dialog.dsa.ts +21 -0
- package/src/examples/02-persistence-dialog.ts +68 -0
- package/src/examples/03-simple-dialog.dsa.ts +23 -0
- package/src/examples/03-simple-dialog.ts +47 -0
- package/src/examples/04-settings-dialog.dsa.ts +29 -0
- package/src/examples/04-settings-dialog.ts +83 -0
- package/src/examples/05-list-dialog.dsa.ts +53 -0
- package/src/examples/05-list-dialog.ts +88 -0
- package/src/examples/06-showcase-dialog.dsa.ts +87 -0
- package/src/examples/06-showcase-dialog.ts +518 -0
- package/src/helpers/custom-action-helper.ts +2 -1
- package/src/helpers/custom-action-installer-helper.ts +306 -20
- package/src/lib/observable.test.ts +416 -0
- package/src/lib/observable.ts +24 -18
- package/src/lib/tree-node.test.ts +21 -0
- package/tsconfig.json +28 -112
- package/webpack.config.js +7 -0
- package/src/samples/hello-world.dsa.ts +0 -8
- package/src/samples/sample-dialog.dsa.ts +0 -47
- package/src/samples/sample-dialog.ts +0 -49
- /package/src/{samples → examples}/config.ts +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { BasicDialog } from '@dsf/dialog/basic-dialog'
|
|
2
|
+
import { PopupMenuItem } from '@dsf/dialog/builders/popup-menu-builder'
|
|
3
|
+
import { getDataItem } from '@dsf/helpers/list-view-helper'
|
|
4
|
+
import { Observable } from '@dsf/lib/observable'
|
|
5
|
+
import { TreeNode } from '@dsf/lib/tree-node'
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
|
|
9
|
+
export interface FileItem {
|
|
10
|
+
name: string
|
|
11
|
+
type: 'folder' | 'file'
|
|
12
|
+
size: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Model
|
|
16
|
+
|
|
17
|
+
export class ListDialogModel {
|
|
18
|
+
keywords$ = new Observable('')
|
|
19
|
+
files$ = new Observable<TreeNode<FileItem>[]>([])
|
|
20
|
+
selected$ = new Observable<FileItem>()
|
|
21
|
+
recentFiles$ = new Observable<string[]>([])
|
|
22
|
+
selectedRecent$ = new Observable<string[]>([])
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Dialog
|
|
26
|
+
|
|
27
|
+
export class ListDialog extends BasicDialog {
|
|
28
|
+
constructor(private readonly model: ListDialogModel) {
|
|
29
|
+
super('05 List View')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected build(): void {
|
|
33
|
+
let add = this.add
|
|
34
|
+
let model = this.model
|
|
35
|
+
|
|
36
|
+
this.builder.options({ resizable: true, width: 640, height: 520 })
|
|
37
|
+
|
|
38
|
+
// Search bar
|
|
39
|
+
add.horizontal((layout) => {
|
|
40
|
+
layout.spacing = 5
|
|
41
|
+
add.label('Search:').minWidth(50)
|
|
42
|
+
add.edit()
|
|
43
|
+
.value(model.keywords$)
|
|
44
|
+
.placeholder('Filter files...')
|
|
45
|
+
.focus()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Tree list with context menu and keyword filter
|
|
49
|
+
add.list.view<FileItem>()
|
|
50
|
+
.expanded(true)
|
|
51
|
+
.items(model.files$)
|
|
52
|
+
.columns(['Name', 'Type', 'Size (KB)'])
|
|
53
|
+
.text(item => [
|
|
54
|
+
item.name,
|
|
55
|
+
item.value?.type ?? '',
|
|
56
|
+
item.value ? String(Math.round(item.value.size / 1024)) : '',
|
|
57
|
+
])
|
|
58
|
+
.data(item => item.value)
|
|
59
|
+
.selected(model.selected$)
|
|
60
|
+
.filter({
|
|
61
|
+
keywords: model.keywords$,
|
|
62
|
+
field: li => li.text(0),
|
|
63
|
+
selectOnFilter: true,
|
|
64
|
+
})
|
|
65
|
+
.contextMenu((_, li) => {
|
|
66
|
+
let file = getDataItem<FileItem>(li)
|
|
67
|
+
let label = file?.name ?? 'item'
|
|
68
|
+
let items: PopupMenuItem[] = [
|
|
69
|
+
{ text: `Open "${label}"`, activated: () => { /* open */ } },
|
|
70
|
+
{ text: `Rename "${label}"`, activated: () => { /* rename */ } },
|
|
71
|
+
{ text: '' },
|
|
72
|
+
{ text: 'Copy Path', activated: () => { /* copy to clipboard */ } },
|
|
73
|
+
{ text: 'Delete', activated: () => { /* delete */ } },
|
|
74
|
+
]
|
|
75
|
+
return add.contextMenu().items(...items).build()
|
|
76
|
+
})
|
|
77
|
+
.build()
|
|
78
|
+
|
|
79
|
+
// Simple list box for recently opened files
|
|
80
|
+
add.group('Recent Files').build(() => {
|
|
81
|
+
add.list.box()
|
|
82
|
+
.items(model.recentFiles$)
|
|
83
|
+
.mode('extended')
|
|
84
|
+
.selected(model.selectedRecent$)
|
|
85
|
+
.build()
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { debug } from '@dsf/common/log'
|
|
2
|
+
import { action } from '@dsf/core/action'
|
|
3
|
+
import { info } from '@dsf/helpers/message-box-helper'
|
|
4
|
+
import { TreeNode } from '@dsf/lib/tree-node'
|
|
5
|
+
import { SceneObject, ShowcaseDialog, ShowcaseDialogModel } from './06-showcase-dialog'
|
|
6
|
+
|
|
7
|
+
// Sample data
|
|
8
|
+
|
|
9
|
+
function buildSceneTree(): TreeNode<SceneObject>[] {
|
|
10
|
+
let id = 0
|
|
11
|
+
const next = () => ++id
|
|
12
|
+
|
|
13
|
+
const group = (name: string, children: TreeNode<SceneObject>[] = []) =>
|
|
14
|
+
new TreeNode<SceneObject>(
|
|
15
|
+
name, name,
|
|
16
|
+
{ id: next(), name, type: 'Group', visible: true, locked: false },
|
|
17
|
+
children,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const obj = (name: string, type: SceneObject['type'], visible = true, locked = false) =>
|
|
21
|
+
new TreeNode<SceneObject>(name, name, { id: next(), name, type, visible, locked })
|
|
22
|
+
|
|
23
|
+
return [
|
|
24
|
+
group('Characters', [
|
|
25
|
+
obj('Victoria 9', 'Figure'),
|
|
26
|
+
obj('Michael 9', 'Figure', true, true),
|
|
27
|
+
obj('Genesis 9', 'Figure', false),
|
|
28
|
+
]),
|
|
29
|
+
group('Environment', [
|
|
30
|
+
obj('Studio Floor', 'Prop'),
|
|
31
|
+
obj('Background', 'Prop', false),
|
|
32
|
+
group('Lights', [
|
|
33
|
+
obj('Key Light', 'Light'),
|
|
34
|
+
obj('Fill Light', 'Light', false),
|
|
35
|
+
obj('Rim Light', 'Light'),
|
|
36
|
+
]),
|
|
37
|
+
]),
|
|
38
|
+
obj('Camera 1', 'Camera'),
|
|
39
|
+
obj('Camera 2', 'Camera', false),
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Action
|
|
44
|
+
|
|
45
|
+
action({ text: '06 Showcase Dialog' }, () => {
|
|
46
|
+
let model = new ShowcaseDialogModel()
|
|
47
|
+
let dialog = new ShowcaseDialog(model)
|
|
48
|
+
|
|
49
|
+
model.objects$.value = buildSceneTree()
|
|
50
|
+
|
|
51
|
+
model.logEntries$.value = [
|
|
52
|
+
'[Info] Script started',
|
|
53
|
+
'[Info] Scene loaded - 4 root nodes',
|
|
54
|
+
'[Debug] Settings restored from DzAppSettings',
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
// Keep the detail panel in sync with the selected object.
|
|
58
|
+
model.selectedObject$.connect(obj => {
|
|
59
|
+
model.detailName$.value = obj?.name ?? ''
|
|
60
|
+
model.detailType$.value = obj?.type ?? ''
|
|
61
|
+
model.detailVisible$.value = obj?.visible ?? false
|
|
62
|
+
model.detailLocked$.value = obj?.locked ?? false
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (!dialog.run()) {
|
|
66
|
+
debug('Showcase dialog cancelled')
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let sel = model.selectedObject$.value
|
|
71
|
+
let settings = model.settings
|
|
72
|
+
const lines = sel
|
|
73
|
+
? [
|
|
74
|
+
`Selected : ${sel.name}`,
|
|
75
|
+
`Type : ${sel.type}`,
|
|
76
|
+
`Visible : ${sel.visible}`,
|
|
77
|
+
`Locked : ${sel.locked}`,
|
|
78
|
+
``,
|
|
79
|
+
`Quality : ${settings.quality$.value}`,
|
|
80
|
+
`Samples : ${settings.samples$.value}`,
|
|
81
|
+
`Format : ${settings.exportFormat$.value}`,
|
|
82
|
+
]
|
|
83
|
+
: ['No object selected']
|
|
84
|
+
|
|
85
|
+
debug(lines.join('\n'))
|
|
86
|
+
info(lines.join('\n'))
|
|
87
|
+
})
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { BasicDialog } from '@dsf/dialog/basic-dialog'
|
|
2
|
+
import { PopupMenuItem } from '@dsf/dialog/builders/popup-menu-builder'
|
|
3
|
+
import { getDataItem } from '@dsf/helpers/list-view-helper'
|
|
4
|
+
import { AppSettings } from '@dsf/lib/settings'
|
|
5
|
+
import { Observable } from '@dsf/lib/observable'
|
|
6
|
+
import { TreeNode } from '@dsf/lib/tree-node'
|
|
7
|
+
import { config } from './config'
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
|
|
11
|
+
export interface SceneObject {
|
|
12
|
+
id: number
|
|
13
|
+
name: string
|
|
14
|
+
type: 'Figure' | 'Prop' | 'Light' | 'Camera' | 'Group'
|
|
15
|
+
visible: boolean
|
|
16
|
+
locked: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Settings (all persisted via DzAppSettings)
|
|
20
|
+
|
|
21
|
+
export class ShowcaseDialogSettings extends AppSettings {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(`${config.author}/06-ShowcaseDialog`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Objects tab
|
|
27
|
+
activeTab$ = this.bindInt('activeTab$', 0)
|
|
28
|
+
flatList$ = this.bindBoolean('flatList$', false)
|
|
29
|
+
showIcons$ = this.bindBoolean('showIcons$', true)
|
|
30
|
+
showVisibleOnly$ = this.bindBoolean('showVisibleOnly$', false)
|
|
31
|
+
splitterState$ = this.bindString('splitterState$', '')
|
|
32
|
+
filterType$ = this.bindString('filterType$', 'All')
|
|
33
|
+
|
|
34
|
+
// Settings tab - Render
|
|
35
|
+
quality$ = this.bindString('quality$', 'Standard')
|
|
36
|
+
samples$ = this.bindInt('samples$', 64)
|
|
37
|
+
scale$ = this.bindFloat('scale$', 1.0)
|
|
38
|
+
|
|
39
|
+
// Settings tab - Export
|
|
40
|
+
exportFormat$ = this.bindString('exportFormat$', 'JSON')
|
|
41
|
+
outputPath$ = this.bindString('outputPath$', '')
|
|
42
|
+
autoSave$ = this.bindBoolean('autoSave$', false)
|
|
43
|
+
|
|
44
|
+
// Settings tab - Behaviour
|
|
45
|
+
notifications$ = this.bindBoolean('notifications$', true)
|
|
46
|
+
logLevel$ = this.bindString('logLevel$', 'Info')
|
|
47
|
+
|
|
48
|
+
// About tab
|
|
49
|
+
showFilters$ = this.bindBoolean('showFilters$', false)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Model
|
|
53
|
+
|
|
54
|
+
export class ShowcaseDialogModel {
|
|
55
|
+
readonly settings = new ShowcaseDialogSettings()
|
|
56
|
+
|
|
57
|
+
objects$ = new Observable<TreeNode<SceneObject>[]>([])
|
|
58
|
+
selectedObject$ = new Observable<SceneObject>()
|
|
59
|
+
refreshObjects$ = new Observable<void>()
|
|
60
|
+
keywords$ = new Observable('')
|
|
61
|
+
logEntries$ = new Observable<string[]>([])
|
|
62
|
+
sceneStats$ = new Observable('')
|
|
63
|
+
selectedSummary$ = new Observable('No object selected')
|
|
64
|
+
|
|
65
|
+
// Detail panel fields - updated externally when selectedObject$ changes
|
|
66
|
+
detailName$ = new Observable('')
|
|
67
|
+
detailType$ = new Observable('')
|
|
68
|
+
detailVisible$ = new Observable(false)
|
|
69
|
+
detailLocked$ = new Observable(false)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Dialog
|
|
73
|
+
|
|
74
|
+
export class ShowcaseDialog extends BasicDialog {
|
|
75
|
+
private sceneListView: DzListView | null = null
|
|
76
|
+
|
|
77
|
+
constructor(private readonly model: ShowcaseDialogModel) {
|
|
78
|
+
super('06 Showcase Dialog', `${config.author}/06-ShowcaseDialog`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
protected build(): void {
|
|
82
|
+
let add = this.add
|
|
83
|
+
let settings = this.model.settings
|
|
84
|
+
|
|
85
|
+
this.builder.options({ resizable: true, width: 780, height: 640 })
|
|
86
|
+
this.dialog.setAcceptButtonText('Apply')
|
|
87
|
+
this.dialog.setCancelButtonText('Close')
|
|
88
|
+
|
|
89
|
+
this.connectInteractions()
|
|
90
|
+
|
|
91
|
+
// Active tab index persists across runs via bindInt.
|
|
92
|
+
add.tab('Objects').bind(settings.activeTab$).build(() => {
|
|
93
|
+
this.buildObjectsTab()
|
|
94
|
+
})
|
|
95
|
+
add.tab('Settings').build(() => {
|
|
96
|
+
this.buildSettingsTab()
|
|
97
|
+
})
|
|
98
|
+
add.tab('About').build(() => {
|
|
99
|
+
this.buildAboutTab()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
this.updateSceneStats()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Tab 1: Objects
|
|
106
|
+
|
|
107
|
+
private buildObjectsTab(): void {
|
|
108
|
+
let add = this.add
|
|
109
|
+
let model = this.model
|
|
110
|
+
let settings = model.settings
|
|
111
|
+
|
|
112
|
+
// Toolbar row
|
|
113
|
+
add.group('Search').horizontal().build((layout) => {
|
|
114
|
+
layout.spacing = 4
|
|
115
|
+
add.edit()
|
|
116
|
+
.value(model.keywords$)
|
|
117
|
+
.placeholder('Filter objects...')
|
|
118
|
+
.focus()
|
|
119
|
+
add.label('Type:')
|
|
120
|
+
add.combo()
|
|
121
|
+
.items(['All', 'Figure', 'Prop', 'Light', 'Camera', 'Group'])
|
|
122
|
+
.selected(settings.filterType$)
|
|
123
|
+
add.button('Refresh')
|
|
124
|
+
.clicked(() => model.refreshObjects$.trigger())
|
|
125
|
+
.toolTip('Reload the object list')
|
|
126
|
+
add.button('Apply Checked Rows')
|
|
127
|
+
.clicked(() => this.applyCheckedRows())
|
|
128
|
+
.toolTip('Copies the list checkbox states back to the scene objects.')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
add.horizontal((layout) => {
|
|
132
|
+
layout.spacing = 6
|
|
133
|
+
add.checkbox('Flat List').value(settings.flatList$)
|
|
134
|
+
add.checkbox('Show Icons').value(settings.showIcons$)
|
|
135
|
+
add.checkbox('Visible Only').value(settings.showVisibleOnly$)
|
|
136
|
+
const statsLabel = add.label(model.sceneStats$.value).build()
|
|
137
|
+
model.sceneStats$.connect(text => { statsLabel.text = text })
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Splitter - position persisted via bindString
|
|
141
|
+
add.splitter()
|
|
142
|
+
.state(settings.splitterState$)
|
|
143
|
+
.strech(2, 1)
|
|
144
|
+
.items(
|
|
145
|
+
// Left pane: tree list with filter + context menu
|
|
146
|
+
add.group().style({ flat: true }).build(() => {
|
|
147
|
+
add.list.view<SceneObject, SceneObject>()
|
|
148
|
+
.row((item, parent, id) => {
|
|
149
|
+
let obj = item.value as SceneObject
|
|
150
|
+
let listItem = new DzCheckListItem(parent, item.isLeaf ? DzCheckListItem.CheckBox : DzCheckListItem.CheckBoxController, id)
|
|
151
|
+
listItem.selectable = true
|
|
152
|
+
listItem.on = obj.visible
|
|
153
|
+
return listItem
|
|
154
|
+
})
|
|
155
|
+
.expanded(true)
|
|
156
|
+
.flat(settings.flatList$)
|
|
157
|
+
.refresh(model.refreshObjects$)
|
|
158
|
+
.items(model.objects$)
|
|
159
|
+
.columns(['Visible', 'Name', 'Type', 'Locked'])
|
|
160
|
+
.text(item => [
|
|
161
|
+
'',
|
|
162
|
+
this.getDisplayName(item.value),
|
|
163
|
+
item.value?.type ?? '',
|
|
164
|
+
item.value?.locked ? 'Yes' : 'No',
|
|
165
|
+
])
|
|
166
|
+
.data(item => item.value)
|
|
167
|
+
.selected(model.selectedObject$)
|
|
168
|
+
.filter({
|
|
169
|
+
keywords: model.keywords$,
|
|
170
|
+
field: li => li.text(0),
|
|
171
|
+
selectOnFilter: true,
|
|
172
|
+
filters: li => {
|
|
173
|
+
let obj = getDataItem<SceneObject>(li)
|
|
174
|
+
if (!obj) return true
|
|
175
|
+
let type = settings.filterType$.value
|
|
176
|
+
let matchesType = type === 'All' || obj.type === type
|
|
177
|
+
let matchesVisibility = !settings.showVisibleOnly$.value || obj.visible
|
|
178
|
+
return matchesType && matchesVisibility
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
.contextMenu((_, li) => {
|
|
182
|
+
let obj = getDataItem<SceneObject>(li)
|
|
183
|
+
let label = obj?.name ?? 'item'
|
|
184
|
+
let items: PopupMenuItem[] = [
|
|
185
|
+
{
|
|
186
|
+
text: `Select "${label}"`,
|
|
187
|
+
activated: () => { /* select in viewport */ },
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
text: obj?.visible ? 'Hide' : 'Show',
|
|
191
|
+
activated: () => {
|
|
192
|
+
if (obj) obj.visible = !obj.visible
|
|
193
|
+
model.refreshObjects$.trigger()
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{ text: '' }, // separator
|
|
197
|
+
{
|
|
198
|
+
text: 'Duplicate',
|
|
199
|
+
activated: () => { /* duplicate */ },
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
text: 'Delete',
|
|
203
|
+
activated: () => { /* delete */ },
|
|
204
|
+
},
|
|
205
|
+
]
|
|
206
|
+
return add.contextMenu().items(...items).build()
|
|
207
|
+
})
|
|
208
|
+
.build((listView) => {
|
|
209
|
+
this.sceneListView = listView
|
|
210
|
+
listView.allColumnsShowFocus = true
|
|
211
|
+
})
|
|
212
|
+
}),
|
|
213
|
+
|
|
214
|
+
// Right pane: detail panel for selected object
|
|
215
|
+
add.group('Details').build(() => {
|
|
216
|
+
add.horizontal((layout) => {
|
|
217
|
+
layout.spacing = 4
|
|
218
|
+
add.label('Name:').minWidth(45)
|
|
219
|
+
add.edit().readOnly().value(model.detailName$)
|
|
220
|
+
})
|
|
221
|
+
add.horizontal((layout) => {
|
|
222
|
+
layout.spacing = 4
|
|
223
|
+
add.label('Type:').minWidth(45)
|
|
224
|
+
add.edit().readOnly().value(model.detailType$)
|
|
225
|
+
})
|
|
226
|
+
add.group('Flags').horizontal().build(() => {
|
|
227
|
+
add.checkbox('Visible').value(model.detailVisible$)
|
|
228
|
+
add.checkbox('Locked').value(model.detailLocked$)
|
|
229
|
+
})
|
|
230
|
+
const summaryLabel = add.label(model.selectedSummary$.value).wordWrap().build()
|
|
231
|
+
model.selectedSummary$.connect(text => { summaryLabel.text = text })
|
|
232
|
+
add.group('Scene Node').build(() => {
|
|
233
|
+
// NodeSelection lets the user pick any node from the scene.
|
|
234
|
+
add.nodeSelection().build()
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
)
|
|
238
|
+
.build()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Tab 2: Settings
|
|
242
|
+
|
|
243
|
+
private buildSettingsTab(): void {
|
|
244
|
+
let add = this.add
|
|
245
|
+
let settings = this.model.settings
|
|
246
|
+
|
|
247
|
+
add.horizontal((layout) => {
|
|
248
|
+
layout.spacing = 8
|
|
249
|
+
|
|
250
|
+
// Left column
|
|
251
|
+
add.vertical(() => {
|
|
252
|
+
add.group('Display').build(() => {
|
|
253
|
+
add.checkbox('Flat List').value(settings.flatList$)
|
|
254
|
+
add.checkbox('Show Icons').value(settings.showIcons$)
|
|
255
|
+
add.checkbox('Visible Only').value(settings.showVisibleOnly$)
|
|
256
|
+
add.horizontal((layout) => {
|
|
257
|
+
layout.spacing = 4
|
|
258
|
+
add.label('Object Type:').minWidth(80)
|
|
259
|
+
add.combo()
|
|
260
|
+
.items(['All', 'Figure', 'Prop', 'Light', 'Camera', 'Group'])
|
|
261
|
+
.selected(settings.filterType$)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// Radio buttons - initialize from persisted quality value;
|
|
266
|
+
// toggled() writes back when the user picks a different option.
|
|
267
|
+
add.group('Render Quality').build(() => {
|
|
268
|
+
add.radio('Draft')
|
|
269
|
+
.value(settings.quality$.value === 'Draft')
|
|
270
|
+
.toggled(v => {
|
|
271
|
+
if (!v) return
|
|
272
|
+
settings.quality$.value = 'Draft'
|
|
273
|
+
settings.samples$.value = 16
|
|
274
|
+
settings.scale$.value = 0.5
|
|
275
|
+
})
|
|
276
|
+
add.radio('Standard')
|
|
277
|
+
.value(settings.quality$.value === 'Standard')
|
|
278
|
+
.toggled(v => {
|
|
279
|
+
if (!v) return
|
|
280
|
+
settings.quality$.value = 'Standard'
|
|
281
|
+
settings.samples$.value = 64
|
|
282
|
+
settings.scale$.value = 1.0
|
|
283
|
+
})
|
|
284
|
+
add.radio('High')
|
|
285
|
+
.value(settings.quality$.value === 'High')
|
|
286
|
+
.toggled(v => {
|
|
287
|
+
if (!v) return
|
|
288
|
+
settings.quality$.value = 'High'
|
|
289
|
+
settings.samples$.value = 256
|
|
290
|
+
settings.scale$.value = 2.0
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// Right column
|
|
296
|
+
add.vertical(() => {
|
|
297
|
+
add.group('Render').build(() => {
|
|
298
|
+
add.horizontal((layout) => {
|
|
299
|
+
layout.spacing = 4
|
|
300
|
+
add.label('Samples:').minWidth(55)
|
|
301
|
+
add.slider('integer')
|
|
302
|
+
.value(settings.samples$)
|
|
303
|
+
.min(1).max(512)
|
|
304
|
+
.build()
|
|
305
|
+
})
|
|
306
|
+
add.horizontal((layout) => {
|
|
307
|
+
layout.spacing = 4
|
|
308
|
+
add.label('Scale:').minWidth(55)
|
|
309
|
+
add.slider('float')
|
|
310
|
+
.value(settings.scale$)
|
|
311
|
+
.min(0.1).max(5.0)
|
|
312
|
+
.build()
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
add.group('Export').build(() => {
|
|
317
|
+
add.horizontal((layout) => {
|
|
318
|
+
layout.spacing = 4
|
|
319
|
+
add.label('Output:').minWidth(55)
|
|
320
|
+
add.edit()
|
|
321
|
+
.value(settings.outputPath$)
|
|
322
|
+
.placeholder('Default output path...')
|
|
323
|
+
})
|
|
324
|
+
add.horizontal((layout) => {
|
|
325
|
+
layout.spacing = 4
|
|
326
|
+
add.label('Format:').minWidth(55)
|
|
327
|
+
// ComboBox - two-way bind to persisted string
|
|
328
|
+
add.combo()
|
|
329
|
+
.items(['JSON', 'XML', 'CSV'])
|
|
330
|
+
.selected(settings.exportFormat$)
|
|
331
|
+
})
|
|
332
|
+
add.checkbox('Auto Save').value(settings.autoSave$)
|
|
333
|
+
add.label('Auto Save enables notifications and switches the log level to Debug.').wordWrap().build()
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
// Color pickers - read the final color from the widget on accept
|
|
339
|
+
add.group('Colors').horizontal().build(() => {
|
|
340
|
+
add.vertical(() => {
|
|
341
|
+
add.label('Background:').build()
|
|
342
|
+
add.color().build()
|
|
343
|
+
})
|
|
344
|
+
add.vertical(() => {
|
|
345
|
+
add.label('Foreground:').build()
|
|
346
|
+
add.color().build()
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
add.group('Behaviour').horizontal().build(() => {
|
|
351
|
+
add.checkbox('Notifications').value(settings.notifications$)
|
|
352
|
+
add.horizontal((layout) => {
|
|
353
|
+
layout.spacing = 4
|
|
354
|
+
add.label('Log Level:')
|
|
355
|
+
// ComboEdit: user can pick from list OR type a custom level
|
|
356
|
+
add.comboEdit()
|
|
357
|
+
.items(['Debug', 'Info', 'Warning', 'Error'])
|
|
358
|
+
.changed(settings.logLevel$)
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Tab 3: About
|
|
364
|
+
|
|
365
|
+
private buildAboutTab(): void {
|
|
366
|
+
let add = this.add
|
|
367
|
+
let model = this.model
|
|
368
|
+
let settings = model.settings
|
|
369
|
+
|
|
370
|
+
add.group('About This Example').build(() => {
|
|
371
|
+
add.label([
|
|
372
|
+
'DAZScript Framework - Showcase Dialog (Example 06)',
|
|
373
|
+
'',
|
|
374
|
+
'Widgets demonstrated in this dialog:',
|
|
375
|
+
' - Tabs with persisted active-tab index (bindInt)',
|
|
376
|
+
' - Splitter with persisted position (bindString)',
|
|
377
|
+
' - list.view - columns, keyword filter, custom filters,',
|
|
378
|
+
' checkbox rows, context menu, refresh trigger, flat/tree toggle,',
|
|
379
|
+
' expanded, data binding, selection binding',
|
|
380
|
+
' - list.box - scrollable list with multi-select',
|
|
381
|
+
' - GroupBox - vertical / horizontal / flat style / visible toggle',
|
|
382
|
+
' - Label - wordWrap, minWidth, alignment',
|
|
383
|
+
' - LineEdit - value binding, placeholder, readOnly',
|
|
384
|
+
' - CheckBox, RadioButton - Observable two-way binding',
|
|
385
|
+
' - Button - clicked, toggle, toolTip',
|
|
386
|
+
' - Slider - integer and float with min/max',
|
|
387
|
+
' - ColorPicker (DzColorWgt)',
|
|
388
|
+
' - ComboBox - items + selected binding',
|
|
389
|
+
' - ComboEdit - items + changed/edited binding',
|
|
390
|
+
' - NodeSelection combo (scene node picker)',
|
|
391
|
+
' - All settings persisted via AppSettings (DzAppSettings)',
|
|
392
|
+
].join('\n')).wordWrap(true).build()
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// Log list - populated externally; demonstrates Observable<string[]> binding
|
|
396
|
+
add.group('Recent Log').build(() => {
|
|
397
|
+
add.list.box()
|
|
398
|
+
.items(model.logEntries$)
|
|
399
|
+
.mode('single')
|
|
400
|
+
.build()
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
// Conditional filter row - group visible when showFilters$ is true
|
|
404
|
+
add.group('Optional Filters').horizontal()
|
|
405
|
+
.visible(settings.showFilters$)
|
|
406
|
+
.build(() => {
|
|
407
|
+
add.label('Tag:').minWidth(30)
|
|
408
|
+
add.comboEdit()
|
|
409
|
+
.items(['render', 'test', 'draft', 'final'])
|
|
410
|
+
.build()
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
add.horizontal((layout) => {
|
|
414
|
+
layout.spacing = 4
|
|
415
|
+
add.checkbox('Show Filters').value(settings.showFilters$)
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private connectInteractions(): void {
|
|
420
|
+
let model = this.model
|
|
421
|
+
let settings = model.settings
|
|
422
|
+
|
|
423
|
+
settings.filterType$.connect(() => model.refreshObjects$.trigger())
|
|
424
|
+
settings.showVisibleOnly$.connect(() => model.refreshObjects$.trigger())
|
|
425
|
+
settings.showIcons$.connect(() => model.refreshObjects$.trigger())
|
|
426
|
+
settings.flatList$.connect(() => model.refreshObjects$.trigger())
|
|
427
|
+
|
|
428
|
+
settings.autoSave$.connect((enabled) => {
|
|
429
|
+
if (!enabled) return
|
|
430
|
+
settings.notifications$.value = true
|
|
431
|
+
settings.logLevel$.value = 'Debug'
|
|
432
|
+
this.addLog('Auto Save enabled; notifications and Debug logging selected.')
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
model.objects$.connect(() => this.updateSceneStats())
|
|
436
|
+
model.refreshObjects$.connect(() => this.updateSceneStats())
|
|
437
|
+
|
|
438
|
+
model.selectedObject$.connect(obj => {
|
|
439
|
+
model.selectedSummary$.value = obj
|
|
440
|
+
? `${obj.name}: ${obj.type}, ${obj.visible ? 'visible' : 'hidden'}, ${obj.locked ? 'locked' : 'unlocked'}`
|
|
441
|
+
: 'No object selected'
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
model.detailVisible$.connect((visible) => {
|
|
445
|
+
let obj = model.selectedObject$.value
|
|
446
|
+
if (!obj || obj.visible === visible) return
|
|
447
|
+
obj.visible = visible
|
|
448
|
+
this.addLog(`${obj.name} visibility changed from the detail checkbox.`)
|
|
449
|
+
model.refreshObjects$.trigger()
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
model.detailLocked$.connect((locked) => {
|
|
453
|
+
let obj = model.selectedObject$.value
|
|
454
|
+
if (!obj || obj.locked === locked) return
|
|
455
|
+
obj.locked = locked
|
|
456
|
+
this.addLog(`${obj.name} lock state changed from the detail checkbox.`)
|
|
457
|
+
model.refreshObjects$.trigger()
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private getDisplayName(obj: SceneObject | null): string {
|
|
462
|
+
if (!obj) return ''
|
|
463
|
+
if (!this.model.settings.showIcons$.value) return obj.name
|
|
464
|
+
|
|
465
|
+
const prefixByType: Record<SceneObject['type'], string> = {
|
|
466
|
+
Figure: '[F]',
|
|
467
|
+
Prop: '[P]',
|
|
468
|
+
Light: '[L]',
|
|
469
|
+
Camera: '[C]',
|
|
470
|
+
Group: '[G]',
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return `${prefixByType[obj.type]} ${obj.name}`
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private addLog(message: string): void {
|
|
477
|
+
let entries = this.model.logEntries$.value.slice()
|
|
478
|
+
entries.push(`[Info] ${message}`)
|
|
479
|
+
if (entries.length > 8) entries.shift()
|
|
480
|
+
this.model.logEntries$.value = entries
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private updateSceneStats(): void {
|
|
484
|
+
let objects = this.getAllObjects()
|
|
485
|
+
let visible = objects.filter(obj => obj.visible).length
|
|
486
|
+
let locked = objects.filter(obj => obj.locked).length
|
|
487
|
+
this.model.sceneStats$.value = `${visible}/${objects.length} visible, ${locked} locked`
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private getAllObjects(): SceneObject[] {
|
|
491
|
+
let objects: SceneObject[] = []
|
|
492
|
+
this.model.objects$.value.forEach(root => {
|
|
493
|
+
root.forEach(node => {
|
|
494
|
+
if (node.value) objects.push(node.value)
|
|
495
|
+
})
|
|
496
|
+
})
|
|
497
|
+
return objects
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private applyCheckedRows(): void {
|
|
501
|
+
if (!this.sceneListView) return
|
|
502
|
+
|
|
503
|
+
this.sceneListView.getItems(DzListView.All).forEach(item => {
|
|
504
|
+
if (!(item as any).inherits('DzCheckListItem')) return
|
|
505
|
+
|
|
506
|
+
let obj = getDataItem<SceneObject>(item)
|
|
507
|
+
if (!obj) return
|
|
508
|
+
|
|
509
|
+
let checkbox = item as DzCheckListItem
|
|
510
|
+
obj.visible = checkbox.state === 0 && !checkbox.on
|
|
511
|
+
? false
|
|
512
|
+
: checkbox.on
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
this.addLog('List checkbox states applied to scene objects.')
|
|
516
|
+
this.model.refreshObjects$.trigger()
|
|
517
|
+
}
|
|
518
|
+
}
|