TitleBar: new options, dedicated close button

- Fix hold callback name
- Add options to handle long titles (truncated by default):
  "title_multines" and "title_shrink_font_to_fit" (allowing
  setTitle() with them makes the code a bit more complex).
- Add options to set bottom line color and horizontal padding.
- Use an added close.svg (based on exit.svg, tweaked to have
  similar size and baseline as other icons) for close button.

IconButton:
- handle hold/hold_release similar to Button.
- new option allow_flash to allow disabling flashing.
reviewable/pr8632/r1^2
poire-z 2 years ago
parent 6a5f330b3b
commit 4546d826ee

@ -600,7 +600,7 @@ function BookMapWidget:init()
left_icon = "notice-info", left_icon = "notice-info",
left_icon_tap_callback = function() self:showHelp() end, left_icon_tap_callback = function() self:showHelp() end,
close_callback = function() self:onClose() end, close_callback = function() self:onClose() end,
hold_close_callback = function() self:onClose(true) end, close_hold_callback = function() self:onClose(true) end,
show_parent = self, show_parent = self,
} }
self.title_bar_h = self.title_bar:getHeight() self.title_bar_h = self.title_bar:getHeight()
@ -1036,6 +1036,8 @@ function BookMapWidget:onClose(close_all_parents)
-- The last one of these (which has no launcher attribute) -- The last one of these (which has no launcher attribute)
-- will do the cleanup below. -- will do the cleanup below.
self.launcher:onClose(true) self.launcher:onClose(true)
else
UIManager:setDirty(self.launcher, "ui")
end end
else else
-- Remove all thumbnails generated for a different target size than -- Remove all thumbnails generated for a different target size than

@ -27,6 +27,7 @@ local IconButton = InputContainer:new{
padding_left = nil, padding_left = nil,
enabled = true, enabled = true,
callback = nil, callback = nil,
allow_flash = true, -- set to false for any IconButton that may close its container
} }
function IconButton:init() function IconButton:init()
@ -85,6 +86,13 @@ function IconButton:initGesListener()
range = self.dimen, range = self.dimen,
}, },
doc = "Hold IconButton", doc = "Hold IconButton",
},
HoldReleaseIconButton = {
GestureRange:new{
ges = "hold_release",
range = self.dimen,
},
doc = "Hold Release IconButton",
} }
} }
end end
@ -92,7 +100,7 @@ end
function IconButton:onTapIconButton() function IconButton:onTapIconButton()
if not self.callback then return end if not self.callback then return end
if G_reader_settings:isFalse("flash_ui") then if G_reader_settings:isFalse("flash_ui") or not self.allow_flash then
self.callback() self.callback()
else else
-- c.f., ui/widget/button for more gnarly details about the implementation, but the flow of the flash_ui codepath essentially goes like this: -- c.f., ui/widget/button for more gnarly details about the implementation, but the flow of the flash_ui codepath essentially goes like this:
@ -133,6 +141,10 @@ function IconButton:onTapIconButton()
end end
function IconButton:onHoldIconButton() function IconButton:onHoldIconButton()
-- If we're going to process this hold, we must make
-- sure to also handle its hold_release below, so it's
-- not propagated up to a MovableContainer
self._hold_handled = nil
if self.enabled and self.hold_callback then if self.enabled and self.hold_callback then
self.hold_callback() self.hold_callback()
elseif self.hold_input then elseif self.hold_input then
@ -140,9 +152,18 @@ function IconButton:onHoldIconButton()
elseif type(self.hold_input_func) == "function" then elseif type(self.hold_input_func) == "function" then
self:onInput(self.hold_input_func()) self:onInput(self.hold_input_func())
elseif self.hold_callback == nil then return end elseif self.hold_callback == nil then return end
self._hold_handled = true
return true return true
end end
function IconButton:onHoldReleaseIconButton()
if self._hold_handled then
self._hold_handled = nil
return true
end
return false
end
function IconButton:onFocus() function IconButton:onFocus()
--quick and dirty, need better way to show focus --quick and dirty, need better way to show focus
self.image.invert = true self.image.invert = true

