Files
zoitechat/src/fe-gtk/macos-shortcuts.c
T

244 lines
9.3 KiB
C

/* 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 <gdk/gdkkeysyms.h>
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;
}