Tame some ButtonTable users into re-using Buttontable instances if possible (#7166)

* QuickDictLookup, ImageViewer, NumberPicker: Smarter `update` that will re-use most of the widget's layout instead of re-instantiating all the things.
* SpinWidget/DoubleSpinWidget: The NumberPicker change above renders a hack to preserve alpha on these widgets almost unnecessary. Also fixed said hack to also apply to the center, value button.

* Button: Don't re-instantiate the frame in setText/setIcon when unnecessary (e.g., no change at all, or no layout change).
* Button: Add a refresh method that repaints and refreshes a *specific* Button (provided it's been painted once) all on its lonesome.

* ConfigDialog: Free everything that's going to be re-instatiated on update
 
* A few more post #7118 fixes:
  * SkimTo: Always flag the chapter nav buttons as vsync
  * Button: Fix the highlight on rounded buttons when vsync is enabled (e.g., it's now entirely visible, instead of showing a weird inverted corner glitch).
  * Some more heuristic tweaks in Menu/TouchMenu/Button/IconButton
* ButtonTable: fix the annoying rounding issue I'd noticed in #7054 ;).

* Enable dithering in TextBoxWidget (e.g., in the Wikipedia full view). This involved moving the HW dithering align fixup to base, where it always ought to have been ;).

* Switch a few widgets that were using "partial" on close to "ui", or, more rarely, "flashui". The intent being to limit "partial" purely to the Reader, because it has a latency cost when mixed with other refreshes, which happens often enough in UI ;).

* Minor documentation tweaks around UIManager's `setDirty` to reflect that change.

* ReaderFooter: Force a footer repaint on resume if it is visible (otherwise, just update it).
* ReaderBookmark: In the same vein, don't repaint an invisible footer on bookmark count changes.
pull/7204/head
NiLuJe 3 years ago committed by GitHub
parent f4f8820575
commit df0bbc9db7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1 +1 @@
Subproject commit 75b629d7ad66510f822fa0dda9c32f402ecd5b08 Subproject commit 43b9a2967954477db8f1fc9cd9ce6f5f7a798049

@ -85,7 +85,7 @@ function SetDefaults:init()
-- opened immediately) we need to set the full screen dirty because -- opened immediately) we need to set the full screen dirty because
-- otherwise only the input dialog part of the screen is refreshed. -- otherwise only the input dialog part of the screen is refreshed.
menu_container.onShow = function() menu_container.onShow = function()
UIManager:setDirty(nil, "partial") UIManager:setDirty(nil, "ui")
end end
self.defaults_menu = Menu:new{ self.defaults_menu = Menu:new{

@ -54,7 +54,7 @@ end
function OPDSCatalog:onCloseWidget() function OPDSCatalog:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self[1].dimen return "ui", self[1].dimen
end) end)
end end

@ -182,7 +182,7 @@ function ReaderBookmark:onToggleBookmark()
pn_or_xp = self.ui.document:getXPointer() pn_or_xp = self.ui.document:getXPointer()
end end
self:toggleBookmark(pn_or_xp) self:toggleBookmark(pn_or_xp)
self.view.footer:onUpdateFooter(true) self.view.footer:onUpdateFooter(self.view.footer_visible)
self.ui:handleEvent(Event:new("SetDogearVisibility", self.ui:handleEvent(Event:new("SetDogearVisibility",
not self.view.dogear_visible)) not self.view.dogear_visible))
UIManager:setDirty(self.view.dialog, "ui") UIManager:setDirty(self.view.dialog, "ui")
@ -426,7 +426,7 @@ function ReaderBookmark:addBookmark(item)
end end
end end
table.insert(self.bookmarks, _middle + direction, item) table.insert(self.bookmarks, _middle + direction, item)
self.view.footer:onUpdateFooter(true) self.view.footer:onUpdateFooter(self.view.footer_visible)
end end
-- binary search of sorted bookmarks -- binary search of sorted bookmarks
@ -470,7 +470,7 @@ function ReaderBookmark:removeBookmark(item)
local v = self.bookmarks[_middle] local v = self.bookmarks[_middle]
if item.datetime == v.datetime and item.page == v.page then if item.datetime == v.datetime and item.page == v.page then
table.remove(self.bookmarks, _middle) table.remove(self.bookmarks, _middle)
self.view.footer:onUpdateFooter(true) self.view.footer:onUpdateFooter(self.view.footer_visible)
return return
elseif self:isBookmarkInPageOrder(item, v) then elseif self:isBookmarkInPageOrder(item, v) then
_end = _middle - 1 _end = _middle - 1
@ -487,7 +487,7 @@ function ReaderBookmark:removeBookmark(item)
local v = self.bookmarks[i] local v = self.bookmarks[i]
if item.datetime == v.datetime and item.page == v.page then if item.datetime == v.datetime and item.page == v.page then
table.remove(self.bookmarks, i) table.remove(self.bookmarks, i)
self.view.footer:onUpdateFooter(true) self.view.footer:onUpdateFooter(self.view.footer_visible)
return return
end end
end end

@ -2166,7 +2166,8 @@ function ReaderFooter:refreshFooter(refresh, signal)
end end
function ReaderFooter:onResume() function ReaderFooter:onResume()
self:onUpdateFooter() -- Force a footer repaint on resume if it was visible
self:onUpdateFooter(self.view.footer_visible)
if self.settings.auto_refresh_time then if self.settings.auto_refresh_time then
self:setupAutoRefreshTime() self:setupAutoRefreshTime()
end end

@ -197,7 +197,7 @@ function SkimToWidget:init()
enabled = true, enabled = true,
width = self.button_width, width = self.button_width,
show_parent = self, show_parent = self,
vsync = not G_reader_settings:isTrue("refresh_on_chapter_boundaries"), vsync = true,
callback = function() callback = function()
local page = self.ui.toc:getNextChapter(self.curr_page) local page = self.ui.toc:getNextChapter(self.curr_page)
if page and page >=1 and page <= self.page_count then if page and page >=1 and page <= self.page_count then
@ -217,7 +217,7 @@ function SkimToWidget:init()
enabled = true, enabled = true,
width = self.button_width, width = self.button_width,
show_parent = self, show_parent = self,
vsync = not G_reader_settings:isTrue("refresh_on_chapter_boundaries"), vsync = true,
callback = function() callback = function()
local page = self.ui.toc:getPreviousChapter(self.curr_page) local page = self.ui.toc:getPreviousChapter(self.curr_page)
if page and page >=1 and page <= self.page_count then if page and page >=1 and page <= self.page_count then

