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