diff --git a/kitty/config.py b/kitty/config.py index 05debbffc..5e41efdf0 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -48,6 +48,8 @@ type_map = { 'window_border_width': float, 'wheel_scroll_multiplier': float, 'click_interval': float, + 'mouse_hide_wait': float, + 'cursor_stop_blinking_after': float, } for name in 'foreground background cursor active_border_color inactive_border_color selection_foreground selection_background'.split(): diff --git a/kitty/glfw.c b/kitty/glfw.c index e4d85da8e..f6833e7da 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -239,6 +239,14 @@ set_should_close(Window *self, PyObject *args) { Py_RETURN_NONE; } +static PyObject* +set_input_mode(Window *self, PyObject *args) { + int which, value; + if (!PyArg_ParseTuple(args, "ii", &which, &value)) return NULL; + glfwSetInputMode(self->window, which, value); + Py_RETURN_NONE; +} + static PyObject* is_key_pressed(Window *self, PyObject *args) { int c; @@ -273,6 +281,7 @@ static PyMethodDef methods[] = { MND(get_cursor_pos, METH_NOARGS), MND(should_close, METH_NOARGS), MND(set_should_close, METH_VARARGS), + MND(set_input_mode, METH_VARARGS), MND(is_key_pressed, METH_VARARGS), MND(set_click_cursor, METH_VARARGS), MND(make_context_current, METH_NOARGS), diff --git a/kitty/kitty.conf b/kitty/kitty.conf index 7ec65a118..20b7f00a2 100644 --- a/kitty/kitty.conf +++ b/kitty/kitty.conf @@ -43,6 +43,14 @@ click_interval 0.5 # that sufficient for most uses. repaint_delay 20 +# Hide mouse cursor after the specified number of seconds of the mouse not being used. Set to +# zero or a negative number to disable mouse cursor hiding. +mouse_hide_wait 3.0 + +# Stop blinking cursor after the specified number of seconds of keyboard inactivity. Set to +# zero or a negative number to never stop blinking. +cursor_stop_blinking_after 5.0 + # The width (in pts) of window borders. Will be rounded to the nearest number of pixels based on screen resolution. window_border_width 2 diff --git a/kitty/tabs.py b/kitty/tabs.py index 6858905d8..3251e4124 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -7,8 +7,9 @@ import io import select import signal import struct +import inspect from collections import deque -from functools import partial +from functools import wraps from threading import Thread, current_thread from queue import Queue, Empty @@ -16,7 +17,8 @@ from .child import Child from .constants import viewport_size, shell_path, appname, set_tab_manager, tab_manager, wakeup, cell_size, MODIFIER_KEYS, main_thread from .fast_data_types import ( glViewport, glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GLFW_PRESS, - GLFW_REPEAT, GLFW_MOUSE_BUTTON_1, glfw_post_empty_event + GLFW_REPEAT, GLFW_MOUSE_BUTTON_1, glfw_post_empty_event, + GLFW_CURSOR_NORMAL, GLFW_CURSOR, GLFW_CURSOR_HIDDEN ) from .fonts import set_font_family from .borders import Borders, BordersProgram @@ -29,6 +31,32 @@ from .utils import handle_unix_signals from .window import Window +def conditional_run(w, i): + if w is None or not w.destroyed: + next(i, None) + + +def callback(func): + ''' Wrapper for function that executes first half (up to a yield statement) + in the UI thread and the rest in the child thread. If the function yields + something, the destroyed attribute of that something is checked before + running the second half. If the function returns before the yield, the + second half is not run. ''' + + assert inspect.isgeneratorfunction(func) + + @wraps(func) + def f(self, *a): + i = func(self, *a) + try: + w = next(i) + except StopIteration: + pass + else: + self.queue_action(conditional_run, w, i) + return f + + class Tab: def __init__(self, opts, args): @@ -120,13 +148,13 @@ class TabManager(Thread): cell_size.width, cell_size.height = set_font_family(opts.font_family, opts.font_size) self.opts, self.args = opts, args self.glfw_window = glfw_window - glfw_window.framebuffer_size_callback = partial(self.queue_action, self.on_window_resize) - glfw_window.char_mods_callback = partial(self.queue_action, self.on_text_input) - glfw_window.key_callback = partial(self.queue_action, self.on_key) - glfw_window.mouse_button_callback = partial(self.queue_action, self.on_mouse_button) - glfw_window.scroll_callback = partial(self.queue_action, self.on_mouse_scroll) - glfw_window.cursor_pos_callback = partial(self.queue_action, self.on_mouse_move) - glfw_window.window_focus_callback = partial(self.queue_action, self.on_focus) + glfw_window.framebuffer_size_callback = self.on_window_resize + glfw_window.char_mods_callback = self.on_text_input + glfw_window.key_callback = self.on_key + glfw_window.mouse_button_callback = self.on_mouse_button + glfw_window.scroll_callback = self.on_mouse_scroll + glfw_window.cursor_pos_callback = self.on_mouse_move + glfw_window.window_focus_callback = self.on_focus self.tabs = deque() self.tabs.append(Tab(opts, args)) self.sprites = Sprites() @@ -137,6 +165,7 @@ class TabManager(Thread): self.sprites.do_layout(cell_size.width, cell_size.height) self.queue_action(self.active_tab.new_window, False) self.glfw_window.set_click_cursor(False) + self.show_mouse_cursor() def signal_received(self): try: @@ -230,10 +259,12 @@ class TabManager(Thread): if w.screen.is_dirty(): self.timers.add_if_missing(self.screen_update_delay, w.update_screen) + @callback def on_window_resize(self, window, w, h): # debounce resize events - self.timers.add(0.02, self.apply_pending_resize, w, h) self.pending_resize = True + yield + self.timers.add(0.02, self.apply_pending_resize, w, h) def apply_pending_resize(self, w, h): viewport_size.width, viewport_size.height = w, h @@ -256,25 +287,35 @@ class TabManager(Thread): if t is not None: return t.active_window + @callback def on_text_input(self, window, codepoint, mods): data = interpret_text_event(codepoint, mods) if data: w = self.active_window if w is not None: + yield w w.write_to_child(data) + @callback def on_key(self, window, key, scancode, action, mods): if action == GLFW_PRESS or action == GLFW_REPEAT: func = get_shortcut(self.opts.keymap, mods, key) tab = self.active_tab - window = self.active_window if func is not None: - func = getattr(self, func, getattr(tab, func, getattr(window, func, None))) - if func is not None: - passthrough = func() + f = getattr(self, func, getattr(tab, func, None)) + if f is not None: + passthrough = f() if not passthrough: return - if window: + window = self.active_window + if window is not None: + yield window + if func is not None: + f = getattr(window, func, None) + if f is not None: + passthrough = f() + if not passthrough: + return if window.screen.auto_repeat_enabled() or action == GLFW_PRESS: if window.char_grid.scrolled_by and key not in MODIFIER_KEYS: window.scroll_end() @@ -282,34 +323,64 @@ class TabManager(Thread): if data: window.write_to_child(data) + @callback def on_focus(self, window, focused): w = self.active_window if w is not None: + yield w w.focus_changed(focused) def window_for_pos(self, x, y): - for w in self.active_tab: - if w.is_visible_in_layout and w.contains(x, y): - return w + tab = self.active_tab + if tab is not None: + for w in tab: + if w.is_visible_in_layout and w.contains(x, y): + return w + @callback def on_mouse_button(self, window, button, action, mods): + self.show_mouse_cursor() w = self.window_for_pos(*window.get_cursor_pos()) - if w is not None: - if button == GLFW_MOUSE_BUTTON_1 and w is not self.active_window: - pass # TODO: Switch focus to this window - w.on_mouse_button(window, button, action, mods) + if w is None: + return + focus_moved = False + old_focus = self.active_window + if button == GLFW_MOUSE_BUTTON_1 and w is not old_focus: + # TODO: Switch focus to this window + focus_moved = True + yield + if focus_moved: + if old_focus is not None and not old_focus.destroyed: + old_focus.focus_changed(False) + w.focus_changed(True) + w.on_mouse_button(window, button, action, mods) + @callback def on_mouse_move(self, window, xpos, ypos): + self.show_mouse_cursor() w = self.window_for_pos(*window.get_cursor_pos()) if w is not None: + yield w w.on_mouse_move(window, xpos, ypos) + @callback def on_mouse_scroll(self, window, x, y): + self.show_mouse_cursor() w = self.window_for_pos(*window.get_cursor_pos()) if w is not None: + yield w w.on_mouse_scroll(window, x, y) # GUI thread API {{{ + + def show_mouse_cursor(self): + self.glfw_window.set_input_mode(GLFW_CURSOR, GLFW_CURSOR_NORMAL) + if self.opts.mouse_hide_wait > 0: + self.ui_timers.add(self.opts.mouse_hide_wait, self.hide_mouse_cursor) + + def hide_mouse_cursor(self): + self.glfw_window.set_input_mode(GLFW_CURSOR, GLFW_CURSOR_HIDDEN) + def render(self): if self.pending_resize: return diff --git a/kitty/timers.py b/kitty/timers.py index 88c7e0b44..8f3e7b632 100644 --- a/kitty/timers.py +++ b/kitty/timers.py @@ -16,21 +16,23 @@ class Timers: def __init__(self): self.timers = [] - def add(self, delay, callback, *args): - self.remove(callback) + def _add(self, delay, callback, args): self.timers.append(Event(monotonic() + delay, callback, args)) self.timers.sort(key=get_at) + def add(self, delay, callback, *args): + self.remove(callback) + self._add(delay, callback, args) + def add_if_missing(self, delay, callback, *args): for ev in self.timers: - if ev.callback is callback: - break - else: - self.add(delay, callback, *args) + if ev.callback == callback: + return + self._add(delay, callback, args) def remove(self, callback): for i, ev in enumerate(self.timers): - if ev.callback is callback: + if ev.callback == callback: break else: return diff --git a/kitty/window.py b/kitty/window.py index aa51a4f59..5ba3cdbe1 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -24,6 +24,7 @@ class Window: def __init__(self, tab, child, opts, args): self.tabref = weakref.ref(tab) + self.destroyed = False self.click_queue = deque(maxlen=3) self.geometry = WindowGeometry(0, 0, 0, 0, 0, 0) self.needs_layout = True @@ -59,6 +60,7 @@ class Window: tab_manager().close_window(self) def destroy(self): + self.destroyed = True self.child.hangup() self.child.get_child_status() # Ensure child does not become zombie # At this point this window can still render to screen using its