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++ • 13.18 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("empty");
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) : Gtk::Stack(), layout(layout), name(name) {
204
auto *empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
205
add(*empty_child, "empty");
206
// Add the stack to a layout manager
207
this->layout->add_stack(this);
208
209
// Hide the stack when the empty child is visible
210
this->property_visible_child_name().signal_changed().connect([this]() {
211
if(this->get_visible_child_name() == "empty") {
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() == "" || this->get_visible_child_name() == "empty") {
221
this->hide();
222
}
223
});
224
225
this->set_visible_child("empty");
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
stack->signal_child_added.connect(update_callback);
244
stack->signal_child_removed.connect(update_callback);
245
this->get_style_context()->add_class("gpanthera-dock-switcher");
246
drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
247
this->add_controller(drop_target);
248
// Process dropped buttons
249
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
250
const auto &widget = static_cast<const Glib::Value<Gtk::Widget*>&>(value).get();
251
252
if(widget) {
253
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
254
this->stack->add_pane(*pane);
255
}
256
}
257
258
return true; // Drop OK
259
}, false);
260
}
261
262
DockStackSwitcher::~DockStackSwitcher() {
263
add_handler.disconnect();
264
remove_handler.disconnect();
265
}
266
267
DockButton::DockButton(DockablePane *pane) : Gtk::Button(), pane(pane) {
268
if(pane->get_icon()) {
269
this->set_child(*copy_image(pane->get_icon()));
270
}
271
this->set_tooltip_text(pane->get_label()->get_text());
272
this->set_halign(Gtk::Align::CENTER);
273
this->set_valign(Gtk::Align::CENTER);
274
this->get_style_context()->add_class("toggle");
275
this->get_style_context()->add_class("gpanthera-dock-button");
276
// Add/remove CSS classes when the pane is shown/hidden
277
active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
278
this->update_active_style();
279
});
280
drag_source = Gtk::DragSource::create();
281
drag_source->set_exclusive(false);
282
// This is to prevent the click handler from taking over grabbing the button
283
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
284
value = Glib::Value<Gtk::Widget*>();
285
value.init(DockablePane::get_type());
286
value.set(pane);
287
// Add the drag source to the button
288
this->add_controller(drag_source);
289
this->signal_clicked().connect([this, pane]() {
290
if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) {
291
pane->get_stack()->set_visible_child("empty");
292
} else {
293
pane->get_stack()->set_visible_child(pane->get_identifier());
294
}
295
});
296
// Provide the drag data
297
drag_source->signal_prepare().connect([this](double, double) {
298
drag_source->set_actions(Gdk::DragAction::MOVE);
299
return Gdk::ContentProvider::create(value);
300
}, false);
301
// Pop out if dragged to an external location
302
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
303
if(reason == Gdk::DragCancelReason::NO_TARGET) {
304
this->pane->pop_out();
305
return true;
306
}
307
return false;
308
}, false);
309
this->update_active_style();
310
}
311
312
void DockButton::update_active_style() {
313
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
314
this->add_css_class("checked");
315
this->add_css_class("gpanthera-dock-button-active");
316
} else {
317
this->remove_css_class("checked");
318
this->remove_css_class("gpanthera-dock-button-active");
319
}
320
}
321
322
DockButton::~DockButton() {
323
active_style_handler.disconnect();
324
}
325
326
void DockStackSwitcher::update_buttons() {
327
// Clear the old buttons
328
auto old_buttons = collect_children(*this);
329
for(auto *button : old_buttons) {
330
remove(*button);
331
}
332
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
333
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
334
auto *button = Gtk::make_managed<DockButton>(pane);
335
if(pane->get_identifier() != Glib::ustring("empty")) {
336
append(*button);
337
}
338
}
339
}
340
}
341
342
void DockStack::add_pane(DockablePane &child) {
343
child.redock(this);
344
}
345
346
void DockStack::add(Gtk::Widget &child, const Glib::ustring &name) {
347
Gtk::Stack::add(child, name);
348
signal_child_added.emit(&child);
349
}
350
351
void DockStack::remove(Gtk::Widget &child) {
352
Gtk::Stack::remove(child);
353
signal_child_removed.emit(&child);
354
}
355
} // namespace gPanthera
356