@ -594,7 +594,17 @@ Registers a widget to be repainted and enqueues a refresh.
the second parameter (refreshtype) can either specify a refreshtype the second parameter (refreshtype) can either specify a refreshtype
(optionally in combination with a refreshregion - which is suggested) (optionally in combination with a refreshregion - which is suggested)
or a function that returns refreshtype AND refreshregion and is called or a function that returns refreshtype AND refreshregion and is called
after painting the widget. *after* painting the widget.
This is an interesting distinction, because a widget's geometry,
usually stored in a field named `dimen`, in only computed at painting time (e.g., during `paintTo`).
The TL;DR being: if you already know the region, you can pass everything by value directly,
(it'll make for slightly more readable debug logs),
but if the region will only be known after the widget has been painted, pass a function.
Note that, technically, it means that stuff passed by value will be enqueued earlier in the refresh stack.
In practice, since the stack of (both types of) refreshes is optimized into as few actual refresh ioctls as possible,
and that during the next `_repaint` tick (which is when `paintTo` for dirty widgets happens),
this shouldn't change much in the grand scheme of things, but it ought to be noted ;).
Here's a quick rundown of what each refreshtype should be used for: Here's a quick rundown of what each refreshtype should be used for:
full: high-fidelity flashing refresh (e.g., large images). full: high-fidelity flashing refresh (e.g., large images).
Highest quality, but highest latency. Highest quality, but highest latency.
@ -615,21 +625,52 @@ flashpartial: like partial, but flashing (and not counting towards flashing prom
Can be used when closing an UI element, to avoid ghosting. Can be used when closing an UI element, to avoid ghosting.
You can even drop the region in these cases, to ensure a fullscreen flash. You can even drop the region in these cases, to ensure a fullscreen flash.
NOTE: On REAGL devices, "flashpartial" will NOT actually flash (by design). NOTE: On REAGL devices, "flashpartial" will NOT actually flash (by design).
As such, even onClose, you might prefer "flashui" in some rare instances. As such, even onCloseWidget, you might prefer "flashui" in some rare instances.
NOTE: You'll notice a trend on UI elements that are usually shown *over* some kind of text NOTE: You'll notice a trend on UI elements that are usually shown *over* some kind of text
of using "ui" onShow & onUpdate, but "partial" onClose. of using "ui" onShow & onUpdate, but "partial" onCloseWidget.
This is by design: "partial" is what the reader uses, as it's tailor-made for pure text This is by design: "partial" is what the reader uses, as it's tailor-made for pure text
over a white background, so this ensures we resume the usual flow of the reader. over a white background, so this ensures we resume the usual flow of the reader.
The same dynamic is true for their flashing counterparts, in the rare instances we enforce flashes. The same dynamic is true for their flashing counterparts, in the rare instances we enforce flashes.
Any kind of "partial" refresh *will* count towards a flashing promotion after FULL_REFRESH_COUNT refreshes, Any kind of "partial" refresh *will* count towards a flashing promotion after FULL_REFRESH_COUNT refreshes,
so making sure your stuff only applies to the proper region is key to avoiding spurious large black flashes. so making sure your stuff only applies to the proper region is key to avoiding spurious large black flashes.
That said, depending on your use case, using "ui" onClose can be a perfectly valid decision, and will ensure That said, depending on your use case, using "ui" onCloseWidget can be a perfectly valid decision,
never seeing a flash because of that widget. and will ensure never seeing a flash because of that widget.
Remember that the FM uses "ui", so, if said widgets are shown over the FM,
prefer using "ui" or "flashui" onCloseWidget.
The final parameter (refreshdither) is an optional hint for devices with hardware dithering support that this repaint The final parameter (refreshdither) is an optional hint for devices with hardware dithering support that this repaint
could benefit from dithering (i.e., it contains an image). could benefit from dithering (i.e., it contains an image).
As far as the actual lifecycle of a widget goes, the rules are:
* What you `show`, you `close`.
* If you know the dimensions of the widget (or simply of the region you want to refresh), you can pass it directly:
* to show (as show calls setDirty),
* to close (as close will also call setDirty on the remaining dirty and visible widgets,
and will also enqueue a refresh based on that if there are dirty widgets).
* Otherwise, you can use, respectively, the `Show` & `CloseWidget` handlers for that via `setDirty` calls.
This can also be useful if *child* widgets have specific needs (e.g., flashing, dithering) that they want to inject in the refresh queue.
* Remember that events propagate children first (in array order, starting at the top), and that if *any* event handler returns true,
the propagation of that specific event for this widget tree stops *immediately*.
(This generally means that, unless you know what you're doing (e.g., a widget that will *always* be used as a parent),
you generally *don't* want to return true in `Show` or `CloseWidget` handlers).
* If any widget requires freeing non-Lua resources (e.g., FFI/C), having a `free` method called from its `CloseWidget` handler is ideal:
this'll ensure that *any* widget including it will be sure that resources are freed when it (or its parent) are closed.
* Note that there *is* a `Close` event, but it is *only* broadcast (e.g., sent to every widget in the window stack;
the same rules about propagation apply, but only per *window-level widget*) at poweroff/reboot, so,
refrain from implementing custom onClose methods if that's not their intended purpose ;).
On the subject of widgets and child widgets,
you might have noticed an unspoken convention across the codebase of widgets having a field called `show_parent`.
Since handling this is entirely at the programmer's behest, here's how we usually use it:
Basically, we cascade a field named `show_parent` to every child widget that matter
(e.g., those that serve an UI purpose, as opposed to, say, a container).
This ensures that every subwidget can reference its actual parent
(ideally, all the way to the window-level widget it belongs to, i.e., the one that was passed to UIManager:show, hence the name ;)),
to, among other things, flag the right widget as setDirty (c.f., those pesky debug warnings when that's done wrong ;p) when they want to request a repaint.
This is why you often see stuff doing, when instantiating a new widget, FancyWidget:new{ show_parent = self.show_parent or self };
meaning, if I'm already a subwidget, cascade my parent, otherwise, it means I'm a window-level widget, so cascade myself as that widget's parent ;).
@usage @usage
UIManager:setDirty(self.widget, "partial") UIManager:setDirty(self.widget, "partial")
@ -1126,20 +1167,6 @@ function UIManager:_refresh(mode, region, dither)
end end
-- A couple helper functions to compute aligned values...
-- c.f., <linux/kernel.h> & ffi/framebuffer_linux.lua
local function ALIGN_DOWN(x, a)
-- x & ~(a-1)
local mask = a - 1
return bit.band(x, bit.bnot(mask))
end
local function ALIGN_UP(x, a)
-- (x + (a-1)) & ~(a-1)
local mask = a - 1
return bit.band(x + mask, bit.bnot(mask))
end
--- Repaints dirty widgets. --- Repaints dirty widgets.
function UIManager:_repaint() function UIManager:_repaint()
-- flag in which we will record if we did any repaints at all -- flag in which we will record if we did any repaints at all
@ -1220,31 +1247,6 @@ function UIManager:_repaint()
refresh.dither = nil refresh.dither = nil
end end
dbg:v("triggering refresh", refresh) dbg:v("triggering refresh", refresh)
-- NOTE: If we're requesting hardware dithering on a partial update, make sure the rectangle is using
-- coordinates aligned to the previous multiple of 8, and dimensions aligned to the next multiple of 8.
-- Otherwise, some unlucky coordinates will play badly with the PxP's own alignment constraints,
-- leading to a refresh where content appears to have moved a few pixels to the side...
-- (Sidebar: this is probably a kernel issue, the EPDC driver is responsible for the alignment fixup,
-- c.f., epdc_process_update @ drivers/video/fbdev/mxc/mxc_epdc_v2_fb.c on a Kobo Mk. 7 kernel...).
if refresh.dither then
-- NOTE: Make sure the coordinates are positive, first! Otherwise, we'd gladly align further down below 0,
-- which would skew the rectangle's position/dimension after checkBounds...
local x_fixup = 0
if refresh.region.x > 0 then
local x_orig = refresh.region.x
refresh.region.x = ALIGN_DOWN(x_orig, 8)
x_fixup = x_orig - refresh.region.x
end
local y_fixup = 0
if refresh.region.y > 0 then
local y_orig = refresh.region.y
refresh.region.y = ALIGN_DOWN(y_orig, 8)
y_fixup = y_orig - refresh.region.y
end
-- And also make sure we won't be inadvertently cropping our rectangle in case of severe alignment fixups...
refresh.region.w = ALIGN_UP(refresh.region.w + (x_fixup * 2), 8)
refresh.region.h = ALIGN_UP(refresh.region.h + (y_fixup * 2), 8)
end
-- Remember the refresh region -- Remember the refresh region
self._last_refresh_region = refresh.region self._last_refresh_region = refresh.region
Screen[refresh_methods[refresh.mode]](Screen, Screen[refresh_methods[refresh.mode]](Screen,

@ -246,7 +246,7 @@ function BookStatusWidget:generateRateGroup(width, height, rating)
end end
function BookStatusWidget:setStar(num) function BookStatusWidget:setStar(num)
--clear previous data -- clear previous data
self.stars_container:clear() self.stars_container:clear()
local stars_group = HorizontalGroup:new{ align = "center" } local stars_group = HorizontalGroup:new{ align = "center" }

@ -29,6 +29,7 @@ local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager") local UIManager = require("ui/uimanager")
local _ = require("gettext") local _ = require("gettext")
local Screen = Device.screen local Screen = Device.screen
local logger = require("logger")
local Button = InputContainer:new{ local Button = InputContainer:new{
text = nil, -- mandatory text = nil, -- mandatory
@ -142,15 +143,25 @@ function Button:init()
end end
function Button:setText(text, width) function Button:setText(text, width)
self.text = text if text ~= self.text then
self.width = width -- Don't trash the frame if we're already a text button, and we're keeping the geometry intact
self:init() if self.text and width and width == self.width then
self.text = text
self.label_widget:setText(text)
else
self.text = text
self.width = width
self:init()
end
end
end end
function Button:setIcon(icon) function Button:setIcon(icon)
self.icon = icon if icon ~= self.icon then
self.width = nil self.icon = icon
self:init() self.width = nil
self:init()
end
end end
function Button:onFocus() function Button:onFocus()
@ -227,6 +238,9 @@ function Button:onTapSelectButton()
if G_reader_settings:isFalse("flash_ui") then if G_reader_settings:isFalse("flash_ui") then
self.callback() self.callback()
else else
-- We need to keep track of whether we actually flipped the frame's invert flag ourselves,
-- to handle the rounded corners shenanigan in the post-callback invert check...
local inverted = false
-- NOTE: self[1] -> self.frame, if you're confused about what this does vs. onFocus/onUnfocus ;). -- NOTE: self[1] -> self.frame, if you're confused about what this does vs. onFocus/onUnfocus ;).
if self.text then if self.text then
-- We only want the button's *highlight* to have rounded corners (otherwise they're redundant, same color as the bg). -- We only want the button's *highlight* to have rounded corners (otherwise they're redundant, same color as the bg).
@ -239,15 +253,17 @@ function Button:onTapSelectButton()
self.label_widget.fgcolor = self.label_widget.fgcolor:invert() self.label_widget.fgcolor = self.label_widget.fgcolor:invert()
-- We do *NOT* set the invert flag, because it just adds an invertRect step at the end of the paintTo process, -- We do *NOT* set the invert flag, because it just adds an invertRect step at the end of the paintTo process,
-- and we've already taken care of inversion in a way that won't mangle the rounded corners. -- and we've already taken care of inversion in a way that won't mangle the rounded corners.
-- The "inverted" local flag allows us to fudge the "did callback invert the frame?" check for these buttons,
-- otherwise setting the invert flag here breaks the highlight for vsync buttons...
else else
self[1].invert = true self[1].invert = true
inverted = true
end end
UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
-- But do make sure the invert flag is set in both cases, mainly for the early return check below
self[1].invert = true
else else
self[1].invert = true self[1].invert = true
inverted = true
UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y)
end end
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
@ -268,7 +284,7 @@ function Button:onTapSelectButton()
-- because that would have a chance to noticeably delay it until the unhighlight. -- because that would have a chance to noticeably delay it until the unhighlight.
end end
if not self[1] or not self[1].invert or not self[1].dimen then if not self[1] or (inverted and not self[1].invert) or not self[1].dimen then
-- If the frame widget no longer exists (destroyed, re-init'ed by setText(), or is no longer inverted: we have nothing to invert back -- If the frame widget no longer exists (destroyed, re-init'ed by setText(), or is no longer inverted: we have nothing to invert back
-- NOTE: This cannot catch orphaned Button instances, c.f., the isSubwidgetShown(self) check below for that. -- NOTE: This cannot catch orphaned Button instances, c.f., the isSubwidgetShown(self) check below for that.
return true return true
@ -288,13 +304,15 @@ function Button:onTapSelectButton()
local top_widget = UIManager:getTopWidget() local top_widget = UIManager:getTopWidget()
if top_widget == self.show_parent or UIManager:isSubwidgetShown(self.show_parent) then if top_widget == self.show_parent or UIManager:isSubwidgetShown(self.show_parent) then
-- If the button can no longer be found inside a shown widget, abort early -- If the button can no longer be found inside a shown widget, abort early
-- (this allows us to catch widgets that instanciate *new* Buttons on every update... (e.g., ButtonTable) :() -- (this allows us to catch widgets that instanciate *new* Buttons on every update... (e.g., some ButtonTable users) :()
if not UIManager:isSubwidgetShown(self) then if not UIManager:isSubwidgetShown(self) then
return true return true
end end
-- If our parent is no longer the toplevel widget, toplevel is now a true modal, and our highlight would clash with that modal's region, -- If our parent is no longer the toplevel widget, toplevel is now a true modal, and our highlight would clash with that modal's region,
-- we have no other choice than repainting the full stack... -- we have no other choice than repainting the full stack...
-- This branch will mainly be taken by stuff that pops up the virtual keyboard (e.g., TextEditor), where said keyboard will always be top-level,
-- hence the exception, because we want to catch modals *over* all that ;).
if top_widget ~= self.show_parent and top_widget ~= "VirtualKeyboard" and top_widget.modal and self[1].dimen:intersectWith(UIManager:getPreviousRefreshRegion()) then if top_widget ~= self.show_parent and top_widget ~= "VirtualKeyboard" and top_widget.modal and self[1].dimen:intersectWith(UIManager:getPreviousRefreshRegion()) then
-- Much like in TouchMenu, the fact that the two intersect means we have no choice but to repaint the full stack to avoid half-painted widgets... -- Much like in TouchMenu, the fact that the two intersect means we have no choice but to repaint the full stack to avoid half-painted widgets...
UIManager:waitForVSync() UIManager:waitForVSync()
@ -319,8 +337,7 @@ function Button:onTapSelectButton()
end) end)
--UIManager:forceRePaint() -- Ensures the unhighlight happens now, instead of potentially waiting and having it batched with something else. --UIManager:forceRePaint() -- Ensures the unhighlight happens now, instead of potentially waiting and having it batched with something else.
else else
-- This branch will mainly be taken by stuff that pops up the virtual keyboard (e.g., TextEditor), where said keyboard will always be top-level, -- Callback closed our parent, we're done
-- (hence the exception in the check above).
return true return true
end end
end end
@ -334,6 +351,22 @@ function Button:onTapSelectButton()
end end
end end
-- Allow repainting and refreshing *a* specific Button, instead of the full screen/parent stack
function Button:refresh()
-- We can only be called on a Button that's already been painted once, which allows us to know where we're positioned,
-- thanks to the frame's geometry.
-- e.g., right after a setText or setIcon is a no-go, as those kill the frame.
-- (Although, setText, if called with the current width, will conserve the frame).
if not self[1].dimen then
logger.dbg("Button:", self, "attempted a repaint in an unpainted frame!")
return
end
UIManager:widgetRepaint(self[1], self[1].dimen.x, self.dimen.y)
UIManager:setDirty(nil, function()
return self.enabled and "fast" or "ui", self[1].dimen
end)
end
function Button:onHoldSelectButton() function Button:onHoldSelectButton()
if self.hold_callback and (self.enabled or self.allow_hold_when_disabled) then if self.hold_callback and (self.enabled or self.allow_hold_when_disabled) then
self.hold_callback() self.hold_callback()

@ -68,7 +68,7 @@ end
function ButtonDialog:onCloseWidget() function ButtonDialog:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self[1][1].dimen return "flashui", self[1][1].dimen
end) end)
end end

@ -96,7 +96,7 @@ end
function ButtonDialogTitle:onCloseWidget() function ButtonDialogTitle:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self[1][1].dimen return "ui", self[1][1].dimen
end) end)
end end

