mirror of
https://github.com/koreader/koreader
synced 2024-11-10 01:10:34 +00:00
5a4e70a19d
These changes make the title bar in File manager, History and Collection windows of the same height. The items and book covers thumbnail dimensions in Mosaic and Detailed list display modes are now equal, and up/down scaling of the thumbnails is not needed when switching between File manager, History and Collection.
497 lines
20 KiB
Lua
497 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
|
|
|
|
lang = nil, -- use this language (string) instead of the UI language
|
|
|
|
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,
|
|
lang = self.lang,
|
|
}
|
|
else
|
|
while true do
|
|
self.title_widget = TextWidget:new{
|
|
text = self.title,
|
|
face = title_face,
|
|
padding = 0,
|
|
lang = self.lang,
|
|
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,
|
|
lang = self.lang,
|
|
}
|
|
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,
|
|
lang = self.lang,
|
|
}
|
|
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,
|
|
lang = self.lang,
|
|
}
|
|
}
|
|
}
|
|
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 not no_refresh then
|
|
UIManager:setDirty(self.show_parent, "ui", self.dimen)
|
|
end
|
|
end
|
|
end
|
|
|
|
function TitleBar:setSubTitle(subtitle, no_refresh)
|
|
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()
|
|
if not no_refresh then
|
|
UIManager:setDirty(self.show_parent, "ui", self.dimen)
|
|
end
|
|
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
|