GTK docking interfaces

By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 gpanthera.cc

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