Template XML and composite widget

The tfe program in the previous section is not so good because many things are crammed into tfepplication.c. And many static variables in tfepplication.c. The file tfeapplication.c should be divided into several files.

The preference dialog is defined by a ui file. And it has GtkBox, GtkLabel and GtkFontButton in it. Such widget can be defined as a composite widget. Composite widget is:

Next subsection shows how to build a preference dialog.

Preference dialog

First, write a template XML file.

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="TfePref" parent="GtkDialog">
    <property name="title">Preferences</property>
    <property name="resizable">FALSE</property>
    <property name="modal">TRUE</property>
    <child internal-child="content_area">
      <object class="GtkBox" id="content_area">
        <child>
          <object class="GtkBox" id="pref_boxh">
            <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
            <property name="spacing">12</property>
            <property name="margin-start">12</property>
            <property name="margin-end">12</property>
            <property name="margin-top">12</property>
            <property name="margin-bottom">12</property>
            <child>
              <object class="GtkLabel" id="fontlabel">
                <property name="label">Font:</property>
                <property name="xalign">1</property>
              </object>
            </child>
            <child>
              <object class="GtkFontButton" id="fontbtn">
              </object>
            </child>
          </object>
        </child>
      </object>
    </child>
  </template>
</interface>

Template tag specifies a composite widget. The value of a class attribute is the object name. It is “TfePref”. A parent attribute specifies the direct parent class of the composite widget. Therefore. TfePref is a child class of GtkDialog. A parent attribute is optional. But it is recommended to specify it. Other lines are the same as before.

The class TfePref is defined like TfeTextView. There are two files tfepref.h and tfepref.c.

The file tfepref.h defines types and declares public functions. The definitions are public and open to any C files.

#ifndef __TFE_PREF_H__
#define __TFE_PREF_H__

#include <gtk/gtk.h>

#define TFE_TYPE_PREF tfe_pref_get_type ()
G_DECLARE_FINAL_TYPE (TfePref, tfe_pref, TFE, PREF, GtkDialog)

GtkWidget *
tfe_pref_new (void);

#endif /* __TFE_PREF_H__ */

The file tfepref.c includes:

#include <gtk/gtk.h>
#include "tfepref.h"

struct _TfePref
{
  GtkDialog parent;
  GSettings *settings;
  GtkFontButton *fontbtn;
};

G_DEFINE_TYPE (TfePref, tfe_pref, GTK_TYPE_DIALOG);

static void
tfe_pref_dispose (GObject *gobject) {
  TfePref *pref = TFE_PREF (gobject);

  g_clear_object (&pref->settings);
  G_OBJECT_CLASS (tfe_pref_parent_class)->dispose (gobject);
}

static void
tfe_pref_init (TfePref *pref) {
  gtk_widget_init_template (GTK_WIDGET (pref));
  pref->settings = g_settings_new ("com.github.ToshioCP.tfe");
  g_settings_bind (pref->settings, "font", pref->fontbtn, "font", G_SETTINGS_BIND_DEFAULT);
}

static void
tfe_pref_class_init (TfePrefClass *class) {
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  object_class->dispose = tfe_pref_dispose;
  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class), "/com/github/ToshioCP/tfe/tfepref.ui");
  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfePref, fontbtn);
}

GtkWidget *
tfe_pref_new (void) {
  return GTK_WIDGET (g_object_new (TFE_TYPE_PREF, NULL));
}

Now, It is very simple to use this dialog. A caller just creates this object and shows it.

TfePref *pref;
pref = tfe_pref_new ();
gtk_window_set_transient_for (GTK_WINDOW (pref), win); /* win is the main window */
gtk_window_present (GTK_WINDOW (pref));

This instance is automatically destroyed when a user clicks on the close button. That’s all. If you want to show the dialog again, just create and show it.

Preference dialog

Alert dialog

It is almost same as preference dialog.

