mirror of https://github.com/koreader/koreader
Add Book map and Page browser features
- Book map: shows a map of content, including TOC, boomarks, read pages, non-linear flows... - Page browser: shows thumbnails of pages. - ReaderThumbnail: new Reader module that provides a service for generating thumbnails of book pages. It makes available these 2 new fullscreen widgets. - ReaderBookmark, ReaderLink, Statistics: add methods to return new views of bookmarks, previous locations and read pages, that are needed by BookMapWidget. - ReaderToc: compute TOC max_depth. - ReaderBookmark, ReaderHighlight: send events on bookmark add/update/remove so thumbnails of the pages impacted can be trashed.reviewable/pr8618/r1
parent
2530e954a2
commit
bc16b32395
@ -0,0 +1,507 @@
|
|||||||
|
local Blitbuffer = require("ffi/blitbuffer")
|
||||||
|
local Cache = require("cache")
|
||||||
|
local Device = require("device")
|
||||||
|
local Geom = require("ui/geometry")
|
||||||
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||||
|
local Persist = require("persist")
|
||||||
|
local RenderImage = require("ui/renderimage")
|
||||||
|
local TileCacheItem = require("document/tilecacheitem")
|
||||||
|
local UIManager = require("ui/uimanager")
|
||||||
|
local Screen = Device.screen
|
||||||
|
local ffiutil = require("ffi/util")
|
||||||
|
local logger = require("logger")
|
||||||
|
local util = require("util")
|
||||||
|
local _ = require("gettext")
|
||||||
|
|
||||||
|
-- This ReaderThumbnail module provides a service for generating thumbnails
|
||||||
|
-- of book pages.
|
||||||
|
-- It handles launching via the menu or Dispatcher/Gestures two fullscreen
|
||||||
|
-- widgets related to showing pages and thumbnails that will make use of
|
||||||
|
-- its services: BookMap and PageBrowser.
|
||||||
|
local ReaderThumbnail = InputContainer:new{}
|
||||||
|
|
||||||
|
function ReaderThumbnail:init()
|
||||||
|
if not Device:isTouchDevice() then
|
||||||
|
-- The BookMap and PageBrowser widgets depend too much on gestures,
|
||||||
|
-- making them work with keys would be hard and very limited, so
|
||||||
|
-- just don't make them available.
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self.ui.menu:registerToMainMenu(self)
|
||||||
|
|
||||||
|
-- Use LuaJIT fast buffer.encode()/decode() when serializing BlitBuffer
|
||||||
|
-- for exchange between subprocess and parent.
|
||||||
|
self.codec = Persist.getCodec("luajit")
|
||||||
|
|
||||||
|
self:setupColor()
|
||||||
|
self.thumbnails_requests = {}
|
||||||
|
self.current_target_size_tag = nil
|
||||||
|
|
||||||
|
-- Ensure no multiple executions, and nextTick() the scheduleIn()
|
||||||
|
-- so we get a chance to process events in-between refreshes and
|
||||||
|
-- this can be interrupted (otherwise, something scheduleIn(0.1),
|
||||||
|
-- if a screen refresh is then done and taking longer than 0.1s,
|
||||||
|
-- would be executed immediately, without emptying any input event).
|
||||||
|
local schedule_step = 0
|
||||||
|
self._ensureTileGeneration_action = function(restart)
|
||||||
|
if restart then
|
||||||
|
UIManager:unschedule(self._ensureTileGeneration_action)
|
||||||
|
schedule_step = 0
|
||||||
|
end
|
||||||
|
if schedule_step == 0 then
|
||||||
|
schedule_step = 1
|
||||||
|
UIManager:nextTick(self._ensureTileGeneration_action)
|
||||||
|
elseif schedule_step == 1 then
|
||||||
|
schedule_step = 2
|
||||||
|
UIManager:scheduleIn(0.1, self._ensureTileGeneration_action)
|
||||||
|
else
|
||||||
|
schedule_step = 0
|
||||||
|
self:ensureTileGeneration()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:addToMainMenu(menu_items)
|
||||||
|
menu_items.book_map = {
|
||||||
|
text = _("Book map"),
|
||||||
|
callback = function()
|
||||||
|
self:onShowBookMap()
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
menu_items.page_browser = {
|
||||||
|
text = _("Page browser"),
|
||||||
|
callback = function()
|
||||||
|
self:onShowPageBrowser()
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:onShowBookMap()
|
||||||
|
local BookMapWidget = require("ui/widget/bookmapwidget")
|
||||||
|
UIManager:show(BookMapWidget:new{
|
||||||
|
ui = self.ui,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:onShowPageBrowser()
|
||||||
|
local PageBrowserWidget = require("ui/widget/pagebrowserwidget")
|
||||||
|
UIManager:show(PageBrowserWidget:new{
|
||||||
|
ui = self.ui,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This is made a module local so we can keep track of pids
|
||||||
|
-- to collect across multiple Reader instantiations
|
||||||
|
local pids_to_collect = {}
|
||||||
|
|
||||||
|
function ReaderThumbnail:collectPids()
|
||||||
|
if #pids_to_collect == 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for i=#pids_to_collect, 1, -1 do
|
||||||
|
if ffiutil.isSubProcessDone(pids_to_collect[i]) then
|
||||||
|
table.remove(pids_to_collect, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return #pids_to_collect > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:setupColor()
|
||||||
|
self.bb_type = self.ui.document.render_color and self.ui.document.color_bb_type or Blitbuffer.TYPE_BB8
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:setupCache()
|
||||||
|
if not self.tile_cache then
|
||||||
|
-- We want to allow browsing at least N pages worth of thumbnails
|
||||||
|
-- without cache trashing. A little more than N pages (because inter
|
||||||
|
-- thumbnail margins) will fit in N * screen size.
|
||||||
|
-- With N=5, this should use from 5 to 15 Mb on a classic eInk device.
|
||||||
|
local N = 5
|
||||||
|
local max_bytes = math.ceil(N * Screen:getWidth() * Screen:getHeight() * Blitbuffer.TYPE_TO_BPP[self.bb_type] / 8)
|
||||||
|
-- We don't really care about limiting any number of slots, so allow
|
||||||
|
-- for at least 5 pages of 10x10 tiles
|
||||||
|
local avg_itemsize = math.ceil(max_bytes / 500)
|
||||||
|
self.tile_cache = Cache:new{
|
||||||
|
size = max_bytes,
|
||||||
|
avg_itemsize = avg_itemsize, -- will make slots=500
|
||||||
|
enable_eviction_cb = true,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:logCacheSize()
|
||||||
|
logger.info(string.format("Thumbnails cache: %d/%d (%s/%s)",
|
||||||
|
self.tile_cache.cache.used_slots(),
|
||||||
|
self.tile_cache.slots,
|
||||||
|
util.getFriendlySize(self.tile_cache.cache.used_size()),
|
||||||
|
util.getFriendlySize(self.tile_cache.size)))
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:resetCache()
|
||||||
|
if self.tile_cache then
|
||||||
|
self.tile_cache:clear()
|
||||||
|
self.tile_cache = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:removeFromCache(hash_subs, remove_only_non_matching)
|
||||||
|
-- Remove from cache all tiles matching any hash from hash_subs.
|
||||||
|
-- IF only_non_matching=true, keep those matching and remove all others.
|
||||||
|
if not self.tile_cache then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if type(hash_subs) ~= "table" then
|
||||||
|
hash_subs = { hash_subs }
|
||||||
|
end
|
||||||
|
local nb_removed, size_removed = 0, 0
|
||||||
|
local to_remove = {}
|
||||||
|
for thash, tile in self.tile_cache.cache:pairs() do
|
||||||
|
local remove = remove_only_non_matching
|
||||||
|
for _, h in ipairs(hash_subs) do
|
||||||
|
if thash:find(h, 1, true) then -- plain text match (no pattern needed)
|
||||||
|
remove = not remove
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if remove then
|
||||||
|
to_remove[thash] = true
|
||||||
|
nb_removed = nb_removed + 1
|
||||||
|
size_removed = size_removed + tile.size
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for thash, _ in pairs(to_remove) do
|
||||||
|
self.tile_cache.cache:delete(thash)
|
||||||
|
logger.dbg("removed cached thumbnail", thash)
|
||||||
|
end
|
||||||
|
return nb_removed, size_removed
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:resetCachedPagesForBookmarks(...)
|
||||||
|
-- Multiple bookmarks may be provided
|
||||||
|
local start_page, end_page
|
||||||
|
for _, bm in ipairs({...}) do
|
||||||
|
if self.ui.rolling then
|
||||||
|
-- Look at all properties that may be xpointers
|
||||||
|
for _, k in ipairs({"page", "pos0", "pos1"}) do
|
||||||
|
if bm[k] and type(bm[k]) == "string" then
|
||||||
|
local p = self.ui.document:getPageFromXPointer(bm[k])
|
||||||
|
if not start_page or p < start_page then
|
||||||
|
start_page = p
|
||||||
|
end
|
||||||
|
if not end_page or p > end_page then
|
||||||
|
end_page = p
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if bm.page and type(bm.page) == "number" then
|
||||||
|
local p = bm.page
|
||||||
|
if not start_page or p < start_page then
|
||||||
|
start_page = p
|
||||||
|
end
|
||||||
|
if not end_page or p > end_page then
|
||||||
|
end_page = p
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if start_page and end_page then
|
||||||
|
local hash_subs_to_remove = {}
|
||||||
|
for p=start_page, end_page do
|
||||||
|
table.insert(hash_subs_to_remove, string.format("p%d-", p))
|
||||||
|
end
|
||||||
|
self:removeFromCache(hash_subs_to_remove)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:tidyCache()
|
||||||
|
if self.current_target_size_tag then
|
||||||
|
-- Remove all thumbnails generated for an older target size
|
||||||
|
self:removeFromCache("-"..self.current_target_size_tag, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:cancelPageThumbnailRequests(batch_id)
|
||||||
|
if batch_id then
|
||||||
|
self.thumbnails_requests[batch_id] = nil
|
||||||
|
else
|
||||||
|
self.thumbnails_requests = {}
|
||||||
|
end
|
||||||
|
if self.req_in_progress and (not batch_id or self.req_in_progress.batch_id == batch_id) then
|
||||||
|
-- Kill any reference to the module cancelling it
|
||||||
|
self.req_in_progress.when_generated_callback = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:getPageThumbnail(page, width, height, batch_id, when_generated_callback)
|
||||||
|
self:setupCache()
|
||||||
|
self.current_target_size_tag = string.format("w%d_h%d", width, height)
|
||||||
|
local hash = string.format("p%d-%s", page, self.current_target_size_tag)
|
||||||
|
local tile = self.tile_cache and self.tile_cache:check(hash)
|
||||||
|
if tile then
|
||||||
|
-- Cached: call callback and we're done.
|
||||||
|
when_generated_callback(tile, batch_id, false)
|
||||||
|
return false -- not delayed
|
||||||
|
end
|
||||||
|
if not self.thumbnails_requests[batch_id] then
|
||||||
|
self.thumbnails_requests[batch_id] = {}
|
||||||
|
end
|
||||||
|
table.insert(self.thumbnails_requests[batch_id], {
|
||||||
|
batch_id = batch_id,
|
||||||
|
hash = hash,
|
||||||
|
page = page,
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
when_generated_callback = when_generated_callback,
|
||||||
|
})
|
||||||
|
-- Start tile generation, avoid multiple ones
|
||||||
|
self._ensureTileGeneration_action(true)
|
||||||
|
return true -- delayed
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:ensureTileGeneration()
|
||||||
|
local has_pids_still_to_collect = self:collectPids()
|
||||||
|
|
||||||
|
local still_in_progress = false
|
||||||
|
if self.req_in_progress then
|
||||||
|
local pid_still_to_collect
|
||||||
|
still_in_progress, pid_still_to_collect = self:checkTileGeneration(self.req_in_progress)
|
||||||
|
if pid_still_to_collect then
|
||||||
|
has_pids_still_to_collect = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not still_in_progress then
|
||||||
|
self.req_in_progress = nil
|
||||||
|
while true do
|
||||||
|
local req_id, requests = next(self.thumbnails_requests)
|
||||||
|
if not req_id then -- no more requests
|
||||||
|
break
|
||||||
|
end
|
||||||
|
local req = table.remove(requests, 1)
|
||||||
|
if #requests == 0 then
|
||||||
|
self.thumbnails_requests[req_id] = nil
|
||||||
|
end
|
||||||
|
if req.when_generated_callback then -- not cancelled since queued
|
||||||
|
-- It might have been generated and cached by a previous batch
|
||||||
|
local tile = self.tile_cache and self.tile_cache:check(req.hash)
|
||||||
|
if tile then
|
||||||
|
req.when_generated_callback(tile, req.batch_id, true)
|
||||||
|
else
|
||||||
|
if self:startTileGeneration(req) then
|
||||||
|
self.req_in_progress = req
|
||||||
|
break
|
||||||
|
else
|
||||||
|
-- Failure starting it: let requester know in case it cares, and forget it
|
||||||
|
req.when_generated_callback(nil, req.batch_id, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if self.req_in_progress or has_pids_still_to_collect or next(self.thumbnails_requests) then
|
||||||
|
self._ensureTileGeneration_action()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:startTileGeneration(request)
|
||||||
|
local pid, parent_read_fd = ffiutil.runInSubProcess(function(pid, child_write_fd)
|
||||||
|
-- Get page image as if drawn on the screen
|
||||||
|
local bb = self:_getPageImage(request.page)
|
||||||
|
-- Scale it to fit in the requested size
|
||||||
|
local scale_factor = math.min(request.width / bb:getWidth(), request.height / bb:getHeight())
|
||||||
|
local target_w = math.floor(bb:getWidth() * scale_factor)
|
||||||
|
local target_h = math.floor(bb:getHeight() * scale_factor)
|
||||||
|
-- local TimeVal = require("ui/timeval")
|
||||||
|
-- local start_tv = TimeVal:now()
|
||||||
|
local tile = TileCacheItem:new{
|
||||||
|
bb = RenderImage:scaleBlitBuffer(bb, target_w, target_h, true),
|
||||||
|
pageno = request.page,
|
||||||
|
}
|
||||||
|
tile.size = tonumber(tile.bb.stride) * tile.bb.h
|
||||||
|
-- logger.info("tile size", tile.bb.w, tile.bb.h, "=>", tile.size)
|
||||||
|
-- logger.info(string.format(" scaling took %.3f seconds, %d bpp", TimeVal:getDuration(start_tv), tile.bb:getBpp()))
|
||||||
|
-- bb:free() -- no need to spend time freeing, we're dying soon anyway!
|
||||||
|
|
||||||
|
ffiutil.writeToFD(child_write_fd, self.codec.serialize(tile:totable()), true)
|
||||||
|
end, true) -- with_pipe = true
|
||||||
|
if pid then
|
||||||
|
-- Store these in the request object itself
|
||||||
|
request.pid = pid
|
||||||
|
request.parent_read_fd = parent_read_fd
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
logger.warn("PageBrowserWidget thumbnail start failure:", parent_read_fd)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:checkTileGeneration(request)
|
||||||
|
local pid, parent_read_fd = request.pid, request.parent_read_fd
|
||||||
|
local stuff_to_read = ffiutil.getNonBlockingReadSize(parent_read_fd) ~= 0
|
||||||
|
local subprocess_done = ffiutil.isSubProcessDone(pid)
|
||||||
|
logger.dbg("subprocess_done:", subprocess_done, " stuff_to_read:", stuff_to_read)
|
||||||
|
if stuff_to_read then
|
||||||
|
-- local TimeVal = require("ui/timeval")
|
||||||
|
-- local start_tv = TimeVal:now()
|
||||||
|
local result, err = self.codec.deserialize(ffiutil.readAllFromFD(parent_read_fd))
|
||||||
|
if result then
|
||||||
|
local tile = TileCacheItem:new{}
|
||||||
|
tile:fromtable(result)
|
||||||
|
if self.tile_cache then
|
||||||
|
self.tile_cache:insert(request.hash, tile)
|
||||||
|
end
|
||||||
|
if request.when_generated_callback then -- not cancelled
|
||||||
|
request.when_generated_callback(tile, request.batch_id, true)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
logger.warn("PageBrowserWidget thumbnail deserialize() failed:", err)
|
||||||
|
if request.when_generated_callback then -- not cancelled
|
||||||
|
request.when_generated_callback(nil, request.batch_id, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- logger.info(string.format(" parsing result from subprocess took %.3f seconds", TimeVal:getDuration(start_tv)))
|
||||||
|
if not subprocess_done then
|
||||||
|
table.insert(pids_to_collect, pid)
|
||||||
|
return false, true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
elseif subprocess_done then
|
||||||
|
-- subprocess_done: process exited with no output
|
||||||
|
ffiutil.readAllFromFD(parent_read_fd) -- close our fd
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
logger.dbg("process not yet done, will check again soon")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:_getPageImage(page)
|
||||||
|
-- This is run in a subprocess: we can tweak all document settings
|
||||||
|
-- to get an adequate image of the page.
|
||||||
|
-- No need to worry about the final state of things: this subprocess
|
||||||
|
-- will die just after drawing the page, and all will be forgotten,
|
||||||
|
-- without impact on the parent process.
|
||||||
|
|
||||||
|
-- Be sure to limit our impact on the disk-saved book state
|
||||||
|
self.ui.saveSettings = function() end -- Be sure nothing is flushed
|
||||||
|
self.ui.statistics = nil -- Don't update statistics for pages we visit
|
||||||
|
|
||||||
|
-- By default, our target page size is the current screen size
|
||||||
|
local target_w, target_h = Screen:getWidth(), Screen:getHeight()
|
||||||
|
|
||||||
|
-- This was all mostly chosen by experimenting.
|
||||||
|
-- Be sure to call the innermost methods enough to get what we want, and
|
||||||
|
-- not upper event handlers that may trigger other unneeded events and stuff.
|
||||||
|
-- Especially, be sure to not trigger any paint on the screen buffer, or
|
||||||
|
-- any processing of input events.
|
||||||
|
-- No need to worry about UIManager:scheduleIn() or :nextTick(), as
|
||||||
|
-- we will die before the callback gets a chance to be run.
|
||||||
|
|
||||||
|
-- Common to ReaderRolling and ReaderPaging
|
||||||
|
self.ui.view.footer_visible = false -- We want no footer on page image
|
||||||
|
if self.ui.view.highlight.lighten_factor < 0.3 then
|
||||||
|
self.ui.view.highlight.lighten_factor = 0.3 -- make lighten highlight a bit darker
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.ui.rolling then
|
||||||
|
-- CRE documents: pages all have the aspect ratio of our screen (alt top status bar
|
||||||
|
-- will be croped out after drawing), we will show them just as rendered.
|
||||||
|
self.ui.view:onSetViewMode("page") -- Get out of scroll mode
|
||||||
|
if self.ui.font.gamma_index < 30 then -- Increase font gamma (if not already increased),
|
||||||
|
self.ui.document:setGammaIndex(30) -- as downscaling will make text grayer
|
||||||
|
end
|
||||||
|
self.ui.document:setImageScaling(false) -- No need for smooth scaling as all will be downscaled
|
||||||
|
self.ui.document:setNightmodeImages(false) -- We don't invert page images even if nightmode set: keep images as-is
|
||||||
|
self.ui.view.state.page = page -- Be on requested page
|
||||||
|
self.ui.document:gotoPage(page) -- Current xpointer needs to be updated for some of what follows
|
||||||
|
self.ui.bookmark:onPageUpdate(page) -- Update dogear state for this page
|
||||||
|
self.ui.pagemap:onPageUpdate(page) -- Update pagemap labels for this page
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.ui.paging then
|
||||||
|
-- With PDF/DJVU/Pics, we will show the native page (no reflow, no crop, no zoom
|
||||||
|
-- to columns...). This makes thumbnail generation faster, and will allow the user
|
||||||
|
-- to get an overview of the book native pages to better decide which option will
|
||||||
|
-- be best to use for the book.
|
||||||
|
-- We also want to get a thumbnail with the aspect ratio of the native page
|
||||||
|
-- (so we don't get a native landscape page smallish and centered with blank above
|
||||||
|
-- and below in a portrait thumbnail, if the screen is in portrait mode).
|
||||||
|
|
||||||
|
self.ui.view.hinting = false -- Disable hinting
|
||||||
|
self.ui.view.page_scroll = false -- Get out of scroll mode
|
||||||
|
self.ui.view.flipping_visible = false -- No page flipping icon
|
||||||
|
self.ui.document.configurable.text_wrap = false -- Get out of reflow mode
|
||||||
|
self.ui.document.configurable.trim_page = 3 -- Page crop: none
|
||||||
|
-- self.ui.document.configurable.trim_page = 1 -- Page crop: auto (very slower)
|
||||||
|
self.ui.document.configurable.auto_straighten = 0 -- No auto straighten
|
||||||
|
-- We can let dewatermark if the user has enabled it, it helps
|
||||||
|
-- limiting annoying eInk refreshes of light gray areas
|
||||||
|
-- self.ui.document.configurable.page_opt = 0 -- No dewatermark
|
||||||
|
-- We won't touch the contrast (to try making text less gray), as it applies on
|
||||||
|
-- images that could get too dark.
|
||||||
|
|
||||||
|
-- Get native page dimensions, and update our target bb dimensions so it gets the
|
||||||
|
-- same aspect ratio (we don't use native dimensions as is, as they may get huge)
|
||||||
|
local dimen = self.ui.document:getPageDimensions(page, 1, 0)
|
||||||
|
local scale_factor = math.min(target_w / dimen.w, target_h / dimen.h)
|
||||||
|
target_w = math.floor(dimen.w * scale_factor)
|
||||||
|
target_h = math.floor(dimen.h * scale_factor)
|
||||||
|
dimen = Geom:new{ w=target_w, h=target_h }
|
||||||
|
-- logger.info("getPageImage", page, dimen, "=>", target_w, target_h, scale_factor)
|
||||||
|
|
||||||
|
-- This seems to do it all well:
|
||||||
|
-- local Event = require("ui/event")
|
||||||
|
-- self.ui:handleEvent(Event:new("SetDimensions", dimen))
|
||||||
|
-- self.ui.view.dogear[1].dimen.w = dimen.w -- (hack... its code uses the Screen width)
|
||||||
|
-- self.ui:handleEvent(Event:new("PageUpdate", page))
|
||||||
|
-- self.ui:handleEvent(Event:new("SetZoomMode", "page"))
|
||||||
|
|
||||||
|
-- Trying to do as little as needed, knowing the internals:
|
||||||
|
self.ui.view:onSetDimensions(dimen)
|
||||||
|
self.ui.view:onBBoxUpdate(nil) -- drop any bbox, draw native page
|
||||||
|
self.ui.view.state.page = page
|
||||||
|
self.ui.view.state.zoom = scale_factor
|
||||||
|
self.ui.view.state.rotation = 0
|
||||||
|
self.ui.view:recalculate()
|
||||||
|
self.ui.view.dogear[1].dimen.w = dimen.w -- (hack... its code uses the Screen width)
|
||||||
|
self.ui.bookmark:onPageUpdate(page) -- Update dogear state for this page
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Draw the page on a new BB with the targetted size
|
||||||
|
local bb = Blitbuffer.new(target_w, target_h, self.bb_type)
|
||||||
|
self.ui.view:paintTo(bb, 0, 0)
|
||||||
|
|
||||||
|
if self.ui.rolling then
|
||||||
|
-- Crop out the top alt status bar if enabled
|
||||||
|
local header_height = self.ui.document:getHeaderHeight()
|
||||||
|
if header_height > 0 then
|
||||||
|
bb = bb:viewport(0, header_height, bb.w, bb.h - header_height)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return bb
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:onCloseDocument()
|
||||||
|
self:cancelPageThumbnailRequests()
|
||||||
|
if self.tile_cache then
|
||||||
|
self:logCacheSize()
|
||||||
|
self.tile_cache:clear()
|
||||||
|
self.tile_cache = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ReaderThumbnail:onColorRenderingUpdate()
|
||||||
|
self:setupColor()
|
||||||
|
self:resetCache()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- CRE: emitted after a re-rendering
|
||||||
|
ReaderThumbnail.onTocReset = ReaderThumbnail.resetCache
|
||||||
|
-- Emitted When adding/removing/updating bookmarks and highlights
|
||||||
|
ReaderThumbnail.onBookmarkAdded = ReaderThumbnail.resetCachedPagesForBookmarks
|
||||||
|
ReaderThumbnail.onBookmarkRemoved = ReaderThumbnail.resetCachedPagesForBookmarks
|
||||||
|
ReaderThumbnail.onBookmarkUpdated = ReaderThumbnail.resetCachedPagesForBookmarks
|
||||||
|
|
||||||
|
return ReaderThumbnail
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,947 @@
|
|||||||
|
local BD = require("ui/bidi")
|
||||||
|
local Blitbuffer = require("ffi/blitbuffer")
|
||||||
|
local CenterContainer = require("ui/widget/container/centercontainer")
|
||||||
|
local Device = require("device")
|
||||||
|
local Event = require("ui/event")
|
||||||
|
local Font = require("ui/font")
|
||||||
|
local FrameContainer = require("ui/widget/container/framecontainer")
|
||||||
|
local Geom = require("ui/geometry")
|
||||||
|
local GestureRange = require("ui/gesturerange")
|
||||||
|
local ImageWidget = require("ui/widget/imagewidget")
|
||||||
|
local InfoMessage = require("ui/widget/infomessage")
|
||||||
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||||
|
local OverlapGroup = require("ui/widget/overlapgroup")
|
||||||
|
local Size = require("ui/size")
|
||||||
|
local TextWidget = require("ui/widget/textwidget")
|
||||||
|
local TitleBar = require("ui/widget/titlebar")
|
||||||
|
local UIManager = require("ui/uimanager")
|
||||||
|
local VerticalGroup = require("ui/widget/verticalgroup")
|
||||||
|
local VerticalSpan = require("ui/widget/verticalspan")
|
||||||
|
local Input = Device.input
|
||||||
|
local Screen = Device.screen
|
||||||
|
local logger = require("logger")
|
||||||
|
local _ = require("gettext")
|
||||||
|
|
||||||
|
-- We use the BookMapRow widget, a local widget defined in bookmapwidget.lua,
|
||||||
|
-- that we made available via BookMapWidget itself
|
||||||
|
local BookMapWidget = require("ui/widget/bookmapwidget")
|
||||||
|
local BookMapRow = BookMapWidget.BookMapRow
|
||||||
|
|
||||||
|
-- PageBrowserWidget: shows thumbnails of pages
|
||||||
|
local PageBrowserWidget = InputContainer:new{
|
||||||
|
title = _("Page browser"),
|
||||||
|
-- Focus page: will be put at the best place in the thumbnail grid
|
||||||
|
-- (that is, the grid will pick thumbnails from pages before and
|
||||||
|
-- after it, and more pages after than before)
|
||||||
|
focus_page = nil,
|
||||||
|
-- Should only be nil on the first launch via ReaderThumbnail
|
||||||
|
launcher = nil,
|
||||||
|
|
||||||
|
_mirroredUI = BD.mirroredUILayout(),
|
||||||
|
}
|
||||||
|
|
||||||
|
function PageBrowserWidget:init()
|
||||||
|
-- Compute non-settings-dependant sizes and options
|
||||||
|
self.dimen = Geom:new{
|
||||||
|
w = Screen:getWidth(),
|
||||||
|
h = Screen:getHeight(),
|
||||||
|
}
|
||||||
|
self.covers_fullscreen = true -- hint for UIManager:_repaint()
|
||||||
|
|
||||||
|
if Device:hasKeys() then
|
||||||
|
self.key_events = {
|
||||||
|
Close = { {"Back"}, doc = "close page" },
|
||||||
|
ScrollRowUp = {{"Up"}, doc = "scroll up"},
|
||||||
|
ScrollRowDown = {{"Down"}, doc = "scrol down"},
|
||||||
|
ScrollPageUp = {{Input.group.PgBack}, doc = "prev page"},
|
||||||
|
ScrollPageDown = {{Input.group.PgFwd}, doc = "next page"},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if Device:isTouchDevice() then
|
||||||
|
self.ges_events.Swipe = {
|
||||||
|
GestureRange:new{
|
||||||
|
ges = "swipe",
|
||||||
|
range = self.dimen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.ges_events.MultiSwipe = {
|
||||||
|
GestureRange:new{
|
||||||
|
ges = "multiswipe",
|
||||||
|
range = self.dimen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.ges_events.Tap = {
|
||||||
|
GestureRange:new{
|
||||||
|
ges = "tap",
|
||||||
|
range = self.dimen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.ges_events.Hold = {
|
||||||
|
GestureRange:new{
|
||||||
|
ges = "hold",
|
||||||
|
range = self.dimen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.ges_events.Pinch = {
|
||||||
|
GestureRange:new{
|
||||||
|
ges = "pinch",
|
||||||
|
range = self.dimen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.ges_events.Spread = {
|
||||||
|
GestureRange:new{
|
||||||
|
ges = "spread",
|
||||||
|
range = self.dimen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Put the BookMapRow left and right border outside of screen
|
||||||
|
self.row_width = self.dimen.w + 2*BookMapRow.pages_frame_border
|
||||||
|
|
||||||
|
self.title_bar = TitleBar:new{
|
||||||
|
fullscreen = true,
|
||||||
|
title = self.title,
|
||||||
|
left_icon = "notice-info",
|
||||||
|
left_icon_tap_callback = function() self:showHelp() end,
|
||||||
|
close_callback = function() self:onClose() end,
|
||||||
|
hold_close_callback = function() self:onClose(true) end,
|
||||||
|
show_parent = self,
|
||||||
|
}
|
||||||
|
self.title_bar_h = self.title_bar:getHeight()
|
||||||
|
|
||||||
|
-- Guess grid TOC span height from its font size
|
||||||
|
-- (it feels this font size does not need to be configurable: too large and
|
||||||
|
-- titles will be too easily truncated, too small and they will be unreadable)
|
||||||
|
self.toc_span_font_name = "infofont"
|
||||||
|
self.toc_span_font_size = 14
|
||||||
|
self.toc_span_face = Font:getFace(self.toc_span_font_name, self.toc_span_font_size)
|
||||||
|
local test_w = TextWidget:new{
|
||||||
|
text = "z",
|
||||||
|
face = self.toc_span_face,
|
||||||
|
}
|
||||||
|
self.span_height = test_w:getSize().h + BookMapRow.toc_span_border
|
||||||
|
test_w:free()
|
||||||
|
|
||||||
|
self.min_nb_rows = 1
|
||||||
|
self.max_nb_rows = 6
|
||||||
|
self.min_nb_cols = 1
|
||||||
|
self.max_nb_cols = 6
|
||||||
|
|
||||||
|
self.ui.toc:fillToc()
|
||||||
|
self.max_toc_depth = self.ui.toc.toc_depth
|
||||||
|
-- We show the toc depth chosen in BookMapWidget, or all of it if not set
|
||||||
|
-- (nothing in this PageBrowserWidget to allow changing it)
|
||||||
|
self.nb_toc_spans = self.ui.doc_settings:readSetting("book_map_toc_depth", self.max_toc_depth)
|
||||||
|
|
||||||
|
-- Row will contain: nb_toc_spans + page slots + spacing (+ some borders)
|
||||||
|
self.statistics_enabled = self.ui.statistics and self.ui.statistics:isEnabled()
|
||||||
|
local page_slots_height_ratio = 1 -- default to 1 * span_height
|
||||||
|
if not self.statistics_enabled and self.nb_toc_spans > 0 then
|
||||||
|
-- Just enough to show page separators below toc spans
|
||||||
|
page_slots_height_ratio = 0.2
|
||||||
|
end
|
||||||
|
self.row_height = math.ceil((self.nb_toc_spans + page_slots_height_ratio + 1) * self.span_height + 2*BookMapRow.pages_frame_border)
|
||||||
|
|
||||||
|
self.grid_width = self.dimen.w
|
||||||
|
self.grid_height = self.dimen.h - self.title_bar_h - self.row_height
|
||||||
|
|
||||||
|
-- We'll draw some kind of static transparent glass over the BookMapRow,
|
||||||
|
-- which should span over the page slots that get their thumbnails shown.
|
||||||
|
self.view_finder_r = Size.radius.window
|
||||||
|
self.view_finder_bw = Size.border.default
|
||||||
|
-- Have its top border noticable above the BookMapRow top border
|
||||||
|
self.view_finder_y = self.dimen.h - self.row_height - 2*self.view_finder_bw
|
||||||
|
-- And put its bottom rounded corner outside of screen
|
||||||
|
self.view_finder_h = self.row_height + 2*self.view_finder_bw + Size.radius.window
|
||||||
|
|
||||||
|
self.grid = OverlapGroup:new{
|
||||||
|
dimen = Geom:new{
|
||||||
|
w = self.grid_width,
|
||||||
|
h = self.grid_height,
|
||||||
|
},
|
||||||
|
allow_mirroring = false,
|
||||||
|
}
|
||||||
|
self.row = CenterContainer:new{
|
||||||
|
dimen = Geom:new{
|
||||||
|
w = self.dimen.w,
|
||||||
|
h = self.row_height,
|
||||||
|
},
|
||||||
|
-- Will contain a BookMapRow wider, with l/r borders outside screen
|
||||||
|
}
|
||||||
|
|
||||||
|
self[1] = FrameContainer:new{
|
||||||
|
width = self.dimen.w,
|
||||||
|
height = self.dimen.h,
|
||||||
|
padding = 0,
|
||||||
|
margin = 0,
|
||||||
|
bordersize = 0,
|
||||||
|
background = Blitbuffer.COLOR_WHITE,
|
||||||
|
VerticalGroup:new{
|
||||||
|
align = "center",
|
||||||
|
self.title_bar,
|
||||||
|
self.grid,
|
||||||
|
self.row,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
-- Get some info that shouldn't change across calls to update() and updateLayout()
|
||||||
|
self.nb_pages = self.ui.document:getPageCount()
|
||||||
|
self.cur_page = self.ui.toc.pageno
|
||||||
|
-- Get bookmarks and highlights from ReaderBookmark
|
||||||
|
self.bookmarked_pages = self.ui.bookmark:getBookmarkedPages()
|
||||||
|
-- Get read page from the statistics plugin if enabled
|
||||||
|
self.read_pages = self.ui.statistics and self.ui.statistics:getCurrentBookReadPages()
|
||||||
|
self.current_session_duration = self.ui.statistics and (os.time() - self.ui.statistics.start_current_period)
|
||||||
|
-- Hidden flows, for first page display, and to draw them gray
|
||||||
|
self.has_hidden_flows = self.ui.document:hasHiddenFlows()
|
||||||
|
if self.has_hidden_flows and #self.ui.document.flows > 0 then
|
||||||
|
self.hidden_flows = {}
|
||||||
|
-- Pick into credocument internal data to build a table
|
||||||
|
-- of {first_page_number, last_page_number) for each flow
|
||||||
|
for flow, tab in ipairs(self.ui.document.flows) do
|
||||||
|
table.insert(self.hidden_flows, { tab[1], tab[1]+tab[2]-1 })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Reference page numbers, for first row page display
|
||||||
|
self.page_labels = nil
|
||||||
|
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
|
||||||
|
self.page_labels = self.ui.document:getPageMap()
|
||||||
|
end
|
||||||
|
-- Location stack
|
||||||
|
self.previous_locations = self.ui.link:getPreviousLocationPages()
|
||||||
|
|
||||||
|
-- Compute settings-dependant sizes and options, and build the inner widgets
|
||||||
|
-- (this will call self:update())
|
||||||
|
self:updateLayout()
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:updateLayout()
|
||||||
|
self.nb_rows = self.ui.doc_settings:readSetting("page_browser_nb_rows")
|
||||||
|
or G_reader_settings:readSetting("page_browser_nb_rows")
|
||||||
|
self.nb_cols = self.ui.doc_settings:readSetting("page_browser_nb_cols")
|
||||||
|
or G_reader_settings:readSetting("page_browser_nb_cols")
|
||||||
|
if not self.nb_rows or not self.nb_cols then
|
||||||
|
-- 3 x 2 seems like a good default, in both portrait or landscape mode
|
||||||
|
self.nb_cols = 3
|
||||||
|
self.nb_rows = 2
|
||||||
|
end
|
||||||
|
self.nb_grid_items = self.nb_rows * self.nb_cols
|
||||||
|
-- Set our items target size
|
||||||
|
self.grid_item_margin = Screen:scaleBySize(10) -- borders will eat into this, it should be larger than borders thin+thick
|
||||||
|
self.grid_item_height = math.floor((self.grid_height - (self.nb_rows)*self.grid_item_margin) / self.nb_rows) -- no need for top margin, title bottom padding is enough
|
||||||
|
self.grid_item_width = math.floor((self.grid_width - (1+self.nb_cols)*self.grid_item_margin) / self.nb_cols)
|
||||||
|
self.grid_item_dimen = Geom:new{
|
||||||
|
w = self.grid_item_width,
|
||||||
|
h = self.grid_item_height
|
||||||
|
}
|
||||||
|
|
||||||
|
self.grid:clear()
|
||||||
|
|
||||||
|
for idx = 1, self.nb_grid_items do
|
||||||
|
local row = math.floor((idx-1)/self.nb_cols) -- start from 0
|
||||||
|
local col = (idx-1) % self.nb_cols
|
||||||
|
if self._mirroredUI then
|
||||||
|
col = self.nb_cols - col - 1
|
||||||
|
end
|
||||||
|
local offset_x = self.grid_item_margin*(col+1) + self.grid_item_width*col
|
||||||
|
local offset_y = self.grid_item_margin*(row) + self.grid_item_height*row -- no need for 1st margin
|
||||||
|
local grid_item = CenterContainer:new{
|
||||||
|
dimen = self.grid_item_dimen:copy(),
|
||||||
|
}
|
||||||
|
table.insert(self.grid, FrameContainer:new{
|
||||||
|
overlap_offset = {offset_x, offset_y},
|
||||||
|
margin = 0,
|
||||||
|
padding = 0,
|
||||||
|
bordersize = 0,
|
||||||
|
background = Blitbuffer.COLOR_WHITE,
|
||||||
|
grid_item,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Put the focused (requested) page at some appropriate place in the grid
|
||||||
|
if self.nb_rows > 1 then -- Multiple rows
|
||||||
|
-- Show the focus page at the rightmost position in the first row
|
||||||
|
self.focus_page_shift = self.nb_cols - 1
|
||||||
|
else -- Single row
|
||||||
|
if self.nb_cols > 2 then -- 3+ columns: show one page behind only
|
||||||
|
self.focus_page_shift = 1
|
||||||
|
else -- 1 or 2 columns: show it first
|
||||||
|
self.focus_page_shift = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Don't go with too small page slots
|
||||||
|
self.pages_per_row = math.max(self.nb_grid_items*3, 20)
|
||||||
|
-- We want our view finder centered over the BookMapRow
|
||||||
|
if self.pages_per_row % 2 ~= self.nb_grid_items % 2 then
|
||||||
|
self.pages_per_row = self.pages_per_row + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update the BookMapRow and page thumbnails for the current view
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:update()
|
||||||
|
if self.requests_batch_id then
|
||||||
|
self.ui.thumbnail:cancelPageThumbnailRequests(self.requests_batch_id)
|
||||||
|
end
|
||||||
|
self.requests_batch_id = "PageBrowserWidget"..tostring(os.time())
|
||||||
|
|
||||||
|
if not self.focus_page then
|
||||||
|
self.focus_page = self.cur_page or 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local grid_page_start = self.focus_page - self.focus_page_shift
|
||||||
|
local grid_page_end = grid_page_start + self.nb_grid_items - 1
|
||||||
|
|
||||||
|
-- Get p_start so that our viewfinder is centered
|
||||||
|
local p_start = math.ceil(grid_page_start + self.nb_grid_items/2 - self.pages_per_row/2)
|
||||||
|
local p_end = p_start + self.pages_per_row - 1
|
||||||
|
local blank_page_slots_before_start = 0
|
||||||
|
local blank_page_slots_after_end = 0 -- used only when _mirroredUI
|
||||||
|
if p_end > self.nb_pages then
|
||||||
|
blank_page_slots_after_end = p_end - self.nb_pages
|
||||||
|
p_end = self.nb_pages
|
||||||
|
end
|
||||||
|
if p_start < 1 then
|
||||||
|
blank_page_slots_before_start = 1 - p_start
|
||||||
|
p_start = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Show the page number or label at the bottom page slot every N slots, with N
|
||||||
|
-- the nb of thumbnails so we get at least one page label in our viewport.
|
||||||
|
local page_texts_cycle = math.min(self.nb_grid_items, 10) -- but max 10
|
||||||
|
local next_p = p_start
|
||||||
|
local cur_page_label_idx = 1
|
||||||
|
local page_texts = {}
|
||||||
|
for p=p_start, p_end do
|
||||||
|
if p >= next_p then
|
||||||
|
-- Only show a page text if there is no indicator on that slot
|
||||||
|
if p ~= self.cur_page and not self.bookmarked_pages[p] and not self.previous_locations[p] then
|
||||||
|
local page_text
|
||||||
|
if self.page_labels then
|
||||||
|
local page_label
|
||||||
|
for idx=cur_page_label_idx, #self.page_labels do
|
||||||
|
local item = self.page_labels[idx]
|
||||||
|
if item.page >= p then
|
||||||
|
if item.page == p then
|
||||||
|
page_label = item.label
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
cur_page_label_idx = idx
|
||||||
|
end
|
||||||
|
if page_label then
|
||||||
|
page_text = self.ui.pagemap:cleanPageLabel(page_label)
|
||||||
|
end
|
||||||
|
elseif self.has_hidden_flows then
|
||||||
|
local flow = self.ui.document:getPageFlow(p)
|
||||||
|
if flow == 0 then
|
||||||
|
page_text = tostring(self.ui.document:getPageNumberInFlow(p))
|
||||||
|
else
|
||||||
|
page_text = string.format("[%d]%d", self.ui.document:getPageNumberInFlow(p), self.ui.document:getPageFlow(p))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
page_text = tostring(p)
|
||||||
|
end
|
||||||
|
if page_text then
|
||||||
|
local page_block, page_block_dx -- centered by default
|
||||||
|
if p == p_start or p == grid_page_start or p == grid_page_end+1 then
|
||||||
|
page_block = "left"
|
||||||
|
page_block_dx = Size.padding.tiny
|
||||||
|
if p == grid_page_start then
|
||||||
|
page_block_dx = page_block_dx + self.view_finder_bw + 1
|
||||||
|
end
|
||||||
|
elseif p == p_end or p == grid_page_end or p == grid_page_start-1 then
|
||||||
|
page_block = "right"
|
||||||
|
page_block_dx = Size.padding.tiny
|
||||||
|
if p == grid_page_end then
|
||||||
|
page_block_dx = page_block_dx + self.view_finder_bw + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
page_texts[p] = {
|
||||||
|
text = page_text,
|
||||||
|
block = page_block,
|
||||||
|
block_dx = page_block_dx,
|
||||||
|
}
|
||||||
|
next_p = p + page_texts_cycle
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- We need to rebuilt the full set of toc spans that will be shown
|
||||||
|
-- Similar (but simplified) to what is done in BookMapWidget.
|
||||||
|
self.toc_depth = self.nb_toc_spans
|
||||||
|
local toc = self.ui.toc.toc
|
||||||
|
local cur_toc_items = {}
|
||||||
|
local row_toc_items = {}
|
||||||
|
local toc_idx = 1
|
||||||
|
while toc_idx <= #toc do
|
||||||
|
-- Find out the toc items that can be shown on this row
|
||||||
|
local item = toc[toc_idx]
|
||||||
|
if item.page > p_end then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
if item.depth <= self.toc_depth then -- ignore lower levels we won't show
|
||||||
|
-- An item at level N closes all previous items at level >= N
|
||||||
|
for lvl = item.depth, self.toc_depth do
|
||||||
|
local done_toc_item = cur_toc_items[lvl]
|
||||||
|
cur_toc_items[lvl] = nil
|
||||||
|
if done_toc_item then
|
||||||
|
done_toc_item.p_end = math.max(item.page - 1, done_toc_item.p_start)
|
||||||
|
if done_toc_item.p_end >= p_start then
|
||||||
|
-- Can go into row_toc_items[lvl]
|
||||||
|
if done_toc_item.p_start < p_start then
|
||||||
|
done_toc_item.p_start = p_start
|
||||||
|
done_toc_item.started_before = true -- no left margin
|
||||||
|
end
|
||||||
|
if not row_toc_items[lvl] then
|
||||||
|
row_toc_items[lvl] = {}
|
||||||
|
end
|
||||||
|
-- We're done with it, we can just move it
|
||||||
|
table.insert(row_toc_items[lvl], done_toc_item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
cur_toc_items[item.depth] = {
|
||||||
|
title = item.title,
|
||||||
|
p_start = item.page,
|
||||||
|
p_end = nil,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
toc_idx = toc_idx + 1
|
||||||
|
end
|
||||||
|
local is_last_row = p_end >= self.nb_pages
|
||||||
|
for lvl = 1, self.nb_toc_spans do -- (no-op/no-loop if flat_map)
|
||||||
|
local active_toc_item = cur_toc_items[lvl]
|
||||||
|
if active_toc_item then
|
||||||
|
if active_toc_item.p_start < p_start then
|
||||||
|
active_toc_item.p_start = p_start
|
||||||
|
active_toc_item.started_before = true -- no left margin
|
||||||
|
end
|
||||||
|
active_toc_item.p_end = p_end
|
||||||
|
active_toc_item.continues_after = not is_last_row -- no right margin (except if last row)
|
||||||
|
-- Look at next TOC item to see if it would close this one
|
||||||
|
local coming_up_toc_item = toc[toc_idx]
|
||||||
|
if coming_up_toc_item and coming_up_toc_item.page == p_end+1 and coming_up_toc_item.depth <= lvl then
|
||||||
|
active_toc_item.continues_after = false -- right margin
|
||||||
|
end
|
||||||
|
if not row_toc_items[lvl] then
|
||||||
|
row_toc_items[lvl] = {}
|
||||||
|
end
|
||||||
|
table.insert(row_toc_items[lvl], active_toc_item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local left_spacing = 0
|
||||||
|
if blank_page_slots_before_start > 0 then
|
||||||
|
left_spacing = BookMapRow:getLeftSpacingForNumberOfPageSlots(blank_page_slots_before_start, self.pages_per_row, self.row_width)
|
||||||
|
end
|
||||||
|
local row = BookMapRow:new{
|
||||||
|
height = self.row_height,
|
||||||
|
width = self.row_width,
|
||||||
|
show_parent = self,
|
||||||
|
left_spacing = left_spacing,
|
||||||
|
nb_toc_spans = self.nb_toc_spans,
|
||||||
|
span_height = self.span_height,
|
||||||
|
font_face = self.toc_span_face,
|
||||||
|
start_page_text = "",
|
||||||
|
start_page = p_start,
|
||||||
|
end_page = p_end,
|
||||||
|
pages_per_row = self.pages_per_row - blank_page_slots_before_start,
|
||||||
|
cur_page = self.cur_page,
|
||||||
|
with_page_sep = true,
|
||||||
|
toc_items = row_toc_items,
|
||||||
|
bookmarked_pages = self.bookmarked_pages,
|
||||||
|
previous_locations = self.previous_locations,
|
||||||
|
hidden_flows = self.hidden_flows,
|
||||||
|
read_pages = self.read_pages,
|
||||||
|
current_session_duration = self.current_session_duration,
|
||||||
|
page_texts = page_texts,
|
||||||
|
}
|
||||||
|
self.row[1] = row
|
||||||
|
|
||||||
|
if self._mirroredUI then
|
||||||
|
self.view_finder_x = row:getPageX(grid_page_end)
|
||||||
|
self.view_finder_w = row:getPageX(grid_page_start, true) - self.view_finder_x
|
||||||
|
if blank_page_slots_after_end > 0 then
|
||||||
|
self.view_finder_x = self.view_finder_x
|
||||||
|
+ BookMapRow:getLeftSpacingForNumberOfPageSlots(blank_page_slots_after_end, self.pages_per_row, self.row_width)
|
||||||
|
+ row.pages_frame_border -- (needed, but not sure why it is needed...)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.view_finder_x = row:getPageX(grid_page_start)
|
||||||
|
self.view_finder_w = row:getPageX(grid_page_end, true) - self.view_finder_x
|
||||||
|
self.view_finder_x = self.view_finder_x + left_spacing
|
||||||
|
end
|
||||||
|
-- we requested with_page_sep, so leave these blank spaces between page slots outside the viewfinder
|
||||||
|
self.view_finder_x = self.view_finder_x + 1
|
||||||
|
self.view_finder_w = self.view_finder_w - 1
|
||||||
|
|
||||||
|
for idx=1, self.nb_grid_items do
|
||||||
|
local p = grid_page_start + idx - 1
|
||||||
|
if p < 1 or p > self.nb_pages then
|
||||||
|
self.grid[idx].page_idx = nil -- no action on Tap
|
||||||
|
self:clearTile(idx)
|
||||||
|
else
|
||||||
|
self.grid[idx].page_idx = p -- go there on Tap
|
||||||
|
local delayed = self.ui.thumbnail:getPageThumbnail(p, self.grid_item_width, self.grid_item_height, self.requests_batch_id, function(tile, batch_id, async_response)
|
||||||
|
if batch_id ~= self.requests_batch_id then
|
||||||
|
-- Response from an obsolete request
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not tile then -- failure notification
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- If tile was in the cache, we get this immediately called with async_response=false,
|
||||||
|
-- and we don't need to do any setDirty as a full one will be done below.
|
||||||
|
self:showTile(idx, p, tile, async_response)
|
||||||
|
end)
|
||||||
|
if delayed then
|
||||||
|
self:clearTile(idx, true)
|
||||||
|
self.wait_for_refresh_on_show_tile = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
UIManager:setDirty(self, function()
|
||||||
|
return "ui", self.dimen
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:paintTo(bb, x, y)
|
||||||
|
-- Paint regular sub widgets the classic way
|
||||||
|
InputContainer.paintTo(self, bb, x, y)
|
||||||
|
-- If we would prefer to see the BookMapRow top border always take the full width
|
||||||
|
-- so it acts as a separator from the thumbnail grid, add this:
|
||||||
|
-- bb:paintRect(0, self.dimen.h - self.row_height, self.dimen.w, BookMapRow.pages_frame_border, Blitbuffer.COLOR_BLACK)
|
||||||
|
-- And explicitely paint our viewfinder over the BookMapRow
|
||||||
|
bb:paintBorder(self.view_finder_x, self.view_finder_y, self.view_finder_w, self.view_finder_h,
|
||||||
|
self.view_finder_bw, Blitbuffer.COLOR_BLACK, self.view_finder_r)
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:clearTile(grid_idx, in_progress, do_refresh)
|
||||||
|
local item_frame = self.grid[grid_idx] -- FrameContainer
|
||||||
|
local item_container = item_frame[1] -- CenterContainer
|
||||||
|
local dimen = item_frame.dimen
|
||||||
|
if item_container[1] then -- TextWidget or FrameContainer
|
||||||
|
if item_container[1].dimen then
|
||||||
|
dimen = item_container[1].dimen:copy()
|
||||||
|
end
|
||||||
|
if item_container[1].free then
|
||||||
|
item_container[1]:free()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Quickly showing the first tile while the whole page is still being refreshed
|
||||||
|
-- can cause some papercut-like refresh glitch on this first tile, with even more
|
||||||
|
-- chances if we put gray things in the initial page (as gray is painted black
|
||||||
|
-- and then becomes gray, making it 2 steps and longer).
|
||||||
|
-- This seems to be mitigated with our self.wait_for_refresh_on_show_tile trick.
|
||||||
|
if in_progress then
|
||||||
|
item_container[1] = TextWidget:new{
|
||||||
|
text = "♲", -- gray symbol (which initially caused refresh glitches)
|
||||||
|
-- Alternatives (mostly from Nerdfont):
|
||||||
|
-- text = "\u{26F6}", -- square with four corners
|
||||||
|
-- text = "\u{ED36}",
|
||||||
|
-- text = "\u{F196}", -- square with plus inside
|
||||||
|
-- text = "\u{ED5F}", -- square with plus at top right
|
||||||
|
-- text = "\u{F141}",
|
||||||
|
-- text = "\u{EB52}",
|
||||||
|
-- text = "\u{EB4F}",
|
||||||
|
-- text = "\u{F021}",
|
||||||
|
face = Font:getFace("cfont", 20),
|
||||||
|
}
|
||||||
|
else
|
||||||
|
item_container[1] = VerticalSpan:new{ width = 0, }
|
||||||
|
end
|
||||||
|
if do_refresh then
|
||||||
|
UIManager:setDirty(self, function()
|
||||||
|
return "ui", dimen
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:showTile(grid_idx, page, tile, do_refresh)
|
||||||
|
local item_frame = self.grid[grid_idx] -- FrameContainer
|
||||||
|
local item_container = item_frame[1] -- CenterContainer
|
||||||
|
if item_container[1] and item_container[1].free then -- TextWidget
|
||||||
|
item_container[1]:free()
|
||||||
|
end
|
||||||
|
local border = page == self.cur_page and Size.border.thick or Size.border.thin
|
||||||
|
local thumb_frame = FrameContainer:new{
|
||||||
|
is_page_thumbnail = true, -- for tap handler
|
||||||
|
margin = 0,
|
||||||
|
padding = 0,
|
||||||
|
bordersize = border,
|
||||||
|
background = Blitbuffer.COLOR_WHITE,
|
||||||
|
ImageWidget:new{
|
||||||
|
image = tile.bb,
|
||||||
|
image_disposable = false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
item_container[1] = thumb_frame
|
||||||
|
-- thumb_frame will overflow its CenterContainer because of the added borders,
|
||||||
|
-- but CenterContainer handles that well. We will refresh the outer dimensions.
|
||||||
|
|
||||||
|
if do_refresh then
|
||||||
|
if self.wait_for_refresh_on_show_tile then
|
||||||
|
self.wait_for_refresh_on_show_tile = nil
|
||||||
|
-- Be sure the main view initial refresh has ended before refreshing
|
||||||
|
-- this first thumbnail, to avoid papercut refresh glitches.
|
||||||
|
UIManager:waitForVSync()
|
||||||
|
end
|
||||||
|
UIManager:setDirty(self, function()
|
||||||
|
return "ui", thumb_frame.dimen
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:showHelp()
|
||||||
|
UIManager:show(InfoMessage:new{
|
||||||
|
text = [[
|
||||||
|
Page browser shows thumbnails of pages.
|
||||||
|
|
||||||
|
The bottom row displays an extract of the book map around the shown pages: see the book map help for details.
|
||||||
|
|
||||||
|
Swipe along the top or left screen edge to change the number of columns or rows.
|
||||||
|
Swipe vertically to move one row, horizontally to move one page.
|
||||||
|
Swipe horizontally in the bottom row to move by the full stripe.
|
||||||
|
Tap in the bottom row on a page to focus thumbnails on this page.
|
||||||
|
Tap on a thumbnail to go read this page.
|
||||||
|
Any multiswipe will close the page browser.]],
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:onClose(close_all_parents)
|
||||||
|
if self.requests_batch_id then
|
||||||
|
self.ui.thumbnail:cancelPageThumbnailRequests(self.requests_batch_id)
|
||||||
|
end
|
||||||
|
logger.dbg("closing PageBrowserWidget")
|
||||||
|
-- Close this widget
|
||||||
|
UIManager:close(self)
|
||||||
|
if self.launcher then
|
||||||
|
-- We were launched by a BookMapWidget, don't do any cleanup.
|
||||||
|
if close_all_parents then
|
||||||
|
-- The last one of these (which has no launcher attribute)
|
||||||
|
-- will do the cleanup below.
|
||||||
|
self.launcher:onClose(true)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Remove all thumbnails generated for a different target size than
|
||||||
|
-- the last one used (no need to keep old sizes if the user played
|
||||||
|
-- with nb_cols/nb_rows, as on next opening, we just need the ones
|
||||||
|
-- with the current size to be available)
|
||||||
|
self.ui.thumbnail:tidyCache()
|
||||||
|
-- Force a GC to free the memory used by the widgets and tiles
|
||||||
|
-- (delay it a bit so this pause is less noticable)
|
||||||
|
UIManager:scheduleIn(0.5, function()
|
||||||
|
collectgarbage()
|
||||||
|
collectgarbage()
|
||||||
|
end)
|
||||||
|
-- As we're getting back to Reader, do a full flashing refresh to remove
|
||||||
|
-- any ghost trace of thumbnails or black page slots
|
||||||
|
UIManager:setDirty(self.ui.dialog, "full")
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:saveSettings(reset)
|
||||||
|
if reset then
|
||||||
|
self.nb_rows = nil
|
||||||
|
self.nb_cols = nil
|
||||||
|
end
|
||||||
|
self.ui.doc_settings:saveSetting("page_browser_nb_rows", self.nb_rows)
|
||||||
|
self.ui.doc_settings:saveSetting("page_browser_nb_cols", self.nb_cols)
|
||||||
|
-- We also save them as global settings, so they will apply on other books
|
||||||
|
-- where they were not already set
|
||||||
|
G_reader_settings:saveSetting("page_browser_nb_rows", self.nb_rows)
|
||||||
|
G_reader_settings:saveSetting("page_browser_nb_cols", self.nb_cols)
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:updateNbCols(value, relative)
|
||||||
|
local new_nb_cols
|
||||||
|
if relative then
|
||||||
|
new_nb_cols = self.nb_cols + value
|
||||||
|
else
|
||||||
|
new_nb_cols = value
|
||||||
|
end
|
||||||
|
if new_nb_cols < self.min_nb_cols then
|
||||||
|
new_nb_cols = self.min_nb_cols
|
||||||
|
end
|
||||||
|
if new_nb_cols > self.max_nb_cols then
|
||||||
|
new_nb_cols = self.max_nb_cols
|
||||||
|
end
|
||||||
|
if new_nb_cols == self.nb_cols then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
self.nb_cols = new_nb_cols
|
||||||
|
self:saveSettings()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:updateNbRows(value, relative)
|
||||||
|
local new_nb_rows
|
||||||
|
if relative then
|
||||||
|
new_nb_rows = self.nb_rows + value
|
||||||
|
else
|
||||||
|
new_nb_rows = value
|
||||||
|
end
|
||||||
|
if new_nb_rows < self.min_nb_rows then
|
||||||
|
new_nb_rows = self.min_nb_rows
|
||||||
|
end
|
||||||
|
if new_nb_rows > self.max_nb_rows then
|
||||||
|
new_nb_rows = self.max_nb_rows
|
||||||
|
end
|
||||||
|
if new_nb_rows == self.nb_rows then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
self.nb_rows = new_nb_rows
|
||||||
|
self:saveSettings()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:updateFocusPage(value, relative)
|
||||||
|
local new_focus_page
|
||||||
|
if relative then
|
||||||
|
new_focus_page = self.focus_page + value
|
||||||
|
else
|
||||||
|
new_focus_page = value
|
||||||
|
end
|
||||||
|
if new_focus_page < 1 then
|
||||||
|
new_focus_page = 1
|
||||||
|
end
|
||||||
|
if new_focus_page > self.nb_pages then
|
||||||
|
new_focus_page = self.nb_pages
|
||||||
|
end
|
||||||
|
if new_focus_page == self.focus_page then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
self.focus_page = new_focus_page
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:onScrollPageUp()
|
||||||
|
if self:updateFocusPage(-self.nb_grid_items, true) then
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:onScrollPageDown()
|
||||||
|
if self:updateFocusPage(self.nb_grid_items, true) then
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:onScrollRowUp()
|
||||||
|
if self:updateFocusPage(-self.nb_cols, true) then
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:onScrollRowDown()
|
||||||
|
if self:updateFocusPage(self.nb_cols, true) then
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:onSwipe(arg, ges)
|
||||||
|
local direction = BD.flipDirectionIfMirroredUILayout(ges.direction)
|
||||||
|
|
||||||
|
if direction == "north" or direction == "south" then
|
||||||
|
-- Swipe along the screen left edge: increase/decrease nb of thumbnail rows
|
||||||
|
-- (Should this be mirrored if RTL UI? It would be consistent with how it
|
||||||
|
-- happens in BookMapWidget - but here, having it on the left is to have it
|
||||||
|
-- less accessible to right handed people so they can scroll up/down more
|
||||||
|
-- easily.)
|
||||||
|
if ges.pos.x < Screen:getWidth() * 1/8 then
|
||||||
|
local rel = direction == "north" and 1 or -1
|
||||||
|
if self:updateNbRows(rel, true) then
|
||||||
|
self:updateLayout()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
-- As onScrollRowUp/Down()
|
||||||
|
local rel = direction == "north" and 1 or -1
|
||||||
|
if self:updateFocusPage(rel*self.nb_cols, true) then
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
elseif direction == "west" or direction == "east" then
|
||||||
|
if ges.pos.y < Screen:getHeight() * 1/8 then
|
||||||
|
-- Swipe along the screen top edge: increase/decrease nb of thumbnail cols
|
||||||
|
local rel = direction == "west" and 1 or -1
|
||||||
|
if self:updateNbCols(rel, true) then
|
||||||
|
self:updateLayout()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
elseif ges.pos.y > Screen:getHeight() - self.row_height then
|
||||||
|
-- Inside BookMapRow at bottom: scroll by a full pages_per_row
|
||||||
|
-- (Handling pan and hold/pan/release when started on view finder
|
||||||
|
-- would be nice, as it might be an intuitive naive action on
|
||||||
|
-- this area... but well...)
|
||||||
|
local rel = direction == "west" and 1 or -1
|
||||||
|
if self:updateFocusPage(rel*self.pages_per_row, true) then
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
-- As onScrollPageUp/Down()
|
||||||
|
local rel = direction == "west" and 1 or -1
|
||||||
|
if self:updateFocusPage(rel*self.nb_grid_items, true) then
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
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
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:onPinch(arg, ges)
|
||||||
|
if ges.direction == "horizontal" then
|
||||||
|
if self:updateNbCols(1, true) then
|
||||||
|
self:updateLayout()
|
||||||
|
end
|
||||||
|
elseif ges.direction == "vertical" then
|
||||||
|
if self:updateNbRows(1, true) then
|
||||||
|
self:updateLayout()
|
||||||
|
end
|
||||||
|
elseif ges.direction == "diagonal" then
|
||||||
|
local updated = self:updateNbCols(1, true)
|
||||||
|
updated = self:updateNbRows(1, true) or updated
|
||||||
|
if updated then
|
||||||
|
self:updateLayout()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:onSpread(arg, ges)
|
||||||
|
if ges.direction == "horizontal" then
|
||||||
|
if self:updateNbCols(-1, true) then
|
||||||
|
self:updateLayout()
|
||||||
|
end
|
||||||
|
elseif ges.direction == "vertical" then
|
||||||
|
if self:updateNbRows(-1, true) then
|
||||||
|
self:updateLayout()
|
||||||
|
end
|
||||||
|
elseif ges.direction == "diagonal" then
|
||||||
|
local updated = self:updateNbCols(-1, true)
|
||||||
|
updated = self:updateNbRows(-1, true) or updated
|
||||||
|
if updated then
|
||||||
|
self:updateLayout()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:onMultiSwipe(arg, ges)
|
||||||
|
-- All swipes gestures are used for navigation.
|
||||||
|
-- Allow for quick closing with any multiswipe.
|
||||||
|
self:onClose()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:onTap(arg, ges)
|
||||||
|
-- If tap in the bottom BookMapRow, put page at tap position
|
||||||
|
-- as focus page, so it goes into our viewfinder
|
||||||
|
if ges.pos.y > Screen:getHeight() - self.row_height then
|
||||||
|
local page = self.row[1]:getPageAtX(ges.pos.x)
|
||||||
|
if page then
|
||||||
|
-- Have it in the middle of viewfinder, and not where
|
||||||
|
-- the self.focus_page_shift would put it
|
||||||
|
page = page - math.floor(self.nb_grid_items/2) + self.focus_page_shift
|
||||||
|
if self:updateFocusPage(page, false) then
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
-- Tap on title: do nothing
|
||||||
|
if ges.pos.y < self.title_bar_h then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
-- If tap on a thumbnail, close widget and go to that page
|
||||||
|
for idx=1, self.nb_grid_items do
|
||||||
|
if ges.pos:intersectWith(self.grid[idx].dimen) then
|
||||||
|
local page = self.grid[idx].page_idx
|
||||||
|
if page and self.grid[idx][1][1].is_page_thumbnail then
|
||||||
|
-- Only allow tap on fully displayed thumbnails.
|
||||||
|
-- Also, a thumbnail might be smaller than the original grid
|
||||||
|
-- item dimension. Be sure the tap is on it (otherwise, it's
|
||||||
|
-- a tap in the inter thumbnail margin, that we'd rather not
|
||||||
|
-- handle)
|
||||||
|
local thumb_frame = self.grid[idx][1][1]
|
||||||
|
if ges.pos:intersectWith(thumb_frame.dimen) then
|
||||||
|
-- On PDF documents, jumping to a page may block for a few
|
||||||
|
-- seconds while the page is rendered. So, make the border
|
||||||
|
-- bigger so the user knows his tap is being processed.
|
||||||
|
local orig_bordersize = thumb_frame.bordersize
|
||||||
|
thumb_frame.bordersize = Size.border.thick * 2
|
||||||
|
local b_inc = thumb_frame.bordersize - orig_bordersize
|
||||||
|
UIManager:widgetRepaint(thumb_frame, thumb_frame.dimen.x-b_inc, thumb_frame.dimen.y-b_inc)
|
||||||
|
Screen:refreshFast(thumb_frame.dimen.x, thumb_frame.dimen.y, thumb_frame.dimen.w, thumb_frame.dimen.h)
|
||||||
|
-- (refresh "fast" will make gray drawn black and may make the
|
||||||
|
-- thumbnail a little uglier - but this enhances the effect
|
||||||
|
-- of "being processed"!)
|
||||||
|
-- Close the BookMapWidget that launched this PageBrowser
|
||||||
|
-- and all their ancestors up to Reader
|
||||||
|
self:onClose(true)
|
||||||
|
self.ui.link:addCurrentLocationToStack()
|
||||||
|
self.ui:handleEvent(Event:new("GotoPage", page))
|
||||||
|
-- Note: with ReaderPaging, if we tap on the thumbnail for the current
|
||||||
|
-- page, nothing would be refreshed. Our :onClose(true) will have the
|
||||||
|
-- last ancestor issue a full refresh that will ensure it is painted.
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- If tap on a blank area, handle as prev/next page, so people
|
||||||
|
-- not friend with swipe can still move around
|
||||||
|
if BD.flipIfMirroredUILayout(ges.pos.x < Screen:getWidth()/2) then
|
||||||
|
self:onScrollPageUp()
|
||||||
|
else
|
||||||
|
self:onScrollPageDown()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function PageBrowserWidget:onHold(arg, ges)
|
||||||
|
-- If hold in the bottom BookMapRow, open a new BookMapWidget
|
||||||
|
-- and focus on this page. We'll show a rounded square below
|
||||||
|
-- our current focus_page to help locating where we were (it's
|
||||||
|
-- quite more complicated to draw a rounded rectangle around
|
||||||
|
-- multiple pages to figure our view finder, as these pages
|
||||||
|
-- may be splitted onto multiple BookMapRows...)
|
||||||
|
if ges.pos.y > Screen:getHeight() - self.row_height then
|
||||||
|
local page = self.row[1]:getPageAtX(ges.pos.x)
|
||||||
|
if page then
|
||||||
|
local extra_symbols_pages = {}
|
||||||
|
extra_symbols_pages[self.focus_page] = 0x25A2 -- white square with rounder corners
|
||||||
|
UIManager:show(BookMapWidget:new{
|
||||||
|
launcher = self,
|
||||||
|
ui = self.ui,
|
||||||
|
focus_page = page,
|
||||||
|
extra_symbols_pages = extra_symbols_pages,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return PageBrowserWidget
|
Loading…
Reference in New Issue