diff --git a/docs/HLSD/deferred-page-construction.md b/docs/HLSD/deferred-page-construction.md new file mode 100644 index 0000000000..748ca65d52 --- /dev/null +++ b/docs/HLSD/deferred-page-construction.md @@ -0,0 +1,220 @@ +# Deferred Page Construction: High Level Design + +## Why it exists + +The main window is a notebook of tabs. When the first frame appears, one tab is on +screen and the others are not; some of them are opened later in the session, some +never, and some only exist for certain printers. Building a tab before the first frame +adds its cost to every startup, whether or not the tab is used. + +This subsystem builds a tab's panel the first time the tab is shown. It also builds the +remaining tabs while the user is idle after startup, in units of tens of milliseconds, +so a click that lands in the middle of one waits for that unit and no longer. Startup +pays only for what the first frame shows, and the other tabs are usually built before +anyone opens them. + +## The parts + +`src/slic3r/GUI/Lazy.hpp`, `LazyPage.hpp`, `StagedBuild.hpp` and `IdleScheduler.hpp/.cpp` +(with its wx-free `PrebuildQueue.hpp`) are independent. A holder can hold anything a +factory makes, a page is a holder with a placeholder widget, a staged object can live +outside a holder, and the scheduler knows none of them; it runs tasks, which `MainFrame` +makes from the holders. + +### Lazy: the holder + +Holds an object that a factory makes on first use, under a name for the log and a place +in the idle queue, both given by the owner. The header has no wx dependency; the busy +cursor for an on-demand build lives in `Lazy.cpp` and is skipped when there is no app, +so the holder is unit-tested. + +- `get()` is null until the object is completely built; `built()` says the same. +- `ensure()` builds whatever is left now, under a busy cursor, and returns the object, or + null in the two cases below. A click on an unbuilt tab or a first open of a dialog goes + through this, and it logs the units and time it took. +- `build_step()` runs one unit of construction and returns true while more remain. The + first unit is the factory call, and each later unit is one `StagedBuild` step if the + type has them. +- `when_built(fn)` runs `fn(object)` now if it exists, otherwise once it is built. +- `pending()` says whether the idle prebuild has work here: not built, and the factory + has not returned null. A null factory result is logged and the holder stays unbuilt. +- A unit that pumps the event loop cannot re-enter the holder; a nested `build_step()` + does nothing and a nested `ensure()` returns null. +- After a unit throws, the holder and the scheduler still run the next one. + +The holder does not own the object; its wx parent does, as for any window. A type with +one instance in the app derives from `LazyInstance`, which points at that instance's +holder (the holder registers itself, and a recreated `MainFrame`'s holder replaces the +old frame's) and gives the type the static entry points the rest of the app uses, +`T::if_built()`, `T::ensure()` and `T::when_built(fn)`. They return null, or do +nothing, while no holder exists, so a caller needs no `mainframe` check. + +`built()` is an atomic flag, since a job worker reads it through the statics +(`MainFrame::get_calibration_curr_tab()`); it is set after the object is complete. + +`LazyBase` is the holder's type-free interface (`name()`, `built()`, `pending()`, +`build_step()`, `prebuild_order()`) and is what the scheduler side sees. + +### LazyPage: the placeholder + +A `wxPanel` placed in the parent in place of the real panel, and a `Lazy` whose +factory makes the panel inside it (by default `new Panel(parent)`). The notebook needs a +page object for the tab to exist and for `show_device()` to insert and remove tabs by +pointer, and the placeholder is that object. `MainFrame` creates every page once, named +after its `TAB_ID_*`, and keeps it for the frame's life; `show_device()` only moves +pages in and out of the book. + +- `Show()` is forwarded to the panel, so a panel's own `Show()` override stays its + activation hook (refresh timers, machine sync) and `SelectPageByName()` works + unchanged. A show builds the panel unless the frame itself is still hidden, because + the book selects its first page as it is inserted; `MainFrame::Show()` shows the + current page again when the frame becomes visible, which builds it. +- `in_book()` says whether the parent notebook currently lists the page, and + `pending()` is that and not built, so a tab `show_device()` has taken out of the book + is not prebuilt. +- A panel built while its page is hidden stays hidden, and a completed panel gets the + dark-mode pass the frame ran before it existed. + +The Compare presets dialog is a holder without a page. `MainFrame` keeps a +`Lazy` whose factory constructs the dialog and binds its events, the +dialog derives from `LazyInstance`, and its callers use +`DiffPresetDialog::ensure()->show()` and `DiffPresetDialog::if_built()` like a tab's +callers do. Saving a preset refreshes the dialog only while it is shown, since `show()` +reloads the presets. + +### StagedBuild: construction in units + +A mixin for a panel whose constructor is too big to be one unit. The constructor builds +the skeleton (sizers, and the parts other code may touch) and queues the rest with +`add_build_step()`. `build_step()` runs one step, and `add_build_steps_of(child)` +forwards a child's steps so a nested panel is spread the same way; the parent is built +only once the child is, including steps the child queues later. Steps run in order, on +the main thread. A `Lazy` recognises a staged type at compile time and runs its steps +one per unit. + +Nothing may touch what a step builds before the last step has run. In practice: + +- nothing paints the panel before it is complete, since a panel built at idle is hidden + with its page and one built on demand finishes inside `ensure()` before the event loop + runs again; +- members created in steps are initialised to null in the header, so a partially built + panel can be destroyed; +- timers and event handlers that use step content check `built()` first + (`MonitorPanel::update_all()`, `CalibrationPanel::update_all()`), and a child's steps + are queued before anything in the constructor can fire such a handler; +- a destructor that disconnects from step content checks `built()` first; +- a constructor or step does not take focus while the panel is off screen + (`IsShownOnScreen()` before `SetFocus()`), since it may run while the user is typing + elsewhere; +- where a step's widgets must keep their place in a sizer that later steps also fill, the + constructor adds an empty slot sizer in that position and the step fills the slot + (`StatusBasePanel`). + +### IdleScheduler: when to build + +A task is any `LazyBase`: a name for the log, `pending()`, `build_step()` and +`prebuild_order()`. `PrebuildQueue` holds the tasks by order (equal order in the order +added) and runs a slice, the units of the first pending task until it completes, the +budget is spent on the clock it is given, or the interrupt predicate says input arrived. +It has no wx dependency and is unit-tested with a fake clock. A task whose work is gone +is passed over and stays in the queue, so a tab that `show_device()` removes and later +re-inserts is pending again. + +`IdleScheduler` drives the queue with the real clock, the app's input timestamp and the +Windows queue check, and logs each unit and slice. Each slice is a timer message: a +period of 250 ms while waiting for the user to go quiet, and a one-shot of zero after a +slice that left work, so the event loop dispatches whatever it has queued (paint, +timers, input) before the next slice runs. Chaining slices with `CallAfter` would not do +this: wx drains pending events fully before the next native message, on every platform. +On GTK the one-shot is 5 ms, because a due GLib timeout runs ahead of the redraw and +idle sources that paint and deliver posted events. +A slice runs only once the user has been idle for the quiet time, and the timer stops +itself once no task is pending. The tick period, quiet time and slice length are +constants in `IdleScheduler.cpp`. A unit cannot be interrupted once started, so the +largest unit bounds click latency on Windows; on the other platforms a click also waits +for the rest of the slice. A unit that pumps the event loop lets the timer fire inside +its own slice, and that tick does nothing. + +The tasks are made by their owners. `MainFrame::prebuild_pages_when_idle()` registers +every `LazyPage` the frame created, in or out of the book (`pending()` is false for a +page out of the book), the Compare presets holder, and the Prepare sidebar's settings +page from `ParamsPanel::settings_page_prebuild()`, whose first unit selects the default +tab if none is selected yet and whose later units build one option group each. +`show_device()` only restarts the timer. `MainFrame` owns the scheduler because it owns +everything the tasks build, and clearing the queue with the frame is what keeps a task +from outliving its object. + +Idle time comes from `GUI_App::FilterEvent`, which timestamps mouse and keyboard events +(`wxEVT_CATEGORY_USER_INPUT` minus command events, which all claim that category), and +`GUI_App::input_idle_ms()` reports it. On Windows a slice additionally refuses to start +when the message queue holds keyboard, button, touch or pen input. Mouse moves are +excluded because Windows synthesises one whenever a window appears under the cursor, +which every unit does. Other platforms use the timestamp alone. + +Once every registered page is built the timer is stopped and the subsystem costs +nothing. + +## Rules + +**What builds before the first frame.** The Prepare tab's plater, because `post_init()` +needs its GL canvas on screen to initialise OpenGL in every startup state, and the start +page the user configured. Home is built by the first `MainFrame::Show()`, Prepare's +settings page by selecting the tab. `post_init()` passes through the Prepare tab for GL +init under `Freeze()` with `MainFrame::select_prepare_for_gl_init()`, which changes the +selection without the page-changed event, so nothing else is built for that pass. + +**Reaching a lazy object.** A caller uses the type's own statics. `T::if_built()` may +return null and is for telling the object something it can live without (a rescale, a +colour change, a status update). `T::ensure()` builds the object and is for navigating +to it or for a caller that is about to show it. A caller that tells the object something +it would not fetch for itself on construction uses `T::when_built()`, which keeps the +message until the object exists. A panel that pulls its state when constructed (the Home +page requests the recent list on load, the Device tab reads the device manager on show) +is reached with `if_built()`; one that cannot pull gets `when_built()`. + +**Unit size.** A unit cannot be interrupted, so it should stay within the slice length on +a fast machine. A constructor above that is staged. A single widget above it is the +floor unless the widget itself is split. + +**Order.** Cheapest and most likely to be opened first, given where `MainFrame` creates +each holder. The settings page is 0 (the Prepare tab's own content), Home 10, Device 20 +(a Bambu user's usual second stop), Calibration 30, Multi-device 40, the web Device +view 50, Project 60 (a second WebView2 instance) and the Compare presets dialog 100; +steps of ten so a new tab takes a value between its neighbours without renumbering +them. `MainFrame::prebuild_pages_when_idle()` registers the tasks once, from +`post_init()`. + +**Never prebuilt.** A holder with a negative order, for a tab that few sessions open +and that costs more to build unasked than it saves (the Design tab), and plugin-provided +tabs, which are Python-side and not lazy pages. + +## Adopting it + +A lazy tab needs: + +1. The panel derived from `LazyInstance`, since a tab's panel has one instance. +2. A `LazyPage*` member in `MainFrame`, created once in `init_tabpanel()` with + its `TAB_ID_*` as the name, its place by the order rule (and a factory if + `new Panel(parent)` is not enough), added to `m_lazy_pages`, and used wherever the + tab is added to or looked up in the notebook. +3. Every use of the panel outside `MainFrame` going through one of the panel's statics, + chosen by the rule above. When converting an existing member, `grep` for it; the + compiler finds the rest. +4. A constructor that copes with the frame already existing and the user being busy + elsewhere, since `wxGetApp().mainframe` is set, the frame may be shown, and the user + may be typing when a lazy panel is built: no `SetFocus()` while off screen, and + whatever the constructor did through a `MainFrame` accessor before (a mode update, a + deferred URL) done in the constructor itself. + +To stage a heavy constructor, inherit `StagedBuild`, keep the skeleton in the constructor, +move the rest into `add_build_step()` lambdas in the original order, and follow the +staged-panel rules above. Measure the units. A step that is still one big widget has to +be split inside that widget or accepted as the floor. + +Verification is by log. `MainFrame::prebuild_pages_when_idle` lists the queue it +registered, `IdleScheduler::tick` reports each completed task by name at info level and +each slice and unit at debug level, and `Lazy::ensure` reports an object a user built +on demand with the units and time it took. A run from the configured start +page shows every registered page complete, in order, with no unit longer than intended. +A click on a tab mid-prebuild shows the finished panel with an `ensure` line for what was +left, and the slices resume for the remaining tasks once the user is idle again. diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 7ac307c87b..584fc2a2aa 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -286,6 +286,8 @@ set(SLIC3R_GUI_SOURCES GUI/FilamentMapDialog.hpp GUI/IconManager.cpp GUI/IconManager.hpp + GUI/IdleScheduler.cpp + GUI/IdleScheduler.hpp GUI/ImageGrid.cpp GUI/ImageGrid.h GUI/ImGuiWrapper.cpp @@ -347,6 +349,10 @@ set(SLIC3R_GUI_SOURCES GUI/KBShortcutsDialog.hpp GUI/KeyChord.cpp GUI/KeyChord.hpp + GUI/Lazy.cpp + GUI/Lazy.hpp + GUI/LazyPage.cpp + GUI/LazyPage.hpp GUI/LibVGCode/LibVGCodeWrapper.hpp GUI/LibVGCode/LibVGCodeWrapper.cpp GUI/LinuxDisplayBackend.cpp @@ -433,6 +439,7 @@ set(SLIC3R_GUI_SOURCES GUI/Plater.hpp GUI/PlateSettingsDialog.cpp GUI/PlateSettingsDialog.hpp + GUI/PrebuildQueue.hpp GUI/Preferences.cpp GUI/Preferences.hpp GUI/PresetBundleDialog.cpp @@ -512,6 +519,7 @@ set(SLIC3R_GUI_SOURCES GUI/SliceInfoPanel.hpp GUI/SlicingProgressNotification.cpp GUI/SlicingProgressNotification.hpp + GUI/StagedBuild.hpp GUI/StatusPanel.cpp GUI/StatusPanel.hpp GUI/StepMeshDialog.cpp diff --git a/src/slic3r/GUI/Auxiliary.cpp b/src/slic3r/GUI/Auxiliary.cpp index 13ca173eb6..14b8d238a2 100644 --- a/src/slic3r/GUI/Auxiliary.cpp +++ b/src/slic3r/GUI/Auxiliary.cpp @@ -863,17 +863,27 @@ void AuxiliaryPanel::init_tabpanel() m_tabpanel->SetBackgroundColour(wxColour("#FEFFFF")); m_tabpanel->Bind(wxEVT_BOOKCTRL_PAGE_CHANGED, [](wxBookCtrlEvent &e) { /* Event handling */ }); - m_designer_panel = new DesignerPanel(m_tabpanel, AuxiliaryFolderType::DESIGNER); - m_pictures_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::MODEL_PICTURE); - m_bill_of_materials_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::BILL_OF_MATERIALS); - 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); + add_build_step([this] { + m_designer_panel = new DesignerPanel(m_tabpanel, AuxiliaryFolderType::DESIGNER); + m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), true); + }); + add_build_step([this] { + m_pictures_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::MODEL_PICTURE); + m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), false); + }); + add_build_step([this] { + m_bill_of_materials_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::BILL_OF_MATERIALS); + m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), false); + }); + add_build_step([this] { + m_assembly_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::ASSEMBLY_GUIDE); + m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), false); + }); + add_build_step([this] { + m_others_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::OTHERS); + m_tabpanel->AddPage(m_others_panel, _L("Others"), false); + Layout(); + }); } wxWindow *AuxiliaryPanel::create_side_tools() diff --git a/src/slic3r/GUI/Auxiliary.hpp b/src/slic3r/GUI/Auxiliary.hpp index 0bced37115..76e6adf97b 100644 --- a/src/slic3r/GUI/Auxiliary.hpp +++ b/src/slic3r/GUI/Auxiliary.hpp @@ -46,6 +46,7 @@ #include "slic3r/GUI/UpgradePanel.hpp" #include "slic3r/GUI/AmsWidgets.hpp" #include "Widgets/SideTools.hpp" +#include "StagedBuild.hpp" #define AUFILE_GREY700 wxColour(107, 107, 107) #define AUFILE_GREY500 wxColour(158, 158, 158) @@ -194,7 +195,7 @@ public: }; -class AuxiliaryPanel : public wxPanel +class AuxiliaryPanel : public wxPanel, public StagedBuild { private: Tabbook *m_tabpanel = {nullptr}; diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp index 88b445534d..16f0a2722c 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.hpp +++ b/src/slic3r/GUI/CAD/DesignPanel.hpp @@ -12,6 +12,7 @@ #include "libslic3r/CAD/CadDocument.hpp" #include "slic3r/GUI/CAD/DesignInteraction.hpp" // CadLevel: what one Esc press means +#include "slic3r/GUI/Lazy.hpp" class ComboBox; // Orca dropdown (Widgets/ComboBox.hpp) — replaces wxChoice everywhere here class StaticBox; // Orca rounded card frame (Widgets/StaticBox.hpp) @@ -46,7 +47,7 @@ class DesignCanvas; // Design (CAD) tab: a sketch-first, Onshape-style form-driven CAD panel. // Sketch and Extrude are independent tools: the user creates a Sketch first, // then selects it and Extrudes to produce a solid. -class DesignPanel : public wxPanel +class DesignPanel : public wxPanel, public LazyInstance { public: explicit DesignPanel(wxWindow* parent); diff --git a/src/slic3r/GUI/CAD/McpControl.cpp b/src/slic3r/GUI/CAD/McpControl.cpp index 8e34e2aac8..0ad05667c3 100644 --- a/src/slic3r/GUI/CAD/McpControl.cpp +++ b/src/slic3r/GUI/CAD/McpControl.cpp @@ -1989,11 +1989,10 @@ json action_set_feature_expr(DesignPanel* panel, const json& params) // Dispatch one parsed request ON THE MAIN THREAD. Returns a JSON-RPC reply string. std::string handle_on_main(const std::string& method, const json& params, const json& id) { - MainFrame* mf = wxGetApp().mainframe; // The panel is built on first use, and in a headless session nobody clicks the tab that // would build it -- so build it here rather than refusing. Safe: this runs on the main // thread (see the CallAfter that dispatches us). - DesignPanel* panel = mf ? mf->ensure_design_panel() : nullptr; + DesignPanel* panel = DesignPanel::ensure(); if (!panel) return rpc_error(id, -32001, "Design panel not ready"); diff --git a/src/slic3r/GUI/CalibrationPanel.cpp b/src/slic3r/GUI/CalibrationPanel.cpp index bdc79c1c8e..fca9fa2be9 100644 --- a/src/slic3r/GUI/CalibrationPanel.cpp +++ b/src/slic3r/GUI/CalibrationPanel.cpp @@ -491,14 +491,15 @@ void CalibrationPanel::init_tabpanel() { selected); } + for (int i = 0; i < (int)CALI_MODE_COUNT; i++) + add_build_steps_of(*m_cali_panels[i]); + // ORCA use standard paddings and keep arrow icon for consistent look between sidebars //for (int i = 0; i < (int)CALI_MODE_COUNT; i++) // m_tabpanel->SetPageImage(i, ""); //auto padding_size = m_tabpanel->GetBtnsListCtrl()->GetPaddingSize(0); //m_tabpanel->GetBtnsListCtrl()->SetPaddingSize({ FromDIP(15), padding_size.y }); - - m_initialized = true; } void CalibrationPanel::init_timer() @@ -534,6 +535,8 @@ void CalibrationPanel::update_print_error_info(int code, std::string msg, std::s } void CalibrationPanel::update_all() { + // Every wizard's pages exist once the last build step has run. + if (!built()) return; NetworkAgent* m_agent = wxGetApp().getAgent(); Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); @@ -597,7 +600,7 @@ void CalibrationPanel::update_all() { void CalibrationPanel::show_status(int status) { - if (!m_initialized) return; + if (!built()) return; if (last_status == status)return; last_status = status; diff --git a/src/slic3r/GUI/CalibrationPanel.hpp b/src/slic3r/GUI/CalibrationPanel.hpp index 1f6b097af9..960c5eed37 100644 --- a/src/slic3r/GUI/CalibrationPanel.hpp +++ b/src/slic3r/GUI/CalibrationPanel.hpp @@ -3,6 +3,7 @@ #include "CalibrationWizard.hpp" #include "Tabbook.hpp" +#include "Lazy.hpp" //#include "Widgets/SideTools.hpp" namespace Slic3r { namespace GUI { @@ -88,7 +89,7 @@ private: }; -class CalibrationPanel : public wxPanel +class CalibrationPanel : public wxPanel, public StagedBuild, public LazyInstance { public: CalibrationPanel(wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxTAB_TRAVERSAL); @@ -109,7 +110,6 @@ protected: int last_status; - bool m_initialized { false }; std::string last_conn_type = "undedefined"; MachineObject* obj{ nullptr }; MachineObject* last_obj { nullptr }; diff --git a/src/slic3r/GUI/CalibrationWizard.cpp b/src/slic3r/GUI/CalibrationWizard.cpp index f80562578d..fa42143f06 100644 --- a/src/slic3r/GUI/CalibrationWizard.cpp +++ b/src/slic3r/GUI/CalibrationWizard.cpp @@ -130,6 +130,15 @@ CalibrationWizard::~CalibrationWizard() ; } +void CalibrationWizard::add_page_step(CalibrationWizardPageStep*& step, std::function make) +{ + add_build_step([this, &step, make = std::move(make)] { + step = new CalibrationWizardPageStep(make()); + m_all_pages_sizer->Add(step->page, 1, wxEXPAND | wxALL, FromDIP(25)); + step->page->Hide(); + }); +} + void CalibrationWizard::on_cali_job_finished(wxCommandEvent& event) { this->on_cali_job_finished(event.GetString()); @@ -520,33 +529,28 @@ void PressureAdvanceWizard::on_cali_job_finished(wxString evt_data) void PressureAdvanceWizard::create_pages() { - start_step = new CalibrationWizardPageStep(new CalibrationPAStartPage(m_scrolledWindow)); - preset_step = new CalibrationWizardPageStep(new CalibrationPresetPage(m_scrolledWindow, m_mode, false)); - cali_step = new CalibrationWizardPageStep(new CalibrationCaliPage(m_scrolledWindow, m_mode)); - save_step = new CalibrationWizardPageStep(new CalibrationPASavePage(m_scrolledWindow)); + add_page_step(start_step, [this] { return new CalibrationPAStartPage(m_scrolledWindow); }); + add_page_step(preset_step, [this] { return new CalibrationPresetPage(m_scrolledWindow, m_mode, false); }); + add_page_step(cali_step, [this] { return new CalibrationCaliPage(m_scrolledWindow, m_mode); }); + add_page_step(save_step, [this] { return new CalibrationPASavePage(m_scrolledWindow); }); - m_all_pages_sizer->Add(start_step->page, 1, wxEXPAND | wxALL, FromDIP(25)); - m_all_pages_sizer->Add(preset_step->page, 1, wxEXPAND | wxALL, FromDIP(25)); - m_all_pages_sizer->Add(cali_step->page, 1, wxEXPAND | wxALL, FromDIP(25)); - m_all_pages_sizer->Add(save_step->page, 1, wxEXPAND | wxALL, FromDIP(25)); + add_build_step([this] { + m_page_steps.push_back(start_step); + m_page_steps.push_back(preset_step); + m_page_steps.push_back(cali_step); + m_page_steps.push_back(save_step); + for (int i = 0; i < m_page_steps.size() -1; i++) { + m_page_steps[i]->chain(m_page_steps[i+1]); + } - m_page_steps.push_back(start_step); - m_page_steps.push_back(preset_step); - m_page_steps.push_back(cali_step); - m_page_steps.push_back(save_step); + for (int i = 0; i < m_page_steps.size(); i++) { + m_page_steps[i]->page->Bind(EVT_CALI_ACTION, &PressureAdvanceWizard::on_cali_action, this); + } - for (int i = 0; i < m_page_steps.size() -1; i++) { - m_page_steps[i]->chain(m_page_steps[i+1]); - } - - for (int i = 0; i < m_page_steps.size(); i++) { - m_page_steps[i]->page->Hide(); - m_page_steps[i]->page->Bind(EVT_CALI_ACTION, &PressureAdvanceWizard::on_cali_action, this); - } - - if (!m_page_steps.empty()) - show_step(m_page_steps.front()); + if (!m_page_steps.empty()) + show_step(m_page_steps.front()); + }); } void PressureAdvanceWizard::on_cali_action(wxCommandEvent& evt) @@ -1053,59 +1057,46 @@ FlowRateWizard::FlowRateWizard(wxWindow* parent, wxWindowID id, const wxPoint& p void FlowRateWizard::create_pages() { - start_step = new CalibrationWizardPageStep(new CalibrationFlowRateStartPage(m_scrolledWindow)); - preset_step = new CalibrationWizardPageStep(new CalibrationPresetPage(m_scrolledWindow, m_mode, false)); + add_page_step(start_step, [this] { return new CalibrationFlowRateStartPage(m_scrolledWindow); }); + add_page_step(preset_step, [this] { return new CalibrationPresetPage(m_scrolledWindow, m_mode, false); }); // manual - cali_coarse_step = new CalibrationWizardPageStep(new CalibrationCaliPage(m_scrolledWindow, m_mode, CaliPageType::CALI_PAGE_CALI)); - coarse_save_step = new CalibrationWizardPageStep(new CalibrationFlowCoarseSavePage(m_scrolledWindow)); - cali_fine_step = new CalibrationWizardPageStep(new CalibrationCaliPage(m_scrolledWindow, m_mode, CaliPageType::CALI_PAGE_FINE_CALI)); - fine_save_step = new CalibrationWizardPageStep(new CalibrationFlowFineSavePage(m_scrolledWindow)); + add_page_step(cali_coarse_step, [this] { return new CalibrationCaliPage(m_scrolledWindow, m_mode, CaliPageType::CALI_PAGE_CALI); }); + add_page_step(coarse_save_step, [this] { return new CalibrationFlowCoarseSavePage(m_scrolledWindow); }); + add_page_step(cali_fine_step, [this] { return new CalibrationCaliPage(m_scrolledWindow, m_mode, CaliPageType::CALI_PAGE_FINE_CALI); }); + add_page_step(fine_save_step, [this] { return new CalibrationFlowFineSavePage(m_scrolledWindow); }); // auto - cali_step = new CalibrationWizardPageStep(new CalibrationCaliPage(m_scrolledWindow, m_mode)); - save_step = new CalibrationWizardPageStep(new CalibrationFlowX1SavePage(m_scrolledWindow)); + add_page_step(cali_step, [this] { return new CalibrationCaliPage(m_scrolledWindow, m_mode); }); + add_page_step(save_step, [this] { return new CalibrationFlowX1SavePage(m_scrolledWindow); }); - m_all_pages_sizer->Add(start_step->page, 1, wxEXPAND | wxALL, FromDIP(25)); - m_all_pages_sizer->Add(preset_step->page, 1, wxEXPAND | wxALL, FromDIP(25)); - m_all_pages_sizer->Add(cali_coarse_step->page, 1, wxEXPAND | wxALL, FromDIP(25)); - m_all_pages_sizer->Add(coarse_save_step->page, 1, wxEXPAND | wxALL, FromDIP(25)); - m_all_pages_sizer->Add(cali_fine_step->page, 1, wxEXPAND | wxALL, FromDIP(25)); - m_all_pages_sizer->Add(fine_save_step->page, 1, wxEXPAND | wxALL, FromDIP(25)); + add_build_step([this] { + m_page_steps.push_back(start_step); + m_page_steps.push_back(preset_step); + m_page_steps.push_back(cali_coarse_step); + m_page_steps.push_back(coarse_save_step); + m_page_steps.push_back(cali_fine_step); + m_page_steps.push_back(fine_save_step); - m_all_pages_sizer->Add(cali_step->page, 1, wxEXPAND | wxALL, FromDIP(25)); - m_all_pages_sizer->Add(save_step->page, 1, wxEXPAND | wxALL, FromDIP(25)); + //m_page_steps.push_back(cali_step); + //m_page_steps.push_back(save_step); - m_page_steps.push_back(start_step); - m_page_steps.push_back(preset_step); - m_page_steps.push_back(cali_coarse_step); - m_page_steps.push_back(coarse_save_step); - m_page_steps.push_back(cali_fine_step); - m_page_steps.push_back(fine_save_step); + for (int i = 0; i < m_page_steps.size() - 1; i++) { + m_page_steps[i]->chain(m_page_steps[i + 1]); + } - //m_page_steps.push_back(cali_step); - //m_page_steps.push_back(save_step); + for (int i = 0; i < m_page_steps.size(); i++) { + m_page_steps[i]->page->Bind(EVT_CALI_ACTION, &FlowRateWizard::on_cali_action, this); + } - for (int i = 0; i < m_page_steps.size() - 1; i++) { - m_page_steps[i]->chain(m_page_steps[i + 1]); - } + cali_step->page->Bind(EVT_CALI_ACTION, &FlowRateWizard::on_cali_action, this); + save_step->page->Bind(EVT_CALI_ACTION, &FlowRateWizard::on_cali_action, this); - // hide all pages - cali_step->page->Hide(); - save_step->page->Hide(); - for (int i = 0; i < m_page_steps.size(); i++) { - m_page_steps[i]->page->Hide(); - m_page_steps[i]->page->Bind(EVT_CALI_ACTION, &FlowRateWizard::on_cali_action, this); - } + if (!m_page_steps.empty()) + show_step(m_page_steps.front()); - - cali_step->page->Bind(EVT_CALI_ACTION, &FlowRateWizard::on_cali_action, this); - save_step->page->Bind(EVT_CALI_ACTION, &FlowRateWizard::on_cali_action, this); - - if (!m_page_steps.empty()) - show_step(m_page_steps.front()); - - set_cali_method(CalibrationMethod::CALI_METHOD_MANUAL); + set_cali_method(CalibrationMethod::CALI_METHOD_MANUAL); + }); } void FlowRateWizard::on_cali_action(wxCommandEvent& evt) diff --git a/src/slic3r/GUI/CalibrationWizard.hpp b/src/slic3r/GUI/CalibrationWizard.hpp index d72d5fcb7b..89c515eb80 100644 --- a/src/slic3r/GUI/CalibrationWizard.hpp +++ b/src/slic3r/GUI/CalibrationWizard.hpp @@ -9,6 +9,7 @@ #include "CalibrationWizardPresetPage.hpp" #include "CalibrationWizardCaliPage.hpp" #include "CalibrationWizardSavePage.hpp" +#include "StagedBuild.hpp" namespace Slic3r { namespace GUI { @@ -36,7 +37,7 @@ struct ConfigIndexValue int index{0}; }; -class CalibrationWizard : public wxPanel { +class CalibrationWizard : public wxPanel, public StagedBuild { public: CalibrationWizard(wxWindow* parent, CalibMode mode, wxWindowID id = wxID_ANY, @@ -79,6 +80,9 @@ public: protected: void on_cali_go_home(); + // Queues a page as a build step, created hidden and added to the pages sizer. + void add_page_step(CalibrationWizardPageStep*& step, std::function make); + protected: /* wx widgets*/ wxScrolledWindow* m_scrolledWindow; diff --git a/src/slic3r/GUI/CalibrationWizardCaliPage.cpp b/src/slic3r/GUI/CalibrationWizardCaliPage.cpp index 152f46f37c..2dc766dca3 100644 --- a/src/slic3r/GUI/CalibrationWizardCaliPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardCaliPage.cpp @@ -192,8 +192,10 @@ void CalibrationCaliPage::update(MachineObject* obj) set_cali_img(); } + // A calibration can run before the Device tab is ever opened, and only its status + // panel shows a print error. if (obj->print_error > 0) { - StatusPanel* status_panel = Slic3r::GUI::wxGetApp().mainframe->m_monitor->get_status_panel(); + StatusPanel* status_panel = MonitorPanel::ensure()->get_status_panel(); status_panel->obj = obj; status_panel->update_error_message(); } diff --git a/src/slic3r/GUI/DeviceErrorDialog.cpp b/src/slic3r/GUI/DeviceErrorDialog.cpp index 2a55d77d24..9790db67bd 100644 --- a/src/slic3r/GUI/DeviceErrorDialog.cpp +++ b/src/slic3r/GUI/DeviceErrorDialog.cpp @@ -538,7 +538,8 @@ void DeviceErrorDialog::on_button_click(ActionButton btn_id) break; } case DeviceErrorDialog::CHECK_ASSISTANT: { - wxGetApp().mainframe->m_monitor->jump_to_HMS(); // go to assistant page + if (MonitorPanel* monitor = MonitorPanel::if_built()) + monitor->jump_to_HMS(); // go to assistant page break; } case DeviceErrorDialog::FILAMENT_EXTRUDED: { @@ -568,7 +569,8 @@ void DeviceErrorDialog::on_button_click(ActionButton btn_id) } case DeviceErrorDialog::JUMP_TO_LIVEVIEW: { Slic3r::GUI::wxGetApp().mainframe->jump_to_monitor(); - Slic3r::GUI::wxGetApp().mainframe->m_monitor->jump_to_LiveView(); + if (MonitorPanel* monitor = MonitorPanel::if_built()) + monitor->jump_to_LiveView(); break; } case DeviceErrorDialog::NO_REMINDER_NEXT_TIME: { @@ -618,7 +620,8 @@ void DeviceErrorDialog::on_button_click(ActionButton btn_id) } case DeviceErrorDialog::OK_JUMP_RACK: { Slic3r::GUI::wxGetApp().mainframe->jump_to_monitor(); - Slic3r::GUI::wxGetApp().mainframe->m_monitor->jump_to_Rack(); + if (MonitorPanel* monitor = MonitorPanel::if_built()) + monitor->jump_to_Rack(); break; } case DeviceErrorDialog::ABORT: { diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp index 44394866c8..00d4c3e809 100644 --- a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp @@ -934,7 +934,8 @@ void wgtDeviceNozzleRackNozzleItem::OnBtnNozzleStatus(wxMouseEvent& evt) dlg.AddButton(wxID_OK,_L("Jump to the upgrade page"), true); if (dlg.ShowModal() == wxID_OK) { - wxGetApp().mainframe->m_monitor->jump_to_Upgrade(); + if (MonitorPanel* monitor = MonitorPanel::if_built()) + monitor->jump_to_Upgrade(); }; } } diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.cpp b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.cpp index fac14e31d9..eae377c147 100644 --- a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.cpp +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.cpp @@ -344,7 +344,8 @@ void wgtDeviceNozzleRackHotendUpdate::OnStatusIconClick(wxMouseEvent& event) if (dlg.ShowModal() == wxID_OK) { - wxGetApp().mainframe->m_monitor->jump_to_Upgrade(); + if (MonitorPanel* monitor = MonitorPanel::if_built()) + monitor->jump_to_Upgrade(); wxCommandEvent evt(wxEVT_NOZZLE_JUMP_UPGRADE, GetId()); evt.SetEventObject(this); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 6b9b977219..1a153ed4fb 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -863,7 +863,7 @@ void GUI_App::post_init() mainframe->Freeze(); #endif plater_->canvas3D()->enable_render(false); - mainframe->select_tab(TAB_ID_PREPARE); + mainframe->select_prepare_for_gl_init(); plater_->select_view_3D("3D"); // The first render happens before the queued new_project() sets the same view. plater_->get_camera().select_view("topfront"); @@ -903,10 +903,10 @@ void GUI_App::post_init() BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished rendering a first frame for test"; } } - if (is_editor()) - mainframe->select_tab(TAB_ID_HOME); - if (app_config->get("default_page") == "1") + if (starts_on_prepare()) mainframe->select_tab(TAB_ID_PREPARE); + else if (is_editor()) + mainframe->select_tab(TAB_ID_HOME); #ifndef __linux__ mainframe->Thaw(); #endif @@ -915,6 +915,7 @@ void GUI_App::post_init() plater_->trigger_restore_project(1); //#endif + mainframe->prebuild_pages_when_idle(); //BBS: remove GCodeViewer as seperate APP logic /*if (this->init_params->start_as_gcodeviewer) { @@ -1929,9 +1930,9 @@ bool GUI_App::hot_reload_network_plugin() m_device_manager->add_user_subscribe(); } - if (mainframe && mainframe->m_monitor) { - mainframe->m_monitor->update_network_version_footer(); - mainframe->m_monitor->set_default(); + if (MonitorPanel* monitor = MonitorPanel::if_built()) { + monitor->update_network_version_footer(); + monitor->set_default(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": reset monitor panel"; } @@ -3422,14 +3423,16 @@ bool GUI_App::on_init_inner() } BOOST_LOG_TRIVIAL(info) << "create the main window"; mainframe = new MainFrame(); - // hide settings tabs after first Layout if (is_editor()) { - mainframe->select_tab(TAB_ID_HOME); + if (starts_on_prepare()) { + mainframe->select_tab(TAB_ID_PREPARE); + } else { + mainframe->select_tab(TAB_ID_HOME); + } } sidebar().obj_list()->init(); //sidebar().aux_list()->init_auxiliary(); - mainframe->m_project->init_auxiliary(); // update_mode(); // !!! do that later SetTopWindow(mainframe); @@ -4140,13 +4143,13 @@ void GUI_App::select_machine(const std::string& agent_id) // Use MonitorPanel::select_machine() to trigger full selection flow // This reuses existing logic for machine switching (UI updates, callbacks, etc.) - if (mainframe && mainframe->m_monitor) { - mainframe->m_monitor->select_machine(dev_id); + if (MonitorPanel* monitor = MonitorPanel::if_built()) { + monitor->select_machine(dev_id); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": triggered select_machine for dev_id=" << dev_id; - } else { - // Fallback if MonitorPanel not available - m_device_manager->set_selected_machine(dev_id); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": fallback set_selected_machine dev_id=" << dev_id; + } else if (m_device_manager->set_selected_machine(dev_id)) { + // The Device tab's own state is set when the tab is built. + MonitorPanel::on_machine_selected(m_device_manager->get_selected_machine()); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": set_selected_machine dev_id=" << dev_id; } } @@ -4670,6 +4673,7 @@ void GUI_App::recreate_GUI(const wxString &msg_name) //BBS: trigger restore project logic here, and skip confirm plater_->trigger_restore_project(1); + mainframe->prebuild_pages_when_idle(); // #ys_FIXME_delete_after_testing Do we still need this ? // CallAfter([]() { @@ -5011,7 +5015,8 @@ void GUI_App::get_login_info(const std::string& provider/* = ORCA_CLOUD_PROVIDER wxString strJS = wxString::Format("window.postMessage(%s)", from_u8(logout_cmd)); GUI::wxGetApp().run_script(strJS); } - mainframe->m_webview->SetLoginPanelVisibility(true); + if (WebViewPanel* home = WebViewPanel::if_built()) + home->SetLoginPanelVisibility(true); } } @@ -5143,9 +5148,9 @@ std::string GUI_App::handle_web_request(std::string cmd) "homepage_bambu_login_or_register", }; if (app_config->get_stealth_mode() && stealth_blocked_info_commands.count(command_str)) { - CallAfter([this] { - if (mainframe && mainframe->m_webview) - mainframe->m_webview->SendCloudProvidersInfo(); + CallAfter([] { + if (WebViewPanel* home = WebViewPanel::if_built()) + home->SendCloudProvidersInfo(); }); return ""; } @@ -5159,8 +5164,8 @@ std::string GUI_App::handle_web_request(std::string cmd) if (dlg.ShowModal() == wxID_OK) { app_config->set_bool("stealth_mode", false); app_config->save(); - if (mainframe && mainframe->m_webview) - mainframe->m_webview->SendCloudProvidersInfo(); + if (WebViewPanel* home = WebViewPanel::if_built()) + home->SendCloudProvidersInfo(); // Continue with login if (command_str == "homepage_login_or_register") this->request_login(true); @@ -5241,8 +5246,8 @@ std::string GUI_App::handle_web_request(std::string cmd) } else if (command_str.compare("get_recent_projects") == 0) { if (mainframe) { - if (mainframe->m_webview) { - mainframe->m_webview->SendRecentList(INT_MAX); + if (WebViewPanel* home = WebViewPanel::if_built()) { + home->SendRecentList(INT_MAX); } } } @@ -7775,8 +7780,8 @@ void GUI_App::on_stealth_mode_enter() BOOST_LOG_TRIVIAL(info) << "logout: on_stealth_mode_enter"; request_user_logout(ORCA_CLOUD_PROVIDER); request_user_logout(BBL_CLOUD_PROVIDER); - if (mainframe && mainframe->m_webview) { - mainframe->m_webview->SendCloudProvidersInfo(); + if (WebViewPanel* home = WebViewPanel::if_built()) { + home->SendCloudProvidersInfo(); } } @@ -8186,6 +8191,24 @@ ConfigOptionMode GUI_App::get_saved_mode() return saved_mode_from_string(app_config->get("user_mode")); } +bool GUI_App::starts_on_prepare() const +{ + return app_config->get("default_page") == "1"; +} + +int GUI_App::input_idle_ms() const +{ + return int(std::chrono::duration_cast(std::chrono::steady_clock::now() - m_last_input).count()); +} + +// Every wxCommandEvent claims the user-input category, so only real mouse and key events count. +int GUI_App::FilterEvent(wxEvent& event) +{ + if (!event.IsCommandEvent() && (event.GetEventCategory() & wxEVT_CATEGORY_USER_INPUT)) + m_last_input = std::chrono::steady_clock::now(); + return Event_Skip; +} + ConfigOptionMode GUI_App::get_mode() { return app_config->get_bool("developer_mode") ? comDevelop : get_saved_mode(); @@ -8237,9 +8260,10 @@ void GUI_App::update_mode() mainframe->m_param_panel->update_mode(); if (mainframe->m_param_dialog) mainframe->m_param_dialog->panel()->update_mode(); - if (mainframe->m_printer_view) - mainframe->m_printer_view->update_mode(); - mainframe->m_webview->update_mode(); + if (PrinterWebView* view = PrinterWebView::if_built()) + view->update_mode(); + if (WebViewPanel* home = WebViewPanel::if_built()) + home->update_mode(); #ifdef _MSW_DARK_MODE if (!wxGetApp().tabs_as_menu()) @@ -8257,9 +8281,10 @@ void GUI_App::update_mode() } void GUI_App::update_internal_development() { - mainframe->m_webview->update_mode(); - if (mainframe->m_printer_view) - mainframe->m_printer_view->update_mode(); + if (WebViewPanel* home = WebViewPanel::if_built()) + home->update_mode(); + if (PrinterWebView* view = PrinterWebView::if_built()) + view->update_mode(); } void GUI_App::show_ip_address_enter_dialog(wxString title) diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 2d55c5e96c..e888a52b06 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -1,6 +1,7 @@ #ifndef slic3r_GUI_App_hpp_ #define slic3r_GUI_App_hpp_ +#include #include #include #include @@ -248,6 +249,7 @@ private: bool m_app_conf_exists{ false }; EAppMode m_app_mode{ EAppMode::Editor }; bool m_is_recreating_gui{ false }; + std::chrono::steady_clock::time_point m_last_input{ std::chrono::steady_clock::now() }; #ifdef __linux__ bool m_opengl_initialized{ false }; #endif @@ -387,6 +389,11 @@ public: bool is_editor() const { return m_app_mode == EAppMode::Editor; } bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; } bool is_recreating_gui() const { return m_is_recreating_gui; } + // Milliseconds since the last mouse or keyboard event the app processed. + int input_idle_ms() const; + int FilterEvent(wxEvent& event) override; + // The Preferences "Default page" choice, stored as its index: 0 Home, 1 Prepare. + bool starts_on_prepare() const; std::string logo_name() const { return is_editor() ? "OrcaSlicer" : "OrcaSlicer-gcodeviewer"; } bool is_closing() const { return m_is_closing.load(std::memory_order_acquire); } diff --git a/src/slic3r/GUI/HMSPanel.cpp b/src/slic3r/GUI/HMSPanel.cpp index cd3d35bfc8..c2a8314bd5 100644 --- a/src/slic3r/GUI/HMSPanel.cpp +++ b/src/slic3r/GUI/HMSPanel.cpp @@ -122,7 +122,8 @@ HMSNotifyItem::HMSNotifyItem(const std::string& dev_id, wxWindow *parent, DevHMS m_hms_content->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) { wxCommandEvent evt(EVT_ALREADY_READ_HMS); evt.SetString(long_error_code); - wxPostEvent(wxGetApp().mainframe->m_monitor, evt); + if (MonitorPanel* monitor = MonitorPanel::if_built()) + wxPostEvent(monitor, evt); if (!m_url.empty()) wxLaunchDefaultBrowser(m_url); }); diff --git a/src/slic3r/GUI/IdleScheduler.cpp b/src/slic3r/GUI/IdleScheduler.cpp new file mode 100644 index 0000000000..2b71a9f8e6 --- /dev/null +++ b/src/slic3r/GUI/IdleScheduler.cpp @@ -0,0 +1,100 @@ +#include "IdleScheduler.hpp" + +#include + +#include + +#include "libslic3r/Utils.hpp" + +#ifdef _WIN32 +#include +#endif + +namespace Slic3r { namespace GUI { + +namespace { + +// The timer's period while waiting for the user to go quiet. +constexpr int tick_ms = 250; +// Input-free time before a slice may start, about a double-click interval, so a user +// mid-gesture is left alone. +constexpr int quiet_ms = 500; +// Budget of one slice. The next slice is a timer message, so paint, timers and input +// queued meanwhile are handled first; a click waits at most a slice plus the unit that +// overran it. +constexpr int slice_ms = 40; +// On GTK a due timer runs ahead of repaints and posted events, so the next slice waits a few ms. +#ifdef __WXGTK__ +constexpr int next_slice_ms = 5; +#else +constexpr int next_slice_ms = 0; +#endif + +// True when unhandled keyboard, button, touch or pen input is queued; only Windows can ask. +bool input_pending() +{ +#ifdef _WIN32 + // Windows synthesizes a mouse move whenever a window appears under the cursor, so those + // do not count. + return (GetQueueStatus(QS_INPUT & ~QS_MOUSEMOVE) >> 16) != 0; +#else + return false; +#endif +} + +} // namespace + +IdleScheduler::IdleScheduler(std::function input_idle_ms) : m_input_idle_ms(std::move(input_idle_ms)) +{ + m_timer.Bind(wxEVT_TIMER, [this](wxTimerEvent&) { tick(); }); +} + +void IdleScheduler::start() +{ + if (!m_timer.IsRunning()) + m_timer.Start(tick_ms); +} + +void IdleScheduler::stop() +{ + m_timer.Stop(); +} + +void IdleScheduler::tick() +{ + // A unit that pumps the event loop lets the timer fire inside its own slice. + if (m_in_slice) + return; + if (!m_queue.pending()) { + stop(); + return; + } + if (m_input_idle_ms() < quiet_ms || input_pending()) { + start(); + return; + } + const auto now_ms = [] { + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + }; + const char* const fn = __FUNCTION__; + m_in_slice = true; + PrebuildQueue::Slice slice; + { + ScopeGuard in_slice([this] { m_in_slice = false; }); + slice = m_queue.run_slice(slice_ms, now_ms, input_pending, [fn](const std::string& name, long long ms) { + BOOST_LOG_TRIVIAL(debug) << fn << ": " << name << ": unit took " << ms << " ms"; + }); + } + if (slice.completed) + BOOST_LOG_TRIVIAL(info) << fn << ": task complete: " << slice.name << ", " << slice.units << " unit(s) in " << slice.ms << " ms this slice"; + else + BOOST_LOG_TRIVIAL(debug) << fn << ": " << slice.name << ": " << slice.units << " unit(s) in " << slice.ms << " ms, yielding"; + if (!slice.remaining) + stop(); + else if (input_pending()) + m_timer.Start(tick_ms); + else + m_timer.StartOnce(next_slice_ms); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/IdleScheduler.hpp b/src/slic3r/GUI/IdleScheduler.hpp new file mode 100644 index 0000000000..7c31e9414c --- /dev/null +++ b/src/slic3r/GUI/IdleScheduler.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +#include +#include + +#include "PrebuildQueue.hpp" + +namespace Slic3r { namespace GUI { + +// Runs the queue's units while the user is idle. Once the user has been idle long enough, +// slices run one per timer message until the work is done or input arrives, so the event +// loop handles what it has between slices. Main thread only. +class IdleScheduler +{ +public: + // input_idle_ms reports how long ago the user last touched the mouse or keyboard. + explicit IdleScheduler(std::function input_idle_ms); + + void add(LazyBase& task) { m_queue.add(task); } + void clear() { m_queue.clear(); } + // The task names in queue order, for a log line. + std::string names() const { return m_queue.names(); } + + // Arms the timer; it stops itself once no task is pending, so call again when a task + // appears. + void start(); + void stop(); + +private: + void tick(); + + PrebuildQueue m_queue; + wxTimer m_timer; + std::function m_input_idle_ms; + bool m_in_slice{ false }; +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Lazy.cpp b/src/slic3r/GUI/Lazy.cpp new file mode 100644 index 0000000000..2a1a74510a --- /dev/null +++ b/src/slic3r/GUI/Lazy.cpp @@ -0,0 +1,27 @@ +#include "Lazy.hpp" + +#include +#include +#include + +namespace Slic3r { namespace GUI { + +LazyBase::OnDemandBuild::OnDemandBuild(const LazyBase& lazy) : m_lazy(lazy), m_started(std::chrono::steady_clock::now()) +{ + // The unit tests have no app. + if (wxTheApp != nullptr) + m_busy = std::make_unique(); +} + +LazyBase::OnDemandBuild::~OnDemandBuild() +{ + BOOST_LOG_TRIVIAL(info) << "Lazy::ensure: built " << m_lazy.name() << " on demand, " << m_units << " unit(s) in " + << std::chrono::duration_cast(std::chrono::steady_clock::now() - m_started).count() << " ms"; +} + +void LazyBase::log_null_factory(const std::string& name) +{ + BOOST_LOG_TRIVIAL(error) << "Lazy::build_step: the factory for " << name << " returned null"; +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Lazy.hpp b/src/slic3r/GUI/Lazy.hpp new file mode 100644 index 0000000000..33e092d887 --- /dev/null +++ b/src/slic3r/GUI/Lazy.hpp @@ -0,0 +1,198 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "StagedBuild.hpp" + +class wxBusyCursor; + +// Deferred construction of one object. Lazy holds the factory and builds the object on +// demand or, unit by unit, from an IdleScheduler; LazyBase is what the scheduler sees of +// it; LazyInstance gives a type with one such object app-wide static access to it. +// docs/HLSD/deferred-page-construction.md covers the design. + +namespace Slic3r { namespace GUI { + +template class LazyInstance; + +// A prebuild task: what PrebuildQueue sees of a Lazy. A type with its own unit sequence +// implements it directly. +class LazyBase +{ +public: + virtual ~LazyBase() = default; + + // Labels the object in logs. + virtual const std::string& name() const = 0; + virtual bool built() const = 0; + // Whether the idle prebuild has work here. + virtual bool pending() const { return !built(); } + // Runs one unit and returns true while more remain. + virtual bool build_step() = 0; + // Position in the idle queue: lower builds first, negative is never prebuilt. + virtual int prebuild_order() const = 0; + +protected: + // Busy cursor around a build the user is waiting on, and a log line after it. + class OnDemandBuild + { + public: + explicit OnDemandBuild(const LazyBase& lazy); + ~OnDemandBuild(); + void unit() { ++m_units; } + + private: + const LazyBase& m_lazy; + std::unique_ptr m_busy; + std::chrono::steady_clock::time_point m_started; + int m_units{ 0 }; + }; + + static void log_null_factory(const std::string& name); +}; + +// Holds an object a factory makes on the first build_step() or ensure(), followed by one +// StagedBuild step per unit if the type has them, and keeps callbacks until the object is +// complete. The object's parent owns it, not the holder. A LazyInstance type is registered +// for its statics; any other type is reached only through the holder. +template +class Lazy : public LazyBase +{ +public: + using Factory = std::function; + + Lazy(std::string name, int order, Factory make) : m_name(std::move(name)), m_order(order), m_make(std::move(make)) + { + if constexpr (registered()) + LazyInstance::s_lazy = this; + } + ~Lazy() override + { + if constexpr (registered()) + if (LazyInstance::s_lazy == this) + LazyInstance::s_lazy = nullptr; + } + Lazy(const Lazy&) = delete; + Lazy& operator=(const Lazy&) = delete; + + // Null until completely built. + T* get() const { return built() ? m_object : nullptr; } + + // Builds whatever is left now and returns the object, null if the factory returned null + // or a nested call finds the object mid-build. + T* ensure() + { + if (!built() && !m_building) { + OnDemandBuild build(*this); + do + build.unit(); + while (build_step()); + } + return get(); + } + + // Runs fn on the object now if it is built, otherwise once it is. + void when_built(std::function fn) + { + if (built()) + fn(*m_object); + else + m_deferred.push_back(std::move(fn)); + } + + const std::string& name() const override { return m_name; } + // Read by a worker thread through the statics; the last write in build_step() releases it. + bool built() const override { return m_complete.load(std::memory_order_acquire); } + bool pending() const override { return !built() && !m_failed; } + int prebuild_order() const override { return m_order; } + + // The factory is the first unit, then one StagedBuild step each. A unit that pumps the + // event loop cannot re-enter; a nested call does nothing. + bool build_step() override + { + if (built() || m_building || m_failed) + return false; + m_building = true; + try { + if (m_object == nullptr) + m_object = m_make(); + else + step(); + } catch (...) { + m_building = false; + throw; + } + m_building = false; + if (m_object == nullptr) { + m_failed = true; + log_null_factory(m_name); + return false; + } + if (steps_remain()) + return true; + m_complete.store(true, std::memory_order_release); + for (auto& fn : m_deferred) + fn(*m_object); + m_deferred.clear(); + return false; + } + +private: + static constexpr bool registered() { return std::is_base_of_v, T>; } + static constexpr bool staged() { return std::is_base_of_v; } + + bool steps_remain() const + { + if constexpr (staged()) + return !m_object->built(); + else + return false; + } + void step() + { + if constexpr (staged()) + m_object->build_step(); + } + + std::string m_name; + int m_order; + Factory m_make; + T* m_object{ nullptr }; + std::atomic m_complete{ false }; + bool m_building{ false }; + bool m_failed{ false }; + std::vector> m_deferred; +}; + +// Mixin for a type with one lazily built instance in the app. The statics reach that +// instance through its Lazy holder (a recreated MainFrame's holder replaces the old +// frame's). +template +class LazyInstance +{ +public: + // Null until completely built. + static Self* if_built() { return s_lazy ? s_lazy->get() : nullptr; } + // Builds the object if needed; null while no holder exists or the holder has no + // object. + static Self* ensure() { return s_lazy ? s_lazy->ensure() : nullptr; } + // Runs fn on the object now if it is built, otherwise once it is. + static void when_built(std::function fn) + { + if (s_lazy) + s_lazy->when_built(std::move(fn)); + } + +private: + friend class Lazy; + inline static Lazy* s_lazy{ nullptr }; +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/LazyPage.cpp b/src/slic3r/GUI/LazyPage.cpp new file mode 100644 index 0000000000..d14947bd80 --- /dev/null +++ b/src/slic3r/GUI/LazyPage.cpp @@ -0,0 +1,14 @@ +#include "LazyPage.hpp" + +#include "GUI_App.hpp" + +namespace Slic3r { namespace GUI { + +void apply_dark_ui_to_lazy_panel(wxWindow* panel) +{ +#ifdef _MSW_DARK_MODE + wxGetApp().UpdateDarkUIWin(panel); +#endif +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/LazyPage.hpp b/src/slic3r/GUI/LazyPage.hpp new file mode 100644 index 0000000000..fc85f69285 --- /dev/null +++ b/src/slic3r/GUI/LazyPage.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include "Lazy.hpp" + +namespace Slic3r { namespace GUI { + +// Applies the app's dark-mode pass to a panel built after the frame's own pass ran. +void apply_dark_ui_to_lazy_panel(wxWindow* panel); + +// A page whose panel is built the first time the page is shown, or earlier by an +// IdleScheduler; nothing builds while the frame is still hidden. The Lazy side holds the +// panel and, for a LazyInstance panel type, registers it for the type's statics. +template +class LazyPage : public wxPanel, public Lazy +{ +public: + using Factory = std::function; + + LazyPage(wxWindow* parent, std::string name, int order, Factory make = [](wxWindow* parent) { return new Panel(parent); }) + : wxPanel(parent), Lazy(std::move(name), order, [this, make = std::move(make)] { + Panel* panel = make(this); + GetSizer()->Add(panel, 1, wxEXPAND); + // Hidden while its page is, as an inserted page would be. + if (!IsShown()) + panel->Hide(); + return panel; + }) + { + SetSizer(new wxBoxSizer(wxVERTICAL)); + // Shown by the book when its tab is selected. + Hide(); + this->when_built([this](Panel& panel) { + apply_dark_ui_to_lazy_panel(&panel); + Layout(); + }); + } + + // The parent book currently lists this page. + bool in_book() const + { + auto* book = dynamic_cast(GetParent()); + return book != nullptr && book->FindPage(this) != wxNOT_FOUND; + } + + // Pending only while the tab is in the book. + bool pending() const override { return in_book() && Lazy::pending(); } + + // Forwarded so the panel's own Show() override stays its activation hook; wx calls this + // virtual from the book's ShowWithEffect() only for wxSHOW_EFFECT_NONE, the default. + bool Show(bool show = true) override + { + const bool changed = wxPanel::Show(show); + if (show) { + // The book shows its first page as it is inserted, before startup has chosen the + // start page, so a hidden frame builds nothing; MainFrame::Show() completes it. + if (this->built() || wxGetTopLevelParent(this)->IsShown()) { + if (Panel* panel = this->ensure()) + panel->Show(true); + // The sizer skipped the panel while the book kept it hidden. + Layout(); + } + } else if (Panel* panel = this->get()) { + panel->Show(false); + } + return changed; + } +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index ca0ef95a76..ec4428a32a 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -317,7 +317,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ // BBS , m_recent_projects(18) , m_settings_dialog(this) - , diff_dialog(this) + , m_idle([] { return wxGetApp().input_idle_ms(); }) + , m_diff_dialog("compare_presets", 100, [this] { return make_diff_dialog(); }) { #ifdef __WXOSX__ set_miniaturizable(GetHandle()); @@ -737,9 +738,6 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ wxGetApp().persist_window_geometry(this, true); wxGetApp().persist_window_geometry(&m_settings_dialog, true); - // bind events from DiffDlg - - bind_diff_dialog(); } bool MainFrame::handle_global_shortcut(const KeyChord& chord) @@ -832,8 +830,10 @@ bool MainFrame::handle_global_shortcut(const KeyChord& chord) return true; } -void MainFrame::bind_diff_dialog() +DiffPresetDialog* MainFrame::make_diff_dialog() { + auto* dialog = new DiffPresetDialog(this); + auto get_tab = [](Preset::Type type) { Tab* null_tab = nullptr; for (Tab* tab : wxGetApp().tabs_list) @@ -842,23 +842,24 @@ void MainFrame::bind_diff_dialog() return null_tab; }; - auto transfer = [this, get_tab](Preset::Type type) { - get_tab(type)->transfer_options(diff_dialog.get_left_preset_name(type), - diff_dialog.get_right_preset_name(type), - diff_dialog.get_selected_options(type)); + auto transfer = [dialog, get_tab](Preset::Type type) { + get_tab(type)->transfer_options(dialog->get_left_preset_name(type), + dialog->get_right_preset_name(type), + dialog->get_selected_options(type)); }; - auto process_options = [this](std::function process) { - const Preset::Type diff_dlg_type = diff_dialog.view_type(); + auto process_options = [dialog](std::function process) { + const Preset::Type diff_dlg_type = dialog->view_type(); if (diff_dlg_type == Preset::TYPE_INVALID) { - for (const Preset::Type& type : diff_dialog.types_list() ) + for (const Preset::Type& type : dialog->types_list() ) process(type); } else process(diff_dlg_type); }; - diff_dialog.Bind(EVT_DIFF_DIALOG_TRANSFER, [process_options, transfer](SimpleEvent&) { process_options(transfer); }); + dialog->Bind(EVT_DIFF_DIALOG_TRANSFER, [process_options, transfer](SimpleEvent&) { process_options(transfer); }); + return dialog; } @@ -1186,8 +1187,9 @@ void MainFrame::update_edge_panels() void MainFrame::shutdown() { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter"; - if (m_project != nullptr) - m_project->shutdown(); + m_idle.stop(); + if (ProjectPanel* project = ProjectPanel::if_built()) + project->shutdown(); m_plugin_pages.shutdown(); if (m_plater != nullptr) m_plater->remove_dock_panes(); @@ -1314,19 +1316,6 @@ void MainFrame::show_option(bool show) } } -#ifdef SLIC3R_CAD -DesignPanel* MainFrame::ensure_design_panel() -{ - if (m_design_panel == nullptr && m_design_page != nullptr) { - wxBusyCursor busy; - m_design_panel = new DesignPanel(m_design_page); - m_design_page->GetSizer()->Add(m_design_panel, 1, wxEXPAND); - m_design_page->Layout(); - } - return m_design_panel; -} -#endif - void MainFrame::init_tabpanel() { // wxNB_NOPAGETHEME: Disable Windows Vista theme for the Notebook background. The theme performance is terrible on // Windows 10 with multiple high resolution displays connected. @@ -1369,23 +1358,20 @@ void MainFrame::init_tabpanel() { // m_param_panel->OnActivate(); #ifdef SLIC3R_CAD else if (m_design_page != nullptr && panel == m_design_page) { - // Built on first activation, never at startup: the panel creates several hundred - // controls and its own GL canvas, which a user who does not open the tab should - // not pay for. - ensure_design_panel(); // Re-sync the Design bed to the active printer: the panel is built before the // printer profile is fully applied, so its bed must refresh on activation or the // grid (true bed) spills past the stale default bed quad. - m_design_panel->on_tab_shown(); + DesignPanel::ensure()->on_tab_shown(); } #endif - else if (panel == m_monitor) { + else if (panel == m_monitor_page) { //monitor } #ifdef SLIC3R_CAD // Any page that is not Design takes the Design status line down with it — see // DesignPanel::on_tab_hidden for why the popup does not follow the page on its own. - if (m_design_panel != nullptr && panel != m_design_page) m_design_panel->on_tab_hidden(); + if (DesignPanel* design = DesignPanel::if_built(); design != nullptr && panel != m_design_page) + design->on_tab_hidden(); #endif #ifndef __APPLE__ if (m_last_selected_tab == TAB_ID_PREPARE) { @@ -1401,13 +1387,14 @@ void MainFrame::init_tabpanel() { }); if (wxGetApp().is_editor()) { - m_webview = new WebViewPanel(m_tabpanel); + m_home_page = new LazyPage(m_tabpanel, TAB_ID_HOME, 10); + m_lazy_pages.push_back(m_home_page); Bind(EVT_LOAD_URL, [this](wxCommandEvent &evt) { wxString url = evt.GetString(); select_tab(TAB_ID_HOME); - m_webview->load_url(url); + WebViewPanel::ensure()->load_url(url); }); - m_tabpanel->AddPage(TAB_ID_HOME, m_webview, "", "tab_home_active"); + m_tabpanel->AddPage(TAB_ID_HOME, m_home_page, "", "tab_home_active"); m_param_panel = new ParamsPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); } @@ -1418,15 +1405,13 @@ void MainFrame::init_tabpanel() { wxGetApp().plater_ = m_plater; #ifdef SLIC3R_CAD - // Stand-in page for the Design tab. The real DesignPanel is built into it the first time - // the tab is selected (see the page-changed handler above), so nothing it constructs sits - // on the startup path. The experimental feature is off by default, and when it is off the - // page is never created, so the tab does not appear at all (the preference takes effect on - // the next start, like the other feature toggles). + // The experimental feature is off by default, and when it is off the page is never + // created, so the tab does not appear at all (the preference takes effect on the next + // start, like the other feature toggles). if (wxGetApp().is_enable_cad_feature()) { - m_design_page = new wxPanel(this); - m_design_page->SetSizer(new wxBoxSizer(wxVERTICAL)); - m_design_page->Hide(); + // Experimental and heavy enough that building it unasked would cost more than it saves. + m_design_page = new LazyPage(this, TAB_ID_DESIGN, -1); + m_lazy_pages.push_back(m_design_page); start_mcp_control_if_enabled(); // opens the MCP socket iff ORCA_CAD_MCP is set } #endif @@ -1434,33 +1419,41 @@ void MainFrame::init_tabpanel() { create_preset_tabs(); //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"), "tab_monitor_active"); + m_monitor_page = new LazyPage(m_tabpanel, TAB_ID_MONITOR, 20); + m_lazy_pages.push_back(m_monitor_page); + m_tabpanel->AddPage(TAB_ID_MONITOR, m_monitor_page, _L("Device"), "tab_monitor_active"); - m_printer_view = new PrinterWebView(m_tabpanel); + m_printer_view_page = new LazyPage(m_tabpanel, TAB_ID_MONITOR_WEB, 50, [this](wxWindow* parent) { + auto* view = new PrinterWebView(parent); + if (!m_printer_url.empty()) + view->load_url(m_printer_url, m_printer_api_key); + return view; + }); + m_lazy_pages.push_back(m_printer_view_page); Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent &evt) { - wxString url = evt.GetString(); - wxString key = evt.GetAPIkey(); //select_tab(MainFrame::tpMonitor); - m_printer_view->load_url(url, key); + m_printer_url = evt.GetString(); + m_printer_api_key = evt.GetAPIkey(); + if (PrinterWebView* view = PrinterWebView::if_built()) + view->load_url(m_printer_url, m_printer_api_key); }); - m_printer_view->Hide(); + m_multi_machine_page = new LazyPage(m_tabpanel, TAB_ID_MULTI_DEVICE, 40); + m_lazy_pages.push_back(m_multi_machine_page); if (wxGetApp().is_enable_multi_machine()) { - 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"), "tab_multi_active"); + m_tabpanel->AddPage(TAB_ID_MULTI_DEVICE, m_multi_machine_page, _L("Multi-device"), "tab_multi_active"); } - m_project = new ProjectPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); - m_project->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(TAB_ID_PROJECT, m_project, _L("Project"), "tab_auxiliary_active"); + m_project_page = new LazyPage(m_tabpanel, TAB_ID_PROJECT, 60); + m_lazy_pages.push_back(m_project_page); + m_tabpanel->AddPage(TAB_ID_PROJECT, m_project_page, _L("Project"), "tab_auxiliary_active"); - m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); - m_calibration->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), "tab_calibration_active"); + // show_device() removes this tab for printers without the Bambu device tab, and the page + // then never builds its panel. + m_calibration_page = new LazyPage(m_tabpanel, TAB_ID_CALIBRATION, 30); + m_lazy_pages.push_back(m_calibration_page); + m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration_page, _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. @@ -1495,67 +1488,44 @@ void MainFrame::show_device(bool should_use_native) { // 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_printer_view_page->Show(false); m_tabpanel->RemovePage(idx); } } if (use_printer_agents) { - if (!m_monitor) { - m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); - m_monitor->SetBackgroundColour(*wxWHITE); - } - - if (m_tabpanel->FindPage(m_monitor) == wxNOT_FOUND) { - if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND) { - m_printer_view->Show(false); + if (!m_monitor_page->in_book()) { + if ((idx = m_tabpanel->FindPage(m_printer_view_page)) != wxNOT_FOUND) { + m_printer_view_page->Show(false); m_tabpanel->RemovePage(idx); } - m_monitor->Show(false); - m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor, + m_monitor_page->Show(false); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor_page, _L("Device"), "tab_monitor_active"); } - if (m_printer_view == nullptr) { - m_printer_view = new PrinterWebView(m_tabpanel); - Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent& evt) { - wxString url = evt.GetString(); - wxString key = evt.GetAPIkey(); - // select_tab(MainFrame::tpMonitor); - m_printer_view->load_url(url, key); - }); - } - if (wxGetApp().is_enable_multi_machine()) { - if (!m_multi_machine) { - m_multi_machine = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); - m_multi_machine->SetBackgroundColour(*wxWHITE); - } // TODO: change the bitmap - if (m_tabpanel->FindPage(m_multi_machine) == wxNOT_FOUND) { - m_multi_machine->Show(false); + if (!m_multi_machine_page->in_book()) { + m_multi_machine_page->Show(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"); + TAB_ID_MULTI_DEVICE, m_multi_machine_page, _L("Multi-device"), "tab_multi_active"); } } - if (!m_calibration) { - m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); - m_calibration->SetBackgroundColour(*wxWHITE); - } - if (m_tabpanel->FindPage(m_calibration) == wxNOT_FOUND) { - m_calibration->Show(false); - m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration, + if (!m_calibration_page->in_book()) { + m_calibration_page->Show(false); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration_page, _L("Calibration"), "tab_calibration_active"); } if (want_web_device_tab) { - if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { - m_printer_view->Show(false); + if ((idx = m_tabpanel->FindPage(m_printer_view_page)) == wxNOT_FOUND) { + m_printer_view_page->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"); + m_printer_view_page, _L("Device (Web)"), "tab_monitor_active"); } else { m_tabpanel->SetPageText(idx, _L("Device (Web)")); } @@ -1567,48 +1537,37 @@ void MainFrame::show_device(bool should_use_native) { fit_tab_labels(); // ORCA on printer change m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above - + if (m_prebuild_started) + m_idle.start(); return; } if (should_use_native) { - if (m_tabpanel->FindPage(m_monitor) != wxNOT_FOUND) { + if (m_monitor_page->in_book()) { fit_tab_labels(); // ORCA on printer change - same button layout return; } // Remove printer view - if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND) { - m_printer_view->Show(false); + if ((idx = m_tabpanel->FindPage(m_printer_view_page)) != wxNOT_FOUND) { + m_printer_view_page->Show(false); m_tabpanel->RemovePage(idx); } - // Create/insert monitor page - if (!m_monitor) { - m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); - m_monitor->SetBackgroundColour(*wxWHITE); - } - m_monitor->Show(false); - m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor, + // Insert monitor page + m_monitor_page->Show(false); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor_page, _L("Device"), "tab_monitor_active"); if (wxGetApp().is_enable_multi_machine()) { - if (!m_multi_machine) { - m_multi_machine = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); - m_multi_machine->SetBackgroundColour(*wxWHITE); - } // TODO: change the bitmap - m_multi_machine->Show(false); - m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MULTI_DEVICE, m_multi_machine, + m_multi_machine_page->Show(false); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MULTI_DEVICE, m_multi_machine_page, _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); + m_calibration_page->Show(false); // Last of the built-in tabs, but plugin tabs already sit past it — anchor rather than // append, so its position doesn't depend on the relayout() below running afterwards. - m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration, + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration_page, _L("Calibration"), "tab_calibration_active"); #ifdef _MSW_DARK_MODE @@ -1616,37 +1575,30 @@ void MainFrame::show_device(bool should_use_native) { #endif // _MSW_DARK_MODE } else { - if (m_tabpanel->FindPage(m_printer_view) != wxNOT_FOUND) { + if (m_printer_view_page->in_book()) { fit_tab_labels(); // ORCA on printer change - same button layout return; } - if ((idx = m_tabpanel->FindPage(m_calibration)) != wxNOT_FOUND) { - m_calibration->Show(false); + if ((idx = m_tabpanel->FindPage(m_calibration_page)) != wxNOT_FOUND) { + m_calibration_page->Show(false); m_tabpanel->RemovePage(idx); } - if ((idx = m_tabpanel->FindPage(m_multi_machine)) != wxNOT_FOUND) { - m_multi_machine->Show(false); + if ((idx = m_tabpanel->FindPage(m_multi_machine_page)) != wxNOT_FOUND) { + m_multi_machine_page->Show(false); m_tabpanel->RemovePage(idx); } - if ((idx = m_tabpanel->FindPage(m_monitor)) != wxNOT_FOUND) { - m_monitor->Show(false); + if ((idx = m_tabpanel->FindPage(m_monitor_page)) != wxNOT_FOUND) { + m_monitor_page->Show(false); m_tabpanel->RemovePage(idx); } - if (m_printer_view == nullptr) { - m_printer_view = new PrinterWebView(m_tabpanel); - Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent& evt) { - wxString url = evt.GetString(); - wxString key = evt.GetAPIkey(); - // select_tab(MainFrame::tpMonitor); - m_printer_view->load_url(url, key); - }); - } - m_printer_view->Show(false); - m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_printer_view, + m_printer_view_page->Show(false); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_printer_view_page, _L("Device"), "tab_monitor_active"); } fit_tab_labels(); // ORCA on printer change m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above + if (m_prebuild_started) + m_idle.start(); } bool MainFrame::is_prepare_or_preview_tab() const @@ -2692,13 +2644,11 @@ void MainFrame::on_dpi_changed(const wxRect& suggested_rect) //BBS GUI refactor: remove unused layout new/dlg //if (m_layout != ESettingsLayout::Dlg) // Do not update tabs if the Settings are in the separated dialog m_param_panel->msw_rescale(); - m_project->msw_rescale(); - if(m_monitor) - m_monitor->msw_rescale(); - if(m_multi_machine) - m_multi_machine->msw_rescale(); - if(m_calibration) - m_calibration->msw_rescale(); + // A panel mid-build gets the pass once it is complete. + ProjectPanel::when_built([](ProjectPanel& project) { project.msw_rescale(); }); + MonitorPanel::when_built([](MonitorPanel& monitor) { monitor.msw_rescale(); }); + MultiMachinePage::when_built([](MultiMachinePage& multi_machine) { multi_machine.msw_rescale(); }); + CalibrationPanel::when_built([](CalibrationPanel& calibration) { calibration.msw_rescale(); }); // BBS #if 0 @@ -2752,7 +2702,8 @@ void MainFrame::on_sys_color_changed() #endif #endif - diff_dialog.on_sys_color_changed(); + if (DiffPresetDialog* dialog = DiffPresetDialog::if_built()) + dialog->on_sys_color_changed(); // BBS m_tabpanel->Rescale(); @@ -2760,10 +2711,8 @@ void MainFrame::on_sys_color_changed() // update Plater wxGetApp().plater()->sys_color_changed(); - if(m_monitor) - m_monitor->on_sys_color_changed(); - if(m_calibration) - m_calibration->on_sys_color_changed(); + MonitorPanel::when_built([](MonitorPanel& monitor) { monitor.on_sys_color_changed(); }); + CalibrationPanel::when_built([](CalibrationPanel& calibration) { calibration.on_sys_color_changed(); }); // update Tabs for (auto tab : wxGetApp().tabs_list) tab->sys_color_changed(); @@ -3696,7 +3645,8 @@ void MainFrame::set_max_recent_count(int max) } wxGetApp().app_config->set_recent_projects(recent_projects); wxGetApp().app_config->save(); - m_webview->SendRecentList(-1); + if (WebViewPanel* home = WebViewPanel::if_built()) + home->SendRecentList(-1); } } @@ -4040,23 +3990,56 @@ void MainFrame::select_tab(wxPanel* panel) select_tab(page_name); } +// Selects the Prepare page without the page-changed event, so the GL canvas is on screen +// and nothing else is built for the pass. +void MainFrame::select_prepare_for_gl_init() +{ + m_tabpanel->ChangeSelection(m_tabpanel->FindPageByName(TAB_ID_PREPARE)); +} + +// The book shows its first page as it is inserted, while the frame is hidden and nothing +// may build; the first show completes that page. +bool MainFrame::Show(bool show) +{ + const bool changed = DPIFrame::Show(show); + if (show && changed && m_tabpanel != nullptr) + if (wxWindow* page = m_tabpanel->GetCurrentPage()) + page->Show(true); + return changed; +} + +// A page out of the book stays registered and is passed over; a negative order is never +// registered. +void MainFrame::prebuild_pages_when_idle() +{ + m_idle.clear(); + if (m_param_panel) + m_idle.add(m_param_panel->settings_page_prebuild()); + for (LazyBase* page : m_lazy_pages) + if (page->prebuild_order() >= 0) + m_idle.add(*page); + m_idle.add(m_diff_dialog); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": prebuild queue: " << m_idle.names(); + m_idle.start(); + m_prebuild_started = true; +} + //BBS void MainFrame::jump_to_monitor(std::string dev_id) { - if(!m_monitor) - return; m_tabpanel->SelectPageByName(TAB_ID_MONITOR); if (!dev_id.empty()) { - ((MonitorPanel*)m_monitor)->select_machine(dev_id); + MonitorPanel::ensure()->select_machine(dev_id); } } void MainFrame::jump_to_multipage() { - if(!m_multi_machine) + if (!m_multi_machine_page->in_book()) return; m_tabpanel->SelectPageByName(TAB_ID_MULTI_DEVICE); - ((MultiMachinePage*)m_multi_machine)->jump_to_send_page(); + if (MultiMachinePage* page = m_multi_machine_page->ensure()) + page->jump_to_send_page(); } @@ -4105,8 +4088,8 @@ void MainFrame::request_select_tab(const wxString& id) } int MainFrame::get_calibration_curr_tab() { - if (m_calibration) - return m_calibration->get_tabpanel()->GetSelection(); + if (CalibrationPanel* calibration = CalibrationPanel::if_built()) + return calibration->get_tabpanel()->GetSelection(); return -1; } @@ -4215,7 +4198,8 @@ void MainFrame::add_to_recent_projects(const wxString& filename) recent_projects.push_back(into_u8(m_recent_projects.GetHistoryFile(i))); } wxGetApp().app_config->set_recent_projects(recent_projects); - m_webview->SendRecentList(0); + if (WebViewPanel* home = WebViewPanel::if_built()) + home->SendRecentList(0); } } @@ -4331,7 +4315,8 @@ void MainFrame::open_recent_project(size_t file_id, wxString const & filename) recent_projects.push_back(into_u8(m_recent_projects.GetHistoryFile(i))); } wxGetApp().app_config->set_recent_projects(recent_projects); - m_webview->SendRecentList(-1); + if (WebViewPanel* home = WebViewPanel::if_built()) + home->SendRecentList(-1); } } } @@ -4354,7 +4339,8 @@ void MainFrame::remove_recent_project(size_t file_id, wxString const &filename) recent_projects.push_back(into_u8(m_recent_projects.GetHistoryFile(i))); } wxGetApp().app_config->set_recent_projects(recent_projects); - m_webview->SendRecentList(-1); + if (WebViewPanel* home = WebViewPanel::if_built()) + home->SendRecentList(-1); } void MainFrame::load_url(wxString url) @@ -4408,14 +4394,14 @@ bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelectedPageName void MainFrame::refresh_plugin_tips() { - if (m_webview != nullptr) - m_webview->ShowNetpluginTip(); + if (WebViewPanel* home = WebViewPanel::if_built()) + home->ShowNetpluginTip(); } void MainFrame::RunScript(wxString js) { - if (m_webview != nullptr) - m_webview->RunScript(js); + if (WebViewPanel* home = WebViewPanel::if_built()) + home->RunScript(js); } void MainFrame::technology_changed() @@ -4524,7 +4510,8 @@ void MainFrame::update_side_preset_ui() //take off multi machine - if(m_multi_machine){m_multi_machine->clear_page();} + if (MultiMachinePage* multi_machine = MultiMachinePage::if_built()) + multi_machine->clear_page(); } void MainFrame::on_select_default_preset(SimpleEvent& evt) diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 66f3b31263..f580de57e0 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -26,6 +26,8 @@ #include "Widgets/SideButton.hpp" #include "Widgets/SideMenuPopup.hpp" #include "FilamentGroupPopup.hpp" +#include "LazyPage.hpp" +#include "IdleScheduler.hpp" #include @@ -136,6 +138,13 @@ class MainFrame : public DPIFrame #endif bool m_loaded {false}; wxTimer* m_reset_title_text_colour_timer{ nullptr }; + IdleScheduler m_idle; + bool m_prebuild_started{ false }; + // Every LazyPage, in and out of the book; prebuild_pages_when_idle() registers them. + std::vector m_lazy_pages; + // The latest EVT_LOAD_PRINTER_URL, applied when the web Device view is built. + wxString m_printer_url; + wxString m_printer_api_key; wxString m_qs_last_input_file = wxEmptyString; wxString m_qs_last_output_file = wxEmptyString; @@ -180,7 +189,7 @@ class MainFrame : public DPIFrame bool can_delete() const; bool can_delete_all() const; bool can_reslice() const; - void bind_diff_dialog(); + DiffPresetDialog* make_diff_dialog(); // BBS wxBoxSizer* create_side_tools(); @@ -377,6 +386,12 @@ public: void select_tab(wxPanel* panel); void select_tab(const wxString& id = wxString()); void request_select_tab(const wxString& id); + // post_init() needs the plater's canvas on screen to initialize OpenGL; this pass does not + // build the settings page. + void select_prepare_for_gl_init(); + // Builds the lazy tab pages while the user is idle; post_init() calls it once. + void prebuild_pages_when_idle(); + bool Show(bool show = true) override; int get_calibration_curr_tab(); void select_view(const std::string& direction); void update_shortcut_labels(); @@ -435,27 +450,21 @@ public: BBLTopbar* m_topbar{ nullptr }; PrintHostQueueDialog* printhost_queue_dlg() { return m_printhost_queue_dlg; } Plater* m_plater { nullptr }; + // Lazy pages, created once and kept for the frame's life; their panels are reached + // through LazyInstance's statics, and show_device() only moves pages in and out of the book. #ifdef SLIC3R_CAD - // The tab page is the placeholder; m_design_panel stays null until the tab is first - // selected, so everything the Design panel builds stays off the startup path. - wxPanel* m_design_page { nullptr }; - DesignPanel* m_design_panel { nullptr }; - // Builds the Design panel if it does not exist yet and returns it (null only before the - // placeholder page itself exists). Main thread only -- it creates wx controls. Both the - // tab activation and the MCP socket go through this: the socket is driven headlessly, - // with nobody to click the tab, and without this every verb would answer "not ready". - DesignPanel* ensure_design_panel(); + LazyPage* m_design_page { nullptr }; #endif //BBS: GUI refactor - MonitorPanel* m_monitor{ nullptr }; + LazyPage* m_monitor_page{ nullptr }; //AuxiliaryPanel* m_auxiliary{ nullptr }; - MultiMachinePage* m_multi_machine{ nullptr }; - ProjectPanel* m_project{ nullptr }; + LazyPage* m_multi_machine_page{ nullptr }; + LazyPage* m_project_page{ nullptr }; - CalibrationPanel* m_calibration{ nullptr }; - WebViewPanel* m_webview { nullptr }; - PrinterWebView* m_printer_view{nullptr}; + LazyPage* m_calibration_page{ nullptr }; + LazyPage* m_home_page { nullptr }; + LazyPage* m_printer_view_page{nullptr}; PluginPages m_plugin_pages; wxLogWindow* m_log_window { nullptr }; // BBS @@ -466,7 +475,8 @@ public: ParamsDialog* m_param_dialog{ nullptr }; //BBS SettingsDialog m_settings_dialog; - DiffPresetDialog diff_dialog; + // The Compare presets dialog, built on first use or at idle through its holder. + Lazy m_diff_dialog; wxWindow* m_plater_page{ nullptr }; PrintHostQueueDialog* m_printhost_queue_dlg; diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 1a6d969988..7352ee3dd7 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -103,6 +103,7 @@ MonitorPanel::MonitorPanel(wxWindow* parent, wxWindowID id, const wxPoint& pos, : wxPanel(parent, id, pos, size, style), m_select_machine(SelectMachinePopup(this)) { + SetBackgroundColour(*wxWHITE); #ifdef __WINDOWS__ SetDoubleBuffered(true); #endif //__WINDOWS__ @@ -180,31 +181,36 @@ void MonitorPanel::init_tabpanel() auto title = m_tabpanel->GetPageText(m_tabpanel->GetSelection()); m_media_file_panel->SwitchStorage(title == _L("Storage")); } - page->SetFocus(); + // The first page is selected while the panel is built off screen. + if (page->IsShownOnScreen()) + page->SetFocus(); update_all(); }, m_tabpanel->GetId()); - //m_status_add_machine_panel = new AddMachinePanel(m_tabpanel); - m_status_info_panel = new StatusPanel(m_tabpanel); + m_status_info_panel = new StatusPanel(m_tabpanel); + // Queued before the page is added, since adding it selects it and runs the handler above, + // where built() must already be false. + add_build_steps_of(*m_status_info_panel); m_tabpanel->AddPage(m_status_info_panel, _L("Status"), true); + add_build_step([this] { + m_media_file_panel = new MediaFilePanel(m_tabpanel); + m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), false); + }); + add_build_step([this] { + m_upgrade_panel = new UpgradePanel(m_tabpanel); + m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), false); + }); + add_build_step([this] { + m_hms_panel = new HMSPanel(m_tabpanel); + m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), false); - 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); + std::string network_ver = Slic3r::NetworkAgent::get_version(); + if (!network_ver.empty()) { + m_tabpanel->SetFooterText(wxString::Format(_L("Network plug-in v%s"), network_ver)); + } - m_upgrade_panel = new UpgradePanel(m_tabpanel); - 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); - - std::string network_ver = Slic3r::NetworkAgent::get_version(); - if (!network_ver.empty()) { - m_tabpanel->SetFooterText(wxString::Format(_L("Network plug-in v%s"), network_ver)); - } - - m_initialized = true; - show_status((int)MonitorStatus::MONITOR_NO_PRINTER); + show_status((int)MonitorStatus::MONITOR_NO_PRINTER); + }); } void MonitorPanel::set_default() @@ -288,19 +294,23 @@ void MonitorPanel::on_select_printer(wxCommandEvent& event) set_default(); update_all(); - MachineObject *obj_ = dev->get_selected_machine(); - if (obj_) { - obj_->last_cali_version = -1; - obj_->reset_pa_cali_history_result(); - obj_->reset_pa_cali_result(); - Sidebar &sidebar = GUI::wxGetApp().sidebar(); - sidebar.update_sync_status(obj_); - sidebar.set_need_auto_sync_after_connect_printer(sidebar.need_auto_sync_extruder_list_after_connect_priner(obj_)); - } + on_machine_selected(dev->get_selected_machine()); Layout(); } +void MonitorPanel::on_machine_selected(MachineObject* obj) +{ + if (obj == nullptr) + return; + obj->last_cali_version = -1; + obj->reset_pa_cali_history_result(); + obj->reset_pa_cali_result(); + Sidebar& sidebar = GUI::wxGetApp().sidebar(); + sidebar.update_sync_status(obj); + sidebar.set_need_auto_sync_after_connect_printer(sidebar.need_auto_sync_extruder_list_after_connect_priner(obj)); +} + void MonitorPanel::on_printer_clicked(wxMouseEvent &event) { auto mouse_pos = ClientToScreen(event.GetPosition()); @@ -331,7 +341,8 @@ void MonitorPanel::on_size(wxSizeEvent &event) void MonitorPanel::update_all() { - if (!m_initialized) + // Every page exists once the last build step has run. + if (!built()) return; NetworkAgent* m_agent = wxGetApp().getAgent(); @@ -412,16 +423,12 @@ void MonitorPanel::update_hms_tag() bool MonitorPanel::Show(bool show) { -#ifdef __APPLE__ - // 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(); DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (show) { +#ifdef __APPLE__ + wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize()); +#endif start_update(); update_network_version_footer(); @@ -448,7 +455,7 @@ bool MonitorPanel::Show(bool show) void MonitorPanel::show_status(int status) { - if (!m_initialized) return; + if (!built()) return; if (last_status == status)return; if ((last_status & (int)MonitorStatus::MONITOR_CONNECTING) != 0) { NetworkAgent* agent = wxGetApp().getAgent(); diff --git a/src/slic3r/GUI/Monitor.hpp b/src/slic3r/GUI/Monitor.hpp index 9252b04559..94474f73b0 100644 --- a/src/slic3r/GUI/Monitor.hpp +++ b/src/slic3r/GUI/Monitor.hpp @@ -49,6 +49,7 @@ #include "slic3r/GUI/AmsWidgets.hpp" #include "Widgets/SideTools.hpp" #include "SelectMachinePop.hpp" +#include "Lazy.hpp" namespace Slic3r { namespace GUI { @@ -72,16 +73,16 @@ public: void msw_rescale(); }; -class MonitorPanel : public wxPanel +class MonitorPanel : public wxPanel, public StagedBuild, public LazyInstance { private: Tabbook* m_tabpanel{ nullptr }; wxSizer* m_main_sizer{ nullptr }; - StatusPanel* m_status_info_panel; - MediaFilePanel* m_media_file_panel; - UpgradePanel* m_upgrade_panel; - HMSPanel* m_hms_panel; + StatusPanel* m_status_info_panel{ nullptr }; + MediaFilePanel* m_media_file_panel{ nullptr }; + UpgradePanel* m_upgrade_panel{ nullptr }; + HMSPanel* m_hms_panel{ nullptr }; /* side tools */ SideTools* m_side_tools{nullptr}; @@ -96,7 +97,6 @@ private: wxBitmap m_arrow_img; int last_status; - bool m_initialized { false }; bool update_flag{false}; wxTimer* m_refresh_timer = nullptr; @@ -125,6 +125,8 @@ public: StatusPanel* get_status_panel() {return m_status_info_panel;}; void select_machine(std::string machine_sn); + // Resets the selected printer's calibration results and syncs the sidebar, without the Device tab. + static void on_machine_selected(MachineObject* obj); void on_timer(wxTimerEvent& event); void on_select_printer(wxCommandEvent& event); void on_printer_clicked(wxMouseEvent &event); diff --git a/src/slic3r/GUI/MultiMachineManagerPage.cpp b/src/slic3r/GUI/MultiMachineManagerPage.cpp index 792c23fb13..d5f9cbce8a 100644 --- a/src/slic3r/GUI/MultiMachineManagerPage.cpp +++ b/src/slic3r/GUI/MultiMachineManagerPage.cpp @@ -21,8 +21,9 @@ MultiMachineItem::MultiMachineItem(wxWindow* parent, MachineObject* obj) Bind(wxEVT_MOTION, &MultiMachineItem::OnMove, this); Bind(EVT_MULTI_DEVICE_VIEW, [obj](auto& e) { wxGetApp().mainframe->jump_to_monitor(obj->get_dev_id()); - if (wxGetApp().mainframe->m_monitor->get_status_panel()->get_media_play_ctrl()) { - wxGetApp().mainframe->m_monitor->get_status_panel()->get_media_play_ctrl()->jump_to_play(); + MonitorPanel* monitor = MonitorPanel::if_built(); + if (monitor && monitor->get_status_panel()->get_media_play_ctrl()) { + monitor->get_status_panel()->get_media_play_ctrl()->jump_to_play(); } }); wxGetApp().UpdateDarkUIWin(this); diff --git a/src/slic3r/GUI/MultiMachinePage.cpp b/src/slic3r/GUI/MultiMachinePage.cpp index dd67e099a0..e48e402405 100644 --- a/src/slic3r/GUI/MultiMachinePage.cpp +++ b/src/slic3r/GUI/MultiMachinePage.cpp @@ -11,6 +11,7 @@ namespace GUI { MultiMachinePage::MultiMachinePage(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style) : wxPanel(parent, id, pos, size, style) { + SetBackgroundColour(*wxWHITE); init_tabpanel(); m_main_sizer = new wxBoxSizer(wxHORIZONTAL); m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxLEFT, 0); diff --git a/src/slic3r/GUI/MultiMachinePage.hpp b/src/slic3r/GUI/MultiMachinePage.hpp index 724eeed2a9..14d531e8ab 100644 --- a/src/slic3r/GUI/MultiMachinePage.hpp +++ b/src/slic3r/GUI/MultiMachinePage.hpp @@ -7,6 +7,7 @@ #include "MultiTaskManagerPage.hpp" #include "MultiMachineManagerPage.hpp" #include "Tabbook.hpp" +#include "Lazy.hpp" #include "wx/button.h" @@ -19,7 +20,7 @@ namespace GUI { #define PICK_LEFT_DEV_STATUS 250 #define PICK_DEVICE_MAX 6 -class MultiMachinePage : public wxPanel +class MultiMachinePage : public wxPanel, public LazyInstance { private: wxTimer* m_refresh_timer = nullptr; diff --git a/src/slic3r/GUI/ParamsPanel.cpp b/src/slic3r/GUI/ParamsPanel.cpp index 5a49df1280..b4fd81706a 100644 --- a/src/slic3r/GUI/ParamsPanel.cpp +++ b/src/slic3r/GUI/ParamsPanel.cpp @@ -290,7 +290,7 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c m_compare_btn = new ScalableButton(m_top_panel, wxID_ANY, "compare", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true); m_compare_btn->SetToolTip(_L("Compare presets")); - m_compare_btn->Bind(wxEVT_BUTTON, ([](wxCommandEvent e) { wxGetApp().mainframe->diff_dialog.show(); })); + m_compare_btn->Bind(wxEVT_BUTTON, ([](wxCommandEvent e) { DiffPresetDialog::ensure()->show(); })); m_setting_btn = new ScalableButton(m_top_panel, wxID_ANY, "table", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true); m_setting_btn->SetToolTip(_L("View all object's settings")); @@ -550,18 +550,36 @@ void ParamsPanel::clear_page() void ParamsPanel::OnActivate() { if (m_current_tab == NULL) - { - //the first time - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": first time opened, set current tab to print"); - // BBS: open/close tab - //m_current_tab = m_tab_print; - set_active_tab(m_tab_print ? m_tab_print : m_tab_filament); - } + select_default_tab(); Tab* cur_tab = dynamic_cast (m_current_tab); if (cur_tab) cur_tab->OnActivate(); } +void ParamsPanel::select_default_tab() +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": first time opened, set current tab to print"); + // BBS: open/close tab + //m_current_tab = m_tab_print; + set_active_tab(m_tab_print ? m_tab_print : m_tab_filament); +} + +bool ParamsPanel::SettingsPagePrebuild::built() const +{ + Tab* tab = dynamic_cast(m_panel.m_current_tab); + return tab != nullptr && !tab->page_build_pending(); +} + +bool ParamsPanel::SettingsPagePrebuild::build_step() +{ + if (m_panel.m_current_tab == nullptr) { + m_panel.select_default_tab(); + return !built(); + } + Tab* tab = dynamic_cast(m_panel.m_current_tab); + return tab != nullptr && tab->page_build_step(); +} + void ParamsPanel::OnToggled(wxCommandEvent& event) { if (m_mode_region && m_mode_region->GetId() == event.GetId()) { diff --git a/src/slic3r/GUI/ParamsPanel.hpp b/src/slic3r/GUI/ParamsPanel.hpp index 91bf3d2a7e..9f9264b480 100644 --- a/src/slic3r/GUI/ParamsPanel.hpp +++ b/src/slic3r/GUI/ParamsPanel.hpp @@ -30,6 +30,7 @@ #include "wxExtensions.hpp" #include "GUI_Utils.hpp" #include "Widgets/Button.hpp" +#include "Lazy.hpp" class ModeSwitchButton; class SwitchButton; @@ -121,6 +122,22 @@ class ParamsPanel : public wxPanel wxPanel* m_current_tab { nullptr }; + // Builds the selected page's option groups at idle; while no tab is selected yet, + // its first unit selects the default one. + class SettingsPagePrebuild : public LazyBase + { + public: + explicit SettingsPagePrebuild(ParamsPanel& panel) : m_panel(panel) {} + const std::string& name() const override { return m_name; } + bool built() const override; + bool build_step() override; + int prebuild_order() const override { return 0; } + + private: + ParamsPanel& m_panel; + std::string m_name{ "settings_page" }; + } m_settings_page_prebuild{ *this }; + bool m_has_object_config { false }; struct Highlighter @@ -149,6 +166,10 @@ class ParamsPanel : public wxPanel //clear the right page void clear_page(); void OnActivate(); + // The print tab, or the filament tab without one. + void select_default_tab(); + // The settings page's prebuild task. + LazyBase& settings_page_prebuild() { return m_settings_page_prebuild; } void set_active_tab(wxPanel*tab); bool is_active_and_shown_tab(wxPanel*tab); void update_mode(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5a10dcd2a7..5eb36f20f9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -10412,8 +10412,8 @@ void Plater::priv::reset(bool apply_presets_change, bool reload_presets) // Same reason, one level up: the Design tab keeps the editable document, not the Model, so // clearing the recipe alone leaves the tab showing the previous project's feature tree — // and its next edit syncs that tree straight back into the new project. - if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr) - wxGetApp().mainframe->m_design_panel->clear_document(); + if (DesignPanel* design = DesignPanel::if_built()) + design->clear_document(); #endif assemble_view->get_canvas3d()->reset_explosion_ratio(); update(); @@ -13184,17 +13184,17 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) // 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; + const bool selecting_web_device_tab = main_frame->m_printer_view_page && + main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view_page; if (selecting_web_device_tab) { // Use the selected discovered machine when the preset has no host. main_frame->load_printer_url(); } 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()) { + if (PrinterWebView* view = PrinterWebView::if_built(); view != nullptr && url.empty()) { // It's missing_connection page, reload so that we can replay the gif image - main_frame->m_printer_view->reload(); + view->reload(); } } } @@ -13925,8 +13925,8 @@ void Plater::priv::unbind_canvas_event_handlers() // The Design tab's viewport is a fourth GLCanvas3D on the same shared GL context, owned by // MainFrame rather than by us — same reach as reset() uses for clear_document(). Null until // the tab has been opened once, so most sessions skip it. - if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr) - wxGetApp().mainframe->m_design_panel->unbind_canvas_event_handlers(); + if (DesignPanel* design = DesignPanel::if_built()) + design->unbind_canvas_event_handlers(); #endif } @@ -13939,8 +13939,8 @@ void Plater::priv::reset_canvas_volumes() preview->get_canvas3d()->reset_volumes(); #ifdef SLIC3R_CAD - if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr) - wxGetApp().mainframe->m_design_panel->reset_canvas_volumes(); + if (DesignPanel* design = DesignPanel::if_built()) + design->reset_canvas_volumes(); #endif } @@ -20002,7 +20002,7 @@ int Plater::export_config_3mf(int plate_idx, Export3mfProgressFn proFn) void Plater::send_calibration_job_finished(wxCommandEvent & evt) { p->main_frame->request_select_tab(TAB_ID_CALIBRATION); - auto calibration_panel = p->main_frame->m_calibration; + CalibrationPanel* calibration_panel = CalibrationPanel::ensure(); if (calibration_panel) { auto curr_wizard = static_cast(calibration_panel->get_tabpanel()->GetPage(evt.GetInt())); wxCommandEvent event(EVT_CALIBRATION_JOB_FINISHED); @@ -20034,8 +20034,8 @@ void Plater::print_job_finished(wxCommandEvent &evt) dev->set_selected_machine(evt.GetString().ToStdString()); 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; + // Selects the status page on a built Device tab; one built by the switch starts there. + MonitorPanel* curr_monitor = MonitorPanel::if_built(); if(curr_monitor) curr_monitor->get_tabpanel()->ChangeSelection(MonitorPanel::PrinterTab::PT_STATUS); } @@ -20775,8 +20775,8 @@ void Plater::update_print_error_info(int code, std::string msg, std::string extr if (p->m_send_to_sdcard_dlg) { p->m_send_to_sdcard_dlg->update_print_error_info(code, msg, extra); } - if (p->main_frame->m_calibration) - p->main_frame->m_calibration->update_print_error_info(code, msg, extra); + if (CalibrationPanel* calibration = CalibrationPanel::if_built()) + calibration->update_print_error_info(code, msg, extra); } wxString Plater::get_project_filename(const wxString& extension) const @@ -21069,7 +21069,8 @@ void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWar { printer_name.Replace("Bambu Lab", "", false); wxString content; - bool device_page = (wxGetApp().mainframe == nullptr) && (wxGetApp().mainframe->m_monitor->IsShown()); + MainFrame* frame = wxGetApp().mainframe; + bool device_page = frame != nullptr && frame->m_monitor_page->in_book(); if (type == PrinterWarningType::NOT_CONNECTED) { if (device_page) { content = wxString::Format(_L("Printer not connected. Please go to the device page to connect %s before syncing."), diff --git a/src/slic3r/GUI/PrebuildQueue.hpp b/src/slic3r/GUI/PrebuildQueue.hpp new file mode 100644 index 0000000000..c013f53865 --- /dev/null +++ b/src/slic3r/GUI/PrebuildQueue.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include + +#include "Lazy.hpp" + +namespace Slic3r { namespace GUI { + +// Holders to build at idle, ordered by prebuild_order(), run one slice at a time. The +// holders outlive the queue. Main thread only. +class PrebuildQueue +{ +public: + struct Slice + { + std::string name; // the task that ran; empty when nothing was pending + int units{ 0 }; // units run + long long ms{ 0 }; // time spent + bool completed{ false }; // the task has no more units + bool remaining{ false }; // some task is still pending + }; + + // Lower order runs first, equal order in the order added. + void add(LazyBase& task) + { + auto after = std::find_if(m_tasks.begin(), m_tasks.end(), [&](const LazyBase* t) { return t->prebuild_order() > task.prebuild_order(); }); + m_tasks.insert(after, &task); + } + + void clear() { m_tasks.clear(); } + + bool pending() const + { + return std::any_of(m_tasks.begin(), m_tasks.end(), [](const LazyBase* t) { return t->pending(); }); + } + + // The task names in queue order, comma separated. + std::string names() const + { + std::string out; + for (const LazyBase* t : m_tasks) + out += (out.empty() ? "" : ", ") + t->name(); + return out; + } + + // Runs units of the first pending task until it completes, budget_ms of now_ms() have + // passed, or interrupt() is true after a unit; on_unit gets the task name and each + // unit's duration. A task with no work is passed over and stays in the queue, so one + // whose work returns is pending again. + Slice run_slice(int budget_ms, + const std::function& now_ms, + const std::function& interrupt, + const std::function& on_unit = {}) + { + Slice slice; + auto next = std::find_if(m_tasks.begin(), m_tasks.end(), [](const LazyBase* t) { return t->pending(); }); + if (next == m_tasks.end()) + return slice; + // The pointer is copied out, since a unit may add tasks and reallocate m_tasks. + LazyBase* const task = *next; + slice.name = task->name(); + const long long started = now_ms(); + for (;;) { + const long long unit_started = now_ms(); + const bool more = task->build_step(); + const long long now = now_ms(); + ++slice.units; + if (on_unit) + on_unit(slice.name, now - unit_started); + slice.ms = now - started; + if (!more) { + slice.completed = true; + break; + } + if (slice.ms >= budget_ms || interrupt()) + break; + } + slice.remaining = pending(); + return slice; + } + +private: + std::vector m_tasks; +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 001866bfd9..fd5012b5ef 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1044,8 +1044,8 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too if (m_bambu_cloud_checkbox) m_bambu_cloud_checkbox->Enable(!enabled); } else if (param == "hide_login_side_panel") { - if (wxGetApp().mainframe && wxGetApp().mainframe->m_webview) { - wxGetApp().mainframe->m_webview->SendCloudProvidersInfo(); + if (WebViewPanel* home = WebViewPanel::if_built()) { + home->SendCloudProvidersInfo(); } } // ORCA: apply the preview dimming change immediately to the currently loaded preview @@ -1254,9 +1254,8 @@ wxBoxSizer *PreferencesDialog::create_item_bambu_cloud(wxString title, wxString app_config->save(); // Update homepage visibility immediately - auto *mainframe = wxGetApp().mainframe; - if (mainframe && mainframe->m_webview) - mainframe->m_webview->SendCloudProvidersInfo(); + if (WebViewPanel* home = WebViewPanel::if_built()) + home->SendCloudProvidersInfo(); }); m_sizer->Add(cb, 0, wxALIGN_CENTER); diff --git a/src/slic3r/GUI/PrinterWebView.hpp b/src/slic3r/GUI/PrinterWebView.hpp index 8a62ce70a2..1ab1e17932 100644 --- a/src/slic3r/GUI/PrinterWebView.hpp +++ b/src/slic3r/GUI/PrinterWebView.hpp @@ -26,6 +26,7 @@ #include "wx/textctrl.h" #include #include +#include "Lazy.hpp" namespace Slic3r { @@ -34,7 +35,7 @@ namespace GUI { class PrinterWebViewHandler; -class PrinterWebView : public wxPanel { +class PrinterWebView : public wxPanel, public LazyInstance { public: PrinterWebView(wxWindow *parent); virtual ~PrinterWebView(); diff --git a/src/slic3r/GUI/Project.cpp b/src/slic3r/GUI/Project.cpp index 6d1eb0e180..8d87939afc 100644 --- a/src/slic3r/GUI/Project.cpp +++ b/src/slic3r/GUI/Project.cpp @@ -44,6 +44,7 @@ const std::vector license_list = { ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) : wxPanel(parent, id, pos, size, style) { + SetBackgroundColour(*wxWHITE); m_project_home_url = wxString::Format("file://%s/web/model/index.html", from_u8(resources_dir())); wxString strlang = wxGetApp().current_language_code_safe(); if (strlang != "") @@ -67,6 +68,7 @@ ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, m_auxiliary = new AuxiliaryPanel(this); m_auxiliary->Hide(); main_sizer->Add(m_auxiliary, wxSizerFlags().Expand().Proportion(1)); + add_build_steps_of(*m_auxiliary); Bind(EVT_AUXILIARY_DONE, [this](wxCommandEvent& e) { update_model_data();}); SetSizer(main_sizer); diff --git a/src/slic3r/GUI/Project.hpp b/src/slic3r/GUI/Project.hpp index a590212cb7..a22bc44430 100644 --- a/src/slic3r/GUI/Project.hpp +++ b/src/slic3r/GUI/Project.hpp @@ -35,6 +35,7 @@ #include "libslic3r/ProjectTask.hpp" #include "wxExtensions.hpp" #include "Auxiliary.hpp" +#include "Lazy.hpp" #define AUFILE_GREY700 wxColour(107, 107, 107) #define AUFILE_GREY500 wxColour(158, 158, 158) @@ -59,7 +60,7 @@ struct project_file{ std::string size; }; -class ProjectPanel : public wxPanel +class ProjectPanel : public wxPanel, public StagedBuild, public LazyInstance { private: std::atomic m_web_init_completed{false}; @@ -90,7 +91,6 @@ public: void msw_rescale(); void update_model_data(); void clear_model_info(); - void init_auxiliary() { m_auxiliary->init_auxiliary(); } bool Show(bool show); void OnScriptMessage(wxWebViewEvent& evt); diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 629aef7296..a9767d6c72 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -3470,7 +3470,7 @@ void SelectMachineDialog::navigate_to_timelapse_page() main_frame->jump_to_monitor(); // then switch to Storage (Media) tab inside Monitor - auto* monitor = dynamic_cast(main_frame->m_monitor); + MonitorPanel* monitor = MonitorPanel::if_built(); if (monitor) { auto* tabpanel = monitor->get_tabpanel(); if (tabpanel) { diff --git a/src/slic3r/GUI/StagedBuild.hpp b/src/slic3r/GUI/StagedBuild.hpp new file mode 100644 index 0000000000..8f4c0454c3 --- /dev/null +++ b/src/slic3r/GUI/StagedBuild.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +namespace Slic3r { namespace GUI { + +// A panel whose constructor builds the skeleton and queues the rest as steps, run in order +// on the main thread, one at a time. +class StagedBuild +{ +public: + virtual ~StagedBuild() = default; + + bool built() const + { + return m_next_step == m_steps.size() && + std::all_of(m_children.begin(), m_children.end(), [](const StagedBuild* child) { return child->built(); }); + } + + // Runs one step and returns true while more remain. Own steps first, then whatever a + // child queued after its steps were forwarded. + bool build_step() + { + if (m_next_step < m_steps.size()) { + // Copied out, since a step may queue more steps and reallocate m_steps. + auto step = std::move(m_steps[m_next_step++]); + step(); + } else if (StagedBuild* child = unfinished_child()) { + child->build_step(); + } + return !built(); + } + +protected: + void add_build_step(std::function step) { m_steps.push_back(std::move(step)); } + + // Queues one step per step the child has now, and keeps the child so that built() waits + // for any it queues later. + void add_build_steps_of(StagedBuild& child) + { + m_children.push_back(&child); + for (size_t i = child.m_next_step; i < child.m_steps.size(); ++i) + add_build_step([&child] { child.build_step(); }); + } + +private: + StagedBuild* unfinished_child() const + { + auto it = std::find_if(m_children.begin(), m_children.end(), [](const StagedBuild* child) { return !child->built(); }); + return it == m_children.end() ? nullptr : *it; + } + + std::vector> m_steps; + std::vector m_children; + size_t m_next_step{ 0 }; +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index fb0029edc3..218371f0b5 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -1328,18 +1328,21 @@ StatusBasePanel::StatusBasePanel(wxWindow *parent, wxWindowID id, const wxPoint wxBoxSizer *bSizer_left = new wxBoxSizer(wxVERTICAL); - auto m_monitoring_sizer = create_monitoring_page(); - bSizer_left->Add(m_monitoring_sizer, 1, wxEXPAND | wxALL, 0); + // The sizers are nested here so each step only appends to its own. + add_build_step([this, bSizer_left] { + auto m_monitoring_sizer = create_monitoring_page(); + bSizer_left->Add(m_monitoring_sizer, 1, wxEXPAND | wxALL, 0); - auto m_panel_separotor1 = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - m_panel_separotor1->SetBackgroundColour(STATUS_PANEL_BG); - m_panel_separotor1->SetMinSize(wxSize(-1, PAGE_SPACING)); - m_panel_separotor1->SetMaxSize(wxSize(-1, PAGE_SPACING)); - m_monitoring_sizer->Add(m_panel_separotor1, 0, wxEXPAND, 0); + auto m_panel_separotor1 = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + m_panel_separotor1->SetBackgroundColour(STATUS_PANEL_BG); + m_panel_separotor1->SetMinSize(wxSize(-1, PAGE_SPACING)); + m_panel_separotor1->SetMaxSize(wxSize(-1, PAGE_SPACING)); + m_monitoring_sizer->Add(m_panel_separotor1, 0, wxEXPAND, 0); - m_project_task_panel = new PrintingTaskPanel(this, PrintingTaskType::PRINGINT); - m_project_task_panel->init_bitmaps(); - m_monitoring_sizer->Add(m_project_task_panel, 0, wxALL | wxEXPAND , 0); + m_project_task_panel = new PrintingTaskPanel(this, PrintingTaskType::PRINGINT); + m_project_task_panel->init_bitmaps(); + m_monitoring_sizer->Add(m_project_task_panel, 0, wxALL | wxEXPAND , 0); + }); // auto m_panel_separotor2 = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); // m_panel_separotor2->SetBackgroundColour(STATUS_PANEL_BG); @@ -1359,8 +1362,10 @@ StatusBasePanel::StatusBasePanel(wxWindow *parent, wxWindowID id, const wxPoint m_machine_ctrl_panel->SetDoubleBuffered(true); auto m_machine_control = create_machine_control_page(m_machine_ctrl_panel); m_machine_ctrl_panel->SetSizer(m_machine_control); - m_machine_ctrl_panel->Layout(); - m_machine_control->Fit(m_machine_ctrl_panel); + add_build_step([this, m_machine_control] { + m_machine_ctrl_panel->Layout(); + m_machine_control->Fit(m_machine_ctrl_panel); + }); bSizer_status_below->Add(m_machine_ctrl_panel, 0, wxALL, 0); @@ -1375,8 +1380,11 @@ StatusBasePanel::StatusBasePanel(wxWindow *parent, wxWindowID id, const wxPoint m_panel_separotor_bottom->SetBackgroundColour(STATUS_PANEL_BG); bSizer_status->Add(m_panel_separotor_bottom, 0, wxEXPAND | wxALL, 0); - this->SetSizerAndFit(bSizer_status); - this->Layout(); + this->SetSizer(bSizer_status); + add_build_step([this, bSizer_status] { + bSizer_status->SetSizeHints(this); + this->Layout(); + }); } StatusBasePanel::~StatusBasePanel() @@ -1614,16 +1622,24 @@ wxBoxSizer *StatusBasePanel::create_machine_control_page(wxWindow *parent) wxBoxSizer *bSizer_control = new wxBoxSizer(wxVERTICAL); - auto temp_axis_ctrl_sizer = create_temp_axis_group(parent); - auto m_filament_load_sizer = create_filament_group(parent); - + // The slot sizers keep each group's place in bSizer_control. + wxBoxSizer *temp_axis_slot = new wxBoxSizer(wxVERTICAL); + wxBoxSizer *filament_slot = new wxBoxSizer(wxVERTICAL); /* ams control box or live nozzle-rack panel (rack printers switch between the two) */ wxSizer *ams_rack_sizer = new wxBoxSizer(wxHORIZONTAL); - ams_rack_sizer->Add(create_ams_group(parent), 0, wxEXPAND | wxLEFT); - m_panel_nozzle_rack = new wgtDeviceNozzleRack(parent); - m_panel_nozzle_rack->Show(false); - ams_rack_sizer->Add(m_panel_nozzle_rack, 0, wxEXPAND | wxLEFT); + add_build_step([this, parent, temp_axis_slot, filament_slot] { + temp_axis_slot->Add(create_temp_axis_group(parent), 0, wxEXPAND); + filament_slot->Add(create_filament_group(parent), 0, wxEXPAND); + }); + add_build_step([this, parent, ams_rack_sizer] { + ams_rack_sizer->Add(create_ams_group(parent), 0, wxEXPAND | wxLEFT); + }); + add_build_step([this, parent, ams_rack_sizer] { + m_panel_nozzle_rack = new wgtDeviceNozzleRack(parent); + m_panel_nozzle_rack->Show(false); + ams_rack_sizer->Add(m_panel_nozzle_rack, 0, wxEXPAND | wxLEFT); + }); m_ams_rack_switch = new SwitchBoard(parent, _L("Filament"), _L("Hotends"), wxSize(FromDIP(126), FromDIP(26))); m_ams_rack_switch->updateState("left"); @@ -1631,12 +1647,12 @@ wxBoxSizer *StatusBasePanel::create_machine_control_page(wxWindow *parent) m_ams_rack_switch->Bind(wxCUSTOMEVT_SWITCH_POS, &StatusBasePanel::on_ams_rack_switch, this); bSizer_control->Add(0, 0, 0, wxTOP, FromDIP(8)); - bSizer_control->Add(temp_axis_ctrl_sizer, 0, wxALIGN_CENTER|wxLEFT|wxRIGHT, FromDIP(8)); + bSizer_control->Add(temp_axis_slot, 0, wxALIGN_CENTER|wxLEFT|wxRIGHT, FromDIP(8)); bSizer_control->Add(m_ams_rack_switch, 0, wxALIGN_CENTRE|wxTOP, FromDIP(6)); bSizer_control->Add(0, 0, 0, wxTOP, FromDIP(6)); bSizer_control->Add(ams_rack_sizer, 0, wxALIGN_CENTER|wxLEFT|wxRIGHT, FromDIP(8)); bSizer_control->Add(0, 0, 0, wxTOP, FromDIP(6)); - bSizer_control->Add(m_filament_load_sizer, 0, wxALIGN_CENTER|wxLEFT|wxRIGHT, FromDIP(8)); + bSizer_control->Add(filament_slot, 0, wxALIGN_CENTER|wxLEFT|wxRIGHT, FromDIP(8)); bSizer_control->Add(0, 0, 0, wxTOP, FromDIP(4)); bSizer_right->Add(bSizer_control, 1, wxEXPAND | wxALL, 0); @@ -2228,8 +2244,10 @@ void StatusBasePanel::expand_filament_loading(wxMouseEvent& e) m_filament_step->Show(tag_show); Layout(); Fit(); - wxGetApp().mainframe->m_monitor->get_status_panel()->Layout(); - wxGetApp().mainframe->m_monitor->Layout(); + if (MonitorPanel* monitor = MonitorPanel::if_built()) { + monitor->get_status_panel()->Layout(); + monitor->Layout(); + } } void StatusBasePanel::show_ams_group(bool show) @@ -2240,7 +2258,8 @@ void StatusBasePanel::show_ams_group(bool show) m_ams_control->Fit(); Layout(); Fit(); - wxGetApp().mainframe->m_monitor->Layout(); + if (MonitorPanel* monitor = MonitorPanel::if_built()) + monitor->Layout(); } // On rack printers, don't clobber the rack view when the user has the switch on "Hotends". @@ -2253,7 +2272,8 @@ void StatusBasePanel::show_ams_group(bool show) m_ams_control->Fit(); Layout(); Fit(); - wxGetApp().mainframe->m_monitor->Layout(); + if (MonitorPanel* monitor = MonitorPanel::if_built()) + monitor->Layout(); } } @@ -2277,8 +2297,10 @@ void StatusBasePanel::show_filament_load_group(bool show) Layout(); Fit(); - wxGetApp().mainframe->m_monitor->get_status_panel()->Layout(); - wxGetApp().mainframe->m_monitor->Layout(); + if (MonitorPanel* monitor = MonitorPanel::if_built()) { + monitor->get_status_panel()->Layout(); + monitor->Layout(); + } } } @@ -2403,6 +2425,12 @@ void StatusPanel::update_camera_state(MachineObject* obj) StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name) : StatusBasePanel(parent, id, pos, size, style) +{ + // Wires the controls the base class builds in steps, so it is the last step. + add_build_step([this] { wire_controls(); }); +} + +void StatusPanel::wire_controls() { init_scaled_buttons(); m_buttons.push_back(m_bpButton_z_10); @@ -2514,47 +2542,50 @@ StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, co StatusPanel::~StatusPanel() { - // Disconnect Events - m_project_task_panel->get_bitmap_thumbnail()->Disconnect(wxEVT_LEFT_DOWN, wxMouseEventHandler(StatusPanel::refresh_thumbnail_webrequest), NULL, this); - m_project_task_panel->get_partskip_button()->Disconnect(wxEVT_LEFT_DOWN, wxCommandEventHandler(StatusPanel::on_subtask_partskip), NULL, this); - m_project_task_panel->get_pause_resume_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_pause_resume), NULL, this); - m_project_task_panel->get_abort_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_abort), NULL, this); - m_project_task_panel->get_market_scoring_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_scoring), NULL, this); - m_project_task_panel->get_market_retry_buttom()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_retry), NULL, this); - m_project_task_panel->get_clean_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_print_error_clean), NULL, this); + // The controls only exist once the last build step has run. + if (built()) { + // Disconnect Events + m_project_task_panel->get_bitmap_thumbnail()->Disconnect(wxEVT_LEFT_DOWN, wxMouseEventHandler(StatusPanel::refresh_thumbnail_webrequest), NULL, this); + m_project_task_panel->get_partskip_button()->Disconnect(wxEVT_LEFT_DOWN, wxCommandEventHandler(StatusPanel::on_subtask_partskip), NULL, this); + m_project_task_panel->get_pause_resume_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_pause_resume), NULL, this); + m_project_task_panel->get_abort_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_abort), NULL, this); + m_project_task_panel->get_market_scoring_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_scoring), NULL, this); + m_project_task_panel->get_market_retry_buttom()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_retry), NULL, this); + m_project_task_panel->get_clean_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_print_error_clean), NULL, this); - m_setting_button->Disconnect(wxEVT_LEFT_DOWN, wxMouseEventHandler(StatusPanel::on_camera_enter), NULL, this); - m_setting_button->Disconnect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(StatusPanel::on_camera_enter), NULL, this); - m_tempCtrl_bed->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_bed_temp_kill_focus), NULL, this); - m_tempCtrl_bed->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_bed_temp_set_focus), NULL, this); - m_tempCtrl_nozzle->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this); - m_tempCtrl_nozzle->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this); + m_setting_button->Disconnect(wxEVT_LEFT_DOWN, wxMouseEventHandler(StatusPanel::on_camera_enter), NULL, this); + m_setting_button->Disconnect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(StatusPanel::on_camera_enter), NULL, this); + m_tempCtrl_bed->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_bed_temp_kill_focus), NULL, this); + m_tempCtrl_bed->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_bed_temp_set_focus), NULL, this); + m_tempCtrl_nozzle->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this); + m_tempCtrl_nozzle->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this); - m_tempCtrl_nozzle_deputy->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this); - m_tempCtrl_nozzle_deputy->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this); + m_tempCtrl_nozzle_deputy->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this); + m_tempCtrl_nozzle_deputy->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this); - m_switch_lamp->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_lamp_switch), NULL, this); - /*m_switch_nozzle_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this); - m_switch_printing_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this); - m_switch_cham_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this);*/ + m_switch_lamp->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_lamp_switch), NULL, this); + /*m_switch_nozzle_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this); + m_switch_printing_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this); + m_switch_cham_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this);*/ - //m_switch_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this); - //m_switch_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this); - m_switch_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this); + //m_switch_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this); + //m_switch_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this); + m_switch_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this); - m_bpButton_xy->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_xy), NULL, this); - m_bpButton_z_10->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_z_up_10), NULL, this); - m_bpButton_z_1->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_z_up_1), NULL, this); - m_bpButton_z_down_1->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_z_down_1), NULL, this); - m_bpButton_z_down_10->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_z_down_10), NULL, this); - m_bpButton_e_10->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_e_up_10), NULL, this); - m_bpButton_e_down_10->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_e_down_10), NULL, this); - m_nozzle_btn_panel->Disconnect(wxCUSTOMEVT_SWITCH_POS, wxCommandEventHandler(StatusPanel::on_nozzle_selected), NULL, this); - m_switch_speed->Disconnect(wxEVT_LEFT_DOWN, wxCommandEventHandler(StatusPanel::on_switch_speed), NULL, this); - m_calibration_btn->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_start_calibration), NULL, this); - m_options_btn->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_show_print_options), NULL, this); - m_safety_btn->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_show_safety_options), NULL, this); - m_parts_btn->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_show_parts_options), NULL, this); + m_bpButton_xy->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_xy), NULL, this); + m_bpButton_z_10->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_z_up_10), NULL, this); + m_bpButton_z_1->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_z_up_1), NULL, this); + m_bpButton_z_down_1->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_z_down_1), NULL, this); + m_bpButton_z_down_10->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_z_down_10), NULL, this); + m_bpButton_e_10->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_e_up_10), NULL, this); + m_bpButton_e_down_10->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_axis_ctrl_e_down_10), NULL, this); + m_nozzle_btn_panel->Disconnect(wxCUSTOMEVT_SWITCH_POS, wxCommandEventHandler(StatusPanel::on_nozzle_selected), NULL, this); + m_switch_speed->Disconnect(wxEVT_LEFT_DOWN, wxCommandEventHandler(StatusPanel::on_switch_speed), NULL, this); + m_calibration_btn->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_start_calibration), NULL, this); + m_options_btn->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_show_print_options), NULL, this); + m_safety_btn->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_show_safety_options), NULL, this); + m_parts_btn->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_show_parts_options), NULL, this); + } // remove warning dialogs if (abort_dlg != nullptr) @@ -5216,7 +5247,9 @@ void StatusPanel::set_default() m_filament_step->Hide(); error_info_reset(); #ifndef __WXGTK__ - SetFocus(); + // Also reached while the panel is built off screen. + if (IsShownOnScreen()) + SetFocus(); #endif } diff --git a/src/slic3r/GUI/StatusPanel.hpp b/src/slic3r/GUI/StatusPanel.hpp index 341bb9ab69..d79c3d3915 100644 --- a/src/slic3r/GUI/StatusPanel.hpp +++ b/src/slic3r/GUI/StatusPanel.hpp @@ -36,6 +36,7 @@ #include "HMS.hpp" #include "PartSkipDialog.hpp" #include "DeviceErrorDialog.hpp" +#include "StagedBuild.hpp" class StepIndicator; @@ -375,7 +376,7 @@ public: void paint(wxPaintEvent&); }; -class StatusBasePanel : public wxScrolledWindow +class StatusBasePanel : public wxScrolledWindow, public StagedBuild { protected: wxBitmap m_item_placeholder; @@ -443,7 +444,7 @@ protected: wxMediaCtrl3 * m_media_ctrl; - MediaPlayCtrl * m_media_play_ctrl; + MediaPlayCtrl * m_media_play_ctrl{nullptr}; Label * m_staticText_printing; wxStaticBitmap *m_bitmap_thumbnail; @@ -629,6 +630,7 @@ class StatusPanel : public StatusBasePanel { private: friend class MonitorPanel; + void wire_controls(); protected: std::shared_ptr m_slice_info_popup; @@ -679,7 +681,7 @@ protected: std::map m_print_connect_types; std::vector