mirror of
https://github.com/koreader/koreader
synced 2024-11-13 19:11:25 +00:00
f58d1c0449
* Bump base https://github.com/koreader/koreader-base/pull/1547 https://github.com/koreader/koreader-base/pull/1548
1460 lines
61 KiB
Lua
1460 lines
61 KiB
Lua
local BD = require("ui/bidi")
|
|
local Blitbuffer = require("ffi/blitbuffer")
|
|
local CenterContainer = require("ui/widget/container/centercontainer")
|
|
local Device = require("device")
|
|
local Font = require("ui/font")
|
|
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 InfoMessage = require("ui/widget/infomessage")
|
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
|
local LeftContainer = require("ui/widget/container/leftcontainer")
|
|
local Menu = require("ui/widget/menu")
|
|
local OverlapGroup = require("ui/widget/overlapgroup")
|
|
local RenderText = require("ui/rendertext")
|
|
local ScrollableContainer = require("ui/widget/container/scrollablecontainer")
|
|
local Size = require("ui/size")
|
|
local TextBoxWidget = require("ui/widget/textboxwidget")
|
|
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 Widget = require("ui/widget/widget")
|
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
|
local Input = Device.input
|
|
local Screen = Device.screen
|
|
local logger = require("logger")
|
|
local util = require("util")
|
|
local _ = require("gettext")
|
|
|
|
-- BookMapRow (reused by PageBrowserWidget)
|
|
local BookMapRow = WidgetContainer:extend{
|
|
width = nil,
|
|
height = nil,
|
|
pages_frame_border = Size.border.default,
|
|
toc_span_border = Size.border.thin,
|
|
-- pages_frame_border = 10, -- for debugging positionning
|
|
-- toc_span_border = 5, -- for debugging positionning
|
|
toc_items = nil, -- Arrays[levels] of arrays[items at this level to show as spans]
|
|
-- Many other options not described here, see BookMapWidget:update()
|
|
-- for the complete list.
|
|
}
|
|
|
|
function BookMapRow:getPageX(page, right_edge)
|
|
local _mirroredUI = BD.mirroredUILayout()
|
|
if right_edge then
|
|
if (not _mirroredUI and page == self.end_page) or
|
|
(_mirroredUI and page == self.start_page) then
|
|
return self.pages_frame_inner_width
|
|
else
|
|
if _mirroredUI then
|
|
return self:getPageX(page-1)
|
|
else
|
|
return self:getPageX(page+1)
|
|
end
|
|
end
|
|
end
|
|
local slot_idx
|
|
if _mirroredUI then
|
|
slot_idx = self.end_page - page
|
|
else
|
|
slot_idx = page - self.start_page
|
|
end
|
|
local x = slot_idx * self.page_slot_width
|
|
x = x + math.floor(self.page_slot_extra * slot_idx / self.nb_page_slots)
|
|
return x
|
|
end
|
|
|
|
function BookMapRow:getPageAtX(x, at_bounds_if_outside)
|
|
x = x - self.pages_frame_offset_x
|
|
if x < 0 then
|
|
if not at_bounds_if_outside then return end
|
|
x = 0
|
|
end
|
|
if x >= self.pages_frame_inner_width then
|
|
if not at_bounds_if_outside then return end
|
|
x = self.pages_frame_inner_width - 1
|
|
end
|
|
-- Reverse of the computation in :getPageX():
|
|
local slot_idx = math.floor(x / (self.page_slot_width + self.page_slot_extra / self.nb_page_slots))
|
|
if BD.mirroredUILayout() then
|
|
return self.end_page - slot_idx
|
|
else
|
|
return self.start_page + slot_idx
|
|
end
|
|
end
|
|
|
|
-- Helper function to be used before instantiating a BookMapRow instance,
|
|
-- to obtain the left_spacing equivalent to not showing nb_pages at start
|
|
-- of a row of pages_per_row items in a width of row_width
|
|
function BookMapRow:getLeftSpacingForNumberOfPageSlots(nb_pages, pages_per_row, row_width)
|
|
-- Bits of the computation done in :init()
|
|
local pages_frame_inner_width = row_width - 2*self.pages_frame_border
|
|
local page_slot_width = math.floor(pages_frame_inner_width / pages_per_row)
|
|
local page_slot_extra = pages_frame_inner_width - page_slot_width * pages_per_row
|
|
-- Bits of the computation done in :getPageX()
|
|
local x = nb_pages * page_slot_width
|
|
x = x + math.floor(page_slot_extra * nb_pages / pages_per_row)
|
|
return x - self.pages_frame_border
|
|
end
|
|
|
|
function BookMapRow:init()
|
|
local _mirroredUI = BD.mirroredUILayout()
|
|
self.dimen = Geom:new{ x = 0, y = 0, w = self.width, h = self.height }
|
|
|
|
-- Keep one span_height under baseline (frame bottom border) for indicators (current page, bookmarks)
|
|
self.pages_frame_height = self.height - self.span_height
|
|
|
|
self.pages_frame_offset_x = self.left_spacing + self.pages_frame_border
|
|
self.pages_frame_width = self.width - self.left_spacing
|
|
self.pages_frame_inner_width = self.pages_frame_width - 2*self.pages_frame_border
|
|
self.page_slot_width = math.floor(self.pages_frame_inner_width / self.pages_per_row)
|
|
self.page_slot_extra = self.pages_frame_inner_width - self.page_slot_width * self.pages_per_row -- will be distributed
|
|
|
|
-- Update the widths if this row contains fewer pages
|
|
self.nb_page_slots = self.end_page - self.start_page + 1
|
|
if self.nb_page_slots ~= self.pages_per_row then
|
|
self.page_slot_extra = math.floor(self.page_slot_extra * self.nb_page_slots / self.pages_per_row)
|
|
self.pages_frame_inner_width = self.page_slot_width * self.nb_page_slots + self.page_slot_extra
|
|
self.pages_frame_width = self.pages_frame_inner_width + 2*self.pages_frame_border
|
|
end
|
|
|
|
if _mirroredUI then
|
|
self.pages_frame_offset_x = self.width - self.pages_frame_width - self.left_spacing + self.pages_frame_border
|
|
end
|
|
|
|
-- We draw a frame container with borders for the book content, with
|
|
-- some space on the left for the start page number, and some space
|
|
-- below the bottom border as spacing before the next row (spacing
|
|
-- that we can use to show current position and hanging bookmarks
|
|
-- and highlights symbols)
|
|
self.pages_frame = OverlapGroup:new{
|
|
dimen = Geom:new{
|
|
w = self.pages_frame_width,
|
|
h = self.pages_frame_height,
|
|
},
|
|
allow_mirroring = false, -- we handle mirroring ourselves below
|
|
FrameContainer:new{
|
|
overlap_align = _mirroredUI and "right" or "left",
|
|
margin = 0,
|
|
padding = 0,
|
|
bordersize = self.pages_frame_border,
|
|
-- color = Blitbuffer.COLOR_GRAY, -- for debugging positionning
|
|
Widget:new{ -- empty widget to give dimensions around which to draw borders
|
|
dimen = Geom:new{
|
|
w = self.pages_frame_inner_width,
|
|
h = self.pages_frame_height - 2*self.pages_frame_border,
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
-- We won't add this margin to the FrameContainer: to be able
|
|
-- to tweak it on some sides, we'll just tweak its overlap
|
|
-- offsets and width to ensure the margins
|
|
local tspan_margin = Size.margin.tiny
|
|
local tspan_padding_h = Size.padding.tiny
|
|
local tspan_height = self.span_height - 2 * (tspan_margin + self.toc_span_border)
|
|
if self.toc_items then
|
|
for lvl, items in pairs(self.toc_items) do
|
|
local offset_y = self.pages_frame_border + self.span_height * (lvl - 1) + tspan_margin
|
|
local prev_p_start, same_p_start_offset_dx
|
|
for __, item in ipairs(items) do
|
|
local text = item.title
|
|
local p_start, p_end = item.p_start, item.p_end
|
|
local started_before, continues_after = item.started_before, item.continues_after
|
|
if _mirroredUI then
|
|
-- Just flip these (beware below, we need to use item.p_start to get
|
|
-- the real start page to account in prev_p_start)
|
|
p_start, p_end = p_end, p_start
|
|
started_before, continues_after = continues_after, started_before
|
|
end
|
|
local offset_x = self:getPageX(p_start)
|
|
local width = self:getPageX(p_end, true) - offset_x
|
|
offset_x = offset_x + self.pages_frame_border
|
|
if prev_p_start == item.p_start then
|
|
-- Multiple TOC items starting on the same page slot:
|
|
-- shift and shorten 2nd++ ones so we see a bit of
|
|
-- the previous overwritten span and we can know this
|
|
-- page slot contains multiple chapters
|
|
if width > same_p_start_offset_dx then
|
|
if not _mirroredUI then
|
|
offset_x = offset_x + same_p_start_offset_dx
|
|
end
|
|
width = width - same_p_start_offset_dx
|
|
same_p_start_offset_dx = same_p_start_offset_dx + self.toc_span_border * 2
|
|
end
|
|
else
|
|
prev_p_start = item.p_start
|
|
same_p_start_offset_dx = self.toc_span_border * 2
|
|
end
|
|
if started_before then
|
|
-- No left margin, have span border overlap with outer border
|
|
offset_x = offset_x - self.toc_span_border
|
|
width = width + self.toc_span_border
|
|
else
|
|
-- Add some left margin
|
|
offset_x = offset_x + tspan_margin
|
|
width = width - tspan_margin
|
|
end
|
|
if continues_after then
|
|
-- No right margin, have span border overlap with outer border
|
|
width = width + self.toc_span_border
|
|
else
|
|
-- Add some right margin
|
|
width = width - tspan_margin
|
|
end
|
|
local text_max_width = width - 2 * (self.toc_span_border + tspan_padding_h)
|
|
local text_widget = nil
|
|
if text_max_width > 0 then
|
|
text_widget = TextWidget:new{
|
|
text = BD.auto(text),
|
|
max_width = text_max_width,
|
|
face = self.font_face,
|
|
padding = 0,
|
|
}
|
|
if text_widget:getWidth() > text_max_width then
|
|
-- May happen with very small max_width when smaller
|
|
-- than the truncation ellipsis
|
|
text_widget:free()
|
|
text_widget = nil
|
|
end
|
|
end
|
|
local span_w = FrameContainer:new{
|
|
overlap_offset = {offset_x, offset_y},
|
|
margin = 0,
|
|
padding = 0,
|
|
bordersize = self.toc_span_border,
|
|
background = Blitbuffer.COLOR_WHITE,
|
|
CenterContainer:new{
|
|
dimen = Geom:new{
|
|
w = width - 2 * self.toc_span_border,
|
|
h = tspan_height,
|
|
},
|
|
text_widget or VerticalSpan:new{ width = 0 },
|
|
}
|
|
}
|
|
table.insert(self.pages_frame, span_w)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- For page numbers:
|
|
self.smaller_font_face = Font:getFace(self.font_face.orig_font, self.font_face.orig_size - 4)
|
|
-- For current page triangle
|
|
self.larger_font_face = Font:getFace(self.font_face.orig_font, self.font_face.orig_size + 6)
|
|
|
|
self.hgroup = HorizontalGroup:new{
|
|
align = "top",
|
|
}
|
|
|
|
if self.left_spacing > 0 then
|
|
local spacing = Size.padding.small
|
|
table.insert(self.hgroup, TextBoxWidget:new{
|
|
text = self.start_page_text,
|
|
width = self.left_spacing - spacing,
|
|
face = self.smaller_font_face,
|
|
line_height = 0, -- no additional line height
|
|
alignment = _mirroredUI and "left" or "right",
|
|
alignment_strict = true,
|
|
})
|
|
table.insert(self.hgroup, HorizontalSpan:new{ width = spacing })
|
|
end
|
|
table.insert(self.hgroup, self.pages_frame)
|
|
|
|
-- Get hidden flows rectangle ready to be painted gray first as background
|
|
self.background_fillers = {}
|
|
if self.hidden_flows then
|
|
for _, flow_edges in ipairs(self.hidden_flows) do
|
|
local f_start, f_end = flow_edges[1], flow_edges[2]
|
|
if f_start <= self.end_page and f_end >= self.start_page then
|
|
local r_start = math.max(f_start, self.start_page)
|
|
local r_end = math.min(f_end, self.end_page)
|
|
local x, w
|
|
if _mirroredUI then
|
|
x = self:getPageX(r_end)
|
|
w = self:getPageX(r_start, true) - x
|
|
else
|
|
x = self:getPageX(r_start)
|
|
w = self:getPageX(r_end, true) - x
|
|
end
|
|
table.insert(self.background_fillers, {
|
|
x = x, y = 0,
|
|
w = w, h = self.pages_frame_height,
|
|
color = Blitbuffer.COLOR_LIGHT_GRAY,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
self[1] = LeftContainer:new{ -- needed only for auto UI mirroring
|
|
dimen = Geom:new{
|
|
w = self.dimen.w,
|
|
h = self.hgroup:getSize().h,
|
|
},
|
|
self.hgroup,
|
|
}
|
|
|
|
-- Get read pages markers and other indicators ready to be drawn
|
|
self.pages_markers = {}
|
|
self.indicators = {}
|
|
self.bottom_texts = {}
|
|
local prev_page_was_read = true -- avoid one at start of row
|
|
local unread_marker_h = math.ceil(self.span_height * 0.05)
|
|
local read_min_h = math.max(math.ceil(self.span_height * 0.1), unread_marker_h+Size.line.thick)
|
|
if self.page_slot_width >= 5 * unread_marker_h then
|
|
-- If page slots are large enough, we can make unread markers a bit taller (so they
|
|
-- are noticable and won't be confused with read page slots)
|
|
unread_marker_h = unread_marker_h * 2
|
|
end
|
|
for page = self.start_page, self.end_page do
|
|
if self.read_pages and self.read_pages[page] then
|
|
local x = self:getPageX(page)
|
|
local w = self:getPageX(page, true) - x
|
|
local h = math.ceil(self.read_pages[page][1] * self.span_height * 0.8)
|
|
h = math.max(h, read_min_h) -- so it's noticable
|
|
local y = self.pages_frame_height - self.pages_frame_border - h + 1
|
|
if self.with_page_sep then
|
|
-- We put the blank at the start of a page slot
|
|
x = x + 1
|
|
w = w - 1
|
|
if w > 2 then
|
|
if page == self.end_page and not _mirroredUI then
|
|
w = w - 1 -- some spacing before right border (like we had at start)
|
|
end
|
|
if page == self.start_page and _mirroredUI then
|
|
w = w - 1
|
|
end
|
|
end
|
|
end
|
|
local color = Blitbuffer.COLOR_BLACK
|
|
if self.current_session_duration and self.read_pages[page][2] < self.current_session_duration then
|
|
color = Blitbuffer.COLOR_GRAY_5
|
|
end
|
|
table.insert(self.pages_markers, {
|
|
x = x, y = y,
|
|
w = w, h = h,
|
|
color = color,
|
|
})
|
|
prev_page_was_read = true
|
|
else
|
|
if self.with_page_sep and not prev_page_was_read then
|
|
local w = Size.line.thin
|
|
local x
|
|
if _mirroredUI then
|
|
x = self:getPageX(page, true) - w
|
|
else
|
|
x = self:getPageX(page)
|
|
end
|
|
local y = self.pages_frame_height - self.pages_frame_border - unread_marker_h + 1
|
|
table.insert(self.pages_markers, {
|
|
x = x, y = y,
|
|
w = w, h = unread_marker_h,
|
|
color = Blitbuffer.COLOR_BLACK,
|
|
})
|
|
end
|
|
prev_page_was_read = false
|
|
end
|
|
-- Indicator for bookmark/highlight type, and current page
|
|
if self.bookmarked_pages[page] then
|
|
local page_bookmark_types = self.bookmarked_pages[page]
|
|
local x = self:getPageX(page)
|
|
local w = self:getPageX(page, true) - x
|
|
x = x + math.ceil(w/2)
|
|
local y = self.pages_frame_height + 1
|
|
-- These 3 icons overlap quite ok, so no need for any shift
|
|
if page_bookmark_types["highlight"] then
|
|
table.insert(self.indicators, {
|
|
x = x, y = y,
|
|
c = 0x2592, -- medium shade
|
|
})
|
|
end
|
|
if page_bookmark_types["note"] then
|
|
table.insert(self.indicators, {
|
|
x = x, y = y,
|
|
c = 0xF040, -- pencil
|
|
rotation = -90,
|
|
shift_x_pct = 0.2, -- 20% looks a bit better
|
|
-- This glyph is a pencil pointing to the bottom left,
|
|
-- so we make it point to the top left and align it so
|
|
-- it points to the page slot it is associated with
|
|
})
|
|
end
|
|
if page_bookmark_types["bookmark"] then
|
|
table.insert(self.indicators, {
|
|
x = x, y = y,
|
|
c = 0xF097, -- empty bookmark
|
|
})
|
|
end
|
|
end
|
|
-- Indicator for previous locations
|
|
if self.previous_locations[page] and page ~= self.cur_page then
|
|
local x = self:getPageX(page)
|
|
local w = self:getPageX(page, true) - x
|
|
x = x + math.ceil(w/2)
|
|
local y = self.pages_frame_height + 1
|
|
if self.bookmarked_pages[page] then
|
|
-- Shift it a bit down to keep bookmark glyph(s) readable
|
|
y = y + math.floor(self.span_height * (1/3))
|
|
end
|
|
local num = self.previous_locations[page]
|
|
table.insert(self.indicators, {
|
|
c = 0x2775 + (num < 10 and num or 10), -- number in solid black circle
|
|
-- c = 0x245F + (num < 20 and num or 20), -- number in white circle
|
|
x = x, y = y,
|
|
})
|
|
end
|
|
-- Extra indicator
|
|
if self.extra_symbols_pages and self.extra_symbols_pages[page] then
|
|
local x = self:getPageX(page)
|
|
local w = self:getPageX(page, true) - x
|
|
x = x + math.ceil(w/2)
|
|
local y = self.pages_frame_height + 1
|
|
if self.bookmarked_pages[page] then
|
|
-- Shift it a bit down to keep bookmark glyph(s) readable
|
|
y = y + math.floor(self.span_height * (1/3))
|
|
end
|
|
table.insert(self.indicators, {
|
|
c = self.extra_symbols_pages[page],
|
|
x = x, y = y,
|
|
})
|
|
end
|
|
-- Current page indicator
|
|
if page == self.cur_page then
|
|
local x = self:getPageX(page)
|
|
local w = self:getPageX(page, true) - x
|
|
x = x + math.ceil(w/2)
|
|
local y = self.pages_frame_height + 1
|
|
if self.bookmarked_pages[page] then
|
|
-- Shift it a bit down to keep bookmark glyph(s) readable
|
|
y = y + math.floor(self.span_height * (1/3))
|
|
end
|
|
table.insert(self.indicators, {
|
|
c = 0x25B2, -- black up-pointing triangle
|
|
x = x, y = y,
|
|
face = self.larger_font_face,
|
|
})
|
|
end
|
|
if self.page_texts and self.page_texts[page] then
|
|
-- These have been put on pages free from any other indicator, so
|
|
-- we can show the page number at the very bottom
|
|
local x = self:getPageX(page)
|
|
local w = self:getPageX(page, true) - x - Size.padding.tiny
|
|
table.insert(self.bottom_texts, {
|
|
text = self.page_texts[page].text,
|
|
x = x,
|
|
slot_width = w,
|
|
block = self.page_texts[page].block,
|
|
block_dx = self.page_texts[page].block_dx,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
function BookMapRow:paintTo(bb, x, y)
|
|
-- Paint background fillers (which are not subwidgets) first
|
|
for _, filler in ipairs(self.background_fillers) do
|
|
bb:paintRect(x + self.pages_frame_offset_x + filler.x, y + filler.y, filler.w, filler.h, filler.color)
|
|
end
|
|
-- Paint regular sub widgets the classic way
|
|
InputContainer.paintTo(self, bb, x, y)
|
|
-- And explicitely paint read pages markers (which are not subwidgets)
|
|
for _, marker in ipairs(self.pages_markers) do
|
|
bb:paintRect(x + self.pages_frame_offset_x + marker.x, y + marker.y, marker.w, marker.h, marker.color)
|
|
end
|
|
-- And explicitely paint indicators (which are not subwidgets)
|
|
for _, indicator in ipairs(self.indicators) do
|
|
local glyph = RenderText:getGlyph(indicator.face or self.font_face, indicator.c)
|
|
local alt_bb
|
|
if indicator.rotation then
|
|
alt_bb = glyph.bb:rotatedCopy(indicator.rotation)
|
|
end
|
|
-- Glyph's bb fit the blackbox of the glyph, so there's no cropping
|
|
-- or complicated positionning to do
|
|
-- By default, just center the glyph at x
|
|
local d_x_pct = indicator.shift_x_pct or 0.5
|
|
local d_x = math.floor(glyph.bb:getWidth() * d_x_pct)
|
|
bb:colorblitFrom(
|
|
alt_bb or glyph.bb,
|
|
x + self.pages_frame_offset_x + indicator.x - d_x,
|
|
y + indicator.y,
|
|
0, 0,
|
|
glyph.bb:getWidth(), glyph.bb:getHeight(),
|
|
Blitbuffer.COLOR_BLACK)
|
|
if alt_bb then
|
|
alt_bb:free()
|
|
end
|
|
end
|
|
-- And explicitely paint bottom texts (which are not subwidgets)
|
|
for _, btext in ipairs(self.bottom_texts) do
|
|
local text_w = TextWidget:new{
|
|
text = btext.text,
|
|
face = self.smaller_font_face,
|
|
padding = 0,
|
|
}
|
|
local d_y = self.height - math.ceil(text_w:getSize().h)
|
|
local d_x
|
|
local text_width = text_w:getWidth()
|
|
local d_width = btext.slot_width - text_width
|
|
if not btext.block then
|
|
-- no block constraint: can be centered
|
|
d_x = math.ceil(d_width / 2)
|
|
else
|
|
if d_width >= 2 * btext.block_dx then
|
|
-- small enough: can be centered
|
|
d_x = math.ceil(d_width / 2)
|
|
elseif btext.block == "left" then
|
|
d_x = btext.block_dx
|
|
else -- "right"
|
|
d_x = d_width - btext.block_dx
|
|
end
|
|
end
|
|
text_w:paintTo(bb, x + self.pages_frame_offset_x + btext.x + d_x, y + d_y)
|
|
text_w:free()
|
|
end
|
|
end
|
|
|
|
-- BookMapWidget: shows a map of content, including TOC, boomarks, read pages, non-linear flows...
|
|
local BookMapWidget = InputContainer:extend{
|
|
title = _("Book map"),
|
|
-- Focus page: show the BookMapRow containing this page
|
|
-- in the middle of screen
|
|
focus_page = nil,
|
|
-- Should only be nil on the first launch via ReaderThumbnail
|
|
launcher = nil,
|
|
-- Extra symbols to show below pages
|
|
extra_symbols_pages = nil,
|
|
|
|
-- Make this local subwidget available for reuse by PageBrowser
|
|
BookMapRow = BookMapRow,
|
|
}
|
|
|
|
function BookMapWidget:init()
|
|
if self.ui.view:shouldInvertBiDiLayoutMirroring() then
|
|
BD.invert()
|
|
end
|
|
|
|
-- Compute non-settings-dependant sizes and options
|
|
self.dimen = Geom:new{
|
|
x = 0,
|
|
y = 0,
|
|
w = Screen:getWidth(),
|
|
h = Screen:getHeight(),
|
|
}
|
|
self.covers_fullscreen = true -- hint for UIManager:_repaint()
|
|
|
|
if Device:hasKeys() then
|
|
self.key_events = {
|
|
Close = { { Input.group.Back } },
|
|
ScrollRowUp = { { "Up" } },
|
|
ScrollRowDown = { { "Down" } },
|
|
ScrollPageUp = { { Input.group.PgBack } },
|
|
ScrollPageDown = { { Input.group.PgFwd } },
|
|
}
|
|
end
|
|
if Device:isTouchDevice() then
|
|
self.ges_events = {
|
|
Swipe = {
|
|
GestureRange:new{
|
|
ges = "swipe",
|
|
range = self.dimen,
|
|
}
|
|
},
|
|
MultiSwipe = {
|
|
GestureRange:new{
|
|
ges = "multiswipe",
|
|
range = self.dimen,
|
|
}
|
|
},
|
|
Tap = {
|
|
GestureRange:new{
|
|
ges = "tap",
|
|
range = self.dimen,
|
|
}
|
|
},
|
|
Pinch = {
|
|
GestureRange:new{
|
|
ges = "pinch",
|
|
range = self.dimen,
|
|
}
|
|
},
|
|
Spread = {
|
|
GestureRange:new{
|
|
ges = "spread",
|
|
range = self.dimen,
|
|
}
|
|
},
|
|
}
|
|
-- No need for any long-press handler: page slots may be small and we can't
|
|
-- really target a precise page slot with our fat finger above it...
|
|
-- Tap will zoom the zone in a PageBrowserWidget where things will be clearer
|
|
-- and allow us to get where we want.
|
|
-- (Also, handling "hold" is a bit more complicated when we have our
|
|
-- ScrollableContainer that would also like to handle it.)
|
|
end
|
|
|
|
-- No real need for any explicite edge and inter-row padding:
|
|
-- we use the scrollbar width on both sides for balance (we may put a start
|
|
-- page number on the left space), and each BookMapRow will have itself some
|
|
-- blank space at bottom below page slots (where we may put hanging markers
|
|
-- for current page and bookmark/highlights)
|
|
self.scrollbar_width = ScrollableContainer:getScrollbarWidth()
|
|
self.row_width = self.dimen.w - self.scrollbar_width
|
|
self.row_left_spacing = self.scrollbar_width
|
|
self.swipe_hint_bar_width = Screen:scaleBySize(6)
|
|
|
|
self.title_bar = TitleBar:new{
|
|
fullscreen = true,
|
|
title = self.title,
|
|
left_icon = "info",
|
|
left_icon_tap_callback = function() self:showHelp() end,
|
|
left_icon_hold_callback = function()
|
|
self:toggleDefaultSettings() -- toggle between user settings and default view
|
|
end,
|
|
close_callback = function() self:onClose() end,
|
|
close_hold_callback = function() self:onClose(true) end,
|
|
show_parent = self,
|
|
}
|
|
self.title_bar_h = self.title_bar:getHeight()
|
|
self.crop_height = self.dimen.h - self.title_bar_h - Size.margin.small - self.swipe_hint_bar_width
|
|
|
|
-- 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()
|
|
|
|
-- Reference font size for flat TOC items, as set (or default) in ReaderToc
|
|
self.reader_toc_font_size = G_reader_settings:readSetting("toc_items_font_size")
|
|
or Menu.getItemFontSize(G_reader_settings:readSetting("toc_items_per_page") or self.ui.toc.toc_items_per_page_default)
|
|
|
|
-- Our container of stacked BookMapRows (and TOC titles in flat map mode)
|
|
self.vgroup = VerticalGroup:new{
|
|
align = "left",
|
|
}
|
|
-- We'll handle all events in this main BookMapWidget: none of the vgroup
|
|
-- children have any handler. Hack into vgroup so it doesn't propagate
|
|
-- events needlessly to its children (the slowness gets noticable when
|
|
-- we have many TOC items in flat map mode - the also needless :paintTo()
|
|
-- don't seen to cause such a noticable slowness)
|
|
self.vgroup.propagateEvent = function() return false end
|
|
|
|
-- Our scrollable container needs to be known as widget.cropping_widget in
|
|
-- the widget that is passed to UIManager:show() for UIManager to ensure
|
|
-- proper interception of inner widget self repainting/invert (mostly used
|
|
-- when flashing for UI feedback that we want to limit to the cropped area).
|
|
self.cropping_widget = ScrollableContainer:new{
|
|
dimen = Geom:new{
|
|
w = self.dimen.w,
|
|
h = self.crop_height,
|
|
},
|
|
show_parent = self,
|
|
ignore_events = {"swipe"},
|
|
self.vgroup,
|
|
}
|
|
|
|
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.cropping_widget,
|
|
}
|
|
}
|
|
|
|
-- Note: some of these could be cached in ReaderThumbnail, and discarded/updated
|
|
-- on some events (ie. TocUpdated, PageUpdate, AddHhighlight...)
|
|
-- Get some info that shouldn't change across calls to update()
|
|
self.nb_pages = self.ui.document:getPageCount()
|
|
self.ui.toc:fillToc()
|
|
self.cur_page = self.ui.toc.pageno
|
|
self.max_toc_depth = self.ui.toc.toc_depth
|
|
-- Get bookmarks and highlights from ReaderBookmark
|
|
self.bookmarked_pages = self.ui.bookmark:getBookmarkedPages()
|
|
-- Get read page from the statistics plugin if enabled
|
|
self.statistics_enabled = self.ui.statistics and self.ui.statistics:isEnabled()
|
|
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
|
|
self:update()
|
|
end
|
|
|
|
function BookMapWidget:update()
|
|
if not self.focus_page then -- Initial display
|
|
-- Focus (show at the middle of screen) on the BookMapRow that contains
|
|
-- current page
|
|
self.focus_page = self.cur_page
|
|
else
|
|
-- We have a previous focus page: if we have not scrolled around, keep
|
|
-- focusing on this one. Otherwise, use the start_page of the BookMapRow
|
|
-- at the middle of screen as the new focus page.
|
|
if self.initial_scroll_offset_y ~= self.cropping_widget._scroll_offset_y then
|
|
local h = math.min(self.vgroup:getSize().h, self.crop_height)
|
|
local row = self:getBookMapRowNearY(h/2)
|
|
if row then
|
|
self.focus_page = row.start_page
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Reset main widgets
|
|
self.vgroup:clear()
|
|
self.cropping_widget:reset()
|
|
|
|
-- Flat book map has each TOC item on a new line, and pages graph underneath.
|
|
-- Non-flat book map shows a grid with TOC items following each others.
|
|
self.flat_map = self.ui.doc_settings:readSetting("book_map_flat", false)
|
|
self.toc_depth = self.ui.doc_settings:readSetting("book_map_toc_depth", self.max_toc_depth)
|
|
if self.flat_map then
|
|
self.nb_toc_spans = 0 -- no span shown in grid
|
|
else
|
|
self.nb_toc_spans = self.toc_depth
|
|
end
|
|
|
|
self.flat_toc_depth_faces = nil
|
|
if self.flat_map then
|
|
self.flat_toc_depth_faces = {}
|
|
-- Use ReaderToc setting font size for items at the lowest depth
|
|
self.flat_toc_depth_faces[self.toc_depth] = Font:getFace(self.toc_span_font_name, self.reader_toc_font_size)
|
|
for lvl=self.toc_depth-1, 1, -1 do
|
|
-- But increase font size for each upper level
|
|
local inc = 2 * (self.toc_depth - lvl)
|
|
self.flat_toc_depth_faces[lvl] = Font:getFace(self.toc_span_font_name, self.reader_toc_font_size + inc)
|
|
end
|
|
-- Use 1.5em with the reference font size for indenting chapters and their BookMapRow
|
|
self.flat_toc_level_indent = Screen:scaleBySize(self.reader_toc_font_size * 1.5)
|
|
end
|
|
|
|
-- Row will contain: nb_toc_spans + page slots + spacing (+ some borders)
|
|
local page_slots_height_ratio = 1 -- default to 1 * span_height
|
|
if not self.statistics_enabled then
|
|
-- If statistics are disabled, we won't show black page slots for read pages.
|
|
-- We can gain a bit of height by reducing the height reserved for these
|
|
-- (don't go too low: we need some height to show the page number on the left).
|
|
if self.flat_map or self.nb_toc_spans == 0 then
|
|
-- Enough to show 4 digits page numbers
|
|
page_slots_height_ratio = 0.7
|
|
elseif self.nb_toc_spans > 0 then
|
|
-- Just enough to show page separators below toc spans
|
|
page_slots_height_ratio = 0.2
|
|
end
|
|
end
|
|
self.row_height = math.ceil((self.nb_toc_spans + page_slots_height_ratio + 1) * self.span_height + 2*BookMapRow.pages_frame_border)
|
|
|
|
if self.flat_map then
|
|
-- Max pages per row, when each page slots takes 1px
|
|
self.max_pages_per_row = math.floor(self.row_width - self.row_left_spacing - 2*Size.border.default
|
|
- Size.span.horizontal_default*self.toc_depth)
|
|
-- Find out the length of the largest chapter that we may show
|
|
local len
|
|
local max_len = 0
|
|
local p = 1
|
|
for _, item in ipairs(self.ui.toc.toc) do
|
|
if item.depth <= self.toc_depth then
|
|
len = item.page - p + 1
|
|
if len > max_len then max_len = len end
|
|
p = item.page
|
|
end
|
|
end
|
|
len = self.nb_pages - p + 1 -- last chapter
|
|
if len > max_len then max_len = len end
|
|
self.fit_pages_per_row = max_len
|
|
else
|
|
-- Max pages per row, when each page slots takes 1px
|
|
self.max_pages_per_row = math.floor(self.row_width - self.row_left_spacing - 2*Size.border.default)
|
|
-- What can fit without scrollbar
|
|
local fit_nb_rows = math.floor(self.crop_height / self.row_height)
|
|
self.fit_pages_per_row = math.ceil(self.nb_pages / fit_nb_rows)
|
|
end
|
|
self.min_pages_per_row = 10
|
|
-- If page slots are at least 4 pixels wide, we can steal one to act as a 1px blank separator
|
|
self.max_pages_per_row_with_sep = math.floor(self.max_pages_per_row / 4)
|
|
if self.fit_pages_per_row < self.min_pages_per_row then
|
|
self.fit_pages_per_row = self.min_pages_per_row
|
|
end
|
|
if self.fit_pages_per_row > self.max_pages_per_row then
|
|
self.fit_pages_per_row = self.max_pages_per_row
|
|
end
|
|
|
|
-- Show the whole book without scrollbar initially
|
|
self.pages_per_row = self.ui.doc_settings:readSetting("book_map_pages_per_row", self.fit_pages_per_row)
|
|
self.page_slot_width = nil -- will be fetched from the first BookMapRow
|
|
|
|
-- Build BookMapRows as we walk the ToC
|
|
local toc = self.ui.toc.toc
|
|
local toc_idx = 1
|
|
local cur_toc_items = {}
|
|
local p_start = 1
|
|
local cur_left_spacing = self.row_left_spacing -- updated when flat_map with previous TOC item indentation
|
|
local cur_page_label_idx = 1
|
|
while true do
|
|
local p_max = p_start + self.pages_per_row - 1 -- max page number in this row
|
|
local p_end = math.min(p_max, self.nb_pages) -- last book page in this row
|
|
-- Find out the toc items that can be shown on this row
|
|
local row_toc_items = {}
|
|
while toc_idx <= #toc do
|
|
local item = toc[toc_idx]
|
|
if item.page > p_max then
|
|
-- This TOC item will close previous items and start on the next row
|
|
break
|
|
end
|
|
if item.depth <= self.toc_depth then -- ignore lower levels we won't show
|
|
if self.flat_map then
|
|
if item.page == p_start then
|
|
cur_left_spacing = self.row_left_spacing + self.flat_toc_level_indent * (item.depth-1)
|
|
local txt_max_width = self.row_width - cur_left_spacing
|
|
table.insert(self.vgroup, HorizontalGroup:new{
|
|
HorizontalSpan:new{
|
|
width = cur_left_spacing,
|
|
},
|
|
TextBoxWidget:new{
|
|
text = self.ui.toc:cleanUpTocTitle(item.title, true),
|
|
width = txt_max_width,
|
|
face = self.flat_toc_depth_faces[item.depth],
|
|
},
|
|
-- Store this TOC item page, so we can tap on it to launch PageBrowser on its page
|
|
toc_item_page = item.page,
|
|
})
|
|
-- Add a bit more spacing for the BookMapRow(s) underneath this Toc item title
|
|
-- (so the page number painted in this spacing feels included in the indentation)
|
|
cur_left_spacing = cur_left_spacing + Size.span.horizontal_default
|
|
-- Note: this variable indentation may make the page slot widths variable across
|
|
-- rows from different levels (and self.fit_pages_per_row not really accurate) :/
|
|
-- Hopefully, it won't be noticable.
|
|
else
|
|
p_max = item.page - 1
|
|
p_end = p_max
|
|
-- Will be reprocessed on a new row
|
|
break
|
|
end
|
|
else
|
|
-- 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
|
|
end
|
|
toc_idx = toc_idx + 1
|
|
end
|
|
local is_last_row = p_end == self.nb_pages
|
|
-- We may have current toc_items that are active and may continue on next row
|
|
-- Add a slightly adjusted copy of the current ones to row_toc_items
|
|
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
|
|
local copied_toc_item = {}
|
|
for k,v in next, active_toc_item, nil do copied_toc_item[k] = v end
|
|
if copied_toc_item.p_start < p_start then
|
|
copied_toc_item.p_start = p_start
|
|
copied_toc_item.started_before = true -- no left margin
|
|
end
|
|
copied_toc_item.p_end = p_end
|
|
copied_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_max+1 and coming_up_toc_item.depth <= lvl then
|
|
copied_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], copied_toc_item)
|
|
end
|
|
end
|
|
|
|
-- Get the page number to display at start of row
|
|
local start_page_text
|
|
if self.page_labels then
|
|
local label
|
|
for idx=cur_page_label_idx, #self.page_labels do
|
|
local item = self.page_labels[idx]
|
|
if item.page > p_start then
|
|
break
|
|
end
|
|
label = item.label
|
|
cur_page_label_idx = idx
|
|
end
|
|
if label then
|
|
start_page_text = self.ui.pagemap:cleanPageLabel(label)
|
|
end
|
|
elseif self.has_hidden_flows then
|
|
local flow = self.ui.document:getPageFlow(p_start)
|
|
if flow == 0 then
|
|
start_page_text = tostring(self.ui.document:getPageNumberInFlow(p_start))
|
|
else
|
|
-- start_page_text = string.format("[%d]%d", self.ui.document:getPageNumberInFlow(p_start), self.ui.document:getPageFlow(p_start))
|
|
-- start_page_text = string.format("/%d\\", self.ui.document:getPageFlow(p_start))
|
|
-- Just don't display anything
|
|
start_page_text = nil
|
|
end
|
|
else
|
|
start_page_text = tostring(p_start)
|
|
end
|
|
if start_page_text then
|
|
start_page_text = table.concat(util.splitToChars(start_page_text), "\n")
|
|
else
|
|
start_page_text = ""
|
|
end
|
|
|
|
local row = BookMapRow:new{
|
|
height = self.row_height,
|
|
width = self.row_width,
|
|
show_parent = self,
|
|
left_spacing = cur_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_text,
|
|
start_page = p_start,
|
|
end_page = p_end,
|
|
pages_per_row = self.pages_per_row,
|
|
cur_page = self.cur_page,
|
|
with_page_sep = self.pages_per_row < self.max_pages_per_row_with_sep,
|
|
toc_items = row_toc_items,
|
|
bookmarked_pages = self.bookmarked_pages,
|
|
previous_locations = self.previous_locations,
|
|
extra_symbols_pages = self.extra_symbols_pages,
|
|
hidden_flows = self.hidden_flows,
|
|
read_pages = self.read_pages,
|
|
current_session_duration = self.current_session_duration,
|
|
}
|
|
table.insert(self.vgroup, row)
|
|
if not self.page_slot_width then
|
|
self.page_slot_width = row.page_slot_width
|
|
end
|
|
if is_last_row then
|
|
break
|
|
end
|
|
p_start = p_max + 1
|
|
end
|
|
|
|
-- Have main VerticalGroup size and subwidgets' offsets computed
|
|
self.vgroup:getSize()
|
|
|
|
-- Scroll so we get the focus page at the middle of screen
|
|
local row, row_idx, row_y, row_h = self:getMatchingVGroupRow(function(r, r_y, r_h) -- luacheck: no unused
|
|
return r.start_page and self.focus_page >= r.start_page and self.focus_page <= r.end_page
|
|
end)
|
|
if row_y then
|
|
local top_y = row_y + row_h/2 - self.crop_height/2
|
|
-- Align it so that we don't see any truncated BookMapRow at top
|
|
row, row_idx, row_y, row_h = self:getMatchingVGroupRow(function(r, r_y, r_h)
|
|
return r_y < top_y and r_y + r_h > top_y
|
|
end)
|
|
if row then
|
|
if top_y - row_y > row_y + row_h - top_y then
|
|
-- Less adjustment if we scroll to align the next row
|
|
top_y = row_y + row_h
|
|
else
|
|
top_y = row_y
|
|
end
|
|
end
|
|
if top_y > 0 then
|
|
self.cropping_widget:initState() -- anticipate this (otherwise delayed and done at :paintTo() time)
|
|
if self.cropping_widget._is_scrollable then
|
|
self.cropping_widget:_scrollBy(0, top_y)
|
|
end
|
|
end
|
|
end
|
|
self.initial_scroll_offset_y = self.cropping_widget._scroll_offset_y
|
|
|
|
UIManager:setDirty(self, function()
|
|
return "ui", self.dimen
|
|
end)
|
|
end
|
|
|
|
|
|
function BookMapWidget:showHelp()
|
|
UIManager:show(InfoMessage:new{
|
|
text = _([[
|
|
Book map displays an overview of the book content.
|
|
|
|
If statistics are enabled, black bars are shown for already read pages (gray for pages read in the current reading session). Their heights vary with the time spent reading the page.
|
|
Chapters are shown above their pages.
|
|
Under the pages can be found some indicators:
|
|
▲ current page
|
|
❶ ❷ … previous locations
|
|
▒ highlighted text
|
|
highlighted text with notes
|
|
bookmarked page
|
|
▢ focused page when coming from Pages browser
|
|
|
|
Tap on a location in the book to browse thumbnails of the pages there.
|
|
Swipe along the left screen edge to change the level of chapters to include in the book map, and the type of book map (grid or flat) when crossing the level 0.
|
|
Swipe along the bottom screen edge to change the width of page slots.
|
|
Swipe or pan vertically on content to scroll.
|
|
Any multiswipe will close the book map.
|
|
|
|
On a newly opened book, the book map will start in grid mode showing all chapter levels, fitting on a single screen, to give the best initial overview of the book's content.
|
|
Long-press on ⓘ to switch between current and initial views.]]),
|
|
})
|
|
end
|
|
|
|
function BookMapWidget:onClose(close_all_parents)
|
|
-- Close this widget
|
|
logger.dbg("closing BookMapWidget")
|
|
UIManager:close(self)
|
|
if self.launcher then
|
|
-- We were launched by a PageBrowserWidget, 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)
|
|
else
|
|
UIManager:setDirty(self.launcher, "ui")
|
|
end
|
|
else
|
|
BD.resetInvert()
|
|
-- 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 BookMapWidget:getMatchingVGroupRow(check_func)
|
|
-- Generic Vertical subwidget search function.
|
|
-- We use some of VerticalGroup's internal data, no need
|
|
-- to keep public copies of these data in here
|
|
for i=1, #self.vgroup do
|
|
local row = self.vgroup[i]
|
|
local y = self.vgroup._offsets[i].y
|
|
local h = (i < #self.vgroup and self.vgroup._offsets[i+1].y or self.vgroup._size.h) - y
|
|
if check_func(row, y, h) then
|
|
return row, i, y, h
|
|
end
|
|
end
|
|
end
|
|
|
|
function BookMapWidget:getVGroupRowAtY(y)
|
|
-- y is expected relative to the ScrollableContainer crop top
|
|
-- (if y is from a screen coordinate, substract 'self.title_bar_h' before calling this)
|
|
y = y + self.cropping_widget._scroll_offset_y
|
|
return self:getMatchingVGroupRow(function(r, r_y, r_h)
|
|
return y >= r_y and y < r_y + r_h
|
|
end)
|
|
end
|
|
|
|
function BookMapWidget:getBookMapRowNearY(y)
|
|
-- y is expected relative to the ScrollableContainer crop top
|
|
-- (if y is from a screen coordinate, substract 'self.title_bar_h' before calling this)
|
|
y = y + self.cropping_widget._scroll_offset_y
|
|
-- Return the BookMapRow at y, or if the vgroup element is a ToC
|
|
-- title (in flat_map mode), return the follow up BookMapRow
|
|
return self:getMatchingVGroupRow(function(r, r_y, r_h)
|
|
return y < r_y + r_h and r.start_page
|
|
end)
|
|
end
|
|
|
|
function BookMapWidget:onScrollPageUp()
|
|
-- Show previous content, ensuring any truncated widget at top is now full at bottom
|
|
local scroll_offset_y = self.cropping_widget._scroll_offset_y
|
|
local row, row_idx, row_y, row_h = self:getVGroupRowAtY(-1) -- luacheck: no unused
|
|
local to_keep = 0
|
|
if row then
|
|
to_keep = row_h - (scroll_offset_y - row_y)
|
|
end
|
|
self.cropping_widget:_scrollBy(0, -(self.crop_height - to_keep))
|
|
return true
|
|
end
|
|
|
|
function BookMapWidget:onScrollPageDown()
|
|
-- Show next content, ensuring any truncated widget at bottom is now full at top
|
|
local scroll_offset_y = self.cropping_widget._scroll_offset_y
|
|
local row, row_idx, row_y, row_h = self:getVGroupRowAtY(self.crop_height) -- luacheck: no unused
|
|
if row then
|
|
self.cropping_widget:_scrollBy(0, row_y - scroll_offset_y)
|
|
else
|
|
self.cropping_widget:_scrollBy(0, self.crop_height)
|
|
end
|
|
return true
|
|
end
|
|
|
|
function BookMapWidget:onScrollRowUp()
|
|
local scroll_offset_y = self.cropping_widget._scroll_offset_y
|
|
local row, row_idx, row_y, row_h = self:getVGroupRowAtY(-1) -- luacheck: no unused
|
|
if row then
|
|
self.cropping_widget:_scrollBy(0, row_y - scroll_offset_y)
|
|
end
|
|
return true
|
|
end
|
|
|
|
function BookMapWidget:onScrollRowDown()
|
|
local scroll_offset_y = self.cropping_widget._scroll_offset_y
|
|
local row, row_idx, row_y, row_h = self:getVGroupRowAtY(0) -- luacheck: no unused
|
|
if row then
|
|
self.cropping_widget:_scrollBy(0, row_y + row_h - scroll_offset_y)
|
|
end
|
|
return true
|
|
end
|
|
|
|
function BookMapWidget:saveSettings(reset)
|
|
if reset then
|
|
self.flat_map = nil
|
|
self.toc_depth = nil
|
|
self.pages_per_row = nil
|
|
end
|
|
self.ui.doc_settings:saveSetting("book_map_flat", self.flat_map)
|
|
self.ui.doc_settings:saveSetting("book_map_toc_depth", self.toc_depth)
|
|
self.ui.doc_settings:saveSetting("book_map_pages_per_row", self.pages_per_row)
|
|
end
|
|
|
|
function BookMapWidget:toggleDefaultSettings()
|
|
if not self.flat_map and self.toc_depth == self.max_toc_depth
|
|
and self.pages_per_row == self.fit_pages_per_row then
|
|
-- Still in default/initial view: restore previous settings (if any)
|
|
self.flat_map = self.ui.doc_settings:readSetting("book_map_previous_flat")
|
|
self.toc_depth = self.ui.doc_settings:readSetting("book_map_previous_toc_depth")
|
|
self.pages_per_row = self.ui.doc_settings:readSetting("book_map_previous_pages_per_row")
|
|
self:saveSettings()
|
|
else
|
|
-- Save previous settings and switch to defaults
|
|
self.ui.doc_settings:saveSetting("book_map_previous_flat", self.flat_map)
|
|
self.ui.doc_settings:saveSetting("book_map_previous_toc_depth", self.toc_depth)
|
|
self.ui.doc_settings:saveSetting("book_map_previous_pages_per_row", self.pages_per_row)
|
|
self:saveSettings(true)
|
|
end
|
|
self:update()
|
|
end
|
|
|
|
function BookMapWidget:updateTocDepth(depth, flat)
|
|
-- if flat == nil, consider value relative, and allow toggling
|
|
-- flatness when crossing 0
|
|
local new_toc_depth = self.toc_depth
|
|
local new_flat_map = self.flat_map
|
|
if flat == nil then
|
|
if self.flat_map then
|
|
-- Reverse increment if flat_map
|
|
new_toc_depth = new_toc_depth - depth
|
|
else
|
|
new_toc_depth = new_toc_depth + depth
|
|
end
|
|
if new_toc_depth < 0 then
|
|
new_toc_depth = - new_toc_depth
|
|
new_flat_map = not new_flat_map
|
|
end
|
|
else
|
|
new_toc_depth = depth
|
|
new_flat_map = flat
|
|
end
|
|
if new_toc_depth < 0 then
|
|
new_toc_depth = 0
|
|
end
|
|
if new_toc_depth > self.max_toc_depth then
|
|
new_toc_depth = self.max_toc_depth
|
|
end
|
|
if new_toc_depth == self.toc_depth and new_flat_map == self.flat_map then
|
|
return false
|
|
end
|
|
self.toc_depth = new_toc_depth
|
|
self.flat_map = new_flat_map
|
|
self:saveSettings()
|
|
return true
|
|
end
|
|
|
|
function BookMapWidget:updatePagesPerRow(value, relative)
|
|
local new_pages_per_row
|
|
if relative then
|
|
new_pages_per_row = self.pages_per_row + value
|
|
else
|
|
new_pages_per_row = value
|
|
end
|
|
if new_pages_per_row < self.min_pages_per_row then
|
|
new_pages_per_row = self.min_pages_per_row
|
|
end
|
|
if new_pages_per_row > self.max_pages_per_row then
|
|
new_pages_per_row = self.max_pages_per_row
|
|
end
|
|
if new_pages_per_row == self.pages_per_row then
|
|
return false
|
|
end
|
|
self.pages_per_row = new_pages_per_row
|
|
self:saveSettings()
|
|
return true
|
|
end
|
|
|
|
function BookMapWidget:onSwipe(arg, ges)
|
|
local _mirroredUI = BD.mirroredUILayout()
|
|
local direction = BD.flipDirectionIfMirroredUILayout(ges.direction)
|
|
if (not _mirroredUI and ges.pos.x < Screen:getWidth() * 1/8) or
|
|
(_mirroredUI and ges.pos.x > Screen:getWidth() * 7/8) then
|
|
-- Swipe along the left screen edge: increase/decrease toc levels shown
|
|
if direction == "north" or direction == "south" then
|
|
local rel = direction == "south" and 1 or -1
|
|
if self:updateTocDepth(rel, nil) then
|
|
self:update()
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
if ges.pos.y > Screen:getHeight() * 7/8 then
|
|
-- Swipe along the bottom screen edge: increase/decrease pages per row
|
|
if direction == "west" or direction == "east" then
|
|
-- Have a swipe distance 0.8 x screen width do *2 or *1/2
|
|
local ratio = ges.distance / Screen:getWidth()
|
|
local new_pages_per_row
|
|
if direction == "west" then -- increase pages per row
|
|
new_pages_per_row = math.ceil(self.pages_per_row * (1 + ratio))
|
|
else
|
|
new_pages_per_row = math.floor(self.pages_per_row / (1 + ratio))
|
|
end
|
|
-- If we are crossing the ideal fit_pages_per_row, stop on it
|
|
if (self.pages_per_row < self.fit_pages_per_row and new_pages_per_row > self.fit_pages_per_row)
|
|
or (self.pages_per_row > self.fit_pages_per_row and new_pages_per_row < self.fit_pages_per_row) then
|
|
new_pages_per_row = self.fit_pages_per_row
|
|
end
|
|
if self:updatePagesPerRow(new_pages_per_row) then
|
|
self:update()
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
-- Let our MovableContainer handle other swipes:
|
|
-- return self.cropping_widget:onScrollableSwipe(arg, ges)
|
|
-- No, we prefer not to, and have swipe north/south do full prev/next page
|
|
-- rather than based on the swipe distance
|
|
if direction == "north" then
|
|
return self:onScrollPageDown()
|
|
elseif direction == "south" then
|
|
return self:onScrollPageUp()
|
|
elseif direction == "west" or direction == "east" then
|
|
return true
|
|
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 BookMapWidget:onPinch(arg, ges)
|
|
local updated = false
|
|
if ges.direction == "horizontal" or ges.direction == "diagonal" then
|
|
local new_pages_per_row = math.ceil(self.pages_per_row * 1.5)
|
|
if (self.pages_per_row < self.fit_pages_per_row and new_pages_per_row > self.fit_pages_per_row)
|
|
or (self.pages_per_row > self.fit_pages_per_row and new_pages_per_row < self.fit_pages_per_row) then
|
|
new_pages_per_row = self.fit_pages_per_row
|
|
end
|
|
if self:updatePagesPerRow(new_pages_per_row) then
|
|
updated = true
|
|
end
|
|
end
|
|
if ges.direction == "vertical" or ges.direction == "diagonal" then
|
|
-- Keep current flat map mode
|
|
if self:updateTocDepth(self.toc_depth-1, self.flat_map) then
|
|
updated = true
|
|
else
|
|
-- Already at 0: toggle flat mode, stay at 0 (no visual feedback though...)
|
|
if self:updateTocDepth(0, not self.flat_map) then
|
|
updated = true
|
|
end
|
|
end
|
|
end
|
|
if updated then
|
|
self:update()
|
|
end
|
|
return true
|
|
end
|
|
|
|
function BookMapWidget:onSpread(arg, ges)
|
|
local updated = false
|
|
if ges.direction == "horizontal" or ges.direction == "diagonal" then
|
|
local new_pages_per_row = math.floor(self.pages_per_row * (2/3))
|
|
if (self.pages_per_row < self.fit_pages_per_row and new_pages_per_row > self.fit_pages_per_row)
|
|
or (self.pages_per_row > self.fit_pages_per_row and new_pages_per_row < self.fit_pages_per_row) then
|
|
new_pages_per_row = self.fit_pages_per_row
|
|
end
|
|
if self:updatePagesPerRow(new_pages_per_row) then
|
|
updated = true
|
|
end
|
|
end
|
|
if ges.direction == "vertical" or ges.direction == "diagonal" then
|
|
if self:updateTocDepth(self.toc_depth+1, self.flat_map) then
|
|
updated = true
|
|
end
|
|
end
|
|
if updated then
|
|
self:update()
|
|
end
|
|
return true
|
|
end
|
|
|
|
function BookMapWidget:onMultiSwipe(arg, ges)
|
|
-- Swipe south (the usual shortcut for closing a full screen window)
|
|
-- is used for navigation. Swipe left/right are free, but a little
|
|
-- unusual for the purpose of closing.
|
|
-- So, allow for quick closing with any multiswipe.
|
|
self:onClose()
|
|
return true
|
|
end
|
|
|
|
function BookMapWidget:onTap(arg, ges)
|
|
if ges.pos:notIntersectWith(self.cropping_widget.dimen) then
|
|
return true
|
|
end
|
|
local x, y = ges.pos.x, ges.pos.y
|
|
local row, row_idx, row_y, row_h = self:getVGroupRowAtY(y-self.title_bar_h) -- luacheck: no unused
|
|
if not row then
|
|
return true
|
|
end
|
|
local page = row.toc_item_page -- it might be a TOC title
|
|
if not page and row.start_page then -- or a BookMapRow
|
|
if BD.mirroredUILayout() then
|
|
x = x - self.scrollbar_width
|
|
end
|
|
page = row:getPageAtX(x, true)
|
|
end
|
|
if page then
|
|
local PageBrowserWidget = require("ui/widget/pagebrowserwidget")
|
|
UIManager:show(PageBrowserWidget:new{
|
|
launcher = self,
|
|
ui = self.ui,
|
|
focus_page = page,
|
|
})
|
|
end
|
|
return true
|
|
end
|
|
|
|
function BookMapWidget:paintTo(bb, x, y)
|
|
-- Paint regular sub widgets the classic way
|
|
InputContainer.paintTo(self, bb, x, y)
|
|
-- And explicitely paint "swipe" hints along the left and bottom borders
|
|
self:paintLeftVerticalSwipeHint(bb, x, y)
|
|
self:paintBottomHorizontalSwipeHint(bb, x, y)
|
|
end
|
|
|
|
function BookMapWidget:paintLeftVerticalSwipeHint(bb, x, y)
|
|
-- Vertical bar with a part of it darker, as a scale showing
|
|
-- selected flat_map and toc_depth. In gray so it's visible
|
|
-- when you look at it, but not distracting when you don't.
|
|
local v = self.vs_hint_info
|
|
if not v then
|
|
-- Compute and remember sizes, positions and info
|
|
v = {}
|
|
v.width = self.swipe_hint_bar_width
|
|
if BD.mirroredUILayout() then
|
|
v.left = Screen:getWidth() - v.width
|
|
else
|
|
v.left = 0
|
|
end
|
|
v.top = self.title_bar_h + math.floor(self.crop_height * 1/6)
|
|
v.height = math.floor(self.crop_height * 4/6)
|
|
v.nb_units = self.max_toc_depth * 2 + 1
|
|
v.unit_h = math.floor(v.height / v.nb_units)
|
|
self.vs_hint_info = v
|
|
end
|
|
-- Paint a vertical light gray bar
|
|
bb:paintRect(v.left, v.top, v.width, v.height, Blitbuffer.COLOR_LIGHT_GRAY)
|
|
-- And paint a part of it in a darker gray
|
|
local unit_idx -- starts from 0
|
|
if self.flat_map then -- upper part of the vertical bar
|
|
unit_idx = self.max_toc_depth - self.toc_depth
|
|
else -- lower part of the vertical bar
|
|
unit_idx = self.max_toc_depth + self.toc_depth
|
|
end
|
|
local dy = unit_idx * v.unit_h
|
|
if unit_idx == v.nb_units - 1 then
|
|
-- avoid possible rounding error for last unit
|
|
dy = v.height - v.unit_h
|
|
end
|
|
bb:paintRect(v.left, v.top + dy, v.width, v.unit_h, Blitbuffer.COLOR_DARK_GRAY)
|
|
end
|
|
|
|
function BookMapWidget:paintBottomHorizontalSwipeHint(bb, x, y)
|
|
-- Horizontal bar with a part of it darker, as a scale showing
|
|
-- selected pages_per_row.
|
|
local h = self.hs_hint_info
|
|
if not h then
|
|
-- Compute and remember sizes, positions and info
|
|
h = {}
|
|
h.height = self.swipe_hint_bar_width
|
|
h.top = Screen:getHeight() - h.height
|
|
h.width = math.floor(Screen:getWidth() * 4/6)
|
|
h.left = math.floor(Screen:getWidth() * 1/6)
|
|
-- We show a fixed width handle with a granular dx
|
|
h.hint_w = math.floor(h.width / 8)
|
|
h.max_dx = h.width - h.hint_w
|
|
self.hs_hint_info = h
|
|
end
|
|
-- Paint a horizontal light gray bar
|
|
bb:paintRect(h.left, h.top, h.width, h.height, Blitbuffer.COLOR_LIGHT_GRAY)
|
|
-- And paint a part of it in a darker gray
|
|
-- (Somebody good at maths could probably do better than this... which
|
|
-- could be related to the increment/ratio we use in onSwipe)
|
|
local cur = self.pages_per_row - self.min_pages_per_row
|
|
local max = self.max_pages_per_row - self.min_pages_per_row
|
|
local dx = math.floor(h.max_dx*(1-math.log(1+cur)/math.log(1+max)))
|
|
if BD.mirroredUILayout() then
|
|
dx = h.max_dx - dx
|
|
end
|
|
bb:paintRect(h.left + dx, h.top, h.hint_w, h.height, Blitbuffer.COLOR_DARK_GRAY)
|
|
end
|
|
|
|
return BookMapWidget
|