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.

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:
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));
}
Open the
text_viewer-application.c
source file and find the TextViewerApplication instance initialization functiontext_viewer_application_init
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, }); }
Modify the TextViewerWindow instance initialization to create a GSimpleAction and add it to the window
from gi.repository import Adw, Gio, Gtk
@Gtk.Template(resource_path='/com/example/TextViewer/window.ui')
class TextViewerWindow(Gtk.ApplicationWindow):
__gtype_name__ = 'TextViewerWindow'
main_text_view = Gtk.Template.Child()
open_button = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
open_action = Gio.SimpleAction(name="open")
open_action.connect("activate", self.open_file_dialog)
self.add_action(open_action)
def open_file_dialog(self, action, _):
pass
Open the
main.py
source file and find the Application instance initialization functionAdd Ctrl + O as the accelerator shortcut for the win.open action
class Application(Adw.Application): def __init__(self): super().__init__(application_id='com.example.TextViewer', flags=Gio.ApplicationFlags.FLAGS_NONE) self.create_action('about', self.on_about_action) self.create_action('preferences', self.on_preferences_action) self.set_accels_for_action('win.open', ['<Ctrl>o'])
Add a construct block to TextViewer.Window to create a SimpleAction and add it to the window
namespace TextViewer {
[GtkTemplate (ui = "/org/example/app/window.ui")]
public class Window : Gtk.ApplicationWindow {
[GtkChild]
private unowned Gtk.TextView main_text_view;
[GtkChild]
private unowned Gtk.Button open_button;
public Window (Gtk.Application app) {
Object (application: app);
}
construct {
var open_action = new SimpleAction ("open", null);
open_action.activate.connect (this.open_file_dialog);
this.add_action (open_action);
}
}
}
Open the
application.vala
source file and find the instance initialization functionAdd Ctrl + O as the accelerator shortcut for the win.open action
public Application () {
Object (application_id: "com.example.TextViewer",
flags: ApplicationFlags.FLAGS_NONE);
}
construct {
ActionEntry[] action_entries = {
{ "about", this.on_about_action },
{ "preferences", this.on_preferences_action },
{ "quit", this.quit }
};
this.add_action_entries (action_entries, this);
this.set_accels_for_action ("app.quit", { "<primary>q" });
this.set_accels_for_action ("win.open", { "<Ctrl>o" });
}
Select a file#
Now that you have added action, you must define the function that will be called when the action is activated.
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));
}
The
on_open_response
function handles the response 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);
}
Inside the
open_file_dialog
method you create a GtkFileChooserNative object, which will present a file selection dialog to the user:
def open_file_dialog(self, action, parameter):
# Create a new file selection dialog, using the "open" mode
# and keep a reference to it
self._native = Gtk.FileChooserNative(
title="Open File",
transient_for=self,
action=Gtk.FileChooserAction.OPEN,
accept_label="_Open",
cancel_label="_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
self._native.connect("response", self.on_open_response)
# Present the dialog to the user
self._native.show()
The
on_open_response
method handles the response of the user once they have selected the file and closed the dialog, or simply closed the dialog without selecting a file:
def on_open_response(self, dialog, response):
# If the user selected a file...
if response == Gtk.ResponseType.ACCEPT:
# ... retrieve the location from the dialog and open it
self.open_file(dialog.get_file())
# Release the reference on the file selection dialog now that we
# do not need it any more
self._native = None
def open_file(self, file):
pass
Inside the
open_file_dialog
method in TextViewer.Window inwindow.vala
, you create a Gtk.FileChooserNative object, which will present a file selection dialog to the user
private void open_file_dialog (Variant? parameter) {
// Create a new file selection dialog, using the "open" mode
// and keep a reference to it
var filechooser = new Gtk.FileChooserNative ("Open File", null, Gtk.FileChooserAction.OPEN, "_Open", "_Cancel") {
transient_for = this
};
filechooser.response.connect ( );
filechooser.show ();
}
When the filechooser emits the
response
signal, the following code in the lambda gets executed. This happens once the user has selected the file and closed the dialog, or simply closed the dialog without selecting a file:
private void open_file_dialog (Variant? parameter) {
// Create a new file selection dialog, using the "open" mode
// and keep a reference to it
var filechooser = new Gtk.FileChooserNative ("Open File", null, Gtk.FileChooserAction.OPEN, "_Open", "_Cancel") {
transient_for = this
};
filechooser.response.connect ((dialog, response) => {
// If the user selected a file...
if (response == Gtk.ResponseType.ACCEPT) {
// ... retrieve the location from the dialog and open it
this.open_file (filechooser.get_file ());
}
});
filechooser.show ();
}
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);
}
def open_file(self, file):
file.load_contents_async(None, self.open_file_complete)
private void open_file (File file) {
file.load_contents_async.begin (null, (object, result) => {});
}
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;
}
}
def open_file_complete(self, file, result):
contents = file.load_contents_finish(result)
if not contents[0]:
path = file.peek_path()
print(f"Unable to open {path}: {contents[1]}")
private void open_file (File file) {
file.load_contents_async.begin (null, (object, result) => {
uint8[] contents;
try {
// Complete the asynchronous operation; this function will either
// give you the contents of the file as a byte array, or will
// raise an exception
file.load_contents_async.end (result, out contents, null);
} catch (Error e) {
// In case of an error, print a warning to the standard error output
stderr.printf ("Unable to open “%s“: %s", file.peek_path (), e.message);
}
});
}
Show the contents inside the text area#
Now that you have the contents of the file, you can display them in the GtkTextView widget.
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;
}
}
Modify the
open_file_complete
function to retrieve the GtkTextBuffer instance that the GtkTextView widget uses to store the text, and set 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);
}
Verify that the contents of the file are encoded using UTF-8, as that is what GTK requires for all its text widgets
def open_file_complete(self, file, result):
contents = file.load_contents_finish(result)
if not contents[0]:
path = file.peek_path()
print(f"Unable to open {path}: {contents[1]}")
return
try:
text = contents[1].decode('utf-8')
except UnicodeError as err:
path = file.peek_path()
print(f"Unable to load the contents of {path}: the file is not encoded with UTF-8")
return
Modify the
open_file_complete
function to retrieve the GtkTextBuffer instance that the GtkTextView widget uses to store the text, and set its contents
def open_file_complete(self, file, result):
contents = file.load_contents_finish(result)
if not contents[0]:
path = file.peek_path()
print(f"Unable to open {path}: {contents[1]}")
return
try:
text = contents[1].decode('utf-8')
except UnicodeError as err:
path = file.peek_path()
print(f"Unable to load the contents of {path}: the file is not encoded with UTF-8")
return
buffer = self.main_text_view.get_buffer()
buffer.set_text(text)
start = buffer.get_start_iter()
buffer.place_cursor(start)
Verify that the contents of the file are encoded using UTF-8, as that is what GTK requires for all its text widgets
private void open_file (File file) {
file.load_contents_async.begin (null, (object, result) => {
uint8[] contents;
try {
// Complete the asynchronous operation; this function will either
// give you the contents of the file as a byte array, or will
// raise an exception
file.load_contents_async.end (result, out contents, null);
} catch (Error e) {
// In case of an error, print a warning to the standard error output
stderr.printf ("Unable to open “%s“: %s", file.peek_path (), e.message);
}
// Ensure that the file is encoded with UTF-8
if (!((string) contents).validate ())
stderr.printf ("Unable to load the contents of “%s”: "+
"the file is not encoded with UTF-8\n",
file.peek_path ());
});
}
Modify the handler of the end of loading to retrieve the Gtk.TextBuffer instance that the Gtk.TextView widget uses to store the text, and set its contents
private void open_file (File file) {
file.load_contents_async.begin (null, (object, result) => {
uint8[] contents;
try {
// Complete the asynchronous operation; this function will either
// give you the contents of the file as a byte array, or will
// raise an exception
file.load_contents_async.end (result, out contents, null);
} catch (Error e) {
// In case of an error, print a warning to the standard error output
stderr.printf ("Unable to open “%s“: %s", file.peek_path (), e.message);
}
// Ensure that the file is encoded with UTF-8
if (!((string) contents).validate ())
stderr.printf ("Unable to load the contents of “%s”: "+
"the file is not encoded with UTF-8\n",
file.peek_path ());
// Retrieve the GtkTextBuffer instance that stores the
// text displayed by the GtkTextView widget
Gtk.TextBuffer buffer = this.main_text_view.buffer;
// Set the text using the contents of the file
buffer.text = (string) contents;
// Reposition the cursor so it's at the start of the text
Gtk.TextIter start;
buffer.get_start_iter (out start);
buffer.place_cursor (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.
Modify the
open_file_complete
function to query the “display name” file attributeSet 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);
}
def open_file_complete(self, file, result):
contents = file.load_contents_finish(result)
info = file.query_info("standard::display-name", Gio.FileQueryInfoFlags.NONE)
if info:
display_name = info.get_attribute_string("standard::display-name")
else:
display_name = file.get_basename()
if not contents[0]:
path = file.peek_path()
print(f"Unable to open {path}: {contents[1]}")
return
try:
text = contents[1].decode('utf-8')
except UnicodeError as err:
path = file.peek_path()
print(f"Unable to load the contents of {path}: the file is not encoded with UTF-8")
return
buffer = self.main_text_view.get_buffer()
buffer.set_text(text)
start = buffer.get_start_iter()
buffer.place_cursor(start)
self.set_title(display_name)
private void open_file (File file) {
file.load_contents_async.begin (null, (object, result) => {
string display_name;
// Query the display name for the file
try {
FileInfo? info = file.query_info ("standard::display-name", FileQueryInfoFlags.NONE);
display_name = info.get_attribute_string ("standard::display-name");
} catch (Error e) {
display_name = file.get_basename ();
}
uint8[] contents;
try {
// Complete the asynchronous operation; this function will either
// give you the contents of the file as a byte array, or will
// raise an exception
file.load_contents_async.end (result, out contents, null);
} catch (Error e) {
// In case of an error, print a warning to the standard error output
stderr.printf ("Unable to open “%s“: %s", file.peek_path (), e.message);
}
// Ensure that the file is encoded with UTF-8
if (!((string) contents).validate ())
stderr.printf ("Unable to load the contents of “%s”: "+
"the file is not encoded with UTF-8\n",
file.peek_path ());
// Retrieve the GtkTextBuffer instance that stores the
// text displayed by the GtkTextView widget
Gtk.TextBuffer buffer = this.main_text_view.buffer;
// Set the text using the contents of the file
buffer.text = (string) contents;
// Reposition the cursor so it's at the start of the text
Gtk.TextIter start;
buffer.get_start_iter (out start);
buffer.place_cursor (start);
// Set the title using the display name
this.title = 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.
Find the
help-overlay.ui
file in the sources directoryFind the GtkShortcutsGroup definition
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:
