/* * A subclass of GtkAction that wraps a GtkComboBoxEntry. * Features: * Setting GtkEntryBox width in characters. * Passing a function for formatting cells. * Displaying a warning if entry text isn't in list. * Check comma separated values in text against list. (Useful for font-family fallbacks.) * Setting names for GtkComboBoxEntry and GtkEntry (actionName_combobox, actionName_entry) * to allow setting resources. * * Author(s): * Tavmjong Bah * Jon A. Cruz * * Copyright (C) 2010 Authors * * Released under GNU GPL, read the file 'COPYING' for more information */ /* * We must provide for both a toolbar item and a menu item. * As we don't know which widgets are used (or even constructed), * we must keep track of things like active entry ourselves. */ #include #include #include #include "ink-comboboxentry-action.h" // Must handle both tool and menu items! static GtkWidget* create_tool_item( GtkAction* action ); static GtkWidget* create_menu_item( GtkAction* action ); // Internal static gint get_active_row_from_text( Ink_ComboBoxEntry_Action* action, const gchar* target_text ); static gint check_comma_separated_text( Ink_ComboBoxEntry_Action* action ); // Callbacks static void combo_box_changed_cb( GtkComboBoxEntry* widget, gpointer data ); static void entry_activate_cb( GtkEntry* widget, gpointer data ); static gboolean match_selected_cb( GtkEntryCompletion* widget, GtkTreeModel* model, GtkTreeIter* iter, gpointer data ); enum { PROP_MODEL = 1, PROP_COMBOBOX, PROP_ENTRY, PROP_ENTRY_WIDTH, PROP_EXTRA_WIDTH, PROP_CELL_DATA_FUNC, PROP_POPUP }; enum { CHANGED = 0, ACTIVATED, N_SIGNALS }; static guint signals[N_SIGNALS] = {0}; static GtkActionClass *ink_comboboxentry_action_parent_class = NULL; static GQuark gDataName = 0; static void ink_comboboxentry_action_finalize (GObject *object) { // Free any allocated resources. G_OBJECT_CLASS (ink_comboboxentry_action_parent_class)->finalize (object); } static void ink_comboboxentry_action_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { Ink_ComboBoxEntry_Action *action = INK_COMBOBOXENTRY_ACTION (object); switch(property_id) { case PROP_MODEL: action->model = GTK_TREE_MODEL( g_value_get_object( value )); break; case PROP_COMBOBOX: action->combobox = GTK_COMBO_BOX_ENTRY( g_value_get_object( value )); break; case PROP_ENTRY: action->entry = GTK_ENTRY( g_value_get_object( value )); break; case PROP_ENTRY_WIDTH: action->entry_width = g_value_get_int( value ); break; case PROP_EXTRA_WIDTH: action->extra_width = g_value_get_int( value ); break; case PROP_CELL_DATA_FUNC: action->cell_data_func = g_value_get_pointer( value ); break; case PROP_POPUP: action->popup = g_value_get_boolean( value ); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } } static void ink_comboboxentry_action_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { Ink_ComboBoxEntry_Action *action = INK_COMBOBOXENTRY_ACTION (object); switch(property_id) { case PROP_MODEL: g_value_set_object (value, action->model); break; case PROP_COMBOBOX: g_value_set_object (value, action->combobox); break; case PROP_ENTRY: g_value_set_object (value, action->entry); break; case PROP_ENTRY_WIDTH: g_value_set_int (value, action->entry_width); break; case PROP_EXTRA_WIDTH: g_value_set_int (value, action->extra_width); break; case PROP_CELL_DATA_FUNC: g_value_set_pointer (value, action->cell_data_func); break; case PROP_POPUP: g_value_set_boolean (value, action->popup); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } } static void ink_comboboxentry_action_connect_proxy (GtkAction *action, GtkWidget *proxy) { /* Override any proxy properties. */ // if (GTK_IS_MENU_ITEM (proxy)) { // } GTK_ACTION_CLASS (ink_comboboxentry_action_parent_class)->connect_proxy (action, proxy); } static void ink_comboboxentry_action_class_init (Ink_ComboBoxEntry_ActionClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); GtkActionClass *gtkaction_class = GTK_ACTION_CLASS (klass); gtkaction_class->connect_proxy = ink_comboboxentry_action_connect_proxy; gobject_class->finalize = ink_comboboxentry_action_finalize; gobject_class->set_property = ink_comboboxentry_action_set_property; gobject_class->get_property = ink_comboboxentry_action_get_property; gDataName = g_quark_from_string("ink_comboboxentry-action"); klass->parent_class.create_tool_item = create_tool_item; klass->parent_class.create_menu_item = create_menu_item; ink_comboboxentry_action_parent_class = GTK_ACTION_CLASS(g_type_class_peek_parent (klass) ); g_object_class_install_property ( gobject_class, PROP_MODEL, g_param_spec_object ("model", "Tree Model", "Tree Model", GTK_TYPE_TREE_MODEL, (GParamFlags)G_PARAM_READWRITE)); g_object_class_install_property ( gobject_class, PROP_COMBOBOX, g_param_spec_object ("combobox", "GtkComboBoxEntry", "GtkComboBoxEntry", GTK_TYPE_WIDGET, (GParamFlags)G_PARAM_READABLE)); g_object_class_install_property ( gobject_class, PROP_ENTRY, g_param_spec_object ("entry", "GtkEntry", "GtkEntry", GTK_TYPE_WIDGET, (GParamFlags)G_PARAM_READABLE)); g_object_class_install_property ( gobject_class, PROP_ENTRY_WIDTH, g_param_spec_int ("entry_width", "EntryBox width", "EntryBox width (characters)", -1.0, 100, -1.0, (GParamFlags)G_PARAM_READWRITE)); g_object_class_install_property ( gobject_class, PROP_EXTRA_WIDTH, g_param_spec_int ("extra_width", "Extra width", "Extra width (px)", -1.0, 500, -1.0, (GParamFlags)G_PARAM_READWRITE)); g_object_class_install_property ( gobject_class, PROP_CELL_DATA_FUNC, g_param_spec_pointer ("cell_data_func", "Cell Data Func", "Cell Deta Function", (GParamFlags)G_PARAM_READWRITE)); g_object_class_install_property ( gobject_class, PROP_POPUP, g_param_spec_boolean ("popup", "Entry Popup", "Entry Popup", false, (GParamFlags)G_PARAM_READWRITE)); // We need to know when GtkComboBoxEvent or Menu ready for reading signals[CHANGED] = g_signal_new( "changed", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET(Ink_ComboBoxEntry_ActionClass, changed), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); // Probably not needed... originally to keep track of key-presses. signals[ACTIVATED] = g_signal_new( "activated", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET(Ink_ComboBoxEntry_ActionClass, activated), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } static void ink_comboboxentry_action_init (Ink_ComboBoxEntry_Action *action) { action->active = -1; action->text = NULL; action->entry_completion = NULL; action->indicator = NULL; action->popup = false; action->warning = NULL; action->altx_name = NULL; } GType ink_comboboxentry_action_get_type () { static GType ink_comboboxentry_action_type = 0; if (!ink_comboboxentry_action_type) { static const GTypeInfo ink_comboboxentry_action_info = { sizeof(Ink_ComboBoxEntry_ActionClass), NULL, /* base_init */ NULL, /* base_finalize */ (GClassInitFunc) ink_comboboxentry_action_class_init, NULL, /* class_finalize */ NULL, /* class_data */ sizeof(Ink_ComboBoxEntry_Action), 0, /* n_preallocs */ (GInstanceInitFunc)ink_comboboxentry_action_init, /* instance_init */ NULL /* value_table */ }; ink_comboboxentry_action_type = g_type_register_static (GTK_TYPE_ACTION, "Ink_ComboBoxEntry_Action", &ink_comboboxentry_action_info, (GTypeFlags)0 ); } return ink_comboboxentry_action_type; } Ink_ComboBoxEntry_Action *ink_comboboxentry_action_new (const gchar *name, const gchar *label, const gchar *tooltip, const gchar *stock_id, GtkTreeModel *model, gint entry_width, gint extra_width, void *cell_data_func ) { g_return_val_if_fail (name != NULL, NULL); return (Ink_ComboBoxEntry_Action*)g_object_new (INK_COMBOBOXENTRY_TYPE_ACTION, "name", name, "label", label, "tooltip", tooltip, "stock-id", stock_id, "model", model, "entry_width", entry_width, "extra_width", extra_width, "cell_data_func", cell_data_func, NULL); } // Create a widget for a toolbar. GtkWidget* create_tool_item( GtkAction* action ) { GtkWidget* item = 0; if ( INK_COMBOBOXENTRY_IS_ACTION( action ) && INK_COMBOBOXENTRY_ACTION(action)->model ) { Ink_ComboBoxEntry_Action* ink_comboboxentry_action = INK_COMBOBOXENTRY_ACTION( action ); gchar *action_name = g_strdup( gtk_action_get_name( action ) ); gchar *combobox_name = g_strjoin( NULL, action_name, "_combobox", NULL ); gchar *entry_name = g_strjoin( NULL, action_name, "_entry", NULL ); g_free( action_name ); item = GTK_WIDGET( gtk_tool_item_new() ); GtkWidget* comboBoxEntry = gtk_combo_box_entry_new_with_model( ink_comboboxentry_action->model, 0 ); // Name it so we can muck with it using an RC file gtk_widget_set_name( comboBoxEntry, combobox_name ); g_free( combobox_name ); { GtkWidget *align = gtk_alignment_new(0, 0.5, 0, 0); gtk_container_add( GTK_CONTAINER(align), comboBoxEntry ); gtk_container_add( GTK_CONTAINER(item), align ); } ink_comboboxentry_action->combobox = GTK_COMBO_BOX_ENTRY(comboBoxEntry); gtk_combo_box_set_active( GTK_COMBO_BOX( comboBoxEntry ), ink_comboboxentry_action->active ); g_signal_connect( G_OBJECT(comboBoxEntry), "changed", G_CALLBACK(combo_box_changed_cb), action ); // Optionally add formatting... if( ink_comboboxentry_action->cell_data_func != NULL ) { GtkCellRenderer *cell = gtk_cell_renderer_text_new(); gtk_cell_layout_clear( GTK_CELL_LAYOUT( comboBoxEntry ) ); gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( comboBoxEntry ), cell, true ); gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT( comboBoxEntry ), cell, GtkCellLayoutDataFunc (ink_comboboxentry_action->cell_data_func), NULL, NULL ); } // Optionally widen the combobox width... which widens the drop-down list in list mode. if( ink_comboboxentry_action->extra_width > 0 ) { GtkRequisition req; gtk_widget_size_request( GTK_WIDGET( ink_comboboxentry_action->combobox ), &req ); gtk_widget_set_size_request( GTK_WIDGET( ink_comboboxentry_action->combobox ), req.width + ink_comboboxentry_action->extra_width, -1 ); } // Get reference to GtkEntry and fiddle a bit with it. GtkWidget *child = gtk_bin_get_child( GTK_BIN(comboBoxEntry) ); // Name it so we can muck with it using an RC file gtk_widget_set_name( child, entry_name ); g_free( entry_name ); if( child && GTK_IS_ENTRY( child ) ) { ink_comboboxentry_action->entry = GTK_ENTRY(child); // Change width if( ink_comboboxentry_action->entry_width > 0 ) { gtk_entry_set_width_chars (GTK_ENTRY (child), ink_comboboxentry_action->entry_width ); } // Add pop-up entry completion if required if( ink_comboboxentry_action->popup ) { ink_comboboxentry_action_popup_enable( ink_comboboxentry_action ); } // Add altx_name if required if( ink_comboboxentry_action->altx_name ) { g_object_set_data( G_OBJECT( child ), ink_comboboxentry_action->altx_name, ink_comboboxentry_action->entry ); } // Add signal for GtkEntry to check if finished typing. g_signal_connect( G_OBJECT(child), "activate", G_CALLBACK(entry_activate_cb), action ); } gtk_activatable_set_related_action( GTK_ACTIVATABLE (item), GTK_ACTION( action ) ); gtk_widget_show_all( item ); } else { item = ink_comboboxentry_action_parent_class->create_tool_item( action ); } return item; } // Create a drop-down menu. GtkWidget* create_menu_item( GtkAction* action ) { GtkWidget* item = 0; item = ink_comboboxentry_action_parent_class->create_menu_item( action ); g_warning( "ink_comboboxentry_action: create_menu_item not implemented" ); // One can easily modify ege-select-one-action routine to implement this. return item; } // Setters/Getters --------------------------------------------------- GtkTreeModel *ink_comboboxentry_action_get_model( Ink_ComboBoxEntry_Action* action ) { return action->model; } GtkComboBoxEntry *ink_comboboxentry_action_get_comboboxentry( Ink_ComboBoxEntry_Action* action ) { return action->combobox; } gchar* ink_comboboxentry_action_get_active_text( Ink_ComboBoxEntry_Action* action ) { gchar* text = g_strdup( action->text ); return text; } gboolean ink_comboboxentry_action_set_active_text( Ink_ComboBoxEntry_Action* ink_comboboxentry_action, const gchar* text ) { g_free( ink_comboboxentry_action->text ); ink_comboboxentry_action->text = g_strdup( text ); // Get active row or -1 if none ink_comboboxentry_action->active = get_active_row_from_text( ink_comboboxentry_action, ink_comboboxentry_action->text ); // Set active row, check that combobox has been created. if( ink_comboboxentry_action->combobox ) { gtk_combo_box_set_active( GTK_COMBO_BOX( ink_comboboxentry_action->combobox ), ink_comboboxentry_action->active ); } // Fiddle with entry if( ink_comboboxentry_action->entry ) { // Explicitly set text in GtkEntry box (won't be set if text not in list). gtk_entry_set_text( ink_comboboxentry_action->entry, text ); // Show or hide warning if( ink_comboboxentry_action->active == -1 && ink_comboboxentry_action->warning != NULL && check_comma_separated_text( ink_comboboxentry_action ) ) { { GtkStockItem item; gboolean isStock = gtk_stock_lookup( GTK_STOCK_DIALOG_WARNING, &item ); if (isStock) { gtk_entry_set_icon_from_stock( ink_comboboxentry_action->entry, GTK_ENTRY_ICON_SECONDARY, GTK_STOCK_DIALOG_WARNING ); } else { gtk_entry_set_icon_from_icon_name( ink_comboboxentry_action->entry, GTK_ENTRY_ICON_SECONDARY, GTK_STOCK_DIALOG_WARNING ); } } // Can't add tooltip until icon set gtk_entry_set_icon_tooltip_text( ink_comboboxentry_action->entry, GTK_ENTRY_ICON_SECONDARY, ink_comboboxentry_action->warning ); } else { gtk_entry_set_icon_from_icon_name( GTK_ENTRY(ink_comboboxentry_action->entry), GTK_ENTRY_ICON_SECONDARY, NULL ); gtk_entry_set_icon_from_stock( GTK_ENTRY(ink_comboboxentry_action->entry), GTK_ENTRY_ICON_SECONDARY, NULL ); } } // Return if active text in list gboolean found = ( ink_comboboxentry_action->active != -1 ); return found; } void ink_comboboxentry_action_set_entry_width( Ink_ComboBoxEntry_Action* action, gint entry_width ) { action->entry_width = entry_width; // Widget may not have been created.... if( action->entry ) { gtk_entry_set_width_chars( GTK_ENTRY(action->entry), entry_width ); } } void ink_comboboxentry_action_set_extra_width( Ink_ComboBoxEntry_Action* action, gint extra_width ) { action->extra_width = extra_width; // Widget may not have been created.... if( action->combobox ) { GtkRequisition req; gtk_widget_size_request( GTK_WIDGET( action->combobox ), &req ); gtk_widget_set_size_request( GTK_WIDGET( action->combobox ), req.width + action->extra_width, -1 ); } } void ink_comboboxentry_action_popup_enable( Ink_ComboBoxEntry_Action* action ) { action->popup = true; // Widget may not have been created.... if( action->entry ) { // Check we don't already have a GtkEntryCompletion if( action->entry_completion ) return; action->entry_completion = gtk_entry_completion_new(); gtk_entry_set_completion( action->entry, action->entry_completion ); gtk_entry_completion_set_model( action->entry_completion, action->model ); gtk_entry_completion_set_text_column( action->entry_completion, 0 ); gtk_entry_completion_set_popup_completion( action->entry_completion, true ); gtk_entry_completion_set_inline_completion( action->entry_completion, false ); gtk_entry_completion_set_inline_selection( action->entry_completion, true ); g_signal_connect (G_OBJECT (action->entry_completion), "match-selected", G_CALLBACK (match_selected_cb), action ); } } void ink_comboboxentry_action_popup_disable( Ink_ComboBoxEntry_Action* action ) { action->popup = false; if( action->entry_completion ) { gtk_object_destroy( GTK_OBJECT( action->entry_completion ) ); action->entry_completion = 0; } } void ink_comboboxentry_action_set_warning( Ink_ComboBoxEntry_Action* action, const gchar* warning ) { g_free( action->warning ); action->warning = g_strdup( warning ); // Widget may not have been created.... if( action->entry ) { gtk_entry_set_icon_tooltip_text( GTK_ENTRY(action->entry), GTK_ENTRY_ICON_SECONDARY, action->warning ); } } void ink_comboboxentry_action_set_altx_name( Ink_ComboBoxEntry_Action* action, const gchar* altx_name ) { g_free( action->altx_name ); action->altx_name = g_strdup( altx_name ); // Widget may not have been created.... if( action->entry ) { g_object_set_data( G_OBJECT(action->entry), action->altx_name, action->entry ); } } // Internal --------------------------------------------------- // Return row of active text or -1 if not found. gint get_active_row_from_text( Ink_ComboBoxEntry_Action* action, const gchar* target_text ) { // Check if text in list gint row = 0; gboolean found = false; GtkTreeIter iter; gboolean valid = gtk_tree_model_get_iter_first( action->model, &iter ); while ( valid ) { // Get text from list entry gchar* text = 0; gtk_tree_model_get( action->model, &iter, 0, &text, -1 ); // Column 0 // Check for match if( strcmp( target_text, text ) == 0 ){ found = true; break; } ++row; valid = gtk_tree_model_iter_next( action->model, &iter ); } if( !found ) row = -1; return row; } // Checks if all comma separated text fragments are in the list. // This is useful for checking if all fonts in a font-family fallback // list are available on the system. // The return value is set to the number of missing text fragments. // This routine could also create a Pango Markup string to show which // fragments are invalid. // It is envisioned that one can construct a Pango Markup String here // so that individual text fragments can be flagged as not being in the // list. static gint check_comma_separated_text( Ink_ComboBoxEntry_Action* action ) { gint ret_val = 0; // Parse fallback_list using a comma as deliminator gchar** tokens = g_strsplit( action->text, ",", 0 ); gint i = 0; while( tokens[i] != NULL ) { // Remove any surrounding white space. g_strstrip( tokens[i] ); if( get_active_row_from_text( action, tokens[i] ) == -1 ) { ret_val += 1; } ++i; } g_strfreev( tokens ); // Pango Markup notes: // GString* Pango_Markup = g_string_new(""); // if not present: // g_string_sprintfa( Pango_Markup, "%s", tokens[i] ); // PangoLayout * pl = gtk_entry_get_layout( entry ); // pango_layout_set_markup( pl, Pango_Markup->str, -1 ); // g_string_free( Pango_Markup, TRUE ); return ret_val; } // Callbacks --------------------------------------------------- static void combo_box_changed_cb( GtkComboBoxEntry* widget, gpointer data ) { // Two things can happen to get here: // An item is selected in the drop-down menu. // Text is typed. // We only react here if an item is selected. // Get action Ink_ComboBoxEntry_Action *act = INK_COMBOBOXENTRY_ACTION( data ); // Check if item selected: gint newActive = gtk_combo_box_get_active( GTK_COMBO_BOX( widget )); if( newActive >= 0 ) { if( newActive != act->active ) { act->active = newActive; g_free( act->text ); act->text = gtk_combo_box_get_active_text( GTK_COMBO_BOX( widget )); // Now let the world know g_signal_emit( G_OBJECT(act), signals[CHANGED], 0 ); } } } static void entry_activate_cb( GtkEntry* widget, gpointer data ) { // Get text from entry box.. check if it matches a menu entry. // Get action Ink_ComboBoxEntry_Action *ink_comboboxentry_action = INK_COMBOBOXENTRY_ACTION( data ); // Get text g_free( ink_comboboxentry_action->text ); ink_comboboxentry_action->text = g_strdup( gtk_entry_get_text( widget ) ); // Get row ink_comboboxentry_action->active = get_active_row_from_text( ink_comboboxentry_action, ink_comboboxentry_action->text ); // Set active row gtk_combo_box_set_active( GTK_COMBO_BOX( ink_comboboxentry_action->combobox), ink_comboboxentry_action->active ); // Now let the world know g_signal_emit( G_OBJECT(ink_comboboxentry_action), signals[CHANGED], 0 ); } static gboolean match_selected_cb( GtkEntryCompletion* /*widget*/, GtkTreeModel* model, GtkTreeIter* iter, gpointer data ) { // Get action Ink_ComboBoxEntry_Action *ink_comboboxentry_action = INK_COMBOBOXENTRY_ACTION( data ); GtkEntry *entry = ink_comboboxentry_action->entry; if( entry) { gchar *family = 0; gtk_tree_model_get(model, iter, 0, &family, -1); // Set text in GtkEntry gtk_entry_set_text (GTK_ENTRY (entry), family ); // Set text in GtkAction g_free( ink_comboboxentry_action->text ); ink_comboboxentry_action->text = family; // Get row ink_comboboxentry_action->active = get_active_row_from_text( ink_comboboxentry_action, ink_comboboxentry_action->text ); // Set active row gtk_combo_box_set_active( GTK_COMBO_BOX( ink_comboboxentry_action->combobox), ink_comboboxentry_action->active ); // Now let the world know g_signal_emit( G_OBJECT(ink_comboboxentry_action), signals[CHANGED], 0 ); return true; } return false; }