GTK docking interfaces

By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 gpanthera.cc

View raw Download
text/x-c++ • 25.5 kiB
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