Its ui file is:

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="TfeAlert" parent="GtkDialog">
    <property name="title">Are you sure?</property>
    <property name="resizable">FALSE</property>
    <property name="modal">TRUE</property>
    <child internal-child="content_area">
      <object class="GtkBox">
        <child>
          <object class="GtkBox">
            <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
            <property name="spacing">12</property>
            <property name="margin-start">12</property>
            <property name="margin-end">12</property>
            <property name="margin-top">12</property>
            <property name="margin-bottom">12</property>
            <child>
              <object class="GtkImage">
                <property name="icon-name">dialog-warning</property>
                <property name="icon-size">GTK_ICON_SIZE_LARGE</property>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="lb_alert">
              </object>
            </child>
          </object>
        </child>
      </object>
    </child>
    <child type="action">
      <object class="GtkButton" id="btn_cancel">
        <property name="label">Cancel</property>
      </object>
    </child>
    <child type="action">
      <object class="GtkButton" id="btn_accept">
        <property name="label">Close</property>
      </object>
    </child>
    <action-widgets>
      <action-widget response="cancel" default="true">btn_cancel</action-widget>
      <action-widget response="accept">btn_accept</action-widget>
    </action-widgets>
  </template>
</interface>

The header file is:

#ifndef __TFE_ALERT_H__
#define __TFE_ALERT_H__

#include <gtk/gtk.h>

#define TFE_TYPE_ALERT tfe_alert_get_type ()
G_DECLARE_FINAL_TYPE (TfeAlert, tfe_alert, TFE, ALERT, GtkDialog)

void
tfe_alert_set_message (TfeAlert *alert, const char *message);

void
tfe_alert_set_button_label (TfeAlert *alert, const char *label);

GtkWidget *
tfe_alert_new (void);

#endif /* __TFE_ALERT_H__ */

There are three public functions. The functions tfe_alert_set_message and tfe_alert_set_button_label sets the label and button name of the alert dialog. For example, if you want to show an alert that the user tries to close without saving the content, set them like:

tfe_alert_set_message (alert, "Contents aren't saved yet.\nAre you sure to close?");
tfe_alert_set_button_label (alert, "Close");

The function tfe_alert_new creates a TfeAlert dialog.

Alert dialog

The C source file is:

#include <gtk/gtk.h>
#include "tfealert.h"

struct _TfeAlert
{
  GtkDialog parent;
  GtkLabel *lb_alert;
  GtkButton *btn_accept;
};

G_DEFINE_TYPE (TfeAlert, tfe_alert, GTK_TYPE_DIALOG);

void
tfe_alert_set_message (TfeAlert *alert, const char *message) {
  gtk_label_set_text (alert->lb_alert, message);
}

void
tfe_alert_set_button_label (TfeAlert *alert, const char *label) {
  gtk_button_set_label (alert->btn_accept, label);
}

static void
tfe_alert_init (TfeAlert *alert) {
  gtk_widget_init_template (GTK_WIDGET (alert));
}

static void
tfe_alert_class_init (TfeAlertClass *class) {
  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class), "/com/github/ToshioCP/tfe/tfealert.ui");
  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfeAlert, lb_alert);
  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfeAlert, btn_accept);
}

GtkWidget *
tfe_alert_new (void) {
  return GTK_WIDGET (g_object_new (TFE_TYPE_ALERT, NULL));
}

The program is almost same as tfepref.c.

The Usage of the alert object is as follows.

  1. Write the “response” signal handler.
  2. Create a TfeAlert object.
  3. Connect “response” signal to a handler
  4. Show the dialog
  5. In the signal handler, do something with regard to the response-id and destroy the dialog.

Top-level window

