Loading Content From A File#

In this lesson you will learn how to ask the user to select a file, load the file’s contents, and then put those contents into the text area of our text viewer.

../../../_images/opening_files.png

Add an “Open” button#

In order to open a file, you need to let the user select it. You can follow these instructions to add a button to the window’s header bar that will open a file selection dialog.

Update the UI definition#

  1. Open the text_viewer-window.ui file

  2. Find the object definition for the GtkHeaderBar widget

  3. Add an object definition for a GtkButton as a child of the header bar, packing it at the leading edge of the window decoration using the start type:

<object class="GtkHeaderBar" id="header_bar">
  <child type="start">
    <object class="GtkButton" id="open_button">
      <property name="label">Open</property>
      <property name="action-name">win.open</property>
    </object>
  </child>
  <child type="end">
    <object class="GtkMenuButton">
      <property name="icon-name">open-menu-symbolic</property>
      <property name="menu-model">primary_menu</property>
    </object>
  </child>
  1. The button has the open_button identifier, so you can bind it in the window template.

  2. The button also has an action-name property set to win.open; this action will be activated when the user presses the button.

Bind the template in your source code#

  1. Open the text_viewer-window.c file

  2. Add the open_button widget to the instance structure of TextViewerWindow:

struct _TextViewerWindow
{
  GtkApplicationWindow  parent_instance;

  /* Template widgets */
  GtkHeaderBar *header_bar;
  GtkTextView *main_text_view;
  GtkButton *open_button;
};
  1. Bind the open_button widget in the class initialization for TextViewerWindow, text_viewer_window_class_init:

static void
text_viewer_window_class_init (TextViewerWindowClass *klass)
{
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  gtk_widget_class_set_template_from_resource (widget_class, "/com/example/TextViewer/text_viewer-window.ui");
  gtk_widget_class_bind_template_child (widget_class,
                                        TextViewerWindow,
                                        header_bar);
  gtk_widget_class_bind_template_child (widget_class,
                                        TextViewerWindow,
                                        main_text_view);
  gtk_widget_class_bind_template_child (widget_class,
                                        TextViewerWindow,
                                        open_button);
}

Add the “Open” action#

Add the open action to the instance initialization for TextViewerWindow.

Once you add the open action to the window, you can address it as win.open:

  1. Modify the TextViewerWindow instance initialization function text_viewer_window_init to create a GSimpleAction and add it to the window

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));
}
  1. Open the text_viewer-application.c source file and find the TextViewerApplication instance initialization function text_viewer_application_init

  2. Add Ctrl + O as the accelerator shortcut for the win.open action

    static void
    text_viewer_application_init (TextViewerApplication *self)
    {
      g_autoptr (GSimpleAction) quit_action =
        g_simple_action_new ("quit", NULL);
      g_signal_connect_swapped (quit_action,
                                "activate",
                                G_CALLBACK (g_application_quit),
                                self);
      g_action_map_add_action (G_ACTION_MAP (self),
                               G_ACTION (quit_action));
    
      g_autoptr (GSimpleAction) about_action =
        g_simple_action_new ("about", NULL);
      g_signal_connect (about_action,
                        "activate",
                        G_CALLBACK (text_viewer_application_show_about),
                        self);
      g_action_map_add_action (G_ACTION_MAP (self),
                               G_ACTION (about_action));
    
      gtk_application_set_accels_for_action (GTK_APPLICATION (self),
                                             "app.quit",
                                             (const char *[]) {
                                               "<Ctrl>q",
                                               NULL,
                                             });
    
      gtk_application_set_accels_for_action (GTK_APPLICATION (self),
                                             "win.open",
                                             (const char *[]) {
                                               "<Ctrl>o",
                                               NULL,
                                             });
    }
    

Select a file#

Now that you have added action, you must define the function that will be called when the action is activated.

  1. Inside the text_viewer_window__open_file_dialog function you create a GtkFileChooserNative object, which will present a file selection dialog to the user:

