new UI code

pull/2/merge
HW 12 years ago
parent 8d74649642
commit 08278d6beb

@ -1,57 +1,237 @@
require "widget"
require "font"
require "commands"
InfoMessage = {
face = Font:getFace("infofont", 25)
--[[
Wrapper Widget that manages focus for a whole dialog
supports a 2D model of active elements
e.g.:
layout = {
{ textinput, textinput },
{ okbutton, cancelbutton }
}
this is a dialog with 2 rows. in the top row, there is the
single (!) widget <textinput>. when the focus is in this
group, left/right movement seems (!) to be doing nothing.
in the second row, there are two widgets and you can move
left/right. also, you can go up from both to reach <textinput>,
and from that go down and (depending on internat coordinates)
reach either <okbutton> or <cancelbutton>.
but notice that this does _not_ do the layout for you,
it rather defines an abstract layout.
]]
FocusManager = InputContainer:new{
selected = nil, -- defaults to x=1, y=1
layout = nil, -- mandatory
movement_allowed = { x = true, y = true }
}
function InfoMessage:show(text,refresh_mode)
debug("# InfoMessage ", text, refresh_mode)
local dialog = CenterContainer:new({
function FocusManager:init()
self.selected = { x = 1, y = 1 }
self.key_events = {
-- these will all generate the same event, just with different arguments
FocusUp = { {"Up"}, doc = "move focus up", event = "FocusMove", args = {0, -1} },
FocusDown = { {"Down"}, doc = "move focus down", event = "FocusMove", args = {0, 1} },
FocusLeft = { {"Left"}, doc = "move focus left", event = "FocusMove", args = {-1, 0} },
FocusRight = { {"Right"}, doc = "move focus right", event = "FocusMove", args = {1, 0} },
}
end
function FocusManager:onFocusMove(args)
local dx, dy = unpack(args)
if (dx ~= 0 and not self.movement_allowed.x)
or (dy ~= 0 and not self.movement_allowed.y) then
return true
end
local current_item = self.layout[self.selected.y][self.selected.x]
while true do
if self.selected.y + dy > #self.layout
or self.selected.y + dy < 1
or self.selected.x + dx > #self.layout[self.selected.y]
or self.selected.x + dx < 1 then
break -- abort when we run into borders
end
self.selected.x = self.selected.x + dx
self.selected.y = self.selected.y + dy
if self.layout[self.selected.y][self.selected.x] ~= current_item
and not self.layout[self.selected.y][self.selected.x].is_inactive then
-- we found a different object to focus
current_item:handleEvent(Event:new("Unfocus"))
self.layout[self.selected.y][self.selected.x]:handleEvent(Event:new("Focus"))
-- trigger a repaint (we need to be the registered widget!)
UIManager:setDirty(self)
break
end
end
return true
end
--[[
a button widget
]]
Button = WidgetContainer:new{
text = nil, -- mandatory
preselect = false
}
function Button:init()
-- set FrameContainer content
self[1] = FrameContainer:new{
margin = 0,
bordersize = 4,
background = 0,
HorizontalGroup:new{
Widget:new{ dimen = { w = 10, h = 0 } },
TextWidget:new{
text = self.text,
face = Font:getFace("cfont", 20)
},
Widget:new{ dimen = { w = 10, h = 0 } }
}
}
if self.preselect then
self[1].color = 15
else
self[1].color = 0
end
end
function Button:onFocus()
self[1].color = 15
return true
end
function Button:onUnfocus()
self[1].color = 0
return true
end
--[[
Widget that shows a message and OK/Cancel buttons
]]
ConfirmBox = FocusManager:new{
text = "no text",
}
function ConfirmBox:init()
self.key_events.Close = { {{"Home","Back"}}, doc = "cancel" }
self.key_events.Select = { {{"Enter","Press"}}, doc = "chose selected option" }
local ok_button = Button:new{
text = "OK"
}
local cancel_button = Button:new{
text = "Cancel",
preselect = true
}
self.layout = { { ok_button, cancel_button } }
self.selected.x = 2 -- Cancel is default
self[1] = CenterContainer:new{
dimen = { w = G_width, h = G_height },
FrameContainer:new{
margin = 2,
background = 0,
HorizontalGroup:new{
ImageWidget:new{
file = "resources/info-i.png"
},
Widget:new{
dimen = { w = 10, h = 0 }
},
VerticalGroup:new{
align = "left",
TextWidget:new{
text = self.text,
face = Font:getFace("cfont", 30)
},
HorizontalGroup:new{
ok_button,
Widget:new{ dimen = { w = 10, h = 0 } },
cancel_button
}
}
}
}
}
end
function ConfirmBox:onClose()
UIManager:close(self)
return true
end
function ConfirmBox:onSelect()
print("selected:", self.selected.x)
UIManager:close(self)
return true
end
--[[
Widget that displays an informational message
it vanishes on key press or after a given timeout
]]
InfoMessage = InputContainer:new{
face = Font:getFace("infofont", 25),
text = "",
timeout = nil,
key_events = {
AnyKeyPressed = { { Input.group.Any }, seqtext = "any key", doc = "close dialog" }
}
}
function InfoMessage:init()
-- we construct the actual content here because self.text is only available now
self[1] = CenterContainer:new{
dimen = { w = G_width, h = G_height },
FrameContainer:new({
FrameContainer:new{
margin = 2,
background = 0,
HorizontalGroup:new({
HorizontalGroup:new{
align = "center",
ImageWidget:new({
ImageWidget:new{
file = "resources/info-i.png"
}),
Widget:new({
},
Widget:new{
dimen = { w = 10, h = 0 }
}),
TextWidget:new({
text = text,
},
TextWidget:new{
text = self.text,
face = Font:getFace("cfont", 30)
})
})
})
})
dialog:paintTo(fb.bb, 0, 0)
dialog:free()
if refresh_mode ~= nil then
fb:refresh(refresh_mode)
end
}
}
}
}
end
function showInfoMsgWithDelay(text, msec, refresh_mode)
if not refresh_mode then refresh_mode = 0 end
Screen:saveCurrentBB()
function InfoMessage:onShow()
-- triggered by the UIManager after we got successfully shown (not yet painted)
if self.timeout then
UIManager:scheduleIn(self.timeout, function() UIManager:close(self) end)
end
return true
end
InfoMessage:show(text)
fb:refresh(refresh_mode)
-- util.usleep(msec*1000)
-- eat the first key release event
local ev = input.waitForEvent()
adjustKeyEvents(ev)
repeat
ok = pcall( function()
ev = input.waitForEvent(msec*1000)
adjustKeyEvents(ev)
end)
until not ok or ev.value == EVENT_VALUE_KEY_PRESS
Screen:restoreFromSavedBB()
fb:refresh(refresh_mode)
function InfoMessage:onAnyKeyPressed()
-- triggered by our defined key events
UIManager:close(self)
return true
end

@ -0,0 +1,20 @@
--[[
Events are messages that are passed through the widget tree
Events need a "name" attribute as minimal data.
In order to see how event propagation works and how to make
widgets event-aware see the implementation in WidgetContainer
below.
]]
Event = {}
function Event:new(name, ...)
local o = {
handler = "on"..name,
args = {...}
}
setmetatable(o, self)
self.__index = self
return o
end

@ -0,0 +1,317 @@
require "event"
-- constants from <linux/input.h>
EV_KEY = 1
-- event values
EVENT_VALUE_KEY_PRESS = 1
EVENT_VALUE_KEY_REPEAT = 2
EVENT_VALUE_KEY_RELEASE = 0
--[[
an interface for key presses
]]
Key = {}
function Key:new(key, modifiers)
local o = { key = key, modifiers = modifiers }
-- we're a hash map, too
o[key] = true
for mod, pressed in pairs(modifiers) do
if pressed then
o[mod] = true
end
end
setmetatable(o, self)
self.__index = self
return o
end
function Key:__tostring()
return table.concat(self:getSequence(), "-")
end
--[[
get a sequence that can be matched against later
use this to let the user press a sequence and then
store this as configuration data (configurable
shortcuts)
]]
function Key:getSequence()
local seq = {}
for mod, pressed in pairs(self.modifiers) do
if pressed then
table.insert(seq, mod)
end
end
table.insert(seq, self.key)
end
--[[
this will match a key against a sequence
the sequence should be a table of key names that
must be pressed together to match.
if an entry in this table is itself a table, at
least one key in this table must match.
E.g.:
Key:match({ "Alt", "K" }) -- match Alt-K
Key:match({ "Alt", { "K", "L" }}) -- match Alt-K _or_ Alt-L
]]
function Key:match(sequence)
local mod_keys = {} -- a hash table for checked modifiers
for _, key in ipairs(sequence) do
if type(key) == "table" then
local found = false
for _, variant in ipairs(key) do
if self[variant] then
found = true
break
end
end
if not found then
-- one of the needed keys is not pressed
return false
end
elseif not self[key] then
-- needed key not pressed
return false
elseif self.modifiers[key] ~= nil then
-- checked key is a modifier key
mod_keys[key] = true
end
end
for mod, pressed in pairs(self.modifiers) do
if pressed and not mod_keys[mod] then
-- additional modifier keys are pressed, don't match
return false
end
end
return true
end
--[[
an interface to get input events
]]
Input = {
event_map = {
[2] = "1", [3] = "2", [4] = "3", [5] = "4", [6] = "5", [7] = "6", [8] = "7", [9] = "8", [10] = "9", [11] = "0",
[16] = "Q", [17] = "W", [18] = "E", [19] = "R", [20] = "T", [21] = "Y", [22] = "U", [23] = "I", [24] = "O", [25] = "P",
[30] = "A", [31] = "S", [32] = "D", [33] = "F", [34] = "G", [35] = "H", [36] = "J", [37] = "K", [38] = "L", [14] = "Del",
[44] = "Z", [45] = "X", [46] = "C", [47] = "V", [48] = "B", [49] = "N", [50] = "M", [52] = ".", [53] = "/", -- only KDX
[28] = "Enter",
[42] = "Shift",
[56] = "Alt",
[57] = " ",
[90] = "AA", -- KDX
[91] = "Back", -- KDX
[92] = "Press", -- KDX
[94] = "Sym", -- KDX
[98] = "Home", -- KDX
[102] = "Home", -- K[3]
[104] = "LPgBack", -- K[3] only
[103] = "Up", -- K[3]
[105] = "Left",
[106] = "Right",
[108] = "Down", -- K[3]
[109] = "RPgBack",
[114] = "VMinus",
[115] = "VPlus",
[122] = "Up", -- KDX
[123] = "Down", -- KDX
[124] = "RPgFwd", -- KDX
[126] = "Sym", -- K[3]
[139] = "Menu",
[158] = "Back", -- K[3]
[190] = "AA", -- K[3]
[191] = "RPgFwd", -- K[3]
[193] = "LPgFwd", -- K[3] only
[194] = "Press", -- K[3]
},
sdl_event_map = {
[10] = "1", [11] = "2", [12] = "3", [13] = "4", [14] = "5", [15] = "6", [16] = "7", [17] = "8", [18] = "9", [19] = "0",
[24] = "Q", [25] = "W", [26] = "E", [27] = "R", [28] = "T", [29] = "Y", [30] = "U", [31] = "I", [32] = "O", [33] = "P",
[38] = "A", [39] = "S", [40] = "D", [41] = "F", [42] = "G", [43] = "H", [44] = "J", [45] = "K", [46] = "L",
[52] = "Z", [53] = "X", [54] = "C", [55] = "V", [56] = "B", [57] = "N", [58] = "M",
[22] = "Back", -- Backspace
[36] = "Enter", -- Enter
[50] = "Shift", -- left shift
[60] = ".",
[61] = "/",
[62] = "Sym", -- right shift key
[64] = "Alt", -- left alt
[65] = " ", -- Spacebar
[67] = "Menu", -- F[1]
[72] = "LPgBack", -- F[6]
[73] = "LPgFwd", -- F[7]
[95] = "VPlus", -- F[11]
[96] = "VMinus", -- F[12]
[105] = "AA", -- right alt key
[110] = "Home", -- Home
[111] = "Up", -- arrow up
[112] = "RPgBack", -- normal PageUp
[113] = "Left", -- arrow left
[114] = "Right", -- arrow right
[115] = "Press", -- End (above arrows)
[116] = "Down", -- arrow down
[117] = "RPgFwd", -- normal PageDown
[119] = "Del", -- Delete
},
rotation = 0,
rotation_map = {
[0] = {},
[1] = { Up = "Right", Right = "Down", Down = "Left", Left = "Up" },
[2] = { Up = "Down", Right = "Left", Down = "Up", Left = "Right" },
[3] = { Up = "Left", Right = "Up", Down = "Right", Left = "Down" }
},
modifiers = {
Alt = false,
Shift = false
},
-- these groups are just helpers:
group = {
Cursor = { "Up", "Down", "Left", "Right" },
Alphabet = {
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
},
AlphaNumeric = {
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
},
Text = {
" ", ".", "/",
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
},
Any = {
" ", ".", "/",
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"Up", "Down", "Left", "Right", "Press",
"Back", "Enter", "Sym", "AA", "Menu", "Home", "Del",
"LPgBack", "RPgBack", "LPgFwd", "RPgFwd"
}
}
}
function Input:init()
if util.isEmulated()==1 then
-- dummy call that will initialize SDL input handling
input.open("")
-- SDL key codes
self.event_map = self.sdl_event_map
else
input.open("slider")
input.open("/dev/input/event0")
input.open("/dev/input/event1")
-- check if we are running on Kindle 3 (additional volume input)
local f=lfs.attributes("/dev/input/event2")
if f then
print("Auto-detected Kindle 3")
input.open("/dev/input/event2")
end
end
end
function Input:waitEvent(timeout)
-- wrapper for input.waitForEvents that will retry for some cases
local ok, ev
while true do
ok, ev = pcall(input.waitForEvent, timeout)
if ok then
break
end
if ev == "Waiting for input failed: timeout\n" then
-- don't report an error on timeout
ev = nil
break
end
debug("got error waiting for events:", ev)
if ev ~= "Waiting for input failed: 4\n" then
-- we abort if the error is not EINTR
break
end
end
if ok and ev then
if ev.type == EV_KEY then
local keycode = self.event_map[ev.code]
if not keycode then
-- do not handle keypress for keys we don't know
return
end
-- take device rotation into account
if self.rotation_map[self.rotation][keycode] then
keycode = self.rotation_map[self.rotation][keycode]
end
-- handle modifier keys
if self.modifiers[keycode] ~= nil then
if ev.value == EVENT_VALUE_KEY_PRESS then
self.modifiers[keycode] = true
elseif ev.value == EVENT_VALUE_KEY_RELEASE then
self.modifiers[keycode] = false
end
return
end
local key = Key:new(keycode, self.modifiers)
if ev.value == EVENT_VALUE_KEY_PRESS then
return Event:new("KeyPress", key)
elseif ev.value == EVENT_VALUE_KEY_RELEASE then
return Event:new("KeyRelease", key)
end
else
-- some other kind of event that we do not know yet
return Event:new("GenericInput", ev)
end
elseif not ok and ev then
return Event:new("InputError", ev)
end
end
--[[
helper function for formatting sequence definitions for output
]]
function Input:sequenceToString(sequence)
local modifiers = {}
local keystring = {"",""} -- first entries reserved for modifier specification
for _, key in ipairs(sequence) do
if type(key) == "table" then
local alternatives = {}
for _, alternative in ipairs(key) do
table.insert(alternatives, alternative)
end
table.insert(keystring, "{")
table.insert(keystring, table.concat(alternatives, "|"))
table.insert(keystring, "}")
elseif self.modifiers[key] ~= nil then
table.insert(modifiers, key)
else
table.insert(keystring, key)
end
end
if #modifiers then
keystring[0] = table.concat(modifiers, "-")
keystring[1] = "-"
end
return table.concat(keystring)
end

178
ui.lua

@ -0,0 +1,178 @@
require "inputevent"
require "widget"
require "screen"
require "dialog"
require "settings" -- for debug(), TODO: put debug() somewhere else
-- we also initialize the framebuffer
fb = einkfb.open("/dev/fb0")
G_width, G_height = fb:getSize()
-- and the input handling
Input:init()
-- there is only one instance of this
UIManager = {
-- change this to set refresh type for next refresh
refresh_type = 1, -- defaults to 1 initially but will be set to 0 after each refresh
_running = true,
_window_stack = {},
_execution_stack = {},
_dirty = {}
}
-- register & show a widget
function UIManager:show(widget, x, y)
-- put widget on top of stack
table.insert(self._window_stack, {x = x or 0, y = y or 0, widget = widget})
-- and schedule it to be painted
self:setDirty(widget)
-- tell the widget that it is shown now
widget:handleEvent(Event:new("Show"))
end
-- unregister a widget
function UIManager:close(widget)
local dirty = false
for i = #self._window_stack, 1, -1 do
if self._window_stack[i].widget == widget then
table.remove(self._window_stack, i)
dirty = true
break
end
end
if dirty then
-- schedule remaining widgets to be painted
for i = 1, #self._window_stack do
self:setDirty(self._window_stack[i].widget)
end
end
end
-- schedule an execution task
function UIManager:schedule(time, action)
table.insert(self._execution_stack, { time = time, action = action })
end
-- schedule task in a certain amount of seconds (fractions allowed) from now
function UIManager:scheduleIn(seconds, action)
local when = { util.gettime() }
local s = math.floor(seconds)
local usecs = (seconds - s) * 1000000
when[1] = when[1] + s
when[2] = when[2] + usecs
if when[2] > 1000000 then
when[1] = when[1] + 1
when[2] = when[2] - 1000000
end
self:schedule(when, action)
end
-- register a widget to be repainted
function UIManager:setDirty(widget)
self._dirty[widget] = true
end
-- signal to quit
function UIManager:quit()
self._running = false
end
-- transmit an event to registered widgets
function UIManager:sendEvent(event)
-- top level widget has first access to the event
local consumed = self._window_stack[#self._window_stack].widget:handleEvent(event)
-- if the event is not consumed, always-active widgets can access it
for _, widget in ipairs(self._window_stack) do
if consumed then
break
end
if widget.widget.is_always_active then
consumed = widget.widget:handleEvent(event)
end
end
end
-- this is the main loop of the UI controller
-- it is intended to manage input events and delegate
-- them to dialogs
function UIManager:run()
self._running = true
while self._running do
local now = { util.gettime() }
-- check if we have timed events in our queue and search next one
local wait_until = nil
for i = #self._execution_stack, 1, -1 do
local task = self._execution_stack[i]
if not task.time
or task.time[1] < now[1]
or task.time[1] == now[1] and task.time[2] < now[2] then
-- task is pending to be executed right now. do it.
task.action()
-- and remove from table
table.remove(self._execution_stack, i)
elseif not wait_until
or wait_until[1] > task.time[1]
or wait_until[1] == task.time[1] and wait_until[2] > task.time[2] then
-- task is to be run in the future _and_ is scheduled
-- earlier than the tasks we looked at already
-- so adjust to the currently examined task instead.
wait_until = task.time
end
end
--debug("---------------------------------------------------")
--debug("exec stack", self._execution_stack)
--debug("window stack", self._window_stack)
--debug("dirty stack", self._dirty)
--debug("---------------------------------------------------")
-- stop when we have no window to show (bug)
if #self._window_stack == 0 then
error("no dialog left to show, would loop endlessly")
end
-- repaint dirty widgets
local dirty = false
for _, widget in ipairs(self._window_stack) do
if self._dirty[widget.widget] then
widget.widget:paintTo(fb.bb, widget.x, widget.y)
-- and remove from list after painting
self._dirty[widget.widget] = nil
-- trigger repaint
dirty = true
end
end
if dirty then
-- refresh FB
fb:refresh(self.refresh_type) -- TODO: refresh explicitly only repainted area
-- reset refresh_type
self.refresh_type = 0
end
-- wait for next event
-- note that we will skip that if in the meantime we have tasks that are ready to run
local input_event = nil
if not wait_until then
-- no pending task, wait endlessly
input_event = Input:waitEvent()
elseif wait_until[1] > now[1]
or wait_until[1] == now[1] and wait_until[2] > now[2] then
-- wait until next task is pending
input_event = Input:waitEvent((wait_until[1] - now[1]) * 1000000 + (wait_until[2] - now[2]))
end
-- delegate input_event to handler
if input_event then
self:sendEvent(input_event)
end
end
end

@ -1,22 +1,28 @@
require "rendertext"
require "graphics"
require "image"
require "event"
require "inputevent"
require "font"
--[[
This is a (useless) generic Widget interface
This is a generic Widget interface
widgets can be queried about their size and can be paint.
that's it for now. Probably we need something more elaborate
later.
if the table that was given to us as parameter has an "init"
method, it will be called. use this to set _instance_ variables
rather than class variables.
]]
Widget = {
dimen = { w = 0, h = 0},
}
Widget = {}
function Widget:new(o)
o = o or {}
local o = o or {}
setmetatable(o, self)
self.__index = self
if o.init then o:init() end
return o
end
@ -27,7 +33,17 @@ end
function Widget:paintTo(bb, x, y)
end
function Widget:free()
--[[
Widgets have a rudimentary event handler/dispatcher that
will call a method "onEventName" for an event with name
"EventName"
These methods
]]
function Widget:handleEvent(event)
if self[event.handler] then
return self[event.handler](self, unpack(event.args))
end
end
--[[
@ -35,12 +51,56 @@ WidgetContainer is a container for another Widget
]]
WidgetContainer = Widget:new()
function WidgetContainer:getSize()
if self.dimen then
-- fixed size
return self.dimen
elseif self[1] then
-- return size of first child widget
return self[1]:getSize()
else
return { w = 0, h = 0 }
end
end
function WidgetContainer:paintTo(bb, x, y)
-- default to pass request to first child widget
if self[1] then
return self[1]:paintTo(bb, x, y)
end
end
function WidgetContainer:propagateEvent(event)
-- propagate to children
for _, widget in ipairs(self) do
if widget:handleEvent(event) then
-- stop propagating when an event handler returns true
return true
end
end
return false
end
--[[
Containers will pass events to children or react on them themselves
]]
function WidgetContainer:handleEvent(event)
-- call our own standard event handler
if not self:propagateEvent(event) then
return Widget.handleEvent(self, event)
else
return true
end
end
function WidgetContainer:free()
for _, widget in ipairs(self) do
widget:free()
if widget.free then widget:free() end
end
end
--[[
CenterContainer centers its content (1 widget) within its own dimensions
]]
@ -49,8 +109,8 @@ CenterContainer = WidgetContainer:new()
function CenterContainer:paintTo(bb, x, y)
local contentSize = self[1]:getSize()
if contentSize.w > self.dimen.w or contentSize.h > self.dimen.h then
-- throw error?
return
-- throw error? paint to scrap buffer and blit partially?
-- for now, we ignore this
end
self[1]:paintTo(bb,
x + (self.dimen.w - contentSize.w)/2,
@ -60,16 +120,16 @@ end
--[[
A FrameContainer is some graphics content (1 widget) that is surrounded by a frame
]]
FrameContainer = WidgetContainer:new({
FrameContainer = WidgetContainer:new{
background = nil,
color = 15,
margin = 0,
bordersize = 2,
padding = 5,
})
}
function FrameContainer:getSize()
local content_size = self[1]:getSize()
local content_size = WidgetContainer.getSize(self)
return {
w = content_size.w + ( self.margin + self.bordersize + self.padding ) * 2,
h = content_size.h + ( self.margin + self.bordersize + self.padding ) * 2
@ -78,7 +138,7 @@ end
function FrameContainer:paintTo(bb, x, y)
local my_size = self:getSize()
if self.background then
bb:paintRect(x, y, my_size.w, my_size.h, self.background)
end
@ -87,22 +147,24 @@ function FrameContainer:paintTo(bb, x, y)
my_size.w - self.margin * 2, my_size.h - self.margin * 2,
self.bordersize, self.color)
end
self[1]:paintTo(bb,
x + self.margin + self.bordersize + self.padding,
y + self.margin + self.bordersize + self.padding)
if self[1] then
self[1]:paintTo(bb,
x + self.margin + self.bordersize + self.padding,
y + self.margin + self.bordersize + self.padding)
end
end
--[[
A TextWidget puts a string on a single line
]]
TextWidget = Widget:new({
TextWidget = Widget:new{
text = nil,
face = nil,
color = 15,
_bb = nil,
_length = 0,
_maxlength = 1200,
})
}
function TextWidget:_render()
local h = self.face.size * 1.5
@ -134,10 +196,10 @@ end
--[[
ImageWidget shows an image from a file
]]
ImageWidget = Widget:new({
ImageWidget = Widget:new{
file = nil,
_bb = nil
})
}
function ImageWidget:_render()
local itype = string.lower(string.match(self.file, ".+%.([^.]+)") or "")
@ -170,10 +232,10 @@ end
--[[
A Layout widget that puts objects besides each others
]]
HorizontalGroup = WidgetContainer:new({
HorizontalGroup = WidgetContainer:new{
align = "center",
_size = nil,
})
}
function HorizontalGroup:getSize()
if not self._size then
@ -217,11 +279,11 @@ end
--[[
A Layout widget that puts objects under each other
]]
VerticalGroup = WidgetContainer:new({
VerticalGroup = WidgetContainer:new{
align = "center",
_size = nil,
_offsets = {}
})
}
function VerticalGroup:getSize()
if not self._size then
@ -261,3 +323,61 @@ function VerticalGroup:free()
self._offsets = {}
WidgetContainer.free(self)
end
--[[
an UnderlineContainer is a WidgetContainer that is able to paint
a line under its child node
]]
UnderlineContainer = WidgetContainer:new{
linesize = 2,
padding = 1,
color = 0,
}
function UnderlineContainer:getSize()
local contentSize = self[1]:getSize()
return { w = contentSize.w, h = contentSize.h + self.linesize + self.padding }
end
function UnderlineContainer:paintTo(bb, x, y)
local contentSize = self[1]:getSize()
self[1]:paintTo(bb, x, y)
bb:paintRect(x, y + contentSize.h + self.padding,
contentSize.w, self.linesize, self.color)
end
--[[
an InputContainer is an WidgetContainer that handles input events
an example for a key_event is this:
PanBy20 = { { "Shift", Input.group.Cursor }, seqtext = "Shift+Cursor", doc = "pan by 20px", event = "Pan", args = 20, is_inactive = true },
PanNormal = { { Input.group.Cursor }, seqtext = "Cursor", doc = "pan by 10 px", event = "Pan", args = 10 },
Quit = { {"Home"} },
it is suggested to reference configurable sequences from another table
and store that table as configuration setting
]]
InputContainer = WidgetContainer:new{
key_events = {}
}
-- the following handler handles keypresses and checks
-- if they lead to a command.
-- if this is the case, we retransmit another event within
-- ourselves
function InputContainer:onKeyPress(key)
for name, seq in pairs(self.key_events) do
if not seq.is_inactive then
for _, oneseq in ipairs(seq) do
if key:match(oneseq) then
local eventname = seq.event or name
return self:handleEvent(Event:new(eventname, seq.args, key))
end
end
end
end
end

@ -0,0 +1,77 @@
require "ui"
-- we create a widget that paints a background:
Background = InputContainer:new{
is_always_active = true, -- receive events when other dialogs are active
key_events = {
OpenDialog = { { "Press" } },
OpenConfirmBox = { { "Del" } },
QuitApplication = { { {"Home","Back"} } }
},
-- contains a gray rectangular desktop
FrameContainer:new{
background = 3,
bordersize = 0,
dimen = { w = G_width, h = G_height }
}
}
function Background:onOpenDialog()
UIManager:show(InfoMessage:new{
text = "Example message.",
timeout = 10
})
end
function Background:onOpenConfirmBox()
UIManager:show(ConfirmBox:new{
text = "Please confirm delete"
})
end
function Background:onInputError()
UIManager:quit()
end
function Background:onQuitApplication()
UIManager:quit()
end
-- example widget: a clock
Clock = FrameContainer:new{
background = 0,
bordersize = 1,
margin = 0,
padding = 1
}
function Clock:schedFunc()
self[1]:free()
self[1] = self:getTextWidget()
UIManager:setDirty(self)
-- reschedule
-- TODO: wait until next real minute shift
UIManager:scheduleIn(60, function() self:schedFunc() end)
end
function Clock:onShow()
self[1] = self:getTextWidget()
self:schedFunc()
end
function Clock:getTextWidget()
return CenterContainer:new{
dimen = { w = 300, h = 25 },
TextWidget:new{
text = os.date("%H:%M"),
face = Font:getFace("cfont", 12)
}
}
end
UIManager:show(Background:new())
UIManager:show(Clock:new())
UIManager:run()
Loading…
Cancel
Save