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.87 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
DockStack *DockStackSwitcher::get_stack() const {
263
return stack;
264
}
265
266
DockStackSwitcher::~DockStackSwitcher() {
267
add_handler.disconnect();
268
remove_handler.disconnect();
269
}
270
271
DockButton::DockButton(DockablePane *pane) : Gtk::Button(), pane(pane) {
272
if(pane->get_icon()) {
273
this->set_child(*copy_image(pane->get_icon()));
274
}
275
this->set_tooltip_text(pane->get_label()->get_text());
276
this->set_halign(Gtk::Align::CENTER);
277
this->set_valign(Gtk::Align::CENTER);
278
this->get_style_context()->add_class("toggle");
279
this->get_style_context()->add_class("gpanthera-dock-button");
280
// Add/remove CSS classes when the pane is shown/hidden
281
active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() {
282
this->update_active_style();
283
});
284
drag_source = Gtk::DragSource::create();
285
drag_source->set_exclusive(false);
286
// This is to prevent the click handler from taking over grabbing the button
287
drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
288
value = Glib::Value<Gtk::Widget*>();
289
value.init(DockablePane::get_type());
290
value.set(pane);
291
// Add the drag source to the button
292
this->add_controller(drag_source);
293
this->signal_clicked().connect([this, pane]() {
294
if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) {
295
pane->get_stack()->set_visible_child("empty");
296
} else {
297
pane->get_stack()->set_visible_child(pane->get_identifier());
298
}
299
});
300
// Provide the drag data
301
drag_source->signal_prepare().connect([this](double, double) {
302
drag_source->set_actions(Gdk::DragAction::MOVE);
303
auto const paintable = Gtk::WidgetPaintable::create();
304
paintable->set_widget(*this);
305
drag_source->set_icon(paintable, 0, 0);
306
return Gdk::ContentProvider::create(value);
307
}, false);
308
// Pop out if dragged to an external location
309
drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) {
310
if(reason == Gdk::DragCancelReason::NO_TARGET) {
311
this->pane->pop_out();
312
return true;
313
}
314
return false;
315
}, false);
316
// Add a drop target to the button
317
auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE);
318
drop_target->set_actions(Gdk::DragAction::MOVE);
319
drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
320
this->add_controller(drop_target);
321
// Process dropped buttons by inserting them after the current button
322
drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) {
323
const auto &widget = static_cast<const Glib::Value<Gtk::Widget*>&>(value).get();
324
325
if(widget) {
326
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
327
auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent());
328
if(switcher) {
329
auto *stack = switcher->get_stack();
330
// Move the button to the new position
331
pane->redock(stack);
332
pane->insert_before(*stack, *this->pane);
333
switcher->update_buttons();
334
}
335
}
336
}
337
338
return true; // Drop OK
339
}, false);
340
this->update_active_style();
341
}
342
343
void DockButton::update_active_style() {
344
if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) {
345
this->add_css_class("checked");
346
this->add_css_class("gpanthera-dock-button-active");
347
} else {
348
this->remove_css_class("checked");
349
this->remove_css_class("gpanthera-dock-button-active");
350
}
351
}
352
353
DockButton::~DockButton() {
354
active_style_handler.disconnect();
355
}
356
357
void DockStackSwitcher::update_buttons() {
358
// Clear the old buttons
359
auto old_buttons = collect_children(*this);
360
for(auto *button : old_buttons) {
361
remove(*button);
362
}
363
for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) {
364
if(auto pane = dynamic_cast<DockablePane*>(widget)) {
365
auto *button = Gtk::make_managed<DockButton>(pane);
366
if(pane->get_identifier() != Glib::ustring("empty")) {
367
append(*button);
368
}
369
}
370
}
371
}
372
373
void DockStack::add_pane(DockablePane &child) {
374
child.redock(this);
375
}
376
377
void DockStack::add(Gtk::Widget &child, const Glib::ustring &name) {
378
Gtk::Stack::add(child, name);
379
signal_child_added.emit(&child);
380
}
381
382
void DockStack::remove(Gtk::Widget &child) {
383
Gtk::Stack::remove(child);
384
signal_child_removed.emit(&child);
385
}
386
387
DraggableNotebook::DraggableNotebook() : Gtk::Notebook() {
388
}
389
390
void DraggableNotebook::append_page(Gtk::Widget &child, const Glib::ustring &tab_label) {
391
Gtk::Notebook::append_page(child, tab_label);
392
this->set_tab_detachable(child, true);
393
}
394
395
} // namespace gPanthera
396