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