mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 18:31:11 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
273ee2baef | ||
|
|
811b587eb0 | ||
|
|
2876374b45 |
@@ -58,6 +58,9 @@ Paths below are relative to this skill. Commands run from the repository root.
|
|||||||
9. **Run the full profile checks before reporting completion.** A vendor-scoped pass is only a
|
9. **Run the full profile checks before reporting completion.** A vendor-scoped pass is only a
|
||||||
development loop. Review also covers version bumps, assets, non-default processes and hardware
|
development loop. Review also covers version bumps, assets, non-default processes and hardware
|
||||||
tuning that CI cannot establish.
|
tuning that CI cannot establish.
|
||||||
|
10. **One all-printer preset per product; color is a runtime property, never a preset.** Never ship
|
||||||
|
presets that differ only by color — CI accepts them, so this is a review call. See
|
||||||
|
[color is a runtime property](references/filament-profiles.md#color-is-a-runtime-property).
|
||||||
|
|
||||||
## Creating or modifying a profile
|
## Creating or modifying a profile
|
||||||
|
|
||||||
@@ -107,6 +110,7 @@ Paths below are relative to this skill. Commands run from the repository root.
|
|||||||
| A setting has no effect | Key spelling/type, `handle_legacy`, or a config key placed on a `machine_model` |
|
| A setting has no effect | Key spelling/type, `handle_legacy`, or a config key placed on a `machine_model` |
|
||||||
| A preset exists but is not selectable | Index registration, `instantiation`, installation and compatibility |
|
| A preset exists but is not selectable | Index registration, `instantiation`, installation and compatibility |
|
||||||
| A filament is missing, duplicated, or matches the wrong spool | [Compatibility and alias shadowing](references/filament-profiles.md#compatible_printers); [ids](references/ids.md) |
|
| A filament is missing, duplicated, or matches the wrong spool | [Compatibility and alias shadowing](references/filament-profiles.md#compatible_printers); [ids](references/ids.md) |
|
||||||
|
| Presets differ only by color, or an all-printer library preset lacks `@System` | [Color is a runtime property](references/filament-profiles.md#color-is-a-runtime-property) |
|
||||||
| A bed temperature is ignored | [Plate-specific temperature keys](references/filament-profiles.md#bed-temperature-is-twelve-keys-not-one) |
|
| A bed temperature is ignored | [Plate-specific temperature keys](references/filament-profiles.md#bed-temperature-is-twelve-keys-not-one) |
|
||||||
| A change is absent from the running app | Version bump and [installed profile location](references/validation.md#testing-in-the-app) |
|
| A change is absent from the running app | Version bump and [installed profile location](references/validation.md#testing-in-the-app) |
|
||||||
| A check fails | [Error → remedy](references/validation.md#error--remedy) |
|
| A check fails | [Error → remedy](references/validation.md#error--remedy) |
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ a brand means adding a folder here; the folder name is a directory label only
|
|||||||
collection, so there is no duplicate-name error.
|
collection, so there is no duplicate-name error.
|
||||||
- You may inherit from an instantiated preset as well as from a base; it is common.
|
- You may inherit from an instantiated preset as well as from a base; it is common.
|
||||||
|
|
||||||
|
## Color is a runtime property
|
||||||
|
|
||||||
|
`filament_id` identifies a product, not a color; filament sync/AMS reads the color from the spool at
|
||||||
|
runtime. A product ships one all-printer preset and the color is chosen at runtime — never a sibling
|
||||||
|
preset that differs only by color. A material family (PLA vs PLA Matte vs PLA Silk) is a new product; a
|
||||||
|
color is not. A printer tune keeps the product alias and does not multiply per color either.
|
||||||
|
|
||||||
|
CI does not catch this — per-color presets pass `check` — so it is a review call.
|
||||||
|
|
||||||
## The two most common contributions
|
## The two most common contributions
|
||||||
|
|
||||||
**A printer vendor tuning a generic.** Keep the `Generic X` base name so the alias shadows the library
|
**A printer vendor tuning a generic.** Keep the `Generic X` base name so the alias shadows the library
|
||||||
|
|||||||
@@ -46,12 +46,17 @@ to the first `@`; the target half is a label except for reserved forms:
|
|||||||
|
|
||||||
- `@base` — a non-instantiated product root. `@base` is convention; a base is really identified by
|
- `@base` — a non-instantiated product root. `@base` is convention; a base is really identified by
|
||||||
`instantiation: "false"` and no `setting_id` ([the three-part shape](filament-profiles.md#the-three-part-shape)).
|
`instantiation: "false"` and no `setting_id` ([the three-part shape](filament-profiles.md#the-three-part-shape)).
|
||||||
- `@System` — the OrcaFilamentLibrary selectable shim. The literal `Generic <mat> @System` is
|
- `@System` — the OrcaFilamentLibrary selectable shim, and the convention for an all-printer product
|
||||||
load-bearing for 3MF/project recovery, beyond the alias rule ([alias shadowing](filament-profiles.md#alias-shadowing)).
|
(`<Product> @System`, empty `compatible_printers`); not enforced, so a deviation is worth a review
|
||||||
|
comment. The literal `Generic <mat> @System` is load-bearing for 3MF/project recovery, beyond the
|
||||||
|
alias rule ([alias shadowing](filament-profiles.md#alias-shadowing)).
|
||||||
- `@<Vendor>`, `@<Vendor> <Model>`, `@<Vendor> <Model> <nozzle> nozzle` — printer tunes, BBL's shape.
|
- `@<Vendor>`, `@<Vendor> <Model>`, `@<Vendor> <Model> <nozzle> nozzle` — printer tunes, BBL's shape.
|
||||||
Other vendors differ (a bare model, a printer serial, Creality's `@<Model>-all`). Specificity is judged
|
Other vendors differ (a bare model, a printer serial, Creality's `@<Model>-all`). Specificity is judged
|
||||||
from `compatible_printers`, not the name
|
from `compatible_printers`, not the name
|
||||||
([one variant, one profile](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product)).
|
([one variant, one profile](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product)).
|
||||||
|
- Color is not part of the product name: `<Product> <Color>` presets are not authored; the color is
|
||||||
|
chosen at runtime
|
||||||
|
([color is a runtime property](filament-profiles.md#color-is-a-runtime-property)).
|
||||||
|
|
||||||
## Not the same as the filename
|
## Not the same as the filename
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ The table highlights gaps that need human review. What CI *does* run:
|
|||||||
| Whether the intended default survived compatibility selection | The sweep can select a different compatible preset |
|
| Whether the intended default survived compatibility selection | The sweep can select a different compatible preset |
|
||||||
| A dangling `compatible_printers` inside an `instantiation: "false"` base | A base never becomes a `Preset`, so the reference check never sees it (a bad `inherits` in a base *is* caught) |
|
| A dangling `compatible_printers` inside an `instantiation: "false"` base | A base never becomes a `Preset`, so the reference check never sees it (a bad `inherits` in a base *is* caught) |
|
||||||
| A `renamed_from` whose old name is still a live preset | The redirect is inert while a live preset carries that name |
|
| A `renamed_from` whose old name is still a live preset | The redirect is inert while a live preset carries that name |
|
||||||
|
| A preset differentiated only by color, or an all-printer library preset without `@System` | Per-color presets split one product across ids and the selector fills with near-duplicates; CI stays green |
|
||||||
| Per-extruder vector length on a multi-nozzle printer | Silently padded (with the **first** value) or truncated |
|
| Per-extruder vector length on a multi-nozzle printer | Silently padded (with the **first** value) or truncated |
|
||||||
|
|
||||||
## 1. Was the vendor `version` bumped?
|
## 1. Was the vendor `version` bumped?
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<!-- Bootstrap page for PluginWebDialog. The real plugin HTML is loaded via
|
<!-- Bootstrap page for WebDialog. The real plugin HTML is loaded via
|
||||||
wxWebView::SetPage once this page finishes loading; this file only exists
|
wxWebView::SetPage once this page finishes loading; this file only exists
|
||||||
to bring the webview up. -->
|
to bring the webview up. -->
|
||||||
<html>
|
<html>
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.12"
|
||||||
|
#
|
||||||
|
# [tool.orcaslicer.plugin]
|
||||||
|
# name = "Dock Panel Demo"
|
||||||
|
# description = "Opens a dockable panel beside the 3D view that lists the objects on the plate."
|
||||||
|
# author = "OrcaSlicer"
|
||||||
|
# version = "0.0.1"
|
||||||
|
# ///
|
||||||
|
"""Dock Panel Demo -- orca.host.ui.create_dock_panel().
|
||||||
|
|
||||||
|
Run it from the Plugins dialog. It opens an HTML panel docked on the right of the 3D view, in the
|
||||||
|
same dock area as the sidebar. Drag its caption to dock it on another side (or float it, where the
|
||||||
|
platform allows), hide it from the page and run the plugin again to bring it back, or close it with
|
||||||
|
its close button or from the page.
|
||||||
|
|
||||||
|
page --orca.postMessage({command: 'refresh'})--> plugin.on_message()
|
||||||
|
page --orca.postMessage({command: 'hide'})--> plugin.on_message() -> panel.hide()
|
||||||
|
page --orca.close()--> panel closes, plugin.on_close()
|
||||||
|
plugin --panel.post({command: 'objects', ...})--> page (orca.onMessage)
|
||||||
|
"""
|
||||||
|
import orca
|
||||||
|
|
||||||
|
PAGE = """
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 12px; font-size: 13px; }
|
||||||
|
h3 { margin: 0 0 4px; font-size: 14px; }
|
||||||
|
.note { margin: 0 0 12px; color: var(--orca-muted); font-size: 12px; }
|
||||||
|
.actions { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
||||||
|
.actions button.quiet { background: transparent; color: var(--orca-fg); border-color: var(--orca-border); }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
td.count { text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
|
#status { margin-top: 10px; color: var(--orca-muted); font-size: 12px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h3>Objects on the plate</h3>
|
||||||
|
<p class="note">Docked beside the 3D view. Drag the caption to move it.</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" id="refresh">Refresh</button>
|
||||||
|
<button type="button" id="hide" class="quiet">Hide</button>
|
||||||
|
<button type="button" id="close" class="quiet">Close</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Name</th><th>Parts</th><th>Copies</th></tr></thead>
|
||||||
|
<tbody id="rows"></tbody>
|
||||||
|
</table>
|
||||||
|
<p id="status">Waiting for the plugin...</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function text(value) {
|
||||||
|
var span = document.createElement("span");
|
||||||
|
span.textContent = value;
|
||||||
|
return span.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(message) {
|
||||||
|
var rows = document.getElementById("rows");
|
||||||
|
var status = document.getElementById("status");
|
||||||
|
if (message.error) {
|
||||||
|
rows.innerHTML = "";
|
||||||
|
status.textContent = message.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rows.innerHTML = message.objects.map(function (object) {
|
||||||
|
return "<tr><td>" + text(object.name) + "</td><td class=\\"count\\">" + object.volumes +
|
||||||
|
"</td><td class=\\"count\\">" + object.instances + "</td></tr>";
|
||||||
|
}).join("");
|
||||||
|
status.textContent = message.objects.length + " object(s), refreshed " + new Date().toLocaleTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
orca.onMessage(function (message) {
|
||||||
|
if (message && message.command === "objects")
|
||||||
|
render(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("refresh").addEventListener("click", function () {
|
||||||
|
orca.postMessage({ command: "refresh" });
|
||||||
|
});
|
||||||
|
document.getElementById("hide").addEventListener("click", function () {
|
||||||
|
orca.postMessage({ command: "hide" });
|
||||||
|
});
|
||||||
|
document.getElementById("close").addEventListener("click", function () {
|
||||||
|
orca.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
orca.postMessage({ command: "refresh" });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def plate_objects():
|
||||||
|
try:
|
||||||
|
model = orca.host.model()
|
||||||
|
except RuntimeError as error:
|
||||||
|
return {"command": "objects", "error": str(error)}
|
||||||
|
return {
|
||||||
|
"command": "objects",
|
||||||
|
"objects": [
|
||||||
|
{"name": obj.name or "(unnamed)", "volumes": obj.volume_count(), "instances": obj.instance_count()}
|
||||||
|
for obj in model.objects()
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DockPanelDemo(orca.script.ScriptPluginCapabilityBase):
|
||||||
|
panel = None
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return "Dock Panel Demo"
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
# The capability instance lives as long as the plugin, so a second run finds the open panel.
|
||||||
|
if self.panel is not None and self.panel.is_open():
|
||||||
|
self.panel.show()
|
||||||
|
return orca.ExecutionResult.success("Dock Panel Demo is already open.")
|
||||||
|
self.panel = orca.host.ui.create_dock_panel(
|
||||||
|
html=PAGE,
|
||||||
|
title="Dock Panel Demo",
|
||||||
|
width=320,
|
||||||
|
height=480,
|
||||||
|
on_message=self.on_message,
|
||||||
|
on_close=self.on_close,
|
||||||
|
dock="right",
|
||||||
|
)
|
||||||
|
return orca.ExecutionResult.success("Dock Panel Demo opened.")
|
||||||
|
|
||||||
|
# Called on the UI thread when the page posts.
|
||||||
|
def on_message(self, message):
|
||||||
|
command = (message or {}).get("command")
|
||||||
|
if command == "refresh":
|
||||||
|
self.panel.post(plate_objects())
|
||||||
|
elif command == "hide":
|
||||||
|
self.panel.hide()
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
self.panel = None
|
||||||
|
|
||||||
|
|
||||||
|
@orca.plugin
|
||||||
|
class DockPanelDemoPlugin(orca.base):
|
||||||
|
def register_capabilities(self):
|
||||||
|
orca.register_capability(DockPanelDemo)
|
||||||
+46
-64
@@ -5,6 +5,7 @@
|
|||||||
//BBS
|
//BBS
|
||||||
#include "Preset.hpp"
|
#include "Preset.hpp"
|
||||||
#include "Exception.hpp"
|
#include "Exception.hpp"
|
||||||
|
#include "InstanceLock.hpp"
|
||||||
#include "LocalesUtils.hpp"
|
#include "LocalesUtils.hpp"
|
||||||
#include "Thread.hpp"
|
#include "Thread.hpp"
|
||||||
#include "format.hpp"
|
#include "format.hpp"
|
||||||
@@ -730,10 +731,13 @@ static bool verify_config_file_checksum(boost::nowide::ifstream &ifs)
|
|||||||
|
|
||||||
|
|
||||||
#ifdef USE_JSON_CONFIG
|
#ifdef USE_JSON_CONFIG
|
||||||
std::string AppConfig::load()
|
std::string AppConfig::load(bool read_only)
|
||||||
{
|
{
|
||||||
json j;
|
json j;
|
||||||
|
|
||||||
|
// Keep another instance from replacing or restoring the file mid-read.
|
||||||
|
InstanceLock instance_lock(read_only ? std::string() : lock_path());
|
||||||
|
|
||||||
// 1) Read the complete config file into a boost::property_tree.
|
// 1) Read the complete config file into a boost::property_tree.
|
||||||
namespace pt = boost::property_tree;
|
namespace pt = boost::property_tree;
|
||||||
pt::ptree tree;
|
pt::ptree tree;
|
||||||
@@ -983,7 +987,6 @@ void AppConfig::save()
|
|||||||
// The config is first written to a file with a PID suffix and then moved
|
// The config is first written to a file with a PID suffix and then moved
|
||||||
// to avoid race conditions with multiple instances of Slic3r
|
// to avoid race conditions with multiple instances of Slic3r
|
||||||
const auto path = config_path();
|
const auto path = config_path();
|
||||||
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
|
||||||
|
|
||||||
json j;
|
json j;
|
||||||
|
|
||||||
@@ -1113,43 +1116,18 @@ void AppConfig::save()
|
|||||||
|
|
||||||
j["local_machines"][local_machine.first] = m_json;
|
j["local_machines"][local_machine.first] = m_json;
|
||||||
}
|
}
|
||||||
boost::nowide::ofstream c;
|
const std::string config_str = j.dump(1, '\t');
|
||||||
c.open(path_pid, std::ios::out | std::ios::trunc);
|
if (write_config_file(path, config_str + "\n", config_str))
|
||||||
c << j.dump(1, '\t') << std::endl;
|
|
||||||
|
|
||||||
#ifdef WIN32
|
|
||||||
// WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API
|
|
||||||
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
|
|
||||||
// we save the config file into a backup first before moving it to the final destination.
|
|
||||||
c << appconfig_md5_hash_line(j.dump(1, '\t'));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
c.close();
|
|
||||||
if (c.fail()) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Failed to write new configuration to " << path_pid << "; aborting attempt to overwrite original configuration";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef WIN32
|
|
||||||
// Make a backup of the configuration file before copying it to the final destination.
|
|
||||||
std::string error_message;
|
|
||||||
std::string backup_path = (boost::format("%1%.bak") % path).str();
|
|
||||||
// Copy configuration file with PID suffix into the configuration file with "bak" suffix.
|
|
||||||
if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS)
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration.";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Rename the config atomically.
|
|
||||||
// On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile.
|
|
||||||
// To cope with that, we already made a backup of the config on Windows.
|
|
||||||
rename_file(path_pid, path);
|
|
||||||
m_dirty = false;
|
m_dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
std::string AppConfig::load()
|
std::string AppConfig::load(bool read_only)
|
||||||
{
|
{
|
||||||
|
// Keep another instance from replacing or restoring the file mid-read.
|
||||||
|
InstanceLock instance_lock(read_only ? std::string() : lock_path());
|
||||||
|
|
||||||
// 1) Read the complete config file into a boost::property_tree.
|
// 1) Read the complete config file into a boost::property_tree.
|
||||||
namespace pt = boost::property_tree;
|
namespace pt = boost::property_tree;
|
||||||
pt::ptree tree;
|
pt::ptree tree;
|
||||||
@@ -1287,7 +1265,6 @@ void AppConfig::save()
|
|||||||
// The config is first written to a file with a PID suffix and then moved
|
// The config is first written to a file with a PID suffix and then moved
|
||||||
// to avoid race conditions with multiple instances of Slic3r
|
// to avoid race conditions with multiple instances of Slic3r
|
||||||
const auto path = config_path();
|
const auto path = config_path();
|
||||||
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
|
||||||
|
|
||||||
std::stringstream config_ss;
|
std::stringstream config_ss;
|
||||||
if (m_mode == EAppMode::Editor)
|
if (m_mode == EAppMode::Editor)
|
||||||
@@ -1323,39 +1300,39 @@ void AppConfig::save()
|
|||||||
// One empty line before the MD5 sum.
|
// One empty line before the MD5 sum.
|
||||||
config_ss << std::endl;
|
config_ss << std::endl;
|
||||||
|
|
||||||
std::string config_str = config_ss.str();
|
const std::string config_str = config_ss.str();
|
||||||
boost::nowide::ofstream c;
|
if (write_config_file(path, config_str, config_str))
|
||||||
c.open(path_pid, std::ios::out | std::ios::trunc);
|
|
||||||
c << config_str;
|
|
||||||
#ifdef WIN32
|
|
||||||
// WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API
|
|
||||||
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
|
|
||||||
// we save the config file into a backup first before moving it to the final destination.
|
|
||||||
c << appconfig_md5_hash_line(config_str);
|
|
||||||
#endif
|
|
||||||
c.close();
|
|
||||||
if (c.fail()) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Failed to write new configuration to " << path_pid << "; aborting attempt to overwrite original configuration";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef WIN32
|
|
||||||
// Make a backup of the configuration file before copying it to the final destination.
|
|
||||||
std::string error_message;
|
|
||||||
std::string backup_path = (boost::format("%1%.bak") % path).str();
|
|
||||||
// Copy configuration file with PID suffix into the configuration file with "bak" suffix.
|
|
||||||
if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS)
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration.";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Rename the config atomically.
|
|
||||||
// On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile.
|
|
||||||
// To cope with that, we already made a backup of the config on Windows.
|
|
||||||
rename_file(path_pid, path);
|
|
||||||
m_dirty = false;
|
m_dirty = false;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
bool AppConfig::write_config_file(const std::string &path, std::string body, const std::string &checksum_source)
|
||||||
|
{
|
||||||
|
// Everything before this is assembly; only the writes need the other instances kept out.
|
||||||
|
InstanceLock instance_lock(lock_path());
|
||||||
|
#ifdef WIN32
|
||||||
|
// WIN32 specific: the final replace is not safe in case of an application crash, there is no atomic "rename file" API
|
||||||
|
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
|
||||||
|
// we save the config file into a backup first before moving it to the final destination.
|
||||||
|
body += appconfig_md5_hash_line(checksum_source);
|
||||||
|
#endif
|
||||||
|
// Not flushed to the device: the idle handler saves on the GUI thread after
|
||||||
|
// any change, and the rename already gives a complete old or new file.
|
||||||
|
if (const std::error_code ec = write_file_atomically(path, body)) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Failed to write the configuration " << path << ": " << ec.message() << "; trying again in 10 s";
|
||||||
|
m_retry_save_at = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_retry_save_at = {};
|
||||||
|
#ifdef WIN32
|
||||||
|
// Written after the config, so the backup never holds a state that was not confirmed written.
|
||||||
|
const std::string backup_path = (boost::format("%1%.bak") % path).str();
|
||||||
|
if (const std::error_code ec = write_file_atomically(backup_path, body))
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Failed to write the backup configuration " << backup_path << ": " << ec.message();
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
|
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
|
||||||
{
|
{
|
||||||
const auto it_v = m_vendors.find(vendor);
|
const auto it_v = m_vendors.find(vendor);
|
||||||
@@ -1844,6 +1821,11 @@ void AppConfig::reset_selections()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string AppConfig::lock_path()
|
||||||
|
{
|
||||||
|
return Slic3r::data_dir().empty() ? std::string() : config_path() + ".lock";
|
||||||
|
}
|
||||||
|
|
||||||
std::string AppConfig::config_path()
|
std::string AppConfig::config_path()
|
||||||
{
|
{
|
||||||
#ifdef USE_JSON_CONFIG
|
#ifdef USE_JSON_CONFIG
|
||||||
@@ -1880,7 +1862,7 @@ bool AppConfig::exists()
|
|||||||
|
|
||||||
std::string AppConfig::load_if_exists()
|
std::string AppConfig::load_if_exists()
|
||||||
{
|
{
|
||||||
return boost::filesystem::exists(loading_path()) ? load() : std::string();
|
return boost::filesystem::exists(loading_path()) ? load(/*read_only=*/true) : std::string();
|
||||||
}
|
}
|
||||||
|
|
||||||
}; // namespace Slic3r
|
}; // namespace Slic3r
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#define slic3r_AppConfig_hpp_
|
#define slic3r_AppConfig_hpp_
|
||||||
|
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <chrono>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "nlohmann/json.hpp"
|
#include "nlohmann/json.hpp"
|
||||||
@@ -121,14 +122,18 @@ public:
|
|||||||
|
|
||||||
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
|
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
|
||||||
// Return an error string, or an empty string on success.
|
// Return an error string, or an empty string on success.
|
||||||
std::string load();
|
std::string load(bool read_only = false);
|
||||||
// Treat a missing config as default state; otherwise load it normally.
|
// Treat a missing config as default state; otherwise load it normally.
|
||||||
|
// The CLI's load: it never saves, so it takes no lock and creates no lock file.
|
||||||
std::string load_if_exists();
|
std::string load_if_exists();
|
||||||
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
|
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
|
||||||
void save();
|
void save();
|
||||||
|
|
||||||
// Does this config need to be saved?
|
// Does this config need to be saved?
|
||||||
bool dirty() const { return m_dirty; }
|
bool dirty() const { return m_dirty; }
|
||||||
|
// False for ten seconds after a failed write, so the idle handler does not
|
||||||
|
// repeat a hopeless attempt on every event; an explicit save() always tries.
|
||||||
|
bool save_due() const { return std::chrono::steady_clock::now() >= m_retry_save_at; }
|
||||||
|
|
||||||
|
|
||||||
void set_dirty() { m_dirty = true; }
|
void set_dirty() { m_dirty = true; }
|
||||||
@@ -338,6 +343,8 @@ public:
|
|||||||
|
|
||||||
// Get the default config path from Slic3r::data_dir().
|
// Get the default config path from Slic3r::data_dir().
|
||||||
std::string config_path();
|
std::string config_path();
|
||||||
|
// Lock file guarding config_path() against other running instances; empty without a data dir.
|
||||||
|
std::string lock_path();
|
||||||
|
|
||||||
// Returns true if the user's data directory comes from before Slic3r 1.40.0 (no updating)
|
// Returns true if the user's data directory comes from before Slic3r 1.40.0 (no updating)
|
||||||
bool legacy_datadir() const { return m_legacy_datadir; }
|
bool legacy_datadir() const { return m_legacy_datadir; }
|
||||||
@@ -448,8 +455,16 @@ private:
|
|||||||
|
|
||||||
// Preset for each machine
|
// Preset for each machine
|
||||||
MachineSettingMap m_printer_settings;
|
MachineSettingMap m_printer_settings;
|
||||||
|
// Writes the assembled config text, and on Windows its checksum and a backup copy; false when the
|
||||||
|
// config itself could not be written, in which case the caller stays dirty and retries. `checksum_source`
|
||||||
|
// is the text load() will verify, which for the JSON config ends before the trailing newline.
|
||||||
|
bool write_config_file(const std::string &path, std::string body, const std::string &checksum_source);
|
||||||
|
|
||||||
// Has any value been modified since the config.ini has been last saved or loaded?
|
// Has any value been modified since the config.ini has been last saved or loaded?
|
||||||
bool m_dirty;
|
bool m_dirty;
|
||||||
|
// After a failed write, save_due() is false for the next ten seconds, so the
|
||||||
|
// idle handler does not repeat a hopeless write on every event.
|
||||||
|
std::chrono::steady_clock::time_point m_retry_save_at{};
|
||||||
// Original version found in the ini file before it was overwritten
|
// Original version found in the ini file before it was overwritten
|
||||||
Semver m_orig_version;
|
Semver m_orig_version;
|
||||||
// Whether the existing version is before system profiles & configuration updating
|
// Whether the existing version is before system profiles & configuration updating
|
||||||
|
|||||||
@@ -304,6 +304,8 @@ set(lisbslic3r_sources
|
|||||||
Geometry/VoronoiUtils.cpp
|
Geometry/VoronoiUtils.cpp
|
||||||
Geometry/VoronoiUtils.hpp
|
Geometry/VoronoiUtils.hpp
|
||||||
Geometry/VoronoiVisualUtils.hpp
|
Geometry/VoronoiVisualUtils.hpp
|
||||||
|
InstanceLock.cpp
|
||||||
|
InstanceLock.hpp
|
||||||
Int128.hpp
|
Int128.hpp
|
||||||
KDTreeIndirect.hpp
|
KDTreeIndirect.hpp
|
||||||
Layer.cpp
|
Layer.cpp
|
||||||
|
|||||||
@@ -1522,11 +1522,9 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
|
|||||||
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
|
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
|
||||||
std::ostringstream ss;
|
std::ostringstream ss;
|
||||||
this->save_to_json(ss, name, from, version);
|
this->save_to_json(ss, name, from, version);
|
||||||
boost::nowide::ofstream c;
|
if (const std::error_code ec = write_file_atomically(file, ss.str()))
|
||||||
c.open(file, std::ios::out | std::ios::trunc);
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": failed to save config to %1%: %2%") % file % ec.message();
|
||||||
c << ss.str();
|
else
|
||||||
c.close();
|
|
||||||
|
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
#include "InstanceLock.hpp"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <system_error>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
|
#include <boost/nowide/fstream.hpp>
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <boost/interprocess/sync/file_lock.hpp>
|
||||||
|
#include <boost/nowide/convert.hpp>
|
||||||
|
#else
|
||||||
|
#include <cerrno>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// LockFileEx, held by this handle alone.
|
||||||
|
using NativeFileLock = boost::interprocess::file_lock;
|
||||||
|
#else
|
||||||
|
// flock(2) rather than an fcntl lock: it belongs to this open file description,
|
||||||
|
// so any other code in the process that opens and closes the lock file, as a
|
||||||
|
// backup or an export walking the data dir might, cannot drop it. An fcntl
|
||||||
|
// lock would go with the first such close.
|
||||||
|
class NativeFileLock
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit NativeFileLock(const char *path) : m_fd(::open(path, O_RDWR | O_CREAT | O_CLOEXEC, 0644))
|
||||||
|
{
|
||||||
|
if (m_fd < 0)
|
||||||
|
throw std::system_error(errno, std::generic_category(), path);
|
||||||
|
}
|
||||||
|
~NativeFileLock() { ::close(m_fd); }
|
||||||
|
bool try_lock()
|
||||||
|
{
|
||||||
|
if (::flock(m_fd, LOCK_EX | LOCK_NB) == 0)
|
||||||
|
return true;
|
||||||
|
// A signal (a child exiting, for one) interrupts the call like any other; the caller polls again.
|
||||||
|
if (errno == EWOULDBLOCK || errno == EINTR)
|
||||||
|
return false;
|
||||||
|
throw std::system_error(errno, std::generic_category(), "flock");
|
||||||
|
}
|
||||||
|
void unlock() { ::flock(m_fd, LOCK_UN); }
|
||||||
|
private:
|
||||||
|
int m_fd;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// One slot per lock file, shared by every guard in the process: one lock
|
||||||
|
// object per path behind a mutex is what makes the guard re-entrant and safe
|
||||||
|
// to use from the preset sync thread and the GUI thread at once. The lock
|
||||||
|
// file is opened by the outermost guard and closed when it goes, so the file
|
||||||
|
// is never held open between guards: whatever is at the path is what gets
|
||||||
|
// locked, and a data dir can be removed once nothing is saving into it. The
|
||||||
|
// file is kept rather than deleted on release because the lock state lives in
|
||||||
|
// the kernel on the open file, and deleting it would let a third instance
|
||||||
|
// lock a fresh file while the second still holds the old one.
|
||||||
|
struct InstanceLock::Slot
|
||||||
|
{
|
||||||
|
std::recursive_mutex mutex;
|
||||||
|
// Non-null exactly while this process holds the file lock.
|
||||||
|
std::unique_ptr<NativeFileLock> file_lock;
|
||||||
|
int depth{0};
|
||||||
|
// Until this point, after a guard could not open, lock or wait out the
|
||||||
|
// file, guards do not touch it.
|
||||||
|
std::chrono::steady_clock::time_point cooldown_until{};
|
||||||
|
};
|
||||||
|
|
||||||
|
InstanceLock::Slot &InstanceLock::slot_for(const std::string &lock_file_path)
|
||||||
|
{
|
||||||
|
// Never freed: a save during static destruction still needs its slot.
|
||||||
|
static auto *registry_mutex = new std::mutex();
|
||||||
|
static auto *registry = new std::map<std::string, std::unique_ptr<Slot>>();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> guard(*registry_mutex);
|
||||||
|
std::unique_ptr<Slot> &slot = (*registry)[lock_file_path];
|
||||||
|
if (! slot)
|
||||||
|
slot = std::make_unique<Slot>();
|
||||||
|
return *slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts the cool-down. Called with the slot mutex held.
|
||||||
|
void InstanceLock::defer(Slot &slot, const std::string &reason)
|
||||||
|
{
|
||||||
|
slot.cooldown_until = std::chrono::steady_clock::now() + cooldown;
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << reason << "; proceeding without the lock for the next " << cooldown.count() << " ms";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates the lock file if needed and opens it, or starts the cool-down.
|
||||||
|
// Called with the slot mutex held.
|
||||||
|
bool InstanceLock::open_lock_file(Slot &slot, const std::string &lock_file_path)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
#ifdef _WIN32
|
||||||
|
// The lock opens an existing file; created once, on the first miss.
|
||||||
|
const std::wstring wide_path = boost::nowide::widen(lock_file_path);
|
||||||
|
try {
|
||||||
|
slot.file_lock = std::make_unique<NativeFileLock>(wide_path.c_str());
|
||||||
|
} catch (const std::exception &) {
|
||||||
|
boost::nowide::ofstream(lock_file_path, std::ios::app).close();
|
||||||
|
slot.file_lock = std::make_unique<NativeFileLock>(wide_path.c_str());
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
slot.file_lock = std::make_unique<NativeFileLock>(lock_file_path.c_str());
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
defer(slot, "Cannot open lock file " + lock_file_path + ": " + e.what() + " (check its owner and permissions)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InstanceLock::InstanceLock(const std::string &lock_file_path, std::chrono::milliseconds timeout)
|
||||||
|
{
|
||||||
|
if (lock_file_path.empty())
|
||||||
|
return;
|
||||||
|
m_slot = &slot_for(lock_file_path);
|
||||||
|
m_slot_guard = std::unique_lock<std::recursive_mutex>(m_slot->mutex);
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (m_slot->depth == 0 && now >= m_slot->cooldown_until && open_lock_file(*m_slot, lock_file_path)) {
|
||||||
|
const auto deadline = now + timeout;
|
||||||
|
bool taken = false;
|
||||||
|
for (;;) {
|
||||||
|
try {
|
||||||
|
if ((taken = m_slot->file_lock->try_lock()))
|
||||||
|
break;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
defer(*m_slot, "Cannot lock " + lock_file_path + ": " + e.what());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (std::chrono::steady_clock::now() >= deadline) {
|
||||||
|
defer(*m_slot, "Another instance has held " + lock_file_path + " for over " + std::to_string(timeout.count()) + " ms");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||||
|
}
|
||||||
|
if (! taken)
|
||||||
|
m_slot->file_lock.reset();
|
||||||
|
}
|
||||||
|
// Counted last, so a throw above leaves the slot exactly as it was found.
|
||||||
|
++ m_slot->depth;
|
||||||
|
m_locked = m_slot->file_lock != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
InstanceLock::~InstanceLock()
|
||||||
|
{
|
||||||
|
if (m_slot == nullptr)
|
||||||
|
return;
|
||||||
|
if (-- m_slot->depth == 0 && m_slot->file_lock) {
|
||||||
|
try {
|
||||||
|
m_slot->file_lock->unlock();
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "Cannot unlock instance lock: " << e.what();
|
||||||
|
}
|
||||||
|
m_slot->file_lock.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
// Scoped write lock on a file shared by every running instance of the
|
||||||
|
// application, such as the app config or the user preset directory: threads
|
||||||
|
// of this process are serialised through a recursive mutex, other processes
|
||||||
|
// through an advisory OS file lock on `lock_file_path`. The lock file is
|
||||||
|
// created on first use and kept; the OS releases the lock when its holder
|
||||||
|
// exits, so a crashed instance never leaves a stale lock behind.
|
||||||
|
//
|
||||||
|
// Best effort: when the lock file cannot be opened or locked, or another
|
||||||
|
// instance still holds it after `timeout`, the guard keeps only the in-process
|
||||||
|
// mutex, locked() reports false and the write proceeds, since a hung instance
|
||||||
|
// must never block another one from saving. For `cooldown` afterwards guards
|
||||||
|
// leave the file alone. The wait for the in-process mutex is bounded only by
|
||||||
|
// the longest critical section, so a guard covers a few file operations and
|
||||||
|
// nothing slower.
|
||||||
|
//
|
||||||
|
// Lock order: the preset collection mutex may be held when a guard is taken
|
||||||
|
// (set_sync_info_and_save() calls save_info() under it), never the reverse;
|
||||||
|
// that is why the guards sit at the leaf readers and writers and why a guard
|
||||||
|
// must not be added around save_user_presets(), which takes the collection
|
||||||
|
// mutex through delete_preset().
|
||||||
|
class InstanceLock
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Long against a critical section of milliseconds, short against the GUI
|
||||||
|
// thread, which is where most guards are taken.
|
||||||
|
static constexpr std::chrono::milliseconds default_timeout{1000};
|
||||||
|
// Long enough that a holder stuck in a debugger does not cost a stall per
|
||||||
|
// save; mutable so tests can shorten it.
|
||||||
|
static inline std::chrono::milliseconds cooldown{10000};
|
||||||
|
|
||||||
|
// An empty path makes the guard a no-op.
|
||||||
|
explicit InstanceLock(const std::string &lock_file_path, std::chrono::milliseconds timeout = default_timeout);
|
||||||
|
~InstanceLock();
|
||||||
|
|
||||||
|
InstanceLock(const InstanceLock &) = delete;
|
||||||
|
InstanceLock &operator=(const InstanceLock &) = delete;
|
||||||
|
|
||||||
|
// True while this process holds the cross-process file lock.
|
||||||
|
bool locked() const { return m_locked; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Slot;
|
||||||
|
static Slot &slot_for(const std::string &lock_file_path);
|
||||||
|
static bool open_lock_file(Slot &slot, const std::string &lock_file_path);
|
||||||
|
static void defer(Slot &slot, const std::string &reason);
|
||||||
|
|
||||||
|
Slot *m_slot{nullptr};
|
||||||
|
std::unique_lock<std::recursive_mutex> m_slot_guard;
|
||||||
|
bool m_locked{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
+82
-34
@@ -48,6 +48,9 @@
|
|||||||
|
|
||||||
#include "libslic3r.h"
|
#include "libslic3r.h"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
#include "InstanceLock.hpp"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
#include "Time.hpp"
|
#include "Time.hpp"
|
||||||
#include "PlaceholderParser.hpp"
|
#include "PlaceholderParser.hpp"
|
||||||
#include "libslic3r/GCode/Thumbnails.hpp"
|
#include "libslic3r/GCode/Thumbnails.hpp"
|
||||||
@@ -104,6 +107,31 @@ std::string get_preset_canonical_name(const std::string &preset_bare_name, const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string user_presets_lock_path(bool read_only)
|
||||||
|
{
|
||||||
|
return read_only || data_dir().empty() ? std::string() : (fs::path(data_dir()) / (PRESET_USER_DIR ".lock")).string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes a preset file the scan could not load, and its .info, under the lock.
|
||||||
|
// Without the lock, in a cool-down, the file may be another instance's fresh
|
||||||
|
// write that this scan merely raced, so it stays for the next scan to judge.
|
||||||
|
static void remove_preset_files(const std::string &preset_file, bool read_only)
|
||||||
|
{
|
||||||
|
if (read_only)
|
||||||
|
return;
|
||||||
|
const std::string lock_path = user_presets_lock_path();
|
||||||
|
InstanceLock instance_lock(lock_path);
|
||||||
|
if (! lock_path.empty() && ! instance_lock.locked()) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "Leaving unreadable preset " << preset_file << " in place: the instance lock is not held";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boost::system::error_code ec;
|
||||||
|
fs::path file_path(preset_file);
|
||||||
|
fs::remove(file_path, ec);
|
||||||
|
file_path.replace_extension(".info");
|
||||||
|
fs::remove(file_path, ec);
|
||||||
|
}
|
||||||
|
|
||||||
std::string get_preset_bare_name(const std::string &canonical_name)
|
std::string get_preset_bare_name(const std::string &canonical_name)
|
||||||
{
|
{
|
||||||
const auto pos = canonical_name.find_last_of('/');
|
const auto pos = canonical_name.find_last_of('/');
|
||||||
@@ -646,18 +674,20 @@ void Preset::save_info(std::string file)
|
|||||||
file = idx_file.string();
|
file = idx_file.string();
|
||||||
}
|
}
|
||||||
|
|
||||||
boost::nowide::ofstream c;
|
|
||||||
c.open(file, std::ios::out | std::ios::trunc);
|
|
||||||
std::string sync_info_to_save;
|
std::string sync_info_to_save;
|
||||||
//BBS: hold is used for stop requesting to server this time
|
//BBS: hold is used for stop requesting to server this time
|
||||||
if (this->sync_info.compare("hold") != 0)
|
if (this->sync_info.compare("hold") != 0)
|
||||||
sync_info_to_save = this->sync_info;
|
sync_info_to_save = this->sync_info;
|
||||||
|
std::ostringstream c;
|
||||||
c << "sync_info" << " = " << sync_info_to_save << std::endl;
|
c << "sync_info" << " = " << sync_info_to_save << std::endl;
|
||||||
c << "user_id" << " = " << this->user_id << std::endl;
|
c << "user_id" << " = " << this->user_id << std::endl;
|
||||||
c << "setting_id" << " = " << this->setting_id << std::endl;
|
c << "setting_id" << " = " << this->setting_id << std::endl;
|
||||||
c << "base_id" << " = " << this->base_id << std::endl;
|
c << "base_id" << " = " << this->base_id << std::endl;
|
||||||
c << "updated_time" << " = " << std::to_string(this->updated_time) << std::endl;
|
c << "updated_time" << " = " << std::to_string(this->updated_time) << std::endl;
|
||||||
c.close();
|
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
|
if (const std::error_code ec = write_file_atomically(file, c.str()))
|
||||||
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << file << ": " << ec.message();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Preset::remove_files(bool cloud_already_deleted)
|
void Preset::remove_files(bool cloud_already_deleted)
|
||||||
@@ -666,6 +696,7 @@ void Preset::remove_files(bool cloud_already_deleted)
|
|||||||
if (this->is_project_embedded) {
|
if (this->is_project_embedded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
// Erase the preset file.
|
// Erase the preset file.
|
||||||
boost::nowide::remove(this->file.c_str());
|
boost::nowide::remove(this->file.c_str());
|
||||||
fs::path idx_path(this->file);
|
fs::path idx_path(this->file);
|
||||||
@@ -702,12 +733,16 @@ void Preset::save(DynamicPrintConfig* parent_config)
|
|||||||
else
|
else
|
||||||
from_str = std::string("Default");
|
from_str = std::string("Default");
|
||||||
|
|
||||||
boost::filesystem::create_directories(fs::path(this->file).parent_path());
|
|
||||||
const std::string bare_name = get_preset_bare_name(this->name);
|
const std::string bare_name = get_preset_bare_name(this->name);
|
||||||
|
|
||||||
|
// What gets written: the diff against the parent, the config plus its
|
||||||
|
// filament id, or the config as is. Built before the lock is taken so the
|
||||||
|
// exclusive window covers only the file writes.
|
||||||
|
DynamicPrintConfig temp_config;
|
||||||
|
const DynamicPrintConfig *to_save = &this->config;
|
||||||
|
|
||||||
//BBS: only save difference if it has parent
|
//BBS: only save difference if it has parent
|
||||||
if (parent_config) {
|
if (parent_config) {
|
||||||
DynamicPrintConfig temp_config;
|
|
||||||
std::vector<std::string> dirty_options = config.diff(*parent_config);
|
std::vector<std::string> dirty_options = config.diff(*parent_config);
|
||||||
|
|
||||||
std::string extruder_id_name, extruder_variant_name;
|
std::string extruder_id_name, extruder_variant_name;
|
||||||
@@ -743,13 +778,22 @@ void Preset::save(DynamicPrintConfig* parent_config)
|
|||||||
opt_dst->set(opt_src);
|
opt_dst->set(opt_src);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
|
to_save = &temp_config;
|
||||||
} else if (!filament_id.empty() && inherits().empty()) {
|
} else if (!filament_id.empty() && inherits().empty()) {
|
||||||
DynamicPrintConfig temp_config = config;
|
temp_config = config;
|
||||||
temp_config.set_key_value(BBL_JSON_KEY_FILAMENT_ID, new ConfigOptionString(filament_id));
|
temp_config.set_key_value(BBL_JSON_KEY_FILAMENT_ID, new ConfigOptionString(filament_id));
|
||||||
temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
|
to_save = &temp_config;
|
||||||
} else {
|
}
|
||||||
this->config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
|
|
||||||
|
std::ostringstream json;
|
||||||
|
to_save->save_to_json(json, bare_name, from_str, this->version.to_string());
|
||||||
|
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
|
boost::filesystem::create_directories(fs::path(this->file).parent_path());
|
||||||
|
if (const std::error_code ec = write_file_atomically(this->file, json.str())) {
|
||||||
|
// No .info either: one without its preset reads as a cloud deletion request.
|
||||||
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << this->file << ": " << ec.message();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " save config for: " << this->name << " and filament_id: " << filament_id << " and base_id: " << this->base_id;
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " save config for: " << this->name << " and filament_id: " << filament_id << " and base_id: " << this->base_id;
|
||||||
|
|
||||||
@@ -770,6 +814,7 @@ void Preset::reload(Preset const &parent)
|
|||||||
std::string reason;
|
std::string reason;
|
||||||
ForwardCompatibilitySubstitutionRule substitution_rule = ForwardCompatibilitySubstitutionRule::Disable;
|
ForwardCompatibilitySubstitutionRule substitution_rule = ForwardCompatibilitySubstitutionRule::Disable;
|
||||||
try {
|
try {
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
ConfigSubstitutions config_substitutions = config.load_from_json(file, substitution_rule, key_values, reason);
|
ConfigSubstitutions config_substitutions = config.load_from_json(file, substitution_rule, key_values, reason);
|
||||||
this->config = parent.config;
|
this->config = parent.config;
|
||||||
this->config.apply(std::move(config));
|
this->config.apply(std::move(config));
|
||||||
@@ -1707,6 +1752,9 @@ void PresetCollection::load_presets(
|
|||||||
//BBS: change to json format
|
//BBS: change to json format
|
||||||
for (auto &dir_entry : boost::filesystem::directory_iterator(dir))
|
for (auto &dir_entry : boost::filesystem::directory_iterator(dir))
|
||||||
{
|
{
|
||||||
|
// Per file, so a save on another thread or in another instance never
|
||||||
|
// waits for the whole scan.
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path(read_only));
|
||||||
std::string file_name = dir_entry.path().filename().string();
|
std::string file_name = dir_entry.path().filename().string();
|
||||||
//if (Slic3r::is_ini_file(dir_entry)) {
|
//if (Slic3r::is_ini_file(dir_entry)) {
|
||||||
if (Slic3r::is_json_file(file_name)) {
|
if (Slic3r::is_json_file(file_name)) {
|
||||||
@@ -1725,30 +1773,26 @@ void PresetCollection::load_presets(
|
|||||||
preset.file = dir_entry.path().string();
|
preset.file = dir_entry.path().string();
|
||||||
// Load the preset file, apply preset values on top of defaults.
|
// Load the preset file, apply preset values on top of defaults.
|
||||||
try {
|
try {
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
std::map<std::string, std::string> key_values;
|
||||||
|
std::string reason;
|
||||||
|
ConfigSubstitutions config_substitutions;
|
||||||
fs::path idx_path(preset.file);
|
fs::path idx_path(preset.file);
|
||||||
idx_path.replace_extension(".info");
|
idx_path.replace_extension(".info");
|
||||||
if (fs::exists(idx_path)) {
|
if (fs::exists(idx_path)) {
|
||||||
preset.load_info(idx_path.string());
|
preset.load_info(idx_path.string());
|
||||||
}
|
}
|
||||||
DynamicPrintConfig config;
|
|
||||||
//BBS: change to json format
|
//BBS: change to json format
|
||||||
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
|
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
|
||||||
std::map<std::string, std::string> key_values;
|
config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
|
||||||
std::string reason;
|
|
||||||
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
|
|
||||||
if (! config_substitutions.empty())
|
|
||||||
substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
|
|
||||||
if (!reason.empty()) {
|
if (!reason.empty()) {
|
||||||
fs::path file_path(preset.file);
|
remove_preset_files(preset.file, read_only);
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
file_path.replace_extension(".info");
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file;
|
BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file;
|
||||||
++m_errors;
|
++m_errors;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (! config_substitutions.empty())
|
||||||
|
substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
|
||||||
|
|
||||||
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
|
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
|
||||||
boost::optional<Semver> version = Semver::parse(version_str);
|
boost::optional<Semver> version = Semver::parse(version_str);
|
||||||
@@ -1832,23 +1876,13 @@ void PresetCollection::load_presets(
|
|||||||
} catch (const std::ifstream::failure &err) {
|
} catch (const std::ifstream::failure &err) {
|
||||||
++m_errors;
|
++m_errors;
|
||||||
BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what();
|
BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what();
|
||||||
fs::path file_path(preset.file);
|
remove_preset_files(preset.file, read_only);
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
file_path.replace_extension(".info");
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
|
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
|
||||||
} catch (const std::runtime_error &err) {
|
} catch (const std::runtime_error &err) {
|
||||||
++m_errors;
|
++m_errors;
|
||||||
BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what();
|
BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what();
|
||||||
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
|
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
|
||||||
fs::path file_path(preset.file);
|
remove_preset_files(preset.file, read_only);
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
file_path.replace_extension(".info");
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preset_loaded_fn != nullptr)
|
if (preset_loaded_fn != nullptr)
|
||||||
@@ -4201,8 +4235,15 @@ void PhysicalPrinter::update_preset_names_in_config()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PhysicalPrinter::save(DynamicPrintConfig* /* parent_config */)
|
||||||
|
{
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
|
this->config.save_to_json(this->file, std::string("Physical_Printer"), std::string("User"), std::string(SLIC3R_VERSION));
|
||||||
|
}
|
||||||
|
|
||||||
void PhysicalPrinter::save(const std::string& file_name_from, const std::string& file_name_to)
|
void PhysicalPrinter::save(const std::string& file_name_from, const std::string& file_name_to)
|
||||||
{
|
{
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
// rename the file
|
// rename the file
|
||||||
boost::nowide::rename(file_name_from.data(), file_name_to.data());
|
boost::nowide::rename(file_name_from.data(), file_name_to.data());
|
||||||
this->file = file_name_to;
|
this->file = file_name_to;
|
||||||
@@ -4334,6 +4375,7 @@ void PhysicalPrinterCollection::load_printers(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
PhysicalPrinter printer(name, this->default_config());
|
PhysicalPrinter printer(name, this->default_config());
|
||||||
printer.file = dir_entry.path().string();
|
printer.file = dir_entry.path().string();
|
||||||
// Load the preset file, apply preset values on top of defaults.
|
// Load the preset file, apply preset values on top of defaults.
|
||||||
@@ -4526,7 +4568,10 @@ bool PhysicalPrinterCollection::delete_printer(const std::string& name)
|
|||||||
|
|
||||||
const PhysicalPrinter& printer = *it;
|
const PhysicalPrinter& printer = *it;
|
||||||
// Erase the preset file.
|
// Erase the preset file.
|
||||||
|
{
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
boost::nowide::remove(printer.file.c_str());
|
boost::nowide::remove(printer.file.c_str());
|
||||||
|
}
|
||||||
m_printers.erase(it);
|
m_printers.erase(it);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4538,7 +4583,10 @@ bool PhysicalPrinterCollection::delete_selected_printer()
|
|||||||
const PhysicalPrinter& printer = this->get_selected_printer();
|
const PhysicalPrinter& printer = this->get_selected_printer();
|
||||||
|
|
||||||
// Erase the preset file.
|
// Erase the preset file.
|
||||||
|
{
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
boost::nowide::remove(printer.file.c_str());
|
boost::nowide::remove(printer.file.c_str());
|
||||||
|
}
|
||||||
// Remove the preset from the list.
|
// Remove the preset from the list.
|
||||||
m_printers.erase(m_printers.begin() + m_idx_selected);
|
m_printers.erase(m_printers.begin() + m_idx_selected);
|
||||||
// unselect all printers
|
// unselect all printers
|
||||||
|
|||||||
@@ -484,6 +484,12 @@ std::string get_preset_canonical_name(const std::string &preset_bare_name, const
|
|||||||
// Tail segment of a canonical name — what's written to the bundle's .json filename and JSON "name" field.
|
// Tail segment of a canonical name — what's written to the bundle's .json filename and JSON "name" field.
|
||||||
std::string get_preset_bare_name(const std::string &canonical_name);
|
std::string get_preset_bare_name(const std::string &canonical_name);
|
||||||
|
|
||||||
|
// Lock file guarding every user preset file under data_dir() against other
|
||||||
|
// running instances and the preset sync thread. Empty without a data dir, and
|
||||||
|
// for a read-only load (the CLI), which never rewrites or deletes and may run
|
||||||
|
// many jobs on one data dir.
|
||||||
|
std::string user_presets_lock_path(bool read_only = false);
|
||||||
|
|
||||||
// Resolve an origin from a directory path when the caller passes Kind::Auto.
|
// Resolve an origin from a directory path when the caller passes Kind::Auto.
|
||||||
PresetOrigin detect_origin_from_path(const boost::filesystem::path &path, const PresetOrigin &explicit_origin = PresetOrigin());
|
PresetOrigin detect_origin_from_path(const boost::filesystem::path &path, const PresetOrigin &explicit_origin = PresetOrigin());
|
||||||
|
|
||||||
@@ -1053,7 +1059,7 @@ public:
|
|||||||
|
|
||||||
//BBS: change to json format
|
//BBS: change to json format
|
||||||
//void save() { this->config.save(this->file); }
|
//void save() { this->config.save(this->file); }
|
||||||
void save(DynamicPrintConfig* parent_config) { this->config.save_to_json(this->file, std::string("Physical_Printer"), std::string("User"), std::string(SLIC3R_VERSION)); }
|
void save(DynamicPrintConfig* parent_config);
|
||||||
void save(const std::string& file_name_from, const std::string& file_name_to);
|
void save(const std::string& file_name_from, const std::string& file_name_to);
|
||||||
|
|
||||||
void update_from_preset(const Preset& preset);
|
void update_from_preset(const Preset& preset);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include <atomic>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
#include "libslic3r.h"
|
#include "libslic3r.h"
|
||||||
#include "I18N.hpp"
|
#include "I18N.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
#include "InstanceLock.hpp"
|
||||||
#include "LocalesUtils.hpp"
|
#include "LocalesUtils.hpp"
|
||||||
#include "Model.hpp"
|
#include "Model.hpp"
|
||||||
#include "TriangleSelector.hpp"
|
#include "TriangleSelector.hpp"
|
||||||
@@ -1227,6 +1229,13 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
|
|||||||
|
|
||||||
const auto user_load_t0 = std::chrono::steady_clock::now();
|
const auto user_load_t0 = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
// Reads one bundle's metadata under the lock, per file, so the lock is never
|
||||||
|
// held when bundles.WriteLock() is taken afterwards.
|
||||||
|
auto load_bundle_metadata = [read_only](const fs::path &metadata_file, BundleMetadata &metadata) {
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path(read_only));
|
||||||
|
return metadata.load_from_json(metadata_file.string());
|
||||||
|
};
|
||||||
|
|
||||||
// Load bundle metadata from _local directory first
|
// Load bundle metadata from _local directory first
|
||||||
fs::path local_dir(folder / PRESET_LOCAL_DIR);
|
fs::path local_dir(folder / PRESET_LOCAL_DIR);
|
||||||
if (fs::exists(local_dir)) {
|
if (fs::exists(local_dir)) {
|
||||||
@@ -1240,7 +1249,7 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
|
|||||||
if (!fs::exists(metadata_file)) continue;
|
if (!fs::exists(metadata_file)) continue;
|
||||||
|
|
||||||
BundleMetadata metadata;
|
BundleMetadata metadata;
|
||||||
if (!metadata.load_from_json(metadata_file.string())) continue;
|
if (!load_bundle_metadata(metadata_file, metadata)) continue;
|
||||||
metadata.print_presets.clear();
|
metadata.print_presets.clear();
|
||||||
metadata.filament_presets.clear();
|
metadata.filament_presets.clear();
|
||||||
metadata.printer_presets.clear();
|
metadata.printer_presets.clear();
|
||||||
@@ -1275,7 +1284,7 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
|
|||||||
if (!fs::exists(metadata_file)) continue;
|
if (!fs::exists(metadata_file)) continue;
|
||||||
|
|
||||||
BundleMetadata metadata;
|
BundleMetadata metadata;
|
||||||
if (!metadata.load_from_json(metadata_file.string())) continue;
|
if (!load_bundle_metadata(metadata_file, metadata)) continue;
|
||||||
metadata.print_presets.clear();
|
metadata.print_presets.clear();
|
||||||
metadata.filament_presets.clear();
|
metadata.filament_presets.clear();
|
||||||
metadata.printer_presets.clear();
|
metadata.printer_presets.clear();
|
||||||
@@ -1609,10 +1618,12 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
|
|||||||
if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message();
|
if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message();
|
||||||
//create temp folder
|
//create temp folder
|
||||||
//std::string user_default_temp_dir = data_dir() + "/" + PRESET_USER_DIR + "/" + DEFAULT_USER_FOLDER_NAME + "/" + "temp";
|
//std::string user_default_temp_dir = data_dir() + "/" + PRESET_USER_DIR + "/" + DEFAULT_USER_FOLDER_NAME + "/" + "temp";
|
||||||
fs::path temp_folder(configs_folder / "temp");
|
// Under cache/, per process and per import, so two instances importing
|
||||||
|
// at once do not clear each other's extraction and no preset scan reads it.
|
||||||
|
static std::atomic<unsigned> import_counter{0};
|
||||||
|
fs::path temp_folder(fs::path(data_dir()) / "cache" / ("import." + std::to_string(get_current_pid()) + "." + std::to_string(import_counter++)));
|
||||||
std::string user_default_temp_dir = temp_folder.make_preferred().string();
|
std::string user_default_temp_dir = temp_folder.make_preferred().string();
|
||||||
if (fs::exists(temp_folder)) fs::remove_all(temp_folder);
|
fs::create_directories(temp_folder, ec);
|
||||||
fs::create_directory(temp_folder, ec);
|
|
||||||
if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message();
|
if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message();
|
||||||
|
|
||||||
file = boost::filesystem::path(file).make_preferred().string();
|
file = boost::filesystem::path(file).make_preferred().string();
|
||||||
@@ -1624,6 +1635,9 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
|
|||||||
status = mz_zip_reader_init_cfile(&zip_archive, zipFile, 0, MZ_ZIP_FLAG_CASE_SENSITIVE | MZ_ZIP_FLAG_IGNORE_PATH);
|
status = mz_zip_reader_init_cfile(&zip_archive, zipFile, 0, MZ_ZIP_FLAG_CASE_SENSITIVE | MZ_ZIP_FLAG_IGNORE_PATH);
|
||||||
if (MZ_FALSE == status) {
|
if (MZ_FALSE == status) {
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Failed to initialize reader ZIP archive";
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Failed to initialize reader ZIP archive";
|
||||||
|
if (zipFile != nullptr)
|
||||||
|
std::fclose(zipFile);
|
||||||
|
fs::remove_all(temp_folder, ec);
|
||||||
return substitutions;
|
return substitutions;
|
||||||
} else {
|
} else {
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Success to initialize reader ZIP archive";
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Success to initialize reader ZIP archive";
|
||||||
@@ -2231,10 +2245,9 @@ void PresetBundle::remove_user_presets_directory(const std::string preset_folder
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, delete directory : %1%") % dir_user_presets;
|
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, delete directory : %1%") % dir_user_presets;
|
||||||
fs::path folder(dir_user_presets);
|
boost::system::error_code ec;
|
||||||
if (fs::exists(folder)) {
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
fs::remove_all(folder);
|
fs::remove_all(fs::path(dir_user_presets), ec);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PresetBundle::update_system_preset_setting_ids(std::map<std::string, std::map<std::string, std::string>>& system_presets)
|
void PresetBundle::update_system_preset_setting_ids(std::map<std::string, std::map<std::string, std::string>>& system_presets)
|
||||||
@@ -7932,9 +7945,13 @@ bool BundleMetadata::save_to_json(const std::string& path) const
|
|||||||
j["filament_presets"] = strip_prefix(this->filament_presets);
|
j["filament_presets"] = strip_prefix(this->filament_presets);
|
||||||
j["printer_presets"] = strip_prefix(this->printer_presets);
|
j["printer_presets"] = strip_prefix(this->printer_presets);
|
||||||
|
|
||||||
boost::nowide::ofstream ofs(path);
|
const std::string content = j.dump(4);
|
||||||
ofs << j.dump(4);
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
return ofs.good();
|
if (const std::error_code ec = write_file_atomically(path, content)) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Failed to save bundle metadata to " << path << ": " << ec.message();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
BOOST_LOG_TRIVIAL(error) << "Failed to save bundle metadata to " << path << ": " << e.what();
|
BOOST_LOG_TRIVIAL(error) << "Failed to save bundle metadata to " << path << ": " << e.what();
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -400,46 +400,24 @@ bool write_cache_blob(const std::string& path, const std::string& blob)
|
|||||||
{
|
{
|
||||||
boost::crc_32_type crc;
|
boost::crc_32_type crc;
|
||||||
crc.process_bytes(blob.data(), blob.size());
|
crc.process_bytes(blob.data(), blob.size());
|
||||||
// Written beside the target and moved into place, as AppConfig::save does:
|
// Written beside the target and moved into place: a cache is truncated and
|
||||||
// a cache is truncated and rewritten in full, so a write that dies partway
|
// rewritten in full, so a write that dies partway would otherwise leave a
|
||||||
// would otherwise leave a header claiming more body than the file holds.
|
// header claiming more body than the file holds.
|
||||||
// The PID suffix also keeps two instances writing the same vendor from
|
|
||||||
// interleaving.
|
|
||||||
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + ".tmp";
|
|
||||||
try {
|
try {
|
||||||
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
|
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
|
||||||
{
|
|
||||||
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
|
|
||||||
if (!ofs.is_open()) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: cannot open for writing: " << tmp_path;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
CacheFileHeader fhdr;
|
CacheFileHeader fhdr;
|
||||||
fhdr.magic = CACHE_MAGIC;
|
fhdr.magic = CACHE_MAGIC;
|
||||||
fhdr.version = CACHE_VERSION;
|
fhdr.version = CACHE_VERSION;
|
||||||
fhdr.data_size = static_cast<uint64_t>(blob.size());
|
fhdr.data_size = static_cast<uint64_t>(blob.size());
|
||||||
fhdr.crc32 = crc.checksum();
|
fhdr.crc32 = crc.checksum();
|
||||||
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
|
const std::string_view header(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
|
||||||
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
|
if (const std::error_code ec = write_file_atomically(path, { header, std::string_view(blob) }, /*binary=*/true)) {
|
||||||
ofs.close(); // flush; close() raises failbit on error
|
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << ec.message();
|
||||||
if (! ofs.good()) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << tmp_path << ")";
|
|
||||||
boost::system::error_code ec;
|
|
||||||
boost::filesystem::remove(tmp_path, ec);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (const std::error_code ec = rename_file(tmp_path, path)) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not move " << tmp_path << " into place: " << ec.message();
|
|
||||||
boost::system::error_code rm;
|
|
||||||
boost::filesystem::remove(tmp_path, rm);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what();
|
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what();
|
||||||
boost::system::error_code ec;
|
|
||||||
boost::filesystem::remove(tmp_path, ec);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <string_view>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
#include <boost/system/error_code.hpp>
|
#include <boost/system/error_code.hpp>
|
||||||
@@ -224,6 +226,21 @@ extern std::vector<std::string> split_string(const std::string &str, char delimi
|
|||||||
// On Windows, the file explorer (or anti-virus or whatever else) often locks the file
|
// On Windows, the file explorer (or anti-virus or whatever else) often locks the file
|
||||||
// for a short while, so the file may not be movable. Retry while we see recoverable errors.
|
// for a short while, so the file may not be movable. Retry while we see recoverable errors.
|
||||||
extern std::error_code rename_file(const std::string &from, const std::string &to);
|
extern std::error_code rename_file(const std::string &from, const std::string &to);
|
||||||
|
// Write `chunks`, in order, to `path` through a temporary file beside it that is
|
||||||
|
// then renamed over the target, so a concurrent reader sees the old or the new
|
||||||
|
// file, never a partial one. The temporary is removed on failure and an existing
|
||||||
|
// target keeps its permissions. Text mode unless `binary`, so Windows writes CRLF
|
||||||
|
// as the streams this replaces did. A target that is not a regular file (a
|
||||||
|
// device or pipe) is written in place, since replacing it would change what it
|
||||||
|
// is, and so is an existing target beside which no temporary can be created or
|
||||||
|
// whose replace the filesystem refuses; a symlink is followed and the file it
|
||||||
|
// names is replaced. On Windows a reader holding the
|
||||||
|
// target open without sharing its deletion, which the C runtime does not, makes
|
||||||
|
// the replace fall back to the in-place write too, so an unlocked reader there
|
||||||
|
// can still see a partial file.
|
||||||
|
extern std::error_code write_file_atomically(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary = false);
|
||||||
|
inline std::error_code write_file_atomically(const std::string &path, const std::string &content, bool binary = false)
|
||||||
|
{ return write_file_atomically(path, { std::string_view(content) }, binary); }
|
||||||
|
|
||||||
enum CopyFileResult {
|
enum CopyFileResult {
|
||||||
SUCCESS = 0,
|
SUCCESS = 0,
|
||||||
|
|||||||
+90
-2
@@ -9,6 +9,8 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <mutex>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -702,13 +704,99 @@ namespace WindowsSupport
|
|||||||
std::error_code rename_file(const std::string &from, const std::string &to)
|
std::error_code rename_file(const std::string &from, const std::string &to)
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
// Retries and moves an open destination aside itself.
|
||||||
return WindowsSupport::rename(from, to);
|
return WindowsSupport::rename(from, to);
|
||||||
#else
|
#else
|
||||||
boost::nowide::remove(to.c_str());
|
// rename(2) replaces an existing target atomically; removing it first would
|
||||||
return std::make_error_code(static_cast<std::errc>(boost::nowide::rename(from.c_str(), to.c_str())));
|
// leave a window in which the file does not exist at all.
|
||||||
|
if (boost::nowide::rename(from.c_str(), to.c_str()) == 0)
|
||||||
|
return {};
|
||||||
|
const int err = errno;
|
||||||
|
// Some mounts (sshfs, gvfs, MTP and a few SMB setups) refuse to replace an
|
||||||
|
// existing target in one step, each with the error it sees fit; every error
|
||||||
|
// is worth the remove-then-rename this always did, except the ones no retry
|
||||||
|
// can help: nothing at the source, a different device, or a directory where
|
||||||
|
// a file was expected and the reverse.
|
||||||
|
const bool worth_retrying = err != ENOENT && err != EXDEV && err != ENOTDIR && err != EISDIR;
|
||||||
|
if (worth_retrying && boost::nowide::remove(to.c_str()) == 0 && boost::nowide::rename(from.c_str(), to.c_str()) == 0)
|
||||||
|
return {};
|
||||||
|
return std::make_error_code(static_cast<std::errc>(err));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::error_code write_whole_file(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
|
||||||
|
{
|
||||||
|
errno = 0;
|
||||||
|
FILE *file = boost::nowide::fopen(path.c_str(), binary ? "wb" : "w");
|
||||||
|
if (file == nullptr)
|
||||||
|
return std::make_error_code(errno != 0 ? static_cast<std::errc>(errno) : std::errc::io_error);
|
||||||
|
bool ok = true;
|
||||||
|
for (const std::string_view chunk : chunks)
|
||||||
|
ok = ok && std::fwrite(chunk.data(), 1, chunk.size(), file) == chunk.size();
|
||||||
|
ok = ok && std::fflush(file) == 0;
|
||||||
|
const int err = ok ? 0 : errno;
|
||||||
|
ok = std::fclose(file) == 0 && ok;
|
||||||
|
if (ok)
|
||||||
|
return {};
|
||||||
|
return std::make_error_code(err != 0 ? static_cast<std::errc>(err) : std::errc::io_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The in-place fallback truncates the target, so two threads of this process
|
||||||
|
// on the same file must not both be in it. One mutex for all such writes: they
|
||||||
|
// are the rare case. Never freed, like the InstanceLock registry, so a save
|
||||||
|
// during static destruction still finds it.
|
||||||
|
static std::error_code write_in_place(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
|
||||||
|
{
|
||||||
|
static auto *mutex = new std::mutex();
|
||||||
|
std::lock_guard<std::mutex> guard(*mutex);
|
||||||
|
return write_whole_file(path, chunks, binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code write_file_atomically(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
|
||||||
|
{
|
||||||
|
boost::system::error_code bec;
|
||||||
|
const boost::filesystem::file_status target = boost::filesystem::symlink_status(path, bec);
|
||||||
|
const bool target_exists = ! bec && boost::filesystem::exists(target);
|
||||||
|
if (target_exists && boost::filesystem::is_symlink(target)) {
|
||||||
|
// A config or preset kept in a dotfiles repository: the link stays,
|
||||||
|
// the file it points to is replaced like any other.
|
||||||
|
const boost::filesystem::path resolved = boost::filesystem::canonical(path, bec);
|
||||||
|
if (! bec && boost::filesystem::is_regular_file(resolved, bec))
|
||||||
|
return write_file_atomically(resolved.string(), chunks, binary);
|
||||||
|
}
|
||||||
|
if (target_exists && ! boost::filesystem::is_regular_file(target))
|
||||||
|
return write_in_place(path, chunks, binary);
|
||||||
|
|
||||||
|
// Unique per process and per call, so two threads writing one target
|
||||||
|
// without a lock never share a temporary.
|
||||||
|
static std::atomic<unsigned> counter{0};
|
||||||
|
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + "." + std::to_string(counter++) + ".tmp";
|
||||||
|
if (const std::error_code ec = write_whole_file(tmp_path, chunks, binary)) {
|
||||||
|
boost::nowide::remove(tmp_path.c_str());
|
||||||
|
if (! target_exists)
|
||||||
|
return ec;
|
||||||
|
// A directory that lets this process write its files but not create
|
||||||
|
// one: losing the save is worse than a reader seeing a partial file.
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "Cannot create a temporary beside " << path << " (" << ec.message() << "); writing in place";
|
||||||
|
return write_in_place(path, chunks, binary);
|
||||||
|
}
|
||||||
|
#ifndef _WIN32
|
||||||
|
// Not on Windows, where a read-only bit on the temporary would stop the rename itself.
|
||||||
|
if (target_exists)
|
||||||
|
boost::filesystem::permissions(tmp_path, target.permissions(), bec);
|
||||||
|
#endif
|
||||||
|
if (const std::error_code ec = rename_file(tmp_path, path)) {
|
||||||
|
boost::nowide::remove(tmp_path.c_str());
|
||||||
|
// A reader on Windows holding the target open without FILE_SHARE_DELETE,
|
||||||
|
// or a mount that cannot replace a file at all. Losing the save is worse
|
||||||
|
// than a reader seeing a partial file, so write in place the way this
|
||||||
|
// used to work before the atomic path existed.
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "Cannot replace " << path << " (" << ec.message() << "); writing in place";
|
||||||
|
return write_in_place(path, chunks, binary);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
// Copied from boost::filesystem.
|
// Copied from boost::filesystem.
|
||||||
// Called by copy_file_linux() in case linux sendfile() API is not supported.
|
// Called by copy_file_linux() in case linux sendfile() API is not supported.
|
||||||
|
|||||||
@@ -139,8 +139,16 @@ set(SLIC3R_GUI_SOURCES
|
|||||||
GUI/TerminalDialog.hpp
|
GUI/TerminalDialog.hpp
|
||||||
GUI/PluginProgressDialog.cpp
|
GUI/PluginProgressDialog.cpp
|
||||||
GUI/PluginProgressDialog.hpp
|
GUI/PluginProgressDialog.hpp
|
||||||
GUI/PluginWebDialog.cpp
|
GUI/WebDialog.cpp
|
||||||
GUI/PluginWebDialog.hpp
|
GUI/WebDialog.hpp
|
||||||
|
GUI/DockPanel.cpp
|
||||||
|
GUI/DockPanel.hpp
|
||||||
|
GUI/WebPanel.cpp
|
||||||
|
GUI/WebPanel.hpp
|
||||||
|
GUI/Widgets/WebHosting.cpp
|
||||||
|
GUI/Widgets/WebHosting.hpp
|
||||||
|
GUI/AuiPaneLayout.cpp
|
||||||
|
GUI/AuiPaneLayout.hpp
|
||||||
GUI/DragCanvas.cpp
|
GUI/DragCanvas.cpp
|
||||||
GUI/DragCanvas.hpp
|
GUI/DragCanvas.hpp
|
||||||
GUI/EditGCodeDialog.cpp
|
GUI/EditGCodeDialog.cpp
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#include "AuiPaneLayout.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
std::string aui_pane_layout_entry(const std::string& layout, const std::string& pane_name)
|
||||||
|
{
|
||||||
|
// Panes are separated by '|'; SavePerspective() escapes a '|' inside a caption as "\|".
|
||||||
|
const std::string prefix = "name=" + pane_name + ";";
|
||||||
|
size_t begin = 0;
|
||||||
|
for (size_t i = 0; i <= layout.size(); ++i) {
|
||||||
|
if (i < layout.size() && (layout[i] != '|' || (i > 0 && layout[i - 1] == '\\')))
|
||||||
|
continue;
|
||||||
|
if (layout.compare(begin, prefix.size(), prefix) == 0)
|
||||||
|
return layout.substr(begin, i - begin);
|
||||||
|
begin = i + 1;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
// The part a wxAuiManager layout string (wxAuiManager::SavePerspective) holds for `pane_name`, in the
|
||||||
|
// form wxAuiManager::LoadPaneInfo() takes, or empty when the layout has no such pane.
|
||||||
|
std::string aui_pane_layout_entry(const std::string& layout, const std::string& pane_name);
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
#include "DockPanel.hpp"
|
||||||
|
|
||||||
|
#include "GUI_App.hpp"
|
||||||
|
#include "Plater.hpp"
|
||||||
|
#include "Widgets/WebHosting.hpp"
|
||||||
|
|
||||||
|
#include <wx/weakref.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
std::string plugin_pane_name(const std::string& plugin_key, const std::string& title)
|
||||||
|
{
|
||||||
|
std::string name = "plugin:" + plugin_key + ":" + title;
|
||||||
|
std::replace_if(name.begin(), name.end(), [](char c) { return c == '|' || c == ';' || c == '=' || c == '\\'; }, '_');
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
DockPanel::DockPanel(wxWindow* parent,
|
||||||
|
const std::string& html,
|
||||||
|
MessageHandler on_message,
|
||||||
|
CloseHandler on_close,
|
||||||
|
CloseHandler on_destroyed)
|
||||||
|
: WebPanel(parent, web_hosting::orca_bridge_script())
|
||||||
|
, m_html(html)
|
||||||
|
, m_on_message(std::move(on_message))
|
||||||
|
, m_on_close(std::move(on_close))
|
||||||
|
, m_on_destroyed(std::move(on_destroyed))
|
||||||
|
{
|
||||||
|
// A link asking for a new window has nowhere to open from a docked panel.
|
||||||
|
browser()->Bind(wxEVT_WEBVIEW_NEWWINDOW, [](wxWebViewEvent& event) { event.Veto(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
DockPanel::~DockPanel()
|
||||||
|
{
|
||||||
|
if (m_on_destroyed)
|
||||||
|
m_on_destroyed();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DockPanel::on_page_message(const std::string& kind, const nlohmann::json& data)
|
||||||
|
{
|
||||||
|
if (kind == "message") {
|
||||||
|
if (m_on_message)
|
||||||
|
m_on_message(data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (kind == "close") {
|
||||||
|
request_close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DockPanel::push_message(const nlohmann::json& data)
|
||||||
|
{
|
||||||
|
if (!m_closing)
|
||||||
|
post_to_page(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DockPanel::fire_close()
|
||||||
|
{
|
||||||
|
if (m_closing)
|
||||||
|
return;
|
||||||
|
m_closing = true;
|
||||||
|
if (m_on_close) {
|
||||||
|
CloseHandler on_close = std::move(m_on_close);
|
||||||
|
m_on_close = nullptr;
|
||||||
|
on_close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DockPanel::request_close()
|
||||||
|
{
|
||||||
|
if (m_closing)
|
||||||
|
return;
|
||||||
|
fire_close();
|
||||||
|
// A page-requested close arrives inside the web view's script callback, so destroy later; another
|
||||||
|
// close path may have destroyed the panel by then.
|
||||||
|
wxWeakRef<DockPanel> self(this);
|
||||||
|
CallAfter([self]() {
|
||||||
|
if (self)
|
||||||
|
self->remove_pane();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void DockPanel::destroy_silently()
|
||||||
|
{
|
||||||
|
m_closing = true;
|
||||||
|
m_on_close = nullptr;
|
||||||
|
remove_pane();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DockPanel::remove_pane()
|
||||||
|
{
|
||||||
|
if (Plater* plater = wxGetApp().plater())
|
||||||
|
plater->remove_dock_pane(this);
|
||||||
|
else
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "WebPanel.hpp"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
// Stable across sessions so the saved layout finds the pane; free of wxAuiManager layout delimiters.
|
||||||
|
std::string plugin_pane_name(const std::string& plugin_key, const std::string& title);
|
||||||
|
|
||||||
|
// A WebPanel docked in the Plater, on the plugin-window bridge minus submit. It can be destroyed
|
||||||
|
// without the GIL, so its hooks must not capture pybind11 objects.
|
||||||
|
class DockPanel : public WebPanel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using MessageHandler = std::function<void(const nlohmann::json& data)>;
|
||||||
|
using CloseHandler = std::function<void()>;
|
||||||
|
|
||||||
|
// on_close fires once, on a user or page close. on_destroyed runs on every destruction and must
|
||||||
|
// touch host-side state only.
|
||||||
|
DockPanel(wxWindow* parent,
|
||||||
|
const std::string& html,
|
||||||
|
MessageHandler on_message,
|
||||||
|
CloseHandler on_close,
|
||||||
|
CloseHandler on_destroyed);
|
||||||
|
~DockPanel() override;
|
||||||
|
|
||||||
|
// Main thread only.
|
||||||
|
void push_message(const nlohmann::json& data);
|
||||||
|
// Fires on_close, then removes the pane.
|
||||||
|
void request_close();
|
||||||
|
// Removes the pane without on_close, for plugin unload. Destroys at once: unload always comes from
|
||||||
|
// the host, never from this panel's own callbacks.
|
||||||
|
void destroy_silently();
|
||||||
|
// Fires on_close at most once; also run by the pane's own close button.
|
||||||
|
void fire_close();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::optional<std::string> page_html() override { return m_html; }
|
||||||
|
bool on_page_message(const std::string& kind, const nlohmann::json& data) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void remove_pane();
|
||||||
|
|
||||||
|
std::string m_html;
|
||||||
|
bool m_closing{false};
|
||||||
|
MessageHandler m_on_message;
|
||||||
|
CloseHandler m_on_close;
|
||||||
|
CloseHandler m_on_destroyed;
|
||||||
|
};
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -85,6 +85,7 @@
|
|||||||
#include "libslic3r/Model.hpp"
|
#include "libslic3r/Model.hpp"
|
||||||
#include "libslic3r/I18N.hpp"
|
#include "libslic3r/I18N.hpp"
|
||||||
#include "libslic3r/PresetBundle.hpp"
|
#include "libslic3r/PresetBundle.hpp"
|
||||||
|
#include "libslic3r/InstanceLock.hpp"
|
||||||
#include "libslic3r/Thread.hpp"
|
#include "libslic3r/Thread.hpp"
|
||||||
#include "libslic3r/miniz_extension.hpp"
|
#include "libslic3r/miniz_extension.hpp"
|
||||||
#include "libslic3r/Utils.hpp"
|
#include "libslic3r/Utils.hpp"
|
||||||
@@ -3531,7 +3532,7 @@ bool GUI_App::on_init_inner()
|
|||||||
update_publish_status();
|
update_publish_status();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_post_initialized && app_config->dirty())
|
if (m_post_initialized && app_config->dirty() && app_config->save_due())
|
||||||
app_config->save();
|
app_config->save();
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -7623,8 +7624,11 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
|
|||||||
|
|
||||||
// Delete the bundle folder and bundle
|
// Delete the bundle folder and bundle
|
||||||
fs::path bundle_folder = fs::path(bundle.path.c_str()).parent_path();
|
fs::path bundle_folder = fs::path(bundle.path.c_str()).parent_path();
|
||||||
|
{
|
||||||
boost::system::error_code ec;
|
boost::system::error_code ec;
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
boost::filesystem::remove_all(bundle_folder, ec);
|
boost::filesystem::remove_all(bundle_folder, ec);
|
||||||
|
}
|
||||||
|
|
||||||
preset_bundle->bundles.WriteLock();
|
preset_bundle->bundles.WriteLock();
|
||||||
preset_bundle->bundles.m_bundles.erase(bundle.id);
|
preset_bundle->bundles.m_bundles.erase(bundle.id);
|
||||||
@@ -8889,6 +8893,7 @@ void GUI_App::preset_deleted_from_cloud(std::string setting_id)
|
|||||||
|
|
||||||
// Delete the .info file after cloud deletion is confirmed
|
// Delete the .info file after cloud deletion is confirmed
|
||||||
if (!preset_file_path.empty() && fs::exists(fs::path(preset_file_path))) {
|
if (!preset_file_path.empty() && fs::exists(fs::path(preset_file_path))) {
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
boost::nowide::remove(preset_file_path.c_str());
|
boost::nowide::remove(preset_file_path.c_str());
|
||||||
BOOST_LOG_TRIVIAL(info) << "Deleted .info file after cloud confirmation: " << preset_file_path;
|
BOOST_LOG_TRIVIAL(info) << "Deleted .info file after cloud confirmation: " << preset_file_path;
|
||||||
}
|
}
|
||||||
@@ -8951,17 +8956,21 @@ void GUI_App::scan_orphaned_info_files()
|
|||||||
fs::path preset_file = info_file;
|
fs::path preset_file = info_file;
|
||||||
preset_file.replace_extension(".json");
|
preset_file.replace_extension(".json");
|
||||||
|
|
||||||
// If .json doesn't exist, .info is orphaned
|
// If .json doesn't exist, .info is orphaned. Read under the lock, so a
|
||||||
if (!fs::exists(preset_file)) {
|
// remove_files() in another instance is seen whole or not at all; the
|
||||||
// Extract setting_id from .info file
|
// delete queue's own mutex is taken after the lock is released.
|
||||||
std::string setting_id = extract_setting_id_from_info(info_file.string());
|
std::string setting_id;
|
||||||
|
{
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
|
if (!fs::exists(preset_file))
|
||||||
|
setting_id = extract_setting_id_from_info(info_file.string());
|
||||||
|
}
|
||||||
if (!setting_id.empty()) {
|
if (!setting_id.empty()) {
|
||||||
// Add to need_delete_presets
|
// Add to need_delete_presets
|
||||||
delete_preset_from_cloud(setting_id, info_file.string());
|
delete_preset_from_cloud(setting_id, info_file.string());
|
||||||
BOOST_LOG_TRIVIAL(info) << "Found orphaned .info file on startup: " << info_file.string();
|
BOOST_LOG_TRIVIAL(info) << "Found orphaned .info file on startup: " << info_file.string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (ec)
|
if (ec)
|
||||||
BOOST_LOG_TRIVIAL(warning) << "scan_orphaned_info_files: failed to scan " << type_dir.string() << ": " << ec.message();
|
BOOST_LOG_TRIVIAL(warning) << "scan_orphaned_info_files: failed to scan " << type_dir.string() << ": " << ec.message();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1189,6 +1189,8 @@ void MainFrame::shutdown()
|
|||||||
if (m_project != nullptr)
|
if (m_project != nullptr)
|
||||||
m_project->shutdown();
|
m_project->shutdown();
|
||||||
m_plugin_pages.shutdown();
|
m_plugin_pages.shutdown();
|
||||||
|
if (m_plater != nullptr)
|
||||||
|
m_plater->remove_dock_panes();
|
||||||
#ifdef __WXGTK__
|
#ifdef __WXGTK__
|
||||||
// Edge panels are child windows — wxWidgets destroys them automatically.
|
// Edge panels are child windows — wxWidgets destroys them automatically.
|
||||||
m_edge_bottom = nullptr;
|
m_edge_bottom = nullptr;
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
#ifdef __WXGTK__
|
#ifdef __WXGTK__
|
||||||
#include "LinuxDisplayBackend.hpp"
|
#include "LinuxDisplayBackend.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
#include "AuiPaneLayout.hpp"
|
||||||
#include "GUI_Utils.hpp"
|
#include "GUI_Utils.hpp"
|
||||||
#include "GUI_Factories.hpp"
|
#include "GUI_Factories.hpp"
|
||||||
#include "wxExtensions.hpp"
|
#include "wxExtensions.hpp"
|
||||||
@@ -6748,6 +6749,14 @@ struct Plater::priv
|
|||||||
|
|
||||||
// GUI elements
|
// GUI elements
|
||||||
AuiMgr m_aui_mgr;
|
AuiMgr m_aui_mgr;
|
||||||
|
// Live dock panes. `on_close` runs when the user closes one from its close button; `shown` is
|
||||||
|
// what the owner asked for.
|
||||||
|
struct DockPane
|
||||||
|
{
|
||||||
|
std::function<void()> on_close;
|
||||||
|
bool shown{true};
|
||||||
|
};
|
||||||
|
std::map<wxWindow*, DockPane> m_dock_panes;
|
||||||
wxString m_default_window_layout;
|
wxString m_default_window_layout;
|
||||||
wxPanel* current_panel{ nullptr };
|
wxPanel* current_panel{ nullptr };
|
||||||
std::vector<wxPanel*> panels;
|
std::vector<wxPanel*> panels;
|
||||||
@@ -6920,6 +6929,11 @@ struct Plater::priv
|
|||||||
void update_sidebar(bool force_update = false);
|
void update_sidebar(bool force_update = false);
|
||||||
void reset_window_layout();
|
void reset_window_layout();
|
||||||
Sidebar::DockingState get_sidebar_docking_state();
|
Sidebar::DockingState get_sidebar_docking_state();
|
||||||
|
void add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||||
|
const wxSize& size, std::function<void()> on_close);
|
||||||
|
void remove_dock_pane(wxWindow* window);
|
||||||
|
void show_dock_pane(wxWindow* window, bool show);
|
||||||
|
bool dock_pane_visible(const DockPane& dock_pane, const wxAuiPaneInfo& pane) const;
|
||||||
|
|
||||||
bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); }
|
bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); }
|
||||||
|
|
||||||
@@ -7500,6 +7514,18 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
|||||||
panel_3d->SetSizer(panel_sizer);
|
panel_3d->SetSizer(panel_sizer);
|
||||||
m_aui_mgr.AddPane(panel_3d, wxAuiPaneInfo().Name("main").CenterPane().PaneBorder(false));
|
m_aui_mgr.AddPane(panel_3d, wxAuiPaneInfo().Name("main").CenterPane().PaneBorder(false));
|
||||||
|
|
||||||
|
q->Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent& evt) {
|
||||||
|
const wxAuiPaneInfo* pane = evt.GetPane();
|
||||||
|
auto it = pane != nullptr ? m_dock_panes.find(pane->window) : m_dock_panes.end();
|
||||||
|
if (it != m_dock_panes.end()) {
|
||||||
|
const std::function<void()> on_close = std::move(it->second.on_close);
|
||||||
|
m_dock_panes.erase(it);
|
||||||
|
if (on_close)
|
||||||
|
on_close();
|
||||||
|
}
|
||||||
|
evt.Skip();
|
||||||
|
});
|
||||||
|
|
||||||
m_default_window_layout = m_aui_mgr.SavePerspective();
|
m_default_window_layout = m_aui_mgr.SavePerspective();
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -8165,6 +8191,14 @@ void Plater::priv::update_sidebar(bool force_update) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const auto& [window, dock_pane] : m_dock_panes) {
|
||||||
|
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
|
||||||
|
if (pane.IsOk() && pane.IsShown() != dock_pane_visible(dock_pane, pane)) {
|
||||||
|
pane.Show(!pane.IsShown());
|
||||||
|
needs_update = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (needs_update) {
|
if (needs_update) {
|
||||||
notification_manager->set_sidebar_collapsed(sidebar.IsShown());
|
notification_manager->set_sidebar_collapsed(sidebar.IsShown());
|
||||||
m_aui_mgr.Update();
|
m_aui_mgr.Update();
|
||||||
@@ -8174,10 +8208,96 @@ void Plater::priv::update_sidebar(bool force_update) {
|
|||||||
void Plater::priv::reset_window_layout()
|
void Plater::priv::reset_window_layout()
|
||||||
{
|
{
|
||||||
m_aui_mgr.LoadPerspective(m_default_window_layout, false);
|
m_aui_mgr.LoadPerspective(m_default_window_layout, false);
|
||||||
|
// Loading a layout docks and hides every pane it does not list, and the default layout lists no
|
||||||
|
// dock panes: a floating dock pane is docked again, like the rest of the window.
|
||||||
|
for (const auto& [window, dock_pane] : m_dock_panes)
|
||||||
|
if (wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window); pane.IsOk())
|
||||||
|
pane.Show(dock_pane_visible(dock_pane, pane));
|
||||||
sidebar_layout.is_collapsed = false;
|
sidebar_layout.is_collapsed = false;
|
||||||
update_sidebar(true);
|
update_sidebar(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Plater::priv::dock_pane_visible(const DockPane& dock_pane, const wxAuiPaneInfo& pane) const
|
||||||
|
{
|
||||||
|
// A floating pane is a top-level window, so it does not hide with the Plater on other tabs.
|
||||||
|
return dock_pane.shown && (!pane.IsFloating() || sidebar_layout.show);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plater::priv::add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||||
|
const wxSize& size, std::function<void()> on_close)
|
||||||
|
{
|
||||||
|
const wxString base_name = wxString::FromUTF8(name);
|
||||||
|
wxString unique_name = base_name;
|
||||||
|
for (int i = 2; m_aui_mgr.GetPane(unique_name).IsOk(); ++i)
|
||||||
|
unique_name = base_name + wxString::Format("#%d", i);
|
||||||
|
|
||||||
|
// A restored layout below already holds pixels.
|
||||||
|
const wxSize pixels = q->FromDIP(size);
|
||||||
|
wxAuiPaneInfo info;
|
||||||
|
info.Name(unique_name).Caption(caption).BestSize(pixels).FloatingSize(pixels).DestroyOnClose(true);
|
||||||
|
if (dock == "left")
|
||||||
|
info.Left();
|
||||||
|
else if (dock == "bottom")
|
||||||
|
info.Bottom();
|
||||||
|
else
|
||||||
|
info.Right();
|
||||||
|
if (dock == "float")
|
||||||
|
info.Float();
|
||||||
|
|
||||||
|
// Put the pane back where it was the last time the window layout was saved with it open.
|
||||||
|
const std::string saved = aui_pane_layout_entry(wxGetApp().app_config->get("window_layout"), unique_name.utf8_string());
|
||||||
|
if (!saved.empty()) {
|
||||||
|
m_aui_mgr.LoadPaneInfo(wxString::FromUTF8(saved), info);
|
||||||
|
info.Caption(caption).DestroyOnClose(true).Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Floating is disabled on Wayland.
|
||||||
|
if ((m_aui_mgr.GetFlags() & wxAUI_MGR_ALLOW_FLOATING) == 0) {
|
||||||
|
info.Dock().Floatable(false);
|
||||||
|
if (info.dock_direction == wxAUI_DOCK_NONE)
|
||||||
|
info.Right();
|
||||||
|
}
|
||||||
|
|
||||||
|
const DockPane& dock_pane = m_dock_panes[window] = DockPane{std::move(on_close)};
|
||||||
|
info.Show(dock_pane_visible(dock_pane, info));
|
||||||
|
m_aui_mgr.AddPane(window, info);
|
||||||
|
|
||||||
|
// wxAUI does not record a dragged sash in best_size, so track the docked size like the sidebar
|
||||||
|
// does, for the saved layout.
|
||||||
|
window->Bind(wxEVT_IDLE, [this, window](wxIdleEvent& evt) {
|
||||||
|
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
|
||||||
|
if (pane.IsOk() && pane.IsShown() && pane.IsDocked() && pane.rect.GetWidth() > 0 && pane.rect.GetHeight() > 0) {
|
||||||
|
const bool horizontal = pane.dock_direction == wxAUI_DOCK_TOP || pane.dock_direction == wxAUI_DOCK_BOTTOM;
|
||||||
|
pane.BestSize(horizontal ? pane.best_size.GetWidth() : pane.rect.GetWidth(),
|
||||||
|
horizontal ? pane.rect.GetHeight() : pane.best_size.GetHeight());
|
||||||
|
}
|
||||||
|
evt.Skip();
|
||||||
|
});
|
||||||
|
|
||||||
|
m_aui_mgr.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plater::priv::remove_dock_pane(wxWindow* window)
|
||||||
|
{
|
||||||
|
m_dock_panes.erase(window);
|
||||||
|
if (m_aui_mgr.DetachPane(window))
|
||||||
|
m_aui_mgr.Update();
|
||||||
|
window->Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plater::priv::show_dock_pane(wxWindow* window, bool show)
|
||||||
|
{
|
||||||
|
const auto it = m_dock_panes.find(window);
|
||||||
|
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
|
||||||
|
if (it == m_dock_panes.end() || !pane.IsOk())
|
||||||
|
return;
|
||||||
|
it->second.shown = show;
|
||||||
|
if (pane.IsShown() == dock_pane_visible(it->second, pane))
|
||||||
|
return;
|
||||||
|
pane.Show(!pane.IsShown());
|
||||||
|
m_aui_mgr.Update();
|
||||||
|
}
|
||||||
|
|
||||||
Sidebar::DockingState Plater::priv::get_sidebar_docking_state() {
|
Sidebar::DockingState Plater::priv::get_sidebar_docking_state() {
|
||||||
if (!sidebar_layout.is_enabled) {
|
if (!sidebar_layout.is_enabled) {
|
||||||
return Sidebar::None;
|
return Sidebar::None;
|
||||||
@@ -17772,6 +17892,19 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_
|
|||||||
|
|
||||||
void Plater::reset_window_layout() { p->reset_window_layout(); }
|
void Plater::reset_window_layout() { p->reset_window_layout(); }
|
||||||
|
|
||||||
|
void Plater::add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||||
|
const wxSize& size, std::function<void()> on_close)
|
||||||
|
{
|
||||||
|
p->add_dock_pane(window, name, caption, dock, size, std::move(on_close));
|
||||||
|
}
|
||||||
|
void Plater::remove_dock_pane(wxWindow* window) { p->remove_dock_pane(window); }
|
||||||
|
void Plater::remove_dock_panes()
|
||||||
|
{
|
||||||
|
while (!p->m_dock_panes.empty())
|
||||||
|
p->remove_dock_pane(p->m_dock_panes.begin()->first);
|
||||||
|
}
|
||||||
|
void Plater::show_dock_pane(wxWindow* window, bool show) { p->show_dock_pane(window, show); }
|
||||||
|
|
||||||
//BBS
|
//BBS
|
||||||
void Plater::select_curr_plate_all() { p->select_curr_plate_all(); }
|
void Plater::select_curr_plate_all() { p->select_curr_plate_all(); }
|
||||||
void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); }
|
void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); }
|
||||||
|
|||||||
@@ -476,6 +476,17 @@ public:
|
|||||||
|
|
||||||
void reset_window_layout();
|
void reset_window_layout();
|
||||||
|
|
||||||
|
// Dock panes sit alongside the sidebar; `window` must be a child of the Plater. `dock` is
|
||||||
|
// "left", "right", "bottom" or "float", and `size` is in DIPs. A pane closed from its own close
|
||||||
|
// button is destroyed after on_close runs; remove_dock_pane() destroys it without calling on_close.
|
||||||
|
void add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||||
|
const wxSize& size, std::function<void()> on_close);
|
||||||
|
void remove_dock_pane(wxWindow* window);
|
||||||
|
void show_dock_pane(wxWindow* window, bool show);
|
||||||
|
// Removes every dock pane without calling on_close, for MainFrame::shutdown() (app exit and a
|
||||||
|
// language switch), while the Plater and any floating frames still exist.
|
||||||
|
void remove_dock_panes();
|
||||||
|
|
||||||
// Called after the Preferences dialog is closed and the program settings are saved.
|
// Called after the Preferences dialog is closed and the program settings are saved.
|
||||||
// Update the UI based on the current preferences.
|
// Update the UI based on the current preferences.
|
||||||
void update_ui_from_settings();
|
void update_ui_from_settings();
|
||||||
|
|||||||
@@ -1,72 +1,15 @@
|
|||||||
#include "PluginWebDialog.hpp"
|
#include "WebDialog.hpp"
|
||||||
|
|
||||||
#include "slic3r/GUI/GUI.hpp"
|
|
||||||
#include "slic3r/GUI/GUI_App.hpp"
|
#include "slic3r/GUI/GUI_App.hpp"
|
||||||
|
#include "slic3r/GUI/Widgets/WebHosting.hpp"
|
||||||
#include <libslic3r/Utils.hpp>
|
|
||||||
|
|
||||||
#include <boost/filesystem.hpp>
|
|
||||||
|
|
||||||
#include <wx/event.h>
|
#include <wx/event.h>
|
||||||
#include <wx/uri.h>
|
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
namespace {
|
WebDialog::WebDialog(wxWindow* parent,
|
||||||
|
|
||||||
// Injected into the top-level page at document start (before the plugin's own
|
|
||||||
// scripts). Defines window.orca as the only host surface the page may use. It
|
|
||||||
// references window.wx lazily (at call time) so it never races the backend's
|
|
||||||
// deferred registration of the "wx" message handler. Guarded against
|
|
||||||
// double-injection so it is harmless if also prepended.
|
|
||||||
constexpr char ORCA_BRIDGE_JS[] = R"JS(
|
|
||||||
(function () {
|
|
||||||
if (window.top !== window.self) return;
|
|
||||||
if (window.orca) return;
|
|
||||||
var handlers = [];
|
|
||||||
function send(kind, data) {
|
|
||||||
try {
|
|
||||||
window.wx.postMessage(JSON.stringify({
|
|
||||||
channel: 'orca', kind: kind, data: (data === undefined ? null : data)
|
|
||||||
}));
|
|
||||||
} catch (e) { /* bridge not ready yet */ }
|
|
||||||
}
|
|
||||||
window.orca = {
|
|
||||||
postMessage: function (d) { send('message', d); },
|
|
||||||
submit: function (d) { send('submit', d); },
|
|
||||||
close: function () { send('close'); },
|
|
||||||
onMessage: function (cb) { if (typeof cb === 'function') handlers.push(cb); }
|
|
||||||
};
|
|
||||||
window.__orcaDispatch = function (payload) {
|
|
||||||
var data = payload ? payload.data : null;
|
|
||||||
for (var i = 0; i < handlers.length; i++) {
|
|
||||||
try { handlers[i](data); } catch (e) {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
)JS";
|
|
||||||
|
|
||||||
// file:// base URL for plugin HTML loaded via SetPage, so self-referencing
|
|
||||||
// relative URLs resolve against the bundled web resources directory.
|
|
||||||
wxString web_base_url()
|
|
||||||
{
|
|
||||||
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
|
|
||||||
return wxString("file://") + from_u8(dir) + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whether a loaded document is the plugin HTML's own base URL. The web view reports the URL it
|
|
||||||
// parsed, so any fragment the page navigated to is ignored and the escaping it applies to what the
|
|
||||||
// resources path holds (a space, a non-ASCII character) is undone first.
|
|
||||||
bool is_content_url(const wxString& url)
|
|
||||||
{
|
|
||||||
return wxURI::Unescape(url.BeforeFirst('#')) == web_base_url();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
|
||||||
const wxString& title,
|
const wxString& title,
|
||||||
const std::string& html,
|
const std::string& html,
|
||||||
const wxSize& size,
|
const wxSize& size,
|
||||||
@@ -84,7 +27,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
|||||||
{
|
{
|
||||||
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
||||||
// is swapped in via SetPage once the bootstrap finishes loading.
|
// is swapped in via SetPage once the bootstrap finishes loading.
|
||||||
create_webview("web/dialog/PluginWebDialog/blank.html", title, size, wxSize(320, 240));
|
create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240));
|
||||||
|
|
||||||
// Paint the window/webview in the themed background so there is no white
|
// Paint the window/webview in the themed background so there is no white
|
||||||
// flash before the (transparent) bootstrap page and plugin HTML render.
|
// flash before the (transparent) bootstrap page and plugin HTML render.
|
||||||
@@ -96,22 +39,22 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
|||||||
// create_webview() via add_user_scripts(); nothing to add here.
|
// 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
|
// 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.
|
// missing/blocked bootstrap resource (e.g. a packaged build) still triggers it.
|
||||||
Bind(wxEVT_WEBVIEW_LOADED, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
|
Bind(wxEVT_WEBVIEW_LOADED, &WebDialog::on_bootstrap_event, this, wv->GetId());
|
||||||
Bind(wxEVT_WEBVIEW_ERROR, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
|
Bind(wxEVT_WEBVIEW_ERROR, &WebDialog::on_bootstrap_event, this, wv->GetId());
|
||||||
Bind(wxEVT_WEBVIEW_NAVIGATED, &PluginWebDialog::on_navigated, this, wv->GetId());
|
Bind(wxEVT_WEBVIEW_NAVIGATED, &WebDialog::on_navigated, this, wv->GetId());
|
||||||
}
|
}
|
||||||
Bind(wxEVT_CLOSE_WINDOW, &PluginWebDialog::on_close_window, this);
|
Bind(wxEVT_CLOSE_WINDOW, &WebDialog::on_close_window, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::add_user_scripts()
|
void WebDialog::add_user_scripts()
|
||||||
{
|
{
|
||||||
if (wxWebView* wv = browser()) {
|
if (wxWebView* wv = browser()) {
|
||||||
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
|
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
|
||||||
wv->AddUserScript(ORCA_BRIDGE_JS);
|
wv->AddUserScript(wxString::FromUTF8(web_hosting::orca_bridge_script()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginWebDialog::~PluginWebDialog()
|
WebDialog::~WebDialog()
|
||||||
{
|
{
|
||||||
// Runs on every destruction path. Deliberately NOT a wxEVT_DESTROY handler:
|
// Runs on every destruction path. Deliberately NOT a wxEVT_DESTROY handler:
|
||||||
// that event is sent from the base ~wxDialog(), after this subclass's members
|
// that event is sent from the base ~wxDialog(), after this subclass's members
|
||||||
@@ -121,19 +64,19 @@ PluginWebDialog::~PluginWebDialog()
|
|||||||
m_on_destroyed();
|
m_on_destroyed();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::post_message(PluginWebDialog* dialog, const nlohmann::json& data)
|
void WebDialog::post_message(WebDialog* dialog, const nlohmann::json& data)
|
||||||
{
|
{
|
||||||
if (dialog != nullptr && dialog->is_open())
|
if (dialog != nullptr && dialog->is_open())
|
||||||
dialog->push_message(data);
|
dialog->push_message(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::request_close(PluginWebDialog* dialog)
|
void WebDialog::request_close(WebDialog* dialog)
|
||||||
{
|
{
|
||||||
if (dialog != nullptr)
|
if (dialog != nullptr)
|
||||||
dialog->Close();
|
dialog->Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::destroy_for_plugin(PluginWebDialog* dialog)
|
void WebDialog::destroy_silently(WebDialog* dialog)
|
||||||
{
|
{
|
||||||
if (dialog == nullptr)
|
if (dialog == nullptr)
|
||||||
return;
|
return;
|
||||||
@@ -147,42 +90,42 @@ void PluginWebDialog::destroy_for_plugin(PluginWebDialog* dialog)
|
|||||||
dialog->Destroy();
|
dialog->Destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::on_bootstrap_event(wxWebViewEvent& event)
|
void WebDialog::on_bootstrap_event(wxWebViewEvent& event)
|
||||||
{
|
{
|
||||||
const bool loaded = event.GetEventType() == wxEVT_WEBVIEW_LOADED;
|
const bool loaded = event.GetEventType() == wxEVT_WEBVIEW_LOADED;
|
||||||
// The first bootstrap load (or its error) triggers the swap to plugin HTML.
|
// The first bootstrap load (or its error) triggers the swap to plugin HTML.
|
||||||
if (!m_content_loaded)
|
if (!m_content_loaded)
|
||||||
load_plugin_content();
|
load_page_html();
|
||||||
// WebKit reloads the SetPage base URL, so a committed load of it that we did not start is a reload.
|
// WebKit reloads the SetPage base URL, so a committed load of it that we did not start is a reload.
|
||||||
// A failed navigation is reported against the page that stayed but never commits. Edge ignores the
|
// A failed navigation is reported against the page that stayed but never commits. Edge ignores the
|
||||||
// base URL and restores SetPage content itself, so nothing matches there.
|
// base URL and restores SetPage content itself, so nothing matches there.
|
||||||
else if (is_content_url(event.GetURL())) {
|
else if (web_hosting::is_content_url(event.GetURL())) {
|
||||||
if (m_own_page_load)
|
if (m_own_page_load)
|
||||||
m_own_page_load = false;
|
m_own_page_load = false;
|
||||||
else if (loaded && m_content_navigated)
|
else if (loaded && m_content_navigated)
|
||||||
load_plugin_content();
|
load_page_html();
|
||||||
}
|
}
|
||||||
if (loaded)
|
if (loaded)
|
||||||
m_content_navigated = false;
|
m_content_navigated = false;
|
||||||
event.Skip();
|
event.Skip();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::on_navigated(wxWebViewEvent& event)
|
void WebDialog::on_navigated(wxWebViewEvent& event)
|
||||||
{
|
{
|
||||||
m_content_navigated = is_content_url(event.GetURL());
|
m_content_navigated = web_hosting::is_content_url(event.GetURL());
|
||||||
event.Skip();
|
event.Skip();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::load_plugin_content()
|
void WebDialog::load_page_html()
|
||||||
{
|
{
|
||||||
m_content_loaded = true;
|
m_content_loaded = true;
|
||||||
if (wxWebView* wv = browser()) {
|
if (wxWebView* wv = browser()) {
|
||||||
m_own_page_load = true;
|
m_own_page_load = true;
|
||||||
wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
|
wv->SetPage(wxString::FromUTF8(m_html), web_hosting::content_base_url());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::on_script_message(const nlohmann::json& payload)
|
void WebDialog::on_script_message(const nlohmann::json& payload)
|
||||||
{
|
{
|
||||||
if (payload.value("channel", std::string()) == "orca") {
|
if (payload.value("channel", std::string()) == "orca") {
|
||||||
const std::string kind = payload.value("kind", std::string());
|
const std::string kind = payload.value("kind", std::string());
|
||||||
@@ -202,7 +145,7 @@ void PluginWebDialog::on_script_message(const nlohmann::json& payload)
|
|||||||
handle_common_script_command(payload);
|
handle_common_script_command(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::push_message(const nlohmann::json& data)
|
void WebDialog::push_message(const nlohmann::json& data)
|
||||||
{
|
{
|
||||||
if (!m_open)
|
if (!m_open)
|
||||||
return;
|
return;
|
||||||
@@ -211,7 +154,7 @@ void PluginWebDialog::push_message(const nlohmann::json& data)
|
|||||||
call_web_handler(envelope, wxT("__orcaDispatch"));
|
call_web_handler(envelope, wxT("__orcaDispatch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::finish(bool submitted, const nlohmann::json& data)
|
void WebDialog::finish(bool submitted, const nlohmann::json& data)
|
||||||
{
|
{
|
||||||
if (!m_open)
|
if (!m_open)
|
||||||
return;
|
return;
|
||||||
@@ -230,7 +173,7 @@ void PluginWebDialog::finish(bool submitted, const nlohmann::json& data)
|
|||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::on_close_window(wxCloseEvent&)
|
void WebDialog::on_close_window(wxCloseEvent&)
|
||||||
{
|
{
|
||||||
if (!m_open) {
|
if (!m_open) {
|
||||||
// finish() already dispatched submit/close and requested the close.
|
// finish() already dispatched submit/close and requested the close.
|
||||||
@@ -250,7 +193,7 @@ void PluginWebDialog::on_close_window(wxCloseEvent&)
|
|||||||
Destroy();
|
Destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::fire_submit(const nlohmann::json& data)
|
void WebDialog::fire_submit(const nlohmann::json& data)
|
||||||
{
|
{
|
||||||
if (m_on_submit) {
|
if (m_on_submit) {
|
||||||
SubmitHandler cb = std::move(m_on_submit);
|
SubmitHandler cb = std::move(m_on_submit);
|
||||||
@@ -258,7 +201,7 @@ void PluginWebDialog::fire_submit(const nlohmann::json& data)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::fire_close()
|
void WebDialog::fire_close()
|
||||||
{
|
{
|
||||||
if (m_close_fired)
|
if (m_close_fired)
|
||||||
return;
|
return;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#ifndef slic3r_GUI_PluginWebDialog_hpp_
|
#ifndef slic3r_GUI_WebDialog_hpp_
|
||||||
#define slic3r_GUI_PluginWebDialog_hpp_
|
#define slic3r_GUI_WebDialog_hpp_
|
||||||
|
|
||||||
#include "Widgets/WebViewHostDialog.hpp"
|
#include "Widgets/WebViewHostDialog.hpp"
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ namespace Slic3r { namespace GUI {
|
|||||||
// GIL held; the plugin layer wraps any Python callables in a GIL-safe holder.
|
// GIL held; the plugin layer wraps any Python callables in a GIL-safe holder.
|
||||||
//
|
//
|
||||||
// Usable both modally (ShowModal -> read result()) and modelessly (Show()).
|
// Usable both modally (ShowModal -> read result()) and modelessly (Show()).
|
||||||
class PluginWebDialog : public Slic3r::GUI::WebViewHostDialog
|
class WebDialog : public Slic3r::GUI::WebViewHostDialog
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
using MessageHandler = std::function<void(const nlohmann::json& data)>;
|
using MessageHandler = std::function<void(const nlohmann::json& data)>;
|
||||||
@@ -32,7 +32,7 @@ public:
|
|||||||
// user/JS-initiated close (while the window is alive). on_destroyed runs from
|
// user/JS-initiated close (while the window is alive). on_destroyed runs from
|
||||||
// the destructor on every path and must touch host-side state only (no Python
|
// the destructor on every path and must touch host-side state only (no Python
|
||||||
// / no derived members).
|
// / no derived members).
|
||||||
PluginWebDialog(wxWindow* parent,
|
WebDialog(wxWindow* parent,
|
||||||
const wxString& title,
|
const wxString& title,
|
||||||
const std::string& html,
|
const std::string& html,
|
||||||
const wxSize& size,
|
const wxSize& size,
|
||||||
@@ -41,11 +41,11 @@ public:
|
|||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed,
|
CloseHandler on_destroyed,
|
||||||
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
|
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
|
||||||
~PluginWebDialog() override;
|
~WebDialog() override;
|
||||||
|
|
||||||
static void post_message(PluginWebDialog* dialog, const nlohmann::json& data);
|
static void post_message(WebDialog* dialog, const nlohmann::json& data);
|
||||||
static void request_close(PluginWebDialog* dialog);
|
static void request_close(WebDialog* dialog);
|
||||||
static void destroy_for_plugin(PluginWebDialog* dialog);
|
static void destroy_silently(WebDialog* dialog);
|
||||||
|
|
||||||
// Push a payload to the page; delivered to handlers registered via
|
// Push a payload to the page; delivered to handlers registered via
|
||||||
// window.orca.onMessage(). MAIN-THREAD ONLY (the plugin layer marshals).
|
// window.orca.onMessage(). MAIN-THREAD ONLY (the plugin layer marshals).
|
||||||
@@ -65,7 +65,7 @@ protected:
|
|||||||
private:
|
private:
|
||||||
void on_bootstrap_event(wxWebViewEvent& event);
|
void on_bootstrap_event(wxWebViewEvent& event);
|
||||||
void on_navigated(wxWebViewEvent& event);
|
void on_navigated(wxWebViewEvent& event);
|
||||||
void load_plugin_content();
|
void load_page_html();
|
||||||
void on_close_window(wxCloseEvent& event);
|
void on_close_window(wxCloseEvent& event);
|
||||||
void fire_submit(const nlohmann::json& data);
|
void fire_submit(const nlohmann::json& data);
|
||||||
void fire_close();
|
void fire_close();
|
||||||
@@ -86,4 +86,4 @@ private:
|
|||||||
|
|
||||||
}} // namespace Slic3r::GUI
|
}} // namespace Slic3r::GUI
|
||||||
|
|
||||||
#endif // slic3r_GUI_PluginWebDialog_hpp_
|
#endif // slic3r_GUI_WebDialog_hpp_
|
||||||
@@ -1481,9 +1481,7 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Written through a temp file and moved into place, as the preset caches
|
// Written through a temp file and moved into place, as the preset caches
|
||||||
// are: half a cache must never be readable, and the PID suffix keeps two
|
// are: half a cache must never be readable.
|
||||||
// instances from interleaving on one temp file.
|
|
||||||
const std::string tmp_path = cache_file.string() + "." + std::to_string(get_current_pid()) + ".tmp";
|
|
||||||
try {
|
try {
|
||||||
json out;
|
json out;
|
||||||
out["format"] = 1;
|
out["format"] = 1;
|
||||||
@@ -1492,18 +1490,9 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
|||||||
for (const char* key : { "model", "machine", "filament", "process" })
|
for (const char* key : { "model", "machine", "filament", "process" })
|
||||||
profile[key] = m_ProfileJson[key];
|
profile[key] = m_ProfileJson[key];
|
||||||
boost::filesystem::create_directories(cache_file.parent_path());
|
boost::filesystem::create_directories(cache_file.parent_path());
|
||||||
{
|
if (const std::error_code ec = write_file_atomically(cache_file.string(), out.dump(-1, ' ', false, json::error_handler_t::ignore), /*binary=*/true))
|
||||||
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
|
|
||||||
ofs << out.dump(-1, ' ', false, json::error_handler_t::ignore);
|
|
||||||
ofs.close();
|
|
||||||
if (! ofs.good())
|
|
||||||
throw std::runtime_error("write failed");
|
|
||||||
}
|
|
||||||
if (const std::error_code ec = rename_file(tmp_path, cache_file.string()))
|
|
||||||
throw std::runtime_error(ec.message());
|
throw std::runtime_error(ec.message());
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
boost::system::error_code rm;
|
|
||||||
boost::filesystem::remove(tmp_path, rm);
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: could not write the profile data cache: " << e.what();
|
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: could not write the profile data cache: " << e.what();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#include "WebPanel.hpp"
|
||||||
|
|
||||||
|
#include "GUI_App.hpp"
|
||||||
|
#include "Widgets/WebHosting.hpp"
|
||||||
|
#include "Widgets/WebView.hpp"
|
||||||
|
#include "Widgets/WebViewHostDialog.hpp"
|
||||||
|
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
|
#include <wx/sizer.h>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
WebPanel::WebPanel(wxWindow* parent, const char* bridge_script)
|
||||||
|
: wxPanel(parent, wxID_ANY)
|
||||||
|
{
|
||||||
|
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||||
|
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
SetSizer(sizer);
|
||||||
|
|
||||||
|
// Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available.
|
||||||
|
m_browser = WebView::CreateWebView(this, web_hosting::bootstrap_url());
|
||||||
|
m_browser->SetBackgroundColour(GetBackgroundColour());
|
||||||
|
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
|
||||||
|
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
|
||||||
|
m_browser->AddUserScript(wxString::FromUTF8(bridge_script));
|
||||||
|
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &WebPanel::on_load_event, this);
|
||||||
|
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &WebPanel::on_load_event, this);
|
||||||
|
m_browser->Bind(wxEVT_WEBVIEW_NAVIGATED, &WebPanel::on_navigated, this);
|
||||||
|
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebPanel::on_script_message, this);
|
||||||
|
m_browser->Bind(EVT_WEBVIEW_RECREATED, &WebPanel::on_webview_recreated, this);
|
||||||
|
sizer->Add(m_browser, 1, wxEXPAND);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebPanel::on_load_event(wxWebViewEvent& event)
|
||||||
|
{
|
||||||
|
const bool loaded = event.GetEventType() == wxEVT_WEBVIEW_LOADED;
|
||||||
|
if (!m_content_loaded) {
|
||||||
|
// The first bootstrap load (or its error) triggers the swap to the plugin HTML.
|
||||||
|
m_content_loaded = true;
|
||||||
|
load_page_html();
|
||||||
|
} else if (!web_hosting::is_content_url(event.GetURL())) {
|
||||||
|
// Not our document (a linked page, a substituted error page), or any document on Edge, which ignores
|
||||||
|
// the base URL and restores SetPage content on a reload itself; either way it takes the app theme.
|
||||||
|
if (loaded)
|
||||||
|
apply_theme();
|
||||||
|
} else if (m_own_page_load) {
|
||||||
|
m_own_page_load = false;
|
||||||
|
// The document-start theme script is fixed at creation, so re-apply the app theme.
|
||||||
|
if (loaded)
|
||||||
|
apply_theme();
|
||||||
|
} else if (loaded && m_content_navigated) {
|
||||||
|
// WebKit reloads the SetPage base URL, so a committed load of it that we did not start is a
|
||||||
|
// reload. A failed navigation is reported against the page that stayed but never commits.
|
||||||
|
load_page_html();
|
||||||
|
}
|
||||||
|
if (loaded)
|
||||||
|
m_content_navigated = false;
|
||||||
|
event.Skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebPanel::on_navigated(wxWebViewEvent& event)
|
||||||
|
{
|
||||||
|
m_content_navigated = web_hosting::is_content_url(event.GetURL());
|
||||||
|
event.Skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebPanel::load_page_html()
|
||||||
|
{
|
||||||
|
if (const std::optional<std::string> html = page_html()) {
|
||||||
|
m_own_page_load = true;
|
||||||
|
m_browser->SetPage(wxString::FromUTF8(*html), web_hosting::content_base_url());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebPanel::on_script_message(wxWebViewEvent& event)
|
||||||
|
{
|
||||||
|
const nlohmann::json payload = nlohmann::json::parse(event.GetString().utf8_string(), nullptr, false);
|
||||||
|
if (!payload.is_object() || payload.value("channel", std::string()) != "orca")
|
||||||
|
return;
|
||||||
|
|
||||||
|
const std::string kind = payload.value("kind", std::string());
|
||||||
|
if (!on_page_message(kind, payload.contains("data") ? payload["data"] : nlohmann::json()))
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "WebPanel ignored a window.orca '" << kind << "' call; this host does not support it";
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebPanel::on_webview_recreated(wxCommandEvent&)
|
||||||
|
{
|
||||||
|
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||||
|
m_browser->SetBackgroundColour(GetBackgroundColour());
|
||||||
|
Refresh();
|
||||||
|
// Handled without Skip(), so WebView::RecreateAll() does not reload the plugin page.
|
||||||
|
apply_theme();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebPanel::apply_theme()
|
||||||
|
{
|
||||||
|
WebView::RunScript(m_browser, wxString::FromUTF8(WebViewHostDialog::theme_apply_script()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebPanel::post_to_page(const std::string& json)
|
||||||
|
{
|
||||||
|
WebView::RunScript(m_browser, wxString::Format(
|
||||||
|
"(function dispatch(payload, attempts) {\n"
|
||||||
|
" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n"
|
||||||
|
" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n"
|
||||||
|
"})({data: %s}, 0);",
|
||||||
|
wxString::FromUTF8(json)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <wx/panel.h>
|
||||||
|
#include <wx/webview.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
// Host-supplied HTML in a web view panel, for any window that embeds or derives from it (today plugin
|
||||||
|
// Pages tabs and docked panels): bootstrap page and swap, theme and bridge scripts, live re-theming, and
|
||||||
|
// window.orca messages routed to on_page_message().
|
||||||
|
class WebPanel : public wxPanel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
WebPanel(wxWindow* parent, const char* bridge_script);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
wxWebView* browser() const { return m_browser; }
|
||||||
|
|
||||||
|
// Delivers an already serialised JSON value to the page's window.orca.onMessage handlers,
|
||||||
|
// waiting briefly for the bridge while the page is still loading. Main thread only.
|
||||||
|
void post_to_page(const std::string& json);
|
||||||
|
|
||||||
|
// The plugin HTML to show once the bootstrap page has loaded, and again when WebKit reloads it;
|
||||||
|
// std::nullopt leaves it blank.
|
||||||
|
virtual std::optional<std::string> page_html() = 0;
|
||||||
|
// A window.orca message from the page; false for a kind this host does not handle (logged).
|
||||||
|
virtual bool on_page_message(const std::string& kind, const nlohmann::json& data) = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void on_load_event(wxWebViewEvent& event);
|
||||||
|
void on_navigated(wxWebViewEvent& event);
|
||||||
|
void on_script_message(wxWebViewEvent& event);
|
||||||
|
void on_webview_recreated(wxCommandEvent& event);
|
||||||
|
void apply_theme();
|
||||||
|
void load_page_html();
|
||||||
|
|
||||||
|
wxWebView* m_browser{nullptr};
|
||||||
|
bool m_content_loaded{false};
|
||||||
|
bool m_own_page_load{false}; // a SetPage of the plugin HTML is in flight
|
||||||
|
bool m_content_navigated{false}; // a navigation to the base URL has committed
|
||||||
|
};
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#include "WebHosting.hpp"
|
||||||
|
|
||||||
|
#include "slic3r/GUI/GUI.hpp"
|
||||||
|
|
||||||
|
#include <libslic3r/Utils.hpp>
|
||||||
|
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
|
||||||
|
#include <wx/uri.h>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI { namespace web_hosting {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Injected into the top-level page at document start (before the plugin's own
|
||||||
|
// scripts). Defines window.orca as the only host surface the page may use. It
|
||||||
|
// references window.wx lazily (at call time) so it never races the backend's
|
||||||
|
// deferred registration of the "wx" message handler. Guarded against
|
||||||
|
// double-injection so it is harmless if also prepended.
|
||||||
|
constexpr char ORCA_BRIDGE_JS[] = R"JS(
|
||||||
|
(function () {
|
||||||
|
if (window.top !== window.self) return;
|
||||||
|
if (window.orca) return;
|
||||||
|
var handlers = [];
|
||||||
|
function send(kind, data) {
|
||||||
|
try {
|
||||||
|
window.wx.postMessage(JSON.stringify({
|
||||||
|
channel: 'orca', kind: kind, data: (data === undefined ? null : data)
|
||||||
|
}));
|
||||||
|
} catch (e) { /* bridge not ready yet */ }
|
||||||
|
}
|
||||||
|
window.orca = {
|
||||||
|
postMessage: function (d) { send('message', d); },
|
||||||
|
submit: function (d) { send('submit', d); },
|
||||||
|
close: function () { send('close'); },
|
||||||
|
onMessage: function (cb) { if (typeof cb === 'function') handlers.push(cb); }
|
||||||
|
};
|
||||||
|
window.__orcaDispatch = function (payload) {
|
||||||
|
var data = payload ? payload.data : null;
|
||||||
|
for (var i = 0; i < handlers.length; i++) {
|
||||||
|
try { handlers[i](data); } catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
)JS";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
wxString bootstrap_url()
|
||||||
|
{
|
||||||
|
return wxString("file://") + from_u8((boost::filesystem::path(resources_dir()) / BOOTSTRAP_PAGE).make_preferred().string());
|
||||||
|
}
|
||||||
|
|
||||||
|
wxString content_base_url()
|
||||||
|
{
|
||||||
|
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
|
||||||
|
return wxString("file://") + from_u8(dir) + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_content_url(const wxString& url)
|
||||||
|
{
|
||||||
|
// The web view reports the URL it parsed, which escapes anything the resources path holds
|
||||||
|
// (a space, a non-ASCII character), while content_base_url() is the raw path.
|
||||||
|
return wxURI::Unescape(url.BeforeFirst('#')) == content_base_url();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* orca_bridge_script() { return ORCA_BRIDGE_JS; }
|
||||||
|
|
||||||
|
}}} // namespace Slic3r::GUI::web_hosting
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <wx/string.h>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI { namespace web_hosting {
|
||||||
|
|
||||||
|
// Shared by the hosts that show plugin HTML: WebDialog and WebPanel.
|
||||||
|
|
||||||
|
// The bundled blank page a plugin web view loads before the plugin HTML is swapped in.
|
||||||
|
constexpr const char* BOOTSTRAP_PAGE = "web/dialog/WebDialog/blank.html";
|
||||||
|
|
||||||
|
// The file:// URL of BOOTSTRAP_PAGE.
|
||||||
|
wxString bootstrap_url();
|
||||||
|
|
||||||
|
// The file:// base URL plugin HTML is loaded against, so relative URLs resolve to bundled resources.
|
||||||
|
wxString content_base_url();
|
||||||
|
|
||||||
|
// Whether `url` is the plugin HTML's base URL, ignoring any fragment. WebKit reports it for the
|
||||||
|
// injected page, a reload and a failed navigation alike, so a match alone is not a new document.
|
||||||
|
bool is_content_url(const wxString& url);
|
||||||
|
|
||||||
|
// The window.orca bridge of plugin windows and docked panels. Pages tabs ship their own.
|
||||||
|
const char* orca_bridge_script();
|
||||||
|
|
||||||
|
}}} // namespace Slic3r::GUI::web_hosting
|
||||||
@@ -75,6 +75,8 @@ if(document.documentElement)
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
std::string WebViewHostDialog::theme_apply_script() { return host_theme_apply_js(); }
|
||||||
|
|
||||||
// Document-start user script: injects the contract <style>, stamps data-orca-theme before
|
// 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
|
// 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().
|
// host-themed pages. The WebView2 timing guard lives in document_start_injector().
|
||||||
@@ -87,7 +89,7 @@ std::string WebViewHostDialog::theme_user_script()
|
|||||||
"if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);");
|
"if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string WebViewHostDialog::plugin_defaults_user_script()
|
std::string WebViewHostDialog::element_defaults_user_script()
|
||||||
{
|
{
|
||||||
std::string css;
|
std::string css;
|
||||||
css += "<style id=\"orca-plugin-defaults\">";
|
css += "<style id=\"orca-plugin-defaults\">";
|
||||||
|
|||||||
@@ -49,9 +49,11 @@ public:
|
|||||||
const std::string& prelude = {},
|
const std::string& prelude = {},
|
||||||
const std::string& on_inject = {});
|
const std::string& on_inject = {});
|
||||||
|
|
||||||
// Shared by modeless Pages tabs and PluginWebDialog.
|
// Shared by WebPanel hosts and WebDialog.
|
||||||
static std::string theme_user_script();
|
static std::string theme_user_script();
|
||||||
static std::string plugin_defaults_user_script();
|
static std::string element_defaults_user_script();
|
||||||
|
// Re-themes an already-loaded page in place, for web views hosted outside a dialog.
|
||||||
|
static std::string theme_apply_script();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
wxWebView* browser() const { return m_browser; }
|
wxWebView* browser() const { return m_browser; }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <system_error>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <boost/format.hpp>
|
#include <boost/format.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
@@ -580,9 +581,10 @@ bool C3DPrinterOS::save_api_session(const std::string &session, const std::strin
|
|||||||
j.put("session", session);
|
j.put("session", session);
|
||||||
j.put("email", email);
|
j.put("email", email);
|
||||||
try {
|
try {
|
||||||
auto temp_path = m_api_session_file_path + ".tmp";
|
std::ostringstream json;
|
||||||
pt::write_json(temp_path, j);
|
pt::write_json(json, j);
|
||||||
boost::filesystem::rename(temp_path, m_api_session_file_path);
|
if (const std::error_code ec = write_file_atomically(m_api_session_file_path, json.str()))
|
||||||
|
throw std::system_error(ec);
|
||||||
} catch (const std::exception &err) {
|
} catch (const std::exception &err) {
|
||||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to write json to file. Path = "
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to write json to file. Path = "
|
||||||
<< m_api_session_file_path
|
<< m_api_session_file_path
|
||||||
|
|||||||
@@ -1475,15 +1475,8 @@ void OrcaCloudServiceAgent::save_sync_state()
|
|||||||
if (sync_state_path.empty())
|
if (sync_state_path.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
if (const std::error_code ec = write_file_atomically(sync_state_path, std::to_string(sync_state.last_sync_timestamp)))
|
||||||
std::string tmp_path = sync_state_path + ".tmp";
|
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to save the sync state: " << ec.message();
|
||||||
std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc);
|
|
||||||
if (ofs.good()) {
|
|
||||||
ofs << std::to_string(sync_state.last_sync_timestamp);
|
|
||||||
ofs.close();
|
|
||||||
boost::filesystem::rename(tmp_path, sync_state_path);
|
|
||||||
}
|
|
||||||
} catch (...) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OrcaCloudServiceAgent::clear_sync_state()
|
void OrcaCloudServiceAgent::clear_sync_state()
|
||||||
@@ -1572,22 +1565,10 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret)
|
|||||||
wxFileName::Mkdir(path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL);
|
wxFileName::Mkdir(path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string tmp_path = secret_fallback_path + ".tmp";
|
if (const std::error_code ec = write_file_atomically(secret_fallback_path, signed_payload, /*binary=*/true))
|
||||||
std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc | std::ios::binary);
|
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot write user secret file " << secret_fallback_path << ": " << ec.message();
|
||||||
if (ofs.good()) {
|
else
|
||||||
ofs << signed_payload;
|
|
||||||
ofs.flush();
|
|
||||||
ofs.close();
|
|
||||||
|
|
||||||
if (wxRenameFile(wxString::FromUTF8(tmp_path.c_str()), wxString::FromUTF8(secret_fallback_path.c_str()), true)) {
|
|
||||||
stored = true;
|
stored = true;
|
||||||
} else {
|
|
||||||
wxRemoveFile(wxString::FromUTF8(tmp_path.c_str()));
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to atomically replace user secret file";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot open user secret file for write - " << secret_fallback_path;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Use wxSecretStore only
|
// Use wxSecretStore only
|
||||||
wxSecretStore store = wxSecretStore::GetDefault();
|
wxSecretStore store = wxSecretStore::GetDefault();
|
||||||
|
|||||||
@@ -249,21 +249,10 @@ bool PluginConfig::save()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to a PID-suffixed file and rename it into place, so a crash mid-write cannot truncate an
|
// Written beside the target and moved into place, so a crash mid-write cannot truncate an
|
||||||
// existing config. Same approach as AppConfig::save().
|
// existing config.
|
||||||
const std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
if (const std::error_code ec = write_file_atomically(path, root.dump(1, '\t') + "\n")) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to write " << path << ": " << ec.message() << "; keeping the existing config";
|
||||||
boost::nowide::ofstream file;
|
|
||||||
file.open(path_pid, std::ios::out | std::ios::trunc);
|
|
||||||
file << root.dump(1, '\t') << std::endl;
|
|
||||||
file.close();
|
|
||||||
if (file.fail()) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to write " << path_pid << "; keeping the existing config";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (const std::error_code rename_ec = rename_file(path_pid, path)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to move " << path_pid << " onto " << path << ": " << rename_ec.message();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,10 @@
|
|||||||
#include <slic3r/GUI/GUI_App.hpp>
|
#include <slic3r/GUI/GUI_App.hpp>
|
||||||
#include <slic3r/GUI/MainFrame.hpp>
|
#include <slic3r/GUI/MainFrame.hpp>
|
||||||
#include <slic3r/GUI/MsgDialog.hpp>
|
#include <slic3r/GUI/MsgDialog.hpp>
|
||||||
|
#include <slic3r/GUI/Plater.hpp>
|
||||||
|
#include <slic3r/GUI/DockPanel.hpp>
|
||||||
#include <slic3r/GUI/PluginProgressDialog.hpp>
|
#include <slic3r/GUI/PluginProgressDialog.hpp>
|
||||||
#include <slic3r/GUI/PluginWebDialog.hpp>
|
#include <slic3r/GUI/WebDialog.hpp>
|
||||||
#include <slic3r/GUI/NotificationManager.hpp>
|
#include <slic3r/GUI/NotificationManager.hpp>
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
#include <wx/defs.h>
|
#include <wx/defs.h>
|
||||||
#include <wx/window.h>
|
#include <wx/window.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <future>
|
#include <future>
|
||||||
@@ -73,7 +76,7 @@ CallablePtr make_holder(py::object obj)
|
|||||||
|
|
||||||
// Adapt a Python callable to a GUI message handler that acquires the GIL and
|
// Adapt a Python callable to a GUI message handler that acquires the GIL and
|
||||||
// swallows/logs exceptions (a raising handler must not escape into wx events).
|
// swallows/logs exceptions (a raising handler must not escape into wx events).
|
||||||
GUI::PluginWebDialog::MessageHandler make_message_adapter(py::object on_message)
|
GUI::WebDialog::MessageHandler make_message_adapter(py::object on_message)
|
||||||
{
|
{
|
||||||
CallablePtr holder = make_holder(std::move(on_message));
|
CallablePtr holder = make_holder(std::move(on_message));
|
||||||
if (!holder)
|
if (!holder)
|
||||||
@@ -91,7 +94,7 @@ GUI::PluginWebDialog::MessageHandler make_message_adapter(py::object on_message)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
GUI::PluginWebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
|
GUI::WebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
|
||||||
{
|
{
|
||||||
CallablePtr holder = make_holder(std::move(on_submit));
|
CallablePtr holder = make_holder(std::move(on_submit));
|
||||||
if (!holder)
|
if (!holder)
|
||||||
@@ -109,6 +112,25 @@ GUI::PluginWebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The plugin's on_close: fired only on a user/JS-initiated close (not forced teardown), while the
|
||||||
|
// window is alive. Empty if the plugin passed None.
|
||||||
|
std::function<void()> make_close_adapter(const CallablePtr& holder)
|
||||||
|
{
|
||||||
|
if (!holder)
|
||||||
|
return nullptr;
|
||||||
|
return [holder]() {
|
||||||
|
PythonGILState gil;
|
||||||
|
if (!gil)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
holder->fn();
|
||||||
|
} catch (py::error_already_set& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
|
||||||
|
PyErr_Clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
// Registry of live plugin UI resources. Keyed by an opaque id; tracks the
|
// Registry of live plugin UI resources. Keyed by an opaque id; tracks the
|
||||||
// owning plugin so all of a plugin's UI can be torn down on unload.
|
// owning plugin so all of a plugin's UI can be torn down on unload.
|
||||||
@@ -350,26 +372,11 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
|||||||
if (!UiRegistry::instance().is_open(new_id))
|
if (!UiRegistry::instance().is_open(new_id))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Plugin's on_close: fired only on a user/JS-initiated close (not forced
|
GUI::WebDialog::CloseHandler on_close = make_close_adapter(close_holder);
|
||||||
// teardown), while the dialog is alive. Empty if the plugin passed None.
|
|
||||||
GUI::PluginWebDialog::CloseHandler on_close;
|
|
||||||
if (close_holder) {
|
|
||||||
on_close = [close_holder]() {
|
|
||||||
PythonGILState gil;
|
|
||||||
if (!gil)
|
|
||||||
return;
|
|
||||||
try {
|
|
||||||
close_holder->fn();
|
|
||||||
} catch (py::error_already_set& e) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
|
|
||||||
PyErr_Clear();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Registry cleanup: GIL-free, runs from the dialog destructor on every path.
|
// Registry cleanup: GIL-free, runs from the dialog destructor on every path.
|
||||||
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
||||||
|
|
||||||
auto* dlg = new GUI::PluginWebDialog(ui_parent(), wxString::FromUTF8(title), html,
|
auto* dlg = new GUI::WebDialog(ui_parent(), wxString::FromUTF8(title), html,
|
||||||
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
|
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
|
||||||
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
|
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
|
||||||
UiRegistry::instance().bind(new_id, dlg, plugin_key);
|
UiRegistry::instance().bind(new_id, dlg, plugin_key);
|
||||||
@@ -387,14 +394,69 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
|||||||
return py::cast(UiWindowHandle{new_id});
|
return py::cast(UiWindowHandle{new_id});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// orca.host.ui.create_dock_panel + UiDockPanel handle
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
constexpr const char* DOCK_POSITIONS[] = {"left", "right", "bottom", "float"};
|
||||||
|
|
||||||
|
struct UiDockPanelHandle
|
||||||
|
{
|
||||||
|
int id{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
py::object ui_create_dock_panel(const std::string& html, const std::string& title, int width, int height,
|
||||||
|
py::object on_message, py::object on_close, const std::string& dock)
|
||||||
|
{
|
||||||
|
if (std::find(std::begin(DOCK_POSITIONS), std::end(DOCK_POSITIONS), dock) == std::end(DOCK_POSITIONS))
|
||||||
|
throw std::invalid_argument("orca.host.ui.create_dock_panel dock must be \"left\", \"right\", \"bottom\" or \"float\"");
|
||||||
|
|
||||||
|
auto msg_adapter = make_message_adapter(std::move(on_message));
|
||||||
|
CallablePtr close_holder = make_holder(std::move(on_close));
|
||||||
|
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
|
||||||
|
const int w = width > 0 ? width : 320;
|
||||||
|
const int h = height > 0 ? height : 480;
|
||||||
|
|
||||||
|
if (wxTheApp == nullptr)
|
||||||
|
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||||
|
|
||||||
|
// Deferred and pre-bound for the same reasons as create_window().
|
||||||
|
const int new_id = UiRegistry::instance().reserve_id();
|
||||||
|
UiRegistry::instance().bind(new_id, nullptr, plugin_key);
|
||||||
|
|
||||||
|
GUI::wxGetApp().CallAfter([new_id, plugin_key, html, title, dock, w, h,
|
||||||
|
msg_adapter = std::move(msg_adapter),
|
||||||
|
close_holder = std::move(close_holder)]() mutable {
|
||||||
|
if (!UiRegistry::instance().is_open(new_id))
|
||||||
|
return;
|
||||||
|
|
||||||
|
GUI::Plater* plater = GUI::wxGetApp().plater();
|
||||||
|
if (plater == nullptr || GUI::wxGetApp().is_closing()) {
|
||||||
|
UiRegistry::instance().remove(new_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
||||||
|
auto* panel = new GUI::DockPanel(plater, html, std::move(msg_adapter), make_close_adapter(close_holder),
|
||||||
|
std::move(on_destroyed));
|
||||||
|
UiRegistry::instance().bind(new_id, panel, plugin_key);
|
||||||
|
plater->add_dock_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
|
||||||
|
wxSize(w, h), [panel]() { panel->fire_close(); });
|
||||||
|
});
|
||||||
|
|
||||||
|
return py::cast(UiDockPanelHandle{new_id});
|
||||||
|
}
|
||||||
|
|
||||||
void handle_post(int id, py::object data)
|
void handle_post(int id, py::object data)
|
||||||
{
|
{
|
||||||
if (wxTheApp == nullptr)
|
if (wxTheApp == nullptr)
|
||||||
return;
|
return;
|
||||||
json j = py_to_json(data); // GIL held (binding body)
|
json j = py_to_json(data); // GIL held (binding body)
|
||||||
GUI::wxGetApp().CallAfter([id, j = std::move(j)]() {
|
GUI::wxGetApp().CallAfter([id, j = std::move(j)]() {
|
||||||
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
|
auto* window = UiRegistry::instance().get_as<wxWindow>(id);
|
||||||
GUI::PluginWebDialog::post_message(d, j);
|
if (auto* panel = dynamic_cast<GUI::DockPanel*>(window))
|
||||||
|
panel->push_message(j);
|
||||||
|
else
|
||||||
|
GUI::WebDialog::post_message(dynamic_cast<GUI::WebDialog*>(window), j);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,8 +465,23 @@ void handle_close(int id)
|
|||||||
if (wxTheApp == nullptr)
|
if (wxTheApp == nullptr)
|
||||||
return;
|
return;
|
||||||
GUI::wxGetApp().CallAfter([id]() {
|
GUI::wxGetApp().CallAfter([id]() {
|
||||||
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
|
auto* window = UiRegistry::instance().get_as<wxWindow>(id);
|
||||||
GUI::PluginWebDialog::request_close(d);
|
if (auto* panel = dynamic_cast<GUI::DockPanel*>(window))
|
||||||
|
panel->request_close();
|
||||||
|
else
|
||||||
|
GUI::WebDialog::request_close(dynamic_cast<GUI::WebDialog*>(window));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_show(int id, bool show)
|
||||||
|
{
|
||||||
|
if (wxTheApp == nullptr)
|
||||||
|
return;
|
||||||
|
GUI::wxGetApp().CallAfter([id, show]() {
|
||||||
|
auto* panel = UiRegistry::instance().get_as<GUI::DockPanel>(id);
|
||||||
|
GUI::Plater* plater = GUI::wxGetApp().plater();
|
||||||
|
if (panel != nullptr && plater != nullptr)
|
||||||
|
plater->show_dock_pane(panel, show);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,6 +640,31 @@ void PluginHostUi::RegisterBindings(pybind11::module_& host)
|
|||||||
"or WINDOW_MODAL. on_message(data) is called on the UI thread when the page posts; on_submit(data) "
|
"or WINDOW_MODAL. on_message(data) is called on the UI thread when the page posts; on_submit(data) "
|
||||||
"is called when the page submits; offload heavy work to a thread and push results back with window.post().");
|
"is called when the page submits; offload heavy work to a thread and push results back with window.post().");
|
||||||
|
|
||||||
|
py::class_<UiDockPanelHandle>(ui, "UiDockPanel", "Handle to a dockable plugin HTML panel created by create_dock_panel().")
|
||||||
|
.def_property_readonly("id", [](const UiDockPanelHandle& h) { return h.id; })
|
||||||
|
.def(
|
||||||
|
"post", [](const UiDockPanelHandle& h, py::object data) { handle_post(h.id, std::move(data)); },
|
||||||
|
py::arg("data"), "Send a payload to the page (delivered to window.orca.onMessage handlers).")
|
||||||
|
.def(
|
||||||
|
"show", [](const UiDockPanelHandle& h) { handle_show(h.id, true); }, "Show the panel again after hide().")
|
||||||
|
.def(
|
||||||
|
"hide", [](const UiDockPanelHandle& h) { handle_show(h.id, false); }, "Hide the panel without closing it.")
|
||||||
|
.def(
|
||||||
|
"close", [](const UiDockPanelHandle& h) { handle_close(h.id); }, "Close the panel (fires on_close).")
|
||||||
|
.def(
|
||||||
|
"is_open", [](const UiDockPanelHandle& h) { return UiRegistry::instance().is_open(h.id); },
|
||||||
|
"Return True until the panel is closed; a hidden panel is still open.");
|
||||||
|
|
||||||
|
ui.def("create_dock_panel", &ui_create_dock_panel, py::arg("html"), py::arg("title") = "OrcaSlicer",
|
||||||
|
py::arg("width") = 320, py::arg("height") = 480, py::arg("on_message") = py::none(),
|
||||||
|
py::arg("on_close") = py::none(), py::arg("dock") = "right",
|
||||||
|
"Open an HTML panel docked beside the 3D view and return a UiDockPanel. dock is \"left\", \"right\", "
|
||||||
|
"\"bottom\" or \"float\", and width/height are in DIPs; the user can move and resize the panel, and a panel "
|
||||||
|
"opened again comes back where the window layout was last saved. The panel belongs to the Prepare and "
|
||||||
|
"Preview tabs. on_message(data) is called on the UI thread when the page posts; window.orca.close() "
|
||||||
|
"or the panel's close button closes it and calls on_close(). A post() made before the page has "
|
||||||
|
"loaded can be dropped, so have the page request its first data.");
|
||||||
|
|
||||||
py::class_<UiProgressHandle>(ui, "ProgressDialog", "Handle to a native progress dialog.")
|
py::class_<UiProgressHandle>(ui, "ProgressDialog", "Handle to a native progress dialog.")
|
||||||
.def(py::init(&new_progress_dialog), py::arg("title"), py::arg("message"), py::arg("maximum") = 100,
|
.def(py::init(&new_progress_dialog), py::arg("title"), py::arg("message"), py::arg("maximum") = 100,
|
||||||
py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
|
py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
|
||||||
@@ -630,8 +732,10 @@ void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
|
|||||||
// Destroy() bypasses wxEVT_CLOSE, so the plugin's on_close is not fired on
|
// Destroy() bypasses wxEVT_CLOSE, so the plugin's on_close is not fired on
|
||||||
// forced teardown (intended); the resource destructor still cleans the registry.
|
// forced teardown (intended); the resource destructor still cleans the registry.
|
||||||
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
|
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
|
||||||
if (auto* dialog = dynamic_cast<GUI::PluginWebDialog*>(window))
|
if (auto* dialog = dynamic_cast<GUI::WebDialog*>(window))
|
||||||
GUI::PluginWebDialog::destroy_for_plugin(dialog);
|
GUI::WebDialog::destroy_silently(dialog);
|
||||||
|
else if (auto* panel = dynamic_cast<GUI::DockPanel*>(window))
|
||||||
|
panel->destroy_silently();
|
||||||
else if (window != nullptr)
|
else if (window != nullptr)
|
||||||
window->Destroy();
|
window->Destroy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
#include "PluginPages.hpp"
|
#include "PluginPages.hpp"
|
||||||
|
|
||||||
#include "libslic3r/AppConfig.hpp"
|
#include "libslic3r/AppConfig.hpp"
|
||||||
#include "slic3r/GUI/GUI.hpp"
|
|
||||||
#include "slic3r/GUI/Notebook.hpp"
|
#include "slic3r/GUI/Notebook.hpp"
|
||||||
#include "slic3r/GUI/GUI_App.hpp"
|
#include "slic3r/GUI/GUI_App.hpp"
|
||||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||||
#include "slic3r/GUI/Widgets/WebView.hpp"
|
|
||||||
#include "slic3r/GUI/Widgets/WebViewHostDialog.hpp"
|
|
||||||
#include "slic3r/GUI/wxExtensions.hpp"
|
#include "slic3r/GUI/wxExtensions.hpp"
|
||||||
#include "slic3r/plugin/PluginManager.hpp"
|
#include "slic3r/plugin/PluginManager.hpp"
|
||||||
|
|
||||||
#include <libslic3r/Utils.hpp>
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include <boost/filesystem/path.hpp>
|
#include <boost/filesystem/path.hpp>
|
||||||
@@ -66,27 +61,11 @@ constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS(
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
|
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
|
||||||
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
|
: GUI::WebPanel(parent, PLUGIN_PAGE_BRIDGE_JS)
|
||||||
, m_cap(std::move(capability))
|
, m_cap(std::move(capability))
|
||||||
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
|
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
|
||||||
{
|
{
|
||||||
auto* topsizer = new wxBoxSizer(wxVERTICAL);
|
browser()->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this);
|
||||||
SetSizer(topsizer);
|
|
||||||
|
|
||||||
m_browser = WebView::CreateWebView(this, bootstrap_url());
|
|
||||||
if (m_browser == nullptr) {
|
|
||||||
wxLogError("Could not initialize plugin page web view");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
|
||||||
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginPage::on_bootstrap_event, this);
|
|
||||||
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginPage::on_bootstrap_event, this);
|
|
||||||
m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this);
|
|
||||||
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginPage::on_script_message, this);
|
|
||||||
m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::theme_user_script()));
|
|
||||||
m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::plugin_defaults_user_script()));
|
|
||||||
m_browser->AddUserScript(PLUGIN_PAGE_BRIDGE_JS);
|
|
||||||
|
|
||||||
const std::shared_ptr<std::atomic<PluginPage*>> lifetime = m_lifetime;
|
const std::shared_ptr<std::atomic<PluginPage*>> lifetime = m_lifetime;
|
||||||
m_cap->set_message_sender([lifetime](const std::string& message) {
|
m_cap->set_message_sender([lifetime](const std::string& message) {
|
||||||
@@ -117,89 +96,54 @@ void PluginPage::detach_capability()
|
|||||||
m_cap.reset();
|
m_cap.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
wxString PluginPage::web_base_url() const
|
std::optional<std::string> PluginPage::page_html()
|
||||||
{
|
{
|
||||||
const auto path = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
|
if (m_cap == nullptr)
|
||||||
return wxString("file://") + GUI::from_u8(path) + "/";
|
return std::nullopt;
|
||||||
}
|
|
||||||
|
|
||||||
wxString PluginPage::bootstrap_url() const
|
|
||||||
{
|
|
||||||
const auto path = (boost::filesystem::path(resources_dir()) / "web/dialog/PluginWebDialog/blank.html").make_preferred().string();
|
|
||||||
return wxString("file://") + GUI::from_u8(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PluginPage::on_bootstrap_event(wxWebViewEvent& event)
|
|
||||||
{
|
|
||||||
load_plugin_content();
|
|
||||||
event.Skip();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PluginPage::load_plugin_content()
|
|
||||||
{
|
|
||||||
if (m_content_loaded || m_browser == nullptr || m_cap == nullptr)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_content_loaded = true;
|
|
||||||
try {
|
try {
|
||||||
m_browser->SetPage(wxString::FromUTF8(m_cap->get_ui()), web_base_url());
|
return m_cap->get_ui();
|
||||||
} catch (const std::exception& error) {
|
} catch (const std::exception& error) {
|
||||||
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "': " << error.what();
|
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "': " << error.what();
|
||||||
detach_capability();
|
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "'";
|
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "'";
|
||||||
detach_capability();
|
|
||||||
}
|
}
|
||||||
|
detach_capability();
|
||||||
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginPage::on_new_window(wxWebViewEvent& event)
|
void PluginPage::on_new_window(wxWebViewEvent& event)
|
||||||
{
|
{
|
||||||
const wxString url = event.GetURL();
|
const wxString url = event.GetURL();
|
||||||
if (!url.empty() && m_browser != nullptr)
|
if (!url.empty())
|
||||||
m_browser->LoadURL(url);
|
browser()->LoadURL(url);
|
||||||
event.Veto();
|
event.Veto();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginPage::on_script_message(wxWebViewEvent& event)
|
bool PluginPage::on_page_message(const std::string& kind, const nlohmann::json& data)
|
||||||
{
|
{
|
||||||
|
if (kind != "message")
|
||||||
|
return false;
|
||||||
if (!m_cap)
|
if (!m_cap)
|
||||||
return;
|
return true;
|
||||||
|
|
||||||
const wxString payload = event.GetString();
|
|
||||||
nlohmann::json root = nlohmann::json::parse(payload.utf8_string(), nullptr, false);
|
|
||||||
if (root.is_discarded() || root.value("channel", std::string()) != "orca" ||
|
|
||||||
root.value("kind", std::string()) != "message")
|
|
||||||
return;
|
|
||||||
|
|
||||||
const auto data = root.find("data");
|
|
||||||
try {
|
try {
|
||||||
m_cap->on_message(data == root.end()
|
m_cap->on_message(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
|
||||||
? "null"
|
|
||||||
: data->dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
|
|
||||||
} catch (const std::exception& error) {
|
} catch (const std::exception& error) {
|
||||||
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "': " << error.what();
|
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "': " << error.what();
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "'";
|
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "'";
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginPage::push_message(const std::string& message)
|
void PluginPage::push_message(const std::string& message)
|
||||||
{
|
{
|
||||||
if (m_browser == nullptr)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// PagesPluginCapability::post_message() already dumps JSON, so accept it as-is; only a
|
// PagesPluginCapability::post_message() already dumps JSON, so accept it as-is; only a
|
||||||
// non-JSON payload needs wrapping as a string literal.
|
// non-JSON payload needs wrapping as a string literal.
|
||||||
const std::string payload = nlohmann::json::accept(message)
|
post_to_page(nlohmann::json::accept(message)
|
||||||
? message
|
? message
|
||||||
: nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace);
|
: nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
|
||||||
|
|
||||||
WebView::RunScript(m_browser, wxString::Format(
|
|
||||||
"(function dispatch(payload, attempts) {\n"
|
|
||||||
" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n"
|
|
||||||
" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n"
|
|
||||||
"})({data: %s}, 0);",
|
|
||||||
wxString::FromUTF8(payload)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginPages::~PluginPages()
|
PluginPages::~PluginPages()
|
||||||
@@ -268,10 +212,6 @@ bool PluginPages::create_page(const PluginCapabilityId& id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto* page = new PluginPage(m_parent, std::move(capability));
|
auto* page = new PluginPage(m_parent, std::move(capability));
|
||||||
if (!page->is_valid()) {
|
|
||||||
page->Destroy();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!icon.empty()) {
|
if (!icon.empty()) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <slic3r/GUI/WebPanel.hpp>
|
||||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||||
#include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp>
|
#include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp>
|
||||||
|
|
||||||
@@ -11,14 +12,13 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <wx/bitmap.h>
|
#include <wx/bitmap.h>
|
||||||
#include <wx/panel.h>
|
|
||||||
#include <wx/webview.h>
|
#include <wx/webview.h>
|
||||||
|
|
||||||
class Notebook;
|
class Notebook;
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
class PluginPage : public wxPanel
|
class PluginPage : public GUI::WebPanel
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability);
|
PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability);
|
||||||
@@ -26,24 +26,20 @@ public:
|
|||||||
|
|
||||||
PluginPage() = delete;
|
PluginPage() = delete;
|
||||||
|
|
||||||
bool is_valid() const { return m_browser != nullptr && m_cap != nullptr; }
|
|
||||||
void detach_capability();
|
void detach_capability();
|
||||||
void on_bootstrap_event(wxWebViewEvent& event);
|
|
||||||
void on_new_window(wxWebViewEvent& event);
|
|
||||||
void on_script_message(wxWebViewEvent& event);
|
|
||||||
void push_message(const std::string& message);
|
void push_message(const std::string& message);
|
||||||
void set_icon(const wxBitmap& icon) { m_icon = icon; }
|
void set_icon(const wxBitmap& icon) { m_icon = icon; }
|
||||||
const wxBitmap& icon() const { return m_icon; }
|
const wxBitmap& icon() const { return m_icon; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::optional<std::string> page_html() override;
|
||||||
|
bool on_page_message(const std::string& kind, const nlohmann::json& data) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void load_plugin_content();
|
void on_new_window(wxWebViewEvent& event);
|
||||||
wxString bootstrap_url() const;
|
|
||||||
wxString web_base_url() const;
|
|
||||||
|
|
||||||
wxWebView* m_browser{nullptr};
|
|
||||||
std::shared_ptr<PagesPluginCapability> m_cap;
|
std::shared_ptr<PagesPluginCapability> m_cap;
|
||||||
std::shared_ptr<std::atomic<PluginPage*>> m_lifetime;
|
std::shared_ptr<std::atomic<PluginPage*>> m_lifetime;
|
||||||
bool m_content_loaded{false};
|
|
||||||
wxBitmap m_icon;
|
wxBitmap m_icon;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_ordering_strategies.cpp
|
test_ordering_strategies.cpp
|
||||||
# test_png_io.cpp
|
# test_png_io.cpp
|
||||||
test_indexed_triangle_set.cpp
|
test_indexed_triangle_set.cpp
|
||||||
|
test_instance_lock.cpp
|
||||||
../libnest2d/printer_parts.cpp
|
../libnest2d/printer_parts.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
|
||||||
|
#include "libslic3r/InstanceLock.hpp"
|
||||||
|
#include "test_utils.hpp"
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
// Sets a process-wide knob for one test and restores it however the test ends.
|
||||||
|
template<typename T> struct ScopedStaticValue
|
||||||
|
{
|
||||||
|
T &ref;
|
||||||
|
T saved;
|
||||||
|
ScopedStaticValue(T &ref, T value) : ref(ref), saved(ref) { ref = value; }
|
||||||
|
~ScopedStaticValue() { ref = saved; }
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_CASE("InstanceLock creates its lock file and holds it for the guard's scope", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile lock_file(".lock");
|
||||||
|
const std::string path = lock_file.string();
|
||||||
|
|
||||||
|
{
|
||||||
|
InstanceLock lock(path);
|
||||||
|
REQUIRE(lock.locked());
|
||||||
|
REQUIRE(boost::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
// Released: a fresh guard gets the lock at once instead of waiting out a timeout.
|
||||||
|
const auto started = std::chrono::steady_clock::now();
|
||||||
|
InstanceLock again(path, 5000ms);
|
||||||
|
REQUIRE(again.locked());
|
||||||
|
// Well inside the timeout it would otherwise have waited out; loose enough for a loaded runner.
|
||||||
|
REQUIRE(std::chrono::steady_clock::now() - started < 4000ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InstanceLock nests within one thread", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile lock_file(".lock");
|
||||||
|
const std::string path = lock_file.string();
|
||||||
|
|
||||||
|
InstanceLock outer(path);
|
||||||
|
{
|
||||||
|
InstanceLock inner(path, 100ms);
|
||||||
|
REQUIRE(inner.locked());
|
||||||
|
}
|
||||||
|
// The inner guard leaving does not release the outer one.
|
||||||
|
REQUIRE(outer.locked());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InstanceLock is a no-op for an empty path and survives an unwritable one", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
|
||||||
|
InstanceLock none("");
|
||||||
|
REQUIRE_FALSE(none.locked());
|
||||||
|
|
||||||
|
// The directory does not exist, so the lock file cannot be created; the
|
||||||
|
// guard still constructs and the write it guards can go ahead.
|
||||||
|
InstanceLock unwritable((dir.path() / "missing" / "shared.lock").string(), 100ms);
|
||||||
|
REQUIRE_FALSE(unwritable.locked());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InstanceLock retries a lock file it could not open once the cool-down passes", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const std::string path = (dir.path() / "later" / "shared.lock").string();
|
||||||
|
ScopedStaticValue cooldown(InstanceLock::cooldown, 300ms);
|
||||||
|
|
||||||
|
bool before_dir, during_cooldown, after_cooldown;
|
||||||
|
const auto started = std::chrono::steady_clock::now();
|
||||||
|
{
|
||||||
|
InstanceLock lock(path, 100ms);
|
||||||
|
before_dir = lock.locked();
|
||||||
|
}
|
||||||
|
boost::filesystem::create_directories(dir.path() / "later");
|
||||||
|
{
|
||||||
|
InstanceLock lock(path, 100ms);
|
||||||
|
during_cooldown = lock.locked();
|
||||||
|
}
|
||||||
|
const bool second_guard_inside_cooldown = std::chrono::steady_clock::now() - started < InstanceLock::cooldown;
|
||||||
|
std::this_thread::sleep_for(400ms);
|
||||||
|
{
|
||||||
|
InstanceLock lock(path, 100ms);
|
||||||
|
after_cooldown = lock.locked();
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIRE_FALSE(before_dir);
|
||||||
|
// A loaded runner may take longer than the cool-down to get here; then the
|
||||||
|
// second guard legitimately retried, so only assert when the timing held.
|
||||||
|
if (second_guard_inside_cooldown)
|
||||||
|
REQUIRE_FALSE(during_cooldown);
|
||||||
|
REQUIRE(after_cooldown);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InstanceLock reopens a lock file that was replaced on disk", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile lock_file(".lock");
|
||||||
|
const std::string path = lock_file.string();
|
||||||
|
{
|
||||||
|
InstanceLock lock(path);
|
||||||
|
REQUIRE(lock.locked());
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::filesystem::remove(path);
|
||||||
|
InstanceLock lock(path);
|
||||||
|
REQUIRE(lock.locked());
|
||||||
|
// Each outermost guard opens the file afresh, so the deleted path is back.
|
||||||
|
REQUIRE(boost::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InstanceLock serialises the threads of one process", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile lock_file(".lock");
|
||||||
|
const std::string path = lock_file.string();
|
||||||
|
|
||||||
|
std::atomic<bool> holder_ready{false};
|
||||||
|
std::atomic<bool> holder_released{false};
|
||||||
|
std::thread holder([&] {
|
||||||
|
InstanceLock lock(path);
|
||||||
|
holder_ready = true;
|
||||||
|
std::this_thread::sleep_for(150ms);
|
||||||
|
holder_released = true;
|
||||||
|
});
|
||||||
|
while (! holder_ready)
|
||||||
|
std::this_thread::yield();
|
||||||
|
|
||||||
|
bool released_before_acquire = false;
|
||||||
|
{
|
||||||
|
InstanceLock lock(path);
|
||||||
|
released_before_acquire = holder_released;
|
||||||
|
}
|
||||||
|
holder.join();
|
||||||
|
REQUIRE(released_before_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// The cross-process side of the lock is a POSIX flock, which a child process
|
||||||
|
// takes here directly; LockFileEx backs the guard on Windows, but spawning a
|
||||||
|
// child there is not worth a test.
|
||||||
|
TEST_CASE("InstanceLock yields to another process and reports it", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile lock_file(".lock");
|
||||||
|
const std::string path = lock_file.string();
|
||||||
|
ScopedStaticValue cooldown(InstanceLock::cooldown, 300ms);
|
||||||
|
|
||||||
|
int child_holds[2], child_may_exit[2];
|
||||||
|
REQUIRE(::pipe(child_holds) == 0);
|
||||||
|
REQUIRE(::pipe(child_may_exit) == 0);
|
||||||
|
|
||||||
|
const pid_t child = ::fork();
|
||||||
|
REQUIRE(child >= 0);
|
||||||
|
if (child == 0) {
|
||||||
|
int fd = ::open(path.c_str(), O_RDWR | O_CREAT, 0644);
|
||||||
|
char byte = ::flock(fd, LOCK_EX | LOCK_NB) == 0 ? '1' : '0';
|
||||||
|
if (::write(child_holds[1], &byte, 1) != 1 || ::read(child_may_exit[0], &byte, 1) != 1)
|
||||||
|
::_exit(1);
|
||||||
|
::_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
char byte = '0';
|
||||||
|
REQUIRE(::read(child_holds[0], &byte, 1) == 1);
|
||||||
|
REQUIRE(byte == '1');
|
||||||
|
|
||||||
|
bool locked_while_child_holds;
|
||||||
|
{
|
||||||
|
InstanceLock lock(path, 100ms);
|
||||||
|
locked_while_child_holds = lock.locked();
|
||||||
|
}
|
||||||
|
// The timed-out wait starts a cool-down: the next guard does not touch the file.
|
||||||
|
const auto started = std::chrono::steady_clock::now();
|
||||||
|
bool locked_during_cooldown;
|
||||||
|
{
|
||||||
|
InstanceLock lock(path, 5000ms);
|
||||||
|
locked_during_cooldown = lock.locked();
|
||||||
|
}
|
||||||
|
const auto cooldown_wait = std::chrono::steady_clock::now() - started;
|
||||||
|
REQUIRE(::write(child_may_exit[1], "x", 1) == 1);
|
||||||
|
int status = 0;
|
||||||
|
REQUIRE(::waitpid(child, &status, 0) == child);
|
||||||
|
for (int fd : {child_holds[0], child_holds[1], child_may_exit[0], child_may_exit[1]})
|
||||||
|
::close(fd);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(locked_while_child_holds);
|
||||||
|
REQUIRE_FALSE(locked_during_cooldown);
|
||||||
|
REQUIRE(cooldown_wait < 4000ms);
|
||||||
|
// Once the cool-down passes, the lock the child released is taken again.
|
||||||
|
std::this_thread::sleep_for(400ms);
|
||||||
|
InstanceLock lock(path);
|
||||||
|
REQUIRE(lock.locked());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
#include <unistd.h> // getuid
|
#include <unistd.h> // getuid
|
||||||
@@ -62,6 +64,113 @@ TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[ut
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write_file_atomically replaces the target and leaves no temporary file", "[utils]") {
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const boost::filesystem::path target = dir.path() / "preset.json";
|
||||||
|
|
||||||
|
REQUIRE_FALSE(write_file_atomically(target.string(), "first"));
|
||||||
|
REQUIRE_FALSE(write_file_atomically(target.string(), "second"));
|
||||||
|
|
||||||
|
std::string content;
|
||||||
|
load_string_file(target, content);
|
||||||
|
REQUIRE(content == "second");
|
||||||
|
size_t entries = 0;
|
||||||
|
for (auto &entry : boost::filesystem::directory_iterator(dir.path())) {
|
||||||
|
(void) entry;
|
||||||
|
++entries;
|
||||||
|
}
|
||||||
|
REQUIRE(entries == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write_file_atomically reports a missing directory and writes nothing", "[utils]") {
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const boost::filesystem::path target = dir.path() / "missing" / "preset.json";
|
||||||
|
|
||||||
|
const std::error_code ec = write_file_atomically(target.string(), "x");
|
||||||
|
REQUIRE(ec == std::errc::no_such_file_or_directory);
|
||||||
|
REQUIRE_FALSE(boost::filesystem::exists(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// The read-only bit on a directory stops file creation only on POSIX.
|
||||||
|
TEST_CASE("write_file_atomically writes in place when no temporary can be created beside an existing target", "[utils]") {
|
||||||
|
if (::geteuid() == 0)
|
||||||
|
SKIP("a read-only directory does not stop root");
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const boost::filesystem::path target = dir.path() / "preset.json";
|
||||||
|
REQUIRE_FALSE(write_file_atomically(target.string(), "first"));
|
||||||
|
boost::filesystem::permissions(dir.path(), boost::filesystem::owner_read | boost::filesystem::owner_exe);
|
||||||
|
|
||||||
|
const std::error_code replaced = write_file_atomically(target.string(), "second");
|
||||||
|
const std::error_code created = write_file_atomically((dir.path() / "new.json").string(), "x");
|
||||||
|
// Restored before any assertion, so a failure never leaves an unremovable directory behind.
|
||||||
|
boost::filesystem::permissions(dir.path(), boost::filesystem::owner_all);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(replaced);
|
||||||
|
REQUIRE(created == std::errc::permission_denied);
|
||||||
|
std::string content;
|
||||||
|
load_string_file(target, content);
|
||||||
|
REQUIRE(content == "second");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TEST_CASE("write_file_atomically keeps bytes intact in binary mode", "[utils]") {
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const boost::filesystem::path target = dir.path() / "blob.bin";
|
||||||
|
const std::string bytes("a\r\nb\0c", 6);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(write_file_atomically(target.string(), bytes, /*binary=*/true));
|
||||||
|
REQUIRE(boost::filesystem::file_size(target) == bytes.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
TEST_CASE("write_file_atomically writes through a symlink and keeps the target's permissions", "[utils]") {
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const boost::filesystem::path real = dir.path() / "real.json";
|
||||||
|
const boost::filesystem::path link = dir.path() / "link.json";
|
||||||
|
REQUIRE_FALSE(write_file_atomically(real.string(), "first"));
|
||||||
|
boost::filesystem::permissions(real, boost::filesystem::owner_read | boost::filesystem::owner_write);
|
||||||
|
boost::filesystem::create_symlink(real, link);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(write_file_atomically(link.string(), "second"));
|
||||||
|
|
||||||
|
REQUIRE(boost::filesystem::is_symlink(boost::filesystem::symlink_status(link)));
|
||||||
|
std::string content;
|
||||||
|
load_string_file(real, content);
|
||||||
|
REQUIRE(content == "second");
|
||||||
|
|
||||||
|
REQUIRE_FALSE(write_file_atomically(real.string(), "third"));
|
||||||
|
const auto perms = boost::filesystem::status(real).permissions() & boost::filesystem::all_all;
|
||||||
|
REQUIRE(perms == (boost::filesystem::owner_read | boost::filesystem::owner_write));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TEST_CASE("write_file_atomically survives two threads writing one target", "[utils]") {
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const boost::filesystem::path target = dir.path() / "shared.json";
|
||||||
|
const std::string a(20000, 'a'), b(20000, 'b');
|
||||||
|
|
||||||
|
std::thread other([&] {
|
||||||
|
for (int i = 0; i < 50; ++i)
|
||||||
|
write_file_atomically(target.string(), a);
|
||||||
|
});
|
||||||
|
for (int i = 0; i < 50; ++i)
|
||||||
|
write_file_atomically(target.string(), b);
|
||||||
|
other.join();
|
||||||
|
|
||||||
|
std::string content;
|
||||||
|
load_string_file(target, content);
|
||||||
|
const bool whole = content == a || content == b;
|
||||||
|
REQUIRE(whole);
|
||||||
|
// No temporary may be left; a scanner on Windows may briefly hold the old
|
||||||
|
// file under another name, so only the temporaries are counted.
|
||||||
|
size_t temporaries = 0;
|
||||||
|
for (auto &entry : boost::filesystem::directory_iterator(dir.path()))
|
||||||
|
if (entry.path().extension() == ".tmp")
|
||||||
|
++temporaries;
|
||||||
|
REQUIRE(temporaries == 0);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("copy_file reports the OS error when the destination cannot be written", "[utils]") {
|
TEST_CASE("copy_file reports the OS error when the destination cannot be written", "[utils]") {
|
||||||
ScopedTemporaryFile source(".txt");
|
ScopedTemporaryFile source(".txt");
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1356,24 +1356,6 @@ TEST_CASE("a header claiming more body than the file holds is rejected", "[Vendo
|
|||||||
REQUIRE_FALSE(bundle.load_vendor_cache(cache, "Bounded", Semver(1, 0, 0)));
|
REQUIRE_FALSE(bundle.load_vendor_cache(cache, "Bounded", Semver(1, 0, 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("a failed write leaves the previous cache in place", "[VendorCache]")
|
|
||||||
{
|
|
||||||
TempDir tmp;
|
|
||||||
const std::string cache = (tmp.path / "Durable.opc").string();
|
|
||||||
REQUIRE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "1.0.0"));
|
|
||||||
const std::string before = slurp(cache);
|
|
||||||
|
|
||||||
// A directory where the temp file wants to go: the write cannot complete,
|
|
||||||
// and must not have destroyed what was already there to find that out.
|
|
||||||
const fs::path blocker = fs::path(cache + "." + std::to_string(get_current_pid()) + ".tmp");
|
|
||||||
fs::create_directories(blocker);
|
|
||||||
|
|
||||||
REQUIRE_FALSE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "2.0.0"));
|
|
||||||
CHECK(slurp(cache) == before);
|
|
||||||
|
|
||||||
fs::remove_all(blocker);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("a cache written by another build's option ordering still loads", "[VendorCache]")
|
TEST_CASE("a cache written by another build's option ordering still loads", "[VendorCache]")
|
||||||
{
|
{
|
||||||
// The regression the fingerprint used to prevent by refusing the file
|
// The regression the fingerprint used to prevent by refusing the file
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
#include <libslic3r/Model.hpp>
|
#include <libslic3r/Model.hpp>
|
||||||
#include <libslic3r/PresetBundle.hpp>
|
#include <libslic3r/PresetBundle.hpp>
|
||||||
#include <libslic3r/TriangleMesh.hpp>
|
#include <libslic3r/TriangleMesh.hpp>
|
||||||
|
#include <slic3r/GUI/DockPanel.hpp>
|
||||||
|
#include <slic3r/GUI/AuiPaneLayout.hpp>
|
||||||
|
#include <slic3r/GUI/Widgets/WebHosting.hpp>
|
||||||
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
||||||
|
|
||||||
|
#include "plugin_test_utils.hpp"
|
||||||
#include "python_test_support.hpp"
|
#include "python_test_support.hpp"
|
||||||
|
|
||||||
#include <pybind11/embed.h>
|
#include <pybind11/embed.h>
|
||||||
@@ -141,6 +145,8 @@ TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app i
|
|||||||
CHECK(ui.attr("WINDOW_MODELESS").cast<long>() == 0);
|
CHECK(ui.attr("WINDOW_MODELESS").cast<long>() == 0);
|
||||||
CHECK(ui.attr("WINDOW_MODAL").cast<long>() == 1);
|
CHECK(ui.attr("WINDOW_MODAL").cast<long>() == 1);
|
||||||
CHECK(has_attr(ui, "UiWindow"));
|
CHECK(has_attr(ui, "UiWindow"));
|
||||||
|
CHECK(has_attr(ui, "create_dock_panel"));
|
||||||
|
CHECK(has_attr(ui, "UiDockPanel"));
|
||||||
|
|
||||||
// With no wx application the UI calls marshal to a main thread that does not
|
// With no wx application the UI calls marshal to a main thread that does not
|
||||||
// exist here; they must fail cleanly with a clear error, not crash.
|
// exist here; they must fail cleanly with a clear error, not crash.
|
||||||
@@ -151,6 +157,74 @@ TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app i
|
|||||||
CHECK(error.matches(PyExc_RuntimeError));
|
CHECK(error.matches(PyExc_RuntimeError));
|
||||||
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ui.attr("create_dock_panel")("<p>panel</p>");
|
||||||
|
FAIL("orca.host.ui.create_dock_panel unexpectedly succeeded without a wx application");
|
||||||
|
} catch (const py::error_already_set& error) {
|
||||||
|
CHECK(error.matches(PyExc_RuntimeError));
|
||||||
|
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positional arguments follow create_window(): width and height come straight after the title.
|
||||||
|
try {
|
||||||
|
ui.attr("create_dock_panel")("<p>panel</p>", "Panel", 400, 300);
|
||||||
|
FAIL("orca.host.ui.create_dock_panel unexpectedly succeeded without a wx application");
|
||||||
|
} catch (const py::error_already_set& error) {
|
||||||
|
CHECK(error.matches(PyExc_RuntimeError));
|
||||||
|
}
|
||||||
|
|
||||||
|
// An unknown dock position is rejected before the application is needed.
|
||||||
|
try {
|
||||||
|
ui.attr("create_dock_panel")("<p>panel</p>", py::arg("dock") = "top");
|
||||||
|
FAIL("orca.host.ui.create_dock_panel accepted an unknown dock position");
|
||||||
|
} catch (const py::error_already_set& error) {
|
||||||
|
CHECK(error.matches(PyExc_ValueError));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Plugin pane names identify the plugin and title without layout delimiters", "[PluginHost]")
|
||||||
|
{
|
||||||
|
using Slic3r::GUI::plugin_pane_name;
|
||||||
|
|
||||||
|
CHECK(plugin_pane_name("dock_demo", "Scene") == "plugin:dock_demo:Scene");
|
||||||
|
CHECK(plugin_pane_name("key", "a|b;c=d\\e").find_first_of("|;=\\") == std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A pane's saved layout entry is found by pane name", "[PluginHost]")
|
||||||
|
{
|
||||||
|
using Slic3r::GUI::aui_pane_layout_entry;
|
||||||
|
|
||||||
|
const std::string sidebar = "name=sidebar;caption=;state=2099196;dir=4;layer=0;row=0;pos=0;bestw=390;besth=900";
|
||||||
|
// The caption holds an escaped '|', which must not end the entry.
|
||||||
|
const std::string plugin = "name=plugin:demo:Scene;caption=Scene \\| stats;state=2099198;dir=2;layer=0;row=1;pos=0;bestw=320;besth=480";
|
||||||
|
const std::string layout = "layout3|" + sidebar + "|" + plugin + "|dock_size(4,0,0)=392|";
|
||||||
|
|
||||||
|
CHECK(aui_pane_layout_entry(layout, "plugin:demo:Scene") == plugin);
|
||||||
|
CHECK(aui_pane_layout_entry(layout, "sidebar") == sidebar);
|
||||||
|
CHECK(aui_pane_layout_entry(layout, "plugin:demo").empty());
|
||||||
|
CHECK(aui_pane_layout_entry("", "plugin:demo:Scene").empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A reloaded plugin page is recognised by its base URL, fragment aside", "[PluginHost]")
|
||||||
|
{
|
||||||
|
using namespace Slic3r::GUI::web_hosting;
|
||||||
|
|
||||||
|
// A resources path holding a space, which the web view reports escaped.
|
||||||
|
const Slic3r::ScopedResourcesDir resources("web content check");
|
||||||
|
|
||||||
|
// The swapped-in page, then after an in-page anchor and a reload.
|
||||||
|
CHECK(is_content_url(content_base_url()));
|
||||||
|
CHECK(is_content_url(content_base_url() + "#tab2"));
|
||||||
|
wxString escaped = content_base_url();
|
||||||
|
escaped.Replace(" ", "%20");
|
||||||
|
REQUIRE(escaped != content_base_url());
|
||||||
|
CHECK(is_content_url(escaped));
|
||||||
|
CHECK(is_content_url(escaped + "#tab2"));
|
||||||
|
// A page the plugin linked to keeps its own URL and must be left alone.
|
||||||
|
CHECK_FALSE(is_content_url(content_base_url() + "guide.html"));
|
||||||
|
CHECK_FALSE(is_content_url("https://example.com/"));
|
||||||
|
CHECK_FALSE(is_content_url(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
|
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
|
||||||
|
|||||||
Reference in New Issue
Block a user