From 3060dc81af1054b3929de63b79af71b48a342874 Mon Sep 17 00:00:00 2001 From: NiLuJe Date: Sun, 10 Jan 2021 01:51:09 +0100 Subject: [PATCH] Revamp "flash_ui" handling (#7118) * Wherever possible, do an actual dumb invert on the Screen BB instead of repainting the widget, *then* inverting it (which is what the "invert" flag does). * Instead of playing with nextTick/tickAfterNext delays, explicitly fence stuff with forceRePaint * And, in the few cases where said Mk. 7 quirk kicks in, make the fences more marked by using a well-placed WAIT_FOR_UPDATE_COMPLETE * Fix an issue in Button where show/hide & enable/disable where actually all toggles, which meant that duplicate calls or timing issues would do the wrong thing. (This broke dimming some icons, and mistakenly dropped the background from FM chevrons, for example). * Speaking of, fix Button's hide/show to actually restore the background properly (there was a stupid typo in the variable name) * Still in Button, fix the insanity of the double repaint on rounded buttons. Turns out it made sense, after all (and was related to said missing background, and bad interaction with invert & text with no background). * KeyValuePage suffered from a similar issue with broken highlights (all black) because of missing backgrounds. * In ConfigDialog, only instanciate IconButtons once (because every tab switch causes a full instantiation; and the initial display implies a full instanciation and an initial tab switch). Otherwise, both instances linger, and catch taps, and as such, double highlights. * ConfigDialog: Restore the "don't repaint ReaderUI" when switching between similarly sized tabs (re #6131). I never could reproduce that on eInk, and I can't now on the emulator, so I'm assuming @poire-z fixed it during the swap to SVG icons. * KeyValuePage: Only instanciate Buttons once (again, this is a widget that goes through a full init every page). Again, caused highlight/dimming issues because buttons were stacked. * Menu: Ditto. * TouchMenu: Now home of the gnarliest unhilight heuristics, because of the sheer amount of different things that can happen (and/or thanks to stuff not flagged covers_fullscreen properly ;p). * Bump base https://github.com/koreader/koreader-base/pull/1280 https://github.com/koreader/koreader-base/pull/1282 https://github.com/koreader/koreader-base/pull/1283 https://github.com/koreader/koreader-base/pull/1284 * Bump android-luajit-launcher https://github.com/koreader/android-luajit-launcher/pull/284 https://github.com/koreader/android-luajit-launcher/pull/285 https://github.com/koreader/android-luajit-launcher/pull/286 https://github.com/koreader/android-luajit-launcher/pull/287 --- base | 2 +- frontend/apps/reader/skimtowidget.lua | 8 ++ frontend/ui/data/koptoptions.lua | 1 - frontend/ui/uimanager.lua | 62 ++++++++- frontend/ui/widget/button.lua | 104 +++++++++----- frontend/ui/widget/checkbutton.lua | 19 ++- frontend/ui/widget/configdialog.lua | 80 ++++++----- frontend/ui/widget/iconbutton.lua | 19 ++- frontend/ui/widget/imagewidget.lua | 2 +- frontend/ui/widget/infomessage.lua | 5 + frontend/ui/widget/keyvaluepage.lua | 37 +++-- frontend/ui/widget/menu.lua | 67 +++++---- frontend/ui/widget/radiobutton.lua | 25 ++-- frontend/ui/widget/touchmenu.lua | 129 +++++++++++------- frontend/util.lua | 27 ++++ platform/android/luajit-launcher | 2 +- .../statistics.koplugin/readerprogress.lua | 1 + 17 files changed, 397 insertions(+), 193 deletions(-) diff --git a/base b/base index 395936d25..dec3df4e5 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit 395936d25688e8bb19d67133ce1c24206f602f8e +Subproject commit dec3df4e5a96d7a039b8e6aeac4000c34c76a83a diff --git a/frontend/apps/reader/skimtowidget.lua b/frontend/apps/reader/skimtowidget.lua index bc16ad35b..f351108ea 100644 --- a/frontend/apps/reader/skimtowidget.lua +++ b/frontend/apps/reader/skimtowidget.lua @@ -123,6 +123,7 @@ function SkimToWidget:init() enabled = true, width = self.button_width, show_parent = self, + vsync = true, callback = function() self:goToPage(self.curr_page - 1) end, @@ -135,6 +136,7 @@ function SkimToWidget:init() enabled = true, width = self.button_width, show_parent = self, + vsync = true, callback = function() self:goToPage(self.curr_page - 10) end, @@ -147,6 +149,7 @@ function SkimToWidget:init() enabled = true, width = self.button_width, show_parent = self, + vsync = true, callback = function() self:goToPage(self.curr_page + 1) end, @@ -159,6 +162,7 @@ function SkimToWidget:init() enabled = true, width = self.button_width, show_parent = self, + vsync = true, callback = function() self:goToPage(self.curr_page + 10) end, @@ -193,6 +197,7 @@ function SkimToWidget:init() enabled = true, width = self.button_width, show_parent = self, + vsync = not G_reader_settings:isTrue("refresh_on_chapter_boundaries"), callback = function() local page = self.ui.toc:getNextChapter(self.curr_page) if page and page >=1 and page <= self.page_count then @@ -212,6 +217,7 @@ function SkimToWidget:init() enabled = true, width = self.button_width, show_parent = self, + vsync = not G_reader_settings:isTrue("refresh_on_chapter_boundaries"), callback = function() local page = self.ui.toc:getPreviousChapter(self.curr_page) if page and page >=1 and page <= self.page_count then @@ -231,6 +237,7 @@ function SkimToWidget:init() enabled = true, width = self.button_width, show_parent = self, + vsync = true, callback = function() self:goToByEvent("GotoNextBookmarkFromPage") end, @@ -248,6 +255,7 @@ function SkimToWidget:init() enabled = true, width = self.button_width, show_parent = self, + vsync = true, callback = function() self:goToByEvent("GotoPreviousBookmarkFromPage") end, diff --git a/frontend/ui/data/koptoptions.lua b/frontend/ui/data/koptoptions.lua index b0b661e68..db626874e 100644 --- a/frontend/ui/data/koptoptions.lua +++ b/frontend/ui/data/koptoptions.lua @@ -320,7 +320,6 @@ The first option ("auto") tries to automatically align reflowed text as it is in item_text = tableOfNumbersToTableOfStrings(FONT_SCALE_FACTORS), item_align_center = 1.0, spacing = 15, - height = 60, item_font_size = FONT_SCALE_DISPLAY_SIZE, args = FONT_SCALE_FACTORS, values = FONT_SCALE_FACTORS, diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index 489bfa073..8851b9027 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -7,7 +7,8 @@ local Event = require("ui/event") local Geom = require("ui/geometry") local dbg = require("dbg") local logger = require("logger") -local util = require("ffi/util") +local ffiUtil = require("ffi/util") +local util = require("util") local _ = require("gettext") local Input = Device.input local Screen = Device.screen @@ -528,7 +529,7 @@ dbg:guard(UIManager, 'schedule', --- Schedules task in a certain amount of seconds (fractions allowed) from now. function UIManager:scheduleIn(seconds, action, ...) - local when = { util.gettime() } + local when = { ffiUtil.gettime() } local s = math.floor(seconds) local usecs = (seconds - s) * MILLION when[1] = when[1] + s @@ -770,9 +771,42 @@ function UIManager:ToggleNightMode(night_mode) end end ---- Get top widget. +--- Get top widget (name if possible, ref otherwise). function UIManager:getTopWidget() - return ((self._window_stack[#self._window_stack] or {}).widget or {}).name + local top = self._window_stack[#self._window_stack] + if not top or not top.widget then + return nil + end + if top.widget.name then + return top.widget.name + end + return top.widget +end + +--- Check if a widget is still in the window stack, or is a subwidget of a widget still in the window stack +function UIManager:isSubwidgetShown(widget, max_depth) + for i = #self._window_stack, 1, -1 do + local matched, depth = util.arrayReferences(self._window_stack[i].widget, widget, max_depth) + if matched then + return matched, depth, self._window_stack[i].widget + end + end + return false +end + +--- Same, but only check window-level widgets (e.g., what's directly registered in the window stack), don't recurse +function UIManager:isWidgetShown(widget) + for i = #self._window_stack, 1, -1 do + if self._window_stack[i].widget == widget then + return true + end + end + return false +end + +-- Returns the region of the previous refresh +function UIManager:getPreviousRefreshRegion() + return self._last_refresh_region end --- Signals to quit. @@ -822,7 +856,7 @@ function UIManager:discardEvents(set_or_seconds) else -- we expect a number usecs = set_or_seconds * MILLION end - local now = { util.gettime() } + local now = { ffiUtil.gettime() } local now_us = now[1] * MILLION + now[2] self._discard_events_till = now_us + usecs end @@ -833,7 +867,7 @@ function UIManager:sendEvent(event) -- Ensure discardEvents if self._discard_events_till then - local now = { util.gettime() } + local now = { ffiUtil.gettime() } local now_us = now[1] * MILLION + now[2] if now_us < self._discard_events_till then return @@ -897,7 +931,7 @@ function UIManager:broadcastEvent(event) end function UIManager:_checkTasks() - local now = { util.gettime() } + local now = { ffiUtil.gettime() } local now_us = now[1] * MILLION + now[2] local wait_until = nil @@ -1193,6 +1227,8 @@ function UIManager:_repaint() 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, refresh.region.x, refresh.region.y, refresh.region.w, refresh.region.h, @@ -1212,6 +1248,10 @@ function UIManager:forceRePaint() self:_repaint() end +function UIManager:waitForVSync() + Screen:refreshWaitForLast() +end + -- Used to repaint a specific sub-widget that isn't on the _window_stack itself -- Useful to avoid repainting a complex widget when we just want to invert an icon, for instance. -- No safety checks on x & y *by design*. I want this to blow up if used wrong. @@ -1222,6 +1262,14 @@ function UIManager:widgetRepaint(widget, x, y) widget:paintTo(Screen.bb, x, y) end +-- Same idea, but does a simple invertRect, without actually repainting anything +function UIManager:widgetInvert(widget, x, y, w, h) + if not widget then return end + + logger.dbg("Explicit widgetInvert:", widget.name or widget.id or tostring(widget), "@ (", x, ",", y, ")") + Screen.bb:invertRect(x, y, w or widget.dimen.w, h or widget.dimen.h) +end + function UIManager:setInputTimeout(timeout) self.INPUT_TIMEOUT = timeout or 200*1000 end diff --git a/frontend/ui/widget/button.lua b/frontend/ui/widget/button.lua index b584dd052..f9336caf3 100644 --- a/frontend/ui/widget/button.lua +++ b/frontend/ui/widget/button.lua @@ -40,6 +40,7 @@ local Button = InputContainer:new{ preselect = false, callback = nil, enabled = true, + hidden = false, allow_hold_when_disabled = false, margin = 0, bordersize = Size.border.button, @@ -53,6 +54,7 @@ local Button = InputContainer:new{ text_font_face = "cfont", text_font_size = 20, text_font_bold = true, + vsync = nil, -- when "flash_ui" is enabled, allow bundling the highlight with the callback, and fence that batch away from the unhighlight. Avoid delays when callback requires a "partial" on Kobo Mk. 7, c.f., ffi/framebuffer_mxcfb for more details. } function Button:init() @@ -164,28 +166,26 @@ function Button:onUnfocus() end function Button:enable() - self.enabled = true - if self.text then - if self.enabled then + if not self.enabled then + if self.text then self.label_widget.fgcolor = Blitbuffer.COLOR_BLACK + self.enabled = true else - self.label_widget.fgcolor = Blitbuffer.COLOR_DARK_GRAY + self.label_widget.dim = false + self.enabled = true end - else - self.label_widget.dim = not self.enabled end end function Button:disable() - self.enabled = false - if self.text then - if self.enabled then - self.label_widget.fgcolor = Blitbuffer.COLOR_BLACK - else + if self.enabled then + if self.text then self.label_widget.fgcolor = Blitbuffer.COLOR_DARK_GRAY + self.enabled = false + else + self.label_widget.dim = true + self.enabled = false end - else - self.label_widget.dim = not self.enabled end end @@ -198,17 +198,19 @@ function Button:enableDisable(enable) end function Button:hide() - if self.icon then - self.frame.orig_background = self[1].background + if self.icon and not self.hidden then + self.frame.orig_background = self.frame.background self.frame.background = nil self.label_widget.hide = true + self.hidden = true end end function Button:show() - if self.icon then + if self.icon and self.hidden then self.label_widget.hide = false - self.frame.background = self[1].old_background + self.frame.background = self.frame.orig_background + self.hidden = false end end @@ -225,42 +227,74 @@ function Button:onTapSelectButton() if G_reader_settings:isFalse("flash_ui") then self.callback() else - -- For most of our buttons, we can't avoid that initial repaint... - self[1].invert = true - UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) - -- NOTE: This completely insane double repaint is needed to avoid cosmetic issues with FrameContainer's rounded corners on Text buttons... - -- On the upside, we now actually get to *see* those rounded corners (as the highlight), where it was a simple square before. - -- c.f., #4554 & #4541 -- 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). -- The nil check is to discriminate the default from callers that explicitly request a specific radius. if self[1].radius == nil then self[1].radius = Size.radius.button + -- And here, it's easier to just invert the bg/fg colors ourselves, + -- so as to preserve the rounded corners in one step. + self[1].background = self[1].background:invert() + self.label_widget.fgcolor = self.label_widget.fgcolor:invert() + -- We do *NOT* set the invert flag, because it just adds an invertRect step at the end of the paintTo process, + -- and we've already taken care of inversion in a way that won't mangle the rounded corners. + else + self[1].invert = 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 + UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) end UIManager:setDirty(nil, function() return "fast", self[1].dimen end) - -- And we also often have to delay the callback to both see the flash and/or avoid tearing artefacts w/ fast refreshes... - UIManager:tickAfterNext(function() - self.callback() - if not self[1] or not self[1].invert or not self[1].dimen then - -- widget no more there (destroyed, re-init'ed by setText(), or not inverted: nothing to invert back - return - end - self[1].invert = false - -- Since we kill the corners, we only need a single repaint. + -- Force the repaint *now*, so we don't have to delay the callback to see the highlight... + if not self.vsync then + -- NOTE: Allow bundling the highlight with the callback when we request vsync, to prevent further delays + UIManager:forceRePaint() -- Ensures we have a chance to see the highlight + end + self.callback() + UIManager:forceRePaint() -- Ensures whatever the callback wanted to paint will be shown *now*... + if self.vsync then + -- NOTE: This is mainly useful when the callback caused a REAGL update that we do not explicitly fence already, + -- (i.e., Kobo Mk. 7). + UIManager:waitForVSync() -- ...and that the EPDC will not wait to coalesce it with the *next* update, + -- 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 the widget no longer exists (destroyed, re-init'ed by setText(), or not inverted: nothing to invert back + return true + end + + -- If the callback closed our parent (which ought to have been the top level widget), abort early + if UIManager:getTopWidget() ~= self.show_parent then + return true + end + + self[1].invert = false + if self.text then if self[1].radius == Size.radius.button then self[1].radius = nil + self[1].background = self[1].background:invert() + self.label_widget.fgcolor = self.label_widget.fgcolor:invert() end + UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(nil, function() - return "fast", self[1].dimen - end) + else + UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) + end + -- If the button was disabled, switch to UI to make sure the gray comes through unharmed ;). + UIManager:setDirty(nil, function() + return self.enabled and "fast" or "ui", self[1].dimen end) + --UIManager:forceRePaint() -- Ensures the unhighlight happens now, instead of potentially waiting and having it batched with something else. end elseif self.tap_input then self:onInput(self.tap_input) diff --git a/frontend/ui/widget/checkbutton.lua b/frontend/ui/widget/checkbutton.lua index f5664a3a3..a9ec0fbd2 100644 --- a/frontend/ui/widget/checkbutton.lua +++ b/frontend/ui/widget/checkbutton.lua @@ -104,14 +104,19 @@ function CheckButton:onTapCheckButton() UIManager:setDirty(nil, function() return "fast", self.dimen end) - UIManager:tickAfterNext(function() - self.callback() - self[1].invert = false - UIManager:widgetRepaint(self[1], self.dimen.x, self.dimen.y) - UIManager:setDirty(nil, function() - return "fast", self.dimen - end) + + -- Force the repaint *now*, so we don't have to delay the callback to see the invert... + UIManager:forceRePaint() + self.callback() + --UIManager:forceRePaint() -- Unnecessary, the check/uncheck process involves too many repaints already + --UIManager:waitForVSync() + + self[1].invert = false + UIManager:widgetRepaint(self[1], self.dimen.x, self.dimen.y) + UIManager:setDirty(nil, function() + return "fast", self.dimen end) + --UIManager:forceRePaint() end elseif self.tap_input then self:onInput(self.tap_input) diff --git a/frontend/ui/widget/configdialog.lua b/frontend/ui/widget/configdialog.lua index c9290e944..5f614fbe3 100644 --- a/frontend/ui/widget/configdialog.lua +++ b/frontend/ui/widget/configdialog.lua @@ -623,7 +623,7 @@ function ConfigOption:init() -- Add it to our CenterContainer table.insert(option_items_container, option_items_group) --add line of item to the second last place in the focusmanager so the menubar stay at the bottom - table.insert(self.config.layout, #self.config.layout,self:_itemGroupToLayoutLine(option_items_group)) + table.insert(self.config.layout, #self.config.layout, self:_itemGroupToLayoutLine(option_items_group)) table.insert(horizontal_group, option_items_container) table.insert(vertical_group, horizontal_group) end -- if show ~= false @@ -672,41 +672,44 @@ local MenuBar = FrameContainer:new{ padding = 0, background = Blitbuffer.COLOR_WHITE, } + function MenuBar:init() local icon_sep_width = Size.padding.button local line_thickness = Size.line.thick local config_options = self.config_dialog.config_options - local menu_items = {} local icon_width = Screen:scaleBySize(DGENERIC_ICON_SIZE) local icon_height = icon_width local icons_width = (icon_width + 2*icon_sep_width) * #config_options local bar_height = icon_height + 2*Size.padding.default - for c = 1, #config_options do - local menu_icon = IconButton:new{ - show_parent = self.config_dialog, - icon = config_options[c].icon, - width = icon_width, - height = icon_height, - callback = function() - self.config_dialog:handleEvent(Event:new("ShowConfigPanel", c)) - end, - } - menu_items[c] = menu_icon + if not self.menu_items then + self.menu_items = {} + for c = 1, #config_options do + local menu_icon = IconButton:new{ + show_parent = self.config_dialog, + icon = config_options[c].icon, + width = icon_width, + height = icon_height, + callback = function() + self.config_dialog:handleEvent(Event:new("ShowConfigPanel", c)) + end, + } + self.menu_items[c] = menu_icon + end end - table.insert(self.config_dialog.layout,menu_items) --for the focusmanager + table.insert(self.config_dialog.layout, self.menu_items) -- for the focusmanager local available_width = Screen:getWidth() - icons_width - -- local padding = math.floor(available_width / #menu_items / 2) -- all for padding - -- local padding = math.floor(available_width / #menu_items / 2 / 2) -- half padding, half spacing ? - local padding = math.min(math.floor(available_width / #menu_items / 2), Screen:scaleBySize(20)) -- as in TouchMenuBar + -- local padding = math.floor(available_width / #self.menu_items / 2) -- all for padding + -- local padding = math.floor(available_width / #self.menu_items / 2 / 2) -- half padding, half spacing ? + local padding = math.min(math.floor(available_width / #self.menu_items / 2), Screen:scaleBySize(20)) -- as in TouchMenuBar if padding > 0 then - for c = 1, #menu_items do - menu_items[c].padding_left = padding - menu_items[c].padding_right = padding - menu_items[c]:update() + for c = 1, #self.menu_items do + self.menu_items[c].padding_left = padding + self.menu_items[c].padding_right = padding + self.menu_items[c]:update() end - available_width = available_width - 2*padding*#menu_items + available_width = available_width - 2*padding*#self.menu_items end - local spacing_width = math.ceil(available_width / (#menu_items+1)) + local spacing_width = math.ceil(available_width / (#self.menu_items+1)) local icon_sep_black = LineWidget:new{ background = Blitbuffer.COLOR_BLACK, @@ -740,17 +743,17 @@ function MenuBar:init() local menu_bar = HorizontalGroup:new{} local line_bar = HorizontalGroup:new{} - for c = 1, #menu_items do + for c = 1, #self.menu_items do table.insert(menu_bar, spacing) table.insert(line_bar, spacing_line) if c == self.panel_index then table.insert(menu_bar, icon_sep_black) table.insert(line_bar, sep_line) - table.insert(menu_bar, menu_items[c]) + table.insert(menu_bar, self.menu_items[c]) table.insert(line_bar, LineWidget:new{ background = Blitbuffer.COLOR_WHITE, dimen = Geom:new{ - w = menu_items[c]:getSize().w, + w = self.menu_items[c]:getSize().w, h = line_thickness, } }) @@ -759,10 +762,10 @@ function MenuBar:init() else table.insert(menu_bar, icon_sep_white) table.insert(line_bar, sep_line) - table.insert(menu_bar, menu_items[c]) + table.insert(menu_bar, self.menu_items[c]) table.insert(line_bar, LineWidget:new{ dimen = Geom:new{ - w = menu_items[c]:getSize().w, + w = self.menu_items[c]:getSize().w, h = line_thickness, } }) @@ -779,7 +782,6 @@ function MenuBar:init() menu_bar, } table.insert(self, vertical_menu) - end --[[ @@ -853,14 +855,22 @@ end function ConfigDialog:update() self.layout = {} - self.config_menubar = MenuBar:new{ - config_dialog = self, - panel_index = self.panel_index, - } + + if self.config_menubar then + self.config_menubar:clear() + self.config_menubar.panel_index = self.panel_index + self.config_menubar:init() + else + self.config_menubar = MenuBar:new{ + config_dialog = self, + panel_index = self.panel_index, + } + end self.config_panel = ConfigPanel:new{ index = self.panel_index, config_dialog = self, } + self.dialog_frame = FrameContainer:new{ background = Blitbuffer.COLOR_WHITE, padding_bottom = 0, -- ensured by MenuBar @@ -896,9 +906,7 @@ function ConfigDialog:onShowConfigPanel(index) -- NOTE: And we also only need to repaint what's behind us when switching to a smaller dialog... -- This is trickier than in touchmenu, because dimen appear to fluctuate before/after painting... -- So we've settled instead for the amount of lines in the panel, as line-height is constant. - -- NOTE: line/widget-height is actually not constant (e.g. the font size widget on the emulator), - -- so do it only when the new nb of widgets is strictly greater than the previous one. - local keep_bg = old_layout_h and #self.layout > old_layout_h + local keep_bg = old_layout_h and #self.layout >= old_layout_h UIManager:setDirty((self.is_fresh or keep_bg) and self or "all", function() local refresh_dimen = old_dimen and old_dimen:combine(self.dialog_frame.dimen) diff --git a/frontend/ui/widget/iconbutton.lua b/frontend/ui/widget/iconbutton.lua index 277553d07..a2281cdbf 100644 --- a/frontend/ui/widget/iconbutton.lua +++ b/frontend/ui/widget/iconbutton.lua @@ -97,19 +97,26 @@ function IconButton:onTapIconButton() else self.image.invert = true -- For ConfigDialog icons, we can't avoid that initial repaint... - UIManager:widgetRepaint(self.image, self.dimen.x + self.padding_left, self.dimen.y + self.padding_top) + UIManager:widgetInvert(self.image, self.dimen.x + self.padding_left, self.dimen.y + self.padding_top) UIManager:setDirty(nil, function() return "fast", self.dimen end) - -- And, we usually need to delay the callback for the same reasons as Button... - UIManager:tickAfterNext(function() - self.callback() + + -- Force the repaint *now*, so we don't have to delay the callback to see the invert... + UIManager:forceRePaint() + self.callback() + 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 - UIManager:widgetRepaint(self.image, self.dimen.x + self.padding_left, self.dimen.y + self.padding_top) + UIManager:widgetInvert(self.image, self.dimen.x + self.padding_left, self.dimen.y + self.padding_top) UIManager:setDirty(nil, function() return "fast", self.dimen end) - end) + --UIManager:forceRePaint() + end end return true end diff --git a/frontend/ui/widget/imagewidget.lua b/frontend/ui/widget/imagewidget.lua index 71be91f92..4e9e9f841 100644 --- a/frontend/ui/widget/imagewidget.lua +++ b/frontend/ui/widget/imagewidget.lua @@ -182,7 +182,7 @@ function ImageWidget:_loadfile() -- Now, if that was *also* one of our icons, and it has an alpha channel, -- compose it against a background-colored BB now, and cache *that*. -- This helps us avoid repeating alpha-blending steps down the line, - -- and also ensures icon highlights/unhilights behave sensibly. + -- and also ensures icon highlights/unhighlights behave sensibly. if self.is_icon then local bbtype = self._bb:getType() if bbtype == Blitbuffer.TYPE_BB8A or bbtype == Blitbuffer.TYPE_BBRGB32 then diff --git a/frontend/ui/widget/infomessage.lua b/frontend/ui/widget/infomessage.lua index 909e44230..ac6d2691b 100644 --- a/frontend/ui/widget/infomessage.lua +++ b/frontend/ui/widget/infomessage.lua @@ -196,6 +196,10 @@ function InfoMessage:init() end function InfoMessage:onCloseWidget() + if self._delayed_show_action then + UIManager:unschedule(self._delayed_show_action) + self._delayed_show_action = nil + end if self.invisible then -- Still invisible, no setDirty needed return true @@ -253,6 +257,7 @@ end function InfoMessage:dismiss() if self._delayed_show_action then UIManager:unschedule(self._delayed_show_action) + self._delayed_show_action = nil end self.dismiss_callback() UIManager:close(self) diff --git a/frontend/ui/widget/keyvaluepage.lua b/frontend/ui/widget/keyvaluepage.lua index 966146748..6a183f673 100644 --- a/frontend/ui/widget/keyvaluepage.lua +++ b/frontend/ui/widget/keyvaluepage.lua @@ -252,6 +252,7 @@ function KeyValueItem:init() self[1] = FrameContainer:new{ padding = frame_padding, bordersize = 0, + background = Blitbuffer.COLOR_WHITE, HorizontalGroup:new{ dimen = self.dimen:copy(), LeftContainer:new{ @@ -281,17 +282,33 @@ function KeyValueItem:onTap() self.callback() else self[1].invert = true - UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) + UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) UIManager:setDirty(nil, function() return "fast", self[1].dimen end) - UIManager:tickAfterNext(function() - self.callback() + + -- Force the repaint *now*, so we don't have to delay the callback to see the invert... + UIManager:forceRePaint() + self.callback() + UIManager:forceRePaint() + --UIManager:waitForVSync() + + -- Has to be scheduled *after* the dict delays for the lookup history pages... + UIManager:scheduleIn(0.75, function() self[1].invert = false - UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) + -- Skip the repaint if we've ended up below something, which is likely. + if UIManager:getTopWidget() ~= self.show_parent then + -- It's generally tricky to get accurate dimensions out of whatever was painted above us, + -- so cheat by comparing against the previous refresh region... + if self[1].dimen:intersectWith(UIManager:getPreviousRefreshRegion()) then + return true + end + end + UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) UIManager:setDirty(nil, function() return "ui", self[1].dimen end) + --UIManager:forceRePaint() end) end end @@ -351,7 +368,7 @@ function KeyValuePage:init() -- return button --- @todo: alternative icon if BD.mirroredUILayout() - self.page_return_arrow = Button:new{ + self.page_return_arrow = self.page_return_arrow or Button:new{ icon = "back.top", callback = function() self:onReturn() end, bordersize = 0, @@ -366,25 +383,25 @@ function KeyValuePage:init() chevron_left, chevron_right = chevron_right, chevron_left chevron_first, chevron_last = chevron_last, chevron_first end - self.page_info_left_chev = Button:new{ + self.page_info_left_chev = self.page_info_left_chev or Button:new{ icon = chevron_left, callback = function() self:prevPage() end, bordersize = 0, show_parent = self, } - self.page_info_right_chev = Button:new{ + self.page_info_right_chev = self.page_info_right_chev or Button:new{ icon = chevron_right, callback = function() self:nextPage() end, bordersize = 0, show_parent = self, } - self.page_info_first_chev = Button:new{ + self.page_info_first_chev = self.page_info_first_chev or Button:new{ icon = chevron_first, callback = function() self:goToPage(1) end, bordersize = 0, show_parent = self, } - self.page_info_last_chev = Button:new{ + self.page_info_last_chev = self.page_info_last_chev or Button:new{ icon = chevron_last, callback = function() self:goToPage(self.pages) end, bordersize = 0, @@ -408,7 +425,7 @@ function KeyValuePage:init() self.page_info_first_chev:hide() self.page_info_last_chev:hide() - self.page_info_text = Button:new{ + self.page_info_text = self.page_info_text or Button:new{ text = "", hold_input = { title = _("Enter page number"), diff --git a/frontend/ui/widget/menu.lua b/frontend/ui/widget/menu.lua index 473530134..54708d192 100644 --- a/frontend/ui/widget/menu.lua +++ b/frontend/ui/widget/menu.lua @@ -472,22 +472,30 @@ function MenuItem:onTapSelect(arg, ges) coroutine.resume(co) else self[1].invert = true - UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) + UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) UIManager:setDirty(nil, function() return "fast", self[1].dimen end) - UIManager:tickAfterNext(function() - logger.dbg("creating coroutine for menu select") - local co = coroutine.create(function() - self.menu:onMenuSelect(self.table, pos) - end) - coroutine.resume(co) - self[1].invert = false - --UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(self.show_parent, function() - return "ui", self[1].dimen - end) + + -- Force the repaint *now*, so we don't have to delay the callback to see the invert... + UIManager:forceRePaint() + logger.dbg("creating coroutine for menu select") + local co = coroutine.create(function() + self.menu:onMenuSelect(self.table, pos) + end) + coroutine.resume(co) + UIManager:forceRePaint() + --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) + --]] + --UIManager:forceRePaint() end return true end @@ -498,18 +506,23 @@ function MenuItem:onHoldSelect(arg, ges) self.menu:onMenuHold(self.table, pos) else self[1].invert = true - UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) + UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) UIManager:setDirty(nil, function() return "fast", self[1].dimen end) - UIManager:tickAfterNext(function() - self.menu:onMenuHold(self.table, pos) - self[1].invert = false - --UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(self.show_parent, function() - return "ui", self[1].dimen - end) + + -- Force the repaint *now*, so we don't have to delay the callback to see the invert... + UIManager:forceRePaint() + self.menu:onMenuHold(self.table, pos) + UIManager:forceRePaint() + --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) + --UIManager:forceRePaint() end return true end @@ -666,25 +679,25 @@ function Menu:init() chevron_left, chevron_right = chevron_right, chevron_left chevron_first, chevron_last = chevron_last, chevron_first end - self.page_info_left_chev = Button:new{ + self.page_info_left_chev = self.page_info_left_chev or Button:new{ icon = chevron_left, callback = function() self:onPrevPage() end, bordersize = 0, show_parent = self.show_parent, } - self.page_info_right_chev = Button:new{ + self.page_info_right_chev = self.page_info_right_chev or Button:new{ icon = chevron_right, callback = function() self:onNextPage() end, bordersize = 0, show_parent = self.show_parent, } - self.page_info_first_chev = Button:new{ + self.page_info_first_chev = self.page_info_first_chev or Button:new{ icon = chevron_first, callback = function() self:onFirstPage() end, bordersize = 0, show_parent = self.show_parent, } - self.page_info_last_chev = Button:new{ + self.page_info_last_chev = self.page_info_last_chev or Button:new{ icon = chevron_last, callback = function() self:onLastPage() end, bordersize = 0, @@ -752,10 +765,10 @@ function Menu:init() end end - self.page_info_text = Button:new{ + self.page_info_text = self.page_info_text or Button:new{ text = "", hold_input = { - title = title_goto , + title = title_goto, type = type_goto, hint_func = hint_func, buttons = buttons, @@ -776,7 +789,7 @@ function Menu:init() } -- return button - self.page_return_arrow = Button:new{ + self.page_return_arrow = self.page_return_arrow or Button:new{ icon = "back.top", callback = function() if self.onReturn then self:onReturn() end diff --git a/frontend/ui/widget/radiobutton.lua b/frontend/ui/widget/radiobutton.lua index 43e50d2df..1466d3fad 100644 --- a/frontend/ui/widget/radiobutton.lua +++ b/frontend/ui/widget/radiobutton.lua @@ -119,14 +119,19 @@ function RadioButton:onTapCheckButton() UIManager:setDirty(nil, function() return "fast", self.dimen end) - UIManager:tickAfterNext(function() - self.callback() - self.frame.invert = false - UIManager:widgetRepaint(self.frame, self.dimen.x, self.dimen.y) - UIManager:setDirty(nil, function() - return "fast", self.dimen - end) + + -- Force the repaint *now*, so we don't have to delay the callback to see the invert... + UIManager:forceRePaint() + self.callback() + --UIManager:forceRePaint() -- Unnecessary, the check/uncheck process involves too many repaints already + --UIManager:waitForVSync() + + self.frame.invert = false + UIManager:widgetRepaint(self.frame, self.dimen.x, self.dimen.y) + UIManager:setDirty(nil, function() + return "fast", self.dimen end) + --UIManager:forceRePaint() end elseif self.tap_input then self:onInput(self.tap_input) @@ -151,7 +156,8 @@ function RadioButton:check(callback) self._radio_button = self._checked_widget self.checked = true self:update() - UIManager:setDirty(self.parent, function() + UIManager:widgetRepaint(self.frame, self.dimen.x, self.dimen.y) + UIManager:setDirty(nil, function() return "fast", self.dimen end) end @@ -160,7 +166,8 @@ function RadioButton:unCheck() self._radio_button = self._unchecked_widget self.checked = false self:update() - UIManager:setDirty(self.parent, function() + UIManager:widgetRepaint(self.frame, self.dimen.x, self.dimen.y) + UIManager:setDirty(nil, function() return "fast", self.dimen end) end diff --git a/frontend/ui/widget/touchmenu.lua b/frontend/ui/widget/touchmenu.lua index cb3eaf3c2..79f1a8283 100644 --- a/frontend/ui/widget/touchmenu.lua +++ b/frontend/ui/widget/touchmenu.lua @@ -158,26 +158,51 @@ function TouchMenuItem:onTapSelect(arg, ges) if G_reader_settings:isFalse("flash_ui") then self.menu:onMenuSelect(self.item) else + -- The item frame's width stops at the text width, but we want it to match the menu's length instead + local highlight_dimen = self.item_frame.dimen + highlight_dimen.w = self.item_frame.width + self.item_frame.invert = true - UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) + UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) UIManager:setDirty(nil, function() - return "fast", self.dimen + return "fast", highlight_dimen end) - -- yield to main UI loop to invert item - UIManager:tickAfterNext(function() - self.menu:onMenuSelect(self.item) - self.item_frame.invert = false - -- NOTE: We can *usually* optimize that repaint away, as most entries in the menu will at least trigger a menu repaint ;). - -- But when stuff doesn't repaint the menu and keeps it open, we need to do it. - -- Since it's an *un*highlight containing text, we make it "ui" and not "fast", both so it won't mangle text, - -- and because "fast" can have some weird side-effects on some devices in this specific instance... - if self.item.hold_keep_menu_open or self.item.keep_menu_open then - --UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) + + -- Force the repaint *now*, so we don't have to delay the callback to see the invert... + UIManager:forceRePaint() + self.menu:onMenuSelect(self.item) + UIManager:forceRePaint() + --UIManager:waitForVSync() + + self.item_frame.invert = false + -- NOTE: We can *usually* optimize that repaint away, as most entries in the menu will at least trigger a menu repaint ;). + -- But when stuff doesn't repaint the menu and keeps it open, we need to do it. + -- Since it's an *un*highlight containing text, we make it "ui" and not "fast", both so it won't mangle text, + -- and because "fast" can have some weird side-effects on some devices in this specific instance... + if self.item.hold_keep_menu_open or self.item.keep_menu_open then + 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, or if a modal was opened outside of our highlight region, we can unhighlight safely + if top_widget == self.menu or highlight_dimen:notIntersectWith(UIManager:getPreviousRefreshRegion()) then + UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) + UIManager:setDirty(nil, function() + return "ui", highlight_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.dimen + return "ui", highlight_dimen end) end - end) + end + --UIManager:forceRePaint() end return true end @@ -192,22 +217,28 @@ function TouchMenuItem:onHoldSelect(arg, ges) if G_reader_settings:isFalse("flash_ui") then self.menu:onMenuHold(self.item) else + -- The item frame's width stops at the text width, but we want it to match the menu's length instead + local highlight_dimen = self.item_frame.dimen + highlight_dimen.w = self.item_frame.width + self.item_frame.invert = true - UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) + UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) UIManager:setDirty(nil, function() - return "fast", self.dimen + return "fast", highlight_dimen end) - UIManager:tickAfterNext(function() - self.menu:onMenuHold(self.item) - end) - UIManager:scheduleIn(0.5, function() - self.item_frame.invert = false - -- NOTE: For some reason, this is finicky (I end up with a solid black bar, i.e., text gets inverted, but not the bg?!) - --UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(self.show_parent, function() - return "ui", self.dimen - end) + + -- Force the repaint *now*, so we don't have to delay the callback to see the invert... + UIManager:forceRePaint() + self.menu:onMenuHold(self.item) + UIManager:forceRePaint() + --UIManager:waitForVSync() + + self.item_frame.invert = false + UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) + UIManager:setDirty(nil, function() + return "ui", highlight_dimen end) + --UIManager:forceRePaint() end return true end @@ -452,7 +483,7 @@ function TouchMenu:init() self.key_events.Press = { {"Press"}, doc = "chose selected item" } local icons = {} - for _,v in ipairs(self.tab_item_table) do + for _, v in ipairs(self.tab_item_table) do table.insert(icons, v.icon) end self.bar = TouchMenuBar:new{ @@ -798,20 +829,16 @@ function TouchMenu:onMenuSelect(item) callback = item.callback_func() end if callback then - -- put stuff in scheduler so we can see - -- the effect of inverted menu item - UIManager:tickAfterNext(function() - -- Provide callback with us, so it can call our - -- closemenu() or updateItems() when it sees fit - -- (if not providing checked or checked_fund, caller - -- must set keep_menu_open=true if that is wished) - callback(self) - if refresh then - self:updateItems() - elseif not item.keep_menu_open then - self:closeMenu() - end - end) + -- Provide callback with us, so it can call our + -- closemenu() or updateItems() when it sees fit + -- (if not providing checked or checked_fund, caller + -- must set keep_menu_open=true if that is wished) + callback(self) + if refresh then + self:updateItems() + elseif not item.keep_menu_open then + self:closeMenu() + end end else table.insert(self.item_table_stack, self.item_table) @@ -842,17 +869,15 @@ function TouchMenu:onMenuHold(item) callback = item.hold_callback_func() end if callback then - UIManager:tickAfterNext(function() - -- With hold, the default is to keep menu open, as we're - -- most often showing a ConfirmBox that can be cancelled - -- (provide hold_keep_menu_open=false to override) - if item.hold_keep_menu_open == false then - self:closeMenu() - end - -- Provide callback with us, so it can call our - -- closemenu() or updateItems() when it sees fit - callback(self) - end) + -- With hold, the default is to keep menu open, as we're + -- most often showing a ConfirmBox that can be cancelled + -- (provide hold_keep_menu_open=false to override) + if item.hold_keep_menu_open == false then + self:closeMenu() + end + -- Provide callback with us, so it can call our + -- closemenu() or updateItems() when it sees fit + callback(self) end elseif item.help_text or type(item.help_text_func) == "function" then local help_text = item.help_text diff --git a/frontend/util.lua b/frontend/util.lua index a2792c61d..1abc154e2 100644 --- a/frontend/util.lua +++ b/frontend/util.lua @@ -384,6 +384,33 @@ function util.arrayContains(t, v, cb) return false end +--- Test whether array t contains a reference to array n (at any depth at or below m) +---- @param t Lua table (array only) +---- @param n Lua table (array only) +---- @int m Max nesting level +function util.arrayReferences(t, n, m, l) + if not m then m = 15 end + if not l then l = 0 end + if l > m then + return false + end + + if type(t) == "table" then + if t == n then + return true, l + end + + for _, v in ipairs(t) do + local matched, depth = util.arrayReferences(v, n, m, l + 1) + if matched then + return matched, depth + end + end + end + + return false +end + -- Merge t2 into t1, overwriting existing elements if they already exist -- Probably not safe with nested tables (c.f., https://stackoverflow.com/q/1283388) ---- @param t1 Lua table diff --git a/platform/android/luajit-launcher b/platform/android/luajit-launcher index af9b9d30c..1884a151f 160000 --- a/platform/android/luajit-launcher +++ b/platform/android/luajit-launcher @@ -1 +1 @@ -Subproject commit af9b9d30c10451a3be423d8fb55f05945a3133f6 +Subproject commit 1884a151f66bb465e684b4f1c7ae02243eb9bc44 diff --git a/plugins/statistics.koplugin/readerprogress.lua b/plugins/statistics.koplugin/readerprogress.lua index f9b7322f4..6fdaa693a 100644 --- a/plugins/statistics.koplugin/readerprogress.lua +++ b/plugins/statistics.koplugin/readerprogress.lua @@ -72,6 +72,7 @@ function ReaderProgress:init() } } end + self.covers_fullscreen = true -- hint for UIManager:_repaint() self[1] = FrameContainer:new{ width = self.width, height = self.height,