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++ • 14.76 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("empty");
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
DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name) : Gtk::Stack(), layout(layout), name(name) {
204
auto *empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
205
add(*empty_child, "empty");
206
// Add the stack to a layout manager
207
this->layout->add_stack(this);
208
209
// Hide the stack when the empty child is visible
210
this->property_visible_child_name().signal_changed().connect([this]() {
211
if(this->get_visible_child_name() == "empty") {
212
this->hide();
213
} else {
214
this->show();
215
}
216
});
217
218
// Also hide when the visible child is removed
219
this->signal_child_removed.connect([this](Gtk::Widget* const &child) {
220
if(this->get_visible_child_name() == "" || this->get_visible_child_name() == "empty") {
221
this->hide();
222
}
223
});
224
225
this->set_visible_child("empty");
226
this->hide();
227
}
228
229
DockWindow::DockWindow(DockablePane *pane) {
230
this->pane = pane;
231
this->set_child(*pane);
232
// Attempting to close the window should redock the pane so it doesn't vanish
233
this->signal_close_request().connect([this]() {
234
this->pane->redock(this->pane->last_stack);
235
return true;
236
}, false);
237
}
238
239
DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
240
auto update_callback = [this](Gtk::Widget*) {
241
this->update_buttons();
242
};
243
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
244
stack->signal_child_added.connect(update_callback);
245
stack->signal_child_removed.connect(update_callback);
246
this->get_style_context()->add_class("gpanthera-dock-switcher");
247
drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
248
this->add_controller(drop_target);
249
// Process dropped buttons
250
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
251
const auto &widget = static_cast<const Glib::Value<Gtk::Widget*>&>(value).get();
252
253
if(widget) {
254
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
255
this->stack->add_pane(*pane);
256
}
257
}
258
259
return true; // Drop OK
260
}, false);
261
}
262
263
DockStack *DockStackSwitcher::get_stack() const {
264
return stack;
265
}
266
267
DockStackSwitcher::~DockStackSwitcher() {
268
add_handler.disconnect();
269
remove_handler.disconnect();
270
}
271
272
DockButton::DockButton(DockablePane *pane) : Gtk::Button(), pane(pane) {
273
if(pane->get_icon()) {
274
this->set_child(*copy_image(pane->get_icon()));
275
}
276
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
277
this->set_tooltip_text(pane->get_label()->get_text());
278
this->set_halign(Gtk::Align::CENTER);
279
this->set_valign(Gtk::Align::CENTER);
280
this->get_style_context()->add_class("toggle");
281
this->get_style_context()->add_class("gpanthera-dock-button");
282
// Add/remove CSS classes when the pane is shown/hidden
283
active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
284
this->update_active_style();
285
});
286
drag_source = Gtk::DragSource::create();
287
drag_source->set_exclusive(false);
288
// This is to prevent the click handler from taking over grabbing the button
289
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
290
value = Glib::Value<Gtk::Widget*>();
291
value.init(DockablePane::get_type());
292
value.set(pane);
293
// Add the drag source to the button
294
this->add_controller(drag_source);
295
this->signal_clicked().connect([this, pane]() {
296
if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) {
297
pane->get_stack()->set_visible_child("empty");
298
} else {
299
pane->get_stack()->set_visible_child(pane->get_identifier());
300
}
301
});
302
// Provide the drag data
303
drag_source->signal_prepare().connect([this](double, double) {
304
drag_source->set_actions(Gdk::DragAction::MOVE);
305
auto const paintable = Gtk::WidgetPaintable::create();
306
paintable->set_widget(*this);
307
drag_source->set_icon(paintable, 0, 0);
308
return Gdk::ContentProvider::create(value);
309
}, false);
310
// Pop out if dragged to an external location
311
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
312
if(reason == Gdk::DragCancelReason::NO_TARGET) {
313
this->pane->pop_out();
314
return true;
315
}
316
return false;
317
}, false);
318
// Add a drop target to the button
319
auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
320
drop_target->set_actions(Gdk::DragAction::MOVE);
321
drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
322
this->add_controller(drop_target);
323
// Process dropped buttons by inserting them after the current button
324
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
325
const auto &widget = static_cast<const Glib::Value<Gtk::Widget*>&>(value).get();
326
327
if(widget) {
328
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
329
auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent());
330
if(switcher) {
331
auto *stack = switcher->get_stack();
332
// Move the button to the new position
333
pane->redock(stack);
334
pane->insert_before(*stack, *this->pane);
335
switcher->update_buttons();
336
}
337
}
338
}
339
340
return true; // Drop OK
341
}, false);
342
this->update_active_style();
343
}
344
345
void DockButton::update_active_style() {
346
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
347
this->add_css_class("checked");
348
this->add_css_class("gpanthera-dock-button-active");
349
} else {
350
this->remove_css_class("checked");
351
this->remove_css_class("gpanthera-dock-button-active");
352
}
353
}
354
355
DockButton::~DockButton() {
356
active_style_handler.disconnect();
357
}
358
359
void DockStackSwitcher::update_buttons() {
360
// Clear the old buttons
361
auto old_buttons = collect_children(*this);
362
for(auto *button : old_buttons) {
363
remove(*button);
364
}
365
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
366
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
367
auto *button = Gtk::make_managed<DockButton>(pane);
368
if(pane->get_identifier() != Glib::ustring("empty")) {
369
append(*button);
370
}
371
}
372
}
373
}
374
375
void DockStack::add_pane(DockablePane &child) {
376
child.redock(this);
377
}
378
379
void DockStack::add(Gtk::Widget &child, const Glib::ustring &name) {
380
Gtk::Stack::add(child, name);
381
signal_child_added.emit(&child);
382
}
383
384
void DockStack::remove(Gtk::Widget &child) {
385
Gtk::Stack::remove(child);
386
signal_child_removed.emit(&child);
387
}
388
} // namespace gPanthera
389