Code

Updated GtkAction subclass to allow "open" selections for a user to type into.
authorJon A. Cruz <jon@joncruz.org>
Wed, 14 Apr 2010 07:06:56 +0000 (00:06 -0700)
committerJon A. Cruz <jon@joncruz.org>
Wed, 14 Apr 2010 07:06:56 +0000 (00:06 -0700)
src/ege-select-one-action.cpp
src/ege-select-one-action.h

index 834b06b3b035b4f876e1fa0f6f0cd9faccb7f5fb..587555c8bc7a5d3617a41f32ca85f8981243a11d 100644 (file)
@@ -18,7 +18,7 @@
  *
  * The Initial Developer of the Original Code is
  * Jon A. Cruz.
- * Portions created by the Initial Developer are Copyright (C) 2007
+ * Portions created by the Initial Developer are Copyright (C) 2010
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
@@ -64,7 +64,11 @@ static void ege_select_one_action_init( EgeSelectOneAction* action );
 static void ege_select_one_action_get_property( GObject* obj, guint propId, GValue* value, GParamSpec * pspec );
 static void ege_select_one_action_set_property( GObject* obj, guint propId, const GValue *value, GParamSpec* pspec );
 
-static void resync_active( EgeSelectOneAction* act, gint active );
+static gint find_text_index(EgeSelectOneAction *act, gchar const* text);
+static void commit_pending_change(EgeSelectOneAction *act);
+static void resync_active( EgeSelectOneAction* act, gint active, gboolean override );
+static void combo_entry_changed_cb( GtkEntry* widget, gpointer user_data );
+static gboolean combo_entry_focus_lost_cb( GtkWidget *widget, GdkEventFocus *event, gpointer data );
 static void combo_changed_cb( GtkComboBox* widget, gpointer user_data );
 static void menu_toggled_cb( GtkWidget* obj, gpointer data );
 static void proxy_action_chagned_cb( GtkRadioAction* action, GtkRadioAction* current, gpointer user_data );
@@ -87,6 +91,12 @@ enum {
     APPEARANCE_MINIMAL, // no label, just choices in a drop-down menu
 };
 
+enum {
+    SELECTION_UNKNOWN = -1,
+    SELECTION_CLOSED = 0,
+    SELECTION_OPEN,
+};
+
 struct _EgeSelectOneActionPrivate
 {
     gint active;
@@ -94,11 +104,15 @@ struct _EgeSelectOneActionPrivate
     gint iconColumn;
     gint tooltipColumn;
     gint appearanceMode;
+    gint selectionMode;
     gint iconSize;
     GType radioActionType;
     GtkTreeModel* model;
-    gchar* iconProperty;
-    gchar* appearance;
+    gchar *iconProperty;
+    gchar *appearance;
+    gchar *selection;
+    gchar *activeText;
+    gchar *pendingText;
 };
 
 #define EGE_SELECT_ONE_ACTION_GET_PRIVATE( o ) ( G_TYPE_INSTANCE_GET_PRIVATE( (o), EGE_SELECT_ONE_ACTION_TYPE, EgeSelectOneActionPrivate ) )
@@ -111,7 +125,8 @@ enum {
     PROP_TOOLTIP_COLUMN,
     PROP_ICON_PROP,
     PROP_ICON_SIZE,
-    PROP_APPEARANCE
+    PROP_APPEARANCE,
+    PROP_SELECTION
 };
 
 GType ege_select_one_action_get_type( void )
@@ -169,7 +184,7 @@ void ege_select_one_action_class_init( EgeSelectOneActionClass* klass )
                                          g_param_spec_int( "active",
                                                            "Active Selection",
                                                            "The index of the selected item",
-                                                           0, 20, 0,
+                                                           -1, 20, 0,
                                                            (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT) ) );
 
         g_object_class_install_property( objClass,
@@ -220,6 +235,14 @@ void ege_select_one_action_class_init( EgeSelectOneActionClass* klass )
                                                               "",
                                                               (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT) ) );
 
+        g_object_class_install_property( objClass,
+                                         PROP_SELECTION,
+                                         g_param_spec_string( "selection",
+                                                              "Selection set open or closed",
+                                                              "'open' to allow edits/additions, 'closed' to disallow.",
+                                                              "",
+                                                              (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT) ) );
+
         signals[CHANGED] = g_signal_new( "changed",
                                          G_TYPE_FROM_CLASS(klass),
                                          G_SIGNAL_RUN_FIRST,
@@ -241,11 +264,15 @@ void ege_select_one_action_init( EgeSelectOneAction* action )
     action->private_data->iconColumn = -1;
     action->private_data->tooltipColumn = -1;
     action->private_data->appearanceMode = APPEARANCE_NONE;
+    action->private_data->selectionMode = SELECTION_CLOSED;
     action->private_data->radioActionType = 0;
     action->private_data->model = 0;
     action->private_data->iconProperty = g_strdup("stock-id");
     action->private_data->iconSize = -1;
     action->private_data->appearance = 0;
+    action->private_data->selection = 0;
+    action->private_data->activeText = 0;
+    action->private_data->pendingText = 0;
 
 /*     g_signal_connect( action, "notify", G_CALLBACK( fixup_labels ), NULL ); */
 }
