A preference dialog

A typical application will have some preferences that should be remembered from one run to the next. Even for our simple example application, we may want to change the font that is used for the content.

We are going to use Gio::Settings to store our preferences. Gio::Settings requires a schema that describes our settings, in our case the org.gtkmm.exampleapp.gschema.xml file.

Before we can make use of this schema in our application, we need to compile it into the binary form that Gio::Settings expects. GIO provides macros to do this in autotools-based projects. See the description of GSettings. Meson provides the compile_schemas() function in the GNOME module.

Next, we need to connect our settings to the widgets that they are supposed to control. One convenient way to do this is to use Gio::Settings::bind() to bind settings keys to object properties, as we do for the transition setting in ExampleAppWindow's constructor.

m_settings = Gio::Settings::create("org.gtkmm.exampleapp");
m_settings->bind("transition", m_stack->property_transition_type());

The code to connect the font setting is a little more involved, since it corresponds to an object property in a Gtk::TextTag that we must first create. The code is in ExampleAppWindow::open_file_view().

auto tag = buffer->create_tag();
m_settings->bind("font", tag->property_font());
buffer->apply_tag(tag, buffer->begin(), buffer->end());

At this point, the application will already react if you change one of the settings, e.g. using the gsettings commandline tool. Of course, we expect the application to provide a preference dialog for these. So lets do that now. Our preference dialog will be a subclass of Gtk::Window, and we'll use the same techniques that we've already seen in ExampleAppWindow: a Gtk::Builder ui file and settings bindings. In this case the bindings are more involved, though. We use Gtk::FontDialogButton and Gtk::DropDown in the preference dialog. The types of the properties in these classes can't be automatically converted to the string type that Gio::Settings requires.

When we've created the prefs.ui file and the ExampleAppPrefs class, we revisit the ExampleApplication::on_action_preferences() method in our application class, and make it open a new preference dialog.

auto prefs_dialog = ExampleAppPrefs::create(*get_active_window());
prefs_dialog->present();

After all this work, our application can now show a preference dialog like this:

Figure 31.5. A preference dialog

A preference dialog

Source Code

File: exampleapplication.h (For use with gtkmm 4)

#include "../step4/exampleapplication.h"
// Equal to the corresponding file in step4

File: exampleappprefs.h (For use with gtkmm 4)

#ifndef GTKMM_EXAMPLEAPPPREFS_H_
#define GTKMM_EXAMPLEAPPPREFS_H_

#include <gtkmm.h>

#ifdef GLIBMM_CHECK_VERSION
#define HAS_GIO_SETTINGS_BIND_WITH_MAPPING GLIBMM_CHECK_VERSION(2,75,0)
#else
#define HAS_GIO_SETTINGS_BIND_WITH_MAPPING 0
#endif

class ExampleAppPrefs : public Gtk::Window
{
public:
  ExampleAppPrefs(BaseObjectType* cobject,
    const Glib::RefPtr<Gtk::Builder>& refBuilder);

  static ExampleAppPrefs* create(Gtk::Window& parent);

protected:
#if HAS_GIO_SETTINGS_BIND_WITH_MAPPING
  // Mappings from Gio::Settings to properties
  static std::optional<unsigned int> map_from_ustring_to_int(const Glib::ustring& transition);
  static std::optional<Glib::ustring> map_from_int_to_ustring(const unsigned int& pos);
#else
  // Signal handlers
  void on_font_setting_changed(const Glib::ustring& key);
  void on_font_selection_changed();
  void on_transition_setting_changed(const Glib::ustring& key);
  void on_transition_selection_changed();
#endif
  Glib::RefPtr<Gtk::Builder> m_refBuilder;
  Glib::RefPtr<Gio::Settings> m_settings;
  Gtk::FontDialogButton* m_font {nullptr};
  Gtk::DropDown* m_transition {nullptr};
};

#endif /* GTKMM_EXAMPLEAPPPREFS_H_ */

File: exampleappwindow.h (For use with gtkmm 4)

#ifndef GTKMM_EXAMPLEAPPWINDOW_H_
#define GTKMM_EXAMPLEAPPWINDOW_H_

#include <gtkmm.h>

