GTK docking interfaces and more

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++ • 15.61 kiB
C++ source, ASCII text
        
            
1
#include "gpanthera.hh"
2
#include <iostream>
3
#include <utility>
4
#include <libintl.h>
5
#include <locale.h>
6
#include <filesystem>
7
#define _(STRING) gettext(STRING)
8
9
namespace gPanthera {
10
std::vector<Gtk::Widget*> collect_children(Gtk::Widget &widget) {
11
// Get a vector of the children of a GTK widget, since the container API was removed in GTK 4
12
std::vector<Gtk::Widget*> children;
13
for(auto *child = widget.get_first_child(); child; child = child->get_next_sibling()) {
14
children.push_back(child);
15
}
16
return children;
17
}
18
19
Gtk::Image *copy_image(Gtk::Image *image) {
20
// Generate a new Gtk::Image with the same contents as an existing one
21
if(image->get_storage_type() == Gtk::Image::Type::PAINTABLE) {
22
return Gtk::make_managed<Gtk::Image>(image->get_paintable());
23
} else if(image->get_storage_type() == Gtk::Image::Type::ICON_NAME) {
24
auto new_image = Gtk::make_managed<Gtk::Image>();
25
new_image->set_from_icon_name(image->get_icon_name());
26
return new_image;
27
} else {
28
return nullptr;
29
}
30
}
31
32
void init() {
33
// Set up gettext configuration
34
setlocale(LC_ALL, "");
35
36
bindtextdomain("gpanthera", "./locales");
37
textdomain("gpanthera");
38
}
39
40
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)
41
: Gtk::Box(Gtk::Orientation::VERTICAL, 0), name(name) {
42
if(icon) {
43
this->icon = icon;
44
}
45
if(stack) {
46
this->stack = stack;
47
}
48
this->layout = std::move(layout);
49
this->label.set_text(label);
50
// This should be replaced with a custom class in the future
51
header = std::make_unique<Gtk::HeaderBar>();
52
header->set_show_title_buttons(false);
53
if(custom_header) {
54
header->set_title_widget(*custom_header);
55
} else {
56
header->set_title_widget(this->label);
57
}
58
header->add_css_class("gpanthera-dock-titlebar");
59
auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>();
60
auto header_menu = Gio::Menu::create();
61
header_menu_button->set_direction(Gtk::ArrowType::NONE);
62
63
// Pane menu
64
this->action_group = Gio::SimpleActionGroup::create();
65
header_menu_button->insert_action_group("win", action_group);
66
67
// Close action
68
auto close_action = Gio::SimpleAction::create("close");
69
close_action->signal_activate().connect([this](const Glib::VariantBase&) {
70
if(this->stack) {
71
this->stack->set_visible_child("");
72
}
73
});
74
action_group->add_action(close_action);
75
header_menu->append(_("Close"), "win.close");
76
77
// Pop out action
78
auto pop_out_action = Gio::SimpleAction::create("pop_out");
79
pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) {
80
if(this->stack) {
81
this->pop_out();
82
}
83
});
84
action_group->add_action(pop_out_action);
85
header_menu->append(_("Pop out"), "win.pop_out");
86
87
// Move menu
88
auto move_menu = Gio::Menu::create();
89
for(auto &this_stack : this->layout->stacks) {
90
auto action_name = "move_" + this_stack->name;
91
auto move_action = Gio::SimpleAction::create(action_name);
92
move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) {
93
this_stack->add_pane(*this);
94
});
95
action_group->add_action(move_action);
96
move_menu->append(this_stack->name, "win." + action_name);
97
}
98
99
// Add move submenu
100
header_menu->append_submenu(_("Move"), move_menu);
101
102
// Switch to traditional (nested) submenus, not sliding
103
auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED);
104
popover_menu->set_has_arrow(false);
105
header_menu_button->set_popover(*popover_menu);
106
107
// TODO: Add a context menu as well
108
109
header->pack_end(*header_menu_button);
110
111
this->prepend(*header);
112
this->child = &child;
113
this->append(child);
114
}
115
116
Gtk::Stack *DockablePane::get_stack() const {
117
return stack;
118
}
119
120
void DockablePane::redock(DockStack *stack) {
121
if(this->window != nullptr) {
122
this->window->hide();
123
// Put the titlebar back
124
this->window->unset_titlebar();
125
this->window->set_decorated(false);
126
this->header->get_style_context()->remove_class("titlebar");
127
this->prepend(*this->header);
128
this->window->unset_child();
129
this->window->close();
130
} else if(this->stack == this->get_parent()) {
131
this->stack->remove(*this);
132
}
133
this->stack = stack;
134
this->last_stack = stack;
135
this->stack->add(*this, this->get_identifier());
136
if(this->window != nullptr) {
137
this->window->destroy();
138
delete this->window;
139
this->window = nullptr;
140
// Re-enable the pop out option
141
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
142
action->set_enabled(true);
143
this->header->get_style_context()->remove_class("gpanthera-dock-titlebar-popout");
144
}
145
}
146
147
void DockablePane::pop_out() {
148
if(this->stack != nullptr) {
149
this->stack->remove(*this);
150
this->stack = nullptr;
151
}
152
153
if(this->window == nullptr) {
154
// Remove the header bar from the pane, so it can be used as the titlebar of the window
155
this->remove(*this->header);
156
this->window = new DockWindow(this);
157
this->window->set_titlebar(*this->header);
158
this->window->set_child(*this);
159
this->window->set_decorated(true);
160
// Grey out the pop-out option
161
auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out"));
162
action->set_enabled(false);
163
this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout");
164
}
165
this->window->present();
166
}
167
168
Glib::ustring DockablePane::get_identifier() const {
169
return name;
170
}
171
172
Gtk::Image *DockablePane::get_icon() const {
173
return icon;
174
}
175
176
Gtk::Widget *DockablePane::get_child() const {
177
return child;
178
}
179
180
Gtk::Label *DockablePane::get_label() {
181
return &label;
182
}
183
184
LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") {
185
}
186
187
void LayoutManager::add_pane(DockablePane *pane) {
188
panes.push_back(pane);
189
}
190
191
void LayoutManager::add_stack(DockStack *stack) {
192
stacks.push_back(stack);
193
}
194
195
void LayoutManager::remove_pane(DockablePane *pane) {
196
panes.erase(std::ranges::remove(panes, pane).begin(), panes.end());
197
}
198
199
void LayoutManager::remove_stack(DockStack *stack) {
200
stacks.erase(std::ranges::remove(stacks, stack).begin(), stacks.end());
201
}
202
203
BaseStack::BaseStack() : Gtk::Stack() {
204
}
205
206
DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name) : BaseStack(), layout(layout), name(name) {
207
auto empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
208
this->add(*empty_child, "");
209
// Add the stack to a layout manager
210
this->layout->add_stack(this);
211
212
// Hide the stack when no child is visible
213
this->property_visible_child_name().signal_changed().connect([this]() {
214
if(this->get_visible_child_name() == "") {
215
this->hide();
216
} else {
217
this->show();
218
}
219
});
220
221
// Also hide when the visible child is removed
222
this->signal_child_removed.connect([this](Gtk::Widget* const &child) {
223
if(this->get_visible_child_name() == "") {
224
this->hide();
225
}
226
});
227
228
this->set_visible_child("");
229
this->hide();
230
}
231
232
DockWindow::DockWindow(DockablePane *pane) {
233
this->pane = pane;
234
this->set_child(*pane);
235
// Attempting to close the window should redock the pane so it doesn't vanish
236
this->signal_close_request().connect([this]() {
237
this->pane->redock(this->pane->last_stack);
238
return true;
239
}, false);
240
}
241
242
DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
243
auto update_callback = [this](Gtk::Widget*) {
244
this->update_buttons();
245
};
246
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
247
stack->signal_child_added.connect(update_callback);
248
stack->signal_child_removed.connect(update_callback);
249
this->get_style_context()->add_class("gpanthera-dock-switcher");
250
drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
251
this->add_controller(drop_target);
252
// Process dropped buttons
253
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
254
const auto &widget = static_cast<const Glib::Value<Gtk::Widget*>&>(value).get();
255
256
if(widget) {
257
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
258
this->stack->add_pane(*pane);
259
}
260
}
261
262
return true; // Drop OK
263
}, false);
264
}
265
266
DockStack *DockStackSwitcher::get_stack() const {
267
return stack;
268
}
269
270
DockStackSwitcher::~DockStackSwitcher() {
271
add_handler.disconnect();
272
remove_handler.disconnect();
273
}
274
275
DockButton::DockButton(DockablePane *pane) : Gtk::ToggleButton(), pane(pane) {
276
if(pane->get_icon()) {
277
this->set_child(*copy_image(pane->get_icon()));
278
}
279
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
280
this->set_tooltip_text(pane->get_label()->get_text());
281
this->set_halign(Gtk::Align::CENTER);
282
this->set_valign(Gtk::Align::CENTER);
283
this->get_style_context()->add_class("toggle");
284
this->get_style_context()->add_class("gpanthera-dock-button");
285
// Add/remove CSS classes when the pane is shown/hidden
286
active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
287
this->update_active_style();
288
});
289
drag_source = Gtk::DragSource::create();
290
drag_source->set_exclusive(false);
291
// This is to prevent the click handler from taking over grabbing the button
292
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
293
value = Glib::Value<Gtk::Widget*>();
294
value.init(DockablePane::get_type());
295
value.set(pane);
296
// Add the drag source to the button
297
this->add_controller(drag_source);
298
this->signal_clicked().connect([this, pane]() {
299
if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) {
300
pane->get_stack()->set_visible_child("");
301
} else {
302
pane->get_stack()->set_visible_child(pane->get_identifier());
303
}
304
});
305
// Provide the drag data
306
drag_source->signal_prepare().connect([this](double, double) {
307
drag_source->set_actions(Gdk::DragAction::MOVE);
308
auto const paintable = Gtk::WidgetPaintable::create();
309
paintable->set_widget(*this);
310
drag_source->set_icon(paintable, 0, 0);
311
return Gdk::ContentProvider::create(value);
312
}, false);
313
// Pop out if dragged to an external location
314
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
315
if(reason == Gdk::DragCancelReason::NO_TARGET) {
316
this->pane->pop_out();
317
return true;
318
}
319
return false;
320
}, false);
321
// Add a drop target to the button
322
auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
323
drop_target->set_actions(Gdk::DragAction::MOVE);
324
drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
325
this->add_controller(drop_target);
326
// Process dropped buttons by inserting them after the current button
327
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
328
const auto &widget = static_cast<const Glib::Value<Gtk::Widget*>&>(value).get();
329
330
if(widget) {
331
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
332
auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent());
333
if(switcher) {
334
auto *stack = switcher->get_stack();
335
// Move the button to the new position
336
pane->redock(stack);
337
pane->insert_before(*stack, *this->pane);
338
switcher->update_buttons();
339
}
340
}
341
}
342
343
return true; // Drop OK
344
}, false);
345
this->update_active_style();
346
}
347
348
void DockButton::update_active_style() {
349
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
350
this->add_css_class("checked");
351
this->add_css_class("gpanthera-dock-button-active");
352
this->set_active(true);
353
} else {
354
this->remove_css_class("checked");
355
this->remove_css_class("gpanthera-dock-button-active");
356
this->set_active(false);
357
}
358
}
359
360
DockButton::~DockButton() {
361
active_style_handler.disconnect();
362
}
363
364
void DockStackSwitcher::update_buttons() {
365
// Clear the old buttons
366
auto old_buttons = collect_children(*this);
367
for(auto *button : old_buttons) {
368
remove(*button);
369
}
370
DockButton* first_child = nullptr;
371
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
372
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
373
auto *button = Gtk::make_managed<DockButton>(pane);
374
if(!first_child) {
375
first_child = button;
376
} else {
377
button->set_group(*first_child);
378
}
379
if(pane->get_identifier() != Glib::ustring("")) {
380
append(*button);
381
}
382
}
383
}
384
}
385
386
void DockStack::add_pane(DockablePane &child) {
387
child.redock(this);
388
}
389
390
void ContentManager::add_stack(ContentStack *stack) {
391
this->stacks.push_back(stack);
392
}
393
394
void ContentManager::remove_stack(ContentStack *stack) {
395
this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end());
396
}
397
398
void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) {
399
Gtk::Stack::add(child, name);
400
signal_child_added.emit(&child);
401
}
402
403
void BaseStack::add(Gtk::Widget &child) {
404
Gtk::Stack::add(child);
405
signal_child_added.emit(&child);
406
}
407
408
void BaseStack::remove(Gtk::Widget &child) {
409
Gtk::Stack::remove(child);
410
signal_child_removed.emit(&child);
411
}
412
413
ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager)
414
: BaseStack(), content_manager(std::move(content_manager)) {
415
this->content_manager->add_stack(this);
416
}
417
} // namespace gPanthera
418