gtkappchooserbutton.c 28.9 KB
Newer Older
Matthias Clasen's avatar
Matthias Clasen committed
1
/* gtkappchooserbutton.c: an app-chooser combobox
2 3 4 5 6 7 8 9 10 11 12 13 14 15
 *
 * Copyright (C) 2010 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
Javier Jardón's avatar
Javier Jardón committed
16
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
17 18 19 20
 *
 * Authors: Cosimo Cecchi <ccecchi@redhat.com>
 */

Matthias Clasen's avatar
Matthias Clasen committed
21 22 23 24 25 26 27
/**
 * SECTION:gtkappchooserbutton
 * @Title: GtkAppChooserButton
 * @Short_description: A button to launch an application chooser dialog
 *
 * The #GtkAppChooserButton is a widget that lets the user select
 * an application. It implements the #GtkAppChooser interface.
28 29 30
 *
 * Initially, a #GtkAppChooserButton selects the first application
 * in its list, which will either be the most-recently used application
31
 * or, if #GtkAppChooserButton:show-default-item is %TRUE, the
32 33 34 35
 * default application.
 *
 * The list of applications shown in a #GtkAppChooserButton includes
 * the recommended applications for the given content type. When
36
 * #GtkAppChooserButton:show-default-item is set, the default application
37
 * is also included. To let the user chooser other applications,
38
 * you can set the #GtkAppChooserButton:show-dialog-item property,
39 40 41 42 43 44 45 46 47
 * which allows to open a full #GtkAppChooserDialog.
 *
 * It is possible to add custom items to the list, using
 * gtk_app_chooser_button_append_custom_item(). These items cause
 * the #GtkAppChooserButton::custom-item-activated signal to be
 * emitted when they are selected.
 *
 * To track changes in the selected application, use the
 * #GtkComboBox::changed signal.
Matthias Clasen's avatar
Matthias Clasen committed
48
 */
Matthias Clasen's avatar
Matthias Clasen committed
49
#include "config.h"
50

51
#include "gtkappchooserbutton.h"
52 53

#include "gtkappchooser.h"
54
#include "gtkappchooserdialog.h"
55 56 57 58 59
#include "gtkappchooserprivate.h"
#include "gtkcelllayout.h"
#include "gtkcellrendererpixbuf.h"
#include "gtkcellrenderertext.h"
#include "gtkcombobox.h"
60 61
#include "gtkdialog.h"
#include "gtkintl.h"
62
#include "gtkmarshalers.h"
63 64

enum {
65
  PROP_SHOW_DIALOG_ITEM = 1,
66
  PROP_SHOW_DEFAULT_ITEM,
67 68 69 70
  PROP_HEADING,
  NUM_PROPERTIES,

  PROP_CONTENT_TYPE = NUM_PROPERTIES
71 72
};

73 74 75 76 77
enum {
  SIGNAL_CUSTOM_ITEM_ACTIVATED,
  NUM_SIGNALS
};

78 79 80
enum {
  COLUMN_APP_INFO,
  COLUMN_NAME,
81
  COLUMN_LABEL,
82 83 84 85 86 87
  COLUMN_ICON,
  COLUMN_CUSTOM,
  COLUMN_SEPARATOR,
  NUM_COLUMNS,
};

88
#define CUSTOM_ITEM_OTHER_APP "gtk-internal-item-other-app"
89

Matthias Clasen's avatar
Matthias Clasen committed
90
static void app_chooser_iface_init  (GtkAppChooserIface *iface);
91

92
static void real_insert_custom_item (GtkAppChooserButton *self,
Matthias Clasen's avatar
Matthias Clasen committed
93 94 95 96 97
                                     const gchar         *name,
                                     const gchar         *label,
                                     GIcon               *icon,
                                     gboolean             custom,
                                     GtkTreeIter         *iter);
98

Matthias Clasen's avatar
Matthias Clasen committed
99 100 101
static void real_insert_separator   (GtkAppChooserButton *self,
                                     gboolean             custom,
                                     GtkTreeIter         *iter);
102

103
static guint signals[NUM_SIGNALS] = { 0, };
104
static GParamSpec *properties[NUM_PROPERTIES];
105

106
struct _GtkAppChooserButtonPrivate {
107
  GtkListStore *store;
108 109

  gchar *content_type;
110
  gchar *heading;
111 112
  gint last_active;
  gboolean show_dialog_item;
113
  gboolean show_default_item;
114 115

  GHashTable *custom_item_names;
116 117
};

