diff --git a/dialog.lua b/dialog.lua index 023136ae8..fa35501d2 100644 --- a/dialog.lua +++ b/dialog.lua @@ -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 . 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 , +and from that go down and (depending on internat coordinates) +reach either or . + +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 diff --git a/event.lua b/event.lua new file mode 100644 index 000000000..e72f6ca23 --- /dev/null +++ b/event.lua @@ -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 diff --git a/inputevent.lua b/inputevent.lua new file mode 100644 index 000000000..4b55e9400 --- /dev/null +++ b/inputevent.lua @@ -0,0 +1,317 @@ +require "event" + +-- constants from +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 diff --git a/ui.lua b/ui.lua new file mode 100644 index 000000000..619bf1fbf --- /dev/null +++ b/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 diff --git a/widget.lua b/widget.lua index aa8ba1034..602688311 100644 --- a/widget.lua +++ b/widget.lua @@ -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 + diff --git a/wtest.lua b/wtest.lua new file mode 100644 index 000000000..13428d2e4 --- /dev/null +++ b/wtest.lua @@ -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()