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
#include <ranges>
10
#include <nlohmann/json.hpp>
11
#include <unordered_set>
12
13
#define _(STRING) dgettext("gpanthera", STRING)
14
15
namespace gPanthera {
16
sigc::signal<void(double, double)> add_context_menu(Gtk::Widget &widget) {
17
sigc::signal<void(double, double)> signal;
18
// Add a context menu to a widget
19
auto gesture_click = Gtk::GestureClick::create();
20
gesture_click->set_button(3);
21
auto gesture_keyboard = Gtk::EventControllerKey::create();
22
gesture_click->signal_pressed().connect([&widget, signal](int n_press, double x, double y) {
23
if(n_press == 1) {
24
signal.emit(x, y);
25
}
26
}, false);
27
gesture_keyboard->signal_key_pressed().connect([&widget, signal](int keyval, int keycode, Gdk::ModifierType state) {
28
if((keyval == GDK_KEY_F10) && (state == Gdk::ModifierType::SHIFT_MASK) || keyval == GDK_KEY_Menu) {
29
// auto popover = Gtk::make_managed<Gtk::PopoverMenu>();
30
// popover->set_menu_model(menu);
31
// popover->set_has_arrow(false);
32
// popover->set_halign(Gtk::Align::START);
33
// popover->set_pointing_to(Gdk::Rectangle(0, 0, 1, 1));
34
// popover->set_parent(widget);
35
// popover->popup();
36
signal.emit(0, 0);
37
}
38
return true;
39
}, false);
40
widget.add_controller(gesture_click);
41
widget.add_controller(gesture_keyboard);
42
return signal;
43
}
44
45
std::vector<Gtk::Widget*> collect_children(Gtk::Widget &widget) {
46
// Get a vector of the children of a GTK widget, since the container API was removed in GTK 4
47
std::vector<Gtk::Widget*> children;
48
for(auto *child = widget.get_first_child(); child; child = child->get_next_sibling()) {
49
children.push_back(child);
50
}
51
return children;
52
}
53
54
Gtk::Image *copy_image(Gtk::Image *image) {
55
// Generate a new Gtk::Image with the same contents as an existing one
56
if(image->get_storage_type() == Gtk::Image::Type::PAINTABLE) {
57
return Gtk::make_managed<Gtk::Image>(image->get_paintable());
58
} else if(image->get_storage_type() == Gtk::Image::Type::ICON_NAME) {
59
auto new_image = Gtk::make_managed<Gtk::Image>();
60
new_image->set_from_icon_name(image->get_icon_name());
61
return new_image;
62
} else if(image->get_storage_type() == Gtk::Image::Type::GICON) {
63
auto new_image = Gtk::make_managed<Gtk::Image>(image->get_gicon());
64
return new_image;
65
} else {
66
return nullptr;
67
}
68
}
69
70
void init() {
71
g_log_set_always_fatal(G_LOG_LEVEL_CRITICAL);
72
// Set up gettext configuration
73
bindtextdomain("gpanthera", "./locales");
74
}
75
76
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)
77
: Gtk::Box(Gtk::Orientation::VERTICAL, 0), name(name) {
78
if(icon) {
79
this->icon = icon;
80
}
81
if(stack) {
82
this->stack = stack;
83
}
84
this->layout = layout;
85
this->layout->add_pane(this);
86
this->label.set_text(label);
87
// This should be replaced with a custom class in the future
88
header = std::make_unique<Gtk::HeaderBar>();
89
header->set_show_title_buttons(false);
90
if(custom_header) {
91
header->set_title_widget(*custom_header);
92
} else {
93
header->set_title_widget(this->label);
94
}
95
header->add_css_class("gpanthera-dock-titlebar");
96
auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>();
97
header_menu_button->set_direction(Gtk::ArrowType::NONE);
98
99
// Pane menu
100
this->action_group = Gio::SimpleActionGroup::create();
101
header_menu_button->insert_action_group("win", action_group);
102
103
// Close action
104
auto close_action = Gio::SimpleAction::create("close");
105
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
106
if(this->stack) {
107
this->stack->set_visible_child("");
108
}
109
});
110
action_group->add_action(close_action);
111
header_menu->append(_("Close"), "win.close");
112
113
// Pop out action
114
auto pop_out_action = Gio::SimpleAction::create("pop_out");
115
pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) {
116
if(this->stack) {
117
this->pop_out();
118
}
119
});
120
action_group->add_action(pop_out_action);
121
header_menu->append(_("Pop out"), "win.pop_out");
122
123
// Move menu
124
auto move_menu = Gio::Menu::create();
125
for(auto &this_stack : this->layout->stacks) {
126
auto action_name = "move_" + this_stack->name;
127
auto move_action = Gio::SimpleAction::create(action_name);
128
move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) {
129
this_stack->add_pane(*this);
130
});
131
action_group->add_action(move_action);
132
move_menu->append(this_stack->name, "win." + action_name);
133
}
134
135
// Add move submenu
136
header_menu->append_submenu(_("Move"), move_menu);
137
138
// Switch to traditional (nested) submenus, not sliding
139
auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED);
140
popover_menu->set_has_arrow(false);
141
header_menu_button->set_popover(*popover_menu);
142
143
// TODO: Add a context menu as well
144
145
header->pack_end(*header_menu_button);
146
147
this->prepend(*header);
148
this->child = &child;
149
this->append(child);
150
}
151
152
Gtk::Stack *DockablePane::get_stack() const {
153
return stack;
154
}
155
156
void DockablePane::redock(DockStack *stack) {
157
if(this->window != nullptr) {
158
this->window->hide();
159
// Put the titlebar back
160
this->window->unset_titlebar();
161
this->window->set_decorated(false);
162
this->header->get_style_context()->remove_class("titlebar");
163
this->prepend(*this->header);
164
this->window->unset_child();
165
this->window->close();
166
} else if(this->get_parent() && this->stack == this->get_parent()) {
167
this->stack->remove(*this);
168
}
169
this->stack = stack;
170
this->last_stack = stack;
171
this->stack->add(*this, this->get_identifier());
172
if(this->window != nullptr) {
173
this->window->destroy();
174
delete this->window;
175
this->window = nullptr;
176
// Re-enable the pop out option
177
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
178
action->set_enabled(true);
179
this->header->get_style_context()->remove_class("gpanthera-dock-titlebar-popout");
180
}
181
layout->signal_pane_moved.emit(this);
182
}
183
184
void DockablePane::pop_out() {
185
if(this->stack != nullptr) {
186
this->stack->remove(*this);
187
this->stack = nullptr;
188
}
189
190
if(this->window == nullptr) {
191
// Remove the header bar from the pane, so it can be used as the titlebar of the window
192
this->remove(*this->header);
193
this->window = new DockWindow(this);
194
this->window->set_titlebar(*this->header);
195
this->window->set_child(*this);
196
this->window->set_decorated(true);
197
// Grey out the pop-out option
198
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
199
action->set_enabled(false);
200
this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout");
201
}
202
this->window->present();
203
this->signal_pane_shown.emit();
204
layout->signal_pane_moved.emit(this);
205
}
206
207
Glib::ustring DockablePane::get_identifier() const {
208
return name;
209
}
210
211
Gtk::Image *DockablePane::get_icon() const {
212
return icon;
213
}
214
215
Gtk::Widget *DockablePane::get_child() const {
216
return child;
217
}
218
219
Gtk::Label *DockablePane::get_label() {
220
return &label;
221
}
222
223
LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") {
224
}
225
226
void LayoutManager::add_pane(DockablePane *pane) {
227
panes.push_back(pane);
228
}
229
230
void LayoutManager::add_stack(DockStack *stack) {
231
stacks.push_back(stack);
232
}
233
234
void LayoutManager::remove_pane(DockablePane *pane) {
235
panes.erase(std::ranges::remove(panes, pane).begin(), panes.end());
236
}
237
238
void LayoutManager::remove_stack(DockStack *stack) {
239
stacks.erase(std::ranges::remove(stacks, stack).begin(), stacks.end());
240
}
241
242
BaseStack::BaseStack() : Gtk::Stack() {
243
}
244
245
DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name, const std::string &id) : BaseStack(), layout(layout), name(name), id(id) {
246
auto empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
247
this->add(*empty_child, "");
248
// Add the stack to a layout manager
249
this->layout->add_stack(this);
250
251
// Hide the stack when no child is visible
252
this->property_visible_child_name().signal_changed().connect([this]() {
253
if(last_pane) {
254
last_pane->signal_pane_hidden.emit();
255
}
256
if(this->get_visible_child_name() == "") {
257
this->hide();
258
} else {
259
this->show();
260
}
261
last_pane = dynamic_cast<DockablePane*>(this->get_visible_child());
262
if(last_pane) {
263
last_pane->signal_pane_shown.emit();
264
}
265
});
266
267
// Also hide when the visible child is removed
268
this->signal_child_removed.connect([this](Gtk::Widget* const &child) {
269
if(this->get_visible_child_name() == "") {
270
last_pane = nullptr;
271
this->hide();
272
}
273
});
274
275
this->set_visible_child("");
276
this->hide();
277
}
278
279
DockWindow::DockWindow(DockablePane *pane) {
280
this->pane = pane;
281
this->set_child(*pane);
282
// Attempting to close the window should redock the pane so it doesn't vanish
283
this->signal_close_request().connect([this]() {
284
this->pane->redock(this->pane->last_stack);
285
return true;
286
}, false);
287
}
288
289
DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
290
auto update_callback = [this](Gtk::Widget*) {
291
this->update_buttons();
292
};
293
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
294
stack->signal_child_added.connect(update_callback);
295
stack->signal_child_removed.connect(update_callback);
296
this->get_style_context()->add_class("gpanthera-dock-switcher");
297
drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
298
this->add_controller(drop_target);
299
// Process dropped buttons
300
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
301
const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get();
302
303
if(widget) {
304
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
305
this->stack->add_pane(*pane);
306
}
307
}
308
309
return true; // Drop OK
310
}, false);
311
}
312
313
DockStack *DockStackSwitcher::get_stack() const {
314
return stack;
315
}
316
317
DockStackSwitcher::~DockStackSwitcher() {
318
add_handler.disconnect();
319
remove_handler.disconnect();
320
}
321
322
DockButton::DockButton(DockablePane *pane) : Gtk::ToggleButton(), pane(pane) {
323
if(pane->get_icon()) {
324
if(auto icon_copy = copy_image(pane->get_icon())) {
325
this->set_child(*icon_copy);
326
}
327
}
328
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
329
this->set_tooltip_text(pane->get_label()->get_text());
330
this->set_halign(Gtk::Align::CENTER);
331
this->set_valign(Gtk::Align::CENTER);
332
this->get_style_context()->add_class("toggle");
333
this->get_style_context()->add_class("gpanthera-dock-button");
334
// Add/remove CSS classes when the pane is shown/hidden
335
active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
336
this->update_active_style();
337
});
338
drag_source = Gtk::DragSource::create();
339
drag_source->set_exclusive(false);
340
// This is to prevent the click handler from taking over grabbing the button
341
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
342
value.init(DockablePane::get_type());
343
value.set(pane);
344
// Add the drag source to the button
345
this->add_controller(drag_source);
346
this->signal_clicked().connect([this, pane]() {
347
if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) {
348
pane->get_stack()->set_visible_child("");
349
} else {
350
pane->get_stack()->set_visible_child(pane->get_identifier());
351
}
352
});
353
// Provide the drag data
354
drag_source->signal_prepare().connect([this](double, double) {
355
drag_source->set_actions(Gdk::DragAction::MOVE);
356
auto const paintable = Gtk::WidgetPaintable::create();
357
paintable->set_widget(*this);
358
drag_source->set_icon(paintable->get_current_image(), 0, 0);
359
return Gdk::ContentProvider::create(value);
360
}, false);
361
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
362
this->set_opacity(0);
363
}, false);
364
// Pop out if dragged to an external location
365
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
366
if(reason == Gdk::DragCancelReason::NO_TARGET) {
367
this->pane->pop_out();
368
return true;
369
}
370
this->set_opacity(1);
371
return false;
372
}, false);
373
// Add a drop target to the button
374
auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
375
drop_target->set_actions(Gdk::DragAction::MOVE);
376
drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
377
this->add_controller(drop_target);
378
// Process dropped buttons by inserting them after the current button
379
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
380
const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get();
381
382
if(widget) {
383
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
384
if(pane->layout != this->pane->layout) {
385
// If the pane is not in the same layout manager, reject
386
return false;
387
}
388
auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent());
389
if(switcher) {
390
auto *stack = switcher->get_stack();
391
// Move the button to the new position
392
pane->redock(stack);
393
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
394
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
395
pane->insert_before(*stack, *this->pane);
396
} else {
397
pane->insert_after(*stack, *this->pane);
398
}
399
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
400
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
401
pane->insert_before(*stack, *this->pane);
402
} else {
403
pane->insert_after(*stack, *this->pane);
404
}
405
}
406
407
switcher->update_buttons();
408
}
409
}
410
}
411
412
return true; // Drop OK
413
}, false);
414
this->update_active_style();
415
}
416
417
void DockButton::update_active_style() {
418
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
419
this->add_css_class("checked");
420
this->add_css_class("gpanthera-dock-button-active");
421
this->set_active(true);
422
} else {
423
this->remove_css_class("checked");
424
this->remove_css_class("gpanthera-dock-button-active");
425
this->set_active(false);
426
}
427
}
428
429
DockButton::~DockButton() {
430
active_style_handler.disconnect();
431
}
432
433
void DockStackSwitcher::update_buttons() {
434
// Clear the old buttons
435
auto old_buttons = collect_children(*this);
436
for(auto *button : old_buttons) {
437
remove(*button);
438
}
439
DockButton* first_child = nullptr;
440
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
441
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
442
auto *button = Gtk::make_managed<DockButton>(pane);
443
if(!first_child) {
444
first_child = button;
445
} else {
446
button->set_group(*first_child);
447
}
448
if(pane->get_identifier() != Glib::ustring("")) {
449
append(*button);
450
}
451
}
452
}
453
}
454
455
void DockStack::add_pane(DockablePane &child) {
456
child.redock(this);
457
}
458
459
void ContentManager::add_stack(ContentStack *stack) {
460
this->stacks.push_back(stack);
461
}
462
463
void ContentManager::remove_stack(ContentStack *stack) {
464
this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end());
465
}
466
467
void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) {
468
Gtk::Stack::add(child, name);
469
signal_child_added.emit(&child);
470
}
471
472
void BaseStack::add(Gtk::Widget &child) {
473
Gtk::Stack::add(child);
474
signal_child_added.emit(&child);
475
}
476
477
void BaseStack::remove(Gtk::Widget &child) {
478
Gtk::Stack::remove(child);
479
signal_child_removed.emit(&child);
480
}
481
482
void ContentStack::disable_children() {
483
auto children = collect_children(*this);
484
for(auto *child : children) {
485
child->set_can_target(false);
486
}
487
}
488
489
void ContentStack::enable_children() {
490
auto children = collect_children(*this);
491
for(auto *child : children) {
492
child->set_can_target(true);
493
}
494
}
495
496
ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager, std::function<bool(ContentPage*)> detach_handler)
497
: BaseStack(), content_manager(content_manager) {
498
this->set_name("gpanthera_content_stack");
499
this->content_manager->add_stack(this);
500
if(detach_handler) {
501
this->detach_handler = detach_handler;
502
this->signal_detach.connect([this](ContentPage *widget) {
503
return this->detach_handler(widget);
504
});
505
}
506
507
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
508
this->add_controller(drop_target);
509
// Some widgets (such as webviews) will consume anything dropped on their ancestors. We have
510
// to disable all child widgets to ensure the drop can be caught by this widget.
511
drop_target->signal_enter().connect([this](double x, double y) {
512
disable_children();
513
return Gdk::DragAction::MOVE;
514
}, false);
515
drop_target->signal_leave().connect([this]() {
516
enable_children();
517
}, false);
518
// Process dropped buttons
519
drop_target->signal_drop().connect([this, content_manager](const Glib::ValueBase &value, double x, double y) {
520
enable_children();
521
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
522
523
if(auto page = dynamic_cast<ContentPage*>(widget)) {
524
if(page->get_stack() == this && !this->get_first_child()->get_next_sibling()) {
525
// Don't allow splitting if there are no more pages
526
return false;
527
}
528
if(!(page->content_manager == this->content_manager)) {
529
// If the page is not in the same content manager, reject
530
return false;
531
}
532
double width = this->get_allocated_width(), height = this->get_allocated_height();
533
// Split based on the drop position
534
if(!(x < width / 4 || x > width * 3 / 4 || y < height / 4 || y > height * 3 / 4)) {
535
if(page->get_stack() == this) {
536
return false;
537
}
538
page->lose_visibility();
539
// If the drop position is not at a quarter to the edges, move to the same stack
540
this->add_page(*page);
541
return true;
542
}
543
page->lose_visibility();
544
auto new_stack = Gtk::make_managed<ContentStack>(this->content_manager, this->detach_handler);
545
auto this_notebook = dynamic_cast<ContentNotebook*>(this->get_parent());
546
auto new_switcher = Gtk::make_managed<ContentTabBar>(new_stack, this_notebook ? this_notebook->get_switcher()->get_orientation() : Gtk::Orientation::HORIZONTAL, this_notebook->get_switcher()->get_extra_child_function());
547
auto new_notebook = Gtk::make_managed<ContentNotebook>(new_stack, new_switcher, this_notebook ? this_notebook->get_tab_position() : Gtk::PositionType::TOP);
548
new_stack->add_page(*page);
549
new_stack->set_visible_child(*page);
550
if(x < width / 4) {
551
this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::START);
552
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
553
paned->set_start_child(*new_notebook);
554
}
555
} else if(x > width * 3 / 4) {
556
this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::END);
557
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
558
paned->set_end_child(*new_notebook);
559
}
560
} else if(y < height / 4) {
561
this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::START);
562
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
563
paned->set_start_child(*new_notebook);
564
}
565
} else if(y > height * 3 / 4) {
566
this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::END);
567
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
568
paned->set_end_child(*new_notebook);
569
}
570
}
571
content_manager->set_last_operated_page(page);
572
}
573
574
return true; // Drop OK
575
}, false);
576
}
577
578
std::function<bool(ContentPage*)> ContentStack::get_detach_handler() const {
579
return this->detach_handler;
580
}
581
582
ContentNotebook::ContentNotebook(ContentStack *stack, ContentTabBar *switcher, Gtk::PositionType tab_position) : Gtk::Box(), stack(stack), switcher(switcher), tab_position(tab_position) {
583
this->append(*stack);
584
if(tab_position == Gtk::PositionType::TOP || tab_position == Gtk::PositionType::BOTTOM) {
585
this->set_orientation(Gtk::Orientation::VERTICAL);
586
if(tab_position == Gtk::PositionType::TOP) {
587
this->prepend(*switcher);
588
switcher->add_css_class("top");
589
} else if(tab_position == Gtk::PositionType::BOTTOM) {
590
this->append(*switcher);
591
switcher->add_css_class("bottom");
592
}
593
} else if(tab_position == Gtk::PositionType::LEFT || tab_position == Gtk::PositionType::RIGHT) {
594
this->set_orientation(Gtk::Orientation::HORIZONTAL);
595
if(tab_position == Gtk::PositionType::LEFT) {
596
this->prepend(*switcher);
597
switcher->add_css_class("left");
598
} else if(tab_position == Gtk::PositionType::RIGHT) {
599
this->append(*switcher);
600
switcher->add_css_class("right");
601
}
602
}
603
this->add_css_class("gpanthera-content-notebook");
604
}
605
606
void ContentStack::make_paned(Gtk::Orientation orientation, Gtk::PackType pack_type) {
607
auto *parent = this->get_parent();
608
if(auto notebook = dynamic_cast<ContentNotebook*>(parent)) {
609
auto *paned = Gtk::make_managed<Gtk::Paned>(orientation);
610
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(notebook->get_parent())) {
611
if(parent_paned->get_start_child() == notebook) {
612
notebook->unparent();
613
parent_paned->set_start_child(*paned);
614
} else if(parent_paned->get_end_child() == notebook) {
615
notebook->unparent();
616
parent_paned->set_end_child(*paned);
617
}
618
} else if(auto box = dynamic_cast<Gtk::Box*>(notebook->get_parent())) {
619
auto previous_child = notebook->get_prev_sibling();
620
box->remove(*notebook);
621
if(previous_child) {
622
paned->insert_after(*box, *previous_child);
623
} else {
624
box->prepend(*paned);
625
}
626
} else if(auto window = dynamic_cast<Gtk::Window*>(notebook->get_parent())) {
627
window->unset_child();
628
window->set_child(*paned);
629
}
630
if(pack_type == Gtk::PackType::START) {
631
paned->set_end_child(*notebook);
632
} else if(pack_type == Gtk::PackType::END) {
633
paned->set_start_child(*notebook);
634
}
635
}
636
}
637
638
ContentTabBar::ContentTabBar(ContentStack *stack, Gtk::Orientation orientation, std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> extra_child_function) : Gtk::Box(orientation), stack(stack), extra_child_function(extra_child_function) {
639
this->get_style_context()->add_class("gpanthera-content-tab-bar");
640
this->set_margin_top(0);
641
this->set_margin_bottom(0);
642
this->set_margin_start(0);
643
this->set_margin_end(0);
644
645
auto update_callback = [this](Gtk::Widget*) {
646
this->update_buttons();
647
};
648
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
649
stack->signal_child_added.connect(update_callback);
650
stack->signal_child_removed.connect(update_callback);
651
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
652
this->add_controller(drop_target);
653
// Process dropped buttons
654
drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) {
655
if(const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get()) {
656
if(auto page = dynamic_cast<ContentPage*>(widget)) {
657
if(page->get_stack() == this->stack) {
658
this->stack->remove(*page);
659
this->stack->add(*page);
660
} else {
661
this->stack->add_page(*page);
662
}
663
}
664
}
665
666
return true; // Drop OK
667
}, false);
668
669
scrolled_window = Gtk::make_managed<Gtk::ScrolledWindow>();
670
scrolled_window->add_css_class("gpanthera-content-tab-bar-scroll");
671
auto viewport = Gtk::make_managed<Gtk::Viewport>(nullptr, nullptr);
672
viewport->add_css_class("gpanthera-content-tab-bar-viewport");
673
tab_box = Gtk::make_managed<Gtk::Box>(orientation);
674
tab_box->add_css_class("gpanthera-content-tab-box");
675
this->prepend(*scrolled_window);
676
scrolled_window->set_child(*viewport);
677
viewport->set_child(*tab_box);
678
679
this->set_orientation(orientation);
680
681
if(this->extra_child_function) {
682
this->append(*this->extra_child_function(this));
683
}
684
}
685
686
std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> ContentTabBar::get_extra_child_function() const {
687
return this->extra_child_function;
688
}
689
690
void ContentTabBar::set_extra_child_function(std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> extra_child_function) {
691
// Note that this doesn't remove the existing extra child
692
this->extra_child_function = extra_child_function;
693
}
694
695
void ContentTabBar::set_orientation(Gtk::Orientation orientation) {
696
this->Gtk::Box::set_orientation(orientation);
697
if(orientation == Gtk::Orientation::HORIZONTAL) {
698
scrolled_window->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::NEVER);
699
tab_box->set_orientation(Gtk::Orientation::HORIZONTAL);
700
scrolled_window->set_hexpand(true);
701
scrolled_window->set_vexpand(false);
702
} else if(orientation == Gtk::Orientation::VERTICAL) {
703
scrolled_window->set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
704
tab_box->set_orientation(Gtk::Orientation::VERTICAL);
705
scrolled_window->set_vexpand(true);
706
scrolled_window->set_hexpand(false);
707
}
708
}
709
710
ContentTabBar::~ContentTabBar() {
711
add_handler.disconnect();
712
remove_handler.disconnect();
713
}
714
715
ContentStack *ContentTabBar::get_stack() const {
716
return stack;
717
}
718
719
void ContentTabBar::update_buttons() {
720
// Clear the old buttons
721
auto old_buttons = collect_children(*this->tab_box);
722
for(auto *button : old_buttons) {
723
if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) {
724
button_button->unset_child();
725
tab_box->remove(*button);
726
}
727
}
728
ContentTab* first_child = nullptr;
729
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
730
if(auto page = dynamic_cast<ContentPage*>(widget)) {
731
auto *button = Gtk::make_managed<ContentTab>(page);
732
if(!first_child) {
733
first_child = button;
734
} else {
735
button->set_group(*first_child);
736
}
737
this->tab_box->append(*button);
738
}
739
}
740
}
741
742
void ContentStack::add_page(ContentPage &child) {
743
child.redock(this);
744
}
745
746
void ContentStack::remove_with_paned() {
747
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
748
Gtk::Widget *child = nullptr;
749
if(this->get_parent() == paned->get_start_child()) {
750
child = paned->get_end_child();
751
} else if(this->get_parent() == paned->get_end_child()) {
752
child = paned->get_start_child();
753
} else {
754
return;
755
}
756
757
g_object_ref(child->gobj()); // Prevent the child from being automatically deleted
758
paned->property_start_child().reset_value(); // Some hacks because the Gtkmm API isn't complete; it doesn't have an unset_start_child() or unset_end_child()
759
paned->property_end_child().reset_value();
760
761
if(child) {
762
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(paned->get_parent())) {
763
if(parent_paned->get_start_child() == paned) {
764
parent_paned->set_start_child(*child);
765
} else if(parent_paned->get_end_child() == paned) {
766
parent_paned->set_end_child(*child);
767
}
768
} else if(auto box = dynamic_cast<Gtk::Box*>(paned->get_parent())) {
769
child->insert_after(*box, *paned);
770
paned->unparent();
771
g_object_unref(child->gobj());
772
} else if(auto window = dynamic_cast<Gtk::Window*>(paned->get_parent())) {
773
window->set_child(*child);
774
g_object_unref(child->gobj());
775
}
776
// Focus the visible page of the former pane's other child
777
if(auto stack = dynamic_cast<ContentStack*>(child->get_last_child())) {
778
if(auto page = dynamic_cast<ContentPage*>(stack->get_visible_child())) {
779
content_manager->set_last_operated_page(page);
780
}
781
}
782
}
783
}
784
}
785
786
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
787
this->set_child(*page->get_tab_widget());
788
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
789
this->set_halign(Gtk::Align::CENTER);
790
this->set_valign(Gtk::Align::CENTER);
791
this->get_style_context()->add_class("toggle");
792
this->get_style_context()->add_class("gpanthera-content-tab");
793
// Add/remove CSS classes when the pane is shown/hidden
794
active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() {
795
this->update_active_style();
796
});
797
drag_source = Gtk::DragSource::create();
798
drag_source->set_exclusive(false);
799
// This is to prevent the click handler from taking over grabbing the button
800
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
801
value.init(ContentPage::get_type());
802
value.set(page);
803
// Add the drag source to the button
804
this->add_controller(drag_source);
805
this->signal_clicked().connect([this, page]() {
806
page->get_stack()->set_visible_child(*page);
807
page->content_manager->set_last_operated_page(page);
808
update_active_style();
809
});
810
// Switch tabs on depress
811
auto gesture_click = Gtk::GestureClick::create();
812
gesture_click->set_button(1);
813
this->add_controller(gesture_click);
814
gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) {
815
page->get_stack()->set_visible_child(*page);
816
page->content_manager->set_last_operated_page(page);
817
update_active_style();
818
});
819
// Provide the drag data
820
drag_source->signal_prepare().connect([this](double, double) {
821
drag_source->set_actions(Gdk::DragAction::MOVE);
822
auto const paintable = Gtk::WidgetPaintable::create();
823
paintable->set_widget(*this);
824
drag_source->set_icon(paintable->get_current_image(), 0, 0);
825
return Gdk::ContentProvider::create(value);
826
}, false);
827
update_active_style();
828
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
829
this->set_opacity(0);
830
}, false);
831
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
832
// Process dropped buttons by inserting them after the current button
833
drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) {
834
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
835
836
if(widget) {
837
if(auto page = dynamic_cast<ContentPage*>(widget)) {
838
if(page->content_manager != this->page->content_manager) {
839
// If the pane is not in the same layout manager, reject
840
return false;
841
}
842
auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent()->get_parent()->get_parent()->get_parent());
843
if(switcher) {
844
auto *stack = switcher->get_stack();
845
// Move the button to the new position
846
page->redock(stack);
847
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
848
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
849
page->insert_before(*stack, *this->page);
850
} else {
851
page->insert_after(*stack, *this->page);
852
}
853
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
854
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
855
page->insert_before(*stack, *this->page);
856
} else {
857
page->insert_after(*stack, *this->page);
858
}
859
}
860
861
switcher->update_buttons();
862
}
863
}
864
}
865
866
return true; // Drop OK
867
}, false);
868
this->add_controller(drop_target);
869
// Pop out if dragged to an external location
870
drag_cancel_handler = drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
871
if(reason == Gdk::DragCancelReason::NO_TARGET) {
872
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
873
bool result = stack->signal_detach.emit(this->page);
874
if(!result) {
875
this->set_opacity(1);
876
}
877
return result;
878
}
879
this->set_opacity(1);
880
return false;
881
}, false);
882
drag_end_handler = drag_source->signal_drag_end().connect([this](const Glib::RefPtr<Gdk::Drag>&, bool drop_ok) {
883
if(drop_ok) {
884
this->set_opacity(1);
885
}
886
}, false);
887
888
// Provide a context menu
889
context_menu = Gio::Menu::create();
890
auto action_group = Gio::SimpleActionGroup::create();
891
this->insert_action_group("win", action_group);
892
893
auto close_action = Gio::SimpleAction::create("close");
894
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
895
this->close();
896
});
897
action_group->add_action(close_action);
898
context_menu->append(_("Close"), "win.close");
899
900
auto close_all_action = Gio::SimpleAction::create("close-all");
901
close_all_action->signal_activate().connect([this](const Glib::VariantBase&) {
902
for(auto page : collect_children(*this->page->get_stack())) {
903
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
904
if(!content_page->signal_close.emit()) {
905
content_page->redock(nullptr);
906
}
907
}
908
}
909
});
910
action_group->add_action(close_all_action);
911
context_menu->append(_("Close all"), "win.close-all");
912
913
auto close_others_action = Gio::SimpleAction::create("close-others");
914
close_others_action->signal_activate().connect([this](const Glib::VariantBase&) {
915
for(auto page : collect_children(*this->page->get_stack())) {
916
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
917
if(content_page != this->page && !content_page->signal_close.emit()) {
918
content_page->redock(nullptr);
919
}
920
}
921
}
922
});
923
action_group->add_action(close_others_action);
924
context_menu->append(_("Close others"), "win.close-others");
925
926
auto detach_action = Gio::SimpleAction::create("detach");
927
detach_action->signal_activate().connect([this](const Glib::VariantBase&) {
928
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
929
bool result = stack->signal_detach.emit(this->page);
930
});
931
action_group->add_action(detach_action);
932
context_menu->append(_("New window"), "win.detach");
933
934
// TODO: Add more actions: "New window", "Split left", "Split right", "Split top", "Split bottom", "Close all", "Close others", "Close to the right", "Close to the left"
935
this->insert_action_group("win", action_group);
936
// Attach the context menu to the button
937
auto context_menu_signal = add_context_menu(*this);
938
popover = Gtk::make_managed<Gtk::PopoverMenu>();
939
popover->set_menu_model(context_menu);
940
popover->set_parent(*this);
941
popover->set_has_arrow(false);
942
popover->set_halign(Gtk::Align::START);
943
context_menu_signal.connect([this, action_group](double x, double y) {
944
if(this->context_menu) {
945
popover->set_pointing_to(Gdk::Rectangle(x, y, 1, 1));
946
popover->popup();
947
}
948
});
949
auto middle_click_controller = Gtk::GestureClick::create();
950
middle_click_controller->set_button(2);
951
middle_click_controller->signal_released().connect([this](int num_presses, double x, double y) {
952
this->close();
953
});
954
this->add_controller(middle_click_controller);
955
control_status_handler = this->page->signal_control_status_changed.connect([this](bool operating) {
956
if(operating) {
957
this->add_css_class("gpanthera-selected-tab");
958
} else {
959
this->remove_css_class("gpanthera-selected-tab");
960
}
961
});
962
}
963
964
void ContentTab::close() {
965
page->close();
966
}
967
968
void ContentPage::close() {
969
if(!this->signal_close.emit()) {
970
if(this->get_next_sibling()) {
971
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling()));
972
this->get_stack()->set_visible_child(*this->get_next_sibling());
973
} else if(this->get_prev_sibling()) {
974
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling()));
975
this->get_stack()->set_visible_child(*this->get_prev_sibling());
976
}
977
this->redock(nullptr);
978
content_manager->signal_page_closed.emit(this);
979
}
980
}
981
982
void ContentTab::update_active_style() {
983
if(this->page->get_stack()->get_visible_child() == this->page) {
984
this->add_css_class("checked");
985
this->add_css_class("gpanthera-dock-button-active");
986
this->set_active(true);
987
} else {
988
this->remove_css_class("checked");
989
this->remove_css_class("gpanthera-dock-button-active");
990
this->set_active(false);
991
}
992
}
993
994
ContentTab::~ContentTab() {
995
active_style_handler.disconnect();
996
drag_end_handler.disconnect();
997
drag_cancel_handler.disconnect();
998
control_status_handler.disconnect();
999
}
1000
1001
Gtk::Widget *ContentPage::get_tab_widget() const {
1002
return this->tab_widget;
1003
}
1004
1005
ContentStack *ContentPage::get_stack() const {
1006
return this->stack;
1007
}
1008
1009
void ContentPage::lose_visibility() {
1010
if(this->get_next_sibling()) {
1011
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling()));
1012
this->get_stack()->set_visible_child(*this->get_next_sibling());
1013
} else if(this->get_prev_sibling()) {
1014
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling()));
1015
this->get_stack()->set_visible_child(*this->get_prev_sibling());
1016
}
1017
}
1018
1019
void ContentPage::redock(ContentStack *stack) {
1020
if(stack == nullptr) {
1021
content_manager->signal_page_closing.emit(this);
1022
if(this->stack) {
1023
this->stack->remove(*this);
1024
/*
1025
The stack may be removed, for example, if the application closes its window if
1026
it was the sole stack inside. Thus, it must be re-checked.
1027
*/
1028
if(this->stack && dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && !this->stack->get_first_child()) {
1029
this->stack->remove_with_paned();
1030
}
1031
}
1032
auto old_stack = this->stack;
1033
if(old_stack) {
1034
if(!old_stack->get_first_child()) {
1035
old_stack->signal_leave_empty.emit();
1036
}
1037
}
1038
this->stack = nullptr;
1039
this->last_stack = nullptr;
1040
return;
1041
}
1042
// Check if the stack is now empty, in which case we should remove it
1043
if(this->stack == stack) {
1044
return;
1045
}
1046
content_manager->signal_page_moving.emit(this);
1047
if(this->stack != nullptr) {
1048
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()))) {
1049
this->stack->remove(*this);
1050
this->stack->remove_with_paned();
1051
} else if(this->get_parent() == this->stack) {
1052
this->stack->remove(*this);
1053
}
1054
}
1055
auto old_stack = this->stack;
1056
this->stack = stack;
1057
this->last_stack = stack;
1058
this->stack->add(*this);
1059
if(old_stack) {
1060
if(!old_stack->get_first_child()) {
1061
old_stack->signal_leave_empty.emit();
1062
}
1063
}
1064
content_manager->signal_page_moved.emit(this);
1065
}
1066
1067
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
1068
Gtk::Overlay(), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) {
1069
this->set_name("gpanthera_content_page");
1070
this->set_child(*child);
1071
this->set_tab_widget(tab_widget);
1072
this->set_margin_top(0);
1073
this->set_margin_bottom(0);
1074
this->set_margin_start(0);
1075
this->set_margin_end(0);
1076
if(stack) {
1077
stack->add_page(*this);
1078
this->content_manager->add_stack(this->stack);
1079
}
1080
auto click_controller = Gtk::GestureClick::create();
1081
click_controller->set_button(0);
1082
click_controller->signal_pressed().connect([this](int num_presses, double x, double y) {
1083
this->content_manager->set_last_operated_page(this);
1084
});
1085
this->property_has_focus().signal_changed().connect([this]() {
1086
if(this->property_has_focus()) {
1087
this->content_manager->set_last_operated_page(this);
1088
}
1089
});
1090
click_controller->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
1091
this->add_controller(click_controller);
1092
}
1093
1094
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
1095
}
1096
1097
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
1098
this->tab_widget = tab_widget;
1099
}
1100
1101
ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) {
1102
this->set_child(*notebook);
1103
this->set_decorated(true);
1104
this->set_resizable(true);
1105
}
1106
1107
Gtk::PositionType ContentNotebook::get_tab_position() const {
1108
return this->tab_position;
1109
}
1110
1111
ContentTabBar *ContentNotebook::get_switcher() const {
1112
return this->switcher;
1113
}
1114
1115
ContentStack *ContentNotebook::get_stack() const {
1116
return this->stack;
1117
}
1118
1119
ContentPage *ContentManager::get_last_operated_page() const {
1120
return this->last_operated_page;
1121
}
1122
1123
void ContentManager::set_last_operated_page(ContentPage *page) {
1124
if(this->last_operated_page) {
1125
this->last_operated_page->signal_control_status_changed.emit(false);
1126
}
1127
this->last_operated_page = page;
1128
if(page) {
1129
page->signal_control_status_changed.emit(true);
1130
this->signal_page_operated.emit(page);
1131
}
1132
}
1133
1134
DockWindow *DockablePane::get_window() const {
1135
return this->window;
1136
}
1137
1138
std::string LayoutManager::get_layout_as_json() const {
1139
nlohmann::json json;
1140
std::unordered_set<DockablePane*> panes_set;
1141
for(auto pane: panes) {
1142
panes_set.insert(pane);
1143
}
1144
for(auto stack: stacks) {
1145
for(auto pane: collect_children(*stack)) {
1146
if(dynamic_cast<DockablePane*>(pane)) {
1147
panes_set.erase(dynamic_cast<DockablePane*>(pane));
1148
json[stack->id].push_back(dynamic_cast<DockablePane*>(pane)->get_identifier());
1149
}
1150
}
1151
}
1152
for(auto pane: panes_set) {
1153
if(pane->get_window()) {
1154
json[""].push_back(pane->get_identifier());
1155
}
1156
}
1157
return json.dump();
1158
}
1159
1160
void LayoutManager::restore_json_layout(const std::string &json_string) {
1161
nlohmann::json json = nlohmann::json::parse(json_string);
1162
for(auto stack: stacks) {
1163
if(json.contains(stack->id)) {
1164
for(auto const &pane_id: json[stack->id]) {
1165
for(auto pane: panes) {
1166
// TODO: could probably be done with a better complexity if there are
1167
// many panes
1168
if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) {
1169
stack->add_pane(*pane);
1170
break;
1171
}
1172
}
1173
}
1174
}
1175
}
1176
// Process popped-out panes
1177
for(auto const &pane_id: json[""]) {
1178
for(auto pane: panes) {
1179
// TODO: could probably be done with a better complexity if there are
1180
// many panes
1181
if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) {
1182
pane->pop_out();
1183
break;
1184
}
1185
}
1186
}
1187
}
1188
} // namespace gPanthera
1189