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} 319this->set_opacity(1); 320return false; 321}, false); 322// Add a drop target to the button 323auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE); 324drop_target->set_actions(Gdk::DragAction::MOVE); 325drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); 326this->add_controller(drop_target); 327// Process dropped buttons by inserting them after the current button 328drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) { 329const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get(); 330 331if(widget) { 332if(auto pane = dynamic_cast<DockablePane*>(widget)) { 333if(pane->layout != this->pane->layout) { 334// If the pane is not in the same layout manager, reject 335return false; 336} 337auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent()); 338if(switcher) { 339auto *stack = switcher->get_stack(); 340// Move the button to the new position 341pane->redock(stack); 342if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) { 343if(x < static_cast<double>(this->get_allocated_width()) / 2) { 344pane->insert_before(*stack, *this->pane); 345} else { 346pane->insert_after(*stack, *this->pane); 347} 348} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) { 349if(y < static_cast<double>(this->get_allocated_height()) / 2) { 350pane->insert_before(*stack, *this->pane); 351} else { 352pane->insert_after(*stack, *this->pane); 353} 354} 355 356switcher->update_buttons(); 357} 358} 359} 360 361return true; // Drop OK 362}, false); 363this->update_active_style(); 364} 365 366void DockButton::update_active_style() { 367if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) { 368this->add_css_class("checked"); 369this->add_css_class("gpanthera-dock-button-active"); 370this->set_active(true); 371} else { 372this->remove_css_class("checked"); 373this->remove_css_class("gpanthera-dock-button-active"); 374this->set_active(false); 375} 376} 377 378DockButton::~DockButton() { 379active_style_handler.disconnect(); 380} 381 382void DockStackSwitcher::update_buttons() { 383// Clear the old buttons 384auto old_buttons = collect_children(*this); 385for(auto *button : old_buttons) { 386remove(*button); 387} 388DockButton* first_child = nullptr; 389for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) { 390if(auto pane = dynamic_cast<DockablePane*>(widget)) { 391auto *button = Gtk::make_managed<DockButton>(pane); 392if(!first_child) { 393first_child = button; 394} else { 395button->set_group(*first_child); 396} 397if(pane->get_identifier() != Glib::ustring("")) { 398append(*button); 399} 400} 401} 402} 403 404void DockStack::add_pane(DockablePane &child) { 405child.redock(this); 406} 407 408void ContentManager::add_stack(ContentStack *stack) { 409this->stacks.push_back(stack); 410} 411 412void ContentManager::remove_stack(ContentStack *stack) { 413this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end()); 414} 415 416void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) { 417Gtk::Stack::add(child, name); 418signal_child_added.emit(&child); 419} 420 421void BaseStack::add(Gtk::Widget &child) { 422Gtk::Stack::add(child); 423signal_child_added.emit(&child); 424} 425 426void BaseStack::remove(Gtk::Widget &child) { 427Gtk::Stack::remove(child); 428signal_child_removed.emit(&child); 429} 430 431ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager) 432: BaseStack(), content_manager(std::move(content_manager)) { 433this->content_manager->add_stack(this); 434} 435 436ContentTabBar::ContentTabBar(ContentStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) { 437this->get_style_context()->add_class("gpanthera-content-tab-bar"); 438this->set_margin_top(0); 439this->set_margin_bottom(0); 440this->set_margin_start(0); 441this->set_margin_end(0); 442 443auto update_callback = [this](Gtk::Widget*) { 444this->update_buttons(); 445}; 446this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST); 447stack->signal_child_added.connect(update_callback); 448stack->signal_child_removed.connect(update_callback); 449drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE); 450this->add_controller(drop_target); 451// Process dropped buttons 452drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) { 453const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get(); 454 455if(widget) { 456if(auto page = dynamic_cast<ContentPage*>(widget)) { 457this->stack->add_page(*page); 458} 459} 460 461return true; // Drop OK 462}, false); 463} 464 465ContentTabBar::~ContentTabBar() { 466add_handler.disconnect(); 467remove_handler.disconnect(); 468} 469 470ContentStack *ContentTabBar::get_stack() const { 471return stack; 472} 473 474void ContentTabBar::update_buttons() { 475// Clear the old buttons 476auto old_buttons = collect_children(*this); 477for(auto *button : old_buttons) { 478if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) { 479button_button->unset_child(); 480} 481remove(*button); 482} 483ContentTab* first_child = nullptr; 484for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) { 485if(auto page = dynamic_cast<ContentPage*>(widget)) { 486auto *button = Gtk::make_managed<ContentTab>(page); 487if(!first_child) { 488first_child = button; 489} else { 490button->set_group(*first_child); 491} 492this->append(*button); 493} 494} 495} 496 497void ContentStack::add_page(ContentPage &child) { 498child.redock(this); 499} 500 501ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) { 502this->set_child(*page->get_tab_widget()); 503this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB); 504this->set_halign(Gtk::Align::CENTER); 505this->set_valign(Gtk::Align::CENTER); 506this->get_style_context()->add_class("toggle"); 507this->get_style_context()->add_class("gpanthera-content-tab"); 508// Add/remove CSS classes when the pane is shown/hidden 509active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() { 510this->update_active_style(); 511}); 512drag_source = Gtk::DragSource::create(); 513drag_source->set_exclusive(false); 514// This is to prevent the click handler from taking over grabbing the button 515drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); 516value.init(ContentPage::get_type()); 517value.set(page); 518// Add the drag source to the button 519this->add_controller(drag_source); 520this->signal_clicked().connect([this, page]() { 521page->get_stack()->set_visible_child(*page); 522update_active_style(); 523}); 524// Switch tabs on depress 525auto gesture_click = Gtk::GestureClick::create(); 526gesture_click->set_button(1); 527this->add_controller(gesture_click); 528gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) { 529page->get_stack()->set_visible_child(*page); 530update_active_style(); 531}); 532// Provide the drag data 533drag_source->signal_prepare().connect([this](double, double) { 534drag_source->set_actions(Gdk::DragAction::MOVE); 535auto const paintable = Gtk::WidgetPaintable::create(); 536paintable->set_widget(*this); 537drag_source->set_icon(paintable->get_current_image(), 0, 0); 538return Gdk::ContentProvider::create(value); 539}, false); 540update_active_style(); 541drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) { 542this->set_opacity(0); 543}, false); 544drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE); 545// Process dropped buttons by inserting them after the current button 546drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) { 547const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get(); 548 549if(widget) { 550if(auto page = dynamic_cast<ContentPage*>(widget)) { 551if(page->content_manager != this->page->content_manager) { 552// If the pane is not in the same layout manager, reject 553return false; 554} 555auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent()); 556if(switcher) { 557auto *stack = switcher->get_stack(); 558// Move the button to the new position 559page->redock(stack); 560if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) { 561if(x < static_cast<double>(this->get_allocated_width()) / 2) { 562page->insert_before(*stack, *this->page); 563} else { 564page->insert_after(*stack, *this->page); 565} 566} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) { 567if(y < static_cast<double>(this->get_allocated_height()) / 2) { 568page->insert_before(*stack, *this->page); 569} else { 570page->insert_after(*stack, *this->page); 571} 572} 573 574switcher->update_buttons(); 575} 576} 577} 578 579return true; // Drop OK 580}, false); 581this->add_controller(drop_target); 582// Pop out if dragged to an external location 583drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) { 584if(reason == Gdk::DragCancelReason::NO_TARGET) { 585auto stack = dynamic_cast<ContentStack*>(this->page->get_stack()); 586this->set_opacity(1); 587return stack->signal_detach.emit(this->page); 588} 589this->set_opacity(1); 590return false; 591}, false); 592} 593 594void ContentTab::update_active_style() { 595if(this->page->get_stack()->get_visible_child() == this->page) { 596this->add_css_class("checked"); 597this->add_css_class("gpanthera-dock-button-active"); 598this->set_active(true); 599} else { 600this->remove_css_class("checked"); 601this->remove_css_class("gpanthera-dock-button-active"); 602this->set_active(false); 603} 604} 605 606ContentTab::~ContentTab() { 607active_style_handler.disconnect(); 608} 609 610Gtk::Widget *ContentPage::get_tab_widget() const { 611return this->tab_widget; 612} 613 614Gtk::Stack *ContentPage::get_stack() const { 615return this->stack; 616} 617 618void ContentPage::redock(ContentStack *stack) { 619this->stack = stack; 620if(this->last_stack != nullptr) { 621this->last_stack->remove(*this); 622} 623this->last_stack = stack; 624this->stack->add(*this); 625} 626 627Gtk::Widget *ContentPage::get_child() const { 628return this->child; 629} 630 631ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) : 632Gtk::Box(Gtk::Orientation::VERTICAL, 0), content_manager(std::move(content_manager)), stack(stack), child(child), tab_widget(tab_widget) { 633this->append(*child); 634this->set_tab_widget(tab_widget); 635this->set_margin_top(0); 636this->set_margin_bottom(0); 637this->set_margin_start(0); 638this->set_margin_end(0); 639if(stack) { 640this->content_manager->add_stack(this->stack); 641this->stack->add_page(*this); 642} 643} 644 645ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") { 646} 647 648void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) { 649this->tab_widget = tab_widget; 650} 651 652} // namespace gPanthera 653