/*
 * Copyright © 2019 Benjamin Otte
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Benjamin Otte <otte@gnome.org>
 */

#include "config.h"

#include "gtkdirectorylist.h"

#include "gtkprivate.h"

/**
 * GtkDirectoryList:
 *
 * `GtkDirectoryList` is a list model that wraps g_file_enumerate_children_async().
 *
 * It presents a `GListModel` and fills it asynchronously with the `GFileInfo`s
 * returned from that function.
 *
 * Enumeration will start automatically when the
 * [property@Gtk.DirectoryList:file] property is set.
 *
 * While the `GtkDirectoryList` is being filled, the
 * [property@Gtk.DirectoryList:loading] property will be set to %TRUE. You can
 * listen to that property if you want to show information like a `GtkSpinner`
 * or a "Loading..." text.
 *
 * If loading fails at any point, the [property@Gtk.DirectoryList:error]
 * property will be set to give more indication about the failure.
 *
 * The `GFileInfo`s returned from a `GtkDirectoryList` have the "standard::file"
 * attribute set to the `GFile` they refer to. This way you can get at the file
 * that is referred to in the same way you would via g_file_enumerator_get_child().
 * This means you do not need access to the `GtkDirectoryList`, but can access
 * the `GFile` directly from the `GFileInfo` when operating with a `GtkListView`
 * or similar.
 */

/* random number that everyone else seems to use, too */
#define FILES_PER_QUERY 100

enum {
  PROP_0,
  PROP_ATTRIBUTES,
  PROP_ERROR,
  PROP_FILE,
  PROP_IO_PRIORITY,
  PROP_ITEM_TYPE,
  PROP_LOADING,
  PROP_MONITORED,
  PROP_N_ITEMS,

  NUM_PROPERTIES
};

typedef struct _QueuedEvent QueuedEvent;
struct _QueuedEvent
{
  GtkDirectoryList *list;
  GFile *file;
  GFileInfo *info;
  GFileMonitorEvent event;
};

static void
free_queued_event (gpointer data)
{
  QueuedEvent *event = data;

  g_clear_object (&event->file);
  g_clear_object (&event->info);
  g_free (event);
}

struct _GtkDirectoryList
{
  GObject parent_instance;

  char *attributes;
  GFile *file;
  GFileMonitor *monitor;
  gboolean monitored;
  int io_priority;

  GCancellable *cancellable;
  GError *error; /* Error while loading */
  GSequence *items; /* Use GPtrArray or GListStore here? */
  GQueue events;
};

struct _GtkDirectoryListClass
{
  GObjectClass parent_class;
};

static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };

static GType
gtk_directory_list_get_item_type (GListModel *list)
{
  return G_TYPE_FILE_INFO;
}

static guint
gtk_directory_list_get_n_items (GListModel *list)
{
  GtkDirectoryList *self = GTK_DIRECTORY_LIST (list);

  return g_sequence_get_length (self->items);
}

static gpointer
gtk_directory_list_get_item (GListModel *list,
                             guint       position)
{
  GtkDirectoryList *self = GTK_DIRECTORY_LIST (list);
  GSequenceIter *iter;

  iter = g_sequence_get_iter_at_pos (self->items, position);

  if (g_sequence_iter_is_end (iter))
    return NULL;
  else
    return g_object_ref (g_sequence_get (iter));
}

static void
gtk_directory_list_model_init (GListModelInterface *iface)
{
  iface->get_item_type = gtk_directory_list_get_item_type;
  iface->get_n_items = gtk_directory_list_get_n_items;
  iface->get_item = gtk_directory_list_get_item;
}

G_DEFINE_TYPE_WITH_CODE (GtkDirectoryList, gtk_directory_list, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_directory_list_model_init))

