diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index 52fd404c3..50b137510 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -497,8 +497,12 @@ function FileManager:init() if Device:hasKeys() then self.key_events.Home = { {"Home"}, doc = "go home" } - --Override the menu.lua way of handling the back key + -- Override the menu.lua way of handling the back key self.file_chooser.key_events.Back = { {"Back"}, doc = "go back" } + if not Device:hasFewKeys() then + -- Also remove the handler assigned to the "Back" key by menu.lua + self.file_chooser.key_events.Close = nil + end end self:handleEvent(Event:new("SetDimensions", self.dimen)) diff --git a/frontend/apps/reader/modules/readerback.lua b/frontend/apps/reader/modules/readerback.lua index d4d10c00c..3cc399f23 100644 --- a/frontend/apps/reader/modules/readerback.lua +++ b/frontend/apps/reader/modules/readerback.lua @@ -1,89 +1,193 @@ +local ConfirmBox = require("ui/widget/confirmbox") local Device = require("device") local Event = require("ui/event") local EventListener = require("ui/widget/eventlistener") +local Notification = require("ui/widget/notification") +local UIManager = require("ui/uimanager") local logger = require("logger") local util = require("util") local _ = require("gettext") +-- This module handles the "Back" key (and the "Back" gesture action). +-- When global setting "back_in_reader" == "previous_read_page", it +-- additionally handles a location stack for each visited page or +-- page view change (when scrolling in a same page) + local ReaderBack = EventListener:new{ location_stack = {}, -- a limit not intended to be a practical limit but just a failsafe max_stack = 5000, - -- allow for disabling back history, and having Back key - -- quit immediately (useful for some developers) - disabled = G_reader_settings:isFalse("enable_back_history"), } function ReaderBack:init() if Device:hasKeys() then self.ui.key_events.Back = { {"Back"}, doc = "Reader back" } end + -- Regular function wrapping our method, to avoid re-creating + -- an anonymous function at each page turn + self._addPreviousLocationToStackCallback = function() + self:_addPreviousLocationToStack() + end end function ReaderBack:_getCurrentLocation() - local current_location - if self.ui.document.info.has_pages then - current_location = self.ui.paging:getBookLocation() + local current_location = self.ui.paging:getBookLocation() + if current_location then + -- We need a copy, as we're getting references to + -- objects ReaderPaging/ReaderView may still modify + local res = {} + for i=1, #current_location do + res[i] = util.tableDeepCopy(current_location[i]) + end + return res + end else - current_location = { + return { xpointer = self.ui.rolling:getBookLocation(), } end - - return current_location end -local ignore_location +function ReaderBack:_areLocationsSimilar(location1, location2) + if self.ui.document.info.has_pages then + -- locations are arrays of k/v tables + if #location1 ~= #location2 then + return false + end + for i=1, #location1 do + if not util.tableEquals(location1[i], location2[i]) then + return false + end + end + return true + else + return location1.xpointer == location2.xpointer + end +end -function ReaderBack:addCurrentLocationToStack() - local location_stack = self.location_stack +function ReaderBack:_addPreviousLocationToStack() local new_location = self:_getCurrentLocation() - if util.tableEquals(ignore_location, new_location) then return end - - table.insert(location_stack, new_location) - - if #location_stack > self.max_stack then - table.remove(location_stack, 1) + if self.cur_location and new_location then + if self:_areLocationsSimilar(self.cur_location, new_location) then + -- Unchanged, don't add it yet + return + end + table.insert(self.location_stack, self.cur_location) + if #self.location_stack > self.max_stack then + table.remove(self.location_stack, 1) + end end -end --- Scroll mode crengine -function ReaderBack:onPosUpdate() - if self.disabled then return end - self:addCurrentLocationToStack() -end - --- Paged media -function ReaderBack:onPageUpdate() - if self.disabled then return end - self:addCurrentLocationToStack() + if new_location then + self.cur_location = new_location + end end -- Called when loading new document function ReaderBack:onReadSettings(config) self.location_stack = {} + self.cur_location = nil end -function ReaderBack:onBack() - local location_stack = self.location_stack +function ReaderBack:_onViewPossiblyUpdated() + if G_reader_settings:readSetting("back_in_reader") == "previous_read_page" then + -- As multiple modules will have their :onPageUpdate()/... called, + -- and some of them will set up the new page with it, we need to + -- delay our handling after all of them are called (otherwise, + -- depending on the order of the calls, we may be have the location + -- of either the previous page or the current one). + UIManager:nextTick(self._addPreviousLocationToStackCallback) + end + self.back_resist = nil +end - if self.disabled then - self.ui:handleEvent(Event:new("Close")) - elseif #location_stack > 1 then - local saved_location = table.remove(location_stack) +-- Hook to events that do/may change page/view (more than one of these events +-- may be sent on a single page turn/scroll, _addPreviousLocationToStack() +-- will ignore those for the same book location): +-- Called after initial page is set up +ReaderBack.onReaderReady = ReaderBack._onViewPossiblyUpdated +-- New page on paged media or crengine in page mode +ReaderBack.onPageUpdate = ReaderBack._onViewPossiblyUpdated +-- New page on crengine in scroll mode +ReaderBack.onPosUpdate = ReaderBack._onViewPossiblyUpdated +-- View updated (possibly on the same page) on paged media +ReaderBack.onViewRecalculate = ReaderBack._onViewPossiblyUpdated +-- View updated (possibly on the same page) on paged media (needed in Reflow mode) +ReaderBack.onPagePositionUpdated = ReaderBack._onViewPossiblyUpdated - if saved_location then - ignore_location = self:_getCurrentLocation() - logger.dbg("[ReaderBack] restoring:", saved_location) - self.ui:handleEvent(Event:new('RestoreBookLocation', saved_location)) +function ReaderBack:onBack() + local back_in_reader = G_reader_settings:readSetting("back_in_reader") or "previous_location" + local back_to_exit = G_reader_settings:readSetting("back_to_exit") or "prompt" + + if back_in_reader == "previous_read_page" then + if #self.location_stack > 0 then + local saved_location = table.remove(self.location_stack) + if saved_location then + -- Reset self.cur_location, which will be updated with the restored + -- saved_location, which will then not be added to the stack + self.cur_location = nil + logger.dbg("[ReaderBack] restoring:", saved_location) + self.ui:handleEvent(Event:new('RestoreBookLocation', saved_location)) + -- Ensure we always have self.cur_location updated, as in some + -- cases (same page), no event that we handle might be sent. + UIManager:nextTick(self._addPreviousLocationToStackCallback) + return true + end + elseif not self.back_resist or back_to_exit == "disable" then + -- Show a one time notification when location stack is empty. + -- On next "Back" only, proceed with the default behaviour (unless + -- it's disabled, in which case we always show this notification) + self.back_resist = true + UIManager:show(Notification:new{ + text = _("Location history is empty."), + timeout = 2, + }) return true + else + self.back_resist = nil end - else - logger.dbg("[ReaderBack] no location history, closing") + elseif back_in_reader == "previous_location" then + -- ReaderLink maintains its own location_stack of less frequent jumps + -- (links or TOC entries followed, skim document...) + if back_to_exit == "disable" then + -- Let ReaderLink always show its notification if empty + self.ui.link:onGoBackLink(true) -- show_notification_if_empty=true + return true + end + if self.back_resist then + -- Notification "Location history is empty" previously shown by ReaderLink + self.back_resist = nil + elseif self.ui.link:onGoBackLink(true) then -- show_notification_if_empty=true + return true -- some location restored + else + -- ReaderLink has shown its notification that location stack is empty. + -- On next "Back" only, proceed with the default behaviour + self.back_resist = true + return true + end + elseif back_in_reader == "filebrowser" then self.ui:handleEvent(Event:new("Home")) + -- Filebrowser will handle next "Back" and ensure back_to_exit + return true + end + + -- location stack empty, or back_in_reader == "default" + if back_to_exit == "always" then + self.ui:handleEvent(Event:new("Close")) + elseif back_to_exit == "disable" then + return true + elseif back_to_exit == "prompt" then + UIManager:show(ConfirmBox:new{ + text = _("Exit KOReader?"), + ok_text = _("Exit"), + ok_callback = function() + self.ui:handleEvent(Event:new("Close")) + end + }) end + return true end return ReaderBack diff --git a/frontend/apps/reader/modules/readerlink.lua b/frontend/apps/reader/modules/readerlink.lua index 029a8742c..b72c64311 100644 --- a/frontend/apps/reader/modules/readerlink.lua +++ b/frontend/apps/reader/modules/readerlink.lua @@ -41,11 +41,8 @@ function ReaderLink:init() doc = "go to selected page link", event = "GotoSelectedPageLink", } - self.key_events.GoBackLink = { - { "Back" }, - doc = "go back from link", - event = "GoBackLink", - } + -- "Back" is handled by ReaderBack, which will call our onGoBackLink() + -- when G_reader_settings:readSetting("back_in_reader") == "previous_location" end if Device:isTouchDevice() then self.ui:registerTouchZones({ diff --git a/frontend/apps/reader/modules/readerpaging.lua b/frontend/apps/reader/modules/readerpaging.lua index fbbcd2c23..35e613dae 100644 --- a/frontend/apps/reader/modules/readerpaging.lua +++ b/frontend/apps/reader/modules/readerpaging.lua @@ -292,6 +292,7 @@ book, the page view will be roughly the same. function ReaderPaging:setPagePosition(page, pos) logger.dbg("set page position", pos) self.page_positions[page] = pos + self.ui:handleEvent(Event:new("PagePositionUpdated")) end --[[ @@ -532,16 +533,34 @@ end function ReaderPaging:onRestoreBookLocation(saved_location) if self.view.page_scroll then - self.view:restoreViewContext(saved_location) - self:_gotoPage(self.view.page_states[1].page, "scrolling") + if self.view:restoreViewContext(saved_location) then + self:_gotoPage(saved_location[1].page, "scrolling") + else + -- If context is unusable (not from scroll mode), trigger + -- this to go at least to its page and redraw it + self.ui:handleEvent(Event:new("PageUpdate", saved_location[1].page)) + end else - -- gotoPage will emit PageUpdate event, which will trigger recalculate + -- gotoPage may emit PageUpdate event, which will trigger recalculate -- in ReaderView and resets the view context. So we need to call - -- restoreViewContext after gotoPage + -- restoreViewContext after gotoPage. + -- But if we're restoring to the same page, it will not emit + -- PageUpdate event - so we need to do it for a correct redrawing + local send_PageUpdate = saved_location[1].page == self.current_page self:_gotoPage(saved_location[1].page) - self.view:restoreViewContext(saved_location) + if not self.view:restoreViewContext(saved_location) then + -- If context is unusable (not from page mode), also + -- send PageUpdate event to go to its page and redraw it + send_PageUpdate = true + end + if send_PageUpdate then + self.ui:handleEvent(Event:new("PageUpdate", saved_location[1].page)) + end end self:setPagePosition(self:getTopPage(), self:getTopPosition()) + -- In some cases (same page, different offset), doing the above + -- might not redraw the screen. Ensure it is. + UIManager:setDirty(self.view.dialog, "partial") return true end diff --git a/frontend/apps/reader/modules/readerview.lua b/frontend/apps/reader/modules/readerview.lua index db12b7b13..49c10a5d6 100644 --- a/frontend/apps/reader/modules/readerview.lua +++ b/frontend/apps/reader/modules/readerview.lua @@ -668,13 +668,25 @@ function ReaderView:getViewContext() end function ReaderView:restoreViewContext(ctx) + -- The format of the context is different depending on page_scroll. + -- If we're asked to restore the other format, just ignore it + -- (our only caller, ReaderPaging:onRestoreBookLocation(), will + -- at least change to the page of the context, which is all that + -- can be done when restoring from a different mode) if self.page_scroll then - self.page_states = ctx + if ctx[1] and ctx[1].visible_area then + self.page_states = ctx + return true + end else - self.state = ctx[1] - self.visible_area = ctx[2] - self.page_area = ctx[3] + if ctx[1] and ctx[1].pos then + self.state = ctx[1] + self.visible_area = ctx[2] + self.page_area = ctx[3] + return true + end end + return false end function ReaderView:onSetRotationMode(rotation) diff --git a/frontend/ui/elements/common_settings_menu_table.lua b/frontend/ui/elements/common_settings_menu_table.lua index 04e134c9e..c857431b3 100644 --- a/frontend/ui/elements/common_settings_menu_table.lua +++ b/frontend/ui/elements/common_settings_menu_table.lua @@ -356,14 +356,52 @@ common_settings.back_in_filemanager = { }, }, } -common_settings.enable_back_history = { - text = _("Enable back history"), - checked_func = function() - return G_reader_settings:nilOrTrue("enable_back_history") - end, - callback = function() - G_reader_settings:flipNilOrTrue("enable_back_history") - end, +common_settings.back_in_reader = { + -- All these options are managed by ReaderBack + text = _("Back in reader"), + sub_item_table = { + { + text_func = function() + local back_to_exit = G_reader_settings:readSetting("back_to_exit") or "prompt" + return T(_("Back to exit (%1)"), + back_to_exit_str[back_to_exit][2]) + end, + checked_func = function() + return G_reader_settings:readSetting("back_in_reader") == "default" + end, + callback = function() + G_reader_settings:saveSetting("back_in_reader", "default") + end, + }, + { + text = _("Go to file browser"), + checked_func = function() + return G_reader_settings:readSetting("back_in_reader") == "filebrowser" + end, + callback = function() + G_reader_settings:saveSetting("back_in_reader", "filebrowser") + end, + }, + { + text = _("Go to previous location"), + checked_func = function() + local back_in_reader = G_reader_settings:readSetting("back_in_reader") + return back_in_reader == "previous_location" or back_in_reader == nil + end, + callback = function() + G_reader_settings:saveSetting("back_in_reader", "previous_location") + end, + }, + { + text = _("Go to previous read page"), + checked_func = function() + return G_reader_settings:readSetting("back_in_reader") == "previous_read_page" + end, + callback = function() + G_reader_settings:saveSetting("back_in_reader", "previous_read_page") + end, + }, + }, } if Device:hasKeys() then common_settings.invert_page_turn_buttons = { diff --git a/frontend/ui/elements/filemanager_menu_order.lua b/frontend/ui/elements/filemanager_menu_order.lua index c47ae2a1f..58a6dafee 100644 --- a/frontend/ui/elements/filemanager_menu_order.lua +++ b/frontend/ui/elements/filemanager_menu_order.lua @@ -54,7 +54,7 @@ local order = { navigation = { "back_to_exit", "back_in_filemanager", - "enable_back_history", + "back_in_reader", "android_volume_keys", "android_camera_key", "android_haptic_feedback", diff --git a/frontend/ui/elements/reader_menu_order.lua b/frontend/ui/elements/reader_menu_order.lua index c60f8211d..f7ece2078 100644 --- a/frontend/ui/elements/reader_menu_order.lua +++ b/frontend/ui/elements/reader_menu_order.lua @@ -75,7 +75,7 @@ local order = { navigation = { "back_to_exit", "back_in_filemanager", - "enable_back_history", + "back_in_reader", "android_volume_keys", "android_camera_key", "android_haptic_feedback", diff --git a/plugins/gestures.koplugin/main.lua b/plugins/gestures.koplugin/main.lua index 9faec52a6..9d0a6d14e 100644 --- a/plugins/gestures.koplugin/main.lua +++ b/plugins/gestures.koplugin/main.lua @@ -479,7 +479,7 @@ Default value: %1]]), Screen.low_pan_rate and 5.0 or 30.0), info_text = T(_([[ Any other taps made within this interval after a first tap will be considered accidental and ignored. -The interval value is in milliseconds and can range from 0 (0 second) to 2000 (2 seconds). +The interval value is in milliseconds and can range from 0 (0 seconds) to 2000 (2 seconds). Default value: %1]]), GestureDetector.TAP_INTERVAL/1000), width = math.floor(Screen:getWidth() * 0.75), value = GestureDetector:getInterval("ges_tap_interval")/1000, @@ -507,7 +507,7 @@ Default value: %1]]), GestureDetector.TAP_INTERVAL/1000), info_text = _([[ Any other taps made within this interval after a first tap will be considered accidental and ignored. -The interval value is in milliseconds and can range from 0 (0 second) to 2000 (2 seconds). +The interval value is in milliseconds and can range from 0 (0 seconds) to 2000 (2 seconds). Default value: 0]]), width = math.floor(Screen:getWidth() * 0.75), value = (G_reader_settings:readSetting("ges_tap_interval_on_keyboard") or 0)/1000,