mirror of
https://github.com/koreader/koreader
synced 2024-10-31 21:20:20 +00:00
3ec818a3cd
add reflow mode in pdf/djvu readers
709 lines
16 KiB
Lua
709 lines
16 KiB
Lua
require "ui/screen"
|
|
require "ui/rendertext"
|
|
require "ui/graphics"
|
|
require "ui/image"
|
|
require "ui/event"
|
|
require "ui/inputevent"
|
|
require "ui/gesturedetector"
|
|
require "ui/font"
|
|
|
|
--[[
|
|
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 = {}
|
|
|
|
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
|
|
|
|
--[[
|
|
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()
|
|
|
|
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
|
|
|
|
--[[
|
|
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 {
|
|
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,
|
|
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
|
|
]]
|
|
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
|
|
|