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