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.16 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
}
773
}
774
}
775
}
776
777
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
778
this->set_child(*page->get_tab_widget());
779
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
780
this->set_halign(Gtk::Align::CENTER);
781
this->set_valign(Gtk::Align::CENTER);
782
this->get_style_context()->add_class("toggle");
783
this->get_style_context()->add_class("gpanthera-content-tab");
784
// Add/remove CSS classes when the pane is shown/hidden
785
active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() {
786
this->update_active_style();
787
});
788
drag_source = Gtk::DragSource::create();
789
drag_source->set_exclusive(false);
790
// This is to prevent the click handler from taking over grabbing the button
791
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
792
value.init(ContentPage::get_type());
793
value.set(page);
794
// Add the drag source to the button
795
this->add_controller(drag_source);
796
this->signal_clicked().connect([this, page]() {
797
page->get_stack()->set_visible_child(*page);
798
page->content_manager->set_last_operated_page(page);
799
update_active_style();
800
});
801
// Switch tabs on depress
802
auto gesture_click = Gtk::GestureClick::create();
803
gesture_click->set_button(1);
804
this->add_controller(gesture_click);
805
gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) {
806
page->get_stack()->set_visible_child(*page);
807
page->content_manager->set_last_operated_page(page);
808
update_active_style();
809
});
810
// Provide the drag data
811
drag_source->signal_prepare().connect([this](double, double) {
812
drag_source->set_actions(Gdk::DragAction::MOVE);
813
auto const paintable = Gtk::WidgetPaintable::create();
814
paintable->set_widget(*this);
815
drag_source->set_icon(paintable->get_current_image(), 0, 0);
816
return Gdk::ContentProvider::create(value);
817
}, false);
818
update_active_style();
819
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
820
this->set_opacity(0);
821
}, false);
822
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
823
// Process dropped buttons by inserting them after the current button
824
drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) {
825
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
826
827
if(widget) {
828
if(auto page = dynamic_cast<ContentPage*>(widget)) {
829
if(page->content_manager != this->page->content_manager) {
830
// If the pane is not in the same layout manager, reject
831
return false;
832
}
833
auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent()->get_parent()->get_parent()->get_parent());
834
if(switcher) {
835
auto *stack = switcher->get_stack();
836
// Move the button to the new position
837
page->redock(stack);
838
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
839
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
840
page->insert_before(*stack, *this->page);
841
} else {
842
page->insert_after(*stack, *this->page);
843
}
844
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
845
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
846
page->insert_before(*stack, *this->page);
847
} else {
848
page->insert_after(*stack, *this->page);
849
}
850
}
851
852
switcher->update_buttons();
853
}
854
}
855
}
856
857
return true; // Drop OK
858
}, false);
859
this->add_controller(drop_target);
860
// Pop out if dragged to an external location
861
drag_cancel_handler = drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
862
if(reason == Gdk::DragCancelReason::NO_TARGET) {
863
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
864
bool result = stack->signal_detach.emit(this->page);
865
if(!result) {
866
this->set_opacity(1);
867
}
868
return result;
869
}
870
this->set_opacity(1);
871
return false;
872
}, false);
873
drag_end_handler = drag_source->signal_drag_end().connect([this](const Glib::RefPtr<Gdk::Drag>&, bool drop_ok) {
874
if(drop_ok) {
875
this->set_opacity(1);
876
}
877
}, false);
878
879
// Provide a context menu
880
context_menu = Gio::Menu::create();
881
auto action_group = Gio::SimpleActionGroup::create();
882
this->insert_action_group("win", action_group);
883
884
auto close_action = Gio::SimpleAction::create("close");
885
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
886
this->close();
887
});
888
action_group->add_action(close_action);
889
context_menu->append(_("Close"), "win.close");
890
891
auto close_all_action = Gio::SimpleAction::create("close-all");
892
close_all_action->signal_activate().connect([this](const Glib::VariantBase&) {
893
for(auto page : collect_children(*this->page->get_stack())) {
894
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
895
if(!content_page->signal_close.emit()) {
896
content_page->redock(nullptr);
897
}
898
}
899
}
900
});
901
action_group->add_action(close_all_action);
902
context_menu->append(_("Close all"), "win.close-all");
903
904
auto close_others_action = Gio::SimpleAction::create("close-others");
905
close_others_action->signal_activate().connect([this](const Glib::VariantBase&) {
906
for(auto page : collect_children(*this->page->get_stack())) {
907
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
908
if(content_page != this->page && !content_page->signal_close.emit()) {
909
content_page->redock(nullptr);
910
}
911
}
912
}
913
});
914
action_group->add_action(close_others_action);
915
context_menu->append(_("Close others"), "win.close-others");
916
917
auto detach_action = Gio::SimpleAction::create("detach");
918
detach_action->signal_activate().connect([this](const Glib::VariantBase&) {
919
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
920
bool result = stack->signal_detach.emit(this->page);
921
});
922
action_group->add_action(detach_action);
923
context_menu->append(_("New window"), "win.detach");
924
925
// TODO: Add more actions: "New window", "Split left", "Split right", "Split top", "Split bottom", "Close all", "Close others", "Close to the right", "Close to the left"
926
this->insert_action_group("win", action_group);
927
// Attach the context menu to the button
928
auto context_menu_signal = add_context_menu(*this);
929
popover = Gtk::make_managed<Gtk::PopoverMenu>();
930
popover->set_menu_model(context_menu);
931
popover->set_parent(*this);
932
popover->set_has_arrow(false);
933
popover->set_halign(Gtk::Align::START);
934
context_menu_signal.connect([this, action_group](double x, double y) {
935
if(this->context_menu) {
936
popover->set_pointing_to(Gdk::Rectangle(x, y, 1, 1));
937
popover->popup();
938
}
939
});
940
auto middle_click_controller = Gtk::GestureClick::create();
941
middle_click_controller->set_button(2);
942
middle_click_controller->signal_released().connect([this](int num_presses, double x, double y) {
943
this->close();
944
});
945
this->add_controller(middle_click_controller);
946
control_status_handler = this->page->signal_control_status_changed.connect([this](bool operating) {
947
if(operating) {
948
this->add_css_class("gpanthera-selected-tab");
949
} else {
950
this->remove_css_class("gpanthera-selected-tab");
951
}
952
});
953
}
954
955
void ContentTab::close() {
956
page->close();
957
}
958
959
void ContentPage::close() {
960
if(!this->signal_close.emit()) {
961
if(this->get_next_sibling()) {
962
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling()));
963
this->get_stack()->set_visible_child(*this->get_next_sibling());
964
} else if(this->get_prev_sibling()) {
965
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling()));
966
this->get_stack()->set_visible_child(*this->get_prev_sibling());
967
}
968
this->redock(nullptr);
969
content_manager->signal_page_closed.emit(this);
970
}
971
}
972
973
void ContentTab::update_active_style() {
974
if(this->page->get_stack()->get_visible_child() == this->page) {
975
this->add_css_class("checked");
976
this->add_css_class("gpanthera-dock-button-active");
977
this->set_active(true);
978
} else {
979
this->remove_css_class("checked");
980
this->remove_css_class("gpanthera-dock-button-active");
981
this->set_active(false);
982
}
983
}
984
985
ContentTab::~ContentTab() {
986
active_style_handler.disconnect();
987
drag_end_handler.disconnect();
988
drag_cancel_handler.disconnect();
989
control_status_handler.disconnect();
990
}
991
992
Gtk::Widget *ContentPage::get_tab_widget() const {
993
return this->tab_widget;
994
}
995
996
ContentStack *ContentPage::get_stack() const {
997
return this->stack;
998
}
999
1000
void ContentPage::lose_visibility() {
1001
if(this->get_next_sibling()) {
1002
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling()));
1003
this->get_stack()->set_visible_child(*this->get_next_sibling());
1004
} else if(this->get_prev_sibling()) {
1005
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling()));
1006
this->get_stack()->set_visible_child(*this->get_prev_sibling());
1007
}
1008
}
1009
1010
void ContentPage::redock(ContentStack *stack) {
1011
if(stack == nullptr) {
1012
content_manager->signal_page_closing.emit(this);
1013
if(this->stack) {
1014
this->stack->remove(*this);
1015
if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && !this->stack->get_first_child()) {
1016
this->stack->remove_with_paned();
1017
}
1018
}
1019
auto old_stack = this->stack;
1020
if(old_stack) {
1021
if(!old_stack->get_first_child()) {
1022
old_stack->signal_leave_empty.emit();
1023
}
1024
}
1025
this->stack = nullptr;
1026
this->last_stack = nullptr;
1027
return;
1028
}
1029
// Check if the stack is now empty, in which case we should remove it
1030
if(this->stack == stack) {
1031
return;
1032
}
1033
content_manager->signal_page_moving.emit(this);
1034
if(this->stack != nullptr) {
1035
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()))) {
1036
this->stack->remove(*this);
1037
this->stack->remove_with_paned();
1038
} else if(this->get_parent() == this->stack) {
1039
this->stack->remove(*this);
1040
}
1041
}
1042
auto old_stack = this->stack;
1043
this->stack = stack;
1044
this->last_stack = stack;
1045
this->stack->add(*this);
1046
if(old_stack) {
1047
if(!old_stack->get_first_child()) {
1048
old_stack->signal_leave_empty.emit();
1049
}
1050
}
1051
content_manager->signal_page_moved.emit(this);
1052
}
1053
1054
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
1055
Gtk::Overlay(), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) {
1056
this->set_name("gpanthera_content_page");
1057
this->set_child(*child);
1058
this->set_tab_widget(tab_widget);
1059
this->set_margin_top(0);
1060
this->set_margin_bottom(0);
1061
this->set_margin_start(0);
1062
this->set_margin_end(0);
1063
if(stack) {
1064
stack->add_page(*this);
1065
this->content_manager->add_stack(this->stack);
1066
}
1067
auto click_controller = Gtk::GestureClick::create();
1068
click_controller->set_button(0);
1069
click_controller->signal_pressed().connect([this](int num_presses, double x, double y) {
1070
this->content_manager->set_last_operated_page(this);
1071
});
1072
this->property_has_focus().signal_changed().connect([this]() {
1073
if(this->property_has_focus()) {
1074
this->content_manager->set_last_operated_page(this);
1075
}
1076
});
1077
click_controller->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
1078
this->add_controller(click_controller);
1079
}
1080
1081
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
1082
}
1083
1084
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
1085
this->tab_widget = tab_widget;
1086
}
1087
1088
ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) {
1089
this->set_child(*notebook);
1090
this->set_decorated(true);
1091
this->set_resizable(true);
1092
}
1093
1094
Gtk::PositionType ContentNotebook::get_tab_position() const {
1095
return this->tab_position;
1096
}
1097
1098
ContentTabBar *ContentNotebook::get_switcher() const {
1099
return this->switcher;
1100
}
1101
1102
ContentStack *ContentNotebook::get_stack() const {
1103
return this->stack;
1104
}
1105
1106
ContentPage *ContentManager::get_last_operated_page() const {
1107
return this->last_operated_page;
1108
}
1109
1110
void ContentManager::set_last_operated_page(ContentPage *page) {
1111
if(this->last_operated_page) {
1112
this->last_operated_page->signal_control_status_changed.emit(false);
1113
}
1114
this->last_operated_page = page;
1115
if(page) {
1116
page->signal_control_status_changed.emit(true);
1117
this->signal_page_operated.emit(page);
1118
}
1119
}
1120
1121
DockWindow *DockablePane::get_window() const {
1122
return this->window;
1123
}
1124
1125
std::string LayoutManager::get_layout_as_json() const {
1126
nlohmann::json json;
1127
std::unordered_set<DockablePane*> panes_set;
1128
for(auto pane: panes) {
1129
panes_set.insert(pane);
1130
}
1131
for(auto stack: stacks) {
1132
for(auto pane: collect_children(*stack)) {
1133
if(dynamic_cast<DockablePane*>(pane)) {
1134
panes_set.erase(dynamic_cast<DockablePane*>(pane));
1135
json[stack->id].push_back(dynamic_cast<DockablePane*>(pane)->get_identifier());
1136
}
1137
}
1138
}
1139
for(auto pane: panes_set) {
1140
if(pane->get_window()) {
1141
json[""].push_back(pane->get_identifier());
1142
}
1143
}
1144
return json.dump();
1145
}
1146
1147
void LayoutManager::restore_json_layout(const std::string &json_string) {
1148
nlohmann::json json = nlohmann::json::parse(json_string);
1149
for(auto stack: stacks) {
1150
if(json.contains(stack->id)) {
1151
for(auto const &pane_id: json[stack->id]) {
1152
for(auto pane: panes) {
1153
// TODO: could probably be done with a better complexity if there are
1154
// many panes
1155
if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) {
1156
stack->add_pane(*pane);
1157
break;
1158
}
1159
}
1160
}
1161
}
1162
}
1163
// Process popped-out panes
1164
for(auto const &pane_id: json[""]) {
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
pane->pop_out();
1170
break;
1171
}
1172
}
1173
}
1174
}
1175
} // namespace gPanthera
1176