local BD = require("ui/bidi") local CenterContainer = require("ui/widget/container/centercontainer") local ConfirmBox = require("ui/widget/confirmbox") local Device = require("device") local Event = require("ui/event") local InputContainer = require("ui/widget/container/inputcontainer") local PluginLoader = require("pluginloader") local Screensaver = require("ui/screensaver") local UIManager = require("ui/uimanager") local logger = require("logger") local dbg = require("dbg") local util = require("util") local Screen = Device.screen local _ = require("gettext") local T = require("ffi/util").template local ReaderMenu = InputContainer:extend{ tab_item_table = nil, menu_items = nil, -- table, mandatory registered_widgets = nil, -- array } function ReaderMenu:init() self.menu_items = { ["KOMenu:menu_buttons"] = { -- top menu }, -- items in top menu navi = { icon = "appbar.navigation", }, typeset = { icon = "appbar.typeset", }, setting = { icon = "appbar.settings", }, tools = { icon = "appbar.tools", }, search = { icon = "appbar.search", }, filemanager = { icon = "appbar.filebrowser", remember = false, callback = function() self:onTapCloseMenu() local file = self.ui.document.file self.ui:onClose() self.ui:showFileManager(file) end, }, main = { icon = "appbar.menu", } } self.registered_widgets = {} self:registerKeyEvents() if G_reader_settings:has("activate_menu") then self.activation_menu = G_reader_settings:readSetting("activate_menu") else self.activation_menu = "swipe_tap" end -- delegate gesture listener to readerui, NOP our own self.ges_events = nil end function ReaderMenu:onGesture() end function ReaderMenu:registerKeyEvents() if Device:hasKeys() then if Device:isTouchDevice() then self.key_events.PressMenu = { { "Menu" } } if Device:hasFewKeys() then self.key_events.PressMenu = { { { "Menu", "Right" } } } end else -- Map Menu key to top menu only, because the bottom menu is only designed for touch devices. self.key_events.KeyPressShowMenu = { { "Menu" } } if Device:hasFewKeys() then self.key_events.KeyPressShowMenu = { { { "Menu", "Right" } } } end end end end ReaderMenu.onPhysicalKeyboardConnected = ReaderMenu.registerKeyEvents function ReaderMenu:getPreviousFile() return require("readhistory"):getPreviousFile(self.ui.document.file) end function ReaderMenu:initGesListener() if not Device:isTouchDevice() then return end local DTAP_ZONE_MENU = G_defaults:readSetting("DTAP_ZONE_MENU") local DTAP_ZONE_MENU_EXT = G_defaults:readSetting("DTAP_ZONE_MENU_EXT") self.ui:registerTouchZones({ { id = "readermenu_tap", ges = "tap", screen_zone = { ratio_x = DTAP_ZONE_MENU.x, ratio_y = DTAP_ZONE_MENU.y, ratio_w = DTAP_ZONE_MENU.w, ratio_h = DTAP_ZONE_MENU.h, }, overrides = { "tap_forward", "tap_backward", }, handler = function(ges) return self:onTapShowMenu(ges) end, }, { id = "readermenu_ext_tap", ges = "tap", screen_zone = { ratio_x = DTAP_ZONE_MENU_EXT.x, ratio_y = DTAP_ZONE_MENU_EXT.y, ratio_w = DTAP_ZONE_MENU_EXT.w, ratio_h = DTAP_ZONE_MENU_EXT.h, }, overrides = { "readermenu_tap", }, handler = function(ges) return self:onTapShowMenu(ges) end, }, { id = "readermenu_swipe", ges = "swipe", screen_zone = { ratio_x = DTAP_ZONE_MENU.x, ratio_y = DTAP_ZONE_MENU.y, ratio_w = DTAP_ZONE_MENU.w, ratio_h = DTAP_ZONE_MENU.h, }, overrides = { "rolling_swipe", "paging_swipe", }, handler = function(ges) return self:onSwipeShowMenu(ges) end, }, { id = "readermenu_ext_swipe", ges = "swipe", screen_zone = { ratio_x = DTAP_ZONE_MENU_EXT.x, ratio_y = DTAP_ZONE_MENU_EXT.y, ratio_w = DTAP_ZONE_MENU_EXT.w, ratio_h = DTAP_ZONE_MENU_EXT.h, }, overrides = { "readermenu_swipe", }, handler = function(ges) return self:onSwipeShowMenu(ges) end, }, { id = "readermenu_pan", ges = "pan", screen_zone = { ratio_x = DTAP_ZONE_MENU.x, ratio_y = DTAP_ZONE_MENU.y, ratio_w = DTAP_ZONE_MENU.w, ratio_h = DTAP_ZONE_MENU.h, }, overrides = { "rolling_pan", "paging_pan", }, handler = function(ges) return self:onSwipeShowMenu(ges) end, }, { id = "readermenu_ext_pan", ges = "pan", screen_zone = { ratio_x = DTAP_ZONE_MENU_EXT.x, ratio_y = DTAP_ZONE_MENU_EXT.y, ratio_w = DTAP_ZONE_MENU_EXT.w, ratio_h = DTAP_ZONE_MENU_EXT.h, }, overrides = { "readermenu_pan", }, handler = function(ges) return self:onSwipeShowMenu(ges) end, }, }) end ReaderMenu.onReaderReady = ReaderMenu.initGesListener function ReaderMenu:setUpdateItemTable() for _, widget in pairs(self.registered_widgets) do local ok, err = pcall(widget.addToMainMenu, widget, self.menu_items) if not ok then logger.err("failed to register widget", widget.name, err) end end -- typeset tab self.menu_items.document_settings = { text = _("Document settings"), sub_item_table = { { text = _("Reset document settings to default"), keep_menu_open = true, callback = function() UIManager:show(ConfirmBox:new{ text = _("Reset current document settings to their default values?\n\nReading position, highlights and bookmarks will be kept.\nThe document will be reloaded."), ok_text = _("Reset"), ok_callback = function() local current_file = self.ui.document.file self:onTapCloseMenu() self.ui:onClose() require("apps/filemanager/filemanagerutil").resetDocumentSettings(current_file) require("apps/reader/readerui"):showReader(current_file) end, }) end, }, { text = _("Save document settings as default"), keep_menu_open = true, separator = true, callback = function() UIManager:show(ConfirmBox:new{ text = _("Save current document settings as default values?"), ok_text = _("Save"), ok_callback = function() self:onTapCloseMenu() self:saveDocumentSettingsAsDefault() UIManager:show(require("ui/widget/notification"):new{ text = _("Default settings updated"), }) end, }) end, }, }, } if not Device:isTouchDevice() then -- This menu entry is a duplicate of the one found in page_turns for touch devices -- but we need to add it here for non-touch devices. table.insert(self.menu_items.document_settings.sub_item_table, { text_func = function() local text = _("Invert document-related dialogs") if G_reader_settings:isTrue("invert_ui_layout") then text = text .. " ★" end return text end, checked_func = function() return self.view:shouldInvertBiDiLayoutMirroring() end, callback = function() UIManager:broadcastEvent(Event:new("ToggleUILayoutMiroring")) end, hold_callback = function(touchmenu_instance) local invert_ui_layout = G_reader_settings:isTrue("invert_ui_layout") local MultiConfirmBox = require("ui/widget/multiconfirmbox") UIManager:show(MultiConfirmBox:new{ text = invert_ui_layout and _("The default (★) for newly opened books is to Invert document-related dialogs.\n\nWould you like to change it?") or _("The default (★) for newly opened books is not to Invert document-related dialogs.\n\nWould you like to change it?"), choice1_text_func = function() return invert_ui_layout and _("Don't invert") or _("Don't invert") .." (★)" end, choice1_callback = function() G_reader_settings:makeFalse("invert_ui_layout") if touchmenu_instance then touchmenu_instance:updateItems() end end, choice2_text_func = function() return invert_ui_layout and _("Invert") .." (★)" or _("Invert") end, choice2_callback = function() G_reader_settings:makeTrue("invert_ui_layout") if touchmenu_instance then touchmenu_instance:updateItems() end end, }) end, help_text = _([[ When enabled the UI direction for the Table of Contents, Book Map, and Page Browser dialogs will mirror the default UI direction. Useful when used alongside 'Invert page turn taps and swipes'.]]), }) end self.menu_items.page_overlap = dofile("frontend/ui/elements/page_overlap.lua") -- settings tab -- insert common settings for id, common_setting in pairs(dofile("frontend/ui/elements/common_settings_menu_table.lua")) do self.menu_items[id] = common_setting end if Device:isTouchDevice() then -- Settings > Taps & Gestures; mostly concerns touch related page turn stuff, and only applies to Reader self.menu_items.page_turns = dofile("frontend/ui/elements/page_turns.lua") end -- Settings > Navigation; while also related to page turns, this mostly concerns physical keys, and applies *everywhere* if Device:hasKeys() then self.menu_items.physical_buttons_setup = dofile("frontend/ui/elements/physical_buttons.lua") end -- insert DjVu render mode submenu just before the last entry (show advanced) -- this is a bit of a hack if self.ui.document.is_djvu then self.menu_items.djvu_render_mode = self.view:getRenderModeMenuTable() end if Device:supportsScreensaver() then local screensaver_sub_item_table = dofile("frontend/ui/elements/screensaver_menu.lua") table.insert(screensaver_sub_item_table, { text = _("Do not show this book cover on sleep screen"), enabled_func = function() local screensaver_type = G_reader_settings:readSetting("screensaver_type") return screensaver_type == "cover" or screensaver_type == "disable" end, checked_func = function() return self.ui.doc_settings:isTrue("exclude_screensaver") end, callback = function() if Screensaver.isExcluded(self.ui) then self.ui.doc_settings:makeFalse("exclude_screensaver") else self.ui.doc_settings:makeTrue("exclude_screensaver") end self.ui:saveSettings() end, }) self.menu_items.screensaver = { text = _("Sleep screen"), sub_item_table = screensaver_sub_item_table, } end -- tools tab self.menu_items.plugin_management = { text = _("Plugin management"), sub_item_table = PluginLoader:genPluginManagerSubItem(), } self.menu_items.patch_management = dofile("frontend/ui/elements/patch_management.lua") -- main menu tab -- insert common info for id, common_setting in pairs(dofile("frontend/ui/elements/common_info_menu_table.lua")) do self.menu_items[id] = common_setting end -- insert common exit for reader for id, common_setting in pairs(dofile("frontend/ui/elements/common_exit_menu_table.lua")) do self.menu_items[id] = common_setting end self.menu_items.open_previous_document = { text_func = function() local previous_file = self:getPreviousFile() if not G_reader_settings:isTrue("open_last_menu_show_filename") or not previous_file then return _("Open previous document") end local path, file_name = util.splitFilePathName(previous_file) -- luacheck: no unused return T(_("Previous: %1"), BD.filename(file_name)) end, enabled_func = function() return self:getPreviousFile() ~= nil end, callback = function() self.ui:onOpenLastDoc() end, hold_callback = function() local previous_file = self:getPreviousFile() UIManager:show(ConfirmBox:new{ text = T(_("Would you like to open the previous document: %1?"), BD.filepath(previous_file)), ok_text = _("OK"), ok_callback = function() self.ui:switchDocument(previous_file) end, }) end } -- NOTE: This is cached via require for ui/plugin/insert_menu's sake... local order = require("ui/elements/reader_menu_order") local MenuSorter = require("ui/menusorter") self.tab_item_table = MenuSorter:mergeAndSort("reader", self.menu_items, order) end dbg:guard(ReaderMenu, 'setUpdateItemTable', function(self) local mock_menu_items = {} for _, widget in pairs(self.registered_widgets) do -- make sure addToMainMenu works in debug mode widget:addToMainMenu(mock_menu_items) end end) function ReaderMenu:saveDocumentSettingsAsDefault() local prefix if self.ui.rolling then G_reader_settings:saveSetting("cre_font", self.ui.font.font_face) G_reader_settings:saveSetting("copt_css", self.ui.document.default_css) local style_tweaks = G_reader_settings:readSetting("style_tweaks") for tweak_id, is_enabled in pairs(self.ui.styletweak.doc_tweaks) do style_tweaks[tweak_id] = is_enabled or nil end prefix = "copt_" else prefix = "kopt_" end for k, v in pairs(self.ui.document.configurable) do G_reader_settings:saveSetting(prefix .. k, v) end end function ReaderMenu:exitOrRestart(callback, force) -- Only restart sets a callback, which suits us just fine for this check ;) if callback and not force and not Device:isStartupScriptUpToDate() then UIManager:show(ConfirmBox:new{ text = _("KOReader's startup script has been updated. You'll need to completely exit KOReader to finalize the update."), ok_text = _("Restart anyway"), ok_callback = function() self:exitOrRestart(callback, true) end, }) return end self:onTapCloseMenu() UIManager:nextTick(function() self.ui:onClose() if callback then callback() end end) end function ReaderMenu:onShowMenu(tab_index, do_not_show) if self.tab_item_table == nil then self:setUpdateItemTable() end local menu_container = CenterContainer:new{ covers_header = true, ignore = "height", dimen = Screen:getSize(), } local main_menu if Device:isTouchDevice() or Device:hasDPad() then local TouchMenu = require("ui/widget/touchmenu") main_menu = TouchMenu:new{ width = Screen:getWidth(), last_index = tab_index or self.last_tab_index, tab_item_table = self.tab_item_table, show_parent = menu_container, not_shown = do_not_show, } else local Menu = require("ui/widget/menu") main_menu = Menu:new{ title = _("Document menu"), item_table = Menu.itemTableFromTouchMenu(self.tab_item_table), width = Screen:getWidth() - 100, show_parent = menu_container, } end main_menu.close_callback = function() self:onCloseReaderMenu() end main_menu.touch_menu_callback = function () self.ui:handleEvent(Event:new("CloseConfigMenu")) end menu_container[1] = main_menu -- maintain a reference to menu_container self.menu_container = menu_container if not do_not_show then UIManager:show(menu_container) end return true end function ReaderMenu:onCloseReaderMenu() if not self.menu_container then return true end self.last_tab_index = self.menu_container[1].last_index self:onSaveSettings() UIManager:close(self.menu_container) self.menu_container = nil return true end function ReaderMenu:onSetDimensions(dimen) -- This widget doesn't support in-place layout updates, so, close & reopen if self.menu_container then self:onCloseReaderMenu() self:onShowMenu() end -- update gesture zones according to new screen dimen -- (On CRe, this will get called a second time by ReaderReady once the document is reloaded). self:initGesListener() end function ReaderMenu:_getTabIndexFromLocation(ges) if self.tab_item_table == nil then self:setUpdateItemTable() end if not ges then return self.last_tab_index -- if the start position is far right elseif ges.pos.x > Screen:getWidth() * (2/3) then return BD.mirroredUILayout() and 1 or #self.tab_item_table -- if the start position is far left elseif ges.pos.x < Screen:getWidth() * (1/3) then return BD.mirroredUILayout() and #self.tab_item_table or 1 -- if center return the last index else return self.last_tab_index end end function ReaderMenu:onSwipeShowMenu(ges) if self.activation_menu ~= "tap" and ges.direction == "south" then if G_reader_settings:nilOrTrue("show_bottom_menu") then self.ui:handleEvent(Event:new("ShowConfigMenu")) end self:onShowMenu(self:_getTabIndexFromLocation(ges)) self.ui:handleEvent(Event:new("HandledAsSwipe")) -- cancel any pan scroll made return true end end function ReaderMenu:onTapShowMenu(ges) if self.activation_menu ~= "swipe" then if G_reader_settings:nilOrTrue("show_bottom_menu") then self.ui:handleEvent(Event:new("ShowConfigMenu")) end self:onShowMenu(self:_getTabIndexFromLocation(ges)) return true end end function ReaderMenu:onPressMenu() if G_reader_settings:nilOrTrue("show_bottom_menu") then self.ui:handleEvent(Event:new("ShowConfigMenu")) end self:onShowMenu() return true end function ReaderMenu:onKeyPressShowMenu(_, key_ev) return self:onShowMenu() end function ReaderMenu:onTapCloseMenu() self:onCloseReaderMenu() self.ui:handleEvent(Event:new("CloseConfigMenu")) end function ReaderMenu:onReadSettings(config) self.last_tab_index = config:readSetting("readermenu_tab_index") or 1 end function ReaderMenu:onSaveSettings() self.ui.doc_settings:saveSetting("readermenu_tab_index", self.last_tab_index) end function ReaderMenu:onMenuSearch() self:onShowMenu(nil, true) self.menu_container[1]:onShowMenuSearch() end function ReaderMenu:registerToMainMenu(widget) table.insert(self.registered_widgets, widget) end function ReaderMenu:onShowCloudStorage() local CloudStorage = require("apps/cloudstorage/cloudstorage") UIManager:show(CloudStorage:new{ ui = self.ui }) return true end return ReaderMenu