2
0
mirror of https://github.com/koreader/koreader synced 2024-10-31 21:20:20 +00:00
koreader/frontend/ui/widget.lua

709 lines
16 KiB
Lua
Raw Normal View History

require "ui/screen"
require "ui/rendertext"
require "ui/graphics"
require "ui/image"
require "ui/event"
require "ui/inputevent"
2012-11-11 06:00:52 +00:00
require "ui/gesturedetector"
require "ui/font"
--[[
2012-04-22 19:29:48 +00:00
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.
2012-04-22 19:29:48 +00:00
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.
]]
2012-04-22 19:29:48 +00:00
Widget = {}
function Widget:new(o)
2012-04-22 19:29:48 +00:00
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
2012-04-22 19:29:48 +00:00
if o.init then o:init() end
return o
end
function Widget:getSize()
return self.dimen
end
function Widget:paintTo(bb, x, y)
end
2012-04-22 19:29:48 +00:00
--[[
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
--[[
WidgetContainer is a container for another Widget
]]
WidgetContainer = Widget:new()
2012-04-22 19:29:48 +00:00
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
2012-11-11 06:00:52 +00:00
return Geom:new{ w = 0, h = 0 }
2012-04-22 19:29:48 +00:00
end
end
--[[
delete all child widgets
]]--
function WidgetContainer:clear()
while table.remove(self) do end
end
2012-04-22 19:29:48 +00:00
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
]]--
2012-04-22 19:29:48 +00:00
function WidgetContainer:handleEvent(event)
if not self:propagateEvent(event) then
-- call our own standard event handler
2012-04-22 19:29:48 +00:00
return Widget.handleEvent(self, event)
else
return true
end
end
function WidgetContainer:free()
for _, widget in ipairs(self) do
2012-04-22 19:29:48 +00:00
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()
2012-04-22 19:29:48 +00:00
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
2012-04-22 19:29:48 +00:00
--[[
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
2012-04-22 19:29:48 +00:00
-- 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
2012-12-19 15:45:51 +00:00
--[[
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
]]
2012-04-22 19:29:48 +00:00
FrameContainer = WidgetContainer:new{
background = nil,
color = 15,
margin = 0,
2012-04-28 06:41:05 +00:00
radius = 0,
bordersize = 2,
padding = 5,
2012-04-22 19:29:48 +00:00
}
function FrameContainer:getSize()
local content_size =self[1]:getSize()
return {
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()
2012-04-22 19:29:48 +00:00
if self.background then
2012-04-30 07:40:43 +00:00
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,
2012-04-28 06:41:05 +00:00
self.bordersize, self.color, self.radius)
end
2012-04-22 19:29:48 +00:00
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
]]
2012-04-22 19:29:48 +00:00
TextWidget = Widget:new{
text = nil,
face = nil,
color = 15,
_bb = nil,
_length = 0,
_height = 0,
_maxlength = 1200,
2012-04-22 19:29:48 +00:00
}
--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())
2012-10-09 16:39:09 +00:00
--@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
2012-10-09 16:39:09 +00:00
--@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
2012-04-28 06:41:05 +00:00
-- 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
]]
2012-04-22 19:29:48 +00:00
ImageWidget = Widget:new{
invert = nil,
file = nil,
_bb = nil
2012-04-22 19:29:48 +00:00
}
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
2012-12-23 23:54:44 +00:00
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
2012-12-19 15:45:51 +00:00
--[[
ProgressWidget shows a progress bar
]]
ProgressWidget = Widget:new{
width = nil,
height = nil,
pecentage = nil,
}
function ProgressWidget:getSize()
return { w = self.width, h = self.height }
end
function ProgressWidget:paintTo(bb, x, y)
local size = self:getSize()
bb:progressBar(x, y, self.width, self.height, size.w, size.h, 2, 2, self.percentage, 15)
end
--[[
A Layout widget that puts objects besides each others
]]
2012-04-22 19:29:48 +00:00
HorizontalGroup = WidgetContainer:new{
align = "center",
_size = nil,
2012-04-22 19:29:48 +00:00
}
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
]]
2012-04-22 19:29:48 +00:00
VerticalGroup = WidgetContainer:new{
align = "center",
_size = nil,
_offsets = {}
2012-04-22 19:29:48 +00:00
}
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
2012-04-22 19:29:48 +00:00
2012-12-07 15:28:55 +00:00
--[[
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
2012-04-22 19:29:48 +00:00
--[[
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
2012-04-22 19:29:48 +00:00
end
function UnderlineContainer:paintTo(bb, x, y)
local content_size = self:getSize()
2012-04-22 19:29:48 +00:00
self[1]:paintTo(bb, x, y)
bb:paintRect(x, y + content_size.h - self.linesize,
content_size.w, self.linesize, self.color)
2012-04-22 19:29:48 +00:00
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
2012-04-22 19:29:48 +00:00
2012-11-11 06:00:52 +00:00
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
2012-04-22 19:29:48 +00:00
-- 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
2012-11-11 06:00:52 +00:00
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)
2012-11-11 06:00:52 +00:00
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