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++ • 39.83 kiB
C++ source, ASCII text
        
            
1
#include "gpanthera.hh"
2
3
#include <cassert>
4
#include <iostream>
5
#include <utility>
6
#include <libintl.h>
7
#include <locale.h>
8
#include <filesystem>
9
#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);
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) : Gtk::Box(orientation), stack(stack) {
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
608
ContentTabBar::~ContentTabBar() {
609
add_handler.disconnect();
610
remove_handler.disconnect();
611
}
612
613
ContentStack *ContentTabBar::get_stack() const {
614
return stack;
615
}
616
617
void ContentTabBar::update_buttons() {
618
// Clear the old buttons
619
auto old_buttons = collect_children(*this);
620
for(auto *button : old_buttons) {
621
if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) {
622
button_button->unset_child();
623
}
624
remove(*button);
625
}
626
ContentTab* first_child = nullptr;
627
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
628
if(auto page = dynamic_cast<ContentPage*>(widget)) {
629
auto *button = Gtk::make_managed<ContentTab>(page);
630
if(!first_child) {
631
first_child = button;
632
} else {
633
button->set_group(*first_child);
634
}
635
this->append(*button);
636
}
637
}
638
}
639
640
void ContentStack::add_page(ContentPage &child) {
641
child.redock(this);
642
}
643
644
void ContentStack::remove_with_paned() {
645
if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) {
646
Gtk::Widget *child = nullptr;
647
if(this->get_parent() == paned->get_start_child()) {
648
child = paned->get_end_child();
649
} else if(this->get_parent() == paned->get_end_child()) {
650
child = paned->get_start_child();
651
} else {
652
return;
653
}
654
655
g_object_ref(child->gobj()); // Prevent the child from being automatically deleted
656
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()
657
paned->property_end_child().reset_value();
658
659
if(child) {
660
if(auto parent_paned = dynamic_cast<Gtk::Paned*>(paned->get_parent())) {
661
if(parent_paned->get_start_child() == paned) {
662
parent_paned->set_start_child(*child);
663
} else if(parent_paned->get_end_child() == paned) {
664
parent_paned->set_end_child(*child);
665
}
666
} else if(auto box = dynamic_cast<Gtk::Box*>(paned->get_parent())) {
667
child->insert_after(*box, *paned);
668
paned->unparent();
669
g_object_unref(child->gobj());
670
}
671
}
672
}
673
}
674
675
ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) {
676
this->set_child(*page->get_tab_widget());
677
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
678
this->set_halign(Gtk::Align::CENTER);
679
this->set_valign(Gtk::Align::CENTER);
680
this->get_style_context()->add_class("toggle");
681
this->get_style_context()->add_class("gpanthera-content-tab");
682
// Add/remove CSS classes when the pane is shown/hidden
683
active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() {
684
this->update_active_style();
685
});
686
drag_source = Gtk::DragSource::create();
687
drag_source->set_exclusive(false);
688
// This is to prevent the click handler from taking over grabbing the button
689
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
690
value.init(ContentPage::get_type());
691
value.set(page);
692
// Add the drag source to the button
693
this->add_controller(drag_source);
694
this->signal_clicked().connect([this, page]() {
695
page->get_stack()->set_visible_child(*page);
696
update_active_style();
697
});
698
// Switch tabs on depress
699
auto gesture_click = Gtk::GestureClick::create();
700
gesture_click->set_button(1);
701
this->add_controller(gesture_click);
702
gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) {
703
page->get_stack()->set_visible_child(*page);
704
update_active_style();
705
});
706
// Provide the drag data
707
drag_source->signal_prepare().connect([this](double, double) {
708
drag_source->set_actions(Gdk::DragAction::MOVE);
709
auto const paintable = Gtk::WidgetPaintable::create();
710
paintable->set_widget(*this);
711
drag_source->set_icon(paintable->get_current_image(), 0, 0);
712
return Gdk::ContentProvider::create(value);
713
}, false);
714
update_active_style();
715
drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) {
716
this->set_opacity(0);
717
}, false);
718
drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE);
719
// Process dropped buttons by inserting them after the current button
720
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
721
const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get();
722
723
if(widget) {
724
if(auto page = dynamic_cast<ContentPage*>(widget)) {
725
if(page->content_manager != this->page->content_manager) {
726
// If the pane is not in the same layout manager, reject
727
return false;
728
}
729
auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent());
730
if(switcher) {
731
auto *stack = switcher->get_stack();
732
// Move the button to the new position
733
page->redock(stack);
734
if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) {
735
if(x < static_cast<double>(this->get_allocated_width()) / 2) {
736
page->insert_before(*stack, *this->page);
737
} else {
738
page->insert_after(*stack, *this->page);
739
}
740
} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) {
741
if(y < static_cast<double>(this->get_allocated_height()) / 2) {
742
page->insert_before(*stack, *this->page);
743
} else {
744
page->insert_after(*stack, *this->page);
745
}
746
}
747
748
switcher->update_buttons();
749
}
750
}
751
}
752
753
return true; // Drop OK
754
}, false);
755
this->add_controller(drop_target);
756
// Pop out if dragged to an external location
757
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
758
if(reason == Gdk::DragCancelReason::NO_TARGET) {
759
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
760
bool result = stack->signal_detach.emit(this->page);
761
if(!result) {
762
this->set_opacity(1);
763
}
764
return result;
765
}
766
this->set_opacity(1);
767
return false;
768
}, false);
769
770
// Provide a context menu
771
context_menu = Gio::Menu::create();
772
auto action_group = Gio::SimpleActionGroup::create();
773
this->insert_action_group("win", action_group);
774
775
auto close_action = Gio::SimpleAction::create("close");
776
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
777
if(!this->page->signal_close.emit()) {
778
if(this->page->get_next_sibling()) {
779
this->page->get_stack()->set_visible_child(*this->page->get_next_sibling());
780
} else if(this->page->get_prev_sibling()) {
781
this->page->get_stack()->set_visible_child(*this->page->get_prev_sibling());
782
}
783
this->page->redock(nullptr);
784
}
785
});
786
action_group->add_action(close_action);
787
context_menu->append(_("Close"), "win.close");
788
789
auto close_all_action = Gio::SimpleAction::create("close-all");
790
close_all_action->signal_activate().connect([this](const Glib::VariantBase&) {
791
for(auto page : collect_children(*this->page->get_stack())) {
792
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
793
if(!content_page->signal_close.emit()) {
794
content_page->redock(nullptr);
795
}
796
}
797
}
798
});
799
action_group->add_action(close_all_action);
800
context_menu->append(_("Close all"), "win.close-all");
801
802
auto close_others_action = Gio::SimpleAction::create("close-others");
803
close_others_action->signal_activate().connect([this](const Glib::VariantBase&) {
804
for(auto page : collect_children(*this->page->get_stack())) {
805
if(auto content_page = dynamic_cast<ContentPage*>(page)) {
806
if(content_page != this->page && !content_page->signal_close.emit()) {
807
content_page->redock(nullptr);
808
}
809
}
810
}
811
});
812
action_group->add_action(close_others_action);
813
context_menu->append(_("Close others"), "win.close-others");
814
815
auto detach_action = Gio::SimpleAction::create("detach");
816
detach_action->signal_activate().connect([this](const Glib::VariantBase&) {
817
auto stack = dynamic_cast<ContentStack*>(this->page->get_stack());
818
bool result = stack->signal_detach.emit(this->page);
819
});
820
action_group->add_action(detach_action);
821
context_menu->append(_("New window"), "win.detach");
822
823
// 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"
824
this->insert_action_group("win", action_group);
825
// Attach the context menu to the button
826
auto context_menu_signal = add_context_menu(*this);
827
context_menu_signal.connect([this, action_group](double x, double y) {
828
if(this->context_menu) {
829
auto popover = Gtk::make_managed<Gtk::PopoverMenu>();
830
popover->set_menu_model(context_menu);
831
popover->set_parent(*this);
832
popover->set_has_arrow(false);
833
popover->set_halign(Gtk::Align::START);
834
popover->set_pointing_to(Gdk::Rectangle(x, y, 1, 1));
835
popover->popup();
836
}
837
});
838
}
839
840
void ContentTab::update_active_style() {
841
if(this->page->get_stack()->get_visible_child() == this->page) {
842
this->add_css_class("checked");
843
this->add_css_class("gpanthera-dock-button-active");
844
this->set_active(true);
845
} else {
846
this->remove_css_class("checked");
847
this->remove_css_class("gpanthera-dock-button-active");
848
this->set_active(false);
849
}
850
}
851
852
ContentTab::~ContentTab() {
853
active_style_handler.disconnect();
854
}
855
856
Gtk::Widget *ContentPage::get_tab_widget() const {
857
return this->tab_widget;
858
}
859
860
ContentStack *ContentPage::get_stack() const {
861
return this->stack;
862
}
863
864
void ContentPage::lose_visibility() {
865
if(this->get_next_sibling()) {
866
this->get_stack()->set_visible_child(*this->get_next_sibling());
867
} else if(this->get_prev_sibling()) {
868
this->get_stack()->set_visible_child(*this->get_prev_sibling());
869
}
870
}
871
872
void ContentPage::redock(ContentStack *stack) {
873
if(stack == nullptr) {
874
if(this->stack) {
875
this->stack->remove(*this);
876
if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && !this->stack->get_first_child()) {
877
this->stack->remove_with_paned();
878
}
879
}
880
auto old_stack = this->stack;
881
if(old_stack) {
882
if(!old_stack->get_first_child()) {
883
old_stack->signal_leave_empty.emit();
884
}
885
}
886
this->stack = nullptr;
887
this->last_stack = nullptr;
888
return;
889
}
890
// Check if the stack is now empty, in which case we should remove it
891
if(this->stack == stack) {
892
return;
893
}
894
if(this->stack != nullptr) {
895
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()))) {
896
this->stack->remove(*this);
897
this->stack->remove_with_paned();
898
} else if(this->get_parent() == this->stack) {
899
this->stack->remove(*this);
900
}
901
}
902
auto old_stack = this->stack;
903
this->stack = stack;
904
this->last_stack = stack;
905
this->stack->add(*this);
906
if(old_stack) {
907
if(!old_stack->get_first_child()) {
908
old_stack->signal_leave_empty.emit();
909
}
910
}
911
}
912
913
Gtk::Widget *ContentPage::get_child() const {
914
return this->child;
915
}
916
917
ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) :
918
Gtk::Box(Gtk::Orientation::VERTICAL, 0), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) {
919
this->set_name("gpanthera_content_page");
920
this->append(*child);
921
this->set_tab_widget(tab_widget);
922
this->set_margin_top(0);
923
this->set_margin_bottom(0);
924
this->set_margin_start(0);
925
this->set_margin_end(0);
926
if(stack) {
927
stack->add_page(*this);
928
this->content_manager->add_stack(this->stack);
929
}
930
}
931
932
ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") {
933
}
934
935
void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) {
936
this->tab_widget = tab_widget;
937
}
938
939
ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) {
940
this->set_child(*notebook);
941
this->set_decorated(true);
942
this->set_resizable(true);
943
}
944
945
Gtk::PositionType ContentNotebook::get_tab_position() const {
946
return this->tab_position;
947
}
948
949
ContentTabBar *ContentNotebook::get_switcher() const {
950
return this->switcher;
951
}
952
953
ContentStack *ContentNotebook::get_stack() const {
954
return this->stack;
955
}
956
} // namespace gPanthera
957