diff --git a/frontend/apps/reader/modules/readerconfig.lua b/frontend/apps/reader/modules/readerconfig.lua index 17638c890..c3a94651e 100644 --- a/frontend/apps/reader/modules/readerconfig.lua +++ b/frontend/apps/reader/modules/readerconfig.lua @@ -130,6 +130,7 @@ function ReaderConfig:onShowConfigMenu() -- show last used panel when opening config dialog self.config_dialog:onShowConfigPanel(self.last_panel_index) UIManager:show(self.config_dialog) + self.ui:handleEvent(Event:new("HandledAsSwipe")) -- cancel any pan scroll made return true end diff --git a/frontend/apps/reader/modules/readerfooter.lua b/frontend/apps/reader/modules/readerfooter.lua index 1b18b77dc..14dafd0a9 100644 --- a/frontend/apps/reader/modules/readerfooter.lua +++ b/frontend/apps/reader/modules/readerfooter.lua @@ -1978,11 +1978,11 @@ function ReaderFooter:_updateFooterText(force_repaint, force_recompute) UIManager:widgetRepaint(self.view.footer, 0, 0) -- We've painted it first to ensure self.footer_content.dimen is sane UIManager:setDirty(self.view.footer, function() - return "ui", self.footer_content.dimen + return self.view.currently_scrolling and "fast" or "ui", self.footer_content.dimen end) else UIManager:setDirty(self.view.dialog, function() - return "ui", refresh_dim + return self.view.currently_scrolling and "fast" or "ui", refresh_dim end) end end diff --git a/frontend/apps/reader/modules/readermenu.lua b/frontend/apps/reader/modules/readermenu.lua index 8a3de60e8..6d564efc5 100644 --- a/frontend/apps/reader/modules/readermenu.lua +++ b/frontend/apps/reader/modules/readermenu.lua @@ -437,6 +437,7 @@ function ReaderMenu:onSwipeShowMenu(ges) self.ui:handleEvent(Event:new("ShowConfigMenu")) end self.ui:handleEvent(Event:new("ShowMenu", self:_getTabIndexFromLocation(ges))) + self.ui:handleEvent(Event:new("HandledAsSwipe")) -- cancel any pan scroll made return true end end diff --git a/frontend/apps/reader/modules/readerpaging.lua b/frontend/apps/reader/modules/readerpaging.lua index 5abb7d0bb..da6212f6c 100644 --- a/frontend/apps/reader/modules/readerpaging.lua +++ b/frontend/apps/reader/modules/readerpaging.lua @@ -7,6 +7,7 @@ local Math = require("optmath") local MultiConfirmBox = require("ui/widget/multiconfirmbox") local Notification = require("ui/widget/notification") local ReaderZooming = require("apps/reader/modules/readerzooming") +local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local bit = require("bit") local logger = require("logger") @@ -32,7 +33,6 @@ local ReaderPaging = InputContainer:new{ pan_rate = 30, -- default 30 ops, will be adjusted in readerui current_page = 0, number_of_pages = 0, - last_pan_relative_y = 0, visible_area = nil, page_area = nil, show_overlap_enable = nil, @@ -41,7 +41,7 @@ local ReaderPaging = InputContainer:new{ inverse_reading_order = nil, page_flipping_mode = false, bookmark_flipping_mode = false, - flip_steps = {0,1,2,5,10,20,50,100} + flip_steps = {0,1,2,5,10,20,50,100}, } function ReaderPaging:init() @@ -101,6 +101,7 @@ function ReaderPaging:init() {"0"}, doc = "go to end", event = "GotoPercent", args = 100, } end + self.pan_interval = TimeVal:new{ usec = 1000000 / self.pan_rate } self.number_of_pages = self.ui.document.info.number_of_pages self.ui.menu:registerToMainMenu(self) end @@ -173,7 +174,6 @@ function ReaderPaging:setupTouchZones() { id = "paging_pan", ges = "pan", - rate = self.pan_rate, screen_zone = { ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1, }, @@ -422,7 +422,43 @@ function ReaderPaging:bookmarkFlipping(flipping_page, flipping_ges) UIManager:setDirty(self.view.dialog, "partial") end +function ReaderPaging:onScrollSettingsUpdated(scroll_method, inertial_scroll_enabled, scroll_activation_delay) + self.scroll_method = scroll_method + self.scroll_activation_delay = TimeVal:new{ usec = scroll_activation_delay * 1000 } + if inertial_scroll_enabled then + self.ui.scrolling:setInertialScrollCallbacks( + function(distance) -- do_scroll_callback + if not self.ui.document then + return false + end + UIManager.currently_scrolling = true + local top_page, top_position = self:getTopPage(), self:getTopPosition() + self:onPanningRel(distance) + return not (top_page == self:getTopPage() and top_position == self:getTopPosition()) + end, + function() -- scroll_done_callback + UIManager.currently_scrolling = false + UIManager:setDirty(self.view.dialog, "partial") + end + ) + else + self.ui.scrolling:setInertialScrollCallbacks(nil, nil) + end +end + function ReaderPaging:onSwipe(_, ges) + if self._pan_has_scrolled then + -- We did some panning but released after a short amount of time, + -- so this gesture ended up being a Swipe - and this swipe was + -- not handled by the other modules (so, not opening the menus). + -- Do as :onPanRelese() and ignore this swipe. + self:onPanRelease() -- no arg, so we know there we come from here + return true + else + self._pan_started = false + UIManager.currently_scrolling = false + self._pan_page_states_to_restore = nil + end local direction = BD.flipDirectionIfMirroredUILayout(ges.direction) if self.bookmark_flipping_mode then self:bookmarkFlipping(self.current_page, ges) @@ -461,16 +497,86 @@ function ReaderPaging:onPan(_, ges) self.view:PanningStart(-ges.relative.x, -ges.relative.y) end elseif ges.direction == "north" or ges.direction == "south" then - local relative_type = "relative" - if self.ui.gesture and self.ui.gesture.multiswipes_enabled then - relative_type = "relative_delayed" - end - -- this is only used when mouse wheel is used if ges.mousewheel_direction and not self.view.page_scroll then + -- Mouse wheel generates a Pan event: in page mode, move one + -- page per event. Scroll mode is handled in the 'else' branch + -- and use the wheeled distance. self:onGotoViewRel(-1 * ges.mousewheel_direction) - else - self:onPanningRel(self.last_pan_relative_y - ges[relative_type].y) - self.last_pan_relative_y = ges[relative_type].y + elseif self.view.page_scroll then + if not self._pan_started then + self._pan_started = true + -- Re-init state variables + self._pan_has_scrolled = false + self._pan_prev_relative_y = 0 + self._pan_to_scroll_later = 0 + self._pan_real_last_time = TimeVal.zero + if ges.mousewheel_direction then + self._pan_activation_time = false + else + self._pan_activation_time = ges.time + self.scroll_activation_delay + end + -- We will restore the previous position if this pan + -- ends up being a swipe or a multiswipe + -- Somehow, accumulating the distances scrolled in a self._pan_dist_to_restore + -- so we can scroll these back may not always put us back to the original + -- position (possibly because of these page_states?). It's safer + -- to remember the original page_states and restore that. We can keep + -- a reference to the original table as onPanningRel() will have this + -- table replaced. + self._pan_page_states_to_restore = self.view.page_states + end + local scroll_now = false + if self._pan_activation_time and ges.time >= self._pan_activation_time then + self._pan_activation_time = false -- We can go on, no need to check again + end + if not self._pan_activation_time and ges.time - self._pan_real_last_time >= self.pan_interval then + scroll_now = true + self._pan_real_last_time = ges.time + end + local scroll_dist = 0 + if self.scroll_method == self.ui.scrolling.SCROLL_METHOD_CLASSIC then + -- Scroll by the distance the finger moved since last pan event, + -- having the document follows the finger + scroll_dist = self._pan_prev_relative_y - ges.relative.y + self._pan_prev_relative_y = ges.relative.y + if not self._pan_has_scrolled then + -- Avoid checking this for each pan, no need once we have scrolled + if self.ui.scrolling:cancelInertialScroll() or self.ui.scrolling:cancelledByTouch() then + -- If this pan or its initial touch did cancel some inertial scrolling, + -- ignore activation delay to allow continuous scrolling + self._pan_activation_time = false + scroll_now = true + self._pan_real_last_time = ges.time + end + end + self.ui.scrolling:accountManualScroll(scroll_dist, ges.time) + elseif self.scroll_method == self.ui.scrolling.SCROLL_METHOD_TURBO then + -- Legacy scrolling "buggy" behaviour, that can actually be nice + -- Scroll by the distance from the initial finger position, this distance + -- controlling the speed of the scrolling) + if scroll_now then + scroll_dist = -ges.relative.y + end + -- We don't accumulate in _pan_to_scroll_later + elseif self.scroll_method == self.ui.scrolling.SCROLL_METHOD_ON_RELEASE then + self._pan_to_scroll_later = -ges.relative.y + if scroll_now then + self._pan_has_scrolled = true -- so we really apply it later + end + scroll_dist = 0 + scroll_now = false + end + if scroll_now then + local dist = self._pan_to_scroll_later + scroll_dist + self._pan_to_scroll_later = 0 + if dist ~= 0 then + self._pan_has_scrolled = true + UIManager.currently_scrolling = true + self:onPanningRel(dist) + end + else + self._pan_to_scroll_later = self._pan_to_scroll_later + scroll_dist + end end end return true @@ -484,12 +590,40 @@ function ReaderPaging:onPanRelease(_, ges) self.view:PanningStop() end else - self.last_pan_relative_y = 0 - -- trigger full refresh to clear ghosting generated by previous fast refresh - UIManager:setDirty(nil, "full") + if self._pan_has_scrolled and self._pan_to_scroll_later ~= 0 then + self:onPanningRel(self._pan_to_scroll_later) + end + self._pan_started = false + self._pan_page_states_to_restore = nil + UIManager.currently_scrolling = false + if self._pan_has_scrolled then + self._pan_has_scrolled = false + -- Don't do any inertial scrolling if pan events come from + -- a mousewheel (which may have itself some inertia) + if (ges and ges.from_mousewheel) or not self.ui.scrolling:startInertialScroll() then + UIManager:setDirty(self.view.dialog, "partial") + end + end end end +function ReaderPaging:onHandledAsSwipe() + if self._pan_started then + -- Restore original position as this pan we've started handling + -- has ended up being a multiswipe or handled as a swipe to open + -- top or bottom menus + if self._pan_has_scrolled then + self.view.page_states = self._pan_page_states_to_restore + self:_gotoPage(self.view.page_states[#self.view.page_states].page, "scrolling") + UIManager:setDirty(self.view.dialog, "ui") + end + self._pan_page_states_to_restore = nil + self._pan_started = false + self._pan_has_scrolled = false + UIManager.currently_scrolling = false + end + return true +end function ReaderPaging:onZoomModeUpdate(new_mode) -- we need to remember zoom mode to handle page turn event self.zoom_mode = new_mode diff --git a/frontend/apps/reader/modules/readerrolling.lua b/frontend/apps/reader/modules/readerrolling.lua index cd21b1786..d174178ba 100644 --- a/frontend/apps/reader/modules/readerrolling.lua +++ b/frontend/apps/reader/modules/readerrolling.lua @@ -116,6 +116,7 @@ function ReaderRolling:init() {"0"}, doc = "go to end", event = "GotoPercent", args = 100, } end + self.pan_interval = TimeVal:new{ usec = 1000000 / self.pan_rate } table.insert(self.ui.postInitCallback, function() self.rendering_hash = self.ui.document:getDocumentRenderingHash() @@ -377,12 +378,19 @@ function ReaderRolling:setupTouchZones() { id = "rolling_pan", ges = "pan", - rate = self.pan_rate, screen_zone = { ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1, }, handler = function(ges) return self:onPan(nil, ges) end, }, + { + id = "rolling_pan_release", + ges = "pan_release", + screen_zone = { + ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1, + }, + handler = function(ges) return self:onPanRelease(nil, ges) end, + }, }) end @@ -515,7 +523,45 @@ function ReaderRolling:getLastPercent() end end +function ReaderRolling:onScrollSettingsUpdated(scroll_method, inertial_scroll_enabled, scroll_activation_delay) + self.scroll_method = scroll_method + self.scroll_activation_delay = TimeVal:new{ usec = scroll_activation_delay * 1000 } + if inertial_scroll_enabled then + self.ui.scrolling:setInertialScrollCallbacks( + function(distance) -- do_scroll_callback + if not self.ui.document then + return false + end + UIManager.currently_scrolling = true + local prev_pos = self.current_pos + self:_gotoPos(prev_pos + distance) + return self.current_pos ~= prev_pos + end, + function() -- scroll_done_callback + UIManager.currently_scrolling = false + if self.ui.document then + self.xpointer = self.ui.document:getXPointer() + end + UIManager:setDirty(self.view.dialog, "partial") + end + ) + else + self.ui.scrolling:setInertialScrollCallbacks(nil, nil) + end +end + function ReaderRolling:onSwipe(_, ges) + if self._pan_has_scrolled then + -- We did some panning but released after a short amount of time, + -- so this gesture ended up being a Swipe - and this swipe was + -- not handled by the other modules (so, not opening the menus). + -- Do as :onPanRelese() and ignore this swipe. + self:onPanRelease() -- no arg, so we know there we come from here + return true + else + self._pan_started = false + UIManager.currently_scrolling = false + end local direction = BD.flipDirectionIfMirroredUILayout(ges.direction) if direction == "west" then if G_reader_settings:nilOrFalse("page_turns_disable_swipe") then @@ -539,19 +585,116 @@ function ReaderRolling:onSwipe(_, ges) end function ReaderRolling:onPan(_, ges) - if self.view.view_mode == "scroll" then - local distance_type = "distance" - if self.ui.gesture and self.ui.gesture.multiswipes_enabled then - distance_type = "distance_delayed" + if ges.direction == "north" or ges.direction == "south" then + if ges.mousewheel_direction and self.view.view_mode == "page" then + -- Mouse wheel generates a Pan event: in page mode, move one + -- page per event. Scroll mode is handled in the 'else' branch + -- and use the wheeled distance. + UIManager:broadcastEvent(Event:new("GotoViewRel", -1 * ges.mousewheel_direction)) + elseif self.view.view_mode == "scroll" then + if not self._pan_started then + self._pan_started = true + -- Re-init state variables + self._pan_has_scrolled = false + self._pan_prev_relative_y = 0 + self._pan_to_scroll_later = 0 + self._pan_real_last_time = TimeVal.zero + if ges.mousewheel_direction then + self._pan_activation_time = false + else + self._pan_activation_time = ges.time + self.scroll_activation_delay + end + -- We will restore the previous position if this pan + -- ends up being a swipe or a multiswipe + self._pan_pos_at_pan_start = self.current_pos + end + local scroll_now = false + if self._pan_activation_time and ges.time >= self._pan_activation_time then + self._pan_activation_time = false -- We can go on, no need to check again + end + if not self._pan_activation_time and ges.time - self._pan_real_last_time >= self.pan_interval then + scroll_now = true + self._pan_real_last_time = ges.time + end + local scroll_dist = 0 + if self.scroll_method == self.ui.scrolling.SCROLL_METHOD_CLASSIC then + -- Scroll by the distance the finger moved since last pan event, + -- having the document follows the finger + scroll_dist = self._pan_prev_relative_y - ges.relative.y + self._pan_prev_relative_y = ges.relative.y + if not self._pan_has_scrolled then + -- Avoid checking this for each pan, no need once we have scrolled + if self.ui.scrolling:cancelInertialScroll() or self.ui.scrolling:cancelledByTouch() then + -- If this pan or its initial touch did cancel some inertial scrolling, + -- ignore activation delay to allow continuous scrolling + self._pan_activation_time = false + scroll_now = true + self._pan_real_last_time = ges.time + end + end + self.ui.scrolling:accountManualScroll(scroll_dist, ges.time) + elseif self.scroll_method == self.ui.scrolling.SCROLL_METHOD_TURBO then + -- Legacy scrolling "buggy" behaviour, that can actually be nice + -- Scroll by the distance from the initial finger position, this distance + -- controlling the speed of the scrolling) + if scroll_now then + scroll_dist = -ges.relative.y + end + -- We don't accumulate in _pan_to_scroll_later + elseif self.scroll_method == self.ui.scrolling.SCROLL_METHOD_ON_RELEASE then + self._pan_to_scroll_later = -ges.relative.y + if scroll_now then + self._pan_has_scrolled = true -- so we really apply it later + end + scroll_dist = 0 + scroll_now = false + end + if scroll_now then + local dist = self._pan_to_scroll_later + scroll_dist + self._pan_to_scroll_later = 0 + if dist ~= 0 then + self._pan_has_scrolled = true + UIManager.currently_scrolling = true + self:_gotoPos(self.current_pos + dist) + -- (We'll update self.xpointer only when done moving, at + -- release/swipe time as it might be expensive) + end + else + self._pan_to_scroll_later = self._pan_to_scroll_later + scroll_dist + end end - if ges.direction == "north" then - self:_gotoPos(self.current_pos + ges[distance_type]) - elseif ges.direction == "south" then - self:_gotoPos(self.current_pos - ges[distance_type]) + end + return true +end + +function ReaderRolling:onPanRelease(_, ges) + if self._pan_has_scrolled and self._pan_to_scroll_later ~= 0 then + self:_gotoPos(self.current_pos + self._pan_to_scroll_later) + end + self._pan_started = false + UIManager.currently_scrolling = false + if self._pan_has_scrolled then + self._pan_has_scrolled = false + self.xpointer = self.ui.document:getXPointer() + -- Don't do any inertial scrolling if pan events come from + -- a mousewheel (which may have itself some inertia) + if (ges and ges.from_mousewheel) or not self.ui.scrolling:startInertialScroll() then + UIManager:setDirty(self.view.dialog, "partial") end - --this is only use when mouse wheel is used - elseif ges.mousewheel_direction and self.view.view_mode == "page" then - UIManager:broadcastEvent(Event:new("GotoViewRel", -1 * ges.mousewheel_direction)) + end +end + +function ReaderRolling:onHandledAsSwipe() + if self._pan_started then + -- Restore original position as this pan we've started handling + -- has ended up being a multiswipe or handled as a swipe to open + -- top or bottom menus + self:_gotoPos(self._pan_pos_at_pan_start) + self._pan_started = false + self._pan_has_scrolled = false + UIManager.currently_scrolling = false + -- No specific refresh: the swipe/multiswipe might show other stuff, + -- and we'd want to avoid a flashing refresh end return true end diff --git a/frontend/apps/reader/modules/readerscrolling.lua b/frontend/apps/reader/modules/readerscrolling.lua new file mode 100644 index 000000000..93848f11b --- /dev/null +++ b/frontend/apps/reader/modules/readerscrolling.lua @@ -0,0 +1,419 @@ +local Device = require("device") +local Event = require("ui/event") +local InputContainer = require("ui/widget/container/inputcontainer") +local TimeVal = require("ui/timeval") +local UIManager = require("ui/uimanager") +local logger = require("logger") +local _ = require("gettext") +local T = require("ffi/util").template +local Screen = Device.screen + +-- This module exposes Scrolling settings, and additionnally +-- handles inertial scrolling on non-eInk devices. + +local SCROLL_METHOD_CLASSIC = "classic" +local SCROLL_METHOD_TURBO = "turbo" +local SCROLL_METHOD_ON_RELEASE = "on_release" + +local ReaderScrolling = InputContainer:new{ + -- Available scrolling methods (make them available to other reader modules) + SCROLL_METHOD_CLASSIC = SCROLL_METHOD_CLASSIC, + SCROLL_METHOD_TURBO = SCROLL_METHOD_TURBO, + SCROLL_METHOD_ON_RELEASE = SCROLL_METHOD_ON_RELEASE, + + scroll_method = SCROLL_METHOD_CLASSIC, + scroll_activation_delay = 0, -- 0 ms + inertial_scroll = false, + + pan_rate = 30, -- default 30 ops, will be adjusted in readerui + scroll_friction = 0.2, -- the lower, the sooner inertial scrolling stops + -- go at ending scrolling soon when we reach steps smaller than this + end_scroll_dist = Screen:scaleBySize(10), + -- no inertial scrolling if 300ms pause without any movement before release + pause_before_release_cancel_duration = TimeVal:new{ sec = 0, usec = 300000 }, + + -- Callbacks to be updated by readerrolling or readerpaging + _do_scroll_callback = function(distance) return false end, + _scroll_done_callback = function() end, + + _inertial_scroll_supported = false, + _inertial_scroll_enabled = false, + _inertial_scroll_interval = 1 / 30, + _inertial_scroll_action_scheduled = false, + _just_reschedule = false, + _last_manual_scroll_dy = 0, + _velocity = 0, +} + +function ReaderScrolling:init() + if not Device:isTouchDevice() then + -- No scroll support, no menu + return + end + + -- The different scrolling methods are handled directly by readerpaging/readerrolling + self.scroll_method = G_reader_settings:readSetting("scroll_method") + + -- Keep inertial scrolling available on the emulator (which advertizes itself as eInk) + if not Device:hasEinkScreen() or Device:isEmulator() then + self._inertial_scroll_supported = true + end + + if self._inertial_scroll_supported then + self.inertial_scroll = G_reader_settings:nilOrTrue("inertial_scroll") + self._inertial_scroll_interval = 1 / self.pan_rate + -- Set this so we don't have to check for nil, and in case + -- we miss a first touch event. + -- We can keep it obsolete, which will result in a long + -- duration and a small/zero velocity that won't hurt. + self._last_manual_scroll_timev = TimeVal.zero + self:_setupAction() + end + + self.ui.menu:registerToMainMenu(self) +end + +function ReaderScrolling:getDefaultScrollActivationDelay() + if (self.ui.gestures and self.ui.gestures.multiswipes_enabled) + or G_reader_settings:readSetting("activate_menu") ~= "tap" then + -- If swipes to show menu or multiswipes are enabled, higher default + -- scroll activation delay to avoid scrolling and restoring when + -- doing swipes + return 500 -- 500ms + end + -- Otherwise, no need for any delay + return 0 +end + +function ReaderScrolling:addToMainMenu(menu_items) + menu_items.scrolling = { + text = _("Scrolling"), + enabled_func = function() + -- Make it only enabled when in continuous/scroll mode + -- (different setting in self.view whether rolling or paging document) + if self.view and (self.view.page_scroll or self.view.view_mode == "scroll") then + return true + end + return false + end, + sub_item_table = { + { + text = _("Classic scrolling"), + help_text = _([[Classic scrolling will move the document with your finger.]]), + checked_func = function() + return self.scroll_method == self.SCROLL_METHOD_CLASSIC + end, + callback = function() + if self.scroll_method ~= self.SCROLL_METHOD_CLASSIC then + self.scroll_method = self.SCROLL_METHOD_CLASSIC + self:applyScrollSettings() + end + end, + }, + { + text = _("Turbo scrolling"), + help_text = _([[ +Turbo scrolling will scroll the document, at each step, by the distance from your initial finger position (rather than by the distance from your previous finger position). +It allows for faster scrolling without the need to lift and reposition your finger.]]), + checked_func = function() + return self.scroll_method == self.SCROLL_METHOD_TURBO + end, + callback = function() + if self.scroll_method ~= self.SCROLL_METHOD_TURBO then + self.scroll_method = self.SCROLL_METHOD_TURBO + self:applyScrollSettings() + end + end, + }, + { + text = _("On-release scrolling"), + help_text = _([[ +On-release scrolling will scroll the document by the panned distance only on finger up. +This is interesting on eInk if you only pan to better adjust page vertical position.]]), + checked_func = function() + return self.scroll_method == self.SCROLL_METHOD_ON_RELEASE + end, + callback = function() + if self.scroll_method ~= self.SCROLL_METHOD_ON_RELEASE then + self.scroll_method = self.SCROLL_METHOD_ON_RELEASE + self:applyScrollSettings() + end + end, + separator = true, + }, + { + text_func = function() + return T(_("Activation delay: %1 ms"), self.scroll_activation_delay) + end, + keep_menu_open = true, + callback = function(touchmenu_instance) + local scroll_activation_delay_default = self:getDefaultScrollActivationDelay() + local SpinWidget = require("ui/widget/spinwidget") + local widget = SpinWidget:new{ + title_text = _("Scroll activation delay"), + info_text = T(_([[ +A delay can be used to avoid scrolling when swipes or multiswipes are intended. + +The delay value is in milliseconds and can range from 0 to 2000 (2 seconds). +Default value: %1 ms]]), scroll_activation_delay_default), + width = math.floor(Screen:getWidth() * 0.75), + value = self.scroll_activation_delay, + value_min = 0, + value_max = 2000, + value_step = 100, + value_hold_step = 500, + ok_text = _("Set delay"), + default_value = scroll_activation_delay_default, + callback = function(spin) + self.scroll_activation_delay = spin.value + self:applyScrollSettings() + if touchmenu_instance then touchmenu_instance:updateItems() end + end + } + UIManager:show(widget) + end, + }, + } + } + if self._inertial_scroll_supported then + -- Add it before "Activation delay" to keep checkboxes together + table.insert(menu_items.scrolling.sub_item_table, 4, { + text = _("Allow inertial scrolling"), + enabled_func = function() + return self.scroll_method == self.SCROLL_METHOD_CLASSIC + end, + checked_func = function() + return self.scroll_method == self.SCROLL_METHOD_CLASSIC and self.inertial_scroll + end, + callback = function() + self.inertial_scroll = not self.inertial_scroll + self:applyScrollSettings() + end, + }) + end +end + +function ReaderScrolling:onReaderReady() + -- We don't know if the gestures plugin is loaded in :init(), but we know it here + self.scroll_activation_delay = G_reader_settings:readSetting("scroll_activation_delay") + or self:getDefaultScrollActivationDelay() + self:applyScrollSettings() +end + +function ReaderScrolling:applyScrollSettings() + G_reader_settings:saveSetting("scroll_method", self.scroll_method) + G_reader_settings:saveSetting("inertial_scroll", self.inertial_scroll) + if self.scroll_activation_delay == self:getDefaultScrollActivationDelay() then + G_reader_settings:delSetting("scroll_activation_delay") + else + G_reader_settings:saveSetting("scroll_activation_delay", self.scroll_activation_delay) + end + if self.scroll_method == self.SCROLL_METHOD_CLASSIC then + self._inertial_scroll_enabled = self.inertial_scroll + else + self._inertial_scroll_enabled = false + end + self:setupTouchZones() + self.ui:handleEvent(Event:new("ScrollSettingsUpdated", self.scroll_method, + self._inertial_scroll_enabled, self.scroll_activation_delay)) +end + +function ReaderScrolling:setupTouchZones() + self.ges_events = {} + self.onGesture = nil + + local zones = { + { + id = "inertial_scrolling_touch", + ges = "touch", + screen_zone = { + ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1, + }, + handler = function(ges) + -- A touch might set the start of the first pan event, + -- that we need to compute its duration + self._last_manual_scroll_timev = ges.time + -- If we are scrolling, a touch cancels it. + -- We want its release (which will trigger a tap) to not change pages. + -- This also allows a pan following this touch to skip any scroll + -- activation delay + self._cancelled_by_touch = self._inertial_scroll_action + and self._inertial_scroll_action(false) + or false + end, + }, + { + id = "inertial_scrolling_tap", + ges = "tap", + screen_zone = { + ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1, + }, + overrides = { + "tap_forward", + "tap_backward", + "readermenu_tap", + "readermenu_ext_tap", + "readerconfigmenu_tap", + "readerconfigmenu_ext_tap", + "readerfooter_tap", + "readerhighlight_tap", + "tap_link", + }, + handler = function() + -- Ignore tap if cancelled by its initial touch + if self._cancelled_by_touch then + self._cancelled_by_touch = false + return true + end + -- Otherwise, let it be handled by other tap handlers + end, + }, + } + if self._inertial_scroll_enabled then + self.ui:registerTouchZones(zones) + else + self.ui:unRegisterTouchZones(zones) + end +end + +function ReaderScrolling:isInertialScrollingEnabled() + return self._inertial_scroll_enabled +end + +function ReaderScrolling:setInertialScrollCallbacks(do_scroll_callback, scroll_done_callback) + self._do_scroll_callback = do_scroll_callback + self._scroll_done_callback = scroll_done_callback +end + +function ReaderScrolling:startInertialScroll() + if not self._inertial_scroll_enabled then + return false + end + return self._inertial_scroll_action(true) +end + +function ReaderScrolling:cancelInertialScroll() + if not self._inertial_scroll_enabled then + return + end + return self._inertial_scroll_action(false) +end + +function ReaderScrolling:cancelledByTouch() + return self._cancelled_by_touch +end + +function ReaderScrolling:accountManualScroll(dy, timev) + if not self._inertial_scroll_enabled then + return + end + self._last_manual_scroll_dy = dy + self._last_manual_scroll_duration = timev - self._last_manual_scroll_timev + self._last_manual_scroll_timev = timev +end + +function ReaderScrolling:_setupAction() + self._inertial_scroll_action = function(action) + -- action can be: + -- - true: stop any previous ongoing inertial scroll, then start a new one + -- (returns true if we started one) + -- - false: just stop any previous ongoing inertial scroll + -- (returns true if we did cancel one) + if action ~= nil then + local cancelled = false + if self._inertial_scroll_action_scheduled then + UIManager:unschedule(self._inertial_scroll_action) + self._inertial_scroll_action_scheduled = false + cancelled = true + self._scroll_done_callback() + logger.dbg("inertial scrolling cancelled") + end + if action == false then + self._last_manual_scroll_dy = 0 + return cancelled + end + + -- Initiate inertial scrolling (action=true), unless we should not + if UIManager:getTime() - self._last_manual_scroll_timev >= self.pause_before_release_cancel_duration then + -- but not if no finger move for 0.3s before finger up + self._last_manual_scroll_dy = 0 + return false + end + if self._last_manual_scroll_duration:isZero() or self._last_manual_scroll_dy == 0 then + return false + end + + -- Initial velocity is the one of the last pan scroll given to accountManualScroll() + local delay = self._last_manual_scroll_duration:tousecs() + if delay < 1 then delay = 1 end -- safety check + self._velocity = self._last_manual_scroll_dy * 1000000 / delay + self._last_manual_scroll_dy = 0 + + self._inertial_scroll_action_scheduled = true + -- We'll keep re-scheduling this same action, which will do + -- alternatively thanks to the _just_reschedule flag: + -- * either, in _inertial_scroll_interval, do a scroll + -- * or, then, at next tick, reschedule 1) + -- This is needed as the first one will cause a repaint that + -- may take more than _inertial_scroll_interval, which if we + -- didn't do that could be run before we process any input, + -- not allowing us to interrupt this inertial scrolling. + self._just_reschedule = false + UIManager:scheduleIn(self._inertial_scroll_interval, self._inertial_scroll_action) + -- self._stats_scroll_iterations = 0 + -- self._stats_scroll_distance = 0 + logger.dbg("inertial scrolling started") + return true + end + if not self._inertial_scroll_action_scheduled then + -- Safety check, shouldn't happen + return + end + if not self.ui.document then + -- might happen if scheduled and run after document is closed + return + end + + if self._just_reschedule then + -- just re-schedule this, so a real scrolling is done after the delay + self._just_reschedule = false + UIManager:scheduleIn(self._inertial_scroll_interval, self._inertial_scroll_action) + return + end + + -- Decrease velocity at each step + self._velocity = self._velocity * math.pow(self.scroll_friction, self._inertial_scroll_interval) + local dist = math.floor(self._velocity * self._inertial_scroll_interval) + if math.abs(dist) < self.end_scroll_dist then + -- Decrease it even more so scrolling stops sooner + self._velocity = self._velocity / 1.5 + end + -- self._stats_scroll_iterations = self._stats_scroll_iterations + 1 + -- self._stats_scroll_distance = self._stats_scroll_distance + dist + + logger.dbg("inertial scrolling by", dist) + local did_scroll = self._do_scroll_callback(dist) + + if did_scroll and math.abs(dist) > 2 then + -- Schedule at next tick the real re-scheduling + self._just_reschedule = true + UIManager:nextTick(self._inertial_scroll_action) + return + end + + -- We're done + self._inertial_scroll_action_scheduled = false + self._scroll_done_callback() + logger.dbg("inertial scrolling ended") + + --[[ + local Notification = require("ui/widget/notification") + UIManager:show(Notification:new{ + text = string.format("%d iterations, %d px scrolled", + self._stats_scroll_iterations, self._stats_scroll_distance), + }) + ]]-- + end +end + +return ReaderScrolling diff --git a/frontend/apps/reader/modules/readerview.lua b/frontend/apps/reader/modules/readerview.lua index 68f1c9966..2aaaa9bde 100644 --- a/frontend/apps/reader/modules/readerview.lua +++ b/frontend/apps/reader/modules/readerview.lua @@ -73,6 +73,9 @@ local ReaderView = OverlapGroup:extend{ flipping_visible = false, -- to ensure periodic flush of settings settings_last_save_tv = nil, + -- might be directly updated by readerpaging/readerrolling when + -- they handle some panning/scrolling, to request "fast" refreshes + currently_scrolling = false, } function ReaderView:init() @@ -614,7 +617,7 @@ function ReaderView:recalculate() end -- Flag a repaint so self:paintTo will be called -- NOTE: This is also unfortunately called during panning, essentially making sure we'll never be using "fast" for pans ;). - UIManager:setDirty(self.dialog, "partial") + UIManager:setDirty(self.dialog, self.currently_scrolling and "fast" or "partial") end function ReaderView:PanningUpdate(dx, dy) diff --git a/frontend/apps/reader/readerui.lua b/frontend/apps/reader/readerui.lua index d561da097..f5d8bcaba 100644 --- a/frontend/apps/reader/readerui.lua +++ b/frontend/apps/reader/readerui.lua @@ -33,6 +33,7 @@ local ReaderFont = require("apps/reader/modules/readerfont") local ReaderGoto = require("apps/reader/modules/readergoto") local ReaderHinting = require("apps/reader/modules/readerhinting") local ReaderHighlight = require("apps/reader/modules/readerhighlight") +local ReaderScrolling = require("apps/reader/modules/readerscrolling") local ReaderKoptListener = require("apps/reader/modules/readerkoptlistener") local ReaderLink = require("apps/reader/modules/readerlink") local ReaderMenu = require("apps/reader/modules/readermenu") @@ -334,6 +335,13 @@ function ReaderUI:init() }) end self.disable_double_tap = G_reader_settings:nilOrTrue("disable_double_tap") + -- scrolling (scroll settings + inertial scrolling) + self:registerModule("scrolling", ReaderScrolling:new{ + pan_rate = pan_rate, + dialog = self.dialog, + ui = self, + view = self.view, + }) -- back location stack self:registerModule("back", ReaderBack:new{ ui = self, diff --git a/frontend/device/sdl/device.lua b/frontend/device/sdl/device.lua index 06862aeff..b67959ec3 100644 --- a/frontend/device/sdl/device.lua +++ b/frontend/device/sdl/device.lua @@ -217,6 +217,7 @@ function Device:init() relative_delayed = fake_ges.relative_delayed, pos = pos, time = ev.time, + from_mousewheel = true, } local fake_pan_ev = Event:new("Pan", nil, fake_ges) local fake_release_ev = Event:new("Gesture", fake_ges_release) diff --git a/frontend/ui/elements/reader_menu_order.lua b/frontend/ui/elements/reader_menu_order.lua index bd69c36b2..458571e47 100644 --- a/frontend/ui/elements/reader_menu_order.lua +++ b/frontend/ui/elements/reader_menu_order.lua @@ -134,6 +134,7 @@ local order = { "----------------------------", "menu_activate", "page_turns", + "scrolling", "ignore_hold_corners", "screen_disable_double_tab", }, diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index 103535c29..6aca33208 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -22,6 +22,7 @@ local UIManager = { FULL_REFRESH_COUNT = G_reader_settings:isTrue("night_mode") and G_reader_settings:readSetting("night_full_refresh_count") or G_reader_settings:readSetting("full_refresh_count") or DEFAULT_FULL_REFRESH_COUNT, refresh_count = 0, + currently_scrolling = false, -- How long to wait between ZMQ wakeups: 50ms. ZMQ_TIMEOUT = 50 * 1000, @@ -1293,6 +1294,10 @@ function UIManager:_refresh(mode, region, dither) return end end + -- Downgrade all refreshes to "fast" when ReaderPaging or ReaderScrolling have set this flag + if self.currently_scrolling then + mode = "fast" + end if not region and mode == "full" then self.refresh_count = 0 -- reset counter on explicit full refresh end diff --git a/plugins/gestures.koplugin/main.lua b/plugins/gestures.koplugin/main.lua index 2f43ea7e2..1dbbb0760 100644 --- a/plugins/gestures.koplugin/main.lua +++ b/plugins/gestures.koplugin/main.lua @@ -1103,7 +1103,8 @@ function Gestures:gestureAction(action, ges) or (ges.ges == "hold" and self.ignore_hold_corners) then return else - Dispatcher:execute(self.ui, action_list, ges) + self.ui:handleEvent(Event:new("HandledAsSwipe")) + Dispatcher:execute(self.ui, action_list, ges) end return true end