mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
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.
This commit is contained in:
2
base
2
base
Submodule base updated: 75b629d7ad...43b9a29679
@@ -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,192 @@ 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
|
||||
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,
|
||||
}
|
||||
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)
|
||||
end
|
||||
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
|
||||
}
|
||||
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
|
||||
-- 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
|
||||
-- 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.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 = 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 +382,25 @@ 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,
|
||||
}
|
||||
|
||||
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{
|
||||
@@ -427,7 +426,160 @@ function ImageViewer:update()
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
self.full_title_bar = self.captioned_title_bar
|
||||
else
|
||||
self.full_title_bar = self.title_bar
|
||||
end
|
||||
|
||||
self.img_container_h = self.img_container_h - self.full_title_bar:getSize().h - self.title_sep:getSize().h
|
||||
end
|
||||
|
||||
-- Update the progress bar
|
||||
if self._images_list then
|
||||
local percent = 1
|
||||
if self._images_list_nb > 1 then
|
||||
percent = (self._images_list_cur - 1) / (self._images_list_nb - 1)
|
||||
end
|
||||
|
||||
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 = 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
|
||||
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,
|
||||
}
|
||||
|
||||
-- Update the final layout
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
self.dithered = true
|
||||
UIManager:setDirty(self, function()
|
||||
local update_region = self.main_frame.dimen:combine(orig_dimen)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user