unify orca web dialog style

This commit is contained in:
SoftFever
2026-07-05 01:17:17 +08:00
parent 383969d456
commit 5d8aa9610a
22 changed files with 439 additions and 544 deletions

View File

@@ -7,7 +7,7 @@
<link rel="stylesheet" href="./styles.css" />
<link rel="stylesheet" type="text/css" href="../../include/global.css" /> <!-- ORCA One for all-->
<link rel="stylesheet" type="text/css" href="../css/common.css" />
<link rel="stylesheet" type="text/css" href="../css/dark.css" />
<link rel="stylesheet" type="text/css" href="../css/theme.css" />
<script type="text/javascript" src="../js/jquery-3.6.0.min.js"></script>
<script type="text/javascript" src="../js/json2.js"></script>
<script type="text/javascript" src="../../data/text.js"></script>

View File

@@ -1,23 +1,11 @@
:root {
--cbr-border-color: #d2d2d7;
--cbr-header-bg: #f6f7f9;
--cbr-panel-bg: #ffffff;
--cbr-input-bg: #ffffff;
--cbr-input-focus-bg: #f2f8f7;
--cbr-label-color: #7b7b84;
--cbr-icon-color: #75757f;
}
@media (prefers-color-scheme: dark) {
:root {
--cbr-border-color: #4a4a51;
--cbr-header-bg: #2f2f34;
--cbr-panel-bg: #2d2d31;
--cbr-input-bg: #2d2d31;
--cbr-input-focus-bg: #3b3b41;
--cbr-label-color: #b9b9bc;
--cbr-icon-color: #b9b9bc;
}
--cbr-border-color: var(--border);
--cbr-header-bg: var(--panel);
--cbr-panel-bg: var(--bg);
--cbr-input-bg: var(--bg);
--cbr-input-focus-bg: var(--row-hover);
--cbr-label-color: var(--muted);
--cbr-icon-color: var(--muted);
}
.cbr-browser-container {

View File

@@ -7,7 +7,7 @@
<link rel="stylesheet" href="./styles.css" />
<link rel="stylesheet" type="text/css" href="../../include/global.css" />
<link rel="stylesheet" type="text/css" href="../css/common.css" />
<link rel="stylesheet" type="text/css" href="../css/dark.css" />
<link rel="stylesheet" type="text/css" href="../css/theme.css" />
<script type="text/javascript" src="../js/jquery-3.6.0.min.js"></script>
<script type="text/javascript" src="../js/json2.js"></script>
<script type="text/javascript" src="../../data/text.js"></script>

View File

@@ -1,36 +1,3 @@
:root {
--bg: #ffffff;
--panel: #ffffff;
--border: #d8d8d8;
--border-strong: #e6e6e6;
--col-sep: #e1e1e1;
--text: #1f2328;
--muted: #6f7782;
--row-hover: #f7f9fb;
--row-selected: #eaf2ff;
--row-selected-outline: #b7d0ff;
--border-soft: #f0f0f0;
--danger: #b42318;
--ok: #137333;
--warn: #a15c00;
--plugin-status-danger: var(--danger);
--plugin-status-ok: var(--ok);
--plugin-status-warn: var(--warn);
--plugin-status-inactive: var(--muted);
--plugin-status-danger-bg: rgba(180, 35, 24, 0.12);
--plugin-status-ok-bg: rgba(19, 115, 51, 0.12);
--plugin-status-warn-bg: rgba(199, 122, 22, 0.12);
--plugin-status-inactive-bg: #edf1f5;
--plugin-status-inactive-text: #44505c;
--plugin-source-mine-bg: rgba(0, 137, 123, 0.12);
--plugin-source-mine-text: #00897b;
--plugin-source-neutral-bg: #edf1f5;
--plugin-source-neutral-text: #44505c;
--plugin-source-subscribed-bg: rgba(37, 99, 170, 0.12);
--plugin-source-subscribed-text: #2563aa;
--plugin-link-text: #00796b;
}
html,
body {
width: 100% !important;
@@ -929,9 +896,9 @@ body {
width: 12px;
height: 12px;
box-sizing: border-box;
border: 1px solid #4a4a51;
border: 1px solid var(--border);
border-radius: 2px;
background: #2d2d31;
background: var(--bg);
}
.plugin-checkbox-input:checked + .plugin-checkbox-mark {

View File

@@ -8,7 +8,7 @@
<link rel="stylesheet" type="text/css" href="../../include/global.css" /> <!-- ORCA One for all-->
<link rel="stylesheet" type="text/css" href="../css/common.css" />
<!-- <link rel="stylesheet" type="text/css" href="23.css" /> -->
<link rel="stylesheet" type="text/css" href="../css/dark.css" />
<link rel="stylesheet" type="text/css" href="../css/theme.css" />
<script type="text/javascript" src="../js/jquery-3.6.0.min.js"></script>
<script type="text/javascript" src="../js/json2.js"></script>
<script type="text/javascript" src="../../data/text.js"></script>

View File

@@ -1,26 +1,3 @@
:root {
--bg: #ffffff;
--panel: #ffffff;
--border: #d8d8d8;
--border-strong: #e6e6e6;
--border-soft: #f0f0f0;
--col-sep: #e1e1e1;
--text: #1f2328;
--row-hover: #f7f9fb;
--row-selected: #eaf2ff;
--row-selected-outline: #b7d0ff;
--footer-bg: #fafafa;
--btn-bg: #ffffff;
--btn-border: #cccccc;
--btn-hover: #f0f0f0;
--ctx-bg: #ffffff;
--ctx-border: #cccccc;
--ctx-hover: #efefef;
}
html, body {
height: 100%;
margin: 0;

View File

@@ -7,7 +7,7 @@
<link rel="stylesheet" href="../../include/xterm/xterm.css" />
<link rel="stylesheet" type="text/css" href="../../include/global.css" />
<link rel="stylesheet" type="text/css" href="../css/common.css" />
<link rel="stylesheet" type="text/css" href="../css/dark.css" />
<link rel="stylesheet" type="text/css" href="../css/theme.css" />
<link rel="stylesheet" href="./styles.css" />
<script type="text/javascript" src="../js/jquery-3.6.0.min.js"></script>
<script type="text/javascript" src="../js/json2.js"></script>

View File

@@ -1,6 +1,40 @@
let term = null;
let fitAddon = null;
// ANSI palettes for the xterm theme — the standard VS Code dark/light sets, picked by
// data-orca-theme. Constant data, so defined once at module scope.
const ANSI_DARK = {
black:"#000000", red:"#cd3131", green:"#0dbc79", yellow:"#e5e510", blue:"#2472c8",
magenta:"#bc3fbc", cyan:"#11a8cd", white:"#e5e5e5",
brightBlack:"#666666", brightRed:"#f14c4c", brightGreen:"#23d18b", brightYellow:"#f5f543",
brightBlue:"#3b8eea", brightMagenta:"#d670d6", brightCyan:"#29b8db", brightWhite:"#ffffff"
};
const ANSI_LIGHT = {
black:"#000000", red:"#cd3131", green:"#107c10", yellow:"#949800", blue:"#0451a5",
magenta:"#bc05bc", cyan:"#0598bc", white:"#555555",
brightBlack:"#8a8a8a", brightRed:"#cd3131", brightGreen:"#14ce14", brightYellow:"#b5ba00",
brightBlue:"#0451a5", brightMagenta:"#bc05bc", brightCyan:"#0598bc", brightWhite:"#a5a5a5"
};
// The xterm.js theme is a plain JS object (it cannot read CSS variables), so build it
// from the host contract: background/foreground come from the injected --orca-* colors;
// the ANSI palette is the fixed light or dark set chosen by data-orca-theme.
function XtermTheme() {
var cs = getComputedStyle(document.documentElement);
var bg = (cs.getPropertyValue('--orca-bg') || '#1e1e1e').trim();
var fg = (cs.getPropertyValue('--orca-fg') || '#d4d4d4').trim();
var dark = document.documentElement.getAttribute('data-orca-theme') !== 'light';
return Object.assign({ background: bg, foreground: fg, cursor: fg }, dark ? ANSI_DARK : ANSI_LIGHT);
}
// Re-apply the xterm theme when the host flips data-orca-theme (live re-theme).
function WatchXtermTheme() {
var obs = new MutationObserver(function () {
if (term) term.options.theme = XtermTheme();
});
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-orca-theme'] });
}
function OnInit() {
if (typeof TranslatePage === "function")
TranslatePage();
@@ -9,30 +43,12 @@ function OnInit() {
cursorBlink: true,
fontSize: 13,
fontFamily: '"Cascadia Code", "Fira Code", "JetBrains Mono", monospace',
theme: {
background: "#1e1e1e",
foreground: "#d4d4d4",
cursor: "#ffffff",
black: "#000000",
red: "#cd3131",
green: "#0dbc79",
yellow: "#e5e510",
blue: "#2472c8",
magenta: "#bc3fbc",
cyan: "#11a8cd",
white: "#e5e5e5",
brightBlack: "#666666",
brightRed: "#f14c4c",
brightGreen: "#23d18b",
brightYellow: "#f5f543",
brightBlue: "#3b8eea",
brightMagenta: "#d670d6",
brightCyan: "#29b8db",
brightWhite: "#ffffff"
},
theme: XtermTheme(),
allowProposedApi: true
});
WatchXtermTheme();
fitAddon = new (FitAddon.FitAddon || FitAddon)();
term.loadAddon(fitAddon);

View File

@@ -7,8 +7,8 @@
html, body {
height: 100%;
overflow: hidden;
background: #1e1e1e;
color: #d4d4d4;
background: var(--bg);
color: var(--text);
}
.app {
@@ -41,34 +41,34 @@ html, body {
#cmd-input {
flex: 1;
padding: 6px 10px;
border: 1px solid #444;
border: 1px solid var(--border);
border-radius: 4px;
background: #2d2d2d;
color: #d4d4d4;
background: var(--bg);
color: var(--text);
font-family: inherit;
font-size: 13px;
outline: none;
}
#cmd-input:focus {
border-color: #666;
border-color: var(--orca-accent);
}
#run-btn {
padding: 6px 20px;
border: none;
border-radius: 4px;
background: #0e639c;
color: #fff;
background: var(--orca-accent);
color: var(--orca-accent-fg);
font-size: 13px;
cursor: pointer;
}
#run-btn:hover {
background: #1177bb;
filter: brightness(1.1);
}
#run-btn:disabled {
background: #555;
opacity: .5;
cursor: not-allowed;
}

View File

@@ -1,116 +0,0 @@
:root {
--bg: #1b1f24;
--panel: #242a31;
--border: #3a424d;
--border-strong: #3a424d;
--border-soft: #313843;
--col-sep: #3a424d;
--text: #e6ebf0;
--muted: #b9c0c8;
--row-hover: #2b3340;
--row-selected: #244945;
--row-selected-outline: #00bfa5;
--plugin-status-danger: #ff7b72;
--plugin-status-ok: #37c871;
--plugin-status-warn: #f0b45a;
--plugin-status-inactive: #b9c0c8;
--plugin-status-danger-bg: rgba(255, 123, 114, 0.16);
--plugin-status-ok-bg: rgba(55, 200, 113, 0.16);
--plugin-status-warn-bg: rgba(240, 180, 90, 0.18);
--plugin-status-inactive-bg: #36363b;
--plugin-status-inactive-text: #c7ccd2;
--plugin-source-mine-bg: rgba(0, 150, 136, 0.18);
--plugin-source-mine-text: #8de5d6;
--plugin-source-neutral-bg: #36363b;
--plugin-source-neutral-text: #c7ccd2;
--plugin-source-subscribed-bg: rgba(88, 166, 255, 0.18);
--plugin-source-subscribed-text: #8fc0f0;
--plugin-link-text: #62d8ca;
--footer-bg: #20262d;
--btn-bg: #2a313a;
--btn-border: #4b5664;
--btn-hover: #333c47;
--ctx-bg: #2a313a;
--ctx-border: #4b5664;
--ctx-hover: #3a4451;
}
*
{
color: #efeff0;
border-color: #B9B9BC;
}
body
{
background-color:#2D2D31; /* ORCA match background color */
color: #efeff0;
}
.ZScrol::-webkit-scrollbar-thumb {/*滚动条里面小方块*/
background-color: #939594;
}
.ZScrol::-webkit-scrollbar-track {/*滚动条里面轨道*/
background: #161817;
}
#Title div
{
color: #009688;
}
.search>input[type=text]{
background-color:#2D2D31;
}
/*---Checkboxes ORCA---*/
input[type=checkbox]{
background-color:#2D2D31;
border-color:#4A4A51;
}
input[type=checkbox]:checked{
background-color:#009688;
}
/*-------Text------*/
.TextS1
{
}
.TextS2
{
color:#B9B9BC;
}
/*---Policy---*/
.TextArea1
{
background-color: #4A4A51;
color: #BEBEC0;
}
/*----Region---*/
.RegionItem:hover
{
background-color:#4C4C55;
}
.RegionSelected:hover
{
background-color:#009688;
color: #fff;
}
/*----Menu----*/
#Title div.TitleUnselected
{
color: #BEBEC0;
}

View File

@@ -0,0 +1,100 @@
/* Shared theme for resources/web/dialog/* pages.
*
* App-matched roles are pulled from the host theme "contract" (:root{--orca-*}),
* injected from live C++ app colors by WebViewHostDialog. Web-only semantics (row
* selection, status/source badges, footer/button/context surfaces) have no native
* equivalent, so they are curated here for light and selected for dark by the
* [data-orca-theme="dark"] attribute the host sets — the single authoritative theme
* signal on all platforms (prefers-color-scheme is intentionally not used).
*
* This file is linked AFTER common.css so its element rules re-theme the shared chrome
* via variables (replacing the retired dark.css). It replaces each page's own :root
* palette and the dark.css <link>. */
:root {
/* --- app-matched roles (from the injected contract) --- */
--bg: var(--orca-bg);
--panel: var(--orca-bg);
--text: var(--orca-fg);
--muted: var(--orca-muted);
--border: var(--orca-border);
--border-strong: var(--orca-border);
--border-soft: var(--orca-border);
--col-sep: var(--orca-border);
--row-selected-outline: var(--orca-accent);
--main-color: var(--orca-accent);
--main-color-hover: var(--orca-accent);
--plugin-link-text: var(--orca-accent);
/* --- web-only semantics: LIGHT --- */
--row-hover: #f7f9fb;
--row-selected: #eaf2ff;
--plugin-status-danger: #b42318;
--plugin-status-ok: #137333;
--plugin-status-warn: #a15c00;
--plugin-status-inactive: var(--muted);
--plugin-status-danger-bg: rgba(180, 35, 24, 0.12);
--plugin-status-ok-bg: rgba(19, 115, 51, 0.12);
--plugin-status-warn-bg: rgba(199, 122, 22, 0.12);
--plugin-status-inactive-bg: #edf1f5;
--plugin-status-inactive-text: #44505c;
--plugin-source-mine-bg: rgba(0, 137, 123, 0.12);
--plugin-source-mine-text: #00897b;
--plugin-source-neutral-bg: #edf1f5;
--plugin-source-neutral-text: #44505c;
--plugin-source-subscribed-bg: rgba(37, 99, 170, 0.12);
--plugin-source-subscribed-text: #2563aa;
--footer-bg: #fafafa;
--btn-bg: #ffffff;
--btn-border: #cccccc;
--btn-hover: #f0f0f0;
--ctx-bg: #ffffff;
--ctx-border: #cccccc;
--ctx-hover: #efefef;
}
:root[data-orca-theme="dark"] {
/* The app-injected border (var(--orca-border) ≈ #36363B in dark) sits almost on top of
the dark panels; lift the border roles a touch so dividers/outlines stay visible. */
--border: #4a4a51;
--border-strong: #4a4a51;
--border-soft: #4a4a51;
--col-sep: #4a4a51;
--row-hover: #2b3340;
--row-selected: #244945;
--plugin-status-danger: #ff7b72;
--plugin-status-ok: #37c871;
--plugin-status-warn: #f0b45a;
--plugin-status-inactive: #b9c0c8;
--plugin-status-danger-bg: rgba(255, 123, 114, 0.16);
--plugin-status-ok-bg: rgba(55, 200, 113, 0.16);
--plugin-status-warn-bg: rgba(240, 180, 90, 0.18);
--plugin-status-inactive-bg: #36363b;
--plugin-status-inactive-text: #c7ccd2;
--plugin-source-mine-bg: rgba(0, 150, 136, 0.18);
--plugin-source-mine-text: #8de5d6;
--plugin-source-neutral-bg: #36363b;
--plugin-source-neutral-text: #c7ccd2;
--plugin-source-subscribed-bg: rgba(88, 166, 255, 0.18);
--plugin-source-subscribed-text: #8fc0f0;
--footer-bg: #20262d;
--btn-bg: #2a313a;
--btn-border: #4b5664;
--btn-hover: #333c47;
--ctx-bg: #2a313a;
--ctx-border: #4b5664;
--ctx-hover: #3a4451;
}
/* Re-theme the shared common.css chrome through variables (replaces dark.css's
* hardcoded element overrides). Placed after common.css in every page's <head>. */
html { background: var(--bg); }
body { background: var(--bg); color: var(--text); }
#Content { color: var(--text); }
#Title div, #Title div.TitleUnselected { color: var(--orca-accent); }
.HyperLink { color: var(--orca-accent); }
.ZScrol::-webkit-scrollbar-thumb { background-color: var(--border); }
.ZScrol::-webkit-scrollbar-track { background: var(--bg); }
input[type="checkbox"] { background-color: var(--bg); border-color: var(--border); }
input[type="checkbox"]:checked { background-color: var(--orca-accent); border-color: var(--orca-accent); }
input[type="checkbox"]::before { box-shadow: inset 1em 1em var(--orca-accent-fg); }

View File

@@ -336,4 +336,4 @@ function ExecuteDarkMode( DarkCssPath )
}
}
SwitchDarkMode( "../css/dark.css" );
// Dialog pages are themed by the injected --orca-* contract + theme.css; the legacy dark.css poll is fully retired.

