Allow middle clicking on a tab to close it

Fixes #4151
This commit is contained in:
Kovid Goyal 2021-10-24 22:56:30 +05:30
parent 6c7420f4e7
commit b316e97a4f
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 57 additions and 17 deletions

View File

@ -76,6 +76,8 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
- Unicode input kitten: Implement scrolling when more results are found than - Unicode input kitten: Implement scrolling when more results are found than
the available display space (:pull:`4068`) 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 - The command line option ``--watcher`` has been deprecated in favor of the
:opt:`watcher` option in :file:`kitty.conf`. It has the advantage of :opt:`watcher` option in :file:`kitty.conf`. It has the advantage of
applying to all windows, not just the initially created ones. Note that applying to all windows, not just the initially created ones. Note that

View File

@ -591,10 +591,10 @@ class Boss:
run_update_check(get_options().update_check_interval * 60 * 60) run_update_check(get_options().update_check_interval * 60 * 60)
self.update_check_started = True 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) tm = self.os_window_map.get(os_window_id)
if tm is not None: 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: def on_window_resize(self, os_window_id: int, w: int, h: int, dpi_changed: bool) -> None:
if dpi_changed: if dpi_changed:

View File

@ -1254,3 +1254,7 @@ def num_users() -> int:
def redirect_mouse_handling(yes: bool) -> None: def redirect_mouse_handling(yes: bool) -> None:
pass pass
def get_click_interval() -> float:
pass

View File

@ -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); unsigned long long notification_id = glfwDBusUserNotify(app_name, icon, summary, body, action_name, timeout, dbus_notification_created_callback, NULL);
return PyLong_FromUnsignedLongLong(notification_id); 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 #endif
id_type id_type
@ -1472,6 +1477,7 @@ static PyMethodDef module_methods[] = {
METHODB(glfw_window_hint, METH_VARARGS), METHODB(glfw_window_hint, METH_VARARGS),
METHODB(get_primary_selection, METH_NOARGS), METHODB(get_primary_selection, METH_NOARGS),
METHODB(x11_display, METH_NOARGS), METHODB(x11_display, METH_NOARGS),
METHODB(get_click_interval, METH_NOARGS),
METHODB(x11_window_id, METH_O), METHODB(x11_window_id, METH_O),
METHODB(set_primary_selection, METH_VARARGS), METHODB(set_primary_selection, METH_VARARGS),
#ifndef __APPLE__ #ifndef __APPLE__

View File

@ -515,13 +515,10 @@ HANDLER(handle_event) {
} }
static void static void
handle_tab_bar_mouse(int button, int UNUSED modifiers) { handle_tab_bar_mouse(int button, int modifiers, int action) {
static monotonic_t last_click_at = 0; if (button > -1) { // dont report motion events, as they are expensive and useless
if (button != GLFW_MOUSE_BUTTON_LEFT || !global_state.callback_os_window->mouse_button_pressed[button]) return; call_boss(handle_click_on_tab, "Kdiii", global_state.callback_os_window->id, global_state.callback_os_window->mouse_x, button, modifiers, action);
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);
} }
static bool static bool
@ -737,7 +734,7 @@ mouse_event(const int button, int modifiers, int action) {
w = window_for_event(&window_idx, &in_tab_bar); w = window_for_event(&window_idx, &in_tab_bar);
if (in_tab_bar) { if (in_tab_bar) {
mouse_cursor_shape = HAND; mouse_cursor_shape = HAND;
handle_tab_bar_mouse(button, modifiers); handle_tab_bar_mouse(button, modifiers, action);
debug("handled by tab bar\n"); debug("handled by tab bar\n");
} else if (w) { } else if (w) {
debug("grabbed: %d\n", w->render_data.screen->modes.mouse_tracking_mode != 0); debug("grabbed: %d\n", w->render_data.screen->modes.mouse_tracking_mode != 0);

View File

@ -8,6 +8,7 @@ from collections import deque
from contextlib import suppress from contextlib import suppress
from functools import partial from functools import partial
from operator import attrgetter from operator import attrgetter
from time import monotonic
from typing import ( from typing import (
Any, Deque, Dict, Generator, Iterable, Iterator, List, NamedTuple, Any, Deque, Dict, Generator, Iterable, Iterator, List, NamedTuple,
Optional, Pattern, Sequence, Tuple, Union, cast Optional, Pattern, Sequence, Tuple, Union, cast
@ -18,9 +19,11 @@ from .child import Child
from .cli_stub import CLIOptions from .cli_stub import CLIOptions
from .constants import appname, kitty_exe from .constants import appname, kitty_exe
from .fast_data_types import ( from .fast_data_types import (
add_tab, attach_window, detach_window, get_boss, get_options, GLFW_MOUSE_BUTTON_LEFT, GLFW_MOUSE_BUTTON_MIDDLE, GLFW_PRESS, GLFW_RELEASE,
mark_tab_bar_dirty, next_window_id, remove_tab, remove_window, ring_bell, add_tab, attach_window, detach_window, get_boss, get_click_interval,
set_active_tab, set_active_window, swap_tabs, sync_os_window_title 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.base import Layout
from .layout.interface import create_layout_object_for, evict_cached_layouts 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 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): class TabDict(TypedDict):
id: int id: int
is_focused: bool 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): 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.os_window_id = os_window_id
self.wm_class = wm_class self.wm_class = wm_class
self.recent_mouse_events: Deque[TabMouseEvent] = deque()
self.wm_name = wm_name self.wm_name = wm_name
self.last_active_tab_id = None self.last_active_tab_id = None
self.args = args self.args = args
@ -965,13 +977,32 @@ class TabManager: # {{{
)) ))
return ans 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) i = self.tab_bar.tab_at(x)
now = monotonic()
if i is None: if i is None:
if is_double: if button == GLFW_MOUSE_BUTTON_LEFT and action == GLFW_PRESS and len(self.recent_mouse_events) > 1:
self.new_tab() 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: 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 @property
def tab_bar_rects(self) -> Tuple[Border, ...]: def tab_bar_rects(self) -> Tuple[Border, ...]: