mirror of
https://github.com/koreader/koreader
synced 2024-11-10 01:10:34 +00:00
9cd305177e
* FocusManager: Fix `focus_flags` check in `moveFocusTo` (0 is truthy in Lua, can't do AND checks like in C ;).) * FileManager+FileChooser: Pass our custom title bar directly to FileChooser (which also means we can now use FC's FocusManager layout directly). * FileChooser/Menu: Get rid of the weird `outer_title_bar` hack, and simply take a `custom_title_bar` pointer to an actual TitleBar instance instead. * FileManager/Menu/ListMenu/CoverMenu: Fix content height computations in `_recalculateDimen` (all the non-FM cases were including an old and now unused padding value, `self.header_padding`, leading to more blank space at the bottom than necessary, and, worse, leading to different item heights between FM views, possibly leading to unnecessary thumbnail scaling !) * ButtonDialog: Proper focus management when the ButtonTable is wrapped in a ScrollableContainer. * ConfigDialog: Implement a stupid workaround for a weird FocusManager issue when going back from `[⋮]` buttons. * ConfigDialog: Don't move the visual focus in `update` (i.e., we use `NOT_FOCUS` now that it works as intended). * DictQuickLookup: Ensures the `Menu` key bind does the exact same thing as the hamburger icon. * DictQuickLookup: Ensure we refocus after having mangled the FocusManager layout (prevents an old focus highlight from lingering on the wrong button). * FileChooser: Stop flagging it as no_title, because it is *never* without a title. (This behavior was a remnant of the previous FM-specific title bar hacks, which are no longer a thing). * FileChooser: Stop calling `mergeTitleBarIntoLayout` twice in `updateItems`. We already call Menu's, which handles it. (Prevents the title bar from being added twice to the FocusManager layout). * FocusManager: Relax the `Unfocus` checks in `moveFocusTo` to ensure we *always* unfocus something (if unfocusing was requested), even if we have to blast the whole widget tree to do so. This ensures callers that mangle self.layout can expect things to work after calling it regardless of how borked the current focus is. * FocusManager: Allow passing `focus_flags` to `refocusWidget`, so that it can be forwarded to the internal `moveFocusTo` call. * FocusManager: The above also allows us to enforce a default that ensures we do *not* send a Focus event on Touch devices, even if they have the hasDPad devcap. This essentially restores the previous/current behavior of not showing the visual feedback from such focus "events" sent programmatically, given the `focus_flags` check fix at the root of this PR ;). * InputDialog: Fix numerous issues relating to double/ghost instances of both InputText and VirtualKeyboard, ensuring we only ever have a single InputText & VK instance live. * InputDialog: Make sure every way we have of hiding the VK play nice together, especially when the `toggleKeyboard` button (shown w/ `add_nav_bar`) is at play. And doubly so when we're `fullscreen`, as hiding the VK implies resizing the widget. * InputText: Make sure we're flagged as in-focus when tapping inside the text field. * InputText: Make sure we don't attempt to show an already-visible VK in the custom `hasDPad` `onFocus` handler. * Menu: Get rid of an old and no longer used (nor meaningful) hack in `onFocus` about the initial/programmatically-sent Focus event. * Menu: Get rid of the unused `header_padding` field mentioned earlier in the FM/FC fixes. * Menu: Use `FOCUS_ONLY_ON_NT` in the explicit `moveFocusTo` call in `updatePageInfo`, so as to keep the current behavior of not showing the visual feedback of this focus on Touch devices. * Menu: Make sure *all* the `moveFocusTo` calls are gated behind the `hasDPad` devcap (previously, that was only the case for `updatePageInfo`, but not `mergeTitleBarIntoLayout` (which is called by `updateItems`). * MultiInputDialog: Actively get rid of the InputText & VK instances from the base class's constructor that we do not use. * MultiInputDialog: Ensure the FocusManager layout is *slightly* less broken (password fields can still be a bit weird, though). * TextViewer: Get rid of the unfocus -> layout mangling -> refocus hack now that `refocusWidget` handles this case sanely. * VirtualKeyboard: Notify our parent InputDialog when we get closed, so it can act accordingly (e.g., resize itself when `fullscreen`). * ScrollableContainer: Implement the necessary machinery for focus handling inside ButtonDialog (specifically, when scrolling via PgUp/PgDwn). * TextEditor: Given the above fixes, the plugin is no longer disabled on non-touch devices. * ReaderBookMark: Make sure we request a full refresh when closing the "Edit note" dialog, as CRe highlights may extend past its dimensions, and if it's closed separately from VK, the refresh would have been limited to its own dimensions, leaving a neat InputDialog-sized hole in the highlights ;).
1023 lines
40 KiB
Lua
1023 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 InputContainer = require("ui/widget/container/inputcontainer")
|
|
local LeftContainer = require("ui/widget/container/leftcontainer")
|
|
local OverlapGroup = require("ui/widget/overlapgroup")
|
|
local ProgressWidget = require("ui/widget/progresswidget")
|
|
local ReadCollection = require("readcollection")
|
|
local Size = require("ui/size")
|
|
local TextBoxWidget = require("ui/widget/textboxwidget")
|
|
local TextWidget = require("ui/widget/textwidget")
|
|
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 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
|
|
local corner_mark
|
|
local reading_mark
|
|
local abandoned_mark
|
|
local complete_mark
|
|
local collection_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{ x = 0, y = 0, 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:copy(),
|
|
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,
|
|
dimen = nil,
|
|
_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 icon_width = math.floor(self.dimen.h*1/5)
|
|
local shortcut_icon_dimen = Geom:new{
|
|
x = 0, y = 0,
|
|
w = icon_width,
|
|
h = icon_width,
|
|
}
|
|
-- 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.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)
|
|
local underline_h = Size.line.focus_indicator
|
|
local underline_padding = Size.padding.tiny
|
|
self._underline_container = UnderlineContainer:new{
|
|
vertical_align = "top",
|
|
padding = underline_padding,
|
|
dimen = Geom:new{
|
|
x = 0, y = 0,
|
|
w = self.width,
|
|
h = self.height + underline_h + underline_padding,
|
|
},
|
|
linesize = underline_h,
|
|
-- widget : will be filled in self:update()
|
|
}
|
|
self[1] = self._underline_container
|
|
-- (This MosaicMenuItem will be taller than self.height, but will be put
|
|
-- in a Container with a fixed height=item_height, so it will overflow it
|
|
-- on the bottom, in the room made by item_margin=Screen:scaleBySize(10),
|
|
-- so we should ensure underline_h + underline_padding stays below that.)
|
|
|
|
-- 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,
|
|
}
|
|
|
|
-- 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 = {
|
|
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
|
|
|
|
self.is_directory = not (self.entry.is_file or self.entry.file)
|
|
if self.is_directory then
|
|
-- 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 -- file
|
|
self.file_deleted = self.entry.dim -- entry with deleted file from History or selected file from FM
|
|
|
|
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 and not self.file_deleted then
|
|
if bookinfo.cover_fetched then
|
|
if bookinfo.has_cover and not self.menu.no_refresh_covers then
|
|
if BookInfoManager.isCachedCoverInvalid(bookinfo, cover_specs) then
|
|
-- there is a thumbnail, but it's smaller than is needed for new grid dimensions,
|
|
-- and it would be ugly if scaled up to the required 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
|
|
end
|
|
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 = BookInfoManager.getCachedCoverSize(bookinfo.cover_w, bookinfo.cover_h, max_img_w, max_img_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
|
|
|
|
-- other paintings are anchored to the sub-widget (cover image)
|
|
local target = self[1][1][1]
|
|
|
|
if self.entry.order == nil -- File manager, History
|
|
and ReadCollection:isFileInCollections(self.filepath) then
|
|
-- top right corner
|
|
local ix, rect_ix
|
|
if BD.mirroredUILayout() then
|
|
ix = math.floor((self.width - target.dimen.w)/2)
|
|
rect_ix = target.bordersize
|
|
else
|
|
ix = self.width - math.ceil((self.width - target.dimen.w)/2) - corner_mark_size
|
|
rect_ix = 0
|
|
end
|
|
local iy = 0
|
|
local rect_size = corner_mark_size - target.bordersize
|
|
bb:paintRect(x+ix+rect_ix, target.dimen.y+target.bordersize, rect_size, rect_size, Blitbuffer.COLOR_GRAY)
|
|
collection_mark:paintTo(bb, x+ix, target.dimen.y+iy)
|
|
end
|
|
|
|
if self.do_hint_opened and self.been_opened then
|
|
-- bottom right corner
|
|
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_size
|
|
end
|
|
local iy = self.height - math.ceil((self.height - target.dimen.h)/2) - corner_mark_size
|
|
-- 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 progress_widget_margin = math.floor((corner_mark_size - progress_widget.height) / 2)
|
|
progress_widget.width = target.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 - target.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 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
|
|
|
|
-- 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()
|
|
self.portrait_mode = Screen:getWidth() <= Screen:getHeight()
|
|
if self.portrait_mode then
|
|
self.nb_cols = self.nb_cols_portrait
|
|
self.nb_rows = self.nb_rows_portrait
|
|
else
|
|
self.nb_cols = self.nb_cols_landscape
|
|
self.nb_rows = self.nb_rows_landscape
|
|
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.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{
|
|
x = 0, y = 0,
|
|
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,
|
|
}
|
|
if collection_mark then
|
|
collection_mark:free()
|
|
end
|
|
collection_mark = IconWidget:new{
|
|
icon = "star.white",
|
|
width = corner_mark_size,
|
|
height = corner_mark_size,
|
|
alpha = true,
|
|
}
|
|
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 cur_row = nil
|
|
local idx_offset = (self.page - 1) * self.perpage
|
|
local line_layout = {}
|
|
for idx = 1, self.perpage do
|
|
local index = idx_offset + idx
|
|
local entry = self.item_table[index]
|
|
if entry == nil then break end
|
|
entry.idx = index
|
|
-- Keyboard shortcuts, as done in Menu
|
|
local item_shortcut, shortcut_style
|
|
if self.is_enable_shortcut then
|
|
item_shortcut = self.item_shortcuts[idx]
|
|
shortcut_style = (idx < 11 or idx > 20) and "square" or "grey_square"
|
|
end
|
|
|
|
if idx % self.nb_cols == 1 then -- new row
|
|
if idx > 1 then
|
|
table.insert(self.layout, line_layout)
|
|
end
|
|
line_layout = {}
|
|
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
|
|
|
|
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:copy(),
|
|
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(line_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.layout, line_layout)
|
|
table.insert(self.item_group, VerticalSpan:new{ width = self.item_margin }) -- bottom padding
|
|
end
|
|
|
|
return MosaicMenu
|