118 119 120 121 122
G_DEFINE_TYPE_WITH_CODE (GtkAppChooserButton, gtk_app_chooser_button, GTK_TYPE_COMBO_BOX,
                         G_ADD_PRIVATE (GtkAppChooserButton)
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_APP_CHOOSER,
                                                app_chooser_iface_init));

123 124
static gboolean
row_separator_func (GtkTreeModel *model,
Matthias Clasen's avatar
Matthias Clasen committed
125 126
                    GtkTreeIter  *iter,
                    gpointer      user_data)
127 128 129 130
{
  gboolean separator;

  gtk_tree_model_get (model, iter,
Matthias Clasen's avatar
Matthias Clasen committed
131 132
                      COLUMN_SEPARATOR, &separator,
                      -1);
133 134 135 136

  return separator;
}

137 138
static void
get_first_iter (GtkListStore *store,
Matthias Clasen's avatar
Matthias Clasen committed
139
                GtkTreeIter  *iter)
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
{
  GtkTreeIter iter2;

  if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), iter))
    {
      /* the model is empty, append */
      gtk_list_store_append (store, iter);
    }
  else
    {
      gtk_list_store_insert_before (store, &iter2, iter);
      *iter = iter2;
    }
}

155
typedef struct {
156
  GtkAppChooserButton *self;
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
  GAppInfo *info;
  gint active_index;
} SelectAppData;

static void
select_app_data_free (SelectAppData *data)
{
  g_clear_object (&data->self);
  g_clear_object (&data->info);

  g_slice_free (SelectAppData, data);
}

static gboolean
select_application_func_cb (GtkTreeModel *model,
Matthias Clasen's avatar
Matthias Clasen committed
172 173 174
                            GtkTreePath  *path,
                            GtkTreeIter  *iter,
                            gpointer      user_data)
175 176
{
  SelectAppData *data = user_data;
Matthias Clasen's avatar
Matthias Clasen committed
177 178
  GAppInfo *app_to_match = data->info;
  GAppInfo *app = NULL;
179
  gboolean custom;
180
  gboolean result;
181 182

  gtk_tree_model_get (model, iter,
183 184 185
                      COLUMN_APP_INFO, &app,
                      COLUMN_CUSTOM, &custom,
                      -1);
186

187
  /* custom items are always after GAppInfos, so iterating further here
188 189 190
   * is just useless.
   */
  if (custom)
191 192
    result = TRUE;
  else if (g_app_info_equal (app, app_to_match))
193 194
    {
      gtk_combo_box_set_active_iter (GTK_COMBO_BOX (data->self), iter);
195
      result = TRUE;
196
    }
197 198 199 200
  else
    result = FALSE;

  g_object_unref (app);
201

202
  return result;
203 204 205
}

static void
206
gtk_app_chooser_button_select_application (GtkAppChooserButton *self,
Matthias Clasen's avatar
Matthias Clasen committed
207
                                           GAppInfo            *info)
208 209 210 211 212 213 214 215
{
  SelectAppData *data;

  data = g_slice_new0 (SelectAppData);
  data->self = g_object_ref (self);
  data->info = g_object_ref (info);

  gtk_tree_model_foreach (GTK_TREE_MODEL (self->priv->store),
216
                          select_application_func_cb, data);
217 218 219 220 221 222

  select_app_data_free (data);
}

static void
other_application_dialog_response_cb (GtkDialog *dialog,
Matthias Clasen's avatar
Matthias Clasen committed
223 224
                                      gint       response_id,
                                      gpointer   user_data)
225
{
226
  GtkAppChooserButton *self = user_data;
227 228 229 230 231
  GAppInfo *info;

  if (response_id != GTK_RESPONSE_OK)
    {
      /* reset the active item, otherwise we are stuck on
232
       * 'Other application…'
233
       */
234
      gtk_combo_box_set_active (GTK_COMBO_BOX (self), self->priv->last_active);
235 236 237 238 239 240
      gtk_widget_destroy (GTK_WIDGET (dialog));
      return;
    }

  info = gtk_app_chooser_get_app_info (GTK_APP_CHOOSER (dialog));

241 242
  gtk_widget_destroy (GTK_WIDGET (dialog));

243 244
  /* refresh the combobox to get the new application */
  gtk_app_chooser_refresh (GTK_APP_CHOOSER (self));
245
  gtk_app_chooser_button_select_application (self, info);
246 247 248 249 250

  g_object_unref (info);
}

static void
251
other_application_item_activated_cb (GtkAppChooserButton *self)
252 253 254 255 256
{
  GtkWidget *dialog, *widget;
  GtkWindow *toplevel;

  toplevel = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self)));