@@ -284,10 +311,14 @@ gchar *ege_select_one_action_get_active_text( EgeSelectOneAction* action )
     gchar *str = 0;
     g_return_val_if_fail( IS_EGE_SELECT_ONE_ACTION(action), 0 );
 
-    if ( gtk_tree_model_iter_nth_child( action->private_data->model, &iter, NULL, action->private_data->active ) ) {
-        gtk_tree_model_get( action->private_data->model, &iter,
-                            action->private_data->labelColumn, &str,
-                            -1 );
+    if ( action->private_data->active >= 0) {
+        if ( gtk_tree_model_iter_nth_child( action->private_data->model, &iter, NULL, action->private_data->active ) ) {
+            gtk_tree_model_get( action->private_data->model, &iter,
+                                action->private_data->labelColumn, &str,
+                                -1 );
+        }
+    } else if ( (action->private_data->active == -1) && action->private_data->activeText ) {
+        str = g_strdup(action->private_data->activeText);
     }
 
     return str;
@@ -347,6 +378,11 @@ void ege_select_one_action_set_appearance( EgeSelectOneAction* action, gchar con
     g_object_set( G_OBJECT(action), "appearance", val, NULL );
 }
 
+void ege_select_one_action_set_selection( EgeSelectOneAction* action, gchar const* val )
+{
+    g_object_set( G_OBJECT(action), "selection", val, NULL );
+}
+
 void ege_select_one_action_get_property( GObject* obj, guint propId, GValue* value, GParamSpec * pspec )
 {
     EgeSelectOneAction* action = EGE_SELECT_ONE_ACTION( obj );
@@ -383,6 +419,10 @@ void ege_select_one_action_get_property( GObject* obj, guint propId, GValue* val
             g_value_set_string( value, action->private_data->appearance );
             break;
 
+        case PROP_SELECTION:
+            g_value_set_string( value, action->private_data->selection );
+            break;
+
         default:
             G_OBJECT_WARN_INVALID_PROPERTY_ID( obj, propId, pspec );
     }
@@ -400,7 +440,7 @@ void ege_select_one_action_set_property( GObject* obj, guint propId, const GValu
 
         case PROP_ACTIVE:
         {
-            resync_active( action, g_value_get_int( value ) );
+            resync_active( action, g_value_get_int( value ), FALSE );
         }
         break;
 
@@ -458,6 +498,23 @@ void ege_select_one_action_set_property( GObject* obj, guint propId, const GValu
         }
         break;
 
+        case PROP_SELECTION:
+        {
+            gchar* tmp = action->private_data->selection;
+            gchar* newVal = g_value_dup_string( value );
+            action->private_data->selection = newVal;
+            g_free( tmp );
+
+            if ( !action->private_data->selection || (strcmp("closed", newVal) == 0) ) {
+                action->private_data->selectionMode = SELECTION_CLOSED;
+            } else if ( strcmp("open", newVal) == 0 ) {
+                action->private_data->selectionMode = SELECTION_OPEN;
+            } else {
+                action->private_data->selectionMode = SELECTION_UNKNOWN;
+            }
+        }
+        break;
+
         default:
             G_OBJECT_WARN_INVALID_PROPERTY_ID( obj, propId, pspec );
     }
@@ -495,6 +552,7 @@ GtkWidget* create_menu_item( GtkAction* action )
             gtk_check_menu_item_set_active( GTK_CHECK_MENU_ITEM(item), index == act->private_data->active );
 
             g_free(str);
+            str = 0;
 
             g_signal_connect( G_OBJECT(item), "toggled", G_CALLBACK(menu_toggled_cb), GINT_TO_POINTER(index) );
 
@@ -629,23 +687,32 @@ GtkWidget* create_tool_item( GtkAction* action )
 
             gtk_container_add( GTK_CONTAINER(item), holder );
         } else {
+            GtkCellRenderer * renderer = 0;
             GtkWidget* holder = gtk_hbox_new( FALSE, 4 );
-            GtkWidget* normal = gtk_combo_box_new_with_model( act->private_data->model );
+            GtkWidget* normal = (act->private_data->selectionMode == SELECTION_OPEN) ?
+                gtk_combo_box_entry_new_with_model( act->private_data->model, act->private_data->labelColumn ) :
+                gtk_combo_box_new_with_model( act->private_data->model );
+            if ((act->private_data->selectionMode == SELECTION_OPEN)) {
+                GtkWidget *child = gtk_bin_get_child( GTK_BIN(normal) );
+                if (GTK_IS_ENTRY(child)) {
+                    gtk_entry_set_width_chars(GTK_ENTRY(child), 4);
+                    g_signal_connect( G_OBJECT(child), "activate", G_CALLBACK(combo_entry_changed_cb), act );
+                    g_signal_connect( G_OBJECT(child), "focus-out-event", G_CALLBACK(combo_entry_focus_lost_cb), act );
+                }
+            } else {
+                if ( act->private_data->iconColumn >= 0 ) {
+                    renderer = gtk_cell_renderer_pixbuf_new();
+                    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT(normal), renderer, TRUE );
 
-            GtkCellRenderer * renderer = 0;
+                    // "icon-name"
+                    gtk_cell_layout_add_attribute( GTK_CELL_LAYOUT(normal), renderer, "stock-id", act->private_data->iconColumn );
+                }
 
-            if ( act->private_data->iconColumn >= 0 ) {
-                renderer = gtk_cell_renderer_pixbuf_new();
+                renderer = gtk_cell_renderer_text_new();
                 gtk_cell_layout_pack_start( GTK_CELL_LAYOUT(normal), renderer, TRUE );
-
-                // "icon-name"
-                gtk_cell_layout_add_attribute( GTK_CELL_LAYOUT(normal), renderer, "stock-id", act->private_data->iconColumn );
+                gtk_cell_layout_add_attribute( GTK_CELL_LAYOUT(normal), renderer, "text", act->private_data->labelColumn );
             }
 
-            renderer = gtk_cell_renderer_text_new();
-            gtk_cell_layout_pack_start( GTK_CELL_LAYOUT(normal), renderer, TRUE );
-            gtk_cell_layout_add_attribute( GTK_CELL_LAYOUT(normal), renderer, "text", act->private_data->labelColumn );
-
             gtk_combo_box_set_active( GTK_COMBO_BOX(normal), act->private_data->active );
 
             g_signal_connect( G_OBJECT(normal), "changed", G_CALLBACK(combo_changed_cb), action );
@@ -664,7 +731,11 @@ GtkWidget* create_tool_item( GtkAction* action )
 
             gtk_box_pack_start( GTK_BOX(holder), normal, FALSE, FALSE, 0 );
 
-            gtk_container_add( GTK_CONTAINER(item), holder );
+            {
+                GtkWidget *align = gtk_alignment_new(0, 0.5, 0, 0);
+                gtk_container_add( GTK_CONTAINER(align), holder);
+                gtk_container_add( GTK_CONTAINER(item), align );
+            }
         }
 
         gtk_widget_show_all( item );
@@ -687,9 +758,9 @@ void disconnect_proxy( GtkAction *action, GtkWidget *proxy )
 }
 
 
