mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
Compare commits
5 Commits
de82435f6e
...
b103e3c690
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b103e3c690 | ||
|
|
7edc3e32b1 | ||
|
|
6db6a2e7ed | ||
|
|
0d564d5f82 | ||
|
|
6d20d346f5 |
@@ -49,13 +49,6 @@ _fsh.QA_CMD_WIDTH = 60 // command path field width in dialog
|
|||||||
_fsh.HL_FG = 230
|
_fsh.HL_FG = 230
|
||||||
_fsh.HL_BG = 255
|
_fsh.HL_BG = 255
|
||||||
|
|
||||||
// Dialog colour pair. Background MUST be opaque (bg 255 is transparent
|
|
||||||
// in TSVM and lets the pixel-layer wallpaper bleed through dialog cells).
|
|
||||||
_fsh.DIALOG_FG = 254
|
|
||||||
_fsh.DIALOG_BG = 242
|
|
||||||
_fsh.FIELD_BG = 240
|
|
||||||
_fsh.DIALOG_DIM_FG = 249
|
|
||||||
|
|
||||||
// Default Quick Access entries when fshrc is missing or empty
|
// Default Quick Access entries when fshrc is missing or empty
|
||||||
_fsh.DEFAULT_QA = [
|
_fsh.DEFAULT_QA = [
|
||||||
["Files", "/tvdos/bin/zsh.js"],
|
["Files", "/tvdos/bin/zsh.js"],
|
||||||
@@ -158,232 +151,6 @@ _fsh.saveConfig = function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the bordered popup background. (row, col) is the top-left, (h, w)
|
|
||||||
// the size. Paints an opaque interior first (otherwise the wallpaper bleeds
|
|
||||||
// through cells with bg 255), then delegates frame drawing to wintex so the
|
|
||||||
// corner/edge glyphs always connect correctly.
|
|
||||||
_fsh.drawDialogFrame = function(row, col, h, w, title) {
|
|
||||||
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
|
||||||
for (let y = 0; y < h; y++) {
|
|
||||||
con.move(row + y, col)
|
|
||||||
print(' '.repeat(w))
|
|
||||||
}
|
|
||||||
let wo = new win.WindowObject(col, row, w, h, function(){}, function(){}, title)
|
|
||||||
wo.isHighlighted = true
|
|
||||||
wo.titleBack = _fsh.DIALOG_BG
|
|
||||||
wo.drawFrame()
|
|
||||||
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slide the visible window so the caret stays inside (cursor at the
|
|
||||||
// rightmost column once it passes the field width).
|
|
||||||
_fsh.fieldScroll = function(cursor, width) {
|
|
||||||
return cursor < width ? 0 : cursor - width + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw a single-line bordered input field at (row, col) with given width.
|
|
||||||
// content is the current text; cursor is the caret offset within content
|
|
||||||
// focused brightens the border colour.
|
|
||||||
_fsh.drawDialogField = function(row, col, width, content, cursor, focused) {
|
|
||||||
let frameFg = focused ? _fsh.DIALOG_FG : _fsh.DIALOG_DIM_FG
|
|
||||||
// Clear the field area (3 rows × width+2 cols) with FIELD_BG first so any
|
|
||||||
// stale chars from a previous render are wiped before we draw on top.
|
|
||||||
con.color_pair(_fsh.DIALOG_FG, _fsh.FIELD_BG)
|
|
||||||
con.move(row + 1, col + 1)
|
|
||||||
print(' '.repeat(width))
|
|
||||||
// Top border
|
|
||||||
con.color_pair(frameFg, _fsh.DIALOG_BG)
|
|
||||||
con.move(row, col)
|
|
||||||
print('\u00DA') // ┌
|
|
||||||
print('\u00C4'.repeat(width)) // ─
|
|
||||||
print('\u00BF') // ┐
|
|
||||||
// Vertical borders + content
|
|
||||||
con.move(row + 1, col)
|
|
||||||
print('\u00B3') // │
|
|
||||||
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
|
||||||
let scroll = _fsh.fieldScroll(cursor, width)
|
|
||||||
let visible = content.substring(scroll, scroll + width)
|
|
||||||
print(visible)
|
|
||||||
con.color_pair(frameFg, _fsh.DIALOG_BG)
|
|
||||||
con.move(row + 1, col + width + 1)
|
|
||||||
print('\u00B3') // │
|
|
||||||
// Bottom border
|
|
||||||
con.move(row + 2, col)
|
|
||||||
print('\u00C0') // └
|
|
||||||
print('\u00C4'.repeat(width)) // ─
|
|
||||||
print('\u00D9') // ┘
|
|
||||||
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw a button as "[ Label ]" at the given position; highlights when focused.
|
|
||||||
_fsh.drawDialogButton = function(row, col, label, focused) {
|
|
||||||
if (focused) con.color_pair(_fsh.HL_FG, _fsh.DIALOG_BG)
|
|
||||||
else con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
|
||||||
con.move(row, col)
|
|
||||||
print("[ " + label + " ]")
|
|
||||||
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modal dialog. opts = {
|
|
||||||
// title: string,
|
|
||||||
// fields: [{label, initial, width}, ...],
|
|
||||||
// allowDelete: bool,
|
|
||||||
// }
|
|
||||||
// Returns {action: "ok"|"cancel"|"delete", values: [string, ...]}.
|
|
||||||
_fsh.showDialog = function(opts) {
|
|
||||||
let fields = opts.fields
|
|
||||||
let values = fields.map(function(f) { return f.initial || "" })
|
|
||||||
// Caret position per field. Start at end of any pre-filled initial text.
|
|
||||||
let cursors = values.map(function(v) { return v.length })
|
|
||||||
|
|
||||||
// Layout
|
|
||||||
let maxFieldW = fields.reduce(function(m, f) { return Math.max(m, f.width) }, 16)
|
|
||||||
let titleW = (opts.title ? opts.title.length : 0) + 4
|
|
||||||
let w = Math.max(maxFieldW + 6, titleW + 4, 24)
|
|
||||||
let buttonsRow = 2 + fields.length * 4 + 1 // 1 label + 3 field rows per field
|
|
||||||
let h = buttonsRow + 2
|
|
||||||
let screen = con.getmaxyx()
|
|
||||||
let row = Math.max(2, Math.floor((screen[0] - h) / 2))
|
|
||||||
let col = Math.max(2, Math.floor((screen[1] - w) / 2))
|
|
||||||
|
|
||||||
// Buttons list: indices follow Tab order after the last field
|
|
||||||
let buttons = [{label: "OK", action: "ok"}, {label: "Cancel", action: "cancel"}]
|
|
||||||
if (opts.allowDelete) buttons.splice(1, 0, {label: "Delete", action: "delete"})
|
|
||||||
|
|
||||||
let focusIdx = 0 // 0..fields.length-1 = field; then buttons
|
|
||||||
let totalFocus = fields.length + buttons.length
|
|
||||||
let done = null // {action, values} when set
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
_fsh.drawDialogFrame(row, col, h, w, opts.title)
|
|
||||||
// Fields
|
|
||||||
for (let i = 0; i < fields.length; i++) {
|
|
||||||
let labelRow = row + 1 + i * 4
|
|
||||||
let fieldRow = labelRow + 1
|
|
||||||
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
|
||||||
con.move(labelRow, col + 2)
|
|
||||||
print(fields[i].label + ":")
|
|
||||||
_fsh.drawDialogField(fieldRow, col + 2, fields[i].width,
|
|
||||||
values[i], cursors[i], i === focusIdx)
|
|
||||||
}
|
|
||||||
// Buttons centred on buttonsRow
|
|
||||||
let totalBtnW = buttons.reduce(function(s, b) { return s + b.label.length + 5 }, 0) - 1
|
|
||||||
let bx = col + Math.floor((w - totalBtnW) / 2)
|
|
||||||
for (let i = 0; i < buttons.length; i++) {
|
|
||||||
let bIdx = fields.length + i
|
|
||||||
_fsh.drawDialogButton(row + buttonsRow, bx, buttons[i].label, bIdx === focusIdx)
|
|
||||||
bx += buttons[i].label.length + 5
|
|
||||||
}
|
|
||||||
// Position the visible caret. Inside a field: place it on the content
|
|
||||||
// row at the cursor offset (corrected for horizontal scroll). On a
|
|
||||||
// button: hide the caret entirely.
|
|
||||||
if (focusIdx < fields.length) {
|
|
||||||
let fldWidth = fields[focusIdx].width
|
|
||||||
let scroll = _fsh.fieldScroll(cursors[focusIdx], fldWidth)
|
|
||||||
let contentRow = row + 1 + focusIdx * 4 + 2
|
|
||||||
let contentCol = col + 2 + 1 + (cursors[focusIdx] - scroll)
|
|
||||||
con.move(contentRow, contentCol)
|
|
||||||
con.curs_set(1)
|
|
||||||
} else {
|
|
||||||
con.curs_set(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render()
|
|
||||||
|
|
||||||
// Note: con.getch() returns TSVM scancodes (defined in JS_INIT.js as
|
|
||||||
// con.KEY_UP=200, KEY_DOWN=208, KEY_LEFT=203, KEY_RIGHT=205,
|
|
||||||
// con.KEY_BACKSPACE=8, KEY_TAB=9, KEY_RETURN=10). Esc isn't in JS_INIT's
|
|
||||||
// map — it arrives as ASCII 27 via keyTyped().
|
|
||||||
while (done === null) {
|
|
||||||
let k = con.getch()
|
|
||||||
|
|
||||||
if (k === 27) { // Esc
|
|
||||||
done = {action: "cancel", values: values}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (k === con.KEY_TAB) {
|
|
||||||
focusIdx = (focusIdx + 1) % totalFocus
|
|
||||||
render()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Up/Down always cycles focus across fields/buttons.
|
|
||||||
if (k === con.KEY_UP) {
|
|
||||||
focusIdx = (focusIdx - 1 + totalFocus) % totalFocus
|
|
||||||
render()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (k === con.KEY_DOWN) {
|
|
||||||
focusIdx = (focusIdx + 1) % totalFocus
|
|
||||||
render()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Left/Right moves the caret inside a field; on a button it cycles.
|
|
||||||
if (k === con.KEY_LEFT) {
|
|
||||||
if (focusIdx < fields.length) {
|
|
||||||
if (cursors[focusIdx] > 0) {
|
|
||||||
cursors[focusIdx] -= 1
|
|
||||||
render()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
focusIdx = (focusIdx - 1 + totalFocus) % totalFocus
|
|
||||||
render()
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (k === con.KEY_RIGHT) {
|
|
||||||
if (focusIdx < fields.length) {
|
|
||||||
if (cursors[focusIdx] < values[focusIdx].length) {
|
|
||||||
cursors[focusIdx] += 1
|
|
||||||
render()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
focusIdx = (focusIdx + 1) % totalFocus
|
|
||||||
render()
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// On a field
|
|
||||||
if (focusIdx < fields.length) {
|
|
||||||
if (k === con.KEY_RETURN) {
|
|
||||||
if (focusIdx < fields.length - 1) {
|
|
||||||
focusIdx += 1
|
|
||||||
} else {
|
|
||||||
focusIdx = fields.length // move to OK button
|
|
||||||
}
|
|
||||||
render()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (k === con.KEY_BACKSPACE) {
|
|
||||||
let c = cursors[focusIdx]
|
|
||||||
if (c > 0) {
|
|
||||||
let v = values[focusIdx]
|
|
||||||
values[focusIdx] = v.substring(0, c - 1) + v.substring(c)
|
|
||||||
cursors[focusIdx] = c - 1
|
|
||||||
render()
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Printable: insert at the caret.
|
|
||||||
if (k >= 32 && k < 256 && values[focusIdx].length < fields[focusIdx].width * 4) {
|
|
||||||
let v = values[focusIdx]
|
|
||||||
let c = cursors[focusIdx]
|
|
||||||
values[focusIdx] = v.substring(0, c) + String.fromCharCode(k) + v.substring(c)
|
|
||||||
cursors[focusIdx] = c + 1
|
|
||||||
render()
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// On a button
|
|
||||||
if (k === con.KEY_RETURN || k === 32) {
|
|
||||||
done = {action: buttons[focusIdx - fields.length].action, values: values}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
con.curs_set(0)
|
|
||||||
return done
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map (mouse char x, mouse char y) to a row index for a widget drawn at
|
// Map (mouse char x, mouse char y) to a row index for a widget drawn at
|
||||||
// (xoff, yoff) with `length` existing entries and `maxRows` total rows.
|
// (xoff, yoff) with `length` existing entries and `maxRows` total rows.
|
||||||
@@ -673,7 +440,7 @@ _fsh.redrawAll = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fsh.openAddTodoDialog = function() {
|
_fsh.openAddTodoDialog = function() {
|
||||||
let res = _fsh.showDialog({
|
let res = win.showDialog({
|
||||||
title: "New Todo",
|
title: "New Todo",
|
||||||
fields: [{label: "Text", initial: "", width: _fsh.TODO_TEXT_WIDTH}],
|
fields: [{label: "Text", initial: "", width: _fsh.TODO_TEXT_WIDTH}],
|
||||||
allowDelete: false
|
allowDelete: false
|
||||||
@@ -690,7 +457,7 @@ _fsh.openAddTodoDialog = function() {
|
|||||||
_fsh.openEditTodoDialog = function(index) {
|
_fsh.openEditTodoDialog = function(index) {
|
||||||
let entry = todoWidget.todoList[index]
|
let entry = todoWidget.todoList[index]
|
||||||
if (!entry) return
|
if (!entry) return
|
||||||
let res = _fsh.showDialog({
|
let res = win.showDialog({
|
||||||
title: "Edit Todo",
|
title: "Edit Todo",
|
||||||
fields: [{label: "Text", initial: entry[0], width: _fsh.TODO_TEXT_WIDTH}],
|
fields: [{label: "Text", initial: entry[0], width: _fsh.TODO_TEXT_WIDTH}],
|
||||||
allowDelete: true
|
allowDelete: true
|
||||||
@@ -709,7 +476,7 @@ _fsh.openEditTodoDialog = function(index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fsh.openAddQaDialog = function() {
|
_fsh.openAddQaDialog = function() {
|
||||||
let res = _fsh.showDialog({
|
let res = win.showDialog({
|
||||||
title: "New Quick Access",
|
title: "New Quick Access",
|
||||||
fields: [
|
fields: [
|
||||||
{label: "Label", initial: "", width: _fsh.QA_LABEL_WIDTH},
|
{label: "Label", initial: "", width: _fsh.QA_LABEL_WIDTH},
|
||||||
@@ -730,7 +497,7 @@ _fsh.openAddQaDialog = function() {
|
|||||||
_fsh.openEditQaDialog = function(index) {
|
_fsh.openEditQaDialog = function(index) {
|
||||||
let entry = quickAccessWidget.entries[index]
|
let entry = quickAccessWidget.entries[index]
|
||||||
if (!entry) return
|
if (!entry) return
|
||||||
let res = _fsh.showDialog({
|
let res = win.showDialog({
|
||||||
title: "Edit Quick Access",
|
title: "Edit Quick Access",
|
||||||
fields: [
|
fields: [
|
||||||
{label: "Label", initial: entry[0], width: _fsh.QA_LABEL_WIDTH},
|
{label: "Label", initial: entry[0], width: _fsh.QA_LABEL_WIDTH},
|
||||||
@@ -908,9 +675,13 @@ while (true) {
|
|||||||
let navRight = edge(KEY_RIGHT)
|
let navRight = edge(KEY_RIGHT)
|
||||||
|
|
||||||
// -- mouse --
|
// -- mouse --
|
||||||
|
// MMIO returns VM-screen pixel coords (origin at the top-left of the framebuffer).
|
||||||
|
// Widget xoff/yoff are passed straight into con.move(y, x), which is 1-indexed, so
|
||||||
|
// we offset by +1 here. Without this the click registers one cell up-and-left from
|
||||||
|
// where the user's pointer is, because pixel 0 = con.move(1, 1).
|
||||||
let pos = readMousePos()
|
let pos = readMousePos()
|
||||||
let charX = (pos[0] / 7) | 0
|
let charX = (pos[0] / 7 | 0) + 1
|
||||||
let charY = (pos[1] / 14) | 0
|
let charY = (pos[1] / 14 | 0) + 1
|
||||||
let mouseMoved = (charX !== prevMouseCharX || charY !== prevMouseCharY)
|
let mouseMoved = (charX !== prevMouseCharX || charY !== prevMouseCharY)
|
||||||
prevMouseCharX = charX
|
prevMouseCharX = charX
|
||||||
prevMouseCharY = charY
|
prevMouseCharY = charY
|
||||||
|
|||||||
@@ -1114,13 +1114,18 @@ inputwork.repeatCount = 0;
|
|||||||
* where:
|
* where:
|
||||||
* "key_down", <key symbol string>, <repeat count>, keycode0, keycode1 .. keycode7
|
* "key_down", <key symbol string>, <repeat count>, keycode0, keycode1 .. keycode7
|
||||||
* "key_change", <key symbol string (what went up)>, 0, keycode0, keycode1 .. keycode7 (remaining keys that are held down)
|
* "key_change", <key symbol string (what went up)>, 0, keycode0, keycode1 .. keycode7 (remaining keys that are held down)
|
||||||
* "mouse_down", pos-x, pos-y, 1 // yes there's only one mouse button :p
|
* "mouse_down", pos-x, pos-y, <button mask: 1=left, 2=right, 4=middle>, keycode0..keycode7
|
||||||
* "mouse_up", pos-x, pos-y, 0
|
* "mouse_up", pos-x, pos-y, <button mask of the released button>, keycode0..keycode7
|
||||||
* "mouse_move", pos-x, pos-y, <button down?>, oldpos-x, oldpos-y
|
* "mouse_move", pos-x, pos-y, <currently-held button mask>, oldpos-x, oldpos-y, keycode0..keycode7
|
||||||
|
* "mouse_wheel", pos-x, pos-y, <-1 for wheel up, +1 for wheel down>, keycode0..keycode7
|
||||||
|
*
|
||||||
|
* Button mask values come from MMIO[36] bits 0..2 (terranmon.txt:52-58). The wheel
|
||||||
|
* bits (6, 7) latch in hardware and clear on read, so a one-shot detent fires once.
|
||||||
|
* Every mouse event carries the currently-held key buffer (same shape as key_down)
|
||||||
|
* so handlers can detect modifiers like Shift+wheel via `event.includes(<keysym>)`.
|
||||||
*/
|
*/
|
||||||
input.withEvent = function(callback) {
|
input.withEvent = function(callback) {
|
||||||
|
|
||||||
// TODO mouse event
|
|
||||||
function arrayEq(a,b) {
|
function arrayEq(a,b) {
|
||||||
for (let i = 0; i < a.length; ++i) {
|
for (let i = 0; i < a.length; ++i) {
|
||||||
if (a[i] !== b[i]) return false;
|
if (a[i] !== b[i]) return false;
|
||||||
@@ -1141,7 +1146,33 @@ input.withEvent = function(callback) {
|
|||||||
|
|
||||||
sys.poke(-40, 255);
|
sys.poke(-40, 255);
|
||||||
let keys = [sys.peek(-41),sys.peek(-42),sys.peek(-43),sys.peek(-44),sys.peek(-45),sys.peek(-46),sys.peek(-47),sys.peek(-48)];
|
let keys = [sys.peek(-41),sys.peek(-42),sys.peek(-43),sys.peek(-44),sys.peek(-45),sys.peek(-46),sys.peek(-47),sys.peek(-48)];
|
||||||
let mouse = [sys.peek(-33) | (sys.peek(-34) << 8), sys.peek(-35) | (sys.peek(-36) << 8), sys.peek(-37)];
|
let mx = (sys.peek(-33) & 0xFF) | ((sys.peek(-34) & 0xFF) << 8);
|
||||||
|
let my = (sys.peek(-35) & 0xFF) | ((sys.peek(-36) & 0xFF) << 8);
|
||||||
|
let mb = sys.peek(-37) & 0xFF; // bits 0..2 = L/R/M held, bit 6 = wheel up, bit 7 = wheel down
|
||||||
|
let mouse = [mx, my, mb];
|
||||||
|
|
||||||
|
// --- mouse dispatch ---
|
||||||
|
let oldMouse = inputwork.oldMouse;
|
||||||
|
let hasOld = oldMouse && oldMouse.length === 3;
|
||||||
|
let oldBtns = hasOld ? (oldMouse[2] & 0x07) : 0;
|
||||||
|
let curBtns = mb & 0x07;
|
||||||
|
let wheelUp = (mb & 0x40) !== 0;
|
||||||
|
let wheelDn = (mb & 0x80) !== 0;
|
||||||
|
|
||||||
|
if (wheelUp) callback(["mouse_wheel", mx, my, -1].concat(keys));
|
||||||
|
if (wheelDn) callback(["mouse_wheel", mx, my, 1].concat(keys));
|
||||||
|
|
||||||
|
let pressed = curBtns & ~oldBtns;
|
||||||
|
let released = oldBtns & ~curBtns;
|
||||||
|
for (let b = 1; b <= 4; b <<= 1) {
|
||||||
|
if (pressed & b) callback(["mouse_down", mx, my, b].concat(keys));
|
||||||
|
if (released & b) callback(["mouse_up", mx, my, b].concat(keys));
|
||||||
|
}
|
||||||
|
if (hasOld && (mx !== oldMouse[0] || my !== oldMouse[1])) {
|
||||||
|
callback(["mouse_move", mx, my, curBtns, oldMouse[0], oldMouse[1]].concat(keys));
|
||||||
|
}
|
||||||
|
// --- end mouse dispatch ---
|
||||||
|
|
||||||
let keyChanged = !arrayEq(keys, inputwork.oldKeys)
|
let keyChanged = !arrayEq(keys, inputwork.oldKeys)
|
||||||
let keyDiff = arrayDiff(keys, inputwork.oldKeys)
|
let keyDiff = arrayDiff(keys, inputwork.oldKeys)
|
||||||
|
|
||||||
|
|||||||
@@ -3378,23 +3378,48 @@ function openHelpPopup() {
|
|||||||
repaint()
|
repaint()
|
||||||
|
|
||||||
let done = false
|
let done = false
|
||||||
|
const buttons = makePopupButtonRow(HELP_POPUP_Y + HELP_POPUP_H - 1, HELP_POPUP_X, HELP_POPUP_W, [
|
||||||
|
{ label: 'OK', action: () => { done = true }, default: true },
|
||||||
|
])
|
||||||
|
buttons.repaint()
|
||||||
|
|
||||||
let eventJustReceived = true
|
let eventJustReceived = true
|
||||||
|
|
||||||
|
pushMousePopup(buttons.regions.concat([
|
||||||
|
// Scroll body: wheel scrolls help text.
|
||||||
|
{ x: HELP_CONTENT_X, y: HELP_CONTENT_Y, w: HELP_CONTENT_W, h: HELP_CONTENT_H, onWheel: (cy, cx, dy) => {
|
||||||
|
scroll += dy * 3
|
||||||
|
if (scroll < 0) scroll = 0
|
||||||
|
if (scroll > maxScroll) scroll = maxScroll
|
||||||
|
repaint()
|
||||||
|
buttons.repaint()
|
||||||
|
}},
|
||||||
|
]))
|
||||||
|
|
||||||
|
const scrollAndRepaint = () => { repaint(); buttons.repaint() }
|
||||||
|
|
||||||
while (!done) {
|
while (!done) {
|
||||||
input.withEvent(ev => {
|
input.withEvent(ev => {
|
||||||
|
if (eventJustReceived && (ev[0] === 'key_down' || ev[0] === 'mouse_down')) {
|
||||||
|
eventJustReceived = false; return
|
||||||
|
}
|
||||||
|
if (dispatchMouseEvent(ev)) return
|
||||||
if (ev[0] !== 'key_down') return
|
if (ev[0] !== 'key_down') return
|
||||||
if (eventJustReceived) { eventJustReceived = false; return }
|
|
||||||
const ks = ev[1]
|
const ks = ev[1]
|
||||||
|
const shiftDown = (ev.includes(59) || ev.includes(60))
|
||||||
|
|
||||||
if (ks === '<ESC>' || ks === '!' || ks === 'q' || ks === '\n') { done = true }
|
if (buttons.keyHandler(ks, shiftDown)) return
|
||||||
else if (ks === '<UP>') { if (scroll > 0) { scroll -= 1; repaint() } }
|
if (ks === '<ESC>' || ks === '!' || ks === 'q') { done = true }
|
||||||
else if (ks === '<DOWN>') { if (scroll < maxScroll) { scroll += 1; repaint() } }
|
else if (ks === '<UP>') { if (scroll > 0) { scroll -= 1; scrollAndRepaint() } }
|
||||||
else if (ks === '<PAGE_UP>') { scroll = Math.max(0, scroll - HELP_CONTENT_H); repaint() }
|
else if (ks === '<DOWN>') { if (scroll < maxScroll) { scroll += 1; scrollAndRepaint() } }
|
||||||
else if (ks === '<PAGE_DOWN>') { scroll = Math.min(maxScroll, scroll + HELP_CONTENT_H); repaint() }
|
else if (ks === '<PAGE_UP>') { scroll = Math.max(0, scroll - HELP_CONTENT_H); scrollAndRepaint() }
|
||||||
else if (ks === '<HOME>') { scroll = 0; repaint() }
|
else if (ks === '<PAGE_DOWN>') { scroll = Math.min(maxScroll, scroll + HELP_CONTENT_H); scrollAndRepaint() }
|
||||||
else if (ks === '<END>') { scroll = maxScroll; repaint() }
|
else if (ks === '<HOME>') { scroll = 0; scrollAndRepaint() }
|
||||||
|
else if (ks === '<END>') { scroll = maxScroll; scrollAndRepaint() }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
popMousePopup()
|
||||||
drawAll()
|
drawAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3432,6 +3457,76 @@ const popupDrawFrame = (wo) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render a centred button "[ Label ]" at (y, x). State drives the colour scheme so
|
||||||
|
// the button can appear normal / keyboard-focused / mouse-hovered / both.
|
||||||
|
// state: 0 = normal, 1 = focused, 2 = hovered, 3 = focused + hovered
|
||||||
|
function drawPopupButton(y, x, label, state) {
|
||||||
|
const txt = `[ ${label} ]`
|
||||||
|
let fore, back
|
||||||
|
if (state === 1) { fore = colWHITE; back = colTabBarBack2 } // focused
|
||||||
|
else if (state === 2) { fore = colWHITE; back = colHighlight } // hovered
|
||||||
|
else if (state === 3) { fore = colBLACK; back = colWHITE } // focused + hovered
|
||||||
|
else { fore = 230; back = colPopupBack } // normal
|
||||||
|
con.color_pair(fore, back)
|
||||||
|
con.move(y, x)
|
||||||
|
print(txt)
|
||||||
|
con.color_pair(colStatus, 255)
|
||||||
|
return { x: x, y: y, w: txt.length, h: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a row of OK/Cancel-style buttons centred under a popup. Each entry:
|
||||||
|
// { label, action() } (and an optional `default: true` to pre-focus)
|
||||||
|
// Returns:
|
||||||
|
// - `regions`: an array suitable for MOUSE_POPUP_STACK.push (handles hover + click)
|
||||||
|
// - `keyHandler(ks) -> bool`: feed key symbols here; returns true if it consumed Tab/Enter
|
||||||
|
// - `repaint()`: redraw all buttons with their current focus/hover state
|
||||||
|
// - `focus`, `hover`: getters/setters via methods (so popups can drive Esc → Cancel)
|
||||||
|
function makePopupButtonRow(y, popupX, popupW, defs) {
|
||||||
|
// Lay out buttons centred along row `y`. Label widths are tracked so we can compute hits.
|
||||||
|
const labels = defs.map(d => `[ ${d.label} ]`)
|
||||||
|
const totalW = labels.reduce((s, l) => s + l.length, 0) + 2 * (defs.length - 1)
|
||||||
|
const startX = popupX + ((popupW - totalW) >>> 1)
|
||||||
|
let cursor = startX
|
||||||
|
const buttons = defs.map((d, i) => {
|
||||||
|
const w = labels[i].length
|
||||||
|
const b = { x: cursor, y, w, label: d.label, action: d.action }
|
||||||
|
cursor += w + 2
|
||||||
|
return b
|
||||||
|
})
|
||||||
|
let focus = Math.max(0, defs.findIndex(d => d.default))
|
||||||
|
if (focus < 0) focus = 0
|
||||||
|
let hover = -1
|
||||||
|
|
||||||
|
const repaint = () => {
|
||||||
|
buttons.forEach((b, i) => {
|
||||||
|
const st = (i === focus ? 1 : 0) | (i === hover ? 2 : 0)
|
||||||
|
drawPopupButton(b.y, b.x, b.label, st)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const regions = buttons.map((b, i) => ({
|
||||||
|
x: b.x, y: b.y, w: b.w, h: b.h || 1,
|
||||||
|
onClick: (cy, cx, btn) => { if (btn === 1) b.action() },
|
||||||
|
onHover: () => { if (hover !== i) { hover = i; repaint() } },
|
||||||
|
onHoverLeave: () => { if (hover === i) { hover = -1; repaint() } },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Tab/Shift+Tab cycles focus; Enter activates. Returns true if the key was consumed.
|
||||||
|
const keyHandler = (ks, shiftDown) => {
|
||||||
|
if (ks === '\t' || ks === '<TAB>') {
|
||||||
|
focus = (focus + (shiftDown ? defs.length - 1 : 1)) % defs.length
|
||||||
|
repaint()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (ks === '\n') { buttons[focus].action(); return true }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return { regions, keyHandler, repaint,
|
||||||
|
getFocus: () => focus, setFocus: (i) => { focus = i; repaint() },
|
||||||
|
activate: (i) => buttons[i].action() }
|
||||||
|
}
|
||||||
|
|
||||||
function drawGotoPopup(popup, buf) {
|
function drawGotoPopup(popup, buf) {
|
||||||
con.color_pair(230, colPopupBack)
|
con.color_pair(230, colPopupBack)
|
||||||
popup.drawFrame()
|
popup.drawFrame()
|
||||||
@@ -3463,8 +3558,8 @@ function applyGoto(num) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openConfirmQuit() {
|
function openConfirmQuit() {
|
||||||
const pw = 25 + hasUnsavedChanges * 4
|
const pw = 28 + hasUnsavedChanges * 4
|
||||||
const ph = 5 + hasUnsavedChanges
|
const ph = 6 + hasUnsavedChanges
|
||||||
const px = ((SCRW - pw) / 2 | 0) + 1
|
const px = ((SCRW - pw) / 2 | 0) + 1
|
||||||
const py = ((SCRH - ph) / 2 | 0)
|
const py = ((SCRH - ph) / 2 | 0)
|
||||||
|
|
||||||
@@ -3477,9 +3572,7 @@ function openConfirmQuit() {
|
|||||||
|
|
||||||
con.move(py + 2, px + 2)
|
con.move(py + 2, px + 2)
|
||||||
con.color_pair(colWHITE, colPopupBack)
|
con.color_pair(colWHITE, colPopupBack)
|
||||||
print('Exit Microtone? ')
|
print('Exit Microtone?')
|
||||||
con.color_pair(230, 240)
|
|
||||||
print('[Y/N]')
|
|
||||||
|
|
||||||
if (hasUnsavedChanges) {
|
if (hasUnsavedChanges) {
|
||||||
con.move(py + 3, px + 2)
|
con.move(py + 3, px + 2)
|
||||||
@@ -3487,29 +3580,40 @@ function openConfirmQuit() {
|
|||||||
print('You have unsaved changes.')
|
print('You have unsaved changes.')
|
||||||
}
|
}
|
||||||
|
|
||||||
con.color_pair(colStatus, 255) // reset colour
|
|
||||||
|
|
||||||
let result = false
|
let result = false
|
||||||
let done = false
|
let done = false
|
||||||
|
|
||||||
|
const buttons = makePopupButtonRow(py + ph - 2, px, pw, [
|
||||||
|
{ label: 'Yes', action: () => { result = true; done = true }, default: true },
|
||||||
|
{ label: 'No', action: () => { done = true } },
|
||||||
|
])
|
||||||
|
buttons.repaint()
|
||||||
|
pushMousePopup(buttons.regions)
|
||||||
|
|
||||||
let eventJustReceived = true
|
let eventJustReceived = true
|
||||||
while (!done) {
|
while (!done) {
|
||||||
input.withEvent(ev => {
|
input.withEvent(ev => {
|
||||||
|
if (eventJustReceived && ev[0] === 'mouse_down') { eventJustReceived = false; return }
|
||||||
|
if (dispatchMouseEvent(ev)) return
|
||||||
if (ev[0] !== 'key_down') return
|
if (ev[0] !== 'key_down') return
|
||||||
if (1 !== ev[2]) return
|
if (1 !== ev[2]) return
|
||||||
const ks = ev[1]
|
const ks = ev[1]
|
||||||
|
const shiftDown = (ev.includes(59) || ev.includes(60))
|
||||||
|
|
||||||
if (ks === 'y' || ks === 'Y' || ks === '\n') { result = true; done = true }
|
if (buttons.keyHandler(ks, shiftDown)) return
|
||||||
|
if (ks === 'y' || ks === 'Y') { result = true; done = true }
|
||||||
else if (ks === 'n' || ks === 'N' || ks === '<ESC>') { done = true }
|
else if (ks === 'n' || ks === 'N' || ks === '<ESC>') { done = true }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
popMousePopup()
|
||||||
if (!result) drawAll()
|
if (!result) drawAll()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function openGotoPopup() {
|
function openGotoPopup() {
|
||||||
const pw = GOTO_POPUP_W
|
const pw = GOTO_POPUP_W
|
||||||
const ph = GOTO_POPUP_H
|
const ph = GOTO_POPUP_H + 2
|
||||||
const px = ((SCRW - pw) / 2 | 0) + 1
|
const px = ((SCRW - pw) / 2 | 0) + 1
|
||||||
const py = ((SCRH - ph) / 2 | 0)
|
const py = ((SCRH - ph) / 2 | 0)
|
||||||
|
|
||||||
@@ -3519,36 +3623,45 @@ function openGotoPopup() {
|
|||||||
|
|
||||||
let buf = ''
|
let buf = ''
|
||||||
let done = false
|
let done = false
|
||||||
drawGotoPopup(popup, buf)
|
let commit = false
|
||||||
|
|
||||||
|
const buttons = makePopupButtonRow(py + ph - 2, px, pw, [
|
||||||
|
{ label: 'OK', action: () => { commit = true; done = true }, default: true },
|
||||||
|
{ label: 'Cancel', action: () => { done = true } },
|
||||||
|
])
|
||||||
|
const repaintAll = () => { drawGotoPopup(popup, buf); buttons.repaint() }
|
||||||
|
repaintAll()
|
||||||
|
pushMousePopup(buttons.regions)
|
||||||
|
|
||||||
let eventJustReceived = true
|
let eventJustReceived = true
|
||||||
|
|
||||||
while (!done) {
|
while (!done) {
|
||||||
input.withEvent(ev => {
|
input.withEvent(ev => {
|
||||||
if (ev[0] !== 'key_down') return
|
if (eventJustReceived && (ev[0] === 'key_down' || ev[0] === 'mouse_down')) {
|
||||||
const ks = ev[1]
|
|
||||||
if (1 !== ev[2]) return // not key just hit
|
|
||||||
|
|
||||||
if (eventJustReceived) { // filter lingering input
|
|
||||||
eventJustReceived = false
|
eventJustReceived = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (dispatchMouseEvent(ev)) return
|
||||||
|
if (ev[0] !== 'key_down') return
|
||||||
|
const ks = ev[1]
|
||||||
|
if (1 !== ev[2]) return // not key just hit
|
||||||
|
const shiftDown = (ev.includes(59) || ev.includes(60))
|
||||||
|
|
||||||
|
if (buttons.keyHandler(ks, shiftDown)) return
|
||||||
if (ks === '<ESC>' || ks === 'x') {
|
if (ks === '<ESC>' || ks === 'x') {
|
||||||
done = true
|
done = true
|
||||||
} else if (ks === '\n') {
|
|
||||||
if (buf.length > 0) applyGoto(parseInt(buf, 16))
|
|
||||||
done = true
|
|
||||||
} else if (ks === '\u0008') {
|
} else if (ks === '\u0008') {
|
||||||
buf = buf.slice(0, -1)
|
buf = buf.slice(0, -1)
|
||||||
drawGotoPopup(popup, buf)
|
repaintAll()
|
||||||
} else if (ks.length === 1 && '0123456789abcdefABCDEF'.includes(ks) && buf.length < 3) {
|
} else if (ks.length === 1 && '0123456789abcdefABCDEF'.includes(ks) && buf.length < 3) {
|
||||||
buf += ks.toUpperCase()
|
buf += ks.toUpperCase()
|
||||||
drawGotoPopup(popup, buf)
|
repaintAll()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
popMousePopup()
|
||||||
|
if (commit && buf.length > 0) applyGoto(parseInt(buf, 16))
|
||||||
drawAll()
|
drawAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3575,7 +3688,7 @@ function openRetunePopup() {
|
|||||||
|
|
||||||
const pw = 42
|
const pw = 42
|
||||||
const listH = Math.min(n, 15)
|
const listH = Math.min(n, 15)
|
||||||
const ph = listH + 5
|
const ph = listH + 7
|
||||||
const px = ((SCRW - pw) / 2 | 0)
|
const px = ((SCRW - pw) / 2 | 0)
|
||||||
const py = ((SCRH - ph) / 2 | 0)
|
const py = ((SCRH - ph) / 2 | 0)
|
||||||
const listX = px + 2
|
const listX = px + 2
|
||||||
@@ -3590,6 +3703,14 @@ function openRetunePopup() {
|
|||||||
if (sel < 0) sel = 0
|
if (sel < 0) sel = 0
|
||||||
let scroll = centerScroll(sel, 0, listH, n)
|
let scroll = centerScroll(sel, 0, listH, n)
|
||||||
|
|
||||||
|
// OK/Cancel button placement (bottom inside row)
|
||||||
|
const btnRow = py + ph - 2
|
||||||
|
const labelOK = `[ OK ]`.length
|
||||||
|
const labelCan = `[ Cancel ]`.length
|
||||||
|
const totalW = labelOK + 2 + labelCan
|
||||||
|
const btnXOk = px + ((pw - totalW) >>> 1)
|
||||||
|
const btnXCan = btnXOk + labelOK + 2
|
||||||
|
|
||||||
const repaint = () => {
|
const repaint = () => {
|
||||||
con.color_pair(230, colPopupBack)
|
con.color_pair(230, colPopupBack)
|
||||||
popup.drawFrame()
|
popup.drawFrame()
|
||||||
@@ -3637,41 +3758,60 @@ function openRetunePopup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
con.move(py + ph - 2, px + 2)
|
con.move(py + ph - 3, px + 2)
|
||||||
con.color_pair(colVoiceHdr, colPopupBack)
|
con.color_pair(colVoiceHdr, colPopupBack)
|
||||||
print(`\u008418u `)
|
print(`\u008418u `)
|
||||||
con.color_pair(colStatus, colPopupBack)
|
con.color_pair(colStatus, colPopupBack)
|
||||||
print(`Sel `)
|
print(`Sel `)
|
||||||
con.color_pair(colVoiceHdr, colPopupBack)
|
con.color_pair(colVoiceHdr, colPopupBack)
|
||||||
print(`ent `)
|
|
||||||
con.color_pair(colStatus, colPopupBack)
|
|
||||||
print(`OK `)
|
|
||||||
con.color_pair(colVoiceHdr, colPopupBack)
|
|
||||||
print(`m `)
|
print(`m `)
|
||||||
con.color_pair(colStatus, colPopupBack)
|
con.color_pair(colStatus, colPopupBack)
|
||||||
print(`Method `)
|
print(`Method`)
|
||||||
con.color_pair(colVoiceHdr, colPopupBack)
|
|
||||||
print(`Q `)
|
buttons.repaint()
|
||||||
con.color_pair(colStatus, colPopupBack)
|
|
||||||
print(`Cancel`)
|
|
||||||
|
|
||||||
con.color_pair(colStatus, 255)
|
con.color_pair(colStatus, 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
repaint()
|
repaint()
|
||||||
|
|
||||||
let done = false
|
|
||||||
let confirmed = false
|
|
||||||
let eventJustReceived = true
|
let eventJustReceived = true
|
||||||
|
|
||||||
|
pushMousePopup(buttons.regions.concat([
|
||||||
|
// List rows: click to select, double-click semantics omitted (clarity over speed).
|
||||||
|
{ x: listX, y: listY, w: listW, h: listH, onClick: (cy, cx, btn) => {
|
||||||
|
if (btn !== 1) return
|
||||||
|
const r = cy - listY
|
||||||
|
const idx = scroll + r
|
||||||
|
if (idx < 0 || idx >= n) return
|
||||||
|
sel = idx; repaint()
|
||||||
|
}, onWheel: (cy, cx, dy) => {
|
||||||
|
sel += dy * 3
|
||||||
|
if (sel < 0) sel = 0
|
||||||
|
if (sel >= n) sel = n - 1
|
||||||
|
scroll = centerScroll(sel, scroll, listH, n)
|
||||||
|
repaint()
|
||||||
|
}},
|
||||||
|
// Method label clickable
|
||||||
|
{ x: px + 2, y: py + 2, w: listW, h: 1, onClick: (cy, cx, btn) => {
|
||||||
|
if (btn !== 1) return
|
||||||
|
method = methodCycle[(methodCycle.indexOf(method) + 1) % methodCycle.length]
|
||||||
|
repaint()
|
||||||
|
}},
|
||||||
|
]))
|
||||||
|
|
||||||
while (!done) {
|
while (!done) {
|
||||||
input.withEvent(ev => {
|
input.withEvent(ev => {
|
||||||
|
if (eventJustReceived && (ev[0] === 'key_down' || ev[0] === 'mouse_down')) {
|
||||||
|
eventJustReceived = false; return
|
||||||
|
}
|
||||||
|
if (dispatchMouseEvent(ev)) return
|
||||||
if (ev[0] !== 'key_down') return
|
if (ev[0] !== 'key_down') return
|
||||||
if (eventJustReceived) { eventJustReceived = false; return }
|
|
||||||
const ks = ev[1]
|
const ks = ev[1]
|
||||||
|
const shiftDown = (ev.includes(59) || ev.includes(60))
|
||||||
|
|
||||||
if (ks === 'Q') { done = true }
|
if (buttons.keyHandler(ks, shiftDown)) return
|
||||||
else if (ks === '\n') { confirmed = true; done = true }
|
if (ks === 'Q' || ks === '<ESC>') { done = true }
|
||||||
else if (ks === 'M' || ks === 'm') {
|
else if (ks === 'M' || ks === 'm') {
|
||||||
method = methodCycle[(methodCycle.indexOf(method) + 1) % methodCycle.length]
|
method = methodCycle[(methodCycle.indexOf(method) + 1) % methodCycle.length]
|
||||||
repaint()
|
repaint()
|
||||||
@@ -3692,6 +3832,8 @@ function openRetunePopup() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
popMousePopup()
|
||||||
|
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
const target = entries[sel]
|
const target = entries[sel]
|
||||||
if (target && target.index !== PITCH_PRESET_IDX) {
|
if (target && target.index !== PITCH_PRESET_IDX) {
|
||||||
@@ -3729,7 +3871,7 @@ function openFlagsPopup() {
|
|||||||
let sel = 0
|
let sel = 0
|
||||||
|
|
||||||
const pw = 28
|
const pw = 28
|
||||||
const ph = items.length + 4
|
const ph = items.length + 6
|
||||||
const px = ((SCRW - pw) / 2 | 0) + 1
|
const px = ((SCRW - pw) / 2 | 0) + 1
|
||||||
const py = ((SCRH - ph) / 2 | 0)
|
const py = ((SCRH - ph) / 2 | 0)
|
||||||
|
|
||||||
@@ -3737,6 +3879,13 @@ function openFlagsPopup() {
|
|||||||
popup.isHighlighted = true
|
popup.isHighlighted = true
|
||||||
popup.titleBack = colPopupBack
|
popup.titleBack = colPopupBack
|
||||||
|
|
||||||
|
let done = false
|
||||||
|
let confirmed = false
|
||||||
|
const buttons = makePopupButtonRow(py + ph - 2, px, pw, [
|
||||||
|
{ label: 'OK', action: () => { confirmed = true; done = true }, default: true },
|
||||||
|
{ label: 'Cancel', action: () => { done = true } },
|
||||||
|
])
|
||||||
|
|
||||||
const repaint = () => {
|
const repaint = () => {
|
||||||
con.color_pair(230, colPopupBack)
|
con.color_pair(230, colPopupBack)
|
||||||
popup.drawFrame()
|
popup.drawFrame()
|
||||||
@@ -3760,32 +3909,48 @@ function openFlagsPopup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
con.move(py + ph - 2, px + 2)
|
con.move(py + ph - 3, px + 2)
|
||||||
con.color_pair(colVoiceHdr, colPopupBack); print(`\u008418u `)
|
con.color_pair(colVoiceHdr, colPopupBack); print(`\u008418u `)
|
||||||
con.color_pair(colStatus, colPopupBack); print('Sel ')
|
con.color_pair(colStatus, colPopupBack); print('Sel ')
|
||||||
con.color_pair(colVoiceHdr, colPopupBack); print('sp ')
|
con.color_pair(colVoiceHdr, colPopupBack); print('sp ')
|
||||||
con.color_pair(colStatus, colPopupBack); print('Tick ')
|
con.color_pair(colStatus, colPopupBack); print('Tick')
|
||||||
con.color_pair(colVoiceHdr, colPopupBack); print('ent ')
|
|
||||||
con.color_pair(colStatus, colPopupBack); print('OK ')
|
buttons.repaint()
|
||||||
con.color_pair(colVoiceHdr, colPopupBack); print('Q ')
|
|
||||||
con.color_pair(colStatus, colPopupBack); print('X')
|
|
||||||
|
|
||||||
con.color_pair(colStatus, 255)
|
con.color_pair(colStatus, 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
repaint()
|
repaint()
|
||||||
|
|
||||||
let done = false
|
|
||||||
let confirmed = false
|
|
||||||
let eventJustReceived = true
|
let eventJustReceived = true
|
||||||
|
|
||||||
|
pushMousePopup(buttons.regions.concat([
|
||||||
|
// Clickable rows — each maps to a selectable index.
|
||||||
|
{ x: px + 2, y: py + 1, w: pw - 4, h: items.length, onClick: (cy, cx, btn) => {
|
||||||
|
if (btn !== 1) return
|
||||||
|
const i = cy - (py + 1)
|
||||||
|
const it = items[i]
|
||||||
|
if (!it || !it.kind) return
|
||||||
|
sel = selectables.indexOf(i)
|
||||||
|
if (sel < 0) sel = 0
|
||||||
|
if (it.kind === 'tone') toneMode = it.idx
|
||||||
|
else if (it.kind === 'intp') intpMode = it.idx
|
||||||
|
repaint()
|
||||||
|
}},
|
||||||
|
]))
|
||||||
|
|
||||||
while (!done) {
|
while (!done) {
|
||||||
input.withEvent(ev => {
|
input.withEvent(ev => {
|
||||||
|
if (eventJustReceived && (ev[0] === 'key_down' || ev[0] === 'mouse_down')) {
|
||||||
|
eventJustReceived = false; return
|
||||||
|
}
|
||||||
|
if (dispatchMouseEvent(ev)) return
|
||||||
if (ev[0] !== 'key_down') return
|
if (ev[0] !== 'key_down') return
|
||||||
const ks = ev[1]
|
const ks = ev[1]
|
||||||
if (eventJustReceived) { eventJustReceived = false; return }
|
const shiftDown = (ev.includes(59) || ev.includes(60))
|
||||||
|
|
||||||
|
if (buttons.keyHandler(ks, shiftDown)) return
|
||||||
if (ks === '<ESC>' || ks === 'q' || ks === 'Q') { done = true; return }
|
if (ks === '<ESC>' || ks === 'q' || ks === 'Q') { done = true; return }
|
||||||
if (ks === '\n') { confirmed = true; done = true; return }
|
|
||||||
if (ks === '<UP>' && sel > 0) { sel--; repaint(); return }
|
if (ks === '<UP>' && sel > 0) { sel--; repaint(); return }
|
||||||
if (ks === '<DOWN>' && sel < selectables.length-1) { sel++; repaint(); return }
|
if (ks === '<DOWN>' && sel < selectables.length-1) { sel++; repaint(); return }
|
||||||
if (ks === ' ') {
|
if (ks === ' ') {
|
||||||
@@ -3798,6 +3963,8 @@ function openFlagsPopup() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
popMousePopup()
|
||||||
|
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
const newFlags = (initialTrackerMixerflags & ~0x1F) |
|
const newFlags = (initialTrackerMixerflags & ~0x1F) |
|
||||||
(toneMode & 3) | ((intpMode & 7) << 2)
|
(toneMode & 3) | ((intpMode & 7) << 2)
|
||||||
@@ -3838,12 +4005,37 @@ function openInlineHexEdit(y, x, digits, initialValue) {
|
|||||||
|
|
||||||
repaint()
|
repaint()
|
||||||
let eventJustReceived = true
|
let eventJustReceived = true
|
||||||
|
|
||||||
|
// Field spans " $XX " — onClick on a digit moves the cursor there.
|
||||||
|
// Outside-click commits (Enter); right-click cancels.
|
||||||
|
// Region order matters: dispatchMouseEvent searches in reverse, so the
|
||||||
|
// field region (registered last) is tested before the catch-all.
|
||||||
|
pushMousePopup([
|
||||||
|
{ x: 1, y: 1, w: SCRW, h: SCRH, onClick: (cy, cx, btn) => {
|
||||||
|
if (btn === 1) done = true
|
||||||
|
else if (btn === 2) { cancelled = true; done = true }
|
||||||
|
}},
|
||||||
|
{ x: x + 2, y: y, w: digits, h: 1, onClick: (cy, cx, btn) => {
|
||||||
|
if (btn === 1) { cur = cx - (x + 2); repaint() }
|
||||||
|
else if (btn === 2) { cancelled = true; done = true }
|
||||||
|
}, onWheel: (cy, cx, dy) => {
|
||||||
|
// Wheel adjusts the digit under the cursor.
|
||||||
|
const digit = parseInt(buf[cur], 16)
|
||||||
|
const next = (digit + (dy < 0 ? 1 : -1) + 16) & 0xF
|
||||||
|
buf = buf.substring(0, cur) + next.toString(16).toUpperCase() + buf.substring(cur + 1)
|
||||||
|
repaint()
|
||||||
|
}},
|
||||||
|
])
|
||||||
|
|
||||||
while (!done) {
|
while (!done) {
|
||||||
input.withEvent(ev => {
|
input.withEvent(ev => {
|
||||||
|
if (eventJustReceived && (ev[0] === 'key_down' || ev[0] === 'mouse_down')) {
|
||||||
|
eventJustReceived = false; return
|
||||||
|
}
|
||||||
|
if (dispatchMouseEvent(ev)) return
|
||||||
if (ev[0] !== 'key_down') return
|
if (ev[0] !== 'key_down') return
|
||||||
if (1 !== ev[2]) return
|
if (1 !== ev[2]) return
|
||||||
const ks = ev[1]
|
const ks = ev[1]
|
||||||
if (eventJustReceived) { eventJustReceived = false; return }
|
|
||||||
|
|
||||||
if (ks === '<ESC>') { cancelled = true; done = true; return }
|
if (ks === '<ESC>') { cancelled = true; done = true; return }
|
||||||
if (ks === '\n') { done = true; return }
|
if (ks === '\n') { done = true; return }
|
||||||
@@ -3861,6 +4053,8 @@ function openInlineHexEdit(y, x, digits, initialValue) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
popMousePopup()
|
||||||
|
|
||||||
return cancelled ? null : parseInt(buf, 16)
|
return cancelled ? null : parseInt(buf, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3879,6 +4073,339 @@ function isExternalPanel(p) {
|
|||||||
return p === VIEW_SAMPLES || p === VIEW_INSTRMNT || p === VIEW_FILE
|
return p === VIEW_SAMPLES || p === VIEW_INSTRMNT || p === VIEW_FILE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// MOUSE INPUT
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Region registry. Coordinates are 1-indexed text cell positions. Each region:
|
||||||
|
// {x, y, w, h, onClick(cy, cx, btn, ev)?, onWheel(cy, cx, dy, ev)?, onRelease(...)?}
|
||||||
|
// MOUSE_GLOBAL — tabs + transport, live for the whole session.
|
||||||
|
// MOUSE_PANEL — per-panel viewport handlers, cleared whenever the panel changes.
|
||||||
|
// MOUSE_POPUP_STACK — popups push their own region set on open and pop on close;
|
||||||
|
// while non-empty, only the topmost set receives mouse events.
|
||||||
|
const MOUSE_GLOBAL = []
|
||||||
|
const MOUSE_PANEL = []
|
||||||
|
const MOUSE_POPUP_STACK = []
|
||||||
|
|
||||||
|
// Wrap push/pop so closing a popup also drops any onHoverLeave that would otherwise
|
||||||
|
// be invoked against the popup's stale regions on the next mouse move.
|
||||||
|
function pushMousePopup(regions) { MOUSE_POPUP_STACK.push(regions); lastHoveredRegion = null }
|
||||||
|
function popMousePopup() { MOUSE_POPUP_STACK.pop(); lastHoveredRegion = null }
|
||||||
|
|
||||||
|
function pixelToCell(px, py) {
|
||||||
|
return [(py / CELL_PH | 0) + 1, (px / CELL_PW | 0) + 1] // [cy, cx], 1-indexed
|
||||||
|
}
|
||||||
|
|
||||||
|
function regionHits(r, cy, cx) {
|
||||||
|
return cy >= r.y && cy < r.y + r.h && cx >= r.x && cx < r.x + r.w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch a mouse event to the topmost matching region. Returns true if handled.
|
||||||
|
// `mouse_move` also fires onHoverLeave for the previously-hovered region so popups can
|
||||||
|
// repaint un-hovered buttons without tracking that themselves.
|
||||||
|
let lastHoveredRegion = null
|
||||||
|
function dispatchMouseEvent(event) {
|
||||||
|
const t = event[0]
|
||||||
|
if (t !== 'mouse_down' && t !== 'mouse_wheel' && t !== 'mouse_up' && t !== 'mouse_move') return false
|
||||||
|
|
||||||
|
const [cy, cx] = pixelToCell(event[1], event[2])
|
||||||
|
const pool = (MOUSE_POPUP_STACK.length > 0)
|
||||||
|
? MOUSE_POPUP_STACK[MOUSE_POPUP_STACK.length - 1]
|
||||||
|
: MOUSE_PANEL.concat(MOUSE_GLOBAL)
|
||||||
|
|
||||||
|
if (t === 'mouse_move') {
|
||||||
|
let hit = null
|
||||||
|
for (let i = pool.length - 1; i >= 0; i--) {
|
||||||
|
const r = pool[i]
|
||||||
|
if (regionHits(r, cy, cx) && (r.onHover || r.onHoverLeave)) { hit = r; break }
|
||||||
|
}
|
||||||
|
if (hit !== lastHoveredRegion) {
|
||||||
|
if (lastHoveredRegion && lastHoveredRegion.onHoverLeave) lastHoveredRegion.onHoverLeave()
|
||||||
|
lastHoveredRegion = hit
|
||||||
|
}
|
||||||
|
if (hit && hit.onHover) { hit.onHover(cy, cx, event); return true }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = pool.length - 1; i >= 0; i--) {
|
||||||
|
const r = pool[i]
|
||||||
|
if (!regionHits(r, cy, cx)) continue
|
||||||
|
if (t === 'mouse_down' && r.onClick) { r.onClick(cy, cx, event[3], event); return true }
|
||||||
|
if (t === 'mouse_wheel' && r.onWheel) { r.onWheel(cy, cx, event[3], event); return true }
|
||||||
|
if (t === 'mouse_up' && r.onRelease) { r.onRelease(cy, cx, event[3], event); return true }
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPanelMouseRegions() { MOUSE_PANEL.length = 0 }
|
||||||
|
function addPanelMouseRegion(x, y, w, h, handlers) { MOUSE_PANEL.push(Object.assign({x, y, w, h}, handlers)) }
|
||||||
|
function addGlobalMouseRegion(x, y, w, h, handlers) { MOUSE_GLOBAL.push(Object.assign({x, y, w, h}, handlers)) }
|
||||||
|
|
||||||
|
// Apply the same panel-switch logic the Tab key path uses.
|
||||||
|
function switchToPanel(newPanel) {
|
||||||
|
if (newPanel === currentPanel) return
|
||||||
|
const wasTimeline = (currentPanel === VIEW_TIMELINE)
|
||||||
|
currentPanel = newPanel
|
||||||
|
applyMuteTransition(currentPanel)
|
||||||
|
if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters()
|
||||||
|
if (isExternalPanel(currentPanel)) {
|
||||||
|
clearPanelMouseRegions()
|
||||||
|
con.clear(); drawAlwaysOnElems(); drawControlHint()
|
||||||
|
pendingExternalDraw = true
|
||||||
|
} else {
|
||||||
|
rebuildPanelMouseRegions()
|
||||||
|
drawAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tab bar regions (registered once; tab geometry is constant) ---
|
||||||
|
function registerTabRegions() {
|
||||||
|
let col = 2 // XOFF, mirrors drawTabBar
|
||||||
|
for (let i = 0; i < PANEL_NAMES.length; i++) {
|
||||||
|
const w = 1 + PANEL_NAMES[i].length + 1 // spcL + name + spcR
|
||||||
|
const tabIdx = i
|
||||||
|
addGlobalMouseRegion(col, 3, w, 1, {
|
||||||
|
onClick: (cy, cx, btn) => { if (btn === 1) switchToPanel(tabIdx) }
|
||||||
|
})
|
||||||
|
col += w + (i < PANEL_NAMES.length - 1 ? TAB_GAP : 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Transport regions (rows 1-2 on the right edge) ---
|
||||||
|
// Order j: 0=stop, 1=playrow, 2=playcue, 3=playall — mirrors drawStatusBar's loop.
|
||||||
|
function registerTransportRegions() {
|
||||||
|
for (let j = 0; j < 4; j++) {
|
||||||
|
const glyphCol = SCRW - 5 * (j + 1) + 3
|
||||||
|
const idx = j
|
||||||
|
addGlobalMouseRegion(glyphCol - 1, 1, 3, 2, {
|
||||||
|
onClick: (cy, cx, btn) => {
|
||||||
|
if (btn !== 1) return
|
||||||
|
if (idx === 0) {
|
||||||
|
if (playbackMode !== PLAYMODE_NONE) { stopPlayback(); drawAlwaysOnElems(); redrawPanel() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// The play handlers vary by panel — match the keyboard shortcut mapping.
|
||||||
|
if (currentPanel === VIEW_PATTERN_DETAILS) {
|
||||||
|
if (idx === 1) startPlayPatternRow()
|
||||||
|
else startPlayPattern()
|
||||||
|
drawPatternsContents(panelPatterns)
|
||||||
|
} else {
|
||||||
|
if (idx === 1) startPlayRow()
|
||||||
|
else if (idx === 2) startPlayCue()
|
||||||
|
else startPlaySong()
|
||||||
|
redrawPanel()
|
||||||
|
}
|
||||||
|
drawAlwaysOnElems()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Per-panel viewport regions ---
|
||||||
|
function rebuildPanelMouseRegions() {
|
||||||
|
clearPanelMouseRegions()
|
||||||
|
if (currentPanel === VIEW_TIMELINE) registerTimelineMouse()
|
||||||
|
else if (currentPanel === VIEW_CUES) registerOrdersMouse()
|
||||||
|
else if (currentPanel === VIEW_PATTERN_DETAILS) registerPatternsMouse()
|
||||||
|
else if (currentPanel === VIEW_PROJECT) registerProjectMouse()
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerTimelineMouse() {
|
||||||
|
addPanelMouseRegion(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, {
|
||||||
|
onClick: (cy, cx, btn) => {
|
||||||
|
if (btn !== 1 || playbackMode !== PLAYMODE_NONE) return
|
||||||
|
const viewRow = cy - PTNVIEW_OFFSET_Y
|
||||||
|
const targetRow = scrollRow + viewRow
|
||||||
|
if (targetRow < 0 || targetRow >= ROWS_PER_PAT) return
|
||||||
|
const oldCursor = cursorRow
|
||||||
|
const oldVoxOff = voiceOff
|
||||||
|
cursorRow = targetRow
|
||||||
|
const relCol = cx - PTNVIEW_OFFSET_X
|
||||||
|
if (relCol >= 0) {
|
||||||
|
const colSlot = (relCol / COLSIZE_TIMELINE_FULL) | 0
|
||||||
|
const targetVox = voiceOff + colSlot
|
||||||
|
if (targetVox >= 0 && targetVox < song.numVoices) {
|
||||||
|
cursorVox = targetVox
|
||||||
|
const fieldX = relCol - colSlot * COLSIZE_TIMELINE_FULL
|
||||||
|
let field = 0
|
||||||
|
for (let k = 0; k < TL_FIELD_OFFSETS.length; k++) if (fieldX >= TL_FIELD_OFFSETS[k]) field = k
|
||||||
|
timelineColCursor = field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clampCursor(); clampVoice()
|
||||||
|
if (voiceOff !== oldVoxOff || Math.abs(cursorRow - oldCursor) >= PTNVIEW_HEIGHT) drawAll()
|
||||||
|
else {
|
||||||
|
drawPatternView(); drawVoiceHeaders(); drawSeparators(separatorStyle)
|
||||||
|
drawAlwaysOnElems(); drawVoiceDetail()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onWheel: (cy, cx, dy) => {
|
||||||
|
if (playbackMode !== PLAYMODE_NONE) return
|
||||||
|
cursorRow += dy * 3
|
||||||
|
clampCursor()
|
||||||
|
drawPatternView(); drawSeparators(separatorStyle); drawAlwaysOnElems(); drawVoiceDetail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerOrdersMouse() {
|
||||||
|
// Layout (1-indexed cells, mirrors drawOrdersRowAt):
|
||||||
|
// cols 1..3 = row number (no column meaning)
|
||||||
|
// col 4 = gap
|
||||||
|
// cols 5..10 = CMD (ordersColCursor = 0)
|
||||||
|
// col 11 = gap
|
||||||
|
// cols 12 + s*4 .. 12 + s*4 + 3 = voice slot s on screen
|
||||||
|
// (ordersColCursor = ordersVoiceOff + s + 1)
|
||||||
|
//
|
||||||
|
// Returns the ordersColCursor value for a given cx, or -1 if not on a column.
|
||||||
|
const colAtX = (cx) => {
|
||||||
|
if (cx >= ORDERS_CMD_X && cx < ORDERS_CMD_X + 6) return 0
|
||||||
|
if (cx >= ORDERS_VOICE_X) {
|
||||||
|
const slot = ((cx - ORDERS_VOICE_X) / ORDERS_VOICE_COL_W) | 0
|
||||||
|
if (slot < 0 || slot >= VOCSIZE_ORDERS) return -1
|
||||||
|
const v = ordersVoiceOff + slot
|
||||||
|
if (v >= song.numVoices) return -1
|
||||||
|
return v + 1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const hscrollBy = (dx) => {
|
||||||
|
const maxOff = Math.max(0, song.numVoices - VOCSIZE_ORDERS)
|
||||||
|
const next = Math.max(0, Math.min(maxOff, ordersVoiceOff + dx))
|
||||||
|
if (next === ordersVoiceOff) return false
|
||||||
|
ordersVoiceOff = next
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header row: click selects a column without touching the row; wheel scrolls
|
||||||
|
// voice columns horizontally (it's the natural place for column navigation).
|
||||||
|
addPanelMouseRegion(1, PTNVIEW_OFFSET_Y - 1, SCRW, 1, {
|
||||||
|
onClick: (cy, cx, btn) => {
|
||||||
|
if (btn !== 1 || playbackMode !== PLAYMODE_NONE) return
|
||||||
|
const col = colAtX(cx)
|
||||||
|
if (col < 0) return
|
||||||
|
ordersColCursor = col
|
||||||
|
clampOrdersHoriz(); redrawPanel(); drawAlwaysOnElems()
|
||||||
|
},
|
||||||
|
onWheel: (cy, cx, dy) => {
|
||||||
|
if (hscrollBy(dy * 3)) { redrawPanel(); drawAlwaysOnElems() }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Content rows: click sets the row and (when on a column) the column too;
|
||||||
|
// wheel scrolls vertically; Shift+wheel scrolls horizontally.
|
||||||
|
addPanelMouseRegion(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, {
|
||||||
|
onClick: (cy, cx, btn, ev) => {
|
||||||
|
if (btn !== 1 || playbackMode !== PLAYMODE_NONE) return
|
||||||
|
const viewRow = cy - PTNVIEW_OFFSET_Y
|
||||||
|
const targetIdx = ordersScroll + viewRow
|
||||||
|
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
|
||||||
|
if (targetIdx < 0 || targetIdx > maxCue) return
|
||||||
|
ordersCursor = targetIdx
|
||||||
|
const col = colAtX(cx)
|
||||||
|
if (col >= 0) ordersColCursor = col
|
||||||
|
scrollOrdersTo(ordersCursor)
|
||||||
|
clampOrdersHoriz()
|
||||||
|
redrawPanel(); drawAlwaysOnElems()
|
||||||
|
},
|
||||||
|
onWheel: (cy, cx, dy, ev) => {
|
||||||
|
const shiftDown = (ev.includes(59) || ev.includes(60))
|
||||||
|
if (shiftDown) {
|
||||||
|
if (hscrollBy(dy * 3)) { redrawPanel(); drawAlwaysOnElems() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
|
||||||
|
ordersCursor += dy * 3
|
||||||
|
if (ordersCursor < 0) ordersCursor = 0
|
||||||
|
if (ordersCursor > maxCue) ordersCursor = maxCue
|
||||||
|
scrollOrdersTo(ordersCursor)
|
||||||
|
redrawPanel(); drawAlwaysOnElems()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerPatternsMouse() {
|
||||||
|
// Left column: pattern list. cx in [PATEDITOR_LIST_X, PATEDITOR_SEP1_X)
|
||||||
|
addPanelMouseRegion(PATEDITOR_LIST_X, PTNVIEW_OFFSET_Y,
|
||||||
|
PATEDITOR_SEP1_X - PATEDITOR_LIST_X, PTNVIEW_HEIGHT, {
|
||||||
|
onClick: (cy, cx, btn) => {
|
||||||
|
if (btn !== 1 || song.numPats === 0 || playbackMode !== PLAYMODE_NONE) return
|
||||||
|
const targetIdx = patternListScroll + (cy - PTNVIEW_OFFSET_Y)
|
||||||
|
if (targetIdx < 0 || targetIdx >= song.numPats) return
|
||||||
|
patternIdx = targetIdx
|
||||||
|
clampPatternIdx(); simStateKey = ''
|
||||||
|
drawPatternsContents(panelPatterns)
|
||||||
|
},
|
||||||
|
onWheel: (cy, cx, dy) => {
|
||||||
|
if (song.numPats === 0) return
|
||||||
|
patternIdx += dy
|
||||||
|
clampPatternIdx(); simStateKey = ''
|
||||||
|
drawPatternsContents(panelPatterns)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Middle grid: pattern editor cells. cx in [PATEDITOR_GRID_X, PATEDITOR_DETAIL_X)
|
||||||
|
addPanelMouseRegion(PATEDITOR_GRID_X, PTNVIEW_OFFSET_Y,
|
||||||
|
PATEDITOR_DETAIL_X - PATEDITOR_GRID_X, PTNVIEW_HEIGHT, {
|
||||||
|
onClick: (cy, cx, btn) => {
|
||||||
|
if (btn !== 1 || song.numPats === 0 || playbackMode !== PLAYMODE_NONE) return
|
||||||
|
const targetRow = patternGridScroll + (cy - PTNVIEW_OFFSET_Y)
|
||||||
|
if (targetRow < 0 || targetRow >= ROWS_PER_PAT) return
|
||||||
|
patternGridRow = targetRow
|
||||||
|
const cellRel = cx - PATEDITOR_CELL_X
|
||||||
|
const fieldOffsets = [0, 5, 8, 11, 14, 15]
|
||||||
|
let field = 0
|
||||||
|
for (let k = 0; k < fieldOffsets.length; k++) if (cellRel >= fieldOffsets[k]) field = k
|
||||||
|
if (field < 0) field = 0; if (field > 5) field = 5
|
||||||
|
patternGridCol = field
|
||||||
|
clampPatternGrid(); simStateKey = ''
|
||||||
|
drawPatternsContents(panelPatterns)
|
||||||
|
},
|
||||||
|
onWheel: (cy, cx, dy) => {
|
||||||
|
if (song.numPats === 0) return
|
||||||
|
patternGridRow += dy * 3
|
||||||
|
clampPatternGrid(); simStateKey = ''
|
||||||
|
drawPatternsContents(panelPatterns)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerProjectMouse() {
|
||||||
|
addPanelMouseRegion(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, {
|
||||||
|
onClick: (cy, cx, btn) => {
|
||||||
|
if (btn !== 1 || playbackMode !== PLAYMODE_NONE) return
|
||||||
|
// Meta rows occupy PTNVIEW_OFFSET_Y .. PTNVIEW_OFFSET_Y + PROJ_META_ROWS_COUNT - 1.
|
||||||
|
// The song list starts at PROJ_SONGLIST_Y + 1.
|
||||||
|
const metaRow = cy - PTNVIEW_OFFSET_Y
|
||||||
|
if (metaRow >= 0 && metaRow < PROJ_META_ROWS_COUNT) {
|
||||||
|
projectCursor = metaRow
|
||||||
|
clampProjectCursor(); redrawPanel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const songRow = cy - (PROJ_SONGLIST_Y + 1)
|
||||||
|
if (songRow >= 0) {
|
||||||
|
const songIdx = projectSongScroll + songRow
|
||||||
|
if (songIdx >= 0 && songIdx < songsMeta.numSongs) {
|
||||||
|
projectCursor = PROJ_META_ROWS_COUNT + songIdx
|
||||||
|
clampProjectCursor(); redrawPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onWheel: (cy, cx, dy) => {
|
||||||
|
const rowsVis = projectSongListRowsVisible()
|
||||||
|
const maxScroll = Math.max(0, songsMeta.numSongs - rowsVis)
|
||||||
|
projectSongScroll += dy * 3
|
||||||
|
if (projectSongScroll < 0) projectSongScroll = 0
|
||||||
|
if (projectSongScroll > maxScroll) projectSongScroll = maxScroll
|
||||||
|
redrawPanel()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
registerTabRegions()
|
||||||
|
registerTransportRegions()
|
||||||
|
rebuildPanelMouseRegions()
|
||||||
|
|
||||||
// Launching a sub-program from inside an input.withEvent callback causes the triggering
|
// Launching a sub-program from inside an input.withEvent callback causes the triggering
|
||||||
// Tab event to leak into the sub-program's own withEvent call (the event hasn't been
|
// Tab event to leak into the sub-program's own withEvent call (the event hasn't been
|
||||||
// consumed yet when the callback is still executing). We avoid this by deferring the
|
// consumed yet when the callback is still executing). We avoid this by deferring the
|
||||||
@@ -3888,6 +4415,7 @@ let pendingExternalDraw = false
|
|||||||
|
|
||||||
while (!exitFlag) {
|
while (!exitFlag) {
|
||||||
input.withEvent(event => {
|
input.withEvent(event => {
|
||||||
|
if (dispatchMouseEvent(event)) return
|
||||||
if (event[0] !== "key_down") return
|
if (event[0] !== "key_down") return
|
||||||
const keysym = event[1]
|
const keysym = event[1]
|
||||||
const keyJustHit = (1 == event[2])
|
const keyJustHit = (1 == event[2])
|
||||||
@@ -3914,9 +4442,11 @@ while (!exitFlag) {
|
|||||||
if (isExternalPanel(currentPanel)) {
|
if (isExternalPanel(currentPanel)) {
|
||||||
// Redraw header now so the tab highlight is visible immediately,
|
// Redraw header now so the tab highlight is visible immediately,
|
||||||
// but defer the actual sub-program launch to after withEvent returns.
|
// but defer the actual sub-program launch to after withEvent returns.
|
||||||
|
clearPanelMouseRegions()
|
||||||
con.clear(); drawAlwaysOnElems(); drawControlHint()
|
con.clear(); drawAlwaysOnElems(); drawControlHint()
|
||||||
pendingExternalDraw = true
|
pendingExternalDraw = true
|
||||||
} else {
|
} else {
|
||||||
|
rebuildPanelMouseRegions()
|
||||||
drawAll()
|
drawAll()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -3947,9 +4477,11 @@ while (!exitFlag) {
|
|||||||
applyMuteTransition(currentPanel)
|
applyMuteTransition(currentPanel)
|
||||||
if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters()
|
if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters()
|
||||||
if (isExternalPanel(currentPanel)) {
|
if (isExternalPanel(currentPanel)) {
|
||||||
|
clearPanelMouseRegions()
|
||||||
con.clear(); drawAlwaysOnElems(); drawControlHint()
|
con.clear(); drawAlwaysOnElems(); drawControlHint()
|
||||||
redrawPanel()
|
redrawPanel()
|
||||||
} else {
|
} else {
|
||||||
|
rebuildPanelMouseRegions()
|
||||||
drawAll()
|
drawAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ const LIST_HEIGHT = HEIGHT - 3
|
|||||||
const FILESIZE_WIDTH = 7
|
const FILESIZE_WIDTH = 7
|
||||||
const FILELIST_WIDTH = WIDTH - SIDEBAR_WIDTH - 3 - FILESIZE_WIDTH
|
const FILELIST_WIDTH = WIDTH - SIDEBAR_WIDTH - 3 - FILESIZE_WIDTH
|
||||||
const POPUP_WIDTH = 52 // always even number
|
const POPUP_WIDTH = 52 // always even number
|
||||||
const POPUP_HEIGHT = 16
|
|
||||||
|
const [SCRPW, SCRPH] = graphics.getPixelDimension()
|
||||||
|
const CELL_PW = (SCRPW / WIDTH) | 0
|
||||||
|
const CELL_PH = (SCRPH / WHEIGHT) | 0
|
||||||
|
|
||||||
const COL_HL_EXT = {
|
const COL_HL_EXT = {
|
||||||
"js": 215,
|
"js": 215,
|
||||||
@@ -69,6 +72,11 @@ const EXEC_FUNS = {
|
|||||||
"taud": (f) => _G.shell.execute(`playtaud "${f}"`),
|
"taud": (f) => _G.shell.execute(`playtaud "${f}"`),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EDIT_FUNS = {
|
||||||
|
"taud": (f) => _G.shell.execute(`microtone "${f}"`),
|
||||||
|
}
|
||||||
|
const DEFAULT_EDITOR = `edit`
|
||||||
|
|
||||||
function makeExecFun(template) {
|
function makeExecFun(template) {
|
||||||
return (f) => _G.shell.execute(template.replaceAll("{0}", `"${f}"`))
|
return (f) => _G.shell.execute(template.replaceAll("{0}", `"${f}"`))
|
||||||
}
|
}
|
||||||
@@ -118,8 +126,52 @@ function loadZfmrc() {
|
|||||||
|
|
||||||
loadZfmrc()
|
loadZfmrc()
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Mouse region registry
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
const MOUSE_PANEL = []
|
||||||
|
let lastHoveredRegion = null
|
||||||
|
|
||||||
|
function pixelToCell(px, py) {
|
||||||
|
return [(py / CELL_PH | 0) + 1, (px / CELL_PW | 0) + 1]
|
||||||
|
}
|
||||||
|
function regionHits(r, cy, cx) {
|
||||||
|
return cy >= r.y && cy < r.y + r.h && cx >= r.x && cx < r.x + r.w
|
||||||
|
}
|
||||||
|
function clearPanelMouseRegions() { MOUSE_PANEL.length = 0; lastHoveredRegion = null }
|
||||||
|
function addPanelMouseRegion(x, y, w, h, handlers) { MOUSE_PANEL.push(Object.assign({x, y, w, h}, handlers)) }
|
||||||
|
|
||||||
|
function dispatchMouseEvent(event) {
|
||||||
|
const t = event[0]
|
||||||
|
if (t !== 'mouse_down' && t !== 'mouse_wheel' && t !== 'mouse_up' && t !== 'mouse_move') return false
|
||||||
|
const [cy, cx] = pixelToCell(event[1], event[2])
|
||||||
|
|
||||||
|
if (t === 'mouse_move') {
|
||||||
|
let hit = null
|
||||||
|
for (let i = MOUSE_PANEL.length - 1; i >= 0; i--) {
|
||||||
|
const r = MOUSE_PANEL[i]
|
||||||
|
if (regionHits(r, cy, cx) && (r.onHover || r.onHoverLeave)) { hit = r; break }
|
||||||
|
}
|
||||||
|
if (hit !== lastHoveredRegion) {
|
||||||
|
if (lastHoveredRegion && lastHoveredRegion.onHoverLeave) lastHoveredRegion.onHoverLeave()
|
||||||
|
lastHoveredRegion = hit
|
||||||
|
}
|
||||||
|
if (hit && hit.onHover) { hit.onHover(cy, cx, event); return true }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = MOUSE_PANEL.length - 1; i >= 0; i--) {
|
||||||
|
const r = MOUSE_PANEL[i]
|
||||||
|
if (!regionHits(r, cy, cx)) continue
|
||||||
|
if (t === 'mouse_down' && r.onClick) { r.onClick(cy, cx, event[3], event); return true }
|
||||||
|
if (t === 'mouse_wheel' && r.onWheel) { r.onWheel(cy, cx, event[3], event); return true }
|
||||||
|
if (t === 'mouse_up' && r.onRelease) { r.onRelease(cy, cx, event[3], event); return true }
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
let windowMode = 0 // 0 == left, 1 == right
|
let windowMode = 0 // 0 == left, 1 == right
|
||||||
let windowFocus = [0] // is a stack; 0: files window, 1: palette window, 2: popup window
|
|
||||||
|
|
||||||
// window states
|
// window states
|
||||||
let path = [["A:", "home"], ["A:"]]
|
let path = [["A:", "home"], ["A:"]]
|
||||||
@@ -347,11 +399,43 @@ let filesPanelDraw = (wo) => {
|
|||||||
con.color_pair(COL_TEXT, COL_BACK)
|
con.color_pair(COL_TEXT, COL_BACK)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
// Op panel buttons. yOff is the row offset (icon) inside the op panel frame;
|
||||||
|
// label sits at yOff+1. Hit regions span both rows.
|
||||||
|
// hitH is the row count for the mouse hit-box. The switch button gets a taller
|
||||||
|
// hit-box than the others because the icon glyph above its label leaves extra
|
||||||
|
// whitespace inside the cell above the first horizontal rule.
|
||||||
|
const OP_BUTTONS = [
|
||||||
|
{ id: 'switch', yOff: 0, hitH: 5, key: 'z' },
|
||||||
|
{ id: 'up', yOff: 6, hitH: 2, key: 'u' },
|
||||||
|
{ id: 'copy', yOff: 9, hitH: 2, key: 'c' },
|
||||||
|
{ id: 'move', yOff: 12, hitH: 2, key: 'v' },
|
||||||
|
{ id: 'delete', yOff: 15, hitH: 2, key: 'd' },
|
||||||
|
{ id: 'mkdir', yOff: 18, hitH: 2, key: 'k' },
|
||||||
|
{ id: 'rename', yOff: 21, hitH: 2, key: 'r' },
|
||||||
|
{ id: 'more', yOff: 24, hitH: 2, key: 'm' },
|
||||||
|
{ id: 'quit', yOff: 27, hitH: 2, key: 'q' },
|
||||||
|
]
|
||||||
|
let opHover = -1
|
||||||
|
|
||||||
let opPanelDraw = (wo) => {
|
let opPanelDraw = (wo) => {
|
||||||
function hr(y) {
|
function hr(i, y) {
|
||||||
|
// draw horizontal rule...
|
||||||
|
con.color_pair(COL_TEXT, 255)
|
||||||
con.move(y, xp)
|
con.move(y, xp)
|
||||||
print(`\x84196u`.repeat(SIDEBAR_WIDTH - 2))
|
print(`\u00C4`.repeat(SIDEBAR_WIDTH - 2))
|
||||||
|
|
||||||
|
// if mouse is up, draw the whole box
|
||||||
|
if (opHover == i) {
|
||||||
|
let moveBack = (i == 0) ? 6 : 3
|
||||||
|
|
||||||
|
con.color_pair(COL_HLTEXT, 255)
|
||||||
|
con.move(y - moveBack, xp)
|
||||||
|
print('\u00CD'.repeat(SIDEBAR_WIDTH - 2))
|
||||||
|
con.move(y, xp)
|
||||||
|
print('\u00CD'.repeat(SIDEBAR_WIDTH - 2))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
function labCol(i) { return (opHover === i) ? COL_HLTEXT : COL_TEXT }
|
||||||
|
|
||||||
con.color_pair(COL_TEXT, COL_BACK)
|
con.color_pair(COL_TEXT, COL_BACK)
|
||||||
|
|
||||||
@@ -360,112 +444,83 @@ let opPanelDraw = (wo) => {
|
|||||||
|
|
||||||
// other panel
|
// other panel
|
||||||
con.move(yp + 2, xp + 3)
|
con.move(yp + 2, xp + 3)
|
||||||
con.prnch((windowMode) ? 0x11 : 0x10)
|
con.color_pair(labCol(0), 255); con.prnch((windowMode) ? 0x11 : 0x10)
|
||||||
con.move(yp + 3, xp)
|
con.move(yp + 3, xp)
|
||||||
print(` \x1B[38;5;${COL_TEXT}m[\x1B[38;5;${COL_HLACTION}mZ\x1B[38;5;${COL_TEXT}m]`)
|
print(` \x1B[38;5;${labCol(0)}m[\x1B[38;5;${COL_HLACTION}mZ\x1B[38;5;${labCol(0)}m]`)
|
||||||
|
|
||||||
hr(yp+5)
|
hr(0, yp+5)
|
||||||
|
|
||||||
// go up
|
// go up
|
||||||
con.mvaddch(yp + 6, xp + 3, 0x18)
|
con.color_pair(labCol(1), 255); con.mvaddch(yp + 6, xp + 3, 0x18)
|
||||||
con.move(yp + 7, xp)
|
con.move(yp + 7, xp)
|
||||||
print(` \x1B[38;5;${COL_TEXT}mGo \x1B[38;5;${COL_HLACTION}mU\x1B[38;5;${COL_TEXT}mp`)
|
print(` \x1B[38;5;${labCol(1)}mGo \x1B[38;5;${COL_HLACTION}mU\x1B[38;5;${labCol(1)}mp`)
|
||||||
|
|
||||||
hr(yp+8)
|
hr(1, yp+8)
|
||||||
|
|
||||||
// copy
|
// copy
|
||||||
con.move(yp + 9, xp + 2)
|
con.move(yp + 9, xp + 2)
|
||||||
con.prnch(0xDB);con.prnch((windowMode) ? 0x1B : 0x1A);con.prnch(0xDB)
|
con.color_pair(labCol(2), 255); con.prnch(0xDB);con.prnch((windowMode) ? 0x1B : 0x1A);con.prnch(0xDB)
|
||||||
con.move(yp + 10, xp)
|
con.move(yp + 10, xp)
|
||||||
print(` \x1B[38;5;${COL_HLACTION}mC\x1B[38;5;${COL_TEXT}mopy`)
|
print(` \x1B[38;5;${COL_HLACTION}mC\x1B[38;5;${labCol(2)}mopy`)
|
||||||
|
|
||||||
hr(yp+11)
|
hr(2, yp+11)
|
||||||
|
|
||||||
// move
|
// move
|
||||||
con.move(yp + 12, xp + 2)
|
con.move(yp + 12, xp + 2)
|
||||||
if (windowMode) con.prnch([0xDB, 0x1B, 0xB0]); else con.prnch([0xB0, 0x1A, 0xDB])
|
con.color_pair(labCol(3), 255); if (windowMode) con.prnch([0xDB, 0x1B, 0xB0]); else con.prnch([0xB0, 0x1A, 0xDB])
|
||||||
con.move(yp + 13, xp)
|
con.move(yp + 13, xp)
|
||||||
print(` \x1B[38;5;${COL_TEXT}mMo\x1B[38;5;${COL_HLACTION}mv\x1B[38;5;${COL_TEXT}me`)
|
print(` \x1B[38;5;${labCol(3)}mMo\x1B[38;5;${COL_HLACTION}mv\x1B[38;5;${labCol(3)}me`)
|
||||||
|
|
||||||
hr(yp+14)
|
hr(3, yp+14)
|
||||||
|
|
||||||
// delete
|
// delete
|
||||||
con.move(yp + 15, xp + 2)
|
con.move(yp + 15, xp + 2)
|
||||||
if (windowMode) con.prnch([0xDB, 0x1A, 0xF9]); else con.prnch([0xF9, 0x1B, 0xDB])
|
con.color_pair(labCol(4), 255); if (windowMode) con.prnch([0xDB, 0x1A, 0xF9]); else con.prnch([0xF9, 0x1B, 0xDB])
|
||||||
con.move(yp + 16, xp)
|
con.move(yp + 16, xp)
|
||||||
print(` \x1B[38;5;${COL_HLACTION}mD\x1B[38;5;${COL_TEXT}melete`)
|
print(` \x1B[38;5;${COL_HLACTION}mD\x1B[38;5;${labCol(4)}melete`)
|
||||||
|
|
||||||
hr(yp+17)
|
hr(4, yp+17)
|
||||||
|
|
||||||
// mkdir
|
// mkdir
|
||||||
con.move(yp + 18, xp + 2)
|
con.move(yp + 18, xp + 2)
|
||||||
|
con.color_pair(labCol(5), 255);
|
||||||
con.prnch(0xDB)
|
con.prnch(0xDB)
|
||||||
con.video_reverse();con.prnch(0x2B);con.video_reverse()
|
con.video_reverse();con.prnch(0x2B);con.video_reverse()
|
||||||
con.prnch(0xDF)
|
con.prnch(0xDF)
|
||||||
con.move(yp + 19, xp)
|
con.move(yp + 19, xp)
|
||||||
print(` \x1B[38;5;${COL_TEXT}mM\x1B[38;5;${COL_HLACTION}mk\x1B[38;5;${COL_TEXT}mDir`)
|
print(` \x1B[38;5;${labCol(5)}mM\x1B[38;5;${COL_HLACTION}mk\x1B[38;5;${labCol(5)}mDir`)
|
||||||
|
|
||||||
hr(yp+20)
|
hr(5, yp+20)
|
||||||
|
|
||||||
// rename
|
// rename
|
||||||
con.move(yp + 21, xp + 2)
|
con.move(yp + 21, xp + 2)
|
||||||
con.prnch(0x4E);con.prnch(0x1A);con.prnch(0x52)
|
con.color_pair(labCol(6), 255); con.prnch(0x4E);con.prnch(0x1A);con.prnch(0x52)
|
||||||
con.move(yp + 22, xp)
|
con.move(yp + 22, xp)
|
||||||
print(` \x1B[38;5;${COL_HLACTION}mR\x1B[38;5;${COL_TEXT}mename`)
|
print(` \x1B[38;5;${COL_HLACTION}mR\x1B[38;5;${labCol(6)}mename`)
|
||||||
|
|
||||||
hr(yp+23)
|
hr(6, yp+23)
|
||||||
|
|
||||||
// the dreaded hamburger menu
|
// the dreaded hamburger menu
|
||||||
con.move(yp + 24, xp + 3)
|
con.move(yp + 24, xp + 3)
|
||||||
con.prnch(0xf0)
|
con.color_pair(labCol(7), 255); con.prnch(0xf0)
|
||||||
con.move(yp + 25, xp)
|
con.move(yp + 25, xp)
|
||||||
print(` \x1B[38;5;${COL_HLACTION}mM\x1B[38;5;${COL_TEXT}more`)
|
print(` \x1B[38;5;${COL_HLACTION}mM\x1B[38;5;${labCol(7)}more`)
|
||||||
|
|
||||||
hr(yp+26)
|
hr(7, yp+26)
|
||||||
|
|
||||||
// quit
|
// quit
|
||||||
con.move(yp + 27, xp + 3)
|
con.move(yp + 27, xp + 3)
|
||||||
con.prnch(0x58)
|
con.color_pair(labCol(8), 255); con.prnch(0x58)
|
||||||
con.move(yp + 28, xp)
|
con.move(yp + 28, xp)
|
||||||
print(` \x1B[38;5;${COL_HLACTION}mQ\x1B[38;5;${COL_TEXT}muit`)
|
print(` \x1B[38;5;${COL_HLACTION}mQ\x1B[38;5;${labCol(8)}muit`)
|
||||||
|
|
||||||
|
|
||||||
|
con.color_pair(COL_TEXT, 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let paletteDraw = (wo) => {
|
|
||||||
function hr(y) {
|
|
||||||
con.move(y, xp)
|
|
||||||
print(`\x84196u`.repeat(POPUP_WIDTH - 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
con.color_pair(COL_TEXT, COL_BACK)
|
|
||||||
|
|
||||||
let xp = wo.x + 1
|
|
||||||
let yp = wo.y + 1
|
|
||||||
|
|
||||||
// erase first
|
|
||||||
for (let y = 0; y <= POPUP_HEIGHT-2; y++) {
|
|
||||||
con.move(yp + y, xp)
|
|
||||||
print(" ".repeat(POPUP_WIDTH-2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// finally draw something
|
|
||||||
con.move(yp, xp)
|
|
||||||
print("More commands (hit m to return):")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let popupDraw = (wo) => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
let filenavOninput = (window, event) => {
|
let filenavOninput = (window, event) => {
|
||||||
|
|
||||||
let eventName = event[0]
|
let eventName = event[0]
|
||||||
if (eventName == "key_down") {
|
if (eventName !== "key_down") return
|
||||||
|
|
||||||
let keysym = event[1]
|
let keysym = event[1]
|
||||||
let keyJustHit = (1 == event[2])
|
let keyJustHit = (1 == event[2])
|
||||||
@@ -474,13 +529,15 @@ let filenavOninput = (window, event) => {
|
|||||||
|
|
||||||
let scrollPeek = (LIST_HEIGHT / 3)|0
|
let scrollPeek = (LIST_HEIGHT / 3)|0
|
||||||
|
|
||||||
if (keyJustHit && keysym == "q") {
|
if (keyJustHit && keysym == "q") actQuit()
|
||||||
exit = true
|
else if (keyJustHit && keysym == "z") actSwitchPanel()
|
||||||
}
|
else if (keyJustHit && keysym == 'u') actGoUp()
|
||||||
else if (keyJustHit && keysym == "z") {
|
else if (keyJustHit && keysym == 'c') actCopy()
|
||||||
windowMode = 1 - windowMode
|
else if (keyJustHit && keysym == 'v') actMove()
|
||||||
redraw() // this would double-redraw (hence no panel switching) or something if redraw() is not merely a request to do so
|
else if (keyJustHit && keysym == 'd') actDelete()
|
||||||
}
|
else if (keyJustHit && keysym == 'k') actMkdir()
|
||||||
|
else if (keyJustHit && keysym == 'r') actRename()
|
||||||
|
else if (keyJustHit && keysym == 'm') actMore()
|
||||||
else if (keysym == "<UP>") {
|
else if (keysym == "<UP>") {
|
||||||
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(-1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
|
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(-1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
|
||||||
drawFilePanel()
|
drawFilePanel()
|
||||||
@@ -498,142 +555,59 @@ let filenavOninput = (window, event) => {
|
|||||||
drawFilePanel()
|
drawFilePanel()
|
||||||
}
|
}
|
||||||
else if (keyJustHit && keycode == 66) { // enter
|
else if (keyJustHit && keycode == 66) { // enter
|
||||||
let selectedFileCache = filePanelCache[windowMode][cursor[windowMode]]
|
actActivate()
|
||||||
let selectedFile = selectedFileCache.file
|
|
||||||
|
|
||||||
//serial.println(`selectedFile = ${selectedFile.fullPath}`)
|
|
||||||
|
|
||||||
if (selectedFile.fullPath[1] == ":" && selectedFile.fullPath[2] == "\\" && selectedFile.fullPath.length == 3) {
|
|
||||||
path[windowMode].push(selectedFile.fullPath)
|
|
||||||
cursor[windowMode] = 0; scroll[windowMode] = 0
|
|
||||||
refreshFilePanelCache(windowMode)
|
|
||||||
drawFilePanel()
|
|
||||||
}
|
|
||||||
else if (selectedFileCache.isDirectory) {
|
|
||||||
//serial.println(`selectedFile.name = ${selectedFile.name}`)
|
|
||||||
path[windowMode].push(selectedFileCache.filename)
|
|
||||||
cursor[windowMode] = 0; scroll[windowMode] = 0
|
|
||||||
refreshFilePanelCache(windowMode)
|
|
||||||
drawFilePanel()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let fileext = selectedFileCache.filename.substring(selectedFileCache.filename.lastIndexOf(".") + 1).toLowerCase()
|
|
||||||
let execfun = EXEC_FUNS[fileext] || ((f) => _G.shell.execute(f))
|
|
||||||
let errorlevel = 0
|
|
||||||
|
|
||||||
con.curs_set(1);clearScr();con.move(1,1)
|
|
||||||
try {
|
|
||||||
//serial.println(selectedFile.fullPath)
|
|
||||||
errorlevel = execfun(selectedFile.fullPath)
|
|
||||||
//serial.println("1 errorlevel = " + errorlevel)
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
// TODO popup error
|
|
||||||
println(e)
|
|
||||||
errorlevel = 1
|
|
||||||
//serial.println("2 errorlevel = " + errorlevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorlevel) {
|
|
||||||
println("Hit Return/Enter key to continue . . . .")
|
|
||||||
sys.read()
|
|
||||||
}
|
|
||||||
|
|
||||||
firstRunLatch = true
|
|
||||||
con.curs_set(0);clearScr()
|
|
||||||
refreshFilePanelCache(windowMode)
|
|
||||||
redraw()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (keyJustHit && keysym == 'u') { // no bksp: used as an exit key for playmov/playwav
|
|
||||||
if (path[windowMode].length >= 1) {
|
|
||||||
path[windowMode].pop()
|
|
||||||
cursor[windowMode] = 0; scroll[windowMode] = 0
|
|
||||||
refreshFilePanelCache(windowMode)
|
|
||||||
drawFilePanel()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// TODO list of drives
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (keyJustHit && keysym == 'm') {
|
|
||||||
makePopup(1); redraw()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Popup wrappers (delegate to win.showDialog in wintex.mjs)
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function showConfirmPopup(title, message) {
|
||||||
let paletteInput = (window, event) => {
|
const res = win.showDialog({
|
||||||
|
title: title,
|
||||||
let eventName = event[0]
|
message: message,
|
||||||
if (eventName == "key_down") {
|
fields: [],
|
||||||
|
buttons: [
|
||||||
let keysym = event[1]
|
{ label: 'OK', action: 'ok', default: true },
|
||||||
let keyJustHit = (1 == event[2])
|
{ label: 'CANCEL', action: 'cancel' },
|
||||||
let keycodes = [event[3],event[4],event[5],event[6],event[7],event[8],event[9],event[10]]
|
],
|
||||||
let keycode = keycodes[0]
|
})
|
||||||
|
return res.action === 'ok'
|
||||||
if (keyJustHit && keysym == 'm') {
|
|
||||||
removePopup(); redraw()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showInputPopup(title, prompt, defaultVal) {
|
||||||
|
const res = win.showDialog({
|
||||||
|
title: title,
|
||||||
|
fields: [{ label: prompt, initial: defaultVal || '', width: POPUP_WIDTH - 6 }],
|
||||||
|
buttons: [
|
||||||
|
{ label: 'OK', action: 'ok', default: true },
|
||||||
|
{ label: 'CANCEL', action: 'cancel' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
return res.action === 'ok' ? res.values[0] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessagePopup(title, message) {
|
||||||
let popupInput = (window, event) => {
|
win.showDialog({
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
fields: [],
|
||||||
|
buttons: [{ label: 'OK', action: 'ok', default: true }],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
let windows = [
|
const windows = [
|
||||||
/*index 0: main three panels*/[
|
|
||||||
new win.WindowObject(1, 2, WIDTH - SIDEBAR_WIDTH, HEIGHT, filenavOninput, filesPanelDraw), // left panel
|
new win.WindowObject(1, 2, WIDTH - SIDEBAR_WIDTH, HEIGHT, filenavOninput, filesPanelDraw), // left panel
|
||||||
new win.WindowObject(WIDTH - SIDEBAR_WIDTH+1, 2, SIDEBAR_WIDTH, HEIGHT, ()=>{}, opPanelDraw),
|
new win.WindowObject(WIDTH - SIDEBAR_WIDTH+1, 2, SIDEBAR_WIDTH, HEIGHT, ()=>{}, opPanelDraw),
|
||||||
// new win.WindowObject(1, 2, SIDEBAR_WIDTH, HEIGHT, ()=>{}, opPanelDraw),
|
|
||||||
new win.WindowObject(SIDEBAR_WIDTH + 1, 2, WIDTH - SIDEBAR_WIDTH, HEIGHT, filenavOninput, filesPanelDraw), // right panel
|
new win.WindowObject(SIDEBAR_WIDTH + 1, 2, WIDTH - SIDEBAR_WIDTH, HEIGHT, filenavOninput, filesPanelDraw), // right panel
|
||||||
],
|
]
|
||||||
/*index 1: commands palette*/[
|
|
||||||
new win.WindowObject((WIDTH - POPUP_WIDTH) / 2, (HEIGHT - POPUP_HEIGHT) / 2, POPUP_WIDTH, POPUP_HEIGHT, paletteInput, paletteDraw, "Commands")
|
|
||||||
],
|
|
||||||
/*index 2: popup messages*/[
|
|
||||||
new win.WindowObject((WIDTH - POPUP_WIDTH) / 2, (HEIGHT - POPUP_HEIGHT) / 2, POPUP_WIDTH, POPUP_HEIGHT, popupInput, popupDraw)
|
|
||||||
]]
|
|
||||||
|
|
||||||
const LEFTPANEL = windows[0][0]
|
const LEFTPANEL = windows[0]
|
||||||
const OPPANEL = windows[0][1]
|
const OPPANEL = windows[1]
|
||||||
const RIGHTPANEL = windows[0][2]
|
const RIGHTPANEL = windows[2]
|
||||||
|
|
||||||
let currentPopup = 0
|
|
||||||
|
|
||||||
function makePopup(index) {
|
|
||||||
currentPopup = index
|
|
||||||
windowFocus.push(currentPopup)
|
|
||||||
for (let i = 0; i < windows.length; i++) {
|
|
||||||
windows[i].forEach(it => {
|
|
||||||
it.isHighlighted = (i == index)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePopup() {
|
|
||||||
windowFocus.pop()
|
|
||||||
const index = windowFocus.last
|
|
||||||
currentPopup = 0
|
|
||||||
for (let i = 0; i < windows.length; i++) {
|
|
||||||
windows[i].forEach(it => {
|
|
||||||
it.isHighlighted = (i == index)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawTitle() {
|
function drawTitle() {
|
||||||
// draw window title
|
// draw window title
|
||||||
@@ -651,18 +625,9 @@ function drawTitle() {
|
|||||||
|
|
||||||
|
|
||||||
function drawFilePanel() {
|
function drawFilePanel() {
|
||||||
// set highlight status
|
windows.forEach((panel, i) => {
|
||||||
const currentTopPanel = windowFocus.last()
|
panel.isHighlighted = (i == 2 * windowMode)
|
||||||
if (currentTopPanel == 0) {
|
})
|
||||||
windows[0].forEach((panel, i)=>{
|
|
||||||
panel.isHighlighted = (i == 2 * windowMode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
windows[0].forEach((panel, i)=>{
|
|
||||||
panel.isHighlighted = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (windowMode) {
|
if (windowMode) {
|
||||||
RIGHTPANEL.drawContents()
|
RIGHTPANEL.drawContents()
|
||||||
RIGHTPANEL.drawFrame()
|
RIGHTPANEL.drawFrame()
|
||||||
@@ -683,14 +648,6 @@ function drawOpPanel() {
|
|||||||
OPPANEL.drawFrame()
|
OPPANEL.drawFrame()
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawPopupPanel() {
|
|
||||||
if (currentPopup) {
|
|
||||||
windows[currentPopup][0].drawContents()
|
|
||||||
windows[currentPopup][0].drawFrame()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function redraw() {
|
function redraw() {
|
||||||
redrawRequested = true
|
redrawRequested = true
|
||||||
}
|
}
|
||||||
@@ -700,7 +657,7 @@ function _redraw() {
|
|||||||
drawTitle()
|
drawTitle()
|
||||||
drawFilePanel()
|
drawFilePanel()
|
||||||
drawOpPanel()
|
drawOpPanel()
|
||||||
drawPopupPanel()
|
setupPanelMouseRegions()
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearScr() {
|
function clearScr() {
|
||||||
@@ -708,6 +665,354 @@ function clearScr() {
|
|||||||
graphics.setBackground(34,51,68)
|
graphics.setBackground(34,51,68)
|
||||||
graphics.clearPixels(255)
|
graphics.clearPixels(255)
|
||||||
graphics.setGraphicsMode(0)
|
graphics.setGraphicsMode(0)
|
||||||
|
con.color_pair(COL_TEXT, COL_BACK)
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// File operations and op-panel actions
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function getCurrentDirStr(side) {
|
||||||
|
return path[side].concat(['']).join("\\").replaceAll('\\\\', '\\')
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampCursorAfterChange() {
|
||||||
|
const len = dirFileList[windowMode].length
|
||||||
|
if (cursor[windowMode] >= len) cursor[windowMode] = Math.max(0, len - 1)
|
||||||
|
const maxScroll = Math.max(0, len - LIST_HEIGHT)
|
||||||
|
if (scroll[windowMode] > maxScroll) scroll[windowMode] = maxScroll
|
||||||
|
if (scroll[windowMode] < 0) scroll[windowMode] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function actSwitchPanel() {
|
||||||
|
windowMode = 1 - windowMode
|
||||||
|
redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
function actGoUp() {
|
||||||
|
if (path[windowMode].length >= 1) {
|
||||||
|
path[windowMode].pop()
|
||||||
|
cursor[windowMode] = 0; scroll[windowMode] = 0
|
||||||
|
refreshFilePanelCache(windowMode)
|
||||||
|
_redraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function actActivate() {
|
||||||
|
let selectedFileCache = filePanelCache[windowMode][cursor[windowMode]]
|
||||||
|
if (!selectedFileCache || !selectedFileCache.file) return
|
||||||
|
let selectedFile = selectedFileCache.file
|
||||||
|
|
||||||
|
if (selectedFile.fullPath[1] == ":" && selectedFile.fullPath[2] == "\\" && selectedFile.fullPath.length == 3) {
|
||||||
|
path[windowMode].push(selectedFile.fullPath)
|
||||||
|
cursor[windowMode] = 0; scroll[windowMode] = 0
|
||||||
|
refreshFilePanelCache(windowMode)
|
||||||
|
_redraw()
|
||||||
|
}
|
||||||
|
else if (selectedFileCache.isDirectory) {
|
||||||
|
path[windowMode].push(selectedFileCache.filename)
|
||||||
|
cursor[windowMode] = 0; scroll[windowMode] = 0
|
||||||
|
refreshFilePanelCache(windowMode)
|
||||||
|
_redraw()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let fileext = selectedFileCache.filename.substring(selectedFileCache.filename.lastIndexOf(".") + 1).toLowerCase()
|
||||||
|
let execfun = EXEC_FUNS[fileext] || ((f) => _G.shell.execute(f))
|
||||||
|
let errorlevel = 0
|
||||||
|
|
||||||
|
con.curs_set(1); clearScr(); con.move(1,1)
|
||||||
|
try {
|
||||||
|
errorlevel = execfun(selectedFile.fullPath)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
println(e)
|
||||||
|
errorlevel = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorlevel) {
|
||||||
|
println("Hit Return/Enter key to continue . . . .")
|
||||||
|
sys.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
firstRunLatch = true
|
||||||
|
con.curs_set(0); clearScr()
|
||||||
|
refreshFilePanelCache(windowMode)
|
||||||
|
redraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function actCopy() {
|
||||||
|
if (path[windowMode].length === 0) return
|
||||||
|
const cache = filePanelCache[windowMode][cursor[windowMode]]
|
||||||
|
if (!cache || !cache.file) return
|
||||||
|
if (cache.isDirectory) { showMessagePopup('Copy', 'Directory copy is not supported.'); _redraw(); return }
|
||||||
|
if (path[1 - windowMode].length === 0) { showMessagePopup('Copy', 'Cannot copy to drive list view.'); _redraw(); return }
|
||||||
|
|
||||||
|
const srcPath = cache.file.fullPath
|
||||||
|
const dstDir = getCurrentDirStr(1 - windowMode)
|
||||||
|
const dstPath = dstDir + cache.file.name
|
||||||
|
if (srcPath === dstPath) { _redraw(); return } // both panels point to same directory
|
||||||
|
|
||||||
|
try {
|
||||||
|
const srcFile = files.open(srcPath)
|
||||||
|
const dstFile = files.open(dstPath)
|
||||||
|
if (!srcFile.exists) { showMessagePopup('Copy', 'Source not found.'); _redraw(); return }
|
||||||
|
if (dstFile.exists) {
|
||||||
|
if (!showConfirmPopup('Copy', `Overwrite "${cache.file.name}"?`)) { _redraw(); return }
|
||||||
|
}
|
||||||
|
if (!dstFile.exists) dstFile.mkFile()
|
||||||
|
dstFile.bwrite(srcFile.bread())
|
||||||
|
try { dstFile.flush() } catch (e) {}
|
||||||
|
try { dstFile.close() } catch (e) {}
|
||||||
|
try { srcFile.close() } catch (e) {}
|
||||||
|
refreshFilePanelCache(1 - windowMode)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
showMessagePopup('Copy failed', e.message || ('' + e))
|
||||||
|
}
|
||||||
|
_redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
function actMove() {
|
||||||
|
if (path[windowMode].length === 0) return
|
||||||
|
const cache = filePanelCache[windowMode][cursor[windowMode]]
|
||||||
|
if (!cache || !cache.file) return
|
||||||
|
if (cache.isDirectory) { showMessagePopup('Move', 'Directory move is not supported.'); _redraw(); return }
|
||||||
|
if (path[1 - windowMode].length === 0) { showMessagePopup('Move', 'Cannot move to drive list view.'); _redraw(); return }
|
||||||
|
|
||||||
|
const srcPath = cache.file.fullPath
|
||||||
|
const dstDir = getCurrentDirStr(1 - windowMode)
|
||||||
|
const dstPath = dstDir + cache.file.name
|
||||||
|
if (srcPath === dstPath) { _redraw(); return } // no-op
|
||||||
|
|
||||||
|
try {
|
||||||
|
const srcFile = files.open(srcPath)
|
||||||
|
const dstFile = files.open(dstPath)
|
||||||
|
if (!srcFile.exists) { showMessagePopup('Move', 'Source not found.'); _redraw(); return }
|
||||||
|
if (dstFile.exists) {
|
||||||
|
if (!showConfirmPopup('Move', `Overwrite "${cache.file.name}"?`)) { _redraw(); return }
|
||||||
|
}
|
||||||
|
if (!dstFile.exists) dstFile.mkFile()
|
||||||
|
dstFile.bwrite(srcFile.bread())
|
||||||
|
try { dstFile.flush() } catch (e) {}
|
||||||
|
try { dstFile.close() } catch (e) {}
|
||||||
|
srcFile.remove()
|
||||||
|
refreshFilePanelCache(windowMode)
|
||||||
|
refreshFilePanelCache(1 - windowMode)
|
||||||
|
clampCursorAfterChange()
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
showMessagePopup('Move failed', e.message || ('' + e))
|
||||||
|
}
|
||||||
|
_redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
function actDelete() {
|
||||||
|
if (path[windowMode].length === 0) return
|
||||||
|
const cache = filePanelCache[windowMode][cursor[windowMode]]
|
||||||
|
if (!cache || !cache.file) return
|
||||||
|
|
||||||
|
const name = cache.file.name
|
||||||
|
const kind = cache.isDirectory ? 'directory' : 'file'
|
||||||
|
if (!showConfirmPopup('Delete', `Delete ${kind} "${name}"?`)) { _redraw(); return }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const status = cache.file.remove()
|
||||||
|
if (status !== undefined && status !== 0 && status !== true) {
|
||||||
|
showMessagePopup('Delete failed', `Cannot delete "${name}" (status ${status}).`)
|
||||||
|
}
|
||||||
|
refreshFilePanelCache(windowMode)
|
||||||
|
clampCursorAfterChange()
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
showMessagePopup('Delete failed', e.message || ('' + e))
|
||||||
|
}
|
||||||
|
_redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
function actMkdir() {
|
||||||
|
if (path[windowMode].length === 0) { showMessagePopup('Mkdir', 'Choose a directory first.'); _redraw(); return }
|
||||||
|
const name = showInputPopup('Make Directory', 'Directory name:', '')
|
||||||
|
if (name === null || name.length === 0) { _redraw(); return }
|
||||||
|
|
||||||
|
const dstPath = getCurrentDirStr(windowMode) + name
|
||||||
|
try {
|
||||||
|
const dstFile = files.open(dstPath)
|
||||||
|
if (dstFile.exists) {
|
||||||
|
showMessagePopup('Mkdir', `"${name}" already exists.`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const ok = dstFile.mkDir()
|
||||||
|
if (!ok) showMessagePopup('Mkdir failed', `Cannot create "${name}".`)
|
||||||
|
else refreshFilePanelCache(windowMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
showMessagePopup('Mkdir failed', e.message || ('' + e))
|
||||||
|
}
|
||||||
|
_redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
function actRename() {
|
||||||
|
if (path[windowMode].length === 0) return
|
||||||
|
const cache = filePanelCache[windowMode][cursor[windowMode]]
|
||||||
|
if (!cache || !cache.file) return
|
||||||
|
if (cache.isDirectory) { showMessagePopup('Rename', 'Directory rename is not supported.'); _redraw(); return }
|
||||||
|
|
||||||
|
const oldName = cache.file.name
|
||||||
|
const newName = showInputPopup('Rename', 'New name:', oldName)
|
||||||
|
if (newName === null || newName.length === 0 || newName === oldName) { _redraw(); return }
|
||||||
|
|
||||||
|
const dirStr = getCurrentDirStr(windowMode)
|
||||||
|
const srcPath = cache.file.fullPath
|
||||||
|
const dstPath = dirStr + newName
|
||||||
|
|
||||||
|
try {
|
||||||
|
const srcFile = files.open(srcPath)
|
||||||
|
const dstFile = files.open(dstPath)
|
||||||
|
if (dstFile.exists) {
|
||||||
|
if (!showConfirmPopup('Rename', `Overwrite "${newName}"?`)) { _redraw(); return }
|
||||||
|
}
|
||||||
|
if (!dstFile.exists) dstFile.mkFile()
|
||||||
|
dstFile.bwrite(srcFile.bread())
|
||||||
|
try { dstFile.flush() } catch (e) {}
|
||||||
|
try { dstFile.close() } catch (e) {}
|
||||||
|
srcFile.remove()
|
||||||
|
refreshFilePanelCache(windowMode)
|
||||||
|
clampCursorAfterChange()
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
showMessagePopup('Rename failed', e.message || ('' + e))
|
||||||
|
}
|
||||||
|
_redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
function actMore() {
|
||||||
|
if (path[windowMode].length === 0) return
|
||||||
|
const cache = filePanelCache[windowMode][cursor[windowMode]]
|
||||||
|
if (!cache || !cache.file || cache.isDirectory) return
|
||||||
|
|
||||||
|
const res = win.showDialog({
|
||||||
|
title: 'More',
|
||||||
|
message: cache.file.name,
|
||||||
|
fields: [],
|
||||||
|
buttons: [
|
||||||
|
{ label: 'Execute', action: 'execute', default: true },
|
||||||
|
{ label: 'Edit', action: 'edit' },
|
||||||
|
{ label: 'Close', action: 'close' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
_redraw()
|
||||||
|
|
||||||
|
if (res.action === 'execute') {
|
||||||
|
actActivate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (res.action === 'edit') {
|
||||||
|
const editfun = EDIT_FUNS[cache.fileext]
|
||||||
|
|| ((f) => _G.shell.execute(`${DEFAULT_EDITOR} "${f}"`))
|
||||||
|
let errorlevel = 0
|
||||||
|
con.curs_set(1); clearScr(); con.move(1, 1)
|
||||||
|
try {
|
||||||
|
errorlevel = editfun(cache.file.fullPath)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
println(e)
|
||||||
|
errorlevel = 1
|
||||||
|
}
|
||||||
|
if (errorlevel) {
|
||||||
|
println("Hit Return/Enter key to continue . . . .")
|
||||||
|
sys.read()
|
||||||
|
}
|
||||||
|
firstRunLatch = true
|
||||||
|
con.curs_set(0); clearScr()
|
||||||
|
refreshFilePanelCache(windowMode)
|
||||||
|
redraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function actQuit() { exit = true }
|
||||||
|
|
||||||
|
function invokeOpAction(id) {
|
||||||
|
if (id === 'switch') actSwitchPanel()
|
||||||
|
else if (id === 'up') actGoUp()
|
||||||
|
else if (id === 'copy') actCopy()
|
||||||
|
else if (id === 'move') actMove()
|
||||||
|
else if (id === 'delete') actDelete()
|
||||||
|
else if (id === 'mkdir') actMkdir()
|
||||||
|
else if (id === 'rename') actRename()
|
||||||
|
else if (id === 'more') actMore()
|
||||||
|
else if (id === 'quit') actQuit()
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Mouse region setup (file list + op buttons)
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function setupPanelMouseRegions() {
|
||||||
|
clearPanelMouseRegions()
|
||||||
|
|
||||||
|
const fp = (windowMode === 0) ? LEFTPANEL : RIGHTPANEL
|
||||||
|
const fpX = fp.x + 1
|
||||||
|
const fpW = fp.width - 2
|
||||||
|
const fpY = fp.y + 2 // first file row (after frame top + header)
|
||||||
|
|
||||||
|
// Wheel-scroll over the file list. Wheel and keyboard are the only inputs allowed
|
||||||
|
// to move the scroll position; hover (below) only moves the caret.
|
||||||
|
addPanelMouseRegion(fpX, fpY, fpW, LIST_HEIGHT, {
|
||||||
|
onWheel: (cy, cx, dy) => {
|
||||||
|
const filesCount = dirFileList[windowMode].length
|
||||||
|
const maxScroll = Math.max(0, filesCount - LIST_HEIGHT)
|
||||||
|
let s = scroll[windowMode] + dy * 3
|
||||||
|
if (s > maxScroll) s = maxScroll
|
||||||
|
if (s < 0) s = 0
|
||||||
|
if (s !== scroll[windowMode]) {
|
||||||
|
scroll[windowMode] = s
|
||||||
|
drawFilePanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// One hover/click region per row so the caret can follow the mouse without
|
||||||
|
// calling scrollVert (which would re-scroll the list near the upper/lower thirds).
|
||||||
|
for (let i = 0; i < LIST_HEIGHT; i++) {
|
||||||
|
const rowIdx = i
|
||||||
|
addPanelMouseRegion(fpX, fpY + i, fpW, 1, {
|
||||||
|
onHover: () => {
|
||||||
|
const target = scroll[windowMode] + rowIdx
|
||||||
|
if (target < dirFileList[windowMode].length && cursor[windowMode] !== target) {
|
||||||
|
cursor[windowMode] = target
|
||||||
|
drawFilePanel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick: (cy, cx, btn) => {
|
||||||
|
if (btn !== 1) return
|
||||||
|
const target = scroll[windowMode] + rowIdx
|
||||||
|
if (target >= dirFileList[windowMode].length) return
|
||||||
|
cursor[windowMode] = target
|
||||||
|
actActivate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Op-panel button hover/click. Each button covers its icon row + label row.
|
||||||
|
const opX = OPPANEL.x + 1
|
||||||
|
const opW = SIDEBAR_WIDTH - 2
|
||||||
|
for (let i = 0; i < OP_BUTTONS.length; i++) {
|
||||||
|
const idx = i
|
||||||
|
const btn = OP_BUTTONS[i]
|
||||||
|
addPanelMouseRegion(opX, OPPANEL.y + 1 + btn.yOff, opW, btn.hitH || 2, {
|
||||||
|
onHover: () => {
|
||||||
|
if (opHover !== idx) { opHover = idx; drawOpPanel() }
|
||||||
|
},
|
||||||
|
onHoverLeave: () => {
|
||||||
|
if (opHover === idx) { opHover = -1; drawOpPanel() }
|
||||||
|
},
|
||||||
|
onClick: (cy, cx, btnNum) => {
|
||||||
|
if (btnNum !== 1) return
|
||||||
|
invokeOpAction(btn.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -717,6 +1022,12 @@ refreshFilePanelCache(0)
|
|||||||
refreshFilePanelCache(1)
|
refreshFilePanelCache(1)
|
||||||
_redraw()
|
_redraw()
|
||||||
|
|
||||||
|
// Drain inherited mouse/key state from whoever launched us. Polling launchers
|
||||||
|
// like fsh.js can hand off with the mouse button still held; without this,
|
||||||
|
// input.withEvent's first call edge-detects that as a fresh mouse_down at the
|
||||||
|
// cursor and activates whichever file row happens to sit there.
|
||||||
|
input.withEvent(() => {})
|
||||||
|
|
||||||
let redrawRequested = false
|
let redrawRequested = false
|
||||||
let exit = false
|
let exit = false
|
||||||
let firstRunLatch = true
|
let firstRunLatch = true
|
||||||
@@ -724,6 +1035,14 @@ let firstRunLatch = true
|
|||||||
while (!exit) {
|
while (!exit) {
|
||||||
input.withEvent(event => {
|
input.withEvent(event => {
|
||||||
|
|
||||||
|
if (dispatchMouseEvent(event)) {
|
||||||
|
if (redrawRequested) {
|
||||||
|
redrawRequested = false
|
||||||
|
_redraw()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let keysym = event[1]
|
let keysym = event[1]
|
||||||
let keyJustHit = (1 == event[2])
|
let keyJustHit = (1 == event[2])
|
||||||
|
|
||||||
@@ -735,7 +1054,7 @@ while (!exit) {
|
|||||||
firstRunLatch = false
|
firstRunLatch = false
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
windows[windowFocus.last()].forEach(it => {
|
windows.forEach(it => {
|
||||||
if (it.isHighlighted) { // double input processing without this? wtf?!
|
if (it.isHighlighted) { // double input processing without this? wtf?!
|
||||||
it.processInput(event)
|
it.processInput(event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,4 +180,349 @@ function scrollHorz(dx, stringSize, stringViewSize, currentCursorPos, currentScr
|
|||||||
return [currentCursorPos, currentScrollPos]
|
return [currentCursorPos, currentScrollPos]
|
||||||
}
|
}
|
||||||
|
|
||||||
exports = { WindowObject, scrollVert, scrollHorz }
|
// ---------------------------------------------------------------------------
|
||||||
|
// Modal dialog with multiple input fields and OK/Cancel-style buttons.
|
||||||
|
//
|
||||||
|
// opts = {
|
||||||
|
// title: string,
|
||||||
|
// message: string | string[]? -- optional body text drawn above fields
|
||||||
|
// fields: [{label, initial?, width}, ...] -- omit / [] for no input field
|
||||||
|
// buttons: [{label, action, default?}, ...] -- defaults to [OK, Cancel] (+ Delete
|
||||||
|
// if `allowDelete:true`)
|
||||||
|
// allowDelete: bool, -- inserts a Delete button (fsh compat)
|
||||||
|
// colours: {fg?, bg?, fieldBg?, dimFg?, hlFg?, focusBg?} -- per-call overrides
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Returns {action, values}: `action` is the chosen button's `action`
|
||||||
|
// (default "ok"/"cancel"/"delete"), or "cancel" on Esc; `values` is the array
|
||||||
|
// of field strings in field order.
|
||||||
|
//
|
||||||
|
// Behaviour:
|
||||||
|
// - Tab / Shift+Tab and arrow Down / Up cycle focus across fields and buttons.
|
||||||
|
// - Left / Right inside a field move the caret; on a button they cycle focus.
|
||||||
|
// - Home / End jump to start / end of the focused field.
|
||||||
|
// - Enter on a field jumps to the next field, then to the first button. Enter
|
||||||
|
// or Space on a button activates it.
|
||||||
|
// - Insert at caret. Backspace deletes left of caret; Forward-Del deletes right.
|
||||||
|
// - Blinking caret (`con.curs_set(1)`) is positioned on the focused field and
|
||||||
|
// hidden when a button has focus.
|
||||||
|
// - Mouse: left-click on a button activates it; click on a field puts focus
|
||||||
|
// on that field and positions the caret under the click. Mouse hover on a
|
||||||
|
// button highlights it.
|
||||||
|
const _dialogScreen = con.getmaxyx()
|
||||||
|
const _dialogPixDim = graphics.getPixelDimension()
|
||||||
|
const _CELL_PW = (_dialogPixDim[0] / _dialogScreen[1]) | 0
|
||||||
|
const _CELL_PH = (_dialogPixDim[1] / _dialogScreen[0]) | 0
|
||||||
|
function _pxToCell(px, py) { return [(py / _CELL_PH | 0) + 1, (px / _CELL_PW | 0) + 1] }
|
||||||
|
|
||||||
|
function showDialog(opts) {
|
||||||
|
const fields = opts.fields || []
|
||||||
|
const values = fields.map(f => (f.initial == null) ? '' : ('' + f.initial))
|
||||||
|
const cursors = values.map(v => v.length)
|
||||||
|
|
||||||
|
let oldFG = con.get_color_fore()
|
||||||
|
let oldBG = con.get_color_back()
|
||||||
|
|
||||||
|
let buttons
|
||||||
|
if (opts.buttons) {
|
||||||
|
buttons = opts.buttons
|
||||||
|
} else {
|
||||||
|
buttons = [{label: 'OK', action: 'ok', default: true}]
|
||||||
|
if (opts.allowDelete) buttons.push({label: 'Delete', action: 'delete'})
|
||||||
|
buttons.push({label: 'Cancel', action: 'cancel'})
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = opts.title || ''
|
||||||
|
const message = opts.message
|
||||||
|
const messageLines = !message ? []
|
||||||
|
: Array.isArray(message) ? message
|
||||||
|
: ('' + message).split('\n')
|
||||||
|
|
||||||
|
const c = opts.colours || {}
|
||||||
|
const fg = (c.fg != null) ? c.fg : 254
|
||||||
|
const bg = (c.bg != null) ? c.bg : 242
|
||||||
|
const fieldBg = (c.fieldBg != null) ? c.fieldBg : 240
|
||||||
|
const dimFg = (c.dimFg != null) ? c.dimFg : 249
|
||||||
|
const hlFg = (c.hlFg != null) ? c.hlFg : 230
|
||||||
|
const focusBg = (c.focusBg != null) ? c.focusBg : bg
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
const maxFieldW = fields.reduce((m, f) => Math.max(m, f.width), 16)
|
||||||
|
const longestMsg = messageLines.reduce((m, l) => Math.max(m, l.length), 0)
|
||||||
|
const titleW = title.length + 4
|
||||||
|
const btnRowW = buttons.reduce((s, b) => s + b.label.length + 5, 0) - 1
|
||||||
|
const w = Math.max(maxFieldW + 6, titleW + 4, longestMsg + 6, btnRowW + 4, 24)
|
||||||
|
const msgTopOff = (messageLines.length > 0) ? 1 : 0
|
||||||
|
const msgRows = messageLines.length + (messageLines.length > 0 ? 1 : 0)
|
||||||
|
const fieldsBlockH = fields.length * 4
|
||||||
|
const buttonsRowOff = 1 + msgRows + (fields.length > 0 ? fieldsBlockH + 1 : 1)
|
||||||
|
const h = buttonsRowOff + 2
|
||||||
|
const screen = con.getmaxyx()
|
||||||
|
const row = Math.max(2, Math.floor((screen[0] - h) / 2))
|
||||||
|
const col = Math.max(2, Math.floor((screen[1] - w) / 2))
|
||||||
|
|
||||||
|
// Pick initial focus: explicit default > first field > first button.
|
||||||
|
let focusIdx = -1
|
||||||
|
for (let i = 0; i < buttons.length; i++) {
|
||||||
|
if (buttons[i].default) { focusIdx = fields.length + i; break }
|
||||||
|
}
|
||||||
|
if (focusIdx < 0) focusIdx = fields.length > 0 ? 0 : fields.length
|
||||||
|
const totalFocus = fields.length + buttons.length
|
||||||
|
let hoverBtn = -1
|
||||||
|
let done = null
|
||||||
|
|
||||||
|
function fieldScroll(cur, fw) { return cur < fw ? 0 : cur - fw + 1 }
|
||||||
|
function fieldLabelRow(i) { return row + 1 + msgRows + i * 4 }
|
||||||
|
function fieldBoxRow(i) { return fieldLabelRow(i) + 1 }
|
||||||
|
function fieldContentRow(i) { return fieldLabelRow(i) + 2 }
|
||||||
|
function fieldBoxCol() { return col + 2 }
|
||||||
|
function fieldContentRegion(i) { return { x: fieldBoxCol() + 1, y: fieldContentRow(i), w: fields[i].width } }
|
||||||
|
|
||||||
|
function buttonRegions() {
|
||||||
|
let bx = col + Math.floor((w - btnRowW) / 2)
|
||||||
|
return buttons.map(b => {
|
||||||
|
const r = { x: bx, y: row + buttonsRowOff, w: b.label.length + 4 }
|
||||||
|
bx += b.label.length + 5
|
||||||
|
return r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFrameBox() {
|
||||||
|
con.color_pair(fg, bg)
|
||||||
|
for (let r = row; r < row + h; r++) {
|
||||||
|
con.move(r, col)
|
||||||
|
print(' '.repeat(w))
|
||||||
|
}
|
||||||
|
const wo = new WindowObject(col, row, w, h, ()=>{}, ()=>{}, title)
|
||||||
|
wo.isHighlighted = true
|
||||||
|
wo.titleBack = bg
|
||||||
|
wo.drawFrame()
|
||||||
|
con.color_pair(fg, bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawMessage() {
|
||||||
|
if (messageLines.length === 0) return
|
||||||
|
con.color_pair(fg, bg)
|
||||||
|
for (let i = 0; i < messageLines.length; i++) {
|
||||||
|
con.move(row + 1 + i, col + 2)
|
||||||
|
print(messageLines[i].padEnd(w - 4, ' '))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawField(i) {
|
||||||
|
const f = fields[i]
|
||||||
|
const fbCol = fieldBoxCol()
|
||||||
|
const fbRow = fieldBoxRow(i)
|
||||||
|
const fw = f.width
|
||||||
|
const focused = (focusIdx === i)
|
||||||
|
const frameFg = focused ? fg : dimFg
|
||||||
|
|
||||||
|
// Label
|
||||||
|
con.color_pair(fg, bg)
|
||||||
|
con.move(fieldLabelRow(i), fbCol)
|
||||||
|
print(f.label + ':')
|
||||||
|
|
||||||
|
// Top border
|
||||||
|
con.color_pair(frameFg, bg)
|
||||||
|
con.move(fbRow, fbCol)
|
||||||
|
print('\u00DA' + '\u00C4'.repeat(fw) + '\u00BF')
|
||||||
|
|
||||||
|
// Side borders + content
|
||||||
|
con.color_pair(frameFg, bg)
|
||||||
|
con.move(fbRow + 1, fbCol)
|
||||||
|
print('\u00B3')
|
||||||
|
con.color_pair(fg, fieldBg)
|
||||||
|
const s = fieldScroll(cursors[i], fw)
|
||||||
|
const vis = values[i].substring(s, s + fw)
|
||||||
|
print(vis.padEnd(fw, ' '))
|
||||||
|
con.color_pair(frameFg, bg)
|
||||||
|
con.move(fbRow + 1, fbCol + fw + 1)
|
||||||
|
print('\u00B3')
|
||||||
|
|
||||||
|
// Bottom border
|
||||||
|
con.move(fbRow + 2, fbCol)
|
||||||
|
print('\u00C0' + '\u00C4'.repeat(fw) + '\u00D9')
|
||||||
|
con.color_pair(fg, bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawButton(i, regions) {
|
||||||
|
const b = buttons[i]
|
||||||
|
const bIdx = fields.length + i
|
||||||
|
const focused = (focusIdx === bIdx)
|
||||||
|
const hovered = (hoverBtn === i)
|
||||||
|
const r = regions[i]
|
||||||
|
let useFg, useBg
|
||||||
|
if (focused && hovered) { useFg = hlFg; useBg = focusBg }
|
||||||
|
else if (focused) { useFg = hlFg; useBg = focusBg }
|
||||||
|
else if (hovered) { useFg = hlFg; useBg = bg }
|
||||||
|
else { useFg = fg; useBg = bg }
|
||||||
|
con.color_pair(useFg, useBg)
|
||||||
|
con.move(r.y, r.x)
|
||||||
|
print('[ ' + b.label + ' ]')
|
||||||
|
con.color_pair(fg, bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionCaret() {
|
||||||
|
if (focusIdx < fields.length) {
|
||||||
|
const fw = fields[focusIdx].width
|
||||||
|
const s = fieldScroll(cursors[focusIdx], fw)
|
||||||
|
con.move(fieldContentRow(focusIdx), fieldBoxCol() + 1 + (cursors[focusIdx] - s))
|
||||||
|
con.curs_set(1)
|
||||||
|
} else {
|
||||||
|
con.curs_set(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
drawFrameBox()
|
||||||
|
drawMessage()
|
||||||
|
for (let i = 0; i < fields.length; i++) drawField(i)
|
||||||
|
const regs = buttonRegions()
|
||||||
|
for (let i = 0; i < buttons.length; i++) drawButton(i, regs)
|
||||||
|
positionCaret()
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveFocus(dir) {
|
||||||
|
focusIdx = (focusIdx + dir + totalFocus) % totalFocus
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateButton(i) {
|
||||||
|
done = { action: buttons[i].action, values: values.slice() }
|
||||||
|
}
|
||||||
|
|
||||||
|
function hitTestMouse(ev) {
|
||||||
|
const cell = _pxToCell(ev[1], ev[2])
|
||||||
|
const cy = cell[0], cx = cell[1]
|
||||||
|
const btnRegs = buttonRegions()
|
||||||
|
for (let i = 0; i < btnRegs.length; i++) {
|
||||||
|
const r = btnRegs[i]
|
||||||
|
if (cy === r.y && cx >= r.x && cx < r.x + r.w) return { kind: 'button', idx: i }
|
||||||
|
}
|
||||||
|
for (let i = 0; i < fields.length; i++) {
|
||||||
|
const r = fieldContentRegion(i)
|
||||||
|
if (cy === r.y && cx >= r.x && cx < r.x + r.w) return { kind: 'field', idx: i, cx: cx, region: r }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
|
||||||
|
let eventJustReceived = true
|
||||||
|
while (done === null) {
|
||||||
|
input.withEvent(ev => {
|
||||||
|
if (eventJustReceived && (ev[0] === 'key_down' || ev[0] === 'mouse_down')) {
|
||||||
|
eventJustReceived = false; return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev[0] === 'mouse_move') {
|
||||||
|
const hit = hitTestMouse(ev)
|
||||||
|
const newHover = (hit && hit.kind === 'button') ? hit.idx : -1
|
||||||
|
if (newHover !== hoverBtn) {
|
||||||
|
hoverBtn = newHover
|
||||||
|
const regs = buttonRegions()
|
||||||
|
for (let i = 0; i < buttons.length; i++) drawButton(i, regs)
|
||||||
|
positionCaret()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ev[0] === 'mouse_down') {
|
||||||
|
if (ev[3] !== 1) return
|
||||||
|
const hit = hitTestMouse(ev)
|
||||||
|
if (!hit) return
|
||||||
|
if (hit.kind === 'button') {
|
||||||
|
focusIdx = fields.length + hit.idx
|
||||||
|
render()
|
||||||
|
activateButton(hit.idx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (hit.kind === 'field') {
|
||||||
|
focusIdx = hit.idx
|
||||||
|
const fw = fields[hit.idx].width
|
||||||
|
const s = fieldScroll(cursors[hit.idx], fw)
|
||||||
|
const newCur = s + (hit.cx - hit.region.x)
|
||||||
|
cursors[hit.idx] = Math.min(values[hit.idx].length, Math.max(0, newCur))
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ev[0] !== 'key_down') return
|
||||||
|
if (1 !== ev[2]) return
|
||||||
|
const ks = ev[1]
|
||||||
|
const shiftDown = (ev.includes(59) || ev.includes(60))
|
||||||
|
|
||||||
|
if (ks === '<ESC>') { done = { action: 'cancel', values: values.slice() }; return }
|
||||||
|
|
||||||
|
if (ks === '\t' || ks === '<TAB>') { moveFocus(shiftDown ? -1 : 1); return }
|
||||||
|
if (ks === '<UP>') { moveFocus(-1); return }
|
||||||
|
if (ks === '<DOWN>') { moveFocus(+1); return }
|
||||||
|
|
||||||
|
if (ks === '<LEFT>') {
|
||||||
|
if (focusIdx < fields.length) {
|
||||||
|
if (cursors[focusIdx] > 0) { cursors[focusIdx] -= 1; render() }
|
||||||
|
} else moveFocus(-1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ks === '<RIGHT>') {
|
||||||
|
if (focusIdx < fields.length) {
|
||||||
|
if (cursors[focusIdx] < values[focusIdx].length) { cursors[focusIdx] += 1; render() }
|
||||||
|
} else moveFocus(+1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ks === '<HOME>') {
|
||||||
|
if (focusIdx < fields.length) { cursors[focusIdx] = 0; render() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ks === '<END>') {
|
||||||
|
if (focusIdx < fields.length) { cursors[focusIdx] = values[focusIdx].length; render() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (focusIdx < fields.length) {
|
||||||
|
if (ks === '\n') {
|
||||||
|
focusIdx = (focusIdx < fields.length - 1) ? focusIdx + 1 : fields.length
|
||||||
|
render()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ks === '') {
|
||||||
|
const cur = cursors[focusIdx]
|
||||||
|
if (cur > 0) {
|
||||||
|
const v = values[focusIdx]
|
||||||
|
values[focusIdx] = v.substring(0, cur - 1) + v.substring(cur)
|
||||||
|
cursors[focusIdx] = cur - 1
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ks === '<FORWARD_DEL>' || ks === '<DEL>') {
|
||||||
|
const cur = cursors[focusIdx]
|
||||||
|
const v = values[focusIdx]
|
||||||
|
if (cur < v.length) {
|
||||||
|
values[focusIdx] = v.substring(0, cur) + v.substring(cur + 1)
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof ks === 'string' && ks.length === 1) {
|
||||||
|
const code = ks.charCodeAt(0)
|
||||||
|
if (code >= 32 && code < 256 && values[focusIdx].length < fields[focusIdx].width * 4) {
|
||||||
|
const v = values[focusIdx]
|
||||||
|
const cur = cursors[focusIdx]
|
||||||
|
values[focusIdx] = v.substring(0, cur) + ks + v.substring(cur)
|
||||||
|
cursors[focusIdx] = cur + 1
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (ks === '\n' || ks === ' ') { activateButton(focusIdx - fields.length); return }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
con.curs_set(0)
|
||||||
|
con.color_pair(oldFG, oldBG)
|
||||||
|
return done
|
||||||
|
}
|
||||||
|
|
||||||
|
exports = { WindowObject, scrollVert, scrollHorz, showDialog }
|
||||||
|
|||||||
@@ -49,7 +49,13 @@ MMIO
|
|||||||
0..31 RO: Raw Keyboard Buffer read. Won't shift the key buffer
|
0..31 RO: Raw Keyboard Buffer read. Won't shift the key buffer
|
||||||
32..33 RO: Mouse X pos
|
32..33 RO: Mouse X pos
|
||||||
34..35 RO: Mouse Y pos
|
34..35 RO: Mouse Y pos
|
||||||
36 RO: Mouse down? (1 for LEFT, 2 for RIGHT, 3 for BOTH)
|
36 RO: Mouse down?
|
||||||
|
bit 0: left
|
||||||
|
bit 1: right
|
||||||
|
bit 2: middle
|
||||||
|
|
||||||
|
bit 6: wheel up
|
||||||
|
bit 7: wheel down
|
||||||
37 RW: Read/Write single key input. Key buffer will be shifted. Manual writing is
|
37 RW: Read/Write single key input. Key buffer will be shifted. Manual writing is
|
||||||
usually unnecessary as such action must be automatically managed via LibGDX
|
usually unnecessary as such action must be automatically managed via LibGDX
|
||||||
input processing.
|
input processing.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import net.torvald.tsvm.CircularArray
|
|||||||
import net.torvald.tsvm.VM
|
import net.torvald.tsvm.VM
|
||||||
import net.torvald.tsvm.isNonZero
|
import net.torvald.tsvm.isNonZero
|
||||||
import net.torvald.tsvm.toInt
|
import net.torvald.tsvm.toInt
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import kotlin.experimental.and
|
import kotlin.experimental.and
|
||||||
|
|
||||||
class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
||||||
@@ -32,6 +33,13 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
|||||||
*/
|
*/
|
||||||
var inputViewport: Viewport? = null
|
var inputViewport: Viewport? = null
|
||||||
private val tmpMouseVec = Vector2()
|
private val tmpMouseVec = Vector2()
|
||||||
|
// Letterbox offset and renderable area inside the inputViewport, set by the host VMGUI.
|
||||||
|
// After unproject, mouse pixel coords are shifted by (inputOriginX, inputOriginY) and
|
||||||
|
// clamped to (inputAreaW, inputAreaH) so apps see VM-screen pixel coords (0..drawWidth).
|
||||||
|
var inputOriginX: Int = 0
|
||||||
|
var inputOriginY: Int = 0
|
||||||
|
var inputAreaW: Int = Int.MAX_VALUE
|
||||||
|
var inputAreaH: Int = Int.MAX_VALUE
|
||||||
|
|
||||||
/** Accepts a keycode */
|
/** Accepts a keycode */
|
||||||
private val keyboardBuffer = CircularArray<Byte>(32, true)
|
private val keyboardBuffer = CircularArray<Byte>(32, true)
|
||||||
@@ -108,7 +116,12 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
|||||||
in 0..31 -> keyboardBuffer[(addr.toInt())] ?: -1
|
in 0..31 -> keyboardBuffer[(addr.toInt())] ?: -1
|
||||||
in 32..33 -> (mouseX.toInt() shr (adi - 32).times(8)).toByte()
|
in 32..33 -> (mouseX.toInt() shr (adi - 32).times(8)).toByte()
|
||||||
in 34..35 -> (mouseY.toInt() shr (adi - 34).times(8)).toByte()
|
in 34..35 -> (mouseY.toInt() shr (adi - 34).times(8)).toByte()
|
||||||
36L -> mouseButtons.toByte() // only bits 0..1 used; higher bits intentionally truncated
|
36L -> {
|
||||||
|
// bit 0: left, bit 1: right, bit 2: middle, bit 6: wheel up, bit 7: wheel down
|
||||||
|
// Wheel bits are latched on scrolled() and cleared on read so a one-shot
|
||||||
|
// detent fires exactly once for the polling app.
|
||||||
|
(mouseButtons or wheelLatch.getAndSet(0)).toByte()
|
||||||
|
}
|
||||||
37L -> {
|
37L -> {
|
||||||
val key = keyboardBuffer.removeTail() ?: -1
|
val key = keyboardBuffer.removeTail() ?: -1
|
||||||
keyPushed = !keyboardBuffer.isEmpty // Clear flag when buffer becomes empty
|
keyPushed = !keyboardBuffer.isEmpty // Clear flag when buffer becomes empty
|
||||||
@@ -290,7 +303,9 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
|||||||
|
|
||||||
private var mouseX: Short = 0
|
private var mouseX: Short = 0
|
||||||
private var mouseY: Short = 0
|
private var mouseY: Short = 0
|
||||||
private var mouseButtons: Int = 0 // bit 0 = LEFT, bit 1 = RIGHT
|
private var mouseButtons: Int = 0 // bit 0 = LEFT, bit 1 = RIGHT, bit 2 = MIDDLE
|
||||||
|
// bits 6 (wheel up) and 7 (wheel down) — set by scrolled(), cleared on MMIO[36] read
|
||||||
|
private val wheelLatch = AtomicInteger(0)
|
||||||
private var systemUptime = 0L
|
private var systemUptime = 0L
|
||||||
private var rtc = 0L
|
private var rtc = 0L
|
||||||
|
|
||||||
@@ -310,18 +325,24 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
|||||||
// VM sees logical framebuffer pixels regardless of window magnification,
|
// VM sees logical framebuffer pixels regardless of window magnification,
|
||||||
// letterboxing or sub-region placement done by an embedding GDX app.
|
// letterboxing or sub-region placement done by an embedding GDX app.
|
||||||
val vp = inputViewport
|
val vp = inputViewport
|
||||||
|
val rawX: Int
|
||||||
|
val rawY: Int
|
||||||
if (vp != null) {
|
if (vp != null) {
|
||||||
tmpMouseVec.set(Gdx.input.x.toFloat(), Gdx.input.y.toFloat())
|
tmpMouseVec.set(Gdx.input.x.toFloat(), Gdx.input.y.toFloat())
|
||||||
vp.unproject(tmpMouseVec)
|
vp.unproject(tmpMouseVec)
|
||||||
mouseX = tmpMouseVec.x.toInt().toShort()
|
rawX = tmpMouseVec.x.toInt()
|
||||||
mouseY = tmpMouseVec.y.toInt().toShort()
|
rawY = tmpMouseVec.y.toInt()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
mouseX = Gdx.input.x.toShort()
|
rawX = Gdx.input.x
|
||||||
mouseY = Gdx.input.y.toShort()
|
rawY = Gdx.input.y
|
||||||
}
|
}
|
||||||
mouseButtons = (if (Gdx.input.isButtonPressed(Input.Buttons.LEFT)) 1 else 0) or
|
// Subtract the letterbox origin so apps see VM-screen pixel coords (0..drawWidth).
|
||||||
(if (Gdx.input.isButtonPressed(Input.Buttons.RIGHT)) 2 else 0)
|
mouseX = (rawX - inputOriginX).coerceIn(0, inputAreaW - 1).toShort()
|
||||||
|
mouseY = (rawY - inputOriginY).coerceIn(0, inputAreaH - 1).toShort()
|
||||||
|
mouseButtons = (if (Gdx.input.isButtonPressed(Input.Buttons.LEFT)) 1 else 0) or
|
||||||
|
(if (Gdx.input.isButtonPressed(Input.Buttons.RIGHT)) 2 else 0) or
|
||||||
|
(if (Gdx.input.isButtonPressed(Input.Buttons.MIDDLE)) 4 else 0)
|
||||||
|
|
||||||
// strobe keys to fill the key read buffer
|
// strobe keys to fill the key read buffer
|
||||||
var keysPushed = 0
|
var keysPushed = 0
|
||||||
@@ -398,8 +419,15 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun scrolled(p0: Float, p1: Float): Boolean {
|
override fun scrolled(amountX: Float, amountY: Float): Boolean {
|
||||||
return false
|
// LibGDX: amountY > 0 = scroll DOWN (toward user), amountY < 0 = scroll UP.
|
||||||
|
// Latch bits 6/7 of MMIO[36]; the latch is cleared the next time MMIO[36] is read.
|
||||||
|
if (Gdx.input.inputProcessor !== this) return false
|
||||||
|
when {
|
||||||
|
amountY < 0f -> wheelLatch.updateAndGet { it or 0x40 }
|
||||||
|
amountY > 0f -> wheelLatch.updateAndGet { it or 0x80 }
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun keyUp(p0: Int): Boolean {
|
override fun keyUp(p0: Int): Boolean {
|
||||||
|
|||||||
@@ -170,6 +170,10 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
|
|||||||
|
|
||||||
Gdx.input.inputProcessor = vm.getIO()
|
Gdx.input.inputProcessor = vm.getIO()
|
||||||
vm.getIO().inputViewport = inputViewport
|
vm.getIO().inputViewport = inputViewport
|
||||||
|
vm.getIO().inputOriginX = (viewportWidth - loaderInfo.drawWidth) / 2
|
||||||
|
vm.getIO().inputOriginY = (viewportHeight - loaderInfo.drawHeight) / 2
|
||||||
|
vm.getIO().inputAreaW = loaderInfo.drawWidth
|
||||||
|
vm.getIO().inputAreaH = loaderInfo.drawHeight
|
||||||
|
|
||||||
if (usememvwr) memvwr = Memvwr(vm)
|
if (usememvwr) memvwr = Memvwr(vm)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user