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