GTK docking interfaces and more

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