class ExampleAppWindow : public Gtk::ApplicationWindow
{
public:
  ExampleAppWindow(BaseObjectType* cobject,
    const Glib::RefPtr<Gtk::Builder>& refBuilder);

  static ExampleAppWindow* create();

  void open_file_view(const Glib::RefPtr<Gio::File>& file);

protected:
  Glib::RefPtr<Gtk::Builder> m_refBuilder;
  Glib::RefPtr<Gio::Settings> m_settings;
  Gtk::Stack* m_stack {nullptr};
  Gtk::MenuButton* m_gears {nullptr};
};

#endif /* GTKMM_EXAMPLEAPPWINDOW_H */

File: exampleapplication.cc (For use with gtkmm 4)

#include "exampleapplication.h"
#include "exampleappwindow.h"
#include "exampleappprefs.h"
#include <iostream>
#include <exception>

ExampleApplication::ExampleApplication()
: Gtk::Application("org.gtkmm.examples.application", Gio::Application::Flags::HANDLES_OPEN)
{
}

Glib::RefPtr<ExampleApplication> ExampleApplication::create()
{
  return Glib::make_refptr_for_instance<ExampleApplication>(new ExampleApplication());
}

ExampleAppWindow* ExampleApplication::create_appwindow()
{
  auto appwindow = ExampleAppWindow::create();

  // Make sure that the application runs for as long this window is still open.
  add_window(*appwindow);

  // A window can be added to an application with Gtk::Application::add_window()
  // or Gtk::Window::set_application(). When all added windows have been hidden
  // or removed, the application stops running (Gtk::Application::run() returns()),
  // unless Gio::Application::hold() has been called.

  // Delete the window when it is hidden.
  appwindow->signal_hide().connect([appwindow](){ delete appwindow; });

  return appwindow;
}

void ExampleApplication::on_startup()
{
  // Call the base class's implementation.
  Gtk::Application::on_startup();

  // Add actions and keyboard accelerators for the menu.
  add_action("preferences", sigc::mem_fun(*this, &ExampleApplication::on_action_preferences));
  add_action("quit", sigc::mem_fun(*this, &ExampleApplication::on_action_quit));
  set_accel_for_action("app.quit", "<Ctrl>Q");
}

void ExampleApplication::on_activate()
{
  try
  {
    // The application has been started, so let's show a window.
    auto appwindow = create_appwindow();
    appwindow->present();
  }
  // If create_appwindow() throws an exception (perhaps from Gtk::Builder),
  // no window has been created, no window has been added to the application,
  // and therefore the application will stop running.
  catch (const Glib::Error& ex)
  {
    std::cerr << "ExampleApplication::on_activate(): " << ex.what() << std::endl;
  }
  catch (const std::exception& ex)
  {
    std::cerr << "ExampleApplication::on_activate(): " << ex.what() << std::endl;
  }
}

void ExampleApplication::on_open(const Gio::Application::type_vec_files& files,
  const Glib::ustring& /* hint */)
{
  // The application has been asked to open some files,
  // so let's open a new view for each one.
  ExampleAppWindow* appwindow = nullptr;
  auto windows = get_windows();
  if (windows.size() > 0)
    appwindow = dynamic_cast<ExampleAppWindow*>(windows[0]);

  try
  {
    if (!appwindow)
      appwindow = create_appwindow();

    for (const auto& file : files)
      appwindow->open_file_view(file);

    appwindow->present();
  }
  catch (const Glib::Error& ex)
  {
    std::cerr << "ExampleApplication::on_open(): " << ex.what() << std::endl;
  }
  catch (const std::exception& ex)
  {
    std::cerr << "ExampleApplication::on_open(): " << ex.what() << std::endl;
  }
}

void ExampleApplication::on_action_preferences()
{
  try
  {
    auto prefs_dialog = ExampleAppPrefs::create(*get_active_window());
    prefs_dialog->present();

    // Delete the dialog when it is hidden.
    prefs_dialog->signal_hide().connect([prefs_dialog](){ delete prefs_dialog; });
  }
  catch (const Glib::Error& ex)
  {
    std::cerr << "ExampleApplication::on_action_preferences(): " << ex.what() << std::endl;
  }
  catch (const std::exception& ex)
  {
    std::cerr << "ExampleApplication::on_action_preferences(): " << ex.what() << std::endl;
  }
}