Matthias Clasen's avatar
Matthias Clasen committed
257 258
  dialog = gtk_app_chooser_dialog_new_for_content_type (toplevel,
                                                        GTK_DIALOG_DESTROY_WITH_PARENT,
259
                                                        self->priv->content_type);
260 261

  gtk_window_set_modal (GTK_WINDOW (dialog), gtk_window_get_modal (toplevel));
262 263
  gtk_app_chooser_dialog_set_heading (GTK_APP_CHOOSER_DIALOG (dialog),
                                      self->priv->heading);
264

265 266
  widget = gtk_app_chooser_dialog_get_widget (GTK_APP_CHOOSER_DIALOG (dialog));
  g_object_set (widget,
267 268 269
                "show-fallback", TRUE,
                "show-other", TRUE,
                NULL);
270 271 272
  gtk_widget_show (dialog);

  g_signal_connect (dialog, "response",
273
                    G_CALLBACK (other_application_dialog_response_cb), self);
274 275 276
}

static void
277
gtk_app_chooser_button_ensure_dialog_item (GtkAppChooserButton *self,
Matthias Clasen's avatar
Matthias Clasen committed
278
                                           GtkTreeIter         *prev_iter)
279
{
280
  GtkTreeIter iter, iter2;
281

282
  if (!self->priv->show_dialog_item || !self->priv->content_type)
283 284
    return;

285 286 287 288 289
  if (prev_iter == NULL)
    gtk_list_store_append (self->priv->store, &iter);
  else
    gtk_list_store_insert_after (self->priv->store, &iter, prev_iter);

290
  real_insert_separator (self, FALSE, &iter);
291
  iter2 = iter;
292

293
  gtk_list_store_insert_after (self->priv->store, &iter, &iter2);
294
  real_insert_custom_item (self, CUSTOM_ITEM_OTHER_APP,
295
                           _("Other application…"), NULL,
296
                           FALSE, &iter);
297 298
}

299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
static void
insert_one_application (GtkAppChooserButton *self,
                        GAppInfo            *app,
                        GtkTreeIter         *iter)
{
  GIcon *icon;

  icon = g_app_info_get_icon (app);

  if (icon == NULL)
    icon = g_themed_icon_new ("application-x-executable");
  else
    g_object_ref (icon);

  gtk_list_store_set (self->priv->store, iter,
                      COLUMN_APP_INFO, app,
                      COLUMN_LABEL, g_app_info_get_name (app),
                      COLUMN_ICON, icon,
                      COLUMN_CUSTOM, FALSE,
                      -1);

  g_object_unref (icon);
}

323
static void
324
gtk_app_chooser_button_populate (GtkAppChooserButton *self)
325 326
{
  GList *recommended_apps = NULL, *l;
327
  GAppInfo *app, *default_app = NULL;
328
  GtkTreeIter iter, iter2;
329
  gboolean cycled_recommended;
330

Fridrich Strba's avatar
Fridrich Strba committed
331
#ifndef G_OS_WIN32
332 333
  if (self->priv->content_type)
    recommended_apps = g_app_info_get_recommended_for_type (self->priv->content_type);
Fridrich Strba's avatar
Fridrich Strba committed
334
#endif
335
  cycled_recommended = FALSE;
336

337 338
  if (self->priv->show_default_item)
    {
339 340
      if (self->priv->content_type)
        default_app = g_app_info_get_default_for_type (self->priv->content_type, FALSE);
341 342 343 344 345 346 347 348 349 350 351 352

      if (default_app != NULL)
        {
          get_first_iter (self->priv->store, &iter);
          cycled_recommended = TRUE;

          insert_one_application (self, default_app, &iter);

          g_object_unref (default_app);
        }
    }

353 354 355 356
  for (l = recommended_apps; l != NULL; l = l->next)
    {
      app = l->data;

357 358
      if (default_app != NULL && g_app_info_equal (app, default_app))
        continue;
359

360
      if (cycled_recommended)
Matthias Clasen's avatar
Matthias Clasen committed
361
        {
362 363
          gtk_list_store_insert_after (self->priv->store, &iter2, &iter);
          iter = iter2;
Matthias Clasen's avatar
Matthias Clasen committed
364
        }
365
      else
Matthias Clasen's avatar
Matthias Clasen committed
366
        {
367 368
          get_first_iter (self->priv->store, &iter);
          cycled_recommended = TRUE;
Matthias Clasen's avatar
Matthias Clasen committed
369
        }
370

371
      insert_one_application (self, app, &iter);
372 373
    }

374 375 376
  if (recommended_apps != NULL)
    g_list_free_full (recommended_apps, g_object_unref);

377 378 379 380 381
  if (!cycled_recommended)
    gtk_app_chooser_button_ensure_dialog_item (self, NULL);
  else
    gtk_app_chooser_button_ensure_dialog_item (self, &iter);

382 383 384 385
  gtk_combo_box_set_active (GTK_COMBO_BOX (self), 0);
}

