gpanthera.cc
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
this->update_active_style();
302
}
303
304
void DockButton::update_active_style() {
305
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
306
this->add_css_class("checked");
307
this->add_css_class("gpanthera-dock-button-active");
308
} else {
309
this->remove_css_class("checked");
310
this->remove_css_class("gpanthera-dock-button-active");
311
}
312
}
313
314
DockButton::~DockButton() {
315
active_style_handler.disconnect();
316
}
317
318
void DockStackSwitcher::update_buttons() {
319
// Clear the old buttons
320
auto old_buttons = collect_children(*this);
321
for(auto *button : old_buttons) {
322
remove(*button);
323
}
324
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
325
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
326
auto *button = Gtk::make_managed<DockButton>(pane);
327
if(pane->get_identifier() != Glib::ustring("empty")) {
328
append(*button);
329
}
330
}
331
}
332
}
333
334
void DockStack::add_pane(DockablePane &child) {
335
child.redock(this);
336
}
337
338
void DockStack::add(Gtk::Widget &child, const Glib::ustring &name) {
339
Gtk::Stack::add(child, name);
340
signal_child_added.emit(&child);
341
}
342
343
void DockStack::remove(Gtk::Widget &child) {
344
Gtk::Stack::remove(child);
345
signal_child_removed.emit(&child);
346
}
347
} // namespace gPanthera
348