As another piece of functionality, we are adding a sidebar, which demonstrates
Gtk::Revealer
and Gtk::ListBox
.
The new widgets are added in the window.ui
file.
The code to populate the sidebar with buttons for the words found in each
file is a little too involved to go into here. But we'll look at the code
to add a checkbutton for the new feature to the menu. A menu item is added to
the ui file gears_menu.ui
.
To connect the menu item to the new show-words
setting, we use a
Gio::Action
corresponding to the given Gio::Settings
key. In ExampleAppWindow
's constructor:
// Connect the menu to the MenuButton m_gears, and bind the show-words setting
// to the win.show-words action and the "Words" menu item.
// (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");
m_gears->set_menu_model(menu);
add_action(m_settings->create_action("show-words"));
What our application looks like now:
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)
#include "../step5/exampleappprefs.h"
// Equal to the corresponding file in step5
File: exampleappwindow.h
(For use with gtkmm 4)
#ifndef GTKMM_EXAMPLEAPPWINDOW_H_
#define GTKMM_EXAMPLEAPPWINDOW_H_
#include <gtkmm.h>
#define HAS_SEARCH_ENTRY2 GTKMM_CHECK_VERSION(4,13,2)
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:
// Signal handlers
void on_search_text_changed();
void on_visible_child_changed();
void on_find_word(const Gtk::Button* button);
void on_reveal_child_changed();
void update_words();
Glib::RefPtr<Gtk::Builder> m_refBuilder;
Glib::RefPtr<Gio::Settings> m_settings;
Gtk::Stack* m_stack {nullptr};
Gtk::ToggleButton* m_search {nullptr};
Gtk::SearchBar* m_searchbar {nullptr};
#if HAS_SEARCH_ENTRY2
Gtk::SearchEntry2* m_searchentry {nullptr};
#else
Gtk::SearchEntry* m_searchentry {nullptr};
#endif
Gtk::MenuButton* m_gears {nullptr};
Gtk::Revealer* m_sidebar {nullptr};
Gtk::ListBox* m_words {nullptr};
Glib::RefPtr<Glib::Binding> m_prop_binding;
};
#endif /* GTKMM_EXAMPLEAPPWINDOW_H */
File: exampleapplication.cc
(For use with gtkmm 4)
#include "../step5/exampleapplication.cc"
// Equal to the corresponding file in step5
File: exampleappprefs.cc
(For use with gtkmm 4)
#include "../step5/exampleappprefs.cc"
// Equal to the corresponding file in step5
File: exampleappwindow.cc
(For use with gtkmm 4)
#include "exampleappwindow.h"
#include <iostream>
#include <stdexcept>
#include <set>
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_search = m_refBuilder->get_widget<Gtk::ToggleButton>("search");
if (!m_search)
throw std::runtime_error("No \"search\" object in window.ui");
m_searchbar = m_refBuilder->get_widget<Gtk::SearchBar>("searchbar");
if (!m_searchbar)
throw std::runtime_error("No \"searchbar\" object in window.ui");
#if HAS_SEARCH_ENTRY2
m_searchentry = m_refBuilder->get_widget<Gtk::SearchEntry2>("searchentry");
#else
m_searchentry = m_refBuilder->get_widget<Gtk::SearchEntry>("searchentry");
#endif
if (!m_searchentry)
throw std::runtime_error("No \"searchentry\" 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");
m_sidebar = m_refBuilder->get_widget<Gtk::Revealer>("sidebar");
if (!m_sidebar)
throw std::runtime_error("No \"sidebar\" object in window.ui");
m_words = m_refBuilder->get_widget<Gtk::ListBox>("words");
if (!m_words)
throw std::runtime_error("No \"words\" object in window.ui");
// Bind settings.
m_settings = Gio::Settings::create("org.gtkmm.exampleapp");
m_settings->bind("transition", m_stack->property_transition_type());
m_settings->bind("show-words", m_sidebar->property_reveal_child());
// Bind properties.
m_prop_binding = Glib::Binding::bind_property(m_search->property_active(),
m_searchbar->property_search_mode_enabled(), Glib::Binding::Flags::BIDIRECTIONAL);
// Connect signal handlers.
m_searchentry->signal_search_changed().connect(
sigc::mem_fun(*this, &ExampleAppWindow::on_search_text_changed));
m_stack->property_visible_child().signal_changed().connect(
sigc::mem_fun(*this, &ExampleAppWindow::on_visible_child_changed));
m_sidebar->property_reveal_child().signal_changed().connect(
sigc::mem_fun(*this, &ExampleAppWindow::on_reveal_child_changed));
// Connect the menu to the MenuButton m_gears, and bind the show-words setting
// to the win.show-words action and the "Words" menu item.
// (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);
add_action(m_settings->create_action("show-words"));
}
//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;
return;
}
auto tag = buffer->create_tag();
m_settings->bind("font", tag->property_font());
buffer->apply_tag(tag, buffer->begin(), buffer->end());
m_search->set_sensitive(true);
update_words();
}
void ExampleAppWindow::on_search_text_changed()
{
const auto text = m_searchentry->get_text();
if (text.empty())
return;
auto tab = dynamic_cast<Gtk::ScrolledWindow*>(m_stack->get_visible_child());
if (!tab)
{
std::cout << "ExampleAppWindow::on_search_text_changed(): No visible child." << std::endl;
return;
}
auto view = dynamic_cast<Gtk::TextView*>(tab->get_child());
if (!view)
{
std::cout << "ExampleAppWindow::on_search_text_changed(): No visible text view." << std::endl;
return;
}
// Very simple-minded search implementation.
auto buffer = view->get_buffer();
Gtk::TextIter match_start;
Gtk::TextIter match_end;
if (buffer->begin().forward_search(text, Gtk::TextSearchFlags::CASE_INSENSITIVE,
match_start, match_end))
{
buffer->select_range(match_start, match_end);
view->scroll_to(match_start);
}
}
void ExampleAppWindow::on_visible_child_changed()
{
m_searchbar->set_search_mode(false);
update_words();
}
void ExampleAppWindow::on_find_word(const Gtk::Button* button)
{
m_searchentry->set_text(button->get_label());
}
void ExampleAppWindow::on_reveal_child_changed()
{
update_words();
}
void ExampleAppWindow::update_words()
{
auto tab = dynamic_cast<Gtk::ScrolledWindow*>(m_stack->get_visible_child());
if (!tab)
return;
auto view = dynamic_cast<Gtk::TextView*>(tab->get_child());
if (!view)
{
std::cout << "ExampleAppWindow::update_words(): No visible text view." << std::endl;
return;
}
auto buffer = view->get_buffer();
// Find all words in the text buffer.
std::set<Glib::ustring> words;
auto start = buffer->begin();
Gtk::TextIter end;
while (start)
{
while (start && !start.starts_word())
++start;
if (!start)
break;
end = start;
end.forward_word_end();
if (start == end)
break;
auto word = buffer->get_text(start, end, false);
words.insert(word.lowercase());
start = end;
}
// Remove old children from the ListBox.
while (auto child = m_words->get_first_child())
m_words->remove(*child);
// Add new child buttons, one per unique word.
for (const auto& word : words)
{
auto row = Gtk::make_managed<Gtk::Button>(word);
row->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this,
&ExampleAppWindow::on_find_word), row));
m_words->append(*row);
}
}
File: main.cc
(For use with gtkmm 4)
#include "../step6/main.cc"
// Equal to the corresponding file in step6
File: gears_menu.ui
(For use with gtkmm 4)
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="menu">
<section>
<item>
<attribute name="label" translatable="yes">_Words</attribute>
<attribute name="action">win.show-words</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Preferences</attribute>
<attribute name="action">app.preferences</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">_Quit</attribute>
<attribute name="action">app.quit</attribute>
</item>
</section>
</menu>
</interface>
File: window.ui
(For use with gtkmm 4)
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkApplicationWindow" id="app_window">
<property name="title" translatable="yes">Example Application</property>
<property name="default-width">600</property>
<property name="default-height">400</property>
<property name="hide-on-close">True</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="header">
<child type="title">
<object class="GtkStackSwitcher" id="tabs">
<property name="stack">stack</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="search">
<property name="sensitive">False</property>
<property name="icon-name">edit-find-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton" id="gears">
<property name="direction">none</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="content_box">
<property name="orientation">vertical</property>
<child>
<object class="GtkSearchBar" id="searchbar">
<child>
<object class="GtkSearchEntry" id="searchentry">
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="hbox">
<child>
<object class="GtkRevealer" id="sidebar">
<property name="transition-type">slide-right</property>
<child>
<object class="GtkScrolledWindow" id="sidebar-sw">
<property name="hscrollbar-policy">never</property>
<child>
<object class="GtkListBox" id="words">
<property name="selection-mode">none</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="transition-duration">500</property>
</object>
</child>
</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>
<key name="show-words" type="b">
<default>false</default>
<summary>Show words</summary>
<description>Whether to show a word list in the sidebar.</description>
</key>
</schema>
</schemalist>