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++ • 22.19 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
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
339
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
340
pane->insert_before(*stack, *this->pane);
341
} else {
342
pane->insert_after(*stack, *this->pane);
343
}
344
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
345
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
346
pane->insert_before(*stack, *this->pane);
347
} else {
348
pane->insert_after(*stack, *this->pane);
349
}
350
}
351
352
switcher->update_buttons();
353
}
354
}
355
}
356
357
return true; // Drop OK
358
}, false);
359
this->update_active_style();
360
}
361
362
void DockButton::update_active_style() {
363
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
364
this->add_css_class("checked");
365
this->add_css_class("gpanthera-dock-button-active");
366
this->set_active(true);
367
} else {
368
this->remove_css_class("checked");
369
this->remove_css_class("gpanthera-dock-button-active");
370
this->set_active(false);
371
}
372
}
373
374
DockButton::~DockButton() {
375
active_style_handler.disconnect();
376
}
377
378
void DockStackSwitcher::update_buttons() {
379
// Clear the old buttons
380
auto old_buttons = collect_children(*this);
381
for(auto *button : old_buttons) {
382
remove(*button);
383
}
384
DockButton* first_child = nullptr;
385
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
386
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
387
auto *button = Gtk::make_managed<DockButton>(pane);
388
if(!first_child) {
389
first_child = button;
390
} else {
391
button->set_group(*first_child);
392
}
393
if(pane->get_identifier() != Glib::ustring("")) {
394
append(*button);
395
}
396
}
397
}
398
}
399
400
void DockStack::add_pane(DockablePane &child) {
401
child.redock(this);
402
}
403
404
void ContentManager::add_stack(ContentStack *stack) {
405
this->stacks.push_back(stack);
406
}
407
408
void ContentManager::remove_stack(ContentStack *stack) {
409
this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end());
410
}
411
412
void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) {
413
Gtk::Stack::add(child, name);
414
signal_child_added.emit(&child);
415
}
416
417
void BaseStack::add(Gtk::Widget &child) {
418
Gtk::Stack::add(child);
419
signal_child_added.emit(&child);
420
}
421
422
void BaseStack::remove(Gtk::Widget &child) {
423
Gtk::Stack::remove(child);
424
signal_child_removed.emit(&child);
425
}
426
427
ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager)
428
: BaseStack(), content_manager(std::move(content_manager)) {
429
this->content_manager->add_stack(this);
430
}
431
432
ContentTabBar::ContentTabBar(ContentStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
433
this->get_style_context()->add_class("gpanthera-content-tab-bar");
434
this->set_margin_top(0);
435
this->set_margin_bottom(0);
436
this->set_margin_start(0);
437
this->set_margin_end(0);
438
439
auto update_callback = [this](Gtk::Widget*) {
440
this->update_buttons();
441
};
442
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
443
stack->signal_child_added.connect(update_callback);
444
stack->signal_child_removed.connect(update_callback);
445
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
446
this->add_controller(drop_target);
447
// Process dropped buttons
448
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
449
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
450
451
if(widget) {
452
if(auto page = dynamic_cast<ContentPage*>(widget)) {
453
this->stack->add_page(*page);
454
}
455
}
456
457
return true; // Drop OK
458
}, false);
459
}
460
461
ContentTabBar::~ContentTabBar() {
462
add_handler.disconnect();
463
remove_handler.disconnect();
464
}
465
466
ContentStack *ContentTabBar::get_stack() const {
467
return stack;
468
}
469
470
void ContentTabBar::update_buttons() {
471
// Clear the old buttons
472
auto old_buttons = collect_children(*this);
473
for(auto *button : old_buttons) {
474
remove(*button);
475
}
476
ContentTab* first_child = nullptr;
477
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
478
if(auto pane = dynamic_cast<ContentPage*>(widget)) {
479
auto *button = Gtk::make_managed<ContentTab>(pane);
480
if(!first_child) {
481
first_child = button;
482
} else {
483
button->set_group(*first_child);
484
}
485
}
486
}
487
}
488
489
void ContentStack::add_page(ContentPage &child) {
490
child.redock(this);
491
}
492
493
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
494
this->set_child(*page->get_tab_widget());
495
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
496
this->set_halign(Gtk::Align::CENTER);
497
this->set_valign(Gtk::Align::CENTER);
498
this->get_style_context()->add_class("toggle");
499
this->get_style_context()->add_class("gpanthera-content-tab");
500
// Add/remove CSS classes when the pane is shown/hidden
501
active_style_handler = this->page->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
502
this->update_active_style();
503
});
504
drag_source = Gtk::DragSource::create();
505
drag_source->set_exclusive(false);
506
// This is to prevent the click handler from taking over grabbing the button
507
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
508
value.init(ContentPage::get_type());
509
value.set(page);
510
// Add the drag source to the button
511
this->add_controller(drag_source);
512
this->signal_clicked().connect([this, page]() {
513
page->get_stack()->set_visible_child(*page);
514
});
515
// Provide the drag data
516
drag_source->signal_prepare().connect([this](double, double) {
517
drag_source->set_actions(Gdk::DragAction::MOVE);
518
auto const paintable = Gtk::WidgetPaintable::create();
519
paintable->set_widget(*this);
520
drag_source->set_icon(paintable, 0, 0);
521
return Gdk::ContentProvider::create(value);
522
}, false);
523
// Pop out if dragged to an external location
524
// TODO: Implement this, allow defining custom behavior at the content manager level
525
}
526
527
void ContentTab::update_active_style() {
528
if(this->page->get_stack()->get_visible_child() == this->page) {
529
this->add_css_class("checked");
530
this->add_css_class("gpanthera-dock-button-active");
531
this->set_active(true);
532
} else {
533
this->remove_css_class("checked");
534
this->remove_css_class("gpanthera-dock-button-active");
535
this->set_active(false);
536
}
537
}
538
539
ContentTab::~ContentTab() {
540
active_style_handler.disconnect();
541
}
542
543
Gtk::Widget *ContentPage::get_tab_widget() const {
544
return this->tab_widget;
545
}
546
547
Gtk::Stack *ContentPage::get_stack() const {
548
return this->stack;
549
}
550
551
void ContentPage::redock(ContentStack *stack) {
552
this->stack = stack;
553
if(this->last_stack != nullptr) {
554
this->last_stack->remove(*this);
555
}
556
this->last_stack = stack;
557
this->stack->add(*this, this->get_child()->get_name());
558
}
559
560
Gtk::Widget *ContentPage::get_child() const {
561
return this->child;
562
}
563
564
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
565
Gtk::Box(Gtk::Orientation::VERTICAL, 0), content_manager(std::move(content_manager)), stack(stack), child(child), tab_widget(tab_widget) {
566
this->content_manager->add_stack(this->stack);
567
this->append(*child);
568
this->set_tab_widget(tab_widget);
569
this->set_margin_top(0);
570
this->set_margin_bottom(0);
571
this->set_margin_start(0);
572
this->set_margin_end(0);
573
}
574
575
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
576
}
577
578
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
579
this->tab_widget = tab_widget;
580
}
581
582
} // namespace gPanthera
583