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++ • 22.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->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
remove(*button);
478
}
479
ContentTab* first_child = nullptr;
480
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
481
if(auto page = dynamic_cast<ContentPage*>(widget)) {
482
auto *button = Gtk::make_managed<ContentTab>(page);
483
if(!first_child) {
484
first_child = button;
485
} else {
486
button->set_group(*first_child);
487
}
488
this->append(*button);
489
}
490
}
491
}
492
493
void ContentStack::add_page(ContentPage &child) {
494
child.redock(this);
495
}
496
497
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
498
this->set_child(*page->get_tab_widget());
499
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
500
this->set_halign(Gtk::Align::CENTER);
501
this->set_valign(Gtk::Align::CENTER);
502
this->get_style_context()->add_class("toggle");
503
this->get_style_context()->add_class("gpanthera-content-tab");
504
// Add/remove CSS classes when the pane is shown/hidden
505
active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() {
506
this->update_active_style();
507
});
508
drag_source = Gtk::DragSource::create();
509
drag_source->set_exclusive(false);
510
// This is to prevent the click handler from taking over grabbing the button
511
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
512
value.init(ContentPage::get_type());
513
value.set(page);
514
// Add the drag source to the button
515
this->add_controller(drag_source);
516
this->signal_clicked().connect([this, page]() {
517
page->get_stack()->set_visible_child(*page);
518
update_active_style();
519
});
520
// Provide the drag data
521
drag_source->signal_prepare().connect([this](double, double) {
522
drag_source->set_actions(Gdk::DragAction::MOVE);
523
auto const paintable = Gtk::WidgetPaintable::create();
524
paintable->set_widget(*this);
525
drag_source->set_icon(paintable, 0, 0);
526
return Gdk::ContentProvider::create(value);
527
}, false);
528
update_active_style();
529
// Pop out if dragged to an external location
530
// TODO: Implement this, allow defining custom behavior at the content manager level
531
}
532
533
void ContentTab::update_active_style() {
534
if(this->page->get_stack()->get_visible_child() == this->page) {
535
this->add_css_class("checked");
536
this->add_css_class("gpanthera-dock-button-active");
537
this->set_active(true);
538
} else {
539
this->remove_css_class("checked");
540
this->remove_css_class("gpanthera-dock-button-active");
541
this->set_active(false);
542
}
543
}
544
545
ContentTab::~ContentTab() {
546
active_style_handler.disconnect();
547
}
548
549
Gtk::Widget *ContentPage::get_tab_widget() const {
550
return this->tab_widget;
551
}
552
553
Gtk::Stack *ContentPage::get_stack() const {
554
return this->stack;
555
}
556
557
void ContentPage::redock(ContentStack *stack) {
558
this->stack = stack;
559
if(this->last_stack != nullptr) {
560
this->last_stack->remove(*this);
561
}
562
this->last_stack = stack;
563
this->stack->add(*this, this->get_child()->get_name());
564
}
565
566
Gtk::Widget *ContentPage::get_child() const {
567
return this->child;
568
}
569
570
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
571
Gtk::Box(Gtk::Orientation::VERTICAL, 0), content_manager(std::move(content_manager)), stack(stack), child(child), tab_widget(tab_widget) {
572
this->content_manager->add_stack(this->stack);
573
this->append(*child);
574
this->set_tab_widget(tab_widget);
575
this->set_margin_top(0);
576
this->set_margin_bottom(0);
577
this->set_margin_start(0);
578
this->set_margin_end(0);
579
}
580
581
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
582
}
583
584
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
585
this->tab_widget = tab_widget;
586
}
587
588
} // namespace gPanthera
589