@ -56,8 +56,8 @@ function ButtonTable:init()
callback = btn_entry.callback, callback = btn_entry.callback,
hold_callback = btn_entry.hold_callback, hold_callback = btn_entry.hold_callback,
vsync = btn_entry.vsync, vsync = btn_entry.vsync,
width = (self.width - sizer_space)/column_cnt, width = math.ceil((self.width - sizer_space)/column_cnt),
max_width = (self.width - sizer_space)/column_cnt - 2*self.sep_width - 2*self.padding, max_width = math.ceil((self.width - sizer_space)/column_cnt - 2*self.sep_width - 2*self.padding),
bordersize = 0, bordersize = 0,
margin = 0, margin = 0,
padding = Size.padding.buttontable, -- a bit taller than standalone buttons, for easier tap padding = Size.padding.buttontable, -- a bit taller than standalone buttons, for easier tap
@ -88,7 +88,7 @@ function ButtonTable:init()
self:addHorizontalSep(true, true, true) self:addHorizontalSep(true, true, true)
end end
if column_cnt > 0 then if column_cnt > 0 then
--Only add line that are not separator to the focusmanager -- Only add lines that are not separator to the focusmanager
table.insert(self.buttons_layout, buttons_layout_line) table.insert(self.buttons_layout, buttons_layout_line)
end end
end -- end for each button line end -- end for each button line

@ -868,6 +868,9 @@ function ConfigDialog:update()
panel_index = self.panel_index, panel_index = self.panel_index,
} }
end end
if self.config_panel then
self.config_panel:free()
end
self.config_panel = ConfigPanel:new{ self.config_panel = ConfigPanel:new{
index = self.panel_index, index = self.panel_index,
config_dialog = self, config_dialog = self,

@ -192,7 +192,7 @@ end
function ConfirmBox:onCloseWidget() function ConfirmBox:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self[1][1].dimen return "ui", self[1][1].dimen
end) end)
end end

@ -53,7 +53,14 @@ end
--[[-- --[[--
Deletes all child widgets. Deletes all child widgets.
]] ]]
function WidgetContainer:clear() function WidgetContainer:clear(skip_free)
-- HorizontalGroup & VerticalGroup call us after already having called free,
-- so allow skipping this one ;).
if not skip_free then
-- Make sure we free 'em before orphaning them...
self:free()
end
while table.remove(self) do end while table.remove(self) do end
end end
@ -113,7 +120,10 @@ end
function WidgetContainer:free() function WidgetContainer:free()
for _, widget in ipairs(self) do for _, widget in ipairs(self) do
if widget.free then widget:free() end if widget.free then
--print("WidgetContainer: Calling free for widget", debug.getinfo(widget.free, "S").short_src, widget, "from", debug.getinfo(self.free, "S").short_src, self)
widget:free()
end
end end
end end

@ -56,6 +56,8 @@ function DateWidget:init()
}, },
} }
end end
-- Actually the widget layout
self:update() self:update()
end end
@ -206,7 +208,7 @@ end
function DateWidget:onCloseWidget() function DateWidget:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self.date_frame.dimen return "ui", self.date_frame.dimen
end) end)
return true return true
end end