static void
386
gtk_app_chooser_button_build_ui (GtkAppChooserButton *self)
387 388
{
  GtkCellRenderer *cell;
389
  GtkCellArea *area;
390 391

  gtk_combo_box_set_model (GTK_COMBO_BOX (self),
Matthias Clasen's avatar
Matthias Clasen committed
392
                           GTK_TREE_MODEL (self->priv->store));
393

394 395
  area = gtk_cell_layout_get_area (GTK_CELL_LAYOUT (self));

396
  gtk_combo_box_set_row_separator_func (GTK_COMBO_BOX (self),
Matthias Clasen's avatar
Matthias Clasen committed
397
                                        row_separator_func, NULL, NULL);
398 399

  cell = gtk_cell_renderer_pixbuf_new ();
400 401 402 403 404
  gtk_cell_area_add_with_properties (area, cell,
                                     "align", FALSE,
                                     "expand", FALSE,
                                     "fixed-size", FALSE,
                                     NULL);
405
  gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (self), cell,
Matthias Clasen's avatar
Matthias Clasen committed
406 407
                                  "gicon", COLUMN_ICON,
                                  NULL);
408 409

  cell = gtk_cell_renderer_text_new ();
410 411 412 413
  gtk_cell_area_add_with_properties (area, cell,
                                     "align", FALSE,
                                     "expand", TRUE,
                                     NULL);
414
  gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (self), cell,
415
                                  "text", COLUMN_LABEL,
Matthias Clasen's avatar
Matthias Clasen committed
416
                                  NULL);
417

418
  gtk_app_chooser_button_populate (self);
419 420 421
}

static void
422
gtk_app_chooser_button_remove_non_custom (GtkAppChooserButton *self)
423 424 425 426 427
{
  GtkTreeModel *model;
  GtkTreeIter iter;
  gboolean custom, res;

Matthias Clasen's avatar
Matthias Clasen committed
428
  model = GTK_TREE_MODEL (self->priv->store);
429 430 431 432 433 434

  if (!gtk_tree_model_get_iter_first (model, &iter))
    return;

  do {
    gtk_tree_model_get (model, &iter,
Matthias Clasen's avatar
Matthias Clasen committed
435 436
                        COLUMN_CUSTOM, &custom,
                        -1);
437 438
    if (custom)
      res = gtk_tree_model_iter_next (model, &iter);
439 440
    else
      res = gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
441 442 443 444
  } while (res);
}

static void
445
gtk_app_chooser_button_changed (GtkComboBox *object)
446
{
447
  GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (object);
448
  GtkTreeIter iter;
449 450 451
  gchar *name = NULL;
  gboolean custom;
  GQuark name_quark;
452

453 454
  if (!gtk_combo_box_get_active_iter (object, &iter))
    return;
455 456

  gtk_tree_model_get (GTK_TREE_MODEL (self->priv->store), &iter,
457 458
                      COLUMN_NAME, &name,
                      COLUMN_CUSTOM, &custom,
Matthias Clasen's avatar
Matthias Clasen committed
459
                      -1);
460

461 462 463 464 465 466
  if (name != NULL)
    {
      if (custom)
        {
          name_quark = g_quark_from_string (name);
          g_signal_emit (self, signals[SIGNAL_CUSTOM_ITEM_ACTIVATED], name_quark, name);
467
          self->priv->last_active = gtk_combo_box_get_active (object);
468 469 470 471 472 473 474 475 476
        }
      else
        {
          /* trigger the dialog internally */
          other_application_item_activated_cb (self);
        }

      g_free (name);
    }
477 478
  else
    self->priv->last_active = gtk_combo_box_get_active (object);
479 480 481
}

static void
482
gtk_app_chooser_button_refresh (GtkAppChooser *object)
483
{
484
  GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (object);
485

486 487
  gtk_app_chooser_button_remove_non_custom (self);
  gtk_app_chooser_button_populate (self);
488 489 490
}

