You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
koreader/plugins/coverbrowser.koplugin/listmenu.lua

1035 lines
43 KiB
Lua

local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
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 LineWidget = require("ui/widget/linewidget")
local Math = require("optmath")
local OverlapGroup = require("ui/widget/overlapgroup")
local RightContainer = require("ui/widget/container/rightcontainer")
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 N_ = _.ngettext
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 "list" 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 scale_by_size = Screen:scaleBySize(1000000) / 1000000
local max_fontsize_fileinfo
-- ItemShortCutIcon (for keyboard navigation) is private to menu.lua and can't be accessed,
-- so we need to redefine it
local ItemShortCutIcon = WidgetContainer:new{
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
-- Based on menu.lua's MenuItem
local ListMenuItem = InputContainer:new{
entry = {},
text = nil,
show_parent = nil,
detail = nil,
dimen = nil,
shortcut = nil,
shortcut_style = "square",
_underline_container = nil,
do_cover_image = false,
do_filename_only = false,
do_hint_opened = false,
been_opened = false,
init_done = false,
bookinfo_found = false,
cover_specs = nil,
has_description = false,
}
function ListMenuItem: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*2/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
-- we need this table per-instance, so we declare it here
if Device:isTouchDevice() then
self.ges_events = {
TapSelect = {
GestureRange:new{
ges = "tap",
range = self.dimen,
},
doc = "Select Menu Item",
},
HoldSelect = {
GestureRange:new{
ges = "hold",
range = self.dimen,
},
doc = "Hold Menu Item",
},
}
end
-- 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) to not shift our vertical aligment
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 ListMenuItem: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
-- we'll add a VerticalSpan of same size as underline container for balance
local dimen = Geom:new{
w = self.width,
h = self.height - 2 * self.underline_h
}
local function _fontSize(nominal)
-- nominal font size is based on 64px ListMenuItem height
-- keep ratio of font size to item height
local font_size = nominal * dimen.h / 64 / scale_by_size
return math.floor(font_size)
end
-- Will speed up a bit if we don't do all font sizes when
-- looking for one that make text fit
local fontsize_dec_step = math.ceil(_fontSize(100) / 100)
-- 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.h - 2*border_size -- width = height, squared
local max_img_h = dimen.h - 2*border_size
local cover_specs = {
sizetag = self.menu.cover_sizetag,
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
-- nb items on the right, directory name on the left
local wright = TextWidget:new{
text = self.mandatory_func and self.mandatory_func() or self.mandatory,
face = Font:getFace("infont", math.min(max_fontsize_fileinfo, _fontSize(15))),
}
local pad_width = Screen:scaleBySize(10) -- on the left, in between, and on the right
local wleft_width = dimen.w - wright:getWidth() - 3*pad_width
local wleft = TextBoxWidget:new{
text = BD.directory(self.text),
face = Font:getFace("cfont", _fontSize(20)),
width = wleft_width,
alignment = "left",
bold = true,
height = dimen.h,
height_adjust = true,
height_overflow_show_ellipsis = true,
}
widget = OverlapGroup:new{
dimen = dimen,
LeftContainer:new{
dimen = dimen,
HorizontalGroup:new{
HorizontalSpan:new{ width = pad_width },
wleft,
}
},
RightContainer:new{
dimen = dimen,
HorizontalGroup:new{
wright,
HorizontalSpan:new{ width = pad_width },
},
},
}
else
if file_mode ~= "file" then
self.file_deleted = true
end
-- File
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
-- trigger recalculation of thumbnail if size changed
if bookinfo.has_cover and bookinfo.cover_sizetag ~= "M" and bookinfo.cover_sizetag ~= cover_specs.sizetag then
if bookinfo.cover_bb then
bookinfo.cover_bb:free()
end
bookinfo = nil
end
else
-- cover was not fetched previously, do as if not found
-- to force a new extraction
bookinfo = nil
end
-- If there's already a cover and it's a "M" size (MosaicMenuItem),
-- we'll use it and scale it down (it may slow a bit rendering,
-- but "M" size may be useful in another view (FileBrowser/History),
-- so we don't replace it).
end
if bookinfo then -- This book is known
self.bookinfo_found = true
local cover_bb_used = false
-- Build the left widget : image if wanted
local wleft = nil
local wleft_width = 0 -- if not do_cover_image
local wleft_height
if self.do_cover_image then
wleft_height = dimen.h
wleft_width = wleft_height -- make it squared
if bookinfo.has_cover and not bookinfo.ignore_cover then
cover_bb_used = true
-- Let ImageWidget do the scaling and give us the final size
local scale_factor = math.min(max_img_w / bookinfo.cover_w, max_img_h / bookinfo.cover_h)
local wimage = ImageWidget:new{
image = bookinfo.cover_bb,
scale_factor = scale_factor,
}
wimage:_render()
local image_size = wimage:getSize() -- get final widget size
wleft = CenterContainer:new{
dimen = Geom:new{ w = wleft_width, h = wleft_height },
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,
wimage,
}
}
-- Let menu know it has some item with images
self.menu._has_cover_images = true
self._has_cover_image = true
else
local fake_cover_w = max_img_w * 0.6
local fake_cover_h = max_img_h
wleft = CenterContainer:new{
dimen = Geom:new{ w = wleft_width, h = wleft_height },
FrameContainer:new{
width = fake_cover_w + 2*border_size,
height = fake_cover_h + 2*border_size,
margin = 0,
padding = 0,
bordersize = border_size,
dim = self.file_deleted,
CenterContainer:new{
dimen = Geom:new{ w = fake_cover_w, h = fake_cover_h },
TextWidget:new{
text = "", -- U+26F6 Square four corners
face = Font:getFace("cfont", _fontSize(20)),
},
},
},
}
end
end
-- In case we got a blitbuffer and didnt use it (ignore_cover), 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
-- Gather some info, mostly for right widget:
-- file size (self.mandatory) (not available with History)
-- file type
-- pages read / nb of pages (not available for crengine doc not opened)
local directory, filename = util.splitFilePathName(self.filepath) -- luacheck: no unused
local filename_without_suffix, filetype = util.splitFileNameSuffix(filename)
local fileinfo_str
if self.mandatory_func then
-- Currently only provided by History, giving the last time read.
-- Just show this date, without the file extension
fileinfo_str = self.mandatory_func()
else
if self.mandatory then
fileinfo_str = BD.wrap(self.mandatory) .. " " .. BD.wrap(filetype)
else
fileinfo_str = filetype
end
if bookinfo._no_provider then
-- for unspported files: don't show extension on the right,
-- keep it in filename
filename_without_suffix = filename
fileinfo_str = self.mandatory
end
end
-- 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
if not self.menu.cover_info_cache then
self.menu.cover_info_cache = {}
end
local pages_str = ""
local percent_finished, status
local pages = bookinfo.pages -- default to those in bookinfo db
if DocSettings:hasSidecarFile(self.filepath) then
self.been_opened = true
if self.menu.cover_info_cache[self.filepath] then
pages, percent_finished, status = unpack(self.menu.cover_info_cache[self.filepath])
else
local docinfo = DocSettings:open(self.filepath)
-- We can get nb of page in the new 'doc_pages' setting, or from the old 'stats.page'
if docinfo.data.doc_pages then
pages = docinfo.data.doc_pages
elseif docinfo.data.stats and docinfo.data.stats.pages then
if docinfo.data.stats.pages ~= 0 then -- crengine with statistics disabled stores 0
pages = docinfo.data.stats.pages
end
end
if docinfo.data.summary and docinfo.data.summary.status then
status = docinfo.data.summary.status
end
percent_finished = docinfo.data.percent_finished
self.menu.cover_info_cache[self.filepath] = {pages, percent_finished, status}
end
end
if status == "complete" or status == "abandoned" then
-- Display these instead of the read %
if pages then
if status == "complete" then
pages_str = T(_("Finished - %1 pages"), pages)
else
pages_str = T(_("On hold - %1 pages"), pages)
end
else
pages_str = status == "complete" and _("Finished") or _("On hold")
end
elseif percent_finished then
if pages then
if BookInfoManager:getSetting("show_pages_read_as_progress") then
pages_str = T(_("Page %1 of %2"), Math.round(percent_finished*pages), pages)
else
pages_str = T(_("%1 % of %2 pages"), math.floor(100*percent_finished), pages)
end
if BookInfoManager:getSetting("show_pages_left_in_progress") then
pages_str = T(_("%1, %2 to read"), pages_str, Math.round(pages-percent_finished*pages), pages)
end
else
pages_str = string.format("%d %%", math.floor(100*percent_finished))
end
else
if pages then
pages_str = T(N_("1 page", "%1 pages", pages), pages)
end
end
-- Build the right widget
local fontsize_info = math.min(max_fontsize_fileinfo, _fontSize(14))
local wfileinfo = TextWidget:new{
text = fileinfo_str,
face = Font:getFace("cfont", fontsize_info),
fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil,
}
local wpageinfo = TextWidget:new{
text = pages_str,
face = Font:getFace("cfont", fontsize_info),
fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil,
}
local wright_width = math.max(wfileinfo:getSize().w, wpageinfo:getSize().w)
local wright_right_padding = Screen:scaleBySize(10)
-- We just built two string to be put one on top of the other, and we want
-- the combo centered. Given the nature of our strings (numbers,
-- uppercase MB/KB on top text, number and lowercase "page" on bottom text),
-- we get the annoying feeling it's not centered but shifted towards top.
-- Let's add a small VerticalSpan at top to give a better visual
-- feeling of centering.
local wright = CenterContainer:new{
dimen = Geom:new{ w = wright_width, h = dimen.h },
VerticalGroup:new{
align = "right",
VerticalSpan:new{ width = Screen:scaleBySize(2) },
wfileinfo,
wpageinfo,
}
}
-- Create or replace corner_mark if needed
local mark_size = math.floor(dimen.h / 6)
-- Just fits under the page info text, which in turn adapts to the ListMenuItem height.
if mark_size ~= corner_mark_size then
corner_mark_size = mark_size
if corner_mark then
corner_mark:free()
end
corner_mark = IconWidget:new{
icon = "dogear.opaque",
rotation_angle = BD.mirroredUILayout() and 180 or 270,
width = corner_mark_size,
height = corner_mark_size,
}
end
-- Build the middle main widget, in the space available
local wmain_left_padding = Screen:scaleBySize(10)
if self.do_cover_image then
-- we need less padding, as cover image, most often in
-- portrait mode, will provide some padding
wmain_left_padding = Screen:scaleBySize(5)
end
local wmain_right_padding = Screen:scaleBySize(10) -- used only for next calculation
local wmain_width = dimen.w - wleft_width - wmain_left_padding - wmain_right_padding - wright_width - wright_right_padding
local fontname_title = "cfont"
local fontname_authors = "cfont"
local fontsize_title = _fontSize(20)
local fontsize_authors = _fontSize(18)
local wtitle, wauthors
local title, authors
local series_mode = BookInfoManager:getSetting("series_mode")
-- whether to use or not title and authors
-- (We wrap each metadata text with BD.auto() to get for each of them
-- the text direction from the first strong character - which should
-- individually be the best thing, and additionnaly prevent shuffling
-- if concatenated.)
if self.do_filename_only or bookinfo.ignore_meta then
title = filename_without_suffix -- made out above
title = BD.auto(title)
authors = nil
else
title = bookinfo.title and bookinfo.title or filename_without_suffix
title = BD.auto(title)
authors = bookinfo.authors
-- If multiple authors (crengine separates them with \n), we
-- can display them on multiple lines, but limit to 2, and
-- append "et al." to the 2nd 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 > 1 and bookinfo.series and series_mode == "series_in_separate_line" then
authors = { T(_("%1 et al."), authors[1]) }
elseif #authors > 2 then
authors = { authors[1], T(_("%1 et al."), authors[2]) }
end
authors = table.concat(authors, "\n")
-- as we'll fit 3 lines instead of 2, we can avoid some loops by starting from a lower font size
fontsize_title = _fontSize(17)
fontsize_authors = _fontSize(15)
elseif authors then
authors = BD.auto(authors)
end
end
-- add Series metadata if requested
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 title then
title = title .. " - " .. bookinfo.series
else
title = bookinfo.series
end
end
if not authors then
if series_mode == "append_series_to_authors" or series_mode == "series_in_separate_line" then
authors = bookinfo.series
end
else
if series_mode == "append_series_to_authors" then
authors = authors .. " - " .. bookinfo.series
elseif series_mode == "series_in_separate_line" then
authors = bookinfo.series .. "\n" .. authors
-- as we'll fit 3 lines instead of 2, we can avoid some loops by starting from a lower font size
fontsize_title = _fontSize(17)
fontsize_authors = _fontSize(15)
end
end
end
if bookinfo.unsupported then
-- Let's show this fact in place of the anyway empty authors slot
authors = T(_("(no book information: %1)"), bookinfo.unsupported)
end
-- Build title and authors texts with decreasing font size
-- till it fits in the space available
while true do
-- Free previously made widgets to avoid memory leaks
if wtitle then
wtitle:free()
end
if wauthors then
wauthors:free()
wauthors = nil
end
-- BookInfoManager:extractBookInfo() made sure
-- to save as nil (NULL) metadata that were an empty string
-- We provide the book language to get a chance to render title
-- and authors with alternate glyphs for that language.
wtitle = TextBoxWidget:new{
text = title,
lang = bookinfo.language,
face = Font:getFace(fontname_title, fontsize_title),
width = wmain_width,
alignment = "left",
bold = true,
fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil,
}
local height = wtitle:getSize().h
if authors then
wauthors = TextBoxWidget:new{
text = authors,
lang = bookinfo.language,
face = Font:getFace(fontname_authors, fontsize_authors),
width = wmain_width,
alignment = "left",
fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil,
}
height = height + wauthors:getSize().h
end
if height < dimen.h then -- we fit !
break
end
-- If we don't fit, decrease both font sizes
fontsize_title = fontsize_title - fontsize_dec_step
fontsize_authors = fontsize_authors - fontsize_dec_step
logger.dbg(title, "recalculate title/author with", fontsize_title)
-- Don't go too low, and get out of this loop
if fontsize_title < 3 or fontsize_authors < 3 then
break
end
end
local wmain = LeftContainer:new{
dimen = dimen,
VerticalGroup:new{
wtitle,
wauthors,
}
}
-- Build the final widget
widget = OverlapGroup:new{
dimen = dimen,
}
if self.do_cover_image then
-- add left widget
if wleft then
-- no need for left padding, as cover image, most often in
-- portrait mode, will have some padding - the rare landscape
-- mode cover image will be stuck to screen side thus
table.insert(widget, wleft)
end
-- pad main widget on the left with size of left widget
wmain = HorizontalGroup:new{
HorizontalSpan:new{ width = wleft_width },
HorizontalSpan:new{ width = wmain_left_padding },
wmain
}
else
-- pad main widget on the left
wmain = HorizontalGroup:new{
HorizontalSpan:new{ width = wmain_left_padding },
wmain
}
end
-- add padded main widget
table.insert(widget, LeftContainer:new{
dimen = dimen,
wmain
})
-- add right widget
table.insert(widget, RightContainer:new{
dimen = dimen,
HorizontalGroup:new{
wright,
HorizontalSpan:new{ width = wright_right_padding },
},
})
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 remaking the same widget
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
--
if self.do_hint_opened and DocSettings:hasSidecarFile(self.filepath) then
self.been_opened = true
end
-- No right widget by default, except in History
local wright
local wright_width = 0
local wright_right_padding = 0
if self.mandatory_func then
-- Currently only provided by History, giving the last time read.
-- If we have it, we need to build a more complex widget with
-- this date on the right
local fileinfo_str = self.mandatory_func()
local fontsize_info = math.min(max_fontsize_fileinfo, _fontSize(14))
local wfileinfo = TextWidget:new{
text = fileinfo_str,
face = Font:getFace("cfont", fontsize_info),
fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil,
}
local wpageinfo = TextWidget:new{ -- Empty but needed for similar positionning
text = "",
face = Font:getFace("cfont", fontsize_info),
}
wright_width = wfileinfo:getSize().w
wright = CenterContainer:new{
dimen = Geom:new{ w = wright_width, h = dimen.h },
VerticalGroup:new{
align = "right",
VerticalSpan:new{ width = Screen:scaleBySize(2) },
wfileinfo,
wpageinfo,
}
}
wright_right_padding = Screen:scaleBySize(10)
end
-- A real simple widget, nothing fancy
local hint = "" -- display hint it's being loaded
if self.file_deleted then -- unless file was deleted (can happen with History)
hint = " " .. _("(deleted)")
end
local text = BD.filename(self.text)
local text_widget
local fontsize_no_bookinfo = _fontSize(18)
repeat
if text_widget then
text_widget:free()
end
text_widget = TextBoxWidget:new{
text = text .. hint,
face = Font:getFace("cfont", fontsize_no_bookinfo),
width = dimen.w - 2 * Screen:scaleBySize(10) - wright_width - wright_right_padding,
alignment = "left",
fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil,
}
-- reduce font size for next loop, in case text widget is too large to fit into ListMenuItem
fontsize_no_bookinfo = fontsize_no_bookinfo - fontsize_dec_step
until text_widget:getSize().h <= dimen.h
widget = LeftContainer:new{
dimen = dimen,
HorizontalGroup:new{
HorizontalSpan:new{ width = Screen:scaleBySize(10) },
text_widget
},
}
if wright then -- last read date, in History, even for deleted files
widget = OverlapGroup:new{
dimen = dimen,
widget,
RightContainer:new{
dimen = dimen,
HorizontalGroup:new{
wright,
HorizontalSpan:new{ width = wright_right_padding },
},
},
}
end
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
-- Add some pad at top to balance with hidden underline line at bottom
self._underline_container[1] = VerticalGroup:new{
VerticalSpan:new{ width = self.underline_h },
widget
}
end
function ListMenuItem: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("ListMenuItem: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 bottom left corner of sub-widget
local target = self[1][1][2]
local ix
if BD.mirroredUILayout() then
ix = target.dimen.w - self.shortcut_icon.dimen.w
else
ix = 0
end
local iy = target.dimen.h - self.shortcut_icon.dimen.h
self.shortcut_icon:paintTo(bb, x+ix, y+iy)
end
-- to which we paint over a dogear if needed
if corner_mark and self.do_hint_opened and self.been_opened then
-- align it on bottom right corner of widget
local ix
if BD.mirroredUILayout() then
ix = 0
else
ix = self.width - corner_mark:getSize().w
end
local iy = self.height - corner_mark:getSize().h
corner_mark:paintTo(bb, x+ix, y+iy)
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
local target = self[1][1][2]
local d_w = Screen:scaleBySize(3)
local d_h = math.ceil(target.dimen.h / 4)
if self.do_cover_image and target[1][1][1] then
-- it has an image, align it on image's framecontainer's right border
target = target[1][1]
local ix
if BD.mirroredUILayout() then
ix = target.dimen.x - d_w + 1
else
ix = target.dimen.x + target.dimen.w - 1
end
bb:paintBorder(ix, target.dimen.y, d_w, d_h, 1)
else
-- no image, align it to the left border
local ix
if BD.mirroredUILayout() then
ix = target.dimen.x + target.dimen.w - d_w
else
ix = x
end
bb:paintBorder(ix, y, d_w, d_h, 1)
end
end
end
-- As done in MenuItem
function ListMenuItem:onFocus()
self._underline_container.color = Blitbuffer.COLOR_BLACK
return true
end
function ListMenuItem:onUnfocus()
self._underline_container.color = Blitbuffer.COLOR_WHITE
return true
end
function ListMenuItem: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 ListMenuItem:onTapSelect(arg)
self.menu:onMenuSelect(self.entry)
return true
end
function ListMenuItem: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 ListMenu = {}
function ListMenu:_recalculateDimen()
self.dimen.w = self.width
self.dimen.h = self.height or Screen:getHeight()
-- Find out available height from other UI elements made in Menu
self.others_height = 0
if self.title_bar then -- Menu: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
else
-- Menu:init() not yet done: other elements used to calculate self.others_heights
-- are not yet defined, so next calculations will be wrong, and we may get
-- a self.perpage higher than it should be: Menu:init() will set a wrong self.page.
-- We'll have to update it, if we want FileManager to get back to the original page.
self.page_recalc_needed_next_time = true
-- Also remember original position (and focused_path), which will be changed by
-- Menu/FileChooser to a probably wrong value
self.itemnum_orig = self.path_items[self.path]
self.focused_path_orig = self.focused_path
end
local available_height = self.dimen.h - self.others_height - Size.line.thin
-- (Note: we can't assign directly to self.perpage and expect it to
-- be 'nil' if it was not defined, as we'll find instead the value
-- defined in the Menu class (14) because of inheritance.)
local files_per_page = BookInfoManager:getSetting("files_per_page")
if files_per_page then
self.perpage = tonumber(files_per_page)
else
-- perpage used to be static and computed from a base of 64px per ListMenuItem,
-- which gave 10 items both in filemanager and history on kobo glo hd.
-- Now that we can change the nb of items, let's start with a similar default
-- and save it so it's known as the initial value by the menu selection widget.
self.perpage = math.floor(available_height / scale_by_size / 64)
BookInfoManager:saveSetting("files_per_page", tostring(self.perpage))
end
self.cover_sizetag = "s" .. self.perpage
if Screen:getWidth() > Screen:getHeight() then -- landscape mode
-- When in landscape mode (only possible with History), adjust
-- perpage so items get a chance to have about the same height
-- as when in portrait mode.
-- This computation is not strictly correct, as "others_height" would
-- have a different value in portrait mode. But let's go with that.
local portrait_available_height = Screen:getWidth() - self.others_height - Size.line.thin
local portrait_item_height = math.floor(portrait_available_height / self.perpage) - Size.line.thin
self.perpage = Math.round(available_height / portrait_item_height)
end
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
-- menu item height based on number of items per page
-- add space for the separator
self.item_height = math.floor(available_height / self.perpage) - Size.line.thin
self.item_width = self.dimen.w
self.item_dimen = Geom:new{
w = self.item_width,
h = self.item_height
}
-- upper limit for file info font to leave enough space for title and author
max_fontsize_fileinfo = available_height / scale_by_size / 32
if self.page_recalc_needed then
-- self.page has probably been set to a wrong value, we recalculate
-- it here as done in Menu:init() or Menu:switchItemTable()
if #self.item_table > 0 then
self.page = math.ceil((self.itemnum_orig or 1) / self.perpage)
end
if self.focused_path_orig then
for num, item in ipairs(self.item_table) do
if item.path == self.focused_path_orig then
self.page = math.floor((num-1) / self.perpage) + 1
break
end
end
end
if self.page_num > 0 and self.page > self.page_num then self.page = self.page_num end
self.page_recalc_needed = nil
self.itemnum_orig = nil
self.focused_path_orig = nil
end
if self.page_recalc_needed_next_time then
self.page_recalc_needed = true
self.page_recalc_needed_next_time = nil
end
end
function ListMenu:_updateItemsBuildUI()
-- Build our list
table.insert(self.item_group, LineWidget:new{
dimen = Geom:new{ w = self.width, h = Size.line.thin },
background = Blitbuffer.COLOR_DARK_GRAY,
style = "solid",
})
local idx_offset = (self.page - 1) * self.perpage
for idx = 1, self.perpage do
local entry = self.item_table[idx_offset + idx]
if entry == nil then break 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 = ListMenuItem:new{
height = self.item_height,
width = self.item_width,
entry = entry,
text = getMenuText(entry),
show_parent = self.show_parent,
mandatory = entry.mandatory,
mandatory_func = entry.mandatory_func,
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,
do_filename_only = self._do_filename_only,
}
table.insert(self.item_group, item_tmp)
table.insert(self.item_group, LineWidget:new{
dimen = Geom:new{ w = self.width, h = Size.line.thin },
background = Blitbuffer.COLOR_DARK_GRAY,
style = "solid",
})
-- 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
end
return ListMenu