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++ • 10.35 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
Gtk::Image *copy_image(Gtk::Image *image) {
19
if(image->get_storage_type() == Gtk::Image::Type::PAINTABLE) {
20
return Gtk::make_managed<Gtk::Image>(image->get_paintable());
21
} else if(image->get_storage_type() == Gtk::Image::Type::ICON_NAME) {
22
auto new_image = Gtk::make_managed<Gtk::Image>();
23
new_image->set_from_icon_name(image->get_icon_name());
24
return new_image;
25
} else {
26
return nullptr;
27
}
28
}
29
30
void init() {
31
// Set up gettext configuration
32
setlocale(LC_ALL, "");
33
34
bindtextdomain("gpanthera", "./locales");
35
textdomain("gpanthera");
36
}
37
38
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)
39
: Gtk::Box(Gtk::Orientation::VERTICAL, 0), name(name) {
40
if(icon) {
41
this->icon = icon;
42
}
43
if(stack) {
44
this->stack = stack;
45
}
46
this->layout = std::move(layout);
47
this->label.set_text(label);
48
header = std::make_unique<Gtk::HeaderBar>();
49
header->set_show_title_buttons(false);
50
if(custom_header) {
51
header->set_title_widget(*custom_header);
52
} else {
53
header->set_title_widget(this->label);
54
}
55
header->add_css_class("gpanthera-dock-titlebar");
56
auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>();
57
auto header_menu = Gio::Menu::create();
58
header_menu_button->set_direction(Gtk::ArrowType::NONE);
59
60
// Pane menu
61
this->action_group = Gio::SimpleActionGroup::create();
62
header_menu_button->insert_action_group("win", action_group);
63
64
// Close action
65
auto close_action = Gio::SimpleAction::create("close");
66
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
67
if(this->stack) {
68
this->stack->set_visible_child("empty");
69
}
70
});
71
action_group->add_action(close_action);
72
header_menu->append(_("Close"), "win.close");
73
74
// Pop out action
75
auto pop_out_action = Gio::SimpleAction::create("pop_out");
76
pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) {
77
if(this->stack) {
78
this->pop_out();
79
}
80
});
81
action_group->add_action(pop_out_action);
82
header_menu->append(_("Pop out"), "win.pop_out");
83
84
// Move menu
85
auto move_menu = Gio::Menu::create();
86
for(auto &this_stack : this->layout->stacks) {
87
auto action_name = "move_" + this_stack->name;
88
auto move_action = Gio::SimpleAction::create(action_name);
89
move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) {
90
this_stack->add_pane(*this);
91
});
92
action_group->add_action(move_action);
93
move_menu->append(this_stack->name, "win." + action_name);
94
}
95
96
// Add move submenu
97
header_menu->append_submenu(_("Move"), move_menu);
98
99
// Switch to traditional (nested) submenus, not sliding
100
auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED);
101
popover_menu->set_has_arrow(false);
102
header_menu_button->set_popover(*popover_menu);
103
104
// TODO: Add a context menu as well
105
106
header->pack_end(*header_menu_button);
107
108
this->prepend(*header);
109
this->child = &child;
110
this->append(child);
111
}
112
113
Gtk::Stack *DockablePane::get_stack() const {
114
return stack;
115
}
116
117
void DockablePane::redock(DockStack *stack) {
118
if(this->window != nullptr) {
119
this->window->hide();
120
this->window->unset_titlebar();
121
this->window->set_decorated(false);
122
this->header->get_style_context()->remove_class("titlebar");
123
this->prepend(*this->header);
124
this->window->unset_child();
125
this->window->close();
126
} else if(this->stack == this->get_parent()) {
127
this->stack->remove(*this);
128
}
129
this->stack = stack;
130
this->last_stack = stack;
131
this->stack->add(*this, this->get_identifier());
132
if(this->window != nullptr) {
133
this->window->destroy();
134
delete this->window;
135
this->window = nullptr;
136
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
137
action->set_enabled(true);
138
this->header->get_style_context()->remove_class("gpanthera-dock-titlebar-popout");
139
}
140
}
141
142
void DockablePane::pop_out() {
143
if(this->stack != nullptr) {
144
this->stack->remove(*this);
145
this->stack = nullptr;
146
}
147
148
if(this->window == nullptr) {
149
this->remove(*this->header);
150
this->window = new DockWindow(this);
151
this->window->set_titlebar(*this->header);
152
this->window->set_child(*this);
153
this->window->set_decorated(true);
154
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
155
action->set_enabled(false);
156
this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout");
157
}
158
this->window->present();
159
}
160
161
Glib::ustring DockablePane::get_identifier() const {
162
return name;
163
}
164
165
Gtk::Image *DockablePane::get_icon() const {
166
return icon;
167
}
168
169
Gtk::Widget *DockablePane::get_child() const {
170
return child;
171
}
172
173
Gtk::Label *DockablePane::get_label() {
174
return &label;
175
}
176
177
LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") {
178
}
179
180
void LayoutManager::add_pane(DockablePane *pane) {
181
panes.push_back(pane);
182
}
183
184
void LayoutManager::add_stack(DockStack *stack) {
185
stacks.push_back(stack);
186
}
187
188
DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name) : Gtk::Stack(), layout(layout), name(name) {
189
auto *empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
190
add(*empty_child, "empty");
191
// Add the stack to a layout manager
192
this->layout->add_stack(this);
193
194
// Hide the stack when the empty child is visible
195
this->property_visible_child_name().signal_changed().connect([this]() {
196
if(this->get_visible_child_name() == "empty") {
197
this->hide();
198
} else {
199
this->show();
200
}
201
});
202
203
this->set_visible_child("empty");
204
this->hide();
205
}
206
207
DockWindow::DockWindow(DockablePane *pane) {
208
this->pane = pane;
209
this->set_child(*pane);
210
// Attempting to close the window should redock the pane so it doesn't vanish
211
this->signal_close_request().connect([this]() {
212
this->pane->redock(this->pane->last_stack);
213
return true;
214
}, false);
215
}
216
217
DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
218
auto update_callback = [this](Gtk::Widget*) {
219
this->update_buttons();
220
};
221
stack->signal_child_added.connect(update_callback);
222
stack->signal_child_removed.connect(update_callback);
223
this->get_style_context()->add_class("gpanthera-dock-switcher");
224
}
225
226
DockStackSwitcher::~DockStackSwitcher() {
227
add_handler.disconnect();
228
remove_handler.disconnect();
229
}
230
231
DockButton::DockButton(DockablePane *pane) : Gtk::Button(), pane(pane) {
232
if(pane->get_icon()) {
233
this->set_child(*copy_image(pane->get_icon()));
234
}
235
this->set_tooltip_text(pane->get_label()->get_text());
236
this->set_halign(Gtk::Align::CENTER);
237
this->set_valign(Gtk::Align::CENTER);
238
this->get_style_context()->add_class("toggle");
239
this->get_style_context()->add_class("gpanthera-dock-button");
240
active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
241
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
242
this->add_css_class("checked");
243
this->add_css_class("gpanthera-dock-button-active");
244
} else {
245
this->remove_css_class("checked");
246
this->remove_css_class("gpanthera-dock-button-active");
247
}
248
});
249
}
250
251
DockButton::~DockButton() {
252
active_style_handler.disconnect();
253
}
254
255
void DockStackSwitcher::update_buttons() {
256
auto old_buttons = collect_children(*this);
257
for(auto *button : old_buttons) {
258
remove(*button);
259
}
260
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
261
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
262
auto *button = Gtk::make_managed<DockButton>(pane);
263
button->signal_clicked().connect([this, pane]() {
264
if(stack->get_visible_child_name() == pane->get_identifier()) {
265
stack->set_visible_child("empty");
266
} else {
267
stack->set_visible_child(pane->get_identifier());
268
}
269
});
270
if(pane->get_identifier() != Glib::ustring("empty")) {
271
append(*button);
272
}
273
}
274
}
275
}
276
277
void DockStack::add_pane(DockablePane &child) {
278
child.redock(this);
279
}
280
281
void DockStack::add(Gtk::Widget &child, const Glib::ustring &name) {
282
Gtk::Stack::add(child, name);
283
signal_child_added.emit(&child);
284
}
285
286
void DockStack::remove(Gtk::Widget &child) {
287
Gtk::Stack::remove(child);
288
signal_child_removed.emit(&child);
289
}
290
} // namespace gPanthera
291