GTK docking interfaces and more

By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 gpanthera.cc

View raw Download
text/x-c++ • 21.45 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 configurationIsn't
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
// This should be replaced with a custom class in the future
49
header = std::make_unique<Gtk::HeaderBar>();
50
header->set_show_title_buttons(false);
51
if(custom_header) {
52
header->set_title_widget(*custom_header);
53
} else {
54
header->set_title_widget(this->label);
55
}
56
header->add_css_class("gpanthera-dock-titlebar");
57
auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>();
58
auto header_menu = Gio::Menu::create();
59
header_menu_button->set_direction(Gtk::ArrowType::NONE);
60
61
// Pane menu
62
this->action_group = Gio::SimpleActionGroup::create();
63
header_menu_button->insert_action_group("win", action_group);
64
65
// Close action
66
auto close_action = Gio::SimpleAction::create("close");
67
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
68
if(this->stack) {
69
this->stack->set_visible_child("");
70
}
71
});
72
action_group->add_action(close_action);
73
header_menu->append(_("Close"), "win.close");
74
75
// Pop out action
76
auto pop_out_action = Gio::SimpleAction::create("pop_out");
77
pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) {
78
if(this->stack) {
79
this->pop_out();
80
}
81
});
82
action_group->add_action(pop_out_action);
83
header_menu->append(_("Pop out"), "win.pop_out");
84
85
// Move menu
86
auto move_menu = Gio::Menu::create();
87
for(auto &this_stack : this->layout->stacks) {
88
auto action_name = "move_" + this_stack->name;
89
auto move_action = Gio::SimpleAction::create(action_name);
90
move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) {
91
this_stack->add_pane(*this);
92
});
93
action_group->add_action(move_action);
94
move_menu->append(this_stack->name, "win." + action_name);
95
}
96
97
// Add move submenu
98
header_menu->append_submenu(_("Move"), move_menu);
99
100
// Switch to traditional (nested) submenus, not sliding
101
auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED);
102
popover_menu->set_has_arrow(false);
103
header_menu_button->set_popover(*popover_menu);
104
105
// TODO: Add a context menu as well
106
107
header->pack_end(*header_menu_button);
108
109
this->prepend(*header);
110
this->child = &child;
111
this->append(child);
112
}
113
114
Gtk::Stack *DockablePane::get_stack() const {
115
return stack;
116
}
117
118
void DockablePane::redock(DockStack *stack) {
119
if(this->window != nullptr) {
120
this->window->hide();
121
// Put the titlebar back
122
this->window->unset_titlebar();
123
this->window->set_decorated(false);
124
this->header->get_style_context()->remove_class("titlebar");
125
this->prepend(*this->header);
126
this->window->unset_child();
127
this->window->close();
128
} else if(this->get_parent() && this->stack == this->get_parent()) {
129
this->stack->remove(*this);
130
}
131
this->stack = stack;
132
this->last_stack = stack;
133
this->stack->add(*this, this->get_identifier());
134
if(this->window != nullptr) {
135
this->window->destroy();
136
delete this->window;
137
this->window = nullptr;
138
// Re-enable the pop out option
139
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
140
action->set_enabled(true);
141
this->header->get_style_context()->remove_class("gpanthera-dock-titlebar-popout");
142
}
143
}
144
145
void DockablePane::pop_out() {
146
if(this->stack != nullptr) {
147
this->stack->remove(*this);
148
this->stack = nullptr;
149
}
150
151
if(this->window == nullptr) {
152
// Remove the header bar from the pane, so it can be used as the titlebar of the window
153
this->remove(*this->header);
154
this->window = new DockWindow(this);
155
this->window->set_titlebar(*this->header);
156
this->window->set_child(*this);
157
this->window->set_decorated(true);
158
// Grey out the pop-out option
159
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
160
action->set_enabled(false);
161
this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout");
162
}
163
this->window->present();
164
}
165
166
Glib::ustring DockablePane::get_identifier() const {
167
return name;
168
}
169
170
Gtk::Image *DockablePane::get_icon() const {
171
return icon;
172
}
173
174
Gtk::Widget *DockablePane::get_child() const {
175
return child;
176
}
177
178
Gtk::Label *DockablePane::get_label() {
179
return &label;
180
}
181
182
LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") {
183
}
184
185
void LayoutManager::add_pane(DockablePane *pane) {
186
panes.push_back(pane);
187
}
188
189
void LayoutManager::add_stack(DockStack *stack) {
190
stacks.push_back(stack);
191
}
192
193
void LayoutManager::remove_pane(DockablePane *pane) {
194
panes.erase(std::ranges::remove(panes, pane).begin(), panes.end());
195
}
196
197
void LayoutManager::remove_stack(DockStack *stack) {
198
stacks.erase(std::ranges::remove(stacks, stack).begin(), stacks.end());
199
}
200
201
BaseStack::BaseStack() : Gtk::Stack() {
202
}
203
204
DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name) : BaseStack(), layout(layout), name(name) {
205
auto empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
206
this->add(*empty_child, "");
207
// Add the stack to a layout manager
208
this->layout->add_stack(this);
209
210
// Hide the stack when no child is visible
211
this->property_visible_child_name().signal_changed().connect([this]() {
212
if(this->get_visible_child_name() == "") {
213
this->hide();
214
} else {
215
this->show();
216
}
217
});
218
219
// Also hide when the visible child is removed
220
this->signal_child_removed.connect([this](Gtk::Widget* const &child) {
221
if(this->get_visible_child_name() == "") {
222
this->hide();
223
}
224
});
225
226
this->set_visible_child("");
227
this->hide();
228
}
229
230
DockWindow::DockWindow(DockablePane *pane) {
231
this->pane = pane;
232
this->set_child(*pane);
233
// Attempting to close the window should redock the pane so it doesn't vanish
234
this->signal_close_request().connect([this]() {
235
this->pane->redock(this->pane->last_stack);
236
return true;
237
}, false);
238
}
239
240
DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
241
auto update_callback = [this](Gtk::Widget*) {
242
this->update_buttons();
243
};
244
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
245
stack->signal_child_added.connect(update_callback);
246
stack->signal_child_removed.connect(update_callback);
247
this->get_style_context()->add_class("gpanthera-dock-switcher");
248
drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
249
this->add_controller(drop_target);
250
// Process dropped buttons
251
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
252
const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get();
253
254
if(widget) {
255
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
256
this->stack->add_pane(*pane);
257
}
258
}
259
260
return true; // Drop OK
261
}, false);
262
}
263
264
DockStack *DockStackSwitcher::get_stack() const {
265
return stack;
266
}
267
268
DockStackSwitcher::~DockStackSwitcher() {
269
add_handler.disconnect();
270
remove_handler.disconnect();
271
}
272
273
DockButton::DockButton(DockablePane *pane) : Gtk::ToggleButton(), pane(pane) {
274
if(pane->get_icon()) {
275
this->set_child(*copy_image(pane->get_icon()));
276
}
277
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
278
this->set_tooltip_text(pane->get_label()->get_text());
279
this->set_halign(Gtk::Align::CENTER);
280
this->set_valign(Gtk::Align::CENTER);
281
this->get_style_context()->add_class("toggle");
282
this->get_style_context()->add_class("gpanthera-dock-button");
283
// Add/remove CSS classes when the pane is shown/hidden
284
active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
285
this->update_active_style();
286
});
287
drag_source = Gtk::DragSource::create();
288
drag_source->set_exclusive(false);
289
// This is to prevent the click handler from taking over grabbing the button
290
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
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("");
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<DockablePane*>&>(value).get();
326
327
if(widget) {
328
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
329
if(pane->layout != this->pane->layout) {
330
// If the pane is not in the same layout manager, reject
331
return false;
332
}
333
auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent());
334
if(switcher) {
335
auto *stack = switcher->get_stack();
336
// Move the button to the new position
337
pane->redock(stack);
338
pane->insert_before(*stack, *this->pane);
339
switcher->update_buttons();
340
}
341
}
342
}
343
344
return true; // Drop OK
345
}, false);
346
this->update_active_style();
347
}
348
349
void DockButton::update_active_style() {
350
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
351
this->add_css_class("checked");
352
this->add_css_class("gpanthera-dock-button-active");
353
this->set_active(true);
354
} else {
355
this->remove_css_class("checked");
356
this->remove_css_class("gpanthera-dock-button-active");
357
this->set_active(false);
358
}
359
}
360
361
DockButton::~DockButton() {
362
active_style_handler.disconnect();
363
}
364
365
void DockStackSwitcher::update_buttons() {
366
// Clear the old buttons
367
auto old_buttons = collect_children(*this);
368
for(auto *button : old_buttons) {
369
remove(*button);
370
}
371
DockButton* first_child = nullptr;
372
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
373
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
374
auto *button = Gtk::make_managed<DockButton>(pane);
375
if(!first_child) {
376
first_child = button;
377
} else {
378
button->set_group(*first_child);
379
}
380
if(pane->get_identifier() != Glib::ustring("")) {
381
append(*button);
382
}
383
}
384
}
385
}
386
387
void DockStack::add_pane(DockablePane &child) {
388
child.redock(this);
389
}
390
391
void ContentManager::add_stack(ContentStack *stack) {
392
this->stacks.push_back(stack);
393
}
394
395
void ContentManager::remove_stack(ContentStack *stack) {
396
this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end());
397
}
398
399
void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) {
400
Gtk::Stack::add(child, name);
401
signal_child_added.emit(&child);
402
}
403
404
void BaseStack::add(Gtk::Widget &child) {
405
Gtk::Stack::add(child);
406
signal_child_added.emit(&child);
407
}
408
409
void BaseStack::remove(Gtk::Widget &child) {
410
Gtk::Stack::remove(child);
411
signal_child_removed.emit(&child);
412
}
413
414
ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager)
415
: BaseStack(), content_manager(std::move(content_manager)) {
416
this->content_manager->add_stack(this);
417
}
418
419
ContentTabBar::ContentTabBar(ContentStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
420
this->get_style_context()->add_class("gpanthera-content-tab-bar");
421
this->set_margin_top(0);
422
this->set_margin_bottom(0);
423
this->set_margin_start(0);
424
this->set_margin_end(0);
425
426
auto update_callback = [this](Gtk::Widget*) {
427
this->update_buttons();
428
};
429
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
430
stack->signal_child_added.connect(update_callback);
431
stack->signal_child_removed.connect(update_callback);
432
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
433
this->add_controller(drop_target);
434
// Process dropped buttons
435
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
436
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
437
438
if(widget) {
439
if(auto page = dynamic_cast<ContentPage*>(widget)) {
440
this->stack->add_page(*page);
441
}
442
}
443
444
return true; // Drop OK
445
}, false);
446
}
447
448
ContentTabBar::~ContentTabBar() {
449
add_handler.disconnect();
450
remove_handler.disconnect();
451
}
452
453
ContentStack *ContentTabBar::get_stack() const {
454
return stack;
455
}
456
457
void ContentTabBar::update_buttons() {
458
// Clear the old buttons
459
auto old_buttons = collect_children(*this);
460
for(auto *button : old_buttons) {
461
remove(*button);
462
}
463
ContentTab* first_child = nullptr;
464
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
465
if(auto pane = dynamic_cast<ContentPage*>(widget)) {
466
auto *button = Gtk::make_managed<ContentTab>(pane);
467
if(!first_child) {
468
first_child = button;
469
} else {
470
button->set_group(*first_child);
471
}
472
}
473
}
474
}
475
476
void ContentStack::add_page(ContentPage &child) {
477
child.redock(this);
478
}
479
480
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
481
this->set_child(*page->get_tab_widget());
482
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
483
this->set_halign(Gtk::Align::CENTER);
484
this->set_valign(Gtk::Align::CENTER);
485
this->get_style_context()->add_class("toggle");
486
this->get_style_context()->add_class("gpanthera-content-tab");
487
// Add/remove CSS classes when the pane is shown/hidden
488
active_style_handler = this->page->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
489
this->update_active_style();
490
});
491
drag_source = Gtk::DragSource::create();
492
drag_source->set_exclusive(false);
493
// This is to prevent the click handler from taking over grabbing the button
494
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
495
value.init(ContentPage::get_type());
496
value.set(page);
497
// Add the drag source to the button
498
this->add_controller(drag_source);
499
this->signal_clicked().connect([this, page]() {
500
page->get_stack()->set_visible_child(*page);
501
});
502
// Provide the drag data
503
drag_source->signal_prepare().connect([this](double, double) {
504
drag_source->set_actions(Gdk::DragAction::MOVE);
505
auto const paintable = Gtk::WidgetPaintable::create();
506
paintable->set_widget(*this);
507
drag_source->set_icon(paintable, 0, 0);
508
return Gdk::ContentProvider::create(value);
509
}, false);
510
// Pop out if dragged to an external location
511
// TODO: Implement this, allow defining custom behavior at the content manager level
512
}
513
514
void ContentTab::update_active_style() {
515
if(this->page->get_stack()->get_visible_child() == this->page) {
516
this->add_css_class("checked");
517
this->add_css_class("gpanthera-dock-button-active");
518
this->set_active(true);
519
} else {
520
this->remove_css_class("checked");
521
this->remove_css_class("gpanthera-dock-button-active");
522
this->set_active(false);
523
}
524
}
525
526
ContentTab::~ContentTab() {
527
active_style_handler.disconnect();
528
}
529
530
Gtk::Widget *ContentPage::get_tab_widget() const {
531
return this->tab_widget;
532
}
533
534
Gtk::Stack *ContentPage::get_stack() const {
535
return this->stack;
536
}
537
538
void ContentPage::redock(ContentStack *stack) {
539
this->stack = stack;
540
if(this->last_stack != nullptr) {
541
this->last_stack->remove(*this);
542
}
543
this->last_stack = stack;
544
this->stack->add(*this, this->get_child()->get_name());
545
}
546
547
Gtk::Widget *ContentPage::get_child() const {
548
return this->child;
549
}
550
551
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
552
Gtk::Box(Gtk::Orientation::VERTICAL, 0), content_manager(std::move(content_manager)), stack(stack), child(child), tab_widget(tab_widget) {
553
this->content_manager->add_stack(this->stack);
554
this->append(*child);
555
this->set_tab_widget(tab_widget);
556
this->set_margin_top(0);
557
this->set_margin_bottom(0);
558
this->set_margin_start(0);
559
this->set_margin_end(0);
560
}
561
562
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
563
}
564
565
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
566
this->tab_widget = tab_widget;
567
}
568
569
} // namespace gPanthera
570