diff --git a/docs/index.rst b/docs/index.rst index 5c4c1c34c..2522295e4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -259,19 +259,18 @@ Mouse features ------------------- * You can hold down :kbd:`ctrl+shift` and click on a URL to open it in a - browser (see also :opt:`open_url_modifiers`). + browser. * You can double click to select a word and then drag to select more words. * You can triple click to select a line and then drag to select more lines. * You can right click to extend a previous selection. * You can hold down :kbd:`ctrl+alt` and drag with the mouse to select in - columns (see also :opt:`rectangle_select_modifiers`). + columns. * Selecting text automatically copies it to the primary clipboard (on platforms with a primary clipboard). * You can middle click to paste from the primary clipboard (on platforms with a primary clipboard). * You can select text with kitty even when a terminal program has grabbed - the mouse by holding down the :kbd:`shift` key (see also - :opt:`terminal_select_modifiers`). + the mouse by holding down the :kbd:`shift` key. Font control diff --git a/kitty/config.py b/kitty/config.py index c26e91d8e..51667cd11 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -326,6 +326,21 @@ def scroll_to_mark(func: str, rest: str) -> FuncArgsType: return func, [parts[0] != 'next', max(0, min(int(parts[1]), 3))] +@func_with_args('mouse_selection') +def mouse_selection(func: str, rest: str) -> FuncArgsType: + cmap = getattr(mouse_selection, 'code_map', None) + if cmap is None: + cmap = { + 'normal': defines.MOUSE_SELECTION_NORMAL, + 'extend': defines.MOUSE_SELECTION_EXTEND, + 'rectangle': defines.MOUSE_SELECTION_RECTANGLE, + 'word': defines.MOUSE_SELECTION_WORD, + 'line': defines.MOUSE_SELECTION_LINE, + } + setattr(mouse_selection, 'code_map', cmap) + return func, [cmap[rest]] + + def parse_key_action(action: str) -> Optional[KeyAction]: parts = action.strip().split(maxsplit=1) func = parts[0] diff --git a/kitty/config_data.py b/kitty/config_data.py index ddab3b590..c45d9b4aa 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -560,10 +560,6 @@ url_style_map = dict( o('url_style', 'curly', option_type=url_style) -o('open_url_modifiers', 'kitty_mod', option_type=to_modifiers, long_text=_(''' -The modifier keys to press when clicking with the -mouse on URLs to open the URL''')) - o('open_url_with', 'default', option_type=to_cmdline, long_text=_(''' The program with which to open URLs that are clicked on. The special value :code:`default` means to use the @@ -613,13 +609,6 @@ Remove spaces at the end of lines when copying to clipboard. A value of :code:`smart` will do it when using normal selections, but not rectangle selections. :code:`always` will always do it.''')) -o('rectangle_select_modifiers', 'ctrl+alt', option_type=to_modifiers, long_text=_(''' -The modifiers to use rectangular selection (i.e. to select text in a -rectangular block with the mouse)''')) - -o('terminal_select_modifiers', 'shift', option_type=to_modifiers, long_text=_(''' -The modifiers to override mouse selection even when a terminal application has grabbed the mouse''')) - o('select_by_word_characters', '@-./_~?&=%+#', long_text=_(''' Characters considered part of a word when double clicking. In addition to these characters any character that is marked as an alphanumeric character in the unicode @@ -1357,6 +1346,17 @@ to force the choice.''')) g('mousemap') # {{{ m('click_url', 'ctrl+shift+left', 'release', 'grabbed,ungrabbed', 'mouse_click_url', _('Click the link under the mouse cursor')) +m('paste_selection', 'middle', 'release', 'grabbed,ungrabbed', 'paste_selection', _('Paste from the primary selection')) +m('extend_selection', 'right', 'press', 'grabbed,ungrabbed', 'mouse_selection extend', _('Extend the current selection')) +for grabbed in (False, True): + modes = 'ungrabbed' + (',grabbed' if grabbed else '') + name_s = '_grabbed' if grabbed else '' + mods_p = 'shift+' if grabbed else '' + m('start_simple_selection' + name_s, mods_p + 'left', 'press', modes, 'mouse_selection normal', _('Start selecting text')) + m('start_rectangle_selection' + name_s, mods_p + 'ctrl+alt+left', 'press', modes, 'mouse_selection rectangle', + _('Start selecting text in a rectangle')) + m('select_word' + name_s, mods_p + 'left', 'doublepress', modes, 'mouse_selection word', _('Select a word')) + m('select_line' + name_s, mods_p + 'left', 'triplepress', modes, 'mouse_selection line', _('Select a line')) # }}} g('shortcuts') # {{{ diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index b78231f04..835dacd44 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -10,6 +10,11 @@ from kitty.fonts.render import FontObject from kitty.options_stub import Options # Constants {{{ +MOUSE_SELECTION_LINE: int +MOUSE_SELECTION_EXTEND: int +MOUSE_SELECTION_NORMAL: int +MOUSE_SELECTION_WORD: int +MOUSE_SELECTION_RECTANGLE: int KITTY_VCS_REV: str NO_CLOSE_REQUESTED: int IMPERATIVE_CLOSE_REQUESTED: int @@ -1157,3 +1162,7 @@ def set_window_padding(os_window_id: int, tab_id: int, window_id: int, left: int def click_mouse_url(os_window_id: int, tab_id: int, window_id: int) -> None: pass + + +def mouse_selection(os_window_id: int, tab_id: int, window_id: int, code: int, button: int) -> None: + pass diff --git a/kitty/mouse.c b/kitty/mouse.c index 1c7094d6d..7c20c3df9 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -219,23 +219,9 @@ set_mouse_cursor_when_dragging(void) { } static inline void -update_drag(bool from_button, Window *w, bool is_release, int modifiers, int button) { +update_drag(Window *w) { Screen *screen = w->render_data.screen; - if (from_button) { - if (is_release) { - global_state.active_drag_in_window = 0; - global_state.active_drag_button = -1; - w->last_drag_scroll_at = 0; - if (screen->selections.in_progress) { - screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, true, false); - } - } - else { - global_state.active_drag_in_window = w->id; - global_state.active_drag_button = button; - screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, modifiers == (int)OPT(rectangle_select_modifiers) || modifiers == ((int)OPT(rectangle_select_modifiers) | (int)OPT(terminal_select_modifiers)), EXTEND_CELL); - } - } else if (screen->selections.in_progress) { + if (screen && screen->selections.in_progress) { screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, false); } set_mouse_cursor_when_dragging(); @@ -246,7 +232,7 @@ do_drag_scroll(Window *w, bool upwards) { Screen *screen = w->render_data.screen; if (screen->linebuf == screen->main_linebuf) { screen_history_scroll(screen, SCROLL_LINE, upwards); - update_drag(false, w, false, 0, -1); + update_drag(w); if (mouse_cursor_shape != ARROW) { mouse_cursor_shape = ARROW; set_mouse_cursor(mouse_cursor_shape); @@ -276,7 +262,6 @@ extend_selection(Window *w, bool ended) { if (screen_has_selection(screen)) { screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, ended, false); } - set_mouse_cursor_when_dragging(); } static inline void @@ -367,7 +352,7 @@ handle_mouse_movement_in_kitty(Window *w, int button, bool mouse_cell_changed) { if (screen->selections.in_progress && (button == global_state.active_drag_button)) { monotonic_t now = monotonic(); if ((now - w->last_drag_scroll_at) >= ms_to_monotonic_t(20ll) || mouse_cell_changed) { - update_drag(false, w, false, 0, button); + update_drag(w); w->last_drag_scroll_at = now; } } @@ -394,8 +379,7 @@ HANDLER(handle_move_event) { bool in_tracking_mode = ( screen->modes.mouse_tracking_mode == ANY_MODE || (screen->modes.mouse_tracking_mode == MOTION_MODE && button >= 0)); - bool has_terminal_select_modifiers = modifiers == (int)OPT(terminal_select_modifiers) || modifiers == ((int)OPT(rectangle_select_modifiers) | (int)OPT(terminal_select_modifiers)); - bool handle_in_kitty = !in_tracking_mode || has_terminal_select_modifiers; + bool handle_in_kitty = !in_tracking_mode || global_state.active_drag_in_window == w->id; if (handle_in_kitty) { handle_mouse_movement_in_kitty(w, button, mouse_cell_changed | cell_half_changed); } else { @@ -405,29 +389,6 @@ HANDLER(handle_move_event) { } } -static inline void -multi_click(Window *w, unsigned int count) { - Screen *screen = w->render_data.screen; - index_type start, end; - SelectionExtendMode mode = EXTEND_CELL; - unsigned int y1, y2; - switch(count) { - case 2: - if (screen_selection_range_for_word(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, &y1, &y2, &start, &end, true)) mode = EXTEND_WORD; - break; - case 3: - if (screen_selection_range_for_line(screen, w->mouse_pos.cell_y, &start, &end)) mode = EXTEND_LINE; - break; - default: - break; - } - if (mode != EXTEND_CELL) { - screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, mode); - screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, true); - } - set_mouse_cursor_when_dragging(); -} - static inline double distance(double x1, double y1, double x2, double y2) { return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); @@ -602,6 +563,59 @@ enter_event() { #endif } +static void +end_drag(Window *w) { + Screen *screen = w->render_data.screen; + global_state.active_drag_in_window = 0; + global_state.active_drag_button = -1; + w->last_drag_scroll_at = 0; + if (screen->selections.in_progress) { + screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, true, false); + } +} + +typedef enum MouseSelectionType { + MOUSE_SELECTION_NORMAL, + MOUSE_SELECTION_EXTEND, + MOUSE_SELECTION_RECTANGLE, + MOUSE_SELECTION_WORD, + MOUSE_SELECTION_LINE, +} MouseSelectionType; + + +void +mouse_selection(Window *w, int code, int button) { + global_state.active_drag_in_window = w->id; + global_state.active_drag_button = button; + Screen *screen = w->render_data.screen; + index_type start, end; + unsigned int y1, y2; +#define S(mode) {\ + screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, mode); \ + screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, true); } + + switch((MouseSelectionType)code) { + case MOUSE_SELECTION_NORMAL: + screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, EXTEND_CELL); + break; + case MOUSE_SELECTION_RECTANGLE: + screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, true, EXTEND_CELL); + break; + case MOUSE_SELECTION_WORD: + if (screen_selection_range_for_word(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, &y1, &y2, &start, &end, true)) S(EXTEND_WORD); + break; + case MOUSE_SELECTION_LINE: + if (screen_selection_range_for_line(screen, w->mouse_pos.cell_y, &start, &end)) S(EXTEND_LINE); + break; + case MOUSE_SELECTION_EXTEND: + extend_selection(w, false); + break; + } + set_mouse_cursor_when_dragging(); +#undef S +} + + void mouse_event(int button, int modifiers, int action) { MouseShape old_cursor = mouse_cursor_shape; @@ -626,7 +640,8 @@ mouse_event(int button, int modifiers, int action) { else if (action == GLFW_RELEASE && button == global_state.active_drag_button) { w = window_for_id(global_state.active_drag_in_window); if (w) { - update_drag(true, w, true, modifiers, button); + end_drag(w); + return; } } } @@ -801,6 +816,11 @@ init_mouse(PyObject *module) { PyModule_AddIntMacro(module, RELEASE); PyModule_AddIntMacro(module, DRAG); PyModule_AddIntMacro(module, MOVE); + PyModule_AddIntMacro(module, MOUSE_SELECTION_NORMAL); + PyModule_AddIntMacro(module, MOUSE_SELECTION_EXTEND); + PyModule_AddIntMacro(module, MOUSE_SELECTION_RECTANGLE); + PyModule_AddIntMacro(module, MOUSE_SELECTION_WORD); + PyModule_AddIntMacro(module, MOUSE_SELECTION_LINE); if (PyModule_AddFunctions(module, module_methods) != 0) return false; return true; } diff --git a/kitty/state.c b/kitty/state.c index 4bcee0504..f9cd0d37b 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -576,11 +576,6 @@ resolve_mods(int mods) { return mods; } -static int -convert_mods(PyObject *obj) { - return resolve_mods(PyLong_AsLong(obj)); -} - static WindowTitleIn window_title_in(PyObject *title_in) { const char *in = PyUnicode_AsUTF8(title_in); @@ -719,9 +714,6 @@ PYWRAP1(set_options) { S(mouse_hide_wait, parse_s_double_to_monotonic_t); S(wheel_scroll_multiplier, PyFloat_AsDouble); S(touch_scroll_multiplier, PyFloat_AsDouble); - S(open_url_modifiers, convert_mods); - S(rectangle_select_modifiers, convert_mods); - S(terminal_select_modifiers, convert_mods); S(click_interval, parse_s_double_to_monotonic_t); S(resize_debounce_time, parse_s_double_to_monotonic_t); S(mark1_foreground, color_as_int); @@ -1163,6 +1155,17 @@ click_mouse_url(id_type os_window_id, id_type tab_id, id_type window_id) { END_WITH_WINDOW; } +static PyObject* +pymouse_selection(PyObject *self UNUSED, PyObject *args) { + id_type os_window_id, tab_id, window_id; + int code, button; + PA("KKKii", &os_window_id, &tab_id, &window_id, &code, &button); + WITH_WINDOW(os_window_id, tab_id, window_id); + mouse_selection(window, code, button); + END_WITH_WINDOW; + Py_RETURN_NONE; +} + THREE_ID_OBJ(update_window_title) THREE_ID(remove_window) THREE_ID(click_mouse_url) @@ -1186,6 +1189,7 @@ static PyMethodDef module_methods[] = { MW(next_window_id, METH_NOARGS), MW(set_options, METH_VARARGS), MW(click_mouse_url, METH_VARARGS), + MW(mouse_selection, METH_VARARGS), MW(set_in_sequence_mode, METH_O), MW(resolve_key_mods, METH_VARARGS), MW(handle_for_window_id, METH_VARARGS), diff --git a/kitty/state.h b/kitty/state.h index fd1c1b0b1..d60de6f65 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -27,9 +27,6 @@ typedef struct { CursorShape cursor_shape; float cursor_beam_thickness; float cursor_underline_thickness; - unsigned int open_url_modifiers; - unsigned int rectangle_select_modifiers; - unsigned int terminal_select_modifiers; unsigned int url_style; unsigned int scrollback_pager_history_size; bool scrollback_fill_enlarged_window; @@ -293,3 +290,4 @@ void update_os_window_title(OSWindow *os_window); void fake_scroll(Window *w, int amount, bool upwards); Window* window_for_window_id(id_type kitty_window_id); void mouse_open_url(Window *w); +void mouse_selection(Window *w, int code, int button); diff --git a/kitty/window.py b/kitty/window.py index d58d094f9..94a48eb35 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -27,8 +27,8 @@ from .fast_data_types import ( MARK, MARK_MASK, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, STRIKETHROUGH, TINT_PROGRAM, KeyEvent, Screen, add_timer, add_window, cell_size_for_window, click_mouse_url, compile_program, encode_key_for_tty, - get_boss, get_clipboard_string, init_cell_program, pt_to_px, - set_clipboard_string, set_titlebar_color, set_window_padding, + get_boss, get_clipboard_string, init_cell_program, mouse_selection, + pt_to_px, set_clipboard_string, set_titlebar_color, set_window_padding, set_window_render_data, update_window_title, update_window_visibility, viewport_for_window ) @@ -297,6 +297,7 @@ class Window: watchers: Optional[Watchers] = None ): self.watchers = watchers or Watchers() + self.current_mouse_event_button = 0 self.prev_osc99_cmd = NotificationCommand() self.action_on_close: Optional[Callable] = None self.action_on_removal: Optional[Callable] = None @@ -533,6 +534,7 @@ class Window: def on_mouse_event(self, event: Dict[str, Any]) -> bool: ev = MouseEvent(**event) + self.current_mouse_event_button = ev.button action = self.opts.mousemap.get(ev) if action is None: return False @@ -796,8 +798,11 @@ class Window: # }}} # mouse actions {{{ - def mouse_click_url(self) -> bool: + def mouse_click_url(self) -> None: click_mouse_url(self.os_window_id, self.tab_id, self.id) + + def mouse_selection(self, code: int) -> None: + mouse_selection(self.os_window_id, self.tab_id, self.id, code, self.current_mouse_event_button) # }}} def text_for_selection(self) -> str: