/* macOS text editing shortcuts. GPL-2.0-or-later; see COPYING. */ #include "macos-shortcuts.h" #include "config.h" #include "../common/zoitechat.h" #include "xtext.h" #include static void (*preferences_callback) (void); static void open_preferences (GtkWindow *window) { if (preferences_callback) preferences_callback (); } static void minimize_window (GtkWindow *window) { gtk_window_iconify (window); } void macos_shortcuts_minimize_active (GtkWindow *fallback) { GList *windows = gtk_window_list_toplevels (); GList *item; GtkWindow *target = fallback; for (item = windows; item; item = item->next) { if (gtk_window_is_active (GTK_WINDOW (item->data))) { target = GTK_WINDOW (item->data); break; } } if (target) minimize_window (target); g_list_free (windows); } static void add_movement (GtkBindingSet *bindings, guint keyval, GdkModifierType mod, GtkMovementStep step, gint count) { gtk_binding_entry_add_signal (bindings, keyval, mod, "move-cursor", 3, GTK_TYPE_MOVEMENT_STEP, step, G_TYPE_INT, count, G_TYPE_BOOLEAN, FALSE); gtk_binding_entry_add_signal (bindings, keyval, mod | GDK_SHIFT_MASK, "move-cursor", 3, GTK_TYPE_MOVEMENT_STEP, step, G_TYPE_INT, count, G_TYPE_BOOLEAN, TRUE); } static void delete_text_view_backwards (GtkTextView *view, gboolean word) { GtkTextBuffer *buffer = gtk_text_view_get_buffer (view); GtkTextIter start, end; gboolean editable = gtk_text_view_get_editable (view); /* GTK3 leaves GTK_DELETE_DISPLAY_LINE_ENDS unimplemented in TextView. * Preserve selection deletion and per-range editability instead of * moving the cursor first or deleting protected text unconditionally. */ if (gtk_text_buffer_get_selection_bounds (buffer, &start, &end)) gtk_text_buffer_delete_selection (buffer, TRUE, editable); else { gtk_text_buffer_get_iter_at_mark (buffer, &end, gtk_text_buffer_get_insert (buffer)); start = end; if (word) gtk_text_iter_backward_word_start (&start); else gtk_text_view_backward_display_line_start (view, &start); gtk_text_buffer_begin_user_action (buffer); gtk_text_buffer_delete_interactive (buffer, &start, &end, editable); gtk_text_buffer_end_user_action (buffer); } gtk_text_view_reset_im_context (view); gtk_text_view_scroll_mark_onscreen (view, gtk_text_buffer_get_insert (buffer)); } static void delete_display_line_start (GtkTextView *view) { delete_text_view_backwards (view, FALSE); } static void delete_word_start (GtkTextView *view) { delete_text_view_backwards (view, TRUE); } void macos_shortcuts_init (void (*callback) (void)) { GType types[] = {GTK_TYPE_ENTRY, GTK_TYPE_TEXT_VIEW}; gpointer window_class; GtkBindingSet *window_bindings; static gboolean initialized = FALSE; guint i; preferences_callback = callback; if (initialized) return; initialized = TRUE; /* GTK's built-in editing bindings use Control, even with Quartz. * Use Command for these three actions on macOS, preserving GTK's * selection, password-field and read-only checks. Entry subclasses * (including the spell-checking input and search fields) inherit these. */ for (i = 0; i < G_N_ELEMENTS (types); i++) { gpointer klass = g_type_class_ref (types[i]); GtkBindingSet *bindings = gtk_binding_set_by_class (klass); gtk_binding_entry_remove (bindings, GDK_KEY_c, GDK_CONTROL_MASK); gtk_binding_entry_remove (bindings, GDK_KEY_x, GDK_CONTROL_MASK); gtk_binding_entry_remove (bindings, GDK_KEY_v, GDK_CONTROL_MASK); gtk_binding_entry_add_signal (bindings, GDK_KEY_c, GDK_META_MASK, "copy-clipboard", 0); gtk_binding_entry_add_signal (bindings, GDK_KEY_x, GDK_META_MASK, "cut-clipboard", 0); gtk_binding_entry_add_signal (bindings, GDK_KEY_v, GDK_META_MASK, "paste-clipboard", 0); /* Two movements select the whole buffer without changing its text. */ gtk_binding_entry_add_signal (bindings, GDK_KEY_a, GDK_META_MASK, "move-cursor", 3, GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_BUFFER_ENDS, G_TYPE_INT, -1, G_TYPE_BOOLEAN, FALSE); gtk_binding_entry_add_signal (bindings, GDK_KEY_a, GDK_META_MASK, "move-cursor", 3, GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_BUFFER_ENDS, G_TYPE_INT, 1, G_TYPE_BOOLEAN, TRUE); add_movement (bindings, GDK_KEY_Left, GDK_META_MASK, GTK_MOVEMENT_DISPLAY_LINE_ENDS, -1); add_movement (bindings, GDK_KEY_Right, GDK_META_MASK, GTK_MOVEMENT_DISPLAY_LINE_ENDS, 1); add_movement (bindings, GDK_KEY_Left, GDK_MOD1_MASK, GTK_MOVEMENT_WORDS, -1); add_movement (bindings, GDK_KEY_Right, GDK_MOD1_MASK, GTK_MOVEMENT_WORDS, 1); add_movement (bindings, GDK_KEY_Up, GDK_META_MASK, GTK_MOVEMENT_BUFFER_ENDS, -1); add_movement (bindings, GDK_KEY_Down, GDK_META_MASK, GTK_MOVEMENT_BUFFER_ENDS, 1); if (types[i] == GTK_TYPE_TEXT_VIEW) { g_signal_new_class_handler ("zoitechat-macos-delete-word-start", types[i], G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK (delete_word_start), NULL, NULL, NULL, G_TYPE_NONE, 0); g_signal_new_class_handler ("zoitechat-macos-delete-line-start", types[i], G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK (delete_display_line_start), NULL, NULL, NULL, G_TYPE_NONE, 0); gtk_binding_entry_add_signal (bindings, GDK_KEY_BackSpace, GDK_META_MASK, "zoitechat-macos-delete-line-start", 0); gtk_binding_entry_add_signal (bindings, GDK_KEY_BackSpace, GDK_MOD1_MASK, "zoitechat-macos-delete-word-start", 0); } else { gtk_binding_entry_add_signal (bindings, GDK_KEY_BackSpace, GDK_MOD1_MASK, "delete-from-cursor", 2, GTK_TYPE_DELETE_TYPE, GTK_DELETE_WORD_ENDS, G_TYPE_INT, -1); gtk_binding_entry_add_signal (bindings, GDK_KEY_BackSpace, GDK_META_MASK, "delete-from-cursor", 2, GTK_TYPE_DELETE_TYPE, GTK_DELETE_DISPLAY_LINE_ENDS, G_TYPE_INT, -1); } g_type_class_unref (klass); } /* Window-level bindings also work in dialogs without a main menu bar. * The native menu uses the same actions when Quartz handles the key. */ window_class = g_type_class_ref (GTK_TYPE_WINDOW); window_bindings = gtk_binding_set_by_class (window_class); g_signal_new_class_handler ("zoitechat-macos-minimize", GTK_TYPE_WINDOW, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK (minimize_window), NULL, NULL, NULL, G_TYPE_NONE, 0); g_signal_new_class_handler ("zoitechat-macos-preferences", GTK_TYPE_WINDOW, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK (open_preferences), NULL, NULL, NULL, G_TYPE_NONE, 0); gtk_binding_entry_add_signal (window_bindings, GDK_KEY_m, GDK_META_MASK, "zoitechat-macos-minimize", 0); gtk_binding_entry_add_signal (window_bindings, GDK_KEY_comma, GDK_META_MASK, "zoitechat-macos-preferences", 0); g_type_class_unref (window_class); } gboolean macos_shortcuts_remap_default (guint *keyval, GdkModifierType *mod, const char *action, const char *data1, const char *data2) { /* Only exact legacy defaults qualify. Custom actions, parameters and * alternate shortcuts must survive loading a configuration unchanged. */ if (data2 && *data2) return FALSE; if (*keyval == GDK_KEY_Tab && *mod == GDK_META_MASK && !g_strcmp0 (action, "Complete nick/command") && !g_strcmp0 (data1, "Up")) { *mod = GDK_CONTROL_MASK; return TRUE; } if ((!data1 || !*data1) && *mod == GDK_MOD1_MASK && ((*keyval == GDK_KEY_Left && !g_strcmp0 (action, "Move front tab left")) || (*keyval == GDK_KEY_Right && !g_strcmp0 (action, "Move front tab right")))) { *mod |= GDK_META_MASK; return TRUE; } if (*keyval == GDK_KEY_m && *mod == GDK_META_MASK && !g_strcmp0 (action, "Menu Shortcut") && !g_strcmp0 (data1, "reset-marker")) { *keyval = GDK_KEY_R; *mod |= GDK_SHIFT_MASK; return TRUE; } return FALSE; } gboolean macos_shortcuts_copy_chat (GtkWidget *entry, GdkEventKey *event, GtkXText *xtext) { gint start, end; GdkModifierType modifiers = event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK | GDK_META_MASK | GDK_SUPER_MASK | GDK_HYPER_MASK); /* XText leaves keyboard focus in the message input. Prefer an input * selection; otherwise Command+C copies the selected chat text. Leave * Command+Shift+C and all other shortcuts to their existing handlers. */ if (gdk_keyval_to_lower (event->keyval) != GDK_KEY_c || modifiers != GDK_META_MASK || gtk_editable_get_selection_bounds (GTK_EDITABLE (entry), &start, &end)) return FALSE; gtk_xtext_copy_selection (xtext); return TRUE; }