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