Enable HW dithering in a few key places (#4541)

* Enable HW dithering on supported devices (Clara HD, Forma; Oasis 2, PW4)
  * FileManager and co. (where appropriate, i.e., when covers are shown)
  * Book Status
  * Reader, where appropriate:
    * CRe: on pages whith image content (for over 7.5% of the screen area, should hopefully leave stuff like bullet points or small scene breaks alone).
    * Other engines: on user-request (in the gear tab of the bottom menu), via the new "Dithering" knob (will only appear on supported devices).
  * ScreenSaver
  * ImageViewer
* Minimize repaints when flash_ui is enabled (by, almost everywhere, only repainting the flashing element, and not the toplevel window which hosts it).
  (The first pass of this involved fixing a few Button instances whose show_parent was wrong, in particular, chevrons in the FM & TopMenu).
* Hunted down a few redundant repaints (unneeded setDirty("all") calls),
  either by switching the widget to nil when only a refresh was needed, and not a repaint,
  or by passing the appropritate widget to setDirty.
  (Note to self: Enable *verbose* debugging to catch broken setDirty calls via its post guard).
  There were also a few instances of 'em right behind a widget close.
* Don't repaint the underlying widget when initially showing TopMenu & ConfigDialog.
  We unfortunately do need to do it when switching tabs, because of their variable heights.
* On Kobo, disabled the extra and completely useless full refresh before suspend/reboot/poweroff, as well as on resume. No more double refreshes!
* Fix another debug guard in Kobo sysfs_light
* Switch ImageWidget & ImageViewer mostly to "ui" updates, which will be better suited to image content pretty much everywhere, REAGL or not.

PS: (Almost 💯 commits! :D)
pull/4556/head
NiLuJe 5 years ago committed by GitHub
parent 5d21990b0e
commit 812e595608
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1 +1 @@
Subproject commit bef742eb7e04d8e1dc5948f8827bf4760158dc45 Subproject commit afd0c6c5a6d50e15ecfede981f5c8bcb768ad43a

@ -159,7 +159,7 @@ function FileManager:init()
function file_chooser:onPathChanged(path) -- luacheck: ignore function file_chooser:onPathChanged(path) -- luacheck: ignore
FileManager.instance.path_text:setText(truncatePath(filemanagerutil.abbreviate(path))) FileManager.instance.path_text:setText(truncatePath(filemanagerutil.abbreviate(path)))
UIManager:setDirty(FileManager.instance, function() UIManager:setDirty(FileManager.instance, function()
return "partial", FileManager.instance.path_text.dimen return "partial", FileManager.instance.path_text.dimen, FileManager.instance.dithered
end) end)
return true return true
end end

@ -417,7 +417,7 @@ function ReaderFooter:addToMainMenu(menu_items)
end end
if should_update then if should_update then
self:updateFooter() self:updateFooter()
UIManager:setDirty("all", "partial") UIManager:setDirty(nil, "ui")
end end
end, end,
} }
@ -436,7 +436,7 @@ function ReaderFooter:addToMainMenu(menu_items)
callback = function() callback = function()
self.settings.disable_progress_bar = not self.settings.disable_progress_bar self.settings.disable_progress_bar = not self.settings.disable_progress_bar
self:updateFooter() self:updateFooter()
UIManager:setDirty("all", "partial") UIManager:setDirty(nil, "ui")
end, end,
}, },
getMinibarOption("toc_markers", self.setTocMarkers), getMinibarOption("toc_markers", self.setTocMarkers),
@ -579,6 +579,8 @@ function ReaderFooter:_updateFooterText()
end end
self.text_container.dimen.w = self.text_width self.text_container.dimen.w = self.text_width
self.horizontal_group:resetLayout() self.horizontal_group:resetLayout()
-- NOTE: This is essentially preventing us from truly using "fast" for panning,
-- since it'll get coalesced in the "fast" panning update, upgrading it to "ui".
UIManager:setDirty(self.view.dialog, function() UIManager:setDirty(self.view.dialog, function()
return "ui", self.footer_content.dimen return "ui", self.footer_content.dimen
end) end)

@ -152,7 +152,8 @@ function ReaderStatus:showStatus(before_show_callback)
if before_show_callback then if before_show_callback then
before_show_callback() before_show_callback()
end end
UIManager:show(status_page) status_page.dithered = true
UIManager:show(status_page, "full")
end end
function ReaderStatus:onReadSettings(config) function ReaderStatus:onReadSettings(config)

@ -76,7 +76,7 @@ function ReaderToc:onPageUpdate(pageno)
if self:isChapterEnd(pageno, 0) then if self:isChapterEnd(pageno, 0) then
self.chapter_refresh = true self.chapter_refresh = true
elseif self:isChapterBegin(pageno, 0) and self.chapter_refresh then elseif self:isChapterBegin(pageno, 0) and self.chapter_refresh then
UIManager:setDirty("all", "full") UIManager:setDirty(nil, "full")
self.chapter_refresh = false self.chapter_refresh = false
else else
self.chapter_refresh = false self.chapter_refresh = false

