1 | /* gtkappchooserbutton.c: an app-chooser button |
2 | * |
3 | * Copyright (C) 2010 Red Hat, Inc. |
4 | * |
5 | * This library is free software; you can redistribute it and/or |
6 | * modify it under the terms of the GNU Library General Public License as |
7 | * published by the Free Software Foundation; either version 2 of the |
8 | * License, or (at your option) any later version. |
9 | * |
10 | * This library is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 | * Library General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU Library General Public |
16 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
17 | * |
18 | * Authors: Cosimo Cecchi <ccecchi@redhat.com> |
19 | */ |
20 | |
21 | /** |
22 | * GtkAppChooserButton: |
23 | * |
24 | * The `GtkAppChooserButton` lets the user select an application. |
25 | * |
26 | * ![An example GtkAppChooserButton](appchooserbutton.png) |
27 | * |
28 | * Initially, a `GtkAppChooserButton` selects the first application |
29 | * in its list, which will either be the most-recently used application |
30 | * or, if [property@Gtk.AppChooserButton:show-default-item] is %TRUE, the |
31 | * default application. |
32 | * |
33 | * The list of applications shown in a `GtkAppChooserButton` includes |
34 | * the recommended applications for the given content type. When |
35 | * [property@Gtk.AppChooserButton:show-default-item] is set, the default |
36 | * application is also included. To let the user chooser other applications, |
37 | * you can set the [property@Gtk.AppChooserButton:show-dialog-item] property, |
38 | * which allows to open a full [class@Gtk.AppChooserDialog]. |
39 | * |
40 | * It is possible to add custom items to the list, using |
41 | * [method@Gtk.AppChooserButton.append_custom_item]. These items cause |
42 | * the [signal@Gtk.AppChooserButton::custom-item-activated] signal to be |
43 | * emitted when they are selected. |
44 | * |
45 | * To track changes in the selected application, use the |
46 | * [signal@Gtk.AppChooserButton::changed] signal. |
47 | * |
48 | * # CSS nodes |
49 | * |
50 | * `GtkAppChooserButton` has a single CSS node with the name “appchooserbutton”. |
51 | */ |
52 | #include "config.h" |
53 | |
54 | #include "gtkappchooserbutton.h" |
55 | |
56 | #include "gtkappchooser.h" |
57 | #include "gtkappchooserdialog.h" |
58 | #include "gtkappchooserprivate.h" |
59 | #include "gtkcelllayout.h" |
60 | #include "gtkcellrendererpixbuf.h" |
61 | #include "gtkcellrenderertext.h" |
62 | #include "gtkcombobox.h" |
63 | #include "gtkwidgetprivate.h" |
64 | #include "gtkdialog.h" |
65 | #include "gtkintl.h" |
66 | #include "gtkmarshalers.h" |
67 | #include "gtkliststore.h" |
68 | |
69 | enum { |
70 | PROP_SHOW_DIALOG_ITEM = 1, |
71 | PROP_SHOW_DEFAULT_ITEM, |
72 | PROP_HEADING, |
73 | PROP_MODAL, |
74 | NUM_PROPERTIES, |
75 | |
76 | PROP_CONTENT_TYPE = NUM_PROPERTIES |
77 | }; |
78 | |
79 | enum { |
80 | SIGNAL_CHANGED, |
81 | SIGNAL_CUSTOM_ITEM_ACTIVATED, |
82 | ACTIVATE, |
83 | NUM_SIGNALS |
84 | }; |
85 | |
86 | enum { |
87 | COLUMN_APP_INFO, |
88 | COLUMN_NAME, |
89 | COLUMN_LABEL, |
90 | COLUMN_ICON, |
91 | COLUMN_CUSTOM, |
92 | COLUMN_SEPARATOR, |
93 | NUM_COLUMNS, |
94 | }; |
95 | |
96 | #define CUSTOM_ITEM_OTHER_APP "gtk-internal-item-other-app" |
97 | |
98 | static void app_chooser_iface_init (GtkAppChooserIface *iface); |
99 | |
100 | static void real_insert_custom_item (GtkAppChooserButton *self, |
101 | const char *name, |
102 | const char *label, |
103 | GIcon *icon, |
104 | gboolean custom, |
105 | GtkTreeIter *iter); |
106 | |
107 | static void real_insert_separator (GtkAppChooserButton *self, |
108 | gboolean custom, |
109 | GtkTreeIter *iter); |
110 | |
111 | static guint signals[NUM_SIGNALS] = { 0, }; |
112 | static GParamSpec *properties[NUM_PROPERTIES]; |
113 | |
114 | typedef struct _GtkAppChooserButtonClass GtkAppChooserButtonClass; |
115 | |
116 | struct _GtkAppChooserButton { |
117 | GtkWidget parent_instance; |
118 | |
119 | GtkWidget *combobox; |
120 | GtkListStore *store; |
121 | |
122 | char *content_type; |
123 | char *heading; |
124 | int last_active; |
125 | gboolean show_dialog_item; |
126 | gboolean show_default_item; |
127 | gboolean modal; |
128 | |
129 | GHashTable *custom_item_names; |
130 | }; |
131 | |
132 | struct _GtkAppChooserButtonClass { |
133 | GtkWidgetClass parent_class; |
134 | |
135 | void (* changed) (GtkAppChooserButton *self); |
136 | void (* custom_item_activated) (GtkAppChooserButton *self, |
137 | const char *item_name); |
138 | void (* activate) (GtkAppChooserButton *self); |
139 | }; |
140 | |
141 | G_DEFINE_TYPE_WITH_CODE (GtkAppChooserButton, gtk_app_chooser_button, GTK_TYPE_WIDGET, |
142 | G_IMPLEMENT_INTERFACE (GTK_TYPE_APP_CHOOSER, |
143 | app_chooser_iface_init)); |
144 | |
145 | static gboolean |
146 | row_separator_func (GtkTreeModel *model, |
147 | GtkTreeIter *iter, |
148 | gpointer user_data) |
149 | { |
150 | gboolean separator; |
151 | |
152 | gtk_tree_model_get (tree_model: model, iter, |
153 | COLUMN_SEPARATOR, &separator, |
154 | -1); |
155 | |
156 | return separator; |
157 | } |
158 | |
159 | static void |
160 | get_first_iter (GtkListStore *store, |
161 | GtkTreeIter *iter) |
162 | { |
163 | GtkTreeIter iter2; |
164 | |
165 | if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), iter)) |
166 | { |
167 | /* the model is empty, append */ |
168 | gtk_list_store_append (list_store: store, iter); |
169 | } |
170 | else |
171 | { |
172 | gtk_list_store_insert_before (list_store: store, iter: &iter2, sibling: iter); |
173 | *iter = iter2; |
174 | } |
175 | } |
176 | |
177 | typedef struct { |
178 | GtkAppChooserButton *self; |
179 | GAppInfo *info; |
180 | int active_index; |
181 | } SelectAppData; |
182 | |
183 | static void |
184 | select_app_data_free (SelectAppData *data) |
185 | { |
186 | g_clear_object (&data->self); |
187 | g_clear_object (&data->info); |
188 | |
189 | g_slice_free (SelectAppData, data); |
190 | } |
191 | |
192 | static gboolean |
193 | select_application_func_cb (GtkTreeModel *model, |
194 | GtkTreePath *path, |
195 | GtkTreeIter *iter, |
196 | gpointer user_data) |
197 | { |
198 | SelectAppData *data = user_data; |
199 | GtkAppChooserButton *self = data->self; |
200 | GAppInfo *app_to_match = data->info; |
201 | GAppInfo *app = NULL; |
202 | gboolean custom; |
203 | gboolean result; |
204 | |
205 | gtk_tree_model_get (tree_model: model, iter, |
206 | COLUMN_APP_INFO, &app, |
207 | COLUMN_CUSTOM, &custom, |
208 | -1); |
209 | |
210 | /* custom items are always after GAppInfos, so iterating further here |
211 | * is just useless. |
212 | */ |
213 | if (custom) |
214 | result = TRUE; |
215 | else if (g_app_info_equal (appinfo1: app, appinfo2: app_to_match)) |
216 | { |
217 | gtk_combo_box_set_active_iter (GTK_COMBO_BOX (self->combobox), iter); |
218 | result = TRUE; |
219 | } |
220 | else |
221 | result = FALSE; |
222 | |
223 | g_object_unref (object: app); |
224 | |
225 | return result; |
226 | } |
227 | |
228 | static void |
229 | gtk_app_chooser_button_select_application (GtkAppChooserButton *self, |
230 | GAppInfo *info) |
231 | { |
232 | SelectAppData *data; |
233 | |
234 | data = g_slice_new0 (SelectAppData); |
235 | data->self = g_object_ref (self); |
236 | data->info = g_object_ref (info); |
237 | |
238 | gtk_tree_model_foreach (GTK_TREE_MODEL (self->store), |
239 | func: select_application_func_cb, user_data: data); |
240 | |
241 | select_app_data_free (data); |
242 | } |
243 | |
244 | static void |
245 | other_application_dialog_response_cb (GtkDialog *dialog, |
246 | int response_id, |
247 | gpointer user_data) |
248 | { |
249 | GtkAppChooserButton *self = user_data; |
250 | GAppInfo *info; |
251 | |
252 | if (response_id != GTK_RESPONSE_OK) |
253 | { |
254 | /* reset the active item, otherwise we are stuck on |
255 | * 'Other application…' |
256 | */ |
257 | gtk_combo_box_set_active (GTK_COMBO_BOX (self->combobox), index_: self->last_active); |
258 | gtk_window_destroy (GTK_WINDOW (dialog)); |
259 | return; |
260 | } |
261 | |
262 | info = gtk_app_chooser_get_app_info (GTK_APP_CHOOSER (dialog)); |
263 | |
264 | gtk_window_destroy (GTK_WINDOW (dialog)); |
265 | |
266 | /* refresh the combobox to get the new application */ |
267 | gtk_app_chooser_refresh (GTK_APP_CHOOSER (self)); |
268 | gtk_app_chooser_button_select_application (self, info); |
269 | |
270 | g_object_unref (object: info); |
271 | } |
272 | |
273 | static void |
274 | other_application_item_activated_cb (GtkAppChooserButton *self) |
275 | { |
276 | GtkWidget *dialog, *widget; |
277 | GtkRoot *root; |
278 | |
279 | root = gtk_widget_get_root (GTK_WIDGET (self)); |
280 | dialog = gtk_app_chooser_dialog_new_for_content_type (GTK_WINDOW (root), |
281 | flags: GTK_DIALOG_DESTROY_WITH_PARENT, |
282 | content_type: self->content_type); |
283 | gtk_window_set_modal (GTK_WINDOW (dialog), modal: self->modal | gtk_window_get_modal (GTK_WINDOW (root))); |
284 | gtk_app_chooser_dialog_set_heading (GTK_APP_CHOOSER_DIALOG (dialog), heading: self->heading); |
285 | |
286 | widget = gtk_app_chooser_dialog_get_widget (GTK_APP_CHOOSER_DIALOG (dialog)); |
287 | g_object_set (object: widget, |
288 | first_property_name: "show-fallback" , TRUE, |
289 | "show-other" , TRUE, |
290 | NULL); |
291 | gtk_widget_show (widget: dialog); |
292 | |
293 | g_signal_connect (dialog, "response" , |
294 | G_CALLBACK (other_application_dialog_response_cb), self); |
295 | } |
296 | |
297 | static void |
298 | gtk_app_chooser_button_ensure_dialog_item (GtkAppChooserButton *self, |
299 | GtkTreeIter *prev_iter) |
300 | { |
301 | GtkTreeIter iter, iter2; |
302 | |
303 | if (!self->show_dialog_item || !self->content_type) |
304 | return; |
305 | |
306 | if (prev_iter == NULL) |
307 | gtk_list_store_append (list_store: self->store, iter: &iter); |
308 | else |
309 | gtk_list_store_insert_after (list_store: self->store, iter: &iter, sibling: prev_iter); |
310 | |
311 | real_insert_separator (self, FALSE, iter: &iter); |
312 | iter2 = iter; |
313 | |
314 | gtk_list_store_insert_after (list_store: self->store, iter: &iter, sibling: &iter2); |
315 | real_insert_custom_item (self, CUSTOM_ITEM_OTHER_APP, |
316 | _("Other application…" ), NULL, |
317 | FALSE, iter: &iter); |
318 | } |
319 | |
320 | static void |
321 | insert_one_application (GtkAppChooserButton *self, |
322 | GAppInfo *app, |
323 | GtkTreeIter *iter) |
324 | { |
325 | GIcon *icon; |
326 | |
327 | icon = g_app_info_get_icon (appinfo: app); |
328 | |
329 | if (icon == NULL) |
330 | icon = g_themed_icon_new (iconname: "application-x-executable" ); |
331 | else |
332 | g_object_ref (icon); |
333 | |
334 | gtk_list_store_set (list_store: self->store, iter, |
335 | COLUMN_APP_INFO, app, |
336 | COLUMN_LABEL, g_app_info_get_name (appinfo: app), |
337 | COLUMN_ICON, icon, |
338 | COLUMN_CUSTOM, FALSE, |
339 | -1); |
340 | |
341 | g_object_unref (object: icon); |
342 | } |
343 | |
344 | static void |
345 | gtk_app_chooser_button_populate (GtkAppChooserButton *self) |
346 | { |
347 | GList *recommended_apps = NULL, *l; |
348 | GAppInfo *app, *default_app = NULL; |
349 | GtkTreeIter iter, iter2; |
350 | gboolean cycled_recommended; |
351 | |
352 | #ifndef G_OS_WIN32 |
353 | if (self->content_type) |
354 | recommended_apps = g_app_info_get_recommended_for_type (content_type: self->content_type); |
355 | #endif |
356 | cycled_recommended = FALSE; |
357 | |
358 | if (self->show_default_item) |
359 | { |
360 | if (self->content_type) |
361 | default_app = g_app_info_get_default_for_type (content_type: self->content_type, FALSE); |
362 | |
363 | if (default_app != NULL) |
364 | { |
365 | get_first_iter (store: self->store, iter: &iter); |
366 | cycled_recommended = TRUE; |
367 | |
368 | insert_one_application (self, app: default_app, iter: &iter); |
369 | |
370 | g_object_unref (object: default_app); |
371 | } |
372 | } |
373 | |
374 | for (l = recommended_apps; l != NULL; l = l->next) |
375 | { |
376 | app = l->data; |
377 | |
378 | if (default_app != NULL && g_app_info_equal (appinfo1: app, appinfo2: default_app)) |
379 | continue; |
380 | |
381 | if (cycled_recommended) |
382 | { |
383 | gtk_list_store_insert_after (list_store: self->store, iter: &iter2, sibling: &iter); |
384 | iter = iter2; |
385 | } |
386 | else |
387 | { |
388 | get_first_iter (store: self->store, iter: &iter); |
389 | cycled_recommended = TRUE; |
390 | } |
391 | |
392 | insert_one_application (self, app, iter: &iter); |
393 | } |
394 | |
395 | if (recommended_apps != NULL) |
396 | g_list_free_full (list: recommended_apps, free_func: g_object_unref); |
397 | |
398 | if (!cycled_recommended) |
399 | gtk_app_chooser_button_ensure_dialog_item (self, NULL); |
400 | else |
401 | gtk_app_chooser_button_ensure_dialog_item (self, prev_iter: &iter); |
402 | |
403 | gtk_combo_box_set_active (GTK_COMBO_BOX (self->combobox), index_: 0); |
404 | } |
405 | |
406 | static void |
407 | gtk_app_chooser_button_build_ui (GtkAppChooserButton *self) |
408 | { |
409 | GtkCellRenderer *cell; |
410 | GtkCellArea *area; |
411 | |
412 | gtk_combo_box_set_model (GTK_COMBO_BOX (self->combobox), |
413 | GTK_TREE_MODEL (self->store)); |
414 | |
415 | area = gtk_cell_layout_get_area (GTK_CELL_LAYOUT (self->combobox)); |
416 | |
417 | gtk_combo_box_set_row_separator_func (GTK_COMBO_BOX (self->combobox), |
418 | func: row_separator_func, NULL, NULL); |
419 | |
420 | cell = gtk_cell_renderer_pixbuf_new (); |
421 | gtk_cell_area_add_with_properties (area, renderer: cell, |
422 | first_prop_name: "align" , FALSE, |
423 | "expand" , FALSE, |
424 | "fixed-size" , FALSE, |
425 | NULL); |
426 | gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (self->combobox), cell, |
427 | "gicon" , COLUMN_ICON, |
428 | NULL); |
429 | |
430 | cell = gtk_cell_renderer_text_new (); |
431 | gtk_cell_area_add_with_properties (area, renderer: cell, |
432 | first_prop_name: "align" , FALSE, |
433 | "expand" , TRUE, |
434 | NULL); |
435 | gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (self->combobox), cell, |
436 | "text" , COLUMN_LABEL, |
437 | NULL); |
438 | |
439 | gtk_app_chooser_button_populate (self); |
440 | } |
441 | |
442 | static void |
443 | gtk_app_chooser_button_remove_non_custom (GtkAppChooserButton *self) |
444 | { |
445 | GtkTreeModel *model; |
446 | GtkTreeIter iter; |
447 | gboolean custom, res; |
448 | |
449 | model = GTK_TREE_MODEL (self->store); |
450 | |
451 | if (!gtk_tree_model_get_iter_first (tree_model: model, iter: &iter)) |
452 | return; |
453 | |
454 | do { |
455 | gtk_tree_model_get (tree_model: model, iter: &iter, |
456 | COLUMN_CUSTOM, &custom, |
457 | -1); |
458 | if (custom) |
459 | res = gtk_tree_model_iter_next (tree_model: model, iter: &iter); |
460 | else |
461 | res = gtk_list_store_remove (GTK_LIST_STORE (model), iter: &iter); |
462 | } while (res); |
463 | } |
464 | |
465 | static void |
466 | gtk_app_chooser_button_changed (GtkComboBox *object, |
467 | gpointer user_data) |
468 | { |
469 | GtkAppChooserButton *self = user_data; |
470 | GtkTreeIter iter; |
471 | char *name = NULL; |
472 | gboolean custom; |
473 | GQuark name_quark; |
474 | |
475 | if (!gtk_combo_box_get_active_iter (combo_box: object, iter: &iter)) |
476 | return; |
477 | |
478 | gtk_tree_model_get (GTK_TREE_MODEL (self->store), iter: &iter, |
479 | COLUMN_NAME, &name, |
480 | COLUMN_CUSTOM, &custom, |
481 | -1); |
482 | |
483 | if (name != NULL) |
484 | { |
485 | if (custom) |
486 | { |
487 | name_quark = g_quark_from_string (string: name); |
488 | g_signal_emit (instance: self, signal_id: signals[SIGNAL_CUSTOM_ITEM_ACTIVATED], detail: name_quark, name); |
489 | self->last_active = gtk_combo_box_get_active (combo_box: object); |
490 | } |
491 | else |
492 | { |
493 | /* trigger the dialog internally */ |
494 | other_application_item_activated_cb (self); |
495 | } |
496 | |
497 | g_free (mem: name); |
498 | } |
499 | else |
500 | self->last_active = gtk_combo_box_get_active (combo_box: object); |
501 | |
502 | g_signal_emit (instance: self, signal_id: signals[SIGNAL_CHANGED], detail: 0); |
503 | } |
504 | |
505 | static void |
506 | gtk_app_chooser_button_refresh (GtkAppChooser *object) |
507 | { |
508 | GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (object); |
509 | |
510 | gtk_app_chooser_button_remove_non_custom (self); |
511 | gtk_app_chooser_button_populate (self); |
512 | } |
513 | |
514 | static GAppInfo * |
515 | gtk_app_chooser_button_get_app_info (GtkAppChooser *object) |
516 | { |
517 | GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (object); |
518 | GtkTreeIter iter; |
519 | GAppInfo *info; |
520 | |
521 | if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (self->combobox), iter: &iter)) |
522 | return NULL; |
523 | |
524 | gtk_tree_model_get (GTK_TREE_MODEL (self->store), iter: &iter, |
525 | COLUMN_APP_INFO, &info, |
526 | -1); |
527 | |
528 | return info; |
529 | } |
530 | |
531 | static void |
532 | gtk_app_chooser_button_constructed (GObject *obj) |
533 | { |
534 | GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (obj); |
535 | |
536 | if (G_OBJECT_CLASS (gtk_app_chooser_button_parent_class)->constructed != NULL) |
537 | G_OBJECT_CLASS (gtk_app_chooser_button_parent_class)->constructed (obj); |
538 | |
539 | gtk_app_chooser_button_build_ui (self); |
540 | } |
541 | |
542 | static void |
543 | gtk_app_chooser_button_set_property (GObject *obj, |
544 | guint property_id, |
545 | const GValue *value, |
546 | GParamSpec *pspec) |
547 | { |
548 | GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (obj); |
549 | |
550 | switch (property_id) |
551 | { |
552 | case PROP_CONTENT_TYPE: |
553 | self->content_type = g_value_dup_string (value); |
554 | break; |
555 | case PROP_SHOW_DIALOG_ITEM: |
556 | gtk_app_chooser_button_set_show_dialog_item (self, setting: g_value_get_boolean (value)); |
557 | break; |
558 | case PROP_SHOW_DEFAULT_ITEM: |
559 | gtk_app_chooser_button_set_show_default_item (self, setting: g_value_get_boolean (value)); |
560 | break; |
561 | case PROP_HEADING: |
562 | gtk_app_chooser_button_set_heading (self, heading: g_value_get_string (value)); |
563 | break; |
564 | case PROP_MODAL: |
565 | gtk_app_chooser_button_set_modal (self, modal: g_value_get_boolean (value)); |
566 | break; |
567 | default: |
568 | G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec); |
569 | break; |
570 | } |
571 | } |
572 | |
573 | static void |
574 | gtk_app_chooser_button_get_property (GObject *obj, |
575 | guint property_id, |
576 | GValue *value, |
577 | GParamSpec *pspec) |
578 | { |
579 | GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (obj); |
580 | |
581 | switch (property_id) |
582 | { |
583 | case PROP_CONTENT_TYPE: |
584 | g_value_set_string (value, v_string: self->content_type); |
585 | break; |
586 | case PROP_SHOW_DIALOG_ITEM: |
587 | g_value_set_boolean (value, v_boolean: self->show_dialog_item); |
588 | break; |
589 | case PROP_SHOW_DEFAULT_ITEM: |
590 | g_value_set_boolean (value, v_boolean: self->show_default_item); |
591 | break; |
592 | case PROP_HEADING: |
593 | g_value_set_string (value, v_string: self->heading); |
594 | break; |
595 | case PROP_MODAL: |
596 | g_value_set_boolean (value, v_boolean: self->modal); |
597 | break; |
598 | default: |
599 | G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec); |
600 | break; |
601 | } |
602 | } |
603 | |
604 | static void |
605 | gtk_app_chooser_button_finalize (GObject *obj) |
606 | { |
607 | GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (obj); |
608 | |
609 | g_hash_table_destroy (hash_table: self->custom_item_names); |
610 | g_free (mem: self->content_type); |
611 | g_free (mem: self->heading); |
612 | g_object_unref (object: self->store); |
613 | gtk_widget_unparent (widget: self->combobox); |
614 | |
615 | G_OBJECT_CLASS (gtk_app_chooser_button_parent_class)->finalize (obj); |
616 | } |
617 | |
618 | static void |
619 | gtk_app_chooser_button_measure (GtkWidget *widget, |
620 | GtkOrientation orientation, |
621 | int for_size, |
622 | int *minimum, |
623 | int *natural, |
624 | int *minimum_baseline, |
625 | int *natural_baseline) |
626 | { |
627 | GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (widget); |
628 | |
629 | gtk_widget_measure (widget: self->combobox, orientation, for_size, |
630 | minimum, natural, |
631 | minimum_baseline, natural_baseline); |
632 | } |
633 | |
634 | static void |
635 | gtk_app_chooser_button_size_allocate (GtkWidget *widget, |
636 | int width, |
637 | int height, |
638 | int baseline) |
639 | { |
640 | GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (widget); |
641 | |
642 | gtk_widget_size_allocate (widget: self->combobox, allocation: &(GtkAllocation){0, 0, width, height}, baseline); |
643 | } |
644 | |
645 | static void |
646 | app_chooser_iface_init (GtkAppChooserIface *iface) |
647 | { |
648 | iface->get_app_info = gtk_app_chooser_button_get_app_info; |
649 | iface->refresh = gtk_app_chooser_button_refresh; |
650 | } |
651 | |
652 | static void |
653 | gtk_app_chooser_button_activate (GtkAppChooserButton *self) |
654 | { |
655 | gtk_widget_activate (widget: self->combobox); |
656 | } |
657 | |
658 | static void |
659 | gtk_app_chooser_button_class_init (GtkAppChooserButtonClass *klass) |
660 | { |
661 | GObjectClass *oclass = G_OBJECT_CLASS (klass); |
662 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); |
663 | |
664 | oclass->set_property = gtk_app_chooser_button_set_property; |
665 | oclass->get_property = gtk_app_chooser_button_get_property; |
666 | oclass->finalize = gtk_app_chooser_button_finalize; |
667 | oclass->constructed = gtk_app_chooser_button_constructed; |
668 | |
669 | widget_class->measure = gtk_app_chooser_button_measure; |
670 | widget_class->size_allocate = gtk_app_chooser_button_size_allocate; |
671 | widget_class->grab_focus = gtk_widget_grab_focus_child; |
672 | widget_class->focus = gtk_widget_focus_child; |
673 | |
674 | klass->activate = gtk_app_chooser_button_activate; |
675 | |
676 | g_object_class_override_property (oclass, property_id: PROP_CONTENT_TYPE, name: "content-type" ); |
677 | |
678 | /** |
679 | * GtkAppChooserButton:show-dialog-item: (attributes org.gtk.Property.get=gtk_app_chooser_button_get_show_dialog_item org.gtk.Property.set=gtk_app_chooser_button_set_show_dialog_item) |
680 | * |
681 | * Determines whether the dropdown menu shows an item to open |
682 | * a `GtkAppChooserDialog`. |
683 | */ |
684 | properties[PROP_SHOW_DIALOG_ITEM] = |
685 | g_param_spec_boolean (name: "show-dialog-item" , |
686 | P_("Include an “Other…” item" ), |
687 | P_("Whether the combobox should include an item that triggers a GtkAppChooserDialog" ), |
688 | FALSE, |
689 | flags: G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY); |
690 | |
691 | /** |
692 | * GtkAppChooserButton:show-default-item: (attributes org.gtk.Property.get=gtk_app_chooser_button_get_show_default_item org.gtk.Property.set=gtk_app_chooser_button_set_show_default_item) |
693 | * |
694 | * Determines whether the dropdown menu shows the default application |
695 | * on top for the provided content type. |
696 | */ |
697 | properties[PROP_SHOW_DEFAULT_ITEM] = |
698 | g_param_spec_boolean (name: "show-default-item" , |
699 | P_("Show default item" ), |
700 | P_("Whether the combobox should show the default application on top" ), |
701 | FALSE, |
702 | flags: G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY); |
703 | |
704 | /** |
705 | * GtkAppChooserButton:heading: (attributes org.gtk.Property.get=gtk_app_chooser_button_get_heading org.gtk.Property.set=gtk_app_chooser_button_set_heading) |
706 | * |
707 | * The text to show at the top of the dialog that can be |
708 | * opened from the button. |
709 | * |
710 | * The string may contain Pango markup. |
711 | */ |
712 | properties[PROP_HEADING] = |
713 | g_param_spec_string (name: "heading" , |
714 | P_("Heading" ), |
715 | P_("The text to show at the top of the dialog" ), |
716 | NULL, |
717 | flags: G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY); |
718 | |
719 | /** |
720 | * GtkAppChooserButton:modal: (attributes org.gtk.Property.get=gtk_app_chooser_button_get_modal org.gtk.Property.set=gtk_app_chooser_button_set_modal) |
721 | * |
722 | * Whether the app chooser dialog should be modal. |
723 | */ |
724 | properties[PROP_MODAL] = |
725 | g_param_spec_boolean (name: "modal" , |
726 | P_("Modal" ), |
727 | P_("Whether the dialog should be modal" ), |
728 | TRUE, |
729 | flags: G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY); |
730 | g_object_class_install_properties (oclass, n_pspecs: NUM_PROPERTIES, pspecs: properties); |
731 | |
732 | /** |
733 | * GtkAppChooserButton::changed: |
734 | * @self: the object which received the signal |
735 | * |
736 | * Emitted when the active application changes. |
737 | */ |
738 | signals[SIGNAL_CHANGED] = |
739 | g_signal_new (I_("changed" ), |
740 | G_OBJECT_CLASS_TYPE (klass), |
741 | signal_flags: G_SIGNAL_RUN_LAST, |
742 | G_STRUCT_OFFSET (GtkAppChooserButtonClass, changed), |
743 | NULL, NULL, |
744 | NULL, |
745 | G_TYPE_NONE, n_params: 0); |
746 | |
747 | |
748 | /** |
749 | * GtkAppChooserButton::custom-item-activated: |
750 | * @self: the object which received the signal |
751 | * @item_name: the name of the activated item |
752 | * |
753 | * Emitted when a custom item is activated. |
754 | * |
755 | * Use [method@Gtk.AppChooserButton.append_custom_item], |
756 | * to add custom items. |
757 | */ |
758 | signals[SIGNAL_CUSTOM_ITEM_ACTIVATED] = |
759 | g_signal_new (I_("custom-item-activated" ), |
760 | GTK_TYPE_APP_CHOOSER_BUTTON, |
761 | signal_flags: G_SIGNAL_RUN_FIRST | G_SIGNAL_DETAILED, |
762 | G_STRUCT_OFFSET (GtkAppChooserButtonClass, custom_item_activated), |
763 | NULL, NULL, |
764 | NULL, |
765 | G_TYPE_NONE, |
766 | n_params: 1, G_TYPE_STRING); |
767 | |
768 | /** |
769 | * GtkAppChooserButton::activate: |
770 | * @widget: the object which received the signal. |
771 | * |
772 | * Emitted to when the button is activated. |
773 | * |
774 | * The `::activate` signal on `GtkAppChooserButton` is an action signal and |
775 | * emitting it causes the button to pop up its dialog. |
776 | * |
777 | * Since: 4.4 |
778 | */ |
779 | signals[ACTIVATE] = |
780 | g_signal_new (I_ ("activate" ), |
781 | G_OBJECT_CLASS_TYPE (oclass), |
782 | signal_flags: G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, |
783 | G_STRUCT_OFFSET (GtkAppChooserButtonClass, activate), |
784 | NULL, NULL, |
785 | NULL, |
786 | G_TYPE_NONE, n_params: 0); |
787 | |
788 | gtk_widget_class_set_activate_signal (widget_class, signal_id: signals[ACTIVATE]); |
789 | |
790 | |
791 | gtk_widget_class_set_css_name (widget_class, I_("appchooserbutton" )); |
792 | } |
793 | |
794 | static void |
795 | gtk_app_chooser_button_init (GtkAppChooserButton *self) |
796 | { |
797 | self->modal = TRUE; |
798 | |
799 | self->custom_item_names = g_hash_table_new_full (hash_func: g_str_hash, key_equal_func: g_str_equal, key_destroy_func: g_free, NULL); |
800 | self->store = gtk_list_store_new (n_columns: NUM_COLUMNS, |
801 | G_TYPE_APP_INFO, |
802 | G_TYPE_STRING, /* name */ |
803 | G_TYPE_STRING, /* label */ |
804 | G_TYPE_ICON, |
805 | G_TYPE_BOOLEAN, /* separator */ |
806 | G_TYPE_BOOLEAN); /* custom */ |
807 | self->combobox = gtk_combo_box_new_with_model (GTK_TREE_MODEL (self->store)); |
808 | gtk_widget_set_parent (widget: self->combobox, GTK_WIDGET (self)); |
809 | |
810 | g_signal_connect (self->combobox, "changed" , |
811 | G_CALLBACK (gtk_app_chooser_button_changed), self); |
812 | } |
813 | |
814 | static gboolean |
815 | app_chooser_button_iter_from_custom_name (GtkAppChooserButton *self, |
816 | const char *name, |
817 | GtkTreeIter *set_me) |
818 | { |
819 | GtkTreeIter iter; |
820 | char *custom_name = NULL; |
821 | |
822 | if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->store), iter: &iter)) |
823 | return FALSE; |
824 | |
825 | do { |
826 | gtk_tree_model_get (GTK_TREE_MODEL (self->store), iter: &iter, |
827 | COLUMN_NAME, &custom_name, |
828 | -1); |
829 | |
830 | if (g_strcmp0 (str1: custom_name, str2: name) == 0) |
831 | { |
832 | g_free (mem: custom_name); |
833 | *set_me = iter; |
834 | |
835 | return TRUE; |
836 | } |
837 | |
838 | g_free (mem: custom_name); |
839 | } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->store), iter: &iter)); |
840 | |
841 | return FALSE; |
842 | } |
843 | |
844 | static void |
845 | real_insert_custom_item (GtkAppChooserButton *self, |
846 | const char *name, |
847 | const char *label, |
848 | GIcon *icon, |
849 | gboolean custom, |
850 | GtkTreeIter *iter) |
851 | { |
852 | if (custom) |
853 | { |
854 | if (g_hash_table_lookup (hash_table: self->custom_item_names, key: name) != NULL) |
855 | { |
856 | g_warning ("Attempting to add custom item %s to GtkAppChooserButton, " |
857 | "when there's already an item with the same name" , name); |
858 | return; |
859 | } |
860 | |
861 | g_hash_table_insert (hash_table: self->custom_item_names, |
862 | key: g_strdup (str: name), GINT_TO_POINTER (1)); |
863 | } |
864 | |
865 | gtk_list_store_set (list_store: self->store, iter, |
866 | COLUMN_NAME, name, |
867 | COLUMN_LABEL, label, |
868 | COLUMN_ICON, icon, |
869 | COLUMN_CUSTOM, custom, |
870 | COLUMN_SEPARATOR, FALSE, |
871 | -1); |
872 | } |
873 | |
874 | static void |
875 | real_insert_separator (GtkAppChooserButton *self, |
876 | gboolean custom, |
877 | GtkTreeIter *iter) |
878 | { |
879 | gtk_list_store_set (list_store: self->store, iter, |
880 | COLUMN_CUSTOM, custom, |
881 | COLUMN_SEPARATOR, TRUE, |
882 | -1); |
883 | } |
884 | |
885 | /** |
886 | * gtk_app_chooser_button_new: |
887 | * @content_type: the content type to show applications for |
888 | * |
889 | * Creates a new `GtkAppChooserButton` for applications |
890 | * that can handle content of the given type. |
891 | * |
892 | * Returns: a newly created `GtkAppChooserButton` |
893 | */ |
894 | GtkWidget * |
895 | gtk_app_chooser_button_new (const char *content_type) |
896 | { |
897 | g_return_val_if_fail (content_type != NULL, NULL); |
898 | |
899 | return g_object_new (GTK_TYPE_APP_CHOOSER_BUTTON, |
900 | first_property_name: "content-type" , content_type, |
901 | NULL); |
902 | } |
903 | |
904 | /** |
905 | * gtk_app_chooser_button_append_separator: |
906 | * @self: a `GtkAppChooserButton` |
907 | * |
908 | * Appends a separator to the list of applications that is shown |
909 | * in the popup. |
910 | */ |
911 | void |
912 | gtk_app_chooser_button_append_separator (GtkAppChooserButton *self) |
913 | { |
914 | GtkTreeIter iter; |
915 | |
916 | g_return_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self)); |
917 | |
918 | gtk_list_store_append (list_store: self->store, iter: &iter); |
919 | real_insert_separator (self, TRUE, iter: &iter); |
920 | } |
921 | |
922 | /** |
923 | * gtk_app_chooser_button_append_custom_item: |
924 | * @self: a `GtkAppChooserButton` |
925 | * @name: the name of the custom item |
926 | * @label: the label for the custom item |
927 | * @icon: the icon for the custom item |
928 | * |
929 | * Appends a custom item to the list of applications that is shown |
930 | * in the popup. |
931 | * |
932 | * The item name must be unique per-widget. Clients can use the |
933 | * provided name as a detail for the |
934 | * [signal@Gtk.AppChooserButton::custom-item-activated] signal, to add a |
935 | * callback for the activation of a particular custom item in the list. |
936 | * |
937 | * See also [method@Gtk.AppChooserButton.append_separator]. |
938 | */ |
939 | void |
940 | gtk_app_chooser_button_append_custom_item (GtkAppChooserButton *self, |
941 | const char *name, |
942 | const char *label, |
943 | GIcon *icon) |
944 | { |
945 | GtkTreeIter iter; |
946 | |
947 | g_return_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self)); |
948 | g_return_if_fail (name != NULL); |
949 | |
950 | gtk_list_store_append (list_store: self->store, iter: &iter); |
951 | real_insert_custom_item (self, name, label, icon, TRUE, iter: &iter); |
952 | } |
953 | |
954 | /** |
955 | * gtk_app_chooser_button_set_active_custom_item: |
956 | * @self: a `GtkAppChooserButton` |
957 | * @name: the name of the custom item |
958 | * |
959 | * Selects a custom item. |
960 | * |
961 | * See [method@Gtk.AppChooserButton.append_custom_item]. |
962 | * |
963 | * Use [method@Gtk.AppChooser.refresh] to bring the selection |
964 | * to its initial state. |
965 | */ |
966 | void |
967 | gtk_app_chooser_button_set_active_custom_item (GtkAppChooserButton *self, |
968 | const char *name) |
969 | { |
970 | GtkTreeIter iter; |
971 | |
972 | g_return_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self)); |
973 | g_return_if_fail (name != NULL); |
974 | |
975 | if (!g_hash_table_contains (hash_table: self->custom_item_names, key: name) || |
976 | !app_chooser_button_iter_from_custom_name (self, name, set_me: &iter)) |
977 | { |
978 | g_warning ("Can't find the item named %s in the app chooser." , name); |
979 | return; |
980 | } |
981 | |
982 | gtk_combo_box_set_active_iter (GTK_COMBO_BOX (self->combobox), iter: &iter); |
983 | } |
984 | |
985 | /** |
986 | * gtk_app_chooser_button_get_show_dialog_item: (attributes org.gtk.Method.get_property=show-dialog-item) |
987 | * @self: a `GtkAppChooserButton` |
988 | * |
989 | * Returns whether the dropdown menu shows an item |
990 | * for a `GtkAppChooserDialog`. |
991 | * |
992 | * Returns: the value of [property@Gtk.AppChooserButton:show-dialog-item] |
993 | */ |
994 | gboolean |
995 | gtk_app_chooser_button_get_show_dialog_item (GtkAppChooserButton *self) |
996 | { |
997 | g_return_val_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self), FALSE); |
998 | |
999 | return self->show_dialog_item; |
1000 | } |
1001 | |
1002 | /** |
1003 | * gtk_app_chooser_button_set_show_dialog_item: (attributes org.gtk.Method.set_property=show-dialog-item) |
1004 | * @self: a `GtkAppChooserButton` |
1005 | * @setting: the new value for [property@Gtk.AppChooserButton:show-dialog-item] |
1006 | * |
1007 | * Sets whether the dropdown menu of this button should show an |
1008 | * entry to trigger a `GtkAppChooserDialog`. |
1009 | */ |
1010 | void |
1011 | gtk_app_chooser_button_set_show_dialog_item (GtkAppChooserButton *self, |
1012 | gboolean setting) |
1013 | { |
1014 | if (self->show_dialog_item != setting) |
1015 | { |
1016 | self->show_dialog_item = setting; |
1017 | |
1018 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_SHOW_DIALOG_ITEM]); |
1019 | |
1020 | gtk_app_chooser_refresh (GTK_APP_CHOOSER (self)); |
1021 | } |
1022 | } |
1023 | |
1024 | /** |
1025 | * gtk_app_chooser_button_get_show_default_item: (attributes org.gtk.Method.get_property=show-default-item) |
1026 | * @self: a `GtkAppChooserButton` |
1027 | * |
1028 | * Returns whether the dropdown menu should show the default |
1029 | * application at the top. |
1030 | * |
1031 | * Returns: the value of [property@Gtk.AppChooserButton:show-default-item] |
1032 | */ |
1033 | gboolean |
1034 | gtk_app_chooser_button_get_show_default_item (GtkAppChooserButton *self) |
1035 | { |
1036 | g_return_val_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self), FALSE); |
1037 | |
1038 | return self->show_default_item; |
1039 | } |
1040 | |
1041 | /** |
1042 | * gtk_app_chooser_button_set_show_default_item: (attributes org.gtk.Method.set_property=show-default-item) |
1043 | * @self: a `GtkAppChooserButton` |
1044 | * @setting: the new value for [property@Gtk.AppChooserButton:show-default-item] |
1045 | * |
1046 | * Sets whether the dropdown menu of this button should show the |
1047 | * default application for the given content type at top. |
1048 | */ |
1049 | void |
1050 | gtk_app_chooser_button_set_show_default_item (GtkAppChooserButton *self, |
1051 | gboolean setting) |
1052 | { |
1053 | g_return_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self)); |
1054 | |
1055 | if (self->show_default_item != setting) |
1056 | { |
1057 | self->show_default_item = setting; |
1058 | |
1059 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_SHOW_DEFAULT_ITEM]); |
1060 | |
1061 | gtk_app_chooser_refresh (GTK_APP_CHOOSER (self)); |
1062 | } |
1063 | } |
1064 | |
1065 | /** |
1066 | * gtk_app_chooser_button_set_heading: (attributes org.gtk.Method.set_property=heading) |
1067 | * @self: a `GtkAppChooserButton` |
1068 | * @heading: a string containing Pango markup |
1069 | * |
1070 | * Sets the text to display at the top of the dialog. |
1071 | * |
1072 | * If the heading is not set, the dialog displays a default text. |
1073 | */ |
1074 | void |
1075 | gtk_app_chooser_button_set_heading (GtkAppChooserButton *self, |
1076 | const char *heading) |
1077 | { |
1078 | g_return_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self)); |
1079 | |
1080 | g_free (mem: self->heading); |
1081 | self->heading = g_strdup (str: heading); |
1082 | |
1083 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_HEADING]); |
1084 | } |
1085 | |
1086 | /** |
1087 | * gtk_app_chooser_button_get_heading: (attributes org.gtk.Method.get_property=heading) |
1088 | * @self: a `GtkAppChooserButton` |
1089 | * |
1090 | * Returns the text to display at the top of the dialog. |
1091 | * |
1092 | * Returns: (nullable): the text to display at the top of the dialog, |
1093 | * or %NULL, in which case a default text is displayed |
1094 | */ |
1095 | const char * |
1096 | gtk_app_chooser_button_get_heading (GtkAppChooserButton *self) |
1097 | { |
1098 | g_return_val_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self), NULL); |
1099 | |
1100 | return self->heading; |
1101 | } |
1102 | |
1103 | /** |
1104 | * gtk_app_chooser_button_set_modal: (attributes org.gtk.Method.set_property=modal) |
1105 | * @self: a `GtkAppChooserButton` |
1106 | * @modal: %TRUE to make the dialog modal |
1107 | * |
1108 | * Sets whether the dialog should be modal. |
1109 | */ |
1110 | void |
1111 | gtk_app_chooser_button_set_modal (GtkAppChooserButton *self, |
1112 | gboolean modal) |
1113 | { |
1114 | g_return_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self)); |
1115 | |
1116 | if (self->modal == modal) |
1117 | return; |
1118 | |
1119 | self->modal = modal; |
1120 | |
1121 | g_object_notify (G_OBJECT (self), property_name: "modal" ); |
1122 | } |
1123 | |
1124 | /** |
1125 | * gtk_app_chooser_button_get_modal: (attributes org.gtk.Method.get_property=modal) |
1126 | * @self: a `GtkAppChooserButton` |
1127 | * |
1128 | * Gets whether the dialog is modal. |
1129 | * |
1130 | * Returns: %TRUE if the dialog is modal |
1131 | */ |
1132 | gboolean |
1133 | gtk_app_chooser_button_get_modal (GtkAppChooserButton *self) |
1134 | { |
1135 | g_return_val_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self), FALSE); |
1136 | |
1137 | return self->modal; |
1138 | } |
1139 | |
1140 | |