@ -105,7 +105,7 @@ function PageBrowserWidget:init()
left_icon = "notice-info", left_icon = "notice-info",
left_icon_tap_callback = function() self:showHelp() end, left_icon_tap_callback = function() self:showHelp() end,
close_callback = function() self:onClose() end, close_callback = function() self:onClose() end,
hold_close_callback = function() self:onClose(true) end, close_hold_callback = function() self:onClose(true) end,
show_parent = self, show_parent = self,
} }
self.title_bar_h = self.title_bar:getHeight() self.title_bar_h = self.title_bar:getHeight()
@ -618,8 +618,8 @@ function PageBrowserWidget:onClose(close_all_parents)
if self.requests_batch_id then if self.requests_batch_id then
self.ui.thumbnail:cancelPageThumbnailRequests(self.requests_batch_id) self.ui.thumbnail:cancelPageThumbnailRequests(self.requests_batch_id)
end end
logger.dbg("closing PageBrowserWidget")
-- Close this widget -- Close this widget
logger.dbg("closing PageBrowserWidget")
UIManager:close(self) UIManager:close(self)
if self.launcher then if self.launcher then
-- We were launched by a BookMapWidget, don't do any cleanup. -- We were launched by a BookMapWidget, don't do any cleanup.
@ -627,6 +627,8 @@ function PageBrowserWidget:onClose(close_all_parents)
-- The last one of these (which has no launcher attribute) -- The last one of these (which has no launcher attribute)
-- will do the cleanup below. -- will do the cleanup below.
self.launcher:onClose(true) self.launcher:onClose(true)
else
UIManager:setDirty(self.launcher, "ui")
end end
else else
-- Remove all thumbnails generated for a different target size than -- Remove all thumbnails generated for a different target size than

