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.16 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) 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
if(page->get_parent() == this->stack) {
637
this->stack->remove(*page);
638
}
639
this->stack->add(*page);
640
}
641
}
642
643
return true; // Drop OK
644
}, false);
645
646
scrolled_window = Gtk::make_managed<Gtk::ScrolledWindow>();
647
auto viewport = Gtk::make_managed<Gtk::Viewport>(nullptr, nullptr);
648
tab_box = Gtk::make_managed<Gtk::Box>(orientation);
649
this->prepend(*scrolled_window);
650
scrolled_window->set_child(*viewport);
651
viewport->set_child(*tab_box);
652
653
this->set_orientation(orientation);
654
655
if(this->extra_child_function) {
656
this->append(*this->extra_child_function(this));
657
}
658
}
659
660
std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> ContentTabBar::get_extra_child_function() const {
661
return this->extra_child_function;
662
}
663
664
void ContentTabBar::set_extra_child_function(std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> extra_child_function) {
665
// Note that this doesn't remove the existing extra child
666
this->extra_child_function = extra_child_function;
667
}
668
669
void ContentTabBar::set_orientation(Gtk::Orientation orientation) {
670
this->Gtk::Box::set_orientation(orientation);
671
if(orientation == Gtk::Orientation::HORIZONTAL) {
672
scrolled_window->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::NEVER);
673
tab_box->set_orientation(Gtk::Orientation::HORIZONTAL);
674
scrolled_window->set_hexpand(true);
675
scrolled_window->set_vexpand(false);
676
} else if(orientation == Gtk::Orientation::VERTICAL) {
677
scrolled_window->set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
678
tab_box->set_orientation(Gtk::Orientation::VERTICAL);
679
scrolled_window->set_vexpand(true);
680
scrolled_window->set_hexpand(false);
681
}
682
}
683
684
ContentTabBar::~ContentTabBar() {
685
add_handler.disconnect();
686
remove_handler.disconnect();
687
}
688
689
ContentStack *ContentTabBar::get_stack() const {
690
return stack;
691
}
692
693
void ContentTabBar::update_buttons() {
694
// Clear the old buttons
695
auto old_buttons = collect_children(*this->tab_box);
696
for(auto *button : old_buttons) {
697
if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) {
698
button_button->unset_child();
699
tab_box->remove(*button);
700
}
701
}
702
ContentTab* first_child = nullptr;
703
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
704
if(auto page = dynamic_cast<ContentPage*>(widget)) {
705
auto *button = Gtk::make_managed<ContentTab>(page);
706
if(!first_child) {
707
first_child = button;
708
} else {
709
button->set_group(*first_child);
710
}
711
this->tab_box->append(*button);
712
}
713
}
714
}
715
716
void ContentStack::add_page(ContentPage &child) {
717
child.redock(this);
718
}
719
720
void ContentStack::remove_with_paned() {
721
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
722
Gtk::Widget *child = nullptr;
723
if(this->get_parent() == paned->get_start_child()) {
724
child = paned->get_end_child();
725
} else if(this->get_parent() == paned->get_end_child()) {
726
child = paned->get_start_child();
727
} else {
728
return;
729
}
730
731
g_object_ref(child->gobj()); // Prevent the child from being automatically deleted
732
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()
733
paned->property_end_child().reset_value();
734
735
if(child) {
736
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(paned->get_parent())) {
737
if(parent_paned->get_start_child() == paned) {
738
parent_paned->set_start_child(*child);
739
} else if(parent_paned->get_end_child() == paned) {
740
parent_paned->set_end_child(*child);
741
}
742
} else if(auto box = dynamic_cast<Gtk::Box*>(paned->get_parent())) {
743
child->insert_after(*box, *paned);
744
paned->unparent();
745
g_object_unref(child->gobj());
746
}
747
}
748
}
749
}
750
751
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
752
this->set_child(*page->get_tab_widget());
753
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
754
this->set_halign(Gtk::Align::CENTER);
755
this->set_valign(Gtk::Align::CENTER);
756
this->get_style_context()->add_class("toggle");
757
this->get_style_context()->add_class("gpanthera-content-tab");
758
// Add/remove CSS classes when the pane is shown/hidden
759
active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() {
760
this->update_active_style();
761
});
762
drag_source = Gtk::DragSource::create();
763
drag_source->set_exclusive(false);
764
// This is to prevent the click handler from taking over grabbing the button
765
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
766
value.init(ContentPage::get_type());
767
value.set(page);
768
// Add the drag source to the button
769
this->add_controller(drag_source);
770
this->signal_clicked().connect([this, page]() {
771
page->get_stack()->set_visible_child(*page);
772
page->content_manager->set_last_operated_page(page);
773
update_active_style();
774
});
775
// Switch tabs on depress
776
auto gesture_click = Gtk::GestureClick::create();
777
gesture_click->set_button(1);
778
this->add_controller(gesture_click);
779
gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) {
780
page->get_stack()->set_visible_child(*page);
781
page->content_manager->set_last_operated_page(page);
782
update_active_style();
783
});
784
// Provide the drag data
785
drag_source->signal_prepare().connect([this](double, double) {
786
drag_source->set_actions(Gdk::DragAction::MOVE);
787
auto const paintable = Gtk::WidgetPaintable::create();
788
paintable->set_widget(*this);
789
drag_source->set_icon(paintable->get_current_image(), 0, 0);
790
return Gdk::ContentProvider::create(value);
791
}, false);
792
update_active_style();
793
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
794
this->set_opacity(0);
795
}, false);
796
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
797
// Process dropped buttons by inserting them after the current button
798
drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) {
799
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
800
801
if(widget) {
802
if(auto page = dynamic_cast<ContentPage*>(widget)) {
803
if(page->content_manager != this->page->content_manager) {
804
// If the pane is not in the same layout manager, reject
805
return false;
806
}
807
auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent()->get_parent()->get_parent()->get_parent());
808
if(switcher) {
809
auto *stack = switcher->get_stack();
810
// Move the button to the new position
811
page->redock(stack);
812
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
813
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
814
page->insert_before(*stack, *this->page);
815
} else {
816
page->insert_after(*stack, *this->page);
817
}
818
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
819
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
820
page->insert_before(*stack, *this->page);
821
} else {
822
page->insert_after(*stack, *this->page);
823
}
824
}
825
826
switcher->update_buttons();
827
}
828
}
829
}
830
831
return true; // Drop OK
832
}, false);
833
this->add_controller(drop_target);
834
// Pop out if dragged to an external location
835
drag_cancel_handler = drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
836
if(reason == Gdk::DragCancelReason::NO_TARGET) {
837
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
838
bool result = stack->signal_detach.emit(this->page);
839
if(!result) {
840
this->set_opacity(1);
841
}
842
return result;
843
}
844
this->set_opacity(1);
845
return false;
846
}, false);
847
drag_end_handler = drag_source->signal_drag_end().connect([this](const Glib::RefPtr<Gdk::Drag>&, bool drop_ok) {
848
if(drop_ok) {
849
this->set_opacity(1);
850
}
851
}, false);
852
853
// Provide a context menu
854
context_menu = Gio::Menu::create();
855
auto action_group = Gio::SimpleActionGroup::create();
856
this->insert_action_group("win", action_group);
857
858
auto close_action = Gio::SimpleAction::create("close");
859
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
860
this->close();
861
});
862
action_group->add_action(close_action);
863
context_menu->append(_("Close"), "win.close");
864
865
auto close_all_action = Gio::SimpleAction::create("close-all");
866
close_all_action->signal_activate().connect([this](const Glib::VariantBase&) {
867
for(auto page : collect_children(*this->page->get_stack())) {
868
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
869
if(!content_page->signal_close.emit()) {
870
content_page->redock(nullptr);
871
}
872
}
873
}
874
});
875
action_group->add_action(close_all_action);
876
context_menu->append(_("Close all"), "win.close-all");
877
878
auto close_others_action = Gio::SimpleAction::create("close-others");
879
close_others_action->signal_activate().connect([this](const Glib::VariantBase&) {
880
for(auto page : collect_children(*this->page->get_stack())) {
881
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
882
if(content_page != this->page && !content_page->signal_close.emit()) {
883
content_page->redock(nullptr);
884
}
885
}
886
}
887
});
888
action_group->add_action(close_others_action);
889
context_menu->append(_("Close others"), "win.close-others");
890
891
auto detach_action = Gio::SimpleAction::create("detach");
892
detach_action->signal_activate().connect([this](const Glib::VariantBase&) {
893
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
894
bool result = stack->signal_detach.emit(this->page);
895
});
896
action_group->add_action(detach_action);
897
context_menu->append(_("New window"), "win.detach");
898
899
// 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"
900
this->insert_action_group("win", action_group);
901
// Attach the context menu to the button
902
auto context_menu_signal = add_context_menu(*this);
903
context_menu_signal.connect([this, action_group](double x, double y) {
904
if(this->context_menu) {
905
auto popover = Gtk::make_managed<Gtk::PopoverMenu>();
906
popover->set_menu_model(context_menu);
907
popover->set_parent(*this);
908
popover->set_has_arrow(false);
909
popover->set_halign(Gtk::Align::START);
910
popover->set_pointing_to(Gdk::Rectangle(x, y, 1, 1));
911
popover->popup();
912
}
913
});
914
auto middle_click_controller = Gtk::GestureClick::create();
915
middle_click_controller->set_button(2);
916
middle_click_controller->signal_released().connect([this](int num_presses, double x, double y) {
917
this->close();
918
});
919
this->add_controller(middle_click_controller);
920
}
921
922
void ContentTab::close() {
923
if(!this->page->signal_close.emit()) {
924
if(this->page->get_next_sibling()) {
925
this->page->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->page->get_next_sibling()));
926
this->page->get_stack()->set_visible_child(*this->page->get_next_sibling());
927
} else if(this->page->get_prev_sibling()) {
928
this->page->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->page->get_prev_sibling()));
929
this->page->get_stack()->set_visible_child(*this->page->get_prev_sibling());
930
}
931
this->page->redock(nullptr);
932
}
933
}
934
935
void ContentTab::update_active_style() {
936
if(this->page->get_stack()->get_visible_child() == this->page) {
937
this->add_css_class("checked");
938
this->add_css_class("gpanthera-dock-button-active");
939
this->set_active(true);
940
} else {
941
this->remove_css_class("checked");
942
this->remove_css_class("gpanthera-dock-button-active");
943
this->set_active(false);
944
}
945
}
946
947
ContentTab::~ContentTab() {
948
active_style_handler.disconnect();
949
drag_end_handler.disconnect();
950
drag_cancel_handler.disconnect();
951
}
952
953
Gtk::Widget *ContentPage::get_tab_widget() const {
954
return this->tab_widget;
955
}
956
957
ContentStack *ContentPage::get_stack() const {
958
return this->stack;
959
}
960
961
void ContentPage::lose_visibility() {
962
if(this->get_next_sibling()) {
963
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling()));
964
this->get_stack()->set_visible_child(*this->get_next_sibling());
965
} else if(this->get_prev_sibling()) {
966
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling()));
967
this->get_stack()->set_visible_child(*this->get_prev_sibling());
968
}
969
}
970
971
void ContentPage::redock(ContentStack *stack) {
972
if(stack == nullptr) {
973
if(this->stack) {
974
this->stack->remove(*this);
975
if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && !this->stack->get_first_child()) {
976
this->stack->remove_with_paned();
977
}
978
}
979
auto old_stack = this->stack;
980
if(old_stack) {
981
if(!old_stack->get_first_child()) {
982
old_stack->signal_leave_empty.emit();
983
}
984
}
985
this->stack = nullptr;
986
this->last_stack = nullptr;
987
return;
988
}
989
// Check if the stack is now empty, in which case we should remove it
990
if(this->stack == stack) {
991
return;
992
}
993
if(this->stack != nullptr) {
994
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()))) {
995
this->stack->remove(*this);
996
this->stack->remove_with_paned();
997
} else if(this->get_parent() == this->stack) {
998
this->stack->remove(*this);
999
}
1000
}
1001
auto old_stack = this->stack;
1002
this->stack = stack;
1003
this->last_stack = stack;
1004
this->stack->add(*this);
1005
if(old_stack) {
1006
if(!old_stack->get_first_child()) {
1007
old_stack->signal_leave_empty.emit();
1008
}
1009
}
1010
}
1011
1012
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
1013
Gtk::Overlay(), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) {
1014
this->set_name("gpanthera_content_page");
1015
this->set_child(*child);
1016
this->set_tab_widget(tab_widget);
1017
this->set_margin_top(0);
1018
this->set_margin_bottom(0);
1019
this->set_margin_start(0);
1020
this->set_margin_end(0);
1021
if(stack) {
1022
stack->add_page(*this);
1023
this->content_manager->add_stack(this->stack);
1024
}
1025
auto click_controller = Gtk::GestureClick::create();
1026
click_controller->set_button(0);
1027
click_controller->signal_pressed().connect([this](int num_presses, double x, double y) {
1028
this->content_manager->set_last_operated_page(this);
1029
});
1030
this->property_has_focus().signal_changed().connect([this]() {
1031
if(this->property_has_focus()) {
1032
this->content_manager->set_last_operated_page(this);
1033
}
1034
});
1035
click_controller->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
1036
this->add_controller(click_controller);
1037
}
1038
1039
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
1040
}
1041
1042
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
1043
this->tab_widget = tab_widget;
1044
}
1045
1046
ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) {
1047
this->set_child(*notebook);
1048
this->set_decorated(true);
1049
this->set_resizable(true);
1050
}
1051
1052
Gtk::PositionType ContentNotebook::get_tab_position() const {
1053
return this->tab_position;
1054
}
1055
1056
ContentTabBar *ContentNotebook::get_switcher() const {
1057
return this->switcher;
1058
}
1059
1060
ContentStack *ContentNotebook::get_stack() const {
1061
return this->stack;
1062
}
1063
1064
ContentPage *ContentManager::get_last_operated_page() const {
1065
return this->last_operated_page;
1066
}
1067
1068
void ContentManager::set_last_operated_page(ContentPage *page) {
1069
this->last_operated_page = page;
1070
this->signal_page_operated.emit(page);
1071
}
1072
1073
DockWindow *DockablePane::get_window() const {
1074
return this->window;
1075
}
1076
1077
std::string LayoutManager::get_layout_as_json() const {
1078
nlohmann::json json;
1079
std::unordered_set<DockablePane*> panes_set;
1080
for(auto pane: panes) {
1081
panes_set.insert(pane);
1082
}
1083
for(auto stack: stacks) {
1084
for(auto pane: collect_children(*stack)) {
1085
if(dynamic_cast<DockablePane*>(pane)) {
1086
panes_set.erase(dynamic_cast<DockablePane*>(pane));
1087
json[stack->id].push_back(dynamic_cast<DockablePane*>(pane)->get_identifier());
1088
}
1089
}
1090
}
1091
for(auto pane: panes_set) {
1092
if(pane->get_window()) {
1093
json[""].push_back(pane->get_identifier());
1094
}
1095
}
1096
return json.dump();
1097
}
1098
1099
void LayoutManager::restore_json_layout(const std::string &json_string) {
1100
nlohmann::json json = nlohmann::json::parse(json_string);
1101
for(auto stack: stacks) {
1102
if(json.contains(stack->id)) {
1103
for(auto const &pane_id: json[stack->id]) {
1104
for(auto pane: panes) {
1105
// TODO: could probably be done with a better complexity if there are
1106
// many panes
1107
if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) {
1108
stack->add_pane(*pane);
1109
break;
1110
}
1111
}
1112
}
1113
}
1114
}
1115
// Process popped-out panes
1116
for(auto const &pane_id: json[""]) {
1117
for(auto pane: panes) {
1118
// TODO: could probably be done with a better complexity if there are
1119
// many panes
1120
if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) {
1121
pane->pop_out();
1122
break;
1123
}
1124
}
1125
}
1126
}
1127
} // namespace gPanthera
1128