mirror of https://github.com/koreader/koreader
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
279 lines
10 KiB
Lua
279 lines
10 KiB
Lua
7 years ago
|
--[[--
|
||
|
Trapper module: provides methods for simple interaction with UI,
|
||
|
without the need for explicit callbacks, for use by linear jobs
|
||
|
between their steps.
|
||
|
|
||
|
Allows code to trap UI (give progress info to UI, ask for user choice),
|
||
|
or get trapped by UI (get interrupted).
|
||
|
Mostly done with coroutines, but hides their usage for simplicity.
|
||
|
]]
|
||
|
|
||
|
|
||
|
local ConfirmBox = require("ui/widget/confirmbox")
|
||
|
local InfoMessage = require("ui/widget/infomessage")
|
||
|
local UIManager = require("ui/uimanager")
|
||
|
local logger = require("logger")
|
||
|
local _ = require("gettext")
|
||
|
|
||
|
local Trapper = {}
|
||
|
|
||
|
--[[--
|
||
|
Executes a function and allows it to be trapped (that is: to use our
|
||
|
other methods).
|
||
|
|
||
|
Simple wrapper function for a coroutine, which is a prerequisite
|
||
|
for all our methods (this simply abstracts the @{coroutine}
|
||
|
business to our callers), and execute it.
|
||
|
|
||
|
(If some code is not wrap()'ed, most of the other methods, when called,
|
||
|
will simply log or fallback to a non-UI action or OK choice.)
|
||
|
|
||
|
This call should be the last step in some event processing code,
|
||
|
as it may return early (the first @{coroutine.yield|coroutine.yield()} in any of the other
|
||
|
methods will return from this function), and later be resumed by @{ui.uimanager|UIManager}.
|
||
|
So any following (unwrapped) code would be then executed while `func`
|
||
|
is half-done, with unintended consequences.
|
||
|
|
||
|
@param func function reference to function to wrap and execute
|
||
|
]]
|
||
|
function Trapper:wrap(func)
|
||
|
-- Catch and log any error happening in func (an error happening
|
||
|
-- in a coroutine just aborts silently the coroutine)
|
||
|
local pcalled_func = function()
|
||
|
-- we use xpcall as it can give a whole stacktrace, unlike pcall
|
||
|
local ok, err = xpcall(func, debug.traceback)
|
||
|
if not ok then
|
||
|
logger.warn("error in wrapped function:", err)
|
||
|
return false
|
||
|
end
|
||
|
return true
|
||
|
-- As a coroutine, we will return at first coroutine.yield(),
|
||
|
-- and the above true/false won't probably be caught by
|
||
|
-- any code, but let's do it anyway.
|
||
|
end
|
||
|
local co = coroutine.create(pcalled_func)
|
||
|
return coroutine.resume(co)
|
||
|
end
|
||
|
|
||
|
--- Returns if code is wrapped
|
||
|
--
|
||
|
-- @treturn boolean true if code is wrapped by Trapper, false otherwise
|
||
|
function Trapper:isWrapped()
|
||
|
if coroutine.running() then
|
||
|
return true
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
--- Clears left-over widget
|
||
|
function Trapper:clear()
|
||
|
if self:isWrapped() then
|
||
|
if self.current_widget then
|
||
|
UIManager:close(self.current_widget)
|
||
|
UIManager:forceRePaint()
|
||
|
self.current_widget = nil
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
--- Clears left-over widget and resets Trapper state
|
||
|
function Trapper:reset()
|
||
|
self:clear()
|
||
|
-- Reset some properties
|
||
|
self.paused_text = nil
|
||
|
self.paused_continue_text = nil
|
||
|
self.paused_abort_text = nil
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
--[[--
|
||
|
Displays an InfoMessage, and catches dismissal.
|
||
|
|
||
|
Display a InfoMessage with text, or keep existing InfoMessage if text = nil,
|
||
|
and return true.
|
||
|
|
||
|
UNLESS the previous widget was itself a InfoMessage and it has been
|
||
|
dismissed (by Tap on the screen), in which case the new InfoMessage
|
||
|
is not displayed, and false is returned.
|
||
|
|
||
|
One can only know a InfoMessage has been dismissed when trying to
|
||
|
display a new one (we can't do better than that with coroutines).
|
||
|
So, don't hesitate to call it regularly (each call costs 100ms), between
|
||
|
steps of the work, to provide good responsiveness.
|
||
|
|
||
|
Trapper:info() is a shortcut to get dismiss info while keeping
|
||
|
the existing InfoMessage displayed.
|
||
|
|
||
|
@string text text to display as an InfoMessage (or nil to keep existing one)
|
||
|
@treturn boolean true if InfoMessage was not dismissed, false if dismissed
|
||
|
|
||
|
@usage
|
||
|
Trapper:info("some text about step or progress")
|
||
|
go_on = Trapper:info()
|
||
|
]]
|
||
|
function Trapper:info(text)
|
||
|
local _coroutine = coroutine.running()
|
||
|
if not _coroutine then
|
||
|
logger.info("unwrapped info:", text)
|
||
|
return true -- not dismissed
|
||
|
end
|
||
|
|
||
|
if self.current_widget and self.current_widget.is_infomessage then
|
||
|
-- We are replacing a InfoMessage with a new InfoMessage: we want to check
|
||
|
-- if the previous one was dismissed.
|
||
|
-- We added a dismiss_callback to our previous InfoMessage. For a Tap
|
||
|
-- to get processed and get our dismiss_callback called, we need to give
|
||
|
-- control for a short time to UIManager: this will be done with
|
||
|
-- the coroutine.yield() that follows.
|
||
|
-- If no dismiss_callback was fired, we need to get this code resumed:
|
||
|
-- that will be done with the following go_on_func schedule in 0.1 second.
|
||
|
local go_on_func = function() coroutine.resume(_coroutine, true) end
|
||
|
-- delay matters: 0.05 or 0.1 seems fine
|
||
|
-- 0.01 is too fast: go_on_func is called before our dismiss_callback is processed
|
||
|
UIManager:scheduleIn(0.1, go_on_func)
|
||
|
|
||
|
local go_on = coroutine.yield() -- gives control back to UIManager
|
||
|
-- go_on is the 2nd arg given to the coroutine.resume() that got us resumed:
|
||
|
-- false if it was a dismiss_callback
|
||
|
-- true if it was the schedule go_on_func
|
||
|
|
||
|
if not go_on then -- dismiss_callback called
|
||
|
UIManager:unschedule(go_on_func) -- no more need for this scheduled action
|
||
|
-- Don't just return false without confirmation (this tap may have been
|
||
|
-- made by error, and we don't want to just cancel a long running job)
|
||
|
local abort_box = ConfirmBox:new{
|
||
|
text = self.paused_text and self.paused_text or _("Paused"),
|
||
|
-- ok and cancel reversed, as tapping outside will
|
||
|
-- get cancel_callback called: if tap outside was the
|
||
|
-- result of a tap error, we want to continue. Cancelling
|
||
|
-- will need an explicit tap on the ok_text button.
|
||
|
cancel_text = self.paused_continue_text and self.paused_continue_text or _("Continue"),
|
||
|
ok_text = self.paused_abort_text and self.paused_abort_text or _("Abort"),
|
||
|
cancel_callback = function()
|
||
|
coroutine.resume(_coroutine, true)
|
||
|
end,
|
||
|
ok_callback = function()
|
||
|
coroutine.resume(_coroutine, false)
|
||
|
end,
|
||
|
}
|
||
|
UIManager:show(abort_box)
|
||
|
-- no need to forceRePaint, UIManager will do it when we yield()
|
||
|
go_on = coroutine.yield() -- abort_box ok/cancel from their coroutine.resume()
|
||
|
UIManager:close(abort_box)
|
||
|
if not go_on then
|
||
|
UIManager:close(self.current_widget)
|
||
|
UIManager:forceRePaint()
|
||
|
return false
|
||
|
end
|
||
|
if self.current_widget then
|
||
|
-- Re-show current widget that was dismissed
|
||
|
-- (this is fine for our simple InfoMessage)
|
||
|
UIManager:show(self.current_widget)
|
||
|
end
|
||
|
UIManager:forceRePaint()
|
||
|
end
|
||
|
-- go_on_func returned result = true, or abort_box did not abort:
|
||
|
-- continue processing
|
||
|
end
|
||
|
|
||
|
-- TODO We should try to flush any pending tap, so past
|
||
|
-- events won't be considered action on the yet to be displayed
|
||
|
-- widget
|
||
|
|
||
|
-- We're going to display a new widget, close previous one
|
||
|
if self.current_widget then
|
||
|
UIManager:close(self.current_widget)
|
||
|
-- no repaint here, we'll do that below when a new one is shown
|
||
|
end
|
||
|
|
||
|
-- dismiss_callback will be checked for at start of next call
|
||
|
self.current_widget = InfoMessage:new{
|
||
|
text = text,
|
||
|
dismiss_callback = function()
|
||
|
coroutine.resume(_coroutine, false)
|
||
|
end,
|
||
|
is_infomessage = true -- flag on our InfoMessages
|
||
|
}
|
||
|
logger.dbg("Showing InfoMessage:", text)
|
||
|
UIManager:show(self.current_widget)
|
||
|
UIManager:forceRePaint()
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
--[[--
|
||
|
Overrides text and button texts on the Paused ConfirmBox.
|
||
|
|
||
|
A ConfirmBox is displayed when an InfoMessage is dismissed
|
||
|
in Trapper:info(), with default text "Paused", and default
|
||
|
buttons "Abort" and "Continue".
|
||
|
|
||
|
@string text ConfirmBox text (default: "Paused")
|
||
|
@string abort_text ConfirmBox "Abort" button text (Trapper:info() returns false)
|
||
|
@string continue_text ConfirmBox "Continue" button text
|
||
|
]]
|
||
|
function Trapper:setPausedText(text, abort_text, continue_text)
|
||
|
if self:isWrapped() then
|
||
|
self.paused_text = text
|
||
|
self.paused_abort_text = abort_text
|
||
|
self.paused_continue_text = continue_text
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
--[[--
|
||
|
Displays a ConfirmBox and gets user's choice.
|
||
|
|
||
|
Display a ConfirmBox with the text and cancel_text/ok_text buttons,
|
||
|
block and wait for user's choice, and return the choice made:
|
||
|
false if Cancel tapped or dismissed, true if OK tapped
|
||
|
|
||
|
@string text text to display in a ConfirmBox
|
||
|
@string cancel_text text for ConfirmBox Cancel button
|
||
|
@string ok_text text for ConfirmBox Ok button
|
||
|
@treturn boolean false if Cancel tapped or dismissed, true if OK tapped
|
||
|
|
||
|
@usage
|
||
|
go_on = Trapper:confirm("Do you want to go on?")
|
||
|
that_selected = Trapper:confirm("Do you want to do this or that?", "this", "that"))
|
||
|
]]
|
||
|
function Trapper:confirm(text, cancel_text, ok_text)
|
||
|
-- With ConfirmBox, Cancel button is on the left, OK button on the right,
|
||
|
-- so buttons order is consistent with this function args
|
||
|
local _coroutine = coroutine.running()
|
||
|
if not _coroutine then
|
||
|
logger.info("unwrapped confirm, returning true to:", text)
|
||
|
return true -- always select "OK" in ConfirmBox if no UI
|
||
|
end
|
||
|
|
||
|
-- TODO We should try to flush any pending tap, so past
|
||
|
-- events won't be considered action on the yet to be displayed
|
||
|
-- widget
|
||
|
|
||
|
-- Close any previous widget
|
||
|
if self.current_widget then
|
||
|
UIManager:close(self.current_widget)
|
||
|
-- no repaint here, we'll do that below when a new one is shown
|
||
|
end
|
||
|
|
||
|
-- We will yield(), and both callbacks will resume() us
|
||
|
self.current_widget = ConfirmBox:new{
|
||
|
text = text,
|
||
|
ok_text = ok_text,
|
||
|
cancel_text = cancel_text,
|
||
|
cancel_callback = function()
|
||
|
coroutine.resume(_coroutine, false)
|
||
|
end,
|
||
|
ok_callback = function()
|
||
|
coroutine.resume(_coroutine, true)
|
||
|
end,
|
||
|
}
|
||
|
logger.dbg("Showing ConfirmBox and waiting for answer:", text)
|
||
|
UIManager:show(self.current_widget)
|
||
|
-- no need to forceRePaint, UIManager will do it when we yield()
|
||
|
local ret = coroutine.yield() -- wait for ConfirmBox callback
|
||
|
logger.dbg("ConfirmBox answers", ret)
|
||
|
return ret
|
||
|
end
|
||
|
|
||
|
return Trapper
|