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++ • 32.35 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 new_switcher = Gtk::make_managed<ContentTabBar>(new_stack, Gtk::Orientation::HORIZONTAL);
463
auto new_notebook = Gtk::make_managed<ContentNotebook>(new_stack, new_switcher);
464
new_stack->add_page(*page);
465
new_stack->set_visible_child(*page);
466
if(this->get_first_child()) {
467
this->set_visible_child(*this->get_first_child());
468
}
469
if(x < width / 4) {
470
this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::START);
471
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
472
paned->set_start_child(*new_notebook);
473
}
474
} else if(x > width * 3 / 4) {
475
this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::END);
476
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
477
paned->set_end_child(*new_notebook);
478
}
479
} else if(y < height / 4) {
480
this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::START);
481
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
482
paned->set_start_child(*new_notebook);
483
}
484
} else if(y > height * 3 / 4) {
485
this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::END);
486
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
487
paned->set_end_child(*new_notebook);
488
}
489
}
490
}
491
492
return true; // Drop OK
493
}, false);
494
}
495
496
ContentNotebook::ContentNotebook(ContentStack *stack, ContentTabBar *switcher) : Gtk::Box(Gtk::Orientation::VERTICAL), stack(stack), switcher(switcher) {
497
this->prepend(*switcher);
498
this->append(*stack);
499
}
500
501
void ContentStack::make_paned(Gtk::Orientation orientation, Gtk::PackType pack_type) {
502
auto *parent = this->get_parent();
503
if(auto notebook = dynamic_cast<ContentNotebook*>(parent)) {
504
auto *paned = Gtk::make_managed<Gtk::Paned>(orientation);
505
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(notebook->get_parent())) {
506
if(parent_paned->get_start_child() == notebook) {
507
notebook->unparent();
508
parent_paned->set_start_child(*paned);
509
} else if(parent_paned->get_end_child() == notebook) {
510
notebook->unparent();
511
parent_paned->set_end_child(*paned);
512
}
513
} else if(auto box = dynamic_cast<Gtk::Box*>(notebook->get_parent())) {
514
auto previous_child = notebook->get_prev_sibling();
515
box->remove(*notebook);
516
if(previous_child) {
517
paned->insert_after(*box, *previous_child);
518
} else {
519
box->prepend(*paned);
520
}
521
}
522
if(pack_type == Gtk::PackType::START) {
523
paned->set_end_child(*notebook);
524
} else if(pack_type == Gtk::PackType::END) {
525
paned->set_start_child(*notebook);
526
}
527
}
528
}
529
530
ContentTabBar::ContentTabBar(ContentStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
531
this->get_style_context()->add_class("gpanthera-content-tab-bar");
532
this->set_margin_top(0);
533
this->set_margin_bottom(0);
534
this->set_margin_start(0);
535
this->set_margin_end(0);
536
537
auto update_callback = [this](Gtk::Widget*) {
538
this->update_buttons();
539
};
540
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
541
stack->signal_child_added.connect(update_callback);
542
stack->signal_child_removed.connect(update_callback);
543
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
544
this->add_controller(drop_target);
545
// Process dropped buttons
546
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
547
if(const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get()) {
548
if(auto page = dynamic_cast<ContentPage*>(widget)) {
549
this->stack->add_page(*page);
550
}
551
}
552
553
return true; // Drop OK
554
}, false);
555
}
556
557
ContentTabBar::~ContentTabBar() {
558
add_handler.disconnect();
559
remove_handler.disconnect();
560
}
561
562
ContentStack *ContentTabBar::get_stack() const {
563
return stack;
564
}
565
566
void ContentTabBar::update_buttons() {
567
// Clear the old buttons
568
auto old_buttons = collect_children(*this);
569
for(auto *button : old_buttons) {
570
if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) {
571
button_button->unset_child();
572
}
573
remove(*button);
574
}
575
ContentTab* first_child = nullptr;
576
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
577
if(auto page = dynamic_cast<ContentPage*>(widget)) {
578
auto *button = Gtk::make_managed<ContentTab>(page);
579
if(!first_child) {
580
first_child = button;
581
} else {
582
button->set_group(*first_child);
583
}
584
this->append(*button);
585
}
586
}
587
}
588
589
void ContentStack::add_page(ContentPage &child) {
590
child.redock(this);
591
}
592
593
void ContentStack::remove_with_paned() {
594
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
595
Gtk::Widget *child = nullptr;
596
if(this->get_parent() == paned->get_start_child()) {
597
child = paned->get_end_child();
598
paned->property_end_child().reset_value();
599
} else if(this->get_parent() == paned->get_end_child()) {
600
child = paned->get_start_child();
601
paned->property_start_child().reset_value();
602
} else {
603
return;
604
}
605
606
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(paned->get_parent())) {
607
if(parent_paned->get_start_child() == paned) {
608
parent_paned->set_start_child(*child);
609
} else if(parent_paned->get_end_child() == paned) {
610
parent_paned->set_end_child(*child);
611
}
612
} else if(auto box = dynamic_cast<Gtk::Box*>(paned->get_parent())) {
613
child->insert_after(*box, *paned);
614
paned->unparent();
615
}
616
}
617
}
618
619
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
620
this->set_child(*page->get_tab_widget());
621
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
622
this->set_halign(Gtk::Align::CENTER);
623
this->set_valign(Gtk::Align::CENTER);
624
this->get_style_context()->add_class("toggle");
625
this->get_style_context()->add_class("gpanthera-content-tab");
626
// Add/remove CSS classes when the pane is shown/hidden
627
active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() {
628
this->update_active_style();
629
});
630
drag_source = Gtk::DragSource::create();
631
drag_source->set_exclusive(false);
632
// This is to prevent the click handler from taking over grabbing the button
633
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
634
value.init(ContentPage::get_type());
635
value.set(page);
636
// Add the drag source to the button
637
this->add_controller(drag_source);
638
this->signal_clicked().connect([this, page]() {
639
page->get_stack()->set_visible_child(*page);
640
update_active_style();
641
});
642
// Switch tabs on depress
643
auto gesture_click = Gtk::GestureClick::create();
644
gesture_click->set_button(1);
645
this->add_controller(gesture_click);
646
gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) {
647
page->get_stack()->set_visible_child(*page);
648
update_active_style();
649
});
650
// Provide the drag data
651
drag_source->signal_prepare().connect([this](double, double) {
652
drag_source->set_actions(Gdk::DragAction::MOVE);
653
auto const paintable = Gtk::WidgetPaintable::create();
654
paintable->set_widget(*this);
655
drag_source->set_icon(paintable->get_current_image(), 0, 0);
656
return Gdk::ContentProvider::create(value);
657
}, false);
658
update_active_style();
659
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
660
this->set_opacity(0);
661
}, false);
662
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
663
// Process dropped buttons by inserting them after the current button
664
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
665
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
666
667
if(widget) {
668
if(auto page = dynamic_cast<ContentPage*>(widget)) {
669
if(page->content_manager != this->page->content_manager) {
670
// If the pane is not in the same layout manager, reject
671
return false;
672
}
673
auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent());
674
if(switcher) {
675
auto *stack = switcher->get_stack();
676
// Move the button to the new position
677
page->redock(stack);
678
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
679
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
680
page->insert_before(*stack, *this->page);
681
} else {
682
page->insert_after(*stack, *this->page);
683
}
684
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
685
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
686
page->insert_before(*stack, *this->page);
687
} else {
688
page->insert_after(*stack, *this->page);
689
}
690
}
691
692
switcher->update_buttons();
693
}
694
}
695
}
696
697
return true; // Drop OK
698
}, false);
699
this->add_controller(drop_target);
700
// Pop out if dragged to an external location
701
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
702
if(reason == Gdk::DragCancelReason::NO_TARGET) {
703
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
704
bool result = stack->signal_detach.emit(this->page);
705
if(!result) {
706
this->set_opacity(1);
707
}
708
return result;
709
}
710
this->set_opacity(1);
711
return false;
712
}, false);
713
}
714
715
void ContentTab::update_active_style() {
716
if(this->page->get_stack()->get_visible_child() == this->page) {
717
this->add_css_class("checked");
718
this->add_css_class("gpanthera-dock-button-active");
719
this->set_active(true);
720
} else {
721
this->remove_css_class("checked");
722
this->remove_css_class("gpanthera-dock-button-active");
723
this->set_active(false);
724
}
725
}
726
727
ContentTab::~ContentTab() {
728
active_style_handler.disconnect();
729
}
730
731
Gtk::Widget *ContentPage::get_tab_widget() const {
732
return this->tab_widget;
733
}
734
735
Gtk::Stack *ContentPage::get_stack() const {
736
return this->stack;
737
}
738
739
void ContentPage::redock(ContentStack *stack) {
740
// Check if the stack is now empty, in which case we should remove it
741
if(this->stack == stack) {
742
return;
743
}
744
if(this->stack != nullptr) {
745
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()))) {
746
this->stack->remove(*this);
747
this->stack->remove_with_paned();
748
} else if(this->get_parent() == this->stack) {
749
this->stack->remove(*this);
750
}
751
}
752
auto old_stack = this->stack;
753
this->stack = stack;
754
this->last_stack = stack;
755
this->stack->add(*this);
756
if(old_stack) {
757
if(!old_stack->get_first_child()) {
758
old_stack->signal_leave_empty.emit();
759
}
760
}
761
}
762
763
Gtk::Widget *ContentPage::get_child() const {
764
return this->child;
765
}
766
767
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
768
Gtk::Box(Gtk::Orientation::VERTICAL, 0), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) {
769
this->set_name("gpanthera_content_page");
770
this->append(*child);
771
this->set_tab_widget(tab_widget);
772
this->set_margin_top(0);
773
this->set_margin_bottom(0);
774
this->set_margin_start(0);
775
this->set_margin_end(0);
776
if(stack) {
777
stack->add_page(*this);
778
this->content_manager->add_stack(this->stack);
779
}
780
}
781
782
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
783
}
784
785
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
786
this->tab_widget = tab_widget;
787
}
788
789
ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) {
790
this->set_child(*notebook);
791
this->set_decorated(true);
792
this->set_resizable(true);
793
}
794
} // namespace gPanthera
795