@ -213,6 +213,23 @@ function ReaderView:paintTo(bb, x, y)
end end
-- stop activity indicator -- stop activity indicator
self.ui:handleEvent(Event:new("StopActivityIndicator")) self.ui:handleEvent(Event:new("StopActivityIndicator"))
-- Most pages should not require dithering
self.dialog.dithered = nil
-- For KOpt, let the user choose.
if self.ui.document.info.has_pages then
if self.document.configurable.hw_dithering == 1 then
self.dialog.dithered = true
end
else
-- Whereas for CRe,
-- If we're attempting to show a large enough amount of image data, request dithering (without triggering another repaint ;)).
local img_count, img_coverage = self.ui.document:getDrawnImagesStatistics()
-- With some nil guards because this may not be implemented in every engine ;).
if img_count and img_count > 0 and img_coverage and img_coverage >= 0.075 then
self.dialog.dithered = true
end
end
end end
--[[ --[[
@ -545,6 +562,9 @@ end
This method is supposed to be only used by ReaderPaging This method is supposed to be only used by ReaderPaging
--]] --]]
function ReaderView:recalculate() function ReaderView:recalculate()
-- Start by resetting the dithering flag early, so it doesn't carry over from the previous page.
self.dialog.dithered = nil
if self.ui.document.info.has_pages and self.state.page then if self.ui.document.info.has_pages and self.state.page then
self.page_area = self:getPageArea( self.page_area = self:getPageArea(
self.state.page, self.state.page,
@ -579,6 +599,7 @@ function ReaderView:recalculate()
self.state.offset.x = (self.dimen.w - self.visible_area.w) / 2 self.state.offset.x = (self.dimen.w - self.visible_area.w) / 2
end end
-- flag a repaint so self:paintTo will be called -- flag a repaint so self:paintTo will be called
-- NOTE: This is also unfortunately called during panning, essentially making sure we'll never be using "fast" for pans ;).
UIManager:setDirty(self.dialog, "partial") UIManager:setDirty(self.dialog, "partial")
end end
@ -588,7 +609,7 @@ function ReaderView:PanningUpdate(dx, dy)
self.visible_area:offsetWithin(self.page_area, dx, dy) self.visible_area:offsetWithin(self.page_area, dx, dy)
if self.visible_area ~= old then if self.visible_area ~= old then
-- flag a repaint -- flag a repaint
UIManager:setDirty(self.dialog, "partial") UIManager:setDirty(self.dialog, "fast")
logger.dbg("on pan: page_area", self.page_area) logger.dbg("on pan: page_area", self.page_area)
logger.dbg("on pan: visible_area", self.visible_area) logger.dbg("on pan: visible_area", self.visible_area)
self.ui:handleEvent( self.ui:handleEvent(
@ -605,7 +626,7 @@ function ReaderView:PanningStart(x, y)
self.visible_area = self.panning_visible_area:copy() self.visible_area = self.panning_visible_area:copy()
self.visible_area:offsetWithin(self.page_area, x, y) self.visible_area:offsetWithin(self.page_area, x, y)
self.ui:handleEvent(Event:new("ViewRecalculate", self.visible_area, self.page_area)) self.ui:handleEvent(Event:new("ViewRecalculate", self.visible_area, self.page_area))
UIManager:setDirty(self.dialog, "partial") UIManager:setDirty(self.dialog, "fast")
end end
function ReaderView:PanningStop() function ReaderView:PanningStop()

@ -26,6 +26,7 @@ local Device = {
needsTouchScreenProbe = no, needsTouchScreenProbe = no,
hasClipboard = yes, -- generic internal clipboard on all devices hasClipboard = yes, -- generic internal clipboard on all devices
hasEinkScreen = yes, hasEinkScreen = yes,
canHWDither = no,
hasColorScreen = no, hasColorScreen = no,
hasBGRFrameBuffer = no, hasBGRFrameBuffer = no,
canToggleGSensor = no, canToggleGSensor = no,
@ -190,7 +191,9 @@ function Device:onPowerEvent(ev)
self.orig_rotation_mode = nil self.orig_rotation_mode = nil
end end
require("ui/screensaver"):show() require("ui/screensaver"):show()
self.screen:refreshFull() if self:needsScreenRefreshAfterResume() then
self.screen:refreshFull()
end
self.screen_saver_mode = true self.screen_saver_mode = true
UIManager:scheduleIn(0.1, function() UIManager:scheduleIn(0.1, function()
local network_manager = require("ui/network/manager") local network_manager = require("ui/network/manager")

@ -32,6 +32,8 @@ local Kobo = Generic:new{
touch_mirrored_x = true, touch_mirrored_x = true,
-- enforce portrait mode on Kobos -- enforce portrait mode on Kobos
isAlwaysPortrait = yes, isAlwaysPortrait = yes,
-- we don't need an extra refreshFull on resume, thank you very much.
needsScreenRefreshAfterResume = no,
-- the internal storage mount point users can write to -- the internal storage mount point users can write to
internal_storage_mount_point = "/mnt/onboard/", internal_storage_mount_point = "/mnt/onboard/",
-- currently only the Aura One and Forma have coloured frontlights -- currently only the Aura One and Forma have coloured frontlights

@ -103,9 +103,9 @@ end
dbg:guard(SysfsLight, 'setNaturalBrightness', dbg:guard(SysfsLight, 'setNaturalBrightness',
function(self, brightness, warmth) function(self, brightness, warmth)
assert(brightness >= 0 and brightness <= 100, assert(brightness == nil or (brightness >= 0 and brightness <= 100),
"Wrong brightness value given!") "Wrong brightness value given!")
assert(warmth >= 0 and warmth <= 100, assert(warmth == nil or (warmth >= 0 and warmth <= 100),
"Wrong warmth value given!") "Wrong warmth value given!")
end) end)

@ -251,6 +251,16 @@ This can also be used to remove some gray background or to convert a grayscale o
end, end,
name_text_hold_callback = optionsutil.showValues, name_text_hold_callback = optionsutil.showValues,
}, },
{
name = "hw_dithering",
name_text = S.HW_DITHERING,
toggle = {S.ON, S.OFF},
values = {1, 0},
default_value = 0,
advanced = true,
show = Device:hasEinkScreen() and Device:canHWDither(),
name_text_hold_callback = optionsutil.showValues,
},
{ {
name = "forced_ocr", name = "forced_ocr",
name_text = S.FORCED_OCR, name_text = S.FORCED_OCR,

@ -32,6 +32,7 @@ S.EMBEDDED_FONTS = _("Embedded Fonts")
S.WRITING_DIR = _("Writing Direction") S.WRITING_DIR = _("Writing Direction")
S.PROGRESS_BAR = _("Progress Bar") S.PROGRESS_BAR = _("Progress Bar")
S.FORCED_OCR = _("Forced OCR") S.FORCED_OCR = _("Forced OCR")
S.HW_DITHERING = _("Dithering")
S.INVERSE_READING_ORDER = _("Inverse Order") S.INVERSE_READING_ORDER = _("Inverse Order")
S.ON = _("on") S.ON = _("on")

@ -387,7 +387,8 @@ function Screensaver:show(event, fallback_message)
covers_fullscreen = covers_fullscreen, covers_fullscreen = covers_fullscreen,
} }
self.left_msg.modal = true self.left_msg.modal = true
-- refresh whole screen for other types -- Refresh whole screen for other types
self.left_msg.dithered = true
UIManager:show(self.left_msg, "full") UIManager:show(self.left_msg, "full")
end end
end end
@ -400,8 +401,7 @@ function Screensaver:close()
UIManager:scheduleIn(screensaver_delay_number, function() UIManager:scheduleIn(screensaver_delay_number, function()
logger.dbg("close screensaver") logger.dbg("close screensaver")
if self.left_msg then if self.left_msg then
UIManager:close(self.left_msg) UIManager:close(self.left_msg, "full")
UIManager:setDirty("all", "full")
self.left_msg = nil self.left_msg = nil
end end
end) end)

@ -56,7 +56,9 @@ function UIManager:init()
self._entered_poweroff_stage = true; self._entered_poweroff_stage = true;
Screen:setRotationMode(0) Screen:setRotationMode(0)
require("ui/screensaver"):show("poweroff", _("Powered off")) require("ui/screensaver"):show("poweroff", _("Powered off"))
Screen:refreshFull() if Device:needsScreenRefreshAfterResume() then
Screen:refreshFull()
end
UIManager:nextTick(function() UIManager:nextTick(function()
Device:saveSettings() Device:saveSettings()
self:broadcastEvent(Event:new("Close")) self:broadcastEvent(Event:new("Close"))
@ -67,7 +69,9 @@ function UIManager:init()
self._entered_poweroff_stage = true; self._entered_poweroff_stage = true;
Screen:setRotationMode(0) Screen:setRotationMode(0)
require("ui/screensaver"):show("reboot", _("Rebooting...")) require("ui/screensaver"):show("reboot", _("Rebooting..."))
Screen:refreshFull() if Device:needsScreenRefreshAfterResume() then
Screen:refreshFull()
end
UIManager:nextTick(function() UIManager:nextTick(function()
Device:saveSettings() Device:saveSettings()
self:broadcastEvent(Event:new("Close")) self:broadcastEvent(Event:new("Close"))
@ -261,13 +265,14 @@ For refreshtype & refreshregion see description of setDirty().
---- @param refreshregion a Geom object ---- @param refreshregion a Geom object
---- @int x ---- @int x
---- @int y ---- @int y
---- @param refreshdither an optional bool
---- @see setDirty ---- @see setDirty
function UIManager:show(widget, refreshtype, refreshregion, x, y) function UIManager:show(widget, refreshtype, refreshregion, x, y, refreshdither)
if not widget then if not widget then
logger.dbg("widget not exist to be shown") logger.dbg("widget not exist to be shown")
return return
end end
logger.dbg("show widget", widget.id or widget.name or "unknown") logger.dbg("show widget:", widget.id or widget.name or tostring(widget))
self._running = true self._running = true
local window = {x = x or 0, y = y or 0, widget = widget} local window = {x = x or 0, y = y or 0, widget = widget}
@ -281,7 +286,7 @@ function UIManager:show(widget, refreshtype, refreshregion, x, y)
end end
end end
-- and schedule it to be painted -- and schedule it to be painted
self:setDirty(widget, refreshtype, refreshregion) self:setDirty(widget, refreshtype, refreshregion, refreshdither)
-- tell the widget that it is shown now -- tell the widget that it is shown now
widget:handleEvent(Event:new("Show")) widget:handleEvent(Event:new("Show"))
-- check if this widget disables double tap gesture -- check if this widget disables double tap gesture
@ -300,13 +305,14 @@ For refreshtype & refreshregion see description of setDirty().
---- @param widget a widget object ---- @param widget a widget object
---- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast" ---- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast"
---- @param refreshregion a Geom object ---- @param refreshregion a Geom object
---- @param refreshdither an optional bool
---- @see setDirty ---- @see setDirty
function UIManager:close(widget, refreshtype, refreshregion) function UIManager:close(widget, refreshtype, refreshregion, refreshdither)
if not widget then if not widget then
logger.dbg("widget to be closed does not exist") logger.dbg("widget to be closed does not exist")
return return
end end
logger.dbg("close widget", widget.id or widget.name) logger.dbg("close widget:", widget.name or widget.id or tostring(widget))
local dirty = false local dirty = false
-- Ensure all the widgets can get onFlushSettings event. -- Ensure all the widgets can get onFlushSettings event.
widget:handleEvent(Event:new("FlushSettings")) widget:handleEvent(Event:new("FlushSettings"))
@ -317,10 +323,19 @@ function UIManager:close(widget, refreshtype, refreshregion)
-- then remove all references to that widget on stack and refresh -- then remove all references to that widget on stack and refresh
for i = #self._window_stack, 1, -1 do for i = #self._window_stack, 1, -1 do
if self._window_stack[i].widget == widget then if self._window_stack[i].widget == widget then
self._dirty[self._window_stack[i].widget] = nil
table.remove(self._window_stack, i) table.remove(self._window_stack, i)
dirty = true dirty = true
elseif self._window_stack[i].widget.disable_double_tap == false then else
Input.disable_double_tap = false -- If anything else on the stack was dithered, honor the hint
if self._window_stack[i].widget.dithered then
refreshdither = true
logger.dbg("Lower widget", self._window_stack[i].widget.name or self._window_stack[i].widget.id or tostring(self._window_stack[i].widget), "was dithered, honoring the dithering hint")
end
if self._window_stack[i].widget.disable_double_tap == false then
Input.disable_double_tap = false
end
end end
end end
if dirty and not widget.invisible then if dirty and not widget.invisible then
@ -328,7 +343,7 @@ function UIManager:close(widget, refreshtype, refreshregion)
for i = 1, #self._window_stack do for i = 1, #self._window_stack do
self:setDirty(self._window_stack[i].widget) self:setDirty(self._window_stack[i].widget)
end end
self:_refresh(refreshtype, refreshregion) self:_refresh(refreshtype, refreshregion, refreshdither)
end end
end end
@ -464,6 +479,8 @@ NOTE: You'll notice a trend on UI elements that are usually shown *over* some ki
That said, depending on your use case, using "ui" onClose can be a perfectly valid decision, and will ensure That said, depending on your use case, using "ui" onClose can be a perfectly valid decision, and will ensure
never seeing a flash because of that widget. never seeing a flash because of that widget.
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).
@usage @usage
@ -475,15 +492,32 @@ UIManager:setDirty(self.widget, function() return "ui", self.someelement.dimen e
---- @param widget a widget object ---- @param widget a widget object
---- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast" ---- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast"
---- @param refreshregion a Geom object ---- @param refreshregion a Geom object
function UIManager:setDirty(widget, refreshtype, refreshregion) ---- @param refreshdither an optional bool
function UIManager:setDirty(widget, refreshtype, refreshregion, refreshdither)
if widget then if widget then
if widget == "all" then if widget == "all" then
-- special case: set all top-level widgets as being "dirty". -- special case: set all top-level widgets as being "dirty".
for i = 1, #self._window_stack do for i = 1, #self._window_stack do
self._dirty[self._window_stack[i].widget] = true self._dirty[self._window_stack[i].widget] = true
-- If any of 'em were dithered, honor their dithering hint
if self._window_stack[i].widget.dithered then
-- NOTE: That works when refreshtype is NOT a function,
-- which is why _repaint does another pass of this check ;).
refreshdither = true
end
end end
elseif not widget.invisible then elseif not widget.invisible then
self._dirty[widget] = true -- We only ever check the dirty flag on top-level widgets, so only set it there!
-- NOTE: Enable verbose debug to catch misbehaving widgets via our post-guard.
for i = 1, #self._window_stack do
if self._window_stack[i].widget == widget then
self._dirty[widget] = true
end
end
-- Again, if it's flagged as dithered, honor that
if widget.dithered then
refreshdither = true
end
end end
end end
-- handle refresh information -- handle refresh information
@ -493,23 +527,23 @@ function UIManager:setDirty(widget, refreshtype, refreshregion)
if dbg.is_on then if dbg.is_on then
-- FIXME: We can't consume the return values of refreshtype by running it, because for a reason that is beyond me (scoping? gc?), that renders it useless later, meaning we then enqueue refreshes with bogus arguments... -- FIXME: We can't consume the return values of refreshtype by running it, because for a reason that is beyond me (scoping? gc?), that renders it useless later, meaning we then enqueue refreshes with bogus arguments...
-- Thankfully, we can track them in _refresh()'s logging very soon after that... -- Thankfully, we can track them in _refresh()'s logging very soon after that...
logger.dbg("setDirty via a func from widget", widget and (widget.name or widget.id or tostring(widget))) logger.dbg("setDirty via a func from widget", widget and (widget.name or widget.id or tostring(widget)) or "nil")
end end
else else
-- otherwise, enqueue refresh -- otherwise, enqueue refresh
self:_refresh(refreshtype, refreshregion) self:_refresh(refreshtype, refreshregion, refreshdither)
if dbg.is_on then if dbg.is_on then
if refreshregion then if refreshregion then
logger.dbg("setDirty", refreshtype and refreshtype or "nil", "from widget", widget and (widget.name or widget.id or tostring(widget)) or "nil", "w/ region", refreshregion.x, refreshregion.y, refreshregion.w, refreshregion.h) logger.dbg("setDirty", refreshtype and refreshtype or "nil", "from widget", widget and (widget.name or widget.id or tostring(widget)) or "nil", "w/ region", refreshregion.x, refreshregion.y, refreshregion.w, refreshregion.h, refreshdither and "AND w/ HW dithering" or "")
else else
logger.dbg("setDirty", refreshtype and refreshtype or "nil", "from widget", widget and (widget.name or widget.id or tostring(widget)) or "nil", "w/ NO region") logger.dbg("setDirty", refreshtype and refreshtype or "nil", "from widget", widget and (widget.name or widget.id or tostring(widget)) or "nil", "w/ NO region", refreshdither and "AND w/ HW dithering" or "")
end end
end end
end end
end end
dbg:guard(UIManager, 'setDirty', dbg:guard(UIManager, 'setDirty',
nil, nil,
function(self, widget, refreshtype, refreshregion) function(self, widget, refreshtype, refreshregion, refreshdither)
if not widget or widget == "all" then return end if not widget or widget == "all" then return end
-- when debugging, we check if we get handed a valid widget, -- when debugging, we check if we get handed a valid widget,
-- which would be a dialog that was previously passed via show() -- which would be a dialog that was previously passed via show()
@ -680,6 +714,8 @@ end
-- precedence of refresh modes: -- precedence of refresh modes:
local refresh_modes = { fast = 1, ui = 2, partial = 3, flashui = 4, flashpartial = 5, full = 6 } local refresh_modes = { fast = 1, ui = 2, partial = 3, flashui = 4, flashpartial = 5, full = 6 }
-- NOTE: We might want to introduce a "force_fast" that points to fast, but has the highest priority,
-- for the few cases where we might *really* want to enforce fast (for stuff like panning or skimming?).
-- refresh methods in framebuffer implementation -- refresh methods in framebuffer implementation
local refresh_methods = { local refresh_methods = {
fast = "refreshFast", fast = "refreshFast",
@ -704,6 +740,20 @@ local function update_mode(mode1, mode2)
end end
end end
--[[
Compares dither hints.
Dither always wins.
--]]
local function update_dither(dither1, dither2)
if dither1 and not dither2 then
logger.dbg("update_dither: Update dither hint", dither2, "to", dither1)
return dither1
else
return dither2
end
end
--[[-- --[[--
Enqueues a refresh. Enqueues a refresh.
@ -716,9 +766,20 @@ UIManager that a certain part of the screen is to be refreshed.
Rect() that specifies the region to be updated Rect() that specifies the region to be updated
optional, update will affect whole screen if not specified. optional, update will affect whole screen if not specified.
Note that this should be the exception. Note that this should be the exception.
@param dither
Bool, a hint to request hardware dithering (if supported)
optional, no dithering requested if not specified or not supported.
--]] --]]
function UIManager:_refresh(mode, region) function UIManager:_refresh(mode, region, dither)
if not mode then return end if not mode then
-- If we're trying to float a dither hint up from a lower widget after a close, mode might be nil...
-- So use the lowest priority refresh mode (short of fast, because that'd do half-toning).
if dither then
mode = "ui"
else
return
end
end
if not region and mode == "full" then if not region and mode == "full" then
self.refresh_count = 0 -- reset counter on explicit full refresh self.refresh_count = 0 -- reset counter on explicit full refresh
end end
@ -763,6 +824,9 @@ function UIManager:_refresh(mode, region)
-- if no region is specified, define default region -- if no region is specified, define default region
region = region or Geom:new{w=Screen:getWidth(), h=Screen:getHeight()} region = region or Geom:new{w=Screen:getWidth(), h=Screen:getHeight()}
-- if no dithering hint was specified, don't request dithering
dither = dither or false
-- NOTE: While, ideally, we shouldn't merge refreshes w/ different waveform modes, -- NOTE: While, ideally, we shouldn't merge refreshes w/ different waveform modes,
-- this allows us to optimize away a number of quirks of our rendering stack -- this allows us to optimize away a number of quirks of our rendering stack
-- (f.g., multiple setDirty calls queued when showing/closing a widget because of update mechanisms), -- (f.g., multiple setDirty calls queued when showing/closing a widget because of update mechanisms),
@ -775,16 +839,18 @@ function UIManager:_refresh(mode, region)
local combined = region:combine(self._refresh_stack[i].region) local combined = region:combine(self._refresh_stack[i].region)
-- update the mode, if needed -- update the mode, if needed
mode = update_mode(mode, self._refresh_stack[i].mode) mode = update_mode(mode, self._refresh_stack[i].mode)
-- dithering hints are viral, one is enough to infect the whole queue
dither = update_dither(dither, self._refresh_stack[i].dither)
-- remove colliding refresh -- remove colliding refresh
table.remove(self._refresh_stack, i) table.remove(self._refresh_stack, i)
-- and try again with combined data -- and try again with combined data
return self:_refresh(mode, combined) return self:_refresh(mode, combined, dither)
end end
end end
-- if we've stopped hitting collisions, enqueue the refresh -- if we've stopped hitting collisions, enqueue the refresh
logger.dbg("_refresh: Enqueued", mode, "update for region", region.x, region.y, region.w, region.h) logger.dbg("_refresh: Enqueued", mode, "update for region", region.x, region.y, region.w, region.h, dither and "w/ HW dithering" or "")
table.insert(self._refresh_stack, {mode = mode, region = region}) table.insert(self._refresh_stack, {mode = mode, region = region, dither = dither})
end end
--- Repaints dirty widgets. --- Repaints dirty widgets.
@ -792,6 +858,8 @@ function UIManager:_repaint()
-- flag in which we will record if we did any repaints at all -- flag in which we will record if we did any repaints at all
-- will trigger a refresh if set. -- will trigger a refresh if set.
local dirty = false local dirty = false
-- remember if any of our repaints were dithered
local dithered = false
-- We don't need to call paintTo() on widgets that are under -- We don't need to call paintTo() on widgets that are under
-- a widget that covers the full screen -- a widget that covers the full screen
@ -820,13 +888,21 @@ function UIManager:_repaint()
-- trigger repaint -- trigger repaint
dirty = true dirty = true
-- if any of 'em were dithered, we'll want to dither the final refresh
if widget.widget.dithered then
logger.dbg("_repaint: it was dithered, infecting the refresh queue")
dithered = true
end
end end
end end
-- execute pending refresh functions -- execute pending refresh functions
for _, refreshfunc in ipairs(self._refresh_func_stack) do for _, refreshfunc in ipairs(self._refresh_func_stack) do
local refreshtype, region = refreshfunc() local refreshtype, region, dither = refreshfunc()
if refreshtype then self:_refresh(refreshtype, region) end -- honor dithering hints from *anywhere* in the dirty stack
dither = update_dither(dither, dithered)
if refreshtype then self:_refresh(refreshtype, region, dither) end
end end
self._refresh_func_stack = {} self._refresh_func_stack = {}
@ -845,7 +921,8 @@ function UIManager:_repaint()
-- but checkBounds & getPhysicalRect will sanitize that in mxc_update @ ffi/framebuffer_mxcfb ;). -- but checkBounds & getPhysicalRect will sanitize that in mxc_update @ ffi/framebuffer_mxcfb ;).
Screen[refresh_methods[refresh.mode]](Screen, Screen[refresh_methods[refresh.mode]](Screen,
refresh.region.x - 1, refresh.region.y - 1, refresh.region.x - 1, refresh.region.y - 1,
refresh.region.w + 2, refresh.region.h + 2) refresh.region.w + 2, refresh.region.h + 2,
refresh.dither)
end end
self._refresh_stack = {} self._refresh_stack = {}
self.refresh_counted = false self.refresh_counted = false
@ -855,6 +932,16 @@ function UIManager:forceRePaint()
self:_repaint() self:_repaint()
end 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.
function UIManager:widgetRepaint(widget, x, y)
if not widget then return end
logger.dbg("Explicit widgetRepaint:", widget.name or widget.id or tostring(widget), "@ (", x, ",", y, ")")
widget:paintTo(Screen.bb, x, y)
end
function UIManager:setInputTimeout(timeout) function UIManager:setInputTimeout(timeout)
self.INPUT_TIMEOUT = timeout or 200*1000 self.INPUT_TIMEOUT = timeout or 200*1000
end end

@ -111,6 +111,8 @@ function BookStatusWidget:init()
padding = 0, padding = 0,
self:getStatusContent(screen_size.w), self:getStatusContent(screen_size.w),
} }
self.dithered = true
end end
function BookStatusWidget:getStats() function BookStatusWidget:getStats()
@ -254,7 +256,9 @@ function BookStatusWidget:setStar(num)
table.insert(self.stars_container, stars_group) table.insert(self.stars_container, stars_group)
UIManager:setDirty(nil, "ui") -- Individual stars are Button, w/ flash_ui, they'll have their own flash.
-- And we need to redraw the full widget, because we don't know the coordinates of stars_container :/.
UIManager:setDirty(self, "ui", nil, true)
return true return true
end end
@ -540,7 +544,7 @@ function BookStatusWidget:onConfigChoose(values, name, event, args, events, posi
if values then if values then
self:onChangeBookStatus(args, position) self:onChangeBookStatus(args, position)
end end
UIManager:setDirty("all", "ui") UIManager:setDirty(nil, "ui", nil, true)
end) end)
end end
@ -558,7 +562,7 @@ function BookStatusWidget:onSwipe(arg, ges_ev)
do end -- luacheck: ignore 541 do end -- luacheck: ignore 541
else -- diagonal swipe else -- diagonal swipe
-- trigger full refresh -- trigger full refresh
UIManager:setDirty(nil, "full") UIManager:setDirty(nil, "full", nil, true)
-- a long diagonal swipe may also be used for taking a screenshot, -- a long diagonal swipe may also be used for taking a screenshot,
-- so let it propagate -- so let it propagate
return false return false
@ -568,8 +572,7 @@ end
function BookStatusWidget:onClose() function BookStatusWidget:onClose()
self:saveSummary() self:saveSummary()
-- NOTE: Flash on close to avoid ghosting, since we show an image. -- NOTE: Flash on close to avoid ghosting, since we show an image.
UIManager:setDirty("all", "flashpartial") UIManager:close(self, "flashpartial")
UIManager:close(self)
return true return true
end end

@ -193,16 +193,18 @@ function Button:onTapSelectButton()
if G_reader_settings:isFalse("flash_ui") then if G_reader_settings:isFalse("flash_ui") then
self.callback() self.callback()
else else
-- NOTE: Flag all widgets as dirty to force a repaint, so we actually get to see the highlight.
-- (For some reason (wrong widget passed to setDirty?), we never saw the effects on the FM chevrons without this hack).
self[1].invert = true self[1].invert = true
UIManager:setDirty("all", function() -- For most of our buttons, we can't avoid that initial repaint...
UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "fast", self[1].dimen return "fast", self[1].dimen
end) 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() UIManager:tickAfterNext(function()
self.callback() self.callback()
self[1].invert = false self[1].invert = false
UIManager:setDirty("all", function() UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "fast", self[1].dimen return "fast", self[1].dimen
end) end)
end) end)

@ -96,14 +96,16 @@ function CheckButton:onTapCheckButton()
if G_reader_settings:isFalse("flash_ui") then if G_reader_settings:isFalse("flash_ui") then
self.callback() self.callback()
else else
self.invert = true self[1].invert = true
UIManager:setDirty(self.show_parent, function() UIManager:widgetRepaint(self[1], self.dimen.x, self.dimen.y)
UIManager:setDirty(nil, function()
return "fast", self.dimen return "fast", self.dimen
end) end)
UIManager:tickAfterNext(function() UIManager:tickAfterNext(function()
self.callback() self.callback()
self.invert = false self[1].invert = false
UIManager:setDirty(self.show_parent, function() UIManager:widgetRepaint(self[1], self.dimen.x, self.dimen.y)
UIManager:setDirty(nil, function()
return "fast", self.dimen return "fast", self.dimen
end) end)
end) end)

@ -711,6 +711,7 @@ Widget that displays config menubar and config panel
local ConfigDialog = FocusManager:new{ local ConfigDialog = FocusManager:new{
--is_borderless = false, --is_borderless = false,
panel_index = 1, panel_index = 1,
is_fresh = true,
} }
function ConfigDialog:init() function ConfigDialog:init()
@ -783,7 +784,7 @@ end
function ConfigDialog:onCloseWidget() function ConfigDialog:onCloseWidget()
-- NOTE: As much as we would like to flash here, don't, because of adverse interactions with touchmenu that might lead to a double flash... -- NOTE: As much as we would like to flash here, don't, because of adverse interactions with touchmenu that might lead to a double flash...
UIManager:setDirty("all", function() UIManager:setDirty(nil, function()
return "partial", self.dialog_frame.dimen return "partial", self.dialog_frame.dimen
end) end)
end end
@ -794,10 +795,11 @@ function ConfigDialog:onShowConfigPanel(index)
self:update() self:update()
-- NOTE: Keep that one as UI to avoid delay when both this and the topmenu are shown. -- NOTE: Keep that one as UI to avoid delay when both this and the topmenu are shown.
-- Plus, this is also called for each tab anyway, so that wouldn't have been great. -- Plus, this is also called for each tab anyway, so that wouldn't have been great.
UIManager:setDirty("all", function() UIManager:setDirty(self.is_fresh and self or "all", function()
local refresh_dimen = local refresh_dimen =
old_dimen and old_dimen:combine(self.dialog_frame.dimen) old_dimen and old_dimen:combine(self.dialog_frame.dimen)
or self.dialog_frame.dimen or self.dialog_frame.dimen
self.is_fresh = false
return "ui", refresh_dimen return "ui", refresh_dimen
end) end)
return true return true

@ -551,7 +551,7 @@ function DictQuickLookup:update()
dimen = self.region, dimen = self.region,
self.movable, self.movable,
} }
UIManager:setDirty("all", function() UIManager:setDirty(self, function()
local update_region = self.dict_frame and self.dict_frame.dimen and self.dict_frame.dimen:combine(orig_dimen) or orig_dimen 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) logger.dbg("update dict region", update_region)
return "partial", update_region return "partial", update_region

@ -276,7 +276,7 @@ function FrontLightWidget:setProgress(num, step, num_warmth)
-- Reset container height to what it actually contains -- Reset container height to what it actually contains
self.fl_container.dimen.h = vertical_group:getSize().h self.fl_container.dimen.h = vertical_group:getSize().h
UIManager:setDirty("all", "ui") UIManager:setDirty(self, "ui")
return true return true
end end
@ -596,7 +596,7 @@ function FrontLightWidget:naturalLightConfigOpen()
self.nl_configure_open = true self.nl_configure_open = true
-- Move to the bottom to make place for the new widget -- Move to the bottom to make place for the new widget
self[1].align="bottom" self[1].align="bottom"
UIManager:setDirty("all", "ui") UIManager:setDirty(self, "ui")
end end
function FrontLightWidget:naturalLightConfigClose() function FrontLightWidget:naturalLightConfigClose()
@ -606,7 +606,7 @@ function FrontLightWidget:naturalLightConfigClose()
self.configure_button:enable() self.configure_button:enable()
self.nl_configure_open = false self.nl_configure_open = false
self[1].align="center" self[1].align="center"
UIManager:setDirty("all", "ui") UIManager:setDirty(self, "ui")
end end
return FrontLightWidget return FrontLightWidget

@ -96,14 +96,17 @@ function IconButton:onTapIconButton()
self.callback() self.callback()
else else
self.image.invert = true self.image.invert = true
UIManager:setDirty(self.show_parent, function() -- 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:setDirty(nil, function()
return "fast", self.dimen return "fast", self.dimen
end) end)
-- Make sure button reacts before doing callback -- And, we usually need to delay the callback for the same reasons as Button...
UIManager:tickAfterNext(function() UIManager:tickAfterNext(function()
self.callback() self.callback()
self.image.invert = false self.image.invert = false
UIManager:setDirty(self.show_parent, function() UIManager:widgetRepaint(self.image, self.dimen.x + self.padding_left, self.dimen.y + self.padding_top)
UIManager:setDirty(nil, function()
return "fast", self.dimen return "fast", self.dimen
end) end)
end) end)

@ -392,16 +392,22 @@ function ImageViewer:update()
self.main_frame, self.main_frame,
} }
} }
UIManager:setDirty("all", function() -- NOTE: We use UI instead of partial, because we do NOT want to end up using a REAGL waveform...
-- NOTE: Disabling dithering here makes for a perfect test-case of how well it works:
-- page turns will show color quantization artefacts (i.e., banding) like crazy,
-- while a long touch will trigger a dithered, flashing full-refresh that'll make everything shiny :).
self.dithered = true
UIManager:setDirty(self, function()
local update_region = self.main_frame.dimen:combine(orig_dimen) local update_region = self.main_frame.dimen:combine(orig_dimen)
logger.dbg("update image region", update_region) logger.dbg("update image region", update_region)
return "partial", update_region return "ui", update_region, true
end) end)
end end
function ImageViewer:onShow() function ImageViewer:onShow()
self.dithered = true
UIManager:setDirty(self, function() UIManager:setDirty(self, function()
return "full", self.main_frame.dimen return "full", self.main_frame.dimen, true
end) end)
return true return true
end end
@ -508,7 +514,8 @@ function ImageViewer:onHoldRelease(_, ges)
self._pan_relative_y = ges.pos.y - self._pan_relative_y self._pan_relative_y = ges.pos.y - self._pan_relative_y
if math.abs(self._pan_relative_x) < self.pan_threshold and math.abs(self._pan_relative_y) < self.pan_threshold then if math.abs(self._pan_relative_x) < self.pan_threshold and math.abs(self._pan_relative_y) < self.pan_threshold then
-- Hold with no move (or less than pan_threshold): use this to trigger full refresh -- Hold with no move (or less than pan_threshold): use this to trigger full refresh
UIManager:setDirty(nil, "full") self.dithered = true
UIManager:setDirty(nil, "full", nil, true)
else else
self:panBy(-self._pan_relative_x, -self._pan_relative_y) self:panBy(-self._pan_relative_x, -self._pan_relative_y)
end end
@ -601,6 +608,7 @@ function ImageViewer:onCloseWidget()
if self._images_list and self._images_list_disposable and self._images_list.free then if self._images_list and self._images_list_disposable and self._images_list.free then
self._images_list.free() self._images_list.free()
end end
-- NOTE: Assume there's no image beneath us, so, no dithering request
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "flashui", self.main_frame.dimen return "flashui", self.main_frame.dimen
end) end)

