dazscript-framework
Version:
The **DazScript Framework** is a TypeScript-based framework for writing Daz Studio scripts. It provides all the advantages of a typed language such as autocompletion, error checking, and method parameter documentation and hinting. The framework also inclu
328 lines (277 loc) • 10.8 kB
text/typescript
import { warn } from '@dsf/common/log';
import { clearColumns, filter, getDataItem, setDataItem } from '@dsf/helpers/list-view-helper';
import { Delayed } from '@dsf/lib/delayed';
import { Observable } from '@dsf/lib/observable';
import { TreeNode } from '@dsf/lib/tree-node';
import { IWidgetBuilder, createWidget } from './widget-builder';
import { WidgetBuilderContext } from './widgets-builder';
type ListViewFilterOptions = {
keywords: Observable<string>,
field: (listItem: DzListViewItem) => string,
selectOnFilter?: boolean,
filters?: (viewItem: DzListViewItem) => boolean,
delay?: { min: number, max: number }
}
export enum ListViewRefreshOptions {
All,
Filters
}
export class ListViewBuilder<TItem, TData> implements IWidgetBuilder<DzListView> {
private readonly context: ListViewBuilderContext<TItem, TData>
constructor(dialogContext: WidgetBuilderContext) {
this.context = new ListViewBuilderContext(dialogContext)
}
visible(value: boolean | Observable<boolean>): this {
this.context.visible = typeof value === 'boolean'
? new Observable(value)
: value
return this
}
/**
* Binds the list rows to a collection of items
* @param items
* @returns
*/
items(items: Observable<TreeNode<TItem>[]>): ListViewBindBuilder<TItem, TData> {
this.context.items = items
return new ListViewBindBuilder(this.context)
}
/**
* Build an item row with the provided function
* @param build the function to use for building a row
* @returns returns a ListViewItem or DzCheckListItem
*/
row(build: (item: TreeNode<TItem>, parent: DzListView | DzListViewItem, id?: number) => DzListViewItem): this {
this.context.rowBuilder = build
return this
}
sorting(sort: boolean | number): this {
if (typeof sort === 'boolean')
this.context.sorting = sort ? 0 : -1
else
this.context.sorting = sort
return this
}
expanded(onOff: boolean | Observable<boolean>): this {
if (typeof onOff === 'boolean')
this.context.expanded = new Observable(onOff)
else
this.context.expanded = onOff
return this
}
flat(onOff: boolean | Observable<boolean>): this {
this.context.flat = typeof onOff === 'boolean'
? new Observable(onOff)
: onOff
return this
}
doubleClicked(bind: Observable<TData>): this {
this.context.doubleClicked = bind
return this
}
refresh(when: Observable<any>, what: ListViewRefreshOptions = ListViewRefreshOptions.All): this {
this.context.refreshWhen = when
this.context.refreshWhat = what
return this
}
contextMenu(fn: (listView: DzListView, item: DzListViewItem, pos: Point) => DzPopupMenu): this {
this.context.contextMenu = fn
return this
}
build(then?: (listView: DzListView) => void): DzListView {
let listView = build(this.context)
then?.(listView)
return listView
}
}
class ListViewBuilderContext<TItem, TData> {
columns: Observable<string[]> = new Observable([])
columnsWidth: (index: number, width: number) => number
items: Observable<TreeNode<TItem>[]> = new Observable([])
text: (item: TreeNode<TItem>) => string[]
data: (item: TreeNode<TItem>) => TData
sorting: number = 0
selected: Observable<TData>
filter: ListViewFilterOptions
doubleClicked: Observable<TData>
rowBuilder: (item: TreeNode<TItem>, parent: DzListView | DzListViewItem, id?: number) => DzListViewItem
contextMenu: (listView: DzListView, item: DzListViewItem, pos: Point) => DzPopupMenu
refreshWhen: Observable<void>
expanded: Observable<boolean>
visible: Observable<boolean> = new Observable(true)
decorated: boolean
flat: Observable<boolean>
refreshWhat: ListViewRefreshOptions = ListViewRefreshOptions.All
constructor(public readonly dialogContext: WidgetBuilderContext) { }
}
class ListViewBindBuilder<TItem, TData> {
constructor(private context: ListViewBuilderContext<TItem, TData>) { }
/**
* Sepecify the text to display on each column
* @param columns
* @returns
*/
columns(columns: string[] | Observable<string[]>, width?: (index: number, width: number) => number): this {
this.context.columns = columns instanceof Observable
? columns
: new Observable(columns)
this.context.columnsWidth = width
return this
}
/**
* Callback for extracting the text to display on each column from every item in the list
* @param from the callback function to extract the text for each item
* @returns a string array for every column text to be display for a given item
*/
text(from: (item: TreeNode<TItem>) => string[]): this {
this.context.text = from
return this
}
/**
* Callback to specify which part of the item will be stored in each row as data. It can be
* the whole item or a property of the item
* @param from the callback function to specify what to store as data
* @returns the data to be stored in each row for a given item
*/
data(from: (item: TreeNode<TItem>) => TData): this {
this.context.data = from
return this
}
/**
* Bind the data of a selected row in the list to an object
* @param bind
* @returns
*/
selected(bind: Observable<TData>): this {
this.context.selected = bind
return this
}
filter(options: ListViewFilterOptions): this {
this.context.filter = options
return this
}
build(then?: (listView: DzListView) => void): DzListView {
let listView = build(this.context)
then?.(listView)
return listView
}
}
const build = <TItem, TData>(context: ListViewBuilderContext<TItem, TData>): DzListView => {
const listView = createWidget(context.dialogContext).build(DzListView)
let rowId = -1
if (context.visible) {
if (context.visible.value === false)
listView.hide()
context.visible.connect((visible) => {
if (visible)
listView.show()
else
listView.hide()
})
}
const buildColumns = (columns: string[]) => {
clearColumns(listView)
columns.forEach(column => {
listView.addColumn(column)
})
}
const buildItem = (item: TreeNode<TItem>, parent: DzListView | DzListViewItem) => {
rowId++
parent = context.flat?.value === true ? listView : parent
let listItem = context.rowBuilder ? context.rowBuilder(item, parent, rowId) : new DzListViewItem(parent, rowId)
if (!listItem) return
listItem.open = context.expanded?.value === true
context.text(item).forEach((text, idx) => {
let data = context.data?.(item)
if (data) {
setDataItem(listItem, data)
}
if (!text || idx >= context.columns.value.length)
return;
listItem.setText(idx, text)
})
item.children.forEach((child) => {
context.decorated = true
buildItem(child, listItem)
})
}
const filterList = (keywords?: string) => {
filter(listView, context.filter.field, keywords ?? context.filter?.keywords?.value, { selectOnFilter: context.filter.selectOnFilter ?? true, filters: context.filter.filters })
}
const buildList = (items: TreeNode<TItem>[], selectedId?: number) => {
rowId = -1
if (!context.text)
return warn('No text function provided for list builder')
listView.clear()
context.columns?.value.forEach((_, index) => {
listView.setColumnWidth(index, 0)
})
listView.allColumnsShowFocus = true
if (context.visible.value === false)
return
if (!items) return
items.forEach((item) => {
buildItem(item, listView)
})
if (context.columnsWidth) {
context.columns?.value.forEach((_, index) => {
listView.setColumnWidth(index, context.columnsWidth(index, listView.columnWidth(index)))
})
}
listView.rootIsDecorated = context.decorated
listView.setSorting(context.sorting)
if (context.sorting >= 0) listView.sort()
if (context.filter?.keywords.value || context.filter?.filters)
filterList()
if (selectedId) {
listView.getItems(DzListView.All).forEach(item => {
if (item.id === selectedId) {
listView.setSelected(item, true)
listView.setCurrentItem(item)
listView.ensureItemVisible(item)
}
})
}
}
const onSelectionChanged = (callback: (item: DzListViewItem, data: TData) => void) => {
(listView as any)["selectionChanged()"].scriptConnect(() => {
callback(listView.selectedItem(), getDataItem(listView.selectedItem()))
})
}
buildColumns(context.columns.value)
context.columns.connect((columns) => {
buildColumns(columns)
})
buildList(context.items?.value)
context.items.connect((items) => {
buildList(items, listView.selectedItem()?.id)
})
context.refreshWhen?.connect(() => {
if (context.refreshWhat === ListViewRefreshOptions.All)
buildList(context.items?.value, listView.selectedItem()?.id)
else {
filterList()
}
})
if (context.selected) {
onSelectionChanged((_, data) => {
context.selected.value = data
})
}
if (context.filter) {
context.filter.keywords.connect((keywords) => {
new Delayed(() => {
filterList(keywords)
}, context.filter.delay?.min ?? 100, context.filter.delay?.max ?? 400).trigger()
})
}
if (context.doubleClicked) {
listView.doubleClicked.scriptConnect((listItem) => {
context.doubleClicked.value = listItem?.getDataItem('data')
})
}
listView.contextMenuRequested.scriptConnect((item: DzListViewItem, pos: Point) => {
context.contextMenu?.(listView, item, pos).exec(pos.cursorPos(), 0)
})
return listView
}