@ -161,61 +161,9 @@ function DictQuickLookup:init()
-- We no longer support setting a default dict with Tap on title. -- We no longer support setting a default dict with Tap on title.
-- self:changeToDefaultDict() -- self:changeToDefaultDict()
-- Now, dictionaries can be ordered (although not yet per-book), so trust the order set -- Now, dictionaries can be ordered (although not yet per-book), so trust the order set
self:changeDictionary(1) -- this will call self:update() self:changeDictionary(1, true) -- don't call update
end
-- Whether currently DictQuickLookup is working without a document.
function DictQuickLookup:isDocless()
return self.ui == nil or self.ui.highlight == nil
end
function DictQuickLookup:getHtmlDictionaryCss()
-- Using Noto Sans because Nimbus doesn't contain the IPA symbols.
-- 'line-height: 1.3' to have it similar to textboxwidget,
-- and follow user's choice on justification
local css_justify = G_reader_settings:nilOrTrue("dict_justify") and "text-align: justify;" or ""
local css = [[
@page {
margin: 0;
font-family: 'Noto Sans';
}
body {
margin: 0;
line-height: 1.3;
]]..css_justify..[[
}
blockquote, dd {
margin: 0 1em;
}
]]
-- MuPDF doesn't currently scale CSS pixels, so we have to use a font-size based measurement.
-- Unfortunately MuPDF doesn't properly support `rem` either, which it bases on a hard-coded
-- value of `16px`, so we have to go with `em` (or `%`).
--
-- These `em`-based margins can vary slightly, but it's the best available compromise.
--
-- We also keep left and right margin the same so it'll display as expected in RTL.
-- Because MuPDF doesn't currently support `margin-start`, this results in a slightly
-- unconventional but hopefully barely noticeable right margin for <dd>.
if self.css then
return css .. self.css
end
return css
end
function DictQuickLookup:update()
local orig_dimen = self.dict_frame and self.dict_frame.dimen or Geom:new{}
local orig_moved_offset = self.movable and self.movable:getMovedOffset()
-- Free our previous widget and subwidgets' resources (especially
-- definitions' TextBoxWidget bb, HtmlBoxWidget bb and MuPDF instance,
-- and scheduled image_update_action)
if self[1] then
self[1]:free()
end
-- And here comes the initial widget layout...
if self.is_wiki then if self.is_wiki then
-- Keep a copy of self.wiki_languages for use -- Keep a copy of self.wiki_languages for use
-- by DictQuickLookup:resyncWikiLanguages() -- by DictQuickLookup:resyncWikiLanguages()
@ -242,7 +190,7 @@ function DictQuickLookup:update()
local title_padding = Size.padding.default local title_padding = Size.padding.default
local title_width = inner_width - 2*title_padding -2*title_margin local title_width = inner_width - 2*title_padding -2*title_margin
local close_button = CloseButton:new{ window = self, padding_top = title_margin, } local close_button = CloseButton:new{ window = self, padding_top = title_margin, }
local dict_title_text = TextWidget:new{ self.dict_title_text = TextWidget:new{
text = self.displaydictname, text = self.displaydictname,
face = Font:getFace("x_smalltfont"), face = Font:getFace("x_smalltfont"),
bold = true, bold = true,
@ -250,15 +198,15 @@ function DictQuickLookup:update()
-- Allow text to eat on the CloseButton padding_left (which -- Allow text to eat on the CloseButton padding_left (which
-- is quite large to ensure a bigger tap area) -- is quite large to ensure a bigger tap area)
} }
local dict_title_widget = dict_title_text local dict_title_widget = self.dict_title_text
if self.is_wiki then if self.is_wiki then
-- Visual hint: title left aligned for dict, but centered for Wikipedia -- Visual hint: title left aligned for dict, but centered for Wikipedia
dict_title_widget = CenterContainer:new{ dict_title_widget = CenterContainer:new{
dimen = Geom:new{ dimen = Geom:new{
w = title_width, w = title_width,
h = dict_title_text:getSize().h, h = self.dict_title_text:getSize().h,
}, },
dict_title_text, self.dict_title_text,
} }
end end
self.dict_title = FrameContainer:new{ self.dict_title = FrameContainer:new{
@ -328,12 +276,19 @@ function DictQuickLookup:update()
self:lookupInputWord(self.lookupword) self:lookupInputWord(self.lookupword)
end, end,
overlap_align = "right", overlap_align = "right",
show_parent = self,
} }
local lookup_edit_button_w = lookup_edit_button:getSize().w local lookup_edit_button_w = lookup_edit_button:getSize().w
-- Nb of results (if set) -- Nb of results (if set)
local lookup_word_nb local lookup_word_nb
local lookup_word_nb_w = 0 local lookup_word_nb_w = 0
if self.displaynb then if self.displaynb then
self.displaynb_text = TextWidget:new{
text = self.displaynb,
face = Font:getFace("cfont", word_font_size),
padding = 0, -- smaller height for better aligmnent with icon
}
lookup_word_nb = FrameContainer:new{ lookup_word_nb = FrameContainer:new{
margin = 0, margin = 0,
bordersize = 0, bordersize = 0,
@ -341,16 +296,12 @@ function DictQuickLookup:update()
padding_left = Size.padding.small, padding_left = Size.padding.small,
padding_right = lookup_edit_button_w + Size.padding.default, padding_right = lookup_edit_button_w + Size.padding.default,
overlap_align = "right", overlap_align = "right",
TextWidget:new{ self.displaynb_text,
text = self.displaynb,
face = Font:getFace("cfont", word_font_size),
padding = 0, -- smaller height for better aligmnent with icon
}
} }
lookup_word_nb_w = lookup_word_nb:getSize().w lookup_word_nb_w = lookup_word_nb:getSize().w
end end
-- Lookup word -- Lookup word
local lookup_word_text = TextWidget:new{ self.lookup_word_text = TextWidget:new{
text = self.displayword, text = self.displayword,
face = Font:getFace(word_font_face, word_font_size), face = Font:getFace(word_font_face, word_font_size),
bold = true, bold = true,
@ -363,7 +314,7 @@ function DictQuickLookup:update()
w = content_width, w = content_width,
h = lookup_height, h = lookup_height,
}, },
lookup_word_text, self.lookup_word_text,
lookup_edit_button, lookup_edit_button,
lookup_word_nb, -- last, as this might be nil lookup_word_nb, -- last, as this might be nil
} }
@ -375,6 +326,7 @@ function DictQuickLookup:update()
buttons = { buttons = {
{ {
{ {
id = "save",
text = _("Save as EPUB"), text = _("Save as EPUB"),
callback = function() callback = function()
local InfoMessage = require("ui/widget/infomessage") local InfoMessage = require("ui/widget/infomessage")
@ -443,6 +395,7 @@ function DictQuickLookup:update()
end, end,
}, },
{ {
id = "close",
text = _("Close"), text = _("Close"),
callback = function() callback = function()
UIManager:close(self) UIManager:close(self)
@ -459,6 +412,7 @@ function DictQuickLookup:update()
buttons = { buttons = {
{ {
{ {
id = "prev_dict",
text = prev_dict_text, text = prev_dict_text,
vsync = true, vsync = true,
enabled = self:isPrevDictAvaiable(), enabled = self:isPrevDictAvaiable(),
@ -470,6 +424,7 @@ function DictQuickLookup:update()
end, end,
}, },
{ {
id = "highlight",
text = self:getHighlightText(), text = self:getHighlightText(),
enabled = self.highlight ~= nil, enabled = self.highlight ~= nil,
callback = function() callback = function()
@ -478,10 +433,16 @@ function DictQuickLookup:update()
else else
self.ui:handleEvent(Event:new("Unhighlight")) self.ui:handleEvent(Event:new("Unhighlight"))
end end
self:update() -- Just update, repaint and refresh *this* button
local this = self.button_table:getButtonById("highlight")
if not this then return end
this:enableDisable(self.highlight ~= nil)
this:setText(self:getHighlightText(), this.width)
this:refresh()
end, end,
}, },
{ {
id = "next_dict",
text = next_dict_text, text = next_dict_text,
vsync = true, vsync = true,
enabled = self:isNextDictAvaiable(), enabled = self:isNextDictAvaiable(),
@ -495,6 +456,7 @@ function DictQuickLookup:update()
}, },
{ {
{ {
id = "wikipedia",
-- if dictionary result, do the same search on wikipedia -- if dictionary result, do the same search on wikipedia
-- if already wiki, get the full page for the current result -- if already wiki, get the full page for the current result
text_func = function() text_func = function()
@ -513,6 +475,7 @@ function DictQuickLookup:update()
}, },
-- Rotate thru available wikipedia languages, or Search in book if dict window -- Rotate thru available wikipedia languages, or Search in book if dict window
{ {
id = "search",
-- if more than one language, enable it and display "current lang > next lang" -- if more than one language, enable it and display "current lang > next lang"
-- otherwise, just display current lang -- otherwise, just display current lang
text = self.is_wiki text = self.is_wiki
@ -532,6 +495,7 @@ function DictQuickLookup:update()
end, end,
}, },
{ {
id = "close",
text = _("Close"), text = _("Close"),
callback = function() callback = function()
-- UIManager:close(self) -- UIManager:close(self)
@ -545,6 +509,7 @@ function DictQuickLookup:update()
-- add a new first row with a single button to follow this link. -- add a new first row with a single button to follow this link.
table.insert(buttons, 1, { table.insert(buttons, 1, {
{ {
id = "link",
text = _("Follow Link"), text = _("Follow Link"),
callback = function() callback = function()
local link = self.selected_link.link or self.selected_link local link = self.selected_link.link or self.selected_link
@ -560,7 +525,7 @@ function DictQuickLookup:update()
-- reach out from the content to the borders a bit more -- reach out from the content to the borders a bit more
local buttons_padding = Size.padding.default local buttons_padding = Size.padding.default
local buttons_width = inner_width - 2*buttons_padding local buttons_width = inner_width - 2*buttons_padding
local button_table = ButtonTable:new{ self.button_table = ButtonTable:new{
width = buttons_width, width = buttons_width,
button_font_face = "cfont", button_font_face = "cfont",
button_font_size = 20, button_font_size = 20,
@ -595,7 +560,7 @@ function DictQuickLookup:update()
+ lookup_word:getSize().h + lookup_word:getSize().h
+ word_to_definition_span:getSize().h + word_to_definition_span:getSize().h
+ definition_to_bottom_span:getSize().h + definition_to_bottom_span:getSize().h
+ button_table:getSize().h + self.button_table:getSize().h
-- To properly adjust the definition to the height of text, we need -- To properly adjust the definition to the height of text, we need
-- the line height a ScrollTextWidget will use for the current font -- the line height a ScrollTextWidget will use for the current font
@ -658,9 +623,8 @@ function DictQuickLookup:update()
end end
end end
local text_widget
if self.is_html then if self.is_html then
text_widget = ScrollHtmlWidget:new{ self.text_widget = ScrollHtmlWidget:new{
html_body = self.definition, html_body = self.definition,
css = self:getHtmlDictionaryCss(), css = self:getHtmlDictionaryCss(),
default_font_size = Screen:scaleBySize(self.dict_font_size), default_font_size = Screen:scaleBySize(self.dict_font_size),
@ -672,7 +636,7 @@ function DictQuickLookup:update()
end, end,
} }
else else
text_widget = ScrollTextWidget:new{ self.text_widget = ScrollTextWidget:new{
text = self.definition, text = self.definition,
face = self.content_face, face = self.content_face,
width = content_width, width = content_width,
@ -694,7 +658,7 @@ function DictQuickLookup:update()
padding_right = content_padding_h, padding_right = content_padding_h,
margin = 0, margin = 0,
bordersize = 0, bordersize = 0,
text_widget, self.text_widget,
} }
self.dict_frame = FrameContainer:new{ self.dict_frame = FrameContainer:new{
@ -730,9 +694,9 @@ function DictQuickLookup:update()
CenterContainer:new{ CenterContainer:new{
dimen = Geom:new{ dimen = Geom:new{
w = inner_width, w = inner_width,
h = button_table:getSize().h, h = self.button_table:getSize().h,
}, },
button_table, self.button_table,
} }
} }
} }
@ -751,7 +715,6 @@ function DictQuickLookup:update()
}, },
self.dict_frame, self.dict_frame,
} }
self.movable:setMovedOffset(orig_moved_offset)
self[1] = WidgetContainer:new{ self[1] = WidgetContainer:new{
align = self.align, align = self.align,
@ -759,9 +722,93 @@ function DictQuickLookup:update()
self.movable, self.movable,
} }
UIManager:setDirty(self, function() UIManager:setDirty(self, function()
local update_region = self.dict_frame and self.dict_frame.dimen and self.dict_frame.dimen:combine(orig_dimen) or orig_dimen return "partial", self.dict_frame.dimen
logger.dbg("update dict region", update_region) end)
return "partial", update_region end
-- Whether currently DictQuickLookup is working without a document.
function DictQuickLookup:isDocless()
return self.ui == nil or self.ui.highlight == nil
end
function DictQuickLookup:getHtmlDictionaryCss()
-- Using Noto Sans because Nimbus doesn't contain the IPA symbols.
-- 'line-height: 1.3' to have it similar to textboxwidget,
-- and follow user's choice on justification
local css_justify = G_reader_settings:nilOrTrue("dict_justify") and "text-align: justify;" or ""
local css = [[
@page {
margin: 0;
font-family: 'Noto Sans';
}
body {
margin: 0;
line-height: 1.3;
]]..css_justify..[[
}
blockquote, dd {
margin: 0 1em;
}
]]
-- MuPDF doesn't currently scale CSS pixels, so we have to use a font-size based measurement.
-- Unfortunately MuPDF doesn't properly support `rem` either, which it bases on a hard-coded
-- value of `16px`, so we have to go with `em` (or `%`).
--
-- These `em`-based margins can vary slightly, but it's the best available compromise.
--
-- We also keep left and right margin the same so it'll display as expected in RTL.
-- Because MuPDF doesn't currently support `margin-start`, this results in a slightly
-- unconventional but hopefully barely noticeable right margin for <dd>.
if self.css then
return css .. self.css
end
return css
end
function DictQuickLookup:update()
-- self[1] is a WidgetContainer, its free method will call free on each of its child widget with a free method.
-- Here, that's the definitions' TextBoxWidget & HtmlBoxWidget,
-- to release their bb, MuPDF instance, and scheduled image_update_action.
self[1]:free()
-- Update TextWidgets
self.dict_title_text:setText(self.displaydictname)
if self.displaynb then
self.displaynb_text:setText(self.displaynb)
end
self.lookup_word_text:setText(self.displayword)
-- Update Buttons
if not self.is_wiki_fullpage then
local prev_dict_btn = self.button_table:getButtonById("prev_dict")
if prev_dict_btn then
prev_dict_btn:enableDisable(self:isPrevDictAvaiable())
end
local next_dict_btn = self.button_table:getButtonById("next_dict")
if next_dict_btn then
next_dict_btn:enableDisable(self:isNextDictAvaiable())
end
end
-- Update main text widgets
if self.is_html then
self.text_widget.htmlbox_widget:setContent(self.definition, self:getHtmlDictionaryCss(), Screen:scaleBySize(self.dict_font_size))
else
self.text_widget.text_widget.text = self.definition
-- NOTE: The recursive free via our WidgetContainer (self[1]) above already free'd us ;)
self.text_widget.text_widget:init()
end
-- Reset alpha to avoid stacking transparency on top of the previous content.
-- NOTE: This doesn't take care of the Scroll*Widget, which will preserve alpha on scroll,
-- leading to increasingly opaque and muddy text as half-tarnsparent stuff gets stacked on top of each other...
self.movable.alpha = nil
UIManager:setDirty(self, function()
return "partial", self.dict_frame.dimen
end) end)
end end
@ -786,12 +833,10 @@ function DictQuickLookup:getInitialVisibleArea()
end end
function DictQuickLookup:onCloseWidget() function DictQuickLookup:onCloseWidget()
-- Free our widget and subwidgets' resources (especially -- Our TextBoxWidget/HtmlBoxWidget/TextWidget/ImageWidget are proper child widgets,
-- definitions' TextBoxWidget bb, HtmlBoxWidget bb and MuPDF instance, -- so this event will propagate to 'em, and they'll free their resources.
-- and scheduled image_update_action)
if self[1] then -- What's left is stuff that isn't directly in our widget tree...
self[1]:free()
end
if self.images_cleanup_needed then if self.images_cleanup_needed then
logger.dbg("freeing lookup results images blitbuffers") logger.dbg("freeing lookup results images blitbuffers")
for _, r in ipairs(self.results) do for _, r in ipairs(self.results) do
@ -869,7 +914,7 @@ function DictQuickLookup:changeToLastDict()
end end
end end
function DictQuickLookup:changeDictionary(index) function DictQuickLookup:changeDictionary(index, skip_update)
if not self.results[index] then return end if not self.results[index] then return end
self.dict_index = index self.dict_index = index
self.dictionary = self.results[index].dict self.dictionary = self.results[index].dict
@ -919,7 +964,10 @@ function DictQuickLookup:changeDictionary(index)
end end
end end
self:update() -- Don't call update when called from init
if not skip_update then
self:update()
end
end end
--[[ No longer used --[[ No longer used

@ -77,14 +77,15 @@ function DoubleSpinWidget:init()
}, },
} }
end end
-- Actually the widget layout
self:update() self:update()
end end
function DoubleSpinWidget:update() function DoubleSpinWidget:update()
-- This picker_update_callback will be redefined later. It is needed -- This picker_update_callback will be redefined later.
-- so we can have our MovableContainer repainted on NumberPickerWidgets -- It's a hack to restore transparency after a Button unhighlight in NumberPicker,
-- update. It is needed if we have enabled transparency on MovableContainer, -- in case the MovableContainer was actually made transparent.
-- otherwise the NumberPicker area gets opaque on update.
local picker_update_callback = function() end local picker_update_callback = function() end
local left_widget = NumberPickerWidget:new{ local left_widget = NumberPickerWidget:new{
show_parent = self, show_parent = self,
@ -290,9 +291,25 @@ function DoubleSpinWidget:update()
return "ui", self.widget_frame.dimen return "ui", self.widget_frame.dimen
end) end)
picker_update_callback = function() picker_update_callback = function()
UIManager:setDirty("all", function() -- If we're actually transparent, force an alpha-aware repaint.
return "ui", self.movable.dimen if self.movable.alpha then
end) if G_reader_settings:nilOrTrue("flash_ui") then
-- It's delayed to the next tick to actually catch a Button unhighlight.
UIManager:nextTick(function()
UIManager:setDirty("all", function()
return "ui", self.movable.dimen
end)
end)
else
-- This should only really be necessary for the up/down buttons here,
-- because they repaint the center value button & text, unlike said button,
-- which just pops up the VK.
-- On the upside, we shouldn't need to delay anything without flash_ui ;).
UIManager:setDirty("all", function()
return "ui", self.movable.dimen
end)
end
end
-- If we'd like to have the values auto-applied, uncomment this: -- If we'd like to have the values auto-applied, uncomment this:
-- self.callback(left_widget:getValue(), right_widget:getValue()) -- self.callback(left_widget:getValue(), right_widget:getValue())
end end
@ -305,7 +322,7 @@ end
function DoubleSpinWidget:onCloseWidget() function DoubleSpinWidget:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self.widget_frame.dimen return "ui", self.widget_frame.dimen
end) end)
return true return true
end end

@ -28,6 +28,7 @@ By default, it's `"on"..Event.name`.
]] ]]
function EventListener:handleEvent(event) function EventListener:handleEvent(event)
if self[event.handler] then if self[event.handler] then
--print("EventListener:handleEvent:", event.handler, "handled by", debug.getinfo(self[event.handler], "S").short_src, self)
return self[event.handler](self, unpack(event.args)) return self[event.handler](self, unpack(event.args))
end end
end end

@ -581,7 +581,7 @@ end
function FrontLightWidget:onCloseWidget() function FrontLightWidget:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "flashpartial", self.light_frame.dimen return "flashui", self.light_frame.dimen
end) end)
return true return true
end end

@ -65,7 +65,8 @@ end
function HorizontalGroup:clear() function HorizontalGroup:clear()
self:free() self:free()
WidgetContainer.clear(self) -- Skip WidgetContainer:clear's free call, we just did that in our own free ;)
WidgetContainer.clear(self, true)
end end
function HorizontalGroup:resetLayout() function HorizontalGroup:resetLayout()
@ -74,6 +75,7 @@ function HorizontalGroup:resetLayout()
end end
function HorizontalGroup:free() function HorizontalGroup:free()
--print("HorizontalGroup:free on", self)
self:resetLayout() self:resetLayout()
WidgetContainer.free(self) WidgetContainer.free(self)
end end

@ -130,6 +130,7 @@ end
-- (ie: in some other widget's update()), to not leak memory with -- (ie: in some other widget's update()), to not leak memory with
-- BlitBuffer zombies -- BlitBuffer zombies
function HtmlBoxWidget:free() function HtmlBoxWidget:free()
--print("HtmlBoxWidget:free on", self)
self:freeBb() self:freeBb()
if self.document then if self.document then

@ -108,15 +108,38 @@ function IconButton:onTapIconButton()
UIManager:forceRePaint() UIManager:forceRePaint()
--UIManager:waitForVSync() --UIManager:waitForVSync()
-- If the callback closed our parent (which may not always be the top-level widget, or even *a* window-level widget; e.g., the Home/+ buttons in the FM), we're done self.image.invert = false
if UIManager:getTopWidget() == self.show_parent or UIManager:isSubwidgetShown(self.show_parent) then -- If the callback closed our parent (which may not always be the top-level widget, or even *a* window-level widget), we're done
self.image.invert = false local top_widget = UIManager:getTopWidget()
if top_widget == self.show_parent or UIManager:isSubwidgetShown(self.show_parent) then
-- If the callback popped up the VK, it prevents us from finessing this any further, so repaint the whole stack
if top_widget == "VirtualKeyboard" then
UIManager:waitForVSync()
UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen
end)
return true
end
-- If the callback popped up a modal above us, repaint the whole stack
if top_widget ~= self.show_parent and top_widget.modal and self.dimen:intersectWith(UIManager:getPreviousRefreshRegion()) then
UIManager:waitForVSync()
UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen
end)
return true
end
-- Otherwise, we can unhighlight it safely
UIManager:widgetInvert(self.image, self.dimen.x + self.padding_left, self.dimen.y + self.padding_top) UIManager:widgetInvert(self.image, self.dimen.x + self.padding_left, self.dimen.y + self.padding_top)
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "fast", self.dimen return "fast", self.dimen
end) end)
--UIManager:forceRePaint() else
-- Callback closed our parent, we're done
return true
end end
--UIManager:forceRePaint()
end end
return true return true
end end

@ -36,16 +36,16 @@ local ImageViewer = InputContainer:new{
file = nil, file = nil,
-- or an already made BlitBuffer (ie: made by Mupdf.renderImageFile()) -- or an already made BlitBuffer (ie: made by Mupdf.renderImageFile())
image = nil, image = nil,
-- whether provided BlitBuffer should be free(), normally true -- whether the provided BlitBuffer should be free'd. Usually true,
-- unless our caller wants to reuse it's provided image -- unless our caller wants to reuse the image it provided
image_disposable = true, image_disposable = true,
-- 'image' can alternatively be a table (list) of multiple BlitBuffers -- 'image' can alternatively be a table (list) of multiple BlitBuffers
-- (or functions returning BlitBuffers). -- (or functions returning BlitBuffers).
-- The table will have its .free() called onClose according to -- The table will have its .free() called onClose according to
-- the image_disposable provided here. -- the image_disposable provided here.
-- Each BlitBuffer in the table (or returned by functions) will be free() -- Each BlitBuffer in the table (or returned by functions) will be free'd
-- if the table has itself an attribute image_disposable=true. -- if the table itself has an image_disposable field set to true.
-- With images list, when switching image, whether to keep previous -- With images list, when switching image, whether to keep previous
-- image pan & zoom -- image pan & zoom
@ -147,25 +147,12 @@ function ImageViewer:init()
self._images_list_disposable = self.image_disposable self._images_list_disposable = self.image_disposable
self.image_disposable = self._images_list.image_disposable self.image_disposable = self._images_list.image_disposable
end end
self:update()
end
function ImageViewer:_clean_image_wg()
-- To be called before re-using / not needing self._image_wg
-- otherwise resources used by its blitbuffer won't be freed
if self._image_wg then
logger.dbg("ImageViewer:_clean_image_wg()")
self._image_wg:free()
self._image_wg = nil
end
end
function ImageViewer:update() -- Widget layout
self:_clean_image_wg() -- clean previous if any
if self._scale_to_fit == nil then -- initialize our toggle if self._scale_to_fit == nil then -- initialize our toggle
self._scale_to_fit = self.scale_factor == 0 and true or false self._scale_to_fit = self.scale_factor == 0
end end
local orig_dimen = self.main_frame and self.main_frame.dimen or Geom:new{} local orig_dimen = Geom:new{}
self.align = "center" self.align = "center"
self.region = Geom:new{ self.region = Geom:new{
x = 0, y = 0, x = 0, y = 0,
@ -180,180 +167,348 @@ function ImageViewer:update()
self.width = Screen:getWidth() - Screen:scaleBySize(40) self.width = Screen:getWidth() - Screen:scaleBySize(40)
end end
local button_table_size = 0 -- Init the buttons no matter what
local button_container local buttons = {
if self.buttons_visible then {
local buttons = {
{ {
{ id = "scale",
text = self._scale_to_fit and _("Original size") or _("Scale"), text = self._scale_to_fit and _("Original size") or _("Scale"),
callback = function() callback = function()
self.scale_factor = self._scale_to_fit and 1 or 0 self.scale_factor = self._scale_to_fit and 1 or 0
self._scale_to_fit = not self._scale_to_fit self._scale_to_fit = not self._scale_to_fit
-- Reset center ratio (may have been modified if some panning was done) -- Reset center ratio (may have been modified if some panning was done)
self._center_x_ratio = 0.5 self._center_x_ratio = 0.5
self._center_y_ratio = 0.5 self._center_y_ratio = 0.5
self:update() self:update()
end, end,
},
{
text = self.rotated and _("No rotation") or _("Rotate"),
callback = function()
self.rotated = not self.rotated and true or false
self:update()
end,
},
{
text = _("Close"),
callback = function()
UIManager:close(self)
end,
},
}, },
} {
local button_table = ButtonTable:new{ id = "rotate",
width = self.width - 2*self.button_padding, text = self.rotated and _("No rotation") or _("Rotate"),
button_font_face = "cfont", callback = function()
button_font_size = 20, self.rotated = not self.rotated and true or false
buttons = buttons, self:update()
zero_sep = true, end,
show_parent = self,
}
button_container = CenterContainer:new{
dimen = Geom:new{
w = self.width,
h = button_table:getSize().h,
}, },
button_table, {
} id = "close",
button_table_size = button_table:getSize().h text = _("Close"),
callback = function()
self:onClose()
end,
},
},
}
self.button_table = ButtonTable:new{
width = self.width - 2*self.button_padding,
button_font_face = "cfont",
button_font_size = 20,
buttons = buttons,
zero_sep = true,
show_parent = self,
}
self.button_container = CenterContainer:new{
dimen = Geom:new{
w = self.width,
h = self.button_table:getSize().h,
},
self.button_table,
}
if self.buttons_visible then
self.button_table_size = self.button_table:getSize().h
else
self.button_table_size = 0
end end
-- height available to our image -- height available to our image
local img_container_h = self.height - button_table_size self.img_container_h = self.height - self.button_table_size
local title_bar, title_sep -- Init the title bar and its components no matter what
-- Toggler (white arrow) for caption, on the left of title
local ctoggler_text
if self.caption_visible then
ctoggler_text = "" -- white arrow (nicer than smaller black arrow ▼)
else
ctoggler_text = "" -- white arrow (nicer than smaller black arrow ►)
end
self.ctoggler_tw = TextWidget:new{
text = ctoggler_text,
face = self.title_face,
}
-- paddings chosen to align nicely with titlew
self.ctoggler = FrameContainer:new{
bordersize = 0,
padding = self.title_padding,
padding_top = self.title_padding + Size.padding.small,
padding_right = 0,
self.ctoggler_tw,
}
if self.caption then
self.ctoggler_width = self.ctoggler:getSize().w
else
self.ctoggler_width = 0
end
self.closeb = CloseButton:new{ window = self, padding_top = Size.padding.tiny, }
self.title_tbw = TextBoxWidget:new{
text = self.title_text,
face = self.title_face,
-- bold = true, -- we're already using a bold font
width = self.width - 2*self.title_padding - 2*self.title_margin - self.closeb:getSize().w - self.ctoggler_width,
}
local title_tbw_padding_bottom = self.title_padding + Size.padding.small
if self.caption and self.caption_visible then
title_tbw_padding_bottom = 0 -- save room between title and caption
end
self.titlew = FrameContainer:new{
padding = self.title_padding,
padding_top = self.title_padding + Size.padding.small,
padding_bottom = title_tbw_padding_bottom,
padding_left = self.caption and self.ctoggler_width or self.title_padding,
margin = self.title_margin,
bordersize = 0,
self.title_tbw,
}
if self.caption then
self.caption_tap_area = self.titlew
end
self.title_bar = OverlapGroup:new{
dimen = {
w = self.width,
h = self.titlew:getSize().h
},
self.titlew,
self.closeb
}
if self.caption then
table.insert(self.title_bar, 1, self.ctoggler)
end
-- Init the caption no matter what
self.caption_tbw = TextBoxWidget:new{
text = self.caption or _("N/A"),
face = self.caption_face,
width = self.width - 2*self.title_padding - 2*self.title_margin - 2*self.caption_padding,
}
local captionw = FrameContainer:new{
padding = self.caption_padding,
padding_top = 0, -- don't waste vertical room for bigger image
padding_bottom = 0,
margin = self.title_margin,
bordersize = 0,
self.caption_tbw,
}
self.captioned_title_bar = VerticalGroup:new{
align = "left",
self.title_bar,
captionw
}
if self.caption and self.caption_visible then
self.full_title_bar = self.captioned_title_bar
else
self.full_title_bar = self.title_bar
end
self.title_sep = LineWidget:new{
dimen = Geom:new{
w = self.width,
h = Size.line.thick,
}
}
-- adjust height available to our image
if self.with_title_bar then if self.with_title_bar then
-- Toggler (white arrow) for caption, on the left of title self.img_container_h = self.img_container_h - self.full_title_bar:getSize().h - self.title_sep:getSize().h
local ctoggler end
local ctoggler_width = 0
if self.caption then -- Init the progress bar no matter what
local ctoggler_text -- progress bar
if self.caption_visible then local percent = 1
ctoggler_text = "" -- white arrow (nicer than smaller black arrow ▼) if self._images_list and self._images_list_nb > 1 then
else percent = (self._images_list_cur - 1) / (self._images_list_nb - 1)
ctoggler_text = "" -- white arrow (nicer than smaller black arrow ►) end
end self.progress_bar = ProgressWidget:new{
-- paddings chosen to align nicely with titlew width = self.width - 2*self.button_padding,
ctoggler = FrameContainer:new{ height = Screen:scaleBySize(5),
bordersize = 0, percentage = percent,
padding = self.title_padding, margin_h = 0,
padding_top = self.title_padding + Size.padding.small, margin_v = 0,
padding_right = 0, radius = 0,
TextWidget:new{ ticks = nil,
text = ctoggler_text, last = nil,
face = self.title_face, }
} self.progress_container = CenterContainer:new{
} dimen = Geom:new{
ctoggler_width = ctoggler:getSize().w w = self.width,
h = self.progress_bar:getSize().h + Size.padding.small,
},
self.progress_bar
}
if self._images_list then
self.img_container_h = self.img_container_h - self.progress_container:getSize().h
end
-- If no buttons and no title are shown, use the full screen
local max_image_h = self.img_container_h
local max_image_w = self.width
-- Otherwise, add paddings around image
if self.buttons_visible or self.with_title_bar then
max_image_h = self.img_container_h - self.image_padding*2
max_image_w = self.width - self.image_padding*2
end
local rotation_angle = 0
if self.rotated then
-- in portrait mode, rotate according to this global setting so we are
-- like in landscape mode
local rotate_clockwise = DLANDSCAPE_CLOCKWISE_ROTATION
if Screen:getWidth() > Screen:getHeight() then
-- in landscape mode, counter-rotate landscape rotation so we are
-- back like in portrait mode
rotate_clockwise = not rotate_clockwise
end end
local closeb = CloseButton:new{ window = self, padding_top = Size.padding.tiny, } rotation_angle = rotate_clockwise and 90 or 270
local title_tbw = TextBoxWidget:new{ end
text = self.title_text,
face = self.title_face, self._image_wg = ImageWidget:new{
-- bold = true, -- we're already using a bold font file = self.file,
width = self.width - 2*self.title_padding - 2*self.title_margin - closeb:getSize().w - ctoggler_width, image = self.image,
image_disposable = false, -- we may re-use self.image
alpha = true, -- we might be showing images with an alpha channel (e.g., from Wikipedia)
width = max_image_w,
height = max_image_h,
rotation_angle = rotation_angle,
scale_factor = self.scale_factor,
center_x_ratio = self._center_x_ratio,
center_y_ratio = self._center_y_ratio,
}
self.image_container = CenterContainer:new{
dimen = Geom:new{
w = self.width,
h = self.img_container_h,
},
self._image_wg,
}
local frame_elements = VerticalGroup:new{ align = "left" }
if self.with_title_bar then
table.insert(frame_elements, self.full_title_bar)
table.insert(frame_elements, self.title_sep)
end
table.insert(frame_elements, self.image_container)
if self._images_list then
table.insert(frame_elements, self.progress_container)
end
if self.buttons_visible then
table.insert(frame_elements, self.button_container)
end
self.main_frame = FrameContainer:new{
radius = not self.fullscreen and 8 or nil,
padding = 0,
margin = 0,
background = Blitbuffer.COLOR_WHITE,
frame_elements,
}
self[1] = WidgetContainer:new{
align = self.align,
dimen = self.region,
FrameContainer:new{
bordersize = 0,
padding = Size.padding.default,
self.main_frame,
} }
}
-- NOTE: We use UI instead of partial, because we do NOT want to end up using a REAGL waveform...
-- NOTE: Disabling dithering here makes for a perfect test-case of how well it works:
-- page turns will show color quantization artefacts (i.e., banding) like crazy,
-- while a long touch will trigger a dithered, flashing full-refresh that'll make everything shiny :).
self.dithered = true
UIManager:setDirty(self, function()
local update_region = self.main_frame.dimen:combine(orig_dimen)
return "ui", update_region, true
end)
end
function ImageViewer:_clean_image_wg()
-- To be called before re-using / disposing of self._image_wg,
-- otherwise resources used by its blitbuffer won't be free'd
if self._image_wg then
logger.dbg("ImageViewer:_clean_image_wg")
self._image_wg:free()
self._image_wg = nil
end
end
function ImageViewer:update()
-- Free our ImageWidget, which is the only thing we'll replace (e.g., leave the TextBoxWidgets alone).
self:_clean_image_wg()
-- Update window geometry
local orig_dimen = self.main_frame.dimen
if self.fullscreen then
self.height = Screen:getHeight()
self.width = Screen:getWidth()
else
self.height = Screen:getHeight() - Screen:scaleBySize(40)
self.width = Screen:getWidth() - Screen:scaleBySize(40)
end
-- Update Buttons
if self.buttons_visible then
local scale_btn = self.button_table:getButtonById("scale")
scale_btn:setText(self._scale_to_fit and _("Original size") or _("Scale"), scale_btn.width)
local rotate_btn = self.button_table:getButtonById("rotate")
rotate_btn:setText(self.rotated and _("No rotation") or _("Rotate"), rotate_btn.width)
self.button_table_size = self.button_table:getSize().h
else
self.button_table_size = 0
end
-- height available to our image
self.img_container_h = self.height - self.button_table_size
-- Update the title bar
if self.with_title_bar then
self.ctoggler_tw:setText(self.caption_visible and "" or "")
-- Padding is dynamic...
local title_tbw_padding_bottom = self.title_padding + Size.padding.small local title_tbw_padding_bottom = self.title_padding + Size.padding.small
if self.caption and self.caption_visible then if self.caption and self.caption_visible then
title_tbw_padding_bottom = 0 -- save room between title and caption title_tbw_padding_bottom = 0
end
local titlew = FrameContainer:new{
padding = self.title_padding,
padding_top = self.title_padding + Size.padding.small,
padding_bottom = title_tbw_padding_bottom,
padding_left = ctoggler and ctoggler_width or self.title_padding,
margin = self.title_margin,
bordersize = 0,
title_tbw,
}
if self.caption then
self.caption_tap_area = titlew
end
title_bar = OverlapGroup:new{
dimen = {
w = self.width,
h = titlew:getSize().h
},
titlew,
closeb
}
if ctoggler then
table.insert(title_bar, 1, ctoggler)
end end
self.titlew.padding_bottom = title_tbw_padding_bottom
self.title_bar.dimen.h = self.titlew:getSize().h
if self.caption and self.caption_visible then if self.caption and self.caption_visible then
local caption_tbw = TextBoxWidget:new{ self.full_title_bar = self.captioned_title_bar
text = self.caption, else
face = self.caption_face, self.full_title_bar = self.title_bar
width = self.width - 2*self.title_padding - 2*self.title_margin - 2*self.caption_padding,
}
local captionw = FrameContainer:new{
padding = self.caption_padding,
padding_top = 0, -- don't waste vertical room for bigger image
padding_bottom = 0,
margin = self.title_margin,
bordersize = 0,
caption_tbw,
}
title_bar = VerticalGroup:new{
align = "left",
title_bar,
captionw
}
end end
title_sep = LineWidget:new{
dimen = Geom:new{ self.img_container_h = self.img_container_h - self.full_title_bar:getSize().h - self.title_sep:getSize().h
w = self.width,
h = Size.line.thick,
}
}
-- adjust height available to our image
img_container_h = img_container_h - title_bar:getSize().h - title_sep:getSize().h
end end
local progress_container -- Update the progress bar
if self._images_list then if self._images_list then
-- progress bar
local percent = 1 local percent = 1
if self._images_list_nb > 1 then if self._images_list_nb > 1 then
percent = (self._images_list_cur - 1) / (self._images_list_nb - 1) percent = (self._images_list_cur - 1) / (self._images_list_nb - 1)
end end
local progress_bar = ProgressWidget:new{
width = self.width - 2*self.button_padding, self.progress_bar:setPercentage(percent)
height = Screen:scaleBySize(5),
percentage = percent, self.img_container_h = self.img_container_h - self.progress_container:getSize().h
margin_h = 0,
margin_v = 0,
radius = 0,
ticks = nil,
last = nil,
}
progress_container = CenterContainer:new{
dimen = Geom:new{
w = self.width,
h = progress_bar:getSize().h + Size.padding.small,
},
progress_bar
}
img_container_h = img_container_h - progress_container:getSize().h
end end
-- Update the image widget itself
-- If no buttons and no title are shown, use the full screen -- If no buttons and no title are shown, use the full screen
local max_image_h = img_container_h local max_image_h = self.img_container_h
local max_image_w = self.width local max_image_w = self.width
-- Otherwise, add paddings around image -- Otherwise, add paddings around image
if self.buttons_visible or self.with_title_bar then if self.buttons_visible or self.with_title_bar then
max_image_h = img_container_h - self.image_padding*2 max_image_h = self.img_container_h - self.image_padding*2
max_image_w = self.width - self.image_padding*2 max_image_w = self.width - self.image_padding*2
end end
@ -383,25 +538,26 @@ function ImageViewer:update()
center_y_ratio = self._center_y_ratio, center_y_ratio = self._center_y_ratio,
} }
local image_container = CenterContainer:new{ self.image_container = CenterContainer:new{
dimen = Geom:new{ dimen = Geom:new{
w = self.width, w = self.width,
h = img_container_h, h = self.img_container_h,
}, },
self._image_wg, self._image_wg,
} }
-- Update the final layout
local frame_elements = VerticalGroup:new{ align = "left" } local frame_elements = VerticalGroup:new{ align = "left" }
if self.with_title_bar then if self.with_title_bar then
table.insert(frame_elements, title_bar) table.insert(frame_elements, self.full_title_bar)
table.insert(frame_elements, title_sep) table.insert(frame_elements, self.title_sep)
end end
table.insert(frame_elements, image_container) table.insert(frame_elements, self.image_container)
if progress_container then if self._images_list then
table.insert(frame_elements, progress_container) table.insert(frame_elements, self.progress_container)
end end
if self.buttons_visible then if self.buttons_visible then
table.insert(frame_elements, button_container) table.insert(frame_elements, self.button_container)
end end
self.main_frame = FrameContainer:new{ self.main_frame = FrameContainer:new{
@ -420,14 +576,10 @@ function ImageViewer:update()
self.main_frame, self.main_frame,
} }
} }
-- NOTE: We use UI instead of partial, because we do NOT want to end up using a REAGL waveform...
-- NOTE: Disabling dithering here makes for a perfect test-case of how well it works:
-- page turns will show color quantization artefacts (i.e., banding) like crazy,
-- while a long touch will trigger a dithered, flashing full-refresh that'll make everything shiny :).
self.dithered = true self.dithered = true
UIManager:setDirty(self, function() UIManager:setDirty(self, function()
local update_region = self.main_frame.dimen:combine(orig_dimen) local update_region = self.main_frame.dimen:combine(orig_dimen)
logger.dbg("update image region", update_region)
return "ui", update_region, true return "ui", update_region, true
end) end)
end end
@ -442,7 +594,7 @@ end
function ImageViewer:switchToImageNum(image_num) function ImageViewer:switchToImageNum(image_num)
if self.image and self.image_disposable and self.image.free then if self.image and self.image_disposable and self.image.free then
logger.dbg("ImageViewer:free(self.image)") logger.dbg("ImageViewer:switchToImageNum: free self.image", self.image)
self.image:free() self.image:free()
self.image = nil self.image = nil
end end
@ -470,7 +622,7 @@ function ImageViewer:onTap(_, ges)
return self:onSaveImageView() return self:onSaveImageView()
end end
end end
if self.caption_tap_area and ges.pos:intersectWith(self.caption_tap_area.dimen) then if self.with_title_bar and self.caption_tap_area and ges.pos:intersectWith(self.caption_tap_area.dimen) then
self.caption_visible = not self.caption_visible self.caption_visible = not self.caption_visible
self:update() self:update()
return true return true
@ -710,17 +862,38 @@ function ImageViewer:onAnyKeyPressed()
end end
function ImageViewer:onCloseWidget() function ImageViewer:onCloseWidget()
-- clean all our BlitBuffer objects when UIManager:close() was called -- Our ImageWidget (self._image_wg) is always a proper child widget, so it'll receive this event,
self:_clean_image_wg() -- and attempt to free its resources accordingly.
-- But, if it didn't have to touch the original BB (self.image) passed to ImageViewer (e.g., no scaling needed),
-- it will *re-use* self.image, and flag it as non-disposable, meaning it will not have been free'd earlier.
-- Since we're the ones who ultimately truly know whether we should dispose of self.image or not, do that now ;).
if self.image and self.image_disposable and self.image.free then if self.image and self.image_disposable and self.image.free then
logger.dbg("ImageViewer:free(self.image)") logger.dbg("ImageViewer:onCloseWidget: free self.image", self.image)
self.image:free() self.image:free()
self.image = nil self.image = nil
end end
-- also clean _images_list if it provides a method for that -- also clean _images_list if it provides a method for that
if self._images_list and self._images_list_disposable and self._images_list.free then if self._images_list and self._images_list_disposable and self._images_list.free then
logger.dbg("ImageViewer:onCloseWidget: free self._images_list", self._images_list)
self._images_list:free() self._images_list:free()
end end
-- Those, on the other hand, are always initialized, but may not actually be in our widget tree right now,
-- depending on what we needed to show, so they might not get sent a CloseWidget event.
-- They (and their FFI/C resources) would eventually get released by the GC, but let's be pedantic ;).
if not self.with_title_bar then
self.captioned_title_bar:free()
end
if not self.caption then
self.ctoggler:free()
end
if not self._images_list then
self.progress_container:free()
end
if not self.buttons_visible then
self.button_container:free()
end
-- NOTE: Assume there's no image beneath us, so, no dithering request -- NOTE: Assume there's no image beneath us, so, no dithering request
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "flashui", self.main_frame.dimen return "flashui", self.main_frame.dimen

@ -482,6 +482,7 @@ end
-- (ie: in some other widget's update()), to not leak memory with -- (ie: in some other widget's update()), to not leak memory with
-- BlitBuffer zombies -- BlitBuffer zombies
function ImageWidget:free() function ImageWidget:free()
--print("ImageWidget:free on", self, "for BB?", self._bb, self._bb_disposable)
if self._bb and self._bb_disposable and self._bb.free then if self._bb and self._bb_disposable and self._bb.free then
self._bb:free() self._bb:free()
self._bb = nil self._bb = nil

@ -491,7 +491,7 @@ end
function InputDialog:onCloseWidget() function InputDialog:onCloseWidget()
self:onClose() self:onClose()
UIManager:setDirty(nil, self.fullscreen and "full" or function() UIManager:setDirty(nil, self.fullscreen and "full" or function()
return "partial", self.dialog_frame.dimen return "ui", self.dialog_frame.dimen
end) end)
end end

@ -155,7 +155,7 @@ end
function KeyboardLayoutDialog:onCloseWidget() function KeyboardLayoutDialog:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self[1][1].dimen return "ui", self[1][1].dimen
end) end)
return true return true
end end

