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:
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 std::optional<Pango::FontDescription> map_from_ustring_to_fontdesc(const Glib::ustring& font); std::optional<Glib::ustring> map_from_fontdesc_to_ustring(const Pango::FontDescription& fontdesc); std::optional<unsigned int> map_from_ustring_to_int(const Glib::ustring& transition); 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(sigc::bind(sigc::mem_fun(*this, &ExampleApplication::on_hide_window), 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_hide_window(Gtk::Window* window) { delete window; } 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(sigc::bind(sigc::mem_fun(*this, &ExampleApplication::on_hide_window), 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, sigc::mem_fun(*this, &ExampleAppPrefs::map_from_ustring_to_fontdesc), sigc::mem_fun(*this, &ExampleAppPrefs::map_from_fontdesc_to_ustring)); m_settings->bind<Glib::ustring, unsigned int>("transition", m_transition->property_selected(), Gio::Settings::BindFlags::DEFAULT, sigc::mem_fun(*this, &ExampleAppPrefs::map_from_ustring_to_int), sigc::mem_fun(*this, &ExampleAppPrefs::map_from_int_to_ustring)); #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<Pango::FontDescription> ExampleAppPrefs::map_from_ustring_to_fontdesc(const Glib::ustring& font) { return Pango::FontDescription(font); } std::optional<Glib::ustring> ExampleAppPrefs::map_from_fontdesc_to_ustring(const Pango::FontDescription& fontdesc) { return fontdesc.to_string(); } 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>