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) { 11std::vector<Gtk::Widget*> children; 12for(auto *child = widget.get_first_child(); child; child = child->get_next_sibling()) { 13children.push_back(child); 14} 15return children; 16} 17 18Gtk::Image *copy_image(Gtk::Image *image) { 19if(image->get_storage_type() == Gtk::Image::Type::PAINTABLE) { 20return Gtk::make_managed<Gtk::Image>(image->get_paintable()); 21} else if(image->get_storage_type() == Gtk::Image::Type::ICON_NAME) { 22auto new_image = Gtk::make_managed<Gtk::Image>(); 23new_image->set_from_icon_name(image->get_icon_name()); 24return new_image; 25} else { 26return nullptr; 27} 28} 29 30void init() { 31// Set up gettext configuration 32setlocale(LC_ALL, ""); 33 34bindtextdomain("gpanthera", "./locales"); 35textdomain("gpanthera"); 36} 37 38DockablePane::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) 39: Gtk::Box(Gtk::Orientation::VERTICAL, 0), name(name) { 40if(icon) { 41this->icon = icon; 42} 43if(stack) { 44this->stack = stack; 45} 46this->layout = std::move(layout); 47this->label.set_text(label); 48header = std::make_unique<Gtk::HeaderBar>(); 49header->set_show_title_buttons(false); 50if(custom_header) { 51header->set_title_widget(*custom_header); 52} else { 53header->set_title_widget(this->label); 54} 55header->add_css_class("gpanthera-dock-titlebar"); 56auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>(); 57auto header_menu = Gio::Menu::create(); 58header_menu_button->set_direction(Gtk::ArrowType::NONE); 59 60// Pane menu 61this->action_group = Gio::SimpleActionGroup::create(); 62header_menu_button->insert_action_group("win", action_group); 63 64// Close action 65auto close_action = Gio::SimpleAction::create("close"); 66close_action->signal_activate().connect([this](const Glib::VariantBase&) { 67if(this->stack) { 68this->stack->set_visible_child("empty"); 69} 70}); 71action_group->add_action(close_action); 72header_menu->append(_("Close"), "win.close"); 73 74// Pop out action 75auto pop_out_action = Gio::SimpleAction::create("pop_out"); 76pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) { 77if(this->stack) { 78this->pop_out(); 79} 80}); 81action_group->add_action(pop_out_action); 82header_menu->append(_("Pop out"), "win.pop_out"); 83 84// Move menu 85auto move_menu = Gio::Menu::create(); 86for(auto &this_stack : this->layout->stacks) { 87auto action_name = "move_" + this_stack->name; 88auto move_action = Gio::SimpleAction::create(action_name); 89move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) { 90this_stack->add_pane(*this); 91}); 92action_group->add_action(move_action); 93move_menu->append(this_stack->name, "win." + action_name); 94} 95 96// Add move submenu 97header_menu->append_submenu(_("Move"), move_menu); 98 99// Switch to traditional (nested) submenus, not sliding 100auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED); 101popover_menu->set_has_arrow(false); 102header_menu_button->set_popover(*popover_menu); 103 104// TODO: Add a context menu as well 105 106header->pack_end(*header_menu_button); 107 108this->prepend(*header); 109this->child = &child; 110this->append(child); 111} 112 113Gtk::Stack *DockablePane::get_stack() const { 114return stack; 115} 116 117void DockablePane::redock(DockStack *stack) { 118if(this->window != nullptr) { 119this->window->hide(); 120this->window->unset_titlebar(); 121this->window->set_decorated(false); 122this->header->get_style_context()->remove_class("titlebar"); 123this->prepend(*this->header); 124this->window->unset_child(); 125this->window->close(); 126} else if(this->stack == this->get_parent()) { 127this->stack->remove(*this); 128} 129this->stack = stack; 130this->last_stack = stack; 131this->stack->add(*this, this->get_identifier()); 132if(this->window != nullptr) { 133this->window->destroy(); 134delete this->window; 135this->window = nullptr; 136auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out")); 137action->set_enabled(true); 138this->header->get_style_context()->remove_class("gpanthera-dock-titlebar-popout"); 139} 140} 141 142void DockablePane::pop_out() { 143if(this->stack != nullptr) { 144this->stack->remove(*this); 145this->stack = nullptr; 146} 147 148if(this->window == nullptr) { 149this->remove(*this->header); 150this->window = new DockWindow(this); 151this->window->set_titlebar(*this->header); 152this->window->set_child(*this); 153this->window->set_decorated(true); 154auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out")); 155action->set_enabled(false); 156this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout"); 157} 158this->window->present(); 159} 160 161Glib::ustring DockablePane::get_identifier() const { 162return name; 163} 164 165Gtk::Image *DockablePane::get_icon() const { 166return icon; 167} 168 169Gtk::Widget *DockablePane::get_child() const { 170return child; 171} 172 173Gtk::Label *DockablePane::get_label() { 174return &label; 175} 176 177LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") { 178} 179 180void LayoutManager::add_pane(DockablePane *pane) { 181panes.push_back(pane); 182} 183 184void LayoutManager::add_stack(DockStack *stack) { 185stacks.push_back(stack); 186} 187 188DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name) : Gtk::Stack(), layout(layout), name(name) { 189auto *empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 190add(*empty_child, "empty"); 191// Add the stack to a layout manager 192this->layout->add_stack(this); 193 194// Hide the stack when the empty child is visible 195this->property_visible_child_name().signal_changed().connect([this]() { 196if(this->get_visible_child_name() == "empty") { 197this->hide(); 198} else { 199this->show(); 200} 201}); 202 203this->set_visible_child("empty"); 204this->hide(); 205} 206 207DockWindow::DockWindow(DockablePane *pane) { 208this->pane = pane; 209this->set_child(*pane); 210// Attempting to close the window should redock the pane so it doesn't vanish 211this->signal_close_request().connect([this]() { 212this->pane->redock(this->pane->last_stack); 213return true; 214}, false); 215} 216 217DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) { 218auto update_callback = [this](Gtk::Widget*) { 219this->update_buttons(); 220}; 221stack->signal_child_added.connect(update_callback); 222stack->signal_child_removed.connect(update_callback); 223this->get_style_context()->add_class("gpanthera-dock-switcher"); 224} 225 226DockStackSwitcher::~DockStackSwitcher() { 227add_handler.disconnect(); 228remove_handler.disconnect(); 229} 230 231DockButton::DockButton(DockablePane *pane) : Gtk::Button(), pane(pane) { 232if(pane->get_icon()) { 233this->set_child(*copy_image(pane->get_icon())); 234} 235this->set_tooltip_text(pane->get_label()->get_text()); 236this->set_halign(Gtk::Align::CENTER); 237this->set_valign(Gtk::Align::CENTER); 238this->get_style_context()->add_class("toggle"); 239this->get_style_context()->add_class("gpanthera-dock-button"); 240active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() { 241if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) { 242this->add_css_class("checked"); 243this->add_css_class("gpanthera-dock-button-active"); 244} else { 245this->remove_css_class("checked"); 246this->remove_css_class("gpanthera-dock-button-active"); 247} 248}); 249} 250 251DockButton::~DockButton() { 252active_style_handler.disconnect(); 253} 254 255void DockStackSwitcher::update_buttons() { 256auto old_buttons = collect_children(*this); 257for(auto *button : old_buttons) { 258remove(*button); 259} 260for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) { 261if(auto pane = dynamic_cast<DockablePane*>(widget)) { 262auto *button = Gtk::make_managed<DockButton>(pane); 263button->signal_clicked().connect([this, pane]() { 264if(stack->get_visible_child_name() == pane->get_identifier()) { 265stack->set_visible_child("empty"); 266} else { 267stack->set_visible_child(pane->get_identifier()); 268} 269}); 270if(pane->get_identifier() != Glib::ustring("empty")) { 271append(*button); 272} 273} 274} 275} 276 277void DockStack::add_pane(DockablePane &child) { 278child.redock(this); 279} 280 281void DockStack::add(Gtk::Widget &child, const Glib::ustring &name) { 282Gtk::Stack::add(child, name); 283signal_child_added.emit(&child); 284} 285 286void DockStack::remove(Gtk::Widget &child) { 287Gtk::Stack::remove(child); 288signal_child_removed.emit(&child); 289} 290} // namespace gPanthera 291