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