@ -2,26 +2,16 @@ local Device = require("device")
local Screen = Device.screen
local Input = require ( " device " ) . input
local Event = require ( " ui/event " )
local Geom = require ( " ui/geometry " )
local util = require ( " ffi/util " )
local DEBUG = require ( " dbg " )
local _ = require ( " gettext " )
-- there is only one instance of this
local UIManager = {
-- force to repaint all the widget is stack, will be reset to false
-- after each ui loop
repaint_all = false ,
-- force to do full refresh, will be reset to false
-- after each ui loop
full_refresh = false ,
-- force to do partial refresh, will be reset to false
-- after each ui loop
partial_refresh = false ,
-- trigger a full refresh when counter reaches FULL_REFRESH_COUNT
FULL_REFRESH_COUNT = G_reader_settings : readSetting ( " full_refresh_count " ) or DRCOUNTMAX ,
refresh_count = 0 ,
-- only update specific regions of the screen
update_regions_func = nil ,
event_handlers = nil ,
@ -31,6 +21,8 @@ local UIManager = {
_execution_stack_dirty = false ,
_dirty = { } ,
_zeromqs = { } ,
_refresh_stack = { } ,
_refresh_func_stack = { } ,
}
function UIManager : init ( )
@ -105,7 +97,7 @@ function UIManager:show(widget, x, y)
end
end
-- and schedule it to be painted
self : setDirty ( widget )
self : setDirty ( widget , " partial " )
-- tell the widget that it is shown now
widget : handleEvent ( Event : new ( " Show " ) )
-- check if this widget disables double tap gesture
@ -135,7 +127,7 @@ function UIManager:close(widget)
if dirty then
-- schedule remaining widgets to be painted
for i = 1 , # self._window_stack do
self : setDirty ( self._window_stack [ i ] . widget )
self : setDirty ( self._window_stack [ i ] . widget , " partial " )
end
end
end
@ -176,16 +168,49 @@ function UIManager:unschedule(action)
end
end
-- register a widget to be repainted
function UIManager : setDirty ( widget , refresh_type )
-- "auto": request full refresh
-- "full": force full refresh
-- "partial": partial refresh
if not refresh_type then
refresh_type = " auto "
end
--[[
register a widget to be repainted and enqueue a refresh
the second parameter ( refreshtype ) can either specify a refreshtype
( optionally in combination with a refreshregion - which is suggested )
or a function that returns refreshtype AND refreshregion and is called
after painting the widget .
E.g . :
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 )
--]]
function UIManager : setDirty ( widget , refreshtype , refreshregion )
if widget then
self._dirty [ widget ] = refresh_type
if widget == " all " then
-- special case: set all top-level widgets as being "dirty".
for i = 1 , # self._window_stack do
self._dirty [ self._window_stack [ i ] . widget ] = true
end
else
self._dirty [ widget ] = true
if DEBUG.is_on then
-- when debugging, we check if we get handed a valid widget,
-- which would be a dialog that was previously passed via show()
local found = false
for i = 1 , # self._window_stack do
if self._window_stack [ i ] . widget == widget then found = true end
end
if not found then
DEBUG ( " INFO: invalid widget for setDirty() " , debug.traceback ( ) )
end
end
end
end
-- handle refresh information
if not refreshtype then return end
if type ( refreshtype ) == " function " then
-- callback, will be issued after painting
table.insert ( self._refresh_func_stack , refreshtype )
else
-- otherwise, enqueue refresh
self : _refresh ( refreshtype , refreshregion )
end
end
@ -252,7 +277,7 @@ function UIManager:sendEvent(event)
end
end
function UIManager : checkTasks( )
function UIManager : _ checkTasks( )
local now = { util.gettime ( ) }
-- check if we have timed events in our queue and search next one
@ -286,36 +311,85 @@ function UIManager:checkTasks()
return wait_until , now
end
-- precedence of refresh modes:
local refresh_modes = { fast = 1 , ui = 2 , partial = 3 , full = 4 }
-- refresh methods in framebuffer implementation
local refresh_methods = {
fast = " refreshFast " ,
ui = " refreshUI " ,
partial = " refreshPartial " ,
full = " refreshFull " ,
}
--[[
refresh mode comparision
will return the mode that takes precedence
--]]
local function update_mode ( mode1 , mode2 )
if refresh_modes [ mode1 ] > refresh_modes [ mode2 ] then
return mode1
else
return mode2
end
end
--[[
enqueue a refresh
Widgets call this in their paintTo ( ) method in order to notify
UIManager that a certain part of the screen is to be refreshed .
mode : refresh mode ( " full " , " partial " , " ui " , " fast " )
region : Rect ( ) that specifies the region to be updated
optional , update will affect whole screen if not specified .
Note that this should be the exception .
--]]
function UIManager : _refresh ( mode , region )
-- default mode is partial
mode = mode or " partial "
-- special case: full screen partial update
-- will get promoted every self.FULL_REFRESH_COUNT updates
if not region and mode == " partial " then
self.refresh_count = ( self.refresh_count + 1 ) % self.FULL_REFRESH_COUNT
if self.refresh_count == self.FULL_REFRESH_COUNT - 1 then
DEBUG ( " promote refresh to full refresh " )
mode = " full "
end
end
-- if no region is specified, define default region
region = region or Geom : new { w = Screen : getWidth ( ) , h = Screen : getHeight ( ) }
for i = 1 , # self._refresh_stack do
-- check for collision with updates that are already enqueued
if region : intersectWith ( self._refresh_stack [ i ] . region ) then
-- combine both refreshes' regions
local combined = region : combine ( self._refresh_stack [ i ] . region )
-- update the mode, if needed
local mode = update_mode ( mode , self._refresh_stack [ i ] . mode )
-- remove colliding update
table.remove ( self._refresh_stack , i )
-- and try again with combined data
return self : _refresh ( mode , combined )
end
end
-- if we hit no (more) collides, enqueue the update
table.insert ( self._refresh_stack , { mode = mode , region = region } )
end
-- repaint dirty widgets
function UIManager : repaint ( )
function UIManager : _ repaint( )
-- flag in which we will record if we did any repaints at all
-- will trigger a refresh if set.
local dirty = false
-- we use this to record requests for certain refresh types
-- TODO: fix this, see below
local force_full_refresh = self.full_refresh
self.full_refresh = false
local force_partial_refresh = self.partial_refresh
self.partial_refresh = false
local force_fast_refresh = false
for _ , widget in ipairs ( self._window_stack ) do
-- paint if repaint_all is request
-- paint also if current widget or any widget underneath is dirty
if self.repaint_all or dirty or self._dirty [ widget.widget ] then
widget.widget : paintTo ( Screen.bb , widget.x , widget.y )
-- self._dirty[widget.widget] may also be "auto"
if self._dirty [ widget.widget ] == " full " then
force_full_refresh = true
elseif self._dirty [ widget.widget ] == " partial " then
force_partial_refresh = true
elseif self._dirty [ widget.widget ] == " fast " then
force_fast_refresh = true
end
-- paint if current widget or any widget underneath is dirty
if dirty or self._dirty [ widget.widget ] then
-- pass hint to widget that we got when setting widget dirty
-- the widget can use this to decide which parts should be refreshed
widget.widget : paintTo ( Screen.bb , widget.x , widget.y , self._dirty [ widget.widget ] )
-- and remove from list after painting
self._dirty [ widget.widget ] = nil
@ -324,42 +398,30 @@ function UIManager:repaint()
dirty = true
end
end
self.repaint_all = false
if dirty then
-- select proper refresh mode
-- TODO: fix this. We should probably do separate refreshes
-- by regional refreshes (e.g. fast refresh, some partial refreshes)
-- and full-screen full refresh
local refresh
if force_fast_refresh then
refresh = Screen.refreshFast
elseif force_partial_refresh then
refresh = Screen.refreshPartial
elseif force_full_refresh or self.refresh_count == self.FULL_REFRESH_COUNT - 1 then
refresh = Screen.refreshFull
-- a full refresh will reset the counter which leads to an automatic full refresh
self.refresh_count = 0
else
-- default
refresh = Screen.refreshPartial
-- increment refresh counter in this case
self.refresh_count = ( self.refresh_count + 1 ) % self.FULL_REFRESH_COUNT
end
-- execute pending refresh functions
for _ , refreshfunc in ipairs ( self._refresh_func_stack ) do
local refreshtype , region = refreshfunc ( )
if refreshtype then self : _refresh ( refreshtype , region ) end
end
self._refresh_func_stack = { }
-- we should have at least one refresh if we did repaint.
-- If we don't, we add one now and print a warning if we
-- are debugging
if dirty and # self._refresh_stack == 0 then
DEBUG ( " WARNING: no refresh got enqueued. Will do a partial full screen refresh, which might be inefficient " )
self : _refresh ( " partial " )
end
if self.update_regions_func then
local update_regions = self.update_regions_func ( )
for _ , update_region in ipairs ( update_regions ) do
-- in some rare cases update region has 1 pixel offset
refresh ( Screen , update_region.x - 1 , update_region.y - 1 ,
update_region.w + 2 , update_region.h + 2 )
end
self.update_regions_func = nil
else
refresh ( Screen )
end
-- execute refreshes:
for _ , refresh in ipairs ( self._refresh_stack ) do
DEBUG ( " triggering refresh " , refresh )
Screen [ refresh_methods [ refresh.mode ] ] ( Screen ,
refresh.region . x - 1 , refresh.region . y - 1 ,
refresh.region . w + 2 , refresh.region . h + 2 )
end
self._refresh_stack = { }
end
-- this is the main loop of the UI controller
@ -373,7 +435,7 @@ function UIManager:run()
-- that will be honored when calculating the time to wait
-- for input events:
repeat
wait_until , now = self : checkTasks( )
wait_until , now = self : _ checkTasks( )
--DEBUG("---------------------------------------------------")
--DEBUG("exec stack", self._execution_stack)
@ -388,7 +450,7 @@ function UIManager:run()
return nil
end
self : repaint( )
self : _ repaint( )
until not self._execution_stack_dirty
-- wait for next event