@ -334,8 +334,9 @@ function ImageWidget:panBy(x, y)
if new_offset_x ~= self._offset_x or new_offset_y ~= self._offset_y then if new_offset_x ~= self._offset_x or new_offset_y ~= self._offset_y then
self._offset_x = new_offset_x self._offset_x = new_offset_x
self._offset_y = new_offset_y self._offset_y = new_offset_y
self.dithered = true
UIManager:setDirty("all", function() UIManager:setDirty("all", function()
return "partial", self.dimen return "ui", self.dimen, true
end) end)
end end
-- return new center ratio, so caller can use them later to create a new -- return new center ratio, so caller can use them later to create a new

@ -234,13 +234,15 @@ function KeyValueItem:onTap()
self.callback() self.callback()
else else
self[1].invert = true self[1].invert = true
UIManager:setDirty(self.show_parent, function() UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "fast", self[1].dimen return "fast", self[1].dimen
end) end)
UIManager:tickAfterNext(function() UIManager:tickAfterNext(function()
self.callback() self.callback()
self[1].invert = false self[1].invert = false
UIManager:setDirty(self.show_parent, function() UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "ui", self[1].dimen return "ui", self[1].dimen
end) end)
end) end)
@ -553,7 +555,7 @@ function KeyValuePage:onReturn()
if self.callback_return then if self.callback_return then
self:callback_return() self:callback_return()
UIManager:close(self) UIManager:close(self)
UIManager:setDirty("all", "ui") UIManager:setDirty(nil, "ui")
end end
end end