static GAppInfo *
491
gtk_app_chooser_button_get_app_info (GtkAppChooser *object)
492
{
493
  GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (object);
494 495 496 497 498 499 500
  GtkTreeIter iter;
  GAppInfo *info;

  if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (self), &iter))
    return NULL;

  gtk_tree_model_get (GTK_TREE_MODEL (self->priv->store), &iter,
Matthias Clasen's avatar
Matthias Clasen committed
501 502
                      COLUMN_APP_INFO, &info,
                      -1);
503 504 505 506 507

  return info;
}

static void
508
gtk_app_chooser_button_constructed (GObject *obj)
509
{
510
  GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (obj);
511

512 513
  if (G_OBJECT_CLASS (gtk_app_chooser_button_parent_class)->constructed != NULL)
    G_OBJECT_CLASS (gtk_app_chooser_button_parent_class)->constructed (obj);
514

515
  gtk_app_chooser_button_build_ui (self);
516 517 518
}

static void
519 520 521 522
gtk_app_chooser_button_set_property (GObject      *obj,
                                     guint         property_id,
                                     const GValue *value,
                                     GParamSpec   *pspec)
523
{
524
  GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (obj);
525 526 527 528 529 530

  switch (property_id)
    {
    case PROP_CONTENT_TYPE:
      self->priv->content_type = g_value_dup_string (value);
      break;
531
    case PROP_SHOW_DIALOG_ITEM:
532
      gtk_app_chooser_button_set_show_dialog_item (self, g_value_get_boolean (value));
533
      break;
534 535 536
    case PROP_SHOW_DEFAULT_ITEM:
      gtk_app_chooser_button_set_show_default_item (self, g_value_get_boolean (value));
      break;
537 538 539
    case PROP_HEADING:
      gtk_app_chooser_button_set_heading (self, g_value_get_string (value));
      break;
540 541 542 543 544 545 546
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec);
      break;
    }
}

static void
547 548 549 550
gtk_app_chooser_button_get_property (GObject    *obj,
                                     guint       property_id,
                                     GValue     *value,
                                     GParamSpec *pspec)
551
{
552
  GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (obj);
553 554 555 556 557 558

  switch (property_id)
    {
    case PROP_CONTENT_TYPE:
      g_value_set_string (value, self->priv->content_type);
      break;
559 560 561
    case PROP_SHOW_DIALOG_ITEM:
      g_value_set_boolean (value, self->priv->show_dialog_item);
      break;
562 563 564
    case PROP_SHOW_DEFAULT_ITEM:
      g_value_set_boolean (value, self->priv->show_default_item);
      break;
565 566 567
    case PROP_HEADING:
      g_value_set_string (value, self->priv->heading);
      break;
568 569 570 571 572 573 574
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec);
      break;
    }
}

static void
575
gtk_app_chooser_button_finalize (GObject *obj)
576
{
577
  GtkAppChooserButton *self = GTK_APP_CHOOSER_BUTTON (obj);
578

579
  g_hash_table_destroy (self->priv->custom_item_names);
580
  g_free (self->priv->content_type);
581
  g_free (self->priv->heading);
582 583
  g_object_unref (self->priv->store);

584
  G_OBJECT_CLASS (gtk_app_chooser_button_parent_class)->finalize (obj);
585 586 587 588 589
}

static void
app_chooser_iface_init (GtkAppChooserIface *iface)
{
590 591
  iface->get_app_info = gtk_app_chooser_button_get_app_info;
  iface->refresh = gtk_app_chooser_button_refresh;
592 593 594
}

