local BD = require("ui/bidi") local Blitbuffer = require("ffi/blitbuffer") local BottomContainer = require("ui/widget/container/bottomcontainer") local CenterContainer = require("ui/widget/container/centercontainer") local Device = require("device") local Event = require("ui/event") local FrameContainer = require("ui/widget/container/framecontainer") local Geom = require("ui/geometry") local GestureRange = require("ui/gesturerange") local HorizontalGroup = require("ui/widget/horizontalgroup") local HorizontalSpan = require("ui/widget/horizontalspan") local InputContainer = require("ui/widget/container/inputcontainer") local LineWidget = require("ui/widget/linewidget") local ScrollHtmlWidget = require("ui/widget/scrollhtmlwidget") local Size = require("ui/size") local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local VerticalGroup = require("ui/widget/verticalgroup") local VerticalSpan = require("ui/widget/verticalspan") local _ = require("gettext") local Screen = Device.screen local T = require("ffi/util").template -- If we wanted to use the default font set for the book, -- we'd need to add a few functions to crengine and cre.cpp -- to get the font files paths (for each font, regular, italic, -- bold...) so we can pass them to MuPDF with: -- @font-face { -- font-family: 'KOReader Footnote Font'; -- src: url("%1"); -- } -- @font-face { -- font-family: 'KOReader Footnote Font'; -- src: url("%2"); -- font-style: italic; -- } -- body { -- font-family: 'KOReader Footnote Font'; -- } -- But it looks quite fine if we use "Noto Sans": the difference in font look -- (Sans, vs probably Serif in the book) will help noticing this is a KOReader -- UI element, and somehow justify the difference in looks. -- Note: we can't use < or > in comments in the CSS, or MuPDF complains with: -- error: css syntax error: unterminated comment. -- So, HTML tags in comments are written upppercase (eg:
  • => LI) -- Independent string for @page, so we can T() it individually, -- without needing to escape % in DEFAULT_CSS local PAGE_CSS = [[ @page { margin: %1 %2 %3 %4; font-family: '%5'; } %6 ]] -- Make default MuPDF styles (source/html/html-layout.c) a bit -- more similar to our epub.css ones, and more condensed to fit -- in a small bottom pannel local DEFAULT_CSS = [[ body { margin: 0; /* MuPDF: margin: 1em */ padding: 0; line-height: 1.3; text-align: justify; } /* We keep left and right margin the same so it also displays as expected in RTL */ h1, h2, h3, h4, h5, h6 { margin: 0; } /* MuPDF: margin: XXem 0 , vary with level */ blockquote { margin: 0 1em; } /* MuPDF: margin: 1em 40px */ p { margin: 0; } /* MuPDF: margin: 1em 0 */ ol { margin: 0; } /* MuPDF: margin: 1em 0; padding: 0 0 0 30pt */ ul { margin: 0; } /* MuPDF: margin: 1em 0; padding: 0 0 0 30pt */ dl { margin: 0; } /* MuPDF: margin: 1em 0 */ dd { margin: 0 1em; } /* MuPDF: margin: 0 0 0 40px */ pre { margin: 0.5em 0; } /* MuPDF: margin: 1em 0 */ a { color: black; } /* MuPDF: color: #06C; */ /* MuPDF has no support for text-decoration, so we can't underline links, * which is fine as we don't really need to provide link support */ /* MuPDF draws the bullet for a standalone LI outside the margin. * Avoid it being displayed if the first node is a LI (in * Wikipedia EPUBs, each footnote is a LI */ body > li { list-style-type: none; } /* MuPDF always aligns the last line to the left when text-align: justify, * which is wrong with RTL. So cancel justification on RTL elements: they * will be correctly aligned to the right */ *[dir=rtl] { text-align: initial; } /* Remove any (possibly multiple) backlinks in Wikipedia EPUBs footnotes */ .noprint { display: none; } /* Style some FB2 tags not known to MuPDF */ strike, strikethrough { text-decoration: line-through; } underline { text-decoration: underline; } emphasis { font-style: italic; } small { font-size: 80%; } big { font-size: 130%; } empty-line { display: block; padding: 0.5em; } image { display: block; padding: 0.4em; border: 0.1em solid black; width: 0; } date { display: block; font-style: italic; } epigraph { display: block; font-style: italic; } poem { display: block; } stanza { display: block; font-style: italic; } v { display: block; text-align: left; hyphenate: none; } text-author { display: block; font-weight: bold; font-style: italic; } /* Attempt to display FB2 footnotes as expected (as crengine does, putting * the footnote number on the same line as the first paragraph via its * support of "display: run-in" and a possibly added autoBoxing element) */ body > section > autoBoxing > *, body > section > autoBoxing > title > *, body > section > title, body > section > title > p, body > section > p { display: inline; } body > section > autoBoxing + p, body > section > p + p { display: block; } body > section > autoBoxing > title, body > section > title { font-weight: bold; } ]] -- Add this if needed for debugging: -- @page { background-color: #cccccc; } -- body { background-color: #eeeeee; } -- Widget to display footnote HTML content local FootnoteWidget = InputContainer:new{ html = nil, css = nil, -- font_face can't really be overriden, it needs to be known by MuPDF font_face = "Noto Sans", -- For the doc_* values, we expect to be provided with the real -- (already scaled) sizes in screen pixels doc_font_size = Screen:scaleBySize(18), doc_margins = { left = Screen:scaleBySize(20), right = Screen:scaleBySize(20), top = Screen:scaleBySize(10), bottom = Screen:scaleBySize(10), }, follow_callback = nil, on_tap_close_callback = nil, close_callback = nil, dialog = nil, } function FootnoteWidget:init() -- Set widget size self.width = Screen:getWidth() self.height = math.floor(Screen:getHeight() * 1/3) -- will be decreased when content is smaller if Device:isTouchDevice() then local range = Geom:new{ x = 0, y = 0, w = Screen:getWidth(), h = Screen:getHeight(), } self.ges_events = { TapClose = { GestureRange:new{ ges = "tap", range = range, } }, SwipeFollow = { GestureRange:new{ ges = "swipe", range = range, } }, HoldStartText = { GestureRange:new{ ges = "hold", range = range, }, }, HoldPanText = { GestureRange:new{ ges = "hold", range = range, }, }, HoldReleaseText = { GestureRange:new{ ges = "hold_release", range = range, }, -- callback function when HoldReleaseText is handled as args args = function(text, hold_duration) if self.dialog then local lookup_target = hold_duration < TimeVal:new{ sec = 3, usec = 0 } and "LookupWord" or "LookupWikipedia" self.dialog:handleEvent( Event:new(lookup_target, text) ) end end }, } end if Device:hasKeys() then self.key_events = { Close = { {Device.input.group.Back}, doc = "cancel" }, Follow = { {"Press"}, doc = "follow link" }, } end -- Workaround bugs in MuPDF: -- There is something with its handling of
    : --
    abc
    def
    : 2 lines, no space in between --
    abc
    def
    : 3 lines, empty line in between -- Remove any attribute, let a
    be a plain
    self.html = self.html:gsub([[]*>]], [[
    ]]) -- Elements with a id= attribute get a line above them (by some internal MuPDF -- code, possibly generate_anchor() in html-layout.c). -- Working around it with the following does not work: *[id] {margin-top: -1em;} -- So, just rename the id= attribute, as we don't follow links in this popup. self.html = self.html:gsub([[(<[^>]* )[iI][dD]=]], [[%1disabledID=]]) -- We may use a font size a bit smaller than the document one (because -- footnotes are usually smaller, and because NotoSans is a bit on the -- larger size when compared to other fonts at the same size) local font_size = G_reader_settings:readSetting("footnote_popup_absolute_font_size") if font_size then font_size = Screen:scaleBySize(font_size) else font_size = self.doc_font_size + (G_reader_settings:readSetting("footnote_popup_relative_font_size") or -2) end -- We want to display the footnote text with the same margins as -- the document, but keep the scrollbar in the right margin, so -- both small footnotes (without scrollbar) and longer ones (with -- scrollbar) have their text aligned with the document text. -- MuPDF, when rendering some footnotes, may put list item -- bullets in its own left margin. To get a chance to have them -- shown, we let MuPDF handle our left margin. local html_left_margin = self.doc_margins.left .. "px" local html_right_margin = "0" if BD.mirroredUILayout() then html_left_margin, html_right_margin = html_right_margin, html_left_margin end local css = T(PAGE_CSS, "0", html_right_margin, "0", html_left_margin, -- top right bottom left self.font_face, DEFAULT_CSS) if self.css then -- add any provided css css = css .. "\n" .. self.css end -- require("logger").dbg("CSS:", css) -- require("logger").dbg("HTML:", self.html) -- Scrollbar on the right: the document may have quite large margins, -- and we don't want a scrollbar that large. -- Ensute a not too large scrollbar, and bring it closer to the screen -- edge, leaving the remaining available room between it and the text. local item_width = math.min(math.ceil(self.doc_margins.right * 2/5), Screen:scaleBySize(10)) local scroll_bar_width = item_width local padding_right = item_width local text_scroll_span = self.doc_margins.right - scroll_bar_width - padding_right if text_scroll_span < padding_right then -- With small doc margins, space between text and scrollbar may get -- too small: switch it with right padding text_scroll_span, padding_right = padding_right, text_scroll_span end local htmlwidget_width = self.width - padding_right -- Top and bottom padding: we'd rather not use document margins: -- they can be large, which makes sense at screen edges, but -- would be too large for our top padding. Also, MuPDF will often -- let some blank area at bottom with its line layout and page -- breaking algorithms, which will visually add to the bottom padding. local padding_top = Size.padding.large local padding_bottom = Size.padding.large local htmlwidget_height = self.height - padding_top - padding_bottom self.htmlwidget = ScrollHtmlWidget:new{ html_body = self.html, css = css, default_font_size = font_size, width = htmlwidget_width, height = htmlwidget_height, scroll_bar_width = scroll_bar_width, text_scroll_span = text_scroll_span, dialog = self.dialog, } -- We only want a top border, so use a LineWidget for that local top_border_size = Size.line.thick local vgroup = VerticalGroup:new{ LineWidget:new{ dimen = Geom:new{ w = self.width, h = top_border_size, } }, VerticalSpan:new{ width = padding_top }, HorizontalGroup:new{ self.htmlwidget, HorizontalSpan:new{ width = padding_right }, }, VerticalSpan:new{ width = padding_bottom }, } -- If htmlwidget contains only one page (small footnote content), -- display only the valuable area: push the blank area down the screen -- (we can do that because no scroll bar will be displayed) local single_page_height = self.htmlwidget:getSinglePageHeight() -- nil if multi-pages if single_page_height then local added_bottom_pad = 0 -- See if needed: -- Add a bit to bottom padding, as getSinglePageHeight() cut can be rough -- added_bottom_pad = math.floor(font_size * 0.2) local reduced_height = single_page_height + top_border_size + padding_top + padding_bottom + added_bottom_pad vgroup = CenterContainer:new{ dimen = Geom:new{ h = reduced_height, w = self.width, }, ignore = "height", vgroup, } self.height = reduced_height -- for close_callback end -- Needed only to set a white background self.container = FrameContainer:new{ background = Blitbuffer.COLOR_WHITE, bordersize = 0, margin = 0, padding = 0, vgroup, } self[1] = BottomContainer:new{ dimen = Screen:getSize(), self.container } end function FootnoteWidget:onShow() UIManager:setDirty(self.dialog, function() return "ui", self.container.dimen end) end function FootnoteWidget:onCloseWidget() UIManager:setDirty(self.dialog, function() return "partial", self.container.dimen end) end function FootnoteWidget:onClose() UIManager:close(self) if self.close_callback then self.close_callback(self.height) end return true end function FootnoteWidget:onFollow() if self.follow_callback then if self.close_callback then self.close_callback(self.height) end return self.follow_callback() end end function FootnoteWidget:onTapClose(arg, ges) if ges.pos:notIntersectWith(self.container.dimen) then UIManager:close(self) -- Allow ReaderLink to check if our dismiss tap -- was itself on another footnote, and display -- it. This avoids having to tap 2 times to -- see another footnote. if self.on_tap_close_callback then self.on_tap_close_callback(arg, ges, self.height) elseif self.close_callback then self.close_callback(self.height) end return true end return false end function FootnoteWidget:onSwipeFollow(arg, ges) local direction = BD.flipDirectionIfMirroredUILayout(ges.direction) if direction == "west" then if self.follow_callback then if self.close_callback then self.close_callback(self.height) end return self.follow_callback() end elseif direction == "south" or direction == "east" then UIManager:close(self) -- We can close with swipe down. If footnote is scrollable, -- this event will be eaten by ScrollHtmlWidget, and it will -- work only when started outside the footnote. -- Also allow closing with swipe east (like we do to go back -- from link) if self.close_callback then self.close_callback(self.height) end return true elseif direction == "north" then -- no use for now do end -- luacheck: ignore 541 else -- diagonal swipe -- trigger full refresh UIManager:setDirty(nil, "full") -- a long diagonal swipe may also be used for taking a screenshot, -- so let it propagate end return false end return FootnoteWidget