-void resync_active( EgeSelectOneAction* act, gint active )
+void resync_active( EgeSelectOneAction* act, gint active, gboolean override )
 {
-    if ( act->private_data->active != active ) {
+    if ( override || (act->private_data->active != active) ) {
         act->private_data->active = active;
         GSList* proxies = gtk_action_get_proxies( GTK_ACTION(act) );
         while ( proxies ) {
@@ -698,9 +769,15 @@ void resync_active( EgeSelectOneAction* act, gint active )
                 GList* children = gtk_container_get_children( GTK_CONTAINER(proxies->data) );
                 if ( children && children->data ) {
                     gpointer combodata = g_object_get_data( G_OBJECT(children->data), "ege-combo-box" );
+                    if (!combodata && GTK_IS_ALIGNMENT(children->data)) {
+                        GList *other = gtk_container_get_children( GTK_CONTAINER(children->data) );
+                         combodata = g_object_get_data( G_OBJECT(other->data), "ege-combo-box" );
+                    }
                     if ( GTK_IS_COMBO_BOX(combodata) ) {
                         GtkComboBox* combo = GTK_COMBO_BOX(combodata);
-                        if ( gtk_combo_box_get_active(combo) != active ) {
+                        if ((active == -1) && (GTK_IS_COMBO_BOX_ENTRY(combo))) {
+                            gtk_entry_set_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(combo))), act->private_data->activeText);
+                        } else if ( gtk_combo_box_get_active(combo) != active ) {
                             gtk_combo_box_set_active( combo, active );
                         }
                     } else if ( GTK_IS_HBOX(children->data) ) {
@@ -747,9 +824,107 @@ void combo_changed_cb( GtkComboBox* widget, gpointer user_data )
 {
     EgeSelectOneAction* act = EGE_SELECT_ONE_ACTION(user_data);
     gint newActive = gtk_combo_box_get_active(widget);
-    if (newActive != act->private_data->active && newActive != -1) {
+    gchar *text = gtk_combo_box_get_active_text(widget);
+
+    if (newActive == -1) {
+        /* indicates the user is entering text for a custom aka "open" value */
+        if (act->private_data->pendingText && text && (strcmp(act->private_data->pendingText, text) == 0) ) {
+            /* The currently entered data matches the last seen */
+        } else {
+            if (act->private_data->pendingText) {
+                g_free(act->private_data->pendingText);
+            }
+            act->private_data->pendingText = text;
+            text = 0;
+        }
+    } else if (newActive != act->private_data->active) {
+        if (act->private_data->pendingText) {
+            g_free(act->private_data->pendingText);
+            act->private_data->pendingText = 0;
+        }
         g_object_set( G_OBJECT(act), "active", newActive, NULL );
     }
+
+    if (text) {
+        g_free(text);
+        text = 0;
+    }
+}
+
+gboolean combo_entry_focus_lost_cb( GtkWidget *widget, GdkEventFocus *event, gpointer data )
+{
+    EgeSelectOneAction* act = EGE_SELECT_ONE_ACTION(data);
+    (void)widget;
+    (void)event;
+
+    commit_pending_change(act);
+
+    return FALSE;
+}
+
+void combo_entry_changed_cb( GtkEntry* widget, gpointer user_data )
+{
+    EgeSelectOneAction* act = EGE_SELECT_ONE_ACTION(user_data);
+    (void)widget;
+    commit_pending_change(act);
+}
+
+void commit_pending_change(EgeSelectOneAction *act)
+{
+    if (act->private_data->pendingText) {
+        if (act->private_data->activeText && (strcmp(act->private_data->pendingText, act->private_data->activeText) == 0)) {
+            /* Was the same value */
+            g_free(act->private_data->pendingText);
+            act->private_data->pendingText = 0;
+        } else {
+            gint matching = find_text_index(act, act->private_data->pendingText);
+
+            if (act->private_data->activeText) {
+                g_free(act->private_data->activeText);
+            }
+            act->private_data->activeText = act->private_data->pendingText;
+            act->private_data->pendingText = 0;
+
+            if (matching >= 0) {
+                g_free(act->private_data->activeText);
+                act->private_data->activeText = 0;
+                g_object_set( G_OBJECT(act), "active", matching, NULL );
+            } else if (act->private_data->active != -1) {
+                g_object_set( G_OBJECT(act), "active", -1, NULL );
+            } else {
+                resync_active( act, -1, TRUE );
+            }
+        }
+    }
+}
+
+gint find_text_index(EgeSelectOneAction *act, gchar const* text)
+{
+    gint index = -1;
+
+    if (text) {
+        GtkTreeIter iter;
+        gboolean valid = gtk_tree_model_get_iter_first( act->private_data->model, &iter );
+        gint curr = 0;
+        while ( valid && (index < 0) ) {
+            gchar* str = 0;
+            gtk_tree_model_get( act->private_data->model, &iter,
+                                act->private_data->labelColumn, &str,
+                                -1 );
+
+            if (str && (strcmp(text, str) == 0)) {
+                index = curr;
+            }
+
+            g_free(str);
+            str = 0;
+
+            curr++;
+            valid = gtk_tree_model_iter_next( act->private_data->model, &iter );
+        }
+    }
+
+    return index;
 }
 
 void menu_toggled_cb( GtkWidget* obj, gpointer data )
index 2ecf7efc52e2de79288b27a6bbf78edebd3f7cf9..16f924370d99d0d423d83e441a24ab36a1c23576 100644 (file)
@@ -20,7 +20,7 @@
  *
  * The Initial Developer of the Original Code is
  * Jon A. Cruz.
- * Portions created by the Initial Developer are Copyright (C) 2007
+ * Portions created by the Initial Developer are Copyright (C) 2010
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
@@ -199,6 +199,15 @@ void ege_select_one_action_set_tooltip_column( EgeSelectOneAction* action, gint
  */
 void ege_select_one_action_set_appearance( EgeSelectOneAction* action, gchar const* val );
 
+/**
+ * Sets to allow or disallow free entry additions to the list.
+ * The default is "closed" selections that do not allow additions/edits.
+ * This is the XForms functional 'selection' attribute: "open", "closed".
+ *
+ * @param action The action to set the tooltip column for.
+ * @param val The value of the selection attribute.
+ */
+void ege_select_one_action_set_selection( EgeSelectOneAction *action, gchar const* val );
 
 /* bit of a work-around */
 void ege_select_one_action_set_radio_action_type( EgeSelectOneAction* action, GType radioActionType );