mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-18 08:22:06 +00:00
Merge branch 'feat/plugin-feature' into feature/plugin-slicing
This commit is contained in:
@@ -2427,18 +2427,19 @@ void PrintConfigDef::init_fff_params()
|
||||
|
||||
// xgettext:no-c-format, no-boost-format
|
||||
def = this->add("adaptive_pressure_advance_overhangs", coBools);
|
||||
def->label = L("Enable adaptive pressure advance for overhangs (beta)");
|
||||
def->tooltip = L("Enable adaptive PA for overhangs as well as when flow changes within the same feature. This is an experimental option, "
|
||||
"as if the PA profile is not set accurately, it will cause uniformity issues on the external surfaces before and after overhangs.\n"
|
||||
"Not compatible with Prusa printers as they pause to process PA changes, which causes delays and defects.");
|
||||
def->label = L("Enable adaptive pressure advance within features (beta)");
|
||||
def->tooltip = L("Enable adaptive PA whenever there are flow changes in a feature, such as line width changes in a corner or overhangs.\n\n"
|
||||
"Not compatible with Prusa printers as they pause to process PA changes, which causes delays and defects.\n\n"
|
||||
"This is an experimental option, as if the PA profile is not set accurately, it will cause uniformity issues.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBools{ false });
|
||||
|
||||
def = this->add("adaptive_pressure_advance_bridges", coFloats);
|
||||
def->label = L("Pressure advance for bridges");
|
||||
def->tooltip = L("Pressure advance value for bridges. Set to 0 to disable.\n\n"
|
||||
"A lower PA value when printing bridges helps reduce the appearance of slight under extrusion immediately after bridges. "
|
||||
"This is caused by the pressure drop in the nozzle when printing in the air and a lower PA helps counteract this.");
|
||||
def->label = L("Static pressure advance for bridges");
|
||||
def->tooltip = L("Static pressure advance value for bridges. Set to 0 to apply the same pressure advance as \n"
|
||||
"equivalent walls (using adaptive settings if enabled).\n\n"
|
||||
"A lower PA value when printing bridges helps reduce the appearance of slight under-extrusion immediately after bridges. "
|
||||
"This is caused by the pressure drop in the nozzle when printing in the air and a lower PA helps counteract this.");
|
||||
def->max = 2;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloats { 0.0 });
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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*>>
|
||||
|
||||
@@ -266,18 +266,6 @@ static void set_config_values(DynamicPrintConfig *config, const std::string &key
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, typename OptionType>
|
||||
static void set_config_values(ModelConfig& config, const std::string &key, T value)
|
||||
{
|
||||
auto config_opt = config.get().option<OptionType>(key);
|
||||
if (config_opt) {
|
||||
config.set_key_value(key, new OptionType(config_opt->values.size(), value));
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(info) << "set_config_values: the key" << key << "is empty.";
|
||||
}
|
||||
}
|
||||
|
||||
bool Plater::has_illegal_filename_characters(const wxString& wxs_name)
|
||||
{
|
||||
std::string name = into_u8(wxs_name);
|
||||
@@ -12928,9 +12916,9 @@ void Plater::_calib_pa_pattern(const Calib_Params& params)
|
||||
|
||||
auto &obj_config = obj->config;
|
||||
if (speeds.size() > 1)
|
||||
set_config_values<double, ConfigOptionFloatsNullable>(obj_config, "outer_wall_speed", tspd);
|
||||
obj_config.set_key_value("outer_wall_speed", new ConfigOptionFloatsNullable(1, tspd));
|
||||
if (accels.size() > 1)
|
||||
set_config_values<double, ConfigOptionFloatsNullable>(obj_config, "outer_wall_acceleration", tacc);
|
||||
obj_config.set_key_value("outer_wall_acceleration", new ConfigOptionFloatsNullable(1, tacc));
|
||||
|
||||
auto cur_plate = get_partplate_list().get_plate(plate_idx);
|
||||
if (!cur_plate) {
|
||||
@@ -13352,7 +13340,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params)
|
||||
set_config_values<double, ConfigOptionFloats>(filament_config, "filament_max_volumetric_speed", 200);
|
||||
filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0});
|
||||
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
|
||||
set_config_values<bool, ConfigOptionBoolsNullable>(obj_cfg, "enable_overhang_speed", false);
|
||||
obj_cfg.set_key_value("enable_overhang_speed", new ConfigOptionBoolsNullable(1, false));
|
||||
obj_cfg.set_key_value("wall_loops", new ConfigOptionInt(1));
|
||||
obj_cfg.set_key_value("alternate_extra_wall", new ConfigOptionBool(false));
|
||||
obj_cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0));
|
||||
|
||||
@@ -10,11 +10,15 @@ PluginProgressDialog::PluginProgressDialog(wxWindow* parent,
|
||||
int maximum,
|
||||
int style,
|
||||
CloseHandler on_destroyed)
|
||||
: wxProgressDialog(title, message, maximum, parent, style)
|
||||
: ProgressDialog(title, message, maximum, parent, style)
|
||||
, m_pulse_message(message)
|
||||
, m_on_destroyed(std::move(on_destroyed))
|
||||
{
|
||||
Bind(wxEVT_CLOSE_WINDOW, &PluginProgressDialog::on_close_window, this);
|
||||
// The base ProgressDialog already binds wxEVT_CLOSE_WINDOW (its OnClose vetoes
|
||||
// a non-cancelable dialog and marks a cancelable one Canceled). We deliberately
|
||||
// don't add a second handler: a user-initiated close surfaces to the plugin as
|
||||
// update()/pulse() returning false, and programmatic close() calls Destroy()
|
||||
// directly, so no extra wiring is needed here.
|
||||
}
|
||||
|
||||
PluginProgressDialog::~PluginProgressDialog()
|
||||
@@ -120,11 +124,6 @@ void PluginProgressDialog::close()
|
||||
Destroy();
|
||||
}
|
||||
|
||||
void PluginProgressDialog::on_close_window(wxCloseEvent& /*event*/)
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
void PluginProgressDialog::on_timer(wxTimerEvent& /*event*/)
|
||||
{
|
||||
if (m_open)
|
||||
|
||||
@@ -5,17 +5,24 @@
|
||||
#include <memory>
|
||||
|
||||
#include <wx/event.h>
|
||||
#include <wx/progdlg.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/timer.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#include "Widgets/ProgressDialog.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// A host-owned progress dialog for Python plugins. This class is deliberately
|
||||
// Python-agnostic; the plugin layer owns any pybind/GIL concerns and marshals
|
||||
// all calls to the UI thread.
|
||||
class PluginProgressDialog : public wxProgressDialog
|
||||
//
|
||||
// It derives from OrcaSlicer's own Slic3r::GUI::ProgressDialog (a real wxDialog
|
||||
// with themable wx children) rather than the native wxProgressDialog: on Windows
|
||||
// wxProgressDialog is a comctl32 TaskDialog running on a worker thread with no
|
||||
// recolorable wx surface, so it cannot follow OrcaSlicer's (OS-independent) dark
|
||||
// theme. The base themes itself via UpdateDlgDarkUI(this) in its Create().
|
||||
class PluginProgressDialog : public ProgressDialog
|
||||
{
|
||||
public:
|
||||
using CloseHandler = std::function<void()>;
|
||||
@@ -52,7 +59,6 @@ public:
|
||||
bool is_open() const { return m_open; }
|
||||
|
||||
private:
|
||||
void on_close_window(wxCloseEvent& event);
|
||||
void on_timer(wxTimerEvent& event);
|
||||
|
||||
bool m_open{true};
|
||||
|
||||
@@ -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,86 +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 at document-start,
|
||||
// before the plugin's own <style>/scripts run (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();
|
||||
std::string js;
|
||||
js += "(function(){";
|
||||
js += "if(document.getElementById('orca-host-theme'))return;";
|
||||
js += "var css=" + style_literal + ";";
|
||||
js += "var head=document.head||document.documentElement;";
|
||||
js += "head.insertAdjacentHTML('afterbegin',css);";
|
||||
js += "})();";
|
||||
return 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).
|
||||
@@ -162,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());
|
||||
@@ -175,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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -231,6 +231,17 @@ bool ProgressDialog::Create(const wxString &title, const wxString &message, int
|
||||
m_sizer_main->Add(m_gauge, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(28));
|
||||
}
|
||||
|
||||
// Optional elapsed/estimated/remaining time labels, created only when the
|
||||
// caller opts in via the wxPD_*_TIME style flags (so callers that don't set
|
||||
// them are unaffected). Update()/Pulse() already refresh these once non-null.
|
||||
if (HasPDFlag(wxPD_ELAPSED_TIME | wxPD_ESTIMATED_TIME | wxPD_REMAINING_TIME)) {
|
||||
wxFlexGridSizer *sizer_times = new wxFlexGridSizer(2, FromDIP(2), FromDIP(8));
|
||||
if (HasPDFlag(wxPD_ELAPSED_TIME)) m_elapsed = CreateLabel(GetElapsedLabel(), sizer_times);
|
||||
if (HasPDFlag(wxPD_ESTIMATED_TIME)) m_estimated = CreateLabel(GetEstimatedLabel(), sizer_times);
|
||||
if (HasPDFlag(wxPD_REMAINING_TIME)) m_remaining = CreateLabel(GetRemainingLabel(), sizer_times);
|
||||
m_sizer_main->Add(sizer_times, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxLEFT | wxRIGHT, FromDIP(12));
|
||||
}
|
||||
|
||||
#ifdef __WXMSW__
|
||||
//m_block_left = new wxWindow(m_gauge, wxID_ANY, wxPoint(0, 0), wxSize(FromDIP(2), PROGRESSDIALOG_GAUGE_SIZE.y * 2));
|
||||
//m_block_left->SetBackgroundColour(PROGRESSDIALOG_DEF_BK);
|
||||
@@ -490,6 +501,14 @@ wxStaticText *ProgressDialog::CreateLabel(const wxString &text, wxSizer *sizer)
|
||||
wxStaticText *label = new wxStaticText(this, wxID_ANY, text);
|
||||
wxStaticText *value = new wxStaticText(this, wxID_ANY, wxGetTranslation("unknown"));
|
||||
|
||||
// Match the message label's look so the times theme with the rest of the
|
||||
// dialog: PROGRESSDIALOG_GREY_700 is a key in the dark-mode colour map, so
|
||||
// UpdateDlgDarkUI() (called at the end of Create()) remaps it in dark mode.
|
||||
for (wxStaticText *st : {label, value}) {
|
||||
st->SetFont(::Label::Body_13);
|
||||
st->SetForegroundColour(PROGRESSDIALOG_GREY_700);
|
||||
}
|
||||
|
||||
// select placement most native or nice on target GUI
|
||||
#if defined(__WXMSW__) || defined(__WXMAC__) || defined(__WXGTK20__)
|
||||
// value and time centered in one row
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -45,8 +45,13 @@
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#if !defined(_WIN32)
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <cerrno>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <uuid/uuid.h>
|
||||
#endif
|
||||
|
||||
@@ -89,6 +94,60 @@ constexpr const char* SECRET_STORE_SERVICE = "OrcaSlicer/Auth";
|
||||
constexpr const char* SECRET_STORE_USER = "orca_refresh_token";
|
||||
constexpr std::chrono::seconds TOKEN_REFRESH_SKEW{900}; // 15 minutes
|
||||
|
||||
// Cross-process advisory lock serializing refresh-token rotation between Orca instances on
|
||||
// this machine. The Supabase refresh token rotates on every use, so read -> spend -> write-back
|
||||
// of the rotated successor must be one critical section across processes; otherwise a second
|
||||
// instance can re-spend a token a peer already rotated, triggering `refresh_token_already_used`.
|
||||
// Blocks until acquired (kernel sleep, no CPU spin). It cannot deadlock permanently because the
|
||||
// refresh POST is bounded (http_post_token uses timeout_max) and both back-ends release
|
||||
// automatically if the holder dies. Same machine only; cross-device concurrency is still
|
||||
// governed by the server's reuse-detection grace window.
|
||||
class InterProcessRefreshTokenLock
|
||||
{
|
||||
public:
|
||||
explicit InterProcessRefreshTokenLock(const std::string& path)
|
||||
{
|
||||
if (path.empty()) { m_locked = true; return; } // no path -> proceed unsynchronized
|
||||
#if defined(_WIN32)
|
||||
const std::wstring name = L"Local\\OrcaTokenRefresh_" + std::to_wstring(std::hash<std::string>{}(path));
|
||||
m_handle = ::CreateMutexW(nullptr, FALSE, name.c_str());
|
||||
if (!m_handle) { m_locked = true; return; } // can't create -> don't block
|
||||
const DWORD r = ::WaitForSingleObject(m_handle, INFINITE); // blocks, no spin
|
||||
m_locked = (r == WAIT_OBJECT_0 || r == WAIT_ABANDONED); // ABANDONED: prior holder crashed
|
||||
#else
|
||||
m_fd = ::open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
|
||||
if (m_fd == -1) { m_locked = true; return; } // can't open -> don't block
|
||||
struct flock fl{};
|
||||
fl.l_type = F_WRLCK; fl.l_whence = SEEK_SET; fl.l_start = 0; fl.l_len = 0;
|
||||
int rc;
|
||||
do { rc = ::fcntl(m_fd, F_SETLKW, &fl); } while (rc == -1 && errno == EINTR); // blocks in kernel
|
||||
m_locked = (rc != -1);
|
||||
#endif
|
||||
}
|
||||
|
||||
~InterProcessRefreshTokenLock()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
if (m_handle) { if (m_locked) ::ReleaseMutex(m_handle); ::CloseHandle(m_handle); }
|
||||
#else
|
||||
if (m_fd != -1) ::close(m_fd); // closing the fd releases the lock we hold
|
||||
#endif
|
||||
}
|
||||
|
||||
bool locked() const { return m_locked; }
|
||||
|
||||
InterProcessRefreshTokenLock(const InterProcessRefreshTokenLock&) = delete;
|
||||
InterProcessRefreshTokenLock& operator=(const InterProcessRefreshTokenLock&) = delete;
|
||||
|
||||
private:
|
||||
bool m_locked = false;
|
||||
#if defined(_WIN32)
|
||||
HANDLE m_handle = nullptr;
|
||||
#else
|
||||
int m_fd = -1;
|
||||
#endif
|
||||
};
|
||||
|
||||
// Return a JSON field only when it is present as a string. Missing or non-string values normalize to empty.
|
||||
std::string get_json_string_field(const json& j, const std::string& key)
|
||||
{
|
||||
@@ -1678,8 +1737,7 @@ RefreshResult OrcaCloudServiceAgent::refresh_now(const std::string& refresh_toke
|
||||
}
|
||||
|
||||
auto worker = [this, refresh_token, reason]() {
|
||||
(void) reason;
|
||||
RefreshResult r = refresh_session_with_token(refresh_token);
|
||||
RefreshResult r = refresh_session_with_token(refresh_token, reason);
|
||||
refresh_running.store(false);
|
||||
return r;
|
||||
};
|
||||
@@ -1699,20 +1757,39 @@ RefreshResult OrcaCloudServiceAgent::refresh_now(const std::string& refresh_toke
|
||||
|
||||
RefreshResult OrcaCloudServiceAgent::refresh_from_storage(const std::string& reason, bool async)
|
||||
{
|
||||
std::string refresh_token = get_refresh_token();
|
||||
if (refresh_token.empty()) {
|
||||
std::string user_secret;
|
||||
if (load_user_secret(user_secret) && !user_secret.empty()) {
|
||||
SessionInfo stored_session;
|
||||
parse_stored_secret(user_secret, refresh_token, stored_session);
|
||||
}
|
||||
(void) async; // currently all callers are calling this function synchronous anyway
|
||||
|
||||
// Blocks until we own the lock (kernel sleep, no spin). Cannot deadlock permanently:
|
||||
// the refresh POST is bounded (http_post_token timeout_max) and the lock auto-releases
|
||||
// on process death, so no holder can keep it longer than that bound.
|
||||
InterProcessRefreshTokenLock lock(token_lock_path());
|
||||
if (!lock.locked()) {
|
||||
// Lock syscall genuinely failed (rare). Proceed best-effort rather than give up.
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: token refresh lock unavailable, "
|
||||
"proceeding unsynchronized (reason=" << reason << ")";
|
||||
}
|
||||
|
||||
std::string refresh_token;
|
||||
std::string user_secret;
|
||||
|
||||
// We read the refresh token from the OS keychain as the source of truth
|
||||
if (load_user_secret(user_secret) && !user_secret.empty()) {
|
||||
SessionInfo stored_session;
|
||||
parse_stored_secret(user_secret, refresh_token, stored_session);
|
||||
}
|
||||
|
||||
// We only read from memory if the read from OS keychain fails
|
||||
if (refresh_token.empty())
|
||||
refresh_token = get_refresh_token();
|
||||
|
||||
if (refresh_token.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: no refresh token available for refresh (reason=" << reason << ")";
|
||||
return RefreshResult::AuthRejected; // no persisted token: nothing to preserve
|
||||
}
|
||||
|
||||
return refresh_now(refresh_token, reason, async);
|
||||
// Synchronous: hold the lock across the network round-trip AND the write-back that
|
||||
// set_user_session performs, so the rotation is atomic w.r.t. other instances.
|
||||
return refresh_now(refresh_token, reason, /*async=*/false);
|
||||
}
|
||||
|
||||
bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const std::string& reason)
|
||||
@@ -1726,8 +1803,14 @@ bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const
|
||||
if (!needs_refresh)
|
||||
return true;
|
||||
|
||||
if (refresh_from_storage(reason, false) == RefreshResult::Success) return true;
|
||||
// First attempt. refresh_from_storage blocks on the cross-process lock and reads the
|
||||
// freshest token from the store, so cross-instance contention is already resolved here.
|
||||
RefreshResult r = refresh_from_storage(reason, false);
|
||||
if (r == RefreshResult::Success) return true;
|
||||
if (r == RefreshResult::AuthRejected) return false; // definitive: retrying the same token can't help
|
||||
|
||||
// One retry, only for a transient network failure of the refresh POST (lost response,
|
||||
// timeout, 429/5xx -> Transient). The retry re-acquires the lock and re-reads the store.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(750));
|
||||
return refresh_from_storage(reason + "_retry", false) == RefreshResult::Success;
|
||||
}
|
||||
@@ -1746,7 +1829,23 @@ static RefreshResult classify_refresh_result(unsigned http_code, bool session_es
|
||||
: RefreshResult::Transient; // 2xx but unusable body
|
||||
}
|
||||
|
||||
RefreshResult OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refresh_token)
|
||||
// Best-effort extraction of GoTrue's "error_code" field (e.g. "refresh_token_already_used").
|
||||
// Returns "" if the body is not a JSON object or lacks the field.
|
||||
static std::string extract_error_code(const std::string& body)
|
||||
{
|
||||
const json j = json::parse(body, nullptr, false);
|
||||
if (j.is_object())
|
||||
return get_json_string_field(j, "error_code");
|
||||
return "";
|
||||
}
|
||||
|
||||
static long long now_epoch_seconds()
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
RefreshResult OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refresh_token, const std::string& reason)
|
||||
{
|
||||
std::string body = "{\"refresh_token\":\"" + refresh_token + "\"}";
|
||||
std::string url = auth_base_url + auth_constants::TOKEN_PATH + "?grant_type=refresh_token";
|
||||
@@ -1755,6 +1854,12 @@ RefreshResult OrcaCloudServiceAgent::refresh_session_with_token(const std::strin
|
||||
// http_post_token sets http_code to 0 when the server could not be reached.
|
||||
http_post_token(body, &response, &http_code, url);
|
||||
|
||||
const long long now_s = now_epoch_seconds();
|
||||
const long long last_ok = last_refresh_success_epoch.load(std::memory_order_relaxed);
|
||||
const long long since_last_ok = (last_ok == 0) ? -1 : (now_s - last_ok); // -1 = no success yet this process
|
||||
const long long since_start = now_s - agent_start_epoch;
|
||||
const std::string log_reason = reason.empty() ? "-" : reason;
|
||||
|
||||
bool established = false;
|
||||
if (http_code >= 200 && http_code < 300) {
|
||||
if (session_handler) {
|
||||
@@ -1768,10 +1873,47 @@ RefreshResult OrcaCloudServiceAgent::refresh_session_with_token(const std::strin
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: token refresh parse exception - " << e.what();
|
||||
}
|
||||
}
|
||||
if (established) {
|
||||
last_refresh_success_epoch.store(now_s, std::memory_order_relaxed);
|
||||
BOOST_LOG_TRIVIAL(info) << "[auth] event=refresh_ok reason=" << log_reason
|
||||
<< " secs_since_last_success=" << since_last_ok;
|
||||
}
|
||||
} else {
|
||||
// Diagnostics to classify a future refresh_token_already_used incident from logs alone
|
||||
// - secs_since_last_success large (or -1 = none this process) => stale token beyond the
|
||||
// reuse-grace window (the ">1h gap" residual).
|
||||
// - stored_differs=true => another instance/device already rotated the token in the
|
||||
// shared secret store (the multi-session residual).
|
||||
const std::string error_code = extract_error_code(response);
|
||||
|
||||
// Only meaningful when the server actually rejected us (not a bare transport failure).
|
||||
bool stored_present = false, stored_differs = false;
|
||||
if (http_code >= 400) {
|
||||
std::string stored_secret, stored_token;
|
||||
SessionInfo ignored;
|
||||
if (load_user_secret(stored_secret) && !stored_secret.empty()
|
||||
&& parse_stored_secret(stored_secret, stored_token, ignored) && !stored_token.empty()) {
|
||||
stored_present = true;
|
||||
stored_differs = (stored_token != refresh_token);
|
||||
}
|
||||
}
|
||||
|
||||
std::string user_id_copy;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(session_mutex);
|
||||
user_id_copy = session.user_id;
|
||||
}
|
||||
|
||||
std::string truncated_response = response.size() > 200 ? response.substr(0, 200) + "..." : response;
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: token refresh failed - http_code=" << http_code
|
||||
<< ", response_body=" << truncated_response;
|
||||
BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh_rejected http_code=" << http_code
|
||||
<< " error_code=" << (error_code.empty() ? "-" : error_code)
|
||||
<< " reason=" << log_reason
|
||||
<< " stored_present=" << (stored_present ? "true" : "false")
|
||||
<< " stored_differs=" << (stored_differs ? "true" : "false")
|
||||
<< " secs_since_last_success=" << since_last_ok
|
||||
<< " secs_since_start=" << since_start
|
||||
<< " user_id=" << (user_id_copy.empty() ? "-" : user_id_copy)
|
||||
<< " response_body=" << truncated_response;
|
||||
}
|
||||
|
||||
return classify_refresh_result(http_code, established);
|
||||
@@ -2240,6 +2382,9 @@ bool OrcaCloudServiceAgent::http_post_token(const std::string& body,
|
||||
resp_body = body;
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error;
|
||||
})
|
||||
// Keep this timeout finite: refresh_from_storage holds a cross-process lock across
|
||||
// this call, so an unbounded refresh POST would let one instance wedge token refresh
|
||||
// for every other Orca instance on the machine.
|
||||
.timeout_max(30)
|
||||
.perform_sync();
|
||||
|
||||
@@ -3447,4 +3592,13 @@ int OrcaCloudServiceAgent::fetch_plugin_changelogs(const std::vector<std::string
|
||||
return first_error;
|
||||
}
|
||||
|
||||
std::string OrcaCloudServiceAgent::token_lock_path() const
|
||||
{
|
||||
if (config_dir.empty())
|
||||
return {};
|
||||
wxFileName lock(wxString::FromUTF8(config_dir.c_str()), "orca_refresh_token.lock");
|
||||
lock.Normalize();
|
||||
return lock.GetFullPath().ToStdString();
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -322,7 +322,7 @@ public:
|
||||
bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason);
|
||||
RefreshResult refresh_from_storage(const std::string& reason, bool async = false);
|
||||
RefreshResult refresh_now(const std::string& refresh_token, const std::string& reason, bool async = false);
|
||||
RefreshResult refresh_session_with_token(const std::string& refresh_token);
|
||||
RefreshResult refresh_session_with_token(const std::string& refresh_token, const std::string& reason = "");
|
||||
|
||||
// Session state helpers. nickname is the human-facing UI label after provider fallback resolution.
|
||||
bool set_user_session(const std::string& token,
|
||||
@@ -385,6 +385,9 @@ private:
|
||||
std::string map_to_json(const std::map<std::string, std::string>& map);
|
||||
void json_to_map(const std::string& json, std::map<std::string, std::string>& map);
|
||||
|
||||
// Refresh token lock
|
||||
std::string token_lock_path() const;
|
||||
|
||||
// Member variables - configuration
|
||||
std::string log_dir;
|
||||
std::string config_dir;
|
||||
@@ -405,6 +408,12 @@ private:
|
||||
SessionInfo session;
|
||||
mutable std::mutex session_mutex;
|
||||
|
||||
// Refresh diagnostics (see docs/analysis/refresh_token_already_used.md). Epoch seconds so the
|
||||
// refresh-failure log can report token staleness without holding a lock or logging any token.
|
||||
std::atomic<long long> last_refresh_success_epoch{0}; // 0 = no success yet this process
|
||||
const long long agent_start_epoch{std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count()};
|
||||
|
||||
// Member variables - connection state
|
||||
bool is_connected{false};
|
||||
bool enable_track{false};
|
||||
|
||||
Reference in New Issue
Block a user