change setDirty/refresh API

See documentation in the code.
In short: There is now one single method, setDirty(), that triggers
repaints and/or refreshes.
All variables in UIManager are gone - at least from an external
perspective. Everything is done through setDirty().
This also allows for easier debugging, since all requests come
in via function calls.
pull/1306/head
Hans-Werner Hilse 10 years ago
parent 2f9386cd69
commit 229c5ad61c

@ -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

Loading…
Cancel
Save