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