From df0bbc9db77ec272fd91e11e41297d45f1b3476e Mon Sep 17 00:00:00 2001 From: NiLuJe Date: Fri, 29 Jan 2021 00:20:15 +0100 Subject: [PATCH] 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. --- base | 2 +- .../filemanager/filemanagersetdefaults.lua | 2 +- frontend/apps/opdscatalog/opdscatalog.lua | 2 +- .../apps/reader/modules/readerbookmark.lua | 8 +- frontend/apps/reader/modules/readerfooter.lua | 3 +- frontend/apps/reader/skimtowidget.lua | 4 +- frontend/ui/uimanager.lua | 90 +-- frontend/ui/widget/bookstatuswidget.lua | 2 +- frontend/ui/widget/button.lua | 57 +- frontend/ui/widget/buttondialog.lua | 2 +- frontend/ui/widget/buttondialogtitle.lua | 2 +- frontend/ui/widget/buttontable.lua | 6 +- frontend/ui/widget/configdialog.lua | 3 + frontend/ui/widget/confirmbox.lua | 2 +- .../ui/widget/container/widgetcontainer.lua | 14 +- frontend/ui/widget/datewidget.lua | 4 +- frontend/ui/widget/dictquicklookup.lua | 220 ++++--- frontend/ui/widget/doublespinwidget.lua | 33 +- frontend/ui/widget/eventlistener.lua | 1 + frontend/ui/widget/frontlightwidget.lua | 2 +- frontend/ui/widget/horizontalgroup.lua | 4 +- frontend/ui/widget/htmlboxwidget.lua | 1 + frontend/ui/widget/iconbutton.lua | 31 +- frontend/ui/widget/imageviewer.lua | 541 ++++++++++++------ frontend/ui/widget/imagewidget.lua | 1 + frontend/ui/widget/inputdialog.lua | 2 +- frontend/ui/widget/keyboardlayoutdialog.lua | 2 +- frontend/ui/widget/menu.lua | 75 ++- frontend/ui/widget/multiconfirmbox.lua | 2 +- frontend/ui/widget/naturallightwidget.lua | 2 +- frontend/ui/widget/numberpickerwidget.lua | 46 +- frontend/ui/widget/openwithdialog.lua | 2 +- frontend/ui/widget/spinwidget.lua | 33 +- frontend/ui/widget/textboxwidget.lua | 32 +- frontend/ui/widget/textwidget.lua | 2 + frontend/ui/widget/timewidget.lua | 4 +- frontend/ui/widget/touchmenu.lua | 9 +- frontend/ui/widget/verticalgroup.lua | 4 +- plugins/coverbrowser.koplugin/covermenu.lua | 1 - 39 files changed, 841 insertions(+), 412 deletions(-) diff --git a/base b/base index 75b629d7a..43b9a2967 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit 75b629d7ad66510f822fa0dda9c32f402ecd5b08 +Subproject commit 43b9a2967954477db8f1fc9cd9ce6f5f7a798049 diff --git a/frontend/apps/filemanager/filemanagersetdefaults.lua b/frontend/apps/filemanager/filemanagersetdefaults.lua index c2693c657..6a451e33d 100644 --- a/frontend/apps/filemanager/filemanagersetdefaults.lua +++ b/frontend/apps/filemanager/filemanagersetdefaults.lua @@ -85,7 +85,7 @@ function SetDefaults:init() -- opened immediately) we need to set the full screen dirty because -- otherwise only the input dialog part of the screen is refreshed. menu_container.onShow = function() - UIManager:setDirty(nil, "partial") + UIManager:setDirty(nil, "ui") end self.defaults_menu = Menu:new{ diff --git a/frontend/apps/opdscatalog/opdscatalog.lua b/frontend/apps/opdscatalog/opdscatalog.lua index db8f2b9fc..788db49d6 100644 --- a/frontend/apps/opdscatalog/opdscatalog.lua +++ b/frontend/apps/opdscatalog/opdscatalog.lua @@ -54,7 +54,7 @@ end function OPDSCatalog:onCloseWidget() UIManager:setDirty(nil, function() - return "partial", self[1].dimen + return "ui", self[1].dimen end) end diff --git a/frontend/apps/reader/modules/readerbookmark.lua b/frontend/apps/reader/modules/readerbookmark.lua index ea61ddebb..afe2d11f4 100644 --- a/frontend/apps/reader/modules/readerbookmark.lua +++ b/frontend/apps/reader/modules/readerbookmark.lua @@ -182,7 +182,7 @@ function ReaderBookmark:onToggleBookmark() pn_or_xp = self.ui.document:getXPointer() end self:toggleBookmark(pn_or_xp) - self.view.footer:onUpdateFooter(true) + self.view.footer:onUpdateFooter(self.view.footer_visible) self.ui:handleEvent(Event:new("SetDogearVisibility", not self.view.dogear_visible)) UIManager:setDirty(self.view.dialog, "ui") @@ -426,7 +426,7 @@ function ReaderBookmark:addBookmark(item) end end table.insert(self.bookmarks, _middle + direction, item) - self.view.footer:onUpdateFooter(true) + self.view.footer:onUpdateFooter(self.view.footer_visible) end -- binary search of sorted bookmarks @@ -470,7 +470,7 @@ function ReaderBookmark:removeBookmark(item) local v = self.bookmarks[_middle] if item.datetime == v.datetime and item.page == v.page then table.remove(self.bookmarks, _middle) - self.view.footer:onUpdateFooter(true) + self.view.footer:onUpdateFooter(self.view.footer_visible) return elseif self:isBookmarkInPageOrder(item, v) then _end = _middle - 1 @@ -487,7 +487,7 @@ function ReaderBookmark:removeBookmark(item) local v = self.bookmarks[i] if item.datetime == v.datetime and item.page == v.page then table.remove(self.bookmarks, i) - self.view.footer:onUpdateFooter(true) + self.view.footer:onUpdateFooter(self.view.footer_visible) return end end diff --git a/frontend/apps/reader/modules/readerfooter.lua b/frontend/apps/reader/modules/readerfooter.lua index c64aeb885..e25c86caf 100644 --- a/frontend/apps/reader/modules/readerfooter.lua +++ b/frontend/apps/reader/modules/readerfooter.lua @@ -2166,7 +2166,8 @@ function ReaderFooter:refreshFooter(refresh, signal) end 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 self:setupAutoRefreshTime() end diff --git a/frontend/apps/reader/skimtowidget.lua b/frontend/apps/reader/skimtowidget.lua index c32d6e6d1..8cf5c96a6 100644 --- a/frontend/apps/reader/skimtowidget.lua +++ b/frontend/apps/reader/skimtowidget.lua @@ -197,7 +197,7 @@ function SkimToWidget:init() enabled = true, width = self.button_width, show_parent = self, - vsync = not G_reader_settings:isTrue("refresh_on_chapter_boundaries"), + vsync = true, callback = function() local page = self.ui.toc:getNextChapter(self.curr_page) if page and page >=1 and page <= self.page_count then @@ -217,7 +217,7 @@ function SkimToWidget:init() enabled = true, width = self.button_width, show_parent = self, - vsync = not G_reader_settings:isTrue("refresh_on_chapter_boundaries"), + vsync = true, callback = function() local page = self.ui.toc:getPreviousChapter(self.curr_page) if page and page >=1 and page <= self.page_count then diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index d76d59263..44077ee8f 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -594,7 +594,17 @@ Registers a widget to be repainted and enqueues a refresh. the second parameter (refreshtype) can either specify a refreshtype (optionally in combination with a refreshregion - which is suggested) 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: full: high-fidelity flashing refresh (e.g., large images). 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. 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). - 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 - 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 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. 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. - That said, depending on your use case, using "ui" onClose can be a perfectly valid decision, and will ensure - never seeing a flash because of that widget. + That said, depending on your use case, using "ui" onCloseWidget can be a perfectly valid decision, + 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 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 UIManager:setDirty(self.widget, "partial") @@ -1126,20 +1167,6 @@ function UIManager:_refresh(mode, region, dither) end --- A couple helper functions to compute aligned values... --- c.f., & 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. function UIManager:_repaint() -- flag in which we will record if we did any repaints at all @@ -1220,31 +1247,6 @@ function UIManager:_repaint() refresh.dither = nil end 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 self._last_refresh_region = refresh.region Screen[refresh_methods[refresh.mode]](Screen, diff --git a/frontend/ui/widget/bookstatuswidget.lua b/frontend/ui/widget/bookstatuswidget.lua index a10b242aa..6c20f644c 100644 --- a/frontend/ui/widget/bookstatuswidget.lua +++ b/frontend/ui/widget/bookstatuswidget.lua @@ -246,7 +246,7 @@ function BookStatusWidget:generateRateGroup(width, height, rating) end function BookStatusWidget:setStar(num) - --clear previous data + -- clear previous data self.stars_container:clear() local stars_group = HorizontalGroup:new{ align = "center" } diff --git a/frontend/ui/widget/button.lua b/frontend/ui/widget/button.lua index a4f2affea..d3af202aa 100644 --- a/frontend/ui/widget/button.lua +++ b/frontend/ui/widget/button.lua @@ -29,6 +29,7 @@ local TextWidget = require("ui/widget/textwidget") local UIManager = require("ui/uimanager") local _ = require("gettext") local Screen = Device.screen +local logger = require("logger") local Button = InputContainer:new{ text = nil, -- mandatory @@ -142,15 +143,25 @@ function Button:init() end function Button:setText(text, width) - self.text = text - self.width = width - self:init() + if text ~= self.text then + -- Don't trash the frame if we're already a text button, and we're keeping the geometry intact + 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 function Button:setIcon(icon) - self.icon = icon - self.width = nil - self:init() + if icon ~= self.icon then + self.icon = icon + self.width = nil + self:init() + end end function Button:onFocus() @@ -227,6 +238,9 @@ function Button:onTapSelectButton() if G_reader_settings:isFalse("flash_ui") then self.callback() 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 ;). if self.text then -- 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() -- 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. + -- 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 self[1].invert = true + inverted = true end 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 self[1].invert = true + inverted = true UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) end UIManager:setDirty(nil, function() @@ -268,7 +284,7 @@ function Button:onTapSelectButton() -- because that would have a chance to noticeably delay it until the unhighlight. 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 -- NOTE: This cannot catch orphaned Button instances, c.f., the isSubwidgetShown(self) check below for that. return true @@ -288,13 +304,15 @@ function Button:onTapSelectButton() local top_widget = UIManager:getTopWidget() 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 - -- (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 return true 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, -- 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 -- 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() @@ -319,8 +337,7 @@ function Button:onTapSelectButton() end) --UIManager:forceRePaint() -- Ensures the unhighlight happens now, instead of potentially waiting and having it batched with something 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, - -- (hence the exception in the check above). + -- Callback closed our parent, we're done return true end end @@ -334,6 +351,22 @@ function Button:onTapSelectButton() 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() if self.hold_callback and (self.enabled or self.allow_hold_when_disabled) then self.hold_callback() diff --git a/frontend/ui/widget/buttondialog.lua b/frontend/ui/widget/buttondialog.lua index e152d57a7..16a33099d 100644 --- a/frontend/ui/widget/buttondialog.lua +++ b/frontend/ui/widget/buttondialog.lua @@ -68,7 +68,7 @@ end function ButtonDialog:onCloseWidget() UIManager:setDirty(nil, function() - return "partial", self[1][1].dimen + return "flashui", self[1][1].dimen end) end diff --git a/frontend/ui/widget/buttondialogtitle.lua b/frontend/ui/widget/buttondialogtitle.lua index bdb76eec6..c412a5d92 100644 --- a/frontend/ui/widget/buttondialogtitle.lua +++ b/frontend/ui/widget/buttondialogtitle.lua @@ -96,7 +96,7 @@ end function ButtonDialogTitle:onCloseWidget() UIManager:setDirty(nil, function() - return "partial", self[1][1].dimen + return "ui", self[1][1].dimen end) end diff --git a/frontend/ui/widget/buttontable.lua b/frontend/ui/widget/buttontable.lua index 596d9822b..c30977165 100644 --- a/frontend/ui/widget/buttontable.lua +++ b/frontend/ui/widget/buttontable.lua @@ -56,8 +56,8 @@ function ButtonTable:init() callback = btn_entry.callback, hold_callback = btn_entry.hold_callback, vsync = btn_entry.vsync, - width = (self.width - sizer_space)/column_cnt, - max_width = (self.width - sizer_space)/column_cnt - 2*self.sep_width - 2*self.padding, + width = math.ceil((self.width - sizer_space)/column_cnt), + max_width = math.ceil((self.width - sizer_space)/column_cnt - 2*self.sep_width - 2*self.padding), bordersize = 0, margin = 0, 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) end 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) end end -- end for each button line diff --git a/frontend/ui/widget/configdialog.lua b/frontend/ui/widget/configdialog.lua index de7109fdc..ee41a5627 100644 --- a/frontend/ui/widget/configdialog.lua +++ b/frontend/ui/widget/configdialog.lua @@ -868,6 +868,9 @@ function ConfigDialog:update() panel_index = self.panel_index, } end + if self.config_panel then + self.config_panel:free() + end self.config_panel = ConfigPanel:new{ index = self.panel_index, config_dialog = self, diff --git a/frontend/ui/widget/confirmbox.lua b/frontend/ui/widget/confirmbox.lua index 68bd463e3..8a9e16584 100644 --- a/frontend/ui/widget/confirmbox.lua +++ b/frontend/ui/widget/confirmbox.lua @@ -192,7 +192,7 @@ end function ConfirmBox:onCloseWidget() UIManager:setDirty(nil, function() - return "partial", self[1][1].dimen + return "ui", self[1][1].dimen end) end diff --git a/frontend/ui/widget/container/widgetcontainer.lua b/frontend/ui/widget/container/widgetcontainer.lua index b4528bcfb..d8e5d383d 100644 --- a/frontend/ui/widget/container/widgetcontainer.lua +++ b/frontend/ui/widget/container/widgetcontainer.lua @@ -53,7 +53,14 @@ end --[[-- 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 end @@ -113,7 +120,10 @@ end function WidgetContainer:free() 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 diff --git a/frontend/ui/widget/datewidget.lua b/frontend/ui/widget/datewidget.lua index 3f8cf68c7..4fdef411d 100644 --- a/frontend/ui/widget/datewidget.lua +++ b/frontend/ui/widget/datewidget.lua @@ -56,6 +56,8 @@ function DateWidget:init() }, } end + + -- Actually the widget layout self:update() end @@ -206,7 +208,7 @@ end function DateWidget:onCloseWidget() UIManager:setDirty(nil, function() - return "partial", self.date_frame.dimen + return "ui", self.date_frame.dimen end) return true end diff --git a/frontend/ui/widget/dictquicklookup.lua b/frontend/ui/widget/dictquicklookup.lua index ef0864f62..1890dcb0e 100644 --- a/frontend/ui/widget/dictquicklookup.lua +++ b/frontend/ui/widget/dictquicklookup.lua @@ -161,61 +161,9 @@ function DictQuickLookup:init() -- We no longer support setting a default dict with Tap on title. -- self:changeToDefaultDict() -- Now, dictionaries can be ordered (although not yet per-book), so trust the order set - self:changeDictionary(1) -- this will call self: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
. - - 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 + self:changeDictionary(1, true) -- don't call update + -- And here comes the initial widget layout... if self.is_wiki then -- Keep a copy of self.wiki_languages for use -- by DictQuickLookup:resyncWikiLanguages() @@ -242,7 +190,7 @@ function DictQuickLookup:update() local title_padding = Size.padding.default local title_width = inner_width - 2*title_padding -2*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, face = Font:getFace("x_smalltfont"), bold = true, @@ -250,15 +198,15 @@ function DictQuickLookup:update() -- Allow text to eat on the CloseButton padding_left (which -- 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 -- Visual hint: title left aligned for dict, but centered for Wikipedia dict_title_widget = CenterContainer:new{ dimen = Geom:new{ w = title_width, - h = dict_title_text:getSize().h, + h = self.dict_title_text:getSize().h, }, - dict_title_text, + self.dict_title_text, } end self.dict_title = FrameContainer:new{ @@ -328,12 +276,19 @@ function DictQuickLookup:update() self:lookupInputWord(self.lookupword) end, overlap_align = "right", + show_parent = self, } local lookup_edit_button_w = lookup_edit_button:getSize().w -- Nb of results (if set) local lookup_word_nb local lookup_word_nb_w = 0 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{ margin = 0, bordersize = 0, @@ -341,16 +296,12 @@ function DictQuickLookup:update() padding_left = Size.padding.small, padding_right = lookup_edit_button_w + Size.padding.default, overlap_align = "right", - TextWidget:new{ - text = self.displaynb, - face = Font:getFace("cfont", word_font_size), - padding = 0, -- smaller height for better aligmnent with icon - } + self.displaynb_text, } lookup_word_nb_w = lookup_word_nb:getSize().w end -- Lookup word - local lookup_word_text = TextWidget:new{ + self.lookup_word_text = TextWidget:new{ text = self.displayword, face = Font:getFace(word_font_face, word_font_size), bold = true, @@ -363,7 +314,7 @@ function DictQuickLookup:update() w = content_width, h = lookup_height, }, - lookup_word_text, + self.lookup_word_text, lookup_edit_button, lookup_word_nb, -- last, as this might be nil } @@ -375,6 +326,7 @@ function DictQuickLookup:update() buttons = { { { + id = "save", text = _("Save as EPUB"), callback = function() local InfoMessage = require("ui/widget/infomessage") @@ -443,6 +395,7 @@ function DictQuickLookup:update() end, }, { + id = "close", text = _("Close"), callback = function() UIManager:close(self) @@ -459,6 +412,7 @@ function DictQuickLookup:update() buttons = { { { + id = "prev_dict", text = prev_dict_text, vsync = true, enabled = self:isPrevDictAvaiable(), @@ -470,6 +424,7 @@ function DictQuickLookup:update() end, }, { + id = "highlight", text = self:getHighlightText(), enabled = self.highlight ~= nil, callback = function() @@ -478,10 +433,16 @@ function DictQuickLookup:update() else self.ui:handleEvent(Event:new("Unhighlight")) 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, }, { + id = "next_dict", text = next_dict_text, vsync = true, enabled = self:isNextDictAvaiable(), @@ -495,6 +456,7 @@ function DictQuickLookup:update() }, { { + id = "wikipedia", -- if dictionary result, do the same search on wikipedia -- if already wiki, get the full page for the current result text_func = function() @@ -513,6 +475,7 @@ function DictQuickLookup:update() }, -- 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" -- otherwise, just display current lang text = self.is_wiki @@ -532,6 +495,7 @@ function DictQuickLookup:update() end, }, { + id = "close", text = _("Close"), callback = function() -- UIManager:close(self) @@ -545,6 +509,7 @@ function DictQuickLookup:update() -- add a new first row with a single button to follow this link. table.insert(buttons, 1, { { + id = "link", text = _("Follow Link"), callback = function() 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 local buttons_padding = Size.padding.default local buttons_width = inner_width - 2*buttons_padding - local button_table = ButtonTable:new{ + self.button_table = ButtonTable:new{ width = buttons_width, button_font_face = "cfont", button_font_size = 20, @@ -595,7 +560,7 @@ function DictQuickLookup:update() + lookup_word:getSize().h + word_to_definition_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 -- the line height a ScrollTextWidget will use for the current font @@ -658,9 +623,8 @@ function DictQuickLookup:update() end end - local text_widget if self.is_html then - text_widget = ScrollHtmlWidget:new{ + self.text_widget = ScrollHtmlWidget:new{ html_body = self.definition, css = self:getHtmlDictionaryCss(), default_font_size = Screen:scaleBySize(self.dict_font_size), @@ -672,7 +636,7 @@ function DictQuickLookup:update() end, } else - text_widget = ScrollTextWidget:new{ + self.text_widget = ScrollTextWidget:new{ text = self.definition, face = self.content_face, width = content_width, @@ -694,7 +658,7 @@ function DictQuickLookup:update() padding_right = content_padding_h, margin = 0, bordersize = 0, - text_widget, + self.text_widget, } self.dict_frame = FrameContainer:new{ @@ -730,9 +694,9 @@ function DictQuickLookup:update() CenterContainer:new{ dimen = Geom:new{ 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.movable:setMovedOffset(orig_moved_offset) self[1] = WidgetContainer:new{ align = self.align, @@ -759,9 +722,93 @@ function DictQuickLookup:update() self.movable, } 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 - logger.dbg("update dict region", update_region) - return "partial", update_region + return "partial", self.dict_frame.dimen + end) +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
. + + 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 @@ -786,12 +833,10 @@ function DictQuickLookup:getInitialVisibleArea() end function DictQuickLookup:onCloseWidget() - -- Free our 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 + -- Our TextBoxWidget/HtmlBoxWidget/TextWidget/ImageWidget are proper child widgets, + -- so this event will propagate to 'em, and they'll free their resources. + + -- What's left is stuff that isn't directly in our widget tree... if self.images_cleanup_needed then logger.dbg("freeing lookup results images blitbuffers") for _, r in ipairs(self.results) do @@ -869,7 +914,7 @@ function DictQuickLookup:changeToLastDict() end end -function DictQuickLookup:changeDictionary(index) +function DictQuickLookup:changeDictionary(index, skip_update) if not self.results[index] then return end self.dict_index = index self.dictionary = self.results[index].dict @@ -919,7 +964,10 @@ function DictQuickLookup:changeDictionary(index) end end - self:update() + -- Don't call update when called from init + if not skip_update then + self:update() + end end --[[ No longer used diff --git a/frontend/ui/widget/doublespinwidget.lua b/frontend/ui/widget/doublespinwidget.lua index c01827be4..876f2bb67 100644 --- a/frontend/ui/widget/doublespinwidget.lua +++ b/frontend/ui/widget/doublespinwidget.lua @@ -77,14 +77,15 @@ function DoubleSpinWidget:init() }, } end + + -- Actually the widget layout self:update() end function DoubleSpinWidget:update() - -- This picker_update_callback will be redefined later. It is needed - -- so we can have our MovableContainer repainted on NumberPickerWidgets - -- update. It is needed if we have enabled transparency on MovableContainer, - -- otherwise the NumberPicker area gets opaque on update. + -- This picker_update_callback will be redefined later. + -- It's a hack to restore transparency after a Button unhighlight in NumberPicker, + -- in case the MovableContainer was actually made transparent. local picker_update_callback = function() end local left_widget = NumberPickerWidget:new{ show_parent = self, @@ -290,9 +291,25 @@ function DoubleSpinWidget:update() return "ui", self.widget_frame.dimen end) picker_update_callback = function() - UIManager:setDirty("all", function() - return "ui", self.movable.dimen - end) + -- If we're actually transparent, force an alpha-aware repaint. + if self.movable.alpha then + 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: -- self.callback(left_widget:getValue(), right_widget:getValue()) end @@ -305,7 +322,7 @@ end function DoubleSpinWidget:onCloseWidget() UIManager:setDirty(nil, function() - return "partial", self.widget_frame.dimen + return "ui", self.widget_frame.dimen end) return true end diff --git a/frontend/ui/widget/eventlistener.lua b/frontend/ui/widget/eventlistener.lua index ff41ae580..0fcbec4fc 100644 --- a/frontend/ui/widget/eventlistener.lua +++ b/frontend/ui/widget/eventlistener.lua @@ -28,6 +28,7 @@ By default, it's `"on"..Event.name`. ]] function EventListener:handleEvent(event) 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)) end end diff --git a/frontend/ui/widget/frontlightwidget.lua b/frontend/ui/widget/frontlightwidget.lua index 56361236c..a54afbe25 100644 --- a/frontend/ui/widget/frontlightwidget.lua +++ b/frontend/ui/widget/frontlightwidget.lua @@ -581,7 +581,7 @@ end function FrontLightWidget:onCloseWidget() UIManager:setDirty(nil, function() - return "flashpartial", self.light_frame.dimen + return "flashui", self.light_frame.dimen end) return true end diff --git a/frontend/ui/widget/horizontalgroup.lua b/frontend/ui/widget/horizontalgroup.lua index 23931a4a2..71cffbdbc 100644 --- a/frontend/ui/widget/horizontalgroup.lua +++ b/frontend/ui/widget/horizontalgroup.lua @@ -65,7 +65,8 @@ end function HorizontalGroup:clear() self:free() - WidgetContainer.clear(self) + -- Skip WidgetContainer:clear's free call, we just did that in our own free ;) + WidgetContainer.clear(self, true) end function HorizontalGroup:resetLayout() @@ -74,6 +75,7 @@ function HorizontalGroup:resetLayout() end function HorizontalGroup:free() + --print("HorizontalGroup:free on", self) self:resetLayout() WidgetContainer.free(self) end diff --git a/frontend/ui/widget/htmlboxwidget.lua b/frontend/ui/widget/htmlboxwidget.lua index 9ac7b0061..448533cf1 100644 --- a/frontend/ui/widget/htmlboxwidget.lua +++ b/frontend/ui/widget/htmlboxwidget.lua @@ -130,6 +130,7 @@ end -- (ie: in some other widget's update()), to not leak memory with -- BlitBuffer zombies function HtmlBoxWidget:free() + --print("HtmlBoxWidget:free on", self) self:freeBb() if self.document then diff --git a/frontend/ui/widget/iconbutton.lua b/frontend/ui/widget/iconbutton.lua index a2281cdbf..d24f4996c 100644 --- a/frontend/ui/widget/iconbutton.lua +++ b/frontend/ui/widget/iconbutton.lua @@ -108,15 +108,38 @@ function IconButton:onTapIconButton() UIManager:forceRePaint() --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 - if UIManager:getTopWidget() == self.show_parent or UIManager:isSubwidgetShown(self.show_parent) then - self.image.invert = false + self.image.invert = false + -- If the callback closed our parent (which may not always be the top-level widget, or even *a* window-level widget), we're done + 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:setDirty(nil, function() return "fast", self.dimen end) - --UIManager:forceRePaint() + else + -- Callback closed our parent, we're done + return true end + --UIManager:forceRePaint() end return true end diff --git a/frontend/ui/widget/imageviewer.lua b/frontend/ui/widget/imageviewer.lua index cb7b39eb4..64e22e668 100644 --- a/frontend/ui/widget/imageviewer.lua +++ b/frontend/ui/widget/imageviewer.lua @@ -36,16 +36,16 @@ local ImageViewer = InputContainer:new{ file = nil, -- or an already made BlitBuffer (ie: made by Mupdf.renderImageFile()) image = nil, - -- whether provided BlitBuffer should be free(), normally true - -- unless our caller wants to reuse it's provided image + -- whether the provided BlitBuffer should be free'd. Usually true, + -- unless our caller wants to reuse the image it provided image_disposable = true, -- 'image' can alternatively be a table (list) of multiple BlitBuffers -- (or functions returning BlitBuffers). -- The table will have its .free() called onClose according to -- the image_disposable provided here. - -- Each BlitBuffer in the table (or returned by functions) will be free() - -- if the table has itself an attribute image_disposable=true. + -- Each BlitBuffer in the table (or returned by functions) will be free'd + -- if the table itself has an image_disposable field set to true. -- With images list, when switching image, whether to keep previous -- image pan & zoom @@ -147,25 +147,12 @@ function ImageViewer:init() self._images_list_disposable = self.image_disposable self.image_disposable = self._images_list.image_disposable 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() - self:_clean_image_wg() -- clean previous if any + -- Widget layout 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 - local orig_dimen = self.main_frame and self.main_frame.dimen or Geom:new{} + local orig_dimen = Geom:new{} self.align = "center" self.region = Geom:new{ x = 0, y = 0, @@ -180,180 +167,348 @@ function ImageViewer:update() self.width = Screen:getWidth() - Screen:scaleBySize(40) end - local button_table_size = 0 - local button_container - if self.buttons_visible then - local buttons = { + -- Init the buttons no matter what + local buttons = { + { { - { - text = self._scale_to_fit and _("Original size") or _("Scale"), - callback = function() - self.scale_factor = self._scale_to_fit and 1 or 0 - self._scale_to_fit = not self._scale_to_fit - -- Reset center ratio (may have been modified if some panning was done) - self._center_x_ratio = 0.5 - self._center_y_ratio = 0.5 - self:update() - 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, - }, + id = "scale", + text = self._scale_to_fit and _("Original size") or _("Scale"), + callback = function() + self.scale_factor = self._scale_to_fit and 1 or 0 + self._scale_to_fit = not self._scale_to_fit + -- Reset center ratio (may have been modified if some panning was done) + self._center_x_ratio = 0.5 + self._center_y_ratio = 0.5 + self:update() + end, }, - } - local 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, - } - button_container = CenterContainer:new{ - dimen = Geom:new{ - w = self.width, - h = button_table:getSize().h, + { + id = "rotate", + text = self.rotated and _("No rotation") or _("Rotate"), + callback = function() + self.rotated = not self.rotated and true or false + self:update() + end, }, - button_table, - } - button_table_size = button_table:getSize().h + { + id = "close", + 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 -- 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 - -- Toggler (white arrow) for caption, on the left of title - local ctoggler - local ctoggler_width = 0 - if self.caption then - 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 - -- paddings chosen to align nicely with titlew - ctoggler = FrameContainer:new{ - bordersize = 0, - padding = self.title_padding, - padding_top = self.title_padding + Size.padding.small, - padding_right = 0, - TextWidget:new{ - text = ctoggler_text, - face = self.title_face, - } - } - ctoggler_width = ctoggler:getSize().w + self.img_container_h = self.img_container_h - self.full_title_bar:getSize().h - self.title_sep:getSize().h + end + + -- Init the progress bar no matter what + -- progress bar + local percent = 1 + if self._images_list and self._images_list_nb > 1 then + percent = (self._images_list_cur - 1) / (self._images_list_nb - 1) + end + self.progress_bar = ProgressWidget:new{ + width = self.width - 2*self.button_padding, + height = Screen:scaleBySize(5), + percentage = percent, + margin_h = 0, + margin_v = 0, + radius = 0, + ticks = nil, + last = nil, + } + self.progress_container = CenterContainer:new{ + dimen = Geom:new{ + 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 - local closeb = CloseButton:new{ window = self, padding_top = Size.padding.tiny, } - local 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 - closeb:getSize().w - ctoggler_width, + rotation_angle = rotate_clockwise and 90 or 270 + end + + self._image_wg = ImageWidget:new{ + file = self.file, + 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 if self.caption and self.caption_visible then - title_tbw_padding_bottom = 0 -- save room between title and caption - 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) + title_tbw_padding_bottom = 0 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 - local caption_tbw = TextBoxWidget:new{ - text = self.caption, - 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, - caption_tbw, - } - title_bar = VerticalGroup:new{ - align = "left", - title_bar, - captionw - } + self.full_title_bar = self.captioned_title_bar + else + self.full_title_bar = self.title_bar end - title_sep = LineWidget:new{ - dimen = Geom:new{ - 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 + + self.img_container_h = self.img_container_h - self.full_title_bar:getSize().h - self.title_sep:getSize().h end - local progress_container + -- Update the progress bar if self._images_list then - -- progress bar local percent = 1 if self._images_list_nb > 1 then percent = (self._images_list_cur - 1) / (self._images_list_nb - 1) end - local progress_bar = ProgressWidget:new{ - width = self.width - 2*self.button_padding, - height = Screen:scaleBySize(5), - percentage = percent, - 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 + + self.progress_bar:setPercentage(percent) + + self.img_container_h = self.img_container_h - self.progress_container:getSize().h end + -- Update the image widget itself -- 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 -- Otherwise, add paddings around image 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 end @@ -383,25 +538,26 @@ function ImageViewer:update() center_y_ratio = self._center_y_ratio, } - local image_container = CenterContainer:new{ + self.image_container = CenterContainer:new{ dimen = Geom:new{ w = self.width, - h = img_container_h, + h = self.img_container_h, }, self._image_wg, } + -- Update the final layout local frame_elements = VerticalGroup:new{ align = "left" } if self.with_title_bar then - table.insert(frame_elements, title_bar) - table.insert(frame_elements, title_sep) + table.insert(frame_elements, self.full_title_bar) + table.insert(frame_elements, self.title_sep) end - table.insert(frame_elements, image_container) - if progress_container then - table.insert(frame_elements, progress_container) + 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, button_container) + table.insert(frame_elements, self.button_container) end self.main_frame = FrameContainer:new{ @@ -420,14 +576,10 @@ function ImageViewer:update() 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) - logger.dbg("update image region", update_region) return "ui", update_region, true end) end @@ -442,7 +594,7 @@ end function ImageViewer:switchToImageNum(image_num) 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 = nil end @@ -470,7 +622,7 @@ function ImageViewer:onTap(_, ges) return self:onSaveImageView() 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:update() return true @@ -710,17 +862,38 @@ function ImageViewer:onAnyKeyPressed() end function ImageViewer:onCloseWidget() - -- clean all our BlitBuffer objects when UIManager:close() was called - self:_clean_image_wg() + -- Our ImageWidget (self._image_wg) is always a proper child widget, so it'll receive this event, + -- 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 - logger.dbg("ImageViewer:free(self.image)") + logger.dbg("ImageViewer:onCloseWidget: free self.image", self.image) self.image:free() self.image = nil end -- 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 + logger.dbg("ImageViewer:onCloseWidget: free self._images_list", self._images_list) self._images_list:free() 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 UIManager:setDirty(nil, function() return "flashui", self.main_frame.dimen diff --git a/frontend/ui/widget/imagewidget.lua b/frontend/ui/widget/imagewidget.lua index 4e9e9f841..01c0a0140 100644 --- a/frontend/ui/widget/imagewidget.lua +++ b/frontend/ui/widget/imagewidget.lua @@ -482,6 +482,7 @@ end -- (ie: in some other widget's update()), to not leak memory with -- BlitBuffer zombies 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 self._bb:free() self._bb = nil diff --git a/frontend/ui/widget/inputdialog.lua b/frontend/ui/widget/inputdialog.lua index bdb6ff403..12d8dec3f 100644 --- a/frontend/ui/widget/inputdialog.lua +++ b/frontend/ui/widget/inputdialog.lua @@ -491,7 +491,7 @@ end function InputDialog:onCloseWidget() self:onClose() UIManager:setDirty(nil, self.fullscreen and "full" or function() - return "partial", self.dialog_frame.dimen + return "ui", self.dialog_frame.dimen end) end diff --git a/frontend/ui/widget/keyboardlayoutdialog.lua b/frontend/ui/widget/keyboardlayoutdialog.lua index 520dffa4d..8fadc4439 100644 --- a/frontend/ui/widget/keyboardlayoutdialog.lua +++ b/frontend/ui/widget/keyboardlayoutdialog.lua @@ -155,7 +155,7 @@ end function KeyboardLayoutDialog:onCloseWidget() UIManager:setDirty(nil, function() - return "partial", self[1][1].dimen + return "ui", self[1][1].dimen end) return true end diff --git a/frontend/ui/widget/menu.lua b/frontend/ui/widget/menu.lua index 54708d192..9f1289cc6 100644 --- a/frontend/ui/widget/menu.lua +++ b/frontend/ui/widget/menu.lua @@ -488,13 +488,47 @@ function MenuItem:onTapSelect(arg, ges) --UIManager:waitForVSync() self[1].invert = false - -- We assume a tap anywhere updates the full menu, so, forgo this, much like in TouchMenu - --[[ - UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(nil, function() - return "ui", self[1].dimen - end) - --]] + + -- Most Menu entries will actually update the full menu, but they may also pop up a few various things, + -- so, pilfer a few heuristics from TouchMenu... + local top_widget = UIManager:getTopWidget() + -- If the callback opened a full-screen widget, we're done + 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() end return true @@ -518,10 +552,25 @@ function MenuItem:onHoldSelect(arg, ges) --UIManager:waitForVSync() self[1].invert = false - UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(nil, function() - return "ui", self[1].dimen - end) + + -- Same idea as for tap, minus the various things that make no sense for a hold callback... + local top_widget = UIManager:getTopWidget() + + -- 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() end return true @@ -949,8 +998,8 @@ function Menu:onCloseWidget() -- we cannot refresh regionally using the dimen field -- because some menus without menu title use VerticalGroup to include -- 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 - -- touch menu) in the filemanager in order to capture tap gesture to popup + -- For example, it's a dirty hack to use two menus (one being this menu and + -- the other touch menu) in the filemanager in order to capture tap gesture to popup -- the filemanager menu. -- NOTE: For the same reason, don't make it flash, -- because that'll trigger when we close the FM and open a book... diff --git a/frontend/ui/widget/multiconfirmbox.lua b/frontend/ui/widget/multiconfirmbox.lua index 4d85e2d36..8f6a77636 100644 --- a/frontend/ui/widget/multiconfirmbox.lua +++ b/frontend/ui/widget/multiconfirmbox.lua @@ -156,7 +156,7 @@ end function MultiConfirmBox:onCloseWidget() UIManager:setDirty(nil, function() - return "partial", self[1][1].dimen + return "ui", self[1][1].dimen end) end diff --git a/frontend/ui/widget/naturallightwidget.lua b/frontend/ui/widget/naturallightwidget.lua index b6bb28123..594b65ce5 100644 --- a/frontend/ui/widget/naturallightwidget.lua +++ b/frontend/ui/widget/naturallightwidget.lua @@ -377,7 +377,7 @@ end function NaturalLightWidget:onCloseWidget() self:closeKeyboard() UIManager:setDirty(nil, function() - return "partial", self.nl_frame.dimen + return "flashui", self.nl_frame.dimen end) -- Tell frontlight widget that we're closed self.fl_widget:naturalLightConfigClose() diff --git a/frontend/ui/widget/numberpickerwidget.lua b/frontend/ui/widget/numberpickerwidget.lua index f34eae54f..b8a0d2167 100644 --- a/frontend/ui/widget/numberpickerwidget.lua +++ b/frontend/ui/widget/numberpickerwidget.lua @@ -62,10 +62,8 @@ function NumberPickerWidget:init() self.value_index = self.value_index or 1 self.value = self.value_table[self.value_index] end - self:update() -end -function NumberPickerWidget:paintWidget() + -- Widget layout local bordersize = Size.border.default local margin = Size.margin.default local button_up = Button:new{ @@ -118,18 +116,19 @@ function NumberPickerWidget:paintWidget() local empty_space = VerticalSpan:new{ width = math.ceil(self.screen_height * 0.01) } - local value = self.value + + self.formatted_value = self.value if not self.value_table then - value = string.format(self.precision, value) + self.formatted_value = string.format(self.precision, self.formatted_value) end local input_dialog local callback_input = nil if self.value_table == nil then - callback_input = function() + callback_input = function() input_dialog = InputDialog:new{ title = _("Enter number"), - input = value, + input = self.formatted_value, input_type = "number", buttons = { { @@ -170,36 +169,33 @@ function NumberPickerWidget:paintWidget() }, }, } + self.update_callback() UIManager:show(input_dialog) input_dialog:onShowKeyboard() end end - local text_value = Button:new{ - text = tostring(value), + self.text_value = Button:new{ + text = tostring(self.formatted_value), bordersize = 0, padding = 0, text_font_face = self.spinner_face.font, text_font_size = self.spinner_face.orig_size, width = self.width, max_width = self.width, + show_parent = self.show_parent, callback = callback_input, } - return VerticalGroup:new{ + + local widget_spinner = VerticalGroup:new{ align = "center", button_up, empty_space, - text_value, + self.text_value, empty_space, button_down, } -end ---[[-- -Update. ---]] -function NumberPickerWidget:update() - local widget_spinner = self:paintWidget() self.frame = FrameContainer:new{ bordersize = 0, padding = Size.padding.default, @@ -217,6 +213,22 @@ function NumberPickerWidget:update() UIManager:setDirty(self.show_parent, function() return "ui", self.dimen 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() end diff --git a/frontend/ui/widget/openwithdialog.lua b/frontend/ui/widget/openwithdialog.lua index 441f2f01d..8bb1b377a 100644 --- a/frontend/ui/widget/openwithdialog.lua +++ b/frontend/ui/widget/openwithdialog.lua @@ -186,7 +186,7 @@ end function OpenWithDialog:onCloseWidget() UIManager:setDirty(nil, function() - return "partial", self[1][1].dimen + return "ui", self.dialog_frame.dimen end) return true end diff --git a/frontend/ui/widget/spinwidget.lua b/frontend/ui/widget/spinwidget.lua index 2c8e9b709..f93050109 100644 --- a/frontend/ui/widget/spinwidget.lua +++ b/frontend/ui/widget/spinwidget.lua @@ -72,14 +72,15 @@ function SpinWidget:init() }, } end + + -- Actually the widget layout self:update() end function SpinWidget:update() - -- This picker_update_callback will be redefined later. It is needed - -- so we can have our MovableContainer repainted on NumberPickerWidgets - -- update. It is needed if we have enabled transparency on MovableContainer, - -- otherwise the NumberPicker area gets opaque on update. + -- This picker_update_callback will be redefined later. + -- It's a hack to restore transparency after a Button unhighlight in NumberPicker, + -- in case the MovableContainer was actually made transparent. local picker_update_callback = function() end local value_widget = NumberPickerWidget:new{ show_parent = self, @@ -242,9 +243,25 @@ function SpinWidget:update() return "ui", self.spin_frame.dimen end) picker_update_callback = function() - UIManager:setDirty("all", function() - return "ui", self.movable.dimen - end) + -- If we're actually transparent, force an alpha-aware repaint. + if self.movable.alpha then + 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 @@ -255,7 +272,7 @@ end function SpinWidget:onCloseWidget() UIManager:setDirty(nil, function() - return "partial", self.spin_frame.dimen + return "ui", self.spin_frame.dimen end) return true end diff --git a/frontend/ui/widget/textboxwidget.lua b/frontend/ui/widget/textboxwidget.lua index b53554948..70df5271b 100644 --- a/frontend/ui/widget/textboxwidget.lua +++ b/frontend/ui/widget/textboxwidget.lua @@ -847,6 +847,10 @@ function TextBoxWidget:_renderImage(start_row_idx) local scheduled_update = self.scheduled_update 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 + -- No image, no dithering + if self.dialog then + self.dialog.dithered = false + end return -- no image on this page end 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() 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). - 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 - 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 local status_height = 0 @@ -965,7 +982,8 @@ function TextBoxWidget:_renderImage(start_row_idx) y = self.dimen.y, w = image.width, h = image.height, - } + }, + true -- Request dithering end) end end) @@ -983,7 +1001,8 @@ function TextBoxWidget:_renderImage(start_row_idx) y = self.dimen.y, w = image.width, h = image.height, - } + }, + true -- Request dithering end) end end @@ -1063,6 +1082,7 @@ function TextBoxWidget:onCloseWidget() end function TextBoxWidget:free(full) + --print("TextBoxWidget:free", full, "on", self) -- logger.dbg("TextBoxWidget:free called") -- We are called with full=false from other methods here whenever -- :_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 -- (we should not free it if full=false as it is re-usable across renderings) self._xtext:free() + self._xtext = nil -- logger.dbg("TextBoxWidget:_xtext:free()") end end @@ -1124,7 +1145,8 @@ function TextBoxWidget:onTapImage(arg, ges) y = self.dimen.y, w = image.width, h = image.height, - } + }, + not self.image_show_alt_text -- Request dithering when showing the image end) return true end diff --git a/frontend/ui/widget/textwidget.lua b/frontend/ui/widget/textwidget.lua index c9bbe59c1..46adaa9b2 100644 --- a/frontend/ui/widget/textwidget.lua +++ b/frontend/ui/widget/textwidget.lua @@ -363,9 +363,11 @@ function TextWidget:paintTo(bb, x, y) end function TextWidget:free() + --print("TextWidget:free on", self) -- Allow not waiting until Lua gc() to cleanup C XText malloc'ed stuff if self._xtext then self._xtext:free() + self._xtext = nil end end diff --git a/frontend/ui/widget/timewidget.lua b/frontend/ui/widget/timewidget.lua index e078046d3..63bd95aac 100644 --- a/frontend/ui/widget/timewidget.lua +++ b/frontend/ui/widget/timewidget.lua @@ -56,6 +56,8 @@ function TimeWidget:init() }, } end + + -- Actually the widget layout self:update() end @@ -191,7 +193,7 @@ end function TimeWidget:onCloseWidget() UIManager:setDirty(nil, function() - return "partial", self.time_frame.dimen + return "ui", self.time_frame.dimen end) return true end diff --git a/frontend/ui/widget/touchmenu.lua b/frontend/ui/widget/touchmenu.lua index 6503f8ddb..b5c0b207a 100644 --- a/frontend/ui/widget/touchmenu.lua +++ b/frontend/ui/widget/touchmenu.lua @@ -186,9 +186,16 @@ function TouchMenuItem:onTapSelect(arg, ges) return true 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) 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 end diff --git a/frontend/ui/widget/verticalgroup.lua b/frontend/ui/widget/verticalgroup.lua index d34419cc5..deec27d3f 100644 --- a/frontend/ui/widget/verticalgroup.lua +++ b/frontend/ui/widget/verticalgroup.lua @@ -60,7 +60,8 @@ end function VerticalGroup:clear() self:free() - WidgetContainer.clear(self) + -- Skip WidgetContainer:clear's free call, we just did that in our own free ;) + WidgetContainer.clear(self, true) end function VerticalGroup:resetLayout() @@ -69,6 +70,7 @@ function VerticalGroup:resetLayout() end function VerticalGroup:free() + --print("VerticalGroup:free on", self) self:resetLayout() WidgetContainer.free(self) end diff --git a/plugins/coverbrowser.koplugin/covermenu.lua b/plugins/coverbrowser.koplugin/covermenu.lua index 308094328..b28a33f8a 100644 --- a/plugins/coverbrowser.koplugin/covermenu.lua +++ b/plugins/coverbrowser.koplugin/covermenu.lua @@ -48,7 +48,6 @@ function CoverMenu:updateItems(select_number) local old_dimen = self.dimen and self.dimen:copy() -- self.layout must be updated for focusmanager self.layout = {} - self.item_group:free() -- avoid memory leaks by calling free() on all our sub-widgets self.item_group:clear() -- strange, best here if resetLayout() are done after _recalculateDimen(), -- unlike what is done in menu.lua