static void
text_viewer_window__open_file_dialog (GAction          *action G_GNUC_UNUSED,
                                      GVariant         *parameter G_GNUC_UNUSED,
                                      TextViewerWindow *self)
{
  // Create a new file selection dialog, using the "open" mode
  GtkFileChooserNative *native =
    gtk_file_chooser_native_new ("Open File",
                                 GTK_WINDOW (self),
                                 GTK_FILE_CHOOSER_ACTION_OPEN,
                                 "_Open",
                                 "_Cancel");

  // Connect the "response" signal of the file selection dialog;
  // this signal is emitted when the user selects a file, or when
  // they cancel the operation
  g_signal_connect (native,
                    "response",
                    G_CALLBACK (on_open_response),
                    self);

  // Present the dialog to the user
  gtk_native_dialog_show (GTK_NATIVE_DIALOG (native));
}
  1. The on_open_response function handles the response of of the user once they have selected the file and closed the dialog, or simply closed the dialog without selecting a file:

static void
on_open_response (GtkNativeDialog  *native,
                  int               response,
                  TextViewerWindow *self)
{
  // If the user selected a file...
  if (response == GTK_RESPONSE_ACCEPT)
    {
      GtkFileChooser *chooser = GTK_FILE_CHOOSER (native);

      // ... retrieve the location from the dialog...
      g_autoptr (GFile) file = gtk_file_chooser_get_file (chooser);

      // ... and open it
      open_file (self, file);
    }

  // Release the reference on the file selection dialog now that we
  // do not need it any more
  g_object_unref (native);
}

Read the contents of a file#

Reading the contents of a file can take an arbitrary amount of time, and block the application’s control flow. For this reason, it’s recommended that you load the file asynchronously. This requires starting the “read” operation in the open_file function:

static void
open_file (TextViewerWindow *self,
           GFile            *file)
{
  g_file_load_contents_async (file,
                              NULL,
                              (GAsyncReadyCallback) open_file_complete,
                              self);
}

Once the asynchronous operation is complete, or if there has been an error, the open_file_complete function will be called, and you will need to complete the asynchronous loading operation:

static void
open_file_complete (GObject          *source_object,
                    GAsyncResult     *result,
                    TextViewerWindow *self)
{
  GFile *file = G_FILE (source_object);

  g_autofree char *contents = NULL;
  gsize length = 0;

  g_autoptr (GError) error = NULL;

  // Complete the asynchronous operation; this function will either
  // give you the contents of the file as a byte array, or will
  // set the error argument
  g_file_load_contents_finish (file,
                               result,
                               &contents,
                               &length,
                               NULL,
                               &error);

  // In case of error, print a warning to the standard error output
  if (error != NULL)
    {
      g_printerr ("Unable to open “%s”: %s\n",
                  g_file_peek_path (file),
                  error->message);
      return;
    }
 }

Show the contents inside the text area#

Now that you have the contents of the file, you can display them in the GtkTextView widget.

  1. Verify that the contents of the file are encoded using UTF-8, as that is what GTK requires for all its text widgets

static void
open_file_complete (GObject          *source_object,
                    GAsyncResult     *result,
                    TextViewerWindow *self)
{
  GFile *file = G_FILE (source_object);

  g_autofree char *contents = NULL;
  gsize length = 0;

  g_autoptr (GError) error = NULL;

  // Complete the asynchronous operation; this function will either
  // give you the contents of the file as a byte array, or will
  // set the error argument
  g_file_load_contents_finish (file,
                               result,
                               &contents,
                               &length,
                               NULL,
                               &error);

  // In case of error, print a warning to the standard error output
  if (error != NULL)
    {
      g_printerr ("Unable to open the “%s”: %s\n",
                  g_file_peek_path (file),
                  error->message);
      return;
    }

  // Ensure that the file is encoded with UTF-8
  if (!g_utf8_validate (contents, length, NULL))
    {
      g_printerr ("Unable to load the contents of “%s”: "
                  "the file is not encoded with UTF-8\n",
                  g_file_peek_path (file));
      return;
    }
}
  1. Modify the open_file_complete function to retrieve the GtkTextBuffer instance that the GtkTextView widget uses to store the text, and set the its contents

