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