[gnome-disk-utility] Add app menu entry to create an empty disk image



commit 68aec0af8e6a4ce1a435282a5ec7ba8bc235963e
Author: Kai Lüke <kailueke riseup net>
Date:   Sat Mar 25 14:50:52 2017 +0100

    Add app menu entry to create an empty disk image
    
    New images can be created with a given size in the selected folder
    (being sparse files depends on the OS, works on Linux).
    They will directly be attached as loop device for further
    formatting/partitioning. The feature is useful in order to set up e.g.
    encrypted LUKS containers which should be shared with other people.

 src/disks/Makefile.am                 |    1 +
 src/disks/gduapplication.c            |   11 +
 src/disks/gdunewdiskimagedialog.c     |  430 +++++++++++++++++++++++++++++++++
 src/disks/gdunewdiskimagedialog.h     |   22 ++
 src/disks/gduwindow.c                 |   71 +++---
 src/disks/gduwindow.h                 |    4 +
 src/disks/gnome-disks.gresource.xml   |    1 +
 src/disks/ui/app-menu.ui              |    4 +
 src/disks/ui/new-disk-image-dialog.ui |  221 +++++++++++++++++
 9 files changed, 734 insertions(+), 31 deletions(-)
---
diff --git a/src/disks/Makefile.am b/src/disks/Makefile.am
index 6e49695..0689618 100644
--- a/src/disks/Makefile.am
+++ b/src/disks/Makefile.am
@@ -52,6 +52,7 @@ gnome_disks_SOURCES =                                                         \
        gducreatefilesystemwidget.h     gducreatefilesystemwidget.c     \
        gduformatdiskdialog.h           gduformatdiskdialog.c           \
        gducreatediskimagedialog.h      gducreatediskimagedialog.c      \
+       gdunewdiskimagedialog.h         gdunewdiskimagedialog.c         \
        gdurestorediskimagedialog.h     gdurestorediskimagedialog.c     \
        gdupasswordstrengthwidget.h     gdupasswordstrengthwidget.c     \
        gduestimator.h                  gduestimator.c                  \
diff --git a/src/disks/gduapplication.c b/src/disks/gduapplication.c
index 84deafa..4901ec4 100644
--- a/src/disks/gduapplication.c
+++ b/src/disks/gduapplication.c
@@ -19,6 +19,7 @@
 #include "gduapplication.h"
 #include "gduformatvolumedialog.h"
 #include "gdurestorediskimagedialog.h"
+#include "gdunewdiskimagedialog.h"
 #include "gduwindow.h"
 #include "gdulocaljob.h"
 
@@ -267,6 +268,15 @@ gdu_application_activate (GApplication *_app)
 /* ---------------------------------------------------------------------------------------------------- */
 
 static void
+new_disk_image_activated (GSimpleAction *action,
+                          GVariant      *parameter,
+                          gpointer       user_data)
+{
+  GduApplication *app = GDU_APPLICATION (user_data);
+  gdu_new_disk_image_dialog_show (app->window);
+}
+
+static void
 attach_disk_image_activated (GSimpleAction *action,
                              GVariant      *parameter,
                              gpointer       user_data)
