Saving The Content To A File

In this lesson you will learn how to add a menu entry with a key shortcut, ask the user to select a file for saving the GtkTextBuffer contents, and save a file asynchronously.

Add the “Save As” menu item

  1. Open the UI definition file for your window and find the primary_menu menu definition at the bottom of the file

  2. Remove the “Preferences” menu item, as we are not going to need it

  3. In place of the removed menu item, add the definition of the Save As menu item:

<menu id="primary_menu">
  <section>
    <item>
      <attribute name="label" translatable="yes">_Save as...</attribute>
      <attribute name="action">win.save-as</attribute>
    </item>
    <item>
      <attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
      <attribute name="action">win.show-help-overlay</attribute>
    </item>
    <item>
      <attribute name="label" translatable="yes">_About {{name}}</attribute>
      <attribute name="action">app.about</attribute>
    </item>
  </section>
</menu>

The “Save as” menu item is bound to the win.save-as action; this means that activating the menu item will activate the save-as action registered on the TextViewerWindow window.

Add the “Save As” action

  1. Open the text_viewer-window.c file, and find the instance initialization function of the TextViewerWindow widget, text_viewer_window_init

  2. Create the save-as action, connect a callback to its activate signal, and add the action to the window

static void
text_viewer_window__open_file_dialog (GAction *action,
                                      GVariant *param,
                                      TextViewerWindow *self);

static void
text_viewer_window__save_file_dialog (GAction *action,
                                      GVariant *param,
                                      TextViewerWindow *self);

static void
text_viewer_window_init (TextViewerWindow *self)
{
  gtk_widget_init_template (GTK_WIDGET (self));

  g_autoptr (GSimpleAction) open_action = g_simple_action_new ("open", NULL);
  g_signal_connect (open_action, "activate", G_CALLBACK (text_viewer_window__open_file_dialog), self);
  g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (open_action));

  g_autoptr (GSimpleAction) save_action = g_simple_action_new ("save-as", NULL);
  g_signal_connect (save_action, "activate", G_CALLBACK (text_viewer_window__save_file_dialog), self);
  g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (save_action));

  GtkTextBuffer *buffer = gtk_text_view_get_buffer (self->main_text_view);
  g_signal_connect (buffer,
                    "notify::cursor-position",
                    G_CALLBACK (text_viewer_window__update_cursor_position),
                    self);
}

Select a file

  1. In the activate callback for the save-as action, create a file selection dialog using the GTK_FILE_CHOOSER_ACTION_SAVE action, and connect to its response signal

static void
text_viewer_window__save_file_dialog (GAction          *action G_GNUC_UNUSED,
                                      GVariant         *param G_GNUC_UNUSED,
                                      TextViewerWindow *self)
{
  g_autoptr (GtkFileDialog) dialog =
    gtk_file_dialog_new ();

  gtk_file_dialog_save (dialog,
                        GTK_WINDOW (self),
                        NULL,
                        on_save_response,
                        self);
}
  1. In the callback, retrieve the GFile for the location selected by the user, and call the save_file() function

static void
on_save_response (GObject      *source,
                  GAsyncResult *result,
                  gpointer      user_data)
{
  GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
  TextViewerWindow *self = user_data;

  g_autoptr (GFile) file =
    gtk_file_dialog_save_finish (dialog, result, NULL);

  if (file != NULL)
    save_file (self, file);
}

Save the contents of the text buffer

  1. In the save_file function, retrieve the contents of the GtkTextBuffer using the start and end GtkTextIter as the bounds of the buffer, then start an asynchronous operation to save the data in the location pointed by the GFile

 static void
 save_file (TextViewerWindow *self,
            GFile            *file)
 {
   GtkTextBuffer *buffer = gtk_text_view_get_buffer (self->main_text_view);

   // Retrieve the iterator at the start of the buffer
   GtkTextIter start;
   gtk_text_buffer_get_start_iter (buffer, &start);

   // Retrieve the iterator at the end of the buffer
   GtkTextIter end;
   gtk_text_buffer_get_end_iter (buffer, &end);

   // Retrieve all the visible text between the two bounds
   char *text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);

   // If there is nothing to save, return early
   if (text == NULL)
     return;

   g_autoptr(GBytes) bytes = g_bytes_new_take (text, strlen (text));

   // Start the asynchronous operation to save the data into the file
   g_file_replace_contents_bytes_async (file
                                        bytes,
                                        NULL,
                                        FALSE,
                                        G_FILE_CREATE_NONE,
                                        NULL,
                                        save_file_complete,
                                        self);
}
  1. In the save_file_complete function, finish the asynchronous operation and report any error

static void
save_file_complete (GObject      *source_object,
                    GAsyncResult *result,
                    gpointer      user_data)
{
  GFile *file = G_FILE (source_object);

  g_autoptr (GError) error =  NULL;
  g_file_replace_contents_finish (file, result, NULL, &error);

  // Query the display name for the file
  g_autofree char *display_name = NULL;
  g_autoptr (GFileInfo) info =
  g_file_query_info (file,
                     "standard::display-name",
                     G_FILE_QUERY_INFO_NONE,
                     NULL,
                     NULL);
  if (info != NULL)
    {
      display_name =
        g_strdup (g_file_info_get_attribute_string (info, "standard::display-name"));
    }
  else
    {
      display_name = g_file_get_basename (file);
    }

  if (error != NULL)
    {
      g_printerr ("Unable to save “%s”: %s\n",
                  display_name,
                  error->message);
    }
}

Add a key shortcut for the “Save As” action

  1. Open the text_viewer-application.c source file and find the TextViewerApplication instance initialization function text_viewer_application_init

  2. Add Ctrl + Shift + S as the accelerator shortcut for the win.save-as action

static void
text_viewer_application_init (TextViewerApplication *self)
{
  // ...
  gtk_application_set_accels_for_action (GTK_APPLICATION (self),
                                         "win.save-as",
                                         (const char *[]) {
                                           "<Ctrl><Shift>s",
                                           NULL,
                                         });
}

Add the “Save As” shortcut to the Keyboard Shortcuts help

  1. Find the help-overlay.ui file in the sources directory

  2. Find the GtkShortcutsGroup definition

  3. Add a new GtkShortcutsShortcut definition for the win.save action in the shortcuts group

<object class="GtkShortcutsGroup">
  <property name="title" translatable="yes" context="shortcut window">General</property>
  <child>
    <object class="GtkShortcutsShortcut">
      <property name="title" translatable="yes" context="shortcut window">Open</property>
      <property name="action-name">win.open</property>
    </object>
  </child>
  <child>
    <object class="GtkShortcutsShortcut">
      <property name="title" translatable="yes" context="shortcut window">Save As</property>
      <property name="action-name">win.save-as</property>
    </object>
  </child>

At the end of this lesson, you should be able to:

  • select the “Save As” menu item from the primary menu

  • select a file from a dialog

  • save the contents of the text viewer in the selected file