static void
595
gtk_app_chooser_button_class_init (GtkAppChooserButtonClass *klass)
596 597 598 599
{
  GObjectClass *oclass = G_OBJECT_CLASS (klass);
  GtkComboBoxClass *combo_class = GTK_COMBO_BOX_CLASS (klass);

600 601 602 603
  oclass->set_property = gtk_app_chooser_button_set_property;
  oclass->get_property = gtk_app_chooser_button_get_property;
  oclass->finalize = gtk_app_chooser_button_finalize;
  oclass->constructed = gtk_app_chooser_button_constructed;
604

605
  combo_class->changed = gtk_app_chooser_button_changed;
606 607 608

  g_object_class_override_property (oclass, PROP_CONTENT_TYPE, "content-type");

609 610 611
  /**
   * GtkAppChooserButton:show-dialog-item:
   *
612 613 614
   * The #GtkAppChooserButton:show-dialog-item property determines
   * whether the dropdown menu should show an item that triggers
   * a #GtkAppChooserDialog when clicked.
615
   */
616
  properties[PROP_SHOW_DIALOG_ITEM] =
617
    g_param_spec_boolean ("show-dialog-item",
618
                          P_("Include an 'Other…' item"),
619 620
                          P_("Whether the combobox should include an item that triggers a GtkAppChooserDialog"),
                          FALSE,
621
                          G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);
622

623 624 625 626 627 628 629 630 631
  /**
   * GtkAppChooserButton:show-default-item:
   *
   * The #GtkAppChooserButton:show-default-item property determines
   * whether the dropdown menu should show the default application
   * on top for the provided content type.
   *
   * Since: 3.2
   */
632
  properties[PROP_SHOW_DEFAULT_ITEM] =
633 634 635 636
    g_param_spec_boolean ("show-default-item",
                          P_("Show default item"),
                          P_("Whether the combobox should show the default application on top"),
                          FALSE,
637
                          G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);
638

639 640 641 642 643 644
  /**
   * GtkAppChooserButton:heading:
   *
   * The text to show at the top of the dialog that can be
   * opened from the button. The string may contain Pango markup.
   */
645 646 647 648 649 650 651 652
  properties[PROP_HEADING] =
    g_param_spec_string ("heading",
                         P_("Heading"),
                         P_("The text to show at the top of the dialog"),
                         NULL,
                         G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (oclass, NUM_PROPERTIES, properties);
653

Cosimo Cecchi's avatar
Cosimo Cecchi committed
654 655 656 657 658 659 660 661 662
  /**
   * GtkAppChooserButton::custom-item-activated:
   * @self: the object which received the signal
   * @item_name: the name of the activated item
   *
   * Emitted when a custom item, previously added with
   * gtk_app_chooser_button_append_custom_item(), is activated from the
   * dropdown menu.
   */
663
  signals[SIGNAL_CUSTOM_ITEM_ACTIVATED] =
664
    g_signal_new (I_("custom-item-activated"),
665 666 667 668
                  GTK_TYPE_APP_CHOOSER_BUTTON,
                  G_SIGNAL_RUN_FIRST | G_SIGNAL_DETAILED,
                  G_STRUCT_OFFSET (GtkAppChooserButtonClass, custom_item_activated),
                  NULL, NULL,
669
                  NULL,
670 671
                  G_TYPE_NONE,
                  1, G_TYPE_STRING);
672 673 674
}

static void
675
gtk_app_chooser_button_init (GtkAppChooserButton *self)
676
{
677
  self->priv = gtk_app_chooser_button_get_instance_private (self);
678
  self->priv->custom_item_names =
Matthias Clasen's avatar
Matthias Clasen committed
679
    g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
680 681 682 683 684 685 686
  self->priv->store = gtk_list_store_new (NUM_COLUMNS,
                                          G_TYPE_APP_INFO,
                                          G_TYPE_STRING, /* name */
                                          G_TYPE_STRING, /* label */
                                          G_TYPE_ICON,
                                          G_TYPE_BOOLEAN, /* separator */
                                          G_TYPE_BOOLEAN); /* custom */
687 688
}

689 690
static gboolean
app_chooser_button_iter_from_custom_name (GtkAppChooserButton *self,
691 692
                                          const gchar         *name,
                                          GtkTreeIter         *set_me)
693 694 695 696
{
  GtkTreeIter iter;
  gchar *custom_name = NULL;

Matthias Clasen's avatar
Matthias Clasen committed
697
  if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->priv->store), &iter))
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718
    return FALSE;

  do {
    gtk_tree_model_get (GTK_TREE_MODEL (self->priv->store), &iter,
                        COLUMN_NAME, &custom_name,
                        -1);

    if (g_strcmp0 (custom_name, name) == 0)
      {
        g_free (custom_name);
        *set_me = iter;

        return TRUE;
      }

    g_free (custom_name);
  } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->priv->store), &iter));

  return FALSE;
}

719
static void
720
real_insert_custom_item (GtkAppChooserButton *self,
Matthias Clasen's avatar
Matthias Clasen committed
721 722 723 724 725
                         const gchar         *name,
                         const gchar         *label,
                         GIcon               *icon,
                         gboolean             custom,
                         GtkTreeIter         *iter)
