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