diff --git a/kitty/glfw.c b/kitty/glfw.c index 05426d581..dadc48178 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -217,6 +217,30 @@ show_mouse_cursor(GLFWwindow *w) { glfwSetInputMode(w, GLFW_CURSOR, GLFW_CURSOR_NORMAL); } +void +cursor_active_callback(GLFWwindow *w, monotonic_t now) { + if (OPT(mouse_show_wait) == 0) { + show_mouse_cursor(w); + } else if (OPT(mouse_show_wait) > 0) { + if (global_state.callback_os_window->mouse_activate_deadline == -1) { + global_state.callback_os_window->mouse_activate_deadline = OPT(mouse_show_wait) + now; + global_state.callback_os_window->mouse_show_threshold = (int) (OPT(mouse_show_wait) * OPT(mouse_show_threshold)); + } else if (now < global_state.callback_os_window->mouse_activate_deadline) { + if (global_state.callback_os_window->mouse_show_threshold > 0) { + global_state.callback_os_window->mouse_show_threshold--; + } + } else { + if ( + now < global_state.callback_os_window->mouse_activate_deadline + s_double_to_monotonic_t(0.5) && + global_state.callback_os_window->mouse_show_threshold == 0 + ) { + show_mouse_cursor(w); + } + global_state.callback_os_window->mouse_activate_deadline = -1; + } + } +} + void blank_os_window(OSWindow *osw) { color_type color = OPT(background); @@ -451,8 +475,8 @@ cursor_enter_callback(GLFWwindow *w, int entered) { double x, y; glfwGetCursorPos(w, &x, &y); debug_input("Mouse cursor entered window: %llu at %fx%f\n", global_state.callback_os_window->id, x, y); - show_mouse_cursor(w); monotonic_t now = monotonic(); + cursor_active_callback(w, now); global_state.callback_os_window->last_mouse_activity_at = now; global_state.callback_os_window->mouse_x = x * global_state.callback_os_window->viewport_x_ratio; global_state.callback_os_window->mouse_y = y * global_state.callback_os_window->viewport_y_ratio; @@ -465,9 +489,9 @@ cursor_enter_callback(GLFWwindow *w, int entered) { static void mouse_button_callback(GLFWwindow *w, int button, int action, int mods) { if (!set_callback_window(w)) return; - show_mouse_cursor(w); - mods_at_last_key_or_button_event = mods; monotonic_t now = monotonic(); + cursor_active_callback(w, now); + mods_at_last_key_or_button_event = mods; OSWindow *window = global_state.callback_os_window; window->last_mouse_activity_at = now; if (button >= 0 && (unsigned int)button < arraysz(global_state.callback_os_window->mouse_button_pressed)) { @@ -489,8 +513,8 @@ mouse_button_callback(GLFWwindow *w, int button, int action, int mods) { static void cursor_pos_callback(GLFWwindow *w, double x, double y) { if (!set_callback_window(w)) return; - show_mouse_cursor(w); monotonic_t now = monotonic(); + cursor_active_callback(w, now); global_state.callback_os_window->last_mouse_activity_at = now; global_state.callback_os_window->cursor_blink_zero_time = now; global_state.callback_os_window->mouse_x = x * global_state.callback_os_window->viewport_x_ratio; @@ -504,8 +528,10 @@ cursor_pos_callback(GLFWwindow *w, double x, double y) { static void scroll_callback(GLFWwindow *w, double xoffset, double yoffset, int flags, int mods) { if (!set_callback_window(w)) return; - show_mouse_cursor(w); monotonic_t now = monotonic(); + if (OPT(mouse_scroll_show)) { + cursor_active_callback(w, now); + } global_state.callback_os_window->last_mouse_activity_at = now; if (is_window_ready_for_callbacks()) scroll_event(xoffset, yoffset, flags, mods); request_tick_callback(); @@ -519,13 +545,13 @@ window_focus_callback(GLFWwindow *w, int focused) { if (!set_callback_window(w)) return; debug_input("\x1b[35mon_focus_change\x1b[m: window id: 0x%llu focused: %d\n", global_state.callback_os_window->id, focused); global_state.callback_os_window->is_focused = focused ? true : false; + monotonic_t now = monotonic(); if (focused) { - show_mouse_cursor(w); + cursor_active_callback(w, now); focus_in_event(); global_state.callback_os_window->last_focused_counter = ++focus_counter; global_state.check_for_active_animated_images = true; } - monotonic_t now = monotonic(); global_state.callback_os_window->last_mouse_activity_at = now; global_state.callback_os_window->cursor_blink_zero_time = now; if (is_window_ready_for_callbacks()) { @@ -1371,6 +1397,8 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { w->is_focused = true; w->cursor_blink_zero_time = now; w->last_mouse_activity_at = now; + w->mouse_activate_deadline = -1; + w->mouse_show_threshold = 0; w->is_semi_transparent = is_semi_transparent; if (want_semi_transparent && !w->is_semi_transparent) { static bool warned = false; diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 660984dcd..39210d89d 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -518,6 +518,36 @@ used. Set to zero to disable mouse cursor hiding. Set to a negative value to hide the mouse cursor immediately when typing text. Disabled by default on macOS as getting it to work robustly with the ever-changing sea of bugs that is Cocoa is too much effort. +''' + ) +opt('mouse_show_wait', '0.0', + option_type='float', ctype='time', + long_text=''' +Waits for the specified number of seconds after mouse events before unhiding the +mouse cursor. Set to zero to unhide mouse cursor immediately on mouse activity. +This is useful to prevent the mouse cursor from unhiding on accidental swipes on +the trackpad. +''' + ) +opt('mouse_show_threshold', '40', + option_type='positive_int', + long_text=''' +Sets the threshold of mouse activity required to unhide the mouse cursor, when +the mouse_show_wait option is non-zero. When mouse_show_wait is zero, this has +no effect. + +For example, if mouse_show_threshold is 40 and mouse_show_wait is 2.5, when +kitty detects a mouse event, it records the number of mouse events in the next +2.5 seconds, and checks if that exceeds 40 * 2.5 = 100. If it does, then the +mouse cursor is unhidden, otherwise nothing happens. +''' + ) +opt('mouse_scroll_show', 'yes', + option_type='to_bool', ctype='bool', + long_text=''' +Controls what mouse events may unhide the mouse cursor. If enabled, both scroll +and movement events may unhide the cursor. If disabled, only mouse movements can +unhide the cursor. ''' ) diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 7b046bc36..19d2d9d15 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1139,6 +1139,15 @@ class Parser: def mouse_hide_wait(self, val: str, ans: dict[str, typing.Any]) -> None: ans['mouse_hide_wait'] = float(val) + def mouse_scroll_show(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['mouse_scroll_show'] = to_bool(val) + + def mouse_show_threshold(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['mouse_show_threshold'] = positive_int(val) + + def mouse_show_wait(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['mouse_show_wait'] = float(val) + def narrow_symbols(self, val: str, ans: dict[str, typing.Any]) -> None: for k, v in narrow_symbols(val): ans["narrow_symbols"][k] = v diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index b1ac92ef1..3031e0299 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -330,6 +330,32 @@ convert_from_opts_mouse_hide_wait(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_mouse_show_wait(PyObject *val, Options *opts) { + opts->mouse_show_wait = parse_s_double_to_monotonic_t(val); +} + +static void +convert_from_opts_mouse_show_wait(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "mouse_show_wait"); + if (ret == NULL) return; + convert_from_python_mouse_show_wait(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_mouse_scroll_show(PyObject *val, Options *opts) { + opts->mouse_scroll_show = PyObject_IsTrue(val); +} + +static void +convert_from_opts_mouse_scroll_show(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "mouse_scroll_show"); + if (ret == NULL) return; + convert_from_python_mouse_scroll_show(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_url_color(PyObject *val, Options *opts) { opts->url_color = color_as_int(val); @@ -1227,6 +1253,10 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_mouse_hide_wait(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_mouse_show_wait(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_mouse_scroll_show(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_url_color(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_url_style(py_opts, opts); diff --git a/kitty/options/types.py b/kitty/options/types.py index 0949ccc83..bf1f55a78 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -396,6 +396,9 @@ option_names = ( 'modify_font', 'mouse_hide_wait', 'mouse_map', + 'mouse_scroll_show', + 'mouse_show_threshold', + 'mouse_show_wait', 'narrow_symbols', 'notify_on_cmd_finish', 'open_url_with', @@ -569,6 +572,9 @@ class Options: mark3_background: Color = Color(242, 116, 188) mark3_foreground: Color = Color(0, 0, 0) mouse_hide_wait: float = 0.0 if is_macos else 3.0 + mouse_scroll_show: bool = True + mouse_show_threshold: int = 40 + mouse_show_wait: float = 0.0 notify_on_cmd_finish: NotifyOnCmdFinish = NotifyOnCmdFinish(when='never', duration=5.0, action='notify', cmdline=(), clear_on=('focus', 'next')) open_url_with: list[str] = ['default'] paste_actions: frozenset[str] = frozenset({'confirm', 'quote-urls-at-prompt'}) diff --git a/kitty/state.h b/kitty/state.h index 2b11c6777..56ff1c7e3 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -39,7 +39,9 @@ struct MenuItem { }; typedef struct { - monotonic_t visual_bell_duration, cursor_blink_interval, cursor_stop_blinking_after, mouse_hide_wait, click_interval; + monotonic_t visual_bell_duration, cursor_blink_interval, cursor_stop_blinking_after, mouse_hide_wait, mouse_show_wait, click_interval; + int mouse_show_threshold; + bool mouse_scroll_show; double wheel_scroll_multiplier, touch_scroll_multiplier; int wheel_scroll_min_lines; bool enable_audio_bell; @@ -286,7 +288,8 @@ typedef struct { } tab_bar_edge_color; bool tab_bar_data_updated; bool is_focused; - monotonic_t cursor_blink_zero_time, last_mouse_activity_at; + monotonic_t cursor_blink_zero_time, last_mouse_activity_at, mouse_activate_deadline; + int mouse_show_threshold; bool has_received_cursor_pos_event; double mouse_x, mouse_y; bool mouse_button_pressed[32]; diff --git a/kitty_tests/options.py b/kitty_tests/options.py index 1eba937d8..e73b65201 100644 --- a/kitty_tests/options.py +++ b/kitty_tests/options.py @@ -57,6 +57,9 @@ def conf_parsing(self): opts = p('font_size 11.37', 'clear_all_shortcuts y', 'color23 red') self.ae(opts.font_size, 11.37) self.ae(opts.mouse_hide_wait, 0 if is_macos else 3) + self.ae(opts.mouse_show_wait, 0) + self.ae(opts.mouse_show_threshold, 40) + self.ae(opts.mouse_scroll_show, True) self.ae(opts.color23, Color(255, 0, 0)) self.assertFalse(opts.keyboard_modes[''].keymap) opts = p('clear_all_shortcuts y', 'map f1 next_window')