@ -488,13 +488,47 @@ function MenuItem:onTapSelect(arg, ges)
--UIManager:waitForVSync() --UIManager:waitForVSync()
self[1].invert = false self[1].invert = false
-- We assume a tap anywhere updates the full menu, so, forgo this, much like in TouchMenu
--[[ -- Most Menu entries will actually update the full menu, but they may also pop up a few various things,
UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) -- so, pilfer a few heuristics from TouchMenu...
UIManager:setDirty(nil, function() local top_widget = UIManager:getTopWidget()
return "ui", self[1].dimen -- If the callback opened a full-screen widget, we're done
end) if top_widget.covers_fullscreen then
--]] return true
end
-- If we're still on top, we're done, as the full list of items has probably been updated by the callback
if top_widget == self.show_parent then
return true
end
-- If the callback opened the Virtual Keyboard, it gets trickier
if top_widget == "VirtualKeyboard" then
-- Unfortunately, we can't really tell full-screen widgets apart from
-- stuff that might just pop the keyboard for a TextInput box...
-- So, a full fenced redraw it is...
UIManager:waitForVSync()
UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen
end)
return true
end
-- If a modal was opened outside of our highlight region, we can unhighlight safely
if self[1].dimen:notIntersectWith(UIManager:getPreviousRefreshRegion()) then
UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "ui", self[1].dimen
end)
else
-- That leaves modals that might have been displayed on top of the highlighted menu entry, in which case,
-- we can't take any shortcuts, as it would invert/paint *over* the popop.
-- Instead, fence the callback to avoid races, and repaint the *full* widget stack properly.
UIManager:waitForVSync()
UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen
end)
end
--UIManager:forceRePaint() --UIManager:forceRePaint()
end end
return true return true
@ -518,10 +552,25 @@ function MenuItem:onHoldSelect(arg, ges)
--UIManager:waitForVSync() --UIManager:waitForVSync()
self[1].invert = false self[1].invert = false
UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function() -- Same idea as for tap, minus the various things that make no sense for a hold callback...
return "ui", self[1].dimen local top_widget = UIManager:getTopWidget()
end)
-- If we're still on top, or a modal was opened outside of our highlight region, we can unhighlight safely
if top_widget == self.show_parent or self[1].dimen:notIntersectWith(UIManager:getPreviousRefreshRegion()) then
UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "ui", self[1].dimen
end)
else
-- That leaves modals that might have been displayed on top of the highlighted menu entry, in which case,
-- we can't take any shortcuts, as it would invert/paint *over* the popop.
-- Instead, fence the callback to avoid races, and repaint the *full* widget stack properly.
UIManager:waitForVSync()
UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen
end)
end
--UIManager:forceRePaint() --UIManager:forceRePaint()
end end
return true return true
@ -949,8 +998,8 @@ function Menu:onCloseWidget()
-- we cannot refresh regionally using the dimen field -- we cannot refresh regionally using the dimen field
-- because some menus without menu title use VerticalGroup to include -- because some menus without menu title use VerticalGroup to include
-- a text widget which is not calculated into the dimen. -- a text widget which is not calculated into the dimen.
-- For example, it's a dirty hack to use two menus(one this menu and one -- For example, it's a dirty hack to use two menus (one being this menu and
-- touch menu) in the filemanager in order to capture tap gesture to popup -- the other touch menu) in the filemanager in order to capture tap gesture to popup
-- the filemanager menu. -- the filemanager menu.
-- NOTE: For the same reason, don't make it flash, -- NOTE: For the same reason, don't make it flash,
-- because that'll trigger when we close the FM and open a book... -- because that'll trigger when we close the FM and open a book...

