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