@ -371,16 +371,24 @@ end
--[[--
Registers and shows a widget .
Modal widget should be always on top .
For refreshtype & refreshregion see description of setDirty ( ) .
Widgets are registered in a stack , from bottom to top in registration order ,
with a few tweaks to handle modals & toasts :
toast widgets are stacked together on top ,
then modal widgets are stacked together , and finally come standard widgets .
If you think about how painting will be handled ( also bottom to top ) , this makes perfect sense ; ) .
For more details about refreshtype , refreshregion & refreshdither see the description of ` setDirty ` .
If refreshtype is omitted , no refresh will be enqueued at this time .
@ param widget a @ { ui.widget . widget | widget } object
@ string refreshtype ` " full " ` , ` " flashpartial " ` , ` " flashui " ` , ` " partial " ` , ` " ui " ` , ` " fast " ` ( optional )
@ param refreshregion a rectangle @ { ui.geometry . Geom | Geom } object ( optional , requires refreshtype to be set )
@ int x horizontal screen offset ( optional , ` 0 ` if omitted )
@ int y vertical screen offset ( optional , ` 0 ` if omitted )
@ bool refreshdither ` true ` if widget requires dithering ( optional , requires refreshtype to be set )
@ see setDirty
] ]
---- @param widget a widget object
---- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast"
---- @param refreshregion a Geom object
---- @int x
---- @int y
---- @param refreshdither an optional bool
---- @see setDirty
function UIManager : show ( widget , refreshtype , refreshregion , x , y , refreshdither )
if not widget then
logger.dbg ( " widget not exist to be shown " )
@ -422,13 +430,18 @@ end
--[[--
Unregisters a widget .
For refreshtype & refreshregion see description of setDirty ( ) .
It will be removed from the stack .
Will flag uncovered widgets as dirty .
For more details about refreshtype , refreshregion & refreshdither see the description of ` setDirty ` .
If refreshtype is omitted , no extra refresh will be enqueued at this time , leaving only those from the uncovered widgets .
@ param widget a @ { ui.widget . widget | widget } object
@ string refreshtype ` " full " ` , ` " flashpartial " ` , ` " flashui " ` , ` " partial " ` , ` " ui " ` , ` " fast " ` ( optional )
@ param refreshregion a rectangle @ { ui.geometry . Geom | Geom } object ( optional , requires refreshtype to be set )
@ bool refreshdither ` true ` if the refresh requires dithering ( optional , requires refreshtype to be set )
@ see setDirty
] ]
---- @param widget a widget object
---- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast"
---- @param refreshregion a Geom object
---- @param refreshdither an optional bool
---- @see setDirty
function UIManager : close ( widget , refreshtype , refreshregion , refreshdither )
if not widget then
logger.dbg ( " widget to be closed does not exist " )
@ -535,7 +548,15 @@ dbg:guard(UIManager, 'schedule',
assert ( action ~= nil )
end )
--- Schedules task in a certain amount of seconds (fractions allowed) from now.
--[[--
Schedules a task to be run a certain amount of seconds from now .
@ number seconds scheduling delay in seconds ( supports decimal values )
@ func action reference to the task to be scheduled ( may be anonymous )
@ param ... optional arguments passed to action
@ see unschedule
] ]
function UIManager : scheduleIn ( seconds , action , ... )
local when = { ffiUtil.gettime ( ) }
local s = math.floor ( seconds )
@ -553,13 +574,32 @@ dbg:guard(UIManager, 'scheduleIn',
assert ( seconds >= 0 , " Only positive seconds allowed " )
end )
function UIManager : nextTick ( action )
return self : scheduleIn ( 0 , action )
--[[--
Schedules a task for the next UI tick .
@ func action reference to the task to be scheduled ( may be anonymous )
@ param ... optional arguments passed to action
@ see scheduleIn
] ]
function UIManager : nextTick ( action , ... )
return self : scheduleIn ( 0 , action , ... )
end
-- Useful to run UI callbacks ASAP without skipping repaints
function UIManager : tickAfterNext ( action )
return self : nextTick ( function ( ) self : nextTick ( action ) end )
--[[--
Schedules a task to be run two UI ticks from now .
Useful to run UI callbacks ASAP without skipping repaints .
@ func action reference to the task to be scheduled ( may be anonymous )
@ param ... optional arguments passed to action
@ see nextTick
] ]
function UIManager : tickAfterNext ( action , ... )
-- Storing varargs is a bit iffy as we don't build LuaJIT w/ 5.2 compat, so we don't have access to table.pack...
-- c.f., http://lua-users.org/wiki/VarargTheSecondClassCitizen
local n = select ( ' # ' , ... )
local va = { ... }
return self : nextTick ( function ( ) self : nextTick ( action , unpack ( va , 1 , n ) ) end )
end
--[[
-- NOTE: This appears to work *nearly* just as well, but does sometimes go too fast (might depend on kernel HZ & NO_HZ settings?)
@ -568,14 +608,18 @@ function UIManager:tickAfterNext(action)
end
--]]
--[[-- Unschedules an execution task.
--[[--
Unschedules a previously scheduled task .
In order to unschedule anonymous functions , store a reference .
@ func action
@ see scheduleIn
@ usage
self.anonymousFunction = function ( ) self : regularFunction ( ) end
UIManager : scheduleIn ( 10 , self.anonymousFunction )
UIManager : scheduleIn ( 10.5 , self.anonymousFunction )
UIManager : unschedule ( self.anonymousFunction )
] ]
function UIManager : unschedule ( action )
@ -592,15 +636,15 @@ dbg:guard(UIManager, 'unschedule',
function ( self , action ) assert ( action ~= nil ) end )
--[[--
Registers a widget to be repainted and enqueues a refresh .
Mark a window - level widget as dirty , enqueuing a repaint & refresh request for that widget , to be processed on the next UI tick .
t he second parameter ( refreshtype ) can either specify a refreshtype
T he second parameter ( refreshtype ) can either specify a refreshtype
( optionally in combination with a refreshregion - which is suggested ,
and an even more optional refreshdither flag if the content requires dithering )
or a function that returns a refreshtype , refreshregion tuple ( or a refreshtype , refreshregion , refreshdither triple )
and is called * after * painting the widget .
and an even more optional refreshdither flag if the content requires dithering ) ;
or a function that returns a refreshtype , refreshregion tuple ( or a refreshtype , refreshregion , refreshdither triple ) ,
which will be called * after * painting the widget .
This is an interesting distinction , because a widget ' s geometry,
usually stored in a field named ` dimen ` , in only computed at painting time ( e.g . , during ` paintTo ` ) .
usually stored in a field named ` dimen ` , is ( generally ) only computed at painting time ( e.g . , during ` paintTo ` ) .
The TL ; DR being : if you already know the region , you can pass everything by value directly ,
( it ' ll make for slightly more readable debug logs),
but if the region will only be known after the widget has been painted , pass a function .
@ -609,50 +653,60 @@ In practice, since the stack of (both types of) refreshes is optimized into as f
and that during the next ` _repaint ` tick ( which is when ` paintTo ` for dirty widgets happens ) ,
this shouldn ' t change much in the grand scheme of things, but it ought to be noted ;).
See ` _repaint ` for more details about how the repaint & refresh queues are processed ,
and ` handleInput ` for more details about when those queues are actually drained .
What you should essentially remember is that ` setDirty ` doesn ' t actually "do" anything visible on its own.
It doesn ' t block, and when it returns, nothing new has actually been painted or refreshed.
It just appends stuff to the paint and / or refresh queues .
Here ' s a quick rundown of what each refreshtype should be used for:
full : high - fidelity flashing refresh ( e.g . , large images ) .
Highest quality , but highest latency .
Don ' t abuse if you only want a flash (in this case, prefer flashpartial or flashui).
partial : medium fidelity refresh ( e.g . , text on a white background ) .
Can be promoted to flashing after FULL_REFRESH_COUNT refreshes .
Don ' t abuse to avoid spurious flashes.
ui : medium fidelity refresh ( e.g . , mixed content ) .
Should apply to most UI elements .
fast : low fidelity refresh ( e.g . , monochrome content ) .
Should apply to most highlighting effects achieved through inversion .
Note that if your highlighted element contains text ,
you might want to keep the unhighlight refresh as " ui " instead , for crisper text .
( Or optimize that refresh away entirely , if you can get away with it ) .
flashui : like ui , but flashing .
Can be used when showing a UI element for the first time , to avoid ghosting .
flashpartial : like partial , but flashing ( and not counting towards flashing promotions ) .
Can be used when closing an UI element , to avoid ghosting .
You can even drop the region in these cases , to ensure a fullscreen flash .
NOTE : On REAGL devices , " flashpartial " will NOT actually flash ( by design ) .
As such , even onCloseWidget , you might prefer " flashui " in some rare instances .
NOTE : You ' ll notice a trend on UI elements that are usually shown *over* some kind of text
of using " ui " onShow & onUpdate , but " partial " onCloseWidget .
This is by design : " partial " is what the reader uses , as it ' s tailor-made for pure text
over a white background , so this ensures we resume the usual flow of the reader .
The same dynamic is true for their flashing counterparts , in the rare instances we enforce flashes .
Any kind of " partial " refresh * will * count towards a flashing promotion after FULL_REFRESH_COUNT refreshes ,
so making sure your stuff only applies to the proper region is key to avoiding spurious large black flashes .
That said , depending on your use case , using " ui " onCloseWidget can be a perfectly valid decision ,
and will ensure never seeing a flash because of that widget .
Remember that the FM uses " ui " , so , if said widgets are shown over the FM ,
prefer using " ui " or " flashui " onCloseWidget .
* ` full ` : high - fidelity flashing refresh ( e.g . , large images ) .
Highest quality , but highest latency .
Don ' t abuse if you only want a flash (in this case, prefer `flashui` or `flashpartial`).
* ` partial ` : medium fidelity refresh ( e.g . , text on a white background ) .
Can be promoted to flashing after ` FULL_REFRESH_COUNT ` refreshes .
Don ' t abuse to avoid spurious flashes.
In practice , this means this should mostly always be limited to ReaderUI .
* ` ui ` : medium fidelity refresh ( e.g . , mixed content ) .
Should apply to most UI elements .
When in doubt , use this .
* ` fast ` : low fidelity refresh ( e.g . , monochrome content ) .
Should apply to most highlighting effects achieved through inversion .
Note that if your highlighted element contains text ,
you might want to keep the unhighlight refresh as ` " ui " ` instead , for crisper text .
( Or optimize that refresh away entirely , if you can get away with it ) .
* ` flashui ` : like ` ui ` , but flashing .
Can be used when showing a UI element for the first time , or when closing one , to avoid ghosting .
* ` flashpartial ` : like ` partial ` , but flashing ( and not counting towards flashing promotions ) .
Can be used when closing an UI element ( usually over ReaderUI ) , to avoid ghosting .
You can even drop the region in these cases , to ensure a fullscreen flash .
NOTE : On REAGL devices , ` flashpartial ` will NOT actually flash ( by design ) .
As such , even onCloseWidget , you might prefer ` flashui ` in most instances .
NOTE : You ' ll notice a trend on UI elements that are usually shown *over* some kind of text (generally ReaderUI)
of using ` " ui " ` onShow & onUpdate , but ` " partial " ` onCloseWidget .
This is by design : ` " partial " ` is what the reader ( ReaderUI ) uses , as it ' s tailor-made for pure text
over a white background , so this ensures we resume the usual flow of the reader .
The same dynamic is true for their flashing counterparts , in the rare instances we enforce flashes .
Any kind of ` " partial " ` refresh * will * count towards a flashing promotion after ` FULL_REFRESH_COUNT ` refreshes ,
so making sure your stuff only applies to the proper region is key to avoiding spurious large black flashes .
That said , depending on your use case , using ` " ui " ` onCloseWidget can be a perfectly valid decision ,
and will ensure never seeing a flash because of that widget .
Remember that the FM uses ` " ui " ` , so , if said widgets are shown over the FM ,
prefer using ` " ui " ` or ` " flashui " ` onCloseWidget .
The final parameter ( refreshdither ) is an optional hint for devices with hardware dithering support that this repaint
could benefit from dithering ( i.e . , it contains an image ) .
could benefit from dithering ( e.g . , because it contains an image ) .
As far as the actual lifecycle of a widget goes , the rules are :
* What you ` show ` , you ` close ` .
* If you know the dimensions of the widget ( or simply of the region you want to refresh ) , you can pass it directly :
* to show ( as show calls setDirty ) ,
* to close ( as close will also call setDirty on the remaining dirty and visible widgets ,
and will also enqueue a refresh based on that if there are dirty widgets ) .
* Otherwise , you can use , respectively , the ` Show ` & ` CloseWidget ` handlers for that via ` setDirty ` calls .
* to ` show ` ( as ` show ` calls ` setDirty ` ) ,
* to ` close ` ( as ` close ` will also call ` setDirty ` on the remaining dirty and visible widgets ,
and will also enqueue a refresh based on that if there are dirty widgets ) .
* Otherwise , you can use , respectively , a widget ' s `Show` & `CloseWidget` handlers for that via `setDirty` calls .
This can also be useful if * child * widgets have specific needs ( e.g . , flashing , dithering ) that they want to inject in the refresh queue .
* Remember that events propagate children first ( in array order , starting at the top ) , and that if * any * event handler returns true ,
the propagation of that specific event for this widget tree stops * immediately * .
@ -660,9 +714,12 @@ As far as the actual lifecycle of a widget goes, the rules are:
you generally * don ' t* want to return true in `Show` or `CloseWidget` handlers).
* If any widget requires freeing non - Lua resources ( e.g . , FFI / C ) , having a ` free ` method called from its ` CloseWidget ` handler is ideal :
this ' ll ensure that *any* widget including it will be sure that resources are freed when it (or its parent) are closed.
* Note that there * is * a ` Close ` event , but it is * only * broadcast ( e.g . , sent to every widget in the window stack ;
the same rules about propagation apply , but only per * window - level widget * ) at poweroff / reboot , so ,
refrain from implementing custom onClose methods if that ' s not their intended purpose ;).
* Note that there * is * a ` Close ` event , but it has very specific use - cases , generally involving * programmatically * ` close ` ing a ` show ` n widget :
* It is broadcast ( e.g . , sent to every widget in the window stack ; the same rules about propagation apply , but only per * window - level widget * )
at poweroff / reboot .
* It can also be used as a keypress handler by @ { ui.widget . container.inputcontainer | InputContainer } , generally bound to the Back key .
Please refrain from implementing custom ` onClose ` methods if that ' s not their intended purpose ;).
On the subject of widgets and child widgets ,
you might have noticed an unspoken convention across the codebase of widgets having a field called ` show_parent ` .
@ -670,23 +727,23 @@ Since handling this is entirely at the programmer's behest, here's how we usuall
Basically , we cascade a field named ` show_parent ` to every child widget that matter
( e.g . , those that serve an UI purpose , as opposed to , say , a container ) .
This ensures that every subwidget can reference its actual parent
( ideally , all the way to the window - level widget it belongs to , i.e . , the one that was passed to UIManager : show , hence the name ; ) ) ,
to , among other things , flag the right widget as setDirty ( c.f . , those pesky debug warnings when that ' s done wrong ;p) when they want to request a repaint.
This is why you often see stuff doing , when instantiating a new widget , FancyWidget : new { show_parent = self.show_parent or self } ;
( ideally , all the way to the window - level widget it belongs to , i.e . , the one that was passed to ` show ` , hence the name ; ) ) ,
to , among other things , flag the right widget for repaint via ` setDirty ` ( c.f . , those pesky debug warnings when that ' s done wrong ;p) when they want to request a repaint.
This is why you often see stuff doing , when instantiating a new widget , ` FancyWidget : new { show_parent = self.show_parent or self } ` ;
meaning , if I ' m already a subwidget, cascade my parent, otherwise, it means I ' m a window - level widget , so cascade myself as that widget ' s parent ;).
Another convention ( that a few things rely on ) is naming a ( persistent ) MovableContainer wrapping a full widget ` movable ` , accessible as an instance field .
This is useful when it ' s used for transparency purposes, which, e.g., Button relies on to handle highlighting inside a transparent widget properly,
This is useful when it ' s used for transparency purposes, which, e.g., `setDirty` and @{ui.widget.button|Button} rely on to handle updating translucent widgets properly,
by checking if self.show_parent . movable exists and is currently translucent ; ) .
When I mentioned passing the * right * widget to ` setDirty ` earlier , what I meant is that setDirty will only actually flag a widget for repaint
When I mentioned passing the * right * widget to ` setDirty ` earlier , what I meant is that ` setDirty ` will only actually flag a widget for repaint
* if * that widget is a window - level widget ( that is , a widget that was passed to ` show ` earlier and hasn ' t been `close` ' d yet ) ,
hence the self.show_parent convention detailed above to get at the proper widget from within a subwidget ; ) .
hence the ` self.show_parent ` convention detailed above to get at the proper widget from within a subwidget ; ) .
Otherwise , you ' ll notice in debug mode that a debug guard will shout at you if that contract is broken,
and what happens in practice is the same thing as if an explicit ` nil ` were passed : no widgets will actually be flagged for repaint ,
and only the * refresh * matching the requested region * will * be enqueued .
This is why you ' ll find a number of valid use-cases for passing a nil here, when you *just* want a screen refresh without a repaint :).
The string " all " is also accepted in place of a widget , and will do the obvious thing : flag the * full * window stack , bottom to top , for repaint ,
This is why you ' ll find a number of valid use-cases for passing a ` nil` here, when you *just* want a screen refresh without a repaint :).
The string ` " all " ` is also accepted in place of a widget , and will do the obvious thing : flag the * full * window stack , bottom to top , for repaint ,
while still honoring the refresh region ( e.g . , this doesn ' t enforce a full-screen refresh).
@ usage
@ -695,11 +752,11 @@ UIManager:setDirty(self.widget, "partial")
UIManager : setDirty ( self.widget , " partial " , Geom : new { x = 10 , y = 10 , w = 100 , h = 50 } )
UIManager : setDirty ( self.widget , function ( ) return " ui " , self.someelement . dimen end )
--]]
---- @param widget a window-level widget object, "all", or nil
---- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast"
---- @param refreshregion an optional Geom object
---- @param refreshdither an optional bool
@ param widget a window - level widget object , ` " all " ` , or ` nil `
@ param refreshtype ` " full " ` , ` " flashpartial " ` , ` " flashui " ` , ` " partial " ` , ` " ui " ` , ` " fast " ` ( or a lambda , see description above )
@ param refreshregion a rectangle @ { ui.geometry . Geom | Geom } object ( optional , omitting it means the region will cover the full screen )
@ bool refreshdither ` true ` if widget requires dithering ( optional )
] ]
function UIManager : setDirty ( widget , refreshtype , refreshregion , refreshdither )
if widget then
if widget == " all " then
@ -715,11 +772,34 @@ function UIManager:setDirty(widget, refreshtype, refreshregion, refreshdither)
end
end
elseif not widget.invisible then
-- 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
-- NOTE: If our widget is translucent, or belongs to a translucent MovableContainer,
-- we'll want to flag everything below it as dirty, too,
-- because doing transparency right requires having an up to date background against which to blend.
-- (The typecheck is because some widgets use an alpha boolean trap for internal alpha handling (e.g., ImageWidget)).
local handle_alpha = false
-- NOTE: We only ever check the dirty flag on top-level widgets, so only set it there!
-- Enable verbose debug to catch misbehaving widgets via our post-guard.
for i = # self._window_stack , 1 , - 1 do
if handle_alpha then
self._dirty [ self._window_stack [ i ] . widget ] = true
logger.dbg ( " setDirty: Marking as dirty widget: " , self._window_stack [ i ] . widget.name or self._window_stack [ i ] . widget.id or tostring ( self._window_stack [ i ] . widget ) , " because it's below translucent widget: " , widget.name or widget.id or tostring ( widget ) )
-- Stop flagging widgets at the uppermost one that covers the full screen
if self._window_stack [ i ] . widget.covers_fullscreen then
break
end
end
if self._window_stack [ i ] . widget == widget then
self._dirty [ widget ] = true
-- We've got a match, now check if it's translucent...
handle_alpha = ( widget.alpha and type ( widget.alpha ) == " number " and widget.alpha < 1 and widget.alpha > 0 )
or ( widget.movable and widget.movable . alpha and widget.movable . alpha < 1 and widget.movable . alpha > 0 )
-- We shouldn't be seeing the same widget at two different spots in the stack, so, we're done,
-- except when we need to keep looping to flag widgets below us in order to handle a translucent widget...
if not handle_alpha then
break
end
end
end
-- Again, if it's flagged as dithered, honor that
@ -731,11 +811,13 @@ function UIManager:setDirty(widget, refreshtype, refreshregion, refreshdither)
-- Another special case: if we did NOT specify a widget, but requested a full refresh nonetheless (i.e., a diagonal swipe),
-- we'll want to check the window stack in order to honor dithering...
if refreshtype == " full " then
for i = 1 , # self._window_stack do
for i = # self._window_stack , 1 , - 1 do
-- If any of 'em were dithered, honor their dithering hint
if self._window_stack [ i ] . widget.dithered then
logger.dbg ( " setDirty full on no specific widget: found a dithered widget, infecting the refresh queue " )
refreshdither = true
-- One is enough ;)
break
end
end
end
@ -745,8 +827,9 @@ function UIManager:setDirty(widget, refreshtype, refreshregion, refreshdither)
-- callback, will be issued after painting
table.insert ( self._refresh_func_stack , refreshtype )
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...
-- Thankfully, we can track them in _refresh()'s logging very soon after that...
-- NOTE: It's too early to tell what the function will return (especially the region), because the widget hasn't been painted yet.
-- Consuming the lambda now also appears have nasty side-effects that render it useless later, subtly breaking a whole lot of things...
-- 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 ) ) or " nil " )
end
else
@ -765,22 +848,28 @@ dbg:guard(UIManager, 'setDirty',
nil ,
function ( self , widget , refreshtype , refreshregion , refreshdither )
if not widget or widget == " all " then return end
-- when debugging, we check if we get handed a valid widget,
-- which would be a dialog that was previously passed via show()
-- when debugging, we check if we were handed a valid window-level widget,
-- which would be a widget that was previously passed to `show`.
local found = false
for i = 1 , # self._window_stack do
if self._window_stack [ i ] . widget == widget then found = true end
if self._window_stack [ i ] . widget == widget then
found = true
break
end
end
if not found then
dbg : v ( " INFO: invalid widget for setDirty() " , debug.traceback ( ) )
end
end )
-- Clear the full repaint & refreshes queues.
-- NOTE: Beware! This doesn't take any prisonners!
-- You shouldn't have to resort to this unless in very specific circumstances!
-- plugins/coverbrowser.koplugin/covermenu.lua building a franken-menu out of buttondialogtitle & buttondialog
-- and wanting to avoid inheriting their original paint/refresh cycle being a prime example.
--[[--
Clear the full repaint & refresh queues .
NOTE : Beware ! This doesn ' t take any prisonners!
You shouldn ' t have to resort to this unless in very specific circumstances!
plugins / coverbrowser.koplugin / covermenu.lua building a franken - menu out of buttondialogtitle & buttondialog
and wanting to avoid inheriting their original paint / refresh cycle being a prime example .
--]]
function UIManager : clearRenderStack ( )
logger.dbg ( " clearRenderStack: Clearing the full render stack! " )
self._dirty = { }
@ -801,9 +890,15 @@ function UIManager:removeZMQ(zeromq)
end
end
--- Sets full refresh rate for e-ink screen.
--
-- Also makes the refresh rate persistent in global reader settings.
--[[--
Sets the full refresh rate for e - ink screens ( ` FULL_REFRESH_COUNT ` ) .
This is the amount of ` " partial " ` refreshes before the next one gets promoted to ` " full " ` .
Also makes the refresh rate persistent in global reader settings .
@ see setDirty
--]]
function UIManager : setRefreshRate ( rate , night_rate )
logger.dbg ( " set screen full refresh rate " , rate , night_rate )
@ -825,11 +920,12 @@ function UIManager:setRefreshRate(rate, night_rate)
end
end
--- Gets full refresh rate for e-ink screen .
--- Returns the full refresh rate for e-ink screens (`FULL_REFRESH_COUNT`) .
function UIManager : getRefreshRate ( )
return G_reader_settings : readSetting ( " full_refresh_count " ) or DEFAULT_FULL_REFRESH_COUNT , G_reader_settings : readSetting ( " night_full_refresh_count " ) or G_reader_settings : readSetting ( " full_refresh_count " ) or DEFAULT_FULL_REFRESH_COUNT
end
--- Toggles Night Mode (i.e., inverted rendering).
function UIManager : ToggleNightMode ( night_mode )
if night_mode then
self.FULL_REFRESH_COUNT = G_reader_settings : readSetting ( " night_full_refresh_count " ) or G_reader_settings : readSetting ( " full_refresh_count " ) or DEFAULT_FULL_REFRESH_COUNT
@ -850,9 +946,13 @@ function UIManager:getTopWidget()
return top.widget
end
--- Get the *second* topmost widget, if there is one (name if possible, ref otherwise).
--- Useful when VirtualKeyboard is involved, as it *always* steals the top spot ;).
--- NOTE: Will skip over VirtualKeyboard instances, plural, in case there are multiple (because, apparently, we can do that.. ugh).
--[[--
Get the * second * topmost widget , if there is one ( name if possible , ref otherwise ) .
Useful when VirtualKeyboard is involved , as it * always * steals the top spot ; ) .
NOTE : Will skip over VirtualKeyboard instances , plural , in case there are multiple ( because , apparently , we can do that .. ugh ) .
--]]
function UIManager : getSecondTopmostWidget ( )
if # self._window_stack <= 1 then
-- Not enough widgets in the stack, bye!
@ -881,7 +981,7 @@ function UIManager:getSecondTopmostWidget()
return nil
end
--- Check if a widget is still in the window stack, or is a subwidget of a widget still in the window stack
--- 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 )
@ -892,7 +992,7 @@ function UIManager:isSubwidgetShown(widget, max_depth)
return false
end
--- Same , but only check window-level widgets (e.g., what's directly registered in the window stack), don't recurse
--- Same as `isSubwidgetShown` , 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
@ -902,7 +1002,11 @@ function UIManager:isWidgetShown(widget)
return false
end
-- Returns the region of the previous refresh
--[[--
Returns the region of the previous refresh .
@ return a rectangle @ { ui.geometry . Geom | Geom } object
] ]
function UIManager : getPreviousRefreshRegion ( )
return self._last_refresh_region
end
@ -930,7 +1034,11 @@ function UIManager:quit()
end
end
--- Request events to be ignored for some duration
--[[--
Request all @ { ui.event . Event | Event } s to be ignored for some duration .
@ param set_or_seconds either ` true ` , in which case a platform - specific delay is chosen , or a duration in seconds ( *** int *** ) .
] ]
function UIManager : discardEvents ( set_or_seconds )
if not set_or_seconds then -- remove any previously set
self._discard_events_till = nil
@ -959,7 +1067,11 @@ function UIManager:discardEvents(set_or_seconds)
self._discard_events_till = now_us + usecs
end
--- Transmits an event to an active widget.
--[[--
Transmits an @ { ui.event . Event | Event } to active widgets .
@ param event an @ { ui.event . Event | Event } object
] ]
function UIManager : sendEvent ( event )
if # self._window_stack == 0 then return end
@ -995,14 +1107,14 @@ function UIManager:sendEvent(event)
end
end
-- if the event is not consumed, active widgets (from top to bottom) can
-- access it. NOTE: _window_stack can shrink on close event
-- if the event is not consumed, active widgets (from top to bottom) can access it.
-- NOTE: _window_stack can shrink when widgets are closed (CloseWidget & Close events).
local checked_widgets = { top_widget }
for i = # self._window_stack , 1 , - 1 do
local widget = self._window_stack [ i ]
if checked_widgets [ widget ] == nil then
-- active widgets has precedence to handle this event
-- N ote: ReaderUI currently only has one active_widget: readerscreenshot
-- N OTE: While FileManager only has a single (screenshotter), ReaderUI has many active_widgets (each ReaderUI module gets added to the list).
if widget.widget . active_widgets then
checked_widgets [ widget ] = true
for _ , active_widget in ipairs ( widget.widget . active_widgets ) do
@ -1011,8 +1123,7 @@ function UIManager:sendEvent(event)
end
if widget.widget . is_always_active then
-- active widgets will handle this event
-- Note: is_always_active widgets currently are widgets that want to show a keyboard
-- and readerconfig
-- NOTE: is_always_active widgets currently are widgets that want to show a VirtualKeyboard or listen to Dispatcher events
checked_widgets [ widget ] = true
if widget.widget : handleEvent ( event ) then return end
end
@ -1020,7 +1131,11 @@ function UIManager:sendEvent(event)
end
end
--- Transmits an event to all registered widgets.
--[[--
Transmits an @ { ui.event . Event | Event } to all registered widgets .
@ param event an @ { ui.event . Event | Event } object
] ]
function UIManager : broadcastEvent ( event )
-- the widget's event handler might close widgets in which case
-- a simple iterator like ipairs would skip over some entries
@ -1092,7 +1207,7 @@ local refresh_methods = {
Compares refresh mode .
Will return the mode that takes precedence .
--] ]
] ]
local function update_mode ( mode1 , mode2 )
if refresh_modes [ mode1 ] > refresh_modes [ mode2 ] then
logger.dbg ( " update_mode: Update refresh mode " , mode2 , " to " , mode1 )
@ -1106,7 +1221,7 @@ 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 )
@ -1119,19 +1234,21 @@ end
--[[--
Enqueues a refresh .
Widgets call this in their paintTo ( ) method in order to notify
Widgets call this in their ` paintTo ( ) ` method in order to notify
UIManager that a certain part of the screen is to be refreshed .
@ param mode
refresh mode ( " full " , " flashpartial " , " flashui " , " partial " , " ui " , " fast " )
@ string mode
refresh mode ( ` " full " ` , ` " flashpartial " ` , ` " flashui " ` , ` " partial " ` , ` " ui " ` , ` " fast " ` )
@ param region
Rect( ) that specifies the region to be updated
o ptional, update will affect whole screen if not specified .
A rectangle @ { ui.geometry . Geom | Geom } object that specifies the region to be updated .
O ptional, update will affect whole screen if not specified .
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 .
--]]
@ bool dither
A hint to request hardware dithering ( if supported ) .
Optional , no dithering requested if not specified or not supported .
@ local Not to be used outside of UIManager !
] ]
function UIManager : _refresh ( mode , region , dither )
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...
@ -1139,6 +1256,8 @@ function UIManager:_refresh(mode, region, dither)
if dither then
mode = " ui "
else
-- Otherwise, this is most likely from a `show` or `close` that wasn't passed specific refresh details,
-- (which is the vast majority of them), in which case we drop it to avoid enqueuing a useless full-screen refresh.
return
end
end
@ -1183,7 +1302,7 @@ function UIManager:_refresh(mode, region, dither)
self.refresh_counted = true
end
-- if no region is specified, define default region
-- if no region is specified, use the screen's dimensions
region = region or Geom : new { w = Screen : getWidth ( ) , h = Screen : getHeight ( ) }
-- if no dithering hint was specified, don't request dithering
@ -1195,7 +1314,8 @@ function UIManager:_refresh(mode, region, dither)
-- as well as a few actually effective merges
-- (e.g., the disappearance of a selection HL with the following menu update).
for i = 1 , # self._refresh_stack do
-- check for collision with refreshes that are already enqueued
-- Check for collision with refreshes that are already enqueued
-- NOTE: intersect *means* intersect: we won't merge edge-to-edge regions (but the EPDC probably will).
if region : intersectWith ( self._refresh_stack [ i ] . region ) then
-- combine both refreshes' regions
local combined = region : combine ( self._refresh_stack [ i ] . region )
@ -1215,8 +1335,16 @@ function UIManager:_refresh(mode, region, dither)
table.insert ( self._refresh_stack , { mode = mode , region = region , dither = dither } )
end
--[[--
Repaints dirty widgets .
This will also drain the refresh queue , effectively refreshing the screen region ( s ) matching those freshly repainted widgets .
There may be refreshes enqueued without any widgets needing to be repainted ( c.f . , ` setDirty ` ' s behavior when passed a `nil` widget),
in which case , nothing is repainted , but the refreshes are still drained and executed .
--- Repaints dirty widgets.
@ local Not to be used outside of UIManager !
--]]
function UIManager : _repaint ( )
-- flag in which we will record if we did any repaints at all
-- will trigger a refresh if set.
@ -1255,12 +1383,15 @@ function UIManager:_repaint()
-- the widget can use this to decide which parts should be refreshed
logger.dbg ( " painting widget: " , widget.widget . name or widget.widget . id or tostring ( widget ) )
Screen : beforePaint ( )
-- NOTE: Nothing actually seems to use the final argument?
-- Could be used by widgets to know whether they're being repainted because they're actually dirty (it's true),
-- or because something below them was (it's nil).
widget.widget : paintTo ( Screen.bb , widget.x , widget.y , self._dirty [ widget.widget ] )
-- and remove from list after painting
self._dirty [ widget.widget ] = nil
-- trigger repaint
-- trigger a repaint for every widget above us, too
dirty = true
-- if any of 'em were dithered, we'll want to dither the final refresh
@ -1313,17 +1444,37 @@ function UIManager:_repaint()
self.refresh_counted = false
end
--- Explicitly drain the paint & refresh queues *now*, instead of waiting for the next UI tick.
function UIManager : forceRePaint ( )
self : _repaint ( )
end
--[[--
Ask the EPDC to * block * until our previous refresh ioctl has completed .
This interacts sanely with the existing low - level handling of this in ` framebuffer_mxcfb `
( i.e . , it doesn ' t even try to wait for a marker that fb has already waited for, and vice-versa).
Will return immediately if it has already completed .
If the device isn ' t a Linux + MXCFB device, this is a NOP.
] ]
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.
--[[--
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 .
This is an explicit repaint * now * : it bypasses and ignores the paint queue ( unlike ` setDirty ` ) .
@ param widget a @ { ui.widget . widget | widget } object
@ int x left origin of widget ( in the Screen buffer , e.g . , ` widget.dimen . x ` )
@ int y top origin of widget ( in the Screen buffer , e.g . , ` widget.dimen . y ` )
] ]
function UIManager : widgetRepaint ( widget , x , y )
if not widget then return end
@ -1331,7 +1482,16 @@ function UIManager:widgetRepaint(widget, x, y)
widget : paintTo ( Screen.bb , x , y )
end
-- Same idea, but does a simple invertRect, without actually repainting anything
--[[--
Same idea as ` widgetRepaint ` , but does a simple ` bb : invertRect ` on the Screen buffer , without actually going through the widget ' s `paintTo` method.
@ param widget a @ { ui.widget . widget | widget } object
@ int x left origin of the rectangle to invert ( in the Screen buffer , e.g . , ` widget.dimen . x ` )
@ int y top origin of the rectangle ( in the Screen buffer , e.g . , ` widget.dimen . y ` )
@ int w width of the rectangle ( optional , will use ` widget.dimen . w ` like ` paintTo ` would if omitted )
@ int h height of the rectangle ( optional , will use ` widget.dimen . h ` like ` paintTo ` would if omitted )
@ see widgetRepaint
--]]
function UIManager : widgetInvert ( widget , x , y , w , h )
if not widget then return end
@ -1457,9 +1617,11 @@ function UIManager:initLooper()
end
end
-- this is the main loop of the UI controller
-- it is intended to manage input events and delegate
-- them to dialogs
--[[--
This is the main loop of the UI controller .
It is intended to manage input events and delegate them to dialogs .
--]]
function UIManager : run ( )
self._running = true
self : initLooper ( )
@ -1483,17 +1645,18 @@ function UIManager:runForever()
return self : run ( )
end
-- The common operations should be performed before suspending the device. Ditto .
-- The common operations that should be performed before suspending the device.
function UIManager : _beforeSuspend ( )
self : flushSettings ( )
self : broadcastEvent ( Event : new ( " Suspend " ) )
end
-- The common operations should be performed after resuming the device. Ditto .
-- The common operations that should be performed after resuming the device.
function UIManager : _afterResume ( )
self : broadcastEvent ( Event : new ( " Resume " ) )
end
-- The common operations that should be performed when the device is plugged to a power source.
function UIManager : _beforeCharging ( )
if G_reader_settings : nilOrTrue ( " enable_charging_led " ) then
Device : toggleChargingLED ( true )
@ -1501,6 +1664,7 @@ function UIManager:_beforeCharging()
self : broadcastEvent ( Event : new ( " Charging " ) )
end
-- The common operations that should be performed when the device is unplugged from a power source.
function UIManager : _afterNotCharging ( )
if G_reader_settings : nilOrTrue ( " enable_charging_led " ) then
Device : toggleChargingLED ( false )
@ -1508,8 +1672,11 @@ function UIManager:_afterNotCharging()
self : broadcastEvent ( Event : new ( " NotCharging " ) )
end
-- Executes all the operations of a suspending request. This function usually puts the device into
-- suspension.
--[[--
Executes all the operations of a suspension ( i.e . , sleep ) request .
This function usually puts the device into suspension .
] ]
function UIManager : suspend ( )
if Device : isCervantes ( ) or Device : isKobo ( ) or Device : isSDL ( ) or Device : isRemarkable ( ) or Device : isSonyPRSTUX ( ) then
self.event_handlers [ " Suspend " ] ( )
@ -1520,7 +1687,11 @@ function UIManager:suspend()
end
end
-- Executes all the operations of a resume request. This function usually wakes up the device.
--[[--
Executes all the operations of a resume ( i.e . , wakeup ) request .
This function usually wakes up the device .
] ]
function UIManager : resume ( )
if Device : isCervantes ( ) or Device : isKobo ( ) or Device : isSDL ( ) or Device : isRemarkable ( ) or Device : isSonyPRSTUX ( ) then
self.event_handlers [ " Resume " ] ( )
@ -1529,19 +1700,27 @@ function UIManager:resume()
end
end
-- Release standby lock once. We're done with whatever we were doing in the background.
-- Standby is re-enabled only after all issued prevents are paired with allowStandby for each one.
--[[--
Release standby lock .
Called once we ' re done with whatever we were doing in the background.
Standby is re - enabled only after all issued prevents are paired with allowStandby for each one .
] ]
function UIManager : allowStandby ( )
assert ( self._prevent_standby_count > 0 , " allowing standby that isn't prevented; you have an allow/prevent mismatch somewhere " )
self._prevent_standby_count = self._prevent_standby_count - 1
end
-- Prevent standby, ie something is happening in background, yet UI may tick.
--[[--
Prevent standby .
i.e . , something is happening in background , yet UI may tick .
] ]
function UIManager : preventStandby ( )
self._prevent_standby_count = self._prevent_standby_count + 1
end
-- A llow/prevent calls above can interminently allow standbys, but we're not interested until
-- The a llow/prevent calls above can interminently allow standbys, but we're not interested until
-- the state change crosses UI tick boundary, which is what self._prev_prevent_standby_count is tracking.
function UIManager : _standbyTransition ( )
if self._prevent_standby_count == 0 and self._prev_prevent_standby_count > 0 then
@ -1558,10 +1737,12 @@ function UIManager:_standbyTransition()
self._prev_prevent_standby_count = self._prevent_standby_count
end
--- Broadcasts a `FlushSettings` Event to *all* widgets.
function UIManager : flushSettings ( )
self : broadcastEvent ( Event : new ( " FlushSettings " ) )
end
--- Sanely restart KOReader (on supported platforms).
function UIManager : restartKOReader ( )
self : quit ( )
-- This is just a magic number to indicate the restart request for shell scripts.