luanti_scripts/fabric_touchscreen_designer.lua
2025-03-28 14:01:18 +03:00

762 lines
22 KiB
Lua

--[[
Touch screen designer.
Simple WYSIWYG editor for touch screens.
Can export configuration through digiline
or through touch screen multi line text field.
Author: SX
--]]
local GRID_SIZE = mem.GRID_SIZE and mem.GRID_SIZE or 0.4
local initialize_designer = function()
mem.ALLOW_RESIZE_W = {
addfield = 1,
addpwdfield = 1,
addbutton = 1,
addimage_button = 1,
addtextarea = 1,
addtextlist = 1,
adddropdown = 1,
addimage = 1,
}
mem.ALLOW_RESIZE_H = {
addimage_button = 1,
addtextarea = 1,
addtextlist = 1,
addimage = 1,
}
mem.SELECTABLE_COMPONENTS = {
button = 1,
buttonexit = 1,
imagebutton = 1,
imagebuttonexit = 1,
}
mem.WORKSPACE_CONTROL = {
command = 'addimage_button',
image = 'signs_road_red.png',
name = '_ctrl',
type = 'ctrl',
label = '.',
W = 0.4,
H = 0.4,
}
mem.CTRL_OFFSET = {
addfield = {
X = -0.3,
Y = -0.4,
},
addpwdfield = {
X = -0.3,
Y = -0.4,
},
}
mem.COMPONENT_DEFAULTS = {
button = {
command = 'addbutton',
name = '_button_',
label = 'untitled',
X = 0.8,
Y = 0.8,
W = 1.6,
H = 0.8,
},
buttonexit = {
variant = '_exit',
command = 'addbutton',
name = '_buttonexit_',
label = 'untitled',
X = 0.8,
Y = 0.8,
W = 1.6,
H = 0.8,
},
imagebutton = {
command = 'addimage_button',
name = "_imagebutton_",
label = "untitled",
image = 'wool_yellow.png',
X = 0.8,
Y = 0.8,
W = 1.6,
H = 0.8,
},
imagebuttonexit = {
variant = '_exit',
command = 'addimage_button',
name = "_imagebuttonexit_",
label = "untitled",
image = 'wool_yellow.png',
X = 0.8,
Y = 0.8,
W = 1.6,
H = 0.8,
},
field = {
command = 'addfield',
name = '_field_',
label = 'untitled',
default = 'value',
X = 0.7,
Y = 0.7,
W = 1.6,
H = 0.8,
},
pwdfield = {
command = 'addpwdfield',
name = '_pwdfield_',
label = 'untitled',
X = 0.7,
Y = 0.7,
W = 1.6,
H = 0.8,
},
label = {
command = 'addlabel',
label = 'untitled',
name = '_label_', -- Deleted on export, required for editor
X = 0.7,
Y = 0.7,
W = 1.2, -- Deleted on export, required for editor
H = 0.8, -- Deleted on export, required for editor
},
vertlabel = {
command = 'addvertlabel',
label = 'untitled',
name = '_vertlabel_', -- Deleted on export, required for editor
X = 0.8,
Y = 0.8,
W = 0.8, -- Deleted on export, required for editor
H = 1.2, -- Deleted on export, required for editor
},
textarea = {
command = "addtextarea",
label = "untitled",
name = "_textarea_",
default = 'value',
X = 0.8,
Y = 0.8,
W = 2.6,
H = 1.8,
},
textlist = {
command = "addtextlist",
label = "untitled",
name = "_textlist_",
listelements = {},
transparent = false,
selected_id = 1,
X = 0.8,
Y = 0.8,
W = 2.6,
H = 1.8,
},
dropdown = {
command = "adddropdown",
label = "untitled",
name = "_dropdown_",
choices = {},
selected_id = 1,
X = 0.8,
Y = 0.8,
W = 1.6,
H = 0.8,
},
}
mem.COMPONENT_TYPES = {}
for k,_ in pairs(mem.COMPONENT_DEFAULTS) do
table.insert(mem.COMPONENT_TYPES, k)
end
end
local ALLOW_RESIZE_W = mem.ALLOW_RESIZE_W
local ALLOW_RESIZE_H = mem.ALLOW_RESIZE_H
local SELECTABLE_COMPONENTS = mem.SELECTABLE_COMPONENTS
local WORKSPACE_CONTROL = mem.WORKSPACE_CONTROL
local CTRL_OFFSET = mem.CTRL_OFFSET
local COMPONENT_DEFAULTS = mem.COMPONENT_DEFAULTS
local COMPONENT_TYPES = mem.COMPONENT_TYPES
local draw_designer = function()
local ui = {
{
command = "addbutton_exit", X=9.6, Y=-0.31, W=0.7, H=0.7,
name = "quit", label = "X"
},
}
if mem.draw_code then
digiline_send("touch", mem.touch_clear_commands)
table.insert(ui, {
command = "addbutton", X=9.5, Y=0.49, W=0.8, H=0.8,
name = "close_code", label = "[[>"
})
table.insert(ui, {
command = "addtextarea", X=0.5, Y=0.5, W=9, H=8.5,
name = "result", label = "Generated Touch Screen configuration code", default = mem.code
})
else
table.insert(ui, {
command = "addbutton", X=mem.draw_toolbar and 9.45 or 9.65, Y=0.5, W=mem.draw_toolbar and 0.85 or 0.65, H=0.8,
name = "hide_toolbar", label = mem.draw_toolbar and "[[>" or "<]]"
})
if not mem.draw_component_editor and mem.draw_workspace then
digiline_send("touch", mem.workspace)
else
digiline_send("touch", { command = "clear" })
end
if mem.draw_toolbar then
-- Static toolbar components
digiline_send("touch", mem.toolbar)
-- Dynamic toolbar components
digiline_send("touch", {
{
command = "addbutton", X=8.45, Y=0.5, W=0.7, H=0.8,
name = "toggle_grid_size", label = string.format('%.1f', GRID_SIZE)
},{
command = "addbutton", X=8.95, Y=1.2, W=0.7, H=0.8,
name = 'toggle_size_move', label = (mem.mode_move and 'move' or 'size')
},{
command = "addbutton", X=8.45, Y=1.9, W=0.7, H=0.8,
name = "edit_component", label = "edit"
},{
command = "addlabel", X=8.6, Y=3.3,
label = mem.selected.ref and string.format('X:%.1f Y:%.1f\nW:%.1f H:%.1f', mem.selected.ref.X, mem.selected.ref.Y, mem.selected.ref.W, mem.selected.ref.H) or 'No selection'
},{
command = "addfield", X=8.75, Y=4.75, W=1.7, H=0.6,
name = 'component_name', label = 'Ref. key:',
default = mem.selected.display_name and mem.selected.display_name or ''
}
})
--[[
for _,v in pairs(mem.toolbar) do
table.insert(ui, v)
end
table.insert(ui, {
command = "addfield", X=8.6, Y=4.9, W=1.7, H=0.6,
name = 'component_name', label = 'Ref. key:',
default = mem.selected.display_name and mem.selected.display_name or ''
})
--]]
end
if mem.draw_component_editor then
local editor_ui= {
{
command = 'addimage_button',
image = 'wool_red.png', X=2.5, Y=4.5, W=2, H=0.8,
name = "edit_component", label = "Cancel"
},{
command = 'addimage_button',
image = 'wool_green.png', X=4.6, Y=4.5, W=2, H=0.8,
name = 'component_editor_save', label = 'Save'
},
}
local input_y = 3.8
if mem.selected.ref.label then
table.insert(editor_ui, {
command = "addfield", X=2.8, Y=input_y, W=4, H=0.8,
name = 'component_label', label = 'Component label:',
default = mem.selected.ref.label
})
input_y = input_y - 1
end
if mem.selected.ref.default then
table.insert(editor_ui, {
command = "addfield", X=2.8, Y=input_y, W=4, H=0.8,
name = 'component_default', label = 'Default value:',
default = mem.selected.ref.default
})
input_y = input_y - 1
end
if mem.selected.ref.image then
table.insert(editor_ui, {
command = "addfield", X=2.8, Y=input_y, W=4, H=0.8,
name = 'component_image', label = 'Image:',
default = mem.selected.ref.image
})
input_y = input_y - 1
end
if mem.selected.ref.choices or mem.selected.ref.listelements then
table.insert(editor_ui, {
command = "addfield", X=2.8, Y=input_y, W=2.6, H=0.8,
name = 'component_listelement', label = 'New entry:',
default = ''
})
table.insert(editor_ui, {
command = "addbutton", X=5.2, Y=input_y - 0.3, W=0.8, H=0.8,
name = "component_listelement_add", label = 'Add'
})
table.insert(editor_ui, {
command = "addbutton", X=6, Y=input_y - 0.3, W=0.8, H=0.8,
name = "component_listelement_del", label = 'Del'
})
input_y = input_y - 2.6
table.insert(editor_ui, {
command = "addtextlist", X = 2.5, Y = input_y, W = 4, H = 2,
label = "Item editor:",
name = "component_editor_items",
listelements = mem.selected.ref.choices or mem.selected.ref.listelements,
selected_id = mem.component_editor_items_idx or 1,
})
input_y = input_y - 1
end
digiline_send("touch", editor_ui)
end
end
digiline_send("touch", ui)
end
local reset = function()
mem.component_index = {}
for k,_ in pairs(COMPONENT_DEFAULTS) do
mem.component_index[k] = 0
end
mem.workspace = {
{ command = "clear" } -- 'clear' command is special case and not indexed in workspace
}
mem.workspace_search = {}
mem.workspace_control = {}
mem.draw_workspace = true
mem.draw_toolbar = true
mem.draw_code = false
mem.mode_move = true
mem.locked = false
mem.code = ''
mem.selected = {
display_name = nil,
ref = nil,
ctrl = nil
}
-- Add only static controls here, keeping cached in memory reduces table constructors
mem.toolbar = {
{
command = "addbutton", X=8.95, Y=0.5, W=0.7, H=0.8,
name = "up", label = "^"
},{
command = "addbutton", X=8.45, Y=1.2, W=0.7, H=0.8,
name = "left", label = "<"
},{
command = "addbutton", X=9.45, Y=1.2, W=0.7, H=0.8,
name = "right", label = ">"
},{
command = "addbutton", X=8.95, Y=1.9, W=0.7, H=0.8,
name = "down", label = "v"
},{
command = "addbutton", X=9.45, Y=1.9, W=0.7, H=0.8,
name = "delete", label = "del"
},{
command = "addbutton", X=8.45, Y=2.6, W=1.7, H=0.8,
name = "reset", label = "Reset"
},{
command = "addtextlist", X=8.45, Y=5.1, W=1.48, H=1.8,
name="component_type", selected_id=1, listelements = COMPONENT_TYPES
},{
command = "addbutton", X=8.45, Y=6.9, W=1.7, H=0.8,
name = "create", label = "Create"
},{
command = "addbutton", X=8.45, Y=7.6, W=1.7, H=0.8,
name = "code", label = "Generate"
},
}
mem.component_type_id = 1
end
local info = function(msg)
digiline_send('lcd', msg)
end
local validate_component_name = function(name)
return type(name) == 'string' and #name > 0
end
local indexof = function(items, value)
for k,v in pairs(items) do
if v == value then
return k
end
end
return nil
end
local textlist_indexof = function(value)
if #value >= 5 and string.sub(value, 1, 4) == 'CHG:' then
return tonumber(string.sub(value, 5, #value))
end
return nil
end
local update_control_position = function(control, component)
if control then
if CTRL_OFFSET[component.command] then
control.X = component.X + CTRL_OFFSET[component.command].X
control.Y = component.Y + CTRL_OFFSET[component.command].Y
else
control.X = component.X
control.Y = component.Y
end
end
end
local get_next_unused_name = function(component_type, name)
local i = 1
local result = name
local prefix = COMPONENT_DEFAULTS[component_type].name
while mem.workspace_search[prefix .. result] do
-- add numbers until free unused name is found
result = name .. '_' .. i
end
return result
end
local create_component_name = function(component_type, name)
if not COMPONENT_DEFAULTS[component_type] then
return false
end
if validate_component_name(name) then
-- attempt to use supplied component name
return get_next_unused_name(component_type, name)
else
-- generate new component name
mem.component_index[component_type] = mem.component_index[component_type] + 1
return get_next_unused_name(component_type, component_type .. mem.component_index[component_type])
end
end
local create_component = function(component_type, name)
local result = {}
for k,v in pairs(COMPONENT_DEFAULTS[component_type]) do
if type(v) == 'table' then
result[k] = {'value'} -- Avoid references to default component values
else
result[k] = v
end
end
result.type = component_type
if result.name then
result.name = result.name .. name
end
return result
end
local create_control_component = function(component)
local result = {}
for k,v in pairs(WORKSPACE_CONTROL) do
result[k] = v
end
result.name = result.name .. component.name
update_control_position(result, component)
return result
end
local move_component = function(selection, X, Y)
if selection.ref then
if X ~= nil and selection.ref.X + X >= 0 and selection.ref.X + selection.ref.W + X <= 10 then
selection.ref.X = selection.ref.X + X
end
if Y ~= nil and selection.ref.Y + Y >= 0 and selection.ref.Y + selection.ref.H + Y <= 8 then
selection.ref.Y = selection.ref.Y + Y
end
update_control_position(selection.ctrl, selection.ref)
end
end
local resize_component = function(selection, W, H)
if not selection.ref then
return false
end
local enable_W = ALLOW_RESIZE_W[selection.ref.command] and true or false
local enable_H = ALLOW_RESIZE_H[selection.ref.command] and true or false
if selection.ref.W and selection.ref.H then
if enable_W and W ~= nil and selection.ref.W >= GRID_SIZE and selection.ref.W <= 10 then
selection.ref.W = selection.ref.W + W
end
if enable_H and H ~= nil and selection.ref.H >= GRID_SIZE and selection.ref.H <= 10 then
selection.ref.H = selection.ref.H + H
end
else
return false
end
return true
end
local nice_name = function(name)
local name_idx = string.find(name, "_", 2, true)
return string.sub(name, name_idx + 1, #name)
end
local deselect = function()
mem.selected.ref = nil
mem.selected.ctrl = nil
mem.selected.display_name = nil
mem.draw_component_editor = false
mem.component_editor_items_idx = nil
end
local select_component = function(name)
if mem.workspace_search[name] then
mem.selected.ref = mem.workspace[mem.workspace_search[name]]
mem.selected.ctrl = nil
mem.selected.display_name = nice_name(name)
return true
elseif mem.workspace_control[name] then
local main_name_idx = string.find(name, "_", #WORKSPACE_CONTROL.name + 1, true)
local main_name = string.sub(name, main_name_idx, #name)
mem.selected.ctrl = mem.workspace[mem.workspace_control[name]]
mem.selected.ref = mem.workspace[mem.workspace_search[main_name]]
mem.selected.display_name = nice_name(main_name)
return true
end
deselect()
return false
end
local workspace_cleanup = function()
mem.workspace_search = {}
mem.workspace_control = {}
workspace = {{command='clear'}}
for index,component in pairs(mem.workspace) do
if component and component.name and component.type ~= 'ctrl' then
table.insert(workspace, component)
mem.workspace_search[component.name] = table.maxn(workspace)
if not SELECTABLE_COMPONENTS[component.type] then
-- Add control anchor button to allow passive component selection
local control = create_control_component(component)
table.insert(workspace, control)
mem.workspace_control[control.name] = table.maxn(workspace)
end
end
end
mem.workspace = workspace
end
local functions = {
up = function()
if mem.mode_move then
move_component(mem.selected, nil, -GRID_SIZE)
else
resize_component(mem.selected, nil, GRID_SIZE)
end
end,
down = function()
if mem.mode_move then
move_component(mem.selected, nil, GRID_SIZE)
else
resize_component(mem.selected, nil, -GRID_SIZE)
end
end,
left = function()
if mem.mode_move then
move_component(mem.selected, -GRID_SIZE, nil)
else
resize_component(mem.selected, -GRID_SIZE, nil)
end
end,
right = function()
if mem.mode_move then
move_component(mem.selected, GRID_SIZE, nil)
else
resize_component(mem.selected, GRID_SIZE, nil)
end
end,
toggle_size_move = function()
mem.mode_move = not mem.mode_move
end,
toggle_grid_size = function()
GRID_SIZE = GRID_SIZE < 1.6 and GRID_SIZE + 0.4 or 0.4
mem.GRID_SIZE = GRID_SIZE
end,
delete = function()
if mem.selected.ref then
local name = mem.selected.ref.name
local id = mem.workspace_search[name]
mem.workspace[id] = nil
workspace_cleanup()
deselect()
end
end,
component_type = function(data)
local component_type_idx = textlist_indexof(data.component_type)
mem.component_type = COMPONENT_TYPES[component_type_idx]
mem.toolbar[7].selected_id = component_type_idx
end,
create = function(data)
local name = create_component_name(mem.component_type, data.component_name)
if name then
local component = create_component(mem.component_type, name)
table.insert(mem.workspace, component)
mem.workspace_search[component.name] = table.maxn(mem.workspace)
if not SELECTABLE_COMPONENTS[mem.component_type] then
-- Add control anchor button to allow passive component selection
local control = create_control_component(component)
table.insert(mem.workspace, control)
mem.workspace_control[control.name] = table.maxn(mem.workspace)
end
select_component(name)
end
end,
hide_toolbar = function()
mem.draw_toolbar = not mem.draw_toolbar
end,
edit_component = function()
mem.component_editor_items_idx = 1
mem.draw_component_editor = not mem.draw_component_editor
end,
component_editor_items = function(data)
mem.component_editor_items_idx = textlist_indexof(data.component_editor_items)
end,
component_listelement_add = function(data)
if mem.selected.ref then
local items = mem.selected.ref.choices or mem.selected.ref.listelements
if mem.component_editor_items_idx then
table.insert(items, mem.component_editor_items_idx + 1, data.component_listelement)
mem.component_editor_items_idx = mem.component_editor_items_idx + 1
elseif items then
table.insert(items, data.component_listelement)
end
else
mem.draw_component_editor = false
deselect()
end
end,
component_listelement_del = function(data)
if mem.selected.ref then
local items = mem.selected.ref.choices or mem.selected.ref.listelements
if items and mem.component_editor_items_idx then
table.remove(items, mem.component_editor_items_idx)
mem.component_editor_items_idx = math.max(1, mem.component_editor_items_idx - 1)
end
else
mem.draw_component_editor = false
deselect()
end
end,
component_editor_save = function(data)
if mem.selected.ref then
if data.component_label then
mem.selected.ref.label = data.component_label
end
if data.component_default then
mem.selected.ref.default = data.component_default
end
if data.component_image then
mem.selected.ref.image = data.component_image
end
end
mem.draw_component_editor = false
deselect()
end,
lock = function()
-- toggle lock, maybe add timeout for lock in public version?
mem.locked = not mem.locked
end,
reset = function()
-- add confirmation message before destroying everything?
reset()
end,
code = function()
local result = {}
local vk = { [true] = '="', [false] = '=', }
local vi = { [true] = '"', [false] = '', }
local ve = { [true] = '",\n', [false] = ',\n', }
local labelcleanup = { name = 1, H = 1, W = 1 }
local indent = 2
table.insert(result, 'digiline_send("touch",{\n')
for i,component in ipairs(mem.workspace) do
if mem.workspace_control[component.name] ~= i then
local islabel = component.command == 'addlabel' or component.command == 'addvertlabel'
table.insert(result, (i == 1 and string.rep(' ', indent) or '') .. '{\n')
indent = indent + 2
local pre = string.rep(' ', indent)
local pos = {}
for k,v in pairs(component) do
if type(v) == 'table' then
digiline_send('dbg', 'serializing table ' .. k)
table.insert(result, string.rep(' ', indent) .. k .. '={\n')
indent = indent + 2
pre = string.rep(' ', indent)
for _,li in ipairs(v) do
local quote = type(li) == 'string'
table.insert(result, pre .. vi[quote] .. li .. ve[quote])
end
indent = indent - 2
table.insert(result, string.rep(' ', indent) .. '},')
elseif k ~= 'type' then
if k == 'X' or k == 'Y' or k == 'H' or k == 'W' then
table.insert(pos, k .. '=' .. v .. ',')
else
local quote = type(v) == 'string'
if islabel and labelcleanup[k] then
-- noop
elseif k == 'name' then
table.insert(result, pre .. k .. vk[quote] .. nice_name(v) .. ve[quote])
else
table.insert(result, pre .. k .. vk[quote] .. tostring(v) .. ve[quote])
end
end
end
end
if table.maxn(pos) > 0 then
table.insert(result, pre .. table.concat(pos, ' ') .. '\n')
end
indent = indent - 2
table.insert(result, string.rep(' ', indent) .. '},')
end
end
table.insert(result, '{X=7.18,label="Created with SX Digi labs Touch Designer",Y=8.11,command="addlabel"}\n})\n')
mem.code = table.concat(result)
mem.draw_code = true
end,
close_code = function()
mem.draw_code = false
end,
quit = function()
deselect()
end,
}
local selectable = function(key)
-- Hack to get around the fact that touch screen will always populate input fields into incoming data
-- FIXME somehow, maybe find match from table?
local m = string.sub(key, 1, 2)
return m == '_b' or m == '_c' or m == '_i'
end
local execute_event = function(data)
for key,_ in pairs(data) do
if selectable(key) then
select_component(key)
return true
elseif functions[key] then
functions[key](data)
return true
end
end
return false
end
if event.type == 'digiline' then
if not execute_event(event.msg) then
local str = ''
for k,v in pairs(event.msg) do
str = str .. k
end
info('Exec failed' .. str)
end
elseif event.type == 'program' then
mem.touch_clear_commands = { command = "clear" }
elseif event.type == 'interrupt' then
if not mem.workspace then
reset()
end
end
if COMPONENT_TYPES then
draw_designer()
else
initialize_designer()
interrupt(1)
end