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.03 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
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