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:extend{ location_stack = nil, -- array -- a limit not intended to be a practical limit but just a failsafe max_stack = 5000, } function ReaderBack:init() if Device:hasKeys() then self.ui.key_events.Back = { { Device.input.group.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() if self.ui.document.info.has_pages then return self.ui.paging:getBookLocation() else return { xpointer = self.ui.rolling:getBookLocation(), } end end 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:_addPreviousLocationToStack() local new_location = self:_getCurrentLocation() 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 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:_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 -- 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 function ReaderBack:onBack() local back_in_reader = G_reader_settings:readSetting("back_in_reader", "previous_location") local back_to_exit = G_reader_settings:readSetting("back_to_exit", "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."), }) return true else self.back_resist = nil end 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