static void
gtk_directory_list_set_property (GObject      *object,
                                 guint         prop_id,
                                 const GValue *value,
                                 GParamSpec   *pspec)
{
  GtkDirectoryList *self = GTK_DIRECTORY_LIST (object);

  switch (prop_id)
    {
    case PROP_ATTRIBUTES:
      gtk_directory_list_set_attributes (self, g_value_get_string (value));
      break;
    case PROP_FILE:
      gtk_directory_list_set_file (self, g_value_get_object (value));
      break;

    case PROP_IO_PRIORITY:
      gtk_directory_list_set_io_priority (self, g_value_get_int (value));
      break;

    case PROP_MONITORED:
      gtk_directory_list_set_monitored (self, g_value_get_boolean (value));
      break;

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

static void
gtk_directory_list_get_property (GObject     *object,
                                 guint        prop_id,
                                 GValue      *value,
                                 GParamSpec  *pspec)
{
  GtkDirectoryList *self = GTK_DIRECTORY_LIST (object);

  switch (prop_id)
    {
    case PROP_ATTRIBUTES:
      g_value_set_string (value, self->attributes);
      break;

    case PROP_ERROR:
      g_value_set_boxed (value, self->error);
      break;

    case PROP_FILE:
      g_value_set_object (value, self->file);
      break;

    case PROP_IO_PRIORITY:
      g_value_set_int (value, self->io_priority);
      break;

    case PROP_ITEM_TYPE:
      g_value_set_gtype (value, G_TYPE_FILE_INFO);
      break;

    case PROP_LOADING:
      g_value_set_boolean (value, gtk_directory_list_is_loading (self));
      break;

    case PROP_MONITORED:
      g_value_set_boolean (value, gtk_directory_list_get_monitored (self));
      break;

    case PROP_N_ITEMS:
      g_value_set_uint (value, g_sequence_get_length (self->items));
      break;

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

static gboolean
gtk_directory_list_stop_loading (GtkDirectoryList *self)
{
  if (self->cancellable == NULL)
    return FALSE;

  g_cancellable_cancel (self->cancellable);
  g_clear_object (&self->cancellable);
  return TRUE;
}

static void directory_changed (GFileMonitor       *monitor,
                               GFile              *file,
                               GFile              *other_file,
                               GFileMonitorEvent   event,
                               gpointer            data);

static void
gtk_directory_list_stop_monitoring (GtkDirectoryList *self)
{
  if (self->monitor)
    g_signal_handlers_disconnect_by_func (self->monitor, directory_changed, self);
  g_clear_object (&self->monitor);
}

static void
gtk_directory_list_dispose (GObject *object)
{
  GtkDirectoryList *self = GTK_DIRECTORY_LIST (object);

  gtk_directory_list_stop_loading (self);
  gtk_directory_list_stop_monitoring (self);

  g_clear_object (&self->file);
  g_clear_pointer (&self->attributes, g_free);

  g_clear_error (&self->error);
  g_clear_pointer (&self->items, g_sequence_free);

  g_queue_foreach (&self->events, (GFunc) free_queued_event, NULL);
  g_queue_clear (&self->events);

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

static void
gtk_directory_list_class_init (GtkDirectoryListClass *class)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (class);

  gobject_class->set_property = gtk_directory_list_set_property;
  gobject_class->get_property = gtk_directory_list_get_property;
  gobject_class->dispose = gtk_directory_list_dispose;

  /**
   * GtkDirectoryList:attributes:
   *
   * The attributes to query.
   */
  properties[PROP_ATTRIBUTES] =
      g_param_spec_string ("attributes", NULL, NULL,
                           NULL,
                           GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkDirectoryList:error:
   *
   * Error encountered while loading files.
   */
  properties[PROP_ERROR] =
      g_param_spec_boxed ("error", NULL, NULL,
                          G_TYPE_ERROR,
                          GTK_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkDirectoryList:file:
   *
   * File to query.
   */
  properties[PROP_FILE] =
      g_param_spec_object ("file", NULL, NULL,
                           G_TYPE_FILE,
                           GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkDirectoryList:io-priority:
   *
   * Priority used when loading.
   */
  properties[PROP_IO_PRIORITY] =
      g_param_spec_int ("io-priority", NULL, NULL,
                        -G_MAXINT, G_MAXINT, G_PRIORITY_DEFAULT,
                        GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkDirectoryList:item-type:
   *
   * The type of items. See [method@Gio.ListModel.get_item_type].
   *
   * Since: 4.8
   **/
  properties[PROP_ITEM_TYPE] =
    g_param_spec_gtype ("item-type", NULL, NULL,
                        G_TYPE_FILE_INFO,
                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  /**
   * GtkDirectoryList:loading: (getter is_loading)
   *
   * %TRUE if files are being loaded.
   */
  properties[PROP_LOADING] =
      g_param_spec_boolean ("loading", NULL, NULL,
                            FALSE,
                            GTK_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkDirectoryList:monitored:
   *
   * %TRUE if the directory is monitored for changed.
   */
  properties[PROP_MONITORED] =
      g_param_spec_boolean ("monitored", NULL, NULL,
                            TRUE,
                            GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkDirectoryList:n-items:
   *
   * The number of items. See [method@Gio.ListModel.get_n_items].
   *
   * Since: 4.8
   **/
  properties[PROP_N_ITEMS] =
    g_param_spec_uint ("n-items", NULL, NULL,
                       0, G_MAXUINT, 0,
                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (gobject_class, NUM_PROPERTIES, properties);
}

static void
gtk_directory_list_init (GtkDirectoryList *self)
{
  self->items = g_sequence_new (g_object_unref);
  self->io_priority = G_PRIORITY_DEFAULT;
  self->monitored = TRUE;
  g_queue_init (&self->events);
}

/**
 * gtk_directory_list_new:
 * @file: (nullable): The file to query
 * @attributes: (nullable): The attributes to query with
 *
 * Creates a new `GtkDirectoryList`.
 *
 * The `GtkDirectoryList` is querying the given @file
 * with the given @attributes.
 *
 * Returns: a new `GtkDirectoryList`
 **/
GtkDirectoryList *
gtk_directory_list_new (const char *attributes,
                        GFile      *file)
{
  g_return_val_if_fail (file == NULL || G_IS_FILE (file), NULL);

  return g_object_new (GTK_TYPE_DIRECTORY_LIST,
                       "attributes", attributes,
                       "file", file,
                       NULL);
}

static void
gtk_directory_list_clear_items (GtkDirectoryList *self)
{
  guint n_items;

  n_items = g_sequence_get_length (self->items);
  if (n_items > 0)
    {
      g_sequence_remove_range (g_sequence_get_begin_iter (self->items),
                               g_sequence_get_end_iter (self->items));

      g_list_model_items_changed (G_LIST_MODEL (self), 0, n_items, 0);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_N_ITEMS]);
    }

  if (self->error)
    {
      g_clear_error (&self->error);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]);
    }
}

static void
gtk_directory_list_enumerator_closed_cb (GObject      *source,
                                         GAsyncResult *res,
                                         gpointer      user_data)
{
  g_file_enumerator_close_finish (G_FILE_ENUMERATOR (source), res, NULL);
}

static void
gtk_directory_list_got_files_cb (GObject      *source,
                                 GAsyncResult *res,
                                 gpointer      user_data)
{
  GtkDirectoryList *self = user_data; /* invalid if cancelled */
  GFileEnumerator *enumerator = G_FILE_ENUMERATOR (source);
  GError *error = NULL;
  GList *l, *files;
  guint n;

  files = g_file_enumerator_next_files_finish (enumerator, res, &error);

  if (files == NULL)
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        {
          g_clear_error (&error);
          return;
        }

      g_file_enumerator_close_async (enumerator,
                                     self->io_priority,
                                     NULL,
                                     gtk_directory_list_enumerator_closed_cb,
                                     NULL);

      g_object_freeze_notify (G_OBJECT (self));

      g_clear_object (&self->cancellable);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LOADING]);

      if (error)
        {
          self->error = error;
          g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]);
        }

      g_object_thaw_notify (G_OBJECT (self));
      return;
    }

  n = 0;
  for (l = files; l; l = l->next)
    {
      GFileInfo *info;
      GFile *file;

      info = l->data;
      file = g_file_enumerator_get_child (enumerator, info);
      g_file_info_set_attribute_object (info, "standard::file", G_OBJECT (file));
      g_object_unref (file);
      g_sequence_append (self->items, info);
      n++;
    }
  g_list_free (files);

  g_file_enumerator_next_files_async (enumerator,
                                      g_file_is_native (self->file) ? 50 * FILES_PER_QUERY : FILES_PER_QUERY,
                                      self->io_priority,
                                      self->cancellable,
                                      gtk_directory_list_got_files_cb,
                                      self);

  if (n > 0)
    {
      g_list_model_items_changed (G_LIST_MODEL (self), g_sequence_get_length (self->items) - n, 0, n);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_N_ITEMS]);
    }
}

static void
gtk_directory_list_got_enumerator_cb (GObject      *source,
                                      GAsyncResult *res,
                                      gpointer      user_data)
{
  GtkDirectoryList *self = user_data; /* invalid if cancelled */
  GFile *file = G_FILE (source);
  GFileEnumerator *enumerator;
  GError *error = NULL;

  enumerator = g_file_enumerate_children_finish (file, res, &error);
  if (enumerator == NULL)
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        {
          g_clear_error (&error);
          return;
        }

      g_object_freeze_notify (G_OBJECT (self));
      self->error = error;
      g_clear_object (&self->cancellable);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LOADING]);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]);
      g_object_thaw_notify (G_OBJECT (self));
      return;
    }

  g_file_enumerator_next_files_async (enumerator,
                                      g_file_is_native (file) ? 50 * FILES_PER_QUERY : FILES_PER_QUERY,
                                      self->io_priority,
                                      self->cancellable,
                                      gtk_directory_list_got_files_cb,
                                      self);
  g_object_unref (enumerator);
}

