Blob Blame History Raw
/* dzl-file-chooser-entry.c
 *
 * Copyright (C) 2016 Christian Hergert <chergert@redhat.com>
 *
 * This file 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 file 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 General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#define G_LOG_DOMAIN "dzl-file-chooser-entry"

#include "config.h"

#include <glib/gi18n.h>

#include "dzl-file-chooser-entry.h"

typedef struct
{
  GtkEntry  *entry;
  GtkButton *button;

  GtkFileChooserDialog *dialog;
  GtkFileFilter *filter;
  GFile *file;
  gchar *title;

  GtkFileChooserAction action;

  guint local_only : 1;
  guint create_folders : 1;
  guint do_overwrite_confirmation : 1;
  guint select_multiple : 1;
  guint show_hidden : 1;
} DzlFileChooserEntryPrivate;

enum {
  PROP_0,
  PROP_ACTION,
  PROP_CREATE_FOLDERS,
  PROP_DO_OVERWRITE_CONFIRMATION,
  PROP_FILE,
  PROP_FILTER,
  PROP_LOCAL_ONLY,
  PROP_SHOW_HIDDEN,
  PROP_MAX_WIDTH_CHARS,
  PROP_TITLE,
  N_PROPS
};

static GParamSpec *properties [N_PROPS];

G_DEFINE_TYPE_EXTENDED (DzlFileChooserEntry,
                        dzl_file_chooser_entry,
                        GTK_TYPE_BIN,
                        0,
                        G_ADD_PRIVATE (DzlFileChooserEntry))

static void
dzl_file_chooser_entry_sync_to_dialog (DzlFileChooserEntry *self)
{
  DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self);
  GtkWidget *toplevel;
  GtkWidget *default_widget;

  g_assert (DZL_IS_FILE_CHOOSER_ENTRY (self));

  if (priv->dialog == NULL)
    return;

  g_object_set (priv->dialog,
                "action", priv->action,
                "create-folders", priv->create_folders,
                "do-overwrite-confirmation", priv->do_overwrite_confirmation,
                "local-only", priv->local_only,
                "show-hidden", priv->show_hidden,
                "filter", priv->filter,
                "title", priv->title,
                NULL);

  if (priv->file != NULL)
    gtk_file_chooser_set_file (GTK_FILE_CHOOSER (priv->dialog), priv->file, NULL);

  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));

  if (GTK_IS_WINDOW (toplevel))
    gtk_window_set_transient_for (GTK_WINDOW (priv->dialog), GTK_WINDOW (toplevel));

  default_widget = gtk_dialog_get_widget_for_response (GTK_DIALOG (priv->dialog),
                                                       GTK_RESPONSE_OK);

  switch (priv->action)
    {
    case GTK_FILE_CHOOSER_ACTION_OPEN:
      gtk_button_set_label (GTK_BUTTON (default_widget), _("Open"));
      break;

    case GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER:
      gtk_button_set_label (GTK_BUTTON (default_widget), _("Select"));
      break;

    case GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER:
      gtk_button_set_label (GTK_BUTTON (default_widget), _("Create"));
      break;

    case GTK_FILE_CHOOSER_ACTION_SAVE:
      gtk_button_set_label (GTK_BUTTON (default_widget), _("Save"));
      break;

    default:
      break;
    }
}

static void
dzl_file_chooser_entry_dialog_response (DzlFileChooserEntry  *self,
                                        gint                  response_id,
                                        GtkFileChooserDialog *dialog)
{
  g_assert (DZL_IS_FILE_CHOOSER_ENTRY (self));
  g_assert (GTK_IS_FILE_CHOOSER_DIALOG (dialog));

  if (response_id == GTK_RESPONSE_OK)
    {
      g_autoptr(GFile) file = NULL;

      file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
      if (file != NULL)
        dzl_file_chooser_entry_set_file (self, file);
    }

  gtk_widget_destroy (GTK_WIDGET (dialog));
}

static void
dzl_file_chooser_entry_ensure_dialog (DzlFileChooserEntry *self)
{
  DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self);

  g_assert (DZL_IS_FILE_CHOOSER_ENTRY (self));

  if (priv->dialog == NULL)
    {
      priv->dialog = g_object_new (GTK_TYPE_FILE_CHOOSER_DIALOG,
                                   "local-only", TRUE,
                                   "modal", TRUE,
                                   NULL);
      g_signal_connect_object (priv->dialog,
                               "response",
                               G_CALLBACK (dzl_file_chooser_entry_dialog_response),
                               self,
                               G_CONNECT_SWAPPED);
      g_signal_connect (priv->dialog,
                        "destroy",
                        G_CALLBACK (gtk_widget_destroyed),
                        &priv->dialog);
      gtk_dialog_add_buttons (GTK_DIALOG (priv->dialog),
                              _("Cancel"), GTK_RESPONSE_CANCEL,
                              _("Open"), GTK_RESPONSE_OK,
                              NULL);
      gtk_dialog_set_default_response (GTK_DIALOG (priv->dialog), GTK_RESPONSE_OK);
    }

  dzl_file_chooser_entry_sync_to_dialog (self);
}

static void
dzl_file_chooser_entry_button_clicked (DzlFileChooserEntry *self,
                                       GtkButton           *button)
{
  DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self);

  g_assert (DZL_IS_FILE_CHOOSER_ENTRY (self));
  g_assert (GTK_IS_BUTTON (button));

  dzl_file_chooser_entry_ensure_dialog (self);
  gtk_window_present (GTK_WINDOW (priv->dialog));
}

static GFile *
file_expand (const gchar *path)
{
  g_autofree gchar *relative = NULL;
  g_autofree gchar *scheme = NULL;

  if (path == NULL)
    return g_file_new_for_path (g_get_home_dir ());

  scheme = g_uri_parse_scheme (path);
  if (scheme != NULL)
    return g_file_new_for_uri (path);

  if (g_path_is_absolute (path))
    return g_file_new_for_path (path);

  relative = g_build_filename (g_get_home_dir (),
                               path[0] == '~' ? &path[1] : path,
                               NULL);

  return g_file_new_for_path (relative);
}

static void
dzl_file_chooser_entry_changed (DzlFileChooserEntry *self,
                                GtkEntry            *entry)
{
  DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self);
  g_autoptr(GFile) file = NULL;

  g_assert (DZL_IS_FILE_CHOOSER_ENTRY (self));
  g_assert (GTK_IS_ENTRY (entry));

  file = file_expand (gtk_entry_get_text (entry));

  if (g_set_object (&priv->file, file))
    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FILE]);
}

static void
dzl_file_chooser_entry_destroy (GtkWidget *widget)
{
  DzlFileChooserEntry *self = (DzlFileChooserEntry *)widget;
  DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self);

  if (priv->dialog != NULL)
    gtk_widget_destroy (GTK_WIDGET (priv->dialog));

  GTK_WIDGET_CLASS (dzl_file_chooser_entry_parent_class)->destroy (widget);
}

static void
dzl_file_chooser_entry_finalize (GObject *object)
{
  DzlFileChooserEntry *self = (DzlFileChooserEntry *)object;
  DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self);

  g_clear_object (&priv->file);
  g_clear_object (&priv->filter);
  g_clear_pointer (&priv->title, g_free);

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

static void
dzl_file_chooser_entry_get_property (GObject    *object,
                                     guint       prop_id,
                                     GValue     *value,
                                     GParamSpec *pspec)
{
  DzlFileChooserEntry *self = DZL_FILE_CHOOSER_ENTRY (object);
  DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self);

  switch (prop_id)
    {
    case PROP_ACTION:
      g_value_set_enum (value, priv->action);
      break;

    case PROP_LOCAL_ONLY:
      g_value_set_boolean (value, priv->local_only);
      break;

    case PROP_CREATE_FOLDERS:
      g_value_set_boolean (value, priv->create_folders);
      break;

    case PROP_DO_OVERWRITE_CONFIRMATION:
      g_value_set_boolean (value, priv->do_overwrite_confirmation);
      break;

    case PROP_SHOW_HIDDEN:
      g_value_set_boolean (value, priv->show_hidden);
      break;

    case PROP_FILTER:
      g_value_set_object (value, priv->filter);
      break;

    case PROP_FILE:
      g_value_take_object (value, dzl_file_chooser_entry_get_file (self));
      break;

    case PROP_MAX_WIDTH_CHARS:
      g_value_set_int (value, gtk_entry_get_max_width_chars (priv->entry));
      break;

    case PROP_TITLE:
      g_value_set_string (value, priv->title);
      break;

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

static void
dzl_file_chooser_entry_set_property (GObject      *object,
                                     guint         prop_id,
                                     const GValue *value,
                                     GParamSpec   *pspec)
{
  DzlFileChooserEntry *self = DZL_FILE_CHOOSER_ENTRY (object);
  DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self);

  switch (prop_id)
    {
    case PROP_ACTION:
      priv->action = g_value_get_enum (value);
      break;

    case PROP_LOCAL_ONLY:
      priv->local_only = g_value_get_boolean (value);
      break;

    case PROP_CREATE_FOLDERS:
      priv->create_folders= g_value_get_boolean (value);
      break;

    case PROP_DO_OVERWRITE_CONFIRMATION:
      priv->do_overwrite_confirmation = g_value_get_boolean (value);
      break;

    case PROP_SHOW_HIDDEN:
      priv->show_hidden = g_value_get_boolean (value);
      break;

    case PROP_FILTER:
      g_clear_object (&priv->filter);
      priv->filter = g_value_dup_object (value);
      break;

    case PROP_FILE:
      dzl_file_chooser_entry_set_file (self, g_value_get_object (value));
      break;

    case PROP_MAX_WIDTH_CHARS:
      gtk_entry_set_max_width_chars (priv->entry, g_value_get_int (value));
      break;

    case PROP_TITLE:
      g_free (priv->title);
      priv->title = g_value_dup_string (value);
      break;

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

  dzl_file_chooser_entry_sync_to_dialog (self);
}

static void
dzl_file_chooser_entry_class_init (DzlFileChooserEntryClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->finalize = dzl_file_chooser_entry_finalize;
  object_class->get_property = dzl_file_chooser_entry_get_property;
  object_class->set_property = dzl_file_chooser_entry_set_property;

  widget_class->destroy = dzl_file_chooser_entry_destroy;

  properties [PROP_ACTION] =
    g_param_spec_enum ("action",
                       NULL,
                       NULL,
                       GTK_TYPE_FILE_CHOOSER_ACTION,
                       GTK_FILE_CHOOSER_ACTION_OPEN,
                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  properties [PROP_CREATE_FOLDERS] =
    g_param_spec_boolean ("create-folders",
                          NULL,
                          NULL,
                          FALSE,
                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  properties [PROP_DO_OVERWRITE_CONFIRMATION] =
    g_param_spec_boolean ("do-overwrite-confirmation",
                          NULL,
                          NULL,
                          FALSE,
                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  properties [PROP_LOCAL_ONLY] =
    g_param_spec_boolean ("local-only",
                          NULL,
                          NULL,
                          FALSE,
                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  properties [PROP_SHOW_HIDDEN] =
    g_param_spec_boolean ("show-hidden",
                          NULL,
                          NULL,
                          FALSE,
                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  properties [PROP_FILTER] =
    g_param_spec_object ("filter",
                         NULL,
                         NULL,
                         GTK_TYPE_FILE_FILTER,
                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  properties [PROP_FILE] =
    g_param_spec_object ("file",
                         NULL,
                         NULL,
                         G_TYPE_FILE,
                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  properties [PROP_MAX_WIDTH_CHARS] =
    g_param_spec_int ("max-width-chars",
                      NULL,
                      NULL,
                      -1,
                      G_MAXINT,
                      -1,
                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  properties [PROP_TITLE] =
    g_param_spec_string ("title",
                         NULL,
                         NULL,
                         NULL,
                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
dzl_file_chooser_entry_init (DzlFileChooserEntry *self)
{
  DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self);
  GtkWidget *hbox;

  hbox = g_object_new (GTK_TYPE_BOX,
                       "orientation", GTK_ORIENTATION_HORIZONTAL,
                       "visible", TRUE,
                       NULL);
  gtk_style_context_add_class (gtk_widget_get_style_context (hbox), "linked");
  gtk_container_add (GTK_CONTAINER (self), hbox);

  priv->entry = g_object_new (GTK_TYPE_ENTRY,
                              "visible", TRUE,
                              NULL);
  g_signal_connect (priv->entry,
                    "destroy",
                    G_CALLBACK (gtk_widget_destroyed),
                    &priv->entry);
  g_signal_connect_object (priv->entry,
                           "changed",
                           G_CALLBACK (dzl_file_chooser_entry_changed),
                           self,
                           G_CONNECT_SWAPPED);
  gtk_container_add_with_properties (GTK_CONTAINER (hbox), GTK_WIDGET (priv->entry),
                                     "expand", TRUE,
                                     NULL);

  priv->button = g_object_new (GTK_TYPE_BUTTON,
                               "label", _("Browse…"),
                               "visible", TRUE,
                               NULL);
  g_signal_connect_object (priv->button,
                           "clicked",
                           G_CALLBACK (dzl_file_chooser_entry_button_clicked),
                           self,
                           G_CONNECT_SWAPPED);
  g_signal_connect (priv->button,
                    "destroy",
                    G_CALLBACK (gtk_widget_destroyed),
                    &priv->button);
  gtk_container_add (GTK_CONTAINER (hbox), GTK_WIDGET (priv->button));
}

static gchar *
file_collapse (GFile *file)
{
  gchar *path = NULL;

  g_assert (!file || G_IS_FILE (file));

  if (file == NULL)
    return g_strdup ("");

  if (!g_file_is_native (file))
    return g_file_get_uri (file);

  path = g_file_get_path (file);

  if (path == NULL)
    return g_strdup ("");

  if (!g_path_is_absolute (path))
    {
      g_autofree gchar *freeme = path;

      path = g_build_filename (g_get_home_dir (), freeme, NULL);
    }

  if (g_str_has_prefix (path, g_get_home_dir ()))
    {
      g_autofree gchar *freeme = path;

      path = g_build_filename ("~",
                               freeme + strlen (g_get_home_dir ()),
                               NULL);
    }

  return path;
}

/**
 * dzl_file_chooser_entry_get_file:
 *
 * Returns the currently selected file or %NULL if there is no selection.
 *
 * Returns: (nullable) (transfer full): A #GFile or %NULL.
 */
GFile *
dzl_file_chooser_entry_get_file (DzlFileChooserEntry *self)
{
  DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_FILE_CHOOSER_ENTRY (self), NULL);

  return priv->file ? g_object_ref (priv->file) : NULL;
}

void
dzl_file_chooser_entry_set_file (DzlFileChooserEntry *self,
                                 GFile               *file)
{
  DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self);
  g_autofree gchar *collapsed = NULL;

  g_return_if_fail (DZL_IS_FILE_CHOOSER_ENTRY (self));

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

  if (file != NULL)
    g_object_ref (file);

  g_clear_object (&priv->file);
  priv->file = file;

  collapsed = file_collapse (file);
  gtk_entry_set_text (priv->entry, collapsed);

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