mirror of
https://github.com/koreader/koreader
synced 2024-11-13 19:11:25 +00:00
fadee1f5dc
Basically: * Use `extend` for class definitions * Use `new` for object instantiations That includes some minor code cleanups along the way: * Updated `Widget`'s docs to make the semantics clearer. * Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283) * Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass). * Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events. * Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier. * Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references. * ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak). * Terminal: Make sure the shell is killed on plugin teardown. * InputText: Fix Home/End/Del physical keys to behave sensibly. * InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...). * OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of. * ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed! * Kobo: Minor code cleanups.
489 lines
20 KiB
Lua
489 lines
20 KiB
Lua
local Device = require("device")
|
|
local Font = require("ui/font")
|
|
local Geom = require("ui/geometry")
|
|
local HorizontalGroup = require("ui/widget/horizontalgroup")
|
|
local HorizontalSpan = require("ui/widget/horizontalspan")
|
|
local IconButton = require("ui/widget/iconbutton")
|
|
local LineWidget = require("ui/widget/linewidget")
|
|
local Math = require("optmath")
|
|
local OverlapGroup = require("ui/widget/overlapgroup")
|
|
local Size = require("ui/size")
|
|
local TextBoxWidget = require("ui/widget/textboxwidget")
|
|
local TextWidget = require("ui/widget/textwidget")
|
|
local UIManager = require("ui/uimanager")
|
|
local VerticalGroup = require("ui/widget/verticalgroup")
|
|
local VerticalSpan = require("ui/widget/verticalspan")
|
|
local Screen = Device.screen
|
|
|
|
local DGENERIC_ICON_SIZE = G_defaults:readSetting("DGENERIC_ICON_SIZE")
|
|
|
|
local TitleBar = OverlapGroup:extend{
|
|
width = nil, -- default to screen width
|
|
fullscreen = false, -- larger font and small adjustments if fullscreen
|
|
align = "center", -- or "left": title & subtitle alignment inside TitleBar ("right" nor supported)
|
|
|
|
with_bottom_line = false,
|
|
bottom_line_color = nil, -- default to black
|
|
bottom_line_h_padding = nil, -- default to 0: full width
|
|
|
|
title = "",
|
|
title_face = nil, -- if not provided, one of these will be used:
|
|
title_face_fullscreen = Font:getFace("smalltfont"),
|
|
title_face_not_fullscreen = Font:getFace("x_smalltfont"),
|
|
-- by default: single line, truncated if overflow -- the default could be made dependant on self.fullscreen
|
|
title_multilines = false, -- multilines if overflow
|
|
title_shrink_font_to_fit = false, -- reduce font size so that single line text fits
|
|
|
|
subtitle = nil,
|
|
subtitle_face = Font:getFace("xx_smallinfofont"),
|
|
subtitle_truncate_left = false, -- default with single line is to truncate right (set to true for a filepath)
|
|
subtitle_fullwidth = false, -- true to allow subtitle to extend below the buttons
|
|
subtitle_multilines = false, -- multilines if overflow
|
|
|
|
info_text = nil, -- additional text displayed below bottom line
|
|
info_text_face = Font:getFace("x_smallinfofont"),
|
|
info_text_h_padding = nil, -- default to title_h_padding
|
|
|
|
title_top_padding = nil, -- computed if none provided
|
|
title_h_padding = Size.padding.large, -- horizontal padding (this replaces button_padding on the inner/title side)
|
|
title_subtitle_v_padding = Screen:scaleBySize(3),
|
|
bottom_v_padding = nil, -- hardcoded default values, different whether with_bottom_line true or false
|
|
|
|
button_padding = Screen:scaleBySize(11), -- fine to keep exit/cross icon diagonally aligned with screen corners
|
|
left_icon = nil,
|
|
left_icon_size_ratio = 0.6,
|
|
left_icon_rotation_angle = 0,
|
|
left_icon_tap_callback = function() end,
|
|
left_icon_hold_callback = function() end,
|
|
left_icon_allow_flash = true,
|
|
right_icon = nil,
|
|
right_icon_size_ratio = 0.6,
|
|
right_icon_rotation_angle = 0,
|
|
right_icon_tap_callback = function() end,
|
|
right_icon_hold_callback = function() end,
|
|
right_icon_allow_flash = true,
|
|
-- set any of these _callback to false to not handle the event
|
|
-- and let it propagate; otherwise the event is discarded
|
|
|
|
-- If provided, use right_icon="exit" and use this as right_icon_tap_callback
|
|
close_callback = nil,
|
|
close_hold_callback = nil,
|
|
|
|
show_parent = nil,
|
|
|
|
-- Internal: remember first sizes computed when title_shrink_font_to_fit=true,
|
|
-- and keep using them after :setTitle() in case a smaller font size is needed,
|
|
-- to keep the TitleBar geometry stable.
|
|
_initial_title_top_padding = nil,
|
|
_initial_title_text_baseline = nil,
|
|
_initial_titlebar_height = nil,
|
|
_initial_filler_height = nil,
|
|
_initial_re_init_needed = nil,
|
|
}
|
|
|
|
function TitleBar:init()
|
|
if self.close_callback then
|
|
self.right_icon = "close"
|
|
self.right_icon_tap_callback = self.close_callback
|
|
self.right_icon_allow_flash = false
|
|
if self.close_hold_callback then
|
|
self.right_icon_hold_callback = function() self.close_hold_callback() end
|
|
end
|
|
end
|
|
|
|
if not self.width then
|
|
self.width = Screen:getWidth()
|
|
end
|
|
|
|
local left_icon_size = Screen:scaleBySize(DGENERIC_ICON_SIZE * self.left_icon_size_ratio)
|
|
local right_icon_size = Screen:scaleBySize(DGENERIC_ICON_SIZE * self.right_icon_size_ratio)
|
|
self.has_left_icon = false
|
|
self.has_right_icon = false
|
|
|
|
-- No button on non-touch device
|
|
local left_icon_reserved_width = 0
|
|
local right_icon_reserved_width = 0
|
|
if self.left_icon then
|
|
self.has_left_icon = true
|
|
left_icon_reserved_width = left_icon_size + self.button_padding
|
|
end
|
|
if self.right_icon then
|
|
self.has_right_icon = true
|
|
right_icon_reserved_width = right_icon_size + self.button_padding
|
|
end
|
|
|
|
if self.align == "center" then
|
|
-- Keep title and subtitle text centered even if single button
|
|
left_icon_reserved_width = math.max(left_icon_reserved_width, right_icon_reserved_width)
|
|
right_icon_reserved_width = left_icon_reserved_width
|
|
end
|
|
local title_max_width = self.width - 2*self.title_h_padding - left_icon_reserved_width - right_icon_reserved_width
|
|
local subtitle_max_width = self.width - 2*self.title_h_padding
|
|
if not self.subtitle_fullwidth then
|
|
subtitle_max_width = subtitle_max_width - left_icon_reserved_width - right_icon_reserved_width
|
|
end
|
|
|
|
-- Title, subtitle, and their alignment
|
|
local title_face = self.title_face
|
|
if not title_face then
|
|
title_face = self.fullscreen and self.title_face_fullscreen or self.title_face_not_fullscreen
|
|
end
|
|
if self.title_multilines then
|
|
self.title_widget = TextBoxWidget:new{
|
|
text = self.title,
|
|
alignment = self.align,
|
|
width = title_max_width,
|
|
face = title_face,
|
|
}
|
|
else
|
|
while true do
|
|
self.title_widget = TextWidget:new{
|
|
text = self.title,
|
|
face = title_face,
|
|
padding = 0,
|
|
max_width = not self.title_shrink_font_to_fit and title_max_width,
|
|
-- truncate if not self.title_shrink_font_to_fit
|
|
}
|
|
if not self.title_shrink_font_to_fit then
|
|
break -- truncation allowed, no loop needed
|
|
end
|
|
if self.title_widget:getWidth() <= title_max_width then
|
|
break -- text with normal font fits, no loop needed
|
|
end
|
|
-- Text doesn't fit
|
|
if not self._initial_titlebar_height then
|
|
-- We're with title_shrink_font_to_fit and in the first :init():
|
|
-- we don't want to go on measuring with this too long text.
|
|
-- We want metrics proper for when text fits, so if later :setTitle()
|
|
-- is called with a text that fits, this text will look allright.
|
|
-- Longer title with a smaller font size should be laid out on the
|
|
-- baseline of a fitted text.
|
|
-- So, go on computing sizes with an empty title. When all is
|
|
-- gathered, we'll re :init() ourselves with the original title,
|
|
-- using the metrics we're computing now (self._initial*).
|
|
self._initial_re_init_needed = true
|
|
self.title_widget:free(true)
|
|
self.title_widget = TextWidget:new{
|
|
text = "",
|
|
face = title_face,
|
|
padding = 0,
|
|
}
|
|
break
|
|
end
|
|
-- otherwise, loop and do the same with a smaller font size
|
|
self.title_widget:free(true)
|
|
title_face = Font:getFace(title_face.orig_font, title_face.orig_size - 1)
|
|
end
|
|
end
|
|
local title_top_padding = self.title_top_padding
|
|
if not title_top_padding then
|
|
-- Compute it so baselines of the text and of the icons align.
|
|
-- Our icons' baselines looks like they could be at 83% to 90% of their height.
|
|
local text_baseline = self.title_widget:getBaseline()
|
|
local icon_height = math.max(left_icon_size, right_icon_size)
|
|
local icon_baseline = icon_height * 0.85 + self.button_padding
|
|
title_top_padding = Math.round(math.max(0, icon_baseline - text_baseline))
|
|
if self.title_shrink_font_to_fit then
|
|
-- Use, or store, the first top padding and baseline we have computed,
|
|
-- so the text stays vertically stable
|
|
if self._initial_title_top_padding then
|
|
-- Use this to have baselines aligned:
|
|
-- title_top_padding = Math.round(self._initial_title_top_padding + self._initial_title_text_baseline - text_baseline)
|
|
-- But then, smaller text is not vertically centered in the title bar.
|
|
-- So, go with just half the baseline difference:
|
|
title_top_padding = Math.round(self._initial_title_top_padding + (self._initial_title_text_baseline - text_baseline)/2)
|
|
else
|
|
self._initial_title_top_padding = title_top_padding
|
|
self._initial_title_text_baseline = text_baseline
|
|
end
|
|
end
|
|
end
|
|
|
|
self.subtitle_widget = nil
|
|
if self.subtitle then
|
|
if self.subtitle_multilines then
|
|
self.subtitle_widget = TextBoxWidget:new{
|
|
text = self.subtitle,
|
|
alignment = self.align,
|
|
width = subtitle_max_width,
|
|
face = self.subtitle_face,
|
|
}
|
|
else
|
|
self.subtitle_widget = TextWidget:new{
|
|
text = self.subtitle,
|
|
face = self.subtitle_face,
|
|
max_width = subtitle_max_width,
|
|
truncate_left = self.subtitle_truncate_left,
|
|
padding = 0,
|
|
}
|
|
end
|
|
end
|
|
-- To debug vertical positionning:
|
|
-- local FrameContainer = require("ui/widget/container/framecontainer")
|
|
-- self.title_widget = FrameContainer:new{ padding=0, margin=0, bordersize=1, self.title_widget}
|
|
-- self.subtitle_widget = FrameContainer:new{ padding=0, margin=0, bordersize=1, self.subtitle_widget}
|
|
|
|
self.title_group = VerticalGroup:new{
|
|
align = self.align,
|
|
overlap_align = self.align,
|
|
VerticalSpan:new{width = title_top_padding},
|
|
}
|
|
if self.align == "left" then
|
|
-- we need to :resetLayout() both VerticalGroup and HorizontalGroup in :setTitle()
|
|
self.inner_title_group = HorizontalGroup:new{
|
|
HorizontalSpan:new{ width = left_icon_reserved_width + self.title_h_padding },
|
|
self.title_widget,
|
|
}
|
|
table.insert(self.title_group, self.inner_title_group)
|
|
else
|
|
table.insert(self.title_group, self.title_widget)
|
|
end
|
|
if self.subtitle_widget then
|
|
table.insert(self.title_group, VerticalSpan:new{width = self.title_subtitle_v_padding})
|
|
if self.align == "left" then
|
|
local span_width = self.title_h_padding
|
|
if not self.subtitle_fullwidth then
|
|
span_width = span_width + left_icon_reserved_width
|
|
end
|
|
self.inner_subtitle_group = HorizontalGroup:new{
|
|
HorizontalSpan:new{ width = span_width },
|
|
self.subtitle_widget,
|
|
}
|
|
table.insert(self.title_group, self.inner_subtitle_group)
|
|
else
|
|
table.insert(self.title_group, self.subtitle_widget)
|
|
end
|
|
end
|
|
table.insert(self, self.title_group)
|
|
|
|
-- This TitleBar widget is an OverlapGroup: all sub elements overlap,
|
|
-- and can overflow or underflow. Its height for its containers is
|
|
-- the one we set as self.dimen.h.
|
|
|
|
self.titlebar_height = self.title_group:getSize().h
|
|
if self.title_shrink_font_to_fit then
|
|
-- Use, or store, the first title_group height we have computed,
|
|
-- so the TitleBar geometry and the bottom line position stay stable
|
|
-- (face height may have changed, even after we kept the baseline
|
|
-- stable, as we did above).
|
|
if self._initial_titlebar_height then
|
|
self.titlebar_height = self._initial_titlebar_height
|
|
else
|
|
self._initial_titlebar_height = self.titlebar_height
|
|
end
|
|
end
|
|
|
|
if self.with_bottom_line then
|
|
-- Be sure we add between the text and the line at least as much padding
|
|
-- as above the text, to keep it vertically centered.
|
|
local title_bottom_padding = math.max(title_top_padding, Size.padding.default)
|
|
local filler_height = self.titlebar_height + title_bottom_padding
|
|
if self.title_shrink_font_to_fit then
|
|
-- Use, or store, the first filler height we have computed,
|
|
if self._initial_filler_height then
|
|
filler_height = self._initial_filler_height
|
|
else
|
|
self._initial_filler_height = filler_height
|
|
end
|
|
end
|
|
local line_widget = LineWidget:new{
|
|
dimen = Geom:new{ w = self.width, h = Size.line.thick },
|
|
background = self.bottom_line_color
|
|
}
|
|
if self.bottom_line_h_padding then
|
|
line_widget.dimen.w = line_widget.dimen.w - 2 * self.bottom_line_h_padding
|
|
line_widget = HorizontalGroup:new{
|
|
HorizontalSpan:new{ width = self.bottom_line_h_padding },
|
|
line_widget,
|
|
}
|
|
end
|
|
local filler_and_bottom_line = VerticalGroup:new{
|
|
VerticalSpan:new{ width = filler_height },
|
|
line_widget,
|
|
}
|
|
table.insert(self, filler_and_bottom_line)
|
|
self.titlebar_height = filler_and_bottom_line:getSize().h
|
|
end
|
|
if not self.bottom_v_padding then
|
|
if self.with_bottom_line then
|
|
self.bottom_v_padding = Size.padding.default
|
|
else
|
|
self.bottom_v_padding = Size.padding.large
|
|
end
|
|
end
|
|
self.titlebar_height = self.titlebar_height + self.bottom_v_padding
|
|
|
|
if self._initial_re_init_needed then
|
|
-- We have computed all the self._initial_ metrics needed.
|
|
self._initial_re_init_needed = nil
|
|
self:clear()
|
|
self:init()
|
|
return
|
|
end
|
|
|
|
if self.info_text then
|
|
local h_padding = self.info_text_h_padding or self.title_h_padding
|
|
local v_padding = self.with_bottom_line and Size.padding.default or 0
|
|
local filler_and_info_text = VerticalGroup:new{
|
|
VerticalSpan:new{ width = self.titlebar_height + v_padding },
|
|
HorizontalGroup:new{
|
|
HorizontalSpan:new{ width = h_padding },
|
|
TextBoxWidget:new{
|
|
text = self.info_text,
|
|
face = self.info_text_face,
|
|
width = self.width - 2 * h_padding,
|
|
}
|
|
}
|
|
}
|
|
table.insert(self, filler_and_info_text)
|
|
self.titlebar_height = filler_and_info_text:getSize().h + self.bottom_v_padding
|
|
end
|
|
|
|
self.dimen = Geom:new{
|
|
x = 0,
|
|
y = 0,
|
|
w = self.width,
|
|
h = self.titlebar_height, -- buttons can overflow this
|
|
}
|
|
|
|
if self.has_left_icon then
|
|
self.left_button = IconButton:new{
|
|
icon = self.left_icon,
|
|
icon_rotation_angle = self.left_icon_rotation_angle,
|
|
width = left_icon_size,
|
|
height = left_icon_size,
|
|
padding = self.button_padding,
|
|
padding_right = 2 * left_icon_size, -- extend button tap zone
|
|
padding_bottom = left_icon_size,
|
|
overlap_align = "left",
|
|
callback = self.left_icon_tap_callback,
|
|
hold_callback = self.left_icon_hold_callback,
|
|
allow_flash = self.left_icon_allow_flash,
|
|
show_parent = self.show_parent,
|
|
}
|
|
table.insert(self, self.left_button)
|
|
end
|
|
if self.has_right_icon then
|
|
self.right_button = IconButton:new{
|
|
icon = self.right_icon,
|
|
icon_rotation_angle = self.right_icon_rotation_angle,
|
|
width = right_icon_size,
|
|
height = right_icon_size,
|
|
padding = self.button_padding,
|
|
padding_left = 2 * right_icon_size, -- extend button tap zone
|
|
padding_bottom = right_icon_size,
|
|
overlap_align = "right",
|
|
callback = self.right_icon_tap_callback,
|
|
hold_callback = self.right_icon_hold_callback,
|
|
allow_flash = self.right_icon_allow_flash,
|
|
show_parent = self.show_parent,
|
|
}
|
|
table.insert(self, self.right_button)
|
|
end
|
|
|
|
-- Call our base class's init (especially since OverlapGroup has very peculiar self.dimen semantics...)
|
|
OverlapGroup.init(self)
|
|
end
|
|
|
|
function TitleBar:paintTo(bb, x, y)
|
|
-- We need to update self.dimen's x and y for any ges.pos:intersectWith(title_bar)
|
|
-- to work. (This is done by FrameContainer, but not by most other widgets... It
|
|
-- should probably be done in all of them, but not sure of side effects...)
|
|
self.dimen.x = x
|
|
self.dimen.y = y
|
|
OverlapGroup.paintTo(self, bb, x, y)
|
|
end
|
|
|
|
function TitleBar:getHeight()
|
|
return self.titlebar_height
|
|
end
|
|
|
|
function TitleBar:setTitle(title, no_refresh)
|
|
if self.title_multilines or self.title_shrink_font_to_fit then
|
|
-- We need to re-init the whole widget as its height or
|
|
-- padding may change.
|
|
local previous_height = self.titlebar_height
|
|
-- Call WidgetContainer:clear() that will call :free() and
|
|
-- will remove subwidgets from the OverlapGroup we are.
|
|
self:clear()
|
|
self.title = title
|
|
self:init()
|
|
if no_refresh then
|
|
-- If caller is sure to handle refresh correctly, it can provides this
|
|
return
|
|
end
|
|
if self.title_multilines and self.titlebar_height ~= previous_height then
|
|
-- Title height have changed, and the upper widget may not have
|
|
-- hooks to refresh a combination of its previous size and new
|
|
-- size: be sure everything is repainted
|
|
UIManager:setDirty("all", "ui")
|
|
else
|
|
UIManager:setDirty(self.show_parent, "ui", self.dimen)
|
|
end
|
|
else
|
|
-- TextWidget with max-width: we can just update its text
|
|
self.title_widget:setText(title)
|
|
if self.inner_title_group then
|
|
self.inner_title_group:resetLayout()
|
|
end
|
|
self.title_group:resetLayout()
|
|
if no_refresh then
|
|
return
|
|
end
|
|
UIManager:setDirty(self.show_parent, "ui", self.dimen)
|
|
end
|
|
end
|
|
|
|
function TitleBar:setSubTitle(subtitle)
|
|
if self.subtitle_widget and not self.subtitle_multilines then -- no TextBoxWidget:setText() available
|
|
self.subtitle_widget:setText(subtitle)
|
|
if self.inner_subtitle_group then
|
|
self.inner_subtitle_group:resetLayout()
|
|
end
|
|
self.title_group:resetLayout()
|
|
UIManager:setDirty(self.show_parent, "ui", self.dimen)
|
|
end
|
|
end
|
|
|
|
function TitleBar:setLeftIcon(icon)
|
|
if self.has_left_icon then
|
|
self.left_button:setIcon(icon)
|
|
UIManager:setDirty(self.show_parent, "ui", self.dimen)
|
|
end
|
|
end
|
|
|
|
function TitleBar:setRightIcon(icon)
|
|
if self.has_right_icon then
|
|
self.right_button:setIcon(icon)
|
|
UIManager:setDirty(self.show_parent, "ui", self.dimen)
|
|
end
|
|
end
|
|
|
|
-- layout for FocusManager
|
|
function TitleBar:generateHorizontalLayout()
|
|
local row = {}
|
|
if self.left_button then
|
|
table.insert(row, self.left_button)
|
|
end
|
|
if self.right_button then
|
|
table.insert(row, self.right_button)
|
|
end
|
|
local layout = {}
|
|
if #row > 0 then
|
|
table.insert(layout, row)
|
|
end
|
|
return layout
|
|
end
|
|
|
|
function TitleBar:generateVerticalLayout()
|
|
local layout = {}
|
|
if self.left_button then
|
|
table.insert(layout, {self.left_button})
|
|
end
|
|
if self.right_button then
|
|
table.insert(layout, {self.right_button})
|
|
end
|
|
return layout
|
|
end
|
|
return TitleBar
|