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