TfeWindow is a child class of GtkApplicationWindow.

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="TfeWindow" parent="GtkApplicationWindow">
    <property name="title">file editor</property>
    <property name="default-width">600</property>
    <property name="default-height">400</property>
    <child>
      <object class="GtkBox" id="boxv">
        <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
        <child>
          <object class="GtkBox" id="boxh">
            <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
            <child>
              <object class="GtkLabel" id="dmy1">
                <property name="width-chars">10</property>
              </object>
            </child>
            <child>
              <object class="GtkButton" id="btno">
                <property name="label">Open</property>
                <property name="action-name">win.open</property>
              </object>
            </child>
            <child>
              <object class="GtkButton" id="btns">
                <property name="label">Save</property>
                <property name="action-name">win.save</property>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="dmy2">
                <property name="hexpand">TRUE</property>
              </object>
            </child>
            <child>
              <object class="GtkButton" id="btnc">
                <property name="label">Close</property>
                <property name="action-name">win.close</property>
              </object>
            </child>
            <child>
              <object class="GtkMenuButton" id="btnm">
                <property name="direction">down</property>
                <property name="halign">start</property>
                <property name="icon-name">open-menu-symbolic</property>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="dmy3">
                <property name="width-chars">10</property>
              </object>
            </child>
          </object>
        </child>
        <child>
          <object class="GtkNotebook" id="nb">
            <property name="scrollable">TRUE</property>
            <property name="hexpand">TRUE</property>
            <property name="vexpand">TRUE</property>
          </object>
        </child>
      </object>
    </child>
  </template>
</interface>

This XML file is almost same as before except template tag and “action-name” property in buttons.

GtkButton implements GtkActionable interface, which has “action-name” property. If this property is set, GtkButton activates the action when it is clicked. For example, if an open button is clicked, “win.open” action will be activated and open_activated handler will be invoked.

This action is also used by “<Control>o” accelerator (See tfeapplication.c). If you used “clicked” signal for the button, you would need its signal handler. Then, there would be two handlers:

These two handlers are almost same. It is inefficient. Connecting buttons to actions is a good way to reduce unnecessary codes.

#ifndef __TFE_WINDOW_H__
#define __TFE_WINDOW_H__

#include <gtk/gtk.h>

#define TFE_TYPE_WINDOW tfe_window_get_type ()
G_DECLARE_FINAL_TYPE (TfeWindow, tfe_window, TFE, WINDOW, GtkApplicationWindow)

void
tfe_window_notebook_page_new (TfeWindow *win);

void
tfe_window_notebook_page_new_with_files (TfeWindow *win, GFile **files, int n_files);

GtkWidget *
tfe_window_new (GtkApplication *app);

#endif /* __TFE_WINDOW_H__ */

There are three public functions. The function tfe_window_notebook_page_new creates a new notebook page. This is a wrapper function for notebook_page_new. It is called by TfeApplication object. The function tfe_window_notebook_page_new_with_files creates notebook pages with a contents read from the given files. The function tfe_window_new creates a TfeWindow instance.

#include <gtk/gtk.h>
#include "tfewindow.h"
#include "tfenotebook.h"
#include "tfepref.h"
#include "tfealert.h"

struct _TfeWindow {
  GtkApplicationWindow parent;
  GtkMenuButton *btnm;
  GtkNotebook *nb;
  gboolean is_quit;
};

G_DEFINE_TYPE (TfeWindow, tfe_window, GTK_TYPE_APPLICATION_WINDOW);

/* alert response signal handler */
static void
alert_response_cb (GtkDialog *alert, int response_id, gpointer user_data) {
  TfeWindow *win = TFE_WINDOW (user_data);

  gtk_window_destroy (GTK_WINDOW (alert));
  if (response_id == GTK_RESPONSE_ACCEPT) {
    if (win->is_quit)
      gtk_window_destroy(GTK_WINDOW (win));
    else
      notebook_page_close (win->nb);
  }
}

