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