/* * mooentry.c * * Copyright (C) 2004-2005 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 MOO_MARSHALS_H #include "mooutils/mooentry.h" #include "mooutils/mooutils-gobject.h" #include #include #include #include #include #include struct _MooEntryPrivate { MooUndoMgr *undo_mgr; gboolean enable_undo; gboolean enable_undo_menu; guint use_ctrl_u : 1; guint grab_selection : 1; guint fool_entry : 1; guint empty : 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 (!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_UNDO_MANAGER, PROP_ENABLE_UNDO, PROP_ENABLE_UNDO_MENU, PROP_GRAB_SELECTION, PROP_EMPTY }; 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_UNDO_MANAGER, g_param_spec_object ("undo-manager", "undo-manager", "undo-manager", MOO_TYPE_UNDO_MGR, G_PARAM_READABLE)); 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", FALSE, 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)); 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_mgr = moo_undo_mgr_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; 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_UNDO_MANAGER: g_value_set_object (value, entry->priv->undo_mgr); 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; 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_mgr); 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_mgr_can_undo (entry->priv->undo_mgr)) moo_undo_mgr_undo (entry->priv->undo_mgr); } void moo_entry_redo (MooEntry *entry) { g_return_if_fail (MOO_IS_ENTRY (entry)); if (entry->priv->enable_undo && moo_undo_mgr_can_redo (entry->priv->undo_mgr)) moo_undo_mgr_redo (entry->priv->undo_mgr); } void moo_entry_begin_undo_group (MooEntry *entry) { g_return_if_fail (MOO_IS_ENTRY (entry)); moo_undo_mgr_start_group (entry->priv->undo_mgr); } void moo_entry_end_undo_group (MooEntry *entry) { g_return_if_fail (MOO_IS_ENTRY (entry)); moo_undo_mgr_end_group (entry->priv->undo_mgr); } void moo_entry_clear_undo (MooEntry *entry) { g_return_if_fail (MOO_IS_ENTRY (entry)); moo_undo_mgr_clear (entry->priv->undo_mgr); } MooUndoMgr* moo_entry_get_undo_mgr (MooEntry *entry) { g_return_val_if_fail (MOO_IS_ENTRY (entry), NULL); return entry->priv->undo_mgr; } GtkWidget* moo_entry_new (void) { return g_object_new (MOO_TYPE_ENTRY, NULL); } static void moo_entry_populate_popup (GtkEntry *gtkentry, GtkMenu *menu) { GtkWidget *item; MooEntry *entry = MOO_ENTRY (gtkentry); if (!entry->priv->enable_undo_menu) return; 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_mgr_can_redo (entry->priv->undo_mgr)); 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_mgr_can_undo (entry->priv->undo_mgr)); g_signal_connect_swapped (item, "activate", G_CALLBACK (moo_entry_undo), entry); } static void moo_entry_delete_from_cursor (GtkEntry *entry, GtkDeleteType type, gint count) { moo_undo_mgr_new_group (MOO_ENTRY(entry)->priv->undo_mgr); GTK_ENTRY_CLASS(moo_entry_parent_class)->delete_from_cursor (entry, type, count); } static void moo_entry_cut_clipboard (GtkEntry *entry) { moo_undo_mgr_new_group (MOO_ENTRY(entry)->priv->undo_mgr); GTK_ENTRY_CLASS(moo_entry_parent_class)->cut_clipboard (entry); } static void moo_entry_paste_clipboard (GtkEntry *entry) { moo_undo_mgr_new_group (MOO_ENTRY(entry)->priv->undo_mgr); 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_mgr_add_action (MOO_ENTRY(editable)->priv->undo_mgr, 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_mgr_add_action (MOO_ENTRY(editable)->priv->undo_mgr, 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 idiotic 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; }