/*
 * gtkappchooserwidget.c: an app-chooser widget
 *
 * Copyright (C) 2004 Novell, Inc.
 * Copyright (C) 2007, 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
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Dave Camp <dave@novell.com>
 *          Alexander Larsson <alexl@redhat.com>
 *          Cosimo Cecchi <ccecchi@redhat.com>
 */

#include "config.h"

#include "gtkappchooserwidget.h"

#include "gtkmarshalers.h"
#include "gtkappchooserwidget.h"
#include "gtkappchooserprivate.h"
#include "gtkliststore.h"
#include "gtkorientable.h"
#include "gtkscrolledwindow.h"
#include "gtklabel.h"
#include "gtkgestureclick.h"
#include "gtkwidgetprivate.h"
#include "gtkprivate.h"
#include "gtkbox.h"
#include "gtklistview.h"
#include "gtksignallistitemfactory.h"
#include "gtklistitem.h"
#include "gtksingleselection.h"
#include "gtkfilterlistmodel.h"
#include "gtkstringfilter.h"
#include "gtksortlistmodel.h"
#include "gtkstringsorter.h"
#include "gtkcustomsorter.h"
#include "gtklistheader.h"
#include "gtksearchentry.h"

#include <string.h>
#include <glib/gi18n-lib.h>
#include <gio/gio.h>

G_GNUC_BEGIN_IGNORE_DEPRECATIONS

/**
 * GtkAppChooserWidget:
 *
 * `GtkAppChooserWidget` is a widget for selecting applications.
 *
 * It is the main building block for [class@Gtk.AppChooserDialog].
 * Most applications only need to use the latter; but you can use
 * this widget as part of a larger widget if you have special needs.
 *
 * `GtkAppChooserWidget` offers detailed control over what applications
 * are shown, using the
 * [property@Gtk.AppChooserWidget:show-default],
 * [property@Gtk.AppChooserWidget:show-recommended],
 * [property@Gtk.AppChooserWidget:show-fallback],
 * [property@Gtk.AppChooserWidget:show-other] and
 * [property@Gtk.AppChooserWidget:show-all] properties. See the
 * [iface@Gtk.AppChooser] documentation for more information about these
 * groups of applications.
 *
 * To keep track of the selected application, use the
 * [signal@Gtk.AppChooserWidget::application-selected] and
 * [signal@Gtk.AppChooserWidget::application-activated] signals.
 *
 * ## CSS nodes
 *
 * `GtkAppChooserWidget` has a single CSS node with name appchooser.
 *
 * Deprecated: 4.10: The application selection widgets should be
 *   implemented according to the design of each platform and/or
 *   application requiring them.
 */

G_DECLARE_FINAL_TYPE (GtkAppItem, gtk_app_item, GTK, APP_ITEM, GObject)

struct _GtkAppItem
{
  GObject parent_instance;
  GAppInfo *app_info;
  gboolean is_default;
  gboolean is_recommended;
  gboolean is_fallback;
};

enum {
  ITEM_PROP_NAME = 1,
  NUM_ITEM_PROPS,
};

static GParamSpec *item_properties[NUM_ITEM_PROPS];

G_DEFINE_TYPE (GtkAppItem, gtk_app_item, G_TYPE_OBJECT)

static void
gtk_app_item_init (GtkAppItem *item)
{
}

