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