From 56c28fc102f399d650de2c0449058a7649695ea1 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 28 Jul 2026 19:17:05 +0800 Subject: [PATCH 01/14] feat: refactor notebook/tabs to be string based instead of fixed index based --- src/slic3r/GUI/Downloader.cpp | 2 +- src/slic3r/GUI/Field.cpp | 6 +- src/slic3r/GUI/GLCanvas3D.cpp | 2 +- src/slic3r/GUI/GUI_App.cpp | 20 ++-- src/slic3r/GUI/MainFrame.cpp | 137 +++++++++++++++---------- src/slic3r/GUI/MainFrame.hpp | 29 +++--- src/slic3r/GUI/Notebook.cpp | 2 + src/slic3r/GUI/Notebook.hpp | 76 ++++++++++++-- src/slic3r/GUI/NotificationManager.cpp | 12 +-- src/slic3r/GUI/Plater.cpp | 67 ++++++------ src/slic3r/GUI/PresetComboBoxes.cpp | 2 +- src/slic3r/GUI/SelectMachine.cpp | 4 +- src/slic3r/GUI/SyncAmsInfoDialog.cpp | 4 +- src/slic3r/GUI/Tab.cpp | 2 +- src/slic3r/Utils/PrintHost.cpp | 2 +- src/slic3r/Utils/SimplyPrint.cpp | 2 +- 16 files changed, 233 insertions(+), 136 deletions(-) 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 bcbc381eff..532091313c 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -330,8 +330,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 a7b063890f..58de15355d 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -9132,7 +9132,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 29aad072f0..1290df70e4 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -798,12 +798,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) { @@ -837,7 +837,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() || @@ -875,9 +875,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 @@ -1814,10 +1814,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); } } @@ -3323,7 +3323,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(); @@ -4487,7 +4487,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 f55ce7cebc..e2b94d11ff 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 && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW)) { 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); + { + 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"), std::string("tab_3d_active"), std::string("tab_3d_active"), false); + m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false); + } 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); @@ -1252,15 +1255,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 +1277,7 @@ void MainFrame::init_tabpanel() { //monitor } #ifndef __APPLE__ - if (sel == tp3DEditor) { + if (m_last_selected_tab == TAB_ID_PREPARE) { m_topbar->EnableUndoRedoItems(); } else { @@ -1309,10 +1311,10 @@ void MainFrame::init_tabpanel() { 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", "tab_home_active", false); m_param_panel = new ParamsPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); } @@ -1327,7 +1329,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"), std::string("tab_monitor_active"), std::string("tab_monitor_active"), false); m_printer_view = new PrinterWebView(m_tabpanel); Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent &evt) { @@ -1342,16 +1344,17 @@ 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"), std::string("tab_multi_active"), std::string("tab_multi_active"), false); } 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"), std::string("tab_auxiliary_active"), std::string("tab_auxiliary_active"), false); 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"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false); + if (m_plater) { // load initial config @@ -1387,7 +1390,11 @@ void MainFrame::show_device(bool bBBLPrinter) { 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")); + { + const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); + const size_t monitor_pos = (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; + m_tabpanel->InsertPage(monitor_pos, TAB_ID_MONITOR, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active")); + } if (wxGetApp().is_enable_multi_machine()) { if (!m_multi_machine) { @@ -1396,17 +1403,22 @@ void MainFrame::show_device(bool bBBLPrinter) { } // 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); + { + const int monitor_idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR); + const size_t multi_pos = (monitor_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(monitor_idx) + 1; + m_tabpanel->InsertPage(multi_pos, TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), + std::string("tab_multi_active"), false); + } } 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"), + // Calibration is always appended last (AddPage), so it lands after whichever of Monitor/Multi-device + // actually got inserted above — no longer position-sensitive now that insertion position is computed + // from FindPageByName rather than a fixed TabPosition index. + m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false); #ifdef _MSW_DARK_MODE @@ -1440,8 +1452,12 @@ void MainFrame::show_device(bool bBBLPrinter) { }); } m_printer_view->Show(false); - m_tabpanel->InsertPage(tpMonitor, m_printer_view, _L("Device"), std::string("tab_monitor_active"), - std::string("tab_monitor_active")); + { + const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); + const size_t monitor_pos = (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; + m_tabpanel->InsertPage(monitor_pos, TAB_ID_MONITOR, m_printer_view, _L("Device"), std::string("tab_monitor_active"), + std::string("tab_monitor_active")); + } } fit_tab_labels(); // ORCA on printer change } @@ -1475,7 +1491,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) { @@ -1793,22 +1809,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 @@ -1917,7 +1933,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); } }); @@ -3063,7 +3079,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 m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, [this]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this); viewMenu->AppendSeparator(); @@ -3072,7 +3088,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( @@ -3081,7 +3097,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 m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, [this]() { return wxGetApp().show_3d_navigator(); }, this); append_menu_check_item(viewMenu, wxID_ANY, _L("Show Gridlines"), _L("Show Gridlines on plate"), @@ -3089,14 +3105,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 m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, [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) && + return (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW) && m_plater->is_sidebar_enabled(); }, this); @@ -3119,7 +3135,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(); @@ -3920,13 +3936,18 @@ void MainFrame::select_tab(wxPanel* panel) wxGetApp().params_dialog()->Popup(); return; } + // page_name cannot be resolved via panel->GetName() — Prepare and Preview + // share the single m_plater window, so the window itself has no single correct + // name (see Global Constraints). Resolve via Notebook's per-slot m_pageNames + // instead, via the index -> id lookup, which works for any page (built-in or not). 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 @@ -3934,7 +3955,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); } @@ -3944,26 +3965,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))) @@ -3972,10 +3993,14 @@ 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) + // NOTE: this checks the ORIGINAL parameter (id), not the resolved new_selection — + // preserving that the fallback-to-last-tab path never triggers this render call + // even if the last selected tab happened to be Prepare. Do not "simplify" to + // new_selection == TAB_ID_PREPARE, that changes behavior. + 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(); } @@ -3984,10 +4009,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); } @@ -4267,7 +4292,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 20229a611e..9bb02b28a6 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -36,6 +36,16 @@ #include "calib_dlg.hpp" #include "MultiMachinePage.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" +#define TAB_ID_MULTI_DEVICE "multi_device" +#define TAB_ID_PROJECT "project" +#define TAB_ID_CALIBRATION "calibration" + #define ENABEL_PRINT_ALL 0 class Notebook; @@ -115,7 +125,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 +224,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 +323,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 diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index ceda3fc0d6..a6906b4550 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -253,6 +253,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..a82be7be56 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -3,6 +3,7 @@ //#ifdef _WIN32 +#include #include #include @@ -42,7 +43,7 @@ private: std::vector m_pageLabels; // ORCA }; -class Notebook: public wxBookCtrlBase +class Notebook : public wxBookCtrlBase { public: Notebook(wxWindow * parent, @@ -103,7 +104,7 @@ public: // by this control) and show it immediately. bool ShowNewPage(wxWindow * page) { - return AddPage(page, wxString(), "", ""); + return AddPage(wxString(), page, wxString(), "", ""); } @@ -136,14 +137,15 @@ public: // Implement base class pure virtual methods. // adds a new page to the control - bool AddPage(wxWindow* page, + bool AddPage(const wxString& id, + wxWindow* page, const wxString& text, const std::string& bmp_name, const std::string& inactive_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, inactive_bmp_name, bSelect); } // Page management @@ -156,6 +158,7 @@ public: if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId)) return false; + m_pageNames.insert(m_pageNames.begin() + n, wxString()); GetBtnsListCtrl()->InsertPage(n, text, bSelect); if (!DoSetSelectionAfterInsertion(n, bSelect)) @@ -165,6 +168,7 @@ public: } bool InsertPage(size_t n, + const wxString& id, wxWindow * page, const wxString & text, const std::string& bmp_name = "", @@ -174,10 +178,17 @@ public: if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) return false; + m_pageNames.insert(m_pageNames.begin() + n, id); GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, inactive_bmp_name); - if (bSelect) - SetSelection(n); + // wxBookCtrlBase::InsertPage() only inserts into the page list and sizes the + // new page to the current page's rect — it never touches visibility. A freshly + // constructed page defaults to shown, so without this it renders on top of + // whatever page is currently selected until the next SetSelection() call hides + // it. Mirrors the pure-virtual InsertPage() override above, which already does + // this correctly. + if (!DoSetSelectionAfterInsertion(n, bSelect)) + page->Hide(); return true; } @@ -251,8 +262,58 @@ public: page->SetFocus(); } + // wxBookCtrlBase::DeleteAllPages() clears its page list directly rather than + // going through DoRemovePage() per page, so it would otherwise leave + // m_pageNames desynchronized (a mutation path outside the four this class + // already keeps in sync). Not currently called on a Notebook anywhere in + // this codebase, but kept correct for the same reason the rest of this + // bookkeeping exists. + virtual bool DeleteAllPages() override + { + m_pageNames.clear(); + return wxBookCtrlBase::DeleteAllPages(); + } + ButtonsListCtrl* GetBtnsListCtrl() const { return static_cast(m_bookctrl); } + 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() { GetBtnsListCtrl()->UpdateMode(); @@ -369,6 +430,7 @@ protected: wxWindow* const win = wxBookCtrlBase::DoRemovePage(page); if (win) { + m_pageNames.erase(m_pageNames.begin() + page); GetBtnsListCtrl()->RemovePage(page); DoSetSelectionAfterRemoval(page); } @@ -394,6 +456,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 feecdfa163..66a9fa88db 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5787,7 +5787,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"); @@ -6569,9 +6569,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); } @@ -7870,7 +7870,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 @@ -7889,7 +7889,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) { @@ -8797,7 +8797,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(); @@ -8826,7 +8826,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(); } @@ -11208,13 +11208,20 @@ 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) { + // Guards against new_sel matching FindPageByName's own wxNOT_FOUND sentinel + // below when a TAB_ID_* isn't currently present in the tabpanel. + e.Skip(); + return; + } + sidebar_layout.show = new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_PREPARE) || + new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_PREVIEW); update_sidebar(); int old_sel = e.GetOldSelection(); const bool is_printer_agent_plugin = NetworkAgentFactory::is_current_printer_agent_plugin(); const bool use_native_device_tab = wxGetApp().preset_bundle && (wxGetApp().preset_bundle->use_bbl_device_tab() || is_printer_agent_plugin); - if (use_native_device_tab && new_sel == MainFrame::tpMonitor) { + if (use_native_device_tab && new_sel == main_frame->m_tabpanel->FindPageByName(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 (!is_printer_agent_plugin && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) { @@ -11226,7 +11233,7 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } } } else { - if (new_sel == MainFrame::tpMonitor && wxGetApp().preset_bundle != nullptr) { + if (new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_MONITOR) && wxGetApp().preset_bundle != nullptr) { auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; wxString url = cfg.opt_string("print_host_webui").empty() ? cfg.opt_string("print_host") : cfg.opt_string("print_host_webui"); if (main_frame->m_printer_view && url.empty()) { @@ -12094,7 +12101,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; } @@ -13051,7 +13058,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); @@ -13200,7 +13207,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(); @@ -13314,7 +13321,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; @@ -13623,7 +13630,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)); @@ -14104,7 +14111,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) @@ -14141,7 +14148,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")) @@ -14218,7 +14225,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")) @@ -14297,7 +14304,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; @@ -14357,7 +14364,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; @@ -14403,7 +14410,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; @@ -14469,7 +14476,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; @@ -14534,7 +14541,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; @@ -14667,7 +14674,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."))); @@ -15340,7 +15347,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; } @@ -15569,7 +15576,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); } } @@ -17383,7 +17390,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())); @@ -17415,7 +17422,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) @@ -17430,7 +17437,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) @@ -18324,7 +18331,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/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 8fdca030c6..a44a4a224e 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -1039,7 +1039,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/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index d6ab50c1a4..ba3bf9f00a 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 c5c007076a..256ceecc2e 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 6bb47059e2..55c050760c 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -6415,7 +6415,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 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); } From e00906a833d588ba62eb9e51e14cae38ac496c94 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 28 Jul 2026 19:17:26 +0800 Subject: [PATCH 02/14] feat: plugin pages --- src/slic3r/CMakeLists.txt | 5 + src/slic3r/GUI/GUI_App.cpp | 14 + src/slic3r/GUI/MainFrame.cpp | 4 + src/slic3r/GUI/MainFrame.hpp | 3 + src/slic3r/GUI/Notebook.cpp | 19 +- src/slic3r/GUI/PluginWebDialog.cpp | 35 +- src/slic3r/GUI/Widgets/WebViewHostDialog.cpp | 54 +++- src/slic3r/GUI/Widgets/WebViewHostDialog.hpp | 4 + src/slic3r/plugin/PythonPluginBridge.cpp | 30 +- src/slic3r/plugin/PythonPluginInterface.hpp | 10 +- src/slic3r/plugin/host/PluginPages.cpp | 298 ++++++++++++++++++ src/slic3r/plugin/host/PluginPages.hpp | 69 ++++ .../pages/PagesPluginCapability.cpp | 59 ++++ .../pages/PagesPluginCapability.hpp | 32 ++ .../pages/PagesPluginCapabilityTrampoline.hpp | 48 +++ .../PrinterAgentPluginCapability.cpp | 4 +- .../PrinterAgentPluginCapability.hpp | 2 +- .../script/ScriptPluginCapability.cpp | 3 +- .../script/ScriptPluginCapability.hpp | 3 +- .../SlicingPipelinePluginCapability.cpp | 3 +- .../SlicingPipelinePluginCapability.hpp | 2 +- 21 files changed, 622 insertions(+), 79 deletions(-) create mode 100644 src/slic3r/plugin/host/PluginPages.cpp create mode 100644 src/slic3r/plugin/host/PluginPages.hpp create mode 100644 src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp create mode 100644 src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp create mode 100644 src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 35cf96d171..6dd17e3ce8 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/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 1290df70e4..e7ca7cc581 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2799,6 +2799,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_capability_load_callback( [refresh_plugins_dialog](const PluginCapabilityId& capability) { if (capability.type == PluginCapabilityType::PrinterConnection) @@ -2811,11 +2821,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](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(); }); } diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index e2b94d11ff..01c698c62c 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1110,6 +1110,7 @@ void MainFrame::update_edge_panels() void MainFrame::shutdown() { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter"; + m_plugin_pages.shutdown(); #ifdef __WXGTK__ // Edge panels are child windows — wxWidgets destroys them automatically. m_edge_bottom = nullptr; @@ -1355,6 +1356,9 @@ void MainFrame::init_tabpanel() { m_calibration->SetBackgroundColour(*wxWHITE); m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false); + // 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 diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 9bb02b28a6..53cc885a27 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -35,6 +35,7 @@ #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. @@ -357,6 +358,7 @@ public: //SoftFever void show_device(bool bBBLPrinter); void fit_tab_labels(); // ORCA + PluginPages& plugin_pages() { return m_plugin_pages; } PA_Calibration_Dlg* m_pa_calib_dlg{ nullptr }; FlowRateCalibrationDialog* m_flow_rate_calib_dlg{ nullptr }; @@ -382,6 +384,7 @@ public: CalibrationPanel* m_calibration{ nullptr }; WebViewPanel* m_webview { nullptr }; PrinterWebView* m_printer_view{nullptr}; + PluginPages m_plugin_pages; wxLogWindow* m_log_window { nullptr }; // BBS //wxBookCtrlBase* m_tabpanel { nullptr }; diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index a6906b4550..4c6430f99c 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}); @@ -135,6 +135,13 @@ void ButtonsListCtrl::SetSelection(int sel) 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( @@ -192,6 +199,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 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/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/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..3e7d534810 --- /dev/null +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -0,0 +1,298 @@ +#include "PluginPages.hpp" + +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/Notebook.hpp" +#include "slic3r/GUI/GUI_App.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 + +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 nlohmann::json data = root.contains("data") ? root["data"] : nlohmann::json(); + try { + m_cap->on_message(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; + + nlohmann::json data = nlohmann::json::parse(message, nullptr, false); + if (data.is_discarded()) + data = message; + + const wxString script = wxString("(function dispatch(payload, attempts) {\n") + + wxString(" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n") + + wxString(" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n") + + wxString("})({data: ") + + wxString::FromUTF8(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)) + + wxString("}, 0);"); + WebView::RunScript(m_browser, script); +} + +PluginPages::~PluginPages() +{ + shutdown(); +} + +void PluginPages::initialize(Notebook* parent) +{ + shutdown(); + m_parent = parent; + if (m_parent == nullptr) + return; + + for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) { + if (capability) + on_cap_register(capability->identity()); + } +} + +void PluginPages::shutdown() +{ + while (!m_pages.empty()) + remove_page(m_pages.begin()->first); + m_parent = nullptr; +} + +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); +} + +void PluginPages::on_cap_register(const PluginCapabilityId& id) +{ + if (m_parent == nullptr || m_pages.find(id) != m_pages.end()) + return; + + auto capability = get_pages_cap(id, true); + if (!capability) + return; + + auto* page = new PluginPage(m_parent, std::move(capability)); + if (!page->is_valid()) { + page->Destroy(); + return; + } + + const wxString title = wxString::FromUTF8(id.name); + const wxString page_id = wxString::FromUTF8("plugin." + id.plugin_key + "." + id.name); + if (!m_parent->AddPage(page_id, page, title, "tab_auxiliary_active", "tab_auxiliary_active", false)) { + page->Destroy(); + return; + } + + m_pages.emplace(id, page); +} + +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(); + if (m_parent != nullptr) { + const int index = m_parent->FindPage(page); + if (index != wxNOT_FOUND) + m_parent->RemovePage(static_cast(index)); + } + page->Destroy(); + m_pages.erase(it); +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp new file mode 100644 index 0000000000..bf70305201 --- /dev/null +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -0,0 +1,69 @@ +#pragma once + +#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); + +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}; +}; + +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); + +private: + std::shared_ptr get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const; + void remove_page(const PluginCapabilityId& id); + + std::map m_pages; + Notebook* m_parent{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..fc5d4a03b8 --- /dev/null +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp @@ -0,0 +1,59 @@ +#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("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..e833d7dbf4 --- /dev/null +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp @@ -0,0 +1,32 @@ +#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; } + + 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..71eb5c5bcc --- /dev/null +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp @@ -0,0 +1,48 @@ +#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_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 From 3145f28bb70b8605664d01fe1d47657ed37ba8a7 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Wed, 29 Jul 2026 19:37:17 +0800 Subject: [PATCH 03/14] feat: support tab icons --- src/slic3r/GUI/Auxiliary.cpp | 10 +- src/slic3r/GUI/CalibrationPanel.cpp | 1 - src/slic3r/GUI/MainFrame.cpp | 25 ++-- src/slic3r/GUI/Monitor.cpp | 10 +- src/slic3r/GUI/MultiMachinePage.cpp | 6 +- src/slic3r/GUI/Notebook.cpp | 27 +++- src/slic3r/GUI/Notebook.hpp | 128 +++++++++++++++--- src/slic3r/GUI/Tabbook.hpp | 26 +--- src/slic3r/GUI/Widgets/Button.cpp | 20 +-- src/slic3r/GUI/Widgets/Button.hpp | 7 +- src/slic3r/plugin/host/PluginPages.cpp | 71 +++++++++- src/slic3r/plugin/host/PluginPages.hpp | 20 ++- .../pages/PagesPluginCapability.cpp | 1 + .../pages/PagesPluginCapability.hpp | 1 + .../pages/PagesPluginCapabilityTrampoline.hpp | 11 ++ 15 files changed, 260 insertions(+), 104 deletions(-) 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/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 01c698c62c..d6dbd4e596 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1018,8 +1018,8 @@ void MainFrame::update_layout() { 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"), std::string("tab_3d_active"), std::string("tab_3d_active"), false); - m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false); + m_tabpanel->InsertPage(prepare_pos, m_plater, _L("Prepare"), false, Notebook::PAGE_PREPARE); + m_tabpanel->InsertPage(prepare_pos + 1, m_plater, _L("Preview"), false, Notebook::PAGE_PREVIEW); } m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0); @@ -1315,7 +1315,7 @@ void MainFrame::init_tabpanel() { select_tab(TAB_ID_HOME); m_webview->load_url(url); }); - m_tabpanel->AddPage(TAB_ID_HOME, m_webview, "", "tab_home_active", "tab_home_active", false); + m_tabpanel->AddPage(m_webview, "", false, Notebook::PAGE_HOME); m_param_panel = new ParamsPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); } @@ -1330,7 +1330,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(TAB_ID_MONITOR, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active"), false); + m_tabpanel->AddPage(m_monitor, _L("Device"), false, Notebook::PAGE_MONITOR); m_printer_view = new PrinterWebView(m_tabpanel); Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent &evt) { @@ -1345,16 +1345,16 @@ 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(TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), std::string("tab_multi_active"), false); + m_tabpanel->AddPage(m_multi_machine, _L("Multi-device"), false, Notebook::PAGE_MULTI_DEVICE); } m_project = new ProjectPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_project->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(TAB_ID_PROJECT, m_project, _L("Project"), std::string("tab_auxiliary_active"), std::string("tab_auxiliary_active"), false); + m_tabpanel->AddPage(m_project, _L("Project"), false, Notebook::PAGE_PROJECT); m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false); + m_tabpanel->AddPage(m_calibration, _L("Calibration"), false, Notebook::PAGE_CALIBRATION); // 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. @@ -1397,7 +1397,7 @@ void MainFrame::show_device(bool bBBLPrinter) { { const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); const size_t monitor_pos = (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; - m_tabpanel->InsertPage(monitor_pos, TAB_ID_MONITOR, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active")); + m_tabpanel->InsertPage(monitor_pos, m_monitor, _L("Device"), false, Notebook::PAGE_MONITOR); } if (wxGetApp().is_enable_multi_machine()) { @@ -1410,8 +1410,7 @@ void MainFrame::show_device(bool bBBLPrinter) { { const int monitor_idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR); const size_t multi_pos = (monitor_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(monitor_idx) + 1; - m_tabpanel->InsertPage(multi_pos, TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), - std::string("tab_multi_active"), false); + m_tabpanel->InsertPage(multi_pos, m_multi_machine, _L("Multi-device"), false, Notebook::PAGE_MULTI_DEVICE); } } if (!m_calibration) { @@ -1422,8 +1421,7 @@ void MainFrame::show_device(bool bBBLPrinter) { // Calibration is always appended last (AddPage), so it lands after whichever of Monitor/Multi-device // actually got inserted above — no longer position-sensitive now that insertion position is computed // from FindPageByName rather than a fixed TabPosition index. - m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), std::string("tab_calibration_active"), - std::string("tab_calibration_active"), false); + m_tabpanel->AddPage(m_calibration, _L("Calibration"), false, Notebook::PAGE_CALIBRATION); #ifdef _MSW_DARK_MODE wxGetApp().UpdateDarkUIWin(this); @@ -1459,8 +1457,7 @@ void MainFrame::show_device(bool bBBLPrinter) { { const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); const size_t monitor_pos = (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; - m_tabpanel->InsertPage(monitor_pos, TAB_ID_MONITOR, m_printer_view, _L("Device"), std::string("tab_monitor_active"), - std::string("tab_monitor_active")); + m_tabpanel->InsertPage(monitor_pos, m_printer_view, _L("Device"), false, Notebook::PAGE_MONITOR); } } fit_tab_labels(); // ORCA on printer change diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 068bcf7e6d..4c26268a21 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, _CTX(L_CONTEXT("Update", "Firmware"), "Firmware"), "", false); + m_tabpanel->AddPage(m_upgrade_panel, _CTX(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()) { 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 4c6430f99c..847996aba9 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -132,7 +132,6 @@ 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); } @@ -152,17 +151,20 @@ 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 /* = ""*/, int imageId /* = wxBookCtrlBase::NO_IMAGE */) { Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER); btn->SetCornerRadius(0); + if (bmp_name.empty() && m_imageList != nullptr && imageId != wxBookCtrlBase::NO_IMAGE && imageId >= 0 && + imageId < m_imageList->GetImageCount()) + btn->SetIcon(m_imageList->GetBitmap(imageId)); + int em = em_unit(this); //BBS set size for button btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10}); @@ -175,8 +177,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(); @@ -232,6 +232,23 @@ bool ButtonsListCtrl::SetPageImage(size_t n, const std::string& bmp_name) const return true; } +bool ButtonsListCtrl::SetPageImage(size_t n, int imageId) +{ + if (n >= m_pageButtons.size()) + return false; + + if (imageId == wxBookCtrlBase::NO_IMAGE) { + m_pageButtons[n]->SetIcon(wxBitmap()); + return true; + } + + if (m_imageList == nullptr || imageId < 0 || imageId >= m_imageList->GetImageCount()) + return false; + + m_pageButtons[n]->SetIcon(m_imageList->GetBitmap(imageId)); + return true; +} + void ButtonsListCtrl::SetPageText(size_t n, const wxString& strText) { Button* btn = m_pageButtons[n]; diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index a82be7be56..859ada37a5 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -3,8 +3,11 @@ //#ifdef _WIN32 +#include #include +#include #include +#include #include class ScalableButton; @@ -24,9 +27,11 @@ 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 = "", int imageId = wxBookCtrlBase::NO_IMAGE); void RemovePage(size_t n); bool SetPageImage(size_t n, const std::string& bmp_name) const; + bool SetPageImage(size_t n, int imageId); + void SetImageList(wxImageList* imageList) { m_imageList = imageList; } void SetPageText(size_t n, const wxString& strText); void SetCompact(size_t n, bool compact); // ORCA wxString GetPageText(size_t n) const; @@ -41,11 +46,22 @@ private: int m_btn_margin; int m_line_margin; std::vector m_pageLabels; // ORCA + wxImageList* m_imageList{nullptr}; }; class Notebook : public wxBookCtrlBase { public: + // Negative values below wxBookCtrlBase::NO_IMAGE are reserved for the built-in + // tabs. Nonnegative values are wxImageList indices supplied by plugin pages. + static constexpr int PAGE_HOME = -2; + static constexpr int PAGE_PREPARE = -3; + static constexpr int PAGE_PREVIEW = -4; + static constexpr int PAGE_MONITOR = -5; + static constexpr int PAGE_MULTI_DEVICE = -6; + static constexpr int PAGE_PROJECT = -7; + static constexpr int PAGE_CALIBRATION = -8; + Notebook(wxWindow * parent, wxWindowID winid = wxID_ANY, const wxPoint & pos = wxDefaultPosition, @@ -104,7 +120,7 @@ public: // by this control) and show it immediately. bool ShowNewPage(wxWindow * page) { - return AddPage(wxString(), page, wxString(), "", ""); + return AddPage(page, wxString(), false, NO_IMAGE); } @@ -136,16 +152,10 @@ public: // Implement base class pure virtual methods. - // adds a new page to the control - bool AddPage(const wxString& id, - wxWindow* page, - const wxString& text, - const std::string& bmp_name, - const std::string& inactive_bmp_name, - bool bSelect = false) + bool AddPage(wxWindow* page, const wxString& text, bool bSelect = false, int imageId = NO_IMAGE) override { DoInvalidateBestSize(); - return InsertPage(GetPageCount(), id, page, text, bmp_name, inactive_bmp_name, bSelect); + return InsertPage(GetPageCount(), page, text, bSelect, imageId); } // Page management @@ -154,12 +164,38 @@ public: const wxString & text, bool bSelect = false, int imageId = NO_IMAGE) override + { + wxString page_name; + std::string bmp_name; + const bool is_fixed_page = get_fixed_page_info(imageId, page_name, bmp_name); + const int stored_image_id = is_fixed_page ? NO_IMAGE : imageId; + + if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, stored_image_id)) + return false; + + m_pageNames.insert(m_pageNames.begin() + n, page_name); + m_pageImageIds.insert(m_pageImageIds.begin() + n, stored_image_id); + GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, stored_image_id); + + if (!DoSetSelectionAfterInsertion(n, bSelect)) + page->Hide(); + + return true; + } + + bool InsertPage(size_t n, + const wxString& id, + wxWindow* page, + const wxString& text, + int imageId, + bool bSelect = false) { if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId)) return false; - m_pageNames.insert(m_pageNames.begin() + n, wxString()); - GetBtnsListCtrl()->InsertPage(n, text, bSelect); + m_pageNames.insert(m_pageNames.begin() + n, id); + m_pageImageIds.insert(m_pageImageIds.begin() + n, imageId); + GetBtnsListCtrl()->InsertPage(n, text, bSelect, "", imageId); if (!DoSetSelectionAfterInsertion(n, bSelect)) page->Hide(); @@ -172,14 +208,14 @@ public: wxWindow * page, const wxString & text, const std::string& bmp_name = "", - const std::string& inactive_bmp_name = "", bool bSelect = false) { if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) return false; m_pageNames.insert(m_pageNames.begin() + n, id); - GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, inactive_bmp_name); + m_pageImageIds.insert(m_pageImageIds.begin() + n, NO_IMAGE); + GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name); // wxBookCtrlBase::InsertPage() only inserts into the page list and sizes the // new page to the current page's rect — it never touches visibility. A freshly @@ -222,8 +258,7 @@ 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; page images use the wx image-list IDs below. virtual bool SetPageText(size_t n, const wxString & strText) override { wxCHECK_MSG(n < GetPageCount(), false, wxS("Invalid page")); @@ -239,14 +274,27 @@ public: return GetBtnsListCtrl()->GetPageText(n); } - virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override + virtual bool SetPageImage(size_t n, int imageId) override { - return false; + if (n >= m_pageImageIds.size()) + return false; + + if (!GetBtnsListCtrl()->SetPageImage(n, imageId)) + return false; + + m_pageImageIds[n] = imageId; + return true; } - virtual int GetPageImage(size_t WXUNUSED(n)) const override + virtual int GetPageImage(size_t n) const override { - return NO_IMAGE; + return n < m_pageImageIds.size() ? m_pageImageIds[n] : NO_IMAGE; + } + + void SetImageList(wxImageList* imageList) + { + m_imageList = imageList; + GetBtnsListCtrl()->SetImageList(imageList); } bool SetPageImage(size_t n, const std::string& bmp_name) @@ -271,6 +319,7 @@ public: virtual bool DeleteAllPages() override { m_pageNames.clear(); + m_pageImageIds.clear(); return wxBookCtrlBase::DeleteAllPages(); } @@ -431,6 +480,7 @@ protected: if (win) { m_pageNames.erase(m_pageNames.begin() + page); + m_pageImageIds.erase(m_pageImageIds.begin() + page); GetBtnsListCtrl()->RemovePage(page); DoSetSelectionAfterRemoval(page); } @@ -454,9 +504,47 @@ protected: } private: + static bool get_fixed_page_info(int imageId, wxString& page_name, std::string& bmp_name) + { + switch (imageId) { + case PAGE_HOME: + page_name = wxS("home"); + bmp_name = "tab_home_active"; + return true; + case PAGE_PREPARE: + page_name = wxS("prepare"); + bmp_name = "tab_3d_active"; + return true; + case PAGE_PREVIEW: + page_name = wxS("preview"); + bmp_name = "tab_preview_active"; + return true; + case PAGE_MONITOR: + page_name = wxS("monitor"); + bmp_name = "tab_monitor_active"; + return true; + case PAGE_MULTI_DEVICE: + page_name = wxS("multi_device"); + bmp_name = "tab_multi_active"; + return true; + case PAGE_PROJECT: + page_name = wxS("project"); + bmp_name = "tab_auxiliary_active"; + return true; + case PAGE_CALIBRATION: + page_name = wxS("calibration"); + bmp_name = "tab_calibration_active"; + return true; + default: + return false; + } + } + void Init(); std::vector m_pageNames; // index-parallel to wxBookCtrlBase::m_pages + std::vector m_pageImageIds; // index-parallel to wxBookCtrlBase::m_pages + wxImageList* m_imageList{nullptr}; wxShowEffect m_showEffect, m_hideEffect; 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..1a8cbefbce 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(); } @@ -260,9 +257,6 @@ void Button::Rescale() if (this->active_icon.bmp().IsOk()) this->active_icon.msw_rescale(); - if (this->inactive_icon.bmp().IsOk()) - this->inactive_icon.msw_rescale(); - messureSize(); if(m_has_style) @@ -293,11 +287,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; + 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 bc093b512a..fdc511ec7c 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -34,12 +34,10 @@ class Button : public StaticBox wxSize minSize; // set by outer wxSize paddingSize; ScalableBitmap active_icon; - ScalableBitmap inactive_icon; StateColor text_color; bool pressedDown = false; - bool m_selected = true; bool canFocus = true; bool isCenter = true; bool vertical = false; @@ -61,8 +59,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; @@ -75,8 +72,6 @@ public: void SetTextColorNormal(wxColor const &color); - void SetSelected(bool selected = true) { m_selected = selected; } - bool Enable(bool enable = true) override; void EnableTooltipEvenDisabled();// The tip will be shown even if the button is disabled diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp index 3e7d534810..3b9b1498ee 100644 --- a/src/slic3r/plugin/host/PluginPages.cpp +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -14,6 +14,8 @@ #include #include +#include +#include #include #include @@ -204,6 +206,11 @@ void PluginPages::initialize(Notebook* parent) if (m_parent == nullptr) return; + // Keep image-list indices stable for the lifetime of this notebook. Removing an image + // would shift every later index, so deregistration only removes the page. + m_image_list = std::make_unique(20, 20, true, 0); + m_parent->SetImageList(m_image_list.get()); + for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) { if (capability) on_cap_register(capability->identity()); @@ -214,6 +221,9 @@ void PluginPages::shutdown() { while (!m_pages.empty()) remove_page(m_pages.begin()->first); + if (m_parent != nullptr) + m_parent->SetImageList(nullptr); + m_image_list.reset(); m_parent = nullptr; } @@ -235,6 +245,15 @@ void PluginPages::on_cap_register(const PluginCapabilityId& id) if (!capability) return; + 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(); @@ -242,8 +261,29 @@ void PluginPages::on_cap_register(const PluginCapabilityId& id) } const wxString title = wxString::FromUTF8(id.name); - const wxString page_id = wxString::FromUTF8("plugin." + id.plugin_key + "." + id.name); - if (!m_parent->AddPage(page_id, page, title, "tab_auxiliary_active", "tab_auxiliary_active", false)) { + + int image_id = wxBookCtrlBase::NO_IMAGE; + if (!icon.empty() && m_image_list) { + try { + boost::filesystem::path icon_path(icon); + const std::string extension = icon_path.extension().string(); + if (extension == ".svg" || extension == ".png") + icon_path.replace_extension(); + + const wxBitmap bitmap = create_scaled_bitmap(icon_path.string(), m_parent, 20); + if (bitmap.IsOk()) + image_id = m_image_list->Add(bitmap); + } 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; + } + } + + page->set_icon_image_id(image_id); + if (!m_parent->AddPage(page, title, false, image_id)) { + if (image_id != wxBookCtrlBase::NO_IMAGE && m_image_list && image_id == m_image_list->GetImageCount() - 1) + m_image_list->Remove(image_id); page->Destroy(); return; } @@ -285,12 +325,39 @@ void PluginPages::remove_page(const PluginCapabilityId& id) return; PluginPage* page = it->second; + const int removed_image_id = page->get_icon_image_id(); page->detach_capability(); if (m_parent != nullptr) { const int index = m_parent->FindPage(page); if (index != wxNOT_FOUND) m_parent->RemovePage(static_cast(index)); } + + if (m_image_list && removed_image_id != wxBookCtrlBase::NO_IMAGE && + removed_image_id >= 0 && removed_image_id < m_image_list->GetImageCount()) { + m_image_list->Remove(removed_image_id); + + // wxImageList IDs are positional. Removing one shifts all later images down by + // one, so update both the page state and the notebook button for those pages. + for (const auto& [other_id, other_page] : m_pages) { + if (other_id == id) + continue; + + const int other_image_id = other_page->get_icon_image_id(); + if (other_image_id <= removed_image_id) + continue; + + const int updated_image_id = other_image_id - 1; + other_page->set_icon_image_id(updated_image_id); + + if (m_parent != nullptr) { + const int other_index = m_parent->FindPage(other_page); + if (other_index != wxNOT_FOUND) + m_parent->SetPageImage(static_cast(other_index), updated_image_id); + } + } + } + page->Destroy(); m_pages.erase(it); } diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp index bf70305201..d925f2eb32 100644 --- a/src/slic3r/plugin/host/PluginPages.hpp +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include @@ -29,16 +31,20 @@ public: void on_new_window(wxWebViewEvent& event); void on_script_message(wxWebViewEvent& event); void push_message(const std::string& message); + void set_icon_image_id(int id) { m_icon_image_id = id; } + int get_icon_image_id() const { return m_icon_image_id; } 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}; + wxWebView* m_browser{nullptr}; + std::shared_ptr m_cap; + std::shared_ptr> m_lifetime; + bool m_content_loaded{false}; + + int m_icon_image_id = wxBookCtrlBase::NO_IMAGE; }; class PluginPages @@ -47,7 +53,7 @@ public: PluginPages() = default; ~PluginPages(); - PluginPages(const PluginPages&) = delete; + PluginPages(const PluginPages&) = delete; PluginPages& operator=(const PluginPages&) = delete; void initialize(Notebook* parent); @@ -63,7 +69,9 @@ private: void remove_page(const PluginCapabilityId& id); std::map m_pages; - Notebook* m_parent{nullptr}; + Notebook* m_parent{nullptr}; + + std::unique_ptr m_image_list; }; } // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp index fc5d4a03b8..e009f4426f 100644 --- a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp @@ -23,6 +23,7 @@ void PagesPluginCapability::RegisterBindings(pybind11::module_& module) .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", diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp index e833d7dbf4..978492006c 100644 --- a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp @@ -18,6 +18,7 @@ public: 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); diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp index 71eb5c5bcc..3fb476c228 100644 --- a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp @@ -13,6 +13,17 @@ class PyPagesPluginCapabilityTrampoline : public 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( From 14b05a4d8e73982af774a2a3d74104fb89f9b9ab Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 30 Jul 2026 01:54:46 +0800 Subject: [PATCH 04/14] fix: regression after merge --- src/slic3r/GUI/Monitor.cpp | 2 +- src/slic3r/GUI/Widgets/Button.hpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 2a057fd1e3..51bd7ed878 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -193,7 +193,7 @@ void MonitorPanel::init_tabpanel() //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); diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index a0778040ca..c98d583c34 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -38,6 +38,7 @@ class Button : public StaticBox StateColor text_color; bool pressedDown = false; + bool m_selected = true; bool canFocus = true; bool isCenter = true; bool vertical = false; From ef4815b26c1bd0a850ee883114445599b7e7c991 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Thu, 6 Aug 2026 06:08:02 +0800 Subject: [PATCH 05/14] fix: crash on windows --- src/slic3r/GUI/MainFrame.cpp | 2 ++ src/slic3r/GUI/Project.cpp | 61 ++++++++++++++++++++++++++++++------ src/slic3r/GUI/Project.hpp | 10 ++++-- src/slic3r/GUI/Tab.cpp | 8 +++-- 4 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 9566426c53..5dc1f0ccfc 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1110,6 +1110,8 @@ 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. 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/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 05631c8afa..469864af46 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -8477,8 +8477,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(); } From 169189498e97531fde24cc8cf73d19922a6003da Mon Sep 17 00:00:00 2001 From: peachismomo Date: Thu, 6 Aug 2026 07:56:37 +0800 Subject: [PATCH 06/14] fix regression after merge --- src/slic3r/GUI/MainFrame.cpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index c3b5728a53..26c03975dc 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1385,7 +1385,7 @@ void MainFrame::show_device(bool should_use_native) { // The legacy page is appended when printer agents are enabled. Remove that // extra page before switching back to the normal native/legacy layout. if (!use_printer_agents) { - if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != tpMonitor) { + if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != m_tabpanel->FindPageByName(TAB_ID_MONITOR)) { m_printer_view->Show(false); m_tabpanel->RemovePage(idx); } @@ -1403,8 +1403,10 @@ 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")); + const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); + const size_t monitor_pos = + (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; + m_tabpanel->InsertPage(monitor_pos, TAB_ID_MONITOR, m_monitor, _L("Device"), "tab_monitor_active", false); } if (m_printer_view == nullptr) { @@ -1425,8 +1427,11 @@ 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); + const int monitor_idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR); + const size_t multi_pos = + (monitor_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(monitor_idx) + 1; + m_tabpanel->InsertPage(multi_pos, TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), + "tab_multi_active", false); } } if (!m_calibration) { @@ -1437,14 +1442,12 @@ void MainFrame::show_device(bool should_use_native) { // 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->AddPage(m_calibration, _L("Calibration"), false, Notebook::PAGE_CALIBRATION); } if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { m_printer_view->Show(false); - m_tabpanel->AddPage(m_printer_view, _L("Device (legacy)"), std::string("tab_monitor_active"), - std::string("tab_monitor_active"), false); + m_tabpanel->AddPage(m_printer_view, _L("Device (legacy)"), false, Notebook::PAGE_MONITOR); } else { m_tabpanel->SetPageText(idx, _L("Device (legacy)")); } From 54fe28ab0885e78a6362b3b8c22738d1160c53e8 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 11 Aug 2026 17:50:20 +0800 Subject: [PATCH 07/14] feat: add max visible pages to app config under Preferences -> General -> Plugins --- src/libslic3r/AppConfig.cpp | 36 ++++ src/libslic3r/AppConfig.hpp | 10 + src/slic3r/GUI/Preferences.cpp | 17 +- src/slic3r/plugin/host/PluginPages.cpp | 257 +++++++++++++++++++++---- src/slic3r/plugin/host/PluginPages.hpp | 15 ++ 5 files changed, 294 insertions(+), 41 deletions(-) diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index a5d0e24eac..970faeb620 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -280,6 +280,20 @@ void AppConfig::set_defaults() set(SETTING_OPENGL_FPS_CAP, std::to_string(fps_cap)); } + if (get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT).empty()) + set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT)); + else { + int visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + try { + visible_count = std::stoi(get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT)); + } + catch (...) { + visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + } + visible_count = std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); + set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(visible_count)); + } + if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty()) set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false); @@ -1630,6 +1644,28 @@ 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::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); +} + +void AppConfig::set_plugin_pages_visible_count(int count) +{ + count = std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); + set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(count)); +} + 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..de5c3a442f 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,11 @@ 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; + void set_plugin_pages_visible_count(int count); + 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/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/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp index 3b9b1498ee..1a40fd8984 100644 --- a/src/slic3r/plugin/host/PluginPages.cpp +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -1,5 +1,6 @@ #include "PluginPages.hpp" +#include "libslic3r/AppConfig.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/Notebook.hpp" #include "slic3r/GUI/GUI_App.hpp" @@ -10,12 +11,15 @@ #include +#include + #include #include #include #include #include +#include #include #include @@ -194,9 +198,119 @@ void PluginPage::push_message(const std::string& message) WebView::RunScript(m_browser, script); } +class PluginPagesOverflowPanel : public wxPanel +{ +public: + explicit PluginPagesOverflowPanel(Notebook* parent) + : wxPanel(parent, wxID_ANY) + , m_notebook(parent) + { + auto* sizer = new wxBoxSizer(wxVERTICAL); + + m_choice = new wxChoice(this, wxID_ANY); + m_choice->Bind(wxEVT_CHOICE, &PluginPagesOverflowPanel::on_choice, this); + sizer->Add(m_choice, wxSizerFlags().Expand().Border(wxALL, FromDIP(4))); + + m_content_sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(m_content_sizer, wxSizerFlags().Expand().Proportion(1)); + + SetSizer(sizer); + } + + void add_entry(const PluginCapabilityId& id, PluginPage* page, const wxString& title) + { + page->Reparent(this); + page->Hide(); + m_entries.push_back({id, page, title}); + m_choice->Append(title); + if (m_entries.size() == 1) + select_index(0); + } + + void select_entry(const PluginCapabilityId& id) + { + for (size_t i = 0; i < m_entries.size(); ++i) { + if (m_entries[i].id == id) { + select_index(i); + return; + } + } + } + + void clear() + { + if (m_shown_index != wxNOT_FOUND) + m_entries[static_cast(m_shown_index)].page->Hide(); + m_content_sizer->Clear(false); + + for (const Entry& entry : m_entries) + entry.page->Reparent(m_notebook); + + m_entries.clear(); + m_choice->Clear(); + m_shown_index = wxNOT_FOUND; + } + + wxString current_title() const { return m_shown_index == wxNOT_FOUND ? wxString() : m_entries[static_cast(m_shown_index)].title; } + int current_image_id() const + { + return m_shown_index == wxNOT_FOUND ? wxBookCtrlBase::NO_IMAGE : m_entries[static_cast(m_shown_index)].page->get_icon_image_id(); + } + +private: + struct Entry + { + PluginCapabilityId id; + PluginPage* page; + wxString title; + }; + + void on_choice(wxCommandEvent&) + { + const int selection = m_choice->GetSelection(); + if (selection != wxNOT_FOUND) + select_index(static_cast(selection)); + } + + void select_index(size_t index) + { + if (index >= m_entries.size()) + return; + + if (m_shown_index != wxNOT_FOUND) + m_entries[static_cast(m_shown_index)].page->Hide(); + + m_content_sizer->Clear(false); + m_content_sizer->Add(m_entries[index].page, wxSizerFlags().Expand().Proportion(1)); + m_entries[index].page->Show(); + Layout(); + + m_shown_index = static_cast(index); + m_choice->SetSelection(static_cast(index)); + + const int tab_index = m_notebook->FindPage(this); + if (tab_index != wxNOT_FOUND) { + m_notebook->SetPageText(static_cast(tab_index), m_entries[index].title); + m_notebook->SetPageImage(static_cast(tab_index), m_entries[index].page->get_icon_image_id()); + } + } + + Notebook* m_notebook{nullptr}; + wxChoice* m_choice{nullptr}; + wxBoxSizer* m_content_sizer{nullptr}; + std::vector m_entries; + int m_shown_index{wxNOT_FOUND}; +}; + PluginPages::~PluginPages() { shutdown(); + // try { + // } catch (const std::exception& error) { + // BOOST_LOG_TRIVIAL(error) << "PluginPages::~PluginPages: shutdown() threw: " << error.what(); + // } catch (...) { + // BOOST_LOG_TRIVIAL(error) << "PluginPages::~PluginPages: shutdown() threw a non-standard exception"; + // } } void PluginPages::initialize(Notebook* parent) @@ -206,15 +320,17 @@ void PluginPages::initialize(Notebook* parent) if (m_parent == nullptr) return; - // Keep image-list indices stable for the lifetime of this notebook. Removing an image - // would shift every later index, so deregistration only removes the page. + m_visible_page_count = GUI::wxGetApp().app_config->get_plugin_pages_visible_count(); + m_notebook_base_index = static_cast(m_parent->GetPageCount()); + m_image_list = std::make_unique(20, 20, true, 0); m_parent->SetImageList(m_image_list.get()); for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) { if (capability) - on_cap_register(capability->identity()); + create_page(capability->identity()); } + relayout(); } void PluginPages::shutdown() @@ -225,6 +341,17 @@ void PluginPages::shutdown() m_parent->SetImageList(nullptr); m_image_list.reset(); m_parent = nullptr; + m_notebook_base_index = 0; +} + +void PluginPages::set_visible_page_count(int count) +{ + const int clamped = std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(count, 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 @@ -236,14 +363,14 @@ std::shared_ptr PluginPages::get_pages_cap(const PluginCa return std::dynamic_pointer_cast(capability); } -void PluginPages::on_cap_register(const PluginCapabilityId& id) +bool PluginPages::create_page(const PluginCapabilityId& id) { - if (m_parent == nullptr || m_pages.find(id) != m_pages.end()) - return; + if (m_pages.find(id) != m_pages.end()) + return false; auto capability = get_pages_cap(id, true); if (!capability) - return; + return false; std::string icon; try { @@ -257,11 +384,9 @@ void PluginPages::on_cap_register(const PluginCapabilityId& id) auto* page = new PluginPage(m_parent, std::move(capability)); if (!page->is_valid()) { page->Destroy(); - return; + return false; } - const wxString title = wxString::FromUTF8(id.name); - int image_id = wxBookCtrlBase::NO_IMAGE; if (!icon.empty() && m_image_list) { try { @@ -281,14 +406,18 @@ void PluginPages::on_cap_register(const PluginCapabilityId& id) } page->set_icon_image_id(image_id); - if (!m_parent->AddPage(page, title, false, image_id)) { - if (image_id != wxBookCtrlBase::NO_IMAGE && m_image_list && image_id == m_image_list->GetImageCount() - 1) - m_image_list->Remove(image_id); - page->Destroy(); - return; - } - 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) @@ -327,39 +456,87 @@ void PluginPages::remove_page(const PluginCapabilityId& id) PluginPage* page = it->second; const int removed_image_id = page->get_icon_image_id(); page->detach_capability(); - if (m_parent != nullptr) { - const int index = m_parent->FindPage(page); - if (index != wxNOT_FOUND) - m_parent->RemovePage(static_cast(index)); - } + + m_pages.erase(it); + m_order.erase(std::remove(m_order.begin(), m_order.end(), id), m_order.end()); if (m_image_list && removed_image_id != wxBookCtrlBase::NO_IMAGE && removed_image_id >= 0 && removed_image_id < m_image_list->GetImageCount()) { m_image_list->Remove(removed_image_id); - // wxImageList IDs are positional. Removing one shifts all later images down by - // one, so update both the page state and the notebook button for those pages. - for (const auto& [other_id, other_page] : m_pages) { - if (other_id == id) - continue; - + // wxImageList IDs are positional. Removing one shifts all later images down by one. + for (auto& [other_id, other_page] : m_pages) { const int other_image_id = other_page->get_icon_image_id(); - if (other_image_id <= removed_image_id) - continue; - - const int updated_image_id = other_image_id - 1; - other_page->set_icon_image_id(updated_image_id); - - if (m_parent != nullptr) { - const int other_index = m_parent->FindPage(other_page); - if (other_index != wxNOT_FOUND) - m_parent->SetPageImage(static_cast(other_index), updated_image_id); - } + if (other_image_id > removed_image_id) + other_page->set_icon_image_id(other_image_id - 1); } } + relayout(); page->Destroy(); - m_pages.erase(it); +} + +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()); + + wxString id_to_reselect = m_parent->GetSelectedPageName(); + + while (m_parent->GetPageCount() > m_notebook_base_index) + m_parent->RemovePage(m_parent->GetPageCount() - 1); + if (m_overflow_panel != nullptr) + m_overflow_panel->clear(); + + const int visible_slots = std::max(1, m_visible_page_count); + const bool need_overflow = static_cast(m_order.size()) > visible_slots; + const size_t individual_count = need_overflow ? static_cast(visible_slots - 1) : m_order.size(); + + for (size_t i = 0; i < individual_count; ++i) { + const PluginCapabilityId& id = m_order[i]; + PluginPage* page = m_pages.at(id); + m_parent->InsertPage(m_parent->GetPageCount(), page_tab_id(id), page, wxString::FromUTF8(id.name), page->get_icon_image_id()); + } + + if (need_overflow) { + if (m_overflow_panel == nullptr) + m_overflow_panel = new PluginPagesOverflowPanel(m_parent); + + bool reselecting_overflow_entry = false; + for (size_t i = individual_count; i < m_order.size(); ++i) { + const PluginCapabilityId& id = m_order[i]; + m_overflow_panel->add_entry(id, m_pages.at(id), wxString::FromUTF8(id.name)); + if (page_tab_id(id) == id_to_reselect) { + m_overflow_panel->select_entry(id); + reselecting_overflow_entry = true; + } + } + if (reselecting_overflow_entry) + id_to_reselect = "plugin.__overflow__"; + + m_parent->InsertPage(m_parent->GetPageCount(), "plugin.__overflow__", m_overflow_panel, + m_overflow_panel->current_title(), m_overflow_panel->current_image_id()); + } else if (m_overflow_panel != nullptr) { + m_overflow_panel->Destroy(); + m_overflow_panel = nullptr; + } + + if (!id_to_reselect.empty()) + m_parent->SelectPageByName(id_to_reselect); } } // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp index d925f2eb32..99213b425b 100644 --- a/src/slic3r/plugin/host/PluginPages.hpp +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -47,6 +48,8 @@ private: int m_icon_image_id = wxBookCtrlBase::NO_IMAGE; }; +class PluginPagesOverflowPanel; + class PluginPages { public: @@ -64,14 +67,26 @@ public: void on_plugin_register(const std::string& plugin_key); void on_plugin_deregister(const std::string& plugin_key); + int get_visible_page_count() const { return m_visible_page_count; } + void set_visible_page_count(int count); + 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 relayout(); + static wxString page_tab_id(const PluginCapabilityId& id); + std::map m_pages; + std::vector m_order; Notebook* m_parent{nullptr}; + size_t m_notebook_base_index{0}; std::unique_ptr m_image_list; + int m_visible_page_count{0}; + + PluginPagesOverflowPanel* m_overflow_panel{nullptr}; }; } // namespace Slic3r From 7be7f075516ef7fbe4b362b12399b918c86ade0a Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 11 Aug 2026 17:50:49 +0800 Subject: [PATCH 08/14] feat: UI for dropdown to select which plugin to show past max visible pages --- src/slic3r/GUI/Notebook.cpp | 18 +++ src/slic3r/GUI/Notebook.hpp | 5 + src/slic3r/plugin/host/PluginPages.cpp | 189 ++++++++----------------- src/slic3r/plugin/host/PluginPages.hpp | 7 +- 4 files changed, 87 insertions(+), 132 deletions(-) diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index 847996aba9..f9db689faa 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -272,6 +272,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() diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index 859ada37a5..da90535481 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -36,6 +36,9 @@ public: 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; @@ -47,6 +50,7 @@ private: int m_line_margin; std::vector m_pageLabels; // ORCA wxImageList* m_imageList{nullptr}; + wxWindow* m_overflow_button{nullptr}; // ORCA }; class Notebook : public wxBookCtrlBase @@ -324,6 +328,7 @@ public: } ButtonsListCtrl* GetBtnsListCtrl() const { return static_cast(m_bookctrl); } + void SetOverflowButton(wxWindow* button) { GetBtnsListCtrl()->SetOverflowButton(button); } int FindPageByName(const wxString& id) const { diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp index 1a40fd8984..551e39877d 100644 --- a/src/slic3r/plugin/host/PluginPages.cpp +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -4,6 +4,7 @@ #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" @@ -19,7 +20,7 @@ #include #include -#include +#include #include #include @@ -198,110 +199,6 @@ void PluginPage::push_message(const std::string& message) WebView::RunScript(m_browser, script); } -class PluginPagesOverflowPanel : public wxPanel -{ -public: - explicit PluginPagesOverflowPanel(Notebook* parent) - : wxPanel(parent, wxID_ANY) - , m_notebook(parent) - { - auto* sizer = new wxBoxSizer(wxVERTICAL); - - m_choice = new wxChoice(this, wxID_ANY); - m_choice->Bind(wxEVT_CHOICE, &PluginPagesOverflowPanel::on_choice, this); - sizer->Add(m_choice, wxSizerFlags().Expand().Border(wxALL, FromDIP(4))); - - m_content_sizer = new wxBoxSizer(wxVERTICAL); - sizer->Add(m_content_sizer, wxSizerFlags().Expand().Proportion(1)); - - SetSizer(sizer); - } - - void add_entry(const PluginCapabilityId& id, PluginPage* page, const wxString& title) - { - page->Reparent(this); - page->Hide(); - m_entries.push_back({id, page, title}); - m_choice->Append(title); - if (m_entries.size() == 1) - select_index(0); - } - - void select_entry(const PluginCapabilityId& id) - { - for (size_t i = 0; i < m_entries.size(); ++i) { - if (m_entries[i].id == id) { - select_index(i); - return; - } - } - } - - void clear() - { - if (m_shown_index != wxNOT_FOUND) - m_entries[static_cast(m_shown_index)].page->Hide(); - m_content_sizer->Clear(false); - - for (const Entry& entry : m_entries) - entry.page->Reparent(m_notebook); - - m_entries.clear(); - m_choice->Clear(); - m_shown_index = wxNOT_FOUND; - } - - wxString current_title() const { return m_shown_index == wxNOT_FOUND ? wxString() : m_entries[static_cast(m_shown_index)].title; } - int current_image_id() const - { - return m_shown_index == wxNOT_FOUND ? wxBookCtrlBase::NO_IMAGE : m_entries[static_cast(m_shown_index)].page->get_icon_image_id(); - } - -private: - struct Entry - { - PluginCapabilityId id; - PluginPage* page; - wxString title; - }; - - void on_choice(wxCommandEvent&) - { - const int selection = m_choice->GetSelection(); - if (selection != wxNOT_FOUND) - select_index(static_cast(selection)); - } - - void select_index(size_t index) - { - if (index >= m_entries.size()) - return; - - if (m_shown_index != wxNOT_FOUND) - m_entries[static_cast(m_shown_index)].page->Hide(); - - m_content_sizer->Clear(false); - m_content_sizer->Add(m_entries[index].page, wxSizerFlags().Expand().Proportion(1)); - m_entries[index].page->Show(); - Layout(); - - m_shown_index = static_cast(index); - m_choice->SetSelection(static_cast(index)); - - const int tab_index = m_notebook->FindPage(this); - if (tab_index != wxNOT_FOUND) { - m_notebook->SetPageText(static_cast(tab_index), m_entries[index].title); - m_notebook->SetPageImage(static_cast(tab_index), m_entries[index].page->get_icon_image_id()); - } - } - - Notebook* m_notebook{nullptr}; - wxChoice* m_choice{nullptr}; - wxBoxSizer* m_content_sizer{nullptr}; - std::vector m_entries; - int m_shown_index{wxNOT_FOUND}; -}; - PluginPages::~PluginPages() { shutdown(); @@ -499,44 +396,78 @@ void PluginPages::relayout() while (m_parent->GetPageCount() > m_notebook_base_index) m_parent->RemovePage(m_parent->GetPageCount() - 1); - if (m_overflow_panel != nullptr) - m_overflow_panel->clear(); const int visible_slots = std::max(1, m_visible_page_count); const bool need_overflow = static_cast(m_order.size()) > visible_slots; - const size_t individual_count = need_overflow ? static_cast(visible_slots - 1) : m_order.size(); - for (size_t i = 0; i < individual_count; ++i) { - const PluginCapabilityId& id = m_order[i]; + // 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); + } + + 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), page->get_icon_image_id()); } if (need_overflow) { - if (m_overflow_panel == nullptr) - m_overflow_panel = new PluginPagesOverflowPanel(m_parent); - - bool reselecting_overflow_entry = false; - for (size_t i = individual_count; i < m_order.size(); ++i) { - const PluginCapabilityId& id = m_order[i]; - m_overflow_panel->add_entry(id, m_pages.at(id), wxString::FromUTF8(id.name)); - if (page_tab_id(id) == id_to_reselect) { - m_overflow_panel->select_entry(id); - reselecting_overflow_entry = true; - } + 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; } - if (reselecting_overflow_entry) - id_to_reselect = "plugin.__overflow__"; - - m_parent->InsertPage(m_parent->GetPageCount(), "plugin.__overflow__", m_overflow_panel, - m_overflow_panel->current_title(), m_overflow_panel->current_image_id()); - } else if (m_overflow_panel != nullptr) { - m_overflow_panel->Destroy(); - m_overflow_panel = nullptr; + m_parent->SetOverflowButton(m_overflow_button); + } else if (m_overflow_button != nullptr) { + m_parent->SetOverflowButton(nullptr); + m_overflow_button->Destroy(); + m_overflow_button = nullptr; } if (!id_to_reselect.empty()) m_parent->SelectPageByName(id_to_reselect); } +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 index 99213b425b..cc54332dbe 100644 --- a/src/slic3r/plugin/host/PluginPages.hpp +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -48,8 +49,6 @@ private: int m_icon_image_id = wxBookCtrlBase::NO_IMAGE; }; -class PluginPagesOverflowPanel; - class PluginPages { public: @@ -76,6 +75,7 @@ private: void remove_page(const PluginCapabilityId& id); void relayout(); + void show_overflow_menu(); static wxString page_tab_id(const PluginCapabilityId& id); std::map m_pages; @@ -86,7 +86,8 @@ private: std::unique_ptr m_image_list; int m_visible_page_count{0}; - PluginPagesOverflowPanel* m_overflow_panel{nullptr}; + std::optional m_swapped_in_id; + wxWindow* m_overflow_button{nullptr}; }; } // namespace Slic3r From 50dfcff0314154c635bf973f8888e18a533e4646 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 13 Aug 2026 14:54:46 +0800 Subject: [PATCH 09/14] fix: plugin pages removing calibration tab --- src/slic3r/GUI/MainFrame.cpp | 2 ++ src/slic3r/plugin/host/PluginPages.cpp | 13 +++++++++---- src/slic3r/plugin/host/PluginPages.hpp | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 26c03975dc..52da60a4a8 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1457,6 +1457,7 @@ void MainFrame::show_device(bool should_use_native) { #endif // _MSW_DARK_MODE fit_tab_labels(); // ORCA on printer change + m_plugin_pages.relayout(); // keep plugin tabs after the native tabs just mutated above return; } @@ -1545,6 +1546,7 @@ void MainFrame::show_device(bool should_use_native) { } } fit_tab_labels(); // ORCA on printer change + m_plugin_pages.relayout(); // keep plugin tabs after the native tabs just mutated above } void MainFrame::fit_tab_labels() diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp index 551e39877d..90e1282dd9 100644 --- a/src/slic3r/plugin/host/PluginPages.cpp +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -218,7 +218,6 @@ void PluginPages::initialize(Notebook* parent) return; m_visible_page_count = GUI::wxGetApp().app_config->get_plugin_pages_visible_count(); - m_notebook_base_index = static_cast(m_parent->GetPageCount()); m_image_list = std::make_unique(20, 20, true, 0); m_parent->SetImageList(m_image_list.get()); @@ -238,7 +237,6 @@ void PluginPages::shutdown() m_parent->SetImageList(nullptr); m_image_list.reset(); m_parent = nullptr; - m_notebook_base_index = 0; } void PluginPages::set_visible_page_count(int count) @@ -369,6 +367,10 @@ void PluginPages::remove_page(const PluginCapabilityId& id) } } + const int idx = m_parent != nullptr ? m_parent->FindPage(page) : wxNOT_FOUND; + if (idx != wxNOT_FOUND) + m_parent->RemovePage(idx); + relayout(); page->Destroy(); } @@ -394,8 +396,11 @@ void PluginPages::relayout() wxString id_to_reselect = m_parent->GetSelectedPageName(); - while (m_parent->GetPageCount() > m_notebook_base_index) - m_parent->RemovePage(m_parent->GetPageCount() - 1); + for (const auto& [id, page] : m_pages) { + const int idx = m_parent->FindPage(page); + if (idx != wxNOT_FOUND) + m_parent->RemovePage(idx); + } const int visible_slots = std::max(1, m_visible_page_count); const bool need_overflow = static_cast(m_order.size()) > visible_slots; diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp index cc54332dbe..3dc86b8aee 100644 --- a/src/slic3r/plugin/host/PluginPages.hpp +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -69,19 +69,19 @@ public: int get_visible_page_count() const { return m_visible_page_count; } 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 relayout(); void show_overflow_menu(); static wxString page_tab_id(const PluginCapabilityId& id); std::map m_pages; std::vector m_order; Notebook* m_parent{nullptr}; - size_t m_notebook_base_index{0}; std::unique_ptr m_image_list; int m_visible_page_count{0}; From 6c0f5eee55bb83212a3f99668b79fc8e4e7d52cc Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 18 Aug 2026 22:17:59 +0800 Subject: [PATCH 10/14] Clarify icon rescaling condition in Button::Rescale method --- src/slic3r/GUI/Widgets/Button.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index 1a8cbefbce..94be6ce301 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -254,7 +254,8 @@ 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(); messureSize(); From ffee4024941006da85c10109826607d8bb44d40a Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 19 Aug 2026 00:27:48 +0800 Subject: [PATCH 11/14] Give the printer-agents web Device tab its own page id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In printer-agents mode the legacy web page was appended under Notebook::PAGE_MONITOR, which resolves to the same "monitor" id as the native Device tab. FindPageByName returns the first match, so PluginPages::relayout() — which saves the selection by name and restores it after rebuilding the tab strip — moved the user off the web tab onto the native one. The tab also disagreed with its own label, being created as "Device (legacy)" and renamed to "Device (Web)" on the next show_device() call. --- src/slic3r/GUI/MainFrame.cpp | 5 +++-- src/slic3r/GUI/MainFrame.hpp | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 334bd9f3d4..9e3d40994b 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1385,7 +1385,7 @@ void MainFrame::show_device(bool should_use_native) { // 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 != m_tabpanel->FindPageByName(TAB_ID_MONITOR)) { + if ((idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR_WEB)) != wxNOT_FOUND) { m_printer_view->Show(false); m_tabpanel->RemovePage(idx); } @@ -1447,7 +1447,8 @@ void MainFrame::show_device(bool should_use_native) { if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { m_printer_view->Show(false); - m_tabpanel->AddPage(m_printer_view, _L("Device (legacy)"), false, Notebook::PAGE_MONITOR); + m_tabpanel->InsertPage(m_tabpanel->GetPageCount(), TAB_ID_MONITOR_WEB, m_printer_view, + _L("Device (Web)"), "tab_monitor_active", false); } else { m_tabpanel->SetPageText(idx, _L("Device (Web)")); } diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 6d8e548ce8..d6e8173288 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -43,6 +43,10 @@ #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" From 02736fee163b09639e9c5780777ccf892152dc22 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 19 Aug 2026 00:27:48 +0800 Subject: [PATCH 12/14] Restore the web Device tab URL load on tab selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selecting the web Device tab loaded the printer's web UI from the selected discovered machine when the preset carried no host. That arm was lost merging main into this branch — two of the three Plater.cpp hunks from #15134 survived, this one did not — leaving the tab blank, since PrinterWebView starts on an empty URL and nothing else navigates it. --- src/slic3r/GUI/Plater.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 9fad2574ab..bf9d697a7a 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -11245,7 +11245,15 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } } } else { - if (new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_MONITOR) && wxGetApp().preset_bundle != nullptr) { + // 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 == main_frame->m_tabpanel->FindPageByName(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()) { From 5be1f8f20910970c31db9bb131d312960c941f13 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 19 Aug 2026 01:37:23 +0800 Subject: [PATCH 13/14] fix crash on Mac --- src/slic3r/GUI/Monitor.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 51bd7ed878..1a6d969988 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -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(); From 1e87d56482f819a412b897860d24305259d24405 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 19 Aug 2026 14:08:19 +0800 Subject: [PATCH 14/14] Micro-refactor --- src/libslic3r/AppConfig.cpp | 23 +-- src/libslic3r/AppConfig.hpp | 1 - src/slic3r/GUI/MainFrame.cpp | 148 +++++++++----------- src/slic3r/GUI/MainFrame.hpp | 2 + src/slic3r/GUI/Notebook.cpp | 24 +--- src/slic3r/GUI/Notebook.hpp | 186 +++++++------------------ src/slic3r/GUI/Plater.cpp | 11 +- src/slic3r/GUI/Widgets/Button.cpp | 2 +- src/slic3r/plugin/host/PluginPages.cpp | 107 +++++++------- src/slic3r/plugin/host/PluginPages.hpp | 12 +- 10 files changed, 181 insertions(+), 335 deletions(-) diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 970faeb620..4dc838172c 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -280,19 +280,8 @@ void AppConfig::set_defaults() set(SETTING_OPENGL_FPS_CAP, std::to_string(fps_cap)); } - if (get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT).empty()) - set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT)); - else { - int visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; - try { - visible_count = std::stoi(get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT)); - } - catch (...) { - visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; - } - visible_count = std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); - set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(visible_count)); - } + // 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); @@ -1657,13 +1646,7 @@ int AppConfig::get_plugin_pages_visible_count() const catch (...) { return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; } - return std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); -} - -void AppConfig::set_plugin_pages_visible_count(int count) -{ - count = std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); - set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(count)); + return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX); } std::vector AppConfig::get_skipped_network_versions() const diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index de5c3a442f..65c57cdb30 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -382,7 +382,6 @@ public: // 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; - void set_plugin_pages_visible_count(int count); std::vector get_skipped_network_versions() const; void add_skipped_network_version(const std::string& version); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 9e3d40994b..3b1cf1dffd 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -722,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->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW)) { + if (m_plater && is_prepare_or_preview_tab()) { m_plater->sidebar().can_search(); } } @@ -1015,12 +1015,12 @@ void MainFrame::update_layout() case ESettingsLayout::Old: { m_plater->Reparent(m_tabpanel); - { - 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, m_plater, _L("Prepare"), false, Notebook::PAGE_PREPARE); - m_tabpanel->InsertPage(prepare_pos + 1, m_plater, _L("Preview"), false, Notebook::PAGE_PREVIEW); - } + // 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) @@ -1290,24 +1290,6 @@ 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()) { @@ -1317,7 +1299,7 @@ void MainFrame::init_tabpanel() { select_tab(TAB_ID_HOME); m_webview->load_url(url); }); - m_tabpanel->AddPage(m_webview, "", false, Notebook::PAGE_HOME); + 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); } @@ -1332,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"), false, Notebook::PAGE_MONITOR); + 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) { @@ -1347,16 +1329,16 @@ 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"), false, Notebook::PAGE_MULTI_DEVICE); + 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"), false, Notebook::PAGE_PROJECT); + 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"), false, Notebook::PAGE_CALIBRATION); + 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. @@ -1382,9 +1364,14 @@ 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) { + // 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); @@ -1403,10 +1390,8 @@ void MainFrame::show_device(bool should_use_native) { m_tabpanel->RemovePage(idx); } m_monitor->Show(false); - const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); - const size_t monitor_pos = - (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; - m_tabpanel->InsertPage(monitor_pos, TAB_ID_MONITOR, m_monitor, _L("Device"), "tab_monitor_active", false); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor, + _L("Device"), "tab_monitor_active"); } if (m_printer_view == nullptr) { @@ -1427,30 +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); - const int monitor_idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR); - const size_t multi_pos = - (monitor_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(monitor_idx) + 1; - m_tabpanel->InsertPage(multi_pos, TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), - "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"), false, Notebook::PAGE_CALIBRATION); + 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->InsertPage(m_tabpanel->GetPageCount(), TAB_ID_MONITOR_WEB, m_printer_view, - _L("Device (Web)"), "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 @@ -1458,7 +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(); // keep plugin tabs after the native tabs just mutated above + m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above return; } @@ -1480,11 +1466,8 @@ void MainFrame::show_device(bool should_use_native) { m_monitor->SetBackgroundColour(*wxWHITE); } m_monitor->Show(false); - { - const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); - const size_t monitor_pos = (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; - m_tabpanel->InsertPage(monitor_pos, m_monitor, _L("Device"), false, Notebook::PAGE_MONITOR); - } + 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) { @@ -1493,21 +1476,18 @@ void MainFrame::show_device(bool should_use_native) { } // TODO: change the bitmap m_multi_machine->Show(false); - { - const int monitor_idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR); - const size_t multi_pos = (monitor_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(monitor_idx) + 1; - m_tabpanel->InsertPage(multi_pos, m_multi_machine, _L("Multi-device"), false, Notebook::PAGE_MULTI_DEVICE); - } + 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 appended last (AddPage), so it lands after whichever of Monitor/Multi-device - // actually got inserted above — no longer position-sensitive now that insertion position is computed - // from FindPageByName rather than a fixed TabPosition index. - m_tabpanel->AddPage(m_calibration, _L("Calibration"), false, Notebook::PAGE_CALIBRATION); + // 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); @@ -1540,14 +1520,17 @@ void MainFrame::show_device(bool should_use_native) { }); } m_printer_view->Show(false); - { - const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); - const size_t monitor_pos = (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; - m_tabpanel->InsertPage(monitor_pos, m_printer_view, _L("Device"), false, Notebook::PAGE_MONITOR); - } + 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(); // keep plugin tabs after the native tabs just mutated above + 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() @@ -3168,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->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, + this, [this]() { return is_prepare_or_preview_tab(); }, [this]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this); viewMenu->AppendSeparator(); @@ -3186,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->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, + 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"), @@ -3194,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->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, + [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->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW) && - m_plater->is_sidebar_enabled(); + return is_prepare_or_preview_tab() && m_plater->is_sidebar_enabled(); }, this); @@ -4025,10 +4007,8 @@ void MainFrame::select_tab(wxPanel* panel) wxGetApp().params_dialog()->Popup(); return; } - // page_name cannot be resolved via panel->GetName() — Prepare and Preview - // share the single m_plater window, so the window itself has no single correct - // name (see Global Constraints). Resolve via Notebook's per-slot m_pageNames - // instead, via the index -> id lookup, which works for any page (built-in or not). + // 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); 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) @@ -4082,10 +4062,8 @@ void MainFrame::select_tab(const wxString& id/* = wxString()*/) m_plater->get_current_canvas3D()->render(); }*/ #endif - // NOTE: this checks the ORIGINAL parameter (id), not the resolved new_selection — - // preserving that the fallback-to-last-tab path never triggers this render call - // even if the last selected tab happened to be Prepare. Do not "simplify" to - // new_selection == TAB_ID_PREPARE, that changes behavior. + // 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) { diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index d6e8173288..2052860d80 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -362,6 +362,8 @@ 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 }; diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index f9db689faa..673508454a 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -156,14 +156,13 @@ void ButtonsListCtrl::SetSelection(int sel) Refresh(); } -bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, int imageId /* = wxBookCtrlBase::NO_IMAGE */) +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() && m_imageList != nullptr && imageId != wxBookCtrlBase::NO_IMAGE && imageId >= 0 && - imageId < m_imageList->GetImageCount()) - btn->SetIcon(m_imageList->GetBitmap(imageId)); + if (bmp_name.empty() && bmp.IsOk()) + btn->SetIcon(bmp); int em = em_unit(this); //BBS set size for button @@ -232,23 +231,6 @@ bool ButtonsListCtrl::SetPageImage(size_t n, const std::string& bmp_name) const return true; } -bool ButtonsListCtrl::SetPageImage(size_t n, int imageId) -{ - if (n >= m_pageButtons.size()) - return false; - - if (imageId == wxBookCtrlBase::NO_IMAGE) { - m_pageButtons[n]->SetIcon(wxBitmap()); - return true; - } - - if (m_imageList == nullptr || imageId < 0 || imageId >= m_imageList->GetImageCount()) - return false; - - m_pageButtons[n]->SetIcon(m_imageList->GetBitmap(imageId)); - return true; -} - void ButtonsListCtrl::SetPageText(size_t n, const wxString& strText) { Button* btn = m_pageButtons[n]; diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index da90535481..4734122b18 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -3,11 +3,11 @@ //#ifdef _WIN32 +#include #include #include -#include #include -#include +#include #include class ScalableButton; @@ -27,11 +27,9 @@ 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 = "", int imageId = wxBookCtrlBase::NO_IMAGE); + 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; - bool SetPageImage(size_t n, int imageId); - void SetImageList(wxImageList* imageList) { m_imageList = imageList; } void SetPageText(size_t n, const wxString& strText); void SetCompact(size_t n, bool compact); // ORCA wxString GetPageText(size_t n) const; @@ -49,23 +47,12 @@ private: int m_btn_margin; int m_line_margin; std::vector m_pageLabels; // ORCA - wxImageList* m_imageList{nullptr}; wxWindow* m_overflow_button{nullptr}; // ORCA }; class Notebook : public wxBookCtrlBase { public: - // Negative values below wxBookCtrlBase::NO_IMAGE are reserved for the built-in - // tabs. Nonnegative values are wxImageList indices supplied by plugin pages. - static constexpr int PAGE_HOME = -2; - static constexpr int PAGE_PREPARE = -3; - static constexpr int PAGE_PREVIEW = -4; - static constexpr int PAGE_MONITOR = -5; - static constexpr int PAGE_MULTI_DEVICE = -6; - static constexpr int PAGE_PROJECT = -7; - static constexpr int PAGE_CALIBRATION = -8; - Notebook(wxWindow * parent, wxWindowID winid = wxID_ANY, const wxPoint & pos = wxDefaultPosition, @@ -156,83 +143,58 @@ public: // Implement base class pure virtual methods. + // 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 = "", + bool bSelect = false) + { + DoInvalidateBestSize(); + return InsertPage(GetPageCount(), id, page, text, bmp_name, bSelect); + } + bool AddPage(wxWindow* page, const wxString& text, bool bSelect = false, int imageId = NO_IMAGE) override { DoInvalidateBestSize(); return InsertPage(GetPageCount(), page, text, bSelect, imageId); } - // Page management - virtual bool InsertPage(size_t n, - wxWindow * page, - const wxString & text, - bool bSelect = false, - int imageId = NO_IMAGE) override - { - wxString page_name; - std::string bmp_name; - const bool is_fixed_page = get_fixed_page_info(imageId, page_name, bmp_name); - const int stored_image_id = is_fixed_page ? NO_IMAGE : imageId; - - if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, stored_image_id)) - return false; - - m_pageNames.insert(m_pageNames.begin() + n, page_name); - m_pageImageIds.insert(m_pageImageIds.begin() + n, stored_image_id); - GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, stored_image_id); - - if (!DoSetSelectionAfterInsertion(n, bSelect)) - page->Hide(); - - return true; - } - - bool InsertPage(size_t n, - const wxString& id, - wxWindow* page, - const wxString& text, - int imageId, - bool bSelect = false) - { - if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId)) - return false; - - m_pageNames.insert(m_pageNames.begin() + n, id); - m_pageImageIds.insert(m_pageImageIds.begin() + n, imageId); - GetBtnsListCtrl()->InsertPage(n, text, bSelect, "", imageId); - - if (!DoSetSelectionAfterInsertion(n, bSelect)) - page->Hide(); - - return true; - } - bool InsertPage(size_t n, const wxString& id, wxWindow * page, const wxString & text, const std::string& bmp_name = "", - bool bSelect = false) + bool bSelect = false, + const wxBitmap& bmp = wxNullBitmap) { if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) return false; m_pageNames.insert(m_pageNames.begin() + n, id); - m_pageImageIds.insert(m_pageImageIds.begin() + n, NO_IMAGE); - GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name); + 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. A freshly - // constructed page defaults to shown, so without this it renders on top of - // whatever page is currently selected until the next SetSelection() call hides - // it. Mirrors the pure-virtual InsertPage() override above, which already does - // this correctly. + // 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; } + virtual bool InsertPage(size_t n, + wxWindow * page, + const wxString & text, + bool bSelect = false, + int WXUNUSED(imageId) = NO_IMAGE) override + { + return InsertPage(n, wxString(), page, text, "", bSelect); + } + virtual int SetSelection(size_t n) override { int ret = DoSetSelection(n, SetSelection_SendEvent); @@ -262,7 +224,8 @@ public: return DoSetSelection(n); } - // Labels are stored by the custom button list; page images use the wx image-list IDs below. + // 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")); @@ -278,27 +241,14 @@ public: return GetBtnsListCtrl()->GetPageText(n); } - virtual bool SetPageImage(size_t n, int imageId) override + virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override { - if (n >= m_pageImageIds.size()) - return false; - - if (!GetBtnsListCtrl()->SetPageImage(n, imageId)) - return false; - - m_pageImageIds[n] = imageId; - return true; + return false; } - virtual int GetPageImage(size_t n) const override + virtual int GetPageImage(size_t WXUNUSED(n)) const override { - return n < m_pageImageIds.size() ? m_pageImageIds[n] : NO_IMAGE; - } - - void SetImageList(wxImageList* imageList) - { - m_imageList = imageList; - GetBtnsListCtrl()->SetImageList(imageList); + return NO_IMAGE; } bool SetPageImage(size_t n, const std::string& bmp_name) @@ -314,22 +264,27 @@ public: page->SetFocus(); } - // wxBookCtrlBase::DeleteAllPages() clears its page list directly rather than - // going through DoRemovePage() per page, so it would otherwise leave - // m_pageNames desynchronized (a mutation path outside the four this class - // already keeps in sync). Not currently called on a Notebook anywhere in - // this codebase, but kept correct for the same reason the rest of this - // bookkeeping exists. + // 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(); - m_pageImageIds.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()) @@ -485,7 +440,6 @@ protected: if (win) { m_pageNames.erase(m_pageNames.begin() + page); - m_pageImageIds.erase(m_pageImageIds.begin() + page); GetBtnsListCtrl()->RemovePage(page); DoSetSelectionAfterRemoval(page); } @@ -509,47 +463,9 @@ protected: } private: - static bool get_fixed_page_info(int imageId, wxString& page_name, std::string& bmp_name) - { - switch (imageId) { - case PAGE_HOME: - page_name = wxS("home"); - bmp_name = "tab_home_active"; - return true; - case PAGE_PREPARE: - page_name = wxS("prepare"); - bmp_name = "tab_3d_active"; - return true; - case PAGE_PREVIEW: - page_name = wxS("preview"); - bmp_name = "tab_preview_active"; - return true; - case PAGE_MONITOR: - page_name = wxS("monitor"); - bmp_name = "tab_monitor_active"; - return true; - case PAGE_MULTI_DEVICE: - page_name = wxS("multi_device"); - bmp_name = "tab_multi_active"; - return true; - case PAGE_PROJECT: - page_name = wxS("project"); - bmp_name = "tab_auxiliary_active"; - return true; - case PAGE_CALIBRATION: - page_name = wxS("calibration"); - bmp_name = "tab_calibration_active"; - return true; - default: - return false; - } - } - void Init(); - std::vector m_pageNames; // index-parallel to wxBookCtrlBase::m_pages - std::vector m_pageImageIds; // index-parallel to wxBookCtrlBase::m_pages - wxImageList* m_imageList{nullptr}; + std::vector m_pageNames; // index-parallel to wxBookCtrlBase::m_pages wxShowEffect m_showEffect, m_hideEffect; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5dd77f4389..aba0910630 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -11221,19 +11221,18 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) const int new_sel = e.GetSelection(); if (new_sel == wxNOT_FOUND) { - // Guards against new_sel matching FindPageByName's own wxNOT_FOUND sentinel - // below when a TAB_ID_* isn't currently present in the tabpanel. + // GetPage(new_sel) below needs a valid index. e.Skip(); return; } - sidebar_layout.show = new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_PREPARE) || - new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_PREVIEW); + 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 == main_frame->m_tabpanel->FindPageByName(TAB_ID_MONITOR)) { + 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()) { @@ -11253,7 +11252,7 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) 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 == main_frame->m_tabpanel->FindPageByName(TAB_ID_MONITOR) && 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()) { diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index 94be6ce301..74ed2cbadd 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -288,7 +288,7 @@ void Button::render(wxDC& dc) wxSize szIcon; wxSize textSize = this->textSize.GetSize(); - ScalableBitmap icon = active_icon; + const ScalableBitmap& icon = active_icon; wxSize padding = this->paddingSize; int spacing = 5; // Wrap text diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp index 90e1282dd9..fed16ee416 100644 --- a/src/slic3r/plugin/host/PluginPages.cpp +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -171,9 +171,11 @@ void PluginPage::on_script_message(wxWebViewEvent& event) root.value("kind", std::string()) != "message") return; - const nlohmann::json data = root.contains("data") ? root["data"] : nlohmann::json(); + const auto data = root.find("data"); try { - m_cap->on_message(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)); + 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 (...) { @@ -186,28 +188,23 @@ void PluginPage::push_message(const std::string& message) if (m_browser == nullptr) return; - nlohmann::json data = nlohmann::json::parse(message, nullptr, false); - if (data.is_discarded()) - data = message; + // 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); - const wxString script = wxString("(function dispatch(payload, attempts) {\n") + - wxString(" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n") + - wxString(" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n") + - wxString("})({data: ") + - wxString::FromUTF8(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)) + - wxString("}, 0);"); - WebView::RunScript(m_browser, script); + 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(); - // try { - // } catch (const std::exception& error) { - // BOOST_LOG_TRIVIAL(error) << "PluginPages::~PluginPages: shutdown() threw: " << error.what(); - // } catch (...) { - // BOOST_LOG_TRIVIAL(error) << "PluginPages::~PluginPages: shutdown() threw a non-standard exception"; - // } } void PluginPages::initialize(Notebook* parent) @@ -219,9 +216,6 @@ void PluginPages::initialize(Notebook* parent) m_visible_page_count = GUI::wxGetApp().app_config->get_plugin_pages_visible_count(); - m_image_list = std::make_unique(20, 20, true, 0); - m_parent->SetImageList(m_image_list.get()); - for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) { if (capability) create_page(capability->identity()); @@ -233,15 +227,12 @@ void PluginPages::shutdown() { while (!m_pages.empty()) remove_page(m_pages.begin()->first); - if (m_parent != nullptr) - m_parent->SetImageList(nullptr); - m_image_list.reset(); m_parent = nullptr; } void PluginPages::set_visible_page_count(int count) { - const int clamped = std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); + const int clamped = std::clamp(count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX); if (clamped == m_visible_page_count) return; @@ -282,17 +273,14 @@ bool PluginPages::create_page(const PluginCapabilityId& id) return false; } - int image_id = wxBookCtrlBase::NO_IMAGE; - if (!icon.empty() && m_image_list) { + 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(); - const wxBitmap bitmap = create_scaled_bitmap(icon_path.string(), m_parent, 20); - if (bitmap.IsOk()) - image_id = m_image_list->Add(bitmap); + 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 (...) { @@ -300,7 +288,6 @@ bool PluginPages::create_page(const PluginCapabilityId& id) } } - page->set_icon_image_id(image_id); m_pages.emplace(id, page); m_order.push_back(id); return true; @@ -349,24 +336,11 @@ void PluginPages::remove_page(const PluginCapabilityId& id) return; PluginPage* page = it->second; - const int removed_image_id = page->get_icon_image_id(); page->detach_capability(); m_pages.erase(it); m_order.erase(std::remove(m_order.begin(), m_order.end(), id), m_order.end()); - if (m_image_list && removed_image_id != wxBookCtrlBase::NO_IMAGE && - removed_image_id >= 0 && removed_image_id < m_image_list->GetImageCount()) { - m_image_list->Remove(removed_image_id); - - // wxImageList IDs are positional. Removing one shifts all later images down by one. - for (auto& [other_id, other_page] : m_pages) { - const int other_image_id = other_page->get_icon_image_id(); - if (other_image_id > removed_image_id) - other_page->set_icon_image_id(other_image_id - 1); - } - } - const int idx = m_parent != nullptr ? m_parent->FindPage(page) : wxNOT_FOUND; if (idx != wxNOT_FOUND) m_parent->RemovePage(idx); @@ -394,14 +368,6 @@ void PluginPages::relayout() }), m_order.end()); - 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); - } - const int visible_slots = std::max(1, m_visible_page_count); const bool need_overflow = static_cast(m_order.size()) > visible_slots; @@ -421,9 +387,37 @@ void PluginPages::relayout() tab_ids.push_back(*m_swapped_in_id); } - 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), page->get_icon_image_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) { @@ -442,9 +436,6 @@ void PluginPages::relayout() m_overflow_button->Destroy(); m_overflow_button = nullptr; } - - if (!id_to_reselect.empty()) - m_parent->SelectPageByName(id_to_reselect); } void PluginPages::show_overflow_menu() diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp index 3dc86b8aee..4d00de6df9 100644 --- a/src/slic3r/plugin/host/PluginPages.hpp +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -10,8 +10,7 @@ #include #include -#include -#include +#include #include #include @@ -33,8 +32,8 @@ public: void on_new_window(wxWebViewEvent& event); void on_script_message(wxWebViewEvent& event); void push_message(const std::string& message); - void set_icon_image_id(int id) { m_icon_image_id = id; } - int get_icon_image_id() const { return m_icon_image_id; } + void set_icon(const wxBitmap& icon) { m_icon = icon; } + const wxBitmap& icon() const { return m_icon; } private: void load_plugin_content(); @@ -45,8 +44,7 @@ private: std::shared_ptr m_cap; std::shared_ptr> m_lifetime; bool m_content_loaded{false}; - - int m_icon_image_id = wxBookCtrlBase::NO_IMAGE; + wxBitmap m_icon; }; class PluginPages @@ -66,7 +64,6 @@ public: void on_plugin_register(const std::string& plugin_key); void on_plugin_deregister(const std::string& plugin_key); - int get_visible_page_count() const { return m_visible_page_count; } void set_visible_page_count(int count); void relayout(); @@ -83,7 +80,6 @@ private: std::vector m_order; Notebook* m_parent{nullptr}; - std::unique_ptr m_image_list; int m_visible_page_count{0}; std::optional m_swapped_in_id;