static void
gtk_directory_list_start_loading (GtkDirectoryList *self)
{
  gboolean was_loading;
  char *glib_apis_suck;

  was_loading = gtk_directory_list_stop_loading (self);
  gtk_directory_list_clear_items (self);

  if (self->file == NULL)
    {
      if (was_loading)
        g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LOADING]);
      return;
    }

  glib_apis_suck = g_strconcat ("standard::name,", self->attributes, NULL);
  self->cancellable = g_cancellable_new ();
  g_file_enumerate_children_async (self->file,
                                   glib_apis_suck,
                                   G_FILE_QUERY_INFO_NONE,
                                   self->io_priority,
                                   self->cancellable,
                                   gtk_directory_list_got_enumerator_cb,
                                   self);
  g_free (glib_apis_suck);

  if (!was_loading)
    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LOADING]);
}

static GSequenceIter *
find_file (GSequence *sequence,
           GFile     *file)
{
  GSequenceIter *iter;

  for (iter = g_sequence_get_begin_iter (sequence);
       !g_sequence_iter_is_end (iter);
       iter = g_sequence_iter_next (iter))
    {
      GFileInfo *item = G_FILE_INFO (g_sequence_get (iter));
      GFile *f = G_FILE (g_file_info_get_attribute_object (item, "standard::file"));

      if (g_file_equal (f, file))
        return iter;
    }

  return NULL;
}

