mirror of https://github.com/koreader/koreader
new UI code
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
|
@ -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
|
@ -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…
Reference in New Issue