static gboolean
close_request_cb (TfeWindow *win) {
  TfeAlert *alert;

  if (has_saved_all (GTK_NOTEBOOK (win->nb)))
    return false;
  else {
    win->is_quit = true;
    alert = TFE_ALERT (tfe_alert_new ());
    gtk_window_set_transient_for (GTK_WINDOW (alert), GTK_WINDOW (win));
    tfe_alert_set_message (alert, "Contents aren't saved yet.\nAre you sure to quit?");
    tfe_alert_set_button_label (alert, "Quit");
    g_signal_connect (GTK_DIALOG (alert), "response", G_CALLBACK (alert_response_cb), win);
    gtk_window_present (GTK_WINDOW (alert));
    return true;
  }
}

/* ----- action activated handlers ----- */
static void
open_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
  TfeWindow *win = TFE_WINDOW (user_data);

  notebook_page_open (GTK_NOTEBOOK (win->nb));
}

static void
save_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
  TfeWindow *win = TFE_WINDOW (user_data);

  notebook_page_save (GTK_NOTEBOOK (win->nb));
}

static void
close_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
  TfeWindow *win = TFE_WINDOW (user_data);
  TfeAlert *alert;

  if (has_saved (win->nb))
    notebook_page_close (win->nb);
  else {
    win->is_quit = false;
    alert = TFE_ALERT (tfe_alert_new ());
    gtk_window_set_transient_for (GTK_WINDOW (alert), GTK_WINDOW (win));
    tfe_alert_set_message (alert, "Contents aren't saved yet.\nAre you sure to close?");
    tfe_alert_set_button_label (alert, "Close");
    g_signal_connect (GTK_DIALOG (alert), "response", G_CALLBACK (alert_response_cb), win);
    gtk_widget_show (GTK_WIDGET (alert));
  }
}

static void
new_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
  TfeWindow *win = TFE_WINDOW (user_data);

  notebook_page_new (GTK_NOTEBOOK (win->nb));
}

static void
saveas_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
  TfeWindow *win = TFE_WINDOW (user_data);

  notebook_page_saveas (GTK_NOTEBOOK (win->nb));
}

static void
pref_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
  TfeWindow *win = TFE_WINDOW (user_data);
  GtkWidget *pref;

  pref = tfe_pref_new ();
  gtk_window_set_transient_for (GTK_WINDOW (pref), GTK_WINDOW (win));
  gtk_window_present (GTK_WINDOW (pref));
}

static void
close_all_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
  TfeWindow *win = TFE_WINDOW (user_data);

  if (close_request_cb (win) == false)
    gtk_window_destroy (GTK_WINDOW (win));
}

/* --- public functions --- */

void
tfe_window_notebook_page_new (TfeWindow *win) {
  notebook_page_new (win->nb);
}

void
tfe_window_notebook_page_new_with_files (TfeWindow *win, GFile **files, int n_files) {
  int i;

  for (i = 0; i < n_files; i++)
    notebook_page_new_with_file (win->nb, files[i]);
  if (gtk_notebook_get_n_pages (win->nb) == 0)
    notebook_page_new (win->nb);
}

static void
tfe_window_init (TfeWindow *win) {
  GtkBuilder *build;
  GMenuModel *menu;

  gtk_widget_init_template (GTK_WIDGET (win));

  build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/menu.ui");
  menu = G_MENU_MODEL (gtk_builder_get_object (build, "menu"));
  gtk_menu_button_set_menu_model (win->btnm, menu);
  g_object_unref(build);

/* ----- action ----- */
  const GActionEntry win_entries[] = {
    { "open", open_activated, NULL, NULL, NULL },
    { "save", save_activated, NULL, NULL, NULL },
    { "close", close_activated, NULL, NULL, NULL },
    { "new", new_activated, NULL, NULL, NULL },
    { "saveas", saveas_activated, NULL, NULL, NULL },
    { "pref", pref_activated, NULL, NULL, NULL },
    { "close-all", close_all_activated, NULL, NULL, NULL }
  };
  g_action_map_add_action_entries (G_ACTION_MAP (win), win_entries, G_N_ELEMENTS (win_entries), win);

  g_signal_connect (GTK_WINDOW (win), "close-request", G_CALLBACK (close_request_cb), NULL);
}

