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++ • 15.61 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
// Get a vector of the children of a GTK widget, since the container API was removed in GTK 4
12
std::vector<Gtk::Widget*> children;
13
for(auto *child = widget.get_first_child(); child; child = child->get_next_sibling()) {
14
children.push_back(child);
15
}
16
return children;
17
}
18
19
Gtk::Image *copy_image(Gtk::Image *image) {
20
// Generate a new Gtk::Image with the same contents as an existing one
21
if(image->get_storage_type() == Gtk::Image::Type::PAINTABLE) {
22
return Gtk::make_managed<Gtk::Image>(image->get_paintable());
23
} else if(image->get_storage_type() == Gtk::Image::Type::ICON_NAME) {
24
auto new_image = Gtk::make_managed<Gtk::Image>();
25
new_image->set_from_icon_name(image->get_icon_name());
26
return new_image;
27
} else {
28
return nullptr;
29
}
30
}
31
32
void init() {
33
// Set up gettext configuration
34
setlocale(LC_ALL, "");
35
36
bindtextdomain("gpanthera", "./locales");
37
textdomain("gpanthera");
38
}
39
40
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)
41
: Gtk::Box(Gtk::Orientation::VERTICAL, 0), name(name) {
42
if(icon) {
43
this->icon = icon;
44
}
45
if(stack) {
46
this->stack = stack;
47
}
48
this->layout = std::move(layout);
49
this->label.set_text(label);
50
// This should be replaced with a custom class in the future
51
header = std::make_unique<Gtk::HeaderBar>();
52
header->set_show_title_buttons(false);
53
if(custom_header) {
54
header->set_title_widget(*custom_header);
55
} else {
56
header->set_title_widget(this->label);
57
}
58
header->add_css_class("gpanthera-dock-titlebar");
59
auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>();
60
auto header_menu = Gio::Menu::create();
61
header_menu_button->set_direction(Gtk::ArrowType::NONE);
62
63
// Pane menu
64
this->action_group = Gio::SimpleActionGroup::create();
65
header_menu_button->insert_action_group("win", action_group);
66
67
// Close action
68
auto close_action = Gio::SimpleAction::create("close");
69
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
70
if(this->stack) {
71
this->stack->set_visible_child("");
72
}
73
});
74
action_group->add_action(close_action);
75
header_menu->append(_("Close"), "win.close");
76
77
// Pop out action
78
auto pop_out_action = Gio::SimpleAction::create("pop_out");
79
pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) {
80
if(this->stack) {
81
this->pop_out();
82
}
83
});
84
action_group->add_action(pop_out_action);
85
header_menu->append(_("Pop out"), "win.pop_out");
86
87
// Move menu
88
auto move_menu = Gio::Menu::create();
89
for(auto &this_stack : this->layout->stacks) {
90
auto action_name = "move_" + this_stack->name;
91
auto move_action = Gio::SimpleAction::create(action_name);
92
move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) {
93
this_stack->add_pane(*this);
94
});
95
action_group->add_action(move_action);
96
move_menu->append(this_stack->name, "win." + action_name);
97
}
98
99
// Add move submenu
100
header_menu->append_submenu(_("Move"), move_menu);
101
102
// Switch to traditional (nested) submenus, not sliding
103
auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED);
104
popover_menu->set_has_arrow(false);
105
header_menu_button->set_popover(*popover_menu);
106
107
// TODO: Add a context menu as well
108
109
header->pack_end(*header_menu_button);
110
111
this->prepend(*header);
112
this->child = &child;
113
this->append(child);
114
}
115
116
Gtk::Stack *DockablePane::get_stack() const {
117
return stack;
118
}
119
120
void DockablePane::redock(DockStack *stack) {
121
if(this->window != nullptr) {
122
this->window->hide();
123
// Put the titlebar back
124
this->window->unset_titlebar();
125
this->window->set_decorated(false);
126
this->header->get_style_context()->remove_class("titlebar");
127
this->prepend(*this->header);
128
this->window->unset_child();
129
this->window->close();
130
} else if(this->stack == this->get_parent()) {
131
this->stack->remove(*this);
132
}
133
this->stack = stack;
134
this->last_stack = stack;
135
this->stack->add(*this, this->get_identifier());
136
if(this->window != nullptr) {
137
this->window->destroy();
138
delete this->window;
139
this->window = nullptr;
140
// Re-enable the pop out option
141
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
142
action->set_enabled(true);
143
this->header->get_style_context()->remove_class("gpanthera-dock-titlebar-popout");
144
}
145
}
146
147
void DockablePane::pop_out() {
148
if(this->stack != nullptr) {
149
this->stack->remove(*this);
150
this->stack = nullptr;
151
}
152
153
if(this->window == nullptr) {
154
// Remove the header bar from the pane, so it can be used as the titlebar of the window
155
this->remove(*this->header);
156
this->window = new DockWindow(this);
157
this->window->set_titlebar(*this->header);
158
this->window->set_child(*this);
159
this->window->set_decorated(true);
160
// Grey out the pop-out option
161
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
162
action->set_enabled(false);
163
this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout");
164
}
165
this->window->present();
166
}
167
168
Glib::ustring DockablePane::get_identifier() const {
169
return name;
170
}
171
172
Gtk::Image *DockablePane::get_icon() const {
173
return icon;
174
}
175
176
Gtk::Widget *DockablePane::get_child() const {
177
return child;
178
}
179
180
Gtk::Label *DockablePane::get_label() {
181
return &label;
182
}
183
184
LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") {
185
}
186
187
void LayoutManager::add_pane(DockablePane *pane) {
188
panes.push_back(pane);
189
}
190
191
void LayoutManager::add_stack(DockStack *stack) {
192
stacks.push_back(stack);
193
}
194
195
void LayoutManager::remove_pane(DockablePane *pane) {
196
panes.erase(std::ranges::remove(panes, pane).begin(), panes.end());
197
}
198
199
void LayoutManager::remove_stack(DockStack *stack) {
200
stacks.erase(std::ranges::remove(stacks, stack).begin(), stacks.end());
201
}
202
203
BaseStack::BaseStack() : Gtk::Stack() {
204
}
205
206
DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name) : BaseStack(), layout(layout), name(name) {
207
auto empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
208
this->add(*empty_child, "");
209
// Add the stack to a layout manager
210
this->layout->add_stack(this);
211
212
// Hide the stack when no child is visible
213
this->property_visible_child_name().signal_changed().connect([this]() {
214
if(this->get_visible_child_name() == "") {
215
this->hide();
216
} else {
217
this->show();
218
}
219
});
220
221
// Also hide when the visible child is removed
222
this->signal_child_removed.connect([this](Gtk::Widget* const &child) {
223
if(this->get_visible_child_name() == "") {
224
this->hide();
225
}
226
});
227
228
this->set_visible_child("");
229
this->hide();
230
}
231
232
DockWindow::DockWindow(DockablePane *pane) {
233
this->pane = pane;
234
this->set_child(*pane);
235
// Attempting to close the window should redock the pane so it doesn't vanish
236
this->signal_close_request().connect([this]() {
237
this->pane->redock(this->pane->last_stack);
238
return true;
239
}, false);
240
}
241
242
DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
243
auto update_callback = [this](Gtk::Widget*) {
244
this->update_buttons();
245
};
246
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
247
stack->signal_child_added.connect(update_callback);
248
stack->signal_child_removed.connect(update_callback);
249
this->get_style_context()->add_class("gpanthera-dock-switcher");
250
drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
251
this->add_controller(drop_target);
252
// Process dropped buttons
253
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
254
const auto &widget = static_cast<const Glib::Value<Gtk::Widget*>&>(value).get();
255
256
if(widget) {
257
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
258
this->stack->add_pane(*pane);
259
}
260
}
261
262
return true; // Drop OK
263
}, false);
264
}
265
266
DockStack *DockStackSwitcher::get_stack() const {
267
return stack;
268
}
269
270
DockStackSwitcher::~DockStackSwitcher() {
271
add_handler.disconnect();
272
remove_handler.disconnect();
273
}
274
275
DockButton::DockButton(DockablePane *pane) : Gtk::ToggleButton(), pane(pane) {
276
if(pane->get_icon()) {
277
this->set_child(*copy_image(pane->get_icon()));
278
}
279
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
280
this->set_tooltip_text(pane->get_label()->get_text());
281
this->set_halign(Gtk::Align::CENTER);
282
this->set_valign(Gtk::Align::CENTER);
283
this->get_style_context()->add_class("toggle");
284
this->get_style_context()->add_class("gpanthera-dock-button");
285
// Add/remove CSS classes when the pane is shown/hidden
286
active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
287
this->update_active_style();
288
});
289
drag_source = Gtk::DragSource::create();
290
drag_source->set_exclusive(false);
291
// This is to prevent the click handler from taking over grabbing the button
292
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
293
value = Glib::Value<Gtk::Widget*>();
294
value.init(DockablePane::get_type());
295
value.set(pane);
296
// Add the drag source to the button
297
this->add_controller(drag_source);
298
this->signal_clicked().connect([this, pane]() {
299
if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) {
300
pane->get_stack()->set_visible_child("");
301
} else {
302
pane->get_stack()->set_visible_child(pane->get_identifier());
303
}
304
});
305
// Provide the drag data
306
drag_source->signal_prepare().connect([this](double, double) {
307
drag_source->set_actions(Gdk::DragAction::MOVE);
308
auto const paintable = Gtk::WidgetPaintable::create();
309
paintable->set_widget(*this);
310
drag_source->set_icon(paintable, 0, 0);
311
return Gdk::ContentProvider::create(value);
312
}, false);
313
// Pop out if dragged to an external location
314
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
315
if(reason == Gdk::DragCancelReason::NO_TARGET) {
316
this->pane->pop_out();
317
return true;
318
}
319
return false;
320
}, false);
321
// Add a drop target to the button
322
auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
323
drop_target->set_actions(Gdk::DragAction::MOVE);
324
drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
325
this->add_controller(drop_target);
326
// Process dropped buttons by inserting them after the current button
327
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
328
const auto &widget = static_cast<const Glib::Value<Gtk::Widget*>&>(value).get();
329
330
if(widget) {
331
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
332
auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent());
333
if(switcher) {
334
auto *stack = switcher->get_stack();
335
// Move the button to the new position
336
pane->redock(stack);
337
pane->insert_before(*stack, *this->pane);
338
switcher->update_buttons();
339
}
340
}
341
}
342
343
return true; // Drop OK
344
}, false);
345
this->update_active_style();
346
}
347
348
void DockButton::update_active_style() {
349
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
350
this->add_css_class("checked");
351
this->add_css_class("gpanthera-dock-button-active");
352
this->set_active(true);
353
} else {
354
this->remove_css_class("checked");
355
this->remove_css_class("gpanthera-dock-button-active");
356
this->set_active(false);
357
}
358
}
359
360
DockButton::~DockButton() {
361
active_style_handler.disconnect();
362
}
363
364
void DockStackSwitcher::update_buttons() {
365
// Clear the old buttons
366
auto old_buttons = collect_children(*this);
367
for(auto *button : old_buttons) {
368
remove(*button);
369
}
370
DockButton* first_child = nullptr;
371
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
372
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
373
auto *button = Gtk::make_managed<DockButton>(pane);
374
if(!first_child) {
375
first_child = button;
376
} else {
377
button->set_group(*first_child);
378
}
379
if(pane->get_identifier() != Glib::ustring("")) {
380
append(*button);
381
}
382
}
383
}
384
}
385
386
void DockStack::add_pane(DockablePane &child) {
387
child.redock(this);
388
}
389
390
void ContentManager::add_stack(ContentStack *stack) {
391
this->stacks.push_back(stack);
392
}
393
394
void ContentManager::remove_stack(ContentStack *stack) {
395
this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end());
396
}
397
398
void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) {
399
Gtk::Stack::add(child, name);
400
signal_child_added.emit(&child);
401
}
402
403
void BaseStack::add(Gtk::Widget &child) {
404
Gtk::Stack::add(child);
405
signal_child_added.emit(&child);
406
}
407
408
void BaseStack::remove(Gtk::Widget &child) {
409
Gtk::Stack::remove(child);
410
signal_child_removed.emit(&child);
411
}
412
413
ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager)
414
: BaseStack(), content_manager(std::move(content_manager)) {
415
this->content_manager->add_stack(this);
416
}
417
} // namespace gPanthera
418