mirror of
https://github.com/koreader/koreader
synced 2024-10-31 21:20:20 +00:00
6d53f83286
* ReaderDictionary: Port delay computations to TimeVal * ReaderHighlight: Port delay computations to TimeVal * ReaderView: Port delay computations to TimeVal * Android: Reset gesture detection state on APP_CMD_TERM_WINDOW. This prevents potentially being stuck in bogus gesture states when switching apps. * GestureDetector: * Port delay computations to TimeVal * Fixed delay computations to handle time warps (large and negative deltas). * Simplified timed callback handling to invalidate timers much earlier, preventing accumulating useless timers that no longer have any chance of ever detecting a gesture. * Fixed state clearing to handle the actual effective slots, instead of hard-coding slot 0 & slot 1. * Simplified timed callback handling in general, and added support for a timerfd backend for better performance and accuracy. * The improved timed callback handling allows us to detect and honor (as much as possible) the three possible clock sources usable by Linux evdev events. The only case where synthetic timestamps are used (and that only to handle timed callbacks) is limited to non-timerfd platforms where input events use a clock source that is *NOT* MONOTONIC. AFAICT, that's pretty much... PocketBook, and that's it? * Input: * Use the <linux/input.h> FFI module instead of re-declaring every constant * Fixed (verbose) debug logging of input events to actually translate said constants properly. * Completely reset gesture detection state on suspend. This should prevent bogus gesture detection on resume. * Refactored the waitEvent loop to make it easier to comprehend (hopefully) and much more efficient. Of specific note, it no longer does a crazy select spam every 100µs, instead computing and relying on sane timeouts, as afforded by switching the UI event/input loop to the MONOTONIC time base, and the refactored timed callbacks in GestureDetector. * reMarkable: Stopped enforcing synthetic timestamps on input events, as it should no longer be necessary. * TimeVal: * Refactored and simplified, especially as far as metamethods are concerned (based on <bsd/sys/time.h>). * Added a host of new methods to query the various POSIX clock sources, and made :now default to MONOTONIC. * Removed the debug guard in __sub, as time going backwards can be a perfectly normal occurrence. * New methods: * Clock sources: :realtime, :monotonic, :monotonic_coarse, :realtime_coarse, :boottime * Utility: :tonumber, :tousecs, :tomsecs, :fromnumber, :isPositive, :isZero * UIManager: * Ported event loop & scheduling to TimeVal, and switched to the MONOTONIC time base. This ensures reliable and consistent scheduling, as time is ensured never to go backwards. * Added a :getTime() method, that returns a cached TimeVal:now(), updated at the top of every UI frame. It's used throughout the codebase to cadge a syscall in circumstances where we are guaranteed that a syscall would return a mostly identical value, because very few time has passed. The only code left that does live syscalls does it because it's actually necessary for accuracy, and the only code left that does that in a REALTIME time base is code that *actually* deals with calendar time (e.g., Statistics). * DictQuickLookup: Port delay computations to TimeVal * FootNoteWidget: Port delay computations to TimeVal * HTMLBoxWidget: Port delay computations to TimeVal * Notification: Port delay computations to TimeVal * TextBoxWidget: Port delay computations to TimeVal * AutoSuspend: Port to TimeVal * AutoTurn: * Fix it so that settings are actually honored. * Port to TimeVal * BackgroundRunner: Port to TimeVal * Calibre: Port benchmarking code to TimeVal * BookInfoManager: Removed unnecessary yield in the metadata extraction subprocess now that subprocesses get scheduled properly. * All in all, these changes reduced the CPU cost of a single tap by a factor of ten (!), and got rid of an insane amount of weird poll/wakeup cycles that must have been hell on CPU schedulers and batteries..
275 lines
7.6 KiB
Lua
275 lines
7.6 KiB
Lua
--[[--
|
|
HTML widget (without scroll bars).
|
|
--]]
|
|
|
|
local Device = require("device")
|
|
local DrawContext = require("ffi/drawcontext")
|
|
local Geom = require("ui/geometry")
|
|
local GestureRange = require("ui/gesturerange")
|
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
|
local Mupdf = require("ffi/mupdf")
|
|
local Screen = Device.screen
|
|
local UIManager = require("ui/uimanager")
|
|
local logger = require("logger")
|
|
local util = require("util")
|
|
|
|
local HtmlBoxWidget = InputContainer:new{
|
|
bb = nil,
|
|
dimen = nil,
|
|
document = nil,
|
|
page_count = 0,
|
|
page_number = 1,
|
|
hold_start_pos = nil,
|
|
hold_start_tv = nil,
|
|
html_link_tapped_callback = nil,
|
|
}
|
|
|
|
function HtmlBoxWidget:init()
|
|
if Device:isTouchDevice() then
|
|
self.ges_events = {
|
|
TapText = {
|
|
GestureRange:new{
|
|
ges = "tap",
|
|
range = function() return self.dimen end,
|
|
},
|
|
},
|
|
}
|
|
end
|
|
end
|
|
|
|
function HtmlBoxWidget:setContent(body, css, default_font_size)
|
|
-- fz_set_user_css is tied to the context instead of the document so to easily support multiple
|
|
-- HTML dictionaries with different CSS, we embed the stylesheet into the HTML instead of using
|
|
-- that function.
|
|
local head = ""
|
|
if css then
|
|
head = string.format("<head><style>%s</style></head>", css)
|
|
end
|
|
local html = string.format("<html>%s<body>%s</body></html>", head, body)
|
|
|
|
-- For some reason in MuPDF <br/> always creates both a line break and an empty line, so we have to
|
|
-- simulate the normal <br/> behavior.
|
|
-- https://bugs.ghostscript.com/show_bug.cgi?id=698351
|
|
html = html:gsub("%<br ?/?%>", " <div></div>")
|
|
|
|
local ok
|
|
ok, self.document = pcall(Mupdf.openDocumentFromText, html, "html")
|
|
if not ok then
|
|
-- self.document contains the error
|
|
logger.warn("HTML loading error:", self.document)
|
|
|
|
body = util.htmlToPlainText(body)
|
|
body = util.htmlEscape(body)
|
|
-- Normally \n would be replaced with <br/>. See the previous comment regarding the bug in MuPDF.
|
|
body = body:gsub("\n", " <div></div>")
|
|
html = string.format("<html>%s<body>%s</body></html>", head, body)
|
|
|
|
ok, self.document = pcall(Mupdf.openDocumentFromText, html, "html")
|
|
if not ok then
|
|
error(self.document)
|
|
end
|
|
end
|
|
|
|
self.document:layoutDocument(self.dimen.w, self.dimen.h, default_font_size)
|
|
|
|
self.page_count = self.document:getPages()
|
|
end
|
|
|
|
function HtmlBoxWidget:_render()
|
|
if self.bb then
|
|
return
|
|
end
|
|
|
|
-- In pdfdocument.lua, color is activated only at the moment of
|
|
-- rendering and then immediately disabled, for safety with kopt.
|
|
-- We do the same here.
|
|
Mupdf.color = Screen:isColorEnabled()
|
|
|
|
local page = self.document:openPage(self.page_number)
|
|
local dc = DrawContext.new()
|
|
self.bb = page:draw_new(dc, self.dimen.w, self.dimen.h, 0, 0)
|
|
page:close()
|
|
|
|
Mupdf.color = false
|
|
end
|
|
|
|
function HtmlBoxWidget:getSize()
|
|
return self.dimen
|
|
end
|
|
|
|
function HtmlBoxWidget:getSinglePageHeight()
|
|
if self.page_count == 1 then
|
|
local page = self.document:openPage(1)
|
|
local x0, y0, x1, y1 = page:getUsedBBox() -- luacheck: no unused
|
|
page:close()
|
|
return math.ceil(y1) -- no content after y1
|
|
end
|
|
end
|
|
|
|
function HtmlBoxWidget:paintTo(bb, x, y)
|
|
self.dimen.x = x
|
|
self.dimen.y = y
|
|
|
|
self:_render()
|
|
|
|
local size = self:getSize()
|
|
|
|
bb:blitFrom(self.bb, x, y, 0, 0, size.w, size.h)
|
|
end
|
|
|
|
function HtmlBoxWidget:freeBb()
|
|
if self.bb and self.bb.free then
|
|
self.bb:free()
|
|
end
|
|
|
|
self.bb = nil
|
|
end
|
|
|
|
-- This will normally be called by our WidgetContainer:free()
|
|
-- But it SHOULD explicitly be called if we are getting replaced
|
|
-- (ie: in some other widget's update()), to not leak memory with
|
|
-- BlitBuffer zombies
|
|
function HtmlBoxWidget:free()
|
|
--print("HtmlBoxWidget:free on", self)
|
|
self:freeBb()
|
|
|
|
if self.document then
|
|
self.document:close()
|
|
self.document = nil
|
|
end
|
|
end
|
|
|
|
function HtmlBoxWidget:onCloseWidget()
|
|
-- free when UIManager:close() was called
|
|
self:free()
|
|
end
|
|
|
|
function HtmlBoxWidget:getPosFromAbsPos(abs_pos)
|
|
local pos = Geom:new{
|
|
x = abs_pos.x - self.dimen.x,
|
|
y = abs_pos.y - self.dimen.y,
|
|
}
|
|
|
|
-- check if the coordinates are actually inside our area
|
|
if pos.x < 0 or pos.x >= self.dimen.w or pos.y < 0 or pos.y >= self.dimen.h then
|
|
return nil
|
|
end
|
|
|
|
return pos
|
|
end
|
|
|
|
function HtmlBoxWidget:onHoldStartText(_, ges)
|
|
self.hold_start_pos = self:getPosFromAbsPos(ges.pos)
|
|
|
|
if not self.hold_start_pos then
|
|
return false -- let event be processed by other widgets
|
|
end
|
|
|
|
self.hold_start_tv = UIManager:getTime()
|
|
|
|
return true
|
|
end
|
|
|
|
function HtmlBoxWidget:onHoldPan(_, ges)
|
|
-- We don't highlight the currently selected text, but just let this
|
|
-- event pop up if we are not currently selecting text
|
|
if not self.hold_start_pos then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
function HtmlBoxWidget:getSelectedText(lines, start_pos, end_pos)
|
|
local found_start = false
|
|
local words = {}
|
|
|
|
for _, line in pairs(lines) do
|
|
for _, w in pairs(line) do
|
|
if type(w) == 'table' then
|
|
if not found_start then
|
|
if start_pos.x >= w.x0 and start_pos.x < w.x1 and start_pos.y >= w.y0 and start_pos.y < w.y1 then
|
|
found_start = true
|
|
elseif end_pos.x >= w.x0 and end_pos.x < w.x1 and end_pos.y >= w.y0 and end_pos.y < w.y1 then
|
|
-- We found end_pos before start_pos, switch them
|
|
found_start = true
|
|
start_pos, end_pos = end_pos, start_pos
|
|
end
|
|
end
|
|
|
|
if found_start then
|
|
table.insert(words, w.word)
|
|
|
|
-- Found the end.
|
|
if end_pos.x >= w.x0 and end_pos.x < w.x1 and end_pos.y >= w.y0 and end_pos.y < w.y1 then
|
|
return words
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return words
|
|
end
|
|
|
|
function HtmlBoxWidget:onHoldReleaseText(callback, ges)
|
|
if not callback then
|
|
return false
|
|
end
|
|
|
|
-- check we have seen a HoldStart event
|
|
if not self.hold_start_pos then
|
|
return false
|
|
end
|
|
|
|
local start_pos = self.hold_start_pos
|
|
self.hold_start_pos = nil
|
|
|
|
local end_pos = self:getPosFromAbsPos(ges.pos)
|
|
if not end_pos then
|
|
return false
|
|
end
|
|
|
|
local hold_duration = UIManager:getTime() - self.hold_start_tv
|
|
|
|
local page = self.document:openPage(self.page_number)
|
|
local lines = page:getPageText()
|
|
page:close()
|
|
|
|
local words = self:getSelectedText(lines, start_pos, end_pos)
|
|
local selected_text = table.concat(words, " ")
|
|
callback(selected_text, hold_duration)
|
|
|
|
return true
|
|
end
|
|
|
|
function HtmlBoxWidget:getLinkByPosition(pos)
|
|
local page = self.document:openPage(self.page_number)
|
|
local links = page:getPageLinks()
|
|
page:close()
|
|
|
|
for _, link in pairs(links) do
|
|
if pos.x >= link.x0 and pos.x < link.x1 and pos.y >= link.y0 and pos.y < link.y1 then
|
|
return link
|
|
end
|
|
end
|
|
end
|
|
|
|
function HtmlBoxWidget:onTapText(arg, ges)
|
|
if G_reader_settings:isFalse("tap_to_follow_links") then
|
|
return
|
|
end
|
|
|
|
if self.html_link_tapped_callback then
|
|
local pos = self:getPosFromAbsPos(ges.pos)
|
|
if pos then
|
|
local link = self:getLinkByPosition(pos)
|
|
if link then
|
|
self.html_link_tapped_callback(link)
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return HtmlBoxWidget
|