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 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