GTK docking interfaces

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++ • 37.64 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
#define _(STRING) gettext(STRING)
10
11
namespace gPanthera {
12
sigc::signal<void(double, double)> add_context_menu(Gtk::Widget &widget) {
13
sigc::signal<void(double, double)> signal;
14
// Add a context menu to a widget
15
auto gesture_click = Gtk::GestureClick::create();
16
gesture_click->set_button(3);
17
auto gesture_keyboard = Gtk::EventControllerKey::create();
18
gesture_click->signal_pressed().connect([&widget, signal](int n_press, double x, double y) {
19
if(n_press == 1) {
20
signal.emit(x, y);
21
}
22
}, false);
23
gesture_keyboard->signal_key_pressed().connect([&widget, signal](int keyval, int keycode, Gdk::ModifierType state) {
24
if((keyval == GDK_KEY_F10) && (state == Gdk::ModifierType::SHIFT_MASK) || keyval == GDK_KEY_Menu) {
25
// auto popover = Gtk::make_managed<Gtk::PopoverMenu>();
26
// popover->set_menu_model(menu);
27
// popover->set_has_arrow(false);
28
// popover->set_halign(Gtk::Align::START);
29
// popover->set_pointing_to(Gdk::Rectangle(0, 0, 1, 1));
30
// popover->set_parent(widget);
31
// popover->popup();
32
signal.emit(0, 0);
33
}
34
return true;
35
}, false);
36
widget.add_controller(gesture_click);
37
widget.add_controller(gesture_keyboard);
38
return signal;
39
}
40
41
std::vector<Gtk::Widget*> collect_children(Gtk::Widget &widget) {
42
// Get a vector of the children of a GTK widget, since the container API was removed in GTK 4
43
std::vector<Gtk::Widget*> children;
44
for(auto *child = widget.get_first_child(); child; child = child->get_next_sibling()) {
45
children.push_back(child);
46
}
47
return children;
48
}
49
50
Gtk::Image *copy_image(Gtk::Image *image) {
51
// Generate a new Gtk::Image with the same contents as an existing one
52
if(image->get_storage_type() == Gtk::Image::Type::PAINTABLE) {
53
return Gtk::make_managed<Gtk::Image>(image->get_paintable());
54
} else if(image->get_storage_type() == Gtk::Image::Type::ICON_NAME) {
55
auto new_image = Gtk::make_managed<Gtk::Image>();
56
new_image->set_from_icon_name(image->get_icon_name());
57
return new_image;
58
} else {
59
return nullptr;
60
}
61
}
62
63
void init() {
64
g_log_set_always_fatal(G_LOG_LEVEL_CRITICAL);
65
// Set up gettext configurationIsn't
66
bindtextdomain("gpanthera", "./locales");
67
textdomain("gpanthera");
68
}
69
70
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)
71
: Gtk::Box(Gtk::Orientation::VERTICAL, 0), name(name) {
72
if(icon) {
73
this->icon = icon;
74
}
75
if(stack) {
76
this->stack = stack;
77
}
78
this->layout = std::move(layout);
79
this->label.set_text(label);
80
// This should be replaced with a custom class in the future
81
header = std::make_unique<Gtk::HeaderBar>();
82
header->set_show_title_buttons(false);
83
if(custom_header) {
84
header->set_title_widget(*custom_header);
85
} else {
86
header->set_title_widget(this->label);
87
}
88
header->add_css_class("gpanthera-dock-titlebar");
89
auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>();
90
header_menu_button->set_direction(Gtk::ArrowType::NONE);
91
92
// Pane menu
93
this->action_group = Gio::SimpleActionGroup::create();
94
header_menu_button->insert_action_group("win", action_group);
95
96
// Close action
97
auto close_action = Gio::SimpleAction::create("close");
98
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
99
if(this->stack) {
100
this->stack->set_visible_child("");
101
}
102
});
103
action_group->add_action(close_action);
104
header_menu->append(_("Close"), "win.close");
105
106
// Pop out action
107
auto pop_out_action = Gio::SimpleAction::create("pop_out");
108
pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) {
109
if(this->stack) {
110
this->pop_out();
111
}
112
});
113
action_group->add_action(pop_out_action);
114
header_menu->append(_("Pop out"), "win.pop_out");
115
116
// Move menu
117
auto move_menu = Gio::Menu::create();
118
for(auto &this_stack : this->layout->stacks) {
119
auto action_name = "move_" + this_stack->name;
120
auto move_action = Gio::SimpleAction::create(action_name);
121
move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) {
122
this_stack->add_pane(*this);
123
});
124
action_group->add_action(move_action);
125
move_menu->append(this_stack->name, "win." + action_name);
126
}
127
128
// Add move submenu
129
header_menu->append_submenu(_("Move"), move_menu);
130
131
// Switch to traditional (nested) submenus, not sliding
132
auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED);
133
popover_menu->set_has_arrow(false);
134
header_menu_button->set_popover(*popover_menu);
135
136
// TODO: Add a context menu as well
137
138
header->pack_end(*header_menu_button);
139
140
this->prepend(*header);
141
this->child = &child;
142
this->append(child);
143
}
144
145
Gtk::Stack *DockablePane::get_stack() const {
146
return stack;
147
}
148
149
void DockablePane::redock(DockStack *stack) {
150
if(this->window != nullptr) {
151
this->window->hide();
152
// Put the titlebar back
153
this->window->unset_titlebar();
154
this->window->set_decorated(false);
155
this->header->get_style_context()->remove_class("titlebar");
156
this->prepend(*this->header);
157
this->window->unset_child();
158
this->window->close();
159
} else if(this->get_parent() && this->stack == this->get_parent()) {
160
this->stack->remove(*this);
161
}
162
this->stack = stack;
163
this->last_stack = stack;
164
this->stack->add(*this, this->get_identifier());
165
if(this->window != nullptr) {
166
this->window->destroy();
167
delete this->window;
168
this->window = nullptr;
169
// Re-enable the pop out option
170
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
171
action->set_enabled(true);
172
this->header->get_style_context()->remove_class("gpanthera-dock-titlebar-popout");
173
}
174
}
175
176
void DockablePane::pop_out() {
177
if(this->stack != nullptr) {
178
this->stack->remove(*this);
179
this->stack = nullptr;
180
}
181
182
if(this->window == nullptr) {
183
// Remove the header bar from the pane, so it can be used as the titlebar of the window
184
this->remove(*this->header);
185
this->window = new DockWindow(this);
186
this->window->set_titlebar(*this->header);
187
this->window->set_child(*this);
188
this->window->set_decorated(true);
189
// Grey out the pop-out option
190
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
191
action->set_enabled(false);
192
this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout");
193
}
194
this->window->present();
195
}
196
197
Glib::ustring DockablePane::get_identifier() const {
198
return name;
199
}
200
201
Gtk::Image *DockablePane::get_icon() const {
202
return icon;
203
}
204
205
Gtk::Widget *DockablePane::get_child() const {
206
return child;
207
}
208
209
Gtk::Label *DockablePane::get_label() {
210
return &label;
211
}
212
213
LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") {
214
}
215
216
void LayoutManager::add_pane(DockablePane *pane) {
217
panes.push_back(pane);
218
}
219
220
void LayoutManager::add_stack(DockStack *stack) {
221
stacks.push_back(stack);
222
}
223
224
void LayoutManager::remove_pane(DockablePane *pane) {
225
panes.erase(std::ranges::remove(panes, pane).begin(), panes.end());
226
}
227
228
void LayoutManager::remove_stack(DockStack *stack) {
229
stacks.erase(std::ranges::remove(stacks, stack).begin(), stacks.end());
230
}
231
232
BaseStack::BaseStack() : Gtk::Stack() {
233
}
234
235
DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name) : BaseStack(), layout(layout), name(name) {
236
auto empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
237
this->add(*empty_child, "");
238
// Add the stack to a layout manager
239
this->layout->add_stack(this);
240
241
// Hide the stack when no child is visible
242
this->property_visible_child_name().signal_changed().connect([this]() {
243
if(this->get_visible_child_name() == "") {
244
this->hide();
245
} else {
246
this->show();
247
}
248
});
249
250
// Also hide when the visible child is removed
251
this->signal_child_removed.connect([this](Gtk::Widget* const &child) {
252
if(this->get_visible_child_name() == "") {
253
this->hide();
254
}
255
});
256
257
this->set_visible_child("");
258
this->hide();
259
}
260
261
DockWindow::DockWindow(DockablePane *pane) {
262
this->pane = pane;
263
this->set_child(*pane);
264
// Attempting to close the window should redock the pane so it doesn't vanish
265
this->signal_close_request().connect([this]() {
266
this->pane->redock(this->pane->last_stack);
267
return true;
268
}, false);
269
}
270
271
DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
272
auto update_callback = [this](Gtk::Widget*) {
273
this->update_buttons();
274
};
275
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
276
stack->signal_child_added.connect(update_callback);
277
stack->signal_child_removed.connect(update_callback);
278
this->get_style_context()->add_class("gpanthera-dock-switcher");
279
drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
280
this->add_controller(drop_target);
281
// Process dropped buttons
282
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
283
const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get();
284
285
if(widget) {
286
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
287
this->stack->add_pane(*pane);
288
}
289
}
290
291
return true; // Drop OK
292
}, false);
293
}
294
295
DockStack *DockStackSwitcher::get_stack() const {
296
return stack;
297
}
298
299
DockStackSwitcher::~DockStackSwitcher() {
300
add_handler.disconnect();
301
remove_handler.disconnect();
302
}
303
304
DockButton::DockButton(DockablePane *pane) : Gtk::ToggleButton(), pane(pane) {
305
if(pane->get_icon()) {
306
this->set_child(*copy_image(pane->get_icon()));
307
}
308
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
309
this->set_tooltip_text(pane->get_label()->get_text());
310
this->set_halign(Gtk::Align::CENTER);
311
this->set_valign(Gtk::Align::CENTER);
312
this->get_style_context()->add_class("toggle");
313
this->get_style_context()->add_class("gpanthera-dock-button");
314
// Add/remove CSS classes when the pane is shown/hidden
315
active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
316
this->update_active_style();
317
});
318
drag_source = Gtk::DragSource::create();
319
drag_source->set_exclusive(false);
320
// This is to prevent the click handler from taking over grabbing the button
321
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
322
value.init(DockablePane::get_type());
323
value.set(pane);
324
// Add the drag source to the button
325
this->add_controller(drag_source);
326
this->signal_clicked().connect([this, pane]() {
327
if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) {
328
pane->get_stack()->set_visible_child("");
329
} else {
330
pane->get_stack()->set_visible_child(pane->get_identifier());
331
}
332
});
333
// Provide the drag data
334
drag_source->signal_prepare().connect([this](double, double) {
335
drag_source->set_actions(Gdk::DragAction::MOVE);
336
auto const paintable = Gtk::WidgetPaintable::create();
337
paintable->set_widget(*this);
338
drag_source->set_icon(paintable->get_current_image(), 0, 0);
339
return Gdk::ContentProvider::create(value);
340
}, false);
341
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
342
this->set_opacity(0);
343
}, false);
344
// Pop out if dragged to an external location
345
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
346
if(reason == Gdk::DragCancelReason::NO_TARGET) {
347
this->pane->pop_out();
348
return true;
349
}
350
this->set_opacity(1);
351
return false;
352
}, false);
353
// Add a drop target to the button
354
auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
355
drop_target->set_actions(Gdk::DragAction::MOVE);
356
drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
357
this->add_controller(drop_target);
358
// Process dropped buttons by inserting them after the current button
359
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
360
const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get();
361
362
if(widget) {
363
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
364
if(pane->layout != this->pane->layout) {
365
// If the pane is not in the same layout manager, reject
366
return false;
367
}
368
auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent());
369
if(switcher) {
370
auto *stack = switcher->get_stack();
371
// Move the button to the new position
372
pane->redock(stack);
373
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
374
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
375
pane->insert_before(*stack, *this->pane);
376
} else {
377
pane->insert_after(*stack, *this->pane);
378
}
379
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
380
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
381
pane->insert_before(*stack, *this->pane);
382
} else {
383
pane->insert_after(*stack, *this->pane);
384
}
385
}
386
387
switcher->update_buttons();
388
}
389
}
390
}
391
392
return true; // Drop OK
393
}, false);
394
this->update_active_style();
395
}
396
397
void DockButton::update_active_style() {
398
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
399
this->add_css_class("checked");
400
this->add_css_class("gpanthera-dock-button-active");
401
this->set_active(true);
402
} else {
403
this->remove_css_class("checked");
404
this->remove_css_class("gpanthera-dock-button-active");
405
this->set_active(false);
406
}
407
}
408
409
DockButton::~DockButton() {
410
active_style_handler.disconnect();
411
}
412
413
void DockStackSwitcher::update_buttons() {
414
// Clear the old buttons
415
auto old_buttons = collect_children(*this);
416
for(auto *button : old_buttons) {
417
remove(*button);
418
}
419
DockButton* first_child = nullptr;
420
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
421
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
422
auto *button = Gtk::make_managed<DockButton>(pane);
423
if(!first_child) {
424
first_child = button;
425
} else {
426
button->set_group(*first_child);
427
}
428
if(pane->get_identifier() != Glib::ustring("")) {
429
append(*button);
430
}
431
}
432
}
433
}
434
435
void DockStack::add_pane(DockablePane &child) {
436
child.redock(this);
437
}
438
439
void ContentManager::add_stack(ContentStack *stack) {
440
this->stacks.push_back(stack);
441
}
442
443
void ContentManager::remove_stack(ContentStack *stack) {
444
this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end());
445
}
446
447
void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) {
448
Gtk::Stack::add(child, name);
449
signal_child_added.emit(&child);
450
}
451
452
void BaseStack::add(Gtk::Widget &child) {
453
Gtk::Stack::add(child);
454
signal_child_added.emit(&child);
455
}
456
457
void BaseStack::remove(Gtk::Widget &child) {
458
Gtk::Stack::remove(child);
459
signal_child_removed.emit(&child);
460
}
461
462
ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager)
463
: BaseStack(), content_manager(std::move(content_manager)) {
464
this->set_name("gpanthera_content_stack");
465
this->content_manager->add_stack(this);
466
467
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
468
this->add_controller(drop_target);
469
// Process dropped buttons
470
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
471
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
472
473
if(auto page = dynamic_cast<ContentPage*>(widget)) {
474
if(page->get_stack() == this && !this->get_first_child()->get_next_sibling()) {
475
// Don't allow splitting if there are no more pages
476
return false;
477
}
478
if(!(page->content_manager == this->content_manager)) {
479
// If the page is not in the same content manager, reject
480
return false;
481
}
482
double width = this->get_allocated_width(), height = this->get_allocated_height();
483
// Split based on the drop position
484
if(!(x < width / 4 || x > width * 3 / 4 || y < height / 4 || y > height * 3 / 4)) {
485
// If the drop position is not at a quarter to the edges, move to the same stack
486
this->add_page(*page);
487
return true;
488
}
489
auto new_stack = Gtk::make_managed<ContentStack>(this->content_manager);
490
auto this_notebook = dynamic_cast<ContentNotebook*>(this->get_parent());
491
auto new_switcher = Gtk::make_managed<ContentTabBar>(new_stack, this_notebook ? this_notebook->get_switcher()->get_orientation() : Gtk::Orientation::HORIZONTAL);
492
auto new_notebook = Gtk::make_managed<ContentNotebook>(new_stack, new_switcher, this_notebook ? this_notebook->get_tab_position() : Gtk::PositionType::TOP);
493
new_stack->add_page(*page);
494
new_stack->set_visible_child(*page);
495
if(this->get_first_child()) {
496
this->set_visible_child(*this->get_first_child());
497
}
498
if(x < width / 4) {
499
this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::START);
500
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
501
paned->set_start_child(*new_notebook);
502
}
503
} else if(x > width * 3 / 4) {
504
this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::END);
505
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
506
paned->set_end_child(*new_notebook);
507
}
508
} else if(y < height / 4) {
509
this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::START);
510
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
511
paned->set_start_child(*new_notebook);
512
}
513
} else if(y > height * 3 / 4) {
514
this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::END);
515
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
516
paned->set_end_child(*new_notebook);
517
}
518
}
519
}
520
521
return true; // Drop OK
522
}, false);
523
}
524
525
ContentNotebook::ContentNotebook(ContentStack *stack, ContentTabBar *switcher, Gtk::PositionType tab_position) : Gtk::Box(), stack(stack), switcher(switcher), tab_position(tab_position) {
526
this->append(*stack);
527
if(tab_position == Gtk::PositionType::TOP || tab_position == Gtk::PositionType::BOTTOM) {
528
this->set_orientation(Gtk::Orientation::VERTICAL);
529
if(tab_position == Gtk::PositionType::TOP) {
530
this->prepend(*switcher);
531
} else if(tab_position == Gtk::PositionType::BOTTOM) {
532
this->append(*switcher);
533
}
534
} else if(tab_position == Gtk::PositionType::LEFT || tab_position == Gtk::PositionType::RIGHT) {
535
this->set_orientation(Gtk::Orientation::HORIZONTAL);
536
if(tab_position == Gtk::PositionType::LEFT) {
537
this->prepend(*switcher);
538
} else if(tab_position == Gtk::PositionType::RIGHT) {
539
this->append(*switcher);
540
}
541
}
542
}
543
544
void ContentStack::make_paned(Gtk::Orientation orientation, Gtk::PackType pack_type) {
545
auto *parent = this->get_parent();
546
if(auto notebook = dynamic_cast<ContentNotebook*>(parent)) {
547
auto *paned = Gtk::make_managed<Gtk::Paned>(orientation);
548
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(notebook->get_parent())) {
549
if(parent_paned->get_start_child() == notebook) {
550
notebook->unparent();
551
parent_paned->set_start_child(*paned);
552
} else if(parent_paned->get_end_child() == notebook) {
553
notebook->unparent();
554
parent_paned->set_end_child(*paned);
555
}
556
} else if(auto box = dynamic_cast<Gtk::Box*>(notebook->get_parent())) {
557
auto previous_child = notebook->get_prev_sibling();
558
box->remove(*notebook);
559
if(previous_child) {
560
paned->insert_after(*box, *previous_child);
561
} else {
562
box->prepend(*paned);
563
}
564
}
565
if(pack_type == Gtk::PackType::START) {
566
paned->set_end_child(*notebook);
567
} else if(pack_type == Gtk::PackType::END) {
568
paned->set_start_child(*notebook);
569
}
570
}
571
}
572
573
ContentTabBar::ContentTabBar(ContentStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
574
this->get_style_context()->add_class("gpanthera-content-tab-bar");
575
this->set_margin_top(0);
576
this->set_margin_bottom(0);
577
this->set_margin_start(0);
578
this->set_margin_end(0);
579
580
auto update_callback = [this](Gtk::Widget*) {
581
this->update_buttons();
582
};
583
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
584
stack->signal_child_added.connect(update_callback);
585
stack->signal_child_removed.connect(update_callback);
586
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
587
this->add_controller(drop_target);
588
// Process dropped buttons
589
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
590
if(const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get()) {
591
if(auto page = dynamic_cast<ContentPage*>(widget)) {
592
this->stack->add_page(*page);
593
}
594
}
595
596
return true; // Drop OK
597
}, false);
598
}
599
600
ContentTabBar::~ContentTabBar() {
601
add_handler.disconnect();
602
remove_handler.disconnect();
603
}
604
605
ContentStack *ContentTabBar::get_stack() const {
606
return stack;
607
}
608
609
void ContentTabBar::update_buttons() {
610
// Clear the old buttons
611
auto old_buttons = collect_children(*this);
612
for(auto *button : old_buttons) {
613
if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) {
614
button_button->unset_child();
615
}
616
remove(*button);
617
}
618
ContentTab* first_child = nullptr;
619
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
620
if(auto page = dynamic_cast<ContentPage*>(widget)) {
621
auto *button = Gtk::make_managed<ContentTab>(page);
622
if(!first_child) {
623
first_child = button;
624
} else {
625
button->set_group(*first_child);
626
}
627
this->append(*button);
628
}
629
}
630
}
631
632
void ContentStack::add_page(ContentPage &child) {
633
child.redock(this);
634
}
635
636
void ContentStack::remove_with_paned() {
637
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
638
Gtk::Widget *child = nullptr;
639
if(this->get_parent() == paned->get_start_child()) {
640
child = paned->get_end_child();
641
} else if(this->get_parent() == paned->get_end_child()) {
642
child = paned->get_start_child();
643
} else {
644
return;
645
}
646
647
g_object_ref(child->gobj()); // Prevent the child from being automatically deleted
648
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()
649
paned->property_end_child().reset_value();
650
651
if(child) {
652
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(paned->get_parent())) {
653
if(parent_paned->get_start_child() == paned) {
654
parent_paned->set_start_child(*child);
655
} else if(parent_paned->get_end_child() == paned) {
656
parent_paned->set_end_child(*child);
657
}
658
} else if(auto box = dynamic_cast<Gtk::Box*>(paned->get_parent())) {
659
std::cout << child->get_name() << std::endl;
660
child->insert_after(*box, *paned);
661
paned->unparent();
662
g_object_unref(child->gobj());
663
}
664
}
665
}
666
}
667
668
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
669
this->set_child(*page->get_tab_widget());
670
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
671
this->set_halign(Gtk::Align::CENTER);
672
this->set_valign(Gtk::Align::CENTER);
673
this->get_style_context()->add_class("toggle");
674
this->get_style_context()->add_class("gpanthera-content-tab");
675
// Add/remove CSS classes when the pane is shown/hidden
676
active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() {
677
this->update_active_style();
678
});
679
drag_source = Gtk::DragSource::create();
680
drag_source->set_exclusive(false);
681
// This is to prevent the click handler from taking over grabbing the button
682
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
683
value.init(ContentPage::get_type());
684
value.set(page);
685
// Add the drag source to the button
686
this->add_controller(drag_source);
687
this->signal_clicked().connect([this, page]() {
688
page->get_stack()->set_visible_child(*page);
689
update_active_style();
690
});
691
// Switch tabs on depress
692
auto gesture_click = Gtk::GestureClick::create();
693
gesture_click->set_button(1);
694
this->add_controller(gesture_click);
695
gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) {
696
page->get_stack()->set_visible_child(*page);
697
update_active_style();
698
});
699
// Provide the drag data
700
drag_source->signal_prepare().connect([this](double, double) {
701
drag_source->set_actions(Gdk::DragAction::MOVE);
702
auto const paintable = Gtk::WidgetPaintable::create();
703
paintable->set_widget(*this);
704
drag_source->set_icon(paintable->get_current_image(), 0, 0);
705
return Gdk::ContentProvider::create(value);
706
}, false);
707
update_active_style();
708
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
709
this->set_opacity(0);
710
}, false);
711
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
712
// Process dropped buttons by inserting them after the current button
713
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
714
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
715
716
if(widget) {
717
if(auto page = dynamic_cast<ContentPage*>(widget)) {
718
if(page->content_manager != this->page->content_manager) {
719
// If the pane is not in the same layout manager, reject
720
return false;
721
}
722
auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent());
723
if(switcher) {
724
auto *stack = switcher->get_stack();
725
// Move the button to the new position
726
page->redock(stack);
727
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
728
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
729
page->insert_before(*stack, *this->page);
730
} else {
731
page->insert_after(*stack, *this->page);
732
}
733
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
734
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
735
page->insert_before(*stack, *this->page);
736
} else {
737
page->insert_after(*stack, *this->page);
738
}
739
}
740
741
switcher->update_buttons();
742
}
743
}
744
}
745
746
return true; // Drop OK
747
}, false);
748
this->add_controller(drop_target);
749
// Pop out if dragged to an external location
750
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
751
if(reason == Gdk::DragCancelReason::NO_TARGET) {
752
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
753
bool result = stack->signal_detach.emit(this->page);
754
if(!result) {
755
this->set_opacity(1);
756
}
757
return result;
758
}
759
this->set_opacity(1);
760
return false;
761
}, false);
762
763
// Provide a context menu
764
context_menu = Gio::Menu::create();
765
auto action_group = Gio::SimpleActionGroup::create();
766
this->insert_action_group("win", action_group);
767
auto close_action = Gio::SimpleAction::create("close");
768
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
769
if(!this->page->signal_close.emit()) {
770
if(this->page->get_next_sibling()) {
771
this->page->get_stack()->set_visible_child(*this->page->get_next_sibling());
772
} else if(this->page->get_prev_sibling()) {
773
this->page->get_stack()->set_visible_child(*this->page->get_prev_sibling());
774
}
775
this->page->redock(nullptr);
776
}
777
});
778
action_group->add_action(close_action);
779
context_menu->append(_("Close"), "win.close");
780
// 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"
781
this->insert_action_group("win", action_group);
782
// Attach the context menu to the button
783
auto context_menu_signal = add_context_menu(*this);
784
context_menu_signal.connect([this, action_group](double x, double y) {
785
if(this->context_menu) {
786
auto popover = Gtk::make_managed<Gtk::PopoverMenu>();
787
popover->set_menu_model(context_menu);
788
popover->set_parent(*this);
789
popover->set_has_arrow(false);
790
popover->set_halign(Gtk::Align::START);
791
popover->set_pointing_to(Gdk::Rectangle(x, y, 1, 1));
792
popover->popup();
793
}
794
});
795
}
796
797
void ContentTab::update_active_style() {
798
if(this->page->get_stack()->get_visible_child() == this->page) {
799
this->add_css_class("checked");
800
this->add_css_class("gpanthera-dock-button-active");
801
this->set_active(true);
802
} else {
803
this->remove_css_class("checked");
804
this->remove_css_class("gpanthera-dock-button-active");
805
this->set_active(false);
806
}
807
}
808
809
ContentTab::~ContentTab() {
810
active_style_handler.disconnect();
811
}
812
813
Gtk::Widget *ContentPage::get_tab_widget() const {
814
return this->tab_widget;
815
}
816
817
ContentStack *ContentPage::get_stack() const {
818
return this->stack;
819
}
820
821
void ContentPage::redock(ContentStack *stack) {
822
if(stack == nullptr) {
823
if(this->stack) {
824
this->stack->remove(*this);
825
if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && !this->stack->get_first_child()) {
826
this->stack->remove_with_paned();
827
}
828
}
829
auto old_stack = this->stack;
830
if(old_stack) {
831
if(!old_stack->get_first_child()) {
832
old_stack->signal_leave_empty.emit();
833
}
834
}
835
this->stack = nullptr;
836
this->last_stack = nullptr;
837
return;
838
}
839
// Check if the stack is now empty, in which case we should remove it
840
if(this->stack == stack) {
841
return;
842
}
843
if(this->stack != nullptr) {
844
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()))) {
845
this->stack->remove(*this);
846
this->stack->remove_with_paned();
847
} else if(this->get_parent() == this->stack) {
848
this->stack->remove(*this);
849
}
850
}
851
auto old_stack = this->stack;
852
this->stack = stack;
853
this->last_stack = stack;
854
this->stack->add(*this);
855
if(old_stack) {
856
if(!old_stack->get_first_child()) {
857
old_stack->signal_leave_empty.emit();
858
}
859
}
860
}
861
862
Gtk::Widget *ContentPage::get_child() const {
863
return this->child;
864
}
865
866
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
867
Gtk::Box(Gtk::Orientation::VERTICAL, 0), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) {
868
this->set_name("gpanthera_content_page");
869
this->append(*child);
870
this->set_tab_widget(tab_widget);
871
this->set_margin_top(0);
872
this->set_margin_bottom(0);
873
this->set_margin_start(0);
874
this->set_margin_end(0);
875
if(stack) {
876
stack->add_page(*this);
877
this->content_manager->add_stack(this->stack);
878
}
879
}
880
881
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
882
}
883
884
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
885
this->tab_widget = tab_widget;
886
}
887
888
ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) {
889
this->set_child(*notebook);
890
this->set_decorated(true);
891
this->set_resizable(true);
892
}
893
894
Gtk::PositionType ContentNotebook::get_tab_position() const {
895
return this->tab_position;
896
}
897
898
ContentTabBar *ContentNotebook::get_switcher() const {
899
return this->switcher;
900
}
901
902
ContentStack *ContentNotebook::get_stack() const {
903
return this->stack;
904
}
905
} // namespace gPanthera
906