koreader/frontend/apps/reader/readerui.lua
Qingping Hou 0c49b915de refactor: add touch zone subsystem to inputcontainer
Touch zone decouples screen size from gesture event registration.

The win here is each individual widget does not need to update
gesture range on screen rotate/resize anymore.

Another advantage is we now have a centralized ordered array to handle
all registered touch event listeners, makes it much easier to resolve
gesture range conflicts between multiple widgets.

This patch also includes the following changes:

* migrate readerpaging to use readerui's touch zone
* migrate readerfooter to use readerui's touch zone
* move inverse read direction setting to touch menu's setting tab
* moved kobolight widget from readerview into readerui
* various dead code cleanups and comments
2016-12-10 16:06:51 -08:00

521 lines
17 KiB
Lua

local InputContainer = require("ui/widget/container/inputcontainer")
local DocumentRegistry = require("document/documentregistry")
local Screenshoter = require("ui/widget/screenshoter")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local ConfirmBox = require("ui/widget/confirmbox")
local lfs = require("libs/libkoreader-lfs")
local DocSettings = require("docsettings")
local UIManager = require("ui/uimanager")
local Geom = require("ui/geometry")
local Device = require("device")
local Screen = require("device").screen
local Event = require("ui/event")
local Cache = require("cache")
local dbg = require("dbg")
local T = require("ffi/util").template
local _ = require("gettext")
local ReaderView = require("apps/reader/modules/readerview")
local ReaderZooming = require("apps/reader/modules/readerzooming")
local ReaderPanning = require("apps/reader/modules/readerpanning")
local ReaderRotation = require("apps/reader/modules/readerrotation")
local ReaderPaging = require("apps/reader/modules/readerpaging")
local ReaderRolling = require("apps/reader/modules/readerrolling")
local ReaderToc = require("apps/reader/modules/readertoc")
local ReaderBookmark = require("apps/reader/modules/readerbookmark")
local ReaderFont = require("apps/reader/modules/readerfont")
local ReaderTypeset = require("apps/reader/modules/readertypeset")
local ReaderMenu = require("apps/reader/modules/readermenu")
local ReaderGoto = require("apps/reader/modules/readergoto")
local ReaderConfig = require("apps/reader/modules/readerconfig")
local ReaderCropping = require("apps/reader/modules/readercropping")
local ReaderKoptListener = require("apps/reader/modules/readerkoptlistener")
local ReaderCoptListener = require("apps/reader/modules/readercoptlistener")
local ReaderHinting = require("apps/reader/modules/readerhinting")
local ReaderHighlight = require("apps/reader/modules/readerhighlight")
local ReaderFrontLight = require("apps/reader/modules/readerfrontlight")
local ReaderDictionary = require("apps/reader/modules/readerdictionary")
local ReaderWikipedia = require("apps/reader/modules/readerwikipedia")
local ReaderHyphenation = require("apps/reader/modules/readerhyphenation")
local ReaderActivityIndicator = require("apps/reader/modules/readeractivityindicator")
local FileManagerHistory = require("apps/filemanager/filemanagerhistory")
local ReaderSearch = require("apps/reader/modules/readersearch")
local ReaderLink = require("apps/reader/modules/readerlink")
local ReaderStatus = require("apps/reader/modules/readerstatus")
local PluginLoader = require("apps/reader/pluginloader")
--[[
This is an abstraction for a reader interface
it works using data gathered from a document interface
]]--
local ReaderUI = InputContainer:new{
name = "ReaderUI",
key_events = {
Close = { { "Home" },
doc = "close document", event = "Close" },
},
active_widgets = {},
-- our own size
dimen = Geom:new{ w = 400, h = 600 },
-- if we have a parent container, it must be referenced for now
dialog = nil,
-- the document interface
document = nil,
-- password for document unlock
password = nil,
postInitCallback = nil,
}
function ReaderUI:registerModule(name, ui_module, always_active)
if name then self[name] = ui_module end
ui_module.name = "reader" .. name
table.insert(always_active and self.active_widgets or self, ui_module)
end
function ReaderUI:registerPostInitCallback(callback)
table.insert(self.postInitCallback, callback)
end
function ReaderUI:init()
self.postInitCallback = {}
-- if we are not the top level dialog ourselves, it must be given in the table
if not self.dialog then
self.dialog = self
end
if Device:hasKeys() then
self.key_events.Back = {
{ "Back" }, doc = "close document",
event = "Close" }
end
self.doc_settings = DocSettings:open(self.document.file)
-- a view container (so it must be child #1!)
self:registerModule("view", ReaderView:new{
dialog = self.dialog,
dimen = self.dimen,
ui = self,
document = self.document,
})
-- goto link controller
self:registerModule("link", ReaderLink:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
-- text highlight
self:registerModule("highlight", ReaderHighlight:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
-- menu widget should be registered after link widget and highlight widget
-- so that taps on link and highlight areas won't popup reader menu
-- reader menu controller
self:registerModule("menu", ReaderMenu:new{
view = self.view,
ui = self
})
-- rotation controller
self:registerModule("rotation", ReaderRotation:new{
dialog = self.dialog,
view = self.view,
ui = self
})
-- Table of content controller
self:registerModule("toc", ReaderToc:new{
dialog = self.dialog,
view = self.view,
ui = self
})
-- bookmark controller
self:registerModule("bookmark", ReaderBookmark:new{
dialog = self.dialog,
view = self.view,
ui = self
})
-- reader goto controller
-- "goto" being a dirty keyword in Lua?
self:registerModule("gotopage", ReaderGoto:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
-- dictionary
self:registerModule("dictionary", ReaderDictionary:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
-- wikipedia
self:registerModule("wikipedia", ReaderWikipedia:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
-- screenshot controller
self:registerModule("screenshot", Screenshoter:new{
prefix = 'Reader',
dialog = self.dialog,
view = self.view,
ui = self
}, true)
-- frontlight controller
if Device:hasFrontlight() then
self:registerModule("frontlight", ReaderFrontLight:new{
dialog = self.dialog,
view = self.view,
ui = self
})
end
-- configuable controller
if self.document.info.configurable then
-- config panel controller
self:registerModule("config", ReaderConfig:new{
configurable = self.document.configurable,
options = self.document.options,
dialog = self.dialog,
view = self.view,
ui = self
})
if self.document.info.has_pages then
-- kopt option controller
self:registerModule("koptlistener", ReaderKoptListener:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
else
-- cre option controller
self:registerModule("crelistener", ReaderCoptListener:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
end
-- activity indicator when some configurations take long take to affect
self:registerModule("activityindicator", ReaderActivityIndicator:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
end
-- for page specific controller
if self.document.info.has_pages then
-- cropping controller
self:registerModule("cropping", ReaderCropping:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
-- paging controller
self:registerModule("paging", ReaderPaging:new{
dialog = self.dialog,
view = self.view,
ui = self
})
-- zooming controller
self:registerModule("zooming", ReaderZooming:new{
dialog = self.dialog,
view = self.view,
ui = self
})
-- panning controller
self:registerModule("panning", ReaderPanning:new{
dialog = self.dialog,
view = self.view,
ui = self
})
-- hinting controller
self:registerModule("hinting", ReaderHinting:new{
dialog = self.dialog,
zoom = self.zooming,
view = self.view,
ui = self,
document = self.document,
})
else
-- make sure we render document first before calling any callback
self:registerPostInitCallback(function()
self.document:loadDocument()
-- used to read additional settings after the document has been
-- loaded (but not rendered yet)
self:handleEvent(Event:new("PreRenderDocument", self.doc_settings))
self.document:render()
end)
-- typeset controller
self:registerModule("typeset", ReaderTypeset:new{
dialog = self.dialog,
view = self.view,
ui = self
})
-- font menu
self:registerModule("font", ReaderFont:new{
dialog = self.dialog,
view = self.view,
ui = self
})
-- hyphenation menu
self:registerModule("hyphenation", ReaderHyphenation:new{
dialog = self.dialog,
view = self.view,
ui = self
})
-- rolling controller
self:registerModule("rolling", ReaderRolling:new{
dialog = self.dialog,
view = self.view,
ui = self
})
self.disable_double_tap = G_reader_settings:readSetting("disable_double_tap") ~= false
end
-- fulltext search
self:registerModule("search", ReaderSearch:new{
dialog = self.dialog,
view = self.view,
ui = self
})
-- book status
self:registerModule("status", ReaderStatus:new{
ui = self,
document = self.document,
view = self.view,
})
-- history view
self:registerModule("history", FileManagerHistory:new{
dialog = self.dialog,
ui = self,
})
-- koreader plugins
for _,plugin_module in ipairs(PluginLoader:loadPlugins()) do
dbg("Loaded plugin", plugin_module.name, "at", plugin_module.path)
self:registerModule(plugin_module.name, plugin_module:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
end
local ReaderKoboLight = require("apps/reader/modules/readerkobolight")
if (Device:isKobo() and Device:hasFrontlight()) then
self:registerModule('kobolight', ReaderKoboLight:new{
dialog = self.dialog,
view = self.view,
ui = self,
})
end
-- we only read settings after all the widgets are initialized
self:handleEvent(Event:new("ReadSettings", self.doc_settings))
for _,v in ipairs(self.postInitCallback) do
v()
end
-- After initialisation notify that document is loaded and rendered
-- CREngine only reports correct page count after rendering is done
-- Need the same event for PDF document
self:handleEvent(Event:new("ReaderReady", self.doc_settings))
end
function ReaderUI:showReader(file)
dbg("show reader ui")
require("readhistory"):addItem(file)
if lfs.attributes(file, "mode") ~= "file" then
UIManager:show(InfoMessage:new{
text = T(_("File '%1' does not exist."), file)
})
return
end
UIManager:show(InfoMessage:new{
text = T(_("Opening file '%1'."), file),
timeout = 0.0,
})
-- doShowReader might block for a long time, so force repaint here
UIManager:forceRePaint()
UIManager:nextTick(function()
dbg("creating coroutine for showing reader")
local co = coroutine.create(function()
self:doShowReader(file)
end)
local ok, err = coroutine.resume(co)
if err ~= nil or ok == false then
io.stderr:write('[!] doShowReader coroutine crashed:\n')
io.stderr:write(debug.traceback(co, err, 1))
UIManager:quit()
end
end)
end
local _running_instance = nil
function ReaderUI:doShowReader(file)
dbg("opening file", file)
-- keep only one instance running
if _running_instance then
_running_instance:onClose()
end
local document = DocumentRegistry:openDocument(file)
if not document then
UIManager:show(InfoMessage:new{
text = _("No reader engine for this file.")
})
return
end
if document.is_locked then
dbg("document is locked")
self._coroutine = coroutine.running() or self._coroutine
self:unlockDocumentWithPassword(document)
if coroutine.running() then
local unlock_success = coroutine.yield()
if not unlock_success then
return
end
end
end
G_reader_settings:saveSetting("lastfile", file)
local reader = ReaderUI:new{
dimen = Screen:getSize(),
document = document,
}
UIManager:show(reader)
_running_instance = reader
end
function ReaderUI:_getRunningInstance()
return _running_instance
end
function ReaderUI:unlockDocumentWithPassword(document, try_again)
dbg("show input password dialog")
self.password_dialog = InputDialog:new{
title = try_again and _("Password is incorrect, try again?")
or _("Input document password"),
buttons = {
{
{
text = _("Cancel"),
enabled = true,
callback = function()
self:closeDialog()
coroutine.resume(self._coroutine)
end,
},
{
text = _("OK"),
enabled = true,
callback = function()
local success = self:onVerifyPassword(document)
self:closeDialog()
if success then
coroutine.resume(self._coroutine, success)
else
self:unlockDocumentWithPassword(document, true)
end
end,
},
},
},
text_type = "password",
}
self.password_dialog:onShowKeyboard()
UIManager:show(self.password_dialog)
end
function ReaderUI:onVerifyPassword(document)
local password = self.password_dialog:getInputText()
return document:unlock(password)
end
function ReaderUI:closeDialog()
self.password_dialog:onClose()
UIManager:close(self.password_dialog)
end
function ReaderUI:onScreenResize(dimen)
self.dimen = dimen
self:updateTouchZonesOnScreenResize(dimen)
end
function ReaderUI:saveSettings()
self:handleEvent(Event:new("SaveSettings"))
self.doc_settings:flush()
G_reader_settings:flush()
end
function ReaderUI:onFlushSettings()
self:saveSettings()
return true
end
function ReaderUI:closeDocument()
self.document:close()
self.document = nil
end
function ReaderUI:notifyCloseDocument()
self:handleEvent(Event:new("CloseDocument"))
if self.document:isEdited() then
local setting = G_reader_settings:readSetting("save_document")
if setting == "always" then
self:closeDocument()
elseif setting == "disable" then
self.document:discardChange()
self:closeDocument()
else
UIManager:show(ConfirmBox:new{
text = _("Do you want to save this document?"),
ok_text = _("Yes"),
cancel_text = _("No"),
ok_callback = function()
self:closeDocument()
end,
cancel_callback = function()
self.document:discardChange()
self:closeDocument()
end,
})
end
else
self:closeDocument()
end
end
function ReaderUI:onClose()
dbg("closing reader")
self:saveSettings()
if self.document ~= nil then
dbg("closing document")
self:notifyCloseDocument()
end
UIManager:close(self.dialog, "full")
-- serialize last used items for later launch
Cache:serialize()
if _running_instance == self then
_running_instance = nil
end
return true
end
return ReaderUI