726
{
727 728
  if (custom)
    {
Matthias Clasen's avatar
Matthias Clasen committed
729
      if (g_hash_table_lookup (self->priv->custom_item_names, name) != NULL)
730 731 732 733 734
        {
          g_warning ("Attempting to add custom item %s to GtkAppChooserButton, "
                     "when there's already an item with the same name", name);
          return;
        }
735

736 737 738
      g_hash_table_insert (self->priv->custom_item_names,
                           g_strdup (name), GINT_TO_POINTER (1));
    }
739 740

  gtk_list_store_set (self->priv->store, iter,
741
                      COLUMN_NAME, name,
742
                      COLUMN_LABEL, label,
743 744 745 746
                      COLUMN_ICON, icon,
                      COLUMN_CUSTOM, custom,
                      COLUMN_SEPARATOR, FALSE,
                      -1);
747 748 749
}

static void
750
real_insert_separator (GtkAppChooserButton *self,
Matthias Clasen's avatar
Matthias Clasen committed
751 752
                       gboolean             custom,
                       GtkTreeIter         *iter)
753 754
{
  gtk_list_store_set (self->priv->store, iter,
Matthias Clasen's avatar
Matthias Clasen committed
755 756 757
                      COLUMN_CUSTOM, custom,
                      COLUMN_SEPARATOR, TRUE,
                      -1);
758 759
}

Matthias Clasen's avatar
Matthias Clasen committed
760
/**
761
 * gtk_app_chooser_button_new:
Matthias Clasen's avatar
Matthias Clasen committed
762 763
 * @content_type: the content type to show applications for
 *
764
 * Creates a new #GtkAppChooserButton for applications
Matthias Clasen's avatar
Matthias Clasen committed
765 766
 * that can handle content of the given type.
 *
767
 * Returns: a newly created #GtkAppChooserButton
Matthias Clasen's avatar
Matthias Clasen committed
768 769 770
 *
 * Since: 3.0
 */
771
GtkWidget *
772
gtk_app_chooser_button_new (const gchar *content_type)
773 774 775
{
  g_return_val_if_fail (content_type != NULL, NULL);

776
  return g_object_new (GTK_TYPE_APP_CHOOSER_BUTTON,
Matthias Clasen's avatar
Matthias Clasen committed
777 778
                       "content-type", content_type,
                       NULL);
779 780
}

Matthias Clasen's avatar
Matthias Clasen committed
781
/**
782 783
 * gtk_app_chooser_button_append_separator:
 * @self: a #GtkAppChooserButton
Matthias Clasen's avatar
Matthias Clasen committed
784 785 786 787 788 789
 *
 * Appends a separator to the list of applications that is shown
 * in the popup.
 *
 * Since: 3.0
 */
790
void
791
gtk_app_chooser_button_append_separator (GtkAppChooserButton *self)
792 793 794
{
  GtkTreeIter iter;

795
  g_return_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self));
796 797

  gtk_list_store_append (self->priv->store, &iter);
798
  real_insert_separator (self, TRUE, &iter);
799 800
}

Matthias Clasen's avatar
Matthias Clasen committed
801
/**
802 803
 * gtk_app_chooser_button_append_custom_item:
 * @self: a #GtkAppChooserButton
804
 * @name: the name of the custom item
Matthias Clasen's avatar
Matthias Clasen committed
805 806 807 808
 * @label: the label for the custom item
 * @icon: the icon for the custom item
 *
 * Appends a custom item to the list of applications that is shown
809
 * in the popup; the item name must be unique per-widget.
810 811 812
 * Clients can use the provided name as a detail for the
 * #GtkAppChooserButton::custom-item-activated signal, to add a
 * callback for the activation of a particular custom item in the list.
813
 * See also gtk_app_chooser_button_append_separator().
Matthias Clasen's avatar
Matthias Clasen committed
814 815 816
 *
 * Since: 3.0
 */
817
void
818 819 820 821
gtk_app_chooser_button_append_custom_item (GtkAppChooserButton *self,
                                           const gchar         *name,
                                           const gchar         *label,
                                           GIcon               *icon)
822 823 824
{
  GtkTreeIter iter;

825
  g_return_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self));
826
  g_return_if_fail (name != NULL);
827 828

  gtk_list_store_append (self->priv->store, &iter);
829
  real_insert_custom_item (self, name, label, icon, TRUE, &iter);
830 831
}

832
/**
833
 * gtk_app_chooser_button_set_active_custom_item:
834 835 836 837 838
 * @self: a #GtkAppChooserButton
 * @name: the name of the custom item
 *
 * Selects a custom item previously added with
 * gtk_app_chooser_button_append_custom_item().
839 840 841
 *
 * Use gtk_app_chooser_refresh() to bring the selection
 * to its initial state.
842 843 844 845 846 847 848 849 850 851 852 853
 *
 * Since: 3.0
 */
