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