@ -156,7 +156,7 @@ end
function MultiConfirmBox:onCloseWidget() function MultiConfirmBox:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self[1][1].dimen return "ui", self[1][1].dimen
end) end)
end end

@ -377,7 +377,7 @@ end
function NaturalLightWidget:onCloseWidget() function NaturalLightWidget:onCloseWidget()
self:closeKeyboard() self:closeKeyboard()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self.nl_frame.dimen return "flashui", self.nl_frame.dimen
end) end)
-- Tell frontlight widget that we're closed -- Tell frontlight widget that we're closed
self.fl_widget:naturalLightConfigClose() self.fl_widget:naturalLightConfigClose()

@ -62,10 +62,8 @@ function NumberPickerWidget:init()
self.value_index = self.value_index or 1 self.value_index = self.value_index or 1
self.value = self.value_table[self.value_index] self.value = self.value_table[self.value_index]
end end
self:update()
end
function NumberPickerWidget:paintWidget() -- Widget layout
local bordersize = Size.border.default local bordersize = Size.border.default
local margin = Size.margin.default local margin = Size.margin.default
local button_up = Button:new{ local button_up = Button:new{
@ -118,18 +116,19 @@ function NumberPickerWidget:paintWidget()
local empty_space = VerticalSpan:new{ local empty_space = VerticalSpan:new{
width = math.ceil(self.screen_height * 0.01) width = math.ceil(self.screen_height * 0.01)
} }
local value = self.value
self.formatted_value = self.value
if not self.value_table then if not self.value_table then
value = string.format(self.precision, value) self.formatted_value = string.format(self.precision, self.formatted_value)
end end
local input_dialog local input_dialog
local callback_input = nil local callback_input = nil
if self.value_table == nil then if self.value_table == nil then
callback_input = function() callback_input = function()
input_dialog = InputDialog:new{ input_dialog = InputDialog:new{
title = _("Enter number"), title = _("Enter number"),
input = value, input = self.formatted_value,
input_type = "number", input_type = "number",
buttons = { buttons = {
{ {
@ -170,36 +169,33 @@ function NumberPickerWidget:paintWidget()
}, },
}, },
} }
self.update_callback()
UIManager:show(input_dialog) UIManager:show(input_dialog)
input_dialog:onShowKeyboard() input_dialog:onShowKeyboard()
end end
end end
local text_value = Button:new{ self.text_value = Button:new{
text = tostring(value), text = tostring(self.formatted_value),
bordersize = 0, bordersize = 0,
padding = 0, padding = 0,
text_font_face = self.spinner_face.font, text_font_face = self.spinner_face.font,
text_font_size = self.spinner_face.orig_size, text_font_size = self.spinner_face.orig_size,
width = self.width, width = self.width,
max_width = self.width, max_width = self.width,
show_parent = self.show_parent,
callback = callback_input, callback = callback_input,
} }
return VerticalGroup:new{
local widget_spinner = VerticalGroup:new{
align = "center", align = "center",
button_up, button_up,
empty_space, empty_space,
text_value, self.text_value,
empty_space, empty_space,
button_down, button_down,
} }
end
--[[--
Update.
--]]
function NumberPickerWidget:update()
local widget_spinner = self:paintWidget()
self.frame = FrameContainer:new{ self.frame = FrameContainer:new{
bordersize = 0, bordersize = 0,
padding = Size.padding.default, padding = Size.padding.default,
@ -217,6 +213,22 @@ function NumberPickerWidget:update()
UIManager:setDirty(self.show_parent, function() UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen return "ui", self.dimen
end) end)
end
--[[--
Update.
--]]
function NumberPickerWidget:update()
self.formatted_value = self.value
if not self.value_table then
self.formatted_value = string.format(self.precision, self.formatted_value)
end
self.text_value:setText(tostring(self.formatted_value), self.width)
UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen
end)
self.update_callback() self.update_callback()
end end

@ -186,7 +186,7 @@ end
function OpenWithDialog:onCloseWidget() function OpenWithDialog:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self[1][1].dimen return "ui", self.dialog_frame.dimen
end) end)
return true return true
end end

