gpanthera.cc
C++ source, ASCII text
1#include "gpanthera.hh" 2 3#include <cassert> 4#include <iostream> 5#include <utility> 6#include <libintl.h> 7#include <locale.h> 8#include <filesystem> 9#include <ranges> 10#include <nlohmann/json.hpp> 11#include <unordered_set> 12 13#define _(STRING) dgettext("gpanthera", STRING) 14 15namespace gPanthera { 16sigc::signal<void(double, double)> add_context_menu(Gtk::Widget &widget) { 17sigc::signal<void(double, double)> signal; 18// Add a context menu to a widget 19auto gesture_click = Gtk::GestureClick::create(); 20gesture_click->set_button(3); 21auto gesture_keyboard = Gtk::EventControllerKey::create(); 22gesture_click->signal_pressed().connect([&widget, signal](int n_press, double x, double y) { 23if(n_press == 1) { 24signal.emit(x, y); 25} 26}, false); 27gesture_keyboard->signal_key_pressed().connect([&widget, signal](int keyval, int keycode, Gdk::ModifierType state) { 28if((keyval == GDK_KEY_F10) && (state == Gdk::ModifierType::SHIFT_MASK) || keyval == GDK_KEY_Menu) { 29// auto popover = Gtk::make_managed<Gtk::PopoverMenu>(); 30// popover->set_menu_model(menu); 31// popover->set_has_arrow(false); 32// popover->set_halign(Gtk::Align::START); 33// popover->set_pointing_to(Gdk::Rectangle(0, 0, 1, 1)); 34// popover->set_parent(widget); 35// popover->popup(); 36signal.emit(0, 0); 37} 38return true; 39}, false); 40widget.add_controller(gesture_click); 41widget.add_controller(gesture_keyboard); 42return signal; 43} 44 45std::vector<Gtk::Widget*> collect_children(Gtk::Widget &widget) { 46// Get a vector of the children of a GTK widget, since the container API was removed in GTK 4 47std::vector<Gtk::Widget*> children; 48for(auto *child = widget.get_first_child(); child; child = child->get_next_sibling()) { 49children.push_back(child); 50} 51return children; 52} 53 54Gtk::Image *copy_image(Gtk::Image *image) { 55// Generate a new Gtk::Image with the same contents as an existing one 56if(image->get_storage_type() == Gtk::Image::Type::PAINTABLE) { 57return Gtk::make_managed<Gtk::Image>(image->get_paintable()); 58} else if(image->get_storage_type() == Gtk::Image::Type::ICON_NAME) { 59auto new_image = Gtk::make_managed<Gtk::Image>(); 60new_image->set_from_icon_name(image->get_icon_name()); 61return new_image; 62} else if(image->get_storage_type() == Gtk::Image::Type::GICON) { 63auto new_image = Gtk::make_managed<Gtk::Image>(image->get_gicon()); 64return new_image; 65} else { 66return nullptr; 67} 68} 69 70void init() { 71g_log_set_always_fatal(G_LOG_LEVEL_CRITICAL); 72// Set up gettext configuration 73bindtextdomain("gpanthera", "./locales"); 74} 75 76DockablePane::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) 77: Gtk::Box(Gtk::Orientation::VERTICAL, 0), name(name) { 78if(icon) { 79this->icon = icon; 80} 81if(stack) { 82this->stack = stack; 83} 84this->layout = layout; 85this->layout->add_pane(this); 86this->label.set_text(label); 87// This should be replaced with a custom class in the future 88header = std::make_unique<Gtk::HeaderBar>(); 89header->set_show_title_buttons(false); 90if(custom_header) { 91header->set_title_widget(*custom_header); 92} else { 93header->set_title_widget(this->label); 94} 95header->add_css_class("gpanthera-dock-titlebar"); 96auto header_menu_button = Gtk::make_managed<Gtk::MenuButton>(); 97header_menu_button->set_direction(Gtk::ArrowType::NONE); 98 99// Pane menu 100this->action_group = Gio::SimpleActionGroup::create(); 101header_menu_button->insert_action_group("win", action_group); 102 103// Close action 104auto close_action = Gio::SimpleAction::create("close"); 105close_action->signal_activate().connect([this](const Glib::VariantBase&) { 106if(this->stack) { 107this->stack->set_visible_child(""); 108} 109}); 110action_group->add_action(close_action); 111header_menu->append(_("Close"), "win.close"); 112 113// Pop out action 114auto pop_out_action = Gio::SimpleAction::create("pop_out"); 115pop_out_action->signal_activate().connect([this](const Glib::VariantBase&) { 116if(this->stack) { 117this->pop_out(); 118} 119}); 120action_group->add_action(pop_out_action); 121header_menu->append(_("Pop out"), "win.pop_out"); 122 123// Move menu 124auto move_menu = Gio::Menu::create(); 125for(auto &this_stack : this->layout->stacks) { 126auto action_name = "move_" + this_stack->name; 127auto move_action = Gio::SimpleAction::create(action_name); 128move_action->signal_activate().connect([this, this_stack](const Glib::VariantBase&) { 129this_stack->add_pane(*this); 130}); 131action_group->add_action(move_action); 132move_menu->append(this_stack->name, "win." + action_name); 133} 134 135// Add move submenu 136header_menu->append_submenu(_("Move"), move_menu); 137 138// Switch to traditional (nested) submenus, not sliding 139auto popover_menu = Gtk::make_managed<Gtk::PopoverMenu>(header_menu, Gtk::PopoverMenu::Flags::NESTED); 140popover_menu->set_has_arrow(false); 141header_menu_button->set_popover(*popover_menu); 142 143// TODO: Add a context menu as well 144 145header->pack_end(*header_menu_button); 146 147this->prepend(*header); 148this->child = &child; 149this->append(child); 150} 151 152Gtk::Stack *DockablePane::get_stack() const { 153return stack; 154} 155 156void DockablePane::redock(DockStack *stack) { 157if(this->window != nullptr) { 158this->window->hide(); 159// Put the titlebar back 160this->window->unset_titlebar(); 161this->window->set_decorated(false); 162this->header->get_style_context()->remove_class("titlebar"); 163this->prepend(*this->header); 164this->window->unset_child(); 165this->window->close(); 166} else if(this->get_parent() && this->stack == this->get_parent()) { 167this->stack->remove(*this); 168} 169this->stack = stack; 170this->last_stack = stack; 171this->stack->add(*this, this->get_identifier()); 172if(this->window != nullptr) { 173this->window->destroy(); 174delete this->window; 175this->window = nullptr; 176// Re-enable the pop out option 177auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out")); 178action->set_enabled(true); 179this->header->get_style_context()->remove_class("gpanthera-dock-titlebar-popout"); 180} 181layout->signal_pane_moved.emit(this); 182} 183 184void DockablePane::pop_out() { 185if(this->stack != nullptr) { 186this->stack->remove(*this); 187this->stack = nullptr; 188} 189 190if(this->window == nullptr) { 191// Remove the header bar from the pane, so it can be used as the titlebar of the window 192this->remove(*this->header); 193this->window = new DockWindow(this); 194this->window->set_titlebar(*this->header); 195this->window->set_child(*this); 196this->window->set_decorated(true); 197// Grey out the pop-out option 198auto action = std::dynamic_pointer_cast<Gio::SimpleAction>(this->action_group->lookup_action("pop_out")); 199action->set_enabled(false); 200this->header->get_style_context()->add_class("gpanthera-dock-titlebar-popout"); 201} 202this->window->present(); 203this->signal_pane_shown.emit(); 204layout->signal_pane_moved.emit(this); 205} 206 207Glib::ustring DockablePane::get_identifier() const { 208return name; 209} 210 211Gtk::Image *DockablePane::get_icon() const { 212return icon; 213} 214 215Gtk::Widget *DockablePane::get_child() const { 216return child; 217} 218 219Gtk::Label *DockablePane::get_label() { 220return &label; 221} 222 223LayoutManager::LayoutManager() : Glib::ObjectBase("LayoutManager") { 224} 225 226void LayoutManager::add_pane(DockablePane *pane) { 227panes.push_back(pane); 228} 229 230void LayoutManager::add_stack(DockStack *stack) { 231stacks.push_back(stack); 232} 233 234void LayoutManager::remove_pane(DockablePane *pane) { 235panes.erase(std::ranges::remove(panes, pane).begin(), panes.end()); 236} 237 238void LayoutManager::remove_stack(DockStack *stack) { 239stacks.erase(std::ranges::remove(stacks, stack).begin(), stacks.end()); 240} 241 242BaseStack::BaseStack() : Gtk::Stack() { 243} 244 245DockStack::DockStack(std::shared_ptr<LayoutManager> layout, const Glib::ustring &name, const std::string &id) : BaseStack(), layout(layout), name(name), id(id) { 246auto empty_child = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0); 247this->add(*empty_child, ""); 248// Add the stack to a layout manager 249this->layout->add_stack(this); 250 251// Hide the stack when no child is visible 252this->property_visible_child_name().signal_changed().connect([this]() { 253if(last_pane) { 254last_pane->signal_pane_hidden.emit(); 255} 256if(this->get_visible_child_name() == "") { 257this->hide(); 258} else { 259this->show(); 260} 261last_pane = dynamic_cast<DockablePane*>(this->get_visible_child()); 262if(last_pane) { 263last_pane->signal_pane_shown.emit(); 264} 265}); 266 267// Also hide when the visible child is removed 268this->signal_child_removed.connect([this](Gtk::Widget* const &child) { 269if(this->get_visible_child_name() == "") { 270last_pane = nullptr; 271this->hide(); 272} 273}); 274 275this->set_visible_child(""); 276this->hide(); 277} 278 279DockWindow::DockWindow(DockablePane *pane) { 280this->pane = pane; 281this->set_child(*pane); 282// Attempting to close the window should redock the pane so it doesn't vanish 283this->signal_close_request().connect([this]() { 284this->pane->redock(this->pane->last_stack); 285return true; 286}, false); 287} 288 289DockStackSwitcher::DockStackSwitcher(DockStack *stack, Gtk::Orientation orientation) : Gtk::Box(orientation), stack(stack) { 290auto update_callback = [this](Gtk::Widget*) { 291this->update_buttons(); 292}; 293this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST); 294stack->signal_child_added.connect(update_callback); 295stack->signal_child_removed.connect(update_callback); 296this->get_style_context()->add_class("gpanthera-dock-switcher"); 297drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE); 298this->add_controller(drop_target); 299// Process dropped buttons 300drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) { 301const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get(); 302 303if(widget) { 304if(auto pane = dynamic_cast<DockablePane*>(widget)) { 305this->stack->add_pane(*pane); 306} 307} 308 309return true; // Drop OK 310}, false); 311} 312 313DockStack *DockStackSwitcher::get_stack() const { 314return stack; 315} 316 317DockStackSwitcher::~DockStackSwitcher() { 318add_handler.disconnect(); 319remove_handler.disconnect(); 320} 321 322DockButton::DockButton(DockablePane *pane) : Gtk::ToggleButton(), pane(pane) { 323if(pane->get_icon()) { 324if(auto icon_copy = copy_image(pane->get_icon())) { 325this->set_child(*icon_copy); 326} 327} 328this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB); 329this->set_tooltip_text(pane->get_label()->get_text()); 330this->set_halign(Gtk::Align::CENTER); 331this->set_valign(Gtk::Align::CENTER); 332this->get_style_context()->add_class("toggle"); 333this->get_style_context()->add_class("gpanthera-dock-button"); 334// Add/remove CSS classes when the pane is shown/hidden 335active_style_handler = this->pane->get_stack()->property_visible_child_name().signal_changed().connect([this]() { 336this->update_active_style(); 337}); 338drag_source = Gtk::DragSource::create(); 339drag_source->set_exclusive(false); 340// This is to prevent the click handler from taking over grabbing the button 341drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); 342value.init(DockablePane::get_type()); 343value.set(pane); 344// Add the drag source to the button 345this->add_controller(drag_source); 346this->signal_clicked().connect([this, pane]() { 347if(pane->get_stack()->get_visible_child_name() == pane->get_identifier()) { 348pane->get_stack()->set_visible_child(""); 349} else { 350pane->get_stack()->set_visible_child(pane->get_identifier()); 351} 352}); 353// Provide the drag data 354drag_source->signal_prepare().connect([this](double, double) { 355drag_source->set_actions(Gdk::DragAction::MOVE); 356auto const paintable = Gtk::WidgetPaintable::create(); 357paintable->set_widget(*this); 358drag_source->set_icon(paintable->get_current_image(), 0, 0); 359return Gdk::ContentProvider::create(value); 360}, false); 361drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) { 362this->set_opacity(0); 363}, false); 364// Pop out if dragged to an external location 365drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) { 366if(reason == Gdk::DragCancelReason::NO_TARGET) { 367this->pane->pop_out(); 368return true; 369} 370this->set_opacity(1); 371return false; 372}, false); 373// Add a drop target to the button 374auto drop_target = Gtk::DropTarget::create(DockablePane::get_type(), Gdk::DragAction::MOVE); 375drop_target->set_actions(Gdk::DragAction::MOVE); 376drop_target->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); 377this->add_controller(drop_target); 378// Process dropped buttons by inserting them after the current button 379drop_target->signal_drop().connect([this](const Glib::ValueBase& value, double x, double y) { 380const auto &widget = static_cast<const Glib::Value<DockablePane*>&>(value).get(); 381 382if(widget) { 383if(auto pane = dynamic_cast<DockablePane*>(widget)) { 384if(pane->layout != this->pane->layout) { 385// If the pane is not in the same layout manager, reject 386return false; 387} 388auto switcher = dynamic_cast<DockStackSwitcher*>(this->get_parent()); 389if(switcher) { 390auto *stack = switcher->get_stack(); 391// Move the button to the new position 392pane->redock(stack); 393if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) { 394if(x < static_cast<double>(this->get_allocated_width()) / 2) { 395pane->insert_before(*stack, *this->pane); 396} else { 397pane->insert_after(*stack, *this->pane); 398} 399} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) { 400if(y < static_cast<double>(this->get_allocated_height()) / 2) { 401pane->insert_before(*stack, *this->pane); 402} else { 403pane->insert_after(*stack, *this->pane); 404} 405} 406 407switcher->update_buttons(); 408} 409} 410} 411 412return true; // Drop OK 413}, false); 414this->update_active_style(); 415} 416 417void DockButton::update_active_style() { 418if(this->pane->get_stack()->get_visible_child_name() == this->pane->get_identifier()) { 419this->add_css_class("checked"); 420this->add_css_class("gpanthera-dock-button-active"); 421this->set_active(true); 422} else { 423this->remove_css_class("checked"); 424this->remove_css_class("gpanthera-dock-button-active"); 425this->set_active(false); 426} 427} 428 429DockButton::~DockButton() { 430active_style_handler.disconnect(); 431} 432 433void DockStackSwitcher::update_buttons() { 434// Clear the old buttons 435auto old_buttons = collect_children(*this); 436for(auto *button : old_buttons) { 437remove(*button); 438} 439DockButton* first_child = nullptr; 440for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) { 441if(auto pane = dynamic_cast<DockablePane*>(widget)) { 442auto *button = Gtk::make_managed<DockButton>(pane); 443if(!first_child) { 444first_child = button; 445} else { 446button->set_group(*first_child); 447} 448if(pane->get_identifier() != Glib::ustring("")) { 449append(*button); 450} 451} 452} 453} 454 455void DockStack::add_pane(DockablePane &child) { 456child.redock(this); 457} 458 459void ContentManager::add_stack(ContentStack *stack) { 460this->stacks.push_back(stack); 461} 462 463void ContentManager::remove_stack(ContentStack *stack) { 464this->stacks.erase(std::ranges::remove(this->stacks, stack).begin(), this->stacks.end()); 465} 466 467void BaseStack::add(Gtk::Widget &child, const Glib::ustring &name) { 468Gtk::Stack::add(child, name); 469signal_child_added.emit(&child); 470} 471 472void BaseStack::add(Gtk::Widget &child) { 473Gtk::Stack::add(child); 474signal_child_added.emit(&child); 475} 476 477void BaseStack::remove(Gtk::Widget &child) { 478Gtk::Stack::remove(child); 479signal_child_removed.emit(&child); 480} 481 482void ContentStack::disable_children() { 483auto children = collect_children(*this); 484for(auto *child : children) { 485child->set_can_target(false); 486} 487} 488 489void ContentStack::enable_children() { 490auto children = collect_children(*this); 491for(auto *child : children) { 492child->set_can_target(true); 493} 494} 495 496ContentStack::ContentStack(std::shared_ptr<ContentManager> content_manager, std::function<bool(ContentPage*)> detach_handler) 497: BaseStack(), content_manager(content_manager) { 498this->set_name("gpanthera_content_stack"); 499this->content_manager->add_stack(this); 500if(detach_handler) { 501this->detach_handler = detach_handler; 502this->signal_detach.connect([this](ContentPage *widget) { 503return this->detach_handler(widget); 504}); 505} 506 507drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE); 508this->add_controller(drop_target); 509// Some widgets (such as webviews) will consume anything dropped on their ancestors. We have 510// to disable all child widgets to ensure the drop can be caught by this widget. 511drop_target->signal_enter().connect([this](double x, double y) { 512disable_children(); 513return Gdk::DragAction::MOVE; 514}, false); 515drop_target->signal_leave().connect([this]() { 516enable_children(); 517}, false); 518// Process dropped buttons 519drop_target->signal_drop().connect([this, content_manager](const Glib::ValueBase &value, double x, double y) { 520enable_children(); 521const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get(); 522 523if(auto page = dynamic_cast<ContentPage*>(widget)) { 524if(page->get_stack() == this && !this->get_first_child()->get_next_sibling()) { 525// Don't allow splitting if there are no more pages 526return false; 527} 528if(!(page->content_manager == this->content_manager)) { 529// If the page is not in the same content manager, reject 530return false; 531} 532double width = this->get_allocated_width(), height = this->get_allocated_height(); 533// Split based on the drop position 534if(!(x < width / 4 || x > width * 3 / 4 || y < height / 4 || y > height * 3 / 4)) { 535if(page->get_stack() == this) { 536return false; 537} 538page->lose_visibility(); 539// If the drop position is not at a quarter to the edges, move to the same stack 540this->add_page(*page); 541return true; 542} 543page->lose_visibility(); 544auto new_stack = Gtk::make_managed<ContentStack>(this->content_manager, this->detach_handler); 545auto this_notebook = dynamic_cast<ContentNotebook*>(this->get_parent()); 546auto new_switcher = Gtk::make_managed<ContentTabBar>(new_stack, this_notebook ? this_notebook->get_switcher()->get_orientation() : Gtk::Orientation::HORIZONTAL, this_notebook->get_switcher()->get_extra_child_function()); 547auto new_notebook = Gtk::make_managed<ContentNotebook>(new_stack, new_switcher, this_notebook ? this_notebook->get_tab_position() : Gtk::PositionType::TOP); 548new_stack->add_page(*page); 549new_stack->set_visible_child(*page); 550if(x < width / 4) { 551this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::START); 552if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) { 553paned->set_start_child(*new_notebook); 554} 555} else if(x > width * 3 / 4) { 556this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::END); 557if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) { 558paned->set_end_child(*new_notebook); 559} 560} else if(y < height / 4) { 561this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::START); 562if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) { 563paned->set_start_child(*new_notebook); 564} 565} else if(y > height * 3 / 4) { 566this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::END); 567if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) { 568paned->set_end_child(*new_notebook); 569} 570} 571content_manager->set_last_operated_page(page); 572} 573 574return true; // Drop OK 575}, false); 576} 577 578std::function<bool(ContentPage*)> ContentStack::get_detach_handler() const { 579return this->detach_handler; 580} 581 582ContentNotebook::ContentNotebook(ContentStack *stack, ContentTabBar *switcher, Gtk::PositionType tab_position) : Gtk::Box(), stack(stack), switcher(switcher), tab_position(tab_position) { 583this->append(*stack); 584if(tab_position == Gtk::PositionType::TOP || tab_position == Gtk::PositionType::BOTTOM) { 585this->set_orientation(Gtk::Orientation::VERTICAL); 586if(tab_position == Gtk::PositionType::TOP) { 587this->prepend(*switcher); 588switcher->add_css_class("top"); 589} else if(tab_position == Gtk::PositionType::BOTTOM) { 590this->append(*switcher); 591switcher->add_css_class("bottom"); 592} 593} else if(tab_position == Gtk::PositionType::LEFT || tab_position == Gtk::PositionType::RIGHT) { 594this->set_orientation(Gtk::Orientation::HORIZONTAL); 595if(tab_position == Gtk::PositionType::LEFT) { 596this->prepend(*switcher); 597switcher->add_css_class("left"); 598} else if(tab_position == Gtk::PositionType::RIGHT) { 599this->append(*switcher); 600switcher->add_css_class("right"); 601} 602} 603this->add_css_class("gpanthera-content-notebook"); 604} 605 606void ContentStack::make_paned(Gtk::Orientation orientation, Gtk::PackType pack_type) { 607auto *parent = this->get_parent(); 608if(auto notebook = dynamic_cast<ContentNotebook*>(parent)) { 609auto *paned = Gtk::make_managed<Gtk::Paned>(orientation); 610if(auto parent_paned = dynamic_cast<Gtk::Paned*>(notebook->get_parent())) { 611if(parent_paned->get_start_child() == notebook) { 612notebook->unparent(); 613parent_paned->set_start_child(*paned); 614} else if(parent_paned->get_end_child() == notebook) { 615notebook->unparent(); 616parent_paned->set_end_child(*paned); 617} 618} else if(auto box = dynamic_cast<Gtk::Box*>(notebook->get_parent())) { 619auto previous_child = notebook->get_prev_sibling(); 620box->remove(*notebook); 621if(previous_child) { 622paned->insert_after(*box, *previous_child); 623} else { 624box->prepend(*paned); 625} 626} else if(auto window = dynamic_cast<Gtk::Window*>(notebook->get_parent())) { 627window->unset_child(); 628window->set_child(*paned); 629} 630if(pack_type == Gtk::PackType::START) { 631paned->set_end_child(*notebook); 632} else if(pack_type == Gtk::PackType::END) { 633paned->set_start_child(*notebook); 634} 635} 636} 637 638ContentTabBar::ContentTabBar(ContentStack *stack, Gtk::Orientation orientation, std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> extra_child_function) : Gtk::Box(orientation), stack(stack), extra_child_function(extra_child_function) { 639this->get_style_context()->add_class("gpanthera-content-tab-bar"); 640this->set_margin_top(0); 641this->set_margin_bottom(0); 642this->set_margin_start(0); 643this->set_margin_end(0); 644 645auto update_callback = [this](Gtk::Widget*) { 646this->update_buttons(); 647}; 648this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST); 649stack->signal_child_added.connect(update_callback); 650stack->signal_child_removed.connect(update_callback); 651drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE); 652this->add_controller(drop_target); 653// Process dropped buttons 654drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) { 655if(const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get()) { 656if(auto page = dynamic_cast<ContentPage*>(widget)) { 657if(page->get_stack() == this->stack) { 658this->stack->remove(*page); 659this->stack->add(*page); 660} else { 661this->stack->add_page(*page); 662} 663} 664} 665 666return true; // Drop OK 667}, false); 668 669scrolled_window = Gtk::make_managed<Gtk::ScrolledWindow>(); 670scrolled_window->add_css_class("gpanthera-content-tab-bar-scroll"); 671auto viewport = Gtk::make_managed<Gtk::Viewport>(nullptr, nullptr); 672viewport->add_css_class("gpanthera-content-tab-bar-viewport"); 673tab_box = Gtk::make_managed<Gtk::Box>(orientation); 674tab_box->add_css_class("gpanthera-content-tab-box"); 675this->prepend(*scrolled_window); 676scrolled_window->set_child(*viewport); 677viewport->set_child(*tab_box); 678 679this->set_orientation(orientation); 680 681if(this->extra_child_function) { 682this->append(*this->extra_child_function(this)); 683} 684} 685 686std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> ContentTabBar::get_extra_child_function() const { 687return this->extra_child_function; 688} 689 690void ContentTabBar::set_extra_child_function(std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> extra_child_function) { 691// Note that this doesn't remove the existing extra child 692this->extra_child_function = extra_child_function; 693} 694 695void ContentTabBar::set_orientation(Gtk::Orientation orientation) { 696this->Gtk::Box::set_orientation(orientation); 697if(orientation == Gtk::Orientation::HORIZONTAL) { 698scrolled_window->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::NEVER); 699tab_box->set_orientation(Gtk::Orientation::HORIZONTAL); 700scrolled_window->set_hexpand(true); 701scrolled_window->set_vexpand(false); 702} else if(orientation == Gtk::Orientation::VERTICAL) { 703scrolled_window->set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); 704tab_box->set_orientation(Gtk::Orientation::VERTICAL); 705scrolled_window->set_vexpand(true); 706scrolled_window->set_hexpand(false); 707} 708} 709 710ContentTabBar::~ContentTabBar() { 711add_handler.disconnect(); 712remove_handler.disconnect(); 713} 714 715ContentStack *ContentTabBar::get_stack() const { 716return stack; 717} 718 719void ContentTabBar::update_buttons() { 720// Clear the old buttons 721auto old_buttons = collect_children(*this->tab_box); 722for(auto *button : old_buttons) { 723if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) { 724button_button->unset_child(); 725tab_box->remove(*button); 726} 727} 728ContentTab* first_child = nullptr; 729for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) { 730if(auto page = dynamic_cast<ContentPage*>(widget)) { 731auto *button = Gtk::make_managed<ContentTab>(page); 732if(!first_child) { 733first_child = button; 734} else { 735button->set_group(*first_child); 736} 737this->tab_box->append(*button); 738} 739} 740} 741 742void ContentStack::add_page(ContentPage &child) { 743child.redock(this); 744} 745 746void ContentStack::remove_with_paned() { 747if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) { 748Gtk::Widget *child = nullptr; 749if(this->get_parent() == paned->get_start_child()) { 750child = paned->get_end_child(); 751} else if(this->get_parent() == paned->get_end_child()) { 752child = paned->get_start_child(); 753} else { 754return; 755} 756 757g_object_ref(child->gobj()); // Prevent the child from being automatically deleted 758paned->property_start_child().reset_value(); // Some hacks because the Gtkmm API isn't complete; it doesn't have an unset_start_child() or unset_end_child() 759paned->property_end_child().reset_value(); 760 761if(child) { 762if(auto parent_paned = dynamic_cast<Gtk::Paned*>(paned->get_parent())) { 763if(parent_paned->get_start_child() == paned) { 764parent_paned->set_start_child(*child); 765} else if(parent_paned->get_end_child() == paned) { 766parent_paned->set_end_child(*child); 767} 768} else if(auto box = dynamic_cast<Gtk::Box*>(paned->get_parent())) { 769child->insert_after(*box, *paned); 770paned->unparent(); 771g_object_unref(child->gobj()); 772} 773} 774} 775} 776 777ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) { 778this->set_child(*page->get_tab_widget()); 779this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB); 780this->set_halign(Gtk::Align::CENTER); 781this->set_valign(Gtk::Align::CENTER); 782this->get_style_context()->add_class("toggle"); 783this->get_style_context()->add_class("gpanthera-content-tab"); 784// Add/remove CSS classes when the pane is shown/hidden 785active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() { 786this->update_active_style(); 787}); 788drag_source = Gtk::DragSource::create(); 789drag_source->set_exclusive(false); 790// This is to prevent the click handler from taking over grabbing the button 791drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); 792value.init(ContentPage::get_type()); 793value.set(page); 794// Add the drag source to the button 795this->add_controller(drag_source); 796this->signal_clicked().connect([this, page]() { 797page->get_stack()->set_visible_child(*page); 798page->content_manager->set_last_operated_page(page); 799update_active_style(); 800}); 801// Switch tabs on depress 802auto gesture_click = Gtk::GestureClick::create(); 803gesture_click->set_button(1); 804this->add_controller(gesture_click); 805gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) { 806page->get_stack()->set_visible_child(*page); 807page->content_manager->set_last_operated_page(page); 808update_active_style(); 809}); 810// Provide the drag data 811drag_source->signal_prepare().connect([this](double, double) { 812drag_source->set_actions(Gdk::DragAction::MOVE); 813auto const paintable = Gtk::WidgetPaintable::create(); 814paintable->set_widget(*this); 815drag_source->set_icon(paintable->get_current_image(), 0, 0); 816return Gdk::ContentProvider::create(value); 817}, false); 818update_active_style(); 819drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) { 820this->set_opacity(0); 821}, false); 822drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE); 823// Process dropped buttons by inserting them after the current button 824drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) { 825const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get(); 826 827if(widget) { 828if(auto page = dynamic_cast<ContentPage*>(widget)) { 829if(page->content_manager != this->page->content_manager) { 830// If the pane is not in the same layout manager, reject 831return false; 832} 833auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent()->get_parent()->get_parent()->get_parent()); 834if(switcher) { 835auto *stack = switcher->get_stack(); 836// Move the button to the new position 837page->redock(stack); 838if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) { 839if(x < static_cast<double>(this->get_allocated_width()) / 2) { 840page->insert_before(*stack, *this->page); 841} else { 842page->insert_after(*stack, *this->page); 843} 844} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) { 845if(y < static_cast<double>(this->get_allocated_height()) / 2) { 846page->insert_before(*stack, *this->page); 847} else { 848page->insert_after(*stack, *this->page); 849} 850} 851 852switcher->update_buttons(); 853} 854} 855} 856 857return true; // Drop OK 858}, false); 859this->add_controller(drop_target); 860// Pop out if dragged to an external location 861drag_cancel_handler = drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) { 862if(reason == Gdk::DragCancelReason::NO_TARGET) { 863auto stack = dynamic_cast<ContentStack*>(this->page->get_stack()); 864bool result = stack->signal_detach.emit(this->page); 865if(!result) { 866this->set_opacity(1); 867} 868return result; 869} 870this->set_opacity(1); 871return false; 872}, false); 873drag_end_handler = drag_source->signal_drag_end().connect([this](const Glib::RefPtr<Gdk::Drag>&, bool drop_ok) { 874if(drop_ok) { 875this->set_opacity(1); 876} 877}, false); 878 879// Provide a context menu 880context_menu = Gio::Menu::create(); 881auto action_group = Gio::SimpleActionGroup::create(); 882this->insert_action_group("win", action_group); 883 884auto close_action = Gio::SimpleAction::create("close"); 885close_action->signal_activate().connect([this](const Glib::VariantBase&) { 886this->close(); 887}); 888action_group->add_action(close_action); 889context_menu->append(_("Close"), "win.close"); 890 891auto close_all_action = Gio::SimpleAction::create("close-all"); 892close_all_action->signal_activate().connect([this](const Glib::VariantBase&) { 893for(auto page : collect_children(*this->page->get_stack())) { 894if(auto content_page = dynamic_cast<ContentPage*>(page)) { 895if(!content_page->signal_close.emit()) { 896content_page->redock(nullptr); 897} 898} 899} 900}); 901action_group->add_action(close_all_action); 902context_menu->append(_("Close all"), "win.close-all"); 903 904auto close_others_action = Gio::SimpleAction::create("close-others"); 905close_others_action->signal_activate().connect([this](const Glib::VariantBase&) { 906for(auto page : collect_children(*this->page->get_stack())) { 907if(auto content_page = dynamic_cast<ContentPage*>(page)) { 908if(content_page != this->page && !content_page->signal_close.emit()) { 909content_page->redock(nullptr); 910} 911} 912} 913}); 914action_group->add_action(close_others_action); 915context_menu->append(_("Close others"), "win.close-others"); 916 917auto detach_action = Gio::SimpleAction::create("detach"); 918detach_action->signal_activate().connect([this](const Glib::VariantBase&) { 919auto stack = dynamic_cast<ContentStack*>(this->page->get_stack()); 920bool result = stack->signal_detach.emit(this->page); 921}); 922action_group->add_action(detach_action); 923context_menu->append(_("New window"), "win.detach"); 924 925// TODO: Add more actions: "New window", "Split left", "Split right", "Split top", "Split bottom", "Close all", "Close others", "Close to the right", "Close to the left" 926this->insert_action_group("win", action_group); 927// Attach the context menu to the button 928auto context_menu_signal = add_context_menu(*this); 929popover = Gtk::make_managed<Gtk::PopoverMenu>(); 930popover->set_menu_model(context_menu); 931popover->set_parent(*this); 932popover->set_has_arrow(false); 933popover->set_halign(Gtk::Align::START); 934context_menu_signal.connect([this, action_group](double x, double y) { 935if(this->context_menu) { 936popover->set_pointing_to(Gdk::Rectangle(x, y, 1, 1)); 937popover->popup(); 938} 939}); 940auto middle_click_controller = Gtk::GestureClick::create(); 941middle_click_controller->set_button(2); 942middle_click_controller->signal_released().connect([this](int num_presses, double x, double y) { 943this->close(); 944}); 945this->add_controller(middle_click_controller); 946control_status_handler = this->page->signal_control_status_changed.connect([this](bool operating) { 947if(operating) { 948this->add_css_class("gpanthera-selected-tab"); 949} else { 950this->remove_css_class("gpanthera-selected-tab"); 951} 952}); 953} 954 955void ContentTab::close() { 956page->close(); 957} 958 959void ContentPage::close() { 960if(!this->signal_close.emit()) { 961if(this->get_next_sibling()) { 962this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling())); 963this->get_stack()->set_visible_child(*this->get_next_sibling()); 964} else if(this->get_prev_sibling()) { 965this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling())); 966this->get_stack()->set_visible_child(*this->get_prev_sibling()); 967} 968this->redock(nullptr); 969content_manager->signal_page_closed.emit(this); 970} 971} 972 973void ContentTab::update_active_style() { 974if(this->page->get_stack()->get_visible_child() == this->page) { 975this->add_css_class("checked"); 976this->add_css_class("gpanthera-dock-button-active"); 977this->set_active(true); 978} else { 979this->remove_css_class("checked"); 980this->remove_css_class("gpanthera-dock-button-active"); 981this->set_active(false); 982} 983} 984 985ContentTab::~ContentTab() { 986active_style_handler.disconnect(); 987drag_end_handler.disconnect(); 988drag_cancel_handler.disconnect(); 989control_status_handler.disconnect(); 990} 991 992Gtk::Widget *ContentPage::get_tab_widget() const { 993return this->tab_widget; 994} 995 996ContentStack *ContentPage::get_stack() const { 997return this->stack; 998} 999 1000void ContentPage::lose_visibility() { 1001if(this->get_next_sibling()) { 1002this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling())); 1003this->get_stack()->set_visible_child(*this->get_next_sibling()); 1004} else if(this->get_prev_sibling()) { 1005this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling())); 1006this->get_stack()->set_visible_child(*this->get_prev_sibling()); 1007} 1008} 1009 1010void ContentPage::redock(ContentStack *stack) { 1011if(stack == nullptr) { 1012content_manager->signal_page_closing.emit(this); 1013if(this->stack) { 1014this->stack->remove(*this); 1015if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && !this->stack->get_first_child()) { 1016this->stack->remove_with_paned(); 1017} 1018} 1019auto old_stack = this->stack; 1020if(old_stack) { 1021if(!old_stack->get_first_child()) { 1022old_stack->signal_leave_empty.emit(); 1023} 1024} 1025this->stack = nullptr; 1026this->last_stack = nullptr; 1027return; 1028} 1029// Check if the stack is now empty, in which case we should remove it 1030if(this->stack == stack) { 1031return; 1032} 1033content_manager->signal_page_moving.emit(this); 1034if(this->stack != nullptr) { 1035if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && (!this->stack->get_first_child() || (this->stack->get_first_child() == this && !this->stack->get_first_child()->get_next_sibling()))) { 1036this->stack->remove(*this); 1037this->stack->remove_with_paned(); 1038} else if(this->get_parent() == this->stack) { 1039this->stack->remove(*this); 1040} 1041} 1042auto old_stack = this->stack; 1043this->stack = stack; 1044this->last_stack = stack; 1045this->stack->add(*this); 1046if(old_stack) { 1047if(!old_stack->get_first_child()) { 1048old_stack->signal_leave_empty.emit(); 1049} 1050} 1051content_manager->signal_page_moved.emit(this); 1052} 1053 1054ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) : 1055Gtk::Overlay(), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) { 1056this->set_name("gpanthera_content_page"); 1057this->set_child(*child); 1058this->set_tab_widget(tab_widget); 1059this->set_margin_top(0); 1060this->set_margin_bottom(0); 1061this->set_margin_start(0); 1062this->set_margin_end(0); 1063if(stack) { 1064stack->add_page(*this); 1065this->content_manager->add_stack(this->stack); 1066} 1067auto click_controller = Gtk::GestureClick::create(); 1068click_controller->set_button(0); 1069click_controller->signal_pressed().connect([this](int num_presses, double x, double y) { 1070this->content_manager->set_last_operated_page(this); 1071}); 1072this->property_has_focus().signal_changed().connect([this]() { 1073if(this->property_has_focus()) { 1074this->content_manager->set_last_operated_page(this); 1075} 1076}); 1077click_controller->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); 1078this->add_controller(click_controller); 1079} 1080 1081ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") { 1082} 1083 1084void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) { 1085this->tab_widget = tab_widget; 1086} 1087 1088ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) { 1089this->set_child(*notebook); 1090this->set_decorated(true); 1091this->set_resizable(true); 1092} 1093 1094Gtk::PositionType ContentNotebook::get_tab_position() const { 1095return this->tab_position; 1096} 1097 1098ContentTabBar *ContentNotebook::get_switcher() const { 1099return this->switcher; 1100} 1101 1102ContentStack *ContentNotebook::get_stack() const { 1103return this->stack; 1104} 1105 1106ContentPage *ContentManager::get_last_operated_page() const { 1107return this->last_operated_page; 1108} 1109 1110void ContentManager::set_last_operated_page(ContentPage *page) { 1111if(this->last_operated_page) { 1112this->last_operated_page->signal_control_status_changed.emit(false); 1113} 1114this->last_operated_page = page; 1115if(page) { 1116page->signal_control_status_changed.emit(true); 1117this->signal_page_operated.emit(page); 1118} 1119} 1120 1121DockWindow *DockablePane::get_window() const { 1122return this->window; 1123} 1124 1125std::string LayoutManager::get_layout_as_json() const { 1126nlohmann::json json; 1127std::unordered_set<DockablePane*> panes_set; 1128for(auto pane: panes) { 1129panes_set.insert(pane); 1130} 1131for(auto stack: stacks) { 1132for(auto pane: collect_children(*stack)) { 1133if(dynamic_cast<DockablePane*>(pane)) { 1134panes_set.erase(dynamic_cast<DockablePane*>(pane)); 1135json[stack->id].push_back(dynamic_cast<DockablePane*>(pane)->get_identifier()); 1136} 1137} 1138} 1139for(auto pane: panes_set) { 1140if(pane->get_window()) { 1141json[""].push_back(pane->get_identifier()); 1142} 1143} 1144return json.dump(); 1145} 1146 1147void LayoutManager::restore_json_layout(const std::string &json_string) { 1148nlohmann::json json = nlohmann::json::parse(json_string); 1149for(auto stack: stacks) { 1150if(json.contains(stack->id)) { 1151for(auto const &pane_id: json[stack->id]) { 1152for(auto pane: panes) { 1153// TODO: could probably be done with a better complexity if there are 1154// many panes 1155if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) { 1156stack->add_pane(*pane); 1157break; 1158} 1159} 1160} 1161} 1162} 1163// Process popped-out panes 1164for(auto const &pane_id: json[""]) { 1165for(auto pane: panes) { 1166// TODO: could probably be done with a better complexity if there are 1167// many panes 1168if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) { 1169pane->pop_out(); 1170break; 1171} 1172} 1173} 1174} 1175} // namespace gPanthera 1176