@ -8,6 +8,7 @@ local LineWidget = require("ui/widget/linewidget")
local Math = require("optmath") local Math = require("optmath")
local OverlapGroup = require("ui/widget/overlapgroup") local OverlapGroup = require("ui/widget/overlapgroup")
local Size = require("ui/size") local Size = require("ui/size")
local TextBoxWidget = require("ui/widget/textboxwidget")
local TextWidget = require("ui/widget/textwidget") local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager") local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup") local VerticalGroup = require("ui/widget/verticalgroup")
@ -17,14 +18,21 @@ local Screen = Device.screen
local TitleBar = OverlapGroup:extend{ local TitleBar = OverlapGroup:extend{
width = nil, -- default to screen width width = nil, -- default to screen width
fullscreen = false, -- larger font and small adjustments if fullscreen fullscreen = false, -- larger font and small adjustments if fullscreen
align = "center", align = "center", -- title & subtitle alignment inside TitleBar
with_bottom_line = false, with_bottom_line = false,
bottom_line_color = nil, -- default to black
bottom_line_h_padding = nil, -- default to 0: full width
title = "", title = "",
subtitle = nil,
title_face = nil, -- if not provided, one of these will be used: title_face = nil, -- if not provided, one of these will be used:
title_face_fullscreen = Font:getFace("smalltfont"), title_face_fullscreen = Font:getFace("smalltfont"),
title_face_not_fullscreen = Font:getFace("x_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_face = Font:getFace("xx_smallinfofont"),
title_top_padding = nil, -- computed if none provided title_top_padding = nil, -- computed if none provided
@ -46,12 +54,23 @@ local TitleBar = OverlapGroup:extend{
-- If provided, use right_icon="exit" and use this as right_icon_tap_callback -- If provided, use right_icon="exit" and use this as right_icon_tap_callback
close_callback = nil, close_callback = nil,
hold_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() function TitleBar:init()
if self.close_callback then if self.close_callback then
self.right_icon = "exit" self.right_icon = "close"
self.right_icon_tap_callback = self.close_callback self.right_icon_tap_callback = self.close_callback
self.right_icon_allow_flash = false self.right_icon_allow_flash = false
if self.close_hold_callback then if self.close_hold_callback then
@ -82,25 +101,86 @@ function TitleBar:init()
end end
-- Title, subtitle, and their alignment -- Title, subtitle, and their alignment
if not self.title_face then local title_face = self.title_face
self.title_face = self.fullscreen and self.title_face_fullscreen or self.title_face_not_fullscreen if not title_face then
title_face = self.fullscreen and self.title_face_fullscreen or self.title_face_not_fullscreen
end end
if not self.title_top_padding then 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()
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()
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. -- 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. -- Our icons' baselines looks like they could be at 83% to 90% of their height.
local face_height, face_baseline = self.title_face.ftface:getHeightAndAscender() -- luacheck: no unused local text_baseline = self.title_widget:getBaseline()
local icon_height = math.max(left_icon_size, right_icon_size) local icon_height = math.max(left_icon_size, right_icon_size)
local icon_baseline = icon_height * 0.85 + self.button_padding local icon_baseline = icon_height * 0.85 + self.button_padding
self.title_top_padding = Math.round(math.max(0, icon_baseline - face_baseline)) 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 end
self.title_widget = TextWidget:new{
text = self.title,
face = self.title_face,
max_width = title_max_width,
padding = 0,
}
self.subtitle_widget = nil self.subtitle_widget = nil
if self.subtitle then if self.subtitle then
-- No specific options for subtitles currently: truncate if too long.
-- We might want to add truncate_left if this gets used for a filepath
-- as in FileManager.
self.subtitle_widget = TextWidget:new{ self.subtitle_widget = TextWidget:new{
text = self.subtitle, text = self.subtitle,
face = self.subtitle_face, face = self.subtitle_face,
@ -115,7 +195,7 @@ function TitleBar:init()
self.title_group = VerticalGroup:new{ self.title_group = VerticalGroup:new{
align = self.align, align = self.align,
VerticalSpan:new{width = self.title_top_padding}, VerticalSpan:new{width = title_top_padding},
self.title_widget, self.title_widget,
self.subtitle_widget and VerticalSpan:new{width = self.title_subtitle_v_padding}, self.subtitle_widget and VerticalSpan:new{width = self.title_subtitle_v_padding},
self.subtitle_widget, self.subtitle_widget,
@ -139,15 +219,45 @@ function TitleBar:init()
-- the one we set as self.dimen.h. -- the one we set as self.dimen.h.
self.titlebar_height = self.title_group:getSize().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 if self.with_bottom_line then
-- Be sure we add between the text and the line at least as much padding -- Be sure we add between the text and the line at least as much padding
-- as above the text, to keep it vertically centered. -- as above the text, to keep it vertically centered.
local title_bottom_padding = math.max(self.title_top_padding, Size.padding.default) 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{ local filler_and_bottom_line = VerticalGroup:new{
VerticalSpan:new{ width = self.titlebar_height + title_bottom_padding }, VerticalSpan:new{ width = filler_height },
LineWidget:new{ line_widget,
dimen = Geom:new{ w = self.width, h = Size.line.thick },
},
} }
table.insert(self, filler_and_bottom_line) table.insert(self, filler_and_bottom_line)
self.titlebar_height = filler_and_bottom_line:getSize().h self.titlebar_height = filler_and_bottom_line:getSize().h
@ -161,6 +271,14 @@ function TitleBar:init()
end end
self.titlebar_height = self.titlebar_height + self.bottom_v_padding 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
self.dimen = Geom:new{ self.dimen = Geom:new{
w = self.width, w = self.width,
h = self.titlebar_height, -- buttons can overflow this h = self.titlebar_height, -- buttons can overflow this
@ -177,7 +295,8 @@ function TitleBar:init()
overlap_align = "left", overlap_align = "left",
callback = self.left_icon_tap_callback, callback = self.left_icon_tap_callback,
hold_callback = self.left_icon_hold_callback, hold_callback = self.left_icon_hold_callback,
allow_flash = self.left_icon_allow_flash allow_flash = self.left_icon_allow_flash,
show_parent = self.show_parent,
} }
table.insert(self, self.left_button) table.insert(self, self.left_button)
end end
@ -192,7 +311,8 @@ function TitleBar:init()
overlap_align = "right", overlap_align = "right",
callback = self.right_icon_tap_callback, callback = self.right_icon_tap_callback,
hold_callback = self.right_icon_hold_callback, hold_callback = self.right_icon_hold_callback,
allow_flash = self.right_icon_allow_flash allow_flash = self.right_icon_allow_flash,
show_parent = self.show_parent,
} }
table.insert(self, self.right_button) table.insert(self, self.right_button)
end end
@ -215,13 +335,40 @@ function TitleBar:getHeight()
return self.titlebar_height return self.titlebar_height
end end
function TitleBar:setTitle(title) function TitleBar:setTitle(title, no_refresh)
self.title_widget:setText(title) if self.title_multilines or self.title_shrink_font_to_fit then
if self.inner_title_group then -- We need to re-init the whole widget as its height or
self.inner_title_group:resetLayout() -- 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
self.title_group:resetLayout()
UIManager:setDirty(self.show_parent, "ui", self.dimen)
end end
function TitleBar:setSubTitle(subtitle) function TitleBar:setSubTitle(subtitle)

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="24"
height="24"
viewBox="2 2 20 20"
enable-background="new 0 0 24.00 24.00"
xml:space="preserve"
id="svg4"
sodipodi:docname="exit.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"><metadata
id="metadata10"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs8" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1381"
id="namedview6"
showgrid="true"
inkscape:zoom="42"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4"
inkscape:document-rotation="0"><inkscape:grid
type="xygrid"
id="grid855" /></sodipodi:namedview>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="M 18,6 5,19"
id="path834" /><path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="M 5,6 18,19"
id="path836"
sodipodi:nodetypes="cc" /></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Loading…
Cancel
Save