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