void ExampleApplication::on_action_quit()
{
  // Gio::Application::quit() will make Gio::Application::run() return,
  // but it's a crude way of ending the program. The window is not removed
  // from the application. Neither the window's nor the application's
  // destructors will be called, because there will be remaining reference
  // counts in both of them. If we want the destructors to be called, we
  // must remove the window from the application. One way of doing this
  // is to hide the window. See comment in create_appwindow().
  auto windows = get_windows();
  for (auto window : windows)
    window->set_visible(false);

  // Not really necessary, when Gtk::Widget::set_visible(false) is called,
  // unless Gio::Application::hold() has been called without a corresponding
  // call to Gio::Application::release().
  quit();
}

File: exampleappprefs.cc (For use with gtkmm 4)

#include "exampleappprefs.h"
#include "exampleappwindow.h"
#include <array>
#include <stdexcept>

namespace
{
struct TransitionTypeStruct
{
  Glib::ustring id;   // Value of "transition" key in Gio::Settings
  Glib::ustring text; // Text in the DropDown list
};

const std::array<TransitionTypeStruct, 3> transitionTypes =
{
  TransitionTypeStruct{"none",             "None"},
  TransitionTypeStruct{"crossfade",        "Fade"},
  TransitionTypeStruct{"slide-left-right", "Slide"}
};

} // anonymous namespace

ExampleAppPrefs::ExampleAppPrefs(BaseObjectType* cobject,
  const Glib::RefPtr<Gtk::Builder>& refBuilder)
: Gtk::Window(cobject),
  m_refBuilder(refBuilder)
{
  m_font = m_refBuilder->get_widget<Gtk::FontDialogButton>("font");
  if (!m_font)
    throw std::runtime_error("No \"font\" object in prefs.ui");

  m_transition = m_refBuilder->get_widget<Gtk::DropDown>("transition");
  if (!m_transition)
    throw std::runtime_error("No \"transition\" object in prefs.ui");

  // DropDown for transition type.
  auto string_list = Gtk::StringList::create();
  for (const auto& transitionType : transitionTypes)
    string_list->append(transitionType.text);

  m_transition->set_model(string_list);

  m_settings = Gio::Settings::create("org.gtkmm.exampleapp");

  // Connect preference properties to the Gio::Settings.
#if HAS_GIO_SETTINGS_BIND_WITH_MAPPING
  m_settings->bind<Glib::ustring, Pango::FontDescription>("font",
    m_font->property_font_desc(), Gio::Settings::BindFlags::DEFAULT,
    [](const auto& font) { return Pango::FontDescription(font); },
    [](const auto& fontdesc) { return fontdesc.to_string(); }
  );
  m_settings->bind<Glib::ustring, unsigned int>("transition",
    m_transition->property_selected(), Gio::Settings::BindFlags::DEFAULT,
    [](const auto& transition) { return map_from_ustring_to_int(transition); },
    [](const auto& pos) { return map_from_int_to_ustring(pos); }
  );
#else
  // This is easier when g_settings_bind_with_mapping() is
  // wrapped in a Gio::Settings method.
  m_settings->signal_changed("font").connect(
    sigc::mem_fun(*this, &ExampleAppPrefs::on_font_setting_changed));
  m_font->property_font_desc().signal_changed().connect(
    sigc::mem_fun(*this, &ExampleAppPrefs::on_font_selection_changed));

  m_settings->signal_changed("transition").connect(
    sigc::mem_fun(*this, &ExampleAppPrefs::on_transition_setting_changed));
  m_transition->property_selected().signal_changed().connect(
    sigc::mem_fun(*this, &ExampleAppPrefs::on_transition_selection_changed));

  // Synchronize the preferences dialog with m_settings.
  on_font_setting_changed("font");
  on_transition_setting_changed("transition");
#endif
}

//static
ExampleAppPrefs* ExampleAppPrefs::create(Gtk::Window& parent)
{
  // Load the Builder file and instantiate its widgets.
  auto refBuilder = Gtk::Builder::create_from_resource("/org/gtkmm/exampleapp/prefs.ui");

  auto dialog = Gtk::Builder::get_widget_derived<ExampleAppPrefs>(refBuilder, "prefs_dialog");
  if (!dialog)
    throw std::runtime_error("No \"prefs_dialog\" object in prefs.ui");

  dialog->set_transient_for(parent);

  return dialog;
}