@ -403,19 +403,21 @@ function MenuItem:onTapSelect(arg, ges)
coroutine.resume(co) coroutine.resume(co)
else else
self[1].invert = true self[1].invert = true
UIManager:setDirty(self.show_parent, function() UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "fast", self[1].dimen return "fast", self[1].dimen
end) end)
UIManager:tickAfterNext(function() UIManager:tickAfterNext(function()
self[1].invert = false
UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen
end)
logger.dbg("creating coroutine for menu select") logger.dbg("creating coroutine for menu select")
local co = coroutine.create(function() local co = coroutine.create(function()
self.menu:onMenuSelect(self.table, pos) self.menu:onMenuSelect(self.table, pos)
end) end)
coroutine.resume(co) 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)
end) end)
end end
return true return true
@ -427,15 +429,17 @@ function MenuItem:onHoldSelect(arg, ges)
self.menu:onMenuHold(self.table, pos) self.menu:onMenuHold(self.table, pos)
else else
self[1].invert = true self[1].invert = true
UIManager:setDirty(self.show_parent, function() UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "fast", self[1].dimen return "fast", self[1].dimen
end) end)
UIManager:tickAfterNext(function() UIManager:tickAfterNext(function()
self.menu:onMenuHold(self.table, pos)
self[1].invert = false self[1].invert = false
--UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(self.show_parent, function() UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen return "ui", self[1].dimen
end) end)
self.menu:onMenuHold(self.table, pos)
end) end)
end end
return true return true
@ -587,25 +591,25 @@ function Menu:init()
icon = "resources/icons/appbar.chevron.left.png", icon = "resources/icons/appbar.chevron.left.png",
callback = function() self:onPrevPage() end, callback = function() self:onPrevPage() end,
bordersize = 0, bordersize = 0,
show_parent = self, show_parent = self.show_parent,
} }
self.page_info_right_chev = Button:new{ self.page_info_right_chev = Button:new{
icon = "resources/icons/appbar.chevron.right.png", icon = "resources/icons/appbar.chevron.right.png",
callback = function() self:onNextPage() end, callback = function() self:onNextPage() end,
bordersize = 0, bordersize = 0,
show_parent = self, show_parent = self.show_parent,
} }
self.page_info_first_chev = Button:new{ self.page_info_first_chev = Button:new{
icon = "resources/icons/appbar.chevron.first.png", icon = "resources/icons/appbar.chevron.first.png",
callback = function() self:onFirstPage() end, callback = function() self:onFirstPage() end,
bordersize = 0, bordersize = 0,
show_parent = self, show_parent = self.show_parent,
} }
self.page_info_last_chev = Button:new{ self.page_info_last_chev = Button:new{
icon = "resources/icons/appbar.chevron.last.png", icon = "resources/icons/appbar.chevron.last.png",
callback = function() self:onLastPage() end, callback = function() self:onLastPage() end,
bordersize = 0, bordersize = 0,
show_parent = self, show_parent = self.show_parent,
} }
self.page_info_spacer = HorizontalSpan:new{ self.page_info_spacer = HorizontalSpan:new{
width = Screen:scaleBySize(32), width = Screen:scaleBySize(32),
@ -699,7 +703,7 @@ function Menu:init()
if self.onReturn then self:onReturn() end if self.onReturn then self:onReturn() end
end, end,
bordersize = 0, bordersize = 0,
show_parent = self, show_parent = self.show_parent,
readonly = self.return_arrow_propagation, readonly = self.return_arrow_propagation,
} }
self.page_return_arrow:hide() self.page_return_arrow:hide()
@ -953,7 +957,7 @@ function Menu:updateItems(select_number)
self.path_text.text = self:truncatePath(self.path) self.path_text.text = self:truncatePath(self.path)
end end
UIManager:setDirty("all", function() UIManager:setDirty(self.show_parent, function()
local refresh_dimen = local refresh_dimen =
old_dimen and old_dimen:combine(self.dimen) old_dimen and old_dimen:combine(self.dimen)
or self.dimen or self.dimen

@ -122,7 +122,7 @@ function MultiInputDialog:init()
}, },
self.dialog_frame, self.dialog_frame,
} }
UIManager:setDirty("all", "full") UIManager:setDirty(self, "ui")
end end
function MultiInputDialog:getFields() function MultiInputDialog:getFields()