@@ -334,6 +344,7 @@ help_activated (GSimpleAction *action,
 
 static GActionEntry app_entries[] =
 {
+  { "new_disk_image", new_disk_image_activated, NULL, NULL, NULL },
   { "attach_disk_image", attach_disk_image_activated, NULL, NULL, NULL },
   { "about", about_activated, NULL, NULL, NULL },
   { "help", help_activated, NULL, NULL, NULL },
diff --git a/src/disks/gdunewdiskimagedialog.c b/src/disks/gdunewdiskimagedialog.c
new file mode 100644
index 0000000..a950d32
--- /dev/null
+++ b/src/disks/gdunewdiskimagedialog.c
@@ -0,0 +1,430 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2017 Kai Lüke
+ *
+ * Licensed under GPL version 2 or later.
+ *
+ * Author: Kai Lüke <kailueke riseup net>
+ */
+
+#include "config.h"
+
+#define _GNU_SOURCE
+#include <fcntl.h>
+#include <math.h>
+#include <glib/gi18n.h>
+#include <gio/gunixfdlist.h>
+#include <gio/gunixinputstream.h>
+#include <gio/gfiledescriptorbased.h>
+
+#include <glib-unix.h>
+#include <sys/ioctl.h>
+#include <linux/fs.h>
+
+#include "gduapplication.h"
+#include "gduwindow.h"
+#include "gdunewdiskimagedialog.h"
+
+#define NUM_UNITS 11
+
+/* Keep in sync with Glade file */
+static const guint64 unit_sizes[NUM_UNITS] = {
+  (1ULL),                /*  0: bytes */
+  (1000ULL),             /*  1: kB */
+  (1000000ULL),          /*  2: MB */
+  (1000000000ULL),       /*  3: GB */
+  (1000000000000ULL),    /*  4: TB */
+  (1000000000000000ULL), /*  5: PB */
+  ((1ULL)<<10),          /*  6: KiB */
+  ((1ULL)<<20),          /*  7: MiB */
+  ((1ULL)<<30),          /*  8: GiB */
+  ((1ULL)<<40),          /*  9: TiB */
+  ((1ULL)<<50),          /* 10: PiB */
+};
+
+/* TODOs / ideas for New Disk Image creation
+ * include a radio toggle to create either
+ * - a full disk image with a partition table or
+ * - just a partition image with a filesystem
+ * (embed respective widgets like gducreatepartitiondialog does)
+ */
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+typedef struct
+{
+  GduWindow *window;
+
+  GtkBuilder *builder;
+  GtkWidget *dialog;
+
+  gint cur_unit_num;
+
+  GtkWidget *size_spinbutton;
+  GtkWidget *size_unit_combobox;
+  GtkAdjustment *size_adjustment;
+  GtkWidget *name_entry;
+  GtkWidget *folder_fcbutton;
+
+  GtkWidget *create_image_button;
+
+  GFile *output_file;
+  GFileOutputStream *output_file_stream;
+
+  gulong response_signal_handler_id;
+
+} DialogData;
+
+static const struct {
+  goffset offset;
+  const gchar *name;
+} widget_mapping[] = {
+  {G_STRUCT_OFFSET (DialogData, size_spinbutton), "size-spinbutton"},
+  {G_STRUCT_OFFSET (DialogData, size_unit_combobox), "size-unit-combobox"},
+  {G_STRUCT_OFFSET (DialogData, size_adjustment), "size-adjustment"},
+  {G_STRUCT_OFFSET (DialogData, name_entry), "name-entry"},
+  {G_STRUCT_OFFSET (DialogData, folder_fcbutton), "folder-fcbutton"},
+  {0, NULL}
+};
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+dialog_data_hide (DialogData *data)
+{
+  if (data->dialog != NULL)
+    {
+      GtkWidget *dialog;
+      if (data->response_signal_handler_id != 0)
+        g_signal_handler_disconnect (data->dialog, data->response_signal_handler_id);
+      dialog = data->dialog;
+      data->dialog = NULL;
+      gtk_widget_hide (dialog);
+      gtk_widget_destroy (dialog);
+      data->dialog = NULL;
+    }
+}
+
+static void
+dialog_data_unref (DialogData *data)
+{
+  dialog_data_hide (data);
+
+  g_clear_object (&data->output_file_stream);
+  g_object_unref (data->window);
+  if (data->builder != NULL)
+    g_object_unref (data->builder);
+  g_free (data);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+dialog_data_complete_and_unref (DialogData *data)
+{
+  dialog_data_hide (data);
+  dialog_data_unref (data);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+new_disk_image_update (DialogData *data)
+{
+  gboolean can_proceed = FALSE;
+
+  if (gtk_adjustment_get_value (data->size_adjustment) > 0 &&
+      strlen (gtk_entry_get_text (GTK_ENTRY (data->name_entry))) > 0)
+    can_proceed = TRUE;
+
+  gtk_dialog_set_response_sensitive (GTK_DIALOG (data->dialog), GTK_RESPONSE_OK, can_proceed);
+}
+
+static void
+on_notify (GObject     *object,
+           GParamSpec  *pspec,
+           gpointer     user_data)
+{
+  DialogData *data = user_data;
+  new_disk_image_update (data);
+}
+
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+set_unit_num (DialogData *data,
+              gint        unit_num)
+{
+  gdouble unit_size;
+  gdouble value;
+  gdouble value_units;
+
+  g_assert (unit_num < NUM_UNITS);
+
+  gtk_combo_box_set_active (GTK_COMBO_BOX (data->size_unit_combobox), unit_num);
+
+  value = gtk_adjustment_get_value (data->size_adjustment) * ((gdouble) unit_sizes[data->cur_unit_num]);
+
+  unit_size = unit_sizes[unit_num];
+  value_units = value / unit_size;
+
+  g_object_freeze_notify (G_OBJECT (data->size_adjustment));
+
+  data->cur_unit_num = unit_num;
+
+  gtk_adjustment_configure (data->size_adjustment,
+                            value_units,
+                            0.0,                    /* lower */
+                            10000000000000.0,       /* upper */
+                            1,                      /* step increment */
+                            100,                    /* page increment */
+                            0.0);                   /* page_size */
+
+  gtk_spin_button_set_digits (GTK_SPIN_BUTTON (data->size_spinbutton), 3);
+
+  gtk_adjustment_set_value (data->size_adjustment, value_units);
+
+  g_object_thaw_notify (G_OBJECT (data->size_adjustment));
+}
+
+static void
+new_disk_image_populate (DialogData *data)
+{
+  gchar *now_string;
+  gchar *proposed_filename = NULL;
+  GTimeZone *tz;
+  GDateTime *now;
+
+  tz = g_time_zone_new_local ();
+  now = g_date_time_new_now (tz);
+  now_string = g_date_time_format (now, "%Y-%m-%d %H%M");
+
+  if (proposed_filename == NULL)
+    {
+      /* Translators: The suggested name for the disk image to create.
+       *              The %s is today's date and time, e.g. "March 2, 1976 6:25AM".
+       */
+      proposed_filename = g_strdup_printf (_("Unnamed (%s).img"),
+                                           now_string);
+    }
+
+  gtk_entry_set_text (GTK_ENTRY (data->name_entry), proposed_filename);
+  g_free (proposed_filename);
+  g_date_time_unref (now);
+  g_time_zone_unref (tz);
+  g_free (now_string);
+
+  gdu_utils_configure_file_chooser_for_disk_images (GTK_FILE_CHOOSER (data->folder_fcbutton),
+                                                    FALSE,   /* set file types */
+                                                    FALSE);  /* allow_compressed */
+  set_unit_num (data, data->cur_unit_num);
+}
+
+static void
+on_size_unit_combobox_changed (GtkComboBox *combobox,
+                               gpointer     user_data)
+{
+  DialogData *data = user_data;
+  gint unit_num;
+
+  unit_num = gtk_combo_box_get_active (GTK_COMBO_BOX (data->size_unit_combobox));
+  set_unit_num (data, unit_num);
+
+  new_disk_image_update (data);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+start_creating (DialogData *data)
+{
+  gboolean ret = TRUE;
+  const gchar *name;
+  GFile *folder;
+  GError *error;
+  guint64 size;
+  gchar *filename = NULL;
+
+  name = gtk_entry_get_text (GTK_ENTRY (data->name_entry));
+  folder = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (data->folder_fcbutton));
+
+  error = NULL;
+  data->output_file = g_file_get_child (folder, name);
+  filename = g_file_get_path(data->output_file);
+  data->output_file_stream = g_file_replace (data->output_file,
+                                             NULL, /* etag */
+                                             FALSE, /* make_backup */
+                                             G_FILE_CREATE_NONE,
+                                             NULL,
+                                             &error);
+  if (data->output_file_stream == NULL)
+    {
+      gdu_utils_show_error (GTK_WINDOW (data->dialog), _("Error opening file for writing"), error);
+      g_clear_error (&error);
+      g_object_unref (folder);
+      dialog_data_complete_and_unref (data);
+      ret = FALSE;
+      goto out;
+    }
+
+  /* now that we know the user picked a folder, update file chooser settings */
+  gdu_utils_file_chooser_for_disk_images_set_default_folder (folder);
+
+  size = gtk_adjustment_get_value (data->size_adjustment) * unit_sizes[data->cur_unit_num];
+  /* will result in a sparse file if supported */
+  if (!g_seekable_truncate (G_SEEKABLE(data->output_file_stream),
+                            size,
+                            NULL,
+                            &error))
+    {
+      g_warning ("Error truncating file output stream: %s (%s, %d)",
+                 error->message, g_quark_to_string (error->domain), error->code);
+      gdu_utils_show_error (GTK_WINDOW (data->dialog), _("Error writing file"), error);
+      g_clear_error (&error);
+    }
+
+  if (!g_output_stream_close (G_OUTPUT_STREAM (data->output_file_stream),
+                              NULL, /* cancellable */
+                              &error))
+    {
+      g_warning ("Error closing file output stream: %s (%s, %d)",
+                 error->message, g_quark_to_string (error->domain), error->code);
+      g_clear_error (&error);
+    }
+  g_clear_object (&data->output_file_stream);
+
+  /* load loop device*/
+  gdu_window_attach_disk_image_helper (data->window, filename, FALSE);
+
+  dialog_data_hide (data);
+ out:
+  g_clear_object (&(data->output_file));
+  g_free (filename);
+  return ret;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+
+/* returns TRUE if OK to overwrite or file doesn't exist */
+static gboolean
+check_overwrite (DialogData *data)
+{
+  GFile *folder = NULL;
+  const gchar *name;
+  gboolean ret = TRUE;
+  GFile *file = NULL;
+  GFileInfo *folder_info = NULL;
+  GtkWidget *dialog;
+  gint response;
+
+  name = gtk_entry_get_text (GTK_ENTRY (data->name_entry));
+  folder = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (data->folder_fcbutton));
+  file = g_file_get_child (folder, name);
+  if (!g_file_query_exists (file, NULL))
+    goto out;
+
+  folder_info = g_file_query_info (folder,
+                                   G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
+                                   G_FILE_QUERY_INFO_NONE,
+                                   NULL,
+                                   NULL);
+  if (folder_info == NULL)
+    goto out;
+
+  dialog = gtk_message_dialog_new (GTK_WINDOW (data->dialog),
+                                   GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
+                                   GTK_MESSAGE_QUESTION,
+                                   GTK_BUTTONS_NONE,
+                                   _("A file named “%s” already exists.  Do you want to replace it?"),
+                                   name);
+  gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+                                            _("The file already exists in “%s”.  Replacing it will overwrite 
its contents."),
+                                            g_file_info_get_display_name (folder_info));
+  gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Cancel"), GTK_RESPONSE_CANCEL);
+  gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Replace"), GTK_RESPONSE_ACCEPT);
+  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
+  response = gtk_dialog_run (GTK_DIALOG (dialog));
+
+  if (response != GTK_RESPONSE_ACCEPT)
+    ret = FALSE;
+
+  gtk_widget_destroy (dialog);
+
+ out:
+  g_clear_object (&folder_info);
+  g_clear_object (&file);
+  g_clear_object (&folder);
+  return ret;
+}
+
+
+static void
+on_dialog_response (GtkDialog     *dialog,
+                    gint           response,
+                    gpointer       user_data)
+{
+  DialogData *data = user_data;
+
+  switch (response)
+    {
+    case GTK_RESPONSE_OK:
+      if (check_overwrite (data))
+        {
+          start_creating (data);
+        }
+      break;
+
+    case GTK_RESPONSE_CLOSE:
+      dialog_data_complete_and_unref (data);
+      break;
+
+    default: /* explicit fallthrough */
+    case GTK_RESPONSE_CANCEL:
+      dialog_data_complete_and_unref (data);
+      break;
+    }
+}
+
+void
+gdu_new_disk_image_dialog_show (GduWindow *window)
+{
+  DialogData *data;
+  guint n;
+
+  data = g_new0 (DialogData, 1);
+  data->window = g_object_ref (window);
+  data->cur_unit_num = 3; /* initalize with GB as unit */
+
+  data->dialog = GTK_WIDGET (gdu_application_new_widget (gdu_window_get_application (window),
+                                                         "new-disk-image-dialog.ui",
+                                                         "new-disk-image-dialog",
+                                                         &data->builder));
+  for (n = 0; widget_mapping[n].name != NULL; n++)
+    {
+      gpointer *p = (gpointer *) ((char *) data + widget_mapping[n].offset);
+      *p = gtk_builder_get_object (data->builder, widget_mapping[n].name);
+    }
+  g_signal_connect (data->name_entry, "notify::text", G_CALLBACK (on_notify), data);
+  g_signal_connect (data->size_adjustment, "notify::value", G_CALLBACK (on_notify), data);
+  g_signal_connect (data->size_unit_combobox, "changed", G_CALLBACK (on_size_unit_combobox_changed), data);
+
+  new_disk_image_populate (data);
+  new_disk_image_update (data);
+
+  gtk_dialog_set_default_response (GTK_DIALOG (data->dialog), GTK_RESPONSE_OK);
+
+  data->response_signal_handler_id = g_signal_connect (data->dialog,
+                                                       "response",
+                                                       G_CALLBACK (on_dialog_response),
+                                                       data);
+
+  gtk_window_set_transient_for (GTK_WINDOW (data->dialog), GTK_WINDOW (window));
+  gtk_window_present (GTK_WINDOW (data->dialog));
+
+  /* Only select the precomputed filename, not the .img extension */
+  gtk_editable_select_region (GTK_EDITABLE (data->name_entry), 0,
+                              strlen (gtk_entry_get_text (GTK_ENTRY (data->name_entry))) - 4);
+}
+
diff --git a/src/disks/gdunewdiskimagedialog.h b/src/disks/gdunewdiskimagedialog.h
new file mode 100644
index 0000000..3118405
--- /dev/null
+++ b/src/disks/gdunewdiskimagedialog.h
@@ -0,0 +1,22 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2017 Kai Lüke
+ *
+ * Licensed under GPL version 2 or later.
+ *
+ * Author: Kai Lüke <kailueke riseup net>
+ */
+
+#ifndef __GDU_NEW_DISK_IMAGE_DIALOG_H__
+#define __GDU_NEW_DISK_IMAGE_DIALOG_H__
+
+#include <gtk/gtk.h>
+#include "gdutypes.h"
+
+G_BEGIN_DECLS
+
+void     gdu_new_disk_image_dialog_show (GduWindow *window);
+
+G_END_DECLS
+
+#endif /* __GDU_NEW_DISK_IMAGE_DIALOG_H__ */
diff --git a/src/disks/gduwindow.c b/src/disks/gduwindow.c
index b8f2d8e..ffe83e4 100644
--- a/src/disks/gduwindow.c
+++ b/src/disks/gduwindow.c
@@ -710,15 +710,50 @@ loop_setup_cb (UDisksManager  *manager,
 
 /* ---------------------------------------------------------------------------------------------------- */
 
+gboolean
+gdu_window_attach_disk_image_helper (GduWindow *window, gchar *filename, gboolean readonly)
+{
+  gint fd = -1;
+  GUnixFDList *fd_list;
+  GVariantBuilder options_builder;
+  fd = open (filename, O_RDWR);
+  if (fd == -1)
+    fd = open (filename, O_RDONLY);
+  if (fd == -1)
+    {
+      GError *error;
+      error = g_error_new (G_IO_ERROR,
+                           g_io_error_from_errno (errno),
+                           "%s", strerror (errno));
+      gdu_utils_show_error (GTK_WINDOW (window),
+                            _("Error attaching disk image"),
+                            error);
+      g_error_free (error);
+      return FALSE;
+    }
+  g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT);
+  if (readonly)
+    g_variant_builder_add (&options_builder, "{sv}", "read-only", g_variant_new_boolean (TRUE));
+  fd_list = g_unix_fd_list_new_from_array (&fd, 1); /* adopts the fd */
+  udisks_manager_call_loop_setup (udisks_client_get_manager (window->client),
+                                  g_variant_new_handle (0),
+                                  g_variant_builder_end (&options_builder),
+                                  fd_list,
+                                  NULL,                       /* GCancellable */
+                                  (GAsyncReadyCallback) loop_setup_cb,
+                                  loop_setup_data_new (window, filename));
+  g_object_unref (fd_list);
+  return TRUE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
 void
 gdu_window_show_attach_disk_image (GduWindow *window)
 {
   GtkWidget *dialog;
   GFile *folder = NULL;
   gchar *filename = NULL;
-  gint fd = -1;
-  GUnixFDList *fd_list;
-  GVariantBuilder options_builder;
   GtkWidget *ro_checkbutton;
 
   dialog = gtk_file_chooser_dialog_new (_("Select Disk Image to Attach"),
@@ -744,39 +779,13 @@ gdu_window_show_attach_disk_image (GduWindow *window)
   filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
   gtk_widget_hide (dialog);
 
-  fd = open (filename, O_RDWR);
-  if (fd == -1)
-    fd = open (filename, O_RDONLY);
-  if (fd == -1)
-    {
-      GError *error;
-      error = g_error_new (G_IO_ERROR,
-                           g_io_error_from_errno (errno),
-                           "%s", strerror (errno));
-      gdu_utils_show_error (GTK_WINDOW (window),
-                            _("Error attaching disk image"),
-                            error);
-      g_error_free (error);
-      goto out;
-    }
+  if (!gdu_window_attach_disk_image_helper (window, filename, gtk_toggle_button_get_active 
(GTK_TOGGLE_BUTTON (ro_checkbutton))))
+    goto out;
 
   /* now that we know the user picked a folder, update file chooser settings */
   folder = gtk_file_chooser_get_current_folder_file (GTK_FILE_CHOOSER (dialog));
   gdu_utils_file_chooser_for_disk_images_set_default_folder (folder);
 
-  g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT);
-  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ro_checkbutton)))
-    g_variant_builder_add (&options_builder, "{sv}", "read-only", g_variant_new_boolean (TRUE));
-  fd_list = g_unix_fd_list_new_from_array (&fd, 1); /* adopts the fd */
-  udisks_manager_call_loop_setup (udisks_client_get_manager (window->client),
-                                  g_variant_new_handle (0),
-                                  g_variant_builder_end (&options_builder),
-                                  fd_list,
-                                  NULL,                       /* GCancellable */
-                                  (GAsyncReadyCallback) loop_setup_cb,
-                                  loop_setup_data_new (window, filename));
-  g_object_unref (fd_list);
-
  out:
   gtk_widget_destroy (dialog);
   g_free (filename);
diff --git a/src/disks/gduwindow.h b/src/disks/gduwindow.h
index d3ab15e..53c5408 100644
--- a/src/disks/gduwindow.h
+++ b/src/disks/gduwindow.h
@@ -51,6 +51,10 @@ gboolean        gdu_window_ensure_unused_list_finish (GduWindow     *window,
                                                       GAsyncResult  *res,
                                                       GError       **error);
 
+gboolean        gdu_window_attach_disk_image_helper (GduWindow *window,
+                                                     gchar     *filename,
+                                                     gboolean   readonly);
+
 G_END_DECLS
 
 #endif /* __GDU_WINDOW_H__ */
diff --git a/src/disks/gnome-disks.gresource.xml b/src/disks/gnome-disks.gresource.xml
index d1d8a40..c3f8589 100644
--- a/src/disks/gnome-disks.gresource.xml
+++ b/src/disks/gnome-disks.gresource.xml
@@ -19,6 +19,7 @@
     <file preprocess="xml-stripblanks">ui/filesystem-create.ui</file>
     <file preprocess="xml-stripblanks">ui/format-disk-dialog.ui</file>
     <file preprocess="xml-stripblanks">ui/format-volume-dialog.ui</file>
+    <file preprocess="xml-stripblanks">ui/new-disk-image-dialog.ui</file>
     <file preprocess="xml-stripblanks">ui/restore-disk-image-dialog.ui</file>
     <file preprocess="xml-stripblanks">ui/smart-dialog.ui</file>
     <file preprocess="xml-stripblanks">ui/unlock-device-dialog.ui</file>
diff --git a/src/disks/ui/app-menu.ui b/src/disks/ui/app-menu.ui
index 1665f86..eea2886 100644
--- a/src/disks/ui/app-menu.ui
+++ b/src/disks/ui/app-menu.ui
@@ -3,6 +3,10 @@
   <menu id='app-menu'>
     <section>
       <item>
+        <attribute name="label" translatable="yes">_New Disk Image…</attribute>
+        <attribute name="action">app.new_disk_image</attribute>
+      </item>
+      <item>
         <attribute name="label" translatable="yes">Attach Disk _Image…</attribute>
         <attribute name="action">app.attach_disk_image</attribute>
       </item>
diff --git a/src/disks/ui/new-disk-image-dialog.ui b/src/disks/ui/new-disk-image-dialog.ui
new file mode 100644
index 0000000..ca7e468
--- /dev/null
+++ b/src/disks/ui/new-disk-image-dialog.ui
@@ -0,0 +1,221 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.0 -->
+  <object class="GtkDialog" id="new-disk-image-dialog">
+    <property name="width_request">550</property>
+    <property name="can_focus">False</property>
+    <property name="border_width">12</property>
+    <property name="title" translatable="yes">New Disk Image</property>
+    <property name="resizable">False</property>
+    <property name="modal">True</property>
+    <property name="type_hint">dialog</property>
+    <child internal-child="vbox">
+      <object class="GtkBox" id="dialog-vbox1">
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">12</property>
+        <child>
+          <object class="GtkGrid" id="grid1">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="hexpand">True</property>
+            <property name="row_spacing">12</property>
+            <property name="column_spacing">12</property>
+            <child>
+              <object class="GtkLabel" id="label3">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">Partition _Size</property>
+                <property name="use_underline">True</property>
+                <property name="mnemonic_widget">size-spinbutton</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="name-label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">_Name</property>
+                <property name="use_underline">True</property>
+                <property name="mnemonic_widget">name-entry</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">1</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="folder-label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">Save in _Folder</property>
+                <property name="use_underline">True</property>
+                <property name="mnemonic_widget">folder-fcbutton</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">2</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkSpinButton" id="size-spinbutton">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="has_tooltip">True</property>
+                <property name="tooltip_markup" translatable="yes">The size of the partition to create, in 
megabytes</property>
+                <property name="tooltip_text" translatable="yes">The size of the partition to create, in 
megabytes</property>
+                <property name="invisible_char">●</property>
+                <property name="activates_default">True</property>
+                <property name="invisible_char_set">True</property>
+                <property name="adjustment">size-adjustment</property>
+                <property name="digits">0</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkEntry" id="name-entry">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="hexpand">True</property>
+                <property name="invisible_char">●</property>
+                <property name="activates_default">True</property>
+                <property name="invisible_char_set">True</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">1</property>
+                <property name="width">2</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkFileChooserButton" id="folder-fcbutton">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="orientation">vertical</property>
+                <property name="action">select-folder</property>
+                <property name="local_only">False</property>
+                <property name="title" translatable="yes">Select a Folder</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">2</property>
+                <property name="width">2</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkComboBoxText" id="size-unit-combobox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="active">2</property>
+                <property name="entry_text_column">0</property>
+                <property name="id_column">1</property>
+                <items>
+                  <item translatable="yes">bytes</item>
+                  <item translatable="yes">kB</item>
+                  <item translatable="yes">MB</item>
+                  <item translatable="yes">GB</item>
+                  <item translatable="yes">TB</item>
+                  <item translatable="yes">PB</item>
+                  <item translatable="yes">KiB</item>
+                  <item translatable="yes">MiB</item>
+                  <item translatable="yes">GiB</item>
+                  <item translatable="yes">TiB</item>
+                  <item translatable="yes">PiB</item>
+                </items>
+              </object>
+              <packing>
+                <property name="left_attach">2</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child internal-child="action_area">
+          <object class="GtkButtonBox" id="dialog-action_area1">
+            <property name="can_focus">False</property>
+            <property name="layout_style">end</property>
+            <child>
+              <object class="GtkButton" id="cancel-button">
+                <property name="label">gtk-cancel</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="use_stock">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="create-image-button">
+                <property name="label" translatable="yes">Attach new _Image…</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="can_default">True</property>
+                <property name="receives_default">True</property>
+                <property name="use_underline">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="pack_type">end</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <action-widgets>
+      <action-widget response="-6">cancel-button</action-widget>
+      <action-widget response="-5">create-image-button</action-widget>
+    </action-widgets>
+  </object>
+  <object class="GtkAdjustment" id="size-adjustment">
+    <property name="upper">100</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">10</property>
+  </object>
+</interface>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]