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
return false;
320
}, false);
321
// Add a drop target to the button
322
auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
323
drop_target->set_actions(Gdk::DragAction::MOVE);
324
drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
325
this->add_controller(drop_target);
326
// Process dropped buttons by inserting them after the current button
327
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
328
const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get();
329
330
if(widget) {
331
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
332
if(pane->layout != this->pane->layout) {
333
// If the pane is not in the same layout manager, reject
334
return false;
335
}
336
auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent());
337
if(switcher) {
338
auto *stack = switcher->get_stack();
339
// Move the button to the new position
340
pane->redock(stack);
341
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
342
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
343
pane->insert_before(*stack, *this->pane);
344
} else {
345
pane->insert_after(*stack, *this->pane);
346
}
347
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
348
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
349
pane->insert_before(*stack, *this->pane);
350
} else {
351
pane->insert_after(*stack, *this->pane);
352
}
353
}
354
355
switcher->update_buttons();
356
}
357
}
358
}
359
360
return true; // Drop OK
361
}, false);
362
this->update_active_style();
363
}
364
365
void DockButton::update_active_style() {
366
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
367
this->add_css_class("checked");
368
this->add_css_class("gpanthera-dock-button-active");
369
this->set_active(true);
370
} else {
371
this->remove_css_class("checked");
372
this->remove_css_class("gpanthera-dock-button-active");
373
this->set_active(false);
374
}
375
}
376
377
DockButton::~DockButton() {
378
active_style_handler.disconnect();
379
}
380
381
void DockStackSwitcher::update_buttons() {
382
// Clear the old buttons
383
auto old_buttons = collect_children(*this);
384
for(auto *button : old_buttons) {
385
remove(*button);
386
}
387
DockButton* first_child = nullptr;
388
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
389
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
390
auto *button = Gtk::make_managed<DockButton>(pane);
391
if(!first_child) {
392
first_child = button;
393
} else {
394
button->set_group(*first_child);
395
}
396
if(pane->get_identifier() != Glib::ustring("")) {
397
append(*button);
398
}
399
}
400
}
401
}
402
403
void DockStack::add_pane(DockablePane &child) {
404
child.redock(this);
405
}
406
407
void ContentManager::add_stack(ContentStack *stack) {
408
this->stacks.push_back(stack);
409
}
410
411
void ContentManager::remove_stack(ContentStack *stack) {
412
this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end());
413
}
414
415
void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) {
416
Gtk::Stack::add(child, name);
417
signal_child_added.emit(&child);
418
}
419
420
void BaseStack::add(Gtk::Widget &child) {
421
Gtk::Stack::add(child);
422
signal_child_added.emit(&child);
423
}
424
425
void BaseStack::remove(Gtk::Widget &child) {
426
Gtk::Stack::remove(child);
427
signal_child_removed.emit(&child);
428
}
429
430
ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager)
431
: BaseStack(), content_manager(std::move(content_manager)) {
432
this->content_manager->add_stack(this);
433
}
434
435
ContentTabBar::ContentTabBar(ContentStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
436
this->get_style_context()->add_class("gpanthera-content-tab-bar");
437
this->set_margin_top(0);
438
this->set_margin_bottom(0);
439
this->set_margin_start(0);
440
this->set_margin_end(0);
441
442
auto update_callback = [this](Gtk::Widget*) {
443
this->update_buttons();
444
};
445
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
446
stack->signal_child_added.connect(update_callback);
447
stack->signal_child_removed.connect(update_callback);
448
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
449
this->add_controller(drop_target);
450
// Process dropped buttons
451
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
452
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
453
454
if(widget) {
455
if(auto page = dynamic_cast<ContentPage*>(widget)) {
456
this->stack->add_page(*page);
457
}
458
}
459
460
return true; // Drop OK
461
}, false);
462
}
463
464
ContentTabBar::~ContentTabBar() {
465
add_handler.disconnect();
466
remove_handler.disconnect();
467
}
468
469
ContentStack *ContentTabBar::get_stack() const {
470
return stack;
471
}
472
473
void ContentTabBar::update_buttons() {
474
// Clear the old buttons
475
auto old_buttons = collect_children(*this);
476
for(auto *button : old_buttons) {
477
remove(*button);
478
}
479
ContentTab* first_child = nullptr;
480
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
481
if(auto page = dynamic_cast<ContentPage*>(widget)) {
482
auto *button = Gtk::make_managed<ContentTab>(page);
483
if(!first_child) {
484
first_child = button;
485
} else {
486
button->set_group(*first_child);
487
}
488
this->append(*button);
489
}
490
}
491
}
492
493
void ContentStack::add_page(ContentPage &child) {
494
child.redock(this);
495
}
496
497
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
498
this->set_child(*page->get_tab_widget());
499
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
500
this->set_halign(Gtk::Align::CENTER);
501
this->set_valign(Gtk::Align::CENTER);
502
this->get_style_context()->add_class("toggle");
503
this->get_style_context()->add_class("gpanthera-content-tab");
504
// Add/remove CSS classes when the pane is shown/hidden
505
active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() {
506
this->update_active_style();
507
});
508
drag_source = Gtk::DragSource::create();
509
drag_source->set_exclusive(false);
510
// This is to prevent the click handler from taking over grabbing the button
511
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
512
value.init(ContentPage::get_type());
513
value.set(page);
514
// Add the drag source to the button
515
this->add_controller(drag_source);
516
this->signal_clicked().connect([this, page]() {
517
page->get_stack()->set_visible_child(*page);
518
update_active_style();
519
});
520
// Provide the drag data
521
drag_source->signal_prepare().connect([this](double, double) {
522
drag_source->set_actions(Gdk::DragAction::MOVE);
523
auto const paintable = Gtk::WidgetPaintable::create();
524
paintable->set_widget(*this);
525
drag_source->set_icon(paintable, 0, 0);
526
return Gdk::ContentProvider::create(value);
527
}, false);
528
update_active_style();
529
// Pop out if dragged to an external location
530
// TODO: Implement this, allow defining custom behavior at the content manager level
531
}
532
533
void ContentTab::update_active_style() {
534
if(this->page->get_stack()->get_visible_child() == this->page) {
535
this->add_css_class("checked");
536
this->add_css_class("gpanthera-dock-button-active");
537
this->set_active(true);
538
} else {
539
this->remove_css_class("checked");
540
this->remove_css_class("gpanthera-dock-button-active");
541
this->set_active(false);
542
}
543
}
544
545
ContentTab::~ContentTab() {
546
active_style_handler.disconnect();
547
}
548
549
Gtk::Widget *ContentPage::get_tab_widget() const {
550
return this->tab_widget;
551
}
552
553
Gtk::Stack *ContentPage::get_stack() const {
554
return this->stack;
555
}
556
557
void ContentPage::redock(ContentStack *stack) {
558
this->stack = stack;
559
if(this->last_stack != nullptr) {
560
this->last_stack->remove(*this);
561
}
562
this->last_stack = stack;
563
this->stack->add(*this, this->get_child()->get_name());
564
}
565
566
Gtk::Widget *ContentPage::get_child() const {
567
return this->child;
568
}
569
570
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
571
Gtk::Box(Gtk::Orientation::VERTICAL, 0), content_manager(std::move(content_manager)), stack(stack), child(child), tab_widget(tab_widget) {
572
this->content_manager->add_stack(this->stack);
573
this->append(*child);
574
this->set_tab_widget(tab_widget);
575
this->set_margin_top(0);
576
this->set_margin_bottom(0);
577
this->set_margin_start(0);
578
this->set_margin_end(0);
579
}
580
581
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
582
}
583
584
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
585
this->tab_widget = tab_widget;
586
}
587
588
} // namespace gPanthera
589