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) { 477if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) { 478button_button->unset_child(); 479} 480remove(*button); 481} 482ContentTab* first_child = nullptr; 483for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) { 484if(auto page = dynamic_cast<ContentPage*>(widget)) { 485auto *button = Gtk::make_managed<ContentTab>(page); 486if(!first_child) { 487first_child = button; 488} else { 489button->set_group(*first_child); 490} 491this->append(*button); 492} 493} 494} 495 496void ContentStack::add_page(ContentPage &child) { 497child.redock(this); 498} 499 500ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) { 501this->set_child(*page->get_tab_widget()); 502this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB); 503this->set_halign(Gtk::Align::CENTER); 504this->set_valign(Gtk::Align::CENTER); 505this->get_style_context()->add_class("toggle"); 506this->get_style_context()->add_class("gpanthera-content-tab"); 507// Add/remove CSS classes when the pane is shown/hidden 508active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() { 509this->update_active_style(); 510}); 511drag_source = Gtk::DragSource::create(); 512drag_source->set_exclusive(false); 513// This is to prevent the click handler from taking over grabbing the button 514drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); 515value.init(ContentPage::get_type()); 516value.set(page); 517// Add the drag source to the button 518this->add_controller(drag_source); 519this->signal_clicked().connect([this, page]() { 520page->get_stack()->set_visible_child(*page); 521update_active_style(); 522}); 523// Switch tabs on depress 524auto gesture_click = Gtk::GestureClick::create(); 525gesture_click->set_button(1); 526this->add_controller(gesture_click); 527gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) { 528page->get_stack()->set_visible_child(*page); 529update_active_style(); 530}); 531// Provide the drag data 532drag_source->signal_prepare().connect([this](double, double) { 533drag_source->set_actions(Gdk::DragAction::MOVE); 534auto const paintable = Gtk::WidgetPaintable::create(); 535paintable->set_widget(*this); 536drag_source->set_icon(paintable->get_current_image(), 0, 0); 537return Gdk::ContentProvider::create(value); 538}, false); 539update_active_style(); 540drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) { 541this->set_opacity(0); 542}, false); 543drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE); 544// Process dropped buttons by inserting them after the current button 545drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) { 546const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get(); 547 548if(widget) { 549if(auto page = dynamic_cast<ContentPage*>(widget)) { 550if(page->content_manager != this->page->content_manager) { 551// If the pane is not in the same layout manager, reject 552return false; 553} 554auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent()); 555if(switcher) { 556auto *stack = switcher->get_stack(); 557// Move the button to the new position 558page->redock(stack); 559if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) { 560if(x < static_cast<double>(this->get_allocated_width()) / 2) { 561page->insert_before(*stack, *this->page); 562} else { 563page->insert_after(*stack, *this->page); 564} 565} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) { 566if(y < static_cast<double>(this->get_allocated_height()) / 2) { 567page->insert_before(*stack, *this->page); 568} else { 569page->insert_after(*stack, *this->page); 570} 571} 572 573switcher->update_buttons(); 574} 575} 576} 577 578return true; // Drop OK 579}, false); 580this->add_controller(drop_target); 581// Pop out if dragged to an external location 582// TODO: Implement this, allow defining custom behavior at the content manager level 583} 584 585void ContentTab::update_active_style() { 586if(this->page->get_stack()->get_visible_child() == this->page) { 587this->add_css_class("checked"); 588this->add_css_class("gpanthera-dock-button-active"); 589this->set_active(true); 590} else { 591this->remove_css_class("checked"); 592this->remove_css_class("gpanthera-dock-button-active"); 593this->set_active(false); 594} 595} 596 597ContentTab::~ContentTab() { 598active_style_handler.disconnect(); 599} 600 601Gtk::Widget *ContentPage::get_tab_widget() const { 602return this->tab_widget; 603} 604 605Gtk::Stack *ContentPage::get_stack() const { 606return this->stack; 607} 608 609void ContentPage::redock(ContentStack *stack) { 610this->stack = stack; 611if(this->last_stack != nullptr) { 612this->last_stack->remove(*this); 613} 614this->last_stack = stack; 615this->stack->add(*this); 616} 617 618Gtk::Widget *ContentPage::get_child() const { 619return this->child; 620} 621 622ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) : 623Gtk::Box(Gtk::Orientation::VERTICAL, 0), content_manager(std::move(content_manager)), stack(stack), child(child), tab_widget(tab_widget) { 624this->append(*child); 625this->set_tab_widget(tab_widget); 626this->set_margin_top(0); 627this->set_margin_bottom(0); 628this->set_margin_start(0); 629this->set_margin_end(0); 630if(stack) { 631this->content_manager->add_stack(this->stack); 632this->stack->add_page(*this); 633} 634} 635 636ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") { 637} 638 639void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) { 640this->tab_widget = tab_widget; 641} 642 643} // namespace gPanthera 644