@ -353,7 +353,7 @@ function NaturalLightWidget:createMainContent(width, height)
table.insert(self.fl_container, vertical_group) table.insert(self.fl_container, vertical_group)
-- Reset container height to what it actually contains -- Reset container height to what it actually contains
self.fl_container.dimen.h = vertical_group:getSize().h self.fl_container.dimen.h = vertical_group:getSize().h
UIManager:setDirty("all", "ui") UIManager:setDirty(self, "ui")
return self.fl_container return self.fl_container
end end

@ -116,14 +116,17 @@ function RadioButton:onTapCheckButton()
if G_reader_settings:isFalse("flash_ui") then if G_reader_settings:isFalse("flash_ui") then
self.callback() self.callback()
else else
self.invert = true -- While I'd like to only flash the button itself, we have to make do with flashing the full width of the TextWidget...
UIManager:setDirty(self.show_parent, function() self.frame.invert = true
UIManager:widgetRepaint(self.frame, self.dimen.x, self.dimen.y)
UIManager:setDirty(nil, function()
return "fast", self.dimen return "fast", self.dimen
end) end)
UIManager:tickAfterNext(function() UIManager:tickAfterNext(function()
self.callback() self.callback()
self.invert = false self.frame.invert = false
UIManager:setDirty(self.show_parent, function() UIManager:widgetRepaint(self.frame, self.dimen.x, self.dimen.y)
UIManager:setDirty(nil, function()
return "fast", self.dimen return "fast", self.dimen
end) end)
end) end)

