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