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
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

View File

@ -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:

View File

@ -1254,3 +1254,7 @@ def num_users() -> int:
def redirect_mouse_handling(yes: bool) -> None:
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);
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__

View File

@ -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);

View File

@ -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, ...]: