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