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); 550content_manager->set_last_operated_page(page); 551if(x < width / 4) { 552this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::START); 553if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) { 554paned->set_start_child(*new_notebook); 555} 556} else if(x > width * 3 / 4) { 557this->make_paned(Gtk::Orientation::HORIZONTAL, Gtk::PackType::END); 558if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) { 559paned->set_end_child(*new_notebook); 560} 561} else if(y < height / 4) { 562this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::START); 563if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) { 564paned->set_start_child(*new_notebook); 565} 566} else if(y > height * 3 / 4) { 567this->make_paned(Gtk::Orientation::VERTICAL, Gtk::PackType::END); 568if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) { 569paned->set_end_child(*new_notebook); 570} 571} 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} 627if(pack_type == Gtk::PackType::START) { 628paned->set_end_child(*notebook); 629} else if(pack_type == Gtk::PackType::END) { 630paned->set_start_child(*notebook); 631} 632} 633} 634 635ContentTabBar::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) { 636this->get_style_context()->add_class("gpanthera-content-tab-bar"); 637this->set_margin_top(0); 638this->set_margin_bottom(0); 639this->set_margin_start(0); 640this->set_margin_end(0); 641 642auto update_callback = [this](Gtk::Widget*) { 643this->update_buttons(); 644}; 645this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB_LIST); 646stack->signal_child_added.connect(update_callback); 647stack->signal_child_removed.connect(update_callback); 648drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE); 649this->add_controller(drop_target); 650// Process dropped buttons 651drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) { 652if(const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get()) { 653if(auto page = dynamic_cast<ContentPage*>(widget)) { 654if(page->get_stack() == this->stack) { 655this->stack->remove(*page); 656this->stack->add(*page); 657} else { 658this->stack->add_page(*page); 659} 660} 661} 662 663return true; // Drop OK 664}, false); 665 666scrolled_window = Gtk::make_managed<Gtk::ScrolledWindow>(); 667scrolled_window->add_css_class("gpanthera-content-tab-bar-scroll"); 668auto viewport = Gtk::make_managed<Gtk::Viewport>(nullptr, nullptr); 669viewport->add_css_class("gpanthera-content-tab-bar-viewport"); 670tab_box = Gtk::make_managed<Gtk::Box>(orientation); 671tab_box->add_css_class("gpanthera-content-tab-box"); 672this->prepend(*scrolled_window); 673scrolled_window->set_child(*viewport); 674viewport->set_child(*tab_box); 675 676this->set_orientation(orientation); 677 678if(this->extra_child_function) { 679this->append(*this->extra_child_function(this)); 680} 681} 682 683std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> ContentTabBar::get_extra_child_function() const { 684return this->extra_child_function; 685} 686 687void ContentTabBar::set_extra_child_function(std::function<Gtk::Widget*(gPanthera::ContentTabBar*)> extra_child_function) { 688// Note that this doesn't remove the existing extra child 689this->extra_child_function = extra_child_function; 690} 691 692void ContentTabBar::set_orientation(Gtk::Orientation orientation) { 693this->Gtk::Box::set_orientation(orientation); 694if(orientation == Gtk::Orientation::HORIZONTAL) { 695scrolled_window->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::NEVER); 696tab_box->set_orientation(Gtk::Orientation::HORIZONTAL); 697scrolled_window->set_hexpand(true); 698scrolled_window->set_vexpand(false); 699} else if(orientation == Gtk::Orientation::VERTICAL) { 700scrolled_window->set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); 701tab_box->set_orientation(Gtk::Orientation::VERTICAL); 702scrolled_window->set_vexpand(true); 703scrolled_window->set_hexpand(false); 704} 705} 706 707ContentTabBar::~ContentTabBar() { 708add_handler.disconnect(); 709remove_handler.disconnect(); 710} 711 712ContentStack *ContentTabBar::get_stack() const { 713return stack; 714} 715 716void ContentTabBar::update_buttons() { 717// Clear the old buttons 718auto old_buttons = collect_children(*this->tab_box); 719for(auto *button : old_buttons) { 720if(auto *button_button = dynamic_cast<Gtk::Button*>(button)) { 721button_button->unset_child(); 722tab_box->remove(*button); 723} 724} 725ContentTab* first_child = nullptr; 726for(auto *widget = stack->get_first_child(); widget; widget = widget->get_next_sibling()) { 727if(auto page = dynamic_cast<ContentPage*>(widget)) { 728auto *button = Gtk::make_managed<ContentTab>(page); 729if(!first_child) { 730first_child = button; 731} else { 732button->set_group(*first_child); 733} 734this->tab_box->append(*button); 735} 736} 737} 738 739void ContentStack::add_page(ContentPage &child) { 740child.redock(this); 741} 742 743void ContentStack::remove_with_paned() { 744if(auto paned = dynamic_cast<Gtk::Paned*>(this->get_parent()->get_parent())) { 745Gtk::Widget *child = nullptr; 746if(this->get_parent() == paned->get_start_child()) { 747child = paned->get_end_child(); 748} else if(this->get_parent() == paned->get_end_child()) { 749child = paned->get_start_child(); 750} else { 751return; 752} 753 754g_object_ref(child->gobj()); // Prevent the child from being automatically deleted 755paned->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() 756paned->property_end_child().reset_value(); 757 758if(child) { 759if(auto parent_paned = dynamic_cast<Gtk::Paned*>(paned->get_parent())) { 760if(parent_paned->get_start_child() == paned) { 761parent_paned->set_start_child(*child); 762} else if(parent_paned->get_end_child() == paned) { 763parent_paned->set_end_child(*child); 764} 765} else if(auto box = dynamic_cast<Gtk::Box*>(paned->get_parent())) { 766child->insert_after(*box, *paned); 767paned->unparent(); 768g_object_unref(child->gobj()); 769} 770} 771} 772} 773 774ContentTab::ContentTab(ContentPage *page) : Gtk::ToggleButton(), page(page) { 775this->set_child(*page->get_tab_widget()); 776this->property_accessible_role().set_value(Gtk::Accessible::Role::TAB); 777this->set_halign(Gtk::Align::CENTER); 778this->set_valign(Gtk::Align::CENTER); 779this->get_style_context()->add_class("toggle"); 780this->get_style_context()->add_class("gpanthera-content-tab"); 781// Add/remove CSS classes when the pane is shown/hidden 782active_style_handler = this->page->get_stack()->property_visible_child().signal_changed().connect([this]() { 783this->update_active_style(); 784}); 785drag_source = Gtk::DragSource::create(); 786drag_source->set_exclusive(false); 787// This is to prevent the click handler from taking over grabbing the button 788drag_source->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); 789value.init(ContentPage::get_type()); 790value.set(page); 791// Add the drag source to the button 792this->add_controller(drag_source); 793this->signal_clicked().connect([this, page]() { 794page->get_stack()->set_visible_child(*page); 795page->content_manager->set_last_operated_page(page); 796update_active_style(); 797}); 798// Switch tabs on depress 799auto gesture_click = Gtk::GestureClick::create(); 800gesture_click->set_button(1); 801this->add_controller(gesture_click); 802gesture_click->signal_pressed().connect([this, page](int num_presses, double x, double y) { 803page->get_stack()->set_visible_child(*page); 804page->content_manager->set_last_operated_page(page); 805update_active_style(); 806}); 807// Provide the drag data 808drag_source->signal_prepare().connect([this](double, double) { 809drag_source->set_actions(Gdk::DragAction::MOVE); 810auto const paintable = Gtk::WidgetPaintable::create(); 811paintable->set_widget(*this); 812drag_source->set_icon(paintable->get_current_image(), 0, 0); 813return Gdk::ContentProvider::create(value); 814}, false); 815update_active_style(); 816drag_source->signal_drag_begin().connect([this](const Glib::RefPtr<Gdk::Drag>&) { 817this->set_opacity(0); 818}, false); 819drop_target = Gtk::DropTarget::create(ContentPage::get_type(), Gdk::DragAction::MOVE); 820// Process dropped buttons by inserting them after the current button 821drop_target->signal_drop().connect([this](const Glib::ValueBase &value, double x, double y) { 822const auto &widget = static_cast<const Glib::Value<ContentPage*>&>(value).get(); 823 824if(widget) { 825if(auto page = dynamic_cast<ContentPage*>(widget)) { 826if(page->content_manager != this->page->content_manager) { 827// If the pane is not in the same layout manager, reject 828return false; 829} 830auto switcher = dynamic_cast<ContentTabBar*>(this->get_parent()->get_parent()->get_parent()->get_parent()); 831if(switcher) { 832auto *stack = switcher->get_stack(); 833// Move the button to the new position 834page->redock(stack); 835if(switcher->get_orientation() == Gtk::Orientation::HORIZONTAL) { 836if(x < static_cast<double>(this->get_allocated_width()) / 2) { 837page->insert_before(*stack, *this->page); 838} else { 839page->insert_after(*stack, *this->page); 840} 841} else if(switcher->get_orientation() == Gtk::Orientation::VERTICAL) { 842if(y < static_cast<double>(this->get_allocated_height()) / 2) { 843page->insert_before(*stack, *this->page); 844} else { 845page->insert_after(*stack, *this->page); 846} 847} 848 849switcher->update_buttons(); 850} 851} 852} 853 854return true; // Drop OK 855}, false); 856this->add_controller(drop_target); 857// Pop out if dragged to an external location 858drag_cancel_handler = drag_source->signal_drag_cancel().connect([this](const Glib::RefPtr<Gdk::Drag>&, Gdk::DragCancelReason reason) { 859if(reason == Gdk::DragCancelReason::NO_TARGET) { 860auto stack = dynamic_cast<ContentStack*>(this->page->get_stack()); 861bool result = stack->signal_detach.emit(this->page); 862if(!result) { 863this->set_opacity(1); 864} 865return result; 866} 867this->set_opacity(1); 868return false; 869}, false); 870drag_end_handler = drag_source->signal_drag_end().connect([this](const Glib::RefPtr<Gdk::Drag>&, bool drop_ok) { 871if(drop_ok) { 872this->set_opacity(1); 873} 874}, false); 875 876// Provide a context menu 877context_menu = Gio::Menu::create(); 878auto action_group = Gio::SimpleActionGroup::create(); 879this->insert_action_group("win", action_group); 880 881auto close_action = Gio::SimpleAction::create("close"); 882close_action->signal_activate().connect([this](const Glib::VariantBase&) { 883this->close(); 884}); 885action_group->add_action(close_action); 886context_menu->append(_("Close"), "win.close"); 887 888auto close_all_action = Gio::SimpleAction::create("close-all"); 889close_all_action->signal_activate().connect([this](const Glib::VariantBase&) { 890for(auto page : collect_children(*this->page->get_stack())) { 891if(auto content_page = dynamic_cast<ContentPage*>(page)) { 892if(!content_page->signal_close.emit()) { 893content_page->redock(nullptr); 894} 895} 896} 897}); 898action_group->add_action(close_all_action); 899context_menu->append(_("Close all"), "win.close-all"); 900 901auto close_others_action = Gio::SimpleAction::create("close-others"); 902close_others_action->signal_activate().connect([this](const Glib::VariantBase&) { 903for(auto page : collect_children(*this->page->get_stack())) { 904if(auto content_page = dynamic_cast<ContentPage*>(page)) { 905if(content_page != this->page && !content_page->signal_close.emit()) { 906content_page->redock(nullptr); 907} 908} 909} 910}); 911action_group->add_action(close_others_action); 912context_menu->append(_("Close others"), "win.close-others"); 913 914auto detach_action = Gio::SimpleAction::create("detach"); 915detach_action->signal_activate().connect([this](const Glib::VariantBase&) { 916auto stack = dynamic_cast<ContentStack*>(this->page->get_stack()); 917bool result = stack->signal_detach.emit(this->page); 918}); 919action_group->add_action(detach_action); 920context_menu->append(_("New window"), "win.detach"); 921 922// 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" 923this->insert_action_group("win", action_group); 924// Attach the context menu to the button 925auto context_menu_signal = add_context_menu(*this); 926popover = Gtk::make_managed<Gtk::PopoverMenu>(); 927popover->set_menu_model(context_menu); 928popover->set_parent(*this); 929popover->set_has_arrow(false); 930popover->set_halign(Gtk::Align::START); 931context_menu_signal.connect([this, action_group](double x, double y) { 932if(this->context_menu) { 933popover->set_pointing_to(Gdk::Rectangle(x, y, 1, 1)); 934popover->popup(); 935} 936}); 937auto middle_click_controller = Gtk::GestureClick::create(); 938middle_click_controller->set_button(2); 939middle_click_controller->signal_released().connect([this](int num_presses, double x, double y) { 940this->close(); 941}); 942this->add_controller(middle_click_controller); 943control_status_handler = this->page->signal_control_status_changed.connect([this](bool operating) { 944if(operating) { 945this->add_css_class("gpanthera-selected-tab"); 946} else { 947this->remove_css_class("gpanthera-selected-tab"); 948} 949}); 950} 951 952void ContentTab::close() { 953page->close(); 954} 955 956void ContentPage::close() { 957if(!this->signal_close.emit()) { 958if(this->get_next_sibling()) { 959this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling())); 960this->get_stack()->set_visible_child(*this->get_next_sibling()); 961} else if(this->get_prev_sibling()) { 962this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling())); 963this->get_stack()->set_visible_child(*this->get_prev_sibling()); 964} 965this->redock(nullptr); 966} 967} 968 969void ContentTab::update_active_style() { 970if(this->page->get_stack()->get_visible_child() == this->page) { 971this->add_css_class("checked"); 972this->add_css_class("gpanthera-dock-button-active"); 973this->set_active(true); 974} else { 975this->remove_css_class("checked"); 976this->remove_css_class("gpanthera-dock-button-active"); 977this->set_active(false); 978} 979} 980 981ContentTab::~ContentTab() { 982active_style_handler.disconnect(); 983drag_end_handler.disconnect(); 984drag_cancel_handler.disconnect(); 985control_status_handler.disconnect(); 986} 987 988Gtk::Widget *ContentPage::get_tab_widget() const { 989return this->tab_widget; 990} 991 992ContentStack *ContentPage::get_stack() const { 993return this->stack; 994} 995 996void ContentPage::lose_visibility() { 997if(this->get_next_sibling()) { 998this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_next_sibling())); 999this->get_stack()->set_visible_child(*this->get_next_sibling()); 1000} else if(this->get_prev_sibling()) { 1001this->content_manager->set_last_operated_page(static_cast<ContentPage*>(this->get_prev_sibling())); 1002this->get_stack()->set_visible_child(*this->get_prev_sibling()); 1003} 1004} 1005 1006void ContentPage::redock(ContentStack *stack) { 1007if(stack == nullptr) { 1008if(this->stack) { 1009this->stack->remove(*this); 1010if(dynamic_cast<ContentNotebook*>(this->stack->get_parent()) && !this->stack->get_first_child()) { 1011this->stack->remove_with_paned(); 1012} 1013} 1014auto old_stack = this->stack; 1015if(old_stack) { 1016if(!old_stack->get_first_child()) { 1017old_stack->signal_leave_empty.emit(); 1018} 1019} 1020this->stack = nullptr; 1021this->last_stack = nullptr; 1022return; 1023} 1024// Check if the stack is now empty, in which case we should remove it 1025if(this->stack == stack) { 1026return; 1027} 1028if(this->stack != nullptr) { 1029if(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()))) { 1030this->stack->remove(*this); 1031this->stack->remove_with_paned(); 1032} else if(this->get_parent() == this->stack) { 1033this->stack->remove(*this); 1034} 1035} 1036auto old_stack = this->stack; 1037this->stack = stack; 1038this->last_stack = stack; 1039this->stack->add(*this); 1040if(old_stack) { 1041if(!old_stack->get_first_child()) { 1042old_stack->signal_leave_empty.emit(); 1043} 1044} 1045} 1046 1047ContentPage::ContentPage(std::shared_ptr<ContentManager> content_manager, ContentStack *stack, Gtk::Widget *child, Gtk::Widget *tab_widget) : 1048Gtk::Overlay(), content_manager(std::move(content_manager)), child(child), tab_widget(tab_widget) { 1049this->set_name("gpanthera_content_page"); 1050this->set_child(*child); 1051this->set_tab_widget(tab_widget); 1052this->set_margin_top(0); 1053this->set_margin_bottom(0); 1054this->set_margin_start(0); 1055this->set_margin_end(0); 1056if(stack) { 1057stack->add_page(*this); 1058this->content_manager->add_stack(this->stack); 1059} 1060auto click_controller = Gtk::GestureClick::create(); 1061click_controller->set_button(0); 1062click_controller->signal_pressed().connect([this](int num_presses, double x, double y) { 1063this->content_manager->set_last_operated_page(this); 1064}); 1065this->property_has_focus().signal_changed().connect([this]() { 1066if(this->property_has_focus()) { 1067this->content_manager->set_last_operated_page(this); 1068} 1069}); 1070click_controller->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); 1071this->add_controller(click_controller); 1072} 1073 1074ContentManager::ContentManager() : Glib::ObjectBase("ContentManager") { 1075} 1076 1077void ContentPage::set_tab_widget(Gtk::Widget *tab_widget) { 1078this->tab_widget = tab_widget; 1079} 1080 1081ContentWindow::ContentWindow(ContentNotebook *notebook) : notebook(notebook) { 1082this->set_child(*notebook); 1083this->set_decorated(true); 1084this->set_resizable(true); 1085} 1086 1087Gtk::PositionType ContentNotebook::get_tab_position() const { 1088return this->tab_position; 1089} 1090 1091ContentTabBar *ContentNotebook::get_switcher() const { 1092return this->switcher; 1093} 1094 1095ContentStack *ContentNotebook::get_stack() const { 1096return this->stack; 1097} 1098 1099ContentPage *ContentManager::get_last_operated_page() const { 1100return this->last_operated_page; 1101} 1102 1103void ContentManager::set_last_operated_page(ContentPage *page) { 1104if(this->last_operated_page) { 1105this->last_operated_page->signal_control_status_changed.emit(false); 1106} 1107this->last_operated_page = page; 1108page->signal_control_status_changed.emit(true); 1109this->signal_page_operated.emit(page); 1110} 1111 1112DockWindow *DockablePane::get_window() const { 1113return this->window; 1114} 1115 1116std::string LayoutManager::get_layout_as_json() const { 1117nlohmann::json json; 1118std::unordered_set<DockablePane*> panes_set; 1119for(auto pane: panes) { 1120panes_set.insert(pane); 1121} 1122for(auto stack: stacks) { 1123for(auto pane: collect_children(*stack)) { 1124if(dynamic_cast<DockablePane*>(pane)) { 1125panes_set.erase(dynamic_cast<DockablePane*>(pane)); 1126json[stack->id].push_back(dynamic_cast<DockablePane*>(pane)->get_identifier()); 1127} 1128} 1129} 1130for(auto pane: panes_set) { 1131if(pane->get_window()) { 1132json[""].push_back(pane->get_identifier()); 1133} 1134} 1135return json.dump(); 1136} 1137 1138void LayoutManager::restore_json_layout(const std::string &json_string) { 1139nlohmann::json json = nlohmann::json::parse(json_string); 1140for(auto stack: stacks) { 1141if(json.contains(stack->id)) { 1142for(auto const &pane_id: json[stack->id]) { 1143for(auto pane: panes) { 1144// TODO: could probably be done with a better complexity if there are 1145// many panes 1146if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) { 1147stack->add_pane(*pane); 1148break; 1149} 1150} 1151} 1152} 1153} 1154// Process popped-out panes 1155for(auto const &pane_id: json[""]) { 1156for(auto pane: panes) { 1157// TODO: could probably be done with a better complexity if there are 1158// many panes 1159if(pane->get_identifier() == static_cast<Glib::ustring>(pane_id.get<std::string>())) { 1160pane->pop_out(); 1161break; 1162} 1163} 1164} 1165} 1166} // namespace gPanthera 1167