From b316e97a4f10e44096e2a681c39ea598bfc36b3c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Oct 2021 22:56:30 +0530 Subject: [PATCH] Allow middle clicking on a tab to close it Fixes #4151 --- docs/changelog.rst | 2 ++ kitty/boss.py | 4 ++-- kitty/fast_data_types.pyi | 4 ++++ kitty/glfw.c | 6 ++++++ kitty/mouse.c | 13 +++++------ kitty/tabs.py | 45 +++++++++++++++++++++++++++++++++------ 6 files changed, 57 insertions(+), 17 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a0db08e6d..6f0fe3e8c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -76,6 +76,8 @@ To update |kitty|, :doc:`follow the instructions `. - Unicode input kitten: Implement scrolling when more results are found than the available display space (:pull:`4068`) +- Allow middle clicking on a tab to close it (:iss:`4151`) + - The command line option ``--watcher`` has been deprecated in favor of the :opt:`watcher` option in :file:`kitty.conf`. It has the advantage of applying to all windows, not just the initially created ones. Note that diff --git a/kitty/boss.py b/kitty/boss.py index 6e56288ee..28fb4e04d 100755 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -591,10 +591,10 @@ class Boss: run_update_check(get_options().update_check_interval * 60 * 60) self.update_check_started = True - def activate_tab_at(self, os_window_id: int, x: int, is_double: bool = False) -> int: + def handle_click_on_tab(self, os_window_id: int, x: int, button: int, modifiers: int, action: int) -> int: tm = self.os_window_map.get(os_window_id) if tm is not None: - tm.activate_tab_at(x, is_double) + tm.handle_click_on_tab(x, button, modifiers, action) def on_window_resize(self, os_window_id: int, w: int, h: int, dpi_changed: bool) -> None: if dpi_changed: diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 3b55f6714..cfe9ce8d2 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1254,3 +1254,7 @@ def num_users() -> int: def redirect_mouse_handling(yes: bool) -> None: pass + + +def get_click_interval() -> float: + pass diff --git a/kitty/glfw.c b/kitty/glfw.c index a52ae7e23..fbe121e62 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1424,6 +1424,11 @@ dbus_send_notification(PyObject *self UNUSED, PyObject *args) { unsigned long long notification_id = glfwDBusUserNotify(app_name, icon, summary, body, action_name, timeout, dbus_notification_created_callback, NULL); return PyLong_FromUnsignedLongLong(notification_id); } + +static PyObject* +get_click_interval(PyObject *self UNUSED, PyObject *args UNUSED) { + return PyFloat_FromDouble(monotonic_t_to_s_double(OPT(click_interval))); +} #endif id_type @@ -1472,6 +1477,7 @@ static PyMethodDef module_methods[] = { METHODB(glfw_window_hint, METH_VARARGS), METHODB(get_primary_selection, METH_NOARGS), METHODB(x11_display, METH_NOARGS), + METHODB(get_click_interval, METH_NOARGS), METHODB(x11_window_id, METH_O), METHODB(set_primary_selection, METH_VARARGS), #ifndef __APPLE__ diff --git a/kitty/mouse.c b/kitty/mouse.c index b8ea84e44..5fab083e0 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -515,13 +515,10 @@ HANDLER(handle_event) { } static void -handle_tab_bar_mouse(int button, int UNUSED modifiers) { - static monotonic_t last_click_at = 0; - if (button != GLFW_MOUSE_BUTTON_LEFT || !global_state.callback_os_window->mouse_button_pressed[button]) return; - monotonic_t now = monotonic(); - bool is_double = now - last_click_at <= OPT(click_interval); - last_click_at = is_double ? 0 : now; - call_boss(activate_tab_at, "KdO", global_state.callback_os_window->id, global_state.callback_os_window->mouse_x, is_double ? Py_True : Py_False); +handle_tab_bar_mouse(int button, int modifiers, int action) { + if (button > -1) { // dont report motion events, as they are expensive and useless + call_boss(handle_click_on_tab, "Kdiii", global_state.callback_os_window->id, global_state.callback_os_window->mouse_x, button, modifiers, action); + } } static bool @@ -737,7 +734,7 @@ mouse_event(const int button, int modifiers, int action) { w = window_for_event(&window_idx, &in_tab_bar); if (in_tab_bar) { mouse_cursor_shape = HAND; - handle_tab_bar_mouse(button, modifiers); + handle_tab_bar_mouse(button, modifiers, action); debug("handled by tab bar\n"); } else if (w) { debug("grabbed: %d\n", w->render_data.screen->modes.mouse_tracking_mode != 0); diff --git a/kitty/tabs.py b/kitty/tabs.py index cbef1aed0..150a58ddf 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -8,6 +8,7 @@ from collections import deque from contextlib import suppress from functools import partial from operator import attrgetter +from time import monotonic from typing import ( Any, Deque, Dict, Generator, Iterable, Iterator, List, NamedTuple, Optional, Pattern, Sequence, Tuple, Union, cast @@ -18,9 +19,11 @@ from .child import Child from .cli_stub import CLIOptions from .constants import appname, kitty_exe from .fast_data_types import ( - add_tab, attach_window, detach_window, get_boss, get_options, - mark_tab_bar_dirty, next_window_id, remove_tab, remove_window, ring_bell, - set_active_tab, set_active_window, swap_tabs, sync_os_window_title + GLFW_MOUSE_BUTTON_LEFT, GLFW_MOUSE_BUTTON_MIDDLE, GLFW_PRESS, GLFW_RELEASE, + add_tab, attach_window, detach_window, get_boss, get_click_interval, + get_options, mark_tab_bar_dirty, next_window_id, remove_tab, remove_window, + ring_bell, set_active_tab, set_active_window, swap_tabs, + sync_os_window_title ) from .layout.base import Layout from .layout.interface import create_layout_object_for, evict_cached_layouts @@ -32,6 +35,14 @@ from .window import Watchers, Window, WindowDict from .window_list import WindowList +class TabMouseEvent(NamedTuple): + button: int = -1 + modifiers: int = 0 + action: int = GLFW_PRESS + at: float = -1000. + tab_idx: Optional[int] = None + + class TabDict(TypedDict): id: int is_focused: bool @@ -657,6 +668,7 @@ class TabManager: # {{{ def __init__(self, os_window_id: int, args: CLIOptions, wm_class: str, wm_name: str, startup_session: Optional[SessionType] = None): self.os_window_id = os_window_id self.wm_class = wm_class + self.recent_mouse_events: Deque[TabMouseEvent] = deque() self.wm_name = wm_name self.last_active_tab_id = None self.args = args @@ -965,13 +977,32 @@ class TabManager: # {{{ )) return ans - def activate_tab_at(self, x: int, is_double: bool = False) -> None: + def handle_click_on_tab(self, x: int, button: int, modifiers: int, action: int) -> None: i = self.tab_bar.tab_at(x) + now = monotonic() if i is None: - if is_double: - self.new_tab() + if button == GLFW_MOUSE_BUTTON_LEFT and action == GLFW_PRESS and len(self.recent_mouse_events) > 1: + ci = get_click_interval() + prev, prev2 = self.recent_mouse_events[-1], self.recent_mouse_events[-2] + if ( + prev.button == button and prev2.button == button and + prev.action == GLFW_RELEASE and prev2.action == GLFW_PRESS and + prev.tab_idx is None and prev2.tab_idx is None and + now - prev.at <= ci and now - prev2.at <= 2 * ci + ): # double click + self.new_tab() + self.recent_mouse_events.clear() + return else: - self.set_active_tab_idx(i) + if action == GLFW_PRESS: + if button == GLFW_MOUSE_BUTTON_LEFT: + self.set_active_tab_idx(i) + elif button == GLFW_MOUSE_BUTTON_MIDDLE: + tab = self.tabs[i] + get_boss().close_tab(tab) + self.recent_mouse_events.append(TabMouseEvent(button, modifiers, action, now, i)) + if len(self.recent_mouse_events) > 5: + self.recent_mouse_events.popleft() @property def tab_bar_rects(self) -> Tuple[Border, ...]: