2
0
mirror of https://github.com/koreader/koreader synced 2024-10-31 21:20:20 +00:00
koreader/frontend/ui/uimanager.lua

541 lines
21 KiB
Lua
Raw Normal View History

local Device = require("device")
local Screen = Device.screen
local Input = require("device").input
2013-10-18 20:38:07 +00:00
local Event = require("ui/event")
local util = require("ffi/util")
2013-10-22 18:51:29 +00:00
local DEBUG = require("dbg")
2013-10-24 13:45:02 +00:00
local _ = require("gettext")
2012-04-22 19:29:48 +00:00
-- cf. koreader-base/ffi-cdecl/include/mxcfb-kindle.h (UPDATE_MODE_* applies to Kobo, too)
local UPDATE_MODE_PARTIAL = 0x0
local UPDATE_MODE_FULL = 0x1
-- NOTE: Those have been confirmed on Kindle devices. Might be completely different on Kobo (except for AUTO)!
local WAVEFORM_MODE_INIT = 0x0 -- Screen goes to white (clears)
local WAVEFORM_MODE_DU = 0x1 -- Grey->white/grey->black
local WAVEFORM_MODE_GC16 = 0x2 -- High fidelity (flashing)
local WAVEFORM_MODE_GC4 = WAVEFORM_MODE_GC16 -- For compatibility
local WAVEFORM_MODE_GC16_FAST = 0x3 -- Medium fidelity
local WAVEFORM_MODE_A2 = 0x4 -- Faster but even lower fidelity
local WAVEFORM_MODE_GL16 = 0x5 -- High fidelity from white transition
local WAVEFORM_MODE_GL16_FAST = 0x6 -- Medium fidelity from white transition
2014-07-06 16:43:50 +00:00
-- Kindle FW >= 5.3
local WAVEFORM_MODE_DU4 = 0x7 -- Medium fidelity 4 level of gray direct update
2014-07-06 16:43:50 +00:00
-- Kindle PW2
local WAVEFORM_MODE_REAGL = 0x8 -- Ghost compensation waveform
local WAVEFORM_MODE_REAGLD = 0x9 -- Ghost compensation waveform with dithering
2014-07-06 16:43:50 +00:00
local WAVEFORM_MODE_AUTO = 0x101
-- Kobo's headers suck, so invent something to avoid magic numbers...
local WAVEFORM_MODE_KOBO_REGAL = 0x7
2012-04-22 19:29:48 +00:00
-- there is only one instance of this
2013-10-18 20:38:07 +00:00
local UIManager = {
default_refresh_type = UPDATE_MODE_PARTIAL,
2014-03-13 13:52:43 +00:00
default_waveform_mode = WAVEFORM_MODE_GC16, -- high fidelity waveform
fast_waveform_mode = WAVEFORM_MODE_A2,
full_refresh_waveform_mode = WAVEFORM_MODE_GC16,
partial_refresh_waveform_mode = WAVEFORM_MODE_GC16,
2014-03-13 13:52:43 +00:00
-- 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,
2014-07-06 19:38:13 +00:00
-- force to do partial refresh, will be reset to false
2014-03-13 13:52:43 +00:00
-- after each ui loop
2014-07-06 19:38:13 +00:00
partial_refresh = false,
2014-03-13 13:52:43 +00:00
-- trigger a full refresh when counter reaches FULL_REFRESH_COUNT
FULL_REFRESH_COUNT = G_reader_settings:readSetting("full_refresh_count") or DRCOUNTMAX,
2014-03-13 13:52:43 +00:00
refresh_count = 0,
2013-03-12 17:18:53 +00:00
event_handlers = nil,
2014-03-13 13:52:43 +00:00
_running = true,
_window_stack = {},
_execution_stack = {},
_dirty = {},
_zeromqs = {},
2012-04-22 19:29:48 +00:00
}
function UIManager:init()
self.event_handlers = {
__default__ = function(input_event)
self:sendEvent(input_event)
end,
SaveState = function()
self:sendEvent(Event:new("FlushSettings"))
end,
Power = function(input_event)
Device:onPowerEvent(input_event)
end,
}
if Device:isKobo() then
self.event_handlers["Suspend"] = function(input_event)
self:sendEvent(Event:new("FlushSettings"))
Device:onPowerEvent(input_event)
end
self.event_handlers["Resume"] = function(input_event)
Device:onPowerEvent(input_event)
self:sendEvent(Event:new("Resume"))
end
self.event_handlers["Light"] = function()
Device:getPowerDevice():toggleFrontlight()
end
self.event_handlers["__default__"] = function(input_event)
if Device.screen_saver_mode then
-- Suspension in Kobo can be interrupted by screen updates. We
-- ignore user touch input here so screen udpate won't be
-- triggered in suspend mode
return
else
self:sendEvent(input_event)
end
end
if KOBO_LIGHT_ON_START and tonumber(KOBO_LIGHT_ON_START) > -1 then
Device:getPowerDevice():setIntensity( math.max( math.min(KOBO_LIGHT_ON_START,100) ,0) )
end
-- Emulate the stock reader refresh behavior...
self.full_refresh_waveform_mode = WAVEFORM_MODE_GC16
-- Request regal waveform on devices that support it (Aura & H2O)
if Device.model == "Kobo_phoenix" or Device.model == "Kobo_dahlia" then
self.partial_refresh_waveform_mode = WAVEFORM_MODE_KOBO_REGAL
else
-- See the note in the Kindle code path later, the stock reader might be using WAVEFORM_MODE_AUTO
self.partial_refresh_waveform_mode = WAVEFORM_MODE_GC16
end
elseif Device:isKindle() then
self.event_handlers["IntoSS"] = function()
self:sendEvent(Event:new("FlushSettings"))
Device:intoScreenSaver()
end
self.event_handlers["OutOfSS"] = function()
Device:outofScreenSaver()
self:sendEvent(Event:new("Resume"))
end
self.event_handlers["Charging"] = function()
Device:usbPlugIn()
end
self.event_handlers["NotCharging"] = function()
Device:usbPlugOut()
self:sendEvent(Event:new("NotCharging"))
end
-- Emulate the stock reader refresh behavior...
--[[
NOTE: For ref, on a Touch (debugPaint & a patched strace are your friend!):
UI: flash: gc16_fast, non-flash: auto (prefers gc16_fast)
Reader: When flash: if to/from img: gc16, else gc16_fast; when non-flash: auto (seems to prefer gl16_fast); Waiting for marker only on flash
On a PW2:
UI: flash: fc16_fast, non-flash: auto (prefers gc16_fast)
Reader: When flash: if to/from img: gc16 (seems to have a larger bias towards this one), else gc16_fast; when non-flash: reagl (w/ UPDATE_MODE_FULL!); Always waits for marker
Note that the bottom status bar is refreshed separately, right after the screen, as a partial, auto (gc16_fast) update, and it is its marker that's waited after...
Non flash lasts longer (dual timeout: 14pgs / 7mins).
--]]
-- We don't really have an easy way to know if we're refreshing the UI, or a page, or if said page contains an image, so go with the highest fidelity option
self.full_refresh_waveform_mode = WAVEFORM_MODE_GC16
-- We spend much more time in the reader than the UI, and our UI isn't very graphic anyway, so go with the reader behavior
if Device.model == "KindlePaperWhite2" then
self.partial_refresh_waveform_mode = WAVEFORM_MODE_REAGL
else
self.partial_refresh_waveform_mode = WAVEFORM_MODE_GL16_FAST
-- NOTE: Or we could go back to what KOReader did before fa55acc in koreader-base, which was also use WAVEFORM_MODE_AUTO ;). I have *no* idea how the driver makes its choice though...
--self.partial_refresh_waveform_mode = WAVEFORM_MODE_AUTO
end
end
end
2012-04-22 19:29:48 +00:00
-- register & show a widget
-- modal widget should be always on the top
2012-04-22 19:29:48 +00:00
function UIManager:show(widget, x, y)
DEBUG("show widget", widget.id)
self._running = true
local window = {x = x or 0, y = y or 0, widget = widget}
-- put this window on top of the toppest non-modal window
for i = #self._window_stack, 0, -1 do
local top_window = self._window_stack[i]
-- skip modal window
if not top_window or not top_window.widget.modal then
table.insert(self._window_stack, i + 1, window)
break
end
end
2014-03-13 13:52:43 +00:00
-- and schedule it to be painted
self:setDirty(widget)
-- tell the widget that it is shown now
widget:handleEvent(Event:new("Show"))
-- check if this widget disables double tap gesture
if widget.disable_double_tap then
Input.disable_double_tap = true
end
2012-04-22 19:29:48 +00:00
end
-- unregister a widget
function UIManager:close(widget)
if not widget then
DEBUG("widget not exist to be closed")
return
end
DEBUG("close widget", widget.id)
2014-03-13 13:52:43 +00:00
Input.disable_double_tap = DGESDETECT_DISABLE_DOUBLE_TAP
local dirty = false
for i = #self._window_stack, 1, -1 do
if self._window_stack[i].widget == widget then
table.remove(self._window_stack, i)
dirty = true
elseif self._window_stack[i].widget.disable_double_tap then
Input.disable_double_tap = true
end
end
if dirty then
-- schedule remaining widgets to be painted
for i = 1, #self._window_stack do
self:setDirty(self._window_stack[i].widget)
end
end
2012-04-22 19:29:48 +00:00
end
-- schedule an execution task
function UIManager:schedule(time, action)
2014-03-13 13:52:43 +00:00
table.insert(self._execution_stack, { time = time, action = action })
2012-04-22 19:29:48 +00:00
end
-- schedule task in a certain amount of seconds (fractions allowed) from now
function UIManager:scheduleIn(seconds, action)
2014-03-13 13:52:43 +00:00
local when = { util.gettime() }
local s = math.floor(seconds)
local usecs = (seconds - s) * 1000000
when[1] = when[1] + s
when[2] = when[2] + usecs
if when[2] > 1000000 then
when[1] = when[1] + 1
when[2] = when[2] - 1000000
end
self:schedule(when, action)
2012-04-22 19:29:48 +00:00
end
function UIManager:unschedule(action)
for i = #self._execution_stack, 1, -1 do
local task = self._execution_stack[i]
if task.action == action then
-- remove from table
table.remove(self._execution_stack, i)
end
end
end
2012-04-22 19:29:48 +00:00
-- register a widget to be repainted
function UIManager:setDirty(widget, refresh_type)
2014-03-13 13:52:43 +00:00
-- "auto": request full refresh
-- "full": force full refresh
-- "partial": partial refresh
if not refresh_type then
refresh_type = "auto"
end
if widget then
self._dirty[widget] = refresh_type
end
2012-04-22 19:29:48 +00:00
end
function UIManager:insertZMQ(zeromq)
table.insert(self._zeromqs, zeromq)
return zeromq
end
function UIManager:removeZMQ(zeromq)
for i = #self._zeromqs, 1, -1 do
if self._zeromqs[i] == zeromq then
table.remove(self._zeromqs, i)
end
end
end
-- set full refresh rate for e-ink screen
-- and make the refresh rate persistant in global reader settings
function UIManager:setRefreshRate(rate)
DEBUG("set screen full refresh rate", rate)
self.FULL_REFRESH_COUNT = rate
G_reader_settings:saveSetting("full_refresh_count", rate)
end
-- get full refresh rate for e-ink screen
function UIManager:getRefreshRate(rate)
return self.FULL_REFRESH_COUNT
end
2012-04-22 19:29:48 +00:00
-- signal to quit
function UIManager:quit()
DEBUG("quit uimanager")
2014-03-13 13:52:43 +00:00
self._running = false
for i = #self._window_stack, 1, -1 do
table.remove(self._window_stack, i)
end
for i = #self._execution_stack, 1, -1 do
table.remove(self._execution_stack, i)
end
for i = #self._zeromqs, 1, -1 do
self._zeromqs[i]:stop()
table.remove(self._zeromqs, i)
end
2012-04-22 19:29:48 +00:00
end
-- transmit an event to registered widgets
function UIManager:sendEvent(event)
if #self._window_stack == 0 then return end
2014-03-13 13:52:43 +00:00
-- top level widget has first access to the event
if self._window_stack[#self._window_stack].widget:handleEvent(event) then
return
end
2012-04-22 19:29:48 +00:00
2014-03-13 13:52:43 +00:00
-- if the event is not consumed, active widgets can access it
for _, widget in ipairs(self._window_stack) do
if widget.widget.is_always_active then
if widget.widget:handleEvent(event) then return end
end
if widget.widget.active_widgets then
for _, active_widget in ipairs(widget.widget.active_widgets) do
if active_widget:handleEvent(event) then return end
end
end
end
2012-04-22 19:29:48 +00:00
end
function UIManager:checkTasks()
2014-03-13 13:52:43 +00:00
local now = { util.gettime() }
2014-03-13 13:52:43 +00:00
-- check if we have timed events in our queue and search next one
local wait_until = nil
local all_tasks_checked
repeat
all_tasks_checked = true
for i = #self._execution_stack, 1, -1 do
local task = self._execution_stack[i]
if not task.time
or task.time[1] < now[1]
or task.time[1] == now[1] and task.time[2] < now[2] then
-- task is pending to be executed right now. do it.
task.action()
-- and remove from table
table.remove(self._execution_stack, i)
-- start loop again, since new tasks might be on the
-- queue now
all_tasks_checked = false
elseif not wait_until
or wait_until[1] > task.time[1]
or wait_until[1] == task.time[1] and wait_until[2] > task.time[2] then
-- task is to be run in the future _and_ is scheduled
-- earlier than the tasks we looked at already
-- so adjust to the currently examined task instead.
wait_until = task.time
end
end
until all_tasks_checked
return wait_until
end
2012-04-22 19:29:48 +00:00
-- this is the main loop of the UI controller
-- it is intended to manage input events and delegate
-- them to dialogs
function UIManager:run()
2014-03-13 13:52:43 +00:00
self._running = true
while self._running do
local now = { util.gettime() }
local wait_until = self:checkTasks()
2013-03-12 17:18:53 +00:00
2014-03-13 13:52:43 +00:00
--DEBUG("---------------------------------------------------")
--DEBUG("exec stack", self._execution_stack)
--DEBUG("window stack", self._window_stack)
--DEBUG("dirty stack", self._dirty)
--DEBUG("---------------------------------------------------")
2012-04-22 19:29:48 +00:00
2014-03-13 13:52:43 +00:00
-- stop when we have no window to show
if #self._window_stack == 0 then
DEBUG("no dialog left to show")
self:quit()
2014-03-13 13:52:43 +00:00
return nil
end
2012-04-22 19:29:48 +00:00
2014-03-13 13:52:43 +00:00
-- repaint dirty widgets
local dirty = false
local request_full_refresh = false
local force_full_refresh = false
2014-07-06 19:38:13 +00:00
local force_partial_refresh = false
2014-03-13 13:52:43 +00:00
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)
2014-03-13 13:52:43 +00:00
if self._dirty[widget.widget] == "auto" then
request_full_refresh = true
end
if self._dirty[widget.widget] == "full" then
force_full_refresh = true
end
if self._dirty[widget.widget] == "partial" then
2014-07-06 19:38:13 +00:00
force_partial_refresh = true
2014-03-13 13:52:43 +00:00
end
if self._dirty[widget.widget] == "fast" then
force_fast_refresh = true
end
-- and remove from list after painting
self._dirty[widget.widget] = nil
-- trigger repaint
dirty = true
end
end
2014-06-04 09:22:45 +00:00
2014-03-13 13:52:43 +00:00
if self.full_refresh then
dirty = true
force_full_refresh = true
end
2014-07-06 19:38:13 +00:00
if self.partial_refresh then
2014-03-13 13:52:43 +00:00
dirty = true
2014-07-06 19:38:13 +00:00
force_partial_refresh = true
2014-03-13 13:52:43 +00:00
end
2014-06-04 09:22:45 +00:00
2014-03-13 13:52:43 +00:00
self.repaint_all = false
self.full_refresh = false
2014-07-06 19:38:13 +00:00
self.partial_refresh = false
2014-06-04 09:22:45 +00:00
2014-03-13 13:52:43 +00:00
local refresh_type = self.default_refresh_type
local waveform_mode = self.default_waveform_mode
if dirty then
2014-07-06 19:38:13 +00:00
if force_partial_refresh or force_fast_refresh then
refresh_type = UPDATE_MODE_PARTIAL
2014-03-13 13:52:43 +00:00
elseif force_full_refresh or self.refresh_count == self.FULL_REFRESH_COUNT - 1 then
refresh_type = UPDATE_MODE_FULL
2014-03-13 13:52:43 +00:00
end
-- Handle the waveform mode selection...
if refresh_type == UPDATE_MODE_FULL then
waveform_mode = self.full_refresh_waveform_mode
else
waveform_mode = self.partial_refresh_waveform_mode
end
2014-03-13 13:52:43 +00:00
if force_fast_refresh then
waveform_mode = self.fast_waveform_mode
end
-- And the PW2 REAGL trickery (they're always full refreshes, but there's no black flash)
if refresh_type == UPDATE_MODE_PARTIAL and (waveform_mode == WAVEFORM_MODE_REAGL or waveform_mode == WAVEFORM_MODE_KOBO_REGAL) then
refresh_type = UPDATE_MODE_FULL
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
Screen:refresh(refresh_type, waveform_mode,
update_region.x-1, update_region.y-1,
update_region.w+2, update_region.h+2)
end
2014-03-13 13:52:43 +00:00
else
Screen:refresh(refresh_type, waveform_mode)
end
-- PW2 REAGL refreshes are always full (but without black flash), but we want to keep our black flash timeout working, so don't reset the count on full REAGL refreshes...
if refresh_type == UPDATE_MODE_FULL and waveform_mode ~= WAVEFORM_MODE_REAGL and waveform_mode ~= WAVEFORM_MODE_KOBO_REGAL then
2014-03-13 13:52:43 +00:00
self.refresh_count = 0
2014-07-06 19:38:13 +00:00
elseif not force_partial_refresh and not force_full_refresh then
2014-03-13 13:52:43 +00:00
self.refresh_count = (self.refresh_count + 1)%self.FULL_REFRESH_COUNT
end
self.update_regions_func = nil
2014-03-13 13:52:43 +00:00
end
2013-03-12 17:18:53 +00:00
2014-03-13 13:52:43 +00:00
self:checkTasks()
2013-03-12 17:18:53 +00:00
2014-03-13 13:52:43 +00:00
-- wait for next event
-- note that we will skip that if in the meantime we have tasks that are ready to run
local input_event = nil
if not wait_until then
if #self._zeromqs > 0 then
-- pending message queue, wait 100ms for input
input_event = Input:waitEvent(1000*100)
if not input_event or input_event.handler == "onInputError" then
for _, zeromq in ipairs(self._zeromqs) do
input_event = zeromq:waitEvent()
if input_event then break end
end
end
else
-- no pending task, wait endlessly
input_event = Input:waitEvent()
end
2014-03-13 13:52:43 +00:00
elseif wait_until[1] > now[1]
or wait_until[1] == now[1] and wait_until[2] > now[2] then
local wait_for = { s = wait_until[1] - now[1], us = wait_until[2] - now[2] }
if wait_for.us < 0 then
wait_for.s = wait_for.s - 1
wait_for.us = 1000000 + wait_for.us
end
-- wait until next task is pending
input_event = Input:waitEvent(wait_for.us, wait_for.s)
end
2012-04-22 19:29:48 +00:00
2014-03-13 13:52:43 +00:00
-- delegate input_event to handler
if input_event then
local handler = self.event_handlers[input_event]
if handler then
handler(input_event)
2014-03-13 13:52:43 +00:00
else
self.event_handlers["__default__"](input_event)
2014-03-13 13:52:43 +00:00
end
end
end
2012-04-22 19:29:48 +00:00
end
2013-10-18 20:38:07 +00:00
function UIManager:getRefreshMenuTable()
local function custom_1() return G_reader_settings:readSetting("refresh_rate_1") or 12 end
local function custom_2() return G_reader_settings:readSetting("refresh_rate_2") or 22 end
local function custom_3() return G_reader_settings:readSetting("refresh_rate_3") or 99 end
local function custom_input(name)
return {
title = _("Input page number for a full refresh"),
type = "number",
hint = "(1 - 99)",
callback = function(input)
local rate = tonumber(input)
G_reader_settings:saveSetting(name, rate)
UIManager:setRefreshRate(rate)
end,
}
end
return {
text = _("E-ink full refresh rate"),
sub_item_table = {
{
text = _("Every page"),
checked_func = function() return UIManager:getRefreshRate() == 1 end,
callback = function() UIManager:setRefreshRate(1) end,
},
{
text = _("Every 6 pages"),
checked_func = function() return UIManager:getRefreshRate() == 6 end,
callback = function() UIManager:setRefreshRate(6) end,
},
{
text_func = function() return _("Custom ") .. "1: " .. custom_1() .. _(" pages") end,
checked_func = function() return UIManager:getRefreshRate() == custom_1() end,
callback = function() UIManager:setRefreshRate(custom_1()) end,
hold_input = custom_input("refresh_rate_1")
},
{
text_func = function() return _("Custom ") .. "2: " .. custom_2() .. _(" pages") end,
checked_func = function() return UIManager:getRefreshRate() == custom_2() end,
callback = function() UIManager:setRefreshRate(custom_2()) end,
hold_input = custom_input("refresh_rate_2")
},
{
text_func = function() return _("Custom ") .. "3: " .. custom_3() .. _(" pages") end,
checked_func = function() return UIManager:getRefreshRate() == custom_3() end,
callback = function() UIManager:setRefreshRate(custom_3()) end,
hold_input = custom_input("refresh_rate_3")
},
}
}
end
UIManager:init()
2013-10-18 20:38:07 +00:00
return UIManager