View File

@@ -1,5 +1,4 @@
#include "ExportPresetBundleDialog.hpp"
#include <slic3r/GUI/Widgets/WebView.hpp>
#include "GUI_App.hpp"
#include "ConfigWizard.hpp"
#include "I18N.hpp"
@@ -12,17 +11,14 @@
#include <wx/sizer.h>
#include <libslic3r/PresetBundle.hpp>
#include <wx/string.h>
#include <slic3r/GUI/Widgets/WebView.hpp>
#include <miniz.h>
#include <slic3r/GUI/MsgDialog.hpp>
namespace Slic3r { namespace GUI {
ExportPresetBundleDialog::ExportPresetBundleDialog(
wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style)
: DPIDialog(parent, id, _L("ExportPresetBundle"), pos, size, style)
: WebViewHostDialog(parent, id, _L("ExportPresetBundle"), pos, size, style)
{
SetBackgroundColour(*wxWHITE);
SetMinSize(DESIGN_WINDOW_SIZE);
Init();
wxGetApp().UpdateDlgDarkUI(this);
}
@@ -38,97 +34,36 @@ ExportPresetBundleDialog::~ExportPresetBundleDialog()
}
}
void ExportPresetBundleDialog::LoadUrl(wxString& url)
{
if (!m_browser)
return;
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << " enter, url=" << url.ToStdString();
WebView::LoadUrl(m_browser, url);
m_browser->SetFocus();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " exit";
}
void ExportPresetBundleDialog::on_dpi_changed(const wxRect& suggested_rect) { this->Refresh(); }
void ExportPresetBundleDialog::Init()
{
wxString TargetUrl = from_u8(
(boost::filesystem::path(resources_dir()) / "web/dialog/ExportPresetDialog/index.html").make_preferred().string());
wxString strlang = wxGetApp().current_language_code_safe();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", strlang=%1%") % into_u8(strlang);
if (strlang != "")
TargetUrl = wxString::Format("%s?lang=%s", std::string(TargetUrl.mb_str()), strlang);
TargetUrl = "file://" + TargetUrl;
// Create the webview
m_browser = WebView::CreateWebView(this, TargetUrl);
if (m_browser == nullptr) {
wxLogError("Could not init m_browser");
return;
}
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
SetTitle(_L("Export Preset Bundle"));
SetSizer(topsizer);
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
// Set a more sensible size for web browsing
wxSize pSize = FromDIP(wxSize(820, 660));
SetSize(pSize);
int screenheight = wxSystemSettings::GetMetric(wxSYS_SCREEN_Y, NULL);
int screenwidth = wxSystemSettings::GetMetric(wxSYS_SCREEN_X, NULL);
int MaxY = (screenheight - pSize.y) > 0 ? (screenheight - pSize.y) / 2 : 0;
wxPoint tmpPT((screenwidth - pSize.x) / 2, MaxY);
Move(tmpPT);
Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &ExportPresetBundleDialog::OnScriptMessage, this, m_browser->GetId());
LoadUrl(TargetUrl);
create_webview("web/dialog/ExportPresetDialog/index.html", _L("Export Preset Bundle"),
wxSize(820, 660), wxSize(640, 640));
}
void ExportPresetBundleDialog::RunScript(const wxString& s)
void ExportPresetBundleDialog::on_script_message(const nlohmann::json& j)
{
if (!m_browser)
if (handle_common_script_command(j))
return;
WebView::RunScript(m_browser, s);
}
void ExportPresetBundleDialog::OnScriptMessage(wxWebViewEvent& e)
{
try {
wxString strInput = e.GetString();
BOOST_LOG_TRIVIAL(trace) << "ExportPresetBundleDialog::OnScriptMessage;OnRecv:" << strInput.c_str();
json j = json::parse(strInput.utf8_string());
wxString strCmd = j["command"];
BOOST_LOG_TRIVIAL(trace) << "ExportPresetBundleDialog::OnScriptMessage;Command:" << strCmd;
if (strCmd == "close_page") {
this->EndModal(wxID_CANCEL);
} else if (strCmd == "request_export_preset_profile") {
InitExportData();
OnRequestPresets();
} else if (strCmd == "export_local") {
wxFileDialog dlg(this, _L("Save preset bundle"), "", "export.orca_bundle", "Orca Preset Bundle (*.orca_bundle)|*.orca_bundle",
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
wxString path;
wxString name;
if (dlg.ShowModal() == wxID_OK) {
path = dlg.GetPath();
wxFileName file_name(path);
name = file_name.GetName();
if (file_name.GetExt().empty()) {
file_name.SetExt("orca_bundle");
path = file_name.GetFullPath();
}
const std::string strCmd = j.value("command", "");
if (strCmd == "request_export_preset_profile") {
InitExportData();
OnRequestPresets();
} else if (strCmd == "export_local") {
wxFileDialog dlg(this, _L("Save preset bundle"), "", "export.orca_bundle",
"Orca Preset Bundle (*.orca_bundle)|*.orca_bundle", wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
wxString path;
wxString name;
if (dlg.ShowModal() == wxID_OK) {
path = dlg.GetPath();
wxFileName file_name(path);
name = file_name.GetName();
if (file_name.GetExt().empty()) {
file_name.SetExt("orca_bundle");
path = file_name.GetFullPath();
}
OnExportData(path, name, j["data"]);
}
} catch (std::exception& e) {
BOOST_LOG_TRIVIAL(trace) << "ExportPresetBundleDialog::OnScriptMessage;Error:" << e.what();
OnExportData(path, name, j.value("data", json()));
}
}
@@ -331,8 +266,7 @@ void ExportPresetBundleDialog::OnRequestPresets()
}
}
wxString strJS = wxString::Format("HandleStudio(%s)", wxString::FromUTF8(res.dump(-1, ' ', false, json::error_handler_t::ignore)));
wxGetApp().CallAfter([this, strJS] { RunScript(strJS); });
call_web_handler(res);
}
void ExportPresetBundleDialog::OnExportData(const wxString& path, const wxString& filename, json data)

View File

@@ -3,6 +3,7 @@
#include "GUI.hpp"
#include "GUI_Utils.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include "libslic3r/AppConfig.hpp"
#include <slic3r/GUI/GUI.hpp>
@@ -11,7 +12,6 @@
#include <wx/language.h>
#include <wx/string.h>
#include <wx/fswatcher.h>
#include <wx/webview.h>
namespace Slic3r { namespace GUI {
@@ -37,7 +37,7 @@ enum ExportCase {
CASE_COUNT,
};
class ExportPresetBundleDialog : public Slic3r::GUI::DPIDialog
class ExportPresetBundleDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
ExportPresetBundleDialog(wxWindow* parent,
@@ -52,16 +52,12 @@ public:
// Utilities
bool seq_top_layer_only_changed() const { return m_seq_top_layer_only_changed; }
bool recreate_GUI() const { return m_recreate_GUI; }
void on_dpi_changed(const wxRect& suggested_rect) override;
void show_export_result(const ExportCase& e);
void Init();
void InitExportData();
// Webview
void LoadUrl(wxString& url);
void OnScriptMessage(wxWebViewEvent& e);
void RunScript(const wxString& s);
void on_script_message(const nlohmann::json& payload) override;
void OnRequestPresets();
void OnExportData(const wxString& path, const wxString& name, json data);
@@ -69,9 +65,6 @@ protected:
bool m_seq_top_layer_only_changed{false};
bool m_recreate_GUI{false};
// Webview
wxWebView* m_browser{nullptr};
// Export Preset
std::unordered_map<std::string, Preset*> m_printer_presets; // first: printer name, second: printer presets have same printer name
std::unordered_map<std::string, std::vector<const Preset*>>

View File

@@ -2,7 +2,6 @@
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include <libslic3r/Utils.hpp>
@@ -16,96 +15,37 @@ namespace Slic3r { namespace GUI {
namespace {
// CSS "#rrggbb" for a wxColour (wxC2S_HTML_SYNTAX is the portable accessor used
// throughout the codebase for color->CSS).
std::string css_color(const wxColour& c) { return c.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); }
// Build a <style> block that matches OrcaSlicer's current theme. Injected at
// document-start (see ctor) so an unstyled plugin page already looks native,
// while plugin CSS still wins:
// * variables live on :root and elements use only low-specificity selectors,
// * nothing is marked !important,
// so any later rule the plugin ships (even an element selector) overrides these.
// Generated in C++ from the live theme — correct for the active light/dark mode
// and accent without the page needing to detect anything.
std::string host_theme_style()
// Low-specificity element defaults (no !important) for UNSTYLED plugin HTML, so a bare
// plugin page looks native while any CSS the plugin ships still wins. Built on the
// --orca-* variables the host injects (see WebViewHostDialog); document-start injected
// AFTER the host contract so the variables are defined (shares the base injector's
// WebView2 timing guard).
std::string plugin_defaults_user_script()
{
GUI_App& app = wxGetApp();
const wxColour bg = app.get_window_default_clr(); // dialog background
const wxColour fg = app.get_label_clr_default(); // primary text
const wxColour muted = app.get_label_clr_sys(); // secondary text
const wxColour border = app.get_highlight_default_clr(); // subtle lines / row hover
const wxColour accent = StateColor::darkModeColorFor(wxColour("#009688")); // ORCA teal
const wxColour accent_fg = *wxWHITE;
const std::string font = app.normal_font().GetFaceName().ToStdString();
std::string s;
s += "<style id=\"orca-host-theme\">";
s += ":root{";
s += "--orca-bg:" + css_color(bg) + ";";
s += "--orca-fg:" + css_color(fg) + ";";
s += "--orca-muted:" + css_color(muted) + ";";
s += "--orca-border:" + css_color(border) + ";";
s += "--orca-accent:" + css_color(accent) + ";";
s += "--orca-accent-fg:" + css_color(accent_fg) + ";";
// The themed face name first, then a portable system-ui fallback stack.
s += "--orca-font:" + (font.empty() ? std::string() : "'" + font + "',") +
"system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;";
s += "color-scheme:" + std::string(app.dark_mode() ? "dark" : "light") + ";";
s += "}";
// Element defaults — low specificity, no !important.
s += "html,body{background:var(--orca-bg);color:var(--orca-fg);"
"font-family:var(--orca-font);font-size:13px;}";
s += "body{margin:0;}";
s += "h1,h2,h3,h4,h5,h6{color:var(--orca-fg);font-weight:600;}";
s += "a{color:var(--orca-accent);}";
s += "hr{border:0;border-top:1px solid var(--orca-border);}";
s += "button{font:inherit;color:var(--orca-accent-fg);background:var(--orca-accent);"
"border:1px solid var(--orca-accent);border-radius:4px;padding:5px 14px;cursor:pointer;}";
s += "button:hover{filter:brightness(1.1);}";
s += "button:disabled{opacity:.5;cursor:default;}";
s += "input,select,textarea{font:inherit;color:var(--orca-fg);"
"background:var(--orca-bg);border:1px solid var(--orca-border);"
"border-radius:4px;padding:4px 8px;}";
s += "input:focus,select:focus,textarea:focus{outline:none;border-color:var(--orca-accent);}";
s += "table{border-collapse:collapse;}";
s += "th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--orca-border);}";
s += "th{color:var(--orca-muted);font-weight:600;}";
// WebKit/Chromium scrollbars themed to the background (no-op on others).
s += "::-webkit-scrollbar{width:12px;height:12px;}";
s += "::-webkit-scrollbar-thumb{background:var(--orca-border);border-radius:6px;}";
s += "::-webkit-scrollbar-track{background:transparent;}";
s += "</style>";
return s;
}
// User script that prepends the host theme into the document, before the plugin's
// own <style>/scripts affect layout (and before first paint). Works whether the
// plugin page has a <head> or is a bare fragment.
std::string host_theme_user_script()
{
// JSON-encode the style so it is a safe JS string literal regardless of
// quotes/newlines it may contain.
const std::string style_literal = nlohmann::json(host_theme_style()).dump();
// On WebView2 a document-start user script runs before <html> exists
// (document.head and document.documentElement are both null), so inserting
// right away would throw and the theme would silently never apply. Inject at
// the first opportunity instead: immediately when a root already exists,
// otherwise the moment <html> appears as a direct child of the observed
// document — still before first paint.
return "(function(){var css=" + style_literal + ";" R"JS(
function inject(){
var root=document.head||document.documentElement;
if(!root)return false;
if(!document.getElementById('orca-host-theme'))
root.insertAdjacentHTML('afterbegin',css);
return true;
}
if(inject())return;
var obs=new MutationObserver(function(){if(inject())obs.disconnect();});
obs.observe(document,{childList:true});
})();)JS";
std::string css;
css += "<style id=\"orca-plugin-defaults\">";
css += "html,body{background:var(--orca-bg);color:var(--orca-fg);"
"font-family:var(--orca-font);font-size:13px;}";
css += "body{margin:0;}";
css += "h1,h2,h3,h4,h5,h6{color:var(--orca-fg);font-weight:600;}";
css += "a{color:var(--orca-accent);}";
css += "hr{border:0;border-top:1px solid var(--orca-border);}";
css += "button{font:inherit;color:var(--orca-accent-fg);background:var(--orca-accent);"
"border:1px solid var(--orca-accent);border-radius:4px;padding:5px 14px;cursor:pointer;}";
css += "button:hover{filter:brightness(1.1);}";
css += "button:disabled{opacity:.5;cursor:default;}";
css += "input,select,textarea{font:inherit;color:var(--orca-fg);"
"background:var(--orca-bg);border:1px solid var(--orca-border);"
"border-radius:4px;padding:4px 8px;}";
css += "input:focus,select:focus,textarea:focus{outline:none;border-color:var(--orca-accent);}";
css += "table{border-collapse:collapse;}";
css += "th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--orca-border);}";
css += "th{color:var(--orca-muted);font-weight:600;}";
css += "::-webkit-scrollbar{width:12px;height:12px;}";
css += "::-webkit-scrollbar-thumb{background:var(--orca-border);border-radius:6px;}";
css += "::-webkit-scrollbar-track{background:transparent;}";
css += "</style>";
return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend");
}
// Injected into every page at document start (before the plugin's own scripts).
@@ -172,11 +112,8 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
if (wxWebView* wv = browser()) {
wv->SetBackgroundColour(wxGetApp().get_window_default_clr());
// Inject the host theme first so its <style> sits ahead of any plugin
// CSS in the document (later same-specificity rules win), making the
// plugin page match OrcaSlicer's light/dark theme by default.
wv->AddUserScript(wxString::FromUTF8(host_theme_user_script()));
wv->AddUserScript(wxString::FromUTF8(ORCA_BRIDGE_JS));
// Theme contract + plugin defaults + bridge are registered by the base
// create_webview() via add_user_scripts(); nothing to add here.
// Swap in the plugin HTML once the bootstrap page settles. Bind ERROR too so a
// missing/blocked bootstrap resource (e.g. a packaged build) still triggers it.
Bind(wxEVT_WEBVIEW_LOADED, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
@@ -185,6 +122,14 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
Bind(wxEVT_CLOSE_WINDOW, &PluginWebDialog::on_close_window, this);
}
void PluginWebDialog::add_user_scripts()
{
if (wxWebView* wv = browser()) {
wv->AddUserScript(wxString::FromUTF8(plugin_defaults_user_script()));
wv->AddUserScript(ORCA_BRIDGE_JS);
}
}
PluginWebDialog::~PluginWebDialog()
{
// Runs on every destruction path. Deliberately NOT a wxEVT_DESTROY handler:

View File

@@ -69,6 +69,7 @@ protected:
void on_script_message(const nlohmann::json& payload) override;
// Plugin HTML is loaded as a raw string, not a localized resource URL.
bool append_language_to_url() const override { return false; }
void add_user_scripts() override;
private:
void on_bootstrap_event(wxWebViewEvent& event);

View File

@@ -12,7 +12,6 @@
#include <libslic3r/PresetBundle.hpp>
#include <wx/string.h>
#include "MainFrame.hpp"
#include <slic3r/GUI/Widgets/WebView.hpp>
#include <miniz.h>
#include <OrcaCloudServiceAgent.hpp>
#include <wx/event.h>
@@ -21,11 +20,9 @@ namespace Slic3r { namespace GUI {
PresetBundleDialog::PresetBundleDialog(
wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style)
: DPIDialog(parent, id, _L("PresetBundle"), pos, size, style)
: WebViewHostDialog(parent, id, _L("PresetBundle"), pos, size, style)
{
wxGetApp().preset_bundle->bundles.PauseRead(); // for the entirety of the preset bundle dialog, we want the update thread to yield.
SetBackgroundColour(*wxWHITE);
SetMinSize(DESIGN_WINDOW_SIZE);
create();
wxGetApp().UpdateDlgDarkUI(this);
@@ -186,55 +183,11 @@ void PresetBundleDialog::RefreshBundleMap()
wxGetApp().preset_bundle->bundles.ReadUnlock();
}
void PresetBundleDialog::load_url(wxString& url)
{
if (!m_browser)
return;
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << " enter, url=" << url.ToStdString();
WebView::LoadUrl(m_browser, url);
m_browser->SetFocus();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " exit";
}
void PresetBundleDialog::create()
{
app_config = get_app_config();
wxString TargetUrl = from_u8(
(boost::filesystem::path(resources_dir()) / "web/dialog/PresetBundleDialog/index.html").make_preferred().string());
wxString strlang = wxGetApp().current_language_code_safe();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", strlang=%1%") % into_u8(strlang);
if (strlang != "")
TargetUrl = wxString::Format("%s?lang=%s", std::string(TargetUrl.mb_str()), strlang);
TargetUrl = "file://" + TargetUrl;
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
SetTitle(_L("Preset Bundle"));
m_browser = WebView::CreateWebView(this, TargetUrl);
if (m_browser == nullptr) {
wxLogError("Could not init m_browser");
return;
}
SetSizer(topsizer);
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
// Set a more sensible size for web browsing
wxSize pSize = FromDIP(wxSize(820, 660));
SetSize(pSize);
int screenheight = wxSystemSettings::GetMetric(wxSYS_SCREEN_Y, NULL);
int screenwidth = wxSystemSettings::GetMetric(wxSYS_SCREEN_X, NULL);
int MaxY = (screenheight - pSize.y) > 0 ? (screenheight - pSize.y) / 2 : 0;
wxPoint tmpPT((screenwidth - pSize.x) / 2, MaxY);
Move(tmpPT);
Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PresetBundleDialog::OnScriptMessage, this, m_browser->GetId());
load_url(TargetUrl);
create_webview("web/dialog/PresetBundleDialog/index.html", _L("Preset Bundle"),
wxSize(820, 660), wxSize(640, 640));
}
bool PresetBundleDialog::DeleteBundleById(const wxString& id)
@@ -292,68 +245,41 @@ bool PresetBundleDialog::DeleteBundleById(const wxString& id)
bool PresetBundleDialog::UnsubscribeBundleById(const std::string& id) { return wxGetApp().unsubscribe_bundle(id); }
void PresetBundleDialog::on_dpi_changed(const wxRect& suggested_rect) { this->Refresh(); }
void PresetBundleDialog::RunScript(const wxString& s)
void PresetBundleDialog::on_script_message(const nlohmann::json& j)
{
if (!m_browser)
if (handle_common_script_command(j))
return;
WebView::RunScript(m_browser, s);
}
void PresetBundleDialog::OnScriptMessage(wxWebViewEvent& e)
{
try {
wxString strInput = e.GetString();
BOOST_LOG_TRIVIAL(trace) << "PresetBundleDialog::OnScriptMessage;OnRecv:" << strInput.c_str();
json j = json::parse(strInput.utf8_string());
wxString strCmd = j["command"];
BOOST_LOG_TRIVIAL(trace) << "PresetBundleDialog::OnScriptMessage;Command:" << strCmd;
if (strCmd == "request_bundles") {
ListBundles();
} else if (strCmd == "refresh_bundles") {
// use the thread to check for updates.
m_check_update_pending.store(true, std::memory_order_relaxed);
} else if (strCmd == "update_bundle") {
std::string id = j["bundle_id"];
auto* evt = new wxCommandEvent(EVT_UPDATE_PRESET_BUNDLE);
evt->SetString(wxString::FromUTF8(id));
wxQueueEvent(&wxGetApp(), evt); // dialog -> GUI_App
} else if (strCmd == "set_auto_update") {
bool enabled = j.value("enabled", false);
// Example persistence location. Adjust key name if you already have one.
app_config->set_bool("preset_bundle_auto_update", enabled ? true : false);
app_config->save();
} else if (strCmd == "close_page") {
this->EndModal(wxID_CANCEL);
} else if (strCmd == "export_page") {
wxGetApp().CallAfter([this]() {
ExportPresetBundleDialog dlg(this);
dlg.ShowModal();
});
} else if (strCmd == "top_row_menu_action") {
if (j["action"] == "open_folder") {
std::string id = j["bundle_id"];
OpenFolder(id);
} else if (j["action"] == "delete_bundle") {
std::string id = j["bundle_id"];
DeleteBundle(id);
} else if (j["action"] == "unsubscribe_bundle") {
std::string id = j["bundle_id"];
UnsubscribeBundle(id);
}
} else if (strCmd == "open_bundle_on_cloud") {
std::string bundle_id = j["bundle_id"];
OpenBundleOnCloud(bundle_id);
}
} catch (std::exception& e) {
BOOST_LOG_TRIVIAL(trace) << "PresetBundleDialog::OnScriptMessage;Error:" << e.what();
const std::string strCmd = j.value("command", "");
if (strCmd == "request_bundles") {
ListBundles();
} else if (strCmd == "refresh_bundles") {
m_check_update_pending.store(true, std::memory_order_relaxed);
} else if (strCmd == "update_bundle") {
std::string id = j.value("bundle_id", "");
auto* evt = new wxCommandEvent(EVT_UPDATE_PRESET_BUNDLE);
evt->SetString(wxString::FromUTF8(id));
wxQueueEvent(&wxGetApp(), evt);
} else if (strCmd == "set_auto_update") {
bool enabled = j.value("enabled", false);
app_config->set_bool("preset_bundle_auto_update", enabled ? true : false);
app_config->save();
} else if (strCmd == "export_page") {
wxGetApp().CallAfter([this]() {
ExportPresetBundleDialog dlg(this);
dlg.ShowModal();
});
} else if (strCmd == "top_row_menu_action") {
const std::string action = j.value("action", "");
const std::string id = j.value("bundle_id", "");
if (action == "open_folder")
OpenFolder(id);
else if (action == "delete_bundle")
DeleteBundle(id);
else if (action == "unsubscribe_bundle")
UnsubscribeBundle(id);
} else if (strCmd == "open_bundle_on_cloud") {
OpenBundleOnCloud(j.value("bundle_id", ""));
}
}
@@ -396,8 +322,7 @@ void PresetBundleDialog::ListBundles()
res["data"].push_back(std::move(temp));
}
wxString strJS = wxString::Format("HandleStudio(%s)", wxString::FromUTF8(res.dump(-1, ' ', false, json::error_handler_t::ignore)));
wxGetApp().CallAfter([this, strJS] { RunScript(strJS); });
call_web_handler(res);
}
void PresetBundleDialog::OpenFolder(const std::string& id)

View File

@@ -3,6 +3,7 @@
#include "GUI.hpp"
#include "GUI_Utils.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include "libslic3r/AppConfig.hpp"
#include <boost/thread/detail/thread.hpp>
#include <libslic3r/PresetBundle.hpp>
@@ -15,7 +16,6 @@
#include <wx/language.h>
#include <wx/string.h>
#include <wx/fswatcher.h>
#include <wx/webview.h>
namespace Slic3r { namespace GUI {
#define DESIGN_GRAY900_COLOR wxColour("#363636") // Label color
@@ -28,7 +28,7 @@ namespace Slic3r { namespace GUI {
#define DESIGN_INPUT_SIZE wxSize(FromDIP(120), -1)
#define DESIGN_LEFT_MARGIN 25
#define VERTICAL_GAP_SIZE FromDIP(4)
class PresetBundleDialog : public Slic3r::GUI::DPIDialog
class PresetBundleDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
PresetBundleDialog(wxWindow* parent,
@@ -47,10 +47,8 @@ public:
bool seq_top_layer_only_changed() const { return m_seq_top_layer_only_changed; }
bool recreate_GUI() const { return m_recreate_GUI; }
void on_dpi_changed(const wxRect& suggested_rect) override;
// webview utilities
void load_url(wxString& url);
void ListBundles();
void OpenFolder(const std::string& id);
void DeleteBundle(const std::string& id);
@@ -59,11 +57,8 @@ public:
void OnPresetBundlePage();
// sends command to webview
void RunScript(const wxString& s);
// webview events
void OnScriptMessage(wxWebViewEvent& e);
void on_script_message(const nlohmann::json& payload) override;
void StartDialogWorker();
void StopDialogWorker();
@@ -86,7 +81,6 @@ protected:
bool m_recreate_GUI{false};
// Webview
wxWebView* m_browser{nullptr};
std::unordered_map<std::string, BundleMetadata> bundle_copy;
boost::thread m_dialog_worker_thread;

View File

@@ -407,6 +407,12 @@ void WebView::RecreateAll()
for (auto webView : g_webviews) {
webView->SetUserAgent(wxString::Format("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) BBL-Slicer/v%s (%s) BBL-Language/%s",
Slic3r::GUI::wxGetApp().get_bbl_client_version(), dark ? "dark" : "light", language_code.mb_str()));
webView->Reload();
// A host-themed WebViewHostDialog re-themes in place (no reload). If it handles
// the event, skip the reload; legacy pages fall through and reload as before
// (their own dark.css swap re-themes them on reload).
wxCommandEvent evt(EVT_WEBVIEW_RECREATED);
evt.SetEventObject(webView);
if (!webView->GetEventHandler()->ProcessEvent(evt))
webView->Reload();
}
}

View File

@@ -2,6 +2,9 @@
#define slic3r_GUI_WebView_hpp_
#include <wx/webview.h>
#include <wx/event.h>
wxDECLARE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent);
class WebView
{

View File

@@ -3,6 +3,8 @@
#include "WebView.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include <nlohmann/json.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
@@ -10,8 +12,104 @@
#include <wx/log.h>
#include <wx/sizer.h>
#include <algorithm>
namespace Slic3r { namespace GUI {
namespace {
// CSS "#rrggbb" for a wxColour (portable accessor used throughout the codebase).
std::string css_color(const wxColour& c) { return c.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); }
// "dark"/"light" for the live app theme — the value of both data-orca-theme and color-scheme.
std::string host_theme_name() { return wxGetApp().dark_mode() ? "dark" : "light"; }
// The host theme "contract": CSS custom properties filled from the LIVE app theme,
// plus color-scheme. Consumed by resources/web/dialog/css/theme.css and by plugin
// content. Variables only — no element styling — so it never fights a page's CSS.
std::string host_theme_vars_css()
{
GUI_App& app = wxGetApp();
const wxColour bg = app.get_window_default_clr();
const wxColour fg = app.get_label_clr_default();
const wxColour muted = app.get_label_clr_sys();
const wxColour border = app.get_highlight_default_clr();
const wxColour accent = StateColor::darkModeColorFor(wxColour("#009688"));
std::string font = app.normal_font().GetFaceName().ToStdString();
// Strip characters that could break out of the CSS value / <style> block.
font.erase(std::remove_if(font.begin(), font.end(), [](char c) {
return c == '\'' || c == '"' || c == '<' || c == '>' || c == '{' || c == '}' || c == ';';
}),
font.end());
std::string s;
s += ":root{";
s += "--orca-bg:" + css_color(bg) + ";";
s += "--orca-fg:" + css_color(fg) + ";";
s += "--orca-muted:" + css_color(muted) + ";";
s += "--orca-border:" + css_color(border) + ";";
s += "--orca-accent:" + css_color(accent) + ";";
s += "--orca-accent-fg:#ffffff;";
s += "--orca-font:" + (font.empty() ? std::string() : "'" + font + "',") +
"system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;";
s += "color-scheme:" + host_theme_name() + ";";
s += "}";
return s;
}
// Document-start user script: injects the contract <style>, stamps data-orca-theme before
// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for
// host-themed pages. The WebView2 timing guard lives in document_start_injector().
std::string host_theme_user_script()
{
const std::string style = "<style id=\"orca-host-theme-vars\">" + host_theme_vars_css() + "</style>";
return WebViewHostDialog::document_start_injector(
style, "orca-host-theme-vars", "afterbegin",
"window.__orcaHostThemed=true;var theme=\"" + host_theme_name() + "\";",
"if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);");
}
// JS to re-theme an already-loaded document live (no reload): replace the injected
// style's contents and update data-orca-theme. Everything downstream (theme.css
// tokens, plugin element defaults, page layout) re-cascades from these values.
std::string host_theme_apply_js()
{
const std::string vars_literal = nlohmann::json(host_theme_vars_css()).dump();
const std::string theme = host_theme_name();
return "(function(){var css=" + vars_literal + ";var theme=\"" + theme + "\";" + R"JS(
var el=document.getElementById('orca-host-theme-vars');
if(el){el.textContent=css;}
else if(document.head){document.head.insertAdjacentHTML('afterbegin','<style id="orca-host-theme-vars"></style>');var e2=document.getElementById('orca-host-theme-vars');if(e2)e2.textContent=css;}
if(document.documentElement)
document.documentElement.setAttribute('data-orca-theme',theme);
})();)JS";
}
} // namespace
std::string WebViewHostDialog::document_start_injector(const std::string& markup,
const char* dom_id,
const char* position,
const std::string& prelude,
const std::string& on_inject)
{
const std::string literal = nlohmann::json(markup).dump();
std::string s;
s += "(function(){";
s += prelude;
s += "var css=" + literal + ";";
s += "function inject(){";
s += "var root=document.head||document.documentElement;if(!root)return false;";
s += "if(!document.getElementById('" + std::string(dom_id) + "'))root.insertAdjacentHTML('" +
std::string(position) + "',css);";
s += on_inject;
s += "return true;}";
s += "if(inject())return;";
s += "var obs=new MutationObserver(function(){if(inject())obs.disconnect();});";
s += "obs.observe(document,{childList:true});})();";
return s;
}
WebViewHostDialog::WebViewHostDialog(wxWindow* parent,
wxWindowID id,
const wxString& title,
@@ -49,6 +147,11 @@ bool WebViewHostDialog::create_webview(const std::string& resource_path,
Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewHostDialog::on_script_message_event, this, m_browser->GetId());
// Inject the shared host theme contract BEFORE the first load so the page paints in
// the app theme with no flash, and re-theme live when the app theme toggles.
register_theme_user_scripts();
m_browser->Bind(EVT_WEBVIEW_RECREATED, &WebViewHostDialog::on_webview_recreated, this);
load_url(target_url);
wxGetApp().UpdateDlgDarkUI(this);
return true;
@@ -130,4 +233,33 @@ void WebViewHostDialog::on_script_message_event(wxWebViewEvent& event)
}
}
void WebViewHostDialog::register_theme_user_scripts()
{
if (!m_browser)
return;
// Added once, at creation. Deliberately no RemoveAllUserScripts() here: the "wx"
// script message handler is registered separately (AddScriptMessageHandler), but on
// some backends RemoveAllUserScripts() drops it too, which would break
// window.wx.postMessage / HandleStudio. Live re-theme goes through apply_theme_live().
m_browser->AddUserScript(wxString::FromUTF8(host_theme_user_script()));
add_user_scripts();
}
void WebViewHostDialog::apply_theme_live()
{
if (!m_browser)
return;
// Update the already-loaded document in place (no reload, no flash) by rewriting the
// injected :root variables + data-orca-theme; the whole cascade re-flows from these.
// The document-start script keeps the creation-time theme for any later reload, and
// these dialogs are not reloaded on a theme toggle (see WebView::RecreateAll).
run_script(wxString::FromUTF8(host_theme_apply_js()));
}
void WebViewHostDialog::on_webview_recreated(wxCommandEvent&)
{
// Handled: do NOT Skip(), so WebView::RecreateAll skips the redundant reload.
apply_theme_live();
}
}} // namespace Slic3r::GUI

View File

@@ -33,6 +33,20 @@ public:
bool run_script(const wxString& script);
void call_web_handler(const nlohmann::json& payload, const wxString& handler = wxT("HandleStudio"));
// Wraps `markup` (an HTML fragment, usually a <style> block) in a document-start user
// script that inserts it once — guarded by element id `dom_id`, at `position` (an
// insertAdjacentHTML target such as "afterbegin"/"beforeend") — retrying via a
// MutationObserver until a root node exists. On WebView2 a document-start script can run
// before <html> exists (document.head and document.documentElement both null), so a bare
// insert would throw and silently never apply. `prelude` is emitted once before the
// injector (extra var/flag declarations); `on_inject` runs inside inject() after each
// successful insert. Both default to empty.
static std::string document_start_injector(const std::string& markup,
const char* dom_id,
const char* position,
const std::string& prelude = {},
const std::string& on_inject = {});
protected:
wxWebView* browser() const { return m_browser; }
@@ -45,8 +59,24 @@ protected:
virtual void on_script_message_parse_error(const wxString& payload, const std::exception& error);
virtual bool append_language_to_url() const { return true; }
// Registers all document-start user scripts: the shared host theme contract first,
// then subclass scripts from add_user_scripts(). Called ONCE, at creation. Live
// re-theme goes through apply_theme_live() (RunScript), not a re-registration —
// calling this again would append duplicate scripts.
void register_theme_user_scripts();
// Subclasses override to add page-specific document-start user scripts (e.g. the
// plugin bridge / unstyled-content defaults). Called AFTER the theme contract is
// added, by register_theme_user_scripts(). Default: none.
virtual void add_user_scripts() {}
// Pushes the current app theme into the already-loaded document without a reload
// (updates the injected :root variables and the data-orca-theme attribute).
void apply_theme_live();
private:
void on_script_message_event(wxWebViewEvent& event);
void on_webview_recreated(wxCommandEvent& event);
wxWebView* m_browser{nullptr};
};