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.07 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
}
970
}
971
972
void ContentTab::update_active_style() {
973
if(this->page->get_stack()->get_visible_child() == this->page) {
974
this->add_css_class("checked");
975
this->add_css_class("gpanthera-dock-button-active");
976
this->set_active(true);
977
} else {
978
this->remove_css_class("checked");
979
this->remove_css_class("gpanthera-dock-button-active");
980
this->set_active(false);
981
}
982
}
983
984
ContentTab::~ContentTab() {
985
active_style_handler.disconnect();
986
drag_end_handler.disconnect();
987
drag_cancel_handler.disconnect();
988
control_status_handler.disconnect();
989
}
990
991
Gtk::Widget *ContentPage::get_tab_widget() const {
992
return this->tab_widget;
993
}
994
995
ContentStack *ContentPage::get_stack() const {
996
return this->stack;
997
}
998
999
void ContentPage::lose_visibility() {
1000
if(this->get_next_sibling()) {
1001
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling()));
1002
this->get_stack()->set_visible_child(*this->get_next_sibling());
1003
} else if(this->get_prev_sibling()) {
1004
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling()));
1005
this->get_stack()->set_visible_child(*this->get_prev_sibling());
1006
}
1007
}
1008
1009
void ContentPage::redock(ContentStack *stack) {
1010
if(stack == nullptr) {
1011
content_manager->signal_page_moving.emit(this);
1012
if(this->stack) {
1013
this->stack->remove(*this);
1014
if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && !this->stack->get_first_child()) {
1015
this->stack->remove_with_paned();
1016
}
1017
}
1018
auto old_stack = this->stack;
1019
if(old_stack) {
1020
if(!old_stack->get_first_child()) {
1021
old_stack->signal_leave_empty.emit();
1022
}
1023
}
1024
this->stack = nullptr;
1025
this->last_stack = nullptr;
1026
return;
1027
}
1028
// Check if the stack is now empty, in which case we should remove it
1029
if(this->stack == stack) {
1030
return;
1031
}
1032
content_manager->signal_page_moving.emit(this);
1033
if(this->stack != nullptr) {
1034
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()))) {
1035
this->stack->remove(*this);
1036
this->stack->remove_with_paned();
1037
} else if(this->get_parent() == this->stack) {
1038
this->stack->remove(*this);
1039
}
1040
}
1041
auto old_stack = this->stack;
1042
this->stack = stack;
1043
this->last_stack = stack;
1044
this->stack->add(*this);
1045
if(old_stack) {
1046
if(!old_stack->get_first_child()) {
1047
old_stack->signal_leave_empty.emit();
1048
}
1049
}
1050
content_manager->signal_page_moved.emit(this);
1051
}
1052
1053
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
1054
Gtk::Overlay(), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) {
1055
this->set_name("gpanthera_content_page");
1056
this->set_child(*child);
1057
this->set_tab_widget(tab_widget);
1058
this->set_margin_top(0);
1059
this->set_margin_bottom(0);
1060
this->set_margin_start(0);
1061
this->set_margin_end(0);
1062
if(stack) {
1063
stack->add_page(*this);
1064
this->content_manager->add_stack(this->stack);
1065
}
1066
auto click_controller = Gtk::GestureClick::create();
1067
click_controller->set_button(0);
1068
click_controller->signal_pressed().connect([this](int num_presses, double x, double y) {
1069
this->content_manager->set_last_operated_page(this);
1070
});
1071
this->property_has_focus().signal_changed().connect([this]() {
1072
if(this->property_has_focus()) {
1073
this->content_manager->set_last_operated_page(this);
1074
}
1075
});
1076
click_controller->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
1077
this->add_controller(click_controller);
1078
}
1079
1080
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
1081
}
1082
1083
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
1084
this->tab_widget = tab_widget;
1085
}
1086
1087
ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) {
1088
this->set_child(*notebook);
1089
this->set_decorated(true);
1090
this->set_resizable(true);
1091
}
1092
1093
Gtk::PositionType ContentNotebook::get_tab_position() const {
1094
return this->tab_position;
1095
}
1096
1097
ContentTabBar *ContentNotebook::get_switcher() const {
1098
return this->switcher;
1099
}
1100
1101
ContentStack *ContentNotebook::get_stack() const {
1102
return this->stack;
1103
}
1104
1105
ContentPage *ContentManager::get_last_operated_page() const {
1106
return this->last_operated_page;
1107
}
1108
1109
void ContentManager::set_last_operated_page(ContentPage *page) {
1110
if(this->last_operated_page) {
1111
this->last_operated_page->signal_control_status_changed.emit(false);
1112
}
1113
this->last_operated_page = page;
1114
page->signal_control_status_changed.emit(true);
1115
this->signal_page_operated.emit(page);
1116
}
1117
1118
DockWindow *DockablePane::get_window() const {
1119
return this->window;
1120
}
1121
1122
std::string LayoutManager::get_layout_as_json() const {
1123
nlohmann::json json;
1124
std::unordered_set<DockablePane*> panes_set;
1125
for(auto pane: panes) {
1126
panes_set.insert(pane);
1127
}
1128
for(auto stack: stacks) {
1129
for(auto pane: collect_children(*stack)) {
1130
if(dynamic_cast<DockablePane*>(pane)) {
1131
panes_set.erase(dynamic_cast<DockablePane*>(pane));
1132
json[stack->id].push_back(dynamic_cast<DockablePane*>(pane)->get_identifier());
1133
}
1134
}
1135
}
1136
for(auto pane: panes_set) {
1137
if(pane->get_window()) {
1138
json[""].push_back(pane->get_identifier());
1139
}
1140
}
1141
return json.dump();
1142
}
1143
1144
void LayoutManager::restore_json_layout(const std::string &json_string) {
1145
nlohmann::json json = nlohmann::json::parse(json_string);
1146
for(auto stack: stacks) {
1147
if(json.contains(stack->id)) {
1148
for(auto const &pane_id: json[stack->id]) {
1149
for(auto pane: panes) {
1150
// TODO: could probably be done with a better complexity if there are
1151
// many panes
1152
if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) {
1153
stack->add_pane(*pane);
1154
break;
1155
}
1156
}
1157
}
1158
}
1159
}
1160
// Process popped-out panes
1161
for(auto const &pane_id: json[""]) {
1162
for(auto pane: panes) {
1163
// TODO: could probably be done with a better complexity if there are
1164
// many panes
1165
if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) {
1166
pane->pop_out();
1167
break;
1168
}
1169
}
1170
}
1171
}
1172
} // namespace gPanthera
1173