Compare commits

..
Author SHA1 Message Date
a6dbf2502d Device tab blank for webui printers after switching language (#14547)
* Save device url in all cases and load printer url after hot-reload finishes

* Recreate web view from scratch as only URL fix seems not robust enough

* Add the same robust browser recreation for WebViewDialog

It should eliminate possible issue with blank Home and other pages
in the same way as Printer page

* Remove redundant fallback leftover

* Fix webview reset state and replay Project info on page reload

The first-show webview reset now runs only on Windows, reloads the last
printer URL and resets the Project page's ready state. The Project tab
replays its 3MF info whenever the page reloads, so it no longer goes
blank after a theme switch or a slow first load. The Device tab no longer
loads an extra time on first open, and the Home tab no longer navigates
twice. NeedsRecreateOnShow() logs is_recreating_gui so one language
switch shows whether the reset ever fires.

* Build plugin pages on first show

A language switch rebuilt every plugin page's browser while the main
window was being recreated, which left plugin tabs blank on Windows.
Plugin pages are now lazy pages, never prebuilt, and are removed left
to right so removing pages never builds one only to destroy it.

A plugin page's script now starts when its tab is first opened;
messages posted before that are dropped.

---------

Co-authored-by: SoftFever <softfeverever@gmail.com>
Co-authored-by: Noisyfox <timemanager.rick@gmail.com>
Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
2026-10-03 20:06:50 +08:00
HanifKoh 8a6377f087 Add Missing Includes Across src/libslic3r (#16068)
* Add Missing Includes Across src/libslic3r

Every libslic3r source and header now directly includes the headers declaring what it uses, rather than relying on the precompiled header or transitive includes. Generated with clang-tidy misc-include-cleaner, with libslic3r headers spelled libslic3r/... so they resolve outside the library's private include paths. MultiMaterialSegmentation.hpp, Support/SupportParameters.hpp and Format/STEP.hpp are made self-contained by hand.

* Make the libslic3r Headers Compile on Their Own

Each now includes, or forward-declares, what it uses instead of relying on what its includers happened to include first. Left out: I18N.hpp, which errors on purpose when included from GUI code, and VoxelizeCSGMesh.hpp and SLA/bicubic.h, which nothing includes and which no longer compile at all.

* Add the Includes Missing From the Hand-Fixed libslic3r Headers

clang-tidy would not edit these headers while they failed to compile on their own, so the first pass skipped them. With the headers now self-contained, a second pass adds the rest.

* Keep Windows Setup Ahead of the Added libslic3r Includes

Print.cpp and Thread.cpp open with a _WIN32 block that has to come first; without the precompiled header, Print.cpp otherwise reaches windows.h through OCCT with NONLS defined and boost/regex fails. OpenVDBUtils.cpp and SLA/SupportTreeBuilder.cpp had includes inside #ifndef NOMINMAX, which libslic3r defines on Windows, so those were skipped there. .clang-tidy also ignores the MSVC STL and UCRT internals, Boost.Multiprecision's fwd.hpp and CPython's Windows include directory.

* Re-Add libslic3r Includes After the Clipper2 2.0.1 Migration

Rebasing onto main took main's version of the files the Clipper2 migration rewrote, so their added includes are restored here, along with includes for main's new code. Clipper2's individual headers are now ignored by clang-tidy: they only build the Z variant through clipper2_z.hpp, which defines USINGZ first, so including clipper.core.h and the like directly broke ClipperZUtils.cpp.
2026-10-03 15:31:11 +08:00
SoftFever c86e33db6d Make the orca-wxwidgets skill find the wx source on Windows and in worktrees 2026-10-03 14:11:21 +08:00
21 changed files with 224 additions and 89 deletions
+10 -2
View File
@@ -30,8 +30,9 @@ how you read the wx docs:
Look things up in the source the app is built from — it beats memory, and 3.3 changed real behaviour: Look things up in the source the app is built from — it beats memory, and 3.3 changed real behaviour:
```bash ```bash
WX=$(find deps -maxdepth 5 -type d -path '*dep_wxWidgets-prefix/src/dep_wxWidgets' | head -1) WX=$(find -L deps -maxdepth 5 -type d -path '*dep_wxWidgets-prefix/src/dep_wxWidgets' 2>/dev/null | head -1)
# macOS: deps/build/<arch>/dep_wxWidgets-prefix/src/dep_wxWidgets Linux: deps/build/dep_wxWidgets-prefix/... # macOS: deps/build/<arch>/dep_wxWidgets-prefix/src/dep_wxWidgets Linux, Windows: deps/<tree>/dep_wxWidgets-prefix/...
# -L follows a worktree's deps/<tree> symlinked to the main checkout. Not a glob: zsh aborts on one that matches nothing.
# If deps are not built: git clone --depth 1 -b v3.3.2 https://github.com/SoftFever/Orca-deps-wxWidgets # If deps are not built: git clone --depth 1 -b v3.3.2 https://github.com/SoftFever/Orca-deps-wxWidgets
grep -n "CaptureMouse" -A 30 $WX/interface/wx/window.h # documented contract (doxygen source) grep -n "CaptureMouse" -A 30 $WX/interface/wx/window.h # documented contract (doxygen source)
grep -rn "@onlyfor\|not implemented" $WX/interface/wx/popupwin.h # documented platform limits grep -rn "@onlyfor\|not implemented" $WX/interface/wx/popupwin.h # documented platform limits
@@ -40,6 +41,13 @@ grep -n "IsDark" $WX/docs/changes.txt # what changed in
grep -n "NotifyCaptureLost" -r $WX/src/osx $WX/src/gtk $WX/src/msw # what each port actually does grep -n "NotifyCaptureLost" -r $WX/src/osx $WX/src/gtk $WX/src/msw # what each port actually does
``` ```
On Windows these lookups are bash: run them from Git Bash. PowerShell has no `grep`, and its `find` is
Windows' text-search `find.exe`. To locate the wx tree from PowerShell:
```powershell
$WX = Resolve-Path deps\*\dep_wxWidgets-prefix\src\dep_wxWidgets, deps\*\*\dep_wxWidgets-prefix\src\dep_wxWidgets -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Path
```
`interface/wx/<class>.h` is the documentation; `src/common` holds shared behaviour and `interface/wx/<class>.h` is the documentation; `src/common` holds shared behaviour and
`src/{msw,osx,gtk,unix,generic}` the per-port implementation. When the docs and the source disagree, `src/{msw,osx,gtk,unix,generic}` the per-port implementation. When the docs and the source disagree,
the source is what runs — the references mark such facts **[source]**. Orca-side design docs live in the source is what runs — the references mark such facts **[source]**. Orca-side design docs live in
@@ -331,6 +331,10 @@ OrcaSlicer: integer spinners are `::SpinInput` ([below](#spininput)); progress b
Contract: Contract:
- A page must be created with the book as its parent and added once; the book owns and deletes it - A page must be created with the book as its parent and added once; the book owns and deletes it
(`interface/wx/bookctrl.h:253-254, 273`). `RemovePage` detaches without deleting, and you then own it (`:324-330`). (`interface/wx/bookctrl.h:253-254, 273`). `RemovePage` detaches without deleting, and you then own it (`:324-330`).
- Removing the selected page selects the page before it (the new first page if it was first) through `SetSelection`,
so that page is shown and PAGE_CHANGING/CHANGED are sent; removing a page before the selection only shifts the
index. This is `wxBookCtrlBase::DoSetSelectionAfterRemoval` (`src/common/bookctrl.cpp:477-495` **[source]**),
called from `DoRemovePage` by `wxSimplebook`, `wxChoicebook`, `wxListbook`, `wxToolbook` and Orca's `Notebook`.
- `GetSelection()` inside a `PAGE_CHANGED` handler may return the old or the new page depending on the platform; use - `GetSelection()` inside a `PAGE_CHANGED` handler may return the old or the new page depending on the platform; use
`event.GetSelection()` (`interface/wx/bookctrl.h:160-166`). `event.GetSelection()` (`interface/wx/bookctrl.h:160-166`).
- `wxSimplebook` has no UI; switch with `ChangeSelection()`. `SetSelection()` sends PAGE_CHANGING/CHANGED - `wxSimplebook` has no UI; switch with `ChangeSelection()`. `SetSelection()` sends PAGE_CHANGING/CHANGED
@@ -10,7 +10,7 @@ wx asserts are compiled out in Orca (`wxDEBUG_LEVEL=0`), so every misuse below t
an assert fails silently. "GTK" means wxGTK3, Orca's Linux default (X11 and Wayland); GTK2 is only an assert fails silently. "GTK" means wxGTK3, Orca's Linux default (X11 and Wayland); GTK2 is only
an opt-out build (`-DDEP_WX_GTK3=OFF`), noted where it differs. Paths starting `interface/`, an opt-out build (`-DDEP_WX_GTK3=OFF`), noted where it differs. Paths starting `interface/`,
`include/`, `src/`, `docs/` are in the wx tree `include/`, `src/`, `docs/` are in the wx tree
(`find deps -maxdepth 5 -type d -path '*dep_wxWidgets-prefix/src/dep_wxWidgets'`); Orca paths are (located as in `SKILL.md` §Ground truth); Orca paths are
relative to `src/slic3r/GUI/`. relative to `src/slic3r/GUI/`.
Contents: [Rules](#rules) · [Mouse capture](#mouse-capture) · [Mouse events](#mouse-events) · Contents: [Rules](#rules) · [Mouse capture](#mouse-capture) · [Mouse events](#mouse-events) ·
@@ -332,7 +332,9 @@ indices: pages come and go per printer and per feature flag.
pages depending on the printer and on `use_printer_agents`; a removed page stays registered but is pages depending on the printer and on `use_printer_agents`; a removed page stays registered but is
not prebuilt (its `LazyPage::in_book()` is false). not prebuilt (its `LazyPage::in_book()` is false).
- Plugin pages are appended by `PluginPages::initialize` (`plugin/host/PluginPages.hpp`) with - Plugin pages are appended by `PluginPages::initialize` (`plugin/host/PluginPages.hpp`) with
namespaced ids (`plugin.<plugin_key>.<name>`) that cannot collide with `TAB_ID_*`. namespaced ids (`plugin.<plugin_key>.<name>`) that cannot collide with `TAB_ID_*`. Each is a
`LazyPage<PluginPage>` with order −1, destroyed when its capability goes away.
→ [Deferred construction](#deferred-construction-lazy-lazypage-stagedbuild-idlescheduler)
### Preset tabs ### Preset tabs
@@ -529,6 +531,32 @@ the main frame does nothing to a panel after creating it.
m_idle.add(m_diff_dialog); m_idle.add(m_diff_dialog);
``` ```
Cite: `IdleScheduler::tick`, `docs/HLSD/deferred-page-construction.md`. Cite: `IdleScheduler::tick`, `docs/HLSD/deferred-page-construction.md`.
- **Rule:** A lazy page that can be destroyed while the main frame lives takes a negative order and
stays out of `m_lazy_pages`.
**Why:** `m_lazy_pages` and `PrebuildQueue` hold raw `LazyBase*` and nothing removes one
(`PrebuildQueue` has only `add` and `clear`). The queue calls `pending()` on every task each slice,
and `prebuild_pages_when_idle` reads every entry of `m_lazy_pages`, so a page destroyed while still
listed can be read after it is freed. A page only taken out of the book is fine: it stays registered
and its `pending()` is false (`MainFrame::show_device`).
```cpp
// Right (PluginPages::create_page): order -1, and no m_lazy_pages.push_back
auto* page = new GUI::LazyPage<PluginPage>(m_parent, name, -1, [capability](wxWindow* parent) {
return new PluginPage(parent, capability);
});
```
Cite: `PluginPages::create_page`, `PluginPages::remove_page`.
- **Rule:** Remove several lazy pages from a book left to right.
**Why:** removing the selected page selects and shows the page before it
(`references/controls-dataview.md` §Book controls), and showing an unbuilt `LazyPage` while the frame
is shown builds it. In any other order the page before the selected one can be one removed next,
built only to be destroyed; left to right it is one that stays (unless the selected page is the
book's first).
```cpp
// Right (PluginPages::shutdown): m_order is the tabs' left-to-right order
for (const PluginCapabilityId& id : std::vector<PluginCapabilityId>(m_order))
remove_page(id);
```
Cite: `PluginPages::shutdown`, `PluginPages::relayout`, `PluginPages::on_plugin_deregister`.
## Plater and Sidebar ## Plater and Sidebar
@@ -6,7 +6,7 @@ to write a custom control on the wx side and how to author an Orca widget on the
`StaticBox`/`StateHandler` foundation. Read it before writing or reviewing any `wxEVT_PAINT` `StaticBox`/`StateHandler` foundation. Read it before writing or reviewing any `wxEVT_PAINT`
handler, `render`/`doRender` method, `messureSize`, or a new class under `src/slic3r/GUI/Widgets/`. handler, `render`/`doRender` method, `messureSize`, or a new class under `src/slic3r/GUI/Widgets/`.
wx cites are relative to the pinned wx 3.3.2 tree (`deps/build/<arch>/dep_wxWidgets-prefix/src/dep_wxWidgets`). wx cites are relative to the pinned wx 3.3.2 tree (located as in `SKILL.md` §Ground truth).
Orca builds wx with `wxBUILD_DEBUG_LEVEL=0` and `libslic3r_gui` with `wxDEBUG_LEVEL=0`: every Orca builds wx with `wxBUILD_DEBUG_LEVEL=0` and `libslic3r_gui` with `wxDEBUG_LEVEL=0`: every
wx assert below is compiled out and `wxCHECK*` returns silently, so paint misuse shows up only as wx assert below is compiled out and `wxCHECK*` returns silently, so paint misuse shows up only as
wrong, missing or stale pixels, never as an assert dialog. wrong, missing or stale pixels, never as an assert dialog.
@@ -78,8 +78,9 @@ Contents: [Rules](#rules) · [The wx build Orca uses](#the-wx-build-orca-uses)
`references/colours-dark-mode.md`. `references/colours-dark-mode.md`.
- The checked-out source is the tree that every wx citation in this skill refers to: - The checked-out source is the tree that every wx citation in this skill refers to:
`deps/build/<arch>/dep_wxWidgets-prefix/src/dep_wxWidgets` on macOS and `deps/build/<arch>/dep_wxWidgets-prefix/src/dep_wxWidgets` on macOS and
`deps/build/dep_wxWidgets-prefix/src/dep_wxWidgets` on Linux. Find it with `deps/<tree>/dep_wxWidgets-prefix/src/dep_wxWidgets` on Linux and Windows (`deps/build` for a
`find deps -maxdepth 5 -type d -path '*dep_wxWidgets-prefix/src/dep_wxWidgets'`. On macOS its release build; `build_win.bat` names the others). Locate it with the bash or PowerShell lookup in
`SKILL.md` §Ground truth. On macOS its
`src/osx/cocoa/colour.mm` already has the patch applied. `src/osx/cocoa/colour.mm` already has the patch applied.
- **Flatpak builds wx separately.** `deps/CMakeLists.txt` leaves `dep_wxWidgets` out of the deps - **Flatpak builds wx separately.** `deps/CMakeLists.txt` leaves `dep_wxWidgets` out of the deps
target when `FLATPAK` is set. Instead, `scripts/flatpak/com.orcaslicer.OrcaSlicer.yml` has its own target when `FLATPAK` is set. Instead, `scripts/flatpak/com.orcaslicer.OrcaSlicer.yml` has its own
@@ -5,7 +5,7 @@ How wx 3.3.2 popups and menus behave on each port, and the Orca wrappers built o
`append_menu_item`, `Plater::PopupMenu` and the macOS menubar versus `BBLTopbar`. Read it before you add `append_menu_item`, `Plater::PopupMenu` and the macOS menubar versus `BBLTopbar`. Read it before you add
or change anything that opens over other UI and must close by itself, or any context menu or menubar item. or change anything that opens over other UI and must close by itself, or any context menu or menubar item.
wx cites are relative to the wx tree root (`deps/build/<arch>/dep_wxWidgets-prefix/src/dep_wxWidgets`). wx cites are relative to the wx tree root (located as in `SKILL.md` §Ground truth).
**[source]** marks behaviour derived from the implementation that the wx docs do not state or contradict. **[source]** marks behaviour derived from the implementation that the wx docs do not state or contradict.
Orca builds wx with `wxBUILD_DEBUG_LEVEL=0`, so every "asserts" below means "fails silently in Orca". Orca builds wx with `wxBUILD_DEBUG_LEVEL=0`, so every "asserts" below means "fails silently in Orca".
"GTK" means wxGTK3 (X11 and Wayland), the default Linux build; GTK2 is only an opt-out (`-DDEP_WX_GTK3=OFF`). "GTK" means wxGTK3 (X11 and Wayland), the default Linux build; GTK2 is only an opt-out (`-DDEP_WX_GTK3=OFF`).
@@ -7,7 +7,7 @@ yields and nested event loops, progress dialogs, startup, shutdown and exception
screen, and the access rules for `wxGetApp()` and `app_config`. Read it whenever code runs off the screen, and the access rules for `wxGetApp()` and `app_config`. Read it whenever code runs off the
main thread, defers work, starts a timer, yields, shows progress, or runs during startup or shutdown. main thread, defers work, starts a timer, yields, shows progress, or runs during startup or shutdown.
wx cites are relative to the pinned wx 3.3.2 tree (`deps/build/<arch>/dep_wxWidgets-prefix/src/dep_wxWidgets`). wx cites are relative to the pinned wx 3.3.2 tree (located as in `SKILL.md` §Ground truth).
Orca builds wx with `wxBUILD_DEBUG_LEVEL=0` and `libslic3r_gui` with `wxDEBUG_LEVEL=0`, so `wxASSERT` Orca builds wx with `wxBUILD_DEBUG_LEVEL=0` and `libslic3r_gui` with `wxDEBUG_LEVEL=0`, so `wxASSERT`
is compiled out and `wxCHECK*` returns silently: a timer started off the main thread never fires on macOS, is compiled out and `wxCHECK*` returns silently: a timer started off the main thread never fires on macOS,
`Exit()` on a loop that is not the active one is ignored, `Start(0)` on macOS fails — all without `Exit()` on a loop that is not the active one is ignored, `Start(0)` on macOS fails — all without
@@ -6,10 +6,9 @@ port; what Orca's `DPIDialog`/`DPIFrame` add; the Orca dialog recipe; and the `M
Read it before writing or reviewing any dialog, frame, close handler, `Destroy()`/`delete`, or code Read it before writing or reviewing any dialog, frame, close handler, `Destroy()`/`delete`, or code
that keeps a pointer to a window across an event, a `CallAfter` or a modal loop. that keeps a pointer to a window across an event, a `CallAfter` or a modal loop.
wx cites are relative to the pinned wx 3.3.2 tree (`find deps -maxdepth 5 -type d -path wx cites are relative to the pinned wx 3.3.2 tree (located as in `SKILL.md` §Ground truth). wx is
'*dep_wxWidgets-prefix/src/dep_wxWidgets'`). wx is built with `wxBUILD_DEBUG_LEVEL=0` and built with `wxBUILD_DEBUG_LEVEL=0` and `libslic3r_gui` with `wxDEBUG_LEVEL=0`: every wx assert
`libslic3r_gui` with `wxDEBUG_LEVEL=0`: every wx assert quoted below is compiled out, so misuse quoted below is compiled out, so misuse fails silently (dropped call, stuck loop, freed memory), never with an assert dialog. "GTK" below
fails silently (dropped call, stuck loop, freed memory), never with an assert dialog. "GTK" below
means wxGTK as Orca builds it on Linux: GTK3 by default (X11 or Wayland); GTK2 is only an opt-out. means wxGTK as Orca builds it on Linux: GTK3 by default (X11 or Wayland); GTK2 is only an opt-out.
Contents: [Rules](#rules) · [1 Creating and parenting](#1-creating-and-parenting-windows) · Contents: [Rules](#rules) · [1 Creating and parenting](#1-creating-and-parenting-windows) ·
@@ -14,7 +14,7 @@ Contents: [Rules](#rules) · [1 Reading the change logs](#1-reading-the-change-l
[8 Migration done in Orca](#8-migration-already-done-in-orca) [8 Migration done in Orca](#8-migration-already-done-in-orca)
All `docs/`, `interface/`, `include/`, `src/`, `build/` cites are relative to the pinned wx tree All `docs/`, `interface/`, `include/`, `src/`, `build/` cites are relative to the pinned wx tree
(`find deps -maxdepth 5 -type d -path '*dep_wxWidgets-prefix/src/dep_wxWidgets'`), except paths (located as in `SKILL.md` §Ground truth), except paths
explicitly called Orca's (`deps/…`, Orca's `src/CMakeLists.txt`) and bare Orca file + symbol cites. explicitly called Orca's (`deps/…`, Orca's `src/CMakeLists.txt`) and bare Orca file + symbol cites.
## Rules ## Rules
+2 -1
View File
@@ -184,7 +184,8 @@ class MainFrame : public DPIFrame
std::string m_name{ "prepare_layout" }; std::string m_name{ "prepare_layout" };
wxSize m_laid_out_size; wxSize m_laid_out_size;
} m_prepare_layout_prebuild{ *this }; } m_prepare_layout_prebuild{ *this };
// Every LazyPage, in and out of the book; prebuild_pages_when_idle() registers them. // Every built-in LazyPage, in and out of the book; prebuild_pages_when_idle() registers them.
// Plugin pages stay out: PluginPages destroys them at runtime.
std::vector<LazyBase*> m_lazy_pages; std::vector<LazyBase*> m_lazy_pages;
// The latest EVT_LOAD_PRINTER_URL, applied when the web Device view is built. // The latest EVT_LOAD_PRINTER_URL, applied when the web Device view is built.
wxString m_printer_url; wxString m_printer_url;
+38 -24
View File
@@ -119,33 +119,13 @@ PrinterWebView::PrinterWebView(wxWindow *parent)
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
// Create the webview // Create the webview
m_browser = WebView::CreateWebView(this, ""); create_browser();
if (m_browser == nullptr) { m_reset_on_show = WebView::NeedsRecreateOnShow();
wxLogError("Could not init m_browser");
return;
}
#ifdef __linux__
inject_vue_resize_workaround(m_browser);
auto cookiesPath = boost::filesystem::path(data_dir() + "/cache/cookies.db");
auto wv = static_cast<WebKitWebView*>(m_browser->GetNativeBackend());
auto wv_ctx = webkit_web_view_get_context(wv);
auto cookieManager = webkit_web_context_get_cookie_manager(wv_ctx);
webkit_cookie_manager_set_persistent_storage(cookieManager, cookiesPath.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
#endif
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PrinterWebView::OnError, this);
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PrinterWebView::OnLoaded, this);
m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PrinterWebView::OnNewWindow, this);
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PrinterWebView::OnScriptMessage, this);
SetSizer(topsizer); SetSizer(topsizer);
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
update_mode();
// Log backend information // Log backend information
/* m_browser->GetUserAgent() may lead crash /* m_browser->GetUserAgent() may lead crash
if (wxGetApp().get_mode() == comDevelop) { if (wxGetApp().get_mode() == comDevelop) {
@@ -177,12 +157,44 @@ PrinterWebView::~PrinterWebView()
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " End"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " End";
} }
void PrinterWebView::create_browser()
{
m_browser = WebView::CreateWebView(this, "");
#ifdef __linux__
inject_vue_resize_workaround(m_browser);
auto cookiesPath = boost::filesystem::path(data_dir() + "/cache/cookies.db");
auto wv = static_cast<WebKitWebView*>(m_browser->GetNativeBackend());
auto wv_ctx = webkit_web_view_get_context(wv);
auto cookieManager = webkit_web_context_get_cookie_manager(wv_ctx);
webkit_cookie_manager_set_persistent_storage(cookieManager, cookiesPath.c_str(), WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
#endif
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PrinterWebView::OnError, this);
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PrinterWebView::OnLoaded, this);
m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PrinterWebView::OnNewWindow, this);
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PrinterWebView::OnScriptMessage, this);
update_mode();
}
void PrinterWebView::reset_browser()
{
m_browser->Destroy(); // also removes it from the sizer
create_browser();
GetSizer()->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
Layout();
// OnLoaded may have cleared m_url_deferred already, so requeue the last url for Show().
m_apikey_sent = false;
m_url_deferred = m_url;
}
void PrinterWebView::load_url(wxString& url, wxString apikey) void PrinterWebView::load_url(wxString& url, wxString apikey)
{ {
// this->Show(); // this->Show();
// this->Raise(); // this->Raise();
if (m_browser == nullptr) m_url = url;
return;
m_apikey = apikey; m_apikey = apikey;
m_apikey_sent = false; m_apikey_sent = false;
m_handler = create_printer_webview_handler(*this); m_handler = create_printer_webview_handler(*this);
@@ -200,6 +212,8 @@ void PrinterWebView::load_url(wxString& url, wxString apikey)
bool PrinterWebView::Show(bool show) bool PrinterWebView::Show(bool show)
{ {
if (show && std::exchange(m_reset_on_show, false))
reset_browser();
if (show && !m_url_deferred.empty()) { if (show && !m_url_deferred.empty()) {
m_browser->LoadURL(m_url_deferred); m_browser->LoadURL(m_url_deferred);
//ORCA: m_url_deferred will be cleared on load success //ORCA: m_url_deferred will be cleared on load success
+5
View File
@@ -58,13 +58,18 @@ private:
friend class PrinterWebViewHandler; friend class PrinterWebViewHandler;
void SendAPIKey(); void SendAPIKey();
void create_browser();
void reset_browser();
wxWebView* m_browser; wxWebView* m_browser;
long m_zoomFactor; long m_zoomFactor;
wxString m_apikey; wxString m_apikey;
bool m_apikey_sent; bool m_apikey_sent;
// Last url passed to load_url(), reloaded after reset_browser().
wxString m_url;
wxString m_url_deferred; wxString m_url_deferred;
std::unique_ptr<PrinterWebViewHandler> m_handler; std::unique_ptr<PrinterWebViewHandler> m_handler;
bool m_reset_on_show{false};
// DECLARE_EVENT_TABLE() // DECLARE_EVENT_TABLE()
}; };
+34 -13
View File
@@ -80,16 +80,10 @@ ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos,
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
m_browser = WebView::CreateWebView(this, m_project_home_url); create_browser();
if (m_browser == nullptr) { m_reset_on_show = WebView::NeedsRecreateOnShow();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("load web view of project page failed");
return;
}
//m_browser->Hide(); //m_browser->Hide();
main_sizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); main_sizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
m_browser->Bind(wxEVT_WEBVIEW_NAVIGATED, &ProjectPanel::on_navigated, this);
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &ProjectPanel::OnScriptMessage, this, m_browser->GetId());
Bind(wxEVT_WEBVIEW_NAVIGATING, &ProjectPanel::onWebNavigating, this, m_browser->GetId());
Bind(EVT_PROJECT_RELOAD, &ProjectPanel::on_reload, this); Bind(EVT_PROJECT_RELOAD, &ProjectPanel::on_reload, this);
@@ -117,6 +111,23 @@ void ProjectPanel::shutdown()
m_reload_task.reset(); m_reload_task.reset();
} }
void ProjectPanel::create_browser()
{
m_browser = WebView::CreateWebView(this, m_project_home_url);
m_browser->Bind(wxEVT_WEBVIEW_NAVIGATED, &ProjectPanel::on_navigated, this);
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &ProjectPanel::OnScriptMessage, this, m_browser->GetId());
m_browser->Bind(wxEVT_WEBVIEW_NAVIGATING, &ProjectPanel::onWebNavigating, this);
}
void ProjectPanel::reset_browser()
{
m_browser->Destroy(); // also removes it from the sizer
create_browser();
GetSizer()->Insert(0, m_browser, wxSizerFlags().Expand().Proportion(1));
Layout();
m_web_init_completed.store(false, std::memory_order_release);
}
// Helper to convert newlines to <br> // Helper to convert newlines to <br>
static std::string convert_newlines_to_br(const std::string& text) { static std::string convert_newlines_to_br(const std::string& text) {
std::string result = text; std::string result = text;
@@ -281,12 +292,13 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore)); wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore));
if (m_web_init_completed.load(std::memory_order_acquire) && if (!cancel_token->load(std::memory_order_acquire) && wxTheApp != nullptr && !wxGetApp().is_closing()) {
!cancel_token->load(std::memory_order_acquire) && wxTheApp != nullptr && !wxGetApp().is_closing()) {
wxGetApp().CallAfter([this, cancel_token, strJS] { wxGetApp().CallAfter([this, cancel_token, strJS] {
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
return; return;
RunScript(strJS.ToStdString()); m_info_script = strJS.ToStdString();
if (m_web_init_completed.load(std::memory_order_acquire))
RunScript(m_info_script);
}); });
} }
}); });
@@ -327,6 +339,11 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt)
} }
else if (strCmd == "request_3mf_info") { else if (strCmd == "request_3mf_info") {
m_web_init_completed.store(true, std::memory_order_release); m_web_init_completed.store(true, std::memory_order_release);
// Replay the stored info after each page load.
CallAfter([this] {
if (!m_info_script.empty())
RunScript(m_info_script);
});
} }
else if (strCmd == "edit_project_info") { else if (strCmd == "edit_project_info") {
show_info_editor(true); show_info_editor(true);
@@ -383,6 +400,8 @@ void ProjectPanel::clear_model_info()
wxGetApp().CallAfter([this, cancel_token, strJS] { wxGetApp().CallAfter([this, cancel_token, strJS] {
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
return; return;
// Runs after any store queued by an earlier reload pass, so stale info is never replayed.
m_info_script.clear();
RunScript(strJS.ToStdString()); RunScript(strJS.ToStdString());
}); });
} }
@@ -523,10 +542,12 @@ void ProjectPanel::RunScript(std::string content)
WebView::RunScript(m_browser, content); WebView::RunScript(m_browser, content);
} }
bool ProjectPanel::Show(bool show) bool ProjectPanel::Show(bool show)
{ {
if (show && std::exchange(m_reset_on_show, false))
reset_browser();
if (show) update_model_data(); if (show) update_model_data();
return wxPanel::Show(show); return wxPanel::Show(show);
} }
}} // namespace Slic3r::GUI }} // namespace Slic3r::GUI
+5
View File
@@ -80,9 +80,14 @@ private:
AuxiliaryPanel* m_auxiliary{nullptr}; AuxiliaryPanel* m_auxiliary{nullptr};
wxString m_project_home_url; wxString m_project_home_url;
wxString m_root_dir; wxString m_root_dir;
// Last show_3mf_info script, also sent whenever the page asks for it.
std::string m_info_script;
bool m_reset_on_show{false};
static inline std::atomic<int> m_sequence_id{8000}; static inline std::atomic<int> m_sequence_id{8000};
void show_info_editor(bool show); void show_info_editor(bool show);
void create_browser();
void reset_browser();
public: public:
+25 -8
View File
@@ -62,10 +62,10 @@ namespace GUI {
WebViewPanel::WebViewPanel(wxWindow *parent) WebViewPanel::WebViewPanel(wxWindow *parent)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
{ {
wxString url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/homepage/index.html"); m_home_url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/homepage/index.html");
wxString strlang = wxGetApp().current_language_code_safe(); wxString strlang = wxGetApp().current_language_code_safe();
if (strlang != "") if (strlang != "")
url += "?lang=" + strlang; m_home_url += "?lang=" + strlang;
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
@@ -109,12 +109,8 @@ WebViewPanel::WebViewPanel(wxWindow *parent)
m_info = new wxInfoBar(this); m_info = new wxInfoBar(this);
topsizer->Add(m_info, wxSizerFlags().Expand()); topsizer->Add(m_info, wxSizerFlags().Expand());
// Create the webview // Create the webview
m_browser = WebView::CreateWebView(this, url); create_browser();
if (m_browser == nullptr) { m_reset_on_show = WebView::NeedsRecreateOnShow();
wxLogError("Could not init m_browser");
return;
}
m_browser->Hide();
SetSizer(topsizer); SetSizer(topsizer);
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
@@ -264,6 +260,27 @@ WebViewPanel::~WebViewPanel()
} }
void WebViewPanel::create_browser()
{
m_browser = WebView::CreateWebView(this, m_home_url);
m_browser->Hide();
}
void WebViewPanel::reset_browser()
{
m_browser->Destroy(); // also removes it from the sizer
create_browser();
GetSizer()->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
Layout();
}
bool WebViewPanel::Show(bool show)
{
if (show && std::exchange(m_reset_on_show, false))
reset_browser();
return wxPanel::Show(show);
}
void WebViewPanel::load_url(wxString& url) void WebViewPanel::load_url(wxString& url)
{ {
this->Show(); this->Show();
+6
View File
@@ -113,9 +113,15 @@ public:
int get_model_mall_detail_url(std::string *url, std::string id); int get_model_mall_detail_url(std::string *url, std::string id);
void update_mode(); void update_mode();
bool Show(bool show = true) override;
private: private:
void create_browser();
void reset_browser();
wxWebView* m_browser; wxWebView* m_browser;
wxString m_home_url;
bool m_reset_on_show{false};
wxButton * m_button_stop; wxButton * m_button_stop;
wxTextCtrl *m_url; wxTextCtrl *m_url;
#if !BBL_RELEASE_TO_PUBLIC #if !BBL_RELEASE_TO_PUBLIC
+11
View File
@@ -381,6 +381,17 @@ void WebView::MarkScriptMessageHandlerAdded(wxWebView * webView)
if (WebViewRef *ref = webview_ref(webView)) if (WebViewRef *ref = webview_ref(webView))
ref->m_script_handler_added = true; ref->m_script_handler_added = true;
} }
bool WebView::NeedsRecreateOnShow()
{
const bool recreating = Slic3r::GUI::wxGetApp().is_recreating_gui();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": is_recreating_gui = " << recreating;
#ifdef __WIN32__
return recreating;
#else
return false;
#endif
}
#if wxUSE_WEBVIEW_EDGE #if wxUSE_WEBVIEW_EDGE
bool WebView::CheckWebViewRuntime() bool WebView::CheckWebViewRuntime()
{ {
+4
View File
@@ -23,6 +23,10 @@ public:
// Marks "wx" as registered so CreateWebView's deferred add skips the duplicate. // Marks "wx" as registered so CreateWebView's deferred add skips the duplicate.
static void MarkScriptMessageHandlerAdded(wxWebView * webView); static void MarkScriptMessageHandlerAdded(wxWebView * webView);
// On Windows, a WebView2 backend created during a GUI rebuild (language switch) can come up
// ignoring every navigation. A panel that gets true here recreates its view on first Show().
static bool NeedsRecreateOnShow();
static void RecreateAll(); static void RecreateAll();
}; };
+32 -25
View File
@@ -1,6 +1,7 @@
#include "PluginPages.hpp" #include "PluginPages.hpp"
#include "libslic3r/AppConfig.hpp" #include "libslic3r/AppConfig.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/Notebook.hpp" #include "slic3r/GUI/Notebook.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Widgets/Button.hpp" #include "slic3r/GUI/Widgets/Button.hpp"
@@ -183,8 +184,10 @@ void PluginPages::initialize(Notebook* parent)
void PluginPages::shutdown() void PluginPages::shutdown()
{ {
while (!m_pages.empty()) // Removing the selected tab selects the tab to its left. In tab order that is a built-in tab,
remove_page(m_pages.begin()->first); // never an unbuilt plugin page that is removed next and would be built only to be destroyed.
for (const PluginCapabilityId& id : std::vector<PluginCapabilityId>(m_order))
remove_page(id);
m_parent = nullptr; m_parent = nullptr;
} }
@@ -225,8 +228,7 @@ bool PluginPages::create_page(const PluginCapabilityId& id)
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to get icon for plugin " << id.plugin_key; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to get icon for plugin " << id.plugin_key;
} }
auto* page = new PluginPage(m_parent, std::move(capability)); wxBitmap bitmap;
if (!icon.empty()) { if (!icon.empty()) {
try { try {
boost::filesystem::path icon_path(icon); boost::filesystem::path icon_path(icon);
@@ -234,7 +236,7 @@ bool PluginPages::create_page(const PluginCapabilityId& id)
if (extension == ".svg" || extension == ".png") if (extension == ".svg" || extension == ".png")
icon_path.replace_extension(); icon_path.replace_extension();
page->set_icon(create_scaled_bitmap(icon_path.string(), m_parent, 20)); bitmap = create_scaled_bitmap(icon_path.string(), m_parent, 20);
} catch (const std::exception& error) { } catch (const std::exception& error) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key << ": " << error.what(); BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key << ": " << error.what();
} catch (...) { } catch (...) {
@@ -242,7 +244,15 @@ bool PluginPages::create_page(const PluginCapabilityId& id)
} }
} }
m_pages.emplace(id, page); // Built the first time its tab is shown, so a language switch never creates the page's browser
// while the main window is being rebuilt. Never prebuilt: the idle queue cannot drop a page
// that remove_page() destroys.
auto* page = new GUI::LazyPage<PluginPage>(m_parent, GUI::into_u8(page_tab_id(id)), -1,
[capability = std::move(capability)](wxWindow* parent) {
return new PluginPage(parent, capability);
});
m_pages.emplace(id, Page{page, bitmap});
m_order.push_back(id); m_order.push_back(id);
return true; return true;
} }
@@ -271,16 +281,10 @@ void PluginPages::on_plugin_register(const std::string& plugin_key)
void PluginPages::on_plugin_deregister(const std::string& plugin_key) void PluginPages::on_plugin_deregister(const std::string& plugin_key)
{ {
for (auto it = m_pages.begin(); it != m_pages.end();) { // In tab order, as in shutdown().
if (it->first.plugin_key != plugin_key) { for (const PluginCapabilityId& id : std::vector<PluginCapabilityId>(m_order))
++it; if (id.plugin_key == plugin_key)
continue; remove_page(id);
}
const PluginCapabilityId id = it->first;
++it;
remove_page(id);
}
} }
void PluginPages::remove_page(const PluginCapabilityId& id) void PluginPages::remove_page(const PluginCapabilityId& id)
@@ -289,8 +293,10 @@ void PluginPages::remove_page(const PluginCapabilityId& id)
if (it == m_pages.end()) if (it == m_pages.end())
return; return;
PluginPage* page = it->second; GUI::LazyPage<PluginPage>* page = it->second.page;
page->detach_capability(); // Only a built page has installed a message sender on the capability.
if (PluginPage* built = page->get())
built->detach_capability();
m_pages.erase(it); m_pages.erase(it);
m_order.erase(std::remove(m_order.begin(), m_order.end(), id), m_order.end()); m_order.erase(std::remove(m_order.begin(), m_order.end(), id), m_order.end());
@@ -348,26 +354,27 @@ void PluginPages::relayout()
bool up_to_date = page_count >= tab_ids.size(); bool up_to_date = page_count >= tab_ids.size();
for (size_t i = 0; up_to_date && i < tab_ids.size(); ++i) for (size_t i = 0; up_to_date && i < tab_ids.size(); ++i)
up_to_date = m_parent->GetPageName(page_count - tab_ids.size() + i) == page_tab_id(tab_ids[i]); up_to_date = m_parent->GetPageName(page_count - tab_ids.size() + i) == page_tab_id(tab_ids[i]);
for (const auto& [id, page] : m_pages) { for (const auto& [id, entry] : m_pages) {
if (!up_to_date) if (!up_to_date)
break; break;
const bool wanted = std::find(tab_ids.begin(), tab_ids.end(), id) != tab_ids.end(); const bool wanted = std::find(tab_ids.begin(), tab_ids.end(), id) != tab_ids.end();
up_to_date = (m_parent->FindPage(page) != wxNOT_FOUND) == wanted; up_to_date = entry.page->in_book() == wanted;
} }
if (!up_to_date) { if (!up_to_date) {
const wxString id_to_reselect = m_parent->GetSelectedPageName(); const wxString id_to_reselect = m_parent->GetSelectedPageName();
for (const auto& [id, page] : m_pages) { // In tab order, as in shutdown().
const int idx = m_parent->FindPage(page); for (const auto& id : m_order) {
const int idx = m_parent->FindPage(m_pages.at(id).page);
if (idx != wxNOT_FOUND) if (idx != wxNOT_FOUND)
m_parent->RemovePage(idx); m_parent->RemovePage(idx);
} }
for (const auto& id : tab_ids) { for (const auto& id : tab_ids) {
PluginPage* page = m_pages.at(id); const Page& entry = m_pages.at(id);
m_parent->InsertPage(m_parent->GetPageCount(), page_tab_id(id), page, wxString::FromUTF8(id.name), "", m_parent->InsertPage(m_parent->GetPageCount(), page_tab_id(id), entry.page, wxString::FromUTF8(id.name), "",
false, page->icon()); false, entry.icon);
} }
if (!id_to_reselect.empty()) if (!id_to_reselect.empty())
+8 -4
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <slic3r/GUI/LazyPage.hpp>
#include <slic3r/GUI/WebPanel.hpp> #include <slic3r/GUI/WebPanel.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp> #include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp> #include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp>
@@ -29,8 +30,6 @@ public:
void detach_capability(); void detach_capability();
void push_message(const std::string& message); void push_message(const std::string& message);
void set_icon(const wxBitmap& icon) { m_icon = icon; }
const wxBitmap& icon() const { return m_icon; }
protected: protected:
std::optional<std::string> page_html() override; std::optional<std::string> page_html() override;
@@ -41,7 +40,6 @@ private:
std::shared_ptr<PagesPluginCapability> m_cap; std::shared_ptr<PagesPluginCapability> m_cap;
std::shared_ptr<std::atomic<PluginPage*>> m_lifetime; std::shared_ptr<std::atomic<PluginPage*>> m_lifetime;
wxBitmap m_icon;
}; };
class PluginPages class PluginPages
@@ -73,7 +71,13 @@ private:
void show_overflow_menu(); void show_overflow_menu();
static wxString page_tab_id(const PluginCapabilityId& id); static wxString page_tab_id(const PluginCapabilityId& id);
std::map<PluginCapabilityId, PluginPage*> m_pages; struct Page
{
GUI::LazyPage<PluginPage>* page{nullptr};
wxBitmap icon;
};
std::map<PluginCapabilityId, Page> m_pages;
std::vector<PluginCapabilityId> m_order; std::vector<PluginCapabilityId> m_order;
Notebook* m_parent{nullptr}; Notebook* m_parent{nullptr};