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: .. code-block:: xml :emphasize-lines: 3-6
_Save as... win.save-as _Keyboard Shortcuts win.show-help-overlay _About {{name}} app.about
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 ------------------------ .. tabs:: .. group-tab:: C 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 .. code-block:: c :emphasize-lines: 6-9, 20-22 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); } .. group-tab:: Python 1. Open the ``window.py`` file, and find the instance initialization method of the **TextViewerWindow** widget 2. Create the **save-as** action, connect a callback to its ``activate`` signal, and add the action to the window .. code-block:: python :emphasize-lines: 8-10 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) save_action = Gio.SimpleAction(name="save-as") save_action.connect("activate", self.save_file_dialog) self.add_action(save_action) buffer = self.main_text_view.get_buffer() buffer.connect("notify::cursor-position", self.update_cursor_position) .. group-tab:: Vala 1. Open the ``window.vala`` file, and find the instance intialization method of the **TextViewer.Window** widget 2. Create the **save-as** action, connect a callback to its ``activate`` signal, and add the action to the window .. code-block:: vala :emphasize-lines: 15-17 namespace TextViewer { public class Window : Gtk.ApplicationWindow { // ... 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); var save_action = new SimpleAction ("save-as", null); save_action.activate.connect (this.save_file_dialog); this.add_action (save_action); Gtk.TextBuffer buffer = this.text_view.buffer; buffer.notify["cursor-position"].connect (this.update_cursor_position); } } } .. group-tab:: JavaScript 1. Open the ``window.js`` file, and find the constructor of the **TextViewer.Window** widget 2. Create the **save-as** action, connect a callback to its ``activate`` signal, and add the action to the window .. code-block:: js :emphasize-lines: 13-15 export const TextViewerWindow = GObject.registerClass({ GTypeName: 'TextViewerWindow', Template: 'resource:///com/example/TextViewer/window.ui', InternalChildren: ['main_text_view', 'open_button', 'cursor_pos'], }, class TextViewerWindow extends Adw.ApplicationWindow { constructor(application) { super({ application }); const openAction = new Gio.SimpleAction({name: 'open'}); openAction.connect('activate', () => this.openFileDialog()); this.add_action(openAction); const saveAction = new Gio.SimpleAction({name: 'save-as'}); saveAction.connect('activate', () => this.saveFileDialog()); this.add_action(saveAction); const buffer = this._main_text_view.buffer; buffer.connect("notify::cursor-position", this.updateCursorPosition.bind(this)); } 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 .. tabs:: .. code-tab:: c 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); } .. code-tab:: python def save_file_dialog(self, action, _): native = Gtk.FileDialog() native.save(self, None, self.on_save_response) .. code-tab:: vala private void save_file_dialog (Variant? parameter) { var filechooser = new Gtk.FileChooserNative ("Save File As", this, Gtk.FileChooserAction.SAVE, "_Save", "_Cancel"); filechooser.response.connect ( ); filechooser.show (); } .. code-tab:: js saveFileDialog() { const fileDialog = new Gtk.FileDialog(); fileDialog.save(this, null, async (self, result) => { // we'll implement it in the next step }); } 2. In the callback, retrieve the **GFile** for the location selected by the user, and call the ``save_file()`` function .. tabs:: .. code-tab:: c 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); } .. code-tab:: python def on_save_response(self, dialog, result): file = dialog.save_finish(result) if file is not None: self.save_file(file) .. code-tab:: vala :emphasize-lines: 8-13 private void save_file_dialog (Variant? parameter) { var filechooser = new Gtk.FileChooserNative ("Save File As", this, Gtk.FileChooserAction.SAVE, "_Save", "_Cancel"); filechooser.response.connect ((dialog, response) { if (response == Gtk.ResponseType.ACCEPT) { File file = filechooser.get_file (); this.save_file (file); } }); filechooser.show (); } .. code-tab:: js :emphasize-lines: 5-13 saveFileDialog() { const fileDialog = new Gtk.FileDialog(); fileDialog.save(this, null, async (self, result) => { try { const file = self.save_finish(result); if (file) { await this.saveFile(file); // we will define this method soon } } catch(_) { // user closed the dialog without selecting any file } }); } Save the contents of the text buffer ------------------------------------ .. tabs:: .. group-tab:: C 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** .. code-block:: c 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); } 2. In the ``save_file_complete`` function, finish the asynchronous operation and report any error .. code-block:: c 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); } } .. group-tab:: Python 1. Import the GLib module alongside Adw, Gio, and Gtk .. code-block:: python from gi.repository import Adw, Gio, GLib, Gtk 2. 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** .. code-block:: python def save_file(self, file): buffer = self.main_text_view.get_buffer() # Retrieve the iterator at the start of the buffer start = buffer.get_start_iter() # Retrieve the iterator at the end of the buffer end = buffer.get_end_iter() # Retrieve all the visible text between the two bounds text = buffer.get_text(start, end, False) # If there is nothing to save, return early if not text: return bytes = GLib.Bytes.new(text.encode('utf-8')) # Start the asynchronous operation to save the data into the file file.replace_contents_bytes_async(bytes, None, False, Gio.FileCreateFlags.NONE, None, self.save_file_complete) 3. In the ``save_file_complete`` function, finish the asynchronous operation and report any error .. code-block:: python def save_file_complete(self, file, result): res = file.replace_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 res: print(f"Unable to save {display_name}") .. group-tab:: Vala 1. In the ``save_file`` function, retrieve the contents of the **Gtk.TextBuffer** using the start and end **Gtk.TextIter** as the bounds of the buffer, then start an asynchronous operation to save the data in the location pointed by the **File** .. code-block:: vala private void save_file (File file) { Gtk.TextBuffer buffer = this.main_text_view.buffer; // Retrieve the iterator at the start of the buffer Gtk.TextIter start; buffer.get_start_iter (out start); // Retrieve the iterator at the end of the buffer Gtk.TextIter end; buffer.get_end_iter (out end); // Retrieve all the visible text between the two bounds string? text = buffer.get_text (start, end, false); if (text == null || text.length == 0) return; var bytes = new Bytes.take (text.data); file.replace_contents_bytes_async.begin (bytes, null, false, FileCreateFlags.NONE, null, (object, result) => { }); } 2. In the lambda that gets called when the asynchronous function ends, finish the asynchronous operation and report any error .. code-block:: vala :emphasize-lines: 25-40 private void save_file (File file) { Gtk.TextBuffer buffer = this.main_text_view.buffer; // Retrieve the iterator at the start of the buffer Gtk.TextIter start; buffer.get_start_iter (out start); // Retrieve the iterator at the end of the buffer Gtk.TextIter end; buffer.get_end_iter (out end); // Retrieve all the visible text between the two bounds string? text = buffer.get_text (start, end, false); if (text == null || text.length == 0) return; var bytes = new Bytes.take (text.data); file.replace_contents_bytes_async.begin (bytes, null, false, FileCreateFlags.NONE, 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 (); } try { file.replace_contents_async.end (result, null); } catch (Error e) { stderr.printf ("Unable to save ā€œ%sā€: %s\n", display_name, e.message); } }); } .. group-tab:: JavaScript 1. Just like we did with the **load_contents_async** function when we were dealing with opening the file, let's turn the Gio's built-in **replace_contents_bytes_async** function into a `Promise` returning one. That way, we're going to be able to utilize the convenient `async`/`await` syntax. Open `main.js`, and add the following lines: .. code-block:: js :emphasize-lines: 13-15 import GObject from 'gi://GObject'; import Gio from 'gi://Gio'; import Gtk from 'gi://Gtk?version=4.0'; import Adw from 'gi://Adw?version=1'; import GLib from 'gi://GLib'; import { TextViewerWindow } from './window.js'; Gio._promisify(Gio.File.prototype, 'load_contents_async', 'load_contents_finish'); Gio._promisify(Gio.File.prototype, 'replace_contents_bytes_async', 'replace_contents_finish'); // ... 2. Back in `window.js`, we need to define a new **saveFile** function since it is going to be called when user selects a file to save the contents to. In the function, we need to 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**. .. code-block:: js async saveFile(file) { const buffer = this._main_text_view.buffer; // Retrieve the start and end iterators const startIterator = buffer.get_start_iter(); const endIterator = buffer.get_end_iter(); // Retrieve all the visible text between the two bounds const text = buffer.get_text(startIterator, endIterator, false); if (text === null || text.length === 0) { logWarning("Text is empty, ignoring"); return; } // Get file's name, which will be needed in case of an error let fileName; try { const fileInfo = file.query_info("standard::display-name", FileQueryInfoFlags.NONE); fileName = fileInfo.get_attribute_string("standard::display-name"); } catch(_) { fileName = file.get_basename(); } try { // Save the file (asynchronously) await file.replace_contents_bytes_async( new GLib.Bytes(text), null, false, Gio.FileCreateFlags.NONE, null); } catch(e) { logError(`Unable to save ${fileName}: ${e.message}`) } } Add a key shortcut for the "Save As" action ------------------------------------------- .. tabs:: .. group-tab:: C 1. Open the ``text_viewer-application.c`` source file and find the **TextViewerApplication** instance initialization function ``text_viewer_application_init`` 2. Add :kbd:`Ctrl` + :kbd:`Shift` + :kbd:`S` as the accelerator shortcut for the **win.save-as** action .. code-block:: c :emphasize-lines: 5-10 static void text_viewer_application_init (TextViewerApplication *self) { // ... gtk_application_set_accels_for_action (GTK_APPLICATION (self), "win.save-as", (const char *[]) { "s", NULL, }); } .. group-tab:: Python 1. Open the ``main.py`` source file and find the instance initialization function for the **Application** class 2. Add :kbd:`Ctrl` + :kbd:`Shift` + :kbd:`S` as the accelerator shortcut for the **win.save-as** action .. code-block:: python :emphasize-lines: 8 class Application(Adw.Application): def __init__(self): super().__init__(application_id='com.example.PyTextViewer', flags=Gio.ApplicationFlags.FLAGS_NONE) # ... self.set_accels_for_action('win.save-as', ['s']) .. group-tab:: Vala 1. Open the ``application.vala`` source file and find the instance initialization function for the **TextViewer.Application** class 2. Add :kbd:`Ctrl` + :kbd:`Shift` + :kbd:`S` as the accelerator shortcut for the **win.save-as** action .. code-block:: vala :emphasize-lines: 9 public Application () { Object (application_id: "com.example.TextViewer", flags: ApplicationFlags.FLAGS_NONE); } construct { // ... this.set_accels_for_action ("win.save-as", { "s" }); } .. group-tab:: JavaScript 1. Open the ``main.js`` source file and find the constructor for the **TextViewerApplication** class 2. Add :kbd:`Ctrl` + :kbd:`Shift` + :kbd:`S` as the accelerator shortcut for the **win.save-as** action .. code-block:: js :emphasize-lines: 5 constructor() { super({application_id: 'com.example.TextViewer', flags: Gio.ApplicationFlags.FLAGS_NONE}); this.set_accels_for_action('win.open', [ 'o' ]); this.set_accels_for_action('win.save-as', [ 's' ]); // ... } 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 .. code-block:: xml :emphasize-lines: 9-14 General Open win.open Save As win.save-as 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