Files
koreader/frontend/apps/reader/modules/readerbookmark.lua
NiLuJe 9cd305177e FocusManager: Fix focus_flags check in moveFocusTo, and deal with the fallout (#12361)
* 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 ;).
2024-08-25 19:34:31 +02:00

1486 lines
56 KiB
Lua

local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local ButtonDialog = require("ui/widget/buttondialog")
local CenterContainer = require("ui/widget/container/centercontainer")
local CheckButton = require("ui/widget/checkbutton")
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local Event = require("ui/event")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local InputContainer = require("ui/widget/container/inputcontainer")
local InputDialog = require("ui/widget/inputdialog")
local LineWidget = require("ui/widget/linewidget")
local Menu = require("ui/widget/menu")
local Size = require("ui/size")
local SpinWidget = require("ui/widget/spinwidget")
local TextViewer = require("ui/widget/textviewer")
local UIManager = require("ui/uimanager")
local Utf8Proc = require("ffi/utf8proc")
local util = require("util")
local _ = require("gettext")
local N_ = _.ngettext
local Screen = require("device").screen
local T = require("ffi/util").template
local ReaderBookmark = InputContainer:extend{
-- mark the type of a bookmark with a symbol + non-expandable space
display_prefix = {
highlight = "\u{2592}\u{2002}", -- "medium shade"
note = "\u{F040}\u{2002}", -- "pencil"
bookmark = "\u{F097}\u{2002}", -- "empty bookmark"
},
display_type = {
highlight = _("highlights"),
note = _("notes"),
bookmark = _("page bookmarks"),
},
}
function ReaderBookmark:init()
self:registerKeyEvents()
if G_reader_settings:hasNot("bookmarks_items_per_page") then
-- The Bookmarks items per page and items' font size can now be
-- configured. Previously, the ones set for the file browser
-- were used. Initialize them from these ones.
local items_per_page = G_reader_settings:readSetting("items_per_page") or Menu.items_per_page_default
G_reader_settings:saveSetting("bookmarks_items_per_page", items_per_page)
local items_font_size = G_reader_settings:readSetting("items_font_size")
if items_font_size and items_font_size ~= Menu.getItemFontSize(items_per_page) then
-- Keep the user items font size if it's not the default for items_per_page
G_reader_settings:saveSetting("bookmarks_items_font_size", items_font_size)
end
end
self.items_text = G_reader_settings:readSetting("bookmarks_items_text_type", "note")
self.items_max_lines = G_reader_settings:readSetting("bookmarks_items_max_lines")
self.ui.menu:registerToMainMenu(self)
-- NOP our own gesture handling
self.ges_events = nil
end
function ReaderBookmark:onGesture() end
function ReaderBookmark:registerKeyEvents()
if Device:hasKeyboard() then
self.key_events.ShowBookmark = { { "B" }, { "Shift", "Left" } }
self.key_events.ToggleBookmark = { { "Shift", "Right" } }
elseif Device:hasScreenKB() then
self.key_events.ShowBookmark = { { "ScreenKB", "Left" } }
self.key_events.ToggleBookmark = { { "ScreenKB", "Right" } }
end
end
ReaderBookmark.onPhysicalKeyboardConnected = ReaderBookmark.registerKeyEvents
function ReaderBookmark:addToMainMenu(menu_items)
menu_items.bookmarks = {
text = _("Bookmarks"),
callback = function()
self:onShowBookmark()
end,
}
if not Device:isTouchDevice() and not (Device:hasScreenKB() or Device:hasSymKey()) then
menu_items.toggle_bookmark = {
text_func = function()
return self:isPageBookmarked() and _("Remove bookmark for current page") or _("Bookmark current page")
end,
callback = function()
self:onToggleBookmark()
end,
}
end
if self.ui.paging then
menu_items.bookmark_browsing_mode = {
text = _("Bookmark browsing mode"),
checked_func = function()
return self.ui.paging.bookmark_flipping_mode
end,
callback = function(touchmenu_instance)
self.ui.paging:onToggleBookmarkFlipping()
touchmenu_instance:closeMenu()
end,
}
end
menu_items.bookmarks_settings = {
text = _("Bookmarks"),
sub_item_table = {
{
text_func = function()
return T(_("Max lines per bookmark: %1"), self.items_max_lines or _("disabled"))
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
local default_value = 4
local spin_wodget = SpinWidget:new{
title_text = _("Max lines per bookmark"),
info_text = _("Set maximum number of lines to enable flexible item heights."),
value = self.items_max_lines or default_value,
value_min = 1,
value_max = 10,
default_value = default_value,
ok_always_enabled = true,
callback = function(spin)
G_reader_settings:saveSetting("bookmarks_items_max_lines", spin.value)
self.items_max_lines = spin.value
touchmenu_instance:updateItems()
end,
extra_text = _("Disable"),
extra_callback = function()
G_reader_settings:delSetting("bookmarks_items_max_lines")
self.items_max_lines = nil
touchmenu_instance:updateItems()
end,
}
UIManager:show(spin_wodget)
end,
},
{
text_func = function()
local curr_perpage = self.items_max_lines and _("flexible")
or G_reader_settings:readSetting("bookmarks_items_per_page")
return T(_("Bookmarks per page: %1"), curr_perpage)
end,
enabled_func = function()
return not self.items_max_lines
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
local curr_perpage = G_reader_settings:readSetting("bookmarks_items_per_page")
local items = SpinWidget:new{
title_text = _("Bookmarks per page"),
value = curr_perpage,
value_min = 6,
value_max = 24,
default_value = Menu.items_per_page_default,
callback = function(spin)
G_reader_settings:saveSetting("bookmarks_items_per_page", spin.value)
touchmenu_instance:updateItems()
end,
}
UIManager:show(items)
end,
},
{
text_func = function()
local curr_perpage = G_reader_settings:readSetting("bookmarks_items_per_page")
local default_font_size = Menu.getItemFontSize(curr_perpage)
local curr_font_size = G_reader_settings:readSetting("bookmarks_items_font_size", default_font_size)
return T(_("Bookmark font size: %1"), curr_font_size)
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
local curr_perpage = G_reader_settings:readSetting("bookmarks_items_per_page")
local default_font_size = Menu.getItemFontSize(curr_perpage)
local curr_font_size = G_reader_settings:readSetting("bookmarks_items_font_size", default_font_size)
local items_font = SpinWidget:new{
title_text = _("Bookmark font size"),
value = curr_font_size,
value_min = 10,
value_max = 72,
default_value = default_font_size,
callback = function(spin)
G_reader_settings:saveSetting("bookmarks_items_font_size", spin.value)
touchmenu_instance:updateItems()
end,
}
UIManager:show(items_font)
end,
},
{
text = _("Shrink bookmark font size to fit more text"),
enabled_func = function()
return not self.items_max_lines
end,
checked_func = function()
return not self.items_max_lines and G_reader_settings:isTrue("bookmarks_items_multilines_show_more_text")
end,
callback = function()
G_reader_settings:flipNilOrFalse("bookmarks_items_multilines_show_more_text")
end,
separator = true,
},
{
text_func = function()
return T(_("Show in items: %1"), self:genShowInItemsMenuItems())
end,
sub_item_table = {
self:genShowInItemsMenuItems("text"),
self:genShowInItemsMenuItems("all"),
self:genShowInItemsMenuItems("note"),
},
},
{
text = _("Show separator between items"),
checked_func = function()
return G_reader_settings:isTrue("bookmarks_items_show_separator")
end,
callback = function()
G_reader_settings:flipNilOrFalse("bookmarks_items_show_separator")
end,
separator = true,
},
{
text_func = function()
return T(_("Sort by: %1"), self:genSortByMenuItems())
end,
sub_item_table = {
self:genSortByMenuItems("page"),
self:genSortByMenuItems("date", true),
-- separator
{
text = _("Reverse sorting"),
checked_func = function()
return G_reader_settings:isTrue("bookmarks_items_reverse_sorting")
end,
callback = function()
G_reader_settings:flipNilOrFalse("bookmarks_items_reverse_sorting")
end,
},
},
},
},
}
menu_items.bookmark_search = {
text = _("Bookmark search"),
enabled_func = function()
return self.ui.annotation:hasAnnotations()
end,
callback = function()
self:onSearchBookmark()
end,
}
end
function ReaderBookmark:genShowInItemsMenuItems(value)
local strings = {
text = _("highlighted text"),
all = _("highlighted text and note"),
note = _("note if set, otherwise highlighted text"),
}
if value == nil then
value = G_reader_settings:readSetting("bookmarks_items_text_type", "note")
return strings[value]
end
return {
text = strings[value],
checked_func = function()
return self.items_text == value
end,
radio = true,
callback = function()
self.items_text = value
G_reader_settings:saveSetting("bookmarks_items_text_type", value)
end,
}
end
function ReaderBookmark:genSortByMenuItems(value, separator)
local strings = {
page = _("page number"),
date = _("date"),
}
local strings_reverse = {
page = _("page number, reverse"),
date = _("date, reverse"),
}
if value == nil then
local curr_value = G_reader_settings:readSetting("bookmarks_items_sorting") or "page"
if G_reader_settings:isTrue("bookmarks_items_reverse_sorting") then
return strings_reverse[curr_value]
else
return strings[curr_value]
end
end
return {
text = strings[value],
checked_func = function()
return value == (G_reader_settings:readSetting("bookmarks_items_sorting") or "page")
end,
radio = true,
callback = function()
G_reader_settings:saveSetting("bookmarks_items_sorting", value ~= "page" and value or nil)
end,
separator = separator,
}
end
-- page bookmarks, dogear
function ReaderBookmark:onToggleBookmark()
self:toggleBookmark()
self.view.dogear:onSetDogearVisibility(not self.view.dogear_visible)
-- Refresh the dogear first, because it might inherit ReaderUI refresh hints.
UIManager:setDirty(self.view.dialog, function()
return "ui",
self.view.dogear:getRefreshRegion()
end)
-- And ask for a footer refresh, in case we have bookmark_count enabled.
-- Assuming the footer is visible, it'll request a refresh regardless, but the EPDC should optimize it out if no content actually changed.
self.view.footer:onUpdateFooter(self.view.footer_visible)
return true
end
function ReaderBookmark:toggleBookmark(pageno)
local pn_or_xp, item
if pageno then
if self.ui.rolling then
pn_or_xp = self.ui.document:getPageXPointer(pageno)
else
pn_or_xp = pageno
end
else
pn_or_xp = self:getCurrentPageNumber()
end
local index = self:getDogearBookmarkIndex(pn_or_xp)
if index then
item = table.remove(self.ui.annotation.annotations, index)
else
local text
local chapter = self.ui.toc:getTocTitleByPage(pn_or_xp)
if chapter == "" then
chapter = nil
else
-- @translators In which chapter title (%1) a note is found.
text = T(_("in %1"), chapter)
end
item = {
page = pn_or_xp,
text = text,
chapter = chapter,
}
self.ui.annotation:addItem(item)
end
self.ui:handleEvent(Event:new("AnnotationsModified", { item }))
end
function ReaderBookmark:setDogearVisibility(pn_or_xp)
local visible = self:isPageBookmarked(pn_or_xp)
self.view.dogear:onSetDogearVisibility(visible)
end
function ReaderBookmark:isPageBookmarked(pn_or_xp)
local page = pn_or_xp or self:getCurrentPageNumber()
return self:getDogearBookmarkIndex(page) and true or false
end
function ReaderBookmark:isBookmarkInPageOrder(a, b)
local a_page = self:getBookmarkPageNumber(a)
local b_page = self:getBookmarkPageNumber(b)
if a_page == b_page then -- have page bookmarks before highlights
return not a.drawer
end
return a_page < b_page
end
function ReaderBookmark:getDogearBookmarkIndex(pn_or_xp)
local doesMatch
if self.ui.paging then
doesMatch = function(p1, p2)
return p1 == p2
end
else
doesMatch = function(p1, p2)
return self.ui.document:getPageFromXPointer(p1) == self.ui.document:getPageFromXPointer(p2)
end
end
local _middle
local _start, _end = 1, #self.ui.annotation.annotations
while _start <= _end do
_middle = bit.rshift(_start + _end, 1)
local v = self.ui.annotation.annotations[_middle]
if not v.drawer and doesMatch(v.page, pn_or_xp) then
return _middle
elseif self:isBookmarkInPageOrder({page = pn_or_xp}, v) then
_end = _middle - 1
else
_start = _middle + 1
end
end
end
-- remove, update bookmark
function ReaderBookmark:removeItem(item)
local index = self.ui.annotation:getItemIndex(item)
if item.pos0 then
self.ui.highlight:deleteHighlight(index) -- will call ReaderBookmark:removeItemByIndex()
else -- dogear bookmark, update it in case we removed a bookmark for current page
self:removeItemByIndex(index)
self:setDogearVisibility(self:getCurrentPageNumber())
end
end
function ReaderBookmark:removeItemByIndex(index)
local item = self.ui.annotation.annotations[index]
local item_type = self.getBookmarkType(item)
if item_type == "highlight" then
self.ui:handleEvent(Event:new("AnnotationsModified", { item, nb_highlights_added = -1 }))
elseif item_type == "note" then
self.ui:handleEvent(Event:new("AnnotationsModified", { item, nb_notes_added = -1 }))
end
table.remove(self.ui.annotation.annotations, index)
self.view.footer:onUpdateFooter(self.view.footer_visible)
end
function ReaderBookmark:deleteItemNote(item)
local index = self.ui.annotation:getItemIndex(item)
self.ui.annotation.annotations[index].note = nil
self.ui:handleEvent(Event:new("AnnotationsModified", { item, nb_highlights_added = 1, nb_notes_added = -1 }))
end
-- navigation
function ReaderBookmark:onPageUpdate(pageno)
local pn_or_xp = self.ui.paging and pageno or self.ui.document:getXPointer()
self:setDogearVisibility(pn_or_xp)
end
function ReaderBookmark:onPosUpdate(pos)
local pn_or_xp = self.ui.document:getXPointer()
self:setDogearVisibility(pn_or_xp)
end
function ReaderBookmark:gotoBookmark(pn_or_xp, marker_xp)
if pn_or_xp then
local event = self.ui.paging and "GotoPage" or "GotoXPointer"
self.ui:handleEvent(Event:new(event, pn_or_xp, marker_xp))
end
end
function ReaderBookmark:getNextBookmarkedPage(pn_or_xp, page_bookmark_only)
local pageno = self:getBookmarkPageNumber({page = pn_or_xp})
for i = 1, #self.ui.annotation.annotations do
local item = self.ui.annotation.annotations[i]
if (not page_bookmark_only or not item.drawer) and pageno < self:getBookmarkPageNumber(item) then
return item.page
end
end
end
function ReaderBookmark:getPreviousBookmarkedPage(pn_or_xp, page_bookmark_only)
local pageno = self:getBookmarkPageNumber({page = pn_or_xp})
for i = #self.ui.annotation.annotations, 1, -1 do
local item = self.ui.annotation.annotations[i]
if (not page_bookmark_only or not item.drawer) and pageno > self:getBookmarkPageNumber(item) then
return item.page
end
end
end
function ReaderBookmark:getFirstBookmarkedPage(pn_or_xp)
if #self.ui.annotation.annotations > 0 then
local pageno = self:getBookmarkPageNumber({page = pn_or_xp})
local item = self.ui.annotation.annotations[1]
if pageno > self:getBookmarkPageNumber(item) then
return item.page
end
end
end
function ReaderBookmark:getLastBookmarkedPage(pn_or_xp)
if #self.ui.annotation.annotations > 0 then
local pageno = self:getBookmarkPageNumber({page = pn_or_xp})
local item = self.ui.annotation.annotations[#self.ui.annotation.annotations]
if pageno < self:getBookmarkPageNumber(item) then
return item.page
end
end
end
function ReaderBookmark:onGotoPreviousBookmark(pn_or_xp)
self:gotoBookmark(self:getPreviousBookmarkedPage(pn_or_xp))
return true
end
function ReaderBookmark:onGotoNextBookmark(pn_or_xp)
self:gotoBookmark(self:getNextBookmarkedPage(pn_or_xp))
return true
end
function ReaderBookmark:onGotoPreviousBookmarkFromPage(add_current_location_to_stack)
if add_current_location_to_stack ~= false then -- nil or true
self.ui.link:addCurrentLocationToStack()
end
local pn_or_xp = self:getCurrentPageNumber()
self:gotoBookmark(self:getPreviousBookmarkedPage(pn_or_xp))
return true
end
function ReaderBookmark:onGotoNextBookmarkFromPage(add_current_location_to_stack)
if add_current_location_to_stack ~= false then -- nil or true
self.ui.link:addCurrentLocationToStack()
end
local pn_or_xp = self:getCurrentPageNumber()
self:gotoBookmark(self:getNextBookmarkedPage(pn_or_xp))
return true
end
function ReaderBookmark:onGotoFirstBookmark(add_current_location_to_stack)
if add_current_location_to_stack ~= false then -- nil or true
self.ui.link:addCurrentLocationToStack()
end
local pn_or_xp = self:getCurrentPageNumber()
self:gotoBookmark(self:getFirstBookmarkedPage(pn_or_xp))
return true
end
function ReaderBookmark:onGotoLastBookmark(add_current_location_to_stack)
if add_current_location_to_stack ~= false then -- nil or true
self.ui.link:addCurrentLocationToStack()
end
local pn_or_xp = self:getCurrentPageNumber()
self:gotoBookmark(self:getLastBookmarkedPage(pn_or_xp))
return true
end
-- bookmarks misc info, helpers
function ReaderBookmark:getCurrentPageNumber()
return self.ui.paging and self.view.state.page or self.ui.document:getXPointer()
end
function ReaderBookmark:getBookmarkPageNumber(bookmark)
return self.ui.paging and bookmark.page or self.ui.document:getPageFromXPointer(bookmark.page)
end
function ReaderBookmark.getBookmarkType(bookmark)
if bookmark.drawer then
if bookmark.note then
return "note"
end
return "highlight"
end
return "bookmark"
end
function ReaderBookmark:getLatestBookmark()
local latest_bookmark, latest_bookmark_idx
local latest_bookmark_datetime = "0"
for i, v in ipairs(self.ui.annotation.annotations) do
if v.datetime > latest_bookmark_datetime then
latest_bookmark_datetime = v.datetime
latest_bookmark = v
latest_bookmark_idx = i
end
end
return latest_bookmark, latest_bookmark_idx
end
function ReaderBookmark:getBookmarkedPages()
local pages = {}
for _, bm in ipairs(self.ui.annotation.annotations) do
local page = self:getBookmarkPageNumber(bm)
local btype = self.getBookmarkType(bm)
if not pages[page] then
pages[page] = {}
end
if not pages[page][btype] then
pages[page][btype] = true
end
end
return pages
end
function ReaderBookmark:getBookmarkPageString(page)
if self.ui.rolling then
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
return self.ui.pagemap:getXPointerPageLabel(page, true)
end
page = self.ui.document:getPageFromXPointer(page)
end
if self.ui.document:hasHiddenFlows() then
local flow = self.ui.document:getPageFlow(page)
page = self.ui.document:getPageNumberInFlow(page)
if flow > 0 then
page = T("[%1]%2", page, flow)
end
end
return tostring(page)
end
function ReaderBookmark:isBookmarkAutoText(bookmark)
-- old bookmarks only
if bookmark.text == "" or bookmark.text == bookmark.notes then
return true
end
local page = self:getBookmarkPageString(bookmark.page)
local auto_text = T(_("Page %1 %2 @ %3"), page, bookmark.notes, bookmark.datetime)
return bookmark.text == auto_text
end
-- bookmark list, dialogs
function ReaderBookmark:onShowBookmark()
self.sorting_mode = G_reader_settings:readSetting("bookmarks_items_sorting") or "page"
self.is_reverse_sorting = G_reader_settings:isTrue("bookmarks_items_reverse_sorting")
-- build up item_table
local item_table = {}
local curr_page_num = self:getCurrentPageNumber()
local curr_page_string = self:getBookmarkPageString(curr_page_num)
local curr_page_index = self.ui.annotation:getInsertionIndex({page = curr_page_num})
local num = #self.ui.annotation.annotations + 1
curr_page_index = self.is_reverse_sorting and num - curr_page_index or curr_page_index
local curr_page_index_filtered = curr_page_index
for i = 1, #self.ui.annotation.annotations do
local v = self.ui.annotation.annotations[self.is_reverse_sorting and num - i or i]
local item = util.tableDeepCopy(v)
item.text_orig = item.text or ""
item.type = self.getBookmarkType(item)
if not self.match_table or self:doesBookmarkMatchTable(item) then
item.text = self:getBookmarkItemText(item)
item.mandatory = self:getBookmarkPageString(item.page)
if (not self.is_reverse_sorting and i >= curr_page_index) or (self.is_reverse_sorting and i <= curr_page_index) then
item.after_curr_page = true
item.mandatory_dim = true
end
if item.mandatory == curr_page_string then
item.bold = true
item.after_curr_page = nil
item.mandatory_dim = nil
end
table.insert(item_table, item)
else
curr_page_index_filtered = curr_page_index_filtered - 1
end
end
local curr_page_datetime
if self.sorting_mode == "date" and #item_table > 0 then
local idx = math.max(1, math.min(curr_page_index_filtered, #item_table))
curr_page_datetime = item_table[idx].datetime
local sort_func = self.is_reverse_sorting and function(a, b) return a.datetime > b.datetime end
or function(a, b) return a.datetime < b.datetime end
table.sort(item_table, sort_func)
end
local items_per_page = G_reader_settings:readSetting("bookmarks_items_per_page")
local items_font_size = G_reader_settings:readSetting("bookmarks_items_font_size", Menu.getItemFontSize(items_per_page))
local multilines_show_more_text = G_reader_settings:isTrue("bookmarks_items_multilines_show_more_text")
local show_separator = G_reader_settings:isTrue("bookmarks_items_show_separator")
self.bookmark_menu = CenterContainer:new{
dimen = Screen:getSize(),
covers_fullscreen = true, -- hint for UIManager:_repaint()
}
local bm_menu = Menu:new{
title = T(_("Bookmarks (%1)"), #item_table),
item_table = item_table,
is_borderless = true,
is_popout = false,
title_bar_fm_style = true,
items_per_page = items_per_page,
items_font_size = items_font_size,
items_max_lines = self.items_max_lines,
multilines_show_more_text = multilines_show_more_text,
line_color = show_separator and Blitbuffer.COLOR_DARK_GRAY or Blitbuffer.COLOR_WHITE,
title_bar_left_icon = "appbar.menu",
on_close_ges = {
GestureRange:new{
ges = "two_finger_swipe",
range = Geom:new{
x = 0, y = 0,
w = Screen:getWidth(),
h = Screen:getHeight(),
},
direction = BD.flipDirectionIfMirroredUILayout("east")
}
},
show_parent = self.bookmark_menu,
}
table.insert(self.bookmark_menu, bm_menu)
local bookmark = self
function bm_menu:onMenuSelect(item)
if self.select_count then
if item.dim then
item.dim = nil
if item.after_curr_page then
item.mandatory_dim = true
end
self.select_count = self.select_count - 1
else
item.dim = true
self.select_count = self.select_count + 1
end
bookmark:updateBookmarkList(nil, -1)
else
bookmark.ui.link:addCurrentLocationToStack()
bookmark:gotoBookmark(item.page, item.pos0)
self.close_callback()
end
end
function bm_menu:onMenuHold(item)
bookmark:showBookmarkDetails(item)
return true
end
function bm_menu:toggleSelectMode()
if self.select_count then
self.select_count = nil
for _, v in ipairs(item_table) do
v.dim = nil
if v.after_curr_page then
v.mandatory_dim = true
end
end
self:setTitleBarLeftIcon("appbar.menu")
else
self.select_count = 0
self:setTitleBarLeftIcon("check")
end
bookmark:updateBookmarkList(nil, -1)
end
function bm_menu:onLeftButtonTap()
local bm_dialog, dialog_title
local buttons = {}
if self.select_count then
local actions_enabled = self.select_count > 0
local more_selections_enabled = self.select_count < #item_table
if actions_enabled then
dialog_title = T(N_("1 bookmark selected", "%1 bookmarks selected", self.select_count), self.select_count)
else
dialog_title = _("No bookmarks selected")
end
table.insert(buttons, {
{
text = _("Select all"),
enabled = more_selections_enabled,
callback = function()
UIManager:close(bm_dialog)
for _, v in ipairs(item_table) do
v.dim = true
end
self.select_count = #item_table
bookmark:updateBookmarkList(nil, -1)
end,
},
{
text = _("Select page"),
enabled = more_selections_enabled,
callback = function()
UIManager:close(bm_dialog)
local item_first = (bm_menu.page - 1) * bm_menu.perpage + 1
local item_last = math.min(item_first + bm_menu.perpage - 1, #item_table)
for i = item_first, item_last do
local v = item_table[i]
if v.dim == nil then
v.dim = true
self.select_count = self.select_count + 1
end
end
bookmark:updateBookmarkList(nil, -1)
end,
},
})
table.insert(buttons, {
{
text = _("Deselect all"),
enabled = actions_enabled,
callback = function()
UIManager:close(bm_dialog)
for _, v in ipairs(item_table) do
v.dim = nil
if v.after_curr_page then
v.mandatory_dim = true
end
end
self.select_count = 0
bookmark:updateBookmarkList(nil, -1)
end,
},
{
text = _("Delete note"),
enabled = actions_enabled,
callback = function()
UIManager:show(ConfirmBox:new{
text = _("Delete bookmark notes?"),
ok_text = _("Delete"),
ok_callback = function()
UIManager:close(bm_dialog)
for _, v in ipairs(item_table) do
if v.dim then
bookmark:deleteItemNote(v)
end
end
self:onClose()
bookmark:onShowBookmark()
end,
})
end,
},
})
table.insert(buttons, {
{
text = _("Exit select mode"),
callback = function()
UIManager:close(bm_dialog)
self:toggleSelectMode()
end,
},
{
text = _("Remove"),
enabled = actions_enabled and not bookmark.ui.highlight.select_mode,
callback = function()
UIManager:show(ConfirmBox:new{
text = _("Remove selected bookmarks?"),
ok_text = _("Remove"),
ok_callback = function()
UIManager:close(bm_dialog)
for i = #item_table, 1, -1 do
if item_table[i].dim then
bookmark:removeItem(item_table[i])
table.remove(item_table, i)
end
end
self.select_count = nil
self:setTitleBarLeftIcon("appbar.menu")
bookmark:updateBookmarkList(item_table, -1)
end,
})
end,
},
})
else -- select mode off
dialog_title = _("Filter by bookmark type")
local actions_enabled = #item_table > 0
local type_count = { highlight = 0, note = 0, bookmark = 0 }
for _, item in ipairs(bookmark.ui.annotation.annotations) do
local item_type = bookmark.getBookmarkType(item)
type_count[item_type] = type_count[item_type] + 1
end
local genBookmarkTypeButton = function(item_type)
return {
text = bookmark.display_prefix[item_type] ..
T(_("%1 (%2)"), bookmark.display_type[item_type], type_count[item_type]),
callback = function()
UIManager:close(bm_dialog)
self:onClose()
bookmark.match_table = { [item_type] = true }
bookmark:onShowBookmark()
end,
}
end
table.insert(buttons, {
{
text = _("All (reset filters)"),
callback = function()
UIManager:close(bm_dialog)
self:onClose()
bookmark:onShowBookmark()
end,
},
genBookmarkTypeButton("highlight"),
})
table.insert(buttons, {
genBookmarkTypeButton("bookmark"),
genBookmarkTypeButton("note"),
})
table.insert(buttons, {}) -- separator
table.insert(buttons, {
{
text = _("Filter by edited highlighted text"),
callback = function()
UIManager:close(bm_dialog)
bookmark:filterByEditedText()
end,
},
})
table.insert(buttons, {
{
text = _("Filter by highlight style"),
callback = function()
UIManager:close(bm_dialog)
bookmark:filterByHighlightStyle()
end,
},
})
table.insert(buttons, {}) -- separator
table.insert(buttons, {
{
text = _("Current page"),
callback = function()
UIManager:close(bm_dialog)
local idx
if bookmark.sorting_mode == "date" then
for i, v in ipairs(item_table) do
if v.datetime == curr_page_datetime then
idx = i
break
end
end
else -- "page"
idx = curr_page_index_filtered
end
bookmark:updateBookmarkList(nil, idx)
end,
},
{
text = _("Latest bookmark"),
enabled = actions_enabled
and not (bookmark.match_table or bookmark.show_edited_only or bookmark.show_drawer_only),
callback = function()
UIManager:close(bm_dialog)
local idx
if bookmark.sorting_mode == "date" then
idx = bookmark.is_reverse_sorting and 1 or #item_table
else -- "page"
idx = select(2, bookmark:getLatestBookmark())
idx = bookmark.is_reverse_sorting and #item_table - idx + 1 or idx
end
bookmark:updateBookmarkList(nil, idx)
bookmark:showBookmarkDetails(item_table[idx])
end,
},
})
table.insert(buttons, {
{
text = _("Select bookmarks"),
enabled = actions_enabled,
callback = function()
UIManager:close(bm_dialog)
self:toggleSelectMode()
end,
},
{
text = _("Search bookmarks"),
enabled = actions_enabled,
callback = function()
UIManager:close(bm_dialog)
bookmark:onSearchBookmark()
end,
},
})
end
bm_dialog = ButtonDialog:new{
title = dialog_title,
title_align = "center",
buttons = buttons,
}
UIManager:show(bm_dialog)
end
function bm_menu:onLeftButtonHold()
self:toggleSelectMode()
return true
end
bm_menu.close_callback = function()
UIManager:close(self.bookmark_menu)
self.bookmark_menu = nil
self.match_table = nil
self.show_edited_only = nil
self.show_drawer_only = nil
end
local idx
if bookmark.sorting_mode == "date" then -- show the most recent bookmark
idx = bookmark.is_reverse_sorting and 1 or #item_table
else -- "page", show bookmark in the current book page
idx = curr_page_index_filtered
end
self:updateBookmarkList(nil, idx)
UIManager:show(self.bookmark_menu)
return true
end
function ReaderBookmark:updateBookmarkList(item_table, item_number)
local bm_menu = self.bookmark_menu[1]
local title
if item_table then
title = T(_("Bookmarks (%1)"), #item_table)
end
local subtitle
if bm_menu.select_count then
subtitle = T(_("Selected: %1"), bm_menu.select_count)
else
if self.show_edited_only then
subtitle = _("Filter: edited highlighted text")
elseif self.show_drawer_only then
subtitle = _("Highlight style:") .. " " .. self.ui.highlight:getHighlightStyleString(self.show_drawer_only):lower()
elseif self.match_table then
if self.match_table.search_str then
subtitle = T(_("Query: %1"), self.match_table.search_str)
else
local types = {}
for type, type_string in pairs(self.display_type) do
if self.match_table[type] then
table.insert(types, type_string)
end
end
table.sort(types)
subtitle = #types > 0 and _("Bookmark type:") .. " " .. table.concat(types, ", ")
end
else
subtitle = ""
end
end
bm_menu:switchItemTable(title, item_table, item_number, nil, subtitle)
end
function ReaderBookmark:getBookmarkItemIndex(item)
if self.match_table or self.show_edited_only or self.show_drawer_only -- filtered
or self.sorting_mode ~= "page" then -- or item_table order does not match with annotations
return self.ui.annotation:getItemIndex(item)
end
if self.is_reverse_sorting then
return #self.ui.annotation.annotations - item.idx + 1
end
return item.idx
end
function ReaderBookmark:getBookmarkItemText(item)
local text
if item.type == "highlight" or self.items_text == "text" then
text = self.display_prefix[item.type] .. item.text_orig
else
if item.type == "note" and self.items_text == "note" then
text = self.display_prefix["note"] .. item.note
else
if item.type == "bookmark" then
text = self.display_prefix["bookmark"]
else -- it is a note, but we show the "highlight" prefix before the highlighted text
text = self.display_prefix["highlight"]
end
if self.items_text == "all" or self.items_text == "note" then
text = text .. item.text_orig
end
if item.note then
text = text .. "\u{2002}" .. self.display_prefix["note"] .. item.note
end
end
end
if self.sorting_mode == "date" then
text = item.datetime .. "\u{2002}" .. text
end
return text
end
function ReaderBookmark:_getDialogHeader(bookmark)
local page_str = bookmark.mandatory or self:getBookmarkPageString(bookmark.page)
return T(_("Page: %1"), page_str) .. " " .. T(_("Time: %1"), bookmark.datetime)
end
function ReaderBookmark:showBookmarkDetails(item)
local bm_menu = self.bookmark_menu[1]
local item_table = bm_menu.item_table
local text = self:_getDialogHeader(item) .. "\n\n"
local prefix = item.type == "bookmark" and self.display_prefix["bookmark"] or self.display_prefix["highlight"]
text = text .. prefix .. item.text_orig
if item.note then
text = text .. "\n\n" .. self.display_prefix["note"] .. item.note
end
local not_select_mode = not bm_menu.select_count and not self.ui.highlight.select_mode
local textviewer
local edit_details_callback = function()
self.details_updated = true
UIManager:close(textviewer)
self:showBookmarkDetails(item_table[item.idx])
end
local _showBookmarkDetails = function(idx)
UIManager:close(textviewer)
self:updateBookmarkList(nil, idx)
self:showBookmarkDetails(item_table[idx])
end
-- Refresh the bookmark list whenever details may have been edited
local _updateBookmarkList = function()
if self.details_updated then
self.details_updated = nil
if self.show_edited_only then
for i = #item_table, 1, -1 do
if not item_table[i].text_edited then
table.remove(item_table, i)
end
end
end
self:updateBookmarkList(item_table, -1)
end
end
textviewer = TextViewer:new{
title = T(_("Bookmark details (%1/%2)"), item.idx, #item_table),
text = text,
text_type = "bookmark",
close_callback = function()
_updateBookmarkList()
end,
buttons_table = {
{
{
text = _("Reset text"),
enabled = item.drawer and not_select_mode and item.text_edited or false,
callback = function()
self:setHighlightedText(item, nil, edit_details_callback)
end,
},
{
text = _("Edit text"),
enabled = item.drawer and not_select_mode or false,
callback = function()
self:editHighlightedText(item, edit_details_callback)
end,
},
},
{
{
text = _("Remove bookmark"),
enabled = not_select_mode,
callback = function()
UIManager:show(ConfirmBox:new{
text = _("Remove this bookmark?"),
ok_text = _("Remove"),
ok_callback = function()
self:removeItem(item)
table.remove(item_table, item.idx)
self:updateBookmarkList(item_table, -1)
UIManager:close(textviewer)
end,
})
end,
},
{
text = item.note and _("Edit note") or _("Add note"),
enabled = not bm_menu.select_count,
callback = function()
self:setBookmarkNote(item, nil, nil, edit_details_callback)
end,
},
},
{
{
text = _("Close"),
callback = function()
_updateBookmarkList()
UIManager:close(textviewer)
end,
},
{
text = _("Go to bookmark"),
enabled = not bm_menu.select_count,
callback = function()
UIManager:close(textviewer)
self.ui.link:addCurrentLocationToStack()
self:gotoBookmark(item.page, item.pos0)
bm_menu.close_callback()
end,
},
},
{
{
text = "▕◁",
enabled = item.idx > 1,
callback = function()
_showBookmarkDetails(1)
end,
},
{
text = "",
enabled = item.idx > 1,
callback = function()
_showBookmarkDetails(item.idx - 1)
end,
},
{
text = "",
enabled = item.idx < #item_table,
callback = function()
_showBookmarkDetails(item.idx + 1)
end,
},
{
text = "▷▏",
enabled = item.idx < #item_table,
callback = function()
_showBookmarkDetails(#item_table)
end,
},
},
}
}
UIManager:show(textviewer)
return true
end
function ReaderBookmark:setBookmarkNote(item_or_index, is_new_note, new_note, caller_callback)
local item, index
if self.bookmark_menu then
item = item_or_index -- in item_table
index = self:getBookmarkItemIndex(item)
else -- from Highlight
index = item_or_index
end
local annotation = self.ui.annotation.annotations[index]
local type_before = item and item.type or self.getBookmarkType(annotation)
local input_text = annotation.note
if new_note then
if input_text then
input_text = input_text .. "\n\n" .. new_note
else
input_text = new_note
end
end
local input_dialog
input_dialog = InputDialog:new{
title = _("Edit note"),
description = " " .. self:_getDialogHeader(annotation),
input = input_text,
allow_newline = true,
add_scroll_buttons = true,
use_available_height = true,
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
-- NOTE: We'll want a full refresh on close, as the CRe highlight may extend past our own dimensions,
-- especially if we're closed separately from our VirtualKeyboard.
UIManager:close(input_dialog, "flashui")
if is_new_note then -- "Add note" called from highlight dialog and cancelled, remove saved highlight
self:removeItemByIndex(index)
end
end,
},
{
text = _("Paste"), -- insert highlighted text
callback = function()
input_dialog:addTextToInput(annotation.text)
end,
},
{
text = _("Save"),
is_enter_default = true,
callback = function()
local value = input_dialog:getInputText()
if value == "" then -- blank input deletes note
value = nil
end
annotation.note = value
local type_after = self.getBookmarkType(annotation)
if type_before ~= type_after then
if type_before == "highlight" then
self.ui:handleEvent(Event:new("AnnotationsModified",
{ annotation, nb_highlights_added = -1, nb_notes_added = 1 }))
else
self.ui:handleEvent(Event:new("AnnotationsModified",
{ annotation, nb_highlights_added = 1, nb_notes_added = -1 }))
end
end
UIManager:close(input_dialog)
if item then
item.note = value
item.type = type_after
item.text = self:getBookmarkItemText(item)
end
caller_callback()
end,
},
}
},
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
end
function ReaderBookmark:editHighlightedText(item, caller_callback)
local input_dialog
input_dialog = InputDialog:new{
title = _("Edit highlighted text"),
description = " " .. self:_getDialogHeader(item),
input = item.text_orig,
allow_newline = true,
add_scroll_buttons = true,
use_available_height = true,
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
},
{
text = _("Save"),
is_enter_default = true,
callback = function()
self:setHighlightedText(item, input_dialog:getInputText(), caller_callback)
UIManager:close(input_dialog)
end,
},
}
},
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
end
function ReaderBookmark:setHighlightedText(item, text, caller_callback)
local edited
if text then
edited = true
else -- reset to selected text
if self.ui.rolling then
text = self.ui.document:getTextFromXPointers(item.pos0, item.pos1)
else
text = self.ui.document:getTextFromPositions(item.pos0, item.pos1).text
end
end
local index = self:getBookmarkItemIndex(item)
self.ui.annotation.annotations[index].text = text
self.ui.annotation.annotations[index].text_edited = edited
item.text_orig = text
item.text = self:getBookmarkItemText(item)
item.text_edited = edited
caller_callback()
end
function ReaderBookmark:onSearchBookmark()
local input_dialog
local check_button_case, separator, check_button_bookmark, check_button_highlight, check_button_note
input_dialog = InputDialog:new{
title = _("Search bookmarks"),
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
},
{
text = _("Search"),
is_enter_default = true,
callback = function()
local search_str = input_dialog:getInputText()
if search_str == "" then
search_str = nil
else
if not check_button_case.checked then
search_str = Utf8Proc.lowercase(util.fixUtf8(search_str, "?"))
end
end
self.match_table = {
search_str = search_str,
bookmark = check_button_bookmark.checked,
highlight = check_button_highlight.checked,
note = check_button_note.checked,
case_sensitive = check_button_case.checked,
}
UIManager:close(input_dialog)
if self.bookmark_menu then -- from bookmark list
local bm_menu = self.bookmark_menu[1]
local item_table = bm_menu.item_table
for i = #item_table, 1, -1 do
if not self:doesBookmarkMatchTable(item_table[i]) then
table.remove(item_table, i)
end
end
self:updateBookmarkList(item_table)
else -- from main menu
self:onShowBookmark()
end
end,
},
},
},
}
check_button_case = CheckButton:new{
text = " " .. _("Case sensitive"),
checked = false,
parent = input_dialog,
}
input_dialog:addWidget(check_button_case)
local separator_width = input_dialog:getAddedWidgetAvailableWidth()
separator = CenterContainer:new{
dimen = Geom:new{
w = separator_width,
h = 2 * Size.span.vertical_large,
},
LineWidget:new{
background = Blitbuffer.COLOR_DARK_GRAY,
dimen = Geom:new{
w = separator_width,
h = Size.line.medium,
}
},
}
input_dialog:addWidget(separator)
check_button_highlight = CheckButton:new{
text = " " .. self.display_prefix["highlight"] .. self.display_type["highlight"],
checked = true,
parent = input_dialog,
}
input_dialog:addWidget(check_button_highlight)
check_button_note = CheckButton:new{
text = " " .. self.display_prefix["note"] .. self.display_type["note"],
checked = true,
parent = input_dialog,
}
input_dialog:addWidget(check_button_note)
check_button_bookmark = CheckButton:new{
text = " " .. self.display_prefix["bookmark"] .. self.display_type["bookmark"],
checked = true,
parent = input_dialog,
}
input_dialog:addWidget(check_button_bookmark)
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
return true
end
function ReaderBookmark:filterByEditedText()
local bm_menu = self.bookmark_menu[1]
local item_table = bm_menu.item_table
for i = #item_table, 1, -1 do
if not item_table[i].text_edited then
table.remove(item_table, i)
end
end
self.show_edited_only = true
self:updateBookmarkList(item_table)
end
function ReaderBookmark:filterByHighlightStyle()
local filter_by_drawer_callback = function(drawer)
local bm_menu = self.bookmark_menu[1]
local item_table = bm_menu.item_table
for i = #item_table, 1, -1 do
if item_table[i].drawer ~= drawer then
table.remove(item_table, i)
end
end
self.show_drawer_only = drawer
self:updateBookmarkList(item_table)
end
self.ui.highlight:showHighlightStyleDialog(filter_by_drawer_callback)
end
function ReaderBookmark:doesBookmarkMatchTable(item)
if self.match_table[item.type] then
if self.match_table.search_str then
local text = item.text_orig
if item.note then -- search in the highlighted text and in the note
text = text .. "\u{FFFF}" .. item.note
end
if not self.match_table.case_sensitive then
text = Utf8Proc.lowercase(util.fixUtf8(text, "?"))
end
return text:find(self.match_table.search_str)
end
return true
end
end
return ReaderBookmark