static void
open_file_complete (GObject          *source_object,
                    GAsyncResult     *result,
                    TextViewerWindow *self)
{
  GFile *file = G_FILE (source_object);

  g_autofree char *contents = NULL;
  gsize length = 0;

  g_autoptr (GError) error = NULL;

  // Complete the asynchronous operation; this function will either
  // give you the contents of the file as a byte array, or will
  // set the error argument
  g_file_load_contents_finish (file,
                               result,
                               &contents,
                               &length,
                               NULL,
                               &error);

  // In case of error, print a warning to the standard error output
  if (error != NULL)
    {
      g_printerr ("Unable to open the “%s”: %s\n",
                  g_file_peek_path (file),
                  error->message);
      return;
    }

  // Ensure that the file is encoded with UTF-8
  if (!g_utf8_validate (contents, length, NULL))
    {
      g_printerr ("Unable to load the contents of “%s”: "
                  "the file is not encoded with UTF-8\n",
                  g_file_peek_path (file));
      return;
    }

  // Retrieve the GtkTextBuffer instance that stores the
  // text displayed by the GtkTextView widget
  GtkTextBuffer *buffer = gtk_text_view_get_buffer (self->main_text_view);

  // Set the text using the contents of the file
  gtk_text_buffer_set_text (buffer, contents, length);

  // Reposition the cursor so it's at the start of the text
  GtkTextIter start;
  gtk_text_buffer_get_start_iter (buffer, &start);
  gtk_text_buffer_place_cursor (buffer, &start);
}

Update the title of the window#

Since the application now is showing the contents of a specific file, you should ensure that the user interface reflects this new state. One way to do this is to update the title of the window with the name of the file.

Since the name of the file uses the raw encoding for files provided by the operating system, we need to query the file for its display name.

  1. Modify the open_file_complete function to query the “display name” file attribute

  2. Set the title of the window using the display name

static void
open_file_complete (GObject          *source_object,
                    GAsyncResult     *result,
                    TextViewerWindow *self)
{
  GFile *file = G_FILE (source_object);

  g_autofree char *contents = NULL;
  gsize length = 0;

  g_autoptr (GError) error = NULL;

  // Complete the asynchronous operation; this function will either
  // give you the contents of the file as a byte array, or will
  // set the error argument
  g_file_load_contents_finish (file,
                               result,
                               &contents,
                               &length,
                               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);
    }

  // In case of error, print a warning to the standard error output
  if (error != NULL)
    {
      g_printerr ("Unable to open “%s”: %s\n",
                  g_file_peek_path (file),
                  error->message);
      return;
    }

  // Ensure that the file is encoded with UTF-8
  if (!g_utf8_validate (contents, length, NULL))
    {
      g_printerr ("Unable to load the contents of “%s”: "
                  "the file is not encoded with UTF-8\n",
                  g_file_peek_path (file));
      return;
    }

  // Retrieve the GtkTextBuffer instance that stores the
  // text displayed by the GtkTextView widget
  GtkTextBuffer *buffer = gtk_text_view_get_buffer (self->main_text_view);

  // Set the text using the contents of the file
  gtk_text_buffer_set_text (buffer, contents, length);

  // Reposition the cursor so it's at the start of the text
  GtkTextIter start;
  gtk_text_buffer_get_start_iter (buffer, &start);
  gtk_text_buffer_place_cursor (buffer, &start);

  // Set the title using the display name
  gtk_window_set_title (GTK_WINDOW (self), display_name);
}

Add the “Open” shortcut to the Keyboard Shortcuts help#

The Keyboard Shortcuts help dialog is part of the GNOME application template in GNOME Builder. GTK automatically handles its creation and the action that presents it to the user.

  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.open 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>

You should now be able to run the application, press the Open button or Ctrl + O, and select a text file in your system. For instance, you can navigate to the text viewer project directory, and select the COPYING file in the sources:

../../../_images/opening_files_main.png