static gboolean
handle_event (QueuedEvent *event)
{
  GtkDirectoryList *self = event->list;
  GFile *file = event->file;
  GFileInfo *info = event->info;
  GSequenceIter *iter;
  unsigned int position;

  switch ((int)event->event)
    {
    case G_FILE_MONITOR_EVENT_MOVED_IN:
    case G_FILE_MONITOR_EVENT_CREATED:
      if (!info)
        return FALSE;

      g_file_info_set_attribute_object (info, "standard::file", G_OBJECT (file));

      iter = find_file (self->items, file);
      if (iter)
        {
          position = g_sequence_iter_get_position (iter);
          g_sequence_set (iter, g_object_ref (info));
          g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 1);
        }
      else
        {
          position = g_sequence_get_length (self->items);
          g_sequence_append (self->items, g_object_ref (info));
          g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
          g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_N_ITEMS]);
        }
      break;

    case G_FILE_MONITOR_EVENT_MOVED_OUT:
    case G_FILE_MONITOR_EVENT_DELETED:
      iter = find_file (self->items, file);
      if (iter)
        {
          position = g_sequence_iter_get_position (iter);
          g_sequence_remove (iter);
          g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 0);
          g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_N_ITEMS]);
        }
      break;

    case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
      if (!info)
        return FALSE;

      g_file_info_set_attribute_object (info, "standard::file", G_OBJECT (file));

      iter = find_file (self->items, file);
      if (iter)
        {
          position = g_sequence_iter_get_position (iter);
          g_sequence_set (iter, g_object_ref (info));
          g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 1);
        }
      break;

    default:
      g_assert_not_reached ();
    }

  return TRUE;
}

