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