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