/* * mooentry.c * * Copyright (C) 2004-2006 by Yevgen Muntyan * * 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 2 of the License, or * (at your option) any later version. * * See COPYING file that comes with this distribution. */ #include "mooutils/moomarshals.h" #include "mooutils/mooentry.h" #include "mooutils/mooundo.h" #include "mooutils/mooutils-gobject.h" #include #include #include #include #include #include struct _MooEntryPrivate { MooUndoStack *undo_stack; gboolean enable_undo; gboolean enable_undo_menu; guint use_ctrl_u : 1; guint grab_selection : 1; guint fool_entry : 1; guint empty : 1; guint special_chars_menu : 1; }; static guint INSERT_ACTION_TYPE; static guint DELETE_ACTION_TYPE; static void moo_entry_class_init (MooEntryClass *klass); static void moo_entry_editable_init (GtkEditableClass *klass); static void moo_entry_init (MooEntry *entry); static void moo_entry_finalize (GObject *object); static void moo_entry_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); static void moo_entry_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); static gboolean moo_entry_button_release (GtkWidget *widget, GdkEventButton *event); static void moo_entry_delete_to_start (MooEntry *entry); static void moo_entry_populate_popup (GtkEntry *entry, GtkMenu *menu); static void moo_entry_delete_from_cursor(GtkEntry *entry, GtkDeleteType type, gint count); static void moo_entry_cut_clipboard (GtkEntry *entry); static void moo_entry_paste_clipboard (GtkEntry *entry); static void moo_entry_do_insert_text (GtkEditable *editable, const gchar *text, gint length, gint *position); static void moo_entry_do_delete_text (GtkEditable *editable, gint start_pos, gint end_pos); static void moo_entry_set_selection_bounds (GtkEditable *editable, gint start_pos, gint end_pos); static gboolean moo_entry_get_selection_bounds (GtkEditable *editable, gint *start_pos, gint *end_pos); static void moo_entry_changed (GtkEditable *editable); static void init_undo_actions (void); static gpointer insert_action_new (GtkEditable *editable, const gchar *text, gint length, gint *position); static gpointer delete_action_new (GtkEditable *editable, gint start_pos, gint end_pos); GType moo_entry_get_type (void) { static GType type = 0; if (G_UNLIKELY (!type)) { static const GTypeInfo info = { sizeof (MooEntryClass), NULL, /* base_init */ NULL, /* base_finalize */ (GClassInitFunc) moo_entry_class_init, NULL, /* class_finalize */ NULL, /* class_data */ sizeof (MooEntry), 0, (GInstanceInitFunc) moo_entry_init, NULL }; static const GInterfaceInfo editable_info = { (GInterfaceInitFunc) moo_entry_editable_init, NULL, NULL }; type = g_type_register_static (GTK_TYPE_ENTRY, "MooEntry", &info, 0); g_type_add_interface_static (type, GTK_TYPE_EDITABLE, &editable_info); } return type; } enum { PROP_0, PROP_ENABLE_UNDO, PROP_ENABLE_UNDO_MENU, PROP_GRAB_SELECTION, PROP_EMPTY, PROP_USE_SPECIAL_CHARS_MENU }; enum { UNDO, REDO, BEGIN_USER_ACTION, END_USER_ACTION, DELETE_TO_START, NUM_SIGNALS }; static guint signals[NUM_SIGNALS]; static GtkEditableClass *parent_editable_iface; static gpointer moo_entry_parent_class; static void moo_entry_class_init (MooEntryClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkEntryClass *entry_class = GTK_ENTRY_CLASS (klass); GtkBindingSet *binding_set; init_undo_actions (); gobject_class->finalize = moo_entry_finalize; gobject_class->set_property = moo_entry_set_property; gobject_class->get_property = moo_entry_get_property; widget_class->button_release_event = moo_entry_button_release; entry_class->populate_popup = moo_entry_populate_popup; entry_class->delete_from_cursor = moo_entry_delete_from_cursor; entry_class->cut_clipboard = moo_entry_cut_clipboard; entry_class->paste_clipboard = moo_entry_paste_clipboard; klass->undo = moo_entry_undo; klass->redo = moo_entry_redo; moo_entry_parent_class = g_type_class_peek_parent (klass); parent_editable_iface = g_type_interface_peek (moo_entry_parent_class, GTK_TYPE_EDITABLE); g_object_class_install_property (gobject_class, PROP_ENABLE_UNDO, g_param_spec_boolean ("enable-undo", "enable-undo", "enable-undo", TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); g_object_class_install_property (gobject_class, PROP_ENABLE_UNDO_MENU, g_param_spec_boolean ("enable-undo-menu", "enable-undo-menu", "enable-undo-menu", TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); g_object_class_install_property (gobject_class, PROP_GRAB_SELECTION, g_param_spec_boolean ("grab-selection", "grab-selection", "grab-selection", TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); g_object_class_install_property (gobject_class, PROP_EMPTY, g_param_spec_boolean ("empty", "empty", "empty", TRUE, G_PARAM_READABLE)); g_object_class_install_property (gobject_class, PROP_USE_SPECIAL_CHARS_MENU, g_param_spec_boolean ("use-special-chars-menu", "use-special-chars-menu", "use-special-chars-menu", FALSE, G_PARAM_READWRITE)); signals[UNDO] = g_signal_lookup ("undo", GTK_TYPE_ENTRY); if (!signals[UNDO]) signals[UNDO] = g_signal_new ("undo", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (MooEntryClass, undo), NULL, NULL, _moo_marshal_VOID__VOID, G_TYPE_NONE, 0); signals[REDO] = g_signal_lookup ("redo", GTK_TYPE_ENTRY); if (!signals[REDO]) signals[REDO] = g_signal_new ("redo", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (MooEntryClass, redo), NULL, NULL, _moo_marshal_VOID__VOID, G_TYPE_NONE, 0); signals[DELETE_TO_START] = _moo_signal_new_cb ("delete-to-start", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK (moo_entry_delete_to_start), NULL, NULL, _moo_marshal_VOID__VOID, G_TYPE_NONE, 0); binding_set = gtk_binding_set_by_class (klass); gtk_binding_entry_add_signal (binding_set, GDK_z, GDK_CONTROL_MASK, "undo", 0); gtk_binding_entry_add_signal (binding_set, GDK_z, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "redo", 0); gtk_binding_entry_add_signal (binding_set, GDK_u, GDK_CONTROL_MASK, "delete-to-start", 0); } static void moo_entry_editable_init (GtkEditableClass *klass) { klass->do_insert_text = moo_entry_do_insert_text; klass->do_delete_text = moo_entry_do_delete_text; klass->set_selection_bounds = moo_entry_set_selection_bounds; klass->get_selection_bounds = moo_entry_get_selection_bounds; klass->changed = moo_entry_changed; } static void moo_entry_init (MooEntry *entry) { entry->priv = g_new0 (MooEntryPrivate, 1); entry->priv->undo_stack = moo_undo_stack_new (entry); entry->priv->use_ctrl_u = TRUE; entry->priv->empty = TRUE; } static void moo_entry_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { MooEntry *entry = MOO_ENTRY (object); switch (prop_id) { case PROP_ENABLE_UNDO: entry->priv->enable_undo = g_value_get_boolean (value); g_object_notify (object, "enable-undo"); break; case PROP_ENABLE_UNDO_MENU: entry->priv->enable_undo_menu = g_value_get_boolean (value); g_object_notify (object, "enable-undo-menu"); break; case PROP_GRAB_SELECTION: entry->priv->grab_selection = g_value_get_boolean (value) ? TRUE : FALSE; g_object_notify (object, "grab-selection"); break; case PROP_USE_SPECIAL_CHARS_MENU: moo_entry_set_use_special_chars_menu (entry, g_value_get_boolean (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void moo_entry_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { MooEntry *entry = MOO_ENTRY (object); switch (prop_id) { case PROP_ENABLE_UNDO: g_value_set_boolean (value, entry->priv->enable_undo); break; case PROP_ENABLE_UNDO_MENU: g_value_set_boolean (value, entry->priv->enable_undo_menu); break; case PROP_GRAB_SELECTION: g_value_set_boolean (value, entry->priv->grab_selection ? TRUE : FALSE); break; case PROP_EMPTY: g_value_set_boolean (value, GTK_ENTRY(entry)->text_length == 0); break; case PROP_USE_SPECIAL_CHARS_MENU: g_value_set_boolean (value, entry->priv->special_chars_menu != 0); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void moo_entry_finalize (GObject *object) { MooEntry *entry = MOO_ENTRY (object); g_object_unref (entry->priv->undo_stack); g_free (entry->priv); G_OBJECT_CLASS (moo_entry_parent_class)->finalize (object); } static void moo_entry_changed (GtkEditable *editable) { MooEntry *entry = MOO_ENTRY (editable); GtkEntry *gtkentry = GTK_ENTRY (editable); gboolean empty = gtkentry->text_length == 0; if ((empty && !entry->priv->empty) || (!empty && entry->priv->empty)) { entry->priv->empty = empty; g_object_notify (G_OBJECT (entry), "empty"); } if (parent_editable_iface->changed) parent_editable_iface->changed (editable); } void moo_entry_undo (MooEntry *entry) { g_return_if_fail (MOO_IS_ENTRY (entry)); if (entry->priv->enable_undo && moo_undo_stack_can_undo (entry->priv->undo_stack)) moo_undo_stack_undo (entry->priv->undo_stack); } void moo_entry_redo (MooEntry *entry) { g_return_if_fail (MOO_IS_ENTRY (entry)); if (entry->priv->enable_undo && moo_undo_stack_can_redo (entry->priv->undo_stack)) moo_undo_stack_redo (entry->priv->undo_stack); } void moo_entry_begin_undo_group (MooEntry *entry) { g_return_if_fail (MOO_IS_ENTRY (entry)); moo_undo_stack_start_group (entry->priv->undo_stack); } void moo_entry_end_undo_group (MooEntry *entry) { g_return_if_fail (MOO_IS_ENTRY (entry)); moo_undo_stack_end_group (entry->priv->undo_stack); } void moo_entry_clear_undo (MooEntry *entry) { g_return_if_fail (MOO_IS_ENTRY (entry)); moo_undo_stack_clear (entry->priv->undo_stack); } GtkWidget* moo_entry_new (void) { return g_object_new (MOO_TYPE_ENTRY, NULL); } static void moo_entry_insert_at_cursor (GtkEntry *entry, const char *text, int len) { int start, end, pos; if (MOO_IS_ENTRY (entry)) moo_entry_begin_undo_group (MOO_ENTRY (entry)); if (gtk_editable_get_selection_bounds (GTK_EDITABLE (entry), &start, &end)) gtk_editable_delete_text (GTK_EDITABLE (entry), start, end); pos = gtk_editable_get_position (GTK_EDITABLE (entry)); gtk_editable_insert_text (GTK_EDITABLE (entry), text, len, &pos); gtk_editable_set_position (GTK_EDITABLE (entry), pos); if (MOO_IS_ENTRY (entry)) moo_entry_end_undo_group (MOO_ENTRY (entry)); } static void special_char_item_activated (GtkWidget *item, GtkEntry *entry) { const char *text; text = g_object_get_data (G_OBJECT (item), "moo-entry-special-char"); g_return_if_fail (text != NULL); moo_entry_insert_at_cursor (entry, text, -1); } static void create_special_char_item (MooEntry *entry, GtkWidget *menu, const char *label, const char *text) { GtkWidget *item; item = gtk_menu_item_new_with_label (label); g_object_set_data (G_OBJECT (item), "moo-entry-special-char", (char*) text); g_signal_connect (item, "activate", G_CALLBACK (special_char_item_activated), entry); gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); } static GtkWidget * create_special_chars_menu (MooEntry *entry) { GtkWidget *menu = gtk_menu_new (); create_special_char_item (entry, menu, "Line End", "\n"); create_special_char_item (entry, menu, "Tab", "\t"); gtk_widget_show_all (menu); return menu; } static void moo_entry_populate_popup (GtkEntry *gtkentry, GtkMenu *menu) { GtkWidget *item; MooEntry *entry = MOO_ENTRY (gtkentry); if (entry->priv->enable_undo_menu) { item = gtk_separator_menu_item_new (); gtk_widget_show (item); gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); item = gtk_image_menu_item_new_from_stock (GTK_STOCK_REDO, NULL); gtk_widget_show (item); gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); gtk_widget_set_sensitive (item, entry->priv->enable_undo && moo_undo_stack_can_redo (entry->priv->undo_stack)); g_signal_connect_swapped (item, "activate", G_CALLBACK (moo_entry_redo), entry); item = gtk_image_menu_item_new_from_stock (GTK_STOCK_UNDO, NULL); gtk_widget_show (item); gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); gtk_widget_set_sensitive (item, entry->priv->enable_undo && moo_undo_stack_can_undo (entry->priv->undo_stack)); g_signal_connect_swapped (item, "activate", G_CALLBACK (moo_entry_undo), entry); } if (entry->priv->special_chars_menu) { GtkWidget *submenu; item = gtk_separator_menu_item_new (); gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); item = gtk_menu_item_new_with_label ("Insert Special Character"); gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); submenu = create_special_chars_menu (entry); gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu); } } void moo_entry_set_use_special_chars_menu (MooEntry *entry, gboolean use) { g_return_if_fail (MOO_IS_ENTRY (entry)); if ((entry->priv->special_chars_menu != 0) == (use != 0)) return; entry->priv->special_chars_menu = use != 0; g_object_notify (G_OBJECT (entry), "use-special-chars-menu"); } static void moo_entry_delete_from_cursor (GtkEntry *entry, GtkDeleteType type, gint count) { moo_undo_stack_new_group (MOO_ENTRY(entry)->priv->undo_stack); GTK_ENTRY_CLASS(moo_entry_parent_class)->delete_from_cursor (entry, type, count); } static void moo_entry_cut_clipboard (GtkEntry *entry) { moo_undo_stack_new_group (MOO_ENTRY(entry)->priv->undo_stack); GTK_ENTRY_CLASS(moo_entry_parent_class)->cut_clipboard (entry); } static void moo_entry_paste_clipboard (GtkEntry *entry) { moo_undo_stack_new_group (MOO_ENTRY(entry)->priv->undo_stack); GTK_ENTRY_CLASS(moo_entry_parent_class)->paste_clipboard (entry); } static void moo_entry_do_insert_text (GtkEditable *editable, const gchar *text, gint length, gint *position) { if (length < 0) length = strlen (text); if (*position < 0) *position = GTK_ENTRY(editable)->text_length; if (length > 0) { moo_undo_stack_add_action (MOO_ENTRY(editable)->priv->undo_stack, INSERT_ACTION_TYPE, insert_action_new (editable, text, length, position)); parent_editable_iface->do_insert_text (editable, text, length, position); } } static void moo_entry_do_delete_text (GtkEditable *editable, gint start_pos, gint end_pos) { if (start_pos == end_pos) return; g_return_if_fail (start_pos >= 0); if (end_pos < 0) { end_pos = GTK_ENTRY(editable)->text_length; } else if (start_pos > end_pos) { int tmp = start_pos; start_pos = end_pos; end_pos = tmp; } if (start_pos < end_pos) { moo_undo_stack_add_action (MOO_ENTRY(editable)->priv->undo_stack, DELETE_ACTION_TYPE, delete_action_new (editable, start_pos, end_pos)); parent_editable_iface->do_delete_text (editable, start_pos, end_pos); } } static void moo_entry_delete_to_start (MooEntry *entry) { if (entry->priv->use_ctrl_u) gtk_editable_delete_text (GTK_EDITABLE (entry), 0, gtk_editable_get_position (GTK_EDITABLE (entry))); } /*********************************************************************/ /* Working around gtk selection business * TODO: make stealing primary optional, independent of * clearing selection */ /* GtkEdiatble::delete_text and GtkWidget::realize might also require this hack */ static void moo_entry_set_selection_bounds (GtkEditable *editable, gint start_pos, gint end_pos) { if (!MOO_ENTRY(editable)->priv->grab_selection) MOO_ENTRY(editable)->priv->fool_entry = TRUE; parent_editable_iface->set_selection_bounds (editable, start_pos, end_pos); MOO_ENTRY(editable)->priv->fool_entry = FALSE; } static gboolean moo_entry_get_selection_bounds (GtkEditable *editable, gint *start_pos, gint *end_pos) { if (MOO_ENTRY(editable)->priv->fool_entry) return FALSE; else return parent_editable_iface->get_selection_bounds (editable, start_pos, end_pos); } static gboolean moo_entry_button_release (GtkWidget *widget, GdkEventButton *event) { gboolean result; if (!MOO_ENTRY(widget)->priv->grab_selection) MOO_ENTRY(widget)->priv->fool_entry = TRUE; result = GTK_WIDGET_CLASS(moo_entry_parent_class)->button_release_event (widget, event); MOO_ENTRY(widget)->priv->fool_entry = FALSE; return result; } /*********************************************************************/ /* Undo/redo */ typedef struct { int pos; char *text; int length; int chars; } InsertAction; typedef struct { int start; int end; char *text; gboolean forward; } DeleteAction; static void insert_action_undo (InsertAction *action, GtkEditable *editable); static void insert_action_redo (InsertAction *action, GtkEditable *editable); static gboolean insert_action_merge (InsertAction *action, InsertAction *what); static void insert_action_destroy (InsertAction *action); static void delete_action_undo (DeleteAction *action, GtkEditable *editable); static void delete_action_redo (DeleteAction *action, GtkEditable *editable); static gboolean delete_action_merge (DeleteAction *action, DeleteAction *what); static void delete_action_destroy (DeleteAction *action); static MooUndoActionClass InsertActionClass = { (MooUndoActionUndo) insert_action_undo, (MooUndoActionRedo) insert_action_redo, (MooUndoActionMerge) insert_action_merge, (MooUndoActionDestroy) insert_action_destroy }; static MooUndoActionClass DeleteActionClass = { (MooUndoActionUndo) delete_action_undo, (MooUndoActionRedo) delete_action_redo, (MooUndoActionMerge) delete_action_merge, (MooUndoActionDestroy) delete_action_destroy }; static void init_undo_actions (void) { INSERT_ACTION_TYPE = moo_undo_action_register (&InsertActionClass); DELETE_ACTION_TYPE = moo_undo_action_register (&DeleteActionClass); } static gpointer insert_action_new (G_GNUC_UNUSED GtkEditable *editable, const gchar *text, gint length, gint *position) { InsertAction *action; if (length < 0) length = strlen (text); g_return_val_if_fail (length > 0, NULL); action = g_new0 (InsertAction, 1); action->pos = *position; action->text = g_strndup (text, length); action->length = length; action->chars = g_utf8_strlen (text, length); return action; } static gpointer delete_action_new (GtkEditable *editable, gint start_pos, gint end_pos) { DeleteAction *action; g_return_val_if_fail (start_pos < end_pos, NULL); action = g_new0 (DeleteAction, 1); action->start = start_pos; action->end = end_pos; action->text = gtk_editable_get_chars (editable, start_pos, end_pos); /* figure out if the user used the Delete or the Backspace key */ if (gtk_editable_get_position (editable) <= action->start) action->forward = TRUE; else action->forward = FALSE; return action; } static void insert_action_undo (InsertAction *action, GtkEditable *editable) { gtk_editable_delete_text (editable, action->pos, action->pos + action->chars); gtk_editable_set_position (editable, action->pos); } static void delete_action_undo (DeleteAction *action, GtkEditable *editable) { int pos_here = action->start; gtk_editable_insert_text (editable, action->text, -1, &pos_here); if (action->forward) gtk_editable_set_position (editable, action->start); else gtk_editable_set_position (editable, action->end); } static void insert_action_redo (InsertAction *action, GtkEditable *editable) { int pos_here = action->pos; gtk_editable_insert_text (editable, action->text, action->length, &pos_here); gtk_editable_set_position (editable, action->pos + action->chars); } static void delete_action_redo (DeleteAction *action, GtkEditable *editable) { gtk_editable_delete_text (editable, action->start, action->end); gtk_editable_set_position (editable, action->start); } static void insert_action_destroy (InsertAction *action) { if (action) { g_free (action->text); g_free (action); } } static void delete_action_destroy (DeleteAction *action) { if (action) { g_free (action->text); g_free (action); } } static gboolean insert_action_merge (InsertAction *last_action, InsertAction *action) { char *tmp; if (action->pos != (last_action->pos + last_action->chars) || (action->text[0] != ' ' && action->text[0] != '\t' && (last_action->text[last_action->length-1] == ' ' || last_action->text[last_action->length-1] == '\t'))) { return FALSE; } tmp = g_strconcat (last_action->text, action->text, NULL); g_free (last_action->text); last_action->length += action->length; last_action->text = tmp; last_action->chars += action->chars; return TRUE; } static gboolean delete_action_merge (DeleteAction *last_action, DeleteAction *action) { char *tmp; if (last_action->forward != action->forward || (last_action->start != action->start && last_action->start != action->end)) { return FALSE; } if (last_action->start == action->start) { char *text_end = g_utf8_offset_to_pointer (last_action->text, last_action->end - last_action->start - 1); /* Deleted with the delete key */ if (action->text[0] != ' ' && action->text[0] != '\t' && (*text_end == ' ' || *text_end == '\t')) { return FALSE; } tmp = g_strconcat (last_action->text, action->text, NULL); g_free (last_action->text); last_action->end += (action->end - action->start); last_action->text = tmp; } else { /* Deleted with the backspace key */ if (action->text[0] != ' ' && action->text[0] != '\t' && (last_action->text[0] == ' ' || last_action->text[0] == '\t')) { return FALSE; } tmp = g_strconcat (action->text, last_action->text, NULL); g_free (last_action->text); last_action->start = action->start; last_action->text = tmp; } return TRUE; }