static void
handle_events (GtkDirectoryList *self)
{
  QueuedEvent *event;

  do
    {
      event = g_queue_peek_tail (&self->events);
      if (!event)
        return;

      if (!handle_event (event))
        return;

      event = g_queue_pop_tail (&self->events);
      free_queued_event (event);
    }
  while (TRUE);
}

static void
got_new_file_info_cb (GObject      *source,
                      GAsyncResult *res,
                      gpointer      data)
{
  QueuedEvent *event = data;
  GtkDirectoryList *self = event->list;
  GFile *file = event->file;

  event->info = g_file_query_info_finish (file, res, NULL);
  handle_events (self);
}

static void
got_existing_file_info_cb (GObject      *source,
                           GAsyncResult *res,
                           gpointer      data)
{
  QueuedEvent *event = data;
  GtkDirectoryList *self = event->list;
  GFile *file = event->file;

  event->info = g_file_query_info_finish (file, res, NULL);
  handle_events (self);
}

static void
directory_changed (GFileMonitor       *monitor,
                   GFile              *file,
                   GFile              *other_file,
                   GFileMonitorEvent   event,
                   gpointer            data)
{
  GtkDirectoryList *self = GTK_DIRECTORY_LIST (data);
  QueuedEvent *ev;

  switch (event)
    {
    case G_FILE_MONITOR_EVENT_MOVED_IN:
    case G_FILE_MONITOR_EVENT_CREATED:
      ev = g_new0 (QueuedEvent, 1);
      ev->list = self;
      ev->event = event;
      ev->file = g_object_ref (file);
      g_queue_push_head (&self->events, ev);

      g_file_query_info_async (file,
                               self->attributes,
                               G_FILE_QUERY_INFO_NONE,
                               self->io_priority,
                               self->cancellable,
                               got_new_file_info_cb,
                               ev);
      break;

    case G_FILE_MONITOR_EVENT_MOVED_OUT:
    case G_FILE_MONITOR_EVENT_DELETED:
      ev = g_new0 (QueuedEvent, 1);
      ev->list = self;
      ev->event = event;
      ev->file = g_object_ref (file);
      g_queue_push_head (&self->events, ev);

      handle_events (self);
      break;

    case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
      ev = g_new0 (QueuedEvent, 1);
      ev->list = self;
      ev->event = event;
      ev->file = g_object_ref (file);
      g_queue_push_head (&self->events, ev);

      g_file_query_info_async (file,
                               self->attributes,
                               G_FILE_QUERY_INFO_NONE,
                               self->io_priority,
                               self->cancellable,
                               got_existing_file_info_cb,
                               ev);
      break;

    case G_FILE_MONITOR_EVENT_RENAMED:
      ev = g_new0 (QueuedEvent, 1);
      ev->list = self;
      ev->event = G_FILE_MONITOR_EVENT_DELETED;
      ev->file = g_object_ref (file);
      g_queue_push_head (&self->events, ev);

      ev = g_new0 (QueuedEvent, 1);
      ev->list = self;
      ev->event = G_FILE_MONITOR_EVENT_CREATED;
      ev->file = g_object_ref (other_file);
      g_queue_push_head (&self->events, ev);

      g_file_query_info_async (other_file,
                               self->attributes,
                               G_FILE_QUERY_INFO_NONE,
                               self->io_priority,
                               self->cancellable,
                               got_existing_file_info_cb,
                               ev);
      break;

    case G_FILE_MONITOR_EVENT_CHANGED:
    case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
    case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
    case G_FILE_MONITOR_EVENT_UNMOUNTED:
    case G_FILE_MONITOR_EVENT_MOVED:
    default:
      break;
    }
}