void
gtk_app_chooser_button_set_active_custom_item (GtkAppChooserButton *self,
                                               const gchar         *name)
{
  GtkTreeIter iter;

  g_return_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self));
  g_return_if_fail (name != NULL);

Matthias Clasen's avatar
Matthias Clasen committed
854
  if (!g_hash_table_contains (self->priv->custom_item_names, name) ||
855 856
      !app_chooser_button_iter_from_custom_name (self, name, &iter))
    {
Matthias Clasen's avatar
Matthias Clasen committed
857
      g_warning ("Can't find the item named %s in the app chooser.", name);
858 859 860 861 862 863
      return;
    }

  gtk_combo_box_set_active_iter (GTK_COMBO_BOX (self), &iter);
}

864 865 866 867 868 869 870 871 872 873 874
/**
 * gtk_app_chooser_button_get_show_dialog_item:
 * @self: a #GtkAppChooserButton
 *
 * Returns the current value of the #GtkAppChooserButton:show-dialog-item
 * property.
 *
 * Returns: the value of #GtkAppChooserButton:show-dialog-item
 *
 * Since: 3.0
 */
875
gboolean
876
gtk_app_chooser_button_get_show_dialog_item (GtkAppChooserButton *self)
877
{
878
  g_return_val_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self), FALSE);
879 880 881 882

  return self->priv->show_dialog_item;
}

883
/**
884
 * gtk_app_chooser_button_set_show_dialog_item:
885 886 887 888 889 890 891 892
 * @self: a #GtkAppChooserButton
 * @setting: the new value for #GtkAppChooserButton:show-dialog-item
 *
 * Sets whether the dropdown menu of this button should show an
 * entry to trigger a #GtkAppChooserDialog.
 *
 * Since: 3.0
 */
893
void
894
gtk_app_chooser_button_set_show_dialog_item (GtkAppChooserButton *self,
895
                                             gboolean             setting)
896 897 898 899 900
{
  if (self->priv->show_dialog_item != setting)
    {
      self->priv->show_dialog_item = setting;

901
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_DIALOG_ITEM]);
902 903 904

      gtk_app_chooser_refresh (GTK_APP_CHOOSER (self));
    }
905
}
906

907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943
/**
 * gtk_app_chooser_button_get_show_default_item:
 * @self: a #GtkAppChooserButton
 *
 * Returns the current value of the #GtkAppChooserButton:show-default-item
 * property.
 *
 * Returns: the value of #GtkAppChooserButton:show-default-item
 *
 * Since: 3.2
 */
gboolean
gtk_app_chooser_button_get_show_default_item (GtkAppChooserButton *self)
{
  g_return_val_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self), FALSE);

  return self->priv->show_default_item;
}

/**
 * gtk_app_chooser_button_set_show_default_item:
 * @self: a #GtkAppChooserButton
 * @setting: the new value for #GtkAppChooserButton:show-default-item
 *
 * Sets whether the dropdown menu of this button should show the
 * default application for the given content type at top.
 *
 * Since: 3.2
 */
void
gtk_app_chooser_button_set_show_default_item (GtkAppChooserButton *self,
                                              gboolean             setting)
{
  if (self->priv->show_default_item != setting)
    {
      self->priv->show_default_item = setting;

944
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_DEFAULT_ITEM]);
945 946 947 948 949

      gtk_app_chooser_refresh (GTK_APP_CHOOSER (self));
    }
}

950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966
/**
 * gtk_app_chooser_button_set_heading:
 * @self: a #GtkAppChooserButton
 * @heading: a string containing Pango markup
 *
 * Sets the text to display at the top of the dialog.
 * If the heading is not set, the dialog displays a default text.
 */
void
gtk_app_chooser_button_set_heading (GtkAppChooserButton *self,
                                    const gchar         *heading)
{
  g_return_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self));

  g_free (self->priv->heading);
  self->priv->heading = g_strdup (heading);

967
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HEADING]);
968 969 970 971 972 973 974 975
}

/**
 * gtk_app_chooser_button_get_heading:
 * @self: a #GtkAppChooserButton
 *
 * Returns the text to display at the top of the dialog.
 *
976
 * Returns: (nullable): the text to display at the top of the dialog,
977
 *     or %NULL, in which case a default text is displayed
978 979 980 981 982 983 984 985
 */
const gchar *
gtk_app_chooser_button_get_heading (GtkAppChooserButton *self)
{
  g_return_val_if_fail (GTK_IS_APP_CHOOSER_BUTTON (self), NULL);

  return self->priv->heading;
}