require "ui/screen" require "ui/rendertext" require "ui/graphics" require "ui/image" require "ui/event" require "ui/inputevent" require "ui/gesturedetector" require "ui/font" --[[ The EventListener is an interface that handles events EventListeners have a rudimentary event handler/dispatcher that will call a method "onEventName" for an event with name "EventName" ]] EventListener = {} function EventListener:new(o) local o = o or {} setmetatable(o, self) self.__index = self if o.init then o:init() end return o end function EventListener:handleEvent(event) if self[event.handler] then return self[event.handler](self, unpack(event.args)) end end --[[ 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 = EventListener:new() function Widget:new(o) local o = o or {} setmetatable(o, self) self.__index = self -- Both o._init and o.init are called on object create. But o._init is used -- for base widget initialization (basic component used to build other -- widgets). While o.init is for higher level widgets, for example Menu -- Widget if o._init then o:_init() end if o.init then o:init() end return o end function Widget:getSize() return self.dimen end function Widget:paintTo(bb, x, y) end --[[ 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 Geom:new{ w = 0, h = 0 } end end --[[ delete all child widgets ]]-- function WidgetContainer:clear() while table.remove(self) do 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) if not self:propagateEvent(event) then -- call our own standard event handler return Widget.handleEvent(self, event) else return true end end function WidgetContainer:free() for _, widget in ipairs(self) do if widget.free then widget:free() end end end --[[ BottomContainer contains its content (1 widget) at the bottom of its own dimensions ]] BottomContainer = WidgetContainer:new() function BottomContainer:paintTo(bb, x, y) local contentSize = self[1]:getSize() if contentSize.w > self.dimen.w or contentSize.h > self.dimen.h then -- 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, y + (self.dimen.h - contentSize.h)) end --[[ CenterContainer centers its content (1 widget) within its own dimensions ]] 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? paint to scrap buffer and blit partially? -- for now, we ignore this end local x_pos = x local y_pos = y if self.ignore ~= "height" then y_pos = y + (self.dimen.h - contentSize.h)/2 end if self.ignore ~= "width" then x_pos = x + (self.dimen.w - contentSize.w)/2 end self[1]:paintTo(bb, x_pos, y_pos) end --[[ LeftContainer aligns its content (1 widget) at the left of its own dimensions ]] LeftContainer = WidgetContainer:new() function LeftContainer:paintTo(bb, x, y) local contentSize = self[1]:getSize() if contentSize.w > self.dimen.w or contentSize.h > self.dimen.h then -- throw error? paint to scrap buffer and blit partially? -- for now, we ignore this end self[1]:paintTo(bb, x , y + (self.dimen.h - contentSize.h)/2) end --[[ RightContainer aligns its content (1 widget) at the right of its own dimensions ]] RightContainer = WidgetContainer:new() function RightContainer:paintTo(bb, x, y) local contentSize = self[1]:getSize() if contentSize.w > self.dimen.w or contentSize.h > self.dimen.h then -- 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), y + (self.dimen.h - contentSize.h)/2) end --[[ A FrameContainer is some graphics content (1 widget) that is surrounded by a frame ]] FrameContainer = WidgetContainer:new{ background = nil, color = 15, margin = 0, radius = 0, bordersize = 2, padding = 5, } function FrameContainer:getSize() local content_size = self[1]:getSize() return Geom:new{ w = content_size.w + ( self.margin + self.bordersize + self.padding ) * 2, h = content_size.h + ( self.margin + self.bordersize + self.padding ) * 2 } end function FrameContainer:paintTo(bb, x, y) local my_size = self:getSize() if self.background then bb:paintRoundedRect(x, y, my_size.w, my_size.h, self.background, self.radius) end if self.bordersize > 0 then bb:paintBorder(x + self.margin, y + self.margin, my_size.w - self.margin * 2, my_size.h - self.margin * 2, self.bordersize, self.color, self.radius) end 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{ text = nil, face = nil, color = 15, _bb = nil, _length = 0, _height = 0, _maxlength = 1200, } --function TextWidget:_render() --local h = self.face.size * 1.3 --self._bb = Blitbuffer.new(self._maxlength, h) --self._length = renderUtf8Text(self._bb, 0, h*0.8, self.face, self.text, self.color) --end function TextWidget:getSize() --if not self._bb then --self:_render() --end --return { w = self._length, h = self._bb:getHeight() } local tsize = sizeUtf8Text(0, Screen:getWidth(), self.face, self.text, true) if not tsize then return Geom:new{} end self._length = tsize.x self._height = self.face.size * 1.5 return Geom:new{ w = self._length, h = self._height, } end function TextWidget:paintTo(bb, x, y) --if not self._bb then --self:_render() --end --bb:blitFrom(self._bb, x, y, 0, 0, self._length, self._bb:getHeight()) --@TODO Don't use kerning for monospaced fonts. (houqp) renderUtf8Text(bb, x, y+self._height*0.7, self.face, self.text, true) end function TextWidget:free() if self._bb then self._bb:free() self._bb = nil end end --[[ A TextWidget that handles long text wrapping ]] TextBoxWidget = Widget:new{ text = nil, face = nil, color = 15, width = 400, -- in pixels line_height = 0.3, -- in em v_list = nil, _bb = nil, _length = 0, } function TextBoxWidget:_wrapGreedyAlg(h_list) local cur_line_width = 0 local space_w = sizeUtf8Text(0, Screen:getWidth(), self.face, " ", true).x local cur_line = {} local v_list = {} for k,w in ipairs(h_list) do cur_line_width = cur_line_width + w.width if cur_line_width <= self.width then cur_line_width = cur_line_width + space_w table.insert(cur_line, w) else -- wrap to next line table.insert(v_list, cur_line) cur_line = {} cur_line_width = w.width + space_w table.insert(cur_line, w) end end -- handle last line table.insert(v_list, cur_line) return v_list end function TextBoxWidget:_getVerticalList(alg) -- build horizontal list h_list = {} for w in self.text:gmatch("%S+") do word_box = {} word_box.word = w word_box.width = sizeUtf8Text(0, Screen:getWidth(), self.face, w, true).x table.insert(h_list, word_box) end -- @TODO check alg here 25.04 2012 (houqp) -- @TODO replace greedy algorithm with K&P algorithm 25.04 2012 (houqp) return self:_wrapGreedyAlg(h_list) end function TextBoxWidget:_render() self.v_list = self:_getVerticalList() local v_list = self.v_list local font_height = self.face.size local line_height_px = self.line_height * font_height local space_w = sizeUtf8Text(0, Screen:getWidth(), self.face, " ", true).x local h = (font_height + line_height_px) * #v_list - line_height_px self._bb = Blitbuffer.new(self.width, h) local y = font_height local pen_x = 0 for _,l in ipairs(v_list) do pen_x = 0 for _,w in ipairs(l) do --@TODO Don't use kerning for monospaced fonts. (houqp) -- refert to cb25029dddc42693cc7aaefbe47e9bd3b7e1a750 in master tree renderUtf8Text(self._bb, pen_x, y*0.8, self.face, w.word, true) pen_x = pen_x + w.width + space_w end y = y + line_height_px + font_height end -- if text is shorter than one line, shrink to text's width if #v_list == 1 then self.width = pen_x end end function TextBoxWidget:getSize() if not self._bb then self:_render() end return { w = self.width, h = self._bb:getHeight() } end function TextBoxWidget:paintTo(bb, x, y) if not self._bb then self:_render() end bb:blitFrom(self._bb, x, y, 0, 0, self.width, self._bb:getHeight()) end function TextBoxWidget:free() if self._bb then self._bb:free() self._bb = nil end end --[[ ImageWidget shows an image from a file ]] ImageWidget = Widget:new{ invert = nil, file = nil, _bb = nil } function ImageWidget:_render() local itype = string.lower(string.match(self.file, ".+%.([^.]+)") or "") if itype == "jpeg" or itype == "jpg" then self._bb = Image.fromJPEG(self.file) elseif itype == "png" then self._bb = Image.fromPNG(self.file) end end function ImageWidget:getSize() if not self._bb then self:_render() end return Geom:new{ w = self._bb:getWidth(), h = self._bb:getHeight() } end function ImageWidget:paintTo(bb, x, y) local size = self:getSize() bb:blitFrom(self._bb, x, y, 0, 0, size.w, size.h) if self.invert then bb:invertRect(x, y, size.w, size.h) end end function ImageWidget:free() if self._bb then self._bb:free() self._bb = nil end end --[[ ProgressWidget shows a progress bar ]] ProgressWidget = Widget:new{ width = nil, height = nil, margin_h = 3, margin_v = 1, radius = 2, bordersize = 1, bordercolor = 15, bgcolor = 0, rectcolor = 10, percentage = nil, } function ProgressWidget:getSize() return { w = self.width, h = self.height } end function ProgressWidget:paintTo(bb, x, y) local my_size = self:getSize() bb:paintRoundedRect(x, y, my_size.w, my_size.h, self.bgcolor, self.radius) bb:paintBorder(x, y, my_size.w, my_size.h, self.bordersize, self.bordercolor, self.radius) bb:paintRect(x+self.margin_h, y+self.margin_v+self.bordersize, (my_size.w-2*self.margin_h)*self.percentage, (my_size.h-2*(self.margin_v+self.bordersize)), self.rectcolor) end function ProgressWidget:setPercentage(percentage) self.percentage = percentage end --[[ A Layout widget that puts objects besides each others ]] HorizontalGroup = WidgetContainer:new{ align = "center", _size = nil, } function HorizontalGroup:getSize() if not self._size then self._size = { w = 0, h = 0 } self._offsets = { } for i, widget in ipairs(self) do local w_size = widget:getSize() self._offsets[i] = { x = self._size.w, y = w_size.h } self._size.w = self._size.w + w_size.w if w_size.h > self._size.h then self._size.h = w_size.h end end end return self._size end function HorizontalGroup:paintTo(bb, x, y) local size = self:getSize() for i, widget in ipairs(self) do if self.align == "center" then widget:paintTo(bb, x + self._offsets[i].x, y + (size.h - self._offsets[i].y) / 2) elseif self.align == "top" then widget:paintTo(bb, x + self._offsets[i].x, y) elseif self.align == "bottom" then widget:paintTo(bb, x + self._offsets[i].x, y + size.h - self._offsets[i].y) end end end function HorizontalGroup:clear() self:free() WidgetContainer.clear(self) end function HorizontalGroup:resetLayout() self._size = nil self._offsets = {} end function HorizontalGroup:free() self:resetLayout() WidgetContainer.free(self) end --[[ Dummy Widget that reserves horizontal space ]] HorizontalSpan = Widget:new{ width = 0, } function HorizontalSpan:getSize() return {w = self.width, h = 0} end --[[ A Layout widget that puts objects under each other ]] VerticalGroup = WidgetContainer:new{ align = "center", _size = nil, _offsets = {} } function VerticalGroup:getSize() if not self._size then self._size = { w = 0, h = 0 } self._offsets = { } for i, widget in ipairs(self) do local w_size = widget:getSize() self._offsets[i] = { x = w_size.w, y = self._size.h, } self._size.h = self._size.h + w_size.h if w_size.w > self._size.w then self._size.w = w_size.w end end end return self._size end function VerticalGroup:paintTo(bb, x, y) local size = self:getSize() for i, widget in ipairs(self) do if self.align == "center" then widget:paintTo(bb, x + (size.w - self._offsets[i].x) / 2, y + self._offsets[i].y) elseif self.align == "left" then widget:paintTo(bb, x, y + self._offsets[i].y) elseif self.align == "right" then widget:paintTo(bb, x + size.w - self._offsets[i].x, y + self._offsets[i].y) end end end function VerticalGroup:clear() self:free() WidgetContainer.clear(self) end function VerticalGroup:resetLayout() self._size = nil self._offsets = {} end function VerticalGroup:free() self:resetLayout() WidgetContainer.free(self) end --[[ A Layout widget that puts objects above each other ]] OverlapGroup = WidgetContainer:new{ _size = nil, } function OverlapGroup:getSize() if not self._size then self._size = {w = 0, h = 0} self._offsets = { x = math.huge, y = math.huge } for i, widget in ipairs(self) do local w_size = widget:getSize() if self._size.h < w_size.h then self._size.h = w_size.h end if self._size.w < w_size.w then self._size.w = w_size.w end end end if self.dimen.w then self._size.w = self.dimen.w end if self.dimen.h then self._size.h = self.dimen.h end return self._size end function OverlapGroup:paintTo(bb, x, y) local size = self:getSize() for i, wget in ipairs(self) do local wget_size = wget:getSize() if wget.align == "right" then wget:paintTo(bb, x+size.w-wget_size.w, y) elseif wget.align == "center" then wget:paintTo(bb, x+(size.w-wget_size.w)/2, y) else -- default to left wget:paintTo(bb, x, y) end end end --[[ Dummy Widget that reserves vertical space ]] VerticalSpan = Widget:new{ width = 0, } function VerticalSpan:getSize() return {w = 0, h = self.width} 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() if self.dimen then return { w = self.dimen.w, h = self.dimen.h } else local contentSize = self[1]:getSize() return { w = contentSize.w, h = contentSize.h + self.linesize + self.padding } end end function UnderlineContainer:paintTo(bb, x, y) local content_size = self:getSize() self[1]:paintTo(bb, x, y) bb:paintRect(x, y + content_size.h - self.linesize, content_size.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{} function InputContainer:_init() -- we need to do deep copy here local new_key_events = {} if self.key_events then for k,v in pairs(self.key_events) do new_key_events[k] = v end end self.key_events = new_key_events local new_ges_events = {} if self.ges_events then for k,v in pairs(self.ges_events) do new_ges_events[k] = v end end self.ges_events = new_ges_events if not self.dimen then self.dimen = Geom:new{} end end function InputContainer:paintTo(bb, x, y) self.dimen.x = x self.dimen.y = y if self[1] then return self[1]:paintTo(bb, x, y) end end -- 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 function InputContainer:onGesture(ev) for name, gsseq in pairs(self.ges_events) do for _, gs_range in ipairs(gsseq) do --DEBUG("gs_range", gs_range) if gs_range:match(ev) then local eventname = gsseq.event or name return self:handleEvent(Event:new(eventname, gsseq.args, ev)) end end end end