2
0
mirror of https://github.com/koreader/koreader synced 2024-11-13 19:11:25 +00:00
koreader/plugins/gestures.koplugin/main.lua

1149 lines
47 KiB
Lua
Raw Normal View History

local BD = require("ui/bidi")
local ConfirmBox = require("ui/widget/confirmbox")
local DataStorage = require("datastorage")
local Device = require("device")
2020-07-17 03:03:23 +00:00
local Dispatcher = require("dispatcher")
local Event = require("ui/event")
2020-07-17 03:03:23 +00:00
local FFIUtil = require("ffi/util")
local Geom = require("ui/geometry")
local GestureDetector = require("device/gesturedetector")
local GestureRange = require("ui/gesturerange")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local InputDialog = require("ui/widget/inputdialog")
2020-07-17 03:03:23 +00:00
local LuaSettings = require("luasettings")
local Screen = require("device").screen
local SpinWidget = require("ui/widget/spinwidget")
local UIManager = require("ui/uimanager")
Clarify our OOP semantics across the codebase (#9586) Basically: * Use `extend` for class definitions * Use `new` for object instantiations That includes some minor code cleanups along the way: * Updated `Widget`'s docs to make the semantics clearer. * Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283) * Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass). * Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events. * Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier. * Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references. * ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak). * Terminal: Make sure the shell is killed on plugin teardown. * InputText: Fix Home/End/Del physical keys to behave sensibly. * InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...). * OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of. * ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed! * Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
2020-07-17 03:03:23 +00:00
local util = require("util")
2020-09-15 18:39:32 +00:00
local T = FFIUtil.template
local time = require("ui/time")
local _ = require("gettext")
local C_ = _.pgettext
if not Device:isTouchDevice() then
return { disabled = true, }
end
Clarify our OOP semantics across the codebase (#9586) Basically: * Use `extend` for class definitions * Use `new` for object instantiations That includes some minor code cleanups along the way: * Updated `Widget`'s docs to make the semantics clearer. * Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283) * Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass). * Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events. * Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier. * Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references. * ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak). * Terminal: Make sure the shell is killed on plugin teardown. * InputText: Fix Home/End/Del physical keys to behave sensibly. * InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...). * OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of. * ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed! * Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
local Gestures = WidgetContainer:extend{
name = "gestures",
2020-07-17 03:03:23 +00:00
settings_data = nil,
gestures = nil,
2020-07-17 03:03:23 +00:00
defaults = nil,
custom_multiswipes = nil,
updated = false,
}
2020-07-17 03:03:23 +00:00
local gestures_path = FFIUtil.joinPath(DataStorage:getSettingsDir(), "gestures.lua")
local gestures_list = {
tap_top_left_corner = _("Top left"),
tap_top_right_corner = _("Top right"),
tap_left_bottom_corner = _("Bottom left"),
tap_right_bottom_corner = _("Bottom right"),
hold_top_left_corner = _("Top left"),
hold_top_right_corner = _("Top right"),
hold_bottom_left_corner = _("Bottom left"),
hold_bottom_right_corner = _("Bottom right"),
one_finger_swipe_left_edge_down = _("Left edge down"),
one_finger_swipe_left_edge_up = _("Left edge up"),
one_finger_swipe_right_edge_down = _("Right edge down"),
one_finger_swipe_right_edge_up = _("Right edge up"),
one_finger_swipe_top_edge_right = _("Top edge right"),
one_finger_swipe_top_edge_left = _("Top edge left"),
one_finger_swipe_bottom_edge_right = _("Bottom edge right"),
one_finger_swipe_bottom_edge_left = _("Bottom edge left"),
double_tap_left_side = _("Left side"),
double_tap_right_side = _("Right side"),
double_tap_top_left_corner = _("Top left"),
double_tap_top_right_corner = _("Top right"),
double_tap_bottom_left_corner = _("Bottom left"),
double_tap_bottom_right_corner = _("Bottom right"),
two_finger_tap_top_left_corner = _("Top left"),
two_finger_tap_top_right_corner = _("Top right"),
two_finger_tap_bottom_left_corner = _("Bottom left"),
two_finger_tap_bottom_right_corner = _("Bottom right"),
short_diagonal_swipe = _("Short diagonal swipe"),
two_finger_swipe_east = "",
two_finger_swipe_west = "",
two_finger_swipe_south = "",
two_finger_swipe_north = "",
two_finger_swipe_northeast = "",
two_finger_swipe_northwest = "",
two_finger_swipe_southeast = "",
two_finger_swipe_southwest = "",
spread_gesture = _("Spread"),
pinch_gesture = _("Pinch"),
rotate_cw = _("Rotate ⤸ 90°"),
rotate_ccw = _("Rotate ⤹ 90°"),
2020-07-17 03:03:23 +00:00
multiswipe = "", -- otherwise registerGesture() won't pick up on multiswipes
multiswipe_west_east = "⬅ ➡",
multiswipe_east_west = "➡ ⬅",
multiswipe_north_east = "⬆ ➡",
multiswipe_north_west = "⬆ ⬅",
multiswipe_north_south = "⬆ ⬇",
multiswipe_east_north = "➡ ⬆",
multiswipe_west_north = "⬅ ⬆",
multiswipe_east_south = "➡ ⬇",
multiswipe_south_north = "⬇ ⬆",
multiswipe_south_east = "⬇ ➡",
multiswipe_south_west = "⬇ ⬅",
multiswipe_west_south = "⬅ ⬇",
multiswipe_north_south_north = "⬆ ⬇ ⬆",
multiswipe_south_north_south = "⬇ ⬆ ⬇",
multiswipe_west_east_west = "⬅ ➡ ⬅",
multiswipe_east_west_east = "➡ ⬅ ➡",
multiswipe_south_west_north = "⬇ ⬅ ⬆",
multiswipe_north_east_south = "⬆ ➡ ⬇",
multiswipe_north_west_south = "⬆ ⬅ ⬇",
multiswipe_west_south_east = "⬅ ⬇ ➡",
multiswipe_west_north_east = "⬅ ⬆ ➡",
multiswipe_east_south_west = "➡ ⬇ ⬅",
multiswipe_east_north_west = "➡ ⬆ ⬅",
multiswipe_south_east_north = "⬇ ➡ ⬆",
multiswipe_east_north_west_east = "➡ ⬆ ⬅ ➡",
multiswipe_south_east_north_south = "⬇ ➡ ⬆ ⬇",
multiswipe_east_south_west_north = "➡ ⬇ ⬅ ⬆",
multiswipe_west_south_east_north = "⬅ ⬇ ➡ ⬆",
multiswipe_south_east_north_west = "⬇ ➡ ⬆ ⬅",
multiswipe_south_west_north_east = "⬇ ⬅ ⬆ ➡",
multiswipe_southeast_northeast = "⬊ ⬈",
multiswipe_northeast_southeast = "⬈ ⬊",
multiswipe_northwest_southwest_northwest = "⬉ ⬋ ⬉",
multiswipe_southeast_southwest_northwest = "⬊ ⬋ ⬉",
multiswipe_southeast_northeast_northwest = "⬊ ⬈ ⬉",
}
local multiswipes_info_text = _([[
Multiswipes allow you to perform complex gestures built up out of multiple swipe directions, never losing touch with the screen.
These advanced gestures consist of either straight swipes or diagonal swipes. To ensure accuracy, they can't be mixed.]])
-- If the gesture contains the "toggle_touch_input" action,
-- mark it "always active" to make sure that InputContainer won't block it after the IgnoreTouchInput Event.
function Gestures:isGestureAlwaysActive(ges, multiswipe_directions)
-- Handle multiswipes properly
-- NOTE: This is a bit clunky, as ges comes from the list of registered touch zones,
-- while multiswipe_directions comes from the actual input event.
-- Alas, all our multiswipe gestures are handled by a single "multiswipe" zone.
if self.multiswipes_enabled then
if ges == "multiswipe" and multiswipe_directions then
ges = "multiswipe_" .. self:safeMultiswipeName(multiswipe_directions)
end
end
return self.gestures[ges] and self.gestures[ges].toggle_touch_input
end
function Gestures:init()
2020-07-17 03:03:23 +00:00
local defaults_path = FFIUtil.joinPath(self.path, "defaults.lua")
if not lfs.attributes(gestures_path, "mode") then
FFIUtil.copyFile(defaults_path, gestures_path)
end
self.ignore_hold_corners = G_reader_settings:isTrue("ignore_hold_corners")
self.multiswipes_enabled = G_reader_settings:isTrue("multiswipes_enabled")
self.is_docless = self.ui == nil or self.ui.document == nil
self.ges_mode = self.is_docless and "gesture_fm" or "gesture_reader"
2020-07-17 03:03:23 +00:00
self.defaults = LuaSettings:open(defaults_path).data[self.ges_mode]
if not self.settings_data then
self.settings_data = LuaSettings:open(gestures_path)
end
self.gestures = self.settings_data.data[self.ges_mode]
self.custom_multiswipes = self.settings_data.data["custom_multiswipes"]
if G_reader_settings:has("gesture_fm") or G_reader_settings:has("gesture_reader") then
-- Migrate old gestures
local Migration = require("migration")
Migration:migrateGestures(self)
end
-- Some of these defaults need to be reversed in RTL mirrored UI,
-- and as we set them in the saved gestures, we need to reset them
-- to the defaults in case of UI language's direction change.
local mirrored_if_rtl = {
tap_top_left_corner = "tap_top_right_corner",
tap_right_bottom_corner = "tap_left_bottom_corner",
double_tap_left_side = "double_tap_right_side",
}
2020-07-17 03:03:23 +00:00
local is_rtl = BD.mirroredUILayout()
if is_rtl then
for k, v in pairs(mirrored_if_rtl) do
2020-07-17 03:03:23 +00:00
self.defaults[k], self.defaults[v] = self.defaults[v], self.defaults[k]
end
end
-- We remember the last UI direction gestures were made on. If it changes,
-- reset the mirrored_if_rtl ones to the default for the new direction.
local ges_dir_setting = self.ges_mode.."ui_lang_direction_rtl"
local prev_lang_dir_rtl = G_reader_settings:isTrue(ges_dir_setting)
if (is_rtl and not prev_lang_dir_rtl) or (not is_rtl and prev_lang_dir_rtl) then
local reset = false
for k, v in pairs(mirrored_if_rtl) do
-- We only replace them if they are still the other direction's default.
-- If not, the user has changed them: let him deal with setting new ones if needed.
2020-07-17 03:03:23 +00:00
if util.tableEquals(self.gestures[k], self.defaults[v]) then
self.gestures[k] = self.defaults[k]
reset = true
end
2020-07-17 03:03:23 +00:00
if util.tableEquals(self.gestures[v], self.defaults[k]) then
self.gestures[v] = self.defaults[v]
reset = true
end
end
if reset then
self.updated = true
logger.info("UI language direction changed: resetting some gestures to direction default")
end
G_reader_settings:flipNilOrFalse(ges_dir_setting)
end
self.ui.menu:registerToMainMenu(self)
Dispatcher:init()
self:initGesture()
-- Overload InputContainer's stub to allow it to recognize "always active" gestures
InputContainer.isGestureAlwaysActive = function(this, ges, multiswipe_directions) return self:isGestureAlwaysActive(ges, multiswipe_directions) end
end
function Gestures:onCloseWidget()
-- Restore the stub implementation on teardown, to avoid pinning a stale instance of ourselves
InputContainer.isGestureAlwaysActive = InputContainer._isGestureAlwaysActive
end
2020-07-17 03:03:23 +00:00
function Gestures:gestureTitleFunc(ges)
local title = gestures_list[ges] or self:friendlyMultiswipeName(ges)
return T(_("%1 (%2)"), title, Dispatcher:menuTextFunc(self.gestures[ges]))
2020-07-17 03:03:23 +00:00
end
function Gestures:genMenu(ges)
local sub_items = {}
if gestures_list[ges] ~= nil then
table.insert(sub_items, {
text = T(_("%1 (default)"), Dispatcher:menuTextFunc(self.defaults[ges])),
2020-07-17 03:03:23 +00:00
keep_menu_open = true,
separator = true,
checked_func = function()
return util.tableEquals(self.gestures[ges], self.defaults[ges])
end,
callback = function()
self.gestures[ges] = util.tableDeepCopy(self.defaults[ges])
self.updated = true
2020-07-17 03:03:23 +00:00
end,
})
end
2020-07-17 03:03:23 +00:00
table.insert(sub_items, {
text = _("Pass through"),
keep_menu_open = true,
checked_func = function()
return self.gestures[ges] == nil
end,
callback = function()
self.gestures[ges] = nil
self.updated = true
2020-07-17 03:03:23 +00:00
end,
})
Dispatcher:addSubMenu(self, sub_items, self.gestures, ges)
2020-07-17 03:03:23 +00:00
return sub_items
end
2020-07-17 03:03:23 +00:00
function Gestures:genSubItem(ges, separator, hold_callback)
local reader_only = {tap_top_left_corner=true, tap_top_right_corner=true,}
2020-07-17 03:03:23 +00:00
local enabled_func
if reader_only[ges] then
enabled_func = function() return self.ges_mode == "gesture_reader" end
end
return {
2020-07-17 03:03:23 +00:00
text_func = function() return self:gestureTitleFunc(ges) end,
enabled_func = enabled_func,
sub_item_table_func = function() return self:genMenu(ges) end,
2020-07-17 03:03:23 +00:00
separator = separator,
hold_callback = hold_callback,
menu_item_id = ges,
}
end
2020-07-17 03:03:23 +00:00
function Gestures:genSubItemTable(gestures)
local sub_item_table = {}
for _, item in ipairs(gestures) do
table.insert(sub_item_table, self:genSubItem(item))
end
2020-07-17 03:03:23 +00:00
return sub_item_table
end
function Gestures:genMultiswipeMenu()
local sub_item_table = {}
-- { multiswipe name, separator }
local multiswipe_list = {
{"multiswipe_west_east",},
{"multiswipe_east_west",},
{"multiswipe_north_south",},
{"multiswipe_south_north", true},
{"multiswipe_north_west",},
{"multiswipe_north_east",},
{"multiswipe_south_west",},
{"multiswipe_south_east",},
{"multiswipe_east_north",},
{"multiswipe_west_north",},
{"multiswipe_east_south",},
{"multiswipe_west_south", true},
{"multiswipe_north_south_north",},
{"multiswipe_south_north_south",},
{"multiswipe_west_east_west",},
{"multiswipe_east_west_east", true},
{"multiswipe_south_west_north",},
{"multiswipe_north_east_south",},
{"multiswipe_north_west_south",},
{"multiswipe_west_south_east",},
{"multiswipe_west_north_east",},
{"multiswipe_east_south_west",},
{"multiswipe_east_north_west",},
{"multiswipe_south_east_north", true},
{"multiswipe_east_north_west_east",},
{"multiswipe_south_east_north_south", true},
{"multiswipe_east_south_west_north",},
{"multiswipe_west_south_east_north",},
{"multiswipe_south_east_north_west",},
{"multiswipe_south_west_north_east", true},
{"multiswipe_southeast_northeast",},
{"multiswipe_northeast_southeast",},
{"multiswipe_northwest_southwest_northwest",},
{"multiswipe_southeast_southwest_northwest",},
{"multiswipe_southeast_northeast_northwest",},
}
2020-07-17 03:03:23 +00:00
for _, item in ipairs(multiswipe_list) do
table.insert(sub_item_table, self:genSubItem(item[1], item[2]))
end
return sub_item_table
end
local multiswipe_to_arrow = {
east = "",
west = "",
north = "",
south = "",
northeast = "",
northwest = "",
southeast = "",
southwest = "",
}
function Gestures:friendlyMultiswipeName(multiswipe)
return multiswipe:gsub("multiswipe", ""):gsub("_", " "):gsub("%S+", multiswipe_to_arrow)
end
function Gestures:safeMultiswipeName(multiswipe)
return multiswipe:gsub(" ", "_")
end
function Gestures:multiswipeRecorder(touchmenu_instance)
local multiswipe_recorder
multiswipe_recorder = InputDialog:new{
title = _("Multiswipe recorder"),
input_hint = _("Make a multiswipe gesture"),
buttons = {
{
2020-07-17 03:03:23 +00:00
{
text = _("Cancel"),
id = "close",
2020-07-17 03:03:23 +00:00
callback = function()
UIManager:close(multiswipe_recorder)
end,
},
{
text = _("Save"),
is_enter_default = true,
callback = function()
local recorded_multiswipe = multiswipe_recorder._raw_multiswipe
if not recorded_multiswipe then return end
logger.dbg("Multiswipe recorder detected:", recorded_multiswipe)
if gestures_list[recorded_multiswipe] ~= nil then
UIManager:show(InfoMessage:new{
text = _("Recorded multiswipe already exists."),
show_icon = false,
timeout = 5,
})
return
end
self.custom_multiswipes[recorded_multiswipe] = true
self.updated = true
2020-07-17 03:03:23 +00:00
--touchmenu_instance.item_table = self:genMultiswipeMenu()
-- We need to update touchmenu_instance.item_table in-place for the upper
-- menu to have it updated too
local item_table = touchmenu_instance.item_table
while #item_table > 0 do
table.remove(item_table, #item_table)
end
for __, v in ipairs(self:genCustomMultiswipeSubmenu()) do
table.insert(item_table, v)
end
touchmenu_instance:updateItems()
UIManager:close(multiswipe_recorder)
end,
},
}
},
}
2020-07-17 03:03:23 +00:00
multiswipe_recorder.ges_events.Multiswipe = {
GestureRange:new{
ges = "multiswipe",
range = Geom:new{
x = 0, y = 0,
w = Screen:getWidth(),
h = Screen:getHeight(),
},
}
}
2020-07-17 03:03:23 +00:00
function multiswipe_recorder:onMultiswipe(arg, ges)
multiswipe_recorder._raw_multiswipe = "multiswipe_" .. Gestures:safeMultiswipeName(ges.multiswipe_directions)
multiswipe_recorder:setInputText(Gestures:friendlyMultiswipeName(ges.multiswipe_directions))
end
2020-07-17 03:03:23 +00:00
UIManager:show(multiswipe_recorder)
end
2020-07-17 03:03:23 +00:00
function Gestures:genCustomMultiswipeSubmenu()
local submenu = {
{
text = _("Multiswipe recorder"),
keep_menu_open = true,
callback = function(touchmenu_instance)
self:multiswipeRecorder(touchmenu_instance)
end,
help_text = _("The number of possible multiswipe gestures is theoretically infinite. With the multiswipe recorder you can easily record your own."),
}
}
for item in FFIUtil.orderedPairs(self.custom_multiswipes) do
local hold_callback = function(touchmenu_instance)
UIManager:show(ConfirmBox:new{
text = T(_("Remove custom multiswipe %1?"), self:friendlyMultiswipeName(item)),
ok_text = _("Remove"),
ok_callback = function()
-- remove from list of custom multiswipes
self.custom_multiswipes[item] = nil
-- remove any settings for the muliswipe
self.settings_data.data["gesture_fm"][item] = nil
self.settings_data.data["gesture_reader"][item] = nil
self.updated = true
2020-07-17 03:03:23 +00:00
--touchmenu_instance.item_table = self:genMultiswipeMenu()
-- We need to update touchmenu_instance.item_table in-place for the upper
-- menu to have it updated too
local item_table = touchmenu_instance.item_table
while #item_table > 0 do
table.remove(item_table, #item_table)
end
2020-07-17 03:03:23 +00:00
for __, v in ipairs(self:genCustomMultiswipeSubmenu()) do
table.insert(item_table, v)
end
touchmenu_instance:updateItems()
end,
2020-07-17 03:03:23 +00:00
})
end
table.insert(submenu, self:genSubItem(item, nil, hold_callback))
end
return submenu
end
function Gestures:addIntervals(menu_items)
menu_items.gesture_intervals = {
text = _("Gesture intervals"),
sub_item_table = {
{
text = _("Text selection rate"),
keep_menu_open = true,
callback = function()
local default_value = Screen.low_pan_rate and 5.0 or 30.0
local current_value = G_reader_settings:readSetting("hold_pan_rate", default_value)
local items = SpinWidget:new{
title_text = _("Text selection rate"),
info_text = _([[
The rate is how often screen will be refreshed per second while selecting text.
Higher values mean faster screen updates, but also use more CPU.]]),
width = math.floor(Screen:getWidth() * 0.75),
value = current_value,
value_min = 1.0,
value_max = 60.0,
value_step = 1,
value_hold_step = 15,
unit = C_("Frequency", "Hz"),
ok_text = _("Set rate"),
default_value = default_value,
callback = function(spin)
G_reader_settings:saveSetting("hold_pan_rate", spin.value)
UIManager:broadcastEvent(Event:new("UpdateHoldPanRate"))
end
}
UIManager:show(items)
end,
separator = true,
},
{
text = _("Tap interval"),
keep_menu_open = true,
callback = function()
local items = SpinWidget:new{
title_text = _("Tap interval"),
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 seconds) to 2000 (2 seconds).]]),
width = math.floor(Screen:getWidth() * 0.75),
value = time.to_ms(GestureDetector.ges_tap_interval),
value_min = 0,
value_max = 2000,
value_step = 50,
value_hold_step = 200,
unit = C_("Time", "ms"),
ok_text = _("Set interval"),
default_value = GestureDetector.TAP_INTERVAL_MS,
callback = function(spin)
G_reader_settings:saveSetting("ges_tap_interval_ms", spin.value)
GestureDetector.ges_tap_interval = time.ms(spin.value)
end
}
UIManager:show(items)
end,
},
{
text = _("Tap interval on keyboard"),
keep_menu_open = true,
callback = function()
local items = SpinWidget:new{
title_text = _("Tap interval on keyboard"),
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 seconds) to 2000 (2 seconds).]]),
width = math.floor(Screen:getWidth() * 0.75),
value = time.to_ms(G_reader_settings:readSetting("ges_tap_interval_on_keyboard_ms", 0)),
value_min = 0,
value_max = 2000,
value_step = 50,
value_hold_step = 200,
unit = C_("Time", "ms"),
ok_text = _("Set interval"),
default_value = 0,
callback = function(spin)
G_reader_settings:saveSetting("ges_tap_interval_on_keyboard_ms", spin.value)
end
}
UIManager:show(items)
end,
},
{
text = _("Double tap interval"),
keep_menu_open = true,
callback = function()
local items = SpinWidget:new{
title_text = _("Double tap interval"),
info_text = _([[
When double tap is enabled, this sets the time to wait for the second tap. A single tap will take at least this long to be detected.
The interval value is in milliseconds and can range from 100 (0.1 seconds) to 2000 (2 seconds).]]),
width = math.floor(Screen:getWidth() * 0.75),
value = time.to_ms(GestureDetector.ges_double_tap_interval),
value_min = 100,
value_max = 2000,
value_step = 100,
value_hold_step = 500,
unit = C_("Time", "ms"),
ok_text = _("Set interval"),
default_value = GestureDetector.DOUBLE_TAP_INTERVAL_MS,
callback = function(spin)
G_reader_settings:saveSetting("ges_double_tap_interval_ms", spin.value)
GestureDetector.ges_double_tap_interval = time.ms(spin.value)
end
}
UIManager:show(items)
end,
},
{
text = _("Two finger tap duration"),
keep_menu_open = true,
callback = function()
local items = SpinWidget:new{
title_text = _("Two finger tap duration"),
info_text = _([[
This sets the allowed duration of any of the two fingers touch/release for the combined event to be considered a two finger tap.
The duration value is in milliseconds and can range from 100 (0.1 seconds) to 2000 (2 seconds).]]),
width = math.floor(Screen:getWidth() * 0.75),
value = time.to_ms(GestureDetector.ges_two_finger_tap_duration),
value_min = 100,
value_max = 2000,
value_step = 100,
value_hold_step = 500,
unit = C_("Time", "ms"),
ok_text = _("Set duration"),
default_value = GestureDetector.TWO_FINGER_TAP_DURATION_MS,
callback = function(spin)
G_reader_settings:saveSetting("ges_two_finger_tap_duration_ms", spin.value)
GestureDetector.ges_two_finger_tap_duration = time.ms(spin.value)
end
}
UIManager:show(items)
end,
},
{
text = _("Long-press interval"),
keep_menu_open = true,
callback = function()
local items = SpinWidget:new{
title_text = _("Long-press interval"),
info_text = _([[
If a touch is not released in this interval, it is considered a long-press. On document text, single word selection will then be triggered.
The interval value is in milliseconds and can range from 100 (0.1 seconds) to 2000 (2 seconds).]]),
width = math.floor(Screen:getWidth() * 0.75),
value = time.to_ms(GestureDetector.ges_hold_interval),
value_min = 100,
value_max = 2000,
value_step = 100,
value_hold_step = 500,
unit = C_("Time", "ms"),
ok_text = _("Set interval"),
default_value = GestureDetector.HOLD_INTERVAL_MS,
callback = function(spin)
G_reader_settings:saveSetting("ges_hold_interval_ms", spin.value)
GestureDetector.ges_hold_interval = time.ms(spin.value)
end
}
UIManager:show(items)
end,
},
{
text = _("Swipe interval"),
keep_menu_open = true,
callback = function()
local items = SpinWidget:new{
title_text = _("Swipe interval"),
info_text = _([[
This sets the maximum delay between the start and the end of a swipe for it to be considered a swipe. Above this interval, it's considered panning.
The interval value is in milliseconds and can range from 100 (0.1 seconds) to 2000 (2 seconds).]]),
width = math.floor(Screen:getWidth() * 0.75),
value = time.to_ms(GestureDetector.ges_swipe_interval),
value_min = 100,
value_max = 2000,
value_step = 100,
unit = C_("Time", "ms"),
value_hold_step = 500,
ok_text = _("Set interval"),
default_value = GestureDetector.SWIPE_INTERVAL_MS,
callback = function(spin)
G_reader_settings:saveSetting("ges_swipe_interval_ms", spin.value)
GestureDetector.ges_swipe_interval = time.ms(spin.value)
end
}
UIManager:show(items)
end,
},
}
}
2020-07-17 03:03:23 +00:00
end
2020-07-17 03:03:23 +00:00
function Gestures:addToMainMenu(menu_items)
menu_items.gesture_manager = {
text = _("Gesture manager"),
sub_item_table = {
{
text = _("Turn on multiswipes"),
checked_func = function() return self.multiswipes_enabled end,
callback = function()
G_reader_settings:toggle("multiswipes_enabled")
2020-07-17 03:03:23 +00:00
self.multiswipes_enabled = G_reader_settings:isTrue("multiswipes_enabled")
end,
help_text = multiswipes_info_text,
},
{
text = _("Multiswipes"),
sub_item_table = self:genMultiswipeMenu(),
enabled_func = function() return self.multiswipes_enabled end,
},
{
text = _("Custom multiswipes"),
sub_item_table = self:genCustomMultiswipeSubmenu(),
enabled_func = function() return self.multiswipes_enabled end,
separator = true,
},
{
text = _("Tap corner"),
sub_item_table = self:genSubItemTable({"tap_top_left_corner", "tap_top_right_corner", "tap_left_bottom_corner", "tap_right_bottom_corner"}),
},
{
text = _("Long-press on corner"),
2020-07-17 03:03:23 +00:00
sub_item_table = self:genSubItemTable({"hold_top_left_corner", "hold_top_right_corner", "hold_bottom_left_corner", "hold_bottom_right_corner"}),
},
{
text = _("One-finger swipe"),
2021-04-27 18:56:15 +00:00
sub_item_table = self:genSubItemTable({"short_diagonal_swipe", "one_finger_swipe_left_edge_down", "one_finger_swipe_left_edge_up", "one_finger_swipe_right_edge_down", "one_finger_swipe_right_edge_up", "one_finger_swipe_top_edge_right", "one_finger_swipe_top_edge_left", "one_finger_swipe_bottom_edge_right", "one_finger_swipe_bottom_edge_left"}),
2020-07-17 03:03:23 +00:00
},
{
text = _("Double tap"),
enabled_func = function()
return self.ges_mode == "gesture_reader" and self.ui.disable_double_tap ~= true
end,
sub_item_table = self:genSubItemTable({"double_tap_left_side", "double_tap_right_side", "double_tap_top_left_corner", "double_tap_top_right_corner", "double_tap_bottom_left_corner", "double_tap_bottom_right_corner"}),
},
},
}
if Device:hasMultitouch() then
2020-07-17 03:03:23 +00:00
table.insert(menu_items.gesture_manager.sub_item_table, {
text = _("Two-finger tap corner"),
2020-07-17 03:03:23 +00:00
sub_item_table = self:genSubItemTable({"two_finger_tap_top_left_corner", "two_finger_tap_top_right_corner", "two_finger_tap_bottom_left_corner", "two_finger_tap_bottom_right_corner"}),
})
table.insert(menu_items.gesture_manager.sub_item_table, {
text = _("Two-finger swipe"),
2020-07-17 03:03:23 +00:00
sub_item_table = self:genSubItemTable({"two_finger_swipe_east", "two_finger_swipe_west", "two_finger_swipe_south", "two_finger_swipe_north", "two_finger_swipe_northeast", "two_finger_swipe_northwest", "two_finger_swipe_southeast", "two_finger_swipe_southwest"}),
})
table.insert(menu_items.gesture_manager.sub_item_table, {
text = _("Spread and pinch"),
2020-07-17 03:03:23 +00:00
sub_item_table = self:genSubItemTable({"spread_gesture", "pinch_gesture"}),
})
table.insert(menu_items.gesture_manager.sub_item_table, {
text = _("Rotation"),
sub_item_table = self:genSubItemTable({"rotate_cw", "rotate_ccw"}),
})
end
2020-07-17 03:03:23 +00:00
self:addIntervals(menu_items)
end
2020-07-17 03:03:23 +00:00
function Gestures:initGesture()
for ges, _ in pairs(gestures_list) do
self:setupGesture(ges)
end
end
2020-07-17 03:03:23 +00:00
function Gestures:setupGesture(ges)
local ges_type
local zone
local overrides
local direction, distance
local zone_fullscreen = {
ratio_x = 0, ratio_y = 0,
ratio_w = 1, ratio_h = 1,
}
local zone_left_edge = {
ratio_x = 0, ratio_y = 0,
ratio_w = 1/8, ratio_h = 1,
}
local zone_right_edge = {
ratio_x = 7/8, ratio_y = 0,
ratio_w = 1/8, ratio_h = 1,
}
local zone_top_edge = {
ratio_x = 0, ratio_y = 0,
ratio_w = 1, ratio_h = 1/8,
}
local zone_bottom_edge = {
ratio_x = 0, ratio_y = 7/8,
ratio_w = 1, ratio_h = 1/8,
}
local dtap_zone_top_left = G_defaults:readSetting("DTAP_ZONE_TOP_LEFT")
local zone_top_left_corner = {
ratio_x = dtap_zone_top_left.x,
ratio_y = dtap_zone_top_left.y,
ratio_w = dtap_zone_top_left.w,
ratio_h = dtap_zone_top_left.h,
}
local dtap_zone_top_right = G_defaults:readSetting("DTAP_ZONE_TOP_RIGHT")
local zone_top_right_corner = {
ratio_x = dtap_zone_top_right.x,
ratio_y = dtap_zone_top_right.y,
ratio_w = dtap_zone_top_right.w,
ratio_h = dtap_zone_top_right.h,
}
local dtap_zone_bottom_left = G_defaults:readSetting("DTAP_ZONE_BOTTOM_LEFT")
local zone_bottom_left_corner = {
ratio_x = dtap_zone_bottom_left.x,
ratio_y = dtap_zone_bottom_left.y,
ratio_w = dtap_zone_bottom_left.w,
ratio_h = dtap_zone_bottom_left.h,
}
local dtap_zone_bottom_right = G_defaults:readSetting("DTAP_ZONE_BOTTOM_RIGHT")
local zone_bottom_right_corner = {
ratio_x = dtap_zone_bottom_right.x,
ratio_y = dtap_zone_bottom_right.y,
ratio_w = dtap_zone_bottom_right.w,
ratio_h = dtap_zone_bottom_right.h,
}
-- NOTE: The defaults are effectively mapped to G_defaults:readSetting("DTAP_ZONE_BACKWARD") & G_defaults:readSetting("DTAP_ZONE_FORWARD")
local ddouble_tap_zone_prev_chapter = G_defaults:readSetting("DDOUBLE_TAP_ZONE_PREV_CHAPTER")
local zone_left = {
ratio_x = ddouble_tap_zone_prev_chapter.x,
ratio_y = ddouble_tap_zone_prev_chapter.y,
ratio_w = ddouble_tap_zone_prev_chapter.w,
ratio_h = ddouble_tap_zone_prev_chapter.h,
}
local ddouble_tap_zone_next_chapter = G_defaults:readSetting("DDOUBLE_TAP_ZONE_NEXT_CHAPTER")
local zone_right = {
ratio_x = ddouble_tap_zone_next_chapter.x,
ratio_y = ddouble_tap_zone_next_chapter.y,
ratio_w = ddouble_tap_zone_next_chapter.w,
ratio_h = ddouble_tap_zone_next_chapter.h,
}
local overrides_tap_corner
local overrides_double_tap_corner
local overrides_hold_corner
local overrides_vertical_edge, overrides_horizontal_edge
local overrides_pan, overrides_pan_release
local overrides_swipe_pan, overrides_swipe_pan_release
if self.is_docless then
overrides_tap_corner = {
"filemanager_ext_tap",
"filemanager_tap",
}
overrides_horizontal_edge = {
"filemanager_ext_swipe",
"filemanager_swipe",
}
else
overrides_tap_corner = {
"readerfooter_tap",
"readerconfigmenu_ext_tap",
"readerconfigmenu_tap",
"readermenu_ext_tap",
"readermenu_tap",
"tap_forward",
"tap_backward",
}
overrides_double_tap_corner = {
"double_tap_left_side",
"double_tap_right_side",
}
overrides_hold_corner = {
-- As hold corners are "ignored" by default, and we have
-- a "Ignore hold on corners" menu item and gesture, let
-- them have priority over word lookup and text selection.
"readerhighlight_hold",
"readerfooter_hold",
}
overrides_vertical_edge = {
"readerconfigmenu_ext_swipe",
"readerconfigmenu_swipe",
"readermenu_ext_swipe",
"readermenu_swipe",
"paging_swipe",
"rolling_swipe",
}
overrides_horizontal_edge = {
"swipe_link",
"readerconfigmenu_ext_swipe",
"readerconfigmenu_swipe",
"readermenu_ext_swipe",
"readermenu_swipe",
"paging_swipe",
"rolling_swipe",
}
overrides_pan = {
"paging_swipe",
"rolling_swipe",
}
overrides_pan_release = {
"paging_pan_release",
}
end
if ges == "multiswipe" then
ges_type = "multiswipe"
zone = zone_fullscreen
direction = {
northeast = true, northwest = true,
southeast = true, southwest = true,
east = true, west = true,
north = true, south = true,
}
elseif ges == "tap_top_left_corner" then
ges_type = "tap"
zone = zone_top_left_corner
overrides = overrides_tap_corner
elseif ges == "tap_top_right_corner" then
ges_type = "tap"
zone = zone_top_right_corner
overrides = overrides_tap_corner
elseif ges == "tap_right_bottom_corner" then
ges_type = "tap"
zone = zone_bottom_right_corner
overrides = overrides_tap_corner
elseif ges == "tap_left_bottom_corner" then
ges_type = "tap"
zone = zone_bottom_left_corner
overrides = overrides_tap_corner
elseif ges == "double_tap_left_side" then
ges_type = "double_tap"
zone = zone_left
elseif ges == "double_tap_right_side" then
ges_type = "double_tap"
zone = zone_right
elseif ges == "double_tap_top_left_corner" then
ges_type = "double_tap"
zone = zone_top_left_corner
overrides = overrides_double_tap_corner
elseif ges == "double_tap_top_right_corner" then
ges_type = "double_tap"
zone = zone_top_right_corner
overrides = overrides_double_tap_corner
elseif ges == "double_tap_bottom_right_corner" then
ges_type = "double_tap"
zone = zone_bottom_right_corner
overrides = overrides_double_tap_corner
elseif ges == "double_tap_bottom_left_corner" then
ges_type = "double_tap"
zone = zone_bottom_left_corner
overrides = overrides_double_tap_corner
elseif ges == "hold_top_left_corner" then
ges_type = "hold"
zone = zone_top_left_corner
overrides = overrides_hold_corner
elseif ges == "hold_top_right_corner" then
ges_type = "hold"
zone = zone_top_right_corner
overrides = overrides_hold_corner
elseif ges == "hold_bottom_right_corner" then
ges_type = "hold"
zone = zone_bottom_right_corner
overrides = overrides_hold_corner
elseif ges == "hold_bottom_left_corner" then
ges_type = "hold"
zone = zone_bottom_left_corner
overrides = overrides_hold_corner
elseif ges == "one_finger_swipe_left_edge_down" then
ges_type = "swipe"
zone = zone_left_edge
direction = {south = true}
overrides = overrides_vertical_edge
overrides_swipe_pan = overrides_pan
overrides_swipe_pan_release = overrides_pan_release
elseif ges == "one_finger_swipe_left_edge_up" then
ges_type = "swipe"
zone = zone_left_edge
direction = {north = true}
overrides = overrides_vertical_edge
overrides_swipe_pan = overrides_pan
overrides_swipe_pan_release = overrides_pan_release
elseif ges == "one_finger_swipe_right_edge_down" then
ges_type = "swipe"
zone = zone_right_edge
direction = {south = true}
overrides = overrides_vertical_edge
overrides_swipe_pan = overrides_pan
overrides_swipe_pan_release = overrides_pan_release
elseif ges == "one_finger_swipe_right_edge_up" then
ges_type = "swipe"
zone = zone_right_edge
direction = {north = true}
overrides = overrides_vertical_edge
overrides_swipe_pan = overrides_pan
overrides_swipe_pan_release = overrides_pan_release
elseif ges == "one_finger_swipe_top_edge_right" then
ges_type = "swipe"
zone = zone_top_edge
direction = {east = true}
overrides = overrides_horizontal_edge
overrides_swipe_pan = overrides_pan
overrides_swipe_pan_release = overrides_pan_release
elseif ges == "one_finger_swipe_top_edge_left" then
ges_type = "swipe"
zone = zone_top_edge
direction = {west = true}
overrides = overrides_horizontal_edge
overrides_swipe_pan = overrides_pan
overrides_swipe_pan_release = overrides_pan_release
elseif ges == "one_finger_swipe_bottom_edge_right" then
ges_type = "swipe"
zone = zone_bottom_edge
direction = {east = true}
overrides = overrides_horizontal_edge
overrides_swipe_pan = overrides_pan
overrides_swipe_pan_release = overrides_pan_release
elseif ges == "one_finger_swipe_bottom_edge_left" then
ges_type = "swipe"
zone = zone_bottom_edge
direction = {west = true}
overrides = overrides_horizontal_edge
overrides_swipe_pan = overrides_pan
overrides_swipe_pan_release = overrides_pan_release
elseif ges == "two_finger_tap_top_left_corner" then
ges_type = "two_finger_tap"
zone = zone_top_left_corner
elseif ges == "two_finger_tap_top_right_corner" then
ges_type = "two_finger_tap"
zone = zone_top_right_corner
elseif ges == "two_finger_tap_bottom_right_corner" then
ges_type = "two_finger_tap"
zone = zone_bottom_right_corner
elseif ges == "two_finger_tap_bottom_left_corner" then
ges_type = "two_finger_tap"
zone = zone_bottom_left_corner
elseif ges == "two_finger_swipe_west" then
ges_type = "two_finger_swipe"
zone = zone_fullscreen
direction = {west = true}
elseif ges == "two_finger_swipe_east" then
ges_type = "two_finger_swipe"
zone = zone_fullscreen
direction = {east = true}
elseif ges == "two_finger_swipe_south" then
ges_type = "two_finger_swipe"
zone = zone_fullscreen
direction = {south = true}
elseif ges == "two_finger_swipe_north" then
ges_type = "two_finger_swipe"
zone = zone_fullscreen
direction = {north = true}
elseif ges == "two_finger_swipe_northwest" then
ges_type = "two_finger_swipe"
zone = zone_fullscreen
direction = {northwest = true}
elseif ges == "two_finger_swipe_northeast" then
ges_type = "two_finger_swipe"
zone = zone_fullscreen
direction = {northeast = true}
elseif ges == "two_finger_swipe_southwest" then
ges_type = "two_finger_swipe"
zone = zone_fullscreen
direction = {southwest = true}
elseif ges == "two_finger_swipe_southeast" then
ges_type = "two_finger_swipe"
zone = zone_fullscreen
direction = {southeast = true}
elseif ges == "short_diagonal_swipe" then
ges_type = "swipe"
zone = {
ratio_x = 0.0, ratio_y = 0,
ratio_w = 1, ratio_h = 1,
}
direction = {northeast = true, northwest = true, southeast = true, southwest = true}
distance = "short"
if self.is_docless then
overrides = {
"filemanager_ext_tap",
"filemanager_tap",
"filemanager_ext_swipe",
"filemanager_swipe",
}
else
overrides = {
"paging_swipe",
"rolling_swipe",
}
end
elseif ges == "spread_gesture" then
ges_type = "spread"
zone = zone_fullscreen
elseif ges == "pinch_gesture" then
ges_type = "pinch"
zone = zone_fullscreen
elseif ges == "rotate_cw" then
ges_type = "rotate"
zone = zone_fullscreen
direction = {cw = true}
elseif ges == "rotate_ccw" then
ges_type = "rotate"
zone = zone_fullscreen
direction = {ccw = true}
else return
end
2020-07-17 03:03:23 +00:00
self:registerGesture(ges, ges_type, zone, overrides, direction, distance)
-- make dummy zone to disable panning and panning_release when gesture is swipe
if ges_type == "swipe" and ges ~= "short_diagonal_swipe" then
local pan_gesture = ges.."_pan"
local pan_release_gesture = ges.."_pan_release"
2020-07-17 03:03:23 +00:00
self:registerGesture(pan_gesture, "pan", zone, overrides_swipe_pan, direction, distance)
self:registerGesture(pan_release_gesture, "pan_release", zone, overrides_swipe_pan_release, direction, distance)
end
end
2020-07-17 03:03:23 +00:00
function Gestures:registerGesture(ges, ges_type, zone, overrides, direction, distance)
self.ui:registerTouchZones({
{
id = ges,
ges = ges_type,
screen_zone = zone,
handler = function(gest)
if distance == "short" and gest.distance > Screen:scaleBySize(300) then return end
if direction and not direction[gest.direction] then return end
if ges == "multiswipe" then
return self:multiswipeAction(gest.multiswipe_directions, gest)
end
2020-07-17 03:03:23 +00:00
return self:gestureAction(ges, gest)
end,
overrides = overrides,
},
})
end
function Gestures:gestureAction(action, ges)
2020-07-17 03:03:23 +00:00
if G_reader_settings:isTrue("gestures_migrated") then
UIManager:show(InfoMessage:new{
text = _("Gestures have been upgraded. You may now set more than one action per gesture."),
show_icon = false,
})
G_reader_settings:delSetting("gestures_migrated")
return true
end
local action_list = self.gestures[action]
if action_list == nil
or (ges.ges == "hold" and self.ignore_hold_corners) then
return
2020-07-17 03:03:23 +00:00
else
self.ui:handleEvent(Event:new("HandledAsSwipe"))
Dispatcher:execute(action_list, ges)
end
return true
end
function Gestures:multiswipeAction(multiswipe_directions, ges)
if self.multiswipes_enabled == nil then
UIManager:show(ConfirmBox:new{
text = _("You have just performed your first multiswipe gesture.") .."\n\n".. multiswipes_info_text,
ok_text = _("Turn on"),
ok_callback = function()
G_reader_settings:makeTrue("multiswipes_enabled")
self.multiswipes_enabled = true
end,
cancel_text = _("Turn off"),
cancel_callback = function()
G_reader_settings:makeFalse("multiswipes_enabled")
self.multiswipes_enabled = false
end,
})
return
else
if not self.multiswipes_enabled then return end
local multiswipe_gesture_name = "multiswipe_"..self:safeMultiswipeName(multiswipe_directions)
2020-07-17 03:03:23 +00:00
return self:gestureAction(multiswipe_gesture_name, ges)
end
end
function Gestures:onIgnoreHoldCorners(ignore_hold_corners)
if ignore_hold_corners == nil then
G_reader_settings:flipNilOrFalse("ignore_hold_corners")
else
G_reader_settings:saveSetting("ignore_hold_corners", ignore_hold_corners)
end
self.ignore_hold_corners = G_reader_settings:isTrue("ignore_hold_corners")
return true
end
2020-07-17 03:03:23 +00:00
function Gestures:onFlushSettings()
if self.settings_data and self.updated then
2020-07-17 03:03:23 +00:00
self.settings_data:flush()
self.updated = false
2020-07-17 03:03:23 +00:00
end
end
return Gestures