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 configurationIsn't 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); 48// This should be replaced with a custom class in the future 49header = std::make_unique<Gtk::HeaderBar>(); 50header->set_show_title_buttons(false); 51if(custom_header) { 52header->set_title_widget(*custom_header); 53} else { 54header->set_title_widget(this->label); 55} 56header->add_css_class("gpanthera-dock-titlebar"); 57auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>(); 58auto header_menu = Gio::Menu::create(); 59header_menu_button->set_direction(Gtk::ArrowType::NONE); 60 61// Pane menu 62this->action_group = Gio::SimpleActionGroup::create(); 63header_menu_button->insert_action_group("win", action_group); 64 65// Close action 66auto close_action = Gio::SimpleAction::create("close"); 67close_action->signal_activate().connect([this](const Glib::VariantBase&) { 68if(this->stack) { 69this->stack->set_visible_child(""); 70} 71}); 72action_group->add_action(close_action); 73header_menu->append(_("Close"), "win.close"); 74 75// Pop out action 76auto pop_out_action = Gio::SimpleAction::create("pop_out"); 77pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) { 78if(this->stack) { 79this->pop_out(); 80} 81}); 82action_group->add_action(pop_out_action); 83header_menu->append(_("Pop out"), "win.pop_out"); 84 85// Move menu 86auto move_menu = Gio::Menu::create(); 87for(auto &this_stack : this->layout->stacks) { 88auto action_name = "move_" + this_stack->name; 89auto move_action = Gio::SimpleAction::create(action_name); 90move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) { 91this_stack->add_pane(*this); 92}); 93action_group->add_action(move_action); 94move_menu->append(this_stack->name, "win." + action_name); 95} 96 97// Add move submenu 98header_menu->append_submenu(_("Move"), move_menu); 99 100// Switch to traditional (nested) submenus, not sliding 101auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED); 102popover_menu->set_has_arrow(false); 103header_menu_button->set_popover(*popover_menu); 104 105// TODO: Add a context menu as well 106 107header->pack_end(*header_menu_button); 108 109this->prepend(*header); 110this->child = &child; 111this->append(child); 112} 113 114Gtk::Stack *DockablePane::get_stack() const { 115return stack; 116} 117 118void DockablePane::redock(DockStack *stack) { 119if(this->window != nullptr) { 120this->window->hide(); 121// Put the titlebar back 122this->window->unset_titlebar(); 123this->window->set_decorated(false); 124this->header->get_style_context()->remove_class("titlebar"); 125this->prepend(*this->header); 126this->window->unset_child(); 127this->window->close(); 128} else if(this->get_parent() && this->stack == this->get_parent()) { 129this->stack->remove(*this); 130} 131this->stack = stack; 132this->last_stack = stack; 133this->stack->add(*this, this->get_identifier()); 134if(this->window != nullptr) { 135this->window->destroy(); 136delete this->window; 137this->window = nullptr; 138// Re-enable the pop out option 139auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out")); 140action->set_enabled(true); 141this->header->get_style_context()->remove_class("gpanthera-dock-titlebar-popout"); 142} 143} 144 145void DockablePane::pop_out() { 146if(this->stack != nullptr) { 147this->stack->remove(*this); 148this->stack = nullptr; 149} 150 151if(this->window == nullptr) { 152// Remove the header bar from the pane, so it can be used as the titlebar of the window 153this->remove(*this->header); 154this->window = new DockWindow(this); 155this->window->set_titlebar(*this->header); 156this->window->set_child(*this); 157this->window->set_decorated(true); 158// Grey out the pop-out option 159auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out")); 160action->set_enabled(false); 161this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout"); 162} 163this->window->present(); 164} 165 166Glib::ustring DockablePane::get_identifier() const { 167return name; 168} 169 170Gtk::Image *DockablePane::get_icon() const { 171return icon; 172} 173 174Gtk::Widget *DockablePane::get_child() const { 175return child; 176} 177 178Gtk::Label *DockablePane::get_label() { 179return &label; 180} 181 182LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") { 183} 184 185void LayoutManager::add_pane(DockablePane *pane) { 186panes.push_back(pane); 187} 188 189void LayoutManager::add_stack(DockStack *stack) { 190stacks.push_back(stack); 191} 192 193void LayoutManager::remove_pane(DockablePane *pane) { 194panes.erase(std::ranges::remove(panes, pane).begin(), panes.end()); 195} 196 197void LayoutManager::remove_stack(DockStack *stack) { 198stacks.erase(std::ranges::remove(stacks, stack).begin(), stacks.end()); 199} 200 201BaseStack::BaseStack() : Gtk::Stack() { 202} 203 204DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name) : BaseStack(), layout(layout), name(name) { 205auto empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 206this->add(*empty_child, ""); 207// Add the stack to a layout manager 208this->layout->add_stack(this); 209 210// Hide the stack when no child is visible 211this->property_visible_child_name().signal_changed().connect([this]() { 212if(this->get_visible_child_name() == "") { 213this->hide(); 214} else { 215this->show(); 216} 217}); 218 219// Also hide when the visible child is removed 220this->signal_child_removed.connect([this](Gtk::Widget* const &child) { 221if(this->get_visible_child_name() == "") { 222this->hide(); 223} 224}); 225 226this->set_visible_child(""); 227this->hide(); 228} 229 230DockWindow::DockWindow(DockablePane *pane) { 231this->pane = pane; 232this->set_child(*pane); 233// Attempting to close the window should redock the pane so it doesn't vanish 234this->signal_close_request().connect([this]() { 235this->pane->redock(this->pane->last_stack); 236return true; 237}, false); 238} 239 240DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) { 241auto update_callback = [this](Gtk::Widget*) { 242this->update_buttons(); 243}; 244this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST); 245stack->signal_child_added.connect(update_callback); 246stack->signal_child_removed.connect(update_callback); 247this->get_style_context()->add_class("gpanthera-dock-switcher"); 248drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE); 249this->add_controller(drop_target); 250// Process dropped buttons 251drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) { 252const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get(); 253 254if(widget) { 255if(auto pane = dynamic_cast<DockablePane*>(widget)) { 256this->stack->add_pane(*pane); 257} 258} 259 260return true; // Drop OK 261}, false); 262} 263 264DockStack *DockStackSwitcher::get_stack() const { 265return stack; 266} 267 268DockStackSwitcher::~DockStackSwitcher() { 269add_handler.disconnect(); 270remove_handler.disconnect(); 271} 272 273DockButton::DockButton(DockablePane *pane) : Gtk::ToggleButton(), pane(pane) { 274if(pane->get_icon()) { 275this->set_child(*copy_image(pane->get_icon())); 276} 277this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB); 278this->set_tooltip_text(pane->get_label()->get_text()); 279this->set_halign(Gtk::Align::CENTER); 280this->set_valign(Gtk::Align::CENTER); 281this->get_style_context()->add_class("toggle"); 282this->get_style_context()->add_class("gpanthera-dock-button"); 283// Add/remove CSS classes when the pane is shown/hidden 284active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() { 285this->update_active_style(); 286}); 287drag_source = Gtk::DragSource::create(); 288drag_source->set_exclusive(false); 289// This is to prevent the click handler from taking over grabbing the button 290drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); 291value.init(DockablePane::get_type()); 292value.set(pane); 293// Add the drag source to the button 294this->add_controller(drag_source); 295this->signal_clicked().connect([this, pane]() { 296if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) { 297pane->get_stack()->set_visible_child(""); 298} else { 299pane->get_stack()->set_visible_child(pane->get_identifier()); 300} 301}); 302// Provide the drag data 303drag_source->signal_prepare().connect([this](double, double) { 304drag_source->set_actions(Gdk::DragAction::MOVE); 305auto const paintable = Gtk::WidgetPaintable::create(); 306paintable->set_widget(*this); 307drag_source->set_icon(paintable->get_current_image(), 0, 0); 308return Gdk::ContentProvider::create(value); 309}, false); 310drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) { 311this->set_opacity(0); 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<DockablePane*>&>(value).get(); 329 330if(widget) { 331if(auto pane = dynamic_cast<DockablePane*>(widget)) { 332if(pane->layout != this->pane->layout) { 333// If the pane is not in the same layout manager, reject 334return false; 335} 336auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent()); 337if(switcher) { 338auto *stack = switcher->get_stack(); 339// Move the button to the new position 340pane->redock(stack); 341if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) { 342if(x < static_cast<double>(this->get_allocated_width()) / 2) { 343pane->insert_before(*stack, *this->pane); 344} else { 345pane->insert_after(*stack, *this->pane); 346} 347} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) { 348if(y < static_cast<double>(this->get_allocated_height()) / 2) { 349pane->insert_before(*stack, *this->pane); 350} else { 351pane->insert_after(*stack, *this->pane); 352} 353} 354 355switcher->update_buttons(); 356} 357} 358} 359 360return true; // Drop OK 361}, false); 362this->update_active_style(); 363} 364 365void DockButton::update_active_style() { 366if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) { 367this->add_css_class("checked"); 368this->add_css_class("gpanthera-dock-button-active"); 369this->set_active(true); 370} else { 371this->remove_css_class("checked"); 372this->remove_css_class("gpanthera-dock-button-active"); 373this->set_active(false); 374} 375} 376 377DockButton::~DockButton() { 378active_style_handler.disconnect(); 379} 380 381void DockStackSwitcher::update_buttons() { 382// Clear the old buttons 383auto old_buttons = collect_children(*this); 384for(auto *button : old_buttons) { 385remove(*button); 386} 387DockButton* first_child = nullptr; 388for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) { 389if(auto pane = dynamic_cast<DockablePane*>(widget)) { 390auto *button = Gtk::make_managed<DockButton>(pane); 391if(!first_child) { 392first_child = button; 393} else { 394button->set_group(*first_child); 395} 396if(pane->get_identifier() != Glib::ustring("")) { 397append(*button); 398} 399} 400} 401} 402 403void DockStack::add_pane(DockablePane &child) { 404child.redock(this); 405} 406 407void ContentManager::add_stack(ContentStack *stack) { 408this->stacks.push_back(stack); 409} 410 411void ContentManager::remove_stack(ContentStack *stack) { 412this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end()); 413} 414 415void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) { 416Gtk::Stack::add(child, name); 417signal_child_added.emit(&child); 418} 419 420void BaseStack::add(Gtk::Widget &child) { 421Gtk::Stack::add(child); 422signal_child_added.emit(&child); 423} 424 425void BaseStack::remove(Gtk::Widget &child) { 426Gtk::Stack::remove(child); 427signal_child_removed.emit(&child); 428} 429 430ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager) 431: BaseStack(), content_manager(std::move(content_manager)) { 432this->content_manager->add_stack(this); 433} 434 435ContentTabBar::ContentTabBar(ContentStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) { 436this->get_style_context()->add_class("gpanthera-content-tab-bar"); 437this->set_margin_top(0); 438this->set_margin_bottom(0); 439this->set_margin_start(0); 440this->set_margin_end(0); 441 442auto update_callback = [this](Gtk::Widget*) { 443this->update_buttons(); 444}; 445this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST); 446stack->signal_child_added.connect(update_callback); 447stack->signal_child_removed.connect(update_callback); 448drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE); 449this->add_controller(drop_target); 450// Process dropped buttons 451drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) { 452const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get(); 453 454if(widget) { 455if(auto page = dynamic_cast<ContentPage*>(widget)) { 456this->stack->add_page(*page); 457} 458} 459 460return true; // Drop OK 461}, false); 462} 463 464ContentTabBar::~ContentTabBar() { 465add_handler.disconnect(); 466remove_handler.disconnect(); 467} 468 469ContentStack *ContentTabBar::get_stack() const { 470return stack; 471} 472 473void ContentTabBar::update_buttons() { 474// Clear the old buttons 475auto old_buttons = collect_children(*this); 476for(auto *button : old_buttons) { 477remove(*button); 478} 479ContentTab* first_child = nullptr; 480for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) { 481if(auto page = dynamic_cast<ContentPage*>(widget)) { 482auto *button = Gtk::make_managed<ContentTab>(page); 483if(!first_child) { 484first_child = button; 485} else { 486button->set_group(*first_child); 487} 488this->append(*button); 489} 490} 491} 492 493void ContentStack::add_page(ContentPage &child) { 494child.redock(this); 495} 496 497ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) { 498this->set_child(*page->get_tab_widget()); 499this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB); 500this->set_halign(Gtk::Align::CENTER); 501this->set_valign(Gtk::Align::CENTER); 502this->get_style_context()->add_class("toggle"); 503this->get_style_context()->add_class("gpanthera-content-tab"); 504// Add/remove CSS classes when the pane is shown/hidden 505active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() { 506this->update_active_style(); 507}); 508drag_source = Gtk::DragSource::create(); 509drag_source->set_exclusive(false); 510// This is to prevent the click handler from taking over grabbing the button 511drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); 512value.init(ContentPage::get_type()); 513value.set(page); 514// Add the drag source to the button 515this->add_controller(drag_source); 516this->signal_clicked().connect([this, page]() { 517page->get_stack()->set_visible_child(*page); 518update_active_style(); 519}); 520// Provide the drag data 521drag_source->signal_prepare().connect([this](double, double) { 522drag_source->set_actions(Gdk::DragAction::MOVE); 523auto const paintable = Gtk::WidgetPaintable::create(); 524paintable->set_widget(*this); 525drag_source->set_icon(paintable, 0, 0); 526return Gdk::ContentProvider::create(value); 527}, false); 528update_active_style(); 529// Pop out if dragged to an external location 530// TODO: Implement this, allow defining custom behavior at the content manager level 531} 532 533void ContentTab::update_active_style() { 534if(this->page->get_stack()->get_visible_child() == this->page) { 535this->add_css_class("checked"); 536this->add_css_class("gpanthera-dock-button-active"); 537this->set_active(true); 538} else { 539this->remove_css_class("checked"); 540this->remove_css_class("gpanthera-dock-button-active"); 541this->set_active(false); 542} 543} 544 545ContentTab::~ContentTab() { 546active_style_handler.disconnect(); 547} 548 549Gtk::Widget *ContentPage::get_tab_widget() const { 550return this->tab_widget; 551} 552 553Gtk::Stack *ContentPage::get_stack() const { 554return this->stack; 555} 556 557void ContentPage::redock(ContentStack *stack) { 558this->stack = stack; 559if(this->last_stack != nullptr) { 560this->last_stack->remove(*this); 561} 562this->last_stack = stack; 563this->stack->add(*this, this->get_child()->get_name()); 564} 565 566Gtk::Widget *ContentPage::get_child() const { 567return this->child; 568} 569 570ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) : 571Gtk::Box(Gtk::Orientation::VERTICAL, 0), content_manager(std::move(content_manager)), stack(stack), child(child), tab_widget(tab_widget) { 572this->content_manager->add_stack(this->stack); 573this->append(*child); 574this->set_tab_widget(tab_widget); 575this->set_margin_top(0); 576this->set_margin_bottom(0); 577this->set_margin_start(0); 578this->set_margin_end(0); 579} 580 581ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") { 582} 583 584void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) { 585this->tab_widget = tab_widget; 586} 587 588} // namespace gPanthera 589