@ -49,8 +49,9 @@ function ScreenSaverWidget:update()
height = self.height, height = self.height,
self.widget, self.widget,
} }
self.dithered = true
self[1] = self.main_frame self[1] = self.main_frame
UIManager:setDirty("all", function() UIManager:setDirty(self, function()
local update_region = self.main_frame.dimen local update_region = self.main_frame.dimen
return "partial", update_region return "partial", update_region
end) end)
@ -66,14 +67,12 @@ end
function ScreenSaverWidget:onTap(_, ges) function ScreenSaverWidget:onTap(_, ges)
if ges.pos:intersectWith(self.main_frame.dimen) then if ges.pos:intersectWith(self.main_frame.dimen) then
self:onClose() self:onClose()
UIManager:setDirty("all", "full")
end end
return true return true
end end
function ScreenSaverWidget:onClose() function ScreenSaverWidget:onClose()
UIManager:close(self) UIManager:close(self, "full")
UIManager:setDirty("all", "full")
return true return true
end end

@ -200,7 +200,7 @@ function TextViewer:init()
dimen = self.region, dimen = self.region,
self.movable, self.movable,
} }
UIManager:setDirty("all", function() UIManager:setDirty(self, function()
local update_region = self.frame.dimen:combine(orig_dimen) local update_region = self.frame.dimen:combine(orig_dimen)
logger.dbg("update region", update_region) logger.dbg("update region", update_region)
return "partial", update_region return "partial", update_region

@ -141,7 +141,8 @@ function TouchMenuItem:onTapSelect(arg, ges)
self.menu:onMenuSelect(self.item) self.menu:onMenuSelect(self.item)
else else
self.item_frame.invert = true self.item_frame.invert = true
UIManager:setDirty(self.show_parent, function() UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "fast", self.dimen return "fast", self.dimen
end) end)
-- yield to main UI loop to invert item -- yield to main UI loop to invert item
@ -153,6 +154,7 @@ function TouchMenuItem:onTapSelect(arg, ges)
-- Since it's an *un*highlight containing text, we make it "ui" and not "fast", both so it won't mangle text, -- 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... -- 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 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)
UIManager:setDirty(self.show_parent, function() UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen return "ui", self.dimen
end) end)
@ -173,7 +175,8 @@ function TouchMenuItem:onHoldSelect(arg, ges)
self.menu:onMenuHold(self.item) self.menu:onMenuHold(self.item)
else else
self.item_frame.invert = true self.item_frame.invert = true
UIManager:setDirty(self.show_parent, function() UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "fast", self.dimen return "fast", self.dimen
end) end)
UIManager:tickAfterNext(function() UIManager:tickAfterNext(function()
@ -181,6 +184,8 @@ function TouchMenuItem:onHoldSelect(arg, ges)
end) end)
UIManager:scheduleIn(0.5, function() UIManager:scheduleIn(0.5, function()
self.item_frame.invert = false 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() UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen return "ui", self.dimen
end) end)
@ -431,13 +436,13 @@ function TouchMenu:init()
icon = "resources/icons/appbar.chevron.left.png", icon = "resources/icons/appbar.chevron.left.png",
callback = function() self:onPrevPage() end, callback = function() self:onPrevPage() end,
bordersize = 0, bordersize = 0,
show_parent = self, show_parent = self.show_parent,
} }
self.page_info_right_chev = Button:new{ self.page_info_right_chev = Button:new{
icon = "resources/icons/appbar.chevron.right.png", icon = "resources/icons/appbar.chevron.right.png",
callback = function() self:onNextPage() end, callback = function() self:onNextPage() end,
bordersize = 0, bordersize = 0,
show_parent = self, show_parent = self.show_parent,
} }
self.page_info_left_chev:hide() self.page_info_left_chev:hide()
self.page_info_right_chev:hide() self.page_info_right_chev:hide()
@ -518,8 +523,8 @@ function TouchMenu:init()
end end
function TouchMenu:onCloseWidget() function TouchMenu:onCloseWidget()
-- NOTE: We pass a nil region to ensure a full-screen flash to avoid ghosting -- NOTE: We don't pass a region in order to ensure a full-screen flash to avoid ghosting
UIManager:setDirty(nil, "flashui", nil) UIManager:setDirty(nil, "flashui")
end end
function TouchMenu:_recalculatePageLayout() function TouchMenu:_recalculatePageLayout()
@ -605,7 +610,8 @@ function TouchMenu:updateItems()
-- NOTE: We use a slightly ugly hack to detect a brand new menu vs. a tab switch, -- NOTE: We use a slightly ugly hack to detect a brand new menu vs. a tab switch,
-- in order to optionally flash on initial menu popup... -- in order to optionally flash on initial menu popup...
UIManager:setDirty("all", function() -- NOTE: Also avoid repainting what's underneath us on initial popup.
UIManager:setDirty(self.is_fresh and self.show_parent or "all", function()
local refresh_dimen = local refresh_dimen =
old_dimen and old_dimen:combine(self.dimen) old_dimen and old_dimen:combine(self.dimen)
or self.dimen or self.dimen

@ -133,7 +133,9 @@ function VirtualKey:update_keyboard(want_flash, want_fast)
if want_fast then if want_fast then
refresh_type = "fast" refresh_type = "fast"
end end
UIManager:setDirty(self.keyboard, function() -- Only repaint the key itself, not the full board...
UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
logger.dbg("update key region", self[1].dimen) logger.dbg("update key region", self[1].dimen)
return refresh_type, self[1].dimen return refresh_type, self[1].dimen
end) end)

@ -146,7 +146,7 @@ function BookInfoManager:createDB()
-- Check version (not updated by previous exec if already there) -- Check version (not updated by previous exec if already there)
local res = db_conn:exec("SELECT value FROM config where key='version';") local res = db_conn:exec("SELECT value FROM config where key='version';")
if res[1][1] ~= BOOKINFO_DB_VERSION then if res[1][1] ~= BOOKINFO_DB_VERSION then
logger.warn("BookInfo cache DB schema updated from version ", res[1][1], "to version", BOOKINFO_DB_VERSION) logger.warn("BookInfo cache DB schema updated from version", res[1][1], "to version", BOOKINFO_DB_VERSION)
logger.warn("Deleting existing", self.db_location, "to recreate it") logger.warn("Deleting existing", self.db_location, "to recreate it")
db_conn:close() db_conn:close()
os.remove(self.db_location) os.remove(self.db_location)
@ -451,7 +451,7 @@ function BookInfoManager:extractBookInfo(filepath, cover_specs)
-- release memory used by uncompressed data: -- release memory used by uncompressed data:
cover_data = nil -- luacheck: no unused cover_data = nil -- luacheck: no unused
dbrow.cover_dataz = SQ3.blob(cover_dataz) -- cast to blob for sqlite dbrow.cover_dataz = SQ3.blob(cover_dataz) -- cast to blob for sqlite
logger.dbg("cover for", filename, "scaled by", scale_factor, "=>", cbb_w, "x", cbb_h, "(compressed from ", dbrow.cover_datalen, " to ", cover_dataz:len()) logger.dbg("cover for", filename, "scaled by", scale_factor, "=>", cbb_w, "x", cbb_h, ", compressed from", dbrow.cover_datalen, "to", cover_dataz:len())
end end
end end
end end
@ -554,7 +554,7 @@ function BookInfoManager:collectSubprocesses()
-- have caused a terminateBackgroundJobs() - if we're here, it's -- have caused a terminateBackgroundJobs() - if we're here, it's
-- that user has left reader in FileBrower and went away) -- that user has left reader in FileBrower and went away)
if util.gettime() > self.subprocesses_last_added_ts + self.subprocesses_killall_timeout_seconds then if util.gettime() > self.subprocesses_last_added_ts + self.subprocesses_killall_timeout_seconds then
logger.warn("Some subprocess were running for too long, killing them") logger.warn("Some subprocesses were running for too long, killing them")
self:terminateBackgroundJobs() self:terminateBackgroundJobs()
-- we'll collect them next time we're run -- we'll collect them next time we're run
end end

