GTK docking interfaces

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