Functions in TfeTextView

In this section I will explain functions in TfeTextView object.

tfe.h and tfetextview.h

tfe.h is a top header file and it includes gtk.h and all the header files. C source files tfeapplication.c and tfenotebook.c include tfe.h at the beginning.

#include <gtk/gtk.h>

#include "../tfetextview/tfetextview.h"
#include "tfenotebook.h"

../tfetextview/tfetextview.h is a header file which describes the public functions in tfetextview.c.

#ifndef __TFE_TEXT_VIEW_H__
#define __TFE_TEXT_VIEW_H__

#include <gtk/gtk.h>

#define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)

/* "open-response" signal response */
enum TfeTextViewOpenResponseType
{
  TFE_OPEN_RESPONSE_SUCCESS,
  TFE_OPEN_RESPONSE_CANCEL,
  TFE_OPEN_RESPONSE_ERROR
};

GFile *
tfe_text_view_get_file (TfeTextView *tv);

void
tfe_text_view_open (TfeTextView *tv, GtkWindow *win);

void
tfe_text_view_save (TfeTextView *tv);

void
tfe_text_view_saveas (TfeTextView *tv);

GtkWidget *
tfe_text_view_new_with_file (GFile *file);

GtkWidget *
tfe_text_view_new (void);

#endif /* __TFE_TEXT_VIEW_H__ */

Functions to create TfeTextView instances

A TfeTextView instance is created with tfe_text_view_new or tfe_text_view_new_with_file.

GtkWidget *tfe_text_view_new (void);

tfe_text_view_new just creates a new TfeTextView instance and returns the pointer to the new instance.

GtkWidget *tfe_text_view_new_with_file (GFile *file);

tfe_text_view_new_with_file is given a Gfile object as an argument and it loads the file into the GtkTextBuffer instance, then returns the pointer to the new instance. If an error occurs during the creation process, NULL is returned.

Each function is defined as follows.

GtkWidget *
tfe_text_view_new_with_file (GFile *file) {
  g_return_val_if_fail (G_IS_FILE (file), NULL);

  GtkWidget *tv;
  GtkTextBuffer *tb;
  char *contents;
  gsize length;

  if (! g_file_load_contents (file, NULL, &contents, &length, NULL, NULL)) /* read error */
    return NULL;

  if ((tv = tfe_text_view_new()) != NULL) {
    tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
    gtk_text_buffer_set_text (tb, contents, length);
    TFE_TEXT_VIEW (tv)->file = g_file_dup (file);
    gtk_text_buffer_set_modified (tb, FALSE);
  }
  g_free (contents);
  return tv;
}

GtkWidget *
tfe_text_view_new (void) {
  return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
}

Save and saveas functions

Save and saveas functions write the contents in the GtkTextBuffer to a file.

void tfe_text_view_save (TfeTextView *tv)

The function tfe_text_view_save writes the contents in the GtkTextBuffer to a file specified by tv->file. If tv->file is NULL, then it shows GtkFileChooserDialog and prompts the user to choose a file to save. Then it saves the contents to the file and sets tv->file to point the GFile instance for the file.

void tfe_text_view_saveas (TfeTextView *tv)

The function saveas uses GtkFileChooserDialog and prompts the user to select a existed file or specify a new file to save. Then, the function changes tv->file and save the contents to the specified file. If an error occurs, it is shown to the user through the message dialog. The error is managed only in the TfeTextView and no information is notified to the caller.

static gboolean
save_file (GFile *file, GtkTextBuffer *tb, GtkWindow *win) {
  GtkTextIter start_iter;
  GtkTextIter end_iter;
  gchar *contents;
  gboolean stat;
  GtkWidget *message_dialog;
  GError *err = NULL;

  gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
  contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
  if (g_file_replace_contents (file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, &err)) {
    gtk_text_buffer_set_modified (tb, FALSE);
    stat = TRUE;
  } else {
    message_dialog = gtk_message_dialog_new (win, GTK_DIALOG_MODAL,
                                             GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
                                            "%s.\n", err->message);
    g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL);
    gtk_widget_show (message_dialog);
    g_error_free (err);
    stat = FALSE;
  }
  g_free (contents);
  return stat;
}

static void
saveas_dialog_response (GtkWidget *dialog, gint response, TfeTextView *tv) {
  GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
  GFile *file;
  GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW);

  if (response == GTK_RESPONSE_ACCEPT) {
    file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
    if (! G_IS_FILE (file))
      g_warning ("TfeTextView: gtk_file_chooser_get_file returns non GFile.\n");
    else if (save_file(file, tb, GTK_WINDOW (win))) {
      if (G_IS_FILE (tv->file))
        g_object_unref (tv->file);
      tv->file = file;
      g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0);
    } else
      g_object_unref (file);
  }
  gtk_window_destroy (GTK_WINDOW (dialog));
}

void
tfe_text_view_save (TfeTextView *tv) {
  g_return_if_fail (TFE_IS_TEXT_VIEW (tv));

  GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
  GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW);

  if (! gtk_text_buffer_get_modified (tb))
    return; /* no need to save it */
  else if (tv->file == NULL)
    tfe_text_view_saveas (tv);
  else if (! G_IS_FILE (tv->file))
    g_error ("TfeTextView: The pointer tv->file isn't NULL nor GFile.\n");
  else
    save_file (tv->file, tb, GTK_WINDOW (win));
}

