2
0
mirror of https://github.com/koreader/koreader synced 2024-11-18 03:25:46 +00:00
koreader/frontend/ui/widget/htmlboxwidget.lua
poire-z f9086a2ba9
EPUB links: show footnotes in popup, larger tap area (#4261)
Adds new options to the Links> submenu, for now only
available and used with CRE documents.
- Allow larger tap area around links
- Ignore external links
- Show footnotes in popup
- Show more links as footnotes

(This last item is mostly for testing and loosening the
footnote detection algorithm, and see how it would behave
with glossary-like links and inter glossary terms links.)

Fix distance computation from gesture position to link by
using segments.
Code for detecting if a link is a footnote is in cre.cpp, and
tweakable a bit with flags in ReaderLink:showAsFoonotePopup().

Footnotes HTML content is displayed by a new FootnoteWidget,
which uses MuPDF for its rendering.
From it, swipe south or tap outside to close, swipe to the left
to follow the original link and jump to the footnote location
in the book.

Also fix tap on highlights after the recent change to use segments
for displaying: use segments also when checking taps.
2018-10-10 18:50:24 +02:00

281 lines
7.8 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 = require("device").screen
local TimeVal = require("ui/timeval")
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
if Mupdf.bgr == nil then
Mupdf.bgr = false
if Device:hasBGRFrameBuffer() then
Mupdf.bgr = true
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 ?/?%>", "&nbsp;<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", "&nbsp;<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()
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 = TimeVal.now()
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 = TimeVal.now() - self.hold_start_tv
hold_duration = hold_duration.sec + (hold_duration.usec/1000000)
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