#if HAS_GIO_SETTINGS_BIND_WITH_MAPPING
std::optional<unsigned int> ExampleAppPrefs::map_from_ustring_to_int(const Glib::ustring& transition)
{
  for (std::size_t i = 0; i < transitionTypes.size(); ++i)
  {
    if (transitionTypes[i].id == transition)
      return i;
  }
  return std::nullopt;
}

std::optional<Glib::ustring> ExampleAppPrefs::map_from_int_to_ustring(const unsigned int& pos)
{
  if (pos >= transitionTypes.size())
    return std::nullopt;
  return transitionTypes[pos].id;
}
#else
void ExampleAppPrefs::on_font_setting_changed(const Glib::ustring& /* key */)
{
  const auto font_setting = m_settings->get_string("font");
  const auto font_button = m_font->get_font_desc().to_string();
  if (font_setting != font_button)
    m_font->set_font_desc(Pango::FontDescription(font_setting));
}

void ExampleAppPrefs::on_font_selection_changed()
{
  const auto font_setting = m_settings->get_string("font");
  const auto font_button = m_font->get_font_desc().to_string();
  if (font_setting != font_button)
    m_settings->set_string("font", font_button);
}

void ExampleAppPrefs::on_transition_setting_changed(const Glib::ustring& /* key */)
{
  const auto transition_setting = m_settings->get_string("transition");
  const auto transition_button = transitionTypes[m_transition->get_selected()].id;
  if (transition_setting != transition_button)
  {
    for (std::size_t i = 0; i < transitionTypes.size(); ++i)
    {
      if (transitionTypes[i].id == transition_setting)
      {
        m_transition->set_selected(i);
        break;
      }
    }
  }
}

void ExampleAppPrefs::on_transition_selection_changed()
{
  const auto pos = m_transition->get_selected();
  if (pos >= transitionTypes.size())
    return;
  const auto transition_setting = m_settings->get_string("transition");
  const auto transition_button = transitionTypes[pos].id;
  if (transition_setting != transition_button)
    m_settings->set_string("transition", transition_button);
}
#endif

File: exampleappwindow.cc (For use with gtkmm 4)

#include "exampleappwindow.h"
#include <iostream>
#include <stdexcept>

ExampleAppWindow::ExampleAppWindow(BaseObjectType* cobject,
  const Glib::RefPtr<Gtk::Builder>& refBuilder)
: Gtk::ApplicationWindow(cobject),
  m_refBuilder(refBuilder)
{
  // Get widgets from the Gtk::Builder file.
  m_stack = m_refBuilder->get_widget<Gtk::Stack>("stack");
  if (!m_stack)
    throw std::runtime_error("No \"stack\" object in window.ui");

  m_gears = m_refBuilder->get_widget<Gtk::MenuButton>("gears");
  if (!m_gears)
    throw std::runtime_error("No \"gears\" object in window.ui");

  // Bind settings.
  m_settings = Gio::Settings::create("org.gtkmm.exampleapp");
  m_settings->bind("transition", m_stack->property_transition_type());

  // Connect the menu to the MenuButton m_gears.
  // (The connection between action and menu item is specified in gears_menu.ui.)
  auto menu_builder = Gtk::Builder::create_from_resource("/org/gtkmm/exampleapp/gears_menu.ui");
  auto menu = menu_builder->get_object<Gio::MenuModel>("menu");
  if (!menu)
    throw std::runtime_error("No \"menu\" object in gears_menu.ui");

  m_gears->set_menu_model(menu);
}

//static
ExampleAppWindow* ExampleAppWindow::create()
{
  // Load the Builder file and instantiate its widgets.
  auto refBuilder = Gtk::Builder::create_from_resource("/org/gtkmm/exampleapp/window.ui");

  auto window = Gtk::Builder::get_widget_derived<ExampleAppWindow>(refBuilder, "app_window");
  if (!window)
    throw std::runtime_error("No \"app_window\" object in window.ui");

  return window;
}

