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