2
0
mirror of https://github.com/koreader/koreader synced 2024-11-08 07:10:27 +00:00
koreader/plugins/coverbrowser.koplugin/mosaicmenu.lua

1009 lines
40 KiB
Lua

local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local BottomContainer = require("ui/widget/container/bottomcontainer")
local CenterContainer = require("ui/widget/container/centercontainer")
local Device = require("device")
local DocSettings = require("docsettings")
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 IconWidget = require("ui/widget/iconwidget")
local ImageWidget = require("ui/widget/imagewidget")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local LeftContainer = require("ui/widget/container/leftcontainer")
local ProgressWidget = require("ui/widget/progresswidget")
local OverlapGroup = require("ui/widget/overlapgroup")
local Size = require("ui/size")
local TextBoxWidget = require("ui/widget/textboxwidget")
local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager")
local UnderlineContainer = require("ui/widget/container/underlinecontainer")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local util = require("util")
local _ = require("gettext")
local Screen = Device.screen
local T = require("ffi/util").template
local getMenuText = require("ui/widget/menu").getMenuText
local BookInfoManager = require("bookinfomanager")
-- Here is the specific UI implementation for "mosaic" display modes
-- (see covermenu.lua for the generic code)
-- We will show a rotated dogear at bottom right corner of cover widget for
-- opened files (the dogear will make it look like a "used book")
-- The ImageWidget Will be created when we know the available height (and
-- recreated if height changes)
local corner_mark_size = -1
local corner_mark
local reading_mark
local abandoned_mark
local complete_mark
local progress_widget
-- ItemShortCutIcon (for keyboard navigation) is private to menu.lua and can't be accessed,
-- so we need to redefine it
local ItemShortCutIcon = WidgetContainer:extend{
dimen = Geom:new{ w = Screen:scaleBySize(22), h = Screen:scaleBySize(22) },
key = nil,
bordersize = Size.border.default,
radius = 0,
style = "square",
}
function ItemShortCutIcon:init()
if not self.key then
return
end
local radius = 0
local background = Blitbuffer.COLOR_WHITE
if self.style == "rounded_corner" then
radius = math.floor(self.width/2)
elseif self.style == "grey_square" then
background = Blitbuffer.COLOR_LIGHT_GRAY
end
local sc_face
if self.key:len() > 1 then
sc_face = Font:getFace("ffont", 14)
else
sc_face = Font:getFace("scfont", 22)
end
self[1] = FrameContainer:new{
padding = 0,
bordersize = self.bordersize,
radius = radius,
background = background,
dimen = self.dimen,
CenterContainer:new{
dimen = self.dimen,
TextWidget:new{
text = self.key,
face = sc_face,
},
},
}
end
-- We may find a better algorithm, or just a set of
-- nice looking combinations of 3 sizes to iterate thru
-- The rendering of the TextBoxWidget we're doing below
-- with decreasing font sizes till it fits is quite expensive.
local FakeCover = FrameContainer:extend{
width = nil,
height = nil,
margin = 0,
padding = 0,
bordersize = Size.border.thin,
dim = nil,
bottom_right_compensate = false,
-- Provided filename, title and authors should not be BD wrapped
filename = nil,
file_deleted = nil,
title = nil,
authors = nil,
-- The *_add should be provided BD wrapped if needed
filename_add = nil,
title_add = nil,
authors_add = nil,
book_lang = nil,
-- these font sizes will be scaleBySize'd by Font:getFace()
authors_font_max = 20,
authors_font_min = 6,
title_font_max = 24,
title_font_min = 10,
filename_font_max = 10,
filename_font_min = 8,
top_pad = Size.padding.default,
bottom_pad = Size.padding.default,
sizedec_step = Screen:scaleBySize(2), -- speeds up a bit if we don't do all font sizes
initial_sizedec = 0,
}
function FakeCover:init()
-- BookInfoManager:extractBookInfo() made sure
-- to save as nil (NULL) metadata that were an empty string
local authors = self.authors
local title = self.title
local filename = self.filename
-- (some engines may have already given filename (without extension) as title)
local bd_wrap_title_as_filename = false
if not title then -- use filename as title (big and centered)
title = filename
filename = nil
if not self.title_add and self.filename_add then
-- filename_add ("…" or "(deleted)") always comes without any title_add
self.title_add = self.filename_add
self.filename_add = nil
end
bd_wrap_title_as_filename = true
end
if filename then
filename = BD.filename(filename)
end
-- If no authors, and title is filename without extension, it was
-- probably made by an engine, and we can consider it a filename, and
-- act according to common usage in naming files.
if not authors and title and self.filename and self.filename:sub(1,title:len()) == title then
bd_wrap_title_as_filename = true
-- Replace a hyphen surrounded by spaces (which most probably was
-- used to separate Authors/Title/Serie/Year/Categorie in the
-- filename with a \n
title = title:gsub(" %- ", "\n")
-- Same with |
title = title:gsub("|", "\n")
-- Also replace underscores with spaces
title = title:gsub("_", " ")
-- Some filenames may also use dots as separators, but dots
-- can also have some meaning, so we can't just remove them.
-- But at least, make dots breakable (they wouldn't be if not
-- followed by a space), by adding to them a zero-width-space,
-- so the dots stay on the right of their preceeding word.
title = title:gsub("%.", ".\u{200B}")
-- Except for a last dot near end of title that might preceed
-- a file extension: we'd rather want the dot and its suffix
-- together on a last line: so, move the zero-width-space
-- before it.
title = title:gsub("%.\u{200B}(%w%w?%w?%w?%w?)$", "\u{200B}.%1")
-- These substitutions will hopefully have no impact with the following BD wrapping
end
if title then
title = bd_wrap_title_as_filename and BD.filename(title) or BD.auto(title)
end
-- If multiple authors (crengine separates them with \n), we
-- can display them on multiple lines, but limit to 3, and
-- append "et al." on a 4th line if there are more
if authors and authors:find("\n") then
authors = util.splitToArray(authors, "\n")
for i=1, #authors do
authors[i] = BD.auto(authors[i])
end
if #authors > 3 then
authors = { authors[1], authors[2], T(_("%1 et al."), authors[3]) }
end
authors = table.concat(authors, "\n")
elseif authors then
authors = BD.auto(authors)
end
-- Add any _add, which must be already BD wrapped if needed
if self.filename_add then
filename = (filename and filename or "") .. self.filename_add
end
if self.title_add then
title = (title and title or "") .. self.title_add
end
if self.authors_add then
authors = (authors and authors or "") .. self.authors_add
end
-- We build the VerticalGroup widget with decreasing font sizes till
-- the widget fits into available height
local width = self.width - 2*(self.bordersize + self.margin + self.padding)
local height = self.height - 2*(self.bordersize + self.margin + self.padding)
local text_width = 7/8 * width -- make width of text smaller to have some padding
local inter_pad
local sizedec = self.initial_sizedec
local authors_wg, title_wg, filename_wg
local loop2 = false -- we may do a second pass with modifier title and authors strings
while true do
-- Free previously made widgets to avoid memory leaks
if authors_wg then
authors_wg:free(true)
authors_wg = nil
end
if title_wg then
title_wg:free(true)
title_wg = nil
end
if filename_wg then
filename_wg:free(true)
filename_wg = nil
end
-- Build new widgets
local texts_height = 0
if authors then
authors_wg = TextBoxWidget:new{
text = authors,
lang = self.book_lang,
face = Font:getFace("cfont", math.max(self.authors_font_max - sizedec, self.authors_font_min)),
width = text_width,
alignment = "center",
}
texts_height = texts_height + authors_wg:getSize().h
end
if title then
title_wg = TextBoxWidget:new{
text = title,
lang = self.book_lang,
face = Font:getFace("cfont", math.max(self.title_font_max - sizedec, self.title_font_min)),
width = text_width,
alignment = "center",
}
texts_height = texts_height + title_wg:getSize().h
end
if filename then
filename_wg = TextBoxWidget:new{
text = filename,
lang = self.book_lang, -- might as well use it for filename
face = Font:getFace("cfont", math.max(self.filename_font_max - sizedec, self.filename_font_min)),
width = self.bottom_right_compensate and width - 2 * corner_mark_size or text_width,
alignment = "center",
}
texts_height = texts_height + filename_wg:getSize().h
end
local free_height = height - texts_height
if authors then
free_height = free_height - self.top_pad
end
if filename then
free_height = free_height - self.bottom_pad
end
inter_pad = math.floor(free_height / 2)
local textboxes_ok = true
if (authors_wg and authors_wg.has_split_inside_word) or (title_wg and title_wg.has_split_inside_word) then
-- We may get a nicer cover at next lower font size
textboxes_ok = false
end
if textboxes_ok and free_height > 0.2 * height then -- enough free space to not look constrained
break
end
-- (We may store the first widgets matching free space requirements but
-- not textboxes_ok, so that if we never ever get textboxes_ok candidate,
-- we can use them instead of the super-small strings-modified we'll have
-- at the end that are worse than the firsts)
sizedec = sizedec + self.sizedec_step
if sizedec > 20 then -- break out of loop when too small
-- but try a 2nd loop with some cleanup to strings (for filenames
-- with no space but hyphen or underscore instead)
if not loop2 then
loop2 = true
sizedec = self.initial_sizedec -- restart from initial big size
if G_reader_settings:nilOrTrue("use_xtext") then
-- With Unicode/libunibreak, a break after a hyphen is allowed,
-- but not around underscores and dots without any space around.
-- So, append a zero-width-space to allow text wrap after them.
if title then
title = title:gsub("_", "_\u{200B}"):gsub("%.", ".\u{200B}")
end
if authors then
authors = authors:gsub("_", "_\u{200B}"):gsub("%.", ".\u{200B}")
end
else
-- Replace underscores and hyphens with spaces, to allow text wrap there.
if title then
title = title:gsub("-", " "):gsub("_", " ")
end
if authors then
authors = authors:gsub("-", " "):gsub("_", " ")
end
end
else -- 2nd loop done, no luck, give up
break
end
end
end
local vgroup = VerticalGroup:new{}
if authors then
table.insert(vgroup, VerticalSpan:new{ width = self.top_pad })
table.insert(vgroup, authors_wg)
end
table.insert(vgroup, VerticalSpan:new{ width = inter_pad })
if title then
table.insert(vgroup, title_wg)
end
table.insert(vgroup, VerticalSpan:new{ width = inter_pad })
if filename then
table.insert(vgroup, filename_wg)
table.insert(vgroup, VerticalSpan:new{ width = self.bottom_pad })
end
if self.file_deleted then
self.dim = true
self.color = Blitbuffer.COLOR_DARK_GRAY
end
-- As we are a FrameContainer, a border will be painted around self[1]
self[1] = CenterContainer:new{
dimen = Geom:new{
w = width,
h = height,
},
vgroup,
}
end
-- Based on menu.lua's MenuItem
local MosaicMenuItem = InputContainer:extend{
entry = nil, -- table, mandatory
text = nil,
show_parent = nil,
detail = nil,
dimen = nil,
shortcut = nil,
shortcut_style = "square",
_underline_container = nil,
do_cover_image = false,
do_hint_opened = false,
been_opened = false,
init_done = false,
bookinfo_found = false,
cover_specs = nil,
has_description = false,
}
function MosaicMenuItem:init()
-- filepath may be provided as 'file' (history) or 'path' (filechooser)
-- store it as attribute so we can use it elsewhere
self.filepath = self.entry.file or self.entry.path
-- As done in MenuItem
-- Squared letter for keyboard navigation
if self.shortcut then
local shortcut_icon_dimen = Geom:new()
shortcut_icon_dimen.w = math.floor(self.dimen.h*1/5)
shortcut_icon_dimen.h = shortcut_icon_dimen.w
-- To keep a simpler widget structure, this shortcut icon will not
-- be part of it, but will be painted over the widget in our paintTo
self.shortcut_icon = ItemShortCutIcon:new{
dimen = shortcut_icon_dimen,
key = self.shortcut,
style = self.shortcut_style,
}
end
self.detail = self.text
self.percent_finished = nil
self.status = nil
-- we need this table per-instance, so we declare it here
self.ges_events = {
TapSelect = {
GestureRange:new{
ges = "tap",
range = self.dimen,
},
},
HoldSelect = {
GestureRange:new{
ges = "hold",
range = self.dimen,
},
},
}
-- We now build the minimal widget container that won't change after udpate()
-- As done in MenuItem
-- for compatibility with keyboard navigation
-- (which does not seem to work well when multiple pages,
-- even with classic menu)
self.underline_h = 1 -- smaller than default (3), don't waste space
self._underline_container = UnderlineContainer:new{
vertical_align = "top",
padding = 0,
dimen = Geom:new{
w = self.width,
h = self.height
},
linesize = self.underline_h,
-- widget : will be filled in self:update()
}
self[1] = self._underline_container
-- Remaining part of initialization is done in update(), because we may
-- have to do it more than once if item not found in db
self:update()
self.init_done = true
end
function MosaicMenuItem:update()
-- We will be a disctinctive widget whether we are a directory,
-- a known file with image / without image, or a not yet known file
local widget
local dimen = Geom:new{
w = self.width,
h = self.height - self.underline_h
}
-- We'll draw a border around cover images, it may not be
-- needed with some covers, but it's nicer when cover is
-- a pure white background (like rendered text page)
local border_size = Size.border.thin
local max_img_w = dimen.w - 2*border_size
local max_img_h = dimen.h - 2*border_size
local cover_specs = {
sizetag = "M",
max_cover_w = max_img_w,
max_cover_h = max_img_h,
}
-- Make it available to our menu, for batch extraction
-- to know what size is needed for current view
if self.do_cover_image then
self.menu.cover_specs = cover_specs
else
self.menu.cover_specs = false
end
local file_mode = lfs.attributes(self.filepath, "mode")
if file_mode == "directory" then
self.is_directory = true
-- Directory : rounded corners
local margin = Screen:scaleBySize(5) -- make directories less wide
local padding = Screen:scaleBySize(5)
border_size = Size.border.thick -- make directories' borders larger
local dimen_in = Geom:new{
w = dimen.w - (margin + padding + border_size)*2,
h = dimen.h - (margin + padding + border_size)*2
}
local text = self.text
if text:match('/$') then -- remove /, more readable
text = text:sub(1, -2)
end
text = BD.directory(text)
local nbitems = TextBoxWidget:new{
text = self.mandatory,
face = Font:getFace("infont", 15),
width = dimen_in.w,
alignment = "center",
}
-- The directory name will be centered, with nbitems at bottom.
-- We could use 2*nbitems:getSize().h to keep that centering,
-- but using 3* will avoid getting the directory name stuck
-- to nbitems.
local available_height = dimen_in.h - 3 * nbitems:getSize().h
local dir_font_size = 20
local directory
while true do
if directory then
directory:free(true)
end
directory = TextBoxWidget:new{
text = text,
face = Font:getFace("cfont", dir_font_size),
width = dimen_in.w,
alignment = "center",
bold = true,
}
if directory:getSize().h <= available_height then
break
end
dir_font_size = dir_font_size - 1
if dir_font_size < 10 then -- don't go too low
directory:free()
directory.height = available_height
directory.height_adjust = true
directory.height_overflow_show_ellipsis = true
directory:init()
break
end
end
widget = FrameContainer:new{
width = dimen.w,
height = dimen.h,
margin = margin,
padding = padding,
bordersize = border_size,
radius = Screen:scaleBySize(10),
OverlapGroup:new{
dimen = dimen_in,
CenterContainer:new{ dimen=dimen_in, directory},
BottomContainer:new{ dimen=dimen_in, nbitems},
},
}
else
local is_file_selected = self.menu.filemanager and self.menu.filemanager.selected_files
and self.menu.filemanager.selected_files[self.filepath]
if file_mode ~= "file" or is_file_selected then
self.file_deleted = true -- dim file
end
-- File : various appearances
if self.do_hint_opened and DocSettings:hasSidecarFile(self.filepath) then
self.been_opened = true
end
local bookinfo = BookInfoManager:getBookInfo(self.filepath, self.do_cover_image)
if bookinfo and self.do_cover_image and not bookinfo.ignore_cover then
if bookinfo.cover_fetched then
if bookinfo.has_cover and bookinfo.cover_sizetag ~= "M" then
-- there is a cover, but it's a small one (made by ListMenuItem),
-- and it would be ugly if scaled up to MosaicMenuItem size:
-- do as if not found to force a new extraction with our size
if bookinfo.cover_bb then
bookinfo.cover_bb:free()
end
bookinfo = nil
-- Note: with the current size differences between FileManager
-- and the History windows, we'll get lower max_img_* in History.
-- So, when one get Items first generated by the other, it will
-- have to do some scaling. Hopefully, people most probably
-- browse a lot more files than have them in history, so
-- it's most probably History that will have to do some scaling.
end
-- if not has_cover, book has no cover, no need to try again
else
-- cover was not fetched previously, do as if not found
-- to force a new extraction
bookinfo = nil
end
end
if bookinfo then -- This book is known
-- Current page / pages are available or more accurate in .sdr/metadata.lua
-- We use a cache (cleaned at end of this browsing session) to store
-- page, percent read and book status from sidecar files, to avoid
-- re-parsing them when re-rendering a visited page
-- This cache is shared with ListMenu, so we need to fill it with the same
-- info here than there, even if we don't need them all here.
if not self.menu.cover_info_cache then
self.menu.cover_info_cache = {}
end
local percent_finished, status
if DocSettings:hasSidecarFile(self.filepath) then
self.been_opened = true
self.menu:updateCache(self.filepath, nil, true, bookinfo.pages) -- create new cache entry if absent
dummy, percent_finished, status =
unpack(self.menu.cover_info_cache[self.filepath], 1, self.menu.cover_info_cache[self.filepath].n)
end
self.percent_finished = percent_finished
self.status = status
self.show_progress_bar = self.status ~= "complete" and BookInfoManager:getSetting("show_progress_in_mosaic") and self.percent_finished
local cover_bb_used = false
self.bookinfo_found = true
-- For wikipedia saved as epub, we made a cover from the 1st pic of the page,
-- which may not say much about the book. So, here, pretend we don't have
-- a cover
if bookinfo.authors and bookinfo.authors:match("^Wikipedia ") then
bookinfo.has_cover = nil
end
if self.do_cover_image and bookinfo.has_cover and not bookinfo.ignore_cover then
cover_bb_used = true
-- Let ImageWidget do the scaling and give us a bb that fit
local scale_factor = math.min(max_img_w / bookinfo.cover_w, max_img_h / bookinfo.cover_h)
local image= ImageWidget:new{
image = bookinfo.cover_bb,
scale_factor = scale_factor,
}
image:_render()
local image_size = image:getSize()
widget = CenterContainer:new{
dimen = dimen,
FrameContainer:new{
width = image_size.w + 2*border_size,
height = image_size.h + 2*border_size,
margin = 0,
padding = 0,
bordersize = border_size,
dim = self.file_deleted,
color = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil,
image,
}
}
-- Let menu know it has some item with images
self.menu._has_cover_images = true
self._has_cover_image = true
else
-- add Series metadata if requested
local series_mode = BookInfoManager:getSetting("series_mode")
local title_add, authors_add
if bookinfo.series then
if bookinfo.series_index then
bookinfo.series = BD.auto(bookinfo.series .. " #" .. bookinfo.series_index)
else
bookinfo.series = BD.auto(bookinfo.series)
end
if series_mode == "append_series_to_title" then
if bookinfo.title then
title_add = " - " .. bookinfo.series
else
title_add = bookinfo.series
end
end
if not bookinfo.authors then
if series_mode == "append_series_to_authors" or series_mode == "series_in_separate_line" then
authors_add = bookinfo.series
end
else
if series_mode == "append_series_to_authors" then
authors_add = " - " .. bookinfo.series
elseif series_mode == "series_in_separate_line" then
authors_add = "\n \n" .. bookinfo.series
end
end
end
local bottom_pad = Size.padding.default
if self.show_progress_bar and self.do_hint_opened then
bottom_pad = corner_mark_size + Screen:scaleBySize(2)
elseif self.show_progress_bar then
bottom_pad = corner_mark_size - Screen:scaleBySize(2)
end
widget = CenterContainer:new{
dimen = dimen,
FakeCover:new{
-- reduced width to make it look less squared, more like a book
width = math.floor(dimen.w * 7/8),
height = dimen.h,
bordersize = border_size,
filename = self.text,
title = not bookinfo.ignore_meta and bookinfo.title,
authors = not bookinfo.ignore_meta and bookinfo.authors,
title_add = not bookinfo.ignore_meta and title_add,
authors_add = not bookinfo.ignore_meta and authors_add,
book_lang = not bookinfo.ignore_meta and bookinfo.language,
file_deleted = self.file_deleted,
bottom_pad = bottom_pad,
bottom_right_compensate = not self.show_progress_bar and self.do_hint_opened,
}
}
end
-- In case we got a blitbuffer and didnt use it (ignore_cover, wikipedia), free it
if bookinfo.cover_bb and not cover_bb_used then
bookinfo.cover_bb:free()
end
-- So we can draw an indicator if this book has a description
if bookinfo.description then
self.has_description = true
end
else -- bookinfo not found
if self.init_done then
-- Non-initial update(), but our widget is still not found:
-- it does not need to change, so avoid making the same FakeCover
return
end
-- If we're in no image mode, don't save images in DB : people
-- who don't care about images will have a smaller DB, but
-- a new extraction will have to be made when one switch to image mode
if self.do_cover_image then
-- Not in db, we're going to fetch some cover
self.cover_specs = cover_specs
end
-- Same as real FakeCover, but let it be squared (like a file)
local hint = "" -- display hint it's being loaded
if self.file_deleted then -- unless file was deleted (can happen with History)
hint = _("(deleted)")
end
widget = CenterContainer:new{
dimen = dimen,
FakeCover:new{
width = dimen.w,
height = dimen.h,
bordersize = border_size,
filename = self.text,
filename_add = "\n" .. hint,
initial_sizedec = 4, -- start with a smaller font when filenames only
file_deleted = self.file_deleted,
}
}
end
end
-- Fill container with our widget
if self._underline_container[1] then
-- There is a previous one, that we need to free()
local previous_widget = self._underline_container[1]
previous_widget:free()
end
self._underline_container[1] = widget
end
function MosaicMenuItem:paintTo(bb, x, y)
-- We used to get non-integer x or y that would cause some mess with image
-- inside FrameContainer were image would be drawn on top of the top border...
-- Fixed by having TextWidget:updateSize() math.ceil()'ing its length and height
-- But let us know if that happens again
if x ~= math.floor(x) or y ~= math.floor(y) then
logger.err("MosaicMenuItem:paintTo() got non-integer x/y :", x, y)
end
-- Original painting
InputContainer.paintTo(self, bb, x, y)
-- to which we paint over the shortcut icon
if self.shortcut_icon then
-- align it on top left corner of widget
local target = self
local ix
if BD.mirroredUILayout() then
ix = target.dimen.w - self.shortcut_icon.dimen.w
else
ix = 0
end
local iy = 0
self.shortcut_icon:paintTo(bb, x+ix, y+iy)
end
if self.do_hint_opened and self.been_opened then
-- align it on bottom right corner of sub-widget
local target = self[1][1][1]
local ix
if BD.mirroredUILayout() then
ix = math.floor((self.width - target.dimen.w)/2)
else
ix = self.width - math.ceil((self.width - target.dimen.w)/2) - corner_mark:getSize().w
end
local iy = self.height - math.ceil((self.height - target.dimen.h)/2) - corner_mark:getSize().h
-- math.ceil() makes it looks better than math.floor()
if self.status == "abandoned" then
corner_mark = abandoned_mark
elseif self.status == "complete" then
corner_mark = complete_mark
else
corner_mark = reading_mark
end
corner_mark:paintTo(bb, x+ix, y+iy)
end
if self.show_progress_bar then
local cover_item = self[1][1][1]
local progress_widget_margin = math.floor((corner_mark_size - progress_widget.height) / 2)
progress_widget.width = cover_item.width - 2*progress_widget_margin
local pos_x = x + math.ceil((self.width - progress_widget.width) / 2)
if self.do_hint_opened then
progress_widget.width = progress_widget.width - corner_mark_size
if BD.mirroredUILayout() then
pos_x = pos_x + corner_mark_size
end
end
local pos_y = y + self.height - math.ceil((self.height - cover_item.height) / 2) - corner_mark_size + progress_widget_margin
if self.status == "abandoned" then
progress_widget.fillcolor = Blitbuffer.COLOR_GRAY_6
else
progress_widget.fillcolor = Blitbuffer.COLOR_BLACK
end
progress_widget:setPercentage(self.percent_finished)
progress_widget:paintTo(bb, pos_x, pos_y)
end
-- to which we paint a small indicator if this book has a description
if self.has_description and not BookInfoManager:getSetting("no_hint_description") then
-- On book's right (for similarity to ListMenuItem)
local target = self[1][1][1]
local d_w = Screen:scaleBySize(3)
local d_h = math.ceil(target.dimen.h / 8)
-- Paint it directly relative to target.dimen.x/y which has been computed at this point
local ix
if BD.mirroredUILayout() then
ix = - d_w + 1
-- Set alternate dimen to be marked as dirty to include this description in refresh
local x_overflow_left = x - target.dimen.x+ix -- positive if overflow
if x_overflow_left > 0 then
self.refresh_dimen = self[1].dimen:copy()
self.refresh_dimen.x = self.refresh_dimen.x - x_overflow_left
self.refresh_dimen.w = self.refresh_dimen.w + x_overflow_left
end
else
ix = target.dimen.w - 1
-- Set alternate dimen to be marked as dirty to include this description in refresh
local x_overflow_right = target.dimen.x+ix+d_w - x - self.dimen.w
if x_overflow_right > 0 then
self.refresh_dimen = self[1].dimen:copy()
self.refresh_dimen.w = self.refresh_dimen.w + x_overflow_right
end
end
local iy = 0
bb:paintBorder(target.dimen.x+ix, target.dimen.y+iy, d_w, d_h, 1)
end
end
-- As done in MenuItem
function MosaicMenuItem:onFocus()
self._underline_container.color = Blitbuffer.COLOR_BLACK
return true
end
function MosaicMenuItem:onUnfocus()
self._underline_container.color = Blitbuffer.COLOR_WHITE
return true
end
function MosaicMenuItem:onShowItemDetail()
UIManager:show(InfoMessage:new{ text = self.detail, })
return true
end
-- The transient color inversions done in MenuItem:onTapSelect
-- and MenuItem:onHoldSelect are ugly when done on an image,
-- so let's not do it
-- Also, no need for 2nd arg 'pos' (only used in readertoc.lua)
function MosaicMenuItem:onTapSelect(arg)
self.menu:onMenuSelect(self.entry)
return true
end
function MosaicMenuItem:onHoldSelect(arg, ges)
self.menu:onMenuHold(self.entry)
return true
end
-- Simple holder of methods that will replace those
-- in the real Menu class or instance
local MosaicMenu = {}
function MosaicMenu:_recalculateDimen()
local portrait_mode = Screen:getWidth() <= Screen:getHeight()
-- 3 x 3 grid by default if not initially provided (4 x 2 in landscape mode)
if portrait_mode then
self.nb_cols = self.nb_cols_portrait or 3
self.nb_rows = self.nb_rows_portrait or 3
else
self.nb_cols = self.nb_cols_landscape or 4
self.nb_rows = self.nb_rows_landscape or 2
end
self.perpage = self.nb_rows * self.nb_cols
self.page_num = math.ceil(#self.item_table / self.perpage)
-- fix current page if out of range
if self.page_num > 0 and self.page > self.page_num then self.page = self.page_num end
-- Find out available height from other UI elements made in Menu
self.others_height = 0
if self.title_bar then -- init() has been done
if not self.is_borderless then
self.others_height = self.others_height + 2
end
if not self.no_title then
self.others_height = self.others_height + self.header_padding
self.others_height = self.others_height + self.title_bar.dimen.h
end
if self.page_info then
self.others_height = self.others_height + self.page_info:getSize().h
end
end
-- Set our items target size
self.item_margin = Screen:scaleBySize(10)
self.item_height = math.floor((self.inner_dimen.h - self.others_height - (1+self.nb_rows)*self.item_margin) / self.nb_rows)
self.item_width = math.floor((self.inner_dimen.w - (1+self.nb_cols)*self.item_margin) / self.nb_cols)
self.item_dimen = Geom:new{
w = self.item_width,
h = self.item_height
}
-- Create or replace corner_mark if needed
-- 1/12 (larger) or 1/16 (smaller) of cover looks allright
local mark_size = math.floor(math.min(self.item_width, self.item_height) / 8)
if mark_size ~= corner_mark_size then
corner_mark_size = mark_size
if corner_mark then
reading_mark:free()
abandoned_mark:free()
complete_mark:free()
end
reading_mark = IconWidget:new{
icon = "dogear.reading",
rotation_angle = BD.mirroredUILayout() and 270 or 0,
width = corner_mark_size,
height = corner_mark_size,
}
abandoned_mark = IconWidget:new{
icon = BD.mirroredUILayout() and "dogear.abandoned.rtl" or "dogear.abandoned",
width = corner_mark_size,
height = corner_mark_size,
}
complete_mark = IconWidget:new{
icon = BD.mirroredUILayout() and "dogear.complete.rtl" or "dogear.complete",
alpha = true,
width = corner_mark_size,
height = corner_mark_size,
}
corner_mark = reading_mark
end
-- Create or replace progress_widget if needed
local progress_bar_width = self.item_width * 0.60;
if not progress_widget or progress_widget.width ~= progress_bar_width then
progress_widget = ProgressWidget:new{
bgcolor = Blitbuffer.COLOR_WHITE,
fillcolor = Blitbuffer.COLOR_BLACK,
bordercolor = Blitbuffer.COLOR_BLACK,
height = Screen:scaleBySize(8),
margin_h = Screen:scaleBySize(1),
width = progress_bar_width,
radius = Size.border.thin,
bordersize = Size.border.default,
}
end
end
function MosaicMenu:_updateItemsBuildUI()
-- Build our grid
local idx_offset = (self.page - 1) * self.perpage
local cur_row = nil
for idx = 1, self.perpage do
local entry = self.item_table[idx_offset + idx]
if entry == nil then break end
if idx % self.nb_cols == 1 then -- new row
table.insert(self.item_group, VerticalSpan:new{ width = self.item_margin })
cur_row = HorizontalGroup:new{}
-- Have items on the possibly non-fully filled last row aligned to the left
local container = self._do_center_partial_rows and CenterContainer or LeftContainer
table.insert(self.item_group, container:new{
dimen = Geom:new{
w = self.inner_dimen.w,
h = self.item_height
},
cur_row
})
table.insert(cur_row, HorizontalSpan:new({ width = self.item_margin }))
end
-- Keyboard shortcuts, as done in Menu
local item_shortcut = nil
local shortcut_style = "square"
if self.is_enable_shortcut then
-- give different shortcut_style to keys in different
-- lines of keyboard
if idx >= 11 and idx <= 20 then
shortcut_style = "grey_square"
end
item_shortcut = self.item_shortcuts[idx]
end
local item_tmp = MosaicMenuItem:new{
height = self.item_height,
width = self.item_width,
entry = entry,
text = getMenuText(entry),
show_parent = self.show_parent,
mandatory = entry.mandatory,
dimen = self.item_dimen:new(),
shortcut = item_shortcut,
shortcut_style = shortcut_style,
menu = self,
do_cover_image = self._do_cover_images,
do_hint_opened = self._do_hint_opened,
}
table.insert(cur_row, item_tmp)
table.insert(cur_row, HorizontalSpan:new({ width = self.item_margin }))
-- this is for focus manager
table.insert(self.layout, {item_tmp})
if not item_tmp.bookinfo_found and not item_tmp.is_directory and not item_tmp.file_deleted then
-- Register this item for update
table.insert(self.items_to_update, item_tmp)
end
end
table.insert(self.item_group, VerticalSpan:new{ width = self.item_margin }) -- bottom padding
end
return MosaicMenu