static void
gtk_app_item_get_property (GObject    *object,
                           guint       prop_id,
                           GValue     *value,
                           GParamSpec *pspec)
{
  GtkAppItem *item = GTK_APP_ITEM (object);

  switch (prop_id)
    {
    case ITEM_PROP_NAME:
      g_value_set_string (value, g_app_info_get_display_name (item->app_info));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
gtk_app_item_finalize (GObject *object)
{
  GtkAppItem *item = GTK_APP_ITEM (object);

  g_object_unref (item->app_info);

  G_OBJECT_CLASS (gtk_app_item_parent_class)->finalize (object);
}

static void
gtk_app_item_class_init (GtkAppItemClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  object_class->get_property = gtk_app_item_get_property;
  object_class->finalize = gtk_app_item_finalize;

  item_properties[ITEM_PROP_NAME] = g_param_spec_string ("name", NULL, NULL,
                                                         NULL,
                                                         G_PARAM_READABLE | G_PARAM_STATIC_NAME);

  g_object_class_install_properties (object_class, NUM_ITEM_PROPS, item_properties);
}

static GtkAppItem *
gtk_app_item_new (GAppInfo *app_info,
                  gboolean is_default,
                  gboolean is_recommended,
                  gboolean is_fallback)
{
  GtkAppItem *item;

  item = g_object_new (gtk_app_item_get_type (), NULL);

  item->app_info = g_object_ref (app_info);
  item->is_default = is_default;
  item->is_recommended = is_recommended;
  item->is_fallback = is_fallback;

  return item;
}

typedef struct _GtkAppChooserWidgetClass   GtkAppChooserWidgetClass;

struct _GtkAppChooserWidget {
  GtkWidget parent_instance;

  GAppInfo *selected_app_info;

  GtkWidget *overlay;

  char *content_type;
  char *default_text;

  guint show_default     : 1;
  guint show_recommended : 1;
  guint show_fallback    : 1;
  guint show_other       : 1;
  guint show_all         : 1;

  GListStore *app_info_store;
  GtkListItemFactory *header_factory;
  GtkStringFilter *filter;
  GtkStringSorter *sorter;
  GtkWidget *program_list;
  GtkWidget *no_apps_label;
  GtkWidget *no_apps;

  GAppInfoMonitor *monitor;

  GtkWidget *popup_menu;
};

struct _GtkAppChooserWidgetClass {
  GtkWidgetClass parent_class;

  void (* application_selected)  (GtkAppChooserWidget *self,
                                  GAppInfo            *app_info);

  void (* application_activated) (GtkAppChooserWidget *self,
                                  GAppInfo            *app_info);
};

enum {
  PROP_SHOW_DEFAULT = 1,
  PROP_SHOW_RECOMMENDED,
  PROP_SHOW_FALLBACK,
  PROP_SHOW_OTHER,
  PROP_SHOW_ALL,
  PROP_DEFAULT_TEXT,
  /* GtkAppChooser */
  PROP_CONTENT_TYPE,
  N_PROPERTIES,
};

static GParamSpec *props[N_PROPERTIES] = { NULL, };

enum {
  SIGNAL_APPLICATION_SELECTED,
  SIGNAL_APPLICATION_ACTIVATED,
  N_SIGNALS
};

static guint signals[N_SIGNALS] = { 0, };

static void gtk_app_chooser_widget_iface_init (GtkAppChooserIface *iface);

G_DEFINE_TYPE_WITH_CODE (GtkAppChooserWidget, gtk_app_chooser_widget, GTK_TYPE_WIDGET,
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_APP_CHOOSER,
                                                gtk_app_chooser_widget_iface_init));

static gboolean
gtk_app_chooser_widget_add_section (GtkAppChooserWidget *self,
                                    gboolean             recommended,
                                    gboolean             fallback,
                                    GList               *applications,
                                    GList               *exclude_apps)
{
  gboolean retval;

  retval = FALSE;

  for (GList *l = applications; l != NULL; l = l->next)
    {
      GAppInfo *app = l->data;
      GtkAppItem *item;

      if (self->content_type != NULL &&
          !g_app_info_supports_uris (app) &&
          !g_app_info_supports_files (app))
        continue;

      if (g_list_find (exclude_apps, app))
        continue;

      item = gtk_app_item_new (app, FALSE, recommended, fallback);
      g_list_store_append (self->app_info_store, item);
      g_object_unref (item);
      retval = TRUE;
    }

  return retval;
}

static void
gtk_app_chooser_add_default (GtkAppChooserWidget *self,
                             GAppInfo            *app)
{
  GtkAppItem *item;

  item = gtk_app_item_new (app, TRUE, FALSE, FALSE);
  g_list_store_append (self->app_info_store, item);
  g_object_unref (item);
}

static void
update_no_applications_label (GtkAppChooserWidget *self)
{
  char *text = NULL, *desc = NULL;
  const char *string;

  if (self->default_text == NULL)
    {
      if (self->content_type)
        desc = g_content_type_get_description (self->content_type);

      string = text = g_strdup_printf (_("No apps found for “%s”."), desc);
      g_free (desc);
    }
  else
    {
      string = self->default_text;
    }

  gtk_label_set_text (GTK_LABEL (self->no_apps_label), string);

  g_free (text);
}

static void
gtk_app_chooser_widget_select_first (GtkAppChooserWidget *self)
{
  gtk_single_selection_set_selected (GTK_SINGLE_SELECTION (gtk_list_view_get_model (GTK_LIST_VIEW (self->program_list))), 0);
}

static void
gtk_app_chooser_widget_real_add_items (GtkAppChooserWidget *self)
{
  GList *all_applications = NULL;
  GList *recommended_apps = NULL;
  GList *fallback_apps = NULL;
  GList *exclude_apps = NULL;
  GAppInfo *default_app = NULL;
  gboolean show_headings;
  gboolean apps_added;

  show_headings = TRUE;
  apps_added = FALSE;

  if (self->show_all)
    show_headings = FALSE;

  gtk_list_view_set_header_factory (GTK_LIST_VIEW (self->program_list),
                                    show_headings ? self->header_factory : NULL);

  if (self->show_default && self->content_type)
    {
      default_app = g_app_info_get_default_for_type (self->content_type, FALSE);

      if (default_app != NULL)
        {
          gtk_app_chooser_add_default (self, default_app);
          apps_added = TRUE;
          exclude_apps = g_list_prepend (exclude_apps, default_app);
        }
    }

#ifndef G_OS_WIN32
  if ((self->content_type && self->show_recommended) || self->show_all)
    {
      if (self->content_type)
	recommended_apps = g_app_info_get_recommended_for_type (self->content_type);

      apps_added |= gtk_app_chooser_widget_add_section (self,
                                                        !self->show_all, /* mark as recommended */
                                                        FALSE, /* mark as fallback */
                                                        recommended_apps, exclude_apps);

      exclude_apps = g_list_concat (exclude_apps,
                                    g_list_copy (recommended_apps));
    }

  if ((self->content_type && self->show_fallback) || self->show_all)
    {
      if (self->content_type)
	fallback_apps = g_app_info_get_fallback_for_type (self->content_type);

      apps_added |= gtk_app_chooser_widget_add_section (self,
                                                        FALSE, /* mark as recommended */
                                                        !self->show_all, /* mark as fallback */
                                                        fallback_apps, exclude_apps);
      exclude_apps = g_list_concat (exclude_apps,
                                    g_list_copy (fallback_apps));
    }
#endif

  if (self->show_other || self->show_all)
    {
      all_applications = g_app_info_get_all ();

      apps_added |= gtk_app_chooser_widget_add_section (self,
                                                        FALSE,
                                                        FALSE,
                                                        all_applications, exclude_apps);
    }

  if (!apps_added)
    update_no_applications_label (self);

  gtk_widget_set_visible (self->no_apps, !apps_added);

  gtk_app_chooser_widget_select_first (self);

  if (default_app != NULL)
    g_object_unref (default_app);

  g_list_free_full (all_applications, g_object_unref);
  g_list_free_full (recommended_apps, g_object_unref);
  g_list_free_full (fallback_apps, g_object_unref);
  g_list_free (exclude_apps);
}

static void
gtk_app_chooser_widget_initialize_items (GtkAppChooserWidget *self)
{
  gtk_app_chooser_refresh (GTK_APP_CHOOSER (self));
}

static void
app_info_changed (GAppInfoMonitor     *monitor,
                  GtkAppChooserWidget *self)
{
  gtk_app_chooser_refresh (GTK_APP_CHOOSER (self));
}

static void
gtk_app_chooser_widget_set_property (GObject      *object,
                                     guint         property_id,
                                     const GValue *value,
                                     GParamSpec   *pspec)
{
  GtkAppChooserWidget *self = GTK_APP_CHOOSER_WIDGET (object);

  switch (property_id)
    {
    case PROP_CONTENT_TYPE:
      self->content_type = g_value_dup_string (value);
      break;
    case PROP_SHOW_DEFAULT:
      gtk_app_chooser_widget_set_show_default (self, g_value_get_boolean (value));
      break;
    case PROP_SHOW_RECOMMENDED:
      gtk_app_chooser_widget_set_show_recommended (self, g_value_get_boolean (value));
      break;
    case PROP_SHOW_FALLBACK:
      gtk_app_chooser_widget_set_show_fallback (self, g_value_get_boolean (value));
      break;
    case PROP_SHOW_OTHER:
      gtk_app_chooser_widget_set_show_other (self, g_value_get_boolean (value));
      break;
    case PROP_SHOW_ALL:
      gtk_app_chooser_widget_set_show_all (self, g_value_get_boolean (value));
      break;
    case PROP_DEFAULT_TEXT:
      gtk_app_chooser_widget_set_default_text (self, g_value_get_string (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gtk_app_chooser_widget_get_property (GObject    *object,
                                     guint       property_id,
                                     GValue     *value,
                                     GParamSpec *pspec)
{
  GtkAppChooserWidget *self = GTK_APP_CHOOSER_WIDGET (object);

  switch (property_id)
    {
    case PROP_CONTENT_TYPE:
      g_value_set_string (value, self->content_type);
      break;
    case PROP_SHOW_DEFAULT:
      g_value_set_boolean (value, self->show_default);
      break;
    case PROP_SHOW_RECOMMENDED:
      g_value_set_boolean (value, self->show_recommended);
      break;
    case PROP_SHOW_FALLBACK:
      g_value_set_boolean (value, self->show_fallback);
      break;
    case PROP_SHOW_OTHER:
      g_value_set_boolean (value, self->show_other);
      break;
    case PROP_SHOW_ALL:
      g_value_set_boolean (value, self->show_all);
      break;
    case PROP_DEFAULT_TEXT:
      g_value_set_string (value, self->default_text);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gtk_app_chooser_widget_constructed (GObject *object)
{
  GtkAppChooserWidget *self = GTK_APP_CHOOSER_WIDGET (object);

  if (G_OBJECT_CLASS (gtk_app_chooser_widget_parent_class)->constructed != NULL)
    G_OBJECT_CLASS (gtk_app_chooser_widget_parent_class)->constructed (object);

  gtk_app_chooser_widget_initialize_items (self);
}

static void
gtk_app_chooser_widget_finalize (GObject *object)
{
  GtkAppChooserWidget *self = GTK_APP_CHOOSER_WIDGET (object);

  g_free (self->content_type);
  g_free (self->default_text);
  g_signal_handlers_disconnect_by_func (self->monitor, app_info_changed, self);
  g_object_unref (self->monitor);
  g_object_unref (self->app_info_store);
  g_object_unref (self->header_factory);
  g_object_unref (self->filter);
  g_object_unref (self->sorter);

  G_OBJECT_CLASS (gtk_app_chooser_widget_parent_class)->finalize (object);
}

static void
gtk_app_chooser_widget_dispose (GObject *object)
{
  GtkAppChooserWidget *self = GTK_APP_CHOOSER_WIDGET (object);

  g_clear_object (&self->selected_app_info);

  g_clear_pointer (&self->overlay, gtk_widget_unparent);

  G_OBJECT_CLASS (gtk_app_chooser_widget_parent_class)->dispose (object);
}

static void
gtk_app_chooser_widget_measure (GtkWidget       *widget,
                                GtkOrientation  orientation,
                                int             for_size,
                                int            *minimum,
                                int            *natural,
                                int            *minimum_baseline,
                                int            *natural_baseline)
{
  GtkAppChooserWidget *self = GTK_APP_CHOOSER_WIDGET (widget);

  gtk_widget_measure (self->overlay, orientation, for_size,
                      minimum, natural,
                      minimum_baseline, natural_baseline);
}

static void
gtk_app_chooser_widget_snapshot (GtkWidget   *widget,
                                 GtkSnapshot *snapshot)
{
  GtkAppChooserWidget *self = GTK_APP_CHOOSER_WIDGET (widget);

  gtk_widget_snapshot_child (widget, self->overlay, snapshot);
}

static void
gtk_app_chooser_widget_size_allocate (GtkWidget *widget,
                                      int        width,
                                      int        height,
                                      int        baseline)
{
  GtkAppChooserWidget *self = GTK_APP_CHOOSER_WIDGET (widget);

  GTK_WIDGET_CLASS (gtk_app_chooser_widget_parent_class)->size_allocate (widget, width, height, baseline);

  gtk_widget_size_allocate (self->overlay,
                            &(GtkAllocation) {
                              0, 0,
                              width, height
                            },baseline);
}

static void
gtk_app_chooser_widget_class_init (GtkAppChooserWidgetClass *klass)
{
  GtkWidgetClass *widget_class;
  GObjectClass *gobject_class;

  gobject_class = G_OBJECT_CLASS (klass);
  gobject_class->dispose = gtk_app_chooser_widget_dispose;
  gobject_class->finalize = gtk_app_chooser_widget_finalize;
  gobject_class->set_property = gtk_app_chooser_widget_set_property;
  gobject_class->get_property = gtk_app_chooser_widget_get_property;
  gobject_class->constructed = gtk_app_chooser_widget_constructed;

  widget_class = GTK_WIDGET_CLASS (klass);
  widget_class->measure = gtk_app_chooser_widget_measure;
  widget_class->size_allocate = gtk_app_chooser_widget_size_allocate;
  widget_class->snapshot = gtk_app_chooser_widget_snapshot;

  props[PROP_CONTENT_TYPE] = g_param_spec_override ("content-type",
      g_object_interface_find_property (g_type_default_interface_peek (GTK_TYPE_APP_CHOOSER), "content-type"));

  /**
   * GtkAppChooserWidget:show-default:
   *
   * Determines whether the app chooser should show the default
   * handler for the content type in a separate section.
   *
   * If %FALSE, the default handler is listed among the recommended
   * applications.
   */
  props[PROP_SHOW_DEFAULT] = g_param_spec_boolean ("show-default", NULL, NULL,
                                                   FALSE,
                                                   G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkAppChooserWidget:show-recommended:
   *
   * Determines whether the app chooser should show a section
   * for recommended applications.
   *
   * If %FALSE, the recommended applications are listed
   * among the other applications.
   */
  props[PROP_SHOW_RECOMMENDED] = g_param_spec_boolean ("show-recommended", NULL, NULL,
                                                       TRUE,
                                                       G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkAppChooserWidget:show-fallback:
   *
   * Determines whether the app chooser should show a section
   * for fallback applications.
   *
   * If %FALSE, the fallback applications are listed among the
   * other applications.
   */
  props[PROP_SHOW_FALLBACK] = g_param_spec_boolean ("show-fallback", NULL, NULL,
                                                    FALSE,
                                                    G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkAppChooserWidget:show-other:
   *
   * Determines whether the app chooser should show a section
   * for other applications.
   */
  props[PROP_SHOW_OTHER] = g_param_spec_boolean ("show-other", NULL, NULL,
                                                 FALSE,
                                                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkAppChooserWidget:show-all:
   *
   * If %TRUE, the app chooser presents all applications
   * in a single list, without subsections for default,
   * recommended or related applications.
   */
  props[PROP_SHOW_ALL] = g_param_spec_boolean ("show-all", NULL, NULL,
                                               FALSE,
                                               G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkAppChooserWidget:default-text:
   *
   * The text that appears in the widget when there are no applications
   * for the given content type.
   */
  props[PROP_DEFAULT_TEXT] = g_param_spec_string ("default-text", NULL, NULL,
                                                  NULL,
                                                  G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (gobject_class, N_PROPERTIES, props);

  /**
   * GtkAppChooserWidget::application-selected:
   * @self: the object which received the signal
   * @application: the selected `GAppInfo`
   *
   * Emitted when an application item is selected from the widget's list.
   */
  signals[SIGNAL_APPLICATION_SELECTED] =
    g_signal_new (I_("application-selected"),
                  GTK_TYPE_APP_CHOOSER_WIDGET,
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GtkAppChooserWidgetClass, application_selected),
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE,
                  1, G_TYPE_APP_INFO);

  /**
   * GtkAppChooserWidget::application-activated:
   * @self: the object which received the signal
   * @application: the activated `GAppInfo`
   *
   * Emitted when an application item is activated from the widget's list.
   *
   * This usually happens when the user double clicks an item, or an item
   * is selected and the user presses one of the keys Space, Shift+Space,
   * Return or Enter.
   */
  signals[SIGNAL_APPLICATION_ACTIVATED] =
    g_signal_new (I_("application-activated"),
                  GTK_TYPE_APP_CHOOSER_WIDGET,
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GtkAppChooserWidgetClass, application_activated),
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE,
                  1, G_TYPE_APP_INFO);

  /* Bind class to template
   */
  gtk_widget_class_set_template_from_resource (widget_class,
					       "/org/gtk/libgtk/ui/gtkappchooserwidget.ui");
  gtk_widget_class_bind_template_child (widget_class, GtkAppChooserWidget, program_list);
  gtk_widget_class_bind_template_child (widget_class, GtkAppChooserWidget, no_apps_label);
  gtk_widget_class_bind_template_child (widget_class, GtkAppChooserWidget, no_apps);
  gtk_widget_class_bind_template_child (widget_class, GtkAppChooserWidget, overlay);

  gtk_widget_class_set_css_name (widget_class, I_("appchooser"));
}

static void
setup_listitem_cb (GtkListItemFactory *factory,
                   GtkListItem        *list_item)
{
  GtkWidget *box;
  GtkWidget *image;
  GtkWidget *label;

  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
  image = gtk_image_new ();
  gtk_accessible_update_property (GTK_ACCESSIBLE (image),
                                  GTK_ACCESSIBLE_PROPERTY_LABEL,
                                  "App icon",
                                  -1);
  gtk_image_set_icon_size (GTK_IMAGE (image), GTK_ICON_SIZE_LARGE);
  gtk_box_append (GTK_BOX (box), image);
  label = gtk_label_new ("");
  gtk_box_append (GTK_BOX (box), label);
  gtk_list_item_set_child (list_item, box);
}

static void
bind_listitem_cb (GtkListItemFactory *factory,
                  GtkListItem        *list_item)
{
  GtkWidget *image;
  GtkWidget *label;
  GtkAppItem *app_item;

  image = gtk_widget_get_first_child (gtk_list_item_get_child (list_item));
  label = gtk_widget_get_next_sibling (image);
  app_item = gtk_list_item_get_item (list_item);

  gtk_image_set_from_gicon (GTK_IMAGE (image), g_app_info_get_icon (app_item->app_info));
  gtk_label_set_label (GTK_LABEL (label), g_app_info_get_display_name (app_item->app_info));
  gtk_list_item_set_accessible_label (list_item, g_app_info_get_display_name (app_item->app_info));
}

static void
setup_header_cb (GtkListItemFactory *factory,
                 GtkListItem        *list_item)
{
  GtkListHeader *header = GTK_LIST_HEADER (list_item);
  GtkWidget *label;

  label = gtk_label_new ("");
  gtk_label_set_xalign (GTK_LABEL (label), 0);
  gtk_widget_add_css_class (label, "heading");
  gtk_widget_set_margin_start (label, 20);
  gtk_widget_set_margin_end (label, 20);
  gtk_widget_set_margin_top (label, 10);
  gtk_widget_set_margin_bottom (label, 10);

  gtk_list_header_set_child (header, label);
}

static void
bind_header_cb (GtkListItemFactory *factory,
                GtkListItem        *list_item)
{
  GtkListHeader *header = GTK_LIST_HEADER (list_item);
  GtkWidget *label;
  GtkAppItem *app_item;

  label = gtk_list_header_get_child (header);
  app_item = gtk_list_header_get_item (header);

  if (app_item->is_default)
    gtk_label_set_label (GTK_LABEL (label), _("Default App"));
  else if (app_item->is_recommended)
    gtk_label_set_label (GTK_LABEL (label), _("Recommended Apps"));
  else if (app_item->is_fallback)
    gtk_label_set_label (GTK_LABEL (label), _("Related Apps"));
  else
    gtk_label_set_label (GTK_LABEL (label), _("Other Apps"));
}

static void
activate_cb (GtkListView         *list,
             guint                position,
             GtkAppChooserWidget *self)
{
  GtkAppItem *app_item;

  app_item = g_list_model_get_item (G_LIST_MODEL (gtk_list_view_get_model (list)), position);

  g_set_object (&self->selected_app_info, app_item->app_info);

  g_signal_emit (self, signals[SIGNAL_APPLICATION_ACTIVATED], 0, self->selected_app_info);

  g_object_unref (app_item);
}

static void
selection_changed_cb (GListModel          *model,
                      GParamSpec          *pspec,
                      GtkAppChooserWidget *self)
{
  guint position;
  GtkAppItem *app_item;

  position = gtk_single_selection_get_selected (GTK_SINGLE_SELECTION (model));
  if (position == GTK_INVALID_LIST_POSITION)
    return;

  app_item = g_list_model_get_item (model, position);

  g_set_object (&self->selected_app_info, app_item->app_info);

  g_signal_emit (self, signals[SIGNAL_APPLICATION_SELECTED], 0, self->selected_app_info);

  g_object_unref (app_item);
}

static int
compare_section (gconstpointer a,
                 gconstpointer b,
                 gpointer      data)
{
  const GtkAppItem *item1 = a;
  const GtkAppItem *item2 = b;

  if (item1->is_default && !item2->is_default)
    return -1;
  else if (!item1->is_default && item2->is_default)
    return 1;

  if (item1->is_recommended && !item2->is_recommended)
    return -1;
  else if (!item1->is_recommended && item2->is_recommended)
    return 1;

  if (item1->is_fallback && !item2->is_fallback)
    return 1;
  else if (!item1->is_fallback && item2->is_fallback)
    return -1;

  return 0;
}

static void
gtk_app_chooser_widget_init (GtkAppChooserWidget *self)
{
  GtkListItemFactory *factory;
  GtkSingleSelection *selection;
  GtkExpression *expression;
  GtkFilterListModel *filter;
  GtkSortListModel *sort;
  GtkCustomSorter *section_sorter;

  gtk_widget_init_template (GTK_WIDGET (self));

  expression = gtk_property_expression_new (gtk_app_item_get_type (), NULL, "name");
  self->filter = gtk_string_filter_new (gtk_expression_ref (expression));
  self->sorter = gtk_string_sorter_new (expression);

  self->app_info_store = g_list_store_new (gtk_app_item_get_type ());
  filter = gtk_filter_list_model_new (G_LIST_MODEL (g_object_ref (self->app_info_store)), GTK_FILTER (g_object_ref (self->filter)));
  sort = gtk_sort_list_model_new (G_LIST_MODEL (filter), GTK_SORTER (g_object_ref (self->sorter)));

  section_sorter = gtk_custom_sorter_new (compare_section, NULL, NULL);
  gtk_sort_list_model_set_section_sorter (sort, GTK_SORTER (section_sorter));
  g_object_unref (section_sorter);

  selection = gtk_single_selection_new (G_LIST_MODEL (sort));

  g_signal_connect (selection, "notify::selected",
                    G_CALLBACK (selection_changed_cb), self);

  gtk_list_view_set_model (GTK_LIST_VIEW (self->program_list), GTK_SELECTION_MODEL (selection));
  g_object_unref (selection);

  factory = gtk_signal_list_item_factory_new ();
  g_signal_connect (factory, "setup", G_CALLBACK (setup_listitem_cb), NULL);
  g_signal_connect (factory, "bind", G_CALLBACK (bind_listitem_cb), NULL);

  gtk_list_view_set_factory (GTK_LIST_VIEW (self->program_list), factory);
  g_object_unref (factory);

  factory = gtk_signal_list_item_factory_new ();
  g_signal_connect (factory, "setup", G_CALLBACK (setup_header_cb), NULL);
  g_signal_connect (factory, "bind", G_CALLBACK (bind_header_cb), NULL);

  gtk_list_view_set_header_factory (GTK_LIST_VIEW (self->program_list), factory);
  self->header_factory = factory;

  g_signal_connect (self->program_list, "activate",
                    G_CALLBACK (activate_cb), self);

  self->monitor = g_app_info_monitor_get ();
  g_signal_connect (self->monitor, "changed",
                    G_CALLBACK (app_info_changed), self);
}

static GAppInfo *
gtk_app_chooser_widget_get_app_info (GtkAppChooser *object)
{
  GtkAppChooserWidget *self = GTK_APP_CHOOSER_WIDGET (object);

  if (self->selected_app_info == NULL)
    return NULL;

  return g_object_ref (self->selected_app_info);
}

static void
gtk_app_chooser_widget_refresh (GtkAppChooser *object)
{
  GtkAppChooserWidget *self = GTK_APP_CHOOSER_WIDGET (object);

  g_list_store_remove_all (self->app_info_store);
  gtk_app_chooser_widget_real_add_items (self);
}

static void
gtk_app_chooser_widget_iface_init (GtkAppChooserIface *iface)
{
  iface->get_app_info = gtk_app_chooser_widget_get_app_info;
  iface->refresh = gtk_app_chooser_widget_refresh;
}

/**
 * gtk_app_chooser_widget_new:
 * @content_type: the content type to show applications for
 *
 * Creates a new `GtkAppChooserWidget` for applications
 * that can handle content of the given type.
 *
 * Returns: a newly created `GtkAppChooserWidget`
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
GtkWidget *
gtk_app_chooser_widget_new (const char *content_type)
{
  return g_object_new (GTK_TYPE_APP_CHOOSER_WIDGET,
                       "content-type", content_type,
                       NULL);
}

/**
 * gtk_app_chooser_widget_set_show_default:
 * @self: a `GtkAppChooserWidget`
 * @setting: the new value for [property@Gtk.AppChooserWidget:show-default]
 *
 * Sets whether the app chooser should show the default handler
 * for the content type in a separate section.
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
void
gtk_app_chooser_widget_set_show_default (GtkAppChooserWidget *self,
                                         gboolean             setting)
{
  g_return_if_fail (GTK_IS_APP_CHOOSER_WIDGET (self));

  if (self->show_default != setting)
    {
      self->show_default = setting;

      g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_DEFAULT]);

      gtk_app_chooser_refresh (GTK_APP_CHOOSER (self));
    }
}

/**
 * gtk_app_chooser_widget_get_show_default:
 * @self: a `GtkAppChooserWidget`
 *
 * Gets whether the app chooser should show the default handler
 * for the content type in a separate section.
 *
 * Returns: the value of [property@Gtk.AppChooserWidget:show-default]
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
gboolean
gtk_app_chooser_widget_get_show_default (GtkAppChooserWidget *self)
{
  g_return_val_if_fail (GTK_IS_APP_CHOOSER_WIDGET (self), FALSE);

  return self->show_default;
}

/**
 * gtk_app_chooser_widget_set_show_recommended:
 * @self: a `GtkAppChooserWidget`
 * @setting: the new value for [property@Gtk.AppChooserWidget:show-recommended]
 *
 * Sets whether the app chooser should show recommended applications
 * for the content type in a separate section.
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
void
gtk_app_chooser_widget_set_show_recommended (GtkAppChooserWidget *self,
                                             gboolean             setting)
{
  g_return_if_fail (GTK_IS_APP_CHOOSER_WIDGET (self));

  if (self->show_recommended != setting)
    {
      self->show_recommended = setting;

      g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_RECOMMENDED]);

      gtk_app_chooser_refresh (GTK_APP_CHOOSER (self));
    }
}

/**
 * gtk_app_chooser_widget_get_show_recommended:
 * @self: a `GtkAppChooserWidget`
 *
 * Gets whether the app chooser should show recommended applications
 * for the content type in a separate section.
 *
 * Returns: the value of [property@Gtk.AppChooserWidget:show-recommended]
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
gboolean
gtk_app_chooser_widget_get_show_recommended (GtkAppChooserWidget *self)
{
  g_return_val_if_fail (GTK_IS_APP_CHOOSER_WIDGET (self), FALSE);

  return self->show_recommended;
}

/**
 * gtk_app_chooser_widget_set_show_fallback:
 * @self: a `GtkAppChooserWidget`
 * @setting: the new value for [property@Gtk.AppChooserWidget:show-fallback]
 *
 * Sets whether the app chooser should show related applications
 * for the content type in a separate section.
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
void
gtk_app_chooser_widget_set_show_fallback (GtkAppChooserWidget *self,
                                          gboolean             setting)
{
  g_return_if_fail (GTK_IS_APP_CHOOSER_WIDGET (self));

  if (self->show_fallback != setting)
    {
      self->show_fallback = setting;

      g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_FALLBACK]);

      gtk_app_chooser_refresh (GTK_APP_CHOOSER (self));
    }
}

/**
 * gtk_app_chooser_widget_get_show_fallback:
 * @self: a `GtkAppChooserWidget`
 *
 * Gets whether the app chooser should show related applications
 * for the content type in a separate section.
 *
 * Returns: the value of [property@Gtk.AppChooserWidget:show-fallback]
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
gboolean
gtk_app_chooser_widget_get_show_fallback (GtkAppChooserWidget *self)
{
  g_return_val_if_fail (GTK_IS_APP_CHOOSER_WIDGET (self), FALSE);

  return self->show_fallback;
}

/**
 * gtk_app_chooser_widget_set_show_other:
 * @self: a `GtkAppChooserWidget`
 * @setting: the new value for [property@Gtk.AppChooserWidget:show-other]
 *
 * Sets whether the app chooser should show applications
 * which are unrelated to the content type.
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
void
gtk_app_chooser_widget_set_show_other (GtkAppChooserWidget *self,
                                       gboolean             setting)
{
  g_return_if_fail (GTK_IS_APP_CHOOSER_WIDGET (self));

  if (self->show_other != setting)
    {
      self->show_other = setting;

      g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_OTHER]);

      gtk_app_chooser_refresh (GTK_APP_CHOOSER (self));
    }
}

/**
 * gtk_app_chooser_widget_get_show_other:
 * @self: a `GtkAppChooserWidget`
 *
 * Gets whether the app chooser should show applications
 * which are unrelated to the content type.
 *
 * Returns: the value of [property@Gtk.AppChooserWidget:show-other]
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
gboolean
gtk_app_chooser_widget_get_show_other (GtkAppChooserWidget *self)
{
  g_return_val_if_fail (GTK_IS_APP_CHOOSER_WIDGET (self), FALSE);

  return self->show_other;
}

/**
 * gtk_app_chooser_widget_set_show_all:
 * @self: a `GtkAppChooserWidget`
 * @setting: the new value for [property@Gtk.AppChooserWidget:show-all]
 *
 * Sets whether the app chooser should show all applications
 * in a flat list.
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
void
gtk_app_chooser_widget_set_show_all (GtkAppChooserWidget *self,
                                     gboolean             setting)
{
  g_return_if_fail (GTK_IS_APP_CHOOSER_WIDGET (self));

  if (self->show_all != setting)
    {
      self->show_all = setting;

      g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_ALL]);

      gtk_app_chooser_refresh (GTK_APP_CHOOSER (self));
    }
}

/**
 * gtk_app_chooser_widget_get_show_all:
 * @self: a `GtkAppChooserWidget`
 *
 * Gets whether the app chooser should show all applications
 * in a flat list.
 *
 * Returns: the value of [property@Gtk.AppChooserWidget:show-all]
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
gboolean
gtk_app_chooser_widget_get_show_all (GtkAppChooserWidget *self)
{
  g_return_val_if_fail (GTK_IS_APP_CHOOSER_WIDGET (self), FALSE);

  return self->show_all;
}

/**
 * gtk_app_chooser_widget_set_default_text:
 * @self: a `GtkAppChooserWidget`
 * @text: the new value for [property@Gtk.AppChooserWidget:default-text]
 *
 * Sets the text that is shown if there are not applications
 * that can handle the content type.
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
void
gtk_app_chooser_widget_set_default_text (GtkAppChooserWidget *self,
                                         const char          *text)
{
  g_return_if_fail (GTK_IS_APP_CHOOSER_WIDGET (self));

  if (g_strcmp0 (text, self->default_text) != 0)
    {
      g_free (self->default_text);
      self->default_text = g_strdup (text);

      g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEFAULT_TEXT]);

      gtk_app_chooser_refresh (GTK_APP_CHOOSER (self));
    }
}

/**
 * gtk_app_chooser_widget_get_default_text:
 * @self: a `GtkAppChooserWidget`
 *
 * Returns the text that is shown if there are not applications
 * that can handle the content type.
 *
 * Returns: (nullable): the value of [property@Gtk.AppChooserWidget:default-text]
 *
 * Deprecated: 4.10: This widget will be removed in GTK 5
 */
const char *
gtk_app_chooser_widget_get_default_text (GtkAppChooserWidget *self)
{
  g_return_val_if_fail (GTK_IS_APP_CHOOSER_WIDGET (self), NULL);

  return self->default_text;
}

static void
changed_cb (GtkEditable         *editable,
            GtkAppChooserWidget *self)
{
  gtk_string_filter_set_search (self->filter, gtk_editable_get_text (editable));
}

void
_gtk_app_chooser_widget_set_search_entry (GtkAppChooserWidget *self,
                                          GtkEditable         *entry)
{
  g_object_bind_property (self->no_apps, "visible",
                          entry, "sensitive",
                          G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);

  g_signal_connect (entry, "changed", G_CALLBACK (changed_cb), self);
}
