GTK docking interfaces and more

Important information: Google announced that, from September 2026, Android devices will require ALL apps to be signed by Google, effectively leading to an iOS situation. Value your right to a computer that does what you want; do not tolerate this monopolistic practice! Contact me if you don't understand why it is bad. Click to learn more.

 gpanthera.cc

View raw Download
text/x-c++ • 7.78 kiB
C++ source, ASCII text
        
            
1
#include "gpanthera.hh"
2
#include <iostream>
3
#include <utility>
4
#include <libintl.h>
5
#include <locale.h>
6
#include <filesystem>
7
#define _(STRING) gettext(STRING)
8
9
namespace gPanthera {
10
std::vector<Gtk::Widget*> collect_children(Gtk::Widget &widget) {
11
std::vector<Gtk::Widget*> children;
12
for(auto *child = widget.get_first_child(); child; child = child->get_next_sibling()) {
13
children.push_back(child);
14
}
15
return children;
16
}
17
18
void init() {
19
// Set up gettext configuration
20
setlocale(LC_ALL, "");
21
22
bindtextdomain("gpanthera", "./locales");
23
textdomain("gpanthera");
24
}
25
26
DockablePane::DockablePane(std::shared_ptr<LayoutManager> layout, Gtk::Widget &child, const Glib::ustring &name, const Glib::ustring &label, Gtk::Image *icon, DockStack *stack, Gtk::Widget *custom_header)
27
: Gtk::Box(Gtk::Orientation::VERTICAL, 0), name(name) {
28
if(icon) {
29
this->icon = icon;
30
}
31
if(stack) {
32
this->stack = stack;
33
}
34
this->layout = std::move(layout);
35
this->label.set_text(label);
36
header = std::make_unique<Gtk::HeaderBar>();
37
header->set_show_title_buttons(false);
38
if(custom_header) {
39
header->set_title_widget(*custom_header);
40
} else {
41
header->set_title_widget(this->label);
42
}
43
auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>();
44
auto header_menu = Gio::Menu::create();
45
header_menu_button->set_direction(Gtk::ArrowType::NONE);
46
47
// Pane menu
48
auto action_group = Gio::SimpleActionGroup::create();
49
header_menu_button->insert_action_group("win", action_group);
50
51
// Close action
52
auto close_action = Gio::SimpleAction::create("close");
53
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
54
if(this->stack) {
55
this->stack->set_visible_child("empty");
56
}
57
});
58
action_group->add_action(close_action);
59
header_menu->append(_("Close"), "win.close");
60
61
// Pop out action
62
auto pop_out_action = Gio::SimpleAction::create("pop_out");
63
pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) {
64
if(this->stack) {
65
this->pop_out();
66
}
67
});
68
action_group->add_action(pop_out_action);
69
header_menu->append(_("Pop out"), "win.pop_out");
70
71
// Switch to traditional (nested) submenus, not sliding
72
auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED);
73
header_menu_button->set_popover(*popover_menu);
74
75
// Move menu
76
auto move_menu = Gio::Menu::create();
77
for(auto &this_stack : this->layout->stacks) {
78
auto action_name = "move_" + this_stack->name;
79
auto move_action = Gio::SimpleAction::create(action_name);
80
move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) {
81
this_stack->add_pane(*this);
82
});
83
action_group->add_action(move_action);
84
move_menu->append(this_stack->name, "win." + action_name);
85
}
86
87
// Add move submenu
88
header_menu->append_submenu(_("Move"), move_menu);
89
90
header->pack_end(*header_menu_button);
91
92
this->prepend(*header);
93
this->child = &child;
94
this->append(child);
95
}
96
97
void DockablePane::redock(DockStack *stack) {
98
if(this->window != nullptr) {
99
this->window->hide();
100
this->window->unset_titlebar();
101
this->window->set_decorated(false);
102
this->header->get_style_context()->remove_class("titlebar");
103
this->prepend(*this->header);
104
this->window->unset_child();
105
this->window->close();
106
} else if(this->stack == this->get_parent()) {
107
this->stack->remove(*this);
108
}
109
this->stack = stack;
110
this->stack->add(*this, this->get_identifier());
111
if(this->window != nullptr) {
112
this->window->destroy();
113
delete this->window;
114
this->window = nullptr;
115
}
116
}
117
118
void DockablePane::pop_out() {
119
if(this->stack != nullptr) {
120
this->stack->remove(*this);
121
this->stack = nullptr;
122
}
123
124
if(this->window == nullptr) {
125
this->remove(*this->header);
126
this->window = new DockWindow(this);
127
this->window->set_titlebar(*this->header);
128
this->window->set_child(*this);
129
this->window->set_decorated(true);
130
this->window->present();
131
}
132
}
133
134
Glib::ustring DockablePane::get_identifier() const {
135
return name;
136
}
137
138
Gtk::Image *DockablePane::get_icon() const {
139
return icon;
140
}
141
142
Gtk::Widget *DockablePane::get_child() const {
143
return child;
144
}
145
146
Gtk::Label *DockablePane::get_label() {
147
return &label;
148
}
149
150
LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") {
151
}
152
153
void LayoutManager::add_pane(DockablePane *pane) {
154
panes.push_back(pane);
155
}
156
157
void LayoutManager::add_stack(DockStack *stack) {
158
stacks.push_back(stack);
159
}
160
161
DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name) : Gtk::Stack(), layout(layout), name(name) {
162
auto *empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
163
add(*empty_child, "empty");
164
// Add the stack to a layout manager
165
this->layout->add_stack(this);
166
}
167
168
DockWindow::DockWindow(DockablePane *pane) {
169
this->pane = pane;
170
this->set_child(*pane);
171
}
172
173
DockStackSwitcher::DockStackSwitcher(DockStack *stack) : Gtk::Box(Gtk::Orientation::HORIZONTAL), stack(stack) {
174
auto update_callback = [this](Gtk::Widget*) {
175
this->update_buttons();
176
};
177
stack->signal_child_added.connect(update_callback);
178
stack->signal_child_removed.connect(update_callback);
179
}
180
181
DockStackSwitcher::~DockStackSwitcher() {
182
add_handler.disconnect();
183
remove_handler.disconnect();
184
}
185
186
DockButton::DockButton(DockablePane *pane) : Gtk::Button(), pane(pane) {
187
if(pane->get_icon()) {
188
auto new_image = Gtk::make_managed<Gtk::Image>(this->pane->get_icon()->get_paintable());
189
this->set_child(*new_image);
190
}
191
this->set_tooltip_text(pane->get_label()->get_text());
192
this->set_halign(Gtk::Align::CENTER);
193
}
194
195
void DockStackSwitcher::update_buttons() {
196
auto old_buttons = collect_children(*this);
197
for(auto *button : old_buttons) {
198
remove(*button);
199
}
200
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
201
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
202
auto *button = Gtk::make_managed<DockButton>(pane);
203
button->signal_clicked().connect([this, pane]() {
204
if(stack->get_visible_child_name() == pane->get_identifier()) {
205
stack->set_visible_child("empty");
206
} else {
207
stack->set_visible_child(pane->get_identifier());
208
}
209
});
210
if(pane->get_identifier() != Glib::ustring("empty")) {
211
append(*button);
212
}
213
}
214
}
215
}
216
217
void DockStack::add_pane(DockablePane &child) {
218
child.redock(this);
219
}
220
221
void DockStack::add(Gtk::Widget &child, const Glib::ustring &name) {
222
Gtk::Stack::add(child, name);
223
signal_child_added.emit(&child);
224
}
225
226
void DockStack::remove(Gtk::Widget &child) {
227
Gtk::Stack::remove(child);
228
signal_child_removed.emit(&child);
229
}
230
} // namespace gPanthera
231