mirror of
https://github.com/koreader/koreader
synced 2024-11-13 19:11:25 +00:00
419 lines
17 KiB
Lua
419 lines
17 KiB
Lua
local Device = require("device")
|
|
local Event = require("ui/event")
|
|
local UIManager = require("ui/uimanager")
|
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
|
local logger = require("logger")
|
|
local time = require("ui/time")
|
|
local _ = require("gettext")
|
|
local C_ = _.pgettext
|
|
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 = WidgetContainer:extend{
|
|
-- 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_ms = 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 = time.ms(300),
|
|
|
|
-- 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 = 0
|
|
self:_setupAction()
|
|
end
|
|
|
|
self.ui.menu:registerToMainMenu(self)
|
|
end
|
|
|
|
function ReaderScrolling:getDefaultScrollActivationDelay_ms()
|
|
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_ms)
|
|
end,
|
|
keep_menu_open = true,
|
|
callback = function(touchmenu_instance)
|
|
local scroll_activation_delay_default_ms = self:getDefaultScrollActivationDelay_ms()
|
|
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_ms),
|
|
width = math.floor(Screen:getWidth() * 0.75),
|
|
value = self.scroll_activation_delay_ms,
|
|
value_min = 0,
|
|
value_max = 2000,
|
|
value_step = 100,
|
|
value_hold_step = 500,
|
|
unit = C_("Time", "ms"),
|
|
ok_text = _("Set delay"),
|
|
default_value = scroll_activation_delay_default_ms,
|
|
callback = function(spin)
|
|
self.scroll_activation_delay_ms = 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_ms = G_reader_settings:readSetting("scroll_activation_delay")
|
|
or self:getDefaultScrollActivationDelay_ms()
|
|
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_ms == self:getDefaultScrollActivationDelay_ms() then
|
|
G_reader_settings:delSetting("scroll_activation_delay")
|
|
else
|
|
G_reader_settings:saveSetting("scroll_activation_delay", self.scroll_activation_delay_ms)
|
|
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_ms))
|
|
end
|
|
|
|
function ReaderScrolling:setupTouchZones()
|
|
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 == 0 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_us = time.to_us(self._last_manual_scroll_duration)
|
|
if delay_us < 1 then delay_us = 1 end -- safety check
|
|
self._velocity = self._last_manual_scroll_dy * time.s(1 / delay_us)
|
|
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 * 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 * (2/3)
|
|
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
|