@ -77,6 +77,7 @@ function CoverMenu:updateItems(select_number)
collectgarbage() collectgarbage()
-- Specific UI building implementation (defined in some other module) -- Specific UI building implementation (defined in some other module)
self._has_cover_images = false
self:_updateItemsBuildUI() self:_updateItemsBuildUI()
-- Set the local variables with the things we know -- Set the local variables with the things we know
@ -89,11 +90,12 @@ function CoverMenu:updateItems(select_number)
if self.show_path then if self.show_path then
self.path_text.text = self:truncatePath(self.path) self.path_text.text = self:truncatePath(self.path)
end end
UIManager:setDirty("all", function() self.show_parent.dithered = self._has_cover_images
UIManager:setDirty(self.show_parent, function()
local refresh_dimen = local refresh_dimen =
old_dimen and old_dimen:combine(self.dimen) old_dimen and old_dimen:combine(self.dimen)
or self.dimen or self.dimen
return "partial", refresh_dimen return "partial", refresh_dimen, self.show_parent.dithered
end) end)
-- As additionally done in FileChooser:updateItems() -- As additionally done in FileChooser:updateItems()
@ -137,13 +139,14 @@ function CoverMenu:updateItems(select_number)
item:update() item:update()
if item.bookinfo_found then if item.bookinfo_found then
logger.dbg(" found", item.text) logger.dbg(" found", item.text)
self.show_parent.dithered = item._has_cover_image
local refreshfunc = function() local refreshfunc = function()
if item.refresh_dimen then if item.refresh_dimen then
-- MosaicMenuItem may exceed its own dimen in its paintTo -- MosaicMenuItem may exceed its own dimen in its paintTo
-- with its "description" hint -- with its "description" hint
return "ui", item.refresh_dimen return "ui", item.refresh_dimen, self.show_parent.dithered
else else
return "ui", item[1].dimen return "ui", item[1].dimen, self.show_parent.dithered
end end
end end
UIManager:setDirty(self.show_parent, refreshfunc) UIManager:setDirty(self.show_parent, refreshfunc)

