gpanthera.cc
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