/* Lists/Selections * * The GtkDropDown widget is a modern alternative to GtkComboBox. * It uses list models instead of tree models, and the content is * displayed using widgets instead of cell renderers. * * This example also shows a custom widget that can replace * GtkEntryCompletion or GtkComboBoxText. It is not currently * part of GTK. */ #include #include "suggestionentry.h" #define STRING_TYPE_HOLDER (string_holder_get_type ()) G_DECLARE_FINAL_TYPE (StringHolder, string_holder, STRING, HOLDER, GObject) struct _StringHolder { GObject parent_instance; char *title; char *icon; char *description; }; G_DEFINE_TYPE (StringHolder, string_holder, G_TYPE_OBJECT); static void string_holder_init (StringHolder *holder) { } static void string_holder_finalize (GObject *object) { StringHolder *holder = STRING_HOLDER (object); g_free (holder->title); g_free (holder->icon); g_free (holder->description); G_OBJECT_CLASS (string_holder_parent_class)->finalize (object); } static void string_holder_class_init (StringHolderClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); object_class->finalize = string_holder_finalize; } static StringHolder * string_holder_new (const char *title, const char *icon, const char *description) { StringHolder *holder = g_object_new (STRING_TYPE_HOLDER, NULL); holder->title = g_strdup (title); holder->icon = g_strdup (icon); holder->description = g_strdup (description); return holder; } static void strings_setup_item_single_line (GtkSignalListItemFactory *factory, GtkListItem *item) { GtkWidget *box, *image, *title; GtkWidget *checkmark; box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10); image = gtk_image_new (); title = gtk_label_new (""); gtk_label_set_xalign (GTK_LABEL (title), 0.0); checkmark = gtk_image_new_from_icon_name ("object-select-symbolic"); gtk_box_append (GTK_BOX (box), image); gtk_box_append (GTK_BOX (box), title); gtk_box_append (GTK_BOX (box), checkmark); g_object_set_data (G_OBJECT (item), "title", title); g_object_set_data (G_OBJECT (item), "image", image); g_object_set_data (G_OBJECT (item), "checkmark", checkmark); gtk_list_item_set_child (item, box); } static void strings_setup_item_full (GtkSignalListItemFactory *factory, GtkListItem *item) { GtkWidget *box, *box2, *image, *title, *description; GtkWidget *checkmark; image = gtk_image_new (); title = gtk_label_new (""); gtk_label_set_xalign (GTK_LABEL (title), 0.0); description = gtk_label_new (""); gtk_label_set_xalign (GTK_LABEL (description), 0.0); gtk_widget_add_css_class (description, "dim-label"); checkmark = gtk_image_new_from_icon_name ("object-select-symbolic"); box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10); box2 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2); gtk_box_append (GTK_BOX (box), image); gtk_box_append (GTK_BOX (box), box2); gtk_box_append (GTK_BOX (box2), title); gtk_box_append (GTK_BOX (box2), description); gtk_box_append (GTK_BOX (box), checkmark); g_object_set_data (G_OBJECT (item), "title", title); g_object_set_data (G_OBJECT (item), "image", image); g_object_set_data (G_OBJECT (item), "description", description); g_object_set_data (G_OBJECT (item), "checkmark", checkmark); gtk_list_item_set_child (item, box); } static void selected_item_changed (GtkDropDown *dropdown, GParamSpec *pspec, GtkListItem *item) { GtkWidget *checkmark; checkmark = g_object_get_data (G_OBJECT (item), "checkmark"); if (gtk_drop_down_get_selected_item (dropdown) == gtk_list_item_get_item (item)) gtk_widget_set_opacity (checkmark, 1.0); else gtk_widget_set_opacity (checkmark, 0.0); } static void strings_bind_item (GtkSignalListItemFactory *factory, GtkListItem *item, gpointer data) { GtkDropDown *dropdown = data; GtkWidget *image, *title, *description; GtkWidget *checkmark; StringHolder *holder; GtkWidget *popup; holder = gtk_list_item_get_item (item); title = g_object_get_data (G_OBJECT (item), "title"); image = g_object_get_data (G_OBJECT (item), "image"); description = g_object_get_data (G_OBJECT (item), "description"); checkmark = g_object_get_data (G_OBJECT (item), "checkmark"); gtk_label_set_label (GTK_LABEL (title), holder->title); if (image) { gtk_image_set_from_icon_name (GTK_IMAGE (image), holder->icon); gtk_widget_set_visible (image, holder->icon != NULL); } if (description) { gtk_label_set_label (GTK_LABEL (description), holder->description); gtk_widget_set_visible (description , holder->description != NULL); } popup = gtk_widget_get_ancestor (title, GTK_TYPE_POPOVER); if (popup && gtk_widget_is_ancestor (popup, GTK_WIDGET (dropdown))) { gtk_widget_show (checkmark); g_signal_connect (dropdown, "notify::selected-item", G_CALLBACK (selected_item_changed), item); selected_item_changed (dropdown, NULL, item); } else { gtk_widget_hide (checkmark); } } static void strings_unbind_item (GtkSignalListItemFactory *factory, GtkListItem *list_item, gpointer data) { GtkDropDown *dropdown = data; g_signal_handlers_disconnect_by_func (dropdown, selected_item_changed, list_item); } static GtkListItemFactory * strings_factory_new (gpointer data, gboolean full) { GtkListItemFactory *factory; factory = gtk_signal_list_item_factory_new (); if (full) g_signal_connect (factory, "setup", G_CALLBACK (strings_setup_item_full), data); else g_signal_connect (factory, "setup", G_CALLBACK (strings_setup_item_single_line), data); g_signal_connect (factory, "bind", G_CALLBACK (strings_bind_item), data); g_signal_connect (factory, "unbind", G_CALLBACK (strings_unbind_item), data); return factory; } static GListModel * strings_model_new (const char *const *titles, const char *const *icons, const char *const *descriptions) { GListStore *store; int i; store = g_list_store_new (STRING_TYPE_HOLDER); for (i = 0; titles[i]; i++) { StringHolder *holder = string_holder_new (titles[i], icons ? icons[i] : NULL, descriptions ? descriptions[i] : NULL); g_list_store_append (store, holder); g_object_unref (holder); } return G_LIST_MODEL (store); } static GtkWidget * drop_down_new_from_strings (const char *const *titles, const char *const *icons, const char *const *descriptions) { GtkWidget *widget; GListModel *model; GtkListItemFactory *factory; GtkListItemFactory *list_factory; g_return_val_if_fail (titles != NULL, NULL); g_return_val_if_fail (icons == NULL || g_strv_length ((char **)icons) == g_strv_length ((char **)titles), NULL); g_return_val_if_fail (descriptions == NULL || g_strv_length ((char **)icons) == g_strv_length ((char **)descriptions), NULL); model = strings_model_new (titles, icons, descriptions); widget = g_object_new (GTK_TYPE_DROP_DOWN, "model", model, NULL); g_object_unref (model); factory = strings_factory_new (widget, FALSE); if (icons != NULL || descriptions != NULL) list_factory = strings_factory_new (widget, TRUE); else list_factory = NULL; g_object_set (widget, "factory", factory, "list-factory", list_factory, NULL); g_object_unref (factory); if (list_factory) g_object_unref (list_factory); return widget; } static char * get_family_name (gpointer item) { return g_strdup (pango_font_family_get_name (PANGO_FONT_FAMILY (item))); } static char * get_title (gpointer item) { return g_strdup (STRING_HOLDER (item)->title); } static char * get_file_name (gpointer item) { return g_strdup (g_file_info_get_display_name (G_FILE_INFO (item))); } static void setup_item (GtkSignalListItemFactory *factory, GtkListItem *item) { GtkWidget *box; GtkWidget *icon; GtkWidget *label; box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10); icon = gtk_image_new (); label = gtk_label_new (""); gtk_label_set_xalign (GTK_LABEL (label), 0); gtk_box_append (GTK_BOX (box), icon); gtk_box_append (GTK_BOX (box), label); gtk_list_item_set_child (item, box); } static void bind_item (GtkSignalListItemFactory *factory, GtkListItem *item) { MatchObject *match = MATCH_OBJECT (gtk_list_item_get_item (item)); GFileInfo *info = G_FILE_INFO (match_object_get_item (match)); GtkWidget *box = gtk_list_item_get_child (item); GtkWidget *icon = gtk_widget_get_first_child (box); GtkWidget *label = gtk_widget_get_last_child (box); gtk_image_set_from_gicon (GTK_IMAGE (icon), g_file_info_get_icon (info)); gtk_label_set_label (GTK_LABEL (label), g_file_info_get_display_name (info)); } static void setup_highlight_item (GtkSignalListItemFactory *factory, GtkListItem *item) { GtkWidget *label; label = gtk_label_new (""); gtk_label_set_xalign (GTK_LABEL (label), 0); gtk_list_item_set_child (item, label); } static void bind_highlight_item (GtkSignalListItemFactory *factory, GtkListItem *item) { MatchObject *obj; GtkWidget *label; PangoAttrList *attrs; PangoAttribute *attr; const char *str; obj = MATCH_OBJECT (gtk_list_item_get_item (item)); label = gtk_list_item_get_child (item); str = match_object_get_string (obj); gtk_label_set_label (GTK_LABEL (label), str); attrs = pango_attr_list_new (); attr = pango_attr_weight_new (PANGO_WEIGHT_BOLD); attr->start_index = match_object_get_match_start (obj); attr->end_index = match_object_get_match_end (obj); pango_attr_list_insert (attrs, attr); gtk_label_set_attributes (GTK_LABEL (label), attrs); pango_attr_list_unref (attrs); } static void match_func (MatchObject *obj, const char *search, gpointer user_data) { char *tmp1, *tmp2; char *p; tmp1 = g_utf8_normalize (match_object_get_string (obj), -1, G_NORMALIZE_ALL); tmp2 = g_utf8_normalize (search, -1, G_NORMALIZE_ALL); if ((p = strstr (tmp1, tmp2)) != NULL) match_object_set_match (obj, p - tmp1, (p - tmp1) + g_utf8_strlen (search, -1), 1); else match_object_set_match (obj, 0, 0, 0); g_free (tmp1); g_free (tmp2); } GtkWidget * do_dropdown (GtkWidget *do_widget) { static GtkWidget *window = NULL; GtkWidget *button, *box, *spin, *check, *hbox, *label, *entry; GListModel *model; GtkExpression *expression; GtkListItemFactory *factory; const char * const times[] = { "1 minute", "2 minutes", "5 minutes", "20 minutes", NULL }; const char * const many_times[] = { "1 minute", "2 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "25 minutes", "30 minutes", "35 minutes", "40 minutes", "45 minutes", "50 minutes", "55 minutes", "1 hour", "2 hours", "3 hours", "5 hours", "6 hours", "7 hours", "8 hours", "9 hours", "10 hours", "11 hours", "12 hours", NULL }; const char * const device_titles[] = { "Digital Output", "Headphones", "Digital Output", "Analog Output", NULL }; const char * const device_icons[] = { "audio-card-symbolic", "audio-headphones-symbolic", "audio-card-symbolic", "audio-card-symbolic", NULL }; const char * const device_descriptions[] = { "Built-in Audio", "Built-in audio", "Thinkpad Tunderbolt 3 Dock USB Audio", "Thinkpad Tunderbolt 3 Dock USB Audio", NULL }; char *cwd; GFile *file; GListModel *dir; GtkStringList *strings; if (!window) { window = gtk_window_new (); gtk_window_set_display (GTK_WINDOW (window), gtk_widget_get_display (do_widget)); gtk_window_set_title (GTK_WINDOW (window), "Selections"); gtk_window_set_resizable (GTK_WINDOW (window), FALSE); g_object_add_weak_pointer (G_OBJECT (window), (gpointer *)&window); hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 20); gtk_widget_set_margin_start (hbox, 20); gtk_widget_set_margin_end (hbox, 20); gtk_widget_set_margin_top (hbox, 20); gtk_widget_set_margin_bottom (hbox, 20); gtk_window_set_child (GTK_WINDOW (window), hbox); box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); gtk_box_append (GTK_BOX (hbox), box); label = gtk_label_new ("Dropdowns"); gtk_widget_add_css_class (label, "title-4"); gtk_box_append (GTK_BOX (box), label); /* A basic dropdown */ button = drop_down_new_from_strings (times, NULL, NULL); gtk_box_append (GTK_BOX (box), button); /* A dropdown using an expression to obtain strings */ button = drop_down_new_from_strings (many_times, NULL, NULL); gtk_drop_down_set_enable_search (GTK_DROP_DOWN (button), TRUE); expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL, 0, NULL, (GCallback)get_title, NULL, NULL); gtk_drop_down_set_expression (GTK_DROP_DOWN (button), expression); gtk_expression_unref (expression); gtk_box_append (GTK_BOX (box), button); button = gtk_drop_down_new (NULL, NULL); model = G_LIST_MODEL (pango_cairo_font_map_get_default ()); gtk_drop_down_set_model (GTK_DROP_DOWN (button), model); gtk_drop_down_set_selected (GTK_DROP_DOWN (button), 0); expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL, 0, NULL, (GCallback)get_family_name, NULL, NULL); gtk_drop_down_set_expression (GTK_DROP_DOWN (button), expression); gtk_expression_unref (expression); gtk_box_append (GTK_BOX (box), button); spin = gtk_spin_button_new_with_range (-1, g_list_model_get_n_items (G_LIST_MODEL (model)), 1); gtk_widget_set_halign (spin, GTK_ALIGN_START); gtk_widget_set_margin_start (spin, 20); g_object_bind_property (button, "selected", spin, "value", G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); gtk_box_append (GTK_BOX (box), spin); check = gtk_check_button_new_with_label ("Enable search"); gtk_widget_set_margin_start (check, 20); g_object_bind_property (button, "enable-search", check, "active", G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); gtk_box_append (GTK_BOX (box), check); g_object_unref (model); /* A dropdown with a separate list factory */ button = drop_down_new_from_strings (device_titles, device_icons, device_descriptions); gtk_box_append (GTK_BOX (box), button); gtk_box_append (GTK_BOX (hbox), gtk_separator_new (GTK_ORIENTATION_VERTICAL)); box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); gtk_box_append (GTK_BOX (hbox), box); label = gtk_label_new ("Suggestions"); gtk_widget_add_css_class (label, "title-4"); gtk_box_append (GTK_BOX (box), label); /* A basic suggestion entry */ entry = suggestion_entry_new (); g_object_set (entry, "placeholder-text", "Words with T or G…", NULL); strings = gtk_string_list_new ((const char *[]){ "GNOME", "gnominious", "Gnomonic projection", "total", "totally", "toto", "tottery", "totterer", "Totten trust", "totipotent", "totipotency", "totemism", "totem pole", "Totara", "totalizer", "totalizator", "totalitarianism", "total parenteral nutrition", "total hysterectomy", "total eclipse", "Totipresence", "Totipalmi", "Tomboy", "zombie", NULL}); suggestion_entry_set_model (SUGGESTION_ENTRY (entry), G_LIST_MODEL (strings)); g_object_unref (strings); gtk_box_append (GTK_BOX (box), entry); /* A suggestion entry using a custom model, and no filtering */ entry = suggestion_entry_new (); cwd = g_get_current_dir (); file = g_file_new_for_path (cwd); dir = G_LIST_MODEL (gtk_directory_list_new ("standard::display-name,standard::content-type,standard::icon,standard::size", file)); suggestion_entry_set_model (SUGGESTION_ENTRY (entry), dir); g_object_unref (dir); g_object_unref (file); g_free (cwd); expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL, 0, NULL, (GCallback)get_file_name, NULL, NULL); suggestion_entry_set_expression (SUGGESTION_ENTRY (entry), expression); gtk_expression_unref (expression); factory = gtk_signal_list_item_factory_new (); g_signal_connect (factory, "setup", G_CALLBACK (setup_item), NULL); g_signal_connect (factory, "bind", G_CALLBACK (bind_item), NULL); suggestion_entry_set_factory (SUGGESTION_ENTRY (entry), factory); g_object_unref (factory); suggestion_entry_set_use_filter (SUGGESTION_ENTRY (entry), FALSE); suggestion_entry_set_show_arrow (SUGGESTION_ENTRY (entry), TRUE); gtk_box_append (GTK_BOX (box), entry); /* A suggestion entry with match highlighting */ entry = suggestion_entry_new (); g_object_set (entry, "placeholder-text", "Destination", NULL); strings = gtk_string_list_new ((const char *[]){ "app-mockups", "settings-mockups", "os-mockups", "software-mockups", "mocktails", NULL}); suggestion_entry_set_model (SUGGESTION_ENTRY (entry), G_LIST_MODEL (strings)); g_object_unref (strings); gtk_box_append (GTK_BOX (box), entry); suggestion_entry_set_match_func (SUGGESTION_ENTRY (entry), match_func, NULL, NULL); factory = gtk_signal_list_item_factory_new (); g_signal_connect (factory, "setup", G_CALLBACK (setup_highlight_item), NULL); g_signal_connect (factory, "bind", G_CALLBACK (bind_highlight_item), NULL); suggestion_entry_set_factory (SUGGESTION_ENTRY (entry), factory); g_object_unref (factory); } if (!gtk_widget_get_visible (window)) gtk_widget_show (window); else gtk_window_destroy (GTK_WINDOW (window)); return window; }