mirror of
https://github.com/20kdc/OC-KittenOS.git
synced 2024-11-26 20:38:06 +11:00
dcd7154ec2
This license change should only increase the amount of stuff people can do in countries where the public domain is not a valid concept. If this license somehow restricts you as opposed to the previous one, please file an issue.
413 lines
11 KiB
Lua
413 lines
11 KiB
Lua
-- Copyright (C) 2018-2021 by KittenOS NEO contributors
|
|
--
|
|
-- Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
|
--
|
|
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
-- THIS SOFTWARE.
|
|
|
|
-- neoux: Implements utilities on top of Everest & event:
|
|
-- Everest crash protection
|
|
|
|
-- Control reference
|
|
-- x/y/w/h: ints, position/size, 1,1 TL
|
|
-- selectable: boolean
|
|
-- key(window, update, char, code, down, keyFlags) (If this returns something truthy, defaults are inhibited)
|
|
-- touch(window, update, x, y, xI, yI, button)
|
|
-- drag(window, update, x, y, xI, yI, button)
|
|
-- drop(window, update, x, y, xI, yI, button)
|
|
-- scroll(window, update, x, y, xI, yI, amount)
|
|
-- clipboard(window, update, contents)
|
|
|
|
-- Global forces reference. Otherwise, nasty duplication happens.
|
|
newNeoux = function (event, neo)
|
|
-- id -> callback
|
|
local lclEvToW = {}
|
|
local everest = neo.requireAccess("x.neo.pub.window", "windowing")
|
|
event.listen("x.neo.pub.window", function (_, window, tp, ...)
|
|
if lclEvToW[window] then
|
|
lclEvToW[window](tp, ...)
|
|
end
|
|
end)
|
|
local neoux = {}
|
|
neoux.fileDialog = function (forWrite, callback, dfn)
|
|
local sync = false
|
|
local rtt = nil
|
|
if not callback then
|
|
sync = true
|
|
callback = function (rt)
|
|
sync = false
|
|
rtt = rt
|
|
end
|
|
end
|
|
local tag = neo.requireAccess("x.neo.pub.base", "filedialog").showFileDialogAsync(forWrite, dfn)
|
|
local f
|
|
f = function (_, fd, tg, re)
|
|
if fd == "filedialog" then
|
|
if tg == tag then
|
|
callback(re)
|
|
event.ignore(f)
|
|
end
|
|
end
|
|
end
|
|
event.listen("x.neo.pub.base", f)
|
|
while sync do
|
|
event.pull()
|
|
end
|
|
return rtt
|
|
end
|
|
-- Creates a wrapper around a window.
|
|
neoux.create = function (w, h, title, callback)
|
|
local window = {}
|
|
local windowCore = everest(w, h, title)
|
|
-- res is the window!
|
|
lclEvToW[windowCore.id] = function (...) callback(window, ...) end
|
|
-- API convenience: args compatible with .create
|
|
window.reset = function (nw, nh, _, cb)
|
|
callback = cb
|
|
w = nw or w
|
|
h = nh or h
|
|
windowCore.setSize(w, h)
|
|
end
|
|
window.getSize = function ()
|
|
return w, h
|
|
end
|
|
window.setSize = function (nw, nh)
|
|
w = nw
|
|
h = nh
|
|
windowCore.setSize(w, h)
|
|
end
|
|
window.getDepth = windowCore.getDepth
|
|
window.span = windowCore.span
|
|
window.recommendPalette = windowCore.recommendPalette
|
|
window.close = function ()
|
|
windowCore.close()
|
|
lclEvToW[windowCore.id] = nil
|
|
windowCore = nil
|
|
end
|
|
return window
|
|
end
|
|
-- Padding function
|
|
neoux.pad = function (...)
|
|
local fmt = require("fmttext")
|
|
return fmt.pad(...)
|
|
end
|
|
-- Text dialog formatting function.
|
|
-- Assumes you've run unicode.safeTextFormat if need be
|
|
neoux.fmtText = function (...)
|
|
local fmt = require("fmttext")
|
|
return fmt.fmtText(...)
|
|
end
|
|
-- UI FRAMEWORK --
|
|
neoux.tcwindow = function (w, h, controls, closing, bg, fg, selIndex, keyFlags)
|
|
local function rotateSelIndex()
|
|
local original = selIndex
|
|
while true do
|
|
selIndex = selIndex + 1
|
|
if not controls[selIndex] then
|
|
selIndex = 1
|
|
end
|
|
if controls[selIndex] then
|
|
if controls[selIndex].selectable then
|
|
return
|
|
end
|
|
end
|
|
if selIndex == original then
|
|
return
|
|
end
|
|
end
|
|
end
|
|
if not selIndex then
|
|
selIndex = #controls
|
|
if #controls == 0 then
|
|
selIndex = 1
|
|
end
|
|
rotateSelIndex()
|
|
end
|
|
keyFlags = keyFlags or {}
|
|
local function moveIndex(vertical, negative)
|
|
if not controls[selIndex] then return end
|
|
local currentMA, currentOA = controls[selIndex].y, controls[selIndex].x
|
|
local currentMAX = controls[selIndex].y + controls[selIndex].h - 1
|
|
if vertical then
|
|
currentMA, currentOA = controls[selIndex].x, controls[selIndex].y
|
|
currentMAX = controls[selIndex].x + controls[selIndex].w - 1
|
|
end
|
|
local bestOA = 9001
|
|
local bestSI = selIndex
|
|
if negative then
|
|
bestOA = -9000
|
|
end
|
|
for k, v in ipairs(controls) do
|
|
if (k ~= selIndex) and v.selectable then
|
|
local ma, oa = v.y, v.x
|
|
local max = v.y + v.h - 1
|
|
if vertical then
|
|
ma, oa = v.x, v.y
|
|
max = v.x + v.w - 1
|
|
end
|
|
if (ma >= currentMA and ma <= currentMAX) or (max >= currentMA and max <= currentMAX)
|
|
or (currentMA >= ma and currentMA <= max) or (currentMAX >= ma and currentMAX <= max) then
|
|
if negative then
|
|
if (oa < currentOA) and (oa > bestOA) then
|
|
bestOA = oa
|
|
bestSI = k
|
|
end
|
|
else
|
|
if (oa > currentOA) and (oa < bestOA) then
|
|
bestOA = oa
|
|
bestSI = k
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
selIndex = bestSI
|
|
end
|
|
|
|
local function doLine(window, a)
|
|
window.span(1, a, (" "):rep(w), bg, fg)
|
|
for k, v in ipairs(controls) do
|
|
if a >= v.y then
|
|
if a < (v.y + v.h) then
|
|
v.line(window, v.x, a, (a - v.y) + 1, bg, fg, selIndex == k)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local function doZone(window, control, cache)
|
|
for i = 1, control.h do
|
|
local l = i + control.y - 1
|
|
if (not cache) or (not cache[l]) then
|
|
doLine(window, l)
|
|
if cache then cache[l] = true end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function moveIndexAU(window, vertical, negative)
|
|
local c1 = controls[selIndex]
|
|
moveIndex(vertical, negative)
|
|
local c2 = controls[selIndex]
|
|
local cache = {}
|
|
if c1 then doZone(window, c1, cache) end
|
|
if c2 then doZone(window, c2, cache) end
|
|
end
|
|
-- Attach .update for external interference
|
|
for k, v in ipairs(controls) do
|
|
v.update = function (window) doZone(window, v, {}) end
|
|
end
|
|
return function (window, ev, a, b, c, d, e)
|
|
-- X,Y,Xi,Yi,B
|
|
if ev == "touch" then
|
|
local found = nil
|
|
for k, v in ipairs(controls) do
|
|
if v.selectable then
|
|
if a >= v.x then
|
|
if a < (v.x + v.w) then
|
|
if b >= v.y then
|
|
if b < (v.y + v.h) then
|
|
found = k
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if found then
|
|
local c1 = controls[selIndex]
|
|
selIndex = found
|
|
local c2 = controls[selIndex]
|
|
local cache = {}
|
|
if c1 then doZone(window, c1, cache) end
|
|
if c2 then
|
|
doZone(window, c2, cache)
|
|
if c2.touch then
|
|
c2.touch(window, function () doZone(window, c2) end, (a - c2.x) + 1, (b - c2.y) + 1, c, d, e)
|
|
end
|
|
end
|
|
end
|
|
-- X,Y,Xi,Yi,B (or D for scroll)
|
|
elseif ev == "drag" or ev == "drop" or ev == "scroll" then
|
|
if controls[selIndex] then
|
|
if controls[selIndex][ev] then
|
|
controls[selIndex][ev](window, function () doZone(window, controls[selIndex]) end, (a - controls[selIndex].x) + 1, (b - controls[selIndex].y) + 1, c, d, e)
|
|
end
|
|
end
|
|
elseif ev == "key" then
|
|
if controls[selIndex] and controls[selIndex].key then
|
|
if controls[selIndex].key(window, function () doZone(window, controls[selIndex]) end, a, b, c, keyFlags) then
|
|
return
|
|
end
|
|
end
|
|
if b == 29 then
|
|
keyFlags.ctrl = c
|
|
elseif b == 157 then
|
|
keyFlags.rctrl = c
|
|
elseif b == 42 then
|
|
keyFlags.shift = c
|
|
elseif b == 54 then
|
|
keyFlags.rshift = c
|
|
elseif not (keyFlags.ctrl or keyFlags.rctrl or keyFlags.shift or keyFlags.rshift) then
|
|
if b == 203 then
|
|
if c then
|
|
moveIndexAU(window, false, true)
|
|
end
|
|
elseif b == 205 then
|
|
if c then
|
|
moveIndexAU(window, false, false)
|
|
end
|
|
elseif b == 200 then
|
|
if c then
|
|
moveIndexAU(window, true, true)
|
|
end
|
|
elseif b == 208 then
|
|
if c then
|
|
moveIndexAU(window, true, false)
|
|
end
|
|
elseif a == 9 then
|
|
if c then
|
|
local c1 = controls[selIndex]
|
|
rotateSelIndex()
|
|
local c2 = controls[selIndex]
|
|
local cache = {}
|
|
if c1 then doZone(window, c1, cache) end
|
|
if c2 then doZone(window, c2, cache) end
|
|
end
|
|
end
|
|
end
|
|
elseif ev == "clipboard" then
|
|
if controls[selIndex] then
|
|
if controls[selIndex].clipboard then
|
|
controls[selIndex].clipboard(window, function () doZone(window, controls[selIndex]) end, a)
|
|
end
|
|
end
|
|
elseif ev == "line" then
|
|
doLine(window, a)
|
|
elseif ev == "close" then
|
|
closing(window)
|
|
end
|
|
end
|
|
end
|
|
neoux.tcrawview = function (x, y, lines)
|
|
return {
|
|
x = x,
|
|
y = y,
|
|
w = unicode.len(lines[1]),
|
|
h = #lines,
|
|
selectable = false,
|
|
line = function (window, x, y, lined, bg, fg, selected)
|
|
-- Can't be selected normally so ignore that flag
|
|
window.span(x, y, lines[lined], bg, fg)
|
|
end
|
|
}
|
|
end
|
|
neoux.tchdivider = function (x, y, w)
|
|
return neoux.tcrawview(x, y, {("-"):rep(w)})
|
|
end
|
|
neoux.tcvdivider = function (x, y, h)
|
|
local n = {}
|
|
for i = 1, h do
|
|
n[i] = "|"
|
|
end
|
|
return neoux.tcrawview(x, y, n)
|
|
end
|
|
neoux.tcbutton = function (x, y, text, callback)
|
|
text = "<" .. text .. ">"
|
|
return {
|
|
x = x,
|
|
y = y,
|
|
w = unicode.len(text),
|
|
h = 1,
|
|
selectable = true,
|
|
key = function (window, update, a, c, d, f)
|
|
if d then
|
|
if a == 13 or a == 32 then
|
|
callback(window)
|
|
return true
|
|
end
|
|
end
|
|
end,
|
|
touch = function (window, update, x, y, button)
|
|
callback(window)
|
|
end,
|
|
line = function (window, x, y, lind, bg, fg, selected)
|
|
local fg1 = fg
|
|
if selected then
|
|
fg = bg
|
|
bg = fg1
|
|
end
|
|
window.span(x, y, text, bg, fg)
|
|
end
|
|
}
|
|
end
|
|
-- Note: w should be at least 2 - this is similar to buttons.
|
|
neoux.tcfield = function (x, y, w, textprop)
|
|
-- compat. workaround for apps which nuke tcfields
|
|
local p = unicode.len(textprop()) + 1
|
|
return {
|
|
x = x,
|
|
y = y,
|
|
w = w,
|
|
h = 1,
|
|
selectable = true,
|
|
key = function (window, update, a, c, d, f)
|
|
if d then
|
|
local ot = textprop()
|
|
local le = require("lineedit")
|
|
p = le.clamp(ot, p)
|
|
if c == 63 then
|
|
neo.requireAccess("x.neo.pub.globals", "clipboard").setSetting("clipboard", ot)
|
|
elseif c == 64 then
|
|
local contents = neo.requireAccess("x.neo.pub.globals", "clipboard").getSetting("clipboard")
|
|
contents = contents:match("^[^\r\n]*")
|
|
textprop(contents)
|
|
update()
|
|
elseif a ~= 9 then
|
|
local lT, lC, lX = le.key(a ~= 0 and unicode.char(a), c, ot, p)
|
|
if lT or lC then
|
|
if lT then textprop(lT) end
|
|
p = lC or p
|
|
update()
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
clipboard = function (window, update, contents)
|
|
contents = contents:match("^[^\r\n]*")
|
|
textprop(contents)
|
|
update()
|
|
end,
|
|
line = function (window, x, y, lind, bg, fg, selected)
|
|
local fg1 = fg
|
|
if selected then
|
|
fg = bg
|
|
bg = fg1
|
|
end
|
|
local t, e, r = textprop(), require("lineedit")
|
|
p = e.clamp(t, p)
|
|
t, r = unicode.safeTextFormat(t, p)
|
|
window.span(x, y, "[" .. e.draw(w - 2, t, selected and r) .. "]", bg, fg)
|
|
end
|
|
}
|
|
end
|
|
neoux.startDialog = function (fmt, title, wait)
|
|
fmt = neoux.fmtText(unicode.safeTextFormat(fmt), 40)
|
|
neoux.create(40, #fmt, title, function (window, ev, a, b, c)
|
|
if ev == "line" then
|
|
window.span(1, a, fmt[a], 0xFFFFFF, 0)
|
|
end
|
|
if ev == "close" then
|
|
window.close()
|
|
wait = nil
|
|
end
|
|
end)
|
|
while wait do
|
|
event.pull()
|
|
end
|
|
end
|
|
return neoux
|
|
end
|
|
return newNeoux
|