void ExampleAppWindow::open_file_view(const Glib::RefPtr<Gio::File>& file)
{
  const Glib::ustring basename = file->get_basename();

  auto scrolled = Gtk::make_managed<Gtk::ScrolledWindow>();
  scrolled->set_expand(true);
  auto view = Gtk::make_managed<Gtk::TextView>();
  view->set_editable(false);
  view->set_cursor_visible(false);
  scrolled->set_child(*view);
  m_stack->add(*scrolled, basename, basename);

  auto buffer = view->get_buffer();
  try
  {
    char* contents = nullptr;
    gsize length = 0;
    
    file->load_contents(contents, length);
    buffer->set_text(contents, contents+length);
    g_free(contents);
  }
  catch (const Glib::Error& ex)
  {
    std::cout << "ExampleAppWindow::open_file_view(\"" << file->get_parse_name()
      << "\"):\n  " << ex.what() << std::endl;
  }

  auto tag = buffer->create_tag();
  m_settings->bind("font", tag->property_font());
  buffer->apply_tag(tag, buffer->begin(), buffer->end());
}

File: main.cc (For use with gtkmm 4)

#include "exampleapplication.h"

int main(int argc, char* argv[])
{
  // Since this example is running uninstalled, we have to help it find its
  // schema. This is *not* necessary in a properly installed application.
  Glib::setenv ("GSETTINGS_SCHEMA_DIR", ".", false);

  auto application = ExampleApplication::create();

  // Start the application, showing the initial window,
  // and opening extra views for any files that it is asked to open,
  // for instance as a command-line parameter.
  // run() will return when the last window has been closed.
  return application->run(argc, argv);
}

File: exampleapp.gresource.xml (For use with gtkmm 4)

<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/org/gtkmm/exampleapp">
    <file preprocess="xml-stripblanks">window.ui</file>
    <file preprocess="xml-stripblanks">gears_menu.ui</file>
    <file preprocess="xml-stripblanks">prefs.ui</file>
  </gresource>
</gresources>

File: prefs.ui (For use with gtkmm 4)

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <object class="GtkWindow" id="prefs_dialog">
    <property name="title" translatable="yes">Preferences</property>
    <property name="resizable">False</property>
    <property name="modal">True</property>
    <property name="hide_on_close">True</property>
    <child>
      <object class="GtkGrid" id="grid">
        <property name="margin-start">12</property>
        <property name="margin-end">12</property>
        <property name="margin-top">12</property>
        <property name="margin-bottom">12</property>
        <property name="row-spacing">12</property>
        <property name="column-spacing">12</property>
        <child>
          <object class="GtkLabel" id="fontlabel">
            <property name="label">_Font:</property>
            <property name="use-underline">True</property>
            <property name="mnemonic-widget">font</property>
            <property name="xalign">1</property>
            <layout>
              <property name="column">0</property>
              <property name="row">0</property>
            </layout>
          </object>
        </child>
        <child>
          <object class="GtkFontDialogButton" id="font">
            <property name="dialog">
              <object class="GtkFontDialog"/>
            </property>
            <layout>
              <property name="column">1</property>
              <property name="row">0</property>
            </layout>
          </object>
        </child>
        <child>
          <object class="GtkLabel" id="transitionlabel">
            <property name="label">_Transition:</property>
            <property name="use-underline">True</property>
            <property name="mnemonic-widget">transition</property>
            <property name="xalign">1</property>
            <layout>
              <property name="column">0</property>
              <property name="row">1</property>
            </layout>
          </object>
        </child>
        <child>
          <object class="GtkDropDown" id="transition">
            <layout>
              <property name="column">1</property>
              <property name="row">1</property>
            </layout>
          </object>
        </child>
      </object>
    </child>
  </object>
</interface>

File: org.gtkmm.exampleapp.gschema.xml (For use with gtkmm 4)

<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
  <schema path="/org/gtkmm/exampleapp/" id="org.gtkmm.exampleapp">
    <key name="font" type="s">
      <default>'Monospace 12'</default>
      <summary>Font</summary>
      <description>The font to be used for content.</description>
    </key>
    <key name="transition" type="s">
      <choices>
        <choice value='none'/>
        <choice value='crossfade'/>
        <choice value='slide-left-right'/>
      </choices>
      <default>'none'</default>
      <summary>Transition</summary>
      <description>The transition to use when switching tabs.</description>
    </key>
  </schema>
</schemalist>