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++ • 33.63 kiB
C++ source, ASCII text
        
            
1
#include "gpanthera.hh"
2
3
#include <cassert>
4
#include <iostream>
5
#include <utility>
6
#include <libintl.h>
7
#include <locale.h>
8
#include <filesystem>
9
#define _(STRING) gettext(STRING)
10
11
namespace gPanthera {
12
std::vector<Gtk::Widget*> collect_children(Gtk::Widget &widget) {
13
// Get a vector of the children of a GTK widget, since the container API was removed in GTK 4
14
std::vector<Gtk::Widget*> children;
15
for(auto *child = widget.get_first_child(); child; child = child->get_next_sibling()) {
16
children.push_back(child);
17
}
18
return children;
19
}
20
21
Gtk::Image *copy_image(Gtk::Image *image) {
22
// Generate a new Gtk::Image with the same contents as an existing one
23
if(image->get_storage_type() == Gtk::Image::Type::PAINTABLE) {
24
return Gtk::make_managed<Gtk::Image>(image->get_paintable());
25
} else if(image->get_storage_type() == Gtk::Image::Type::ICON_NAME) {
26
auto new_image = Gtk::make_managed<Gtk::Image>();
27
new_image->set_from_icon_name(image->get_icon_name());
28
return new_image;
29
} else {
30
return nullptr;
31
}
32
}
33
34
void init() {
35
g_log_set_always_fatal(G_LOG_LEVEL_CRITICAL);
36
// Set up gettext configurationIsn't
37
bindtextdomain("gpanthera", "./locales");
38
textdomain("gpanthera");
39
}
40
41
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)
42
: Gtk::Box(Gtk::Orientation::VERTICAL, 0), name(name) {
43
if(icon) {
44
this->icon = icon;
45
}
46
if(stack) {
47
this->stack = stack;
48
}
49
this->layout = std::move(layout);
50
this->label.set_text(label);
51
// This should be replaced with a custom class in the future
52
header = std::make_unique<Gtk::HeaderBar>();
53
header->set_show_title_buttons(false);
54
if(custom_header) {
55
header->set_title_widget(*custom_header);
56
} else {
57
header->set_title_widget(this->label);
58
}
59
header->add_css_class("gpanthera-dock-titlebar");
60
auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>();
61
auto header_menu = Gio::Menu::create();
62
header_menu_button->set_direction(Gtk::ArrowType::NONE);
63
64
// Pane menu
65
this->action_group = Gio::SimpleActionGroup::create();
66
header_menu_button->insert_action_group("win", action_group);
67
68
// Close action
69
auto close_action = Gio::SimpleAction::create("close");
70
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
71
if(this->stack) {
72
this->stack->set_visible_child("");
73
}
74
});
75
action_group->add_action(close_action);
76
header_menu->append(_("Close"), "win.close");
77
78
// Pop out action
79
auto pop_out_action = Gio::SimpleAction::create("pop_out");
80
pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) {
81
if(this->stack) {
82
this->pop_out();
83
}
84
});
85
action_group->add_action(pop_out_action);
86
header_menu->append(_("Pop out"), "win.pop_out");
87
88
// Move menu
89
auto move_menu = Gio::Menu::create();
90
for(auto &this_stack : this->layout->stacks) {
91
auto action_name = "move_" + this_stack->name;
92
auto move_action = Gio::SimpleAction::create(action_name);
93
move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) {
94
this_stack->add_pane(*this);
95
});
96
action_group->add_action(move_action);
97
move_menu->append(this_stack->name, "win." + action_name);
98
}
99
100
// Add move submenu
101
header_menu->append_submenu(_("Move"), move_menu);
102
103
// Switch to traditional (nested) submenus, not sliding
104
auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED);
105
popover_menu->set_has_arrow(false);
106
header_menu_button->set_popover(*popover_menu);
107
108
// TODO: Add a context menu as well
109
110
header->pack_end(*header_menu_button);
111
112
this->prepend(*header);
113
this->child = &child;
114
this->append(child);
115
}
116
117
Gtk::Stack *DockablePane::get_stack() const {
118
return stack;
119
}
120
121
void DockablePane::redock(DockStack *stack) {
122
if(this->window != nullptr) {
123
this->window->hide();
124
// Put the titlebar back
125
this->window->unset_titlebar();
126
this->window->set_decorated(false);
127
this->header->get_style_context()->remove_class("titlebar");
128
this->prepend(*this->header);
129
this->window->unset_child();
130
this->window->close();
131
} else if(this->get_parent() && this->stack == this->get_parent()) {
132
this->stack->remove(*this);
133
}
134
this->stack = stack;
135
this->last_stack = stack;
136
this->stack->add(*this, this->get_identifier());
137
if(this->window != nullptr) {
138
this->window->destroy();
139
delete this->window;
140
this->window = nullptr;
141
// Re-enable the pop out option
142
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
143
action->set_enabled(true);
144
this->header->get_style_context()->remove_class("gpanthera-dock-titlebar-popout");
145
}
146
}
147
148
void DockablePane::pop_out() {
149
if(this->stack != nullptr) {
150
this->stack->remove(*this);
151
this->stack = nullptr;
152
}
153
154
if(this->window == nullptr) {
155
// Remove the header bar from the pane, so it can be used as the titlebar of the window
156
this->remove(*this->header);
157
this->window = new DockWindow(this);
158
this->window->set_titlebar(*this->header);
159
this->window->set_child(*this);
160
this->window->set_decorated(true);
161
// Grey out the pop-out option
162
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
163
action->set_enabled(false);
164
this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout");
165
}
166
this->window->present();
167
}
168
169
Glib::ustring DockablePane::get_identifier() const {
170
return name;
171
}
172
173
Gtk::Image *DockablePane::get_icon() const {
174
return icon;
175
}
176
177
Gtk::Widget *DockablePane::get_child() const {
178
return child;
179
}
180
181
Gtk::Label *DockablePane::get_label() {
182
return &label;
183
}
184
185
LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") {
186
}
187
188
void LayoutManager::add_pane(DockablePane *pane) {
189
panes.push_back(pane);
190
}
191
192
void LayoutManager::add_stack(DockStack *stack) {
193
stacks.push_back(stack);
194
}
195
196
void LayoutManager::remove_pane(DockablePane *pane) {
197
panes.erase(std::ranges::remove(panes, pane).begin(), panes.end());
198
}
199
200
void LayoutManager::remove_stack(DockStack *stack) {
201
stacks.erase(std::ranges::remove(stacks, stack).begin(), stacks.end());
202
}
203
204
BaseStack::BaseStack() : Gtk::Stack() {
205
}
206
207
DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name) : BaseStack(), layout(layout), name(name) {
208
auto empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
209
this->add(*empty_child, "");
210
// Add the stack to a layout manager
211
this->layout->add_stack(this);
212
213
// Hide the stack when no child is visible
214
this->property_visible_child_name().signal_changed().connect([this]() {
215
if(this->get_visible_child_name() == "") {
216
this->hide();
217
} else {
218
this->show();
219
}
220
});
221
222
// Also hide when the visible child is removed
223
this->signal_child_removed.connect([this](Gtk::Widget* const &child) {
224
if(this->get_visible_child_name() == "") {
225
this->hide();
226
}
227
});
228
229
this->set_visible_child("");
230
this->hide();
231
}
232
233
DockWindow::DockWindow(DockablePane *pane) {
234
this->pane = pane;
235
this->set_child(*pane);
236
// Attempting to close the window should redock the pane so it doesn't vanish
237
this->signal_close_request().connect([this]() {
238
this->pane->redock(this->pane->last_stack);
239
return true;
240
}, false);
241
}
242
243
DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
244
auto update_callback = [this](Gtk::Widget*) {
245
this->update_buttons();
246
};
247
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
248
stack->signal_child_added.connect(update_callback);
249
stack->signal_child_removed.connect(update_callback);
250
this->get_style_context()->add_class("gpanthera-dock-switcher");
251
drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
252
this->add_controller(drop_target);
253
// Process dropped buttons
254
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
255
const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get();
256
257
if(widget) {
258
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
259
this->stack->add_pane(*pane);
260
}
261
}
262
263
return true; // Drop OK
264
}, false);
265
}
266
267
DockStack *DockStackSwitcher::get_stack() const {
268
return stack;
269
}
270
271
DockStackSwitcher::~DockStackSwitcher() {
272
add_handler.disconnect();
273
remove_handler.disconnect();
274
}
275
276
DockButton::DockButton(DockablePane *pane) : Gtk::ToggleButton(), pane(pane) {
277
if(pane->get_icon()) {
278
this->set_child(*copy_image(pane->get_icon()));
279
}
280
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
281
this->set_tooltip_text(pane->get_label()->get_text());
282
this->set_halign(Gtk::Align::CENTER);
283
this->set_valign(Gtk::Align::CENTER);
284
this->get_style_context()->add_class("toggle");
285
this->get_style_context()->add_class("gpanthera-dock-button");
286
// Add/remove CSS classes when the pane is shown/hidden
287
active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
288
this->update_active_style();
289
});
290
drag_source = Gtk::DragSource::create();
291
drag_source->set_exclusive(false);
292
// This is to prevent the click handler from taking over grabbing the button
293
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
294
value.init(DockablePane::get_type());
295
value.set(pane);
296
// Add the drag source to the button
297
this->add_controller(drag_source);
298
this->signal_clicked().connect([this, pane]() {
299
if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) {
300
pane->get_stack()->set_visible_child("");
301
} else {
302
pane->get_stack()->set_visible_child(pane->get_identifier());
303
}
304
});
305
// Provide the drag data
306
drag_source->signal_prepare().connect([this](double, double) {
307
drag_source->set_actions(Gdk::DragAction::MOVE);
308
auto const paintable = Gtk::WidgetPaintable::create();
309
paintable->set_widget(*this);
310
drag_source->set_icon(paintable->get_current_image(), 0, 0);
311
return Gdk::ContentProvider::create(value);
312
}, false);
313
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
314
this->set_opacity(0);
315
}, false);
316
// Pop out if dragged to an external location
317
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
318
if(reason == Gdk::DragCancelReason::NO_TARGET) {
319
this->pane->pop_out();
320
return true;
321
}
322
this->set_opacity(1);
323
return false;
324
}, false);
325
// Add a drop target to the button
326
auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
327
drop_target->set_actions(Gdk::DragAction::MOVE);
328
drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
329
this->add_controller(drop_target);
330
// Process dropped buttons by inserting them after the current button
331
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
332
const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get();
333
334
if(widget) {
335
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
336
if(pane->layout != this->pane->layout) {
337
// If the pane is not in the same layout manager, reject
338
return false;
339
}
340
auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent());
341
if(switcher) {
342
auto *stack = switcher->get_stack();
343
// Move the button to the new position
344
pane->redock(stack);
345
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
346
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
347
pane->insert_before(*stack, *this->pane);
348
} else {
349
pane->insert_after(*stack, *this->pane);
350
}
351
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
352
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
353
pane->insert_before(*stack, *this->pane);
354
} else {
355
pane->insert_after(*stack, *this->pane);
356
}
357
}
358
359
switcher->update_buttons();
360
}
361
}
362
}
363
364
return true; // Drop OK
365
}, false);
366
this->update_active_style();
367
}
368
369
void DockButton::update_active_style() {
370
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
371
this->add_css_class("checked");
372
this->add_css_class("gpanthera-dock-button-active");
373
this->set_active(true);
374
} else {
375
this->remove_css_class("checked");
376
this->remove_css_class("gpanthera-dock-button-active");
377
this->set_active(false);
378
}
379
}
380
381
DockButton::~DockButton() {
382
active_style_handler.disconnect();
383
}
384
385
void DockStackSwitcher::update_buttons() {
386
// Clear the old buttons
387
auto old_buttons = collect_children(*this);
388
for(auto *button : old_buttons) {
389
remove(*button);
390
}
391
DockButton* first_child = nullptr;
392
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
393
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
394
auto *button = Gtk::make_managed<DockButton>(pane);
395
if(!first_child) {
396
first_child = button;
397
} else {
398
button->set_group(*first_child);
399
}
400
if(pane->get_identifier() != Glib::ustring("")) {
401
append(*button);
402
}
403
}
404
}
405
}
406
407
void DockStack::add_pane(DockablePane &child) {
408
child.redock(this);
409
}
410
411
void ContentManager::add_stack(ContentStack *stack) {
412
this->stacks.push_back(stack);
413
}
414
415
void ContentManager::remove_stack(ContentStack *stack) {
416
this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end());
417
}
418
419
void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) {
420
Gtk::Stack::add(child, name);
421
signal_child_added.emit(&child);
422
}
423
424
void BaseStack::add(Gtk::Widget &child) {
425
Gtk::Stack::add(child);
426
signal_child_added.emit(&child);
427
}
428
429
void BaseStack::remove(Gtk::Widget &child) {
430
Gtk::Stack::remove(child);
431
signal_child_removed.emit(&child);
432
}
433
434
ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager)
435
: BaseStack(), content_manager(std::move(content_manager)) {
436
this->set_name("gpanthera_content_stack");
437
this->content_manager->add_stack(this);
438
439
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
440
this->add_controller(drop_target);
441
// Process dropped buttons
442
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
443
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
444
445
if(auto page = dynamic_cast<ContentPage*>(widget)) {
446
if(page->get_stack() == this && !this->get_first_child()->get_next_sibling()) {
447
// Don't allow splitting if there are no more pages
448
return false;
449
}
450
if(!(page->content_manager == this->content_manager)) {
451
// If the page is not in the same content manager, reject
452
return false;
453
}
454
double width = this->get_allocated_width(), height = this->get_allocated_height();
455
// Split based on the drop position
456
if(!(x < width / 4 || x > width * 3 / 4 || y < height / 4 || y > height * 3 / 4)) {
457
// If the drop position is not at a quarter to the edges, move to the same stack
458
this->add_page(*page);
459
return true;
460
}
461
auto new_stack = Gtk::make_managed<ContentStack>(this->content_manager);
462
auto this_notebook = dynamic_cast<ContentNotebook*>(this->get_parent());
463
auto new_switcher = Gtk::make_managed<ContentTabBar>(new_stack, this_notebook ? this_notebook->get_switcher()->get_orientation() : Gtk::Orientation::HORIZONTAL);
464
auto new_notebook = Gtk::make_managed<ContentNotebook>(new_stack, new_switcher, this_notebook ? this_notebook->get_tab_position() : Gtk::PositionType::TOP);
465
new_stack->add_page(*page);
466
new_stack->set_visible_child(*page);
467
if(this->get_first_child()) {
468
this->set_visible_child(*this->get_first_child());
469
}
470
if(x < width / 4) {
471
this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::START);
472
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
473
paned->set_start_child(*new_notebook);
474
}
475
} else if(x > width * 3 / 4) {
476
this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::END);
477
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
478
paned->set_end_child(*new_notebook);
479
}
480
} else if(y < height / 4) {
481
this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::START);
482
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
483
paned->set_start_child(*new_notebook);
484
}
485
} else if(y > height * 3 / 4) {
486
this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::END);
487
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
488
paned->set_end_child(*new_notebook);
489
}
490
}
491
}
492
493
return true; // Drop OK
494
}, false);
495
}
496
497
ContentNotebook::ContentNotebook(ContentStack *stack, ContentTabBar *switcher, Gtk::PositionType tab_position) : Gtk::Box(), stack(stack), switcher(switcher), tab_position(tab_position) {
498
this->append(*stack);
499
if(tab_position == Gtk::PositionType::TOP || tab_position == Gtk::PositionType::BOTTOM) {
500
this->set_orientation(Gtk::Orientation::VERTICAL);
501
if(tab_position == Gtk::PositionType::TOP) {
502
this->prepend(*switcher);
503
} else if(tab_position == Gtk::PositionType::BOTTOM) {
504
this->append(*switcher);
505
}
506
} else if(tab_position == Gtk::PositionType::LEFT || tab_position == Gtk::PositionType::RIGHT) {
507
this->set_orientation(Gtk::Orientation::HORIZONTAL);
508
if(tab_position == Gtk::PositionType::LEFT) {
509
this->prepend(*switcher);
510
} else if(tab_position == Gtk::PositionType::RIGHT) {
511
this->append(*switcher);
512
}
513
}
514
}
515
516
void ContentStack::make_paned(Gtk::Orientation orientation, Gtk::PackType pack_type) {
517
auto *parent = this->get_parent();
518
if(auto notebook = dynamic_cast<ContentNotebook*>(parent)) {
519
auto *paned = Gtk::make_managed<Gtk::Paned>(orientation);
520
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(notebook->get_parent())) {
521
if(parent_paned->get_start_child() == notebook) {
522
notebook->unparent();
523
parent_paned->set_start_child(*paned);
524
} else if(parent_paned->get_end_child() == notebook) {
525
notebook->unparent();
526
parent_paned->set_end_child(*paned);
527
}
528
} else if(auto box = dynamic_cast<Gtk::Box*>(notebook->get_parent())) {
529
auto previous_child = notebook->get_prev_sibling();
530
box->remove(*notebook);
531
if(previous_child) {
532
paned->insert_after(*box, *previous_child);
533
} else {
534
box->prepend(*paned);
535
}
536
}
537
if(pack_type == Gtk::PackType::START) {
538
paned->set_end_child(*notebook);
539
} else if(pack_type == Gtk::PackType::END) {
540
paned->set_start_child(*notebook);
541
}
542
}
543
}
544
545
ContentTabBar::ContentTabBar(ContentStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
546
this->get_style_context()->add_class("gpanthera-content-tab-bar");
547
this->set_margin_top(0);
548
this->set_margin_bottom(0);
549
this->set_margin_start(0);
550
this->set_margin_end(0);
551
552
auto update_callback = [this](Gtk::Widget*) {
553
this->update_buttons();
554
};
555
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
556
stack->signal_child_added.connect(update_callback);
557
stack->signal_child_removed.connect(update_callback);
558
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
559
this->add_controller(drop_target);
560
// Process dropped buttons
561
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
562
if(const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get()) {
563
if(auto page = dynamic_cast<ContentPage*>(widget)) {
564
this->stack->add_page(*page);
565
}
566
}
567
568
return true; // Drop OK
569
}, false);
570
}
571
572
ContentTabBar::~ContentTabBar() {
573
add_handler.disconnect();
574
remove_handler.disconnect();
575
}
576
577
ContentStack *ContentTabBar::get_stack() const {
578
return stack;
579
}
580
581
void ContentTabBar::update_buttons() {
582
// Clear the old buttons
583
auto old_buttons = collect_children(*this);
584
for(auto *button : old_buttons) {
585
if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) {
586
button_button->unset_child();
587
}
588
remove(*button);
589
}
590
ContentTab* first_child = nullptr;
591
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
592
if(auto page = dynamic_cast<ContentPage*>(widget)) {
593
auto *button = Gtk::make_managed<ContentTab>(page);
594
if(!first_child) {
595
first_child = button;
596
} else {
597
button->set_group(*first_child);
598
}
599
this->append(*button);
600
}
601
}
602
}
603
604
void ContentStack::add_page(ContentPage &child) {
605
child.redock(this);
606
}
607
608
void ContentStack::remove_with_paned() {
609
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
610
Gtk::Widget *child = nullptr;
611
if(this->get_parent() == paned->get_start_child()) {
612
child = paned->get_end_child();
613
paned->property_end_child().reset_value();
614
} else if(this->get_parent() == paned->get_end_child()) {
615
child = paned->get_start_child();
616
paned->property_start_child().reset_value();
617
} else {
618
return;
619
}
620
621
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(paned->get_parent())) {
622
if(parent_paned->get_start_child() == paned) {
623
parent_paned->set_start_child(*child);
624
} else if(parent_paned->get_end_child() == paned) {
625
parent_paned->set_end_child(*child);
626
}
627
} else if(auto box = dynamic_cast<Gtk::Box*>(paned->get_parent())) {
628
child->insert_after(*box, *paned);
629
paned->unparent();
630
}
631
}
632
}
633
634
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
635
this->set_child(*page->get_tab_widget());
636
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
637
this->set_halign(Gtk::Align::CENTER);
638
this->set_valign(Gtk::Align::CENTER);
639
this->get_style_context()->add_class("toggle");
640
this->get_style_context()->add_class("gpanthera-content-tab");
641
// Add/remove CSS classes when the pane is shown/hidden
642
active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() {
643
this->update_active_style();
644
});
645
drag_source = Gtk::DragSource::create();
646
drag_source->set_exclusive(false);
647
// This is to prevent the click handler from taking over grabbing the button
648
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
649
value.init(ContentPage::get_type());
650
value.set(page);
651
// Add the drag source to the button
652
this->add_controller(drag_source);
653
this->signal_clicked().connect([this, page]() {
654
page->get_stack()->set_visible_child(*page);
655
update_active_style();
656
});
657
// Switch tabs on depress
658
auto gesture_click = Gtk::GestureClick::create();
659
gesture_click->set_button(1);
660
this->add_controller(gesture_click);
661
gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) {
662
page->get_stack()->set_visible_child(*page);
663
update_active_style();
664
});
665
// Provide the drag data
666
drag_source->signal_prepare().connect([this](double, double) {
667
drag_source->set_actions(Gdk::DragAction::MOVE);
668
auto const paintable = Gtk::WidgetPaintable::create();
669
paintable->set_widget(*this);
670
drag_source->set_icon(paintable->get_current_image(), 0, 0);
671
return Gdk::ContentProvider::create(value);
672
}, false);
673
update_active_style();
674
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
675
this->set_opacity(0);
676
}, false);
677
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
678
// Process dropped buttons by inserting them after the current button
679
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
680
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
681
682
if(widget) {
683
if(auto page = dynamic_cast<ContentPage*>(widget)) {
684
if(page->content_manager != this->page->content_manager) {
685
// If the pane is not in the same layout manager, reject
686
return false;
687
}
688
auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent());
689
if(switcher) {
690
auto *stack = switcher->get_stack();
691
// Move the button to the new position
692
page->redock(stack);
693
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
694
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
695
page->insert_before(*stack, *this->page);
696
} else {
697
page->insert_after(*stack, *this->page);
698
}
699
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
700
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
701
page->insert_before(*stack, *this->page);
702
} else {
703
page->insert_after(*stack, *this->page);
704
}
705
}
706
707
switcher->update_buttons();
708
}
709
}
710
}
711
712
return true; // Drop OK
713
}, false);
714
this->add_controller(drop_target);
715
// Pop out if dragged to an external location
716
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
717
if(reason == Gdk::DragCancelReason::NO_TARGET) {
718
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
719
bool result = stack->signal_detach.emit(this->page);
720
if(!result) {
721
this->set_opacity(1);
722
}
723
return result;
724
}
725
this->set_opacity(1);
726
return false;
727
}, false);
728
}
729
730
void ContentTab::update_active_style() {
731
if(this->page->get_stack()->get_visible_child() == this->page) {
732
this->add_css_class("checked");
733
this->add_css_class("gpanthera-dock-button-active");
734
this->set_active(true);
735
} else {
736
this->remove_css_class("checked");
737
this->remove_css_class("gpanthera-dock-button-active");
738
this->set_active(false);
739
}
740
}
741
742
ContentTab::~ContentTab() {
743
active_style_handler.disconnect();
744
}
745
746
Gtk::Widget *ContentPage::get_tab_widget() const {
747
return this->tab_widget;
748
}
749
750
Gtk::Stack *ContentPage::get_stack() const {
751
return this->stack;
752
}
753
754
void ContentPage::redock(ContentStack *stack) {
755
// Check if the stack is now empty, in which case we should remove it
756
if(this->stack == stack) {
757
return;
758
}
759
if(this->stack != nullptr) {
760
if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && (!this->stack->get_first_child() || (this->stack->get_first_child() == this && !this->stack->get_first_child()->get_next_sibling()))) {
761
this->stack->remove(*this);
762
this->stack->remove_with_paned();
763
} else if(this->get_parent() == this->stack) {
764
this->stack->remove(*this);
765
}
766
}
767
auto old_stack = this->stack;
768
this->stack = stack;
769
this->last_stack = stack;
770
this->stack->add(*this);
771
if(old_stack) {
772
if(!old_stack->get_first_child()) {
773
old_stack->signal_leave_empty.emit();
774
}
775
}
776
}
777
778
Gtk::Widget *ContentPage::get_child() const {
779
return this->child;
780
}
781
782
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
783
Gtk::Box(Gtk::Orientation::VERTICAL, 0), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) {
784
this->set_name("gpanthera_content_page");
785
this->append(*child);
786
this->set_tab_widget(tab_widget);
787
this->set_margin_top(0);
788
this->set_margin_bottom(0);
789
this->set_margin_start(0);
790
this->set_margin_end(0);
791
if(stack) {
792
stack->add_page(*this);
793
this->content_manager->add_stack(this->stack);
794
}
795
}
796
797
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
798
}
799
800
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
801
this->tab_widget = tab_widget;
802
}
803
804
ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) {
805
this->set_child(*notebook);
806
this->set_decorated(true);
807
this->set_resizable(true);
808
}
809
810
Gtk::PositionType ContentNotebook::get_tab_position() const {
811
return this->tab_position;
812
}
813
814
ContentTabBar *ContentNotebook::get_switcher() const {
815
return this->switcher;
816
}
817
818
ContentStack *ContentNotebook::get_stack() const {
819
return this->stack;
820
}
821
} // namespace gPanthera
822