static void
gtk_directory_list_start_monitoring (GtkDirectoryList *self)
{
  g_assert (self->monitor == NULL);
  self->monitor = g_file_monitor_directory (self->file, G_FILE_MONITOR_WATCH_MOVES, NULL, NULL);
  if (self->monitor)
    g_signal_connect (self->monitor, "changed", G_CALLBACK (directory_changed), self);
}

static void
gtk_directory_list_update_monitoring (GtkDirectoryList *self)
{
  gtk_directory_list_stop_monitoring (self);
  if (self->file && self->monitored)
    gtk_directory_list_start_monitoring (self);
}

/**
 * gtk_directory_list_set_file:
 * @self: a `GtkDirectoryList`
 * @file: (nullable): the `GFile` to be enumerated
 *
 * Sets the @file to be enumerated and starts the enumeration.
 *
 * If @file is %NULL, the result will be an empty list.
 */
void
gtk_directory_list_set_file (GtkDirectoryList *self,
                             GFile            *file)
{
  g_return_if_fail (GTK_IS_DIRECTORY_LIST (self));
  g_return_if_fail (file == NULL || G_IS_FILE (file));

  if (self->file == file ||
      (self->file && file && g_file_equal (self->file, file)))
    return;

  g_object_freeze_notify (G_OBJECT (self));

  g_set_object (&self->file, file);

  gtk_directory_list_update_monitoring (self);
  gtk_directory_list_start_loading (self);

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FILE]);

  g_object_thaw_notify (G_OBJECT (self));
}

/**
 * gtk_directory_list_get_file:
 * @self: a `GtkDirectoryList`
 *
 * Gets the file whose children are currently enumerated.
 *
 * Returns: (nullable) (transfer none): The file whose children are enumerated
 **/
GFile *
gtk_directory_list_get_file (GtkDirectoryList *self)
{
  g_return_val_if_fail (GTK_IS_DIRECTORY_LIST (self), NULL);

  return self->file;
}

/**
 * gtk_directory_list_set_attributes:
 * @self: a `GtkDirectoryList`
 * @attributes: (nullable): the attributes to enumerate
 *
 * Sets the @attributes to be enumerated and starts the enumeration.
 *
 * If @attributes is %NULL, the list of file infos will still be created, it will just
 * not contain any extra attributes.
 */
void
gtk_directory_list_set_attributes (GtkDirectoryList *self,
                                   const char       *attributes)
{
  g_return_if_fail (GTK_IS_DIRECTORY_LIST (self));

  if (self->attributes == attributes)
    return;

  g_object_freeze_notify (G_OBJECT (self));

  g_free (self->attributes);
  self->attributes = g_strdup (attributes);

  gtk_directory_list_start_loading (self);

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ATTRIBUTES]);

  g_object_thaw_notify (G_OBJECT (self));
}

/**
 * gtk_directory_list_get_attributes:
 * @self: a `GtkDirectoryList`
 *
 * Gets the attributes queried on the children.
 *
 * Returns: (nullable) (transfer none): The queried attributes
 */
const char *
gtk_directory_list_get_attributes (GtkDirectoryList *self)
{
  g_return_val_if_fail (GTK_IS_DIRECTORY_LIST (self), NULL);

  return self->attributes;
}

