GTK docking interfaces and more

Important information: Google announced that, from September 2026, Android devices will require ALL apps to be signed by Google, effectively leading to an iOS situation. Value your right to a computer that does what you want; do not tolerate this monopolistic practice! Contact me if you don't understand why it is bad. Click to learn more.

 gpanthera.cc

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