@ -72,14 +72,15 @@ function SpinWidget:init()
}, },
} }
end end
-- Actually the widget layout
self:update() self:update()
end end
function SpinWidget:update() function SpinWidget:update()
-- This picker_update_callback will be redefined later. It is needed -- This picker_update_callback will be redefined later.
-- so we can have our MovableContainer repainted on NumberPickerWidgets -- It's a hack to restore transparency after a Button unhighlight in NumberPicker,
-- update. It is needed if we have enabled transparency on MovableContainer, -- in case the MovableContainer was actually made transparent.
-- otherwise the NumberPicker area gets opaque on update.
local picker_update_callback = function() end local picker_update_callback = function() end
local value_widget = NumberPickerWidget:new{ local value_widget = NumberPickerWidget:new{
show_parent = self, show_parent = self,
@ -242,9 +243,25 @@ function SpinWidget:update()
return "ui", self.spin_frame.dimen return "ui", self.spin_frame.dimen
end) end)
picker_update_callback = function() picker_update_callback = function()
UIManager:setDirty("all", function() -- If we're actually transparent, force an alpha-aware repaint.
return "ui", self.movable.dimen if self.movable.alpha then
end) if G_reader_settings:nilOrTrue("flash_ui") then
-- It's delayed to the next tick to actually catch a Button unhighlight.
UIManager:nextTick(function()
UIManager:setDirty("all", function()
return "ui", self.movable.dimen
end)
end)
else
-- This should only really be necessary for the up/down buttons here,
-- because they repaint the center value button & text, unlike said button,
-- which just pops up the VK.
-- On the upside, we shouldn't need to delay anything without flash_ui ;).
UIManager:setDirty("all", function()
return "ui", self.movable.dimen
end)
end
end
end end
end end
@ -255,7 +272,7 @@ end
function SpinWidget:onCloseWidget() function SpinWidget:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self.spin_frame.dimen return "ui", self.spin_frame.dimen
end) end)
return true return true
end end