/**
 * gtk_directory_list_set_io_priority:
 * @self: a `GtkDirectoryList`
 * @io_priority: IO priority to use
 *
 * Sets the IO priority to use while loading directories.
 *
 * Setting the priority while @self is loading will reprioritize the
 * ongoing load as soon as possible.
 *
 * The default IO priority is %G_PRIORITY_DEFAULT, which is higher than
 * the GTK redraw priority. If you are loading a lot of directories in
 * parallel, lowering it to something like %G_PRIORITY_DEFAULT_IDLE
 * may increase responsiveness.
 */
void
gtk_directory_list_set_io_priority (GtkDirectoryList *self,
                                    int               io_priority)
{
  g_return_if_fail (GTK_IS_DIRECTORY_LIST (self));

  if (self->io_priority == io_priority)
    return;

  self->io_priority = io_priority;

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_IO_PRIORITY]);
}

/**
 * gtk_directory_list_get_io_priority:
 * @self: a `GtkDirectoryList`
 *
 * Gets the IO priority set via gtk_directory_list_set_io_priority().
 *
 * Returns: The IO priority.
 */
int
gtk_directory_list_get_io_priority (GtkDirectoryList *self)
{
  g_return_val_if_fail (GTK_IS_DIRECTORY_LIST (self), G_PRIORITY_DEFAULT);

  return self->io_priority;
}

/**
 * gtk_directory_list_is_loading: (get-property loading)
 * @self: a `GtkDirectoryList`
 *
 * Returns %TRUE if the children enumeration is currently in
 * progress.
 *
 * Files will be added to @self from time to time while loading is
 * going on. The order in which are added is undefined and may change
 * in between runs.
 *
 * Returns: %TRUE if @self is loading
 */
gboolean
gtk_directory_list_is_loading (GtkDirectoryList *self)
{
  g_return_val_if_fail (GTK_IS_DIRECTORY_LIST (self), FALSE);

  return self->cancellable != NULL;
}

/**
 * gtk_directory_list_get_error:
 * @self: a `GtkDirectoryList`
 *
 * Gets the loading error, if any.
 *
 * If an error occurs during the loading process, the loading process
 * will finish and this property allows querying the error that happened.
 * This error will persist until a file is loaded again.
 *
 * An error being set does not mean that no files were loaded, and all
 * successfully queried files will remain in the list.
 *
 * Returns: (nullable) (transfer none): The loading error or %NULL if
 *   loading finished successfully
 */
const GError *
gtk_directory_list_get_error (GtkDirectoryList *self)
{
  g_return_val_if_fail (GTK_IS_DIRECTORY_LIST (self), FALSE);

  return self->error;
}

/**
 * gtk_directory_list_set_monitored:
 * @self: a `GtkDirectoryList`
 * @monitored: %TRUE to monitor the directory for changes
 *
 * Sets whether the directory list will monitor the directory
 * for changes.
 *
 * If monitoring is enabled, the ::items-changed signal will
 * be emitted when the directory contents change.
 *
 *
 * When monitoring is turned on after the initial creation
 * of the directory list, the directory is reloaded to avoid
 * missing files that appeared between the initial loading
 * and when monitoring was turned on.
 */
void
gtk_directory_list_set_monitored (GtkDirectoryList *self,
                                  gboolean          monitored)
{
  g_return_if_fail (GTK_IS_DIRECTORY_LIST (self));

  if (self->monitored == monitored)
    return;

  self->monitored = monitored;

  gtk_directory_list_update_monitoring (self);
  if (monitored)
    gtk_directory_list_start_loading (self);

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MONITORED]);
}

/**
 * gtk_directory_list_get_monitored:
 * @self: a `GtkDirectoryList`
 *
 * Returns whether the directory list is monitoring
 * the directory for changes.
 *
 * Returns: %TRUE if the directory is monitored
 */
gboolean
gtk_directory_list_get_monitored (GtkDirectoryList *self)
{
  g_return_val_if_fail (GTK_IS_DIRECTORY_LIST (self), TRUE);

  return self->monitored;
}
