local BD = require("ui/bidi") local ConfirmBox = require("ui/widget/confirmbox") local DataStorage = require("datastorage") local Device = require("device") local Dispatcher = require("dispatcher") local Event = require("ui/event") 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") local LuaSettings = require("luasettings") local Screen = require("device").screen local SpinWidget = require("ui/widget/spinwidget") local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") local lfs = require("libs/libkoreader-lfs") local logger = require("logger") local util = require("util") local T = FFIUtil.template local time = require("ui/time") local _ = require("gettext") local C_ = _.pgettext if not Device:isTouchDevice() then return { disabled = true, } end local Gestures = WidgetContainer:extend{ name = "gestures", settings_data = nil, gestures = nil, defaults = nil, custom_multiswipes = nil, updated = false, } 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 clockwise ⤸ 90°"), rotate_ccw = _("Rotate counterclockwise ⤹ 90°"), 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 "toggle_touch_input" or "touch_input_on" actions, or is set "Always active" manually, -- 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 local gest = self.gestures[ges] return gest and (gest.toggle_touch_input or gest.touch_input_on or (gest.settings and gest.settings.always_active)) end function Gestures:init() 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" 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", } local is_rtl = BD.mirroredUILayout() if is_rtl then for k, v in pairs(mirrored_if_rtl) do 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. if util.tableEquals(self.gestures[k], self.defaults[v]) then self.gestures[k] = self.defaults[k] reset = true end 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 function Gestures:gestureTitleFunc(ges) local title = gestures_list[ges] or self:friendlyMultiswipeName(ges) return T(_("%1 (%2)"), title, Dispatcher:menuTextFunc(self.gestures[ges])) 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])), 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 end, }) end 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 end, }) Dispatcher:addSubMenu(self, sub_items, self.gestures, ges) sub_items.max_per_page = nil -- restore default, settings in page 2 table.insert(sub_items, { text = _("Anchor QuickMenu to gesture position"), checked_func = function() return self.gestures[ges] ~= nil and self.gestures[ges].settings ~= nil and self.gestures[ges].settings.anchor_quickmenu end, callback = function() if self.gestures[ges] then if self.gestures[ges].settings then if self.gestures[ges].settings.anchor_quickmenu then self.gestures[ges].settings.anchor_quickmenu = nil if next(self.gestures[ges].settings) == nil then self.gestures[ges].settings = nil end else self.gestures[ges].settings.anchor_quickmenu = true end else self.gestures[ges].settings = {["anchor_quickmenu"] = true} end self.updated = true end end, }) table.insert(sub_items, { text = _("Always active"), checked_func = function() return self.gestures[ges] ~= nil and self.gestures[ges].settings ~= nil and self.gestures[ges].settings.always_active end, callback = function() if self.gestures[ges] then if self.gestures[ges].settings then if self.gestures[ges].settings.always_active then self.gestures[ges].settings.always_active = nil if next(self.gestures[ges].settings) == nil then self.gestures[ges].settings = nil end else self.gestures[ges].settings.always_active = true end else self.gestures[ges].settings = {["always_active"] = true} end self.updated = true end end, }) return sub_items end function Gestures:genSubItem(ges, separator, hold_callback) local reader_only = {tap_top_left_corner=true, tap_top_right_corner=true, hold_top_left_corner=true,} local enabled_func if reader_only[ges] then enabled_func = function() return self.ges_mode == "gesture_reader" end end return { text_func = function() return self:gestureTitleFunc(ges) end, enabled_func = enabled_func, sub_item_table_func = function() return self:genMenu(ges) end, separator = separator, hold_callback = hold_callback, ignored_by_menu_search = true, -- This item is not strictly duplicated, but its subitems are. -- Ignoring it speeds up search. } end function Gestures:genSubItemTable(gestures) local sub_item_table = {} for _, item in ipairs(gestures) do table.insert(sub_item_table, self:genSubItem(item)) end 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",}, } 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 = { { { text = _("Cancel"), id = "close", 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 --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, }, } }, } multiswipe_recorder.ges_events.Multiswipe = { GestureRange:new{ ges = "multiswipe", range = Geom:new{ x = 0, y = 0, w = Screen:getWidth(), h = Screen:getHeight(), }, } } 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 UIManager:show(multiswipe_recorder) end 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 --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() end, }) 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, }, } } end 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") 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"), 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"), 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"}), }, { 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 table.insert(menu_items.gesture_manager.sub_item_table, { text = _("Two-finger tap corner"), 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"), 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"), sub_item_table = self:genSubItemTable({"spread_gesture", "pinch_gesture"}), }) table.insert(menu_items.gesture_manager.sub_item_table, { text = _("Two-finger rotation"), sub_item_table = self:genSubItemTable({"rotate_cw", "rotate_ccw"}), }) end self:addIntervals(menu_items) end function Gestures:initGesture() for ges, _ in pairs(gestures_list) do self:setupGesture(ges) end end 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 dswipe_zone_left_edge = G_defaults:readSetting("DSWIPE_ZONE_LEFT_EDGE") local zone_left_edge = { ratio_x = dswipe_zone_left_edge.x, ratio_y = dswipe_zone_left_edge.y, ratio_w = dswipe_zone_left_edge.w, ratio_h = dswipe_zone_left_edge.h, } local dswipe_zone_right_edge = G_defaults:readSetting("DSWIPE_ZONE_RIGHT_EDGE") local zone_right_edge = { ratio_x = dswipe_zone_right_edge.x, ratio_y = dswipe_zone_right_edge.y, ratio_w = dswipe_zone_right_edge.w, ratio_h = dswipe_zone_right_edge.h, } local dswipe_zone_top_edge = G_defaults:readSetting("DSWIPE_ZONE_TOP_EDGE") local zone_top_edge = { ratio_x = dswipe_zone_top_edge.x, ratio_y = dswipe_zone_top_edge.y, ratio_w = dswipe_zone_top_edge.w, ratio_h = dswipe_zone_top_edge.h, } local dswipe_zone_bottom_edge = G_defaults:readSetting("DSWIPE_ZONE_BOTTOM_EDGE") local zone_bottom_edge = { ratio_x = dswipe_zone_bottom_edge.x, ratio_y = dswipe_zone_bottom_edge.y, ratio_w = dswipe_zone_bottom_edge.w, ratio_h = dswipe_zone_bottom_edge.h, } 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 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" 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 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 return self:gestureAction(ges, gest) end, overrides = overrides, }, }) end function Gestures:gestureAction(action, ges) 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 else self.ui:handleEvent(Event:new("HandledAsSwipe")) local exec_props = { gesture = ges } if action_list.settings and action_list.settings.anchor_quickmenu then exec_props.qm_anchor = ges.end_pos or ges.pos end Dispatcher:execute(action_list, exec_props) 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) 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 function Gestures:onFlushSettings() if self.settings_data and self.updated then self.settings_data:flush() self.updated = false end end function Gestures:updateProfiles(action_old_name, action_new_name) for _, section in ipairs({ "gesture_fm", "gesture_reader" }) do local gestures = self.settings_data.data[section] for gesture_name, gesture in pairs(gestures) do if gesture[action_old_name] then if gesture.settings and gesture.settings.order then for i, action in ipairs(gesture.settings.order) do if action == action_old_name then if action_new_name then gesture.settings.order[i] = action_new_name else table.remove(gesture.settings.order, i) if #gesture.settings.order == 0 then gesture.settings.order = nil if next(gesture.settings) == nil then gesture.settings = nil end end end break end end end gesture[action_old_name] = nil if action_new_name then gesture[action_new_name] = true else if next(gesture) == nil then self.settings_data.data[section][gesture_name] = nil end end self.updated = true end end end end return Gestures