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.5 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
if(!this->page->signal_close.emit()) {
852
if(this->page->get_next_sibling()) {
853
this->page->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->page->get_next_sibling()));
854
this->page->get_stack()->set_visible_child(*this->page->get_next_sibling());
855
} else if(this->page->get_prev_sibling()) {
856
this->page->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->page->get_prev_sibling()));
857
this->page->get_stack()->set_visible_child(*this->page->get_prev_sibling());
858
}
859
this->page->redock(nullptr);
860
}
861
});
862
action_group->add_action(close_action);
863
context_menu->append(_("Close"), "win.close");
864
865
auto close_all_action = Gio::SimpleAction::create("close-all");
866
close_all_action->signal_activate().connect([this](const Glib::VariantBase&) {
867
for(auto page : collect_children(*this->page->get_stack())) {
868
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
869
if(!content_page->signal_close.emit()) {
870
content_page->redock(nullptr);
871
}
872
}
873
}
874
});
875
action_group->add_action(close_all_action);
876
context_menu->append(_("Close all"), "win.close-all");
877
878
auto close_others_action = Gio::SimpleAction::create("close-others");
879
close_others_action->signal_activate().connect([this](const Glib::VariantBase&) {
880
for(auto page : collect_children(*this->page->get_stack())) {
881
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
882
if(content_page != this->page && !content_page->signal_close.emit()) {
883
content_page->redock(nullptr);
884
}
885
}
886
}
887
});
888
action_group->add_action(close_others_action);
889
context_menu->append(_("Close others"), "win.close-others");
890
891
auto detach_action = Gio::SimpleAction::create("detach");
892
detach_action->signal_activate().connect([this](const Glib::VariantBase&) {
893
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
894
bool result = stack->signal_detach.emit(this->page);
895
});
896
action_group->add_action(detach_action);
897
context_menu->append(_("New window"), "win.detach");
898
899
// 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"
900
this->insert_action_group("win", action_group);
901
// Attach the context menu to the button
902
auto context_menu_signal = add_context_menu(*this);
903
context_menu_signal.connect([this, action_group](double x, double y) {
904
if(this->context_menu) {
905
auto popover = Gtk::make_managed<Gtk::PopoverMenu>();
906
popover->set_menu_model(context_menu);
907
popover->set_parent(*this);
908
popover->set_has_arrow(false);
909
popover->set_halign(Gtk::Align::START);
910
popover->set_pointing_to(Gdk::Rectangle(x, y, 1, 1));
911
popover->popup();
912
}
913
});
914
}
915
916
void ContentTab::update_active_style() {
917
if(this->page->get_stack()->get_visible_child() == this->page) {
918
this->add_css_class("checked");
919
this->add_css_class("gpanthera-dock-button-active");
920
this->set_active(true);
921
} else {
922
this->remove_css_class("checked");
923
this->remove_css_class("gpanthera-dock-button-active");
924
this->set_active(false);
925
}
926
}
927
928
ContentTab::~ContentTab() {
929
active_style_handler.disconnect();
930
drag_end_handler.disconnect();
931
drag_cancel_handler.disconnect();
932
}
933
934
Gtk::Widget *ContentPage::get_tab_widget() const {
935
return this->tab_widget;
936
}
937
938
ContentStack *ContentPage::get_stack() const {
939
return this->stack;
940
}
941
942
void ContentPage::lose_visibility() {
943
if(this->get_next_sibling()) {
944
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling()));
945
this->get_stack()->set_visible_child(*this->get_next_sibling());
946
} else if(this->get_prev_sibling()) {
947
this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling()));
948
this->get_stack()->set_visible_child(*this->get_prev_sibling());
949
}
950
}
951
952
void ContentPage::redock(ContentStack *stack) {
953
if(stack == nullptr) {
954
if(this->stack) {
955
this->stack->remove(*this);
956
if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && !this->stack->get_first_child()) {
957
this->stack->remove_with_paned();
958
}
959
}
960
auto old_stack = this->stack;
961
if(old_stack) {
962
if(!old_stack->get_first_child()) {
963
old_stack->signal_leave_empty.emit();
964
}
965
}
966
this->stack = nullptr;
967
this->last_stack = nullptr;
968
return;
969
}
970
// Check if the stack is now empty, in which case we should remove it
971
if(this->stack == stack) {
972
return;
973
}
974
if(this->stack != nullptr) {
975
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()))) {
976
this->stack->remove(*this);
977
this->stack->remove_with_paned();
978
} else if(this->get_parent() == this->stack) {
979
this->stack->remove(*this);
980
}
981
}
982
auto old_stack = this->stack;
983
this->stack = stack;
984
this->last_stack = stack;
985
this->stack->add(*this);
986
if(old_stack) {
987
if(!old_stack->get_first_child()) {
988
old_stack->signal_leave_empty.emit();
989
}
990
}
991
}
992
993
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
994
Gtk::Overlay(), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) {
995
this->set_name("gpanthera_content_page");
996
this->set_child(*child);
997
this->set_tab_widget(tab_widget);
998
this->set_margin_top(0);
999
this->set_margin_bottom(0);
1000
this->set_margin_start(0);
1001
this->set_margin_end(0);
1002
if(stack) {
1003
stack->add_page(*this);
1004
this->content_manager->add_stack(this->stack);
1005
}
1006
auto click_controller = Gtk::GestureClick::create();
1007
click_controller->set_button(0);
1008
click_controller->signal_pressed().connect([this](int num_presses, double x, double y) {
1009
this->content_manager->set_last_operated_page(this);
1010
});
1011
this->property_has_focus().signal_changed().connect([this]() {
1012
if(this->property_has_focus()) {
1013
this->content_manager->set_last_operated_page(this);
1014
}
1015
});
1016
click_controller->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
1017
this->add_controller(click_controller);
1018
}
1019
1020
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
1021
}
1022
1023
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
1024
this->tab_widget = tab_widget;
1025
}
1026
1027
ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) {
1028
this->set_child(*notebook);
1029
this->set_decorated(true);
1030
this->set_resizable(true);
1031
}
1032
1033
Gtk::PositionType ContentNotebook::get_tab_position() const {
1034
return this->tab_position;
1035
}
1036
1037
ContentTabBar *ContentNotebook::get_switcher() const {
1038
return this->switcher;
1039
}
1040
1041
ContentStack *ContentNotebook::get_stack() const {
1042
return this->stack;
1043
}
1044
1045
ContentPage *ContentManager::get_last_operated_page() const {
1046
return this->last_operated_page;
1047
}
1048
1049
void ContentManager::set_last_operated_page(ContentPage *page) {
1050
this->last_operated_page = page;
1051
this->signal_page_operated.emit(page);
1052
}
1053
} // namespace gPanthera
1054