@ -847,6 +847,10 @@ function TextBoxWidget:_renderImage(start_row_idx)
local scheduled_update = self.scheduled_update local scheduled_update = self.scheduled_update
self.scheduled_update = nil -- reset it, so we don't have to whenever we return below self.scheduled_update = nil -- reset it, so we don't have to whenever we return below
if not self.line_num_to_image or not self.line_num_to_image[start_row_idx] then if not self.line_num_to_image or not self.line_num_to_image[start_row_idx] then
-- No image, no dithering
if self.dialog then
self.dialog.dithered = false
end
return -- no image on this page return -- no image on this page
end end
local image = self.line_num_to_image[start_row_idx] local image = self.line_num_to_image[start_row_idx]
@ -891,9 +895,22 @@ function TextBoxWidget:_renderImage(start_row_idx)
local bbtype = image.bb:getType() local bbtype = image.bb:getType()
if bbtype == Blitbuffer.TYPE_BB8A or bbtype == Blitbuffer.TYPE_BBRGB32 then if bbtype == Blitbuffer.TYPE_BB8A or bbtype == Blitbuffer.TYPE_BBRGB32 then
-- NOTE: MuPDF feeds us premultiplied alpha (and we don't care w/ GifLib, as alpha is all or nothing). -- NOTE: MuPDF feeds us premultiplied alpha (and we don't care w/ GifLib, as alpha is all or nothing).
self._bb:pmulalphablitFrom(image.bb, self.width - image.width, 0) if Screen.sw_dithering then
self._bb:ditherpmulalphablitFrom(image.bb, self.width - image.width, 0)
else
self._bb:pmulalphablitFrom(image.bb, self.width - image.width, 0)
end
else else
self._bb:blitFrom(image.bb, self.width - image.width, 0) if Screen.sw_dithering then
self._bb:ditherblitFrom(image.bb, self.width - image.width, 0)
else
self._bb:blitFrom(image.bb, self.width - image.width, 0)
end
end
-- Request dithering
if self.dialog then
self.dialog.dithered = true
end end
end end
local status_height = 0 local status_height = 0
@ -965,7 +982,8 @@ function TextBoxWidget:_renderImage(start_row_idx)
y = self.dimen.y, y = self.dimen.y,
w = image.width, w = image.width,
h = image.height, h = image.height,
} },
true -- Request dithering
end) end)
end end
end) end)
@ -983,7 +1001,8 @@ function TextBoxWidget:_renderImage(start_row_idx)
y = self.dimen.y, y = self.dimen.y,
w = image.width, w = image.width,
h = image.height, h = image.height,
} },
true -- Request dithering
end) end)
end end
end end
@ -1063,6 +1082,7 @@ function TextBoxWidget:onCloseWidget()
end end
function TextBoxWidget:free(full) function TextBoxWidget:free(full)
--print("TextBoxWidget:free", full, "on", self)
-- logger.dbg("TextBoxWidget:free called") -- logger.dbg("TextBoxWidget:free called")
-- We are called with full=false from other methods here whenever -- We are called with full=false from other methods here whenever
-- :_renderText() is to be called to render a new page (when scrolling -- :_renderText() is to be called to render a new page (when scrolling
@ -1087,6 +1107,7 @@ function TextBoxWidget:free(full)
-- Allow not waiting until Lua gc() to cleanup C XText malloc'ed stuff -- Allow not waiting until Lua gc() to cleanup C XText malloc'ed stuff
-- (we should not free it if full=false as it is re-usable across renderings) -- (we should not free it if full=false as it is re-usable across renderings)
self._xtext:free() self._xtext:free()
self._xtext = nil
-- logger.dbg("TextBoxWidget:_xtext:free()") -- logger.dbg("TextBoxWidget:_xtext:free()")
end end
end end
@ -1124,7 +1145,8 @@ function TextBoxWidget:onTapImage(arg, ges)
y = self.dimen.y, y = self.dimen.y,
w = image.width, w = image.width,
h = image.height, h = image.height,
} },
not self.image_show_alt_text -- Request dithering when showing the image
end) end)
return true return true
end end

@ -363,9 +363,11 @@ function TextWidget:paintTo(bb, x, y)
end end
function TextWidget:free() function TextWidget:free()
--print("TextWidget:free on", self)
-- Allow not waiting until Lua gc() to cleanup C XText malloc'ed stuff -- Allow not waiting until Lua gc() to cleanup C XText malloc'ed stuff
if self._xtext then if self._xtext then
self._xtext:free() self._xtext:free()
self._xtext = nil
end end
end end

@ -56,6 +56,8 @@ function TimeWidget:init()
}, },
} }
end end
-- Actually the widget layout
self:update() self:update()
end end
@ -191,7 +193,7 @@ end
function TimeWidget:onCloseWidget() function TimeWidget:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self.time_frame.dimen return "ui", self.time_frame.dimen
end) end)
return true return true
end end

@ -186,9 +186,16 @@ function TouchMenuItem:onTapSelect(arg, ges)
return true return true
end end
-- If the callback opened the Virtual Keyboard, we're done -- If the callback opened the Virtual Keyboard, it gets trickier
-- (this is for TextEditor, Terminal & co) -- (this is for TextEditor, Terminal & co)
if top_widget == "VirtualKeyboard" then if top_widget == "VirtualKeyboard" then
-- Unfortunately, we can't really tell full-screen widgets (e.g., TextEditor, Terminal) apart from
-- stuff that might just pop the keyboard for a TextInput box...
-- So, a full fenced redraw it is...
UIManager:waitForVSync()
UIManager:setDirty(self.show_parent, function()
return "ui", highlight_dimen
end)
return true return true
end end

@ -60,7 +60,8 @@ end
function VerticalGroup:clear() function VerticalGroup:clear()
self:free() self:free()
WidgetContainer.clear(self) -- Skip WidgetContainer:clear's free call, we just did that in our own free ;)
WidgetContainer.clear(self, true)
end end
function VerticalGroup:resetLayout() function VerticalGroup:resetLayout()
@ -69,6 +70,7 @@ function VerticalGroup:resetLayout()
end end
function VerticalGroup:free() function VerticalGroup:free()
--print("VerticalGroup:free on", self)
self:resetLayout() self:resetLayout()
WidgetContainer.free(self) WidgetContainer.free(self)
end end

@ -48,7 +48,6 @@ function CoverMenu:updateItems(select_number)
local old_dimen = self.dimen and self.dimen:copy() local old_dimen = self.dimen and self.dimen:copy()
-- self.layout must be updated for focusmanager -- self.layout must be updated for focusmanager
self.layout = {} self.layout = {}
self.item_group:free() -- avoid memory leaks by calling free() on all our sub-widgets
self.item_group:clear() self.item_group:clear()
-- strange, best here if resetLayout() are done after _recalculateDimen(), -- strange, best here if resetLayout() are done after _recalculateDimen(),
-- unlike what is done in menu.lua -- unlike what is done in menu.lua

Loading…
Cancel
Save