GTK docking interfaces and more

Important information: Google announced that, from September 2026, Android devices will require ALL apps to be signed by Google, effectively leading to an iOS situation. Value your right to a computer that does what you want; do not tolerate this monopolistic practice! Contact me if you don't understand why it is bad. Click to learn more.

 gpanthera.cc

View raw Download
text/x-c++ • 41.6 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, std::function<bool(ContentPage*)> detach_handler)
463
: BaseStack(), content_manager(std::move(content_manager)) {
464
this->set_name("gpanthera_content_stack");
465
this->content_manager->add_stack(this);
466
if(detach_handler) {
467
this->detach_handler = detach_handler;
468
this->signal_detach.connect([this](ContentPage *widget) {
469
return this->detach_handler(widget);
470
});
471
}
472
473
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
474
this->add_controller(drop_target);
475
// Process dropped buttons
476
drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) {
477
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
478
479
if(auto page = dynamic_cast<ContentPage*>(widget)) {
480
if(page->get_stack() == this && !this->get_first_child()->get_next_sibling()) {
481
// Don't allow splitting if there are no more pages
482
return false;
483
}
484
if(!(page->content_manager == this->content_manager)) {
485
// If the page is not in the same content manager, reject
486
return false;
487
}
488
double width = this->get_allocated_width(), height = this->get_allocated_height();
489
// Split based on the drop position
490
if(!(x < width / 4 || x > width * 3 / 4 || y < height / 4 || y > height * 3 / 4)) {
491
if(page->get_stack() == this) {
492
return false;
493
}
494
page->lose_visibility();
495
// If the drop position is not at a quarter to the edges, move to the same stack
496
this->add_page(*page);
497
return true;
498
}
499
page->lose_visibility();
500
auto new_stack = Gtk::make_managed<ContentStack>(this->content_manager, this->detach_handler);
501
auto this_notebook = dynamic_cast<ContentNotebook*>(this->get_parent());
502
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());
503
auto new_notebook = Gtk::make_managed<ContentNotebook>(new_stack, new_switcher, this_notebook ? this_notebook->get_tab_position() : Gtk::PositionType::TOP);
504
new_stack->add_page(*page);
505
new_stack->set_visible_child(*page);
506
if(x < width / 4) {
507
this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::START);
508
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
509
paned->set_start_child(*new_notebook);
510
}
511
} else if(x > width * 3 / 4) {
512
this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::END);
513
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
514
paned->set_end_child(*new_notebook);
515
}
516
} else if(y < height / 4) {
517
this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::START);
518
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
519
paned->set_start_child(*new_notebook);
520
}
521
} else if(y > height * 3 / 4) {
522
this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::END);
523
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
524
paned->set_end_child(*new_notebook);
525
}
526
}
527
}
528
529
return true; // Drop OK
530
}, false);
531
}
532
533
std::function<bool(ContentPage*)> ContentStack::get_detach_handler() const {
534
return this->detach_handler;
535
}
536
537
ContentNotebook::ContentNotebook(ContentStack *stack, ContentTabBar *switcher, Gtk::PositionType tab_position) : Gtk::Box(), stack(stack), switcher(switcher), tab_position(tab_position) {
538
this->append(*stack);
539
if(tab_position == Gtk::PositionType::TOP || tab_position == Gtk::PositionType::BOTTOM) {
540
this->set_orientation(Gtk::Orientation::VERTICAL);
541
if(tab_position == Gtk::PositionType::TOP) {
542
this->prepend(*switcher);
543
} else if(tab_position == Gtk::PositionType::BOTTOM) {
544
this->append(*switcher);
545
}
546
} else if(tab_position == Gtk::PositionType::LEFT || tab_position == Gtk::PositionType::RIGHT) {
547
this->set_orientation(Gtk::Orientation::HORIZONTAL);
548
if(tab_position == Gtk::PositionType::LEFT) {
549
this->prepend(*switcher);
550
} else if(tab_position == Gtk::PositionType::RIGHT) {
551
this->append(*switcher);
552
}
553
}
554
}
555
556
void ContentStack::make_paned(Gtk::Orientation orientation, Gtk::PackType pack_type) {
557
auto *parent = this->get_parent();
558
if(auto notebook = dynamic_cast<ContentNotebook*>(parent)) {
559
auto *paned = Gtk::make_managed<Gtk::Paned>(orientation);
560
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(notebook->get_parent())) {
561
if(parent_paned->get_start_child() == notebook) {
562
notebook->unparent();
563
parent_paned->set_start_child(*paned);
564
} else if(parent_paned->get_end_child() == notebook) {
565
notebook->unparent();
566
parent_paned->set_end_child(*paned);
567
}
568
} else if(auto box = dynamic_cast<Gtk::Box*>(notebook->get_parent())) {
569
auto previous_child = notebook->get_prev_sibling();
570
box->remove(*notebook);
571
if(previous_child) {
572
paned->insert_after(*box, *previous_child);
573
} else {
574
box->prepend(*paned);
575
}
576
}
577
if(pack_type == Gtk::PackType::START) {
578
paned->set_end_child(*notebook);
579
} else if(pack_type == Gtk::PackType::END) {
580
paned->set_start_child(*notebook);
581
}
582
}
583
}
584
585
ContentTabBar::ContentTabBar(ContentStack *stack, Gtk::Orientation orientation, std::function<Gtk::Widget*(Gtk::Widget*)> extra_child_function) : Gtk::Box(orientation), stack(stack), extra_child_function(extra_child_function) {
586
this->get_style_context()->add_class("gpanthera-content-tab-bar");
587
this->set_margin_top(0);
588
this->set_margin_bottom(0);
589
this->set_margin_start(0);
590
this->set_margin_end(0);
591
592
auto update_callback = [this](Gtk::Widget*) {
593
this->update_buttons();
594
};
595
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
596
stack->signal_child_added.connect(update_callback);
597
stack->signal_child_removed.connect(update_callback);
598
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
599
this->add_controller(drop_target);
600
// Process dropped buttons
601
drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) {
602
if(const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get()) {
603
if(auto page = dynamic_cast<ContentPage*>(widget)) {
604
this->stack->add_page(*page);
605
}
606
}
607
608
return true; // Drop OK
609
}, false);
610
611
scrolled_window = Gtk::make_managed<Gtk::ScrolledWindow>();
612
auto viewport = Gtk::make_managed<Gtk::Viewport>(nullptr, nullptr);
613
tab_box = Gtk::make_managed<Gtk::Box>(orientation);
614
this->prepend(*scrolled_window);
615
scrolled_window->set_child(*viewport);
616
viewport->set_child(*tab_box);
617
618
this->set_orientation(orientation);
619
620
if(this->extra_child_function) {
621
this->append(*this->extra_child_function(this));
622
}
623
}
624
625
std::function<Gtk::Widget*(Gtk::Widget*)> ContentTabBar::get_extra_child_function() const {
626
return this->extra_child_function;
627
}
628
629
void ContentTabBar::set_orientation(Gtk::Orientation orientation) {
630
this->Gtk::Box::set_orientation(orientation);
631
if(orientation == Gtk::Orientation::HORIZONTAL) {
632
scrolled_window->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::NEVER);
633
tab_box->set_orientation(Gtk::Orientation::HORIZONTAL);
634
scrolled_window->set_hexpand(true);
635
scrolled_window->set_vexpand(false);
636
} else if(orientation == Gtk::Orientation::VERTICAL) {
637
scrolled_window->set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
638
tab_box->set_orientation(Gtk::Orientation::VERTICAL);
639
scrolled_window->set_vexpand(true);
640
scrolled_window->set_hexpand(false);
641
}
642
}
643
644
ContentTabBar::~ContentTabBar() {
645
add_handler.disconnect();
646
remove_handler.disconnect();
647
}
648
649
ContentStack *ContentTabBar::get_stack() const {
650
return stack;
651
}
652
653
void ContentTabBar::update_buttons() {
654
// Clear the old buttons
655
auto old_buttons = collect_children(*this->tab_box);
656
for(auto *button : old_buttons) {
657
if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) {
658
button_button->unset_child();
659
tab_box->remove(*button);
660
}
661
}
662
ContentTab* first_child = nullptr;
663
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
664
if(auto page = dynamic_cast<ContentPage*>(widget)) {
665
auto *button = Gtk::make_managed<ContentTab>(page);
666
if(!first_child) {
667
first_child = button;
668
} else {
669
button->set_group(*first_child);
670
}
671
this->tab_box->append(*button);
672
}
673
}
674
}
675
676
void ContentStack::add_page(ContentPage &child) {
677
child.redock(this);
678
}
679
680
void ContentStack::remove_with_paned() {
681
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
682
Gtk::Widget *child = nullptr;
683
if(this->get_parent() == paned->get_start_child()) {
684
child = paned->get_end_child();
685
} else if(this->get_parent() == paned->get_end_child()) {
686
child = paned->get_start_child();
687
} else {
688
return;
689
}
690
691
g_object_ref(child->gobj()); // Prevent the child from being automatically deleted
692
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()
693
paned->property_end_child().reset_value();
694
695
if(child) {
696
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(paned->get_parent())) {
697
if(parent_paned->get_start_child() == paned) {
698
parent_paned->set_start_child(*child);
699
} else if(parent_paned->get_end_child() == paned) {
700
parent_paned->set_end_child(*child);
701
}
702
} else if(auto box = dynamic_cast<Gtk::Box*>(paned->get_parent())) {
703
child->insert_after(*box, *paned);
704
paned->unparent();
705
g_object_unref(child->gobj());
706
}
707
}
708
}
709
}
710
711
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
712
this->set_child(*page->get_tab_widget());
713
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
714
this->set_halign(Gtk::Align::CENTER);
715
this->set_valign(Gtk::Align::CENTER);
716
this->get_style_context()->add_class("toggle");
717
this->get_style_context()->add_class("gpanthera-content-tab");
718
// Add/remove CSS classes when the pane is shown/hidden
719
active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() {
720
this->update_active_style();
721
});
722
drag_source = Gtk::DragSource::create();
723
drag_source->set_exclusive(false);
724
// This is to prevent the click handler from taking over grabbing the button
725
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
726
value.init(ContentPage::get_type());
727
value.set(page);
728
// Add the drag source to the button
729
this->add_controller(drag_source);
730
this->signal_clicked().connect([this, page]() {
731
page->get_stack()->set_visible_child(*page);
732
update_active_style();
733
});
734
// Switch tabs on depress
735
auto gesture_click = Gtk::GestureClick::create();
736
gesture_click->set_button(1);
737
this->add_controller(gesture_click);
738
gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) {
739
page->get_stack()->set_visible_child(*page);
740
update_active_style();
741
});
742
// Provide the drag data
743
drag_source->signal_prepare().connect([this](double, double) {
744
drag_source->set_actions(Gdk::DragAction::MOVE);
745
auto const paintable = Gtk::WidgetPaintable::create();
746
paintable->set_widget(*this);
747
drag_source->set_icon(paintable->get_current_image(), 0, 0);
748
return Gdk::ContentProvider::create(value);
749
}, false);
750
update_active_style();
751
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
752
this->set_opacity(0);
753
}, false);
754
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
755
// Process dropped buttons by inserting them after the current button
756
drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) {
757
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
758
759
if(widget) {
760
if(auto page = dynamic_cast<ContentPage*>(widget)) {
761
if(page->content_manager != this->page->content_manager) {
762
// If the pane is not in the same layout manager, reject
763
return false;
764
}
765
auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent()->get_parent()->get_parent()->get_parent());
766
if(switcher) {
767
auto *stack = switcher->get_stack();
768
// Move the button to the new position
769
page->redock(stack);
770
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
771
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
772
page->insert_before(*stack, *this->page);
773
} else {
774
page->insert_after(*stack, *this->page);
775
}
776
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
777
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
778
page->insert_before(*stack, *this->page);
779
} else {
780
page->insert_after(*stack, *this->page);
781
}
782
}
783
784
switcher->update_buttons();
785
}
786
}
787
}
788
789
return true; // Drop OK
790
}, false);
791
this->add_controller(drop_target);
792
// Pop out if dragged to an external location
793
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
794
if(reason == Gdk::DragCancelReason::NO_TARGET) {
795
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
796
bool result = stack->signal_detach.emit(this->page);
797
this->set_opacity(1);
798
return result;
799
}
800
this->set_opacity(1);
801
return false;
802
}, false);
803
drag_source->signal_drag_end().connect([this](const Glib::RefPtr<Gdk::Drag>&, bool drop_ok) {
804
this->set_opacity(1);
805
}, false);
806
807
// Provide a context menu
808
context_menu = Gio::Menu::create();
809
auto action_group = Gio::SimpleActionGroup::create();
810
this->insert_action_group("win", action_group);
811
812
auto close_action = Gio::SimpleAction::create("close");
813
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
814
if(!this->page->signal_close.emit()) {
815
if(this->page->get_next_sibling()) {
816
this->page->get_stack()->set_visible_child(*this->page->get_next_sibling());
817
} else if(this->page->get_prev_sibling()) {
818
this->page->get_stack()->set_visible_child(*this->page->get_prev_sibling());
819
}
820
this->page->redock(nullptr);
821
}
822
});
823
action_group->add_action(close_action);
824
context_menu->append(_("Close"), "win.close");
825
826
auto close_all_action = Gio::SimpleAction::create("close-all");
827
close_all_action->signal_activate().connect([this](const Glib::VariantBase&) {
828
for(auto page : collect_children(*this->page->get_stack())) {
829
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
830
if(!content_page->signal_close.emit()) {
831
content_page->redock(nullptr);
832
}
833
}
834
}
835
});
836
action_group->add_action(close_all_action);
837
context_menu->append(_("Close all"), "win.close-all");
838
839
auto close_others_action = Gio::SimpleAction::create("close-others");
840
close_others_action->signal_activate().connect([this](const Glib::VariantBase&) {
841
for(auto page : collect_children(*this->page->get_stack())) {
842
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
843
if(content_page != this->page && !content_page->signal_close.emit()) {
844
content_page->redock(nullptr);
845
}
846
}
847
}
848
});
849
action_group->add_action(close_others_action);
850
context_menu->append(_("Close others"), "win.close-others");
851
852
auto detach_action = Gio::SimpleAction::create("detach");
853
detach_action->signal_activate().connect([this](const Glib::VariantBase&) {
854
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
855
bool result = stack->signal_detach.emit(this->page);
856
});
857
action_group->add_action(detach_action);
858
context_menu->append(_("New window"), "win.detach");
859
860
// 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"
861
this->insert_action_group("win", action_group);
862
// Attach the context menu to the button
863
auto context_menu_signal = add_context_menu(*this);
864
context_menu_signal.connect([this, action_group](double x, double y) {
865
if(this->context_menu) {
866
auto popover = Gtk::make_managed<Gtk::PopoverMenu>();
867
popover->set_menu_model(context_menu);
868
popover->set_parent(*this);
869
popover->set_has_arrow(false);
870
popover->set_halign(Gtk::Align::START);
871
popover->set_pointing_to(Gdk::Rectangle(x, y, 1, 1));
872
popover->popup();
873
}
874
});
875
}
876
877
void ContentTab::update_active_style() {
878
if(this->page->get_stack()->get_visible_child() == this->page) {
879
this->add_css_class("checked");
880
this->add_css_class("gpanthera-dock-button-active");
881
this->set_active(true);
882
} else {
883
this->remove_css_class("checked");
884
this->remove_css_class("gpanthera-dock-button-active");
885
this->set_active(false);
886
}
887
}
888
889
ContentTab::~ContentTab() {
890
active_style_handler.disconnect();
891
}
892
893
Gtk::Widget *ContentPage::get_tab_widget() const {
894
return this->tab_widget;
895
}
896
897
ContentStack *ContentPage::get_stack() const {
898
return this->stack;
899
}
900
901
void ContentPage::lose_visibility() {
902
if(this->get_next_sibling()) {
903
this->get_stack()->set_visible_child(*this->get_next_sibling());
904
} else if(this->get_prev_sibling()) {
905
this->get_stack()->set_visible_child(*this->get_prev_sibling());
906
}
907
}
908
909
void ContentPage::redock(ContentStack *stack) {
910
if(stack == nullptr) {
911
if(this->stack) {
912
this->stack->remove(*this);
913
if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && !this->stack->get_first_child()) {
914
this->stack->remove_with_paned();
915
}
916
}
917
auto old_stack = this->stack;
918
if(old_stack) {
919
if(!old_stack->get_first_child()) {
920
old_stack->signal_leave_empty.emit();
921
}
922
}
923
this->stack = nullptr;
924
this->last_stack = nullptr;
925
return;
926
}
927
// Check if the stack is now empty, in which case we should remove it
928
if(this->stack == stack) {
929
return;
930
}
931
if(this->stack != nullptr) {
932
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()))) {
933
this->stack->remove(*this);
934
this->stack->remove_with_paned();
935
} else if(this->get_parent() == this->stack) {
936
this->stack->remove(*this);
937
}
938
}
939
auto old_stack = this->stack;
940
this->stack = stack;
941
this->last_stack = stack;
942
this->stack->add(*this);
943
if(old_stack) {
944
if(!old_stack->get_first_child()) {
945
old_stack->signal_leave_empty.emit();
946
}
947
}
948
}
949
950
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
951
Gtk::Overlay(), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) {
952
this->set_name("gpanthera_content_page");
953
this->set_child(*child);
954
this->set_tab_widget(tab_widget);
955
this->set_margin_top(0);
956
this->set_margin_bottom(0);
957
this->set_margin_start(0);
958
this->set_margin_end(0);
959
if(stack) {
960
stack->add_page(*this);
961
this->content_manager->add_stack(this->stack);
962
}
963
}
964
965
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
966
}
967
968
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
969
this->tab_widget = tab_widget;
970
}
971
972
ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) {
973
this->set_child(*notebook);
974
this->set_decorated(true);
975
this->set_resizable(true);
976
}
977
978
Gtk::PositionType ContentNotebook::get_tab_position() const {
979
return this->tab_position;
980
}
981
982
ContentTabBar *ContentNotebook::get_switcher() const {
983
return this->switcher;
984
}
985
986
ContentStack *ContentNotebook::get_stack() const {
987
return this->stack;
988
}
989
} // namespace gPanthera
990