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