diff options
Diffstat (limited to 'ui/dialogs')
-rw-r--r-- | ui/dialogs/CancelDialog.js | 60 | ||||
-rw-r--r-- | ui/dialogs/ConfirmDialog.js | 76 | ||||
-rw-r--r-- | ui/dialogs/Dialog.js | 55 | ||||
-rw-r--r-- | ui/dialogs/FilePickerForm.js | 79 | ||||
-rw-r--r-- | ui/dialogs/OpenFileDialog.js | 108 | ||||
-rw-r--r-- | ui/dialogs/index.js | 16 |
6 files changed, 394 insertions, 0 deletions
diff --git a/ui/dialogs/CancelDialog.js b/ui/dialogs/CancelDialog.js new file mode 100644 index 0000000..9069d43 --- /dev/null +++ b/ui/dialogs/CancelDialog.js @@ -0,0 +1,60 @@ +import {Button, Form} from 'tui-lib/ui/controls' +import {Label, Pane} from 'tui-lib/ui/presentation' +import {FocusElement} from 'tui-lib/ui/primitives' + +import telc from 'tui-lib/util/telchars' + +export default class CancelDialog extends FocusElement { + // A basic cancel dialog. Has one buttons, cancel, and a label. + // The escape (esc) key can be used to exit the dialog (which sends a + // 'cancelled' event, as the cancel button also does). + + constructor(text) { + super() + + this.pane = new Pane() + this.addChild(this.pane) + + this.cancelBtn = new Button('Cancel') + this.pane.addChild(this.cancelBtn) + + this.label = new Label(text) + this.pane.addChild(this.label) + + this.initEventListeners() + } + + initEventListeners() { + this.cancelBtn.on('pressed', () => this.cancelPressed()) + } + + fixLayout() { + this.w = this.parent.contentW + this.h = this.parent.contentH + + this.pane.w = Math.max(40, 4 + this.label.w) + this.pane.h = 7 + this.pane.centerInParent() + + this.label.x = Math.floor((this.pane.contentW - this.label.w) / 2) + this.label.y = 1 + + this.cancelBtn.x = Math.floor( + (this.pane.contentW - this.cancelBtn.w) / 2) + this.cancelBtn.y = this.pane.contentH - 2 + } + + selected() { + this.root.select(this.cancelBtn) + } + + keyPressed(keyBuf) { + if (telc.isCancel(keyBuf)) { + this.emit('cancelled') + } + } + + cancelPressed() { + this.emit('cancelled') + } +} diff --git a/ui/dialogs/ConfirmDialog.js b/ui/dialogs/ConfirmDialog.js new file mode 100644 index 0000000..c0bcfae --- /dev/null +++ b/ui/dialogs/ConfirmDialog.js @@ -0,0 +1,76 @@ +import {Button, Form} from 'tui-lib/ui/controls' +import {Label, Pane} from 'tui-lib/ui/presentation' +import {FocusElement} from 'tui-lib/ui/primitives' + +import telc from 'tui-lib/util/telchars' + +export default class ConfirmDialog extends FocusElement { + // A basic yes/no dialog. Has two buttons, confirm/cancel, and a label. + // The escape (esc) key can be used to exit the dialog (which sends a + // 'cancelled' event, as the cancel button also does). + + constructor(text) { + super() + + this.pane = new Pane() + this.addChild(this.pane) + + this.form = new Form() + this.pane.addChild(this.form) + + this.confirmBtn = new Button('Confirm') + this.form.addInput(this.confirmBtn) + + this.cancelBtn = new Button('Cancel') + this.form.addInput(this.cancelBtn) + + this.label = new Label(text) + this.form.addChild(this.label) + + this.initEventListeners() + } + + initEventListeners() { + this.confirmBtn.on('pressed', () => this.confirmPressed()) + this.cancelBtn.on('pressed', () => this.cancelPressed()) + } + + fixLayout() { + this.w = this.parent.contentW + this.h = this.parent.contentH + + this.pane.w = Math.max(40, 2 + this.label.w) + this.pane.h = 7 + this.pane.centerInParent() + + this.form.w = this.pane.contentW + this.form.h = this.pane.contentH + + this.label.x = Math.floor((this.form.contentW - this.label.w) / 2) + this.label.y = 1 + + this.confirmBtn.x = 1 + this.confirmBtn.y = this.form.contentH - 2 + + this.cancelBtn.x = this.form.right - this.cancelBtn.w - 1 + this.cancelBtn.y = this.form.contentH - 2 + } + + selected() { + this.root.select(this.form) + } + + keyPressed(keyBuf) { + if (telc.isCancel(keyBuf)) { + this.emit('cancelled') + } + } + + confirmPressed() { + this.emit('confirmed') + } + + cancelPressed() { + this.emit('cancelled') + } +} diff --git a/ui/dialogs/Dialog.js b/ui/dialogs/Dialog.js new file mode 100644 index 0000000..19565f5 --- /dev/null +++ b/ui/dialogs/Dialog.js @@ -0,0 +1,55 @@ +import {Pane} from 'tui-lib/ui/presentation' +import {FocusElement} from 'tui-lib/ui/primitives' + +import telc from 'tui-lib/util/telchars' + +export default class Dialog extends FocusElement { + // A simple base dialog. + // + // Emits the 'cancelled' event when the cancel key (escape) is pressed, + // which should (probably) be handled by the dialog's creator. + // + // Doesn't do anything when focused by default - this should be overridden + // in subclasses. + // + // Automatically adjusts to fill its parent. Has a pane child (this.pane), + // but the pane isn't adjusted at all (you should change its size and + // likely center it in your subclass). + + constructor() { + super() + + this.pane = new Pane() + this.addChild(this.pane) + } + + fixLayout() { + this.w = this.parent.contentW + this.h = this.parent.contentH + } + + open() { + this.oldSelectedElement = this.root.selectedElement + this.opened() + this.visible = true + this.root.select(this) + this.fixLayout() + } + + close() { + this.closed() + this.visible = false + this.root.select(this.oldSelectedElement) + } + + opened() {} + + closed() {} + + keyPressed(keyBuf) { + if (telc.isCancel(keyBuf)) { + this.emit('cancelled') + return false + } + } +} diff --git a/ui/dialogs/FilePickerForm.js b/ui/dialogs/FilePickerForm.js new file mode 100644 index 0000000..6414818 --- /dev/null +++ b/ui/dialogs/FilePickerForm.js @@ -0,0 +1,79 @@ +import {readdir, stat} from 'node:fs/promises' +import path from 'node:path' + +import {compare as naturalCompare} from 'natural-orderby' + +import {Button, ListScrollForm} from 'tui-lib/ui/controls' + +export default class FilePickerForm extends ListScrollForm { + fillItems(dirPath) { + this.inputs = [] + this.children = [] + + const button = new Button('..Loading..') + this.addInput(button) + this.firstInput(false) + + readdir(dirPath).then( + async items => { + this.removeInput(button) + + const processedItems = await Promise.all(items.map(item => { + const itemPath = path.resolve(dirPath, item) + return stat(itemPath).then(s => { + return { + path: itemPath, + label: item + (s.isDirectory() ? '/' : ''), + isDirectory: s.isDirectory() + } + }) + })) + + const compare = naturalCompare() + processedItems.sort((a, b) => { + if (a.isDirectory === b.isDirectory) { + return compare(a.label, b.label) + } else { + if (a.isDirectory) { + return -1 + } else { + return +1 + } + } + }) + + processedItems.unshift({ + path: path.resolve(dirPath, '..'), + label: '../', + isDirectory: true + }) + + let y = 0 + for (const item of processedItems) { + const itemButton = new Button(item.label) + itemButton.y = y + y++ + this.addInput(itemButton) + + itemButton.on('pressed', () => { + if (item.isDirectory) { + this.emit('browsingDirectory', item.path) + this.fillItems(item.path) + } else { + this.emit('selected', item.path) + } + }) + } + + console.log('HALLO.', false) + this.firstInput(false) + this.fixLayout() + }, + () => { + button.text = 'Failed to read path! (Cancel)' + button.on('pressed', () => { + this.emit('canceled') + }) + }) + } +} diff --git a/ui/dialogs/OpenFileDialog.js b/ui/dialogs/OpenFileDialog.js new file mode 100644 index 0000000..970e291 --- /dev/null +++ b/ui/dialogs/OpenFileDialog.js @@ -0,0 +1,108 @@ +import path from 'node:path' + +import {Button, Form, TextInput} from 'tui-lib/ui/controls' +import {Label} from 'tui-lib/ui/presentation' + +import Dialog from './Dialog.js' +import FilePickerForm from './FilePickerForm.js' + +export default class OpenFileDialog extends Dialog { + constructor() { + super() + + this.visible = false + + this.form = new Form() + this.pane.addChild(this.form) + + this.filePathLabel = new Label('Enter file path:') + this.filePathInput = new TextInput() + this.openButton = new Button('Open') + this.cancelButton = new Button('Cancel') + + this.filePickerForm = new FilePickerForm() + this.filePickerForm.captureTab = false + + this.form.addChild(this.filePathLabel) + this.form.addInput(this.filePathInput) + this.form.addInput(this.filePickerForm) + this.form.addInput(this.openButton) + this.form.addInput(this.cancelButton) + + this._resolve = null + + this.openButton.on('pressed', () => { + this._resolve(this.filePathInput.value) + }) + + this.filePathInput.on('value', () => { + this._resolve(this.filePathInput.value) + }) + + { + const cb = append => p => { + this.filePathInput.setValue((path.relative(process.cwd(), p) || '.') + append) + } + + this.filePickerForm.on('selected', cb('')) + this.filePickerForm.on('browsingDirectory', cb('/')) + } + + this.cancelButton.on('pressed', () => { + this._resolve(null) + }) + + const dir = (this.lastFilePath + ? path.relative(process.cwd(), path.dirname(this.lastFilePath)) + '/' + : './') + + this.filePathInput.setValue(dir) + this.filePickerForm.fillItems(dir) + } + + fixLayout() { + super.fixLayout() + + this.pane.w = Math.min(this.contentW, 40) + this.pane.h = Math.min(this.contentH, 20) + this.pane.centerInParent() + + this.form.w = this.pane.contentW + this.form.h = this.pane.contentH + + this.filePathLabel.x = 0 + this.filePathLabel.y = 0 + + this.filePathInput.x = this.filePathLabel.right + 2 + this.filePathInput.y = this.filePathLabel.y + this.filePathInput.w = this.form.contentW - this.filePathInput.x + + this.filePickerForm.x = 0 + this.filePickerForm.y = this.filePathInput.y + 2 + this.filePickerForm.w = this.form.contentW + this.filePickerForm.h = this.form.contentH - this.filePickerForm.y - 2 + + this.openButton.x = 0 + this.openButton.y = this.form.contentH - 1 + + this.cancelButton.x = this.openButton.right + 2 + this.cancelButton.y = this.openButton.y + } + + selected() { + this.form.firstInput() + } + + go() { + this.visible = true + this.root.select(this) + + return new Promise(resolve => { + this._resolve = resolve + }).then(filePath => { + this.visible = false + this.lastFilePath = filePath + return filePath + }) + } +} diff --git a/ui/dialogs/index.js b/ui/dialogs/index.js new file mode 100644 index 0000000..5cb9f04 --- /dev/null +++ b/ui/dialogs/index.js @@ -0,0 +1,16 @@ +// +// Import mapping: +// +// controls, presentation, primitives -> +// CancelDialog +// ConfirmDialog +// +// Dialog -> OpenFileDialog +// FilePickerForm -> OpenFileDialog +// + +export {default as CancelDialog} from './CancelDialog.js' +export {default as ConfirmDialog} from './ConfirmDialog.js' +export {default as Dialog} from './Dialog.js' +export {default as FilePickerForm} from './FilePickerForm.js' +export {default as OpenFileDialog} from './OpenFileDialog.js' |