void
tfe_text_view_saveas (TfeTextView *tv) {
  g_return_if_fail (TFE_IS_TEXT_VIEW (tv));

  GtkWidget *dialog;
  GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW);

  dialog = gtk_file_chooser_dialog_new ("Save file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_SAVE,
                                      "Cancel", GTK_RESPONSE_CANCEL,
                                      "Save", GTK_RESPONSE_ACCEPT,
                                      NULL);
  g_signal_connect (dialog, "response", G_CALLBACK (saveas_dialog_response), tv);
  gtk_widget_show (dialog);
}
Saveas process

When you use GtkFileChooserDialog, you need to divide the program into two parts. One is a function which creates GtkFileChooserDialog and the other is a signal handler. The function just creates and shows GtkFileChooserDialog. The rest is done by the handler. It gets Gfile from GtkFileChooserDialog and saves the buffer to the file by calling save_file.

Open function

Open function shows GtkFileChooserDialog to users and prompts them to choose a file. Then it reads the file and puts the text into GtkTextBuffer.

void tfe_text_view_open (TfeTextView *tv, GtkWindow *win);

The parameter win is the top-level window. It will be a transient parent window of GtkFileChooserDialog when the dialog is created. This allows window managers to keep the dialog on top of the parent window, or center the dialog over the parent window. It is possible to give no parent window to the dialog. However, it is encouraged to give a parent window to dialog. This function might be called just after tv has been created. In that case, tv has not been incorporated into the widget hierarchy. Therefore it is impossible to get the top-level window from tv. That’s why the function needs win parameter.

This function is usually called when the buffer of tv is empty. However, even if the buffer is not empty, tfe_text_view_open doesn’t treat it as an error. If you want to revert the buffer, calling this function is appropriate. Otherwise probably bad things will happen.

static void
open_dialog_response(GtkWidget *dialog, gint response, TfeTextView *tv) {
  GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
  GFile *file;
  char *contents;
  gsize length;
  GtkWidget *message_dialog;
  GError *err = NULL;

  if (response != GTK_RESPONSE_ACCEPT)
    g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_CANCEL);
  else if (! G_IS_FILE (file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)))) {
    g_warning ("TfeTextView: gtk_file_chooser_get_file returns non GFile.\n");
    g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR);
  } else if (! g_file_load_contents (file, NULL, &contents, &length, NULL, &err)) { /* read error */
    g_object_unref (file);
    message_dialog = gtk_message_dialog_new (GTK_WINDOW (dialog), GTK_DIALOG_MODAL,
                                             GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
                                            "%s.\n", err->message);
    g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL);
    gtk_widget_show (message_dialog);
    g_error_free (err);
    g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR);
  } else {
    gtk_text_buffer_set_text (tb, contents, length);
    g_free (contents);
    if (G_IS_FILE (tv->file))
      g_object_unref (tv->file);
    tv->file = file;
    gtk_text_buffer_set_modified (tb, FALSE);
    g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_SUCCESS);
    g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0);
  }
  gtk_window_destroy (GTK_WINDOW (dialog));
}

void
tfe_text_view_open (TfeTextView *tv, GtkWindow *win) {
  g_return_if_fail (TFE_IS_TEXT_VIEW (tv));
  g_return_if_fail (GTK_IS_WINDOW (win));

  GtkWidget *dialog;

  dialog = gtk_file_chooser_dialog_new ("Open file", win, GTK_FILE_CHOOSER_ACTION_OPEN,
                                        "Cancel", GTK_RESPONSE_CANCEL,
                                        "Open", GTK_RESPONSE_ACCEPT,
                                        NULL);
  g_signal_connect (dialog, "response", G_CALLBACK (open_dialog_response), tv);
  gtk_widget_show (dialog);
}

Now let’s think about the whole process between the caller and TfeTextView. It is shown in the following diagram and you would think that it is really complicated. Because signal is the only way for GtkFileChooserDialog to communicate with others. In GTK 3, gtk_dialog_run function is available. It simplifies the process. However, in GTK 4, gtk_dialog_run is unavailable any more.

Caller and TfeTextView
  1. A caller gets a pointer tv to a TfeTextView instance by calling tfe_text_view_new.
  2. The caller connects the handler (left bottom in the diagram) and the signal “open-response”.
  3. It calls tfe_text_view_open to prompt the user to select a file from GtkFileChooserDialog.
  4. The dialog emits a signal and it invokes the handler open_dialog_response.
  5. The handler reads the file and inserts the text into GtkTextBuffer and emits a signal to inform the status as a response code.
  6. The handler out of the TfeTextView receives the signal.

Getting Gfile

gtk_text_view_get_file is a simple function shown as follows.

GFile *
tfe_text_view_get_file (TfeTextView *tv) {
  g_return_val_if_fail (TFE_IS_TEXT_VIEW (tv), NULL);

  if (G_IS_FILE (tv->file))
    return g_file_dup (tv->file);
  else
    return NULL;
}

The important thing is to duplicate tv->file. Otherwise, if the caller frees the GFile object, tv->file is no more guaranteed to point the GFile. Another reason to use g_file_dup is that GFile isn’t thread-safe. If you use GFile in the different thread, the duplication is necessary. See Gio API Reference, g_file_dup.

The API document and source file of tfetextview.c

Refer API document of TfeTextView. Its original markdown file is under the directory src/tfetextview.

All the source files are listed in Section 16. You can find them under src/tfe5 and src/tfetextview directories.