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.15 kiB
C++ source, ASCII text
        
            
1
#include "gpanthera.hh"
2
3
#include <cassert>
4
#include <iostream>
5
#include <utility>
6
#include <libintl.h>
7
#include <locale.h>
8
#include <filesystem>
9
#include <ranges>
10
#include <nlohmann/json.hpp>
11
#include <unordered_set>
12
13
#define _(STRING) dgettext("gpanthera", STRING)
14
15
namespace gPanthera {
16
sigc::signal<void(double, double)> add_context_menu(Gtk::Widget &widget) {
17
sigc::signal<void(double, double)> signal;
18
// Add a context menu to a widget
19
auto gesture_click = Gtk::GestureClick::create();
20
gesture_click->set_button(3);
21
auto gesture_keyboard = Gtk::EventControllerKey::create();
22
gesture_click->signal_pressed().connect([&widget, signal](int n_press, double x, double y) {
23
if(n_press == 1) {
24
signal.emit(x, y);
25
}
26
}, false);
27
gesture_keyboard->signal_key_pressed().connect([&widget, signal](int keyval, int keycode, Gdk::ModifierType state) {
28
if((keyval == GDK_KEY_F10) && (state == Gdk::ModifierType::SHIFT_MASK) || keyval == GDK_KEY_Menu) {
29
// auto popover = Gtk::make_managed<Gtk::PopoverMenu>();
30
// popover->set_menu_model(menu);
31
// popover->set_has_arrow(false);
32
// popover->set_halign(Gtk::Align::START);
33
// popover->set_pointing_to(Gdk::Rectangle(0, 0, 1, 1));
34
// popover->set_parent(widget);
35
// popover->popup();
36
signal.emit(0, 0);
37
}
38
return true;
39
}, false);
40
widget.add_controller(gesture_click);
41
widget.add_controller(gesture_keyboard);
42
return signal;
43
}
44
45
std::vector<Gtk::Widget*> collect_children(Gtk::Widget &widget) {
46
// Get a vector of the children of a GTK widget, since the container API was removed in GTK 4
47
std::vector<Gtk::Widget*> children;
48
for(auto *child = widget.get_first_child(); child; child = child->get_next_sibling()) {
49
children.push_back(child);
50
}
51
return children;
52
}
53
54
Gtk::Image *copy_image(Gtk::Image *image) {
55
// Generate a new Gtk::Image with the same contents as an existing one
56
if(image->get_storage_type() == Gtk::Image::Type::PAINTABLE) {
57
return Gtk::make_managed<Gtk::Image>(image->get_paintable());
58
} else if(image->get_storage_type() == Gtk::Image::Type::ICON_NAME) {
59
auto new_image = Gtk::make_managed<Gtk::Image>();
60
new_image->set_from_icon_name(image->get_icon_name());
61
return new_image;
62
} else {
63
return nullptr;
64
}
65
}
66
67
void init() {
68
g_log_set_always_fatal(G_LOG_LEVEL_CRITICAL);
69
// Set up gettext configuration
70
bindtextdomain("gpanthera", "./locales");
71
}
72
73
DockablePane::DockablePane(std::shared_ptr<LayoutManager> layout, Gtk::Widget &child, const Glib::ustring &name, const Glib::ustring &label, Gtk::Image *icon, DockStack *stack, Gtk::Widget *custom_header)
74
: Gtk::Box(Gtk::Orientation::VERTICAL, 0), name(name) {
75
if(icon) {
76
this->icon = icon;
77
}
78
if(stack) {
79
this->stack = stack;
80
}
81
this->layout = layout;
82
this->layout->add_pane(this);
83
this->label.set_text(label);
84
// This should be replaced with a custom class in the future
85
header = std::make_unique<Gtk::HeaderBar>();
86
header->set_show_title_buttons(false);
87
if(custom_header) {
88
header->set_title_widget(*custom_header);
89
} else {
90
header->set_title_widget(this->label);
91
}
92
header->add_css_class("gpanthera-dock-titlebar");
93
auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>();
94
header_menu_button->set_direction(Gtk::ArrowType::NONE);
95
96
// Pane menu
97
this->action_group = Gio::SimpleActionGroup::create();
98
header_menu_button->insert_action_group("win", action_group);
99
100
// Close action
101
auto close_action = Gio::SimpleAction::create("close");
102
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
103
if(this->stack) {
104
this->stack->set_visible_child("");
105
}
106
});
107
action_group->add_action(close_action);
108
header_menu->append(_("Close"), "win.close");
109
110
// Pop out action
111
auto pop_out_action = Gio::SimpleAction::create("pop_out");
112
pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) {
113
if(this->stack) {
114
this->pop_out();
115
}
116
});
117
action_group->add_action(pop_out_action);
118
header_menu->append(_("Pop out"), "win.pop_out");
119
120
// Move menu
121
auto move_menu = Gio::Menu::create();
122
for(auto &this_stack : this->layout->stacks) {
123
auto action_name = "move_" + this_stack->name;
124
auto move_action = Gio::SimpleAction::create(action_name);
125
move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) {
126
this_stack->add_pane(*this);
127
});
128
action_group->add_action(move_action);
129
move_menu->append(this_stack->name, "win." + action_name);
130
}
131
132
// Add move submenu
133
header_menu->append_submenu(_("Move"), move_menu);
134
135
// Switch to traditional (nested) submenus, not sliding
136
auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED);
137
popover_menu->set_has_arrow(false);
138
header_menu_button->set_popover(*popover_menu);
139
140
// TODO: Add a context menu as well
141
142
header->pack_end(*header_menu_button);
143
144
this->prepend(*header);
145
this->child = &child;
146
this->append(child);
147
}
148
149
Gtk::Stack *DockablePane::get_stack() const {
150
return stack;
151
}
152
153
void DockablePane::redock(DockStack *stack) {
154
if(this->window != nullptr) {
155
this->window->hide();
156
// Put the titlebar back
157
this->window->unset_titlebar();
158
this->window->set_decorated(false);
159
this->header->get_style_context()->remove_class("titlebar");
160
this->prepend(*this->header);
161
this->window->unset_child();
162
this->window->close();
163
} else if(this->get_parent() && this->stack == this->get_parent()) {
164
this->stack->remove(*this);
165
}
166
this->stack = stack;
167
this->last_stack = stack;
168
this->stack->add(*this, this->get_identifier());
169
if(this->window != nullptr) {
170
this->window->destroy();
171
delete this->window;
172
this->window = nullptr;
173
// Re-enable the pop out option
174
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
175
action->set_enabled(true);
176
this->header->get_style_context()->remove_class("gpanthera-dock-titlebar-popout");
177
}
178
layout->signal_pane_moved.emit(this);
179
}
180
181
void DockablePane::pop_out() {
182
if(this->stack != nullptr) {
183
this->stack->remove(*this);
184
this->stack = nullptr;
185
}
186
187
if(this->window == nullptr) {
188
// Remove the header bar from the pane, so it can be used as the titlebar of the window
189
this->remove(*this->header);
190
this->window = new DockWindow(this);
191
this->window->set_titlebar(*this->header);
192
this->window->set_child(*this);
193
this->window->set_decorated(true);
194
// Grey out the pop-out option
195
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
196
action->set_enabled(false);
197
this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout");
198
}
199
this->window->present();
200
layout->signal_pane_moved.emit(this);
201
}
202
203
Glib::ustring DockablePane::get_identifier() const {
204
return name;
205
}
206
207
Gtk::Image *DockablePane::get_icon() const {
208
return icon;
209
}
210
211
Gtk::Widget *DockablePane::get_child() const {
212
return child;
213
}
214
215
Gtk::Label *DockablePane::get_label() {
216
return &label;
217
}
218
219
LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") {
220
}
221
222
void LayoutManager::add_pane(DockablePane *pane) {
223
panes.push_back(pane);
224
}
225
226
void LayoutManager::add_stack(DockStack *stack) {
227
stacks.push_back(stack);
228
}
229
230
void LayoutManager::remove_pane(DockablePane *pane) {
231
panes.erase(std::ranges::remove(panes, pane).begin(), panes.end());
232
}
233
234
void LayoutManager::remove_stack(DockStack *stack) {
235
stacks.erase(std::ranges::remove(stacks, stack).begin(), stacks.end());
236
}
237
238
BaseStack::BaseStack() : Gtk::Stack() {
239
}
240
241
DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name, const std::string &id) : BaseStack(), layout(layout), name(name), id(id) {
242
auto empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
243
this->add(*empty_child, "");
244
// Add the stack to a layout manager
245
this->layout->add_stack(this);
246
247
// Hide the stack when no child is visible
248
this->property_visible_child_name().signal_changed().connect([this]() {
249
if(this->get_visible_child_name() == "") {
250
this->hide();
251
} else {
252
this->show();
253
}
254
});
255
256
// Also hide when the visible child is removed
257
this->signal_child_removed.connect([this](Gtk::Widget* const &child) {
258
if(this->get_visible_child_name() == "") {
259
this->hide();
260
}
261
});
262
263
this->set_visible_child("");
264
this->hide();
265
}
266
267
DockWindow::DockWindow(DockablePane *pane) {
268
this->pane = pane;
269
this->set_child(*pane);
270
// Attempting to close the window should redock the pane so it doesn't vanish
271
this->signal_close_request().connect([this]() {
272
this->pane->redock(this->pane->last_stack);
273
return true;
274
}, false);
275
}
276
277
DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
278
auto update_callback = [this](Gtk::Widget*) {
279
this->update_buttons();
280
};
281
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
282
stack->signal_child_added.connect(update_callback);
283
stack->signal_child_removed.connect(update_callback);
284
this->get_style_context()->add_class("gpanthera-dock-switcher");
285
drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
286
this->add_controller(drop_target);
287
// Process dropped buttons
288
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
289
const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get();
290
291
if(widget) {
292
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
293
this->stack->add_pane(*pane);
294
}
295
}
296
297
return true; // Drop OK
298
}, false);
299
}
300
301
DockStack *DockStackSwitcher::get_stack() const {
302
return stack;
303
}
304
305
DockStackSwitcher::~DockStackSwitcher() {
306
add_handler.disconnect();
307
remove_handler.disconnect();
308
}
309
310
DockButton::DockButton(DockablePane *pane) : Gtk::ToggleButton(), pane(pane) {
311
if(pane->get_icon()) {
312
this->set_child(*copy_image(pane->get_icon()));
313
}
314
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
315
this->set_tooltip_text(pane->get_label()->get_text());
316
this->set_halign(Gtk::Align::CENTER);
317
this->set_valign(Gtk::Align::CENTER);
318
this->get_style_context()->add_class("toggle");
319
this->get_style_context()->add_class("gpanthera-dock-button");
320
// Add/remove CSS classes when the pane is shown/hidden
321
active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
322
this->update_active_style();
323
});
324
drag_source = Gtk::DragSource::create();
325
drag_source->set_exclusive(false);
326
// This is to prevent the click handler from taking over grabbing the button
327
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
328
value.init(DockablePane::get_type());
329
value.set(pane);
330
// Add the drag source to the button
331
this->add_controller(drag_source);
332
this->signal_clicked().connect([this, pane]() {
333
if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) {
334
pane->get_stack()->set_visible_child("");
335
} else {
336
pane->get_stack()->set_visible_child(pane->get_identifier());
337
}
338
});
339
// Provide the drag data
340
drag_source->signal_prepare().connect([this](double, double) {
341
drag_source->set_actions(Gdk::DragAction::MOVE);
342
auto const paintable = Gtk::WidgetPaintable::create();
343
paintable->set_widget(*this);
344
drag_source->set_icon(paintable->get_current_image(), 0, 0);
345
return Gdk::ContentProvider::create(value);
346
}, false);
347
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
348
this->set_opacity(0);
349
}, false);
350
// Pop out if dragged to an external location
351
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
352
if(reason == Gdk::DragCancelReason::NO_TARGET) {
353
this->pane->pop_out();
354
return true;
355
}
356
this->set_opacity(1);
357
return false;
358
}, false);
359
// Add a drop target to the button
360
auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
361
drop_target->set_actions(Gdk::DragAction::MOVE);
362
drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
363
this->add_controller(drop_target);
364
// Process dropped buttons by inserting them after the current button
365
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
366
const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get();
367
368
if(widget) {
369
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
370
if(pane->layout != this->pane->layout) {
371
// If the pane is not in the same layout manager, reject
372
return false;
373
}
374
auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent());
375
if(switcher) {
376
auto *stack = switcher->get_stack();
377
// Move the button to the new position
378
pane->redock(stack);
379
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
380
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
381
pane->insert_before(*stack, *this->pane);
382
} else {
383
pane->insert_after(*stack, *this->pane);
384
}
385
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
386
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
387
pane->insert_before(*stack, *this->pane);
388
} else {
389
pane->insert_after(*stack, *this->pane);
390
}
391
}
392
393
switcher->update_buttons();
394
}
395
}
396
}
397
398
return true; // Drop OK
399
}, false);
400
this->update_active_style();
401
}
402
403
void DockButton::update_active_style() {
404
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
405
this->add_css_class("checked");
406
this->add_css_class("gpanthera-dock-button-active");
407
this->set_active(true);
408
} else {
409
this->remove_css_class("checked");
410
this->remove_css_class("gpanthera-dock-button-active");
411
this->set_active(false);
412
}
413
}
414
415
DockButton::~DockButton() {
416
active_style_handler.disconnect();
417
}
418
419
void DockStackSwitcher::update_buttons() {
420
// Clear the old buttons
421
auto old_buttons = collect_children(*this);
422
for(auto *button : old_buttons) {
423
remove(*button);
424
}
425
DockButton* first_child = nullptr;
426
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
427
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
428
auto *button = Gtk::make_managed<DockButton>(pane);
429
if(!first_child) {
430
first_child = button;
431
} else {
432
button->set_group(*first_child);
433
}
434
if(pane->get_identifier() != Glib::ustring("")) {
435
append(*button);
436
}
437
}
438
}
439
}
440
441
void DockStack::add_pane(DockablePane &child) {
442
child.redock(this);
443
}
444
445
void ContentManager::add_stack(ContentStack *stack) {
446
this->stacks.push_back(stack);
447
}
448
449
void ContentManager::remove_stack(ContentStack *stack) {
450
this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end());
451
}
452
453
void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) {
454
Gtk::Stack::add(child, name);
455
signal_child_added.emit(&child);
456
}
457
458
void BaseStack::add(Gtk::Widget &child) {
459
Gtk::Stack::add(child);
460
signal_child_added.emit(&child);
461
}
462
463
void BaseStack::remove(Gtk::Widget &child) {
464
Gtk::Stack::remove(child);
465
signal_child_removed.emit(&child);
466
}
467
468
void ContentStack::disable_children() {
469
auto children = collect_children(*this);
470
for(auto *child : children) {
471
child->set_can_target(false);
472
}
473
}
474
475
void ContentStack::enable_children() {
476
auto children = collect_children(*this);
477
for(auto *child : children) {
478
child->set_can_target(true);
479
}
480
}
481
482
ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager, std::function<bool(ContentPage*)> detach_handler)
483
: BaseStack(), content_manager(content_manager) {
484
this->set_name("gpanthera_content_stack");
485
this->content_manager->add_stack(this);
486
if(detach_handler) {
487
this->detach_handler = detach_handler;
488
this->signal_detach.connect([this](ContentPage *widget) {
489
return this->detach_handler(widget);
490
});
491
}
492
493
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
494
this->add_controller(drop_target);
495
// Some widgets (such as webviews) will consume anything dropped on their ancestors. We have
496
// to disable all child widgets to ensure the drop can be caught by this widget.
497
drop_target->signal_enter().connect([this](double x, double y) {
498
disable_children();
499
return Gdk::DragAction::MOVE;
500
}, false);
501
drop_target->signal_leave().connect([this]() {
502
enable_children();
503
}, false);
504
// Process dropped buttons
505
drop_target->signal_drop().connect([this, content_manager](const Glib::ValueBase &value, double x, double y) {
506
enable_children();
507
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
508
509
if(auto page = dynamic_cast<ContentPage*>(widget)) {
510
if(page->get_stack() == this && !this->get_first_child()->get_next_sibling()) {
511
// Don't allow splitting if there are no more pages
512
return false;
513
}
514
if(!(page->content_manager == this->content_manager)) {
515
// If the page is not in the same content manager, reject
516
return false;
517
}
518
double width = this->get_allocated_width(), height = this->get_allocated_height();
519
// Split based on the drop position
520
if(!(x < width / 4 || x > width * 3 / 4 || y < height / 4 || y > height * 3 / 4)) {
521
if(page->get_stack() == this) {
522
return false;
523
}
524
page->lose_visibility();
525
// If the drop position is not at a quarter to the edges, move to the same stack
526
this->add_page(*page);
527
return true;
528
}
529
page->lose_visibility();
530
auto new_stack = Gtk::make_managed<ContentStack>(this->content_manager, this->detach_handler);
531
auto this_notebook = dynamic_cast<ContentNotebook*>(this->get_parent());
532
auto new_switcher = Gtk::make_managed<ContentTabBar>(new_stack, this_notebook ? this_notebook->get_switcher()->get_orientation() : Gtk::Orientation::HORIZONTAL, this_notebook->get_switcher()->get_extra_child_function());
533
auto new_notebook = Gtk::make_managed<ContentNotebook>(new_stack, new_switcher, this_notebook ? this_notebook->get_tab_position() : Gtk::PositionType::TOP);
534
new_stack->add_page(*page);
535
new_stack->set_visible_child(*page);
536
content_manager->set_last_operated_page(page);
537
if(x < width / 4) {
538
this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::START);
539
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
540
paned->set_start_child(*new_notebook);
541
}
542
} else if(x > width * 3 / 4) {
543
this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::END);
544
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
545
paned->set_end_child(*new_notebook);
546
}
547
} else if(y < height / 4) {
548
this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::START);
549
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
550
paned->set_start_child(*new_notebook);
551
}
552
} else if(y > height * 3 / 4) {
553
this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::END);
554
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
555
paned->set_end_child(*new_notebook);
556
}
557
}
558
}
559
560
return true; // Drop OK
561
}, false);
562
}
563
564
std::function<bool(ContentPage*)> ContentStack::get_detach_handler() const {
565
return this->detach_handler;
566
}
567
568
ContentNotebook::ContentNotebook(ContentStack *stack, ContentTabBar *switcher, Gtk::PositionType tab_position) : Gtk::Box(), stack(stack), switcher(switcher), tab_position(tab_position) {
569
this->append(*stack);
570
if(tab_position == Gtk::PositionType::TOP || tab_position == Gtk::PositionType::BOTTOM) {
571
this->set_orientation(Gtk::Orientation::VERTICAL);
572
if(tab_position == Gtk::PositionType::TOP) {
573
this->prepend(*switcher);
574
switcher->add_css_class("top");
575
} else if(tab_position == Gtk::PositionType::BOTTOM) {
576
this->append(*switcher);
577
switcher->add_css_class("bottom");
578
}
579
} else if(tab_position == Gtk::PositionType::LEFT || tab_position == Gtk::PositionType::RIGHT) {
580
this->set_orientation(Gtk::Orientation::HORIZONTAL);
581
if(tab_position == Gtk::PositionType::LEFT) {
582
this->prepend(*switcher);
583
switcher->add_css_class("left");
584
} else if(tab_position == Gtk::PositionType::RIGHT) {
585
this->append(*switcher);
586
switcher->add_css_class("right");
587
}
588
}
589
this->add_css_class("gpanthera-content-notebook");
590
}
591
592
void ContentStack::make_paned(Gtk::Orientation orientation, Gtk::PackType pack_type) {
593
auto *parent = this->get_parent();
594
if(auto notebook = dynamic_cast<ContentNotebook*>(parent)) {
595
auto *paned = Gtk::make_managed<Gtk::Paned>(orientation);
596
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(notebook->get_parent())) {
597
if(parent_paned->get_start_child() == notebook) {
598
notebook->unparent();
599
parent_paned->set_start_child(*paned);
600
} else if(parent_paned->get_end_child() == notebook) {
601
notebook->unparent();
602
parent_paned->set_end_child(*paned);
603
}
604
} else if(auto box = dynamic_cast<Gtk::Box*>(notebook->get_parent())) {
605
auto previous_child = notebook->get_prev_sibling();
606
box->remove(*notebook);
607
if(previous_child) {
608
paned->insert_after(*box, *previous_child);
609
} else {
610
box->prepend(*paned);
611
}
612
}
613
if(pack_type == Gtk::PackType::START) {
614
paned->set_end_child(*notebook);
615
} else if(pack_type == Gtk::PackType::END) {
616
paned->set_start_child(*notebook);
617
}
618
}
619
}
620
621
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) {
622
this->get_style_context()->add_class("gpanthera-content-tab-bar");
623
this->set_margin_top(0);
624
this->set_margin_bottom(0);
625
this->set_margin_start(0);
626
this->set_margin_end(0);
627
628
auto update_callback = [this](Gtk::Widget*) {
629
this->update_buttons();
630
};
631
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
632
stack->signal_child_added.connect(update_callback);
633
stack->signal_child_removed.connect(update_callback);
634
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
635
this->add_controller(drop_target);
636
// Process dropped buttons
637
drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) {
638
if(const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get()) {
639
if(auto page = dynamic_cast<ContentPage*>(widget)) {
640
if(page->get_stack() == this->stack) {
641
this->stack->remove(*page);
642
this->stack->add(*page);
643
} else {
644
this->stack->add_page(*page);
645
}
646
}
647
}
648
649
return true; // Drop OK
650
}, false);
651
652
scrolled_window = Gtk::make_managed<Gtk::ScrolledWindow>();
653
scrolled_window->add_css_class("gpanthera-content-tab-bar-scroll");
654
auto viewport = Gtk::make_managed<Gtk::Viewport>(nullptr, nullptr);
655
viewport->add_css_class("gpanthera-content-tab-bar-viewport");
656
tab_box = Gtk::make_managed<Gtk::Box>(orientation);
657
tab_box->add_css_class("gpanthera-content-tab-box");
658
this->prepend(*scrolled_window);
659
scrolled_window->set_child(*viewport);
660
viewport->set_child(*tab_box);
661
662
this->set_orientation(orientation);
663
664
if(this->extra_child_function) {
665
this->append(*this->extra_child_function(this));
666
}
667
}
668
669
std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> ContentTabBar::get_extra_child_function() const {
670
return this->extra_child_function;
671
}
672
673
void ContentTabBar::set_extra_child_function(std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> extra_child_function) {
674
// Note that this doesn't remove the existing extra child
675
this->extra_child_function = extra_child_function;
676
}
677
678
void ContentTabBar::set_orientation(Gtk::Orientation orientation) {
679
this->Gtk::Box::set_orientation(orientation);
680
if(orientation == Gtk::Orientation::HORIZONTAL) {
681
scrolled_window->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::NEVER);
682
tab_box->set_orientation(Gtk::Orientation::HORIZONTAL);
683
scrolled_window->set_hexpand(true);
684
scrolled_window->set_vexpand(false);
685
} else if(orientation == Gtk::Orientation::VERTICAL) {
686
scrolled_window->set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
687
tab_box->set_orientation(Gtk::Orientation::VERTICAL);
688
scrolled_window->set_vexpand(true);
689
scrolled_window->set_hexpand(false);
690
}
691
}
692
693
ContentTabBar::~ContentTabBar() {
694
add_handler.disconnect();
695
remove_handler.disconnect();
696
}
697
698
ContentStack *ContentTabBar::get_stack() const {
699
return stack;
700
}
701
702
void ContentTabBar::update_buttons() {
703
// Clear the old buttons
704
auto old_buttons = collect_children(*this->tab_box);
705
for(auto *button : old_buttons) {
706
if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) {
707
button_button->unset_child();
708
tab_box->remove(*button);
709
}
710
}
711
ContentTab* first_child = nullptr;
712
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
713
if(auto page = dynamic_cast<ContentPage*>(widget)) {
714
auto *button = Gtk::make_managed<ContentTab>(page);
715
if(!first_child) {
716
first_child = button;
717
} else {
718
button->set_group(*first_child);
719
}
720
this->tab_box->append(*button);
721
}
722
}
723
}
724
725
void ContentStack::add_page(ContentPage &child) {
726
child.redock(this);
727
}
728
729
void ContentStack::remove_with_paned() {
730
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
731
Gtk::Widget *child = nullptr;
732
if(this->get_parent() == paned->get_start_child()) {
733
child = paned->get_end_child();
734
} else if(this->get_parent() == paned->get_end_child()) {
735
child = paned->get_start_child();
736
} else {
737
return;
738
}
739
740
g_object_ref(child->gobj()); // Prevent the child from being automatically deleted
741
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()
742
paned->property_end_child().reset_value();
743
744
if(child) {
745
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(paned->get_parent())) {
746
if(parent_paned->get_start_child() == paned) {
747
parent_paned->set_start_child(*child);
748
} else if(parent_paned->get_end_child() == paned) {
749
parent_paned->set_end_child(*child);
750
}
751
} else if(auto box = dynamic_cast<Gtk::Box*>(paned->get_parent())) {
752
child->insert_after(*box, *paned);
753
paned->unparent();
754
g_object_unref(child->gobj());
755
}
756
}
757
}
758
}
759
760
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
761
this->set_child(*page->get_tab_widget());
762
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
763
this->set_halign(Gtk::Align::CENTER);
764
this->set_valign(Gtk::Align::CENTER);
765
this->get_style_context()->add_class("toggle");
766
this->get_style_context()->add_class("gpanthera-content-tab");
767
// Add/remove CSS classes when the pane is shown/hidden
768
active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() {
769
this->update_active_style();
770
});
771
drag_source = Gtk::DragSource::create();
772
drag_source->set_exclusive(false);
773
// This is to prevent the click handler from taking over grabbing the button
774
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
775
value.init(ContentPage::get_type());
776
value.set(page);
777
// Add the drag source to the button
778
this->add_controller(drag_source);
779
this->signal_clicked().connect([this, page]() {
780
page->get_stack()->set_visible_child(*page);
781
page->content_manager->set_last_operated_page(page);
782
update_active_style();
783
});
784
// Switch tabs on depress
785
auto gesture_click = Gtk::GestureClick::create();
786
gesture_click->set_button(1);
787
this->add_controller(gesture_click);
788
gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) {
789
page->get_stack()->set_visible_child(*page);
790
page->content_manager->set_last_operated_page(page);
791
update_active_style();
792
});
793
// Provide the drag data
794
drag_source->signal_prepare().connect([this](double, double) {
795
drag_source->set_actions(Gdk::DragAction::MOVE);
796
auto const paintable = Gtk::WidgetPaintable::create();
797
paintable->set_widget(*this);
798
drag_source->set_icon(paintable->get_current_image(), 0, 0);
799
return Gdk::ContentProvider::create(value);
800
}, false);
801
update_active_style();
802
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
803
this->set_opacity(0);
804
}, false);
805
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
806
// Process dropped buttons by inserting them after the current button
807
drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) {
808
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
809
810
if(widget) {
811
if(auto page = dynamic_cast<ContentPage*>(widget)) {
812
if(page->content_manager != this->page->content_manager) {
813
// If the pane is not in the same layout manager, reject
814
return false;
815
}
816
auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent()->get_parent()->get_parent()->get_parent());
817
if(switcher) {
818
auto *stack = switcher->get_stack();
819
// Move the button to the new position
820
page->redock(stack);
821
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
822
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
823
page->insert_before(*stack, *this->page);
824
} else {
825
page->insert_after(*stack, *this->page);
826
}
827
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
828
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
829
page->insert_before(*stack, *this->page);
830
} else {
831
page->insert_after(*stack, *this->page);
832
}
833
}
834
835
switcher->update_buttons();
836
}
837
}
838
}
839
840
return true; // Drop OK
841
}, false);
842
this->add_controller(drop_target);
843
// Pop out if dragged to an external location
844
drag_cancel_handler = drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
845
if(reason == Gdk::DragCancelReason::NO_TARGET) {
846
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
847
bool result = stack->signal_detach.emit(this->page);
848
if(!result) {
849
this->set_opacity(1);
850
}
851
return result;
852
}
853
this->set_opacity(1);
854
return false;
855
}, false);
856
drag_end_handler = drag_source->signal_drag_end().connect([this](const Glib::RefPtr<Gdk::Drag>&, bool drop_ok) {
857
if(drop_ok) {
858
this->set_opacity(1);
859
}
860
}, false);
861
862
// Provide a context menu
863
context_menu = Gio::Menu::create();
864
auto action_group = Gio::SimpleActionGroup::create();
865
this->insert_action_group("win", action_group);
866
867
auto close_action = Gio::SimpleAction::create("close");
868
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
869
this->close();
870
});
871
action_group->add_action(close_action);
872
context_menu->append(_("Close"), "win.close");
873
874
auto close_all_action = Gio::SimpleAction::create("close-all");
875
close_all_action->signal_activate().connect([this](const Glib::VariantBase&) {
876
for(auto page : collect_children(*this->page->get_stack())) {
877
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
878
if(!content_page->signal_close.emit()) {
879
content_page->redock(nullptr);
880
}
881
}
882
}
883
});
884
action_group->add_action(close_all_action);
885
context_menu->append(_("Close all"), "win.close-all");
886
887
auto close_others_action = Gio::SimpleAction::create("close-others");
888
close_others_action->signal_activate().connect([this](const Glib::VariantBase&) {
889
for(auto page : collect_children(*this->page->get_stack())) {
890
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
891
if(content_page != this->page && !content_page->signal_close.emit()) {
892
content_page->redock(nullptr);
893
}
894
}
895
}
896
});
897
action_group->add_action(close_others_action);
898
context_menu->append(_("Close others"), "win.close-others");
899
900
auto detach_action = Gio::SimpleAction::create("detach");
901
detach_action->signal_activate().connect([this](const Glib::VariantBase&) {
902
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
903
bool result = stack->signal_detach.emit(this->page);
904
});
905
action_group->add_action(detach_action);
906
context_menu->append(_("New window"), "win.detach");
907
908
// 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"
909
this->insert_action_group("win", action_group);
910
// Attach the context menu to the button
911
auto context_menu_signal = add_context_menu(*this);
912
popover = Gtk::make_managed<Gtk::PopoverMenu>();
913
popover->set_menu_model(context_menu);
914
popover->set_parent(*this);
915
popover->set_has_arrow(false);
916
popover->set_halign(Gtk::Align::START);
917
context_menu_signal.connect([this, action_group](double x, double y) {
918
if(this->context_menu) {
919
popover->set_pointing_to(Gdk::Rectangle(x, y, 1, 1));
920
popover->popup();
921
}
922
});
923
auto middle_click_controller = Gtk::GestureClick::create();
924
middle_click_controller->set_button(2);
925
middle_click_controller->signal_released().connect([this](int num_presses, double x, double y) {
926
this->close();
927
});
928
this->add_controller(middle_click_controller);
929
control_status_handler = this->page->signal_control_status_changed.connect([this](bool operating) {
930
if(operating) {
931
this->add_css_class("gpanthera-selected-tab");
932
} else {
933
this->remove_css_class("gpanthera-selected-tab");
934
}
935
});
936
}
937
938
void ContentTab::close() {
939
page->close();
940
}
941
942
void ContentPage::close() {
943
if(!this->signal_close.emit()) {
944
if(this->get_next_sibling()) {
945
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling()));
946
this->get_stack()->set_visible_child(*this->get_next_sibling());
947
} else if(this->get_prev_sibling()) {
948
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling()));
949
this->get_stack()->set_visible_child(*this->get_prev_sibling());
950
}
951
this->redock(nullptr);
952
}
953
}
954
955
void ContentTab::update_active_style() {
956
if(this->page->get_stack()->get_visible_child() == this->page) {
957
this->add_css_class("checked");
958
this->add_css_class("gpanthera-dock-button-active");
959
this->set_active(true);
960
} else {
961
this->remove_css_class("checked");
962
this->remove_css_class("gpanthera-dock-button-active");
963
this->set_active(false);
964
}
965
}
966
967
ContentTab::~ContentTab() {
968
active_style_handler.disconnect();
969
drag_end_handler.disconnect();
970
drag_cancel_handler.disconnect();
971
control_status_handler.disconnect();
972
}
973
974
Gtk::Widget *ContentPage::get_tab_widget() const {
975
return this->tab_widget;
976
}
977
978
ContentStack *ContentPage::get_stack() const {
979
return this->stack;
980
}
981
982
void ContentPage::lose_visibility() {
983
if(this->get_next_sibling()) {
984
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling()));
985
this->get_stack()->set_visible_child(*this->get_next_sibling());
986
} else if(this->get_prev_sibling()) {
987
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling()));
988
this->get_stack()->set_visible_child(*this->get_prev_sibling());
989
}
990
}
991
992
void ContentPage::redock(ContentStack *stack) {
993
if(stack == nullptr) {
994
if(this->stack) {
995
this->stack->remove(*this);
996
if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && !this->stack->get_first_child()) {
997
this->stack->remove_with_paned();
998
}
999
}
1000
auto old_stack = this->stack;
1001
if(old_stack) {
1002
if(!old_stack->get_first_child()) {
1003
old_stack->signal_leave_empty.emit();
1004
}
1005
}
1006
this->stack = nullptr;
1007
this->last_stack = nullptr;
1008
return;
1009
}
1010
// Check if the stack is now empty, in which case we should remove it
1011
if(this->stack == stack) {
1012
return;
1013
}
1014
if(this->stack != nullptr) {
1015
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()))) {
1016
this->stack->remove(*this);
1017
this->stack->remove_with_paned();
1018
} else if(this->get_parent() == this->stack) {
1019
this->stack->remove(*this);
1020
}
1021
}
1022
auto old_stack = this->stack;
1023
this->stack = stack;
1024
this->last_stack = stack;
1025
this->stack->add(*this);
1026
if(old_stack) {
1027
if(!old_stack->get_first_child()) {
1028
old_stack->signal_leave_empty.emit();
1029
}
1030
}
1031
}
1032
1033
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
1034
Gtk::Overlay(), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) {
1035
this->set_name("gpanthera_content_page");
1036
this->set_child(*child);
1037
this->set_tab_widget(tab_widget);
1038
this->set_margin_top(0);
1039
this->set_margin_bottom(0);
1040
this->set_margin_start(0);
1041
this->set_margin_end(0);
1042
if(stack) {
1043
stack->add_page(*this);
1044
this->content_manager->add_stack(this->stack);
1045
}
1046
auto click_controller = Gtk::GestureClick::create();
1047
click_controller->set_button(0);
1048
click_controller->signal_pressed().connect([this](int num_presses, double x, double y) {
1049
this->content_manager->set_last_operated_page(this);
1050
});
1051
this->property_has_focus().signal_changed().connect([this]() {
1052
if(this->property_has_focus()) {
1053
this->content_manager->set_last_operated_page(this);
1054
}
1055
});
1056
click_controller->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
1057
this->add_controller(click_controller);
1058
}
1059
1060
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
1061
}
1062
1063
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
1064
this->tab_widget = tab_widget;
1065
}
1066
1067
ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) {
1068
this->set_child(*notebook);
1069
this->set_decorated(true);
1070
this->set_resizable(true);
1071
}
1072
1073
Gtk::PositionType ContentNotebook::get_tab_position() const {
1074
return this->tab_position;
1075
}
1076
1077
ContentTabBar *ContentNotebook::get_switcher() const {
1078
return this->switcher;
1079
}
1080
1081
ContentStack *ContentNotebook::get_stack() const {
1082
return this->stack;
1083
}
1084
1085
ContentPage *ContentManager::get_last_operated_page() const {
1086
return this->last_operated_page;
1087
}
1088
1089
void ContentManager::set_last_operated_page(ContentPage *page) {
1090
if(this->last_operated_page) {
1091
this->last_operated_page->signal_control_status_changed.emit(false);
1092
}
1093
this->last_operated_page = page;
1094
page->signal_control_status_changed.emit(true);
1095
this->signal_page_operated.emit(page);
1096
}
1097
1098
DockWindow *DockablePane::get_window() const {
1099
return this->window;
1100
}
1101
1102
std::string LayoutManager::get_layout_as_json() const {
1103
nlohmann::json json;
1104
std::unordered_set<DockablePane*> panes_set;
1105
for(auto pane: panes) {
1106
panes_set.insert(pane);
1107
}
1108
for(auto stack: stacks) {
1109
for(auto pane: collect_children(*stack)) {
1110
if(dynamic_cast<DockablePane*>(pane)) {
1111
panes_set.erase(dynamic_cast<DockablePane*>(pane));
1112
json[stack->id].push_back(dynamic_cast<DockablePane*>(pane)->get_identifier());
1113
}
1114
}
1115
}
1116
for(auto pane: panes_set) {
1117
if(pane->get_window()) {
1118
json[""].push_back(pane->get_identifier());
1119
}
1120
}
1121
return json.dump();
1122
}
1123
1124
void LayoutManager::restore_json_layout(const std::string &json_string) {
1125
nlohmann::json json = nlohmann::json::parse(json_string);
1126
for(auto stack: stacks) {
1127
if(json.contains(stack->id)) {
1128
for(auto const &pane_id: json[stack->id]) {
1129
for(auto pane: panes) {
1130
// TODO: could probably be done with a better complexity if there are
1131
// many panes
1132
if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) {
1133
stack->add_pane(*pane);
1134
break;
1135
}
1136
}
1137
}
1138
}
1139
}
1140
// Process popped-out panes
1141
for(auto const &pane_id: json[""]) {
1142
for(auto pane: panes) {
1143
// TODO: could probably be done with a better complexity if there are
1144
// many panes
1145
if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) {
1146
pane->pop_out();
1147
break;
1148
}
1149
}
1150
}
1151
}
1152
} // namespace gPanthera
1153