diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index a5d0e24eac..4dc838172c 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -280,6 +280,9 @@ void AppConfig::set_defaults() set(SETTING_OPENGL_FPS_CAP, std::to_string(fps_cap)); } + // The getter already defaults, parses and clamps; write back what it resolves to. + set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(get_plugin_pages_visible_count())); + if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty()) set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false); @@ -1630,6 +1633,22 @@ void AppConfig::set_network_plugin_version(const std::string& version) set(SETTING_NETWORK_PLUGIN_VERSION, version); } +int AppConfig::get_plugin_pages_visible_count() const +{ + std::string value = get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT); + if (value.empty()) + return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + + int visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + try { + visible_count = std::stoi(value); + } + catch (...) { + return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + } + return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX); +} + std::vector AppConfig::get_skipped_network_versions() const { std::vector result; diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index 2c83ebb488..65c57cdb30 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -41,6 +41,11 @@ using namespace nlohmann; #define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao" #define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals" +#define SETTING_PLUGIN_PAGES_VISIBLE_COUNT "plugin_pages_visible_count" +#define PLUGIN_PAGES_VISIBLE_COUNT_MIN 1 +#define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5 +#define PLUGIN_PAGES_VISIBLE_COUNT_MAX 10 + #if defined(_WIN32) || defined(_WIN64) #define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09" #else @@ -374,6 +379,10 @@ public: std::string get_network_plugin_version() const; void set_network_plugin_version(const std::string& version); + // Number of plugin pages shown as fixed tabs before the rest are collapsed into a + // dropdown on the last tab. + int get_plugin_pages_visible_count() const; + std::vector get_skipped_network_versions() const; void add_skipped_network_version(const std::string& version); bool is_network_version_skipped(const std::string& version) const; diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index b98396f943..9174b044ec 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -620,6 +620,8 @@ set(SLIC3R_GUI_SOURCES plugin/host/PluginHostSlicing.cpp plugin/host/PluginHostUi.cpp plugin/host/PluginHostUi.hpp + plugin/host/PluginPages.cpp + plugin/host/PluginPages.hpp plugin/CloudPluginService.cpp plugin/CloudPluginService.hpp plugin/PluginFsUtils.cpp @@ -640,6 +642,9 @@ set(SLIC3R_GUI_SOURCES plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp + plugin/pluginTypes/pages/PagesPluginCapability.hpp + plugin/pluginTypes/pages/PagesPluginCapability.cpp + plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp plugin/pluginTypes/script/ScriptPluginCapability.hpp plugin/pluginTypes/script/ScriptPluginCapability.cpp plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp diff --git a/src/slic3r/GUI/Auxiliary.cpp b/src/slic3r/GUI/Auxiliary.cpp index 95244436a3..13ca173eb6 100644 --- a/src/slic3r/GUI/Auxiliary.cpp +++ b/src/slic3r/GUI/Auxiliary.cpp @@ -869,11 +869,11 @@ void AuxiliaryPanel::init_tabpanel() m_assembly_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::ASSEMBLY_GUIDE); m_others_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::OTHERS); - m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), "", true); - m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), "", false); - m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), "", false); - m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), "", false); - m_tabpanel->AddPage(m_others_panel, _L("Others"), "", false); + m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), true); + m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), false); + m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), false); + m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), false); + m_tabpanel->AddPage(m_others_panel, _L("Others"), false); } wxWindow *AuxiliaryPanel::create_side_tools() diff --git a/src/slic3r/GUI/CalibrationPanel.cpp b/src/slic3r/GUI/CalibrationPanel.cpp index b006509adf..bdc79c1c8e 100644 --- a/src/slic3r/GUI/CalibrationPanel.cpp +++ b/src/slic3r/GUI/CalibrationPanel.cpp @@ -488,7 +488,6 @@ void CalibrationPanel::init_tabpanel() { selected = true; m_tabpanel->AddPage(m_cali_panels[i], get_calibration_type_name(m_cali_panels[i]->get_calibration_mode()), - "", selected); } diff --git a/src/slic3r/GUI/Downloader.cpp b/src/slic3r/GUI/Downloader.cpp index c61b2716fc..0d37a0eca6 100644 --- a/src/slic3r/GUI/Downloader.cpp +++ b/src/slic3r/GUI/Downloader.cpp @@ -134,7 +134,7 @@ void Downloader::start_download(const std::string& full_url) Plater* plater = wxGetApp().plater(); mainframe->Freeze(); - mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); + mainframe->select_tab(TAB_ID_PREPARE); plater->select_view_3D("3D"); plater->select_view("plate"); plater->get_current_canvas3D()->zoom_to_bed(); diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index 8d05de13a4..dbbf5ec5e2 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -331,8 +331,10 @@ void Field::PostInitialize() } default: break; } - if (tab_id >= 0) - wxGetApp().mainframe->select_tab(tab_id); + if (tab_id >= 0) { + static constexpr const char* kShortcutTabIds[] = {TAB_ID_HOME, TAB_ID_PREPARE, TAB_ID_PREVIEW, TAB_ID_MONITOR}; + wxGetApp().mainframe->select_tab(kShortcutTabIds[tab_id]); + } if (tab_id > 0) // tab panel should be focused for correct navigation between tabs wxGetApp().tab_panel()->SetFocus(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index f6ffb75405..fa6902de8d 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -9196,7 +9196,7 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() view3d_canvas->get_gizmos_manager().reset_all_states(); // close all gizmos view3d_canvas->reload_scene(true); } - app.mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); + app.mainframe->select_tab(TAB_ID_PREPARE); } } }); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 2e0cba0f21..ffde09dbca 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -813,12 +813,12 @@ void GUI_App::post_init() m_open_method = "url"; } else { if (this->init_params->input_gcode) { - mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + mainframe->select_tab(TAB_ID_PREPARE); plater_->select_view_3D("3D"); this->plater()->load_gcode(from_u8(this->init_params->input_files.front())); m_open_method = "gcode"; } else { - mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + mainframe->select_tab(TAB_ID_PREPARE); plater_->select_view_3D("3D"); wxArrayString input_files; for (auto& file : this->init_params->input_files) { @@ -852,7 +852,7 @@ void GUI_App::post_init() mainframe->Freeze(); #endif plater_->canvas3D()->enable_render(false); - mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + mainframe->select_tab(TAB_ID_PREPARE); plater_->select_view_3D("3D"); //BBS init the opengl resource here if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() || @@ -890,9 +890,9 @@ void GUI_App::post_init() } } if (is_editor()) - mainframe->select_tab(size_t(0)); + mainframe->select_tab(TAB_ID_HOME); if (app_config->get("default_page") == "1") - mainframe->select_tab(size_t(1)); + mainframe->select_tab(TAB_ID_PREPARE); #ifndef __linux__ mainframe->Thaw(); #endif @@ -1829,10 +1829,10 @@ bool GUI_App::hot_reload_network_plugin() wxWindowDisabler disabler; if (mainframe) { - int current_tab = mainframe->m_tabpanel->GetSelection(); - if (current_tab == MainFrame::TabPosition::tpMonitor) { + wxString current_tab = mainframe->m_tabpanel->GetSelectedPageName(); + if (current_tab == TAB_ID_MONITOR) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": navigating away from Monitor tab before unload"; - mainframe->m_tabpanel->SetSelection(MainFrame::TabPosition::tp3DEditor); + mainframe->m_tabpanel->SelectPageByName(TAB_ID_PREPARE); } } @@ -2851,6 +2851,16 @@ void GUI_App::init_plugin_gui_wiring() plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin); plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin); + plugin_mgr.subscribe_on_load_callback([](const std::string& plugin_key) { + if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr) + return; + wxGetApp().mainframe->plugin_pages().on_plugin_register(plugin_key); + }); + plugin_mgr.subscribe_on_unload_callback([](const std::string& plugin_key) { + if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr) + return; + wxGetApp().mainframe->plugin_pages().on_plugin_deregister(plugin_key); + }); plugin_mgr.subscribe_on_load_callback(refresh_printer_agent_dropdown_after_load); plugin_mgr.subscribe_on_unload_callback(switch_printer_agent_after_unload); plugin_mgr.subscribe_on_capability_load_callback( @@ -2866,11 +2876,15 @@ void GUI_App::init_plugin_gui_wiring() if (Plater* plater = wxGetApp().plater()) plater->revalidate_current_plate_if_plugins_missing(); }); + if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe) + wxGetApp().mainframe->plugin_pages().on_cap_register(capability); }); plugin_mgr.subscribe_on_capability_unload_callback( [refresh_plugins_dialog, switch_printer_agent_after_unload](const PluginCapabilityId& capability) { if (capability.type == PluginCapabilityType::PrinterConnection) NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name); + if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe) + wxGetApp().mainframe->plugin_pages().on_cap_deregister(capability); refresh_plugins_dialog(); switch_printer_agent_after_unload(capability.plugin_key); }); @@ -3382,7 +3396,7 @@ bool GUI_App::on_init_inner() mainframe = new MainFrame(); // hide settings tabs after first Layout if (is_editor()) { - mainframe->select_tab(size_t(0)); + mainframe->select_tab(TAB_ID_HOME); } sidebar().obj_list()->init(); @@ -4590,7 +4604,7 @@ void GUI_App::recreate_GUI(const wxString &msg_name) mainframe = new MainFrame(); if (is_editor()) // hide settings tabs after first Layout - mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + mainframe->select_tab(TAB_ID_PREPARE); // Propagate model objects to object list. sidebar().obj_list()->init(); //sidebar().aux_list()->init_auxiliary(); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 5a0e70b74c..3b1cf1dffd 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -493,9 +493,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ }); //BBS - Bind(EVT_SELECT_TAB, [this](wxCommandEvent&evt) { - TabPosition pos = (TabPosition)evt.GetInt(); - m_tabpanel->SetSelection(pos); + Bind(EVT_SELECT_TAB, [this](wxCommandEvent& evt) { + m_tabpanel->SelectPageByName(evt.GetString()); }); Bind(EVT_SYNC_CLOUD_PRESET, &MainFrame::on_select_default_preset, this); @@ -702,7 +701,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ } return;} #endif - if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SetSelection(tpPreview); } return; } + if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; } if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') { m_plater->apply_background_progress(); m_print_enable = get_enable_print_status(); @@ -723,7 +722,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'S') { if (can_save_as()) m_plater->save_project(true); return;} else if (evt.CmdDown() && evt.GetKeyCode() == 'S') { if (can_save()) m_plater->save_project(); return;} if (evt.CmdDown() && evt.GetKeyCode() == 'F') { - if (m_plater && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview)) { + if (m_plater && is_prepare_or_preview_tab()) { m_plater->sidebar().can_search(); } } @@ -1007,8 +1006,8 @@ void MainFrame::update_layout() m_layout = layout; // From the very beginning the Print settings should be selected - //m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? 0 : 1; - m_last_selected_tab = 1; + //m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? TAB_ID_HOME : TAB_ID_PREPARE; + m_last_selected_tab = TAB_ID_PREPARE; // Set new settings switch (m_layout) @@ -1016,14 +1015,18 @@ void MainFrame::update_layout() case ESettingsLayout::Old: { m_plater->Reparent(m_tabpanel); - m_tabpanel->InsertPage(tp3DEditor, m_plater, _L("Prepare"), std::string("tab_3d_active"), std::string("tab_3d_active"), false); - m_tabpanel->InsertPage(tpPreview, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false); + // Right after Home — or first, when there is no Home tab (PositionAfter() would + // append instead, and by now the other built-in tabs are already in place). + const int home_idx = m_tabpanel->FindPageByName(TAB_ID_HOME); + const size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast(home_idx) + 1; + m_tabpanel->InsertPage(prepare_pos, TAB_ID_PREPARE, m_plater, _L("Prepare"), "tab_3d_active"); + m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), "tab_preview_active"); m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0); m_tabpanel->Bind(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, [this](wxCommandEvent& evt) { // jump to 3deditor under preview_only mode - if (evt.GetId() == tp3DEditor){ + if (evt.GetId() == m_tabpanel->FindPageByName(TAB_ID_PREPARE)) { Sidebar& sidebar = GUI::wxGetApp().sidebar(); if (sidebar.need_auto_sync_after_connect_printer()) { sidebar.set_need_auto_sync_after_connect_printer(false); @@ -1107,6 +1110,9 @@ void MainFrame::update_edge_panels() void MainFrame::shutdown() { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter"; + if (m_project != nullptr) + m_project->shutdown(); + m_plugin_pages.shutdown(); #ifdef __WXGTK__ // Edge panels are child windows — wxWidgets destroys them automatically. m_edge_bottom = nullptr; @@ -1252,15 +1258,14 @@ void MainFrame::init_tabpanel() { #endif //BBS wxWindow* panel = m_tabpanel->GetCurrentPage(); - int sel = m_tabpanel->GetSelection(); //wxString page_text = m_tabpanel->GetPageText(sel); - m_last_selected_tab = m_tabpanel->GetSelection(); + m_last_selected_tab = m_tabpanel->GetSelectedPageName(); if (panel == m_plater) { - if (sel == tp3DEditor) { + if (m_last_selected_tab == TAB_ID_PREPARE) { wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D)); m_param_panel->OnActivate(); } - else if (sel == tpPreview) { + else if (m_last_selected_tab == TAB_ID_PREVIEW) { m_plater->reset_check_status(); if (!m_plater->check_ams_status(m_slice_select == eSliceAll)) return; @@ -1275,7 +1280,7 @@ void MainFrame::init_tabpanel() { //monitor } #ifndef __APPLE__ - if (sel == tp3DEditor) { + if (m_last_selected_tab == TAB_ID_PREPARE) { m_topbar->EnableUndoRedoItems(); } else { @@ -1285,34 +1290,16 @@ void MainFrame::init_tabpanel() { if (panel) panel->SetFocus(); - - /*switch (sel) { - case TabPosition::tpHome: - show_option(false); - break; - case TabPosition::tp3DEditor: - show_option(true); - break; - case TabPosition::tpPreview: - show_option(true); - break; - case TabPosition::tpMonitor: - show_option(false); - break; - default: - show_option(false); - break; - }*/ }); if (wxGetApp().is_editor()) { m_webview = new WebViewPanel(m_tabpanel); Bind(EVT_LOAD_URL, [this](wxCommandEvent &evt) { wxString url = evt.GetString(); - select_tab(MainFrame::tpHome); + select_tab(TAB_ID_HOME); m_webview->load_url(url); }); - m_tabpanel->AddPage(m_webview, "", "tab_home_active", "tab_home_active", false); + m_tabpanel->AddPage(TAB_ID_HOME, m_webview, "", "tab_home_active"); m_param_panel = new ParamsPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); } @@ -1327,7 +1314,7 @@ void MainFrame::init_tabpanel() { //BBS add pages m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_monitor->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active"), false); + m_tabpanel->AddPage(TAB_ID_MONITOR, m_monitor, _L("Device"), "tab_monitor_active"); m_printer_view = new PrinterWebView(m_tabpanel); Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent &evt) { @@ -1342,16 +1329,20 @@ void MainFrame::init_tabpanel() { m_multi_machine = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_multi_machine->SetBackgroundColour(*wxWHITE); // TODO: change the bitmap - m_tabpanel->AddPage(m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), std::string("tab_multi_active"), false); + m_tabpanel->AddPage(TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), "tab_multi_active"); } m_project = new ProjectPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_project->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(m_project, _L("Project"), std::string("tab_auxiliary_active"), std::string("tab_auxiliary_active"), false); + m_tabpanel->AddPage(TAB_ID_PROJECT, m_project, _L("Project"), "tab_auxiliary_active"); m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false); + m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), "tab_calibration_active"); + + // Plugin pages are appended after the built-in tabs; their ids are namespaced + // (plugin..) so they can't collide with the built-in TAB_ID_* constants. + m_plugin_pages.initialize(m_tabpanel); if (m_plater) { // load initial config @@ -1373,10 +1364,15 @@ void MainFrame::show_device(bool should_use_native) { const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); - // The web page is appended when printer agents are enabled. Remove that - // extra page before switching back to the normal native/Web layout. - if (!use_printer_agents) { - if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != tpMonitor) { + // The web Device page is the extra tab printer-agents mode shows alongside the native one. + // Printers that drive the native Bambu device tab have nothing to put in it, so they don't + // get it — otherwise a Bambu user sees two Device tabs, one of them permanently empty. + const bool want_web_device_tab = use_printer_agents && wxGetApp().preset_bundle != nullptr && + !wxGetApp().preset_bundle->use_bbl_device_tab(); + + // Remove the extra page before switching to any layout that shouldn't have it. + if (!want_web_device_tab) { + if ((idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR_WEB)) != wxNOT_FOUND) { m_printer_view->Show(false); m_tabpanel->RemovePage(idx); } @@ -1394,8 +1390,8 @@ void MainFrame::show_device(bool should_use_native) { m_tabpanel->RemovePage(idx); } m_monitor->Show(false); - m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"), - std::string("tab_monitor_active")); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor, + _L("Device"), "tab_monitor_active"); } if (m_printer_view == nullptr) { @@ -1416,28 +1412,31 @@ void MainFrame::show_device(bool should_use_native) { // TODO: change the bitmap if (m_tabpanel->FindPage(m_multi_machine) == wxNOT_FOUND) { m_multi_machine->Show(false); - m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), - std::string("tab_multi_active"), false); + // Past the web Device tab when it is already there, so enabling multi-machine + // later can't wedge this page between the two Device tabs. + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR_WEB, TAB_ID_MONITOR}), + TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), "tab_multi_active"); } } if (!m_calibration) { m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration->SetBackgroundColour(*wxWHITE); } - // Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled, - // the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position. if (m_tabpanel->FindPage(m_calibration) == wxNOT_FOUND) { m_calibration->Show(false); - m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), - std::string("tab_calibration_active"), false); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration, + _L("Calibration"), "tab_calibration_active"); } - if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { - m_printer_view->Show(false); - m_tabpanel->AddPage(m_printer_view, _L("Device (Web)"), std::string("tab_monitor_active"), - std::string("tab_monitor_active"), false); - } else { - m_tabpanel->SetPageText(idx, _L("Device (Web)")); + if (want_web_device_tab) { + if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { + m_printer_view->Show(false); + // Immediately right of the native Device tab, not at the end of the tab bar. + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MONITOR_WEB, + m_printer_view, _L("Device (Web)"), "tab_monitor_active"); + } else { + m_tabpanel->SetPageText(idx, _L("Device (Web)")); + } } #ifdef _MSW_DARK_MODE @@ -1445,6 +1444,7 @@ void MainFrame::show_device(bool should_use_native) { #endif // _MSW_DARK_MODE fit_tab_labels(); // ORCA on printer change + m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above return; } @@ -1466,7 +1466,8 @@ void MainFrame::show_device(bool should_use_native) { m_monitor->SetBackgroundColour(*wxWHITE); } m_monitor->Show(false); - m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active")); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor, + _L("Device"), "tab_monitor_active"); if (wxGetApp().is_enable_multi_machine()) { if (!m_multi_machine) { @@ -1475,18 +1476,18 @@ void MainFrame::show_device(bool should_use_native) { } // TODO: change the bitmap m_multi_machine->Show(false); - m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), - std::string("tab_multi_active"), false); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MULTI_DEVICE, m_multi_machine, + _L("Multi-device"), "tab_multi_active"); } if (!m_calibration) { m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration->SetBackgroundColour(*wxWHITE); } m_calibration->Show(false); - // Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled, - // the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position. - m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), - std::string("tab_calibration_active"), false); + // Last of the built-in tabs, but plugin tabs already sit past it — anchor rather than + // append, so its position doesn't depend on the relayout() below running afterwards. + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration, + _L("Calibration"), "tab_calibration_active"); #ifdef _MSW_DARK_MODE wxGetApp().UpdateDarkUIWin(this); @@ -1519,10 +1520,17 @@ void MainFrame::show_device(bool should_use_native) { }); } m_printer_view->Show(false); - m_tabpanel->InsertPage(tpMonitor, m_printer_view, _L("Device"), std::string("tab_monitor_active"), - std::string("tab_monitor_active")); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_printer_view, + _L("Device"), "tab_monitor_active"); } fit_tab_labels(); // ORCA on printer change + m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above +} + +bool MainFrame::is_prepare_or_preview_tab() const +{ + const wxString tab = m_tabpanel->GetSelectedPageName(); + return tab == TAB_ID_PREPARE || tab == TAB_ID_PREVIEW; } void MainFrame::fit_tab_labels() @@ -1554,7 +1562,7 @@ void MainFrame::fit_tab_labels() bool MainFrame::preview_only_hint() { if (m_plater && (m_plater->only_gcode_mode() || (m_plater->using_exported_file()))) { - BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2% in preview mode")%m_tabpanel->GetSelection() %tp3DEditor; + BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2% in preview mode")%m_tabpanel->GetSelectedPageName() %wxString(TAB_ID_PREPARE); ConfirmBeforeSendDialog confirm_dlg(this, wxID_ANY, _L("Warning")); confirm_dlg.Bind(EVT_SECONDARY_CHECK_CONFIRM, [this](wxCommandEvent& e) { @@ -1872,22 +1880,22 @@ bool MainFrame::can_clone() const { bool MainFrame::can_select() const { - return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->model().objects.empty(); + return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->model().objects.empty(); } bool MainFrame::can_deselect() const { - return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->is_selection_empty(); + return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->is_selection_empty(); } bool MainFrame::can_delete() const { - return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->is_selection_empty(); + return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->is_selection_empty(); } bool MainFrame::can_delete_all() const { - return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->model().objects.empty(); + return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->model().objects.empty(); } bool MainFrame::can_reslice() const @@ -1996,7 +2004,7 @@ wxBoxSizer* MainFrame::create_side_tools() wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); else wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); - this->m_tabpanel->SetSelection(tpPreview); + this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } }); @@ -3143,7 +3151,7 @@ void MainFrame::init_menubar_as_editor() wxGetApp().app_config->set_bool("auto_perspective", !wxGetApp().app_config->get_bool("auto_perspective")); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, + this, [this]() { return is_prepare_or_preview_tab(); }, [this]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this); viewMenu->AppendSeparator(); @@ -3152,7 +3160,7 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_gcode_window(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelection() == tpPreview; }, + this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, [this]() { return wxGetApp().show_gcode_window(); }, this); append_menu_check_item( @@ -3161,7 +3169,7 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_3d_navigator(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, + this, [this]() { return is_prepare_or_preview_tab(); }, [this]() { return wxGetApp().show_3d_navigator(); }, this); append_menu_check_item(viewMenu, wxID_ANY, _L("Show Gridlines"), _L("Show Gridlines on plate"), @@ -3169,15 +3177,14 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_plate_gridlines(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, this, - [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, + [this]() { return is_prepare_or_preview_tab(); }, [this]() { return wxGetApp().show_plate_gridlines(); }, this); append_menu_item( viewMenu, wxID_ANY, _L("Reset Window Layout"), _L("Reset to default window layout"), [this](wxCommandEvent&) { m_plater->reset_window_layout(); }, "", this, [this]() { - return (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview) && - m_plater->is_sidebar_enabled(); + return is_prepare_or_preview_tab() && m_plater->is_sidebar_enabled(); }, this); @@ -3199,7 +3206,7 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_outline(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor; }, + this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE; }, [this]() { return wxGetApp().show_outline(); }, this); /*viewMenu->AppendSeparator(); @@ -4000,13 +4007,16 @@ void MainFrame::select_tab(wxPanel* panel) wxGetApp().params_dialog()->Popup(); return; } + // Not panel->GetName(): Prepare and Preview share the single m_plater window, so the + // window has no one correct name. The slot -> id lookup is the only correct resolution. int page_idx = m_tabpanel->FindPage(panel); - if (page_idx == tp3DEditor && m_tabpanel->GetSelection() == tpPreview) + wxString page_name = (page_idx == wxNOT_FOUND) ? wxString() : m_tabpanel->GetPageName(static_cast(page_idx)); + if (page_name == TAB_ID_PREPARE && m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW) return; //BBS GUI refactor: remove unused layout new/dlg /*if (page_idx != wxNOT_FOUND && m_layout == ESettingsLayout::Dlg) page_idx++;*/ - select_tab(size_t(page_idx)); + select_tab(page_name); } //BBS @@ -4014,7 +4024,7 @@ void MainFrame::jump_to_monitor(std::string dev_id) { if(!m_monitor) return; - m_tabpanel->SetSelection(tpMonitor); + m_tabpanel->SelectPageByName(TAB_ID_MONITOR); if (!dev_id.empty()) { ((MonitorPanel*)m_monitor)->select_machine(dev_id); } @@ -4024,26 +4034,26 @@ void MainFrame::jump_to_multipage() { if(!m_multi_machine) return; - m_tabpanel->SetSelection(tpMultiDevice); + m_tabpanel->SelectPageByName(TAB_ID_MULTI_DEVICE); ((MultiMachinePage*)m_multi_machine)->jump_to_send_page(); } //BBS GUI refactor: remove unused layout new/dlg -void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) +void MainFrame::select_tab(const wxString& id/* = wxString()*/) { //bool tabpanel_was_hidden = false; // Controls on page are created on active page of active tab now. // We should select/activate tab before its showing to avoid an UI-flickering - auto select = [this, tab](bool was_hidden) { - // when tab == -1, it means we should show the last selected tab + auto select = [this, id](bool was_hidden) { + // when id is empty, it means we should show the last selected tab //BBS GUI refactor: remove unused layout new/dlg //size_t new_selection = tab == (size_t)(-1) ? m_last_selected_tab : (m_layout == ESettingsLayout::Dlg && tab != 0) ? tab - 1 : tab; - size_t new_selection = tab == (size_t)(-1) ? m_last_selected_tab : tab; + wxString new_selection = id.empty() ? m_last_selected_tab : id; - if (m_tabpanel->GetSelection() != (int)new_selection) - m_tabpanel->SetSelection(new_selection); + if (m_tabpanel->GetSelectedPageName() != new_selection) + m_tabpanel->SelectPageByName(new_selection); #ifdef _MSW_DARK_MODE /*if (wxGetApp().tabs_as_menu()) { if (Tab* cur_tab = dynamic_cast(m_tabpanel->GetPage(new_selection))) @@ -4052,10 +4062,12 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) m_plater->get_current_canvas3D()->render(); }*/ #endif - if (tab == MainFrame::tp3DEditor && m_layout == ESettingsLayout::Old) + // Intentionally `id`, not `new_selection`: the fallback-to-last-tab path must not + // trigger this render even when the last selected tab was Prepare. + if (id == TAB_ID_PREPARE && m_layout == ESettingsLayout::Old) m_plater->canvas3D()->render(); else if (was_hidden) { - Tab* cur_tab = dynamic_cast(m_tabpanel->GetPage(new_selection)); + Tab* cur_tab = dynamic_cast(m_tabpanel->GetPageByName(new_selection)); if (cur_tab) cur_tab->OnActivate(); } @@ -4064,10 +4076,10 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) select(false); } -void MainFrame::request_select_tab(TabPosition pos) +void MainFrame::request_select_tab(const wxString& id) { wxCommandEvent* evt = new wxCommandEvent(EVT_SELECT_TAB); - evt->SetInt(pos); + evt->SetString(id); wxQueueEvent(this, evt); } @@ -4359,7 +4371,7 @@ void MainFrame::load_printer_url() } } -bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelection() == TabPosition::tpMonitor; } +bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelectedPageName() == TAB_ID_MONITOR; } void MainFrame::refresh_plugin_tips() diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index a614783c31..2052860d80 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -35,6 +35,21 @@ #include "PrinterWebView.hpp" #include "calib_dlg.hpp" #include "MultiMachinePage.hpp" +#include "slic3r/plugin/host/PluginPages.hpp" + +// Stable identifiers for MainFrame::m_tabpanel's built-in pages. These are +// names rather than positional indices so optional pages cannot shift them. +#define TAB_ID_HOME "home" +#define TAB_ID_PREPARE "prepare" +#define TAB_ID_PREVIEW "preview" +#define TAB_ID_MONITOR "monitor" +// Printer-agents mode shows the legacy web page alongside the native Device tab, so it needs an +// id of its own: sharing TAB_ID_MONITOR makes every name lookup resolve to whichever of the two +// comes first, which silently defeats PluginPages' selection round-trip across a tab relayout. +#define TAB_ID_MONITOR_WEB "monitor_web" +#define TAB_ID_MULTI_DEVICE "multi_device" +#define TAB_ID_PROJECT "project" +#define TAB_ID_CALIBRATION "calibration" #define ENABEL_PRINT_ALL 0 @@ -115,7 +130,7 @@ class MainFrame : public DPIFrame wxMenuItem* m_menu_item_reslice_now { nullptr }; wxSizer* m_main_sizer{ nullptr }; - size_t m_last_selected_tab; + wxString m_last_selected_tab; std::string get_base_name(const wxString &full_name, const char *extension = nullptr) const; std::string get_dir_name(const wxString &full_name) const; @@ -214,19 +229,6 @@ public: #ifdef __APPLE__ bool get_mac_full_screen() { return m_mac_fullscreen; } #endif - //BBS GUI refactor - enum TabPosition - { - tpHome = 0, - tp3DEditor = 1, - tpPreview = 2, - tpMonitor = 3, - tpMultiDevice = 4, - tpProject = 5, - tpCalibration = 6, - tpAuxiliary = 7, - toDebugTool = 8, - }; //BBS: add slice&&print status update logic enum SlicePrintEventType @@ -326,8 +328,8 @@ public: // When tab == -1, will be selected last selected tab //BBS: GUI refactor void select_tab(wxPanel* panel); - void select_tab(size_t tab = size_t(-1)); - void request_select_tab(TabPosition pos); + void select_tab(const wxString& id = wxString()); + void request_select_tab(const wxString& id); int get_calibration_curr_tab(); void select_view(const std::string& direction); // Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig @@ -360,6 +362,9 @@ public: //SoftFever void show_device(bool should_use_native); void fit_tab_labels(); // ORCA + // True while either of the two tabs backed by m_plater is selected. + bool is_prepare_or_preview_tab() const; + PluginPages& plugin_pages() { return m_plugin_pages; } PA_Calibration_Dlg* m_pa_calib_dlg{ nullptr }; FlowRateCalibrationDialog* m_flow_rate_calib_dlg{ nullptr }; @@ -385,7 +390,8 @@ public: CalibrationPanel* m_calibration{ nullptr }; WebViewPanel* m_webview { nullptr }; PrinterWebView* m_printer_view{nullptr}; - wxLogWindow* m_log_window { nullptr }; + PluginPages m_plugin_pages; + wxLogWindow* m_log_window { nullptr }; // BBS //wxBookCtrlBase* m_tabpanel { nullptr }; Notebook* m_tabpanel{ nullptr }; diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 9f074d3f93..1a6d969988 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -186,17 +186,17 @@ void MonitorPanel::init_tabpanel() //m_status_add_machine_panel = new AddMachinePanel(m_tabpanel); m_status_info_panel = new StatusPanel(m_tabpanel); - m_tabpanel->AddPage(m_status_info_panel, _L("Status"), "", true); + m_tabpanel->AddPage(m_status_info_panel, _L("Status"), true); m_media_file_panel = new MediaFilePanel(m_tabpanel); - m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), "", false); - //m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), "", false); + m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), false); + //m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), false); m_upgrade_panel = new UpgradePanel(m_tabpanel); - m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), "", false); + m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), false); m_hms_panel = new HMSPanel(m_tabpanel); - m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), "", false); + m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), false); std::string network_ver = Slic3r::NetworkAgent::get_version(); if (!network_ver.empty()) { @@ -413,7 +413,10 @@ void MonitorPanel::update_hms_tag() bool MonitorPanel::Show(bool show) { #ifdef __APPLE__ - wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize()); + // Notebook::InsertPage() hides every page it appends, so this also runs while MainFrame is + // still constructing, before GUI_App::mainframe is assigned. Same guard as Plater::Show(). + if (wxGetApp().mainframe) + wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize()); #endif NetworkAgent* m_agent = wxGetApp().getAgent(); diff --git a/src/slic3r/GUI/MultiMachinePage.cpp b/src/slic3r/GUI/MultiMachinePage.cpp index b9b71ad670..88d03007b9 100644 --- a/src/slic3r/GUI/MultiMachinePage.cpp +++ b/src/slic3r/GUI/MultiMachinePage.cpp @@ -86,9 +86,9 @@ void MultiMachinePage::init_tabpanel() m_cloud_task_manager = new CloudTaskManagerPage(m_tabpanel); m_machine_manager = new MultiMachineManagerPage(m_tabpanel); - m_tabpanel->AddPage(m_machine_manager, _L("Device"), "", true); - m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), "", false); - m_tabpanel->AddPage(m_cloud_task_manager, _L("Task Sent"), "", false); + m_tabpanel->AddPage(m_machine_manager, _L("Device"), true); + m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), false); + m_tabpanel->AddPage(m_cloud_task_manager, _L("Task Sent"), false); } void MultiMachinePage::init_timer() diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index ceda3fc0d6..673508454a 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -120,11 +120,11 @@ void ButtonsListCtrl::Rescale() void ButtonsListCtrl::SetSelection(int sel) { - if (m_selection == sel) + if (m_selection == sel && sel >= 0 && sel < static_cast(m_pageButtons.size())) return; // BBS: change button color wxColour selected_btn_bg("#009688"); // Gradient #009688 - if (m_selection >= 0) { + if (m_selection >= 0 && m_selection < static_cast(m_pageButtons.size())) { StateColor bg_color = StateColor( std::pair{wxColour(107, 107, 107), (int) StateColor::Hovered}, std::pair{wxColour(59, 68, 70), (int) StateColor::Normal}); @@ -132,9 +132,15 @@ void ButtonsListCtrl::SetSelection(int sel) StateColor text_color = StateColor( std::pair{wxColour(254,254, 254), (int) StateColor::Normal} ); - m_pageButtons[m_selection]->SetSelected(false); m_pageButtons[m_selection]->SetTextColor(text_color); } + + if (sel < 0 || sel >= static_cast(m_pageButtons.size())) { + m_selection = -1; + Refresh(); + return; + } + m_selection = sel; StateColor bg_color = StateColor( @@ -145,17 +151,19 @@ void ButtonsListCtrl::SetSelection(int sel) StateColor text_color = StateColor( std::pair{wxColour(254, 254, 254), (int) StateColor::Normal} ); - m_pageButtons[m_selection]->SetSelected(true); m_pageButtons[m_selection]->SetTextColor(text_color); Refresh(); } -bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const std::string &inactive_bmp_name) +bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */) { Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER); btn->SetCornerRadius(0); + if (bmp_name.empty() && bmp.IsOk()) + btn->SetIcon(bmp); + int em = em_unit(this); //BBS set size for button btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10}); @@ -168,8 +176,6 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* StateColor text_color = StateColor( std::pair{wxColour(254,254, 254), (int) StateColor::Normal}); btn->SetTextColor(text_color); - btn->SetInactiveIcon(inactive_bmp_name); - btn->SetSelected(false); btn->Bind(wxEVT_BUTTON, [this, btn](wxCommandEvent& event) { if (auto it = std::find(m_pageButtons.begin(), m_pageButtons.end(), btn); it != m_pageButtons.end()) { auto sel = it - m_pageButtons.begin(); @@ -192,6 +198,14 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* void ButtonsListCtrl::RemovePage(size_t n) { + if (n >= m_pageButtons.size()) + return; + + if (m_selection == static_cast(n)) + m_selection = -1; + else if (m_selection > static_cast(n)) + --m_selection; + Button* btn = m_pageButtons[n]; m_pageButtons.erase(m_pageButtons.begin() + n); m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA @@ -240,6 +254,24 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const return btn->GetLabel(); } +// ORCA +void ButtonsListCtrl::SetOverflowButton(wxWindow* button) +{ + if (m_overflow_button == button) + return; + + if (m_overflow_button != nullptr) + m_sizer->Detach(m_overflow_button); + + m_overflow_button = button; + + if (m_overflow_button != nullptr) + // Right after the tab buttons (index 0), ahead of any stretch spacer / side_tools. + m_sizer->Insert(1, m_overflow_button, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxBOTTOM, m_btn_margin); + + m_sizer->Layout(); +} + //#endif // _WIN32 void Notebook::Init() @@ -253,6 +285,8 @@ void Notebook::Init() m_showTimeout = m_hideTimeout = 0; + m_pageNames.clear(); + /* On Linux, Gstreamer wxMediaCtrl does not seem to get along well with * 32-bit X11 visuals (the overlay does not work). Is this a wxWindows * bug? Is this a Gstreamer bug? No idea, but it is our problem ... diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index d333956561..4734122b18 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -3,7 +3,11 @@ //#ifdef _WIN32 +#include +#include +#include #include +#include #include class ScalableButton; @@ -23,13 +27,16 @@ public: void SetSelection(int sel); void UpdateMode(); void Rescale(); - bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", const std::string &inactive_bmp_name = ""); + bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", const wxBitmap &bmp = wxNullBitmap); void RemovePage(size_t n); bool SetPageImage(size_t n, const std::string& bmp_name) const; void SetPageText(size_t n, const wxString& strText); void SetCompact(size_t n, bool compact); // ORCA wxString GetPageText(size_t n) const; wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA + // ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g. + // an overflow indicator. Pass nullptr to remove it; ownership stays with the caller. + void SetOverflowButton(wxWindow* button); private: wxFlexGridSizer* m_buttons_sizer; @@ -40,9 +47,10 @@ private: int m_btn_margin; int m_line_margin; std::vector m_pageLabels; // ORCA + wxWindow* m_overflow_button{nullptr}; // ORCA }; -class Notebook: public wxBookCtrlBase +class Notebook : public wxBookCtrlBase { public: Notebook(wxWindow * parent, @@ -103,7 +111,7 @@ public: // by this control) and show it immediately. bool ShowNewPage(wxWindow * page) { - return AddPage(page, wxString(), "", ""); + return AddPage(page, wxString(), false, NO_IMAGE); } @@ -135,51 +143,56 @@ public: // Implement base class pure virtual methods. - // adds a new page to the control - bool AddPage(wxWindow* page, + // Page management. Every insertion funnels through the InsertPage() below; `id` is the + // stable page name FindPageByName() resolves. Built-in tabs name a resource bitmap, + // plugin pages hand over a ready wxBitmap; wx's own imageId overloads carry neither. + bool AddPage(const wxString& id, + wxWindow* page, const wxString& text, - const std::string& bmp_name, - const std::string& inactive_bmp_name, + const std::string& bmp_name = "", bool bSelect = false) { DoInvalidateBestSize(); - return InsertPage(GetPageCount(), page, text, bmp_name, inactive_bmp_name, bSelect); + return InsertPage(GetPageCount(), id, page, text, bmp_name, bSelect); } - // Page management - virtual bool InsertPage(size_t n, - wxWindow * page, - const wxString & text, - bool bSelect = false, - int imageId = NO_IMAGE) override + bool AddPage(wxWindow* page, const wxString& text, bool bSelect = false, int imageId = NO_IMAGE) override { - if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId)) + DoInvalidateBestSize(); + return InsertPage(GetPageCount(), page, text, bSelect, imageId); + } + + bool InsertPage(size_t n, + const wxString& id, + wxWindow * page, + const wxString & text, + const std::string& bmp_name = "", + bool bSelect = false, + const wxBitmap& bmp = wxNullBitmap) + { + if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) return false; - GetBtnsListCtrl()->InsertPage(n, text, bSelect); + m_pageNames.insert(m_pageNames.begin() + n, id); + GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, bmp); + // wxBookCtrlBase::InsertPage() only inserts into the page list and sizes the new + // page to the current page's rect — it never touches visibility, and a freshly + // constructed page defaults to shown. Without this it renders on top of whatever + // page is currently selected until the next SetSelection() call hides it. if (!DoSetSelectionAfterInsertion(n, bSelect)) page->Hide(); return true; } - bool InsertPage(size_t n, - wxWindow * page, - const wxString & text, - const std::string& bmp_name = "", - const std::string& inactive_bmp_name = "", - bool bSelect = false) + virtual bool InsertPage(size_t n, + wxWindow * page, + const wxString & text, + bool bSelect = false, + int WXUNUSED(imageId) = NO_IMAGE) override { - if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) - return false; - - GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, inactive_bmp_name); - - if (bSelect) - SetSelection(n); - - return true; + return InsertPage(n, wxString(), page, text, "", bSelect); } virtual int SetSelection(size_t n) override @@ -211,8 +224,8 @@ public: return DoSetSelection(n); } - // Neither labels nor images are supported but we still store the labels - // just in case the user code attaches some importance to them. + // Labels are stored by the custom button list; wx's image-list API is unused — tab icons + // are set directly on the buttons, either from a resource name or a ready wxBitmap. virtual bool SetPageText(size_t n, const wxString & strText) override { wxCHECK_MSG(n < GetPageCount(), false, wxS("Invalid page")); @@ -251,7 +264,64 @@ public: page->SetFocus(); } + // The base clears its page list directly instead of calling DoRemovePage() per page, + // which would leave m_pageNames behind. No caller today; kept in sync regardless. + virtual bool DeleteAllPages() override + { + m_pageNames.clear(); + return wxBookCtrlBase::DeleteAllPages(); + } + ButtonsListCtrl* GetBtnsListCtrl() const { return static_cast(m_bookctrl); } + void SetOverflowButton(wxWindow* button) { GetBtnsListCtrl()->SetOverflowButton(button); } + + // Insertion index just past the first of `ids` that is present, or the end of the bar + // if none is — lets call sites state tab order as "after X" instead of re-deriving it. + size_t PositionAfter(std::initializer_list ids) const + { + for (const char* id : ids) + if (const int idx = FindPageByName(id); idx != wxNOT_FOUND) + return static_cast(idx) + 1; + return GetPageCount(); + } + + int FindPageByName(const wxString& id) const + { + if (id.empty()) + return wxNOT_FOUND; + for (size_t i = 0; i < m_pageNames.size(); ++i) + if (m_pageNames[i] == id) + return static_cast(i); + return wxNOT_FOUND; + } + + wxWindow* GetPageByName(const wxString& id) const + { + const int idx = FindPageByName(id); + return idx == wxNOT_FOUND ? nullptr : GetPage(static_cast(idx)); + } + + bool SelectPageByName(const wxString& id) + { + const int idx = FindPageByName(id); + if (idx == wxNOT_FOUND) + return false; + SetSelection(static_cast(idx)); + return true; + } + + // Inverse of FindPageByName: index -> id. Empty string for an out-of-range + // index or a page that was never given an id (e.g. settings Tab pages). + wxString GetPageName(size_t n) const + { + return n < m_pageNames.size() ? m_pageNames[n] : wxString(); + } + + wxString GetSelectedPageName() const + { + const int sel = GetSelection(); + return sel < 0 ? wxString() : GetPageName(static_cast(sel)); + } void UpdateMode() { @@ -369,6 +439,7 @@ protected: wxWindow* const win = wxBookCtrlBase::DoRemovePage(page); if (win) { + m_pageNames.erase(m_pageNames.begin() + page); GetBtnsListCtrl()->RemovePage(page); DoSetSelectionAfterRemoval(page); } @@ -394,6 +465,8 @@ protected: private: void Init(); + std::vector m_pageNames; // index-parallel to wxBookCtrlBase::m_pages + wxShowEffect m_showEffect, m_hideEffect; diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 8e81f0654c..5e83ad845f 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1918,7 +1918,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L""); } else { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } return false; }; @@ -1985,7 +1985,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException wxGetApp().sidebar().jump_to_option(opt, opt_type, L""); } else { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } return false; }; @@ -2015,7 +2015,7 @@ void NotificationManager::push_slicing_error_notification(const std::string &tex if (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); } } if (!ovs.empty()) { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); wxGetApp().obj_list()->select_items(ovs); } return false; @@ -2046,7 +2046,7 @@ void NotificationManager::push_slicing_warning_notification(const std::string& t auto& objects = wxGetApp().model().objects; auto iter = std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }); if (iter != objects.end()) { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); wxGetApp().obj_list()->select_items({ {*iter, nullptr} }); } return false; @@ -2693,7 +2693,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s if (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); } } if (!ovs.empty()) { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); wxGetApp().obj_list()->select_items(ovs); wxGetApp().obj_list()->update_selections_on_canvas(); } @@ -2777,7 +2777,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s } } - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (!sel_items.empty()) { obj_list->select_items(sel_items); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b115f3101c..af6b564ab1 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5799,7 +5799,7 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi #endif // WIN32 m_mainframe.Raise(); - m_mainframe.select_tab(size_t(MainFrame::tp3DEditor)); + m_mainframe.select_tab(TAB_ID_PREPARE); if (wxGetApp().is_editor()) m_plater.select_view_3D("3D"); @@ -6581,9 +6581,9 @@ void Plater::priv::select_next_view_3D() { if (current_panel == view3D) - wxGetApp().mainframe->select_tab(size_t(MainFrame::tpPreview)); + wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); else if (current_panel == preview) - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); // else if (current_panel == assemble_view) // set_current_panel(view3D); } @@ -7882,7 +7882,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ q->select_plate(first_plate_index); //set to 3d tab q->select_view_3D("Preview"); - wxGetApp().mainframe->select_tab(MainFrame::tpPreview); + wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); } else { //set to 3d tab @@ -7901,7 +7901,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ else { //always set to 3D after loading files q->select_view_3D("3D"); - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } if (load_model) { @@ -8809,7 +8809,7 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni } } - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (inst_idx != -1) { auto* model = wxGetApp().obj_list()->GetModel(); @@ -8838,7 +8838,7 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni } else { auto iter = id.id ? std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }) : objects.end(); if (iter != objects.end()) { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); wxGetApp().obj_list()->select_items({{*iter, nullptr}}); wxGetApp().obj_list()->update_selections_on_canvas(); } @@ -11222,13 +11222,19 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } const int new_sel = e.GetSelection(); - sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview; + if (new_sel == wxNOT_FOUND) { + // GetPage(new_sel) below needs a valid index. + e.Skip(); + return; + } + const wxString new_name = main_frame->m_tabpanel->GetPageName(new_sel); + sidebar_layout.show = new_name == TAB_ID_PREPARE || new_name == TAB_ID_PREVIEW; update_sidebar(); int old_sel = e.GetOldSelection(); const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); const bool use_native_device_tab = wxGetApp().preset_bundle && (wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents); - if (use_native_device_tab && new_sel == MainFrame::tpMonitor) { + if (use_native_device_tab && new_name == TAB_ID_MONITOR) { // BBL network module is only required for BBL-vendor printers. // Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it. if (!use_printer_agents && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) { @@ -11240,12 +11246,15 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } } } else { + // Pointer test, not a name lookup: in printer-agents mode this page is TAB_ID_MONITOR_WEB + // while the native Device tab holds TAB_ID_MONITOR, and in legacy-web mode it holds + // TAB_ID_MONITOR itself. const bool selecting_web_device_tab = main_frame->m_printer_view && main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view; if (selecting_web_device_tab) { // Use the selected discovered machine when the preset has no host. main_frame->load_printer_url(); - } else if (new_sel == MainFrame::tpMonitor && wxGetApp().preset_bundle != nullptr) { + } else if (new_name == TAB_ID_MONITOR && wxGetApp().preset_bundle != nullptr) { auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); if (main_frame->m_printer_view && url.empty()) { @@ -12108,7 +12117,7 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); else wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); - wxGetApp().mainframe->m_tabpanel->SetSelection(MainFrame::TabPosition::tpPreview); + wxGetApp().mainframe->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return false; } @@ -13065,7 +13074,7 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ get_notification_manager()->clear_all(); if (!silent) - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); //get_partplate_list().reinit(); //get_partplate_list().update_slice_context_to_current_plate(p->background_process); @@ -13214,7 +13223,7 @@ void Plater::load_project(wxString const& filename2, if (!m_exported_file) { p->select_view("topfront"); p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } else { p->partplate_list.select_plate_view(); @@ -13328,7 +13337,7 @@ void Plater::import_model_id(wxString download_info) const int max_retries = 3; /* jump to 3D eidtor */ - wxGetApp().mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); /* prepare progress dialog */ bool cont = true; @@ -13637,7 +13646,7 @@ void Plater::calib_pa(const Calib_Params& params) { const auto calib_pa_name = wxString::Format(L"Pressure Advance Test"); new_project(false, false, calib_pa_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; print_config->set_key_value("overhang_reverse", new ConfigOptionBool(false)); @@ -14118,7 +14127,7 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) { if (new_project(false, false, calib_name) == wxID_CANCEL) return; - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (is_linear) { if (pass == 1) @@ -14155,7 +14164,7 @@ void Plater::calib_temp(const Calib_Params& params) { const auto calib_temp_name = wxString::Format(L"Nozzle temperature test"); new_project(false, false, calib_temp_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Temp_Tower) return; if (!add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc")) @@ -14235,7 +14244,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) { const auto calib_vol_speed_name = wxString::Format(L"Max volumetric speed test"); new_project(false, false, calib_vol_speed_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Vol_speed_Tower) return; if (!add_model(false, Slic3r::resources_dir() + "/calib/volumetric_speed/SpeedTestStructure.drc")) @@ -14314,7 +14323,7 @@ void Plater::calib_retraction(const Calib_Params& params) { const auto calib_retraction_name = wxString::Format(L"Retraction"); new_project(false, false, calib_retraction_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Retraction_tower) return; @@ -14374,7 +14383,7 @@ void Plater::calib_VFA(const Calib_Params& params) { const auto calib_vfa_name = wxString::Format(L"VFA test"); new_project(false, false, calib_vfa_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_VFA_Tower) return; @@ -14457,7 +14466,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) { const auto calib_input_shaping_name = wxString::Format(L"Input shaping Frequency test"); new_project(false, false, calib_input_shaping_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Input_shaping_freq) return; @@ -14523,7 +14532,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) { const auto calib_input_shaping_name = wxString::Format(L"Input shaping Damping test"); new_project(false, false, calib_input_shaping_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Input_shaping_damp) return; @@ -14588,7 +14597,7 @@ void Plater::Calib_Cornering(const Calib_Params& params) { const auto Calib_Cornering = wxString::Format(L"Cornering test"); new_project(false, false, Calib_Cornering); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Cornering) return; @@ -14721,7 +14730,7 @@ void Plater::load_gcode(const wxString& filename) //p->gcode_result.reset(); //reset_gcode_toolpaths(); p->preview->reload_print(m_only_gcode); - wxGetApp().mainframe->select_tab(MainFrame::tpPreview); + wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); p->set_current_panel(p->preview, true); p->get_current_canvas3D()->render(); //p->notification_manager->bbl_show_plateinfo_notification(into_u8(_L("Preview only mode for gcode file."))); @@ -15394,7 +15403,7 @@ LoadType determine_load_type(std::string filename, std::string override_setting) wxGetApp().app_config->set("import_project_action", std::to_string(choice)); // BBS: jump to plater panel - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); return load_type; } @@ -15623,7 +15632,7 @@ void Plater::reset_with_confirm() .ShowModal() == wxID_YES) { reset(); // BBS: jump to plater panel - wxGetApp().mainframe->select_tab(size_t(0)); + wxGetApp().mainframe->select_tab(TAB_ID_HOME); } } @@ -17437,7 +17446,7 @@ int Plater::export_config_3mf(int plate_idx, Export3mfProgressFn proFn) //BBS void Plater::send_calibration_job_finished(wxCommandEvent & evt) { - p->main_frame->request_select_tab(MainFrame::TabPosition::tpCalibration); + p->main_frame->request_select_tab(TAB_ID_CALIBRATION); auto calibration_panel = p->main_frame->m_calibration; if (calibration_panel) { auto curr_wizard = static_cast(calibration_panel->get_tabpanel()->GetPage(evt.GetInt())); @@ -17469,7 +17478,7 @@ void Plater::print_job_finished(wxCommandEvent &evt) if (!dev) return; dev->set_selected_machine(evt.GetString().ToStdString()); - p->main_frame->request_select_tab(MainFrame::TabPosition::tpMonitor); + p->main_frame->request_select_tab(TAB_ID_MONITOR); //jump to monitor and select device status panel MonitorPanel* curr_monitor = p->main_frame->m_monitor; if(curr_monitor) @@ -17484,7 +17493,7 @@ void Plater::send_job_finished(wxCommandEvent& evt) send_gcode_finish(evt.GetString()); p->hide_send_to_printer_dlg(); - //p->main_frame->request_select_tab(MainFrame::TabPosition::tpMonitor); + //p->main_frame->request_select_tab(TAB_ID_MONITOR); ////jump to monitor and select device status panel //MonitorPanel* curr_monitor = p->main_frame->m_monitor; //if (curr_monitor) @@ -18378,7 +18387,7 @@ void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWar MessageDialog dlg(this, content, title, wxOK | wxFORWARD | wxICON_WARNING, _L("Device Page")); auto result = dlg.ShowModal(); if (result == wxFORWARD) { - wxGetApp().mainframe->select_tab(size_t(MainFrame::tpMonitor)); + wxGetApp().mainframe->select_tab(TAB_ID_MONITOR); } } diff --git a/src/slic3r/GUI/PluginWebDialog.cpp b/src/slic3r/GUI/PluginWebDialog.cpp index 1808f21ce9..d89aac7270 100644 --- a/src/slic3r/GUI/PluginWebDialog.cpp +++ b/src/slic3r/GUI/PluginWebDialog.cpp @@ -15,39 +15,6 @@ namespace Slic3r { namespace GUI { namespace { -// Low-specificity element defaults (no !important) for UNSTYLED plugin HTML, so a bare -// plugin page looks native while any CSS the plugin ships still wins. Built on the -// --orca-* variables the host injects (see WebViewHostDialog); document-start injected -// AFTER the host contract so the variables are defined (shares the base injector's -// WebView2 timing guard). -std::string plugin_defaults_user_script() -{ - std::string css; - css += ""; - return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend"); -} - // Injected into the top-level page at document start (before the plugin's own // scripts). Defines window.orca as the only host surface the page may use. It // references window.wx lazily (at call time) so it never races the backend's @@ -129,7 +96,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent, void PluginWebDialog::add_user_scripts() { if (wxWebView* wv = browser()) { - wv->AddUserScript(wxString::FromUTF8(plugin_defaults_user_script())); + wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script())); wv->AddUserScript(ORCA_BRIDGE_JS); } } diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index f802ba6ecb..ca115b9773 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1748,11 +1748,26 @@ void PreferencesDialog::create_items() g_sizer->Add(item_pop_up_filament_map_dialog); #endif + //// GENERAL > Plugins + g_sizer->Add(create_item_title(_L("Plugins")), 1, wxEXPAND); + + auto item_plugin_pages_visible_count = create_item_spinctrl( + _L("Visible plugin pages"), + "", + _L("pages"), + _L("Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab."), + SETTING_PLUGIN_PAGES_VISIBLE_COUNT, + PLUGIN_PAGES_VISIBLE_COUNT_MIN, + PLUGIN_PAGES_VISIBLE_COUNT_MAX, + [](int value) { wxGetApp().mainframe->plugin_pages().set_visible_page_count(value); } + ); + g_sizer->Add(item_plugin_pages_visible_count); + g_sizer->AddSpacer(FromDIP(10)); sizer_page->Add(g_sizer, 0, wxEXPAND); ////////////////////////// - //// CONTROL TAB + //// CONTROL TAB ///////////////////////////////////// m_pref_tabs->AppendItem(_L("Control")); f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0)); diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index c979fc3212..f78af21a99 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -1042,7 +1042,7 @@ bool PlaterPresetComboBox::switch_to_tab() //BBS Select NoteBook Tab params if (tab->GetParent() == wxGetApp().params_panel()) - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); else { wxGetApp().params_dialog()->Popup(); tab->OnActivate(); diff --git a/src/slic3r/GUI/Project.cpp b/src/slic3r/GUI/Project.cpp index 57410b1202..6d1eb0e180 100644 --- a/src/slic3r/GUI/Project.cpp +++ b/src/slic3r/GUI/Project.cpp @@ -74,7 +74,18 @@ ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, Fit(); } -ProjectPanel::~ProjectPanel() {} +ProjectPanel::~ProjectPanel() +{ + shutdown(); +} + +void ProjectPanel::shutdown() +{ + m_reload_cancel_token->store(true, std::memory_order_release); + if (m_reload_task && m_reload_task->joinable()) + m_reload_task->join(); + m_reload_task.reset(); +} // Helper to convert newlines to
static std::string convert_newlines_to_br(const std::string& text) { @@ -101,7 +112,17 @@ void ProjectPanel::onWebNavigating(wxWebViewEvent& evt) void ProjectPanel::on_reload(wxCommandEvent& evt) { - boost::thread reload = boost::thread([this] { + if (wxTheApp == nullptr || wxGetApp().is_closing() || + m_reload_cancel_token->load(std::memory_order_acquire)) + return; + + if (m_reload_task && m_reload_task->joinable()) + m_reload_task->join(); + + const auto cancel_token = m_reload_cancel_token; + m_reload_task = std::make_unique([this, cancel_token] { + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; std::string update_type; std::string license; std::string model_name; @@ -115,6 +136,9 @@ void ProjectPanel::on_reload(wxCommandEvent& evt) std::map> files; + if (wxGetApp().plater() == nullptr) + return; + Model model = wxGetApp().plater()->model(); auto model_info = model.model_info; @@ -156,7 +180,14 @@ void ProjectPanel::on_reload(wxCommandEvent& evt) std::string file_path = encode_path(wxGetApp().plater()->model().get_auxiliary_file_temp_path().c_str()); if (!file_path.empty()) { files = Reload(file_path); - wxGetApp().CallAfter([this, file_path, files] { m_auxiliary->Reload(file_path, files); }); + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; + + wxGetApp().CallAfter([this, cancel_token, file_path, files] { + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; + m_auxiliary->Reload(file_path, files); + }); } else { clear_model_info(); return; @@ -215,15 +246,18 @@ void ProjectPanel::on_reload(wxCommandEvent& evt) json m_Res = json::object(); m_Res["command"] = "show_3mf_info"; - m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id++); + m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed)); m_Res["model"] = j; wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore)); - if (m_web_init_completed) { - wxGetApp().CallAfter([this, strJS] { + if (m_web_init_completed.load(std::memory_order_acquire) && + !cancel_token->load(std::memory_order_acquire) && wxTheApp != nullptr && !wxGetApp().is_closing()) { + wxGetApp().CallAfter([this, cancel_token, strJS] { + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; RunScript(strJS.ToStdString()); - }); + }); } }); } @@ -264,7 +298,7 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt) } } else if (strCmd == "request_3mf_info") { - m_web_init_completed = true; + m_web_init_completed.store(true, std::memory_order_release); } else if (strCmd == "edit_project_info") { show_info_editor(true); @@ -307,13 +341,20 @@ void ProjectPanel::update_model_data() void ProjectPanel::clear_model_info() { + if (wxTheApp == nullptr || wxGetApp().is_closing() || + m_reload_cancel_token->load(std::memory_order_acquire)) + return; + json m_Res = json::object(); m_Res["command"] = "clear_3mf_info"; - m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id++); + m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed)); wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore)); - wxGetApp().CallAfter([this, strJS] { + const auto cancel_token = m_reload_cancel_token; + wxGetApp().CallAfter([this, cancel_token, strJS] { + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; RunScript(strJS.ToStdString()); }); } diff --git a/src/slic3r/GUI/Project.hpp b/src/slic3r/GUI/Project.hpp index 0071685e7d..a41f76ba7e 100644 --- a/src/slic3r/GUI/Project.hpp +++ b/src/slic3r/GUI/Project.hpp @@ -26,9 +26,11 @@ #include "nlohmann/json.hpp" #include "slic3r/Utils/json_diff.hpp" +#include #include #include #include +#include #include "Event.hpp" #include "libslic3r/ProjectTask.hpp" #include "wxExtensions.hpp" @@ -60,14 +62,17 @@ struct project_file{ class ProjectPanel : public wxPanel { private: - bool m_web_init_completed = {false}; + std::atomic m_web_init_completed{false}; bool m_reload_already = {false}; + std::shared_ptr> m_reload_cancel_token{std::make_shared>(false)}; + std::unique_ptr m_reload_task; + wxWebView* m_browser = {nullptr}; AuxiliaryPanel* m_auxiliary{nullptr}; wxString m_project_home_url; wxString m_root_dir; - static inline int m_sequence_id = 8000; + static inline std::atomic m_sequence_id{8000}; void show_info_editor(bool show); @@ -75,6 +80,7 @@ private: public: ProjectPanel(wxWindow *parent, wxWindowID id = wxID_ANY, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize, long style = wxTAB_TRAVERSAL); ~ProjectPanel(); + void shutdown(); void onWebNavigating(wxWebViewEvent& evt); diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 6cc988b879..6238849ebc 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -1088,8 +1088,8 @@ void SelectMachineDialog::sync_ams_mapping_result(std::vector &res } } relayout_nozzle_cards(); - auto tab_index = (MainFrame::TabPosition) dynamic_cast(wxGetApp().tab_panel())->GetSelection(); - if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) { + wxString tab_name = wxGetApp().tab_panel()->GetSelectedPageName(); + if (tab_name == TAB_ID_PREPARE || tab_name == TAB_ID_PREVIEW) { updata_thumbnail_data_after_connected_printer(); } } diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.cpp b/src/slic3r/GUI/SyncAmsInfoDialog.cpp index 3c3ea8b609..ea67ad5b31 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.cpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.cpp @@ -1218,8 +1218,8 @@ void SyncAmsInfoDialog::sync_ams_mapping_result(std::vector &resul iter++; } } - auto tab_index = (MainFrame::TabPosition) dynamic_cast(wxGetApp().tab_panel())->GetSelection(); - if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) { + wxString tab_name = wxGetApp().tab_panel()->GetSelectedPageName(); + if (tab_name == TAB_ID_PREPARE || tab_name == TAB_ID_PREVIEW) { updata_thumbnail_data_after_connected_printer(); } } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 0ea685a02c..bec3bed20b 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -6458,7 +6458,7 @@ void Tab::load_current_preset() std::string bmp_name = tab->type() == Slic3r::Preset::TYPE_FILAMENT ? "spool" : tab->type() == Slic3r::Preset::TYPE_SLA_MATERIAL ? "" : "cog"; tab->Hide(); // #ys_WORKAROUND : Hide tab before inserting to avoid unwanted rendering of the tab - dynamic_cast(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), tab, tab->title(), bmp_name); + dynamic_cast(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), wxString(), tab, tab->title(), bmp_name); } else #endif @@ -8537,8 +8537,12 @@ void Page::activate(ConfigOptionMode mode, std::function throw_if_cancel #ifdef __WXMSW__ // BBS: fix field control position - wxTheApp->CallAfter([this]() { - for (auto group : m_optgroups) { + wxTheApp->CallAfter([wp = std::weak_ptr(shared_from_this())]() { + auto page = wp.lock(); + if (!page) + return; + + for (auto group : page->m_optgroups) { if (group->custom_ctrl) group->custom_ctrl->fixup_items_positions(); } diff --git a/src/slic3r/GUI/Tabbook.hpp b/src/slic3r/GUI/Tabbook.hpp index 0cea1b8326..7f10e9dd8d 100644 --- a/src/slic3r/GUI/Tabbook.hpp +++ b/src/slic3r/GUI/Tabbook.hpp @@ -108,7 +108,7 @@ public: // by this control) and show it immediately. bool ShowNewPage(wxWindow * page) { - return AddPage(page, wxString(), ""/*true *//* select it */); + return AddPage(page, wxString()); } // Set effect to use for showing/hiding pages. @@ -139,14 +139,13 @@ public: // Implement base class pure virtual methods. - // adds a new page to the control bool AddPage(wxWindow* page, const wxString& text, - const std::string& bmp_name, - bool bSelect = false) + bool bSelect = false, + int imageId = NO_IMAGE) override { DoInvalidateBestSize(); - return InsertNewPage(GetPageCount(), page, text, bmp_name, bSelect); + return InsertPage(GetPageCount(), page, text, bSelect, imageId); } //// Page management @@ -167,23 +166,6 @@ public: return true; } - bool InsertNewPage(size_t n, - wxWindow * page, - const wxString & text, - const std::string& bmp_name = "", - bool bSelect = false) - { - if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) - return false; - - GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name); - - if (bSelect) - SetSelection(n); - - return true; - } - bool RemovePage(size_t n) { if (!wxBookCtrlBase::RemovePage(n)) diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index e236c84e67..74ed2cbadd 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -95,14 +95,11 @@ void Button::SetIcon(const wxString& icon) } } -void Button::SetInactiveIcon(const wxString &icon) +void Button::SetIcon(const wxBitmap& icon) { - if (!icon.IsEmpty()) { - // BBS set button icon default size to 20 - this->inactive_icon = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt()); - } else { - this->inactive_icon = ScalableBitmap(); - } + this->active_icon = ScalableBitmap(); + this->active_icon.bmp() = icon; + messureSize(); Refresh(); } @@ -257,12 +254,10 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type) void Button::Rescale() { - if (this->active_icon.bmp().IsOk()) + // Only a named icon can be re-rasterized; one set from a wxBitmap has no source file, + if (!this->active_icon.name().empty()) this->active_icon.msw_rescale(); - if (this->inactive_icon.bmp().IsOk()) - this->inactive_icon.msw_rescale(); - messureSize(); if(m_has_style) @@ -293,11 +288,7 @@ void Button::render(wxDC& dc) wxSize szIcon; wxSize textSize = this->textSize.GetSize(); - ScalableBitmap icon; - if (m_selected || ((states & (int)StateColor::State::Hovered) != 0)) - icon = active_icon; - else - icon = inactive_icon; + const ScalableBitmap& icon = active_icon; wxSize padding = this->paddingSize; int spacing = 5; // Wrap text diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index 94b245a75b..c98d583c34 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -34,7 +34,6 @@ class Button : public StaticBox wxSize minSize; // set by outer wxSize paddingSize; ScalableBitmap active_icon; - ScalableBitmap inactive_icon; StateColor text_color; @@ -61,8 +60,7 @@ public: bool SetFont(const wxFont& font) override; void SetIcon(const wxString& icon); - - void SetInactiveIcon(const wxString& icon); + void SetIcon(const wxBitmap& icon); void SetMinSize(const wxSize& size) override; void SetMaxSize(const wxSize& size) override; diff --git a/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp b/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp index 5e6026d1cf..044fe33cde 100644 --- a/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp +++ b/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp @@ -57,18 +57,6 @@ std::string host_theme_vars_css() return s; } -// Document-start user script: injects the contract "; - return WebViewHostDialog::document_start_injector( - style, "orca-host-theme-vars", "afterbegin", - "window.__orcaHostThemed=true;var theme=\"" + host_theme_name() + "\";", - "if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);"); -} - // JS to re-theme an already-loaded document live (no reload): replace the injected // style's contents and update data-orca-theme. Everything downstream (theme.css // tokens, plugin element defaults, page layout) re-cascades from these values. @@ -87,6 +75,46 @@ if(document.documentElement) } // namespace +// Document-start user script: injects the contract "; + return document_start_injector( + style, "orca-host-theme-vars", "afterbegin", + "window.__orcaHostThemed=true;var theme=\"" + host_theme_name() + "\";", + "if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);"); +} + +std::string WebViewHostDialog::plugin_defaults_user_script() +{ + std::string css; + css += ""; + return document_start_injector(css, "orca-plugin-defaults", "beforeend"); +} + std::string WebViewHostDialog::document_start_injector(const std::string& markup, const char* dom_id, const char* position, @@ -244,7 +272,7 @@ void WebViewHostDialog::register_theme_user_scripts() // script message handler is registered separately (AddScriptMessageHandler), but on // some backends RemoveAllUserScripts() drops it too, which would break // window.wx.postMessage / HandleStudio. Live re-theme goes through apply_theme_live(). - m_browser->AddUserScript(wxString::FromUTF8(host_theme_user_script())); + m_browser->AddUserScript(wxString::FromUTF8(theme_user_script())); add_user_scripts(); } diff --git a/src/slic3r/GUI/Widgets/WebViewHostDialog.hpp b/src/slic3r/GUI/Widgets/WebViewHostDialog.hpp index ed21f15f94..119e3b5955 100644 --- a/src/slic3r/GUI/Widgets/WebViewHostDialog.hpp +++ b/src/slic3r/GUI/Widgets/WebViewHostDialog.hpp @@ -49,6 +49,10 @@ public: const std::string& prelude = {}, const std::string& on_inject = {}); + // Shared by modeless Pages tabs and PluginWebDialog. + static std::string theme_user_script(); + static std::string plugin_defaults_user_script(); + protected: wxWebView* browser() const { return m_browser; } diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 7f69d5e087..cc15805256 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -368,7 +368,7 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job) emit_progress(100); if (the_job.switch_to_device_tab) { const auto mainframe = GUI::wxGetApp().mainframe; - mainframe->request_select_tab(MainFrame::TabPosition::tpMonitor); + mainframe->request_select_tab(TAB_ID_MONITOR); } } } diff --git a/src/slic3r/Utils/SimplyPrint.cpp b/src/slic3r/Utils/SimplyPrint.cpp index c1e5235d98..bbfd5209c9 100644 --- a/src/slic3r/Utils/SimplyPrint.cpp +++ b/src/slic3r/Utils/SimplyPrint.cpp @@ -325,7 +325,7 @@ bool SimplyPrint::do_temp_upload(const boost::filesystem::path& file_path, wxLaunchDefaultBrowser(url); } else { const auto mainframe = GUI::wxGetApp().mainframe; - mainframe->request_select_tab(MainFrame::TabPosition::tpMonitor); + mainframe->request_select_tab(TAB_ID_MONITOR); mainframe->load_printer_url(url); } diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index 328ebbace1..40f317c016 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -19,6 +19,7 @@ #include "PyPluginPackage.hpp" #include "PyPluginTrampoline.hpp" #include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp" +#include "pluginTypes/pages/PagesPluginCapability.hpp" #include "pluginTypes/script/ScriptPluginCapability.hpp" #include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp" @@ -319,17 +320,17 @@ void bind_python_api(pybind11::module_& m) { m.doc() = "OrcaSlicer plugin API"; - auto pluginTypes = py::enum_(m, "PluginType", "Available plugin capability groups") - .value("PrinterConnection", PluginCapabilityType::PrinterConnection) - .value("Automation", PluginCapabilityType::Automation) - .value("Analysis", PluginCapabilityType::Analysis) - .value("Importer", PluginCapabilityType::Importer) - .value("Exporter", PluginCapabilityType::Exporter) - .value("Visualization", PluginCapabilityType::Visualization) - .value("Script", PluginCapabilityType::Script) - .value("SlicingPipeline", PluginCapabilityType::SlicingPipeline) - .value("Unknown", PluginCapabilityType::Unknown) - .export_values(); + py::enum_(m, "PluginType", "Available plugin capability groups") + .value("PrinterConnection", PluginCapabilityType::PrinterConnection) + .value("Pages", PluginCapabilityType::Pages) + .value("Analysis", PluginCapabilityType::Analysis) + .value("Importer", PluginCapabilityType::Importer) + .value("Exporter", PluginCapabilityType::Exporter) + .value("Visualization", PluginCapabilityType::Visualization) + .value("Script", PluginCapabilityType::Script) + .value("SlicingPipeline", PluginCapabilityType::SlicingPipeline) + .value("Unknown", PluginCapabilityType::Unknown) + .export_values(); py::enum_(m, "PluginResult", "Execution summary code") .value("Success", PluginResult::Success) @@ -419,9 +420,10 @@ void bind_python_api(pybind11::module_& m) BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings"; // Make sure you register your bindings here - PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes); - ScriptPluginCapability::RegisterBindings(m, pluginTypes); - SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes); + PrinterAgentPluginCapability::RegisterBindings(m); + PagesPluginCapability::RegisterBindings(m); + ScriptPluginCapability::RegisterBindings(m); + SlicingPipelinePluginCapability::RegisterBindings(m); PluginHost::RegisterBindings(m); BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings"; diff --git a/src/slic3r/plugin/PythonPluginInterface.hpp b/src/slic3r/plugin/PythonPluginInterface.hpp index 4a6df06441..8b7518cf1c 100644 --- a/src/slic3r/plugin/PythonPluginInterface.hpp +++ b/src/slic3r/plugin/PythonPluginInterface.hpp @@ -12,7 +12,7 @@ namespace Slic3r { -enum class PluginCapabilityType { PrinterConnection = 0, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown }; +enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown }; struct PluginCapabilityId { @@ -39,7 +39,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type) { switch (type) { case PluginCapabilityType::PrinterConnection: return "printer-connection"; - case PluginCapabilityType::Automation: return "automation"; + case PluginCapabilityType::Pages: return "pages"; case PluginCapabilityType::Analysis: return "analysis"; case PluginCapabilityType::Importer: return "importer"; case PluginCapabilityType::Exporter: return "exporter"; @@ -54,7 +54,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type { switch (type) { case PluginCapabilityType::PrinterConnection: return "Printer connection"; - case PluginCapabilityType::Automation: return "Automation"; + case PluginCapabilityType::Pages: return "Pages"; case PluginCapabilityType::Analysis: return "Analysis"; case PluginCapabilityType::Importer: return "Importer"; case PluginCapabilityType::Exporter: return "Exporter"; @@ -76,8 +76,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view if (lowered == "printer-connection") return PluginCapabilityType::PrinterConnection; - if (lowered == "automation") - return PluginCapabilityType::Automation; + if (lowered == "pages") + return PluginCapabilityType::Pages; if (lowered == "analysis") return PluginCapabilityType::Analysis; if (lowered == "importer") diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp new file mode 100644 index 0000000000..fed16ee416 --- /dev/null +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -0,0 +1,469 @@ +#include "PluginPages.hpp" + +#include "libslic3r/AppConfig.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/Notebook.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Widgets/Button.hpp" +#include "slic3r/GUI/Widgets/WebView.hpp" +#include "slic3r/GUI/Widgets/WebViewHostDialog.hpp" +#include "slic3r/GUI/wxExtensions.hpp" +#include "slic3r/plugin/PluginManager.hpp" + +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace Slic3r { +namespace { + +constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS( +(function () { + if (window.top !== window.self) return; + if (window.orca) return; + var handlers = []; + function deliver(payload, attempts) { + try { + if (window.wx && typeof window.wx.postMessage === 'function') { + window.wx.postMessage(payload); + return; + } + } catch (e) { /* retry while the native handler is being registered */ } + if (attempts < 100) + window.setTimeout(function () { deliver(payload, attempts + 1); }, 25); + } + function send(data) { + deliver(JSON.stringify({ + channel: 'orca', kind: 'message', data: (data === undefined ? null : data) + }), 0); + } + window.orca = { + postMessage: function (data) { send(data); }, + onMessage: function (callback) { + if (typeof callback === 'function') handlers.push(callback); + } + }; + window.__orcaDispatch = function (payload) { + var data = payload ? payload.data : null; + for (var i = 0; i < handlers.length; i++) { + try { handlers[i](data); } catch (e) {} + } + }; +})(); +)JS"; + +} // namespace + +PluginPage::PluginPage(wxWindow* parent, std::shared_ptr capability) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) + , m_cap(std::move(capability)) + , m_lifetime(std::make_shared>(this)) +{ + auto* topsizer = new wxBoxSizer(wxVERTICAL); + SetSizer(topsizer); + + m_browser = WebView::CreateWebView(this, bootstrap_url()); + if (m_browser == nullptr) { + wxLogError("Could not initialize plugin page web view"); + return; + } + + topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); + m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginPage::on_bootstrap_event, this); + m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginPage::on_bootstrap_event, this); + m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this); + m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginPage::on_script_message, this); + m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::theme_user_script())); + m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::plugin_defaults_user_script())); + m_browser->AddUserScript(PLUGIN_PAGE_BRIDGE_JS); + + const std::shared_ptr> lifetime = m_lifetime; + m_cap->set_message_sender([lifetime](const std::string& message) { + if (wxTheApp == nullptr) + return; + + GUI::wxGetApp().CallAfter([lifetime, message] { + if (PluginPage* page = lifetime->load(std::memory_order_acquire)) + page->push_message(message); + }); + }); + +} + +PluginPage::~PluginPage() +{ + detach_capability(); + if (m_lifetime) + m_lifetime->store(nullptr, std::memory_order_release); +} + +void PluginPage::detach_capability() +{ + if (m_lifetime) + m_lifetime->store(nullptr, std::memory_order_release); + if (m_cap) + m_cap->clear_message_sender(); + m_cap.reset(); +} + +wxString PluginPage::web_base_url() const +{ + const auto path = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string(); + return wxString("file://") + GUI::from_u8(path) + "/"; +} + +wxString PluginPage::bootstrap_url() const +{ + const auto path = (boost::filesystem::path(resources_dir()) / "web/dialog/PluginWebDialog/blank.html").make_preferred().string(); + return wxString("file://") + GUI::from_u8(path); +} + +void PluginPage::on_bootstrap_event(wxWebViewEvent& event) +{ + load_plugin_content(); + event.Skip(); +} + +void PluginPage::load_plugin_content() +{ + if (m_content_loaded || m_browser == nullptr || m_cap == nullptr) + return; + + m_content_loaded = true; + try { + m_browser->SetPage(wxString::FromUTF8(m_cap->get_ui()), web_base_url()); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "': " << error.what(); + detach_capability(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "'"; + detach_capability(); + } +} + +void PluginPage::on_new_window(wxWebViewEvent& event) +{ + const wxString url = event.GetURL(); + if (!url.empty() && m_browser != nullptr) + m_browser->LoadURL(url); + event.Veto(); +} + +void PluginPage::on_script_message(wxWebViewEvent& event) +{ + if (!m_cap) + return; + + const wxString payload = event.GetString(); + nlohmann::json root = nlohmann::json::parse(payload.utf8_string(), nullptr, false); + if (root.is_discarded() || root.value("channel", std::string()) != "orca" || + root.value("kind", std::string()) != "message") + return; + + const auto data = root.find("data"); + try { + m_cap->on_message(data == root.end() + ? "null" + : data->dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "': " << error.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "'"; + } +} + +void PluginPage::push_message(const std::string& message) +{ + if (m_browser == nullptr) + return; + + // PagesPluginCapability::post_message() already dumps JSON, so accept it as-is; only a + // non-JSON payload needs wrapping as a string literal. + const std::string payload = nlohmann::json::accept(message) + ? message + : nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace); + + WebView::RunScript(m_browser, wxString::Format( + "(function dispatch(payload, attempts) {\n" + " if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n" + " if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n" + "})({data: %s}, 0);", + wxString::FromUTF8(payload))); +} + +PluginPages::~PluginPages() +{ + shutdown(); +} + +void PluginPages::initialize(Notebook* parent) +{ + shutdown(); + m_parent = parent; + if (m_parent == nullptr) + return; + + m_visible_page_count = GUI::wxGetApp().app_config->get_plugin_pages_visible_count(); + + for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) { + if (capability) + create_page(capability->identity()); + } + relayout(); +} + +void PluginPages::shutdown() +{ + while (!m_pages.empty()) + remove_page(m_pages.begin()->first); + m_parent = nullptr; +} + +void PluginPages::set_visible_page_count(int count) +{ + const int clamped = std::clamp(count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX); + if (clamped == m_visible_page_count) + return; + + m_visible_page_count = clamped; + relayout(); +} + +std::shared_ptr PluginPages::get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const +{ + auto capability = PluginManager::instance().get_plugin_capability(id, /*only_enabled=*/false); + if (!capability || capability->is_enabled() != is_enabled || capability->type() != PluginCapabilityType::Pages) + return nullptr; + + return std::dynamic_pointer_cast(capability); +} + +bool PluginPages::create_page(const PluginCapabilityId& id) +{ + if (m_pages.find(id) != m_pages.end()) + return false; + + auto capability = get_pages_cap(id, true); + if (!capability) + return false; + + std::string icon; + try { + icon = capability->get_icon(); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to get icon for plugin " << id.plugin_key << ": " << error.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to get icon for plugin " << id.plugin_key; + } + + auto* page = new PluginPage(m_parent, std::move(capability)); + if (!page->is_valid()) { + page->Destroy(); + return false; + } + + if (!icon.empty()) { + try { + boost::filesystem::path icon_path(icon); + const std::string extension = icon_path.extension().string(); + if (extension == ".svg" || extension == ".png") + icon_path.replace_extension(); + + page->set_icon(create_scaled_bitmap(icon_path.string(), m_parent, 20)); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key << ": " << error.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key; + } + } + + m_pages.emplace(id, page); + m_order.push_back(id); + return true; +} + +void PluginPages::on_cap_register(const PluginCapabilityId& id) +{ + if (m_parent == nullptr) + return; + + if (create_page(id)) + relayout(); +} + +void PluginPages::on_cap_deregister(const PluginCapabilityId& id) +{ + remove_page(id); +} + +void PluginPages::on_plugin_register(const std::string& plugin_key) +{ + for (const auto& capability : PluginManager::instance().get_plugin_capabilities(plugin_key, PluginCapabilityType::Pages)) { + if (capability) + on_cap_register(capability->identity()); + } +} + +void PluginPages::on_plugin_deregister(const std::string& plugin_key) +{ + for (auto it = m_pages.begin(); it != m_pages.end();) { + if (it->first.plugin_key != plugin_key) { + ++it; + continue; + } + + const PluginCapabilityId id = it->first; + ++it; + remove_page(id); + } +} + +void PluginPages::remove_page(const PluginCapabilityId& id) +{ + auto it = m_pages.find(id); + if (it == m_pages.end()) + return; + + PluginPage* page = it->second; + page->detach_capability(); + + m_pages.erase(it); + m_order.erase(std::remove(m_order.begin(), m_order.end(), id), m_order.end()); + + const int idx = m_parent != nullptr ? m_parent->FindPage(page) : wxNOT_FOUND; + if (idx != wxNOT_FOUND) + m_parent->RemovePage(idx); + + relayout(); + page->Destroy(); +} + +wxString PluginPages::page_tab_id(const PluginCapabilityId& id) +{ + return wxString::FromUTF8("plugin." + id.plugin_key + "." + id.name); +} + +void PluginPages::relayout() +{ + if (m_parent == nullptr) + return; + + m_order.erase(std::remove_if(m_order.begin(), m_order.end(), + [this](const PluginCapabilityId& id) { + const bool orphaned = m_pages.find(id) == m_pages.end(); + if (orphaned) + BOOST_LOG_TRIVIAL(error) << "PluginPages::relayout: '" << id.name << "' was in m_order but not m_pages, dropping"; + return orphaned; + }), + m_order.end()); + + const int visible_slots = std::max(1, m_visible_page_count); + const bool need_overflow = static_cast(m_order.size()) > visible_slots; + + // Every visible slot is a normal, individual tab hosting its own page. When there's + // overflow, the last slot's page is swappable via m_overflow_button/show_overflow_menu() + // rather than being a fixed page — m_swapped_in_id tracks which one currently sits there. + std::vector tab_ids; + if (!need_overflow) { + tab_ids = m_order; + m_swapped_in_id.reset(); + } else { + const auto overflow_begin = m_order.begin() + (visible_slots - 1); + tab_ids.assign(m_order.begin(), overflow_begin); + + if (!m_swapped_in_id || std::find(overflow_begin, m_order.end(), *m_swapped_in_id) == m_order.end()) + m_swapped_in_id = *overflow_begin; + tab_ids.push_back(*m_swapped_in_id); + } + + // MainFrame::show_device() relayouts on every printer change and most of those change + // nothing, so only touch the notebook when the trailing slots don't already spell out + // tab_ids — a rebuild destroys and recreates every tab button and rasterizes every icon. + const size_t page_count = m_parent->GetPageCount(); + bool up_to_date = page_count >= tab_ids.size(); + for (size_t i = 0; up_to_date && i < tab_ids.size(); ++i) + up_to_date = m_parent->GetPageName(page_count - tab_ids.size() + i) == page_tab_id(tab_ids[i]); + for (const auto& [id, page] : m_pages) { + if (!up_to_date) + break; + const bool wanted = std::find(tab_ids.begin(), tab_ids.end(), id) != tab_ids.end(); + up_to_date = (m_parent->FindPage(page) != wxNOT_FOUND) == wanted; + } + + if (!up_to_date) { + const wxString id_to_reselect = m_parent->GetSelectedPageName(); + + for (const auto& [id, page] : m_pages) { + const int idx = m_parent->FindPage(page); + if (idx != wxNOT_FOUND) + m_parent->RemovePage(idx); + } + + for (const auto& id : tab_ids) { + PluginPage* page = m_pages.at(id); + m_parent->InsertPage(m_parent->GetPageCount(), page_tab_id(id), page, wxString::FromUTF8(id.name), "", + false, page->icon()); + } + + if (!id_to_reselect.empty()) + m_parent->SelectPageByName(id_to_reselect); + } + + if (need_overflow) { + if (m_overflow_button == nullptr) { + auto* btn = new Button(m_parent->GetBtnsListCtrl(), wxString(L"\u25BE"), wxString(), wxNO_BORDER); + btn->SetCornerRadius(0); + const int em = em_unit(m_parent); + btn->SetMinSize({40 * em / 10, 36 * em / 10}); + btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { show_overflow_menu(); }); + GUI::wxGetApp().UpdateDarkUI(btn); + m_overflow_button = btn; + } + m_parent->SetOverflowButton(m_overflow_button); + } else if (m_overflow_button != nullptr) { + m_parent->SetOverflowButton(nullptr); + m_overflow_button->Destroy(); + m_overflow_button = nullptr; + } +} + +void PluginPages::show_overflow_menu() +{ + const int visible_slots = std::max(1, m_visible_page_count); + if (m_overflow_button == nullptr || static_cast(m_order.size()) <= visible_slots) + return; + + const std::vector overflow_ids(m_order.begin() + (visible_slots - 1), m_order.end()); + + wxMenu menu; + for (size_t i = 0; i < overflow_ids.size(); ++i) + menu.AppendRadioItem(static_cast(wxID_HIGHEST + 1 + i), wxString::FromUTF8(overflow_ids[i].name)); + if (m_swapped_in_id) { + const auto it = std::find(overflow_ids.begin(), overflow_ids.end(), *m_swapped_in_id); + if (it != overflow_ids.end()) + menu.Check(static_cast(wxID_HIGHEST + 1 + (it - overflow_ids.begin())), true); + } + + menu.Bind(wxEVT_MENU, [this, overflow_ids](wxCommandEvent& evt) { + const size_t index = static_cast(evt.GetId() - (wxID_HIGHEST + 1)); + if (index >= overflow_ids.size()) + return; + m_swapped_in_id = overflow_ids[index]; + relayout(); + m_parent->SelectPageByName(page_tab_id(*m_swapped_in_id)); + }); + m_overflow_button->PopupMenu(&menu); +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp new file mode 100644 index 0000000000..4d00de6df9 --- /dev/null +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +class Notebook; + +namespace Slic3r { + +class PluginPage : public wxPanel +{ +public: + PluginPage(wxWindow* parent, std::shared_ptr capability); + ~PluginPage() override; + + PluginPage() = delete; + + bool is_valid() const { return m_browser != nullptr && m_cap != nullptr; } + void detach_capability(); + void on_bootstrap_event(wxWebViewEvent& event); + void on_new_window(wxWebViewEvent& event); + void on_script_message(wxWebViewEvent& event); + void push_message(const std::string& message); + void set_icon(const wxBitmap& icon) { m_icon = icon; } + const wxBitmap& icon() const { return m_icon; } + +private: + void load_plugin_content(); + wxString bootstrap_url() const; + wxString web_base_url() const; + + wxWebView* m_browser{nullptr}; + std::shared_ptr m_cap; + std::shared_ptr> m_lifetime; + bool m_content_loaded{false}; + wxBitmap m_icon; +}; + +class PluginPages +{ +public: + PluginPages() = default; + ~PluginPages(); + + PluginPages(const PluginPages&) = delete; + PluginPages& operator=(const PluginPages&) = delete; + + void initialize(Notebook* parent); + void shutdown(); + + void on_cap_register(const PluginCapabilityId& id); + void on_cap_deregister(const PluginCapabilityId& id); + void on_plugin_register(const std::string& plugin_key); + void on_plugin_deregister(const std::string& plugin_key); + + void set_visible_page_count(int count); + + void relayout(); + +private: + std::shared_ptr get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const; + bool create_page(const PluginCapabilityId& id); + void remove_page(const PluginCapabilityId& id); + + void show_overflow_menu(); + static wxString page_tab_id(const PluginCapabilityId& id); + + std::map m_pages; + std::vector m_order; + Notebook* m_parent{nullptr}; + + int m_visible_page_count{0}; + + std::optional m_swapped_in_id; + wxWindow* m_overflow_button{nullptr}; +}; + +} // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp new file mode 100644 index 0000000000..e009f4426f --- /dev/null +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp @@ -0,0 +1,60 @@ +#include "PagesPluginCapability.hpp" +#include "PagesPluginCapabilityTrampoline.hpp" + +#include "../../PluginFsUtils.hpp" + +#include +#include + +#include + +namespace py = pybind11; + +namespace Slic3r { + +void PagesPluginCapability::RegisterBindings(pybind11::module_& module) +{ + BOOST_LOG_TRIVIAL(debug) << "Registering orca.pages bindings"; + + auto pages = module.def_submodule("pages", "Plugin page API"); + + py::class_>(pages, "PagesPluginCapabilityBase") + .def(py::init<>()) + .def("get_type", &PagesPluginCapability::get_type) + .def("get_ui", &PagesPluginCapability::get_ui) + .def("get_icon", &PagesPluginCapability::get_icon) + .def("on_message", &PagesPluginCapability::on_message) + .def( + "post_message", + [](PagesPluginCapability& capability, py::object data) { + capability.post_message(py_to_json(data).dump()); + }, + py::arg("data"), "Send a JSON-compatible value to the page's window.orca.onMessage handlers."); +} + +void PagesPluginCapability::post_message(std::string message) +{ + std::function sender; + { + std::lock_guard lock(m_message_mutex); + sender = m_message_sender; + } + + if (sender) + sender(message); +} + +void PagesPluginCapability::set_message_sender(std::function sender) +{ + std::lock_guard lock(m_message_mutex); + m_message_sender = std::move(sender); +} + +void PagesPluginCapability::clear_message_sender() +{ + std::lock_guard lock(m_message_mutex); + m_message_sender = nullptr; +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp new file mode 100644 index 0000000000..978492006c --- /dev/null +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp @@ -0,0 +1,33 @@ +#ifndef slic3r_PagesPluginCapability_hpp_ +#define slic3r_PagesPluginCapability_hpp_ + +#include "../../PythonPluginInterface.hpp" +#include "pybind11/pybind11.h" + +#include +#include +#include + +namespace Slic3r { +class PagesPluginCapability : public PluginCapabilityInterface +{ +public: + static void RegisterBindings(pybind11::module_& module); + + PluginCapabilityType get_type() const override { return PluginCapabilityType::Pages; } + + virtual std::string get_ui() = 0; + virtual void on_message(std::string message) { (void) message; } + virtual std::string get_icon() { return {}; } + + void post_message(std::string message); + void set_message_sender(std::function sender); + void clear_message_sender(); + +private: + mutable std::mutex m_message_mutex; + std::function m_message_sender; +}; +} // namespace Slic3r + +#endif diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp new file mode 100644 index 0000000000..3fb476c228 --- /dev/null +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "PagesPluginCapability.hpp" +#include "../../PluginFsUtils.hpp" +#include "../../PyPluginTrampoline.hpp" + +#include + +namespace Slic3r { + +class PyPagesPluginCapabilityTrampoline : public PyPluginCommonTrampoline +{ +public: + using PyPluginCommonTrampoline::PyPluginCommonTrampoline; + + std::string get_icon() override + { + ORCA_PY_OVERRIDE_AUDITED( + ::Slic3r::PluginAuditManager::AuditMode::Loading, + [] {}, + PYBIND11_OVERRIDE, + std::string, + PagesPluginCapability, + get_icon); + } + + std::string get_ui() override + { + ORCA_PY_OVERRIDE_AUDITED( + ::Slic3r::PluginAuditManager::AuditMode::Loading, + [] {}, + PYBIND11_OVERRIDE_PURE, + std::string, + PagesPluginCapability, + get_ui); + } + + void on_message(std::string message) override + { + PluginCapabilityInterface::RefCounter ref_counter(*this); + PythonGILState gil; + if (!gil) + throw std::runtime_error("Python interpreter is shutting down"); + + ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading); + + pybind11::function override = pybind11::get_override(static_cast(this), "on_message"); + if (!override) + return; + + nlohmann::json data = nlohmann::json::parse(message, nullptr, false); + if (data.is_discarded()) + data = message; + + ORCA_PY_LOGGED_OVERRIDE_BODY(override(::Slic3r::json_to_py(data))); + } +}; + +} // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp index b3d3d5d44c..d428775c12 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp @@ -13,10 +13,8 @@ namespace py = pybind11; namespace Slic3r { -void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_& pluginTypes) +void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module) { - (void) pluginTypes; - auto printer_agent_module = module.def_submodule("printer_agent", "Printer Agent API"); py::enum_(printer_agent_module, "FilamentSyncMode") diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp index 33ad211b9c..ede7c6a9b8 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp @@ -19,7 +19,7 @@ namespace Slic3r { class PrinterAgentPluginCapability : public PluginCapabilityInterface, public IPrinterAgent { public: - static void RegisterBindings(pybind11::module_& module, pybind11::enum_& pluginTypes); + static void RegisterBindings(pybind11::module_& module); PluginCapabilityType get_type() const override { return PluginCapabilityType::PrinterConnection; } diff --git a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.cpp index 35a259edf6..712ba9b653 100644 --- a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.cpp @@ -9,9 +9,8 @@ namespace py = pybind11; namespace Slic3r { -void ScriptPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_& pluginTypes) +void ScriptPluginCapability::RegisterBindings(pybind11::module_& module) { - (void) pluginTypes; BOOST_LOG_TRIVIAL(debug) << "Registering orca.script bindings"; auto script = module.def_submodule("script", "Script Plugins API"); diff --git a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp index cb5bc45c08..fb1319e560 100644 --- a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp @@ -11,8 +11,7 @@ public: virtual ExecutionResult execute() = 0; - static void RegisterBindings(pybind11::module_ &module, - pybind11::enum_ &pluginTypes); + static void RegisterBindings(pybind11::module_ &module); }; } // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp index f4569aebba..d2e630242d 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp @@ -8,8 +8,7 @@ namespace Slic3r { bool SlicingPipelineContext::cancelled() const { return print && print->canceled(); } -void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::enum_& pluginTypes) { - (void) pluginTypes; // unused: this capability defines its own Step enum (below) rather than extending the shared PluginCapabilityType enum. +void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module) { auto slicing = module.def_submodule("slicing", "Slicing pipeline API (research/experimental)."); py::enum_(slicing, "Step") diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp index 639c087371..da0dbcbcbd 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp @@ -37,7 +37,7 @@ public: // Runs on the slicing worker thread. Do not call orca.host.ui.* here: the UI thread can be // blocked waiting on the slicing worker, so a marshaled UI call from this thread can deadlock. virtual ExecutionResult execute(SlicingPipelineContext& ctx) = 0; - static void RegisterBindings(pybind11::module_& module, pybind11::enum_& pluginTypes); + static void RegisterBindings(pybind11::module_& module); }; } // namespace Slic3r