@ -288,6 +288,9 @@ function ListMenuItem:update()
wimage, wimage,
} }
} }
-- Let menu know it has some item with images
self.menu._has_cover_images = true
self._has_cover_image = true
else else
-- empty element the size of an image -- empty element the size of an image
wleft = CenterContainer:new{ wleft = CenterContainer:new{

@ -498,6 +498,9 @@ function MosaicMenuItem:update()
image, image,
} }
} }
-- Let menu know it has some item with images
self.menu._has_cover_images = true
self._has_cover_image = true
else else
-- add Series metadata if requested -- add Series metadata if requested
if bookinfo.series then if bookinfo.series then

@ -173,14 +173,16 @@ function DoubleKeyValueItem:onTap()
UIManager:close(info) UIManager:close(info)
else else
self[1].invert = true self[1].invert = true
UIManager:setDirty(self.show_parent, function() UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "fast", self[1].dimen return "fast", self[1].dimen
end) end)
UIManager:tickAfterNext(function() UIManager:tickAfterNext(function()
self.callback() self.callback()
UIManager:close(info) UIManager:close(info)
self[1].invert = false self[1].invert = false
UIManager:setDirty(self.show_parent, function() UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
return "ui", self[1].dimen return "ui", self[1].dimen
end) end)
end) end)

@ -260,8 +260,7 @@ function GoodreadsBook:onAnyKeyPressed()
end end
function GoodreadsBook:onClose() function GoodreadsBook:onClose()
UIManager:setDirty("all") UIManager:close(self, "flashui")
UIManager:close(self)
return true return true
end end

@ -235,8 +235,7 @@ function KoboLight:addToMainMenu(menu_items)
(self:disabled() and _("Do you want to enable the frontlight gesture controller?") or _("Do you want to disable the frontlight gesture controller?")), (self:disabled() and _("Do you want to enable the frontlight gesture controller?") or _("Do you want to disable the frontlight gesture controller?")),
ok_text = self:disabled() and _("Enable") or _("Disable"), ok_text = self:disabled() and _("Enable") or _("Disable"),
ok_callback = function() ok_callback = function()
UIManager:close(image) UIManager:close(image, "full")
UIManager:setDirty("all", "full")
UIManager:show(InfoMessage:new{ UIManager:show(InfoMessage:new{
text = T(_("You have %1 the frontlight gesture controller. It will take effect on next restart."), text = T(_("You have %1 the frontlight gesture controller. It will take effect on next restart."),
self:disabled() and _("enabled") or _("disabled")) self:disabled() and _("enabled") or _("disabled"))
@ -245,8 +244,7 @@ function KoboLight:addToMainMenu(menu_items)
end, end,
cancel_text = _("Close"), cancel_text = _("Close"),
cancel_callback = function() cancel_callback = function()
UIManager:close(image) UIManager:close(image, "full")
UIManager:setDirty("all", "full")
end, end,
}) })
UIManager:setDirty("all", "full") UIManager:setDirty("all", "full")

Loading…
Cancel
Save