static void
tfe_window_class_init (TfeWindowClass *class) {
  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class), "/com/github/ToshioCP/tfe/tfewindow.ui");
  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfeWindow, btnm);
  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfeWindow, nb);
}

GtkWidget *
tfe_window_new (GtkApplication *app) {
  return GTK_WIDGET (g_object_new (TFE_TYPE_WINDOW, "application", app, NULL));
}

TfeApplication

The file tfeaplication.h and tfeapplication.c are now very simple. The following is the header file.

#pragma once

#include <gtk/gtk.h>

#define TFE_TYPE_APPLICATION tfe_application_get_type ()
G_DECLARE_FINAL_TYPE (TfeApplication, tfe_application, TFE, APPLICATION, GtkApplication)

TfeApplication *
tfe_application_new (const char* application_id, GApplicationFlags flags);

The following is tfeapplication.c. It defines the application and supports:

#include <gtk/gtk.h>
#include "tfeapplication.h"
#include "tfewindow.h"
#include "pfd2css.h"

struct _TfeApplication {
  GtkApplication parent;
  TfeWindow *win;
  GSettings *settings;
  GtkCssProvider *provider;
};

G_DEFINE_TYPE (TfeApplication, tfe_application, GTK_TYPE_APPLICATION)

/* gsettings changed::font signal handler */
static void
changed_font_cb (GSettings *settings, char *key, gpointer user_data) {
  TfeApplication *app = TFE_APPLICATION (user_data);
  char *font, *s, *css;
  PangoFontDescription *pango_font_desc;

  font = g_settings_get_string (app->settings, "font");
  pango_font_desc = pango_font_description_from_string (font);
  g_free (font);
  s = pfd2css (pango_font_desc); // converts Pango Font Description into CSS style string
  css = g_strdup_printf ("textview {%s}", s);
  gtk_css_provider_load_from_data (app->provider, css, -1);
  g_free (s);
  g_free (css);
}

/* ----- activate, open, startup handlers ----- */
static void
app_activate (GApplication *application) {
  TfeApplication *app = TFE_APPLICATION (application);

  tfe_window_notebook_page_new (app->win);
  gtk_window_present (GTK_WINDOW (app->win));
}

static void
app_open (GApplication *application, GFile ** files, gint n_files, const gchar *hint) {
  TfeApplication *app = TFE_APPLICATION (application);

  tfe_window_notebook_page_new_with_files (app->win, files, n_files);
  gtk_window_present (GTK_WINDOW (app->win));
}

static void
app_startup (GApplication *application) {
  TfeApplication *app = TFE_APPLICATION (application);
  GtkCssProvider *provider0;
  GdkDisplay *display;
  int i;

  app->win = TFE_WINDOW (tfe_window_new (GTK_APPLICATION (app)));
  provider0 = gtk_css_provider_new ();
  gtk_css_provider_load_from_data (provider0, "textview {padding: 10px;}", -1);
  display = gdk_display_get_default ();
  gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider0),
                                              GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
  g_object_unref (provider0);
  app->provider = gtk_css_provider_new ();
  gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (app->provider),
                                              GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
  app->settings = g_settings_new ("com.github.ToshioCP.tfe");
  g_signal_connect (app->settings, "changed::font", G_CALLBACK (changed_font_cb), app);
  changed_font_cb(app->settings, "font", app);

/* ----- accelerator ----- */ 
  struct {
    const char *action;
    const char *accels[2];
  } action_accels[] = {
    { "win.open", { "<Control>o", NULL } },
    { "win.save", { "<Control>s", NULL } },
    { "win.close", { "<Control>w", NULL } },
    { "win.new", { "<Control>n", NULL } },
    { "win.saveas", { "<Shift><Control>s", NULL } },
    { "win.close-all", { "<Control>q", NULL } },
  };

  for (i = 0; i < G_N_ELEMENTS(action_accels); i++)
    gtk_application_set_accels_for_action(GTK_APPLICATION(app), action_accels[i].action, action_accels[i].accels);
}

static void
tfe_application_dispose (GObject *gobject) {
  TfeApplication *app = TFE_APPLICATION (gobject);

  g_clear_object (&app->settings);
  g_clear_object (&app->provider);
  G_OBJECT_CLASS (tfe_application_parent_class)->dispose (gobject);
}

static void
tfe_application_init (TfeApplication *app) {
  g_signal_connect (G_APPLICATION (app), "startup", G_CALLBACK (app_startup), NULL);
  g_signal_connect (G_APPLICATION (app), "activate", G_CALLBACK (app_activate), NULL);
  g_signal_connect (G_APPLICATION (app), "open", G_CALLBACK (app_open), NULL);
}

static void
tfe_application_class_init (TfeApplicationClass *class) {
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  object_class->dispose = tfe_application_dispose;
}

TfeApplication *
tfe_application_new (const char* application_id, GApplicationFlags flags) {
  return TFE_APPLICATION (g_object_new (TFE_TYPE_APPLICATION, "application-id", application_id, "flags", flags, NULL));
}

Other files

main.c

#include <gtk/gtk.h>
#include "tfeapplication.h"

#define APPLICATION_ID "com.github.ToshioCP.tfe"

int
main (int argc, char **argv) {
  TfeApplication *app;
  int stat;

  app = tfe_application_new (APPLICATION_ID, G_APPLICATION_HANDLES_OPEN);
  stat =g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);
  return stat;
}

CSS related files pfd2css.h and pfd2css.c are the same as the previous section.

Resource XML file.

<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/com/github/ToshioCP/tfe">
    <file>tfewindow.ui</file>
    <file>tfepref.ui</file>
    <file>tfealert.ui</file>
    <file>menu.ui</file>
  </gresource>
</gresources>

GSchema XML file

<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
  <schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">
    <key name="font" type="s">
      <default>'Monospace 12'</default>
      <summary>Font</summary>
      <description>A font to be used for textview.</description>
    </key>
  </schema>
</schemalist>

Meson.build

project('tfe', 'c')

gtkdep = dependency('gtk4')

gnome=import('gnome')
resources = gnome.compile_resources('resources','tfe.gresource.xml')
gnome.compile_schemas(build_by_default: true, depend_files: 'com.github.ToshioCP.tfe.gschema.xml')

sourcefiles=files('main.c', 'tfeapplication.c', 'tfewindow.c', 'tfenotebook.c', 'tfepref.c', 'tfealert.c', 'pfd2css.c', '../tfetextview/tfetextview.c')

executable('tfe', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true)

schema_dir = get_option('prefix') / get_option('datadir') / 'glib-2.0/schemas/'
install_data('com.github.ToshioCP.tfe.gschema.xml', install_dir: schema_dir)
gnome.post_install (glib_compile_schemas: true)

Compilation and installation.

If you want to install it to your local area, use --prefix=$HOME/.local or --prefix=$HOME option. If you want to install it to the system area, no option is needed. It will be installed under /user/local directory.

$ meson --prefix=$HOME/.local _build
$ ninja -C _build
$ ninja -C _build install

You need root privilege to install it to the system area..

$ meson _build
$ ninja -C _build
$ sudo ninja -C _build install

Source files are in src/tfe7 directory.

Composite widgets give us two advantages.

We made a very small text editor. You can add features to this editor. When you add a new feature, be careful about the structure of the program. Maybe you need to divide a file into several files like this section. It isn’t good to put many things into one file. And it is important to think about the relationship between source files and widget structures.