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++ • 14.68 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
DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name) : BaseStack(), layout(layout), name(name) {
204
auto empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
205
this->add(*empty_child, "");
206
// Add the stack to a layout manager
207
this->layout->add_stack(this);
208
209
// Hide the stack when no child is visible
210
this->property_visible_child_name().signal_changed().connect([this]() {
211
if(this->get_visible_child_name() == "") {
212
this->hide();
213
} else {
214
this->show();
215
}
216
});
217
218
// Also hide when the visible child is removed
219
this->signal_child_removed.connect([this](Gtk::Widget* const &child) {
220
if(this->get_visible_child_name() == "") {
221
this->hide();
222
}
223
});
224
225
this->set_visible_child("");
226
this->hide();
227
}
228
229
DockWindow::DockWindow(DockablePane *pane) {
230
this->pane = pane;
231
this->set_child(*pane);
232
// Attempting to close the window should redock the pane so it doesn't vanish
233
this->signal_close_request().connect([this]() {
234
this->pane->redock(this->pane->last_stack);
235
return true;
236
}, false);
237
}
238
239
DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) {
240
auto update_callback = [this](Gtk::Widget*) {
241
this->update_buttons();
242
};
243
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST);
244
stack->signal_child_added.connect(update_callback);
245
stack->signal_child_removed.connect(update_callback);
246
this->get_style_context()->add_class("gpanthera-dock-switcher");
247
drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
248
this->add_controller(drop_target);
249
// Process dropped buttons
250
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
251
const auto &widget = static_cast<const Glib::Value<Gtk::Widget*>&>(value).get();
252
253
if(widget) {
254
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
255
this->stack->add_pane(*pane);
256
}
257
}
258
259
return true; // Drop OK
260
}, false);
261
}
262
263
DockStack *DockStackSwitcher::get_stack() const {
264
return stack;
265
}
266
267
DockStackSwitcher::~DockStackSwitcher() {
268
add_handler.disconnect();
269
remove_handler.disconnect();
270
}
271
272
DockButton::DockButton(DockablePane *pane) : Gtk::Button(), pane(pane) {
273
if(pane->get_icon()) {
274
this->set_child(*copy_image(pane->get_icon()));
275
}
276
this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB);
277
this->set_tooltip_text(pane->get_label()->get_text());
278
this->set_halign(Gtk::Align::CENTER);
279
this->set_valign(Gtk::Align::CENTER);
280
this->get_style_context()->add_class("toggle");
281
this->get_style_context()->add_class("gpanthera-dock-button");
282
// Add/remove CSS classes when the pane is shown/hidden
283
active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
284
this->update_active_style();
285
});
286
drag_source = Gtk::DragSource::create();
287
drag_source->set_exclusive(false);
288
// This is to prevent the click handler from taking over grabbing the button
289
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
290
value = Glib::Value<Gtk::Widget*>();
291
value.init(DockablePane::get_type());
292
value.set(pane);
293
// Add the drag source to the button
294
this->add_controller(drag_source);
295
this->signal_clicked().connect([this, pane]() {
296
if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) {
297
pane->get_stack()->set_visible_child("");
298
} else {
299
pane->get_stack()->set_visible_child(pane->get_identifier());
300
}
301
});
302
// Provide the drag data
303
drag_source->signal_prepare().connect([this](double, double) {
304
drag_source->set_actions(Gdk::DragAction::MOVE);
305
auto const paintable = Gtk::WidgetPaintable::create();
306
paintable->set_widget(*this);
307
drag_source->set_icon(paintable, 0, 0);
308
return Gdk::ContentProvider::create(value);
309
}, false);
310
// Pop out if dragged to an external location
311
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
312
if(reason == Gdk::DragCancelReason::NO_TARGET) {
313
this->pane->pop_out();
314
return true;
315
}
316
return false;
317
}, false);
318
// Add a drop target to the button
319
auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
320
drop_target->set_actions(Gdk::DragAction::MOVE);
321
drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
322
this->add_controller(drop_target);
323
// Process dropped buttons by inserting them after the current button
324
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
325
const auto &widget = static_cast<const Glib::Value<Gtk::Widget*>&>(value).get();
326
327
if(widget) {
328
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
329
auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent());
330
if(switcher) {
331
auto *stack = switcher->get_stack();
332
// Move the button to the new position
333
pane->redock(stack);
334
pane->insert_before(*stack, *this->pane);
335
switcher->update_buttons();
336
}
337
}
338
}
339
340
return true; // Drop OK
341
}, false);
342
this->update_active_style();
343
}
344
345
void DockButton::update_active_style() {
346
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
347
this->add_css_class("checked");
348
this->add_css_class("gpanthera-dock-button-active");
349
} else {
350
this->remove_css_class("checked");
351
this->remove_css_class("gpanthera-dock-button-active");
352
}
353
}
354
355
DockButton::~DockButton() {
356
active_style_handler.disconnect();
357
}
358
359
void DockStackSwitcher::update_buttons() {
360
// Clear the old buttons
361
auto old_buttons = collect_children(*this);
362
for(auto *button : old_buttons) {
363
remove(*button);
364
}
365
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
366
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
367
auto *button = Gtk::make_managed<DockButton>(pane);
368
if(pane->get_identifier() != Glib::ustring("")) {
369
append(*button);
370
}
371
}
372
}
373
}
374
375
void DockStack::add_pane(DockablePane &child) {
376
child.redock(this);
377
}
378
379
void DockStack::add(Gtk::Widget &child, const Glib::ustring &name) {
380
Gtk::Stack::add(child, name);
381
signal_child_added.emit(&child);
382
}
383
384
void DockStack::remove(Gtk::Widget &child) {
385
Gtk::Stack::remove(child);
386
signal_child_removed.emit(&child);
387
}
388
} // namespace gPanthera
389