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
-- 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{

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

@ -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

@ -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

@ -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

@ -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., <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.
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,

@ -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" }

@ -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()

@ -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

@ -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

@ -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

@ -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,

@ -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

@ -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

@ -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

@ -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 <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
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 <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
@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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...

@ -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

@ -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()

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

Loading…
Cancel
Save