/* ide-source-view-mode.c * * Copyright 2015 Alexander Larsson * Copyright 2015-2019 Christian Hergert * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "ide-source-view-mode" #include "config.h" #include #include #include #include "ide-source-view.h" #include "ide-source-view-mode.h" struct _IdeSourceViewMode { GtkWidget parent_instance; GtkWidget *view; char *name; char *display_name; gchar *default_mode; IdeSourceViewModeType type; guint has_selection : 1; }; G_DEFINE_FINAL_TYPE (IdeSourceViewMode, ide_source_view_mode, GTK_TYPE_WIDGET) enum { PROP_0, PROP_NAME, LAST_PROP }; static GParamSpec *properties [LAST_PROP]; static void update_selection (IdeSourceViewMode *self) { GtkStyleContext *style_context; g_assert (IDE_IS_SOURCE_VIEW_MODE (self)); style_context = gtk_widget_get_style_context (GTK_WIDGET (self)); if (self->has_selection) gtk_style_context_add_class (style_context, "has-selection"); else gtk_style_context_remove_class (style_context, "has-selection"); } static void get_param (IdeSourceViewMode *self, const gchar *param, GValue *value) { GtkStyleContext *context; g_assert (IDE_IS_SOURCE_VIEW_MODE (self)); g_assert (param != NULL); g_assert (value != NULL); context = gtk_widget_get_style_context (GTK_WIDGET (self)); gtk_style_context_get_style_property (context, param, value); } static gboolean get_boolean_param (IdeSourceViewMode *self, const gchar *param) { GValue value = { 0 }; gboolean ret; g_value_init (&value, G_TYPE_BOOLEAN); get_param (self, param, &value); ret = g_value_get_boolean (&value); g_value_unset (&value); return ret; } static gchar * get_string_param (IdeSourceViewMode *self, const gchar *param) { GValue value = { 0 }; gchar *ret; g_value_init (&value, G_TYPE_STRING); get_param (self, param, &value); ret = g_value_dup_string (&value); g_value_unset (&value); return ret; } const gchar * ide_source_view_mode_get_default_mode (IdeSourceViewMode *self) { /* * instead of switching back to "default" mode, use this mode instead * if no other mode is specified. */ return self->default_mode; } const gchar * ide_source_view_mode_get_display_name (IdeSourceViewMode *self) { return self->display_name; } gboolean ide_source_view_mode_get_repeat_insert_with_count (IdeSourceViewMode *self) { /* * If count is 10, and you type -, you will get ---------- */ return get_boolean_param (self, "repeat-insert-with-count"); } gboolean ide_source_view_mode_get_suppress_unbound (IdeSourceViewMode *self) { /* * unknown keypresses are swallowed. you probably want to use this * with a transient mode. */ return get_boolean_param (self, "suppress-unbound"); } gboolean ide_source_view_mode_get_block_cursor (IdeSourceViewMode *self) { /* * fakes a block cursor by using overwrite mode in textview. * you probably want to use this with "suppress-unbound". */ return get_boolean_param (self, "block-cursor"); } gboolean ide_source_view_mode_get_keep_mark_on_char (IdeSourceViewMode *self) { /* forces the source view to not let the cursor reach the end of the * line (basically an iter over \n). this is useful for emulating vim */ return get_boolean_param (self, "keep-mark-on-char"); } static void ide_source_view_mode_destroy (GtkWidget *widget) { IdeSourceViewMode *self = IDE_SOURCE_VIEW_MODE (widget); g_clear_object (&self->view); g_clear_pointer (&self->name, g_free); g_clear_pointer (&self->default_mode, g_free); g_clear_pointer (&self->display_name, g_free); self->type = 0; GTK_WIDGET_CLASS (ide_source_view_mode_parent_class)->destroy (widget); } static void proxy_closure_marshal (GClosure *closure, GValue *return_value, guint n_param_values, const GValue *param_values, gpointer invocation_hint, gpointer marshal_data) { g_autoptr(IdeSourceViewMode) self = NULL; GValue *param_copy; g_assert (closure != NULL); g_assert (param_values != NULL); /* * To be absolutely sure about reference counting with GValue and other * ownership mishaps, this proxy makes a full copy of all parameters. It is * certainly slower, but we are only activated from keybindings and the user * can only type so fast... */ self = g_value_dup_object (¶m_values[0]); g_assert (IDE_IS_SOURCE_VIEW_MODE (self)); if (self->view == NULL) { g_warning ("Cannot proxy signal after destroy has been called"); return; } /* Just spill to the stack, we only have a small number of params */ param_copy = g_alloca (sizeof (GValue) * n_param_values); memset (param_copy, 0, sizeof (GValue) * n_param_values); /* Swap the first object into the IdeSourceView */ g_value_init (¶m_copy[0], G_OBJECT_TYPE (self->view)); g_value_set_object (¶m_copy[0], self->view); /* Copy the rest of the parameters across */ for (guint i = 1; i < n_param_values; i++) { g_value_init (¶m_copy[i], G_VALUE_TYPE (¶m_values[i])); g_value_copy (¶m_values[i], ¶m_copy[i]); } /* Emit the signal on the source view */ g_signal_emitv (param_copy, GPOINTER_TO_UINT (closure->data), 0, return_value); /* Cleanup, dropping our references. */ for (guint i = 0; i < n_param_values; i++) g_value_unset (¶m_copy[i]); } static void proxy_all_action_signals (GType type) { g_autofree guint *signals = NULL; GSignalQuery query; guint n_signals = 0; g_assert (g_type_is_a (type, G_TYPE_OBJECT)); signals = g_signal_list_ids (type, &n_signals); for (guint i = 0; i < n_signals; i++) { g_signal_query (signals[i], &query); /* We don't support detailed signals */ if (query.signal_flags & G_SIGNAL_DETAILED) continue; /* Only proxy keybinding action signals */ if (query.signal_flags & G_SIGNAL_ACTION) { GClosure *proxy; proxy = g_closure_new_simple (sizeof (GClosure), GINT_TO_POINTER (query.signal_id)); g_closure_set_meta_marshal (proxy, NULL, proxy_closure_marshal); g_signal_newv (query.signal_name, IDE_TYPE_SOURCE_VIEW_MODE, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, proxy, NULL, NULL, NULL, query.return_type, query.n_params, (GType *)query.param_types); /* Proxy ownership is stolen when sinking the closure */ } } } const gchar * ide_source_view_mode_get_name (IdeSourceViewMode *mode) { g_return_val_if_fail (IDE_IS_SOURCE_VIEW_MODE (mode), NULL); return mode->name; } static void ide_source_view_mode_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { IdeSourceViewMode *mode = IDE_SOURCE_VIEW_MODE(object); switch (prop_id) { case PROP_NAME: g_value_set_string (value, ide_source_view_mode_get_name (mode)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); } } static void ide_source_view_mode_class_init (IdeSourceViewModeClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkBindingSet *binding_set, *parent_binding_set; GType type; object_class->get_property = ide_source_view_mode_get_property; widget_class->destroy = ide_source_view_mode_destroy; gtk_widget_class_set_css_name (widget_class, "idesourceviewmode"); properties [PROP_NAME] = g_param_spec_string ("name", "Name", "The name of the mode.", NULL, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_properties (object_class, LAST_PROP, properties); gtk_widget_class_install_style_property (GTK_WIDGET_CLASS (klass), g_param_spec_boolean ("suppress-unbound", "Supress Unbound", "Suppress Unbound Keypresses", FALSE, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS))); gtk_widget_class_install_style_property (GTK_WIDGET_CLASS (klass), g_param_spec_boolean ("block-cursor", "Block Cursor", "Use fake block cursor by " "using overwrite mode.", FALSE, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS))); gtk_widget_class_install_style_property (GTK_WIDGET_CLASS (klass), g_param_spec_boolean ("keep-mark-on-char", "Keep Mark on Char", "Don't allow the cursor to " "move to line end.", FALSE, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS))); gtk_widget_class_install_style_property (GTK_WIDGET_CLASS (klass), g_param_spec_string ("display-name", "Display Name", "Display name for mode", NULL, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS))); gtk_widget_class_install_style_property (GTK_WIDGET_CLASS (klass), g_param_spec_string ("default-mode", "Default Mode", "Suggest a followup default mode", NULL, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS))); gtk_widget_class_install_style_property (GTK_WIDGET_CLASS (klass), g_param_spec_boolean ("repeat-insert-with-count", "Repeat Insert with Count", "Use the current count to " "repeat the insertion.", FALSE, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS))); /* Proxy all action signals from source view */ type = IDE_TYPE_SOURCE_VIEW; while (type != G_TYPE_INVALID && type != GTK_TYPE_WIDGET) { proxy_all_action_signals (type); type = g_type_parent (type); } /* Add unbind all entries from parent classes (which is really just the GtkWidget ones) so that we *only* add stuff via modes. Any default ones are handled in the normal fallback paths after mode elements are done. */ binding_set = gtk_binding_set_by_class (klass); type = g_type_parent (IDE_TYPE_SOURCE_VIEW_MODE); while (type) { parent_binding_set = gtk_binding_set_find (g_type_name (type)); type = g_type_parent (type); if (parent_binding_set) { GtkBindingEntry *entry = parent_binding_set->entries; while (entry != NULL) { gtk_binding_entry_skip (binding_set, entry->keyval, entry->modifiers); entry = entry->set_next; } } } } static void ide_source_view_mode_init (IdeSourceViewMode *mode) { } static gboolean is_modifier_key (GdkEventKey *event) { static const guint modifier_keyvals[] = { GDK_KEY_Shift_L, GDK_KEY_Shift_R, GDK_KEY_Shift_Lock, GDK_KEY_Caps_Lock, GDK_KEY_ISO_Lock, GDK_KEY_Control_L, GDK_KEY_Control_R, GDK_KEY_Meta_L, GDK_KEY_Meta_R, GDK_KEY_Alt_L, GDK_KEY_Alt_R, GDK_KEY_Super_L, GDK_KEY_Super_R, GDK_KEY_Hyper_L, GDK_KEY_Hyper_R, GDK_KEY_ISO_Level3_Shift, GDK_KEY_ISO_Next_Group, GDK_KEY_ISO_Prev_Group, GDK_KEY_ISO_First_Group, GDK_KEY_ISO_Last_Group, GDK_KEY_Mode_switch, GDK_KEY_Num_Lock, GDK_KEY_Multi_key, GDK_KEY_Scroll_Lock, 0 }; const guint *ac_val; ac_val = modifier_keyvals; while (*ac_val) { if (event->keyval == *ac_val++) return TRUE; } return FALSE; } static gboolean toplevel_is_offscreen (GdkWindow *window) { /* * FIXME: This function is a workaround for a segfault in gdk_window_beep() * with offscreen windows. * * https://bugzilla.gnome.org/show_bug.cgi?id=748341 */ for (; window != NULL; window = gdk_window_get_parent (window)) { GdkWindowType type; type = gdk_window_get_window_type (window); if (type == GDK_WINDOW_OFFSCREEN) return TRUE; } return FALSE; } static gboolean can_suppress (const GdkEventKey *event) { /* * This is rather tricky because we don't know what can be activated * in the bubble up phase of event delivery. Looking at ->string isn't * very safe when input methods are in play. So we just hard code some * things we know about common keybindings. * * If you are wondering why you're getting beeps in the editor while * activating some keybinding you've added, you found the right spot! */ if ((event->state & GDK_MODIFIER_MASK) != 0) return FALSE; switch (event->keyval) { case GDK_KEY_F1: case GDK_KEY_F2: case GDK_KEY_F3: case GDK_KEY_F4: case GDK_KEY_F5: case GDK_KEY_F6: case GDK_KEY_F7: case GDK_KEY_F8: case GDK_KEY_F9: case GDK_KEY_F10: case GDK_KEY_F11: case GDK_KEY_F12: return FALSE; default: return TRUE; } } gboolean _ide_source_view_mode_do_event (IdeSourceViewMode *mode, GdkEventKey *event, gboolean *remove) { GtkStyleContext *context; gboolean suppress_unbound; gboolean handled; g_return_val_if_fail (IDE_IS_SOURCE_VIEW_MODE (mode), FALSE); g_return_val_if_fail (event, FALSE); g_return_val_if_fail (remove, FALSE); context = gtk_widget_get_style_context (GTK_WIDGET (mode)); suppress_unbound = ide_source_view_mode_get_suppress_unbound (mode); g_object_ref (context); gtk_style_context_save (context); gtk_style_context_add_class (context, mode->name); handled = gtk_bindings_activate_event (G_OBJECT (mode), event); gtk_style_context_restore (context); g_object_unref (context); /* possibly re-apply has_selection */ update_selection (mode); *remove = FALSE; switch (mode->type) { case IDE_SOURCE_VIEW_MODE_TYPE_TRANSIENT: if (handled) { *remove = TRUE; } else { if (!is_modifier_key (event)) { if (!toplevel_is_offscreen (event->window)) gtk_widget_error_bell (mode->view); handled = TRUE; *remove = TRUE; } } break; case IDE_SOURCE_VIEW_MODE_TYPE_PERMANENT: { /* Don't block possible accelerators, but suppress others. */ if (!handled && suppress_unbound && can_suppress (event)) { if (!is_modifier_key (event) && !toplevel_is_offscreen (event->window)) gdk_window_beep (event->window); /* cancel any inflight macros */ g_signal_emit_by_name (mode->view, "end-macro"); handled = TRUE; } } break; case IDE_SOURCE_VIEW_MODE_TYPE_MODAL: handled = TRUE; break; default: g_assert_not_reached (); } return handled; } IdeSourceViewMode * _ide_source_view_mode_new (GtkWidget *view, const char *name, IdeSourceViewModeType type) { IdeSourceViewMode *mode; mode = g_object_new (IDE_TYPE_SOURCE_VIEW_MODE, NULL); mode->view = g_object_ref (view); mode->name = g_strdup (name); mode->type = type; if (mode->name != NULL) { GtkStyleContext *context; context = gtk_widget_get_style_context (GTK_WIDGET (mode)); gtk_style_context_add_class (context, mode->name); } mode->default_mode = get_string_param (mode, "default-mode"); mode->display_name = get_string_param (mode, "display-name"); IDE_TRACE_MSG ("suppress_unbound = %d", ide_source_view_mode_get_suppress_unbound (mode)); IDE_TRACE_MSG ("block_cursor = %d", ide_source_view_mode_get_block_cursor (mode)); IDE_TRACE_MSG ("type = %d", (int)mode->type); IDE_TRACE_MSG ("default_mode = %s", mode->default_mode ?: "(null)"); IDE_TRACE_MSG ("display_name = %s", mode->display_name ?: "(null)"); return g_object_ref_sink (mode); } IdeSourceViewModeType ide_source_view_mode_get_mode_type (IdeSourceViewMode *self) { g_return_val_if_fail (IDE_IS_SOURCE_VIEW_MODE (self), 0); return self->type; } void ide_source_view_mode_set_has_indenter (IdeSourceViewMode *self, gboolean has_indenter) { GtkStyleContext *style_context; g_assert (IDE_IS_SOURCE_VIEW_MODE (self)); style_context = gtk_widget_get_style_context (GTK_WIDGET (self)); if (has_indenter) gtk_style_context_add_class (style_context, "has-indenter"); else gtk_style_context_remove_class (style_context, "has-indenter"); } void ide_source_view_mode_set_has_selection (IdeSourceViewMode *self, gboolean has_selection) { g_assert (IDE_IS_SOURCE_VIEW_MODE (self)); self->has_selection = !!has_selection; update_selection (self); }