2
0
mirror of https://github.com/koreader/koreader synced 2024-11-13 19:11:25 +00:00
koreader/frontend/ui/widget/titlebar.lua
NiLuJe fadee1f5dc
Clarify our OOP semantics across the codebase (#9586)
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.
2022-10-06 02:14:48 +02:00

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