mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-22 16:32:40 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3124943292 | ||
|
|
8240984ca8 | ||
|
|
ce41b5a3cb | ||
|
|
8fd93907fd | ||
|
|
8056b32450 | ||
|
|
fabd08af1e | ||
|
|
33a8265a11 | ||
|
|
d96682a798 | ||
|
|
64d04a75f3 | ||
|
|
bf22ef2a82 | ||
|
|
dfd3444ae7 |
@@ -58,9 +58,6 @@ 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
|
||||
development loop. Review also covers version bumps, assets, non-default processes and hardware
|
||||
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
|
||||
|
||||
@@ -110,7 +107,6 @@ 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 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) |
|
||||
| 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 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) |
|
||||
|
||||
@@ -52,15 +52,6 @@ a brand means adding a folder here; the folder name is a directory label only
|
||||
collection, so there is no duplicate-name error.
|
||||
- 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
|
||||
|
||||
**A printer vendor tuning a generic.** Keep the `Generic X` base name so the alias shadows the library
|
||||
|
||||
@@ -46,17 +46,12 @@ 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
|
||||
`instantiation: "false"` and no `setting_id` ([the three-part shape](filament-profiles.md#the-three-part-shape)).
|
||||
- `@System` — the OrcaFilamentLibrary selectable shim, and the convention for an all-printer product
|
||||
(`<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)).
|
||||
- `@System` — the OrcaFilamentLibrary selectable shim. 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.
|
||||
Other vendors differ (a bare model, a printer serial, Creality's `@<Model>-all`). Specificity is judged
|
||||
from `compatible_printers`, not the name
|
||||
([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
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ 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 |
|
||||
| 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 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 |
|
||||
|
||||
## 1. Was the vendor `version` bumped?
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
# re-sliced on its own to see whether it changes the G-code
|
||||
# harness - the GUI-vs-CLI parity harness (metrics only, never fails)
|
||||
# Both test the latest successful build_all.yml Linux AppImage from main, with
|
||||
# sources checked out at the commit that build was made from; a manual run can
|
||||
# name another branch, or pin one build by its run id. Nothing here gates a
|
||||
# build or a PR.
|
||||
# sources checked out at the commit that build was made from. Nothing here
|
||||
# gates a build or a PR.
|
||||
name: Parity Nightly
|
||||
|
||||
on:
|
||||
@@ -21,13 +20,9 @@ on:
|
||||
required: false
|
||||
default: "main"
|
||||
build_branch:
|
||||
description: "branch whose newest successful build_all artifact to test (a PR build is the PR merged into its base; sources are checked out at the PR head)"
|
||||
description: "branch whose latest successful build_all artifact to test"
|
||||
required: false
|
||||
default: "main"
|
||||
build_run_id:
|
||||
description: "build_all run id to test instead of build_branch's newest (same PR caveat)"
|
||||
required: false
|
||||
default: ""
|
||||
fixtures:
|
||||
description: "harness fixture ids, space-separated (empty = all)"
|
||||
required: false
|
||||
@@ -55,53 +50,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
BRANCH: ${{ inputs.build_branch || 'main' }}
|
||||
RUN_ID: ${{ inputs.build_run_id }}
|
||||
SCHEDULED: ${{ github.event_name == 'schedule' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "$RUN_ID" ]; then
|
||||
[[ $RUN_ID =~ ^[0-9]+$ ]] || { echo "build_run_id must be a numeric run id, got '$RUN_ID'" >&2; exit 1; }
|
||||
# a pinned build is read directly, not through a search; it must come
|
||||
# from this repository, because the later jobs check out its commit here
|
||||
found=$(gh api "repos/$GH_REPO/actions/runs/$RUN_ID" --jq \
|
||||
'select(.path == ".github/workflows/build_all.yml" and .conclusion == "success"
|
||||
and .head_repository.full_name == env.GH_REPO)
|
||||
| "\(.id) \(.head_sha) \(.created_at)"')
|
||||
[ -n "$found" ] || { echo "run $RUN_ID is not a successful build_all run of $GH_REPO" >&2; exit 1; }
|
||||
else
|
||||
# GitHub serves filtered run listings (branch=, status=, head_sha=, ...)
|
||||
# from a search index that has returned weeks-old results, while the
|
||||
# unfiltered listing stays current, so list unfiltered and filter here.
|
||||
# The repository check keeps out fork PRs whose branch has the same
|
||||
# name. A feature branch is normally built only for its PR, and a PR
|
||||
# build compiles the PR merged into its base rather than head_sha, so
|
||||
# a build of the branch itself (push or dispatch) is preferred when
|
||||
# the same page has one.
|
||||
pick='([.workflow_runs[] | select(.head_branch == env.BRANCH and .conclusion == "success"
|
||||
and .head_repository.full_name == env.GH_REPO)]
|
||||
| map(select(.event != "pull_request"))[0] // .[0])
|
||||
| select(.) | "\(.id) \(.head_sha) \(.created_at)"'
|
||||
# a page of 100 runs spans about a day and a half; a manual run may
|
||||
# target a branch that last built weeks ago
|
||||
pages=3
|
||||
if [ "$SCHEDULED" != true ]; then pages=20; fi
|
||||
found=""
|
||||
for page in $(seq "$pages"); do
|
||||
found=$(gh api "repos/$GH_REPO/actions/workflows/build_all.yml/runs?per_page=100&page=$page" --jq "$pick")
|
||||
if [ -n "$found" ]; then break; fi
|
||||
done
|
||||
[ -n "$found" ] || { echo "no successful $BRANCH build among the last $((pages * 100)) build_all runs; pass build_run_id to test an older one" >&2; exit 1; }
|
||||
fi
|
||||
read -r run_id head_sha created <<< "$found"
|
||||
# the nightly fails rather than report on a stale build
|
||||
if [ "$SCHEDULED" = true ] && [ $(( $(date +%s) - $(date -d "$created" +%s) )) -gt 172800 ]; then
|
||||
echo "newest $BRANCH build $run_id is from $created, over 48 hours old" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'run_id=%s\nhead_sha=%s\n' "$run_id" "$head_sha" >> "$GITHUB_OUTPUT"
|
||||
gh run list --workflow build_all.yml \
|
||||
--branch "${{ inputs.build_branch || 'main' }}" \
|
||||
--status success --limit 1 --json databaseId,headSha \
|
||||
--jq '"run_id=\(.[0].databaseId)\nhead_sha=\(.[0].headSha)"' \
|
||||
>> "$GITHUB_OUTPUT"
|
||||
cat "$GITHUB_OUTPUT"
|
||||
echo "Testing build [$run_id](https://github.com/$GH_REPO/actions/runs/$run_id) of \`$head_sha\`, built $created" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
effect:
|
||||
name: Override sweep effect stage (shard ${{ matrix.shard }})
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Bootstrap page for WebDialog. The real plugin HTML is loaded via
|
||||
<!-- Bootstrap page for PluginWebDialog. The real plugin HTML is loaded via
|
||||
wxWebView::SetPage once this page finishes loading; this file only exists
|
||||
to bring the webview up. -->
|
||||
<html>
|
||||
@@ -1,146 +0,0 @@
|
||||
# /// 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
-5
@@ -8,6 +8,7 @@
|
||||
#include "I18N.hpp"
|
||||
#include "GCode.hpp"
|
||||
#include "Exception.hpp"
|
||||
#include "LifecycleEvents.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "EdgeGrid.hpp"
|
||||
#include "Geometry/ConvexHull.hpp"
|
||||
@@ -2476,6 +2477,14 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
m_writer.set_is_bbl_machine(print->is_BBL_printer());
|
||||
print->set_started(psGCodeExport);
|
||||
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = std::to_string(print->model().id().id);
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
ctx.msg = path;
|
||||
fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx);
|
||||
}
|
||||
|
||||
// check if any custom gcode contains keywords used by the gcode processor to
|
||||
// produce time estimation and gcode toolpaths
|
||||
std::vector<std::pair<std::string, std::string>> validation_res = DoExport::validate_custom_gcode(*print);
|
||||
@@ -2511,12 +2520,22 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
m_processor.set_print(print);
|
||||
GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor);
|
||||
if (! file.is_open()) {
|
||||
BOOST_LOG_TRIVIAL(error) << std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n" << std::endl;
|
||||
std::string err_msg = std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n";
|
||||
BOOST_LOG_TRIVIAL(error) << err_msg << std::endl;
|
||||
if (!fs::exists(folder)) {
|
||||
//fs::create_directory(folder);
|
||||
BOOST_LOG_TRIVIAL(error) << "the parent path " + folder.string() +" is not there!!!" << std::endl;
|
||||
std::string add_err_msg = "the parent path " + folder.string() +" is not there!!!";
|
||||
BOOST_LOG_TRIVIAL(error) << add_err_msg << std::endl;
|
||||
err_msg += add_err_msg;
|
||||
}
|
||||
throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n");
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = std::to_string(print->model().id().id);
|
||||
ctx.code = LifecycleEvtCode::Error;
|
||||
ctx.msg = std::string(path) + "\n" + err_msg;
|
||||
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
|
||||
}
|
||||
throw Slic3r::RuntimeError(err_msg);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -2527,11 +2546,18 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
boost::nowide::remove(path_tmp.c_str());
|
||||
throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n");
|
||||
}
|
||||
} catch (std::exception & /* ex */) {
|
||||
} catch (std::exception &ex) {
|
||||
// Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown.
|
||||
// Close and remove the file.
|
||||
file.close();
|
||||
boost::nowide::remove(path_tmp.c_str());
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = std::to_string(print->model().id().id);
|
||||
ctx.code = LifecycleEvtCode::Error;
|
||||
ctx.msg = std::string(path) + "\n" + ex.what();
|
||||
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
file.close();
|
||||
@@ -2637,6 +2663,13 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
|
||||
std::error_code ret = rename_file(path_tmp, path);
|
||||
if (ret) {
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = std::to_string(print->model().id().id);
|
||||
ctx.code = LifecycleEvtCode::Error;
|
||||
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
|
||||
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
|
||||
}
|
||||
throw Slic3r::RuntimeError(
|
||||
std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "error code " + ret.message() + '\n' +
|
||||
"Is " + path_tmp + " locked?" + '\n');
|
||||
@@ -2647,7 +2680,15 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Exporting G-code finished" << log_memory_info();
|
||||
print->set_done(psGCodeExport);
|
||||
|
||||
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = std::to_string(print->model().id().id);
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
ctx.msg = path;
|
||||
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
|
||||
}
|
||||
|
||||
// Orca: label_object_enabled reflects whether objects are labeled in the g-code (EXCLUDE_OBJECT /
|
||||
// M486), which is driven by exclude_object for every printer
|
||||
if(result != nullptr)
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
#pragma once
|
||||
|
||||
// LifecycleEvents.hpp
|
||||
// --------------------
|
||||
// Application lifecycle events (project, slicing, plate editing, preset, printer connection, and
|
||||
// job activity) that other subsystems -- chiefly the plugin layer above libslic3r -- may want to
|
||||
// observe. Lives in libslic3r rather than the plugin layer because some events fire from inside
|
||||
// the slicing engine itself; see fire_lifecycle_event() below.
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r
|
||||
{
|
||||
enum class LifecycleEvent {
|
||||
// Project (3mf)
|
||||
NewProject,
|
||||
ProjectOpened,
|
||||
ProjectBeforeSave,
|
||||
ProjectAfterSave,
|
||||
ProjectClosed,
|
||||
ProjectDirtyChanged,
|
||||
|
||||
// Slicing pipeline
|
||||
SliceStarted,
|
||||
SliceGeometryFinished,
|
||||
GCodeExportStarted,
|
||||
GCodeExportFinished,
|
||||
SlicingJobComplete,
|
||||
|
||||
// Plate/model editing
|
||||
ObjectAdded,
|
||||
ObjectDeleted,
|
||||
ObjectTransformed,
|
||||
ObjectChanged,
|
||||
ObjectRenamed,
|
||||
PlateCreated,
|
||||
PlateDeleted,
|
||||
PlateSelected,
|
||||
PlateRenamed,
|
||||
|
||||
// Preset
|
||||
PresetSelected,
|
||||
PresetSaved,
|
||||
|
||||
// Printer/device
|
||||
PrintStateChanged,
|
||||
DeviceOnlineChanged,
|
||||
DeviceDiscovered,
|
||||
DeviceSelected,
|
||||
DeviceConnected,
|
||||
DeviceDisconnected,
|
||||
UploadStarted,
|
||||
UploadFinished,
|
||||
|
||||
// Print/send jobs
|
||||
PrintJobStarted,
|
||||
PrintJobFinished,
|
||||
SendJobStarted,
|
||||
SendJobFinished,
|
||||
};
|
||||
|
||||
// Scoped so callers must qualify (LifecycleEvtCode::Error, not ERROR) -- ERROR/OK collide with
|
||||
// Windows macros (wingdi.h) as unqualified names.
|
||||
enum class LifecycleEvtCode { Ok, Error, Warn };
|
||||
|
||||
struct LifecycleEventContext
|
||||
{
|
||||
// The primary subject identifier for the event. This is event-specific (for example, a
|
||||
// project/output path, preset name, device id, or object name), must not contain status or
|
||||
// prose, and may be empty when the event has no single subject.
|
||||
std::string name;
|
||||
|
||||
// Outcome of the operation represented by the event. For state-change and start events,
|
||||
// Ok means that the event occurred; it does not imply that a future operation succeeded.
|
||||
LifecycleEvtCode code = LifecycleEvtCode::Ok;
|
||||
|
||||
// Optional human-readable detail or diagnostic text. It is not a stable parsing contract;
|
||||
// machine-readable data should be represented by a dedicated field or event instead.
|
||||
std::string msg;
|
||||
|
||||
// Stable subject/object identifier, when the source model provides one.
|
||||
std::string id;
|
||||
|
||||
// Previous value for rename and other before/after events.
|
||||
std::string previous_name;
|
||||
|
||||
// Device identifier for printer and job events.
|
||||
std::string device_id;
|
||||
|
||||
// Job identifier when the originating queue/task provides one.
|
||||
std::string job_id;
|
||||
|
||||
// Source subsystem or operation detail, suitable for filtering but not guaranteed to be
|
||||
// exhaustive across versions.
|
||||
std::string source;
|
||||
|
||||
// Plate, object, or volume index when the source uses an index rather than a stable id.
|
||||
int index = -1;
|
||||
|
||||
// Aggregate project dirty state for ProjectDirtyChanged.
|
||||
bool dirty = false;
|
||||
};
|
||||
|
||||
inline std::string lifecycle_event_to_string(LifecycleEvent event)
|
||||
{
|
||||
switch (event) {
|
||||
case LifecycleEvent::NewProject: return "NewProject";
|
||||
case LifecycleEvent::ProjectOpened: return "ProjectOpened";
|
||||
case LifecycleEvent::ProjectBeforeSave: return "ProjectBeforeSave";
|
||||
case LifecycleEvent::ProjectAfterSave: return "ProjectAfterSave";
|
||||
case LifecycleEvent::ProjectClosed: return "ProjectClosed";
|
||||
case LifecycleEvent::ProjectDirtyChanged: return "ProjectDirtyChanged";
|
||||
|
||||
case LifecycleEvent::SliceStarted: return "SliceStarted";
|
||||
case LifecycleEvent::SliceGeometryFinished: return "SliceGeometryFinished";
|
||||
case LifecycleEvent::GCodeExportStarted: return "GCodeExportStarted";
|
||||
case LifecycleEvent::GCodeExportFinished: return "GCodeExportFinished";
|
||||
case LifecycleEvent::SlicingJobComplete: return "SlicingJobComplete";
|
||||
|
||||
case LifecycleEvent::ObjectAdded: return "ObjectAdded";
|
||||
case LifecycleEvent::ObjectDeleted: return "ObjectDeleted";
|
||||
case LifecycleEvent::ObjectTransformed: return "ObjectTransformed";
|
||||
case LifecycleEvent::ObjectChanged: return "ObjectChanged";
|
||||
case LifecycleEvent::ObjectRenamed: return "ObjectRenamed";
|
||||
case LifecycleEvent::PlateCreated: return "PlateCreated";
|
||||
case LifecycleEvent::PlateDeleted: return "PlateDeleted";
|
||||
case LifecycleEvent::PlateSelected: return "PlateSelected";
|
||||
case LifecycleEvent::PlateRenamed: return "PlateRenamed";
|
||||
|
||||
case LifecycleEvent::PresetSelected: return "PresetSelected";
|
||||
case LifecycleEvent::PresetSaved: return "PresetSaved";
|
||||
case LifecycleEvent::PrintStateChanged: return "PrintStateChanged";
|
||||
|
||||
case LifecycleEvent::DeviceOnlineChanged: return "DeviceOnlineChanged";
|
||||
case LifecycleEvent::DeviceDiscovered: return "DeviceDiscovered";
|
||||
case LifecycleEvent::DeviceSelected: return "DeviceSelected";
|
||||
case LifecycleEvent::DeviceConnected: return "DeviceConnected";
|
||||
case LifecycleEvent::DeviceDisconnected: return "DeviceDisconnected";
|
||||
|
||||
case LifecycleEvent::UploadStarted: return "UploadStarted";
|
||||
case LifecycleEvent::UploadFinished: return "UploadFinished";
|
||||
case LifecycleEvent::PrintJobStarted: return "PrintJobStarted";
|
||||
case LifecycleEvent::PrintJobFinished: return "PrintJobFinished";
|
||||
case LifecycleEvent::SendJobStarted: return "SendJobStarted";
|
||||
case LifecycleEvent::SendJobFinished: return "SendJobFinished";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string lifecycle_evt_code_to_string(LifecycleEvtCode code)
|
||||
{
|
||||
switch (code) {
|
||||
case LifecycleEvtCode::Ok: return "Ok";
|
||||
case LifecycleEvtCode::Error: return "Error";
|
||||
case LifecycleEvtCode::Warn: return "Warn";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// Global cross-layer seam (mirrors ConfigBase::set_resolve_capability_fn): any libslic3r code can
|
||||
// fire a lifecycle event without depending on the plugin layer above it, which installs the
|
||||
// dispatcher here at startup. Not tied to Print/GCode specifically, since nothing here should
|
||||
// require callers to hold a Print& just to report an event.
|
||||
using LifecycleHookFn = std::function<void(LifecycleEvent, const LifecycleEventContext&)>;
|
||||
|
||||
inline LifecycleHookFn& lifecycle_hook_fn()
|
||||
{
|
||||
static LifecycleHookFn fn;
|
||||
return fn;
|
||||
}
|
||||
|
||||
inline void set_lifecycle_hook_fn(LifecycleHookFn fn) { lifecycle_hook_fn() = std::move(fn); }
|
||||
|
||||
inline void fire_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx)
|
||||
{
|
||||
if (const LifecycleHookFn& fn = lifecycle_hook_fn(); fn)
|
||||
fn(event, ctx);
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "LifecycleEvents.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "Time.hpp"
|
||||
#include "PlaceholderParser.hpp"
|
||||
@@ -2972,6 +2973,7 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
|
||||
// 1) Find the preset with a new_name or create a new one,
|
||||
// initialize it with the edited config.
|
||||
auto it = this->find_preset_internal(new_name);
|
||||
const bool preset_existed = (it != m_presets.end() && it->name == new_name);
|
||||
if (it != m_presets.end() && it->name == new_name) {
|
||||
// Preset with the same name found.
|
||||
Preset &preset = *it;
|
||||
@@ -3079,6 +3081,14 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
|
||||
this->get_selected_preset().save(&(parent_preset->config));
|
||||
else
|
||||
this->get_selected_preset().save(nullptr);
|
||||
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = new_name;
|
||||
ctx.msg = preset_existed ? "overwrite" : "new";
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
fire_lifecycle_event(LifecycleEvent::PresetSaved, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// A detached standalone preset for the Full Publish receiver: create a user preset holding
|
||||
|
||||
+39
-1
@@ -14,6 +14,7 @@
|
||||
#include "Flow.hpp"
|
||||
#include "Geometry/ConvexHull.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "LifecycleEvents.hpp"
|
||||
#include "ShortestPath.hpp"
|
||||
#include "Thread.hpp"
|
||||
#include "Time.hpp"
|
||||
@@ -2694,6 +2695,13 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
if (m_objects.empty())
|
||||
return;
|
||||
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = std::to_string(m_model.id().id);
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
|
||||
}
|
||||
|
||||
for (PrintObject *obj : m_objects)
|
||||
obj->clear_shared_object();
|
||||
|
||||
@@ -3312,6 +3320,13 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Slicing process finished." << log_memory_info();
|
||||
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = std::to_string(m_model.id().id);
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// G-code export process, running at a background thread.
|
||||
@@ -4911,6 +4926,14 @@ void Print::set_gcode_file_invalidated()
|
||||
//BBS: add gcode file preload logic
|
||||
void Print::export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb)
|
||||
{
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = std::to_string(m_model.id().id);
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
ctx.msg = file;
|
||||
fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx);
|
||||
}
|
||||
|
||||
try {
|
||||
GCodeProcessor processor;
|
||||
GCodeProcessor::s_IsBBLPrinter = is_BBL_printer();
|
||||
@@ -4930,13 +4953,28 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
||||
*result = std::move(processor.extract_result());
|
||||
result->filament_change_sequence = filament_seq_loaded;
|
||||
result->nozzle_change_sequence = nozzle_seq_loaded;
|
||||
} catch (std::exception & /* ex */) {
|
||||
} catch (std::exception &ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = std::to_string(m_model.id().id);
|
||||
ctx.code = LifecycleEvtCode::Error;
|
||||
ctx.msg = file + "\n" + ex.what();
|
||||
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
|
||||
}
|
||||
throw Slic3r::RuntimeError(
|
||||
std::string("Failed to process the G-code file ") + file + " from previous 3mf\n");
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": process the G-code file %1% successfully")%file.c_str();
|
||||
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = std::to_string(m_model.id().id);
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
ctx.msg = file;
|
||||
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
std::tuple<float, float> Print::object_skirt_offset(double margin_height) const
|
||||
|
||||
@@ -139,16 +139,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/TerminalDialog.hpp
|
||||
GUI/PluginProgressDialog.cpp
|
||||
GUI/PluginProgressDialog.hpp
|
||||
GUI/WebDialog.cpp
|
||||
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/PluginWebDialog.cpp
|
||||
GUI/PluginWebDialog.hpp
|
||||
GUI/DragCanvas.cpp
|
||||
GUI/DragCanvas.hpp
|
||||
GUI/EditGCodeDialog.cpp
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#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
|
||||
@@ -1,11 +0,0 @@
|
||||
#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
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
|
||||
#include "libslic3r/Time.hpp"
|
||||
@@ -355,7 +356,10 @@ namespace Slic3r
|
||||
obj->bind_state = "free";
|
||||
|
||||
obj->last_alive = Slic3r::Utils::get_current_time_utc();
|
||||
obj->m_is_online = true;
|
||||
// Route through set_online_state() (rather than writing m_is_online directly) so the
|
||||
// DeviceOnlineChanged lifecycle event fires consistently; same effective value/behavior
|
||||
// here since the object was already online in the common case.
|
||||
obj->set_online_state(true);
|
||||
obj->set_dev_name(dev_name);
|
||||
/* if (!obj->dev_ip.empty()) {
|
||||
Slic3r::GUI::wxGetApp().app_config->set_str("ip_address", obj->dev_id, obj->dev_ip);
|
||||
@@ -373,6 +377,10 @@ namespace Slic3r
|
||||
obj->bind_sec_link = sec_link;
|
||||
obj->dev_connection_name = connection_name;
|
||||
obj->bind_ssdp_version = ssdp_version;
|
||||
// Discovery establishes the initial reachability state. Do not report it as an
|
||||
// online transition; DeviceDiscovered below is the lifecycle event for a new
|
||||
// device. Subsequent updates route through set_online_state(), so a known device
|
||||
// still emits DeviceOnlineChanged when its reachability actually changes.
|
||||
obj->m_is_online = true;
|
||||
|
||||
//load access code
|
||||
@@ -389,6 +397,15 @@ namespace Slic3r
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " New Machine, dev_id= " << dev_id
|
||||
<< ", ip = " << dev_ip <<", printer_name = " << dev_name
|
||||
<< ", con_type= " << connect_type <<", signal= " << printer_signal << ", bind_state= " << bind_state;
|
||||
|
||||
// First discovery of a genuinely new device (not a periodic SSDP/heartbeat update to
|
||||
// an already-known one, which is handled in the branch above).
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = dev_id;
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
fire_lifecycle_event(LifecycleEvent::DeviceDiscovered, ctx);
|
||||
}
|
||||
}
|
||||
update_local_machine(*obj);
|
||||
}
|
||||
@@ -984,8 +1001,14 @@ namespace Slic3r
|
||||
}
|
||||
|
||||
void DeviceManager::OnSelectedMachineChanged(const std::string& /*pre_dev_id*/,
|
||||
const std::string& /*new_dev_id*/)
|
||||
const std::string& new_dev_id)
|
||||
{
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = new_dev_id; // empty string is a valid deselection
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
fire_lifecycle_event(LifecycleEvent::DeviceSelected, ctx);
|
||||
}
|
||||
if (MachineObject* obj_ = get_selected_machine()) {
|
||||
GUI::wxGetApp().sidebar().update_sync_status(obj_);
|
||||
if(m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "libslic3r/Time.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
|
||||
@@ -2626,7 +2627,15 @@ void MachineObject::reset()
|
||||
|
||||
void MachineObject::set_print_state(std::string status)
|
||||
{
|
||||
const bool changed = (print_status != status);
|
||||
print_status = status;
|
||||
if (changed) {
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = dev_id;
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
ctx.msg = print_status;
|
||||
fire_lifecycle_event(LifecycleEvent::PrintStateChanged, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
int MachineObject::connect(bool use_openssl)
|
||||
@@ -2648,6 +2657,10 @@ int MachineObject::connect(bool use_openssl)
|
||||
int MachineObject::disconnect()
|
||||
{
|
||||
if (m_agent) {
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = dev_id;
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx);
|
||||
return m_agent->disconnect_printer();
|
||||
}
|
||||
return -1;
|
||||
@@ -2678,8 +2691,16 @@ bool MachineObject::is_connecting()
|
||||
|
||||
void MachineObject::set_online_state(bool on_off)
|
||||
{
|
||||
const bool changed = (m_is_online != on_off);
|
||||
m_is_online = on_off;
|
||||
if (!on_off) m_active_state = NotActive;
|
||||
if (changed) {
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = dev_id;
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
ctx.msg = on_off ? "online" : "offline";
|
||||
fire_lifecycle_event(LifecycleEvent::DeviceOnlineChanged, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
bool MachineObject::is_info_ready(bool check_version) const
|
||||
@@ -4601,8 +4622,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
try {
|
||||
if (j.contains("event")) {
|
||||
if (j["event"].contains("event")) {
|
||||
if (j["event"]["event"].get<std::string>() == "client.disconnected")
|
||||
if (j["event"]["event"].get<std::string>() == "client.disconnected") {
|
||||
set_online_state(false);
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = dev_id;
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx);
|
||||
}
|
||||
else if (j["event"]["event"].get<std::string>() == "client.connected")
|
||||
set_online_state(true);
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
#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
|
||||
@@ -1,54 +0,0 @@
|
||||
#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
|
||||
@@ -43,6 +43,7 @@
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp"
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
#include "slic3r/Utils/MacDarkMode.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
#include <slic3r/GUI/GUI_Utils.hpp>
|
||||
|
||||
@@ -5026,8 +5027,16 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
|
||||
//BBS: nofity object list to update
|
||||
wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items();
|
||||
|
||||
if (object_moved)
|
||||
if (object_moved) {
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
ctx.msg = "moved";
|
||||
if (done.size() == 1)
|
||||
ctx.name = m_model->objects[done.begin()->first]->name;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
|
||||
|
||||
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED));
|
||||
}
|
||||
|
||||
// BBS: support wipe-tower for multi-plates
|
||||
for (int plate_id = 0; plate_id < wipe_tower_origins.size(); plate_id++) {
|
||||
@@ -5148,8 +5157,16 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type)
|
||||
//BBS: nofity object list to update
|
||||
wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items();
|
||||
|
||||
if (!done.empty())
|
||||
if (!done.empty()) {
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
ctx.msg = "rotated";
|
||||
if (done.size() == 1)
|
||||
ctx.name = m_model->objects[done.begin()->first]->name;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
|
||||
|
||||
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED));
|
||||
}
|
||||
|
||||
m_dirty = true;
|
||||
}
|
||||
@@ -5240,8 +5257,16 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type)
|
||||
//BBS: notify object info update
|
||||
wxGetApp().plater()->show_object_info();
|
||||
|
||||
if (!done.empty())
|
||||
if (!done.empty()) {
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
ctx.msg = "scaled";
|
||||
if (done.size() == 1)
|
||||
ctx.name = m_model->objects[done.begin()->first]->name;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
|
||||
|
||||
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED));
|
||||
}
|
||||
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
@@ -2182,6 +2182,11 @@ void GUI_App::init_networking_callbacks()
|
||||
obj->command_get_access_code();
|
||||
if (m_agent)
|
||||
m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer());
|
||||
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = obj->get_dev_id();
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
fire_lifecycle_event(LifecycleEvent::DeviceConnected, ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2220,6 +2225,11 @@ void GUI_App::init_networking_callbacks()
|
||||
obj->command_get_version();
|
||||
event.SetInt(0);
|
||||
event.SetString(obj->get_dev_id());
|
||||
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = obj->get_dev_id();
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
fire_lifecycle_event(LifecycleEvent::DeviceConnected, ctx);
|
||||
} else if (state == ConnectStatus::ConnectStatusFailed) {
|
||||
// Orca: only update status if same device id
|
||||
if (m_device_manager->selected_machine != dev_id) return;
|
||||
@@ -2235,10 +2245,22 @@ void GUI_App::init_networking_callbacks()
|
||||
wxGetApp().show_dialog(text);
|
||||
}
|
||||
event.SetInt(-1);
|
||||
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = dev_id;
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx);
|
||||
}
|
||||
} else if (state == ConnectStatus::ConnectStatusLost) {
|
||||
m_device_manager->set_selected_machine("");
|
||||
event.SetInt(-1);
|
||||
BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = lost";
|
||||
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = dev_id;
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx);
|
||||
} else {
|
||||
event.SetInt(-1);
|
||||
BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = " << state;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "GUI_ObjectList.hpp"
|
||||
#include "GUI_Factories.hpp"
|
||||
//#include "GUI_ObjectLayers.hpp"
|
||||
@@ -10,6 +11,7 @@
|
||||
#include "BitmapComboBox.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
#include "OptionsGroup.hpp"
|
||||
#include "Tab.hpp"
|
||||
@@ -1159,17 +1161,39 @@ void ObjectList::update_name_in_model(const wxDataViewItem& item) const
|
||||
if (m_objects_model->GetItemType(item) & itObject) {
|
||||
std::string name = m_objects_model->GetName(item).ToUTF8().data();
|
||||
if (obj->name != name) {
|
||||
const std::string previous_name = obj->name;
|
||||
obj->name = name;
|
||||
// if object has just one volume, rename this volume too
|
||||
if (obj->volumes.size() == 1)
|
||||
obj->volumes[0]->name = obj->name;
|
||||
Slic3r::save_object_mesh(*obj);
|
||||
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = name;
|
||||
ctx.previous_name = previous_name;
|
||||
ctx.id = std::to_string(obj->id().id);
|
||||
ctx.index = obj_idx;
|
||||
ctx.source = "object";
|
||||
fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (volume_id < 0) return;
|
||||
obj->volumes[volume_id]->name = m_objects_model->GetName(item).ToUTF8().data();
|
||||
std::string name = m_objects_model->GetName(item).ToUTF8().data();
|
||||
if (obj->volumes[volume_id]->name == name)
|
||||
return;
|
||||
|
||||
const std::string previous_name = obj->volumes[volume_id]->name;
|
||||
obj->volumes[volume_id]->name = name;
|
||||
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = name;
|
||||
ctx.previous_name = previous_name;
|
||||
ctx.id = std::to_string(obj->volumes[volume_id]->id().id);
|
||||
ctx.index = volume_id;
|
||||
ctx.source = "volume";
|
||||
fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx);
|
||||
}
|
||||
|
||||
void ObjectList::update_name_in_list(int obj_idx, int vol_idx) const
|
||||
@@ -3519,7 +3543,14 @@ void ObjectList::delete_all_connectors_for_object(int obj_idx)
|
||||
obj->delete_connectors();
|
||||
|
||||
if (obj->volumes.empty() || !obj->has_solid_mesh()) {
|
||||
const std::string deleted_obj_name = obj->name;
|
||||
model.delete_object(idx);
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = deleted_obj_name;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
|
||||
}
|
||||
m_objects_model->Delete(m_objects_model->GetItemById(idx));
|
||||
continue;
|
||||
}
|
||||
@@ -4088,6 +4119,13 @@ void ObjectList::add_object_to_list(size_t obj_idx, bool call_selection_changed,
|
||||
const auto item = m_objects_model->AddObject(model_object, warning_bitmap, model_object->is_cut());
|
||||
Expand(m_objects_model->GetParent(item));
|
||||
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = model_object->name;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectAdded, ctx);
|
||||
}
|
||||
|
||||
if (!do_info_update)
|
||||
return;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "slic3r/GUI/NotificationManager.hpp"
|
||||
#include "slic3r/GUI/format.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
#include "libnest2d/common.hpp"
|
||||
|
||||
@@ -698,6 +699,13 @@ void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) {
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":arrange m_unprintable: name: %4%, bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale<double>(ap.translation(X)) % unscale<double>(ap.translation(Y)) % ap.name;
|
||||
}
|
||||
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
ctx.msg = "arranged";
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
|
||||
}
|
||||
|
||||
m_plater->update();
|
||||
// BBS
|
||||
//wxGetApp().obj_manipul()->set_dirty();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
#include "libnest2d/common.hpp"
|
||||
|
||||
#include <numeric>
|
||||
@@ -347,6 +348,13 @@ void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
m_plater->arrange();
|
||||
}
|
||||
m_plater->update();
|
||||
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
ctx.msg = "arranged";
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
m_plater->mark_plate_toolbar_image_dirty();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/NotificationManager.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
|
||||
|
||||
@@ -254,6 +255,12 @@ void OrientJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
mesh.apply();
|
||||
}
|
||||
|
||||
if (!m_selected.empty()) {
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
ctx.msg = "auto_oriented";
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
|
||||
}
|
||||
|
||||
m_plater->update();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "PrintJob.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
@@ -133,6 +134,13 @@ wxString PrintJob::get_http_error_msg(unsigned int status, std::string body)
|
||||
|
||||
void PrintJob::process(Ctl &ctl)
|
||||
{
|
||||
LifecycleEventContext start_ctx;
|
||||
start_ctx.name = m_project_name;
|
||||
start_ctx.device_id = m_dev_id;
|
||||
start_ctx.source = "print_job";
|
||||
fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx);
|
||||
m_lifecycle_started = true;
|
||||
|
||||
/* display info */
|
||||
std::string msg;
|
||||
int curr_percent = 10;
|
||||
@@ -686,6 +694,7 @@ void PrintJob::process(Ctl &ctl)
|
||||
}
|
||||
wxQueueEvent(m_plater, evt);
|
||||
m_job_finished = true;
|
||||
m_lifecycle_success = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,6 +707,19 @@ void PrintJob::finalize(bool canceled, std::exception_ptr &eptr) {
|
||||
eptr = std::current_exception();
|
||||
}
|
||||
|
||||
if (m_lifecycle_started && !m_lifecycle_finished) {
|
||||
LifecycleEventContext finish_ctx;
|
||||
finish_ctx.name = m_project_name;
|
||||
finish_ctx.device_id = m_dev_id;
|
||||
finish_ctx.source = "print_job";
|
||||
finish_ctx.code = canceled ? LifecycleEvtCode::Warn :
|
||||
(eptr || !m_lifecycle_success ? LifecycleEvtCode::Error : LifecycleEvtCode::Ok);
|
||||
finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" :
|
||||
(m_lifecycle_success ? "" : "failed"));
|
||||
fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx);
|
||||
m_lifecycle_finished = true;
|
||||
}
|
||||
|
||||
if (canceled || eptr)
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ class PrintJob : public Job
|
||||
std::function<void()> m_success_fun{nullptr};
|
||||
std::string m_dev_id;
|
||||
bool m_job_finished{ false };
|
||||
bool m_lifecycle_started{ false };
|
||||
bool m_lifecycle_finished{ false };
|
||||
bool m_lifecycle_success{ false };
|
||||
int m_print_job_completed_id = 0;
|
||||
wxString m_completed_evt_data;
|
||||
std::function<void()> m_enter_ip_address_fun_fail{ nullptr };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "SendJob.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
@@ -148,6 +149,13 @@ void SendJob::process(Ctl &ctl)
|
||||
}
|
||||
}
|
||||
|
||||
LifecycleEventContext start_ctx;
|
||||
start_ctx.name = m_project_name;
|
||||
start_ctx.device_id = m_dev_id;
|
||||
start_ctx.source = "send_job";
|
||||
fire_lifecycle_event(LifecycleEvent::SendJobStarted, start_ctx);
|
||||
m_lifecycle_started = true;
|
||||
|
||||
int total_plate_num = m_plater->get_partplate_list().get_plate_count();
|
||||
|
||||
PartPlate* plate = m_plater->get_partplate_list().get_plate(job_data.plate_idx);
|
||||
@@ -424,6 +432,19 @@ void SendJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
eptr = std::current_exception();
|
||||
}
|
||||
|
||||
if (m_lifecycle_started && !m_lifecycle_finished) {
|
||||
LifecycleEventContext finish_ctx;
|
||||
finish_ctx.name = m_project_name;
|
||||
finish_ctx.device_id = m_dev_id;
|
||||
finish_ctx.source = "send_job";
|
||||
finish_ctx.code = canceled ? LifecycleEvtCode::Warn :
|
||||
(eptr ? LifecycleEvtCode::Error : (m_job_finished ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error));
|
||||
finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" :
|
||||
(m_job_finished ? "" : "failed"));
|
||||
fire_lifecycle_event(LifecycleEvent::SendJobFinished, finish_ctx);
|
||||
m_lifecycle_finished = true;
|
||||
}
|
||||
|
||||
if (canceled || eptr)
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ class SendJob : public Job
|
||||
PrintPrepareData job_data;
|
||||
std::string m_dev_id;
|
||||
bool m_job_finished{ false };
|
||||
bool m_lifecycle_started{ false };
|
||||
bool m_lifecycle_finished{ false };
|
||||
int m_print_job_completed_id = 0;
|
||||
bool m_is_check_mode{false};
|
||||
bool m_check_and_continue{false};
|
||||
|
||||
@@ -1189,8 +1189,6 @@ void MainFrame::shutdown()
|
||||
if (m_project != nullptr)
|
||||
m_project->shutdown();
|
||||
m_plugin_pages.shutdown();
|
||||
if (m_plater != nullptr)
|
||||
m_plater->remove_dock_panes();
|
||||
#ifdef __WXGTK__
|
||||
// Edge panels are child windows — wxWidgets destroys them automatically.
|
||||
m_edge_bottom = nullptr;
|
||||
@@ -3426,6 +3424,11 @@ void MainFrame::init_menubar_as_editor()
|
||||
wxGetApp().open_preferences();
|
||||
},
|
||||
"", nullptr, []() { return true; }, this, 1);
|
||||
parent_menu->AppendSeparator();
|
||||
append_shortcut_item(
|
||||
parent_menu, Shortcut::SpeedDial, false, _L("Open speed dial"), "",
|
||||
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
|
||||
"", nullptr, []() { return true; }, this);
|
||||
//parent_menu->Insert(1, preference_item);
|
||||
#endif
|
||||
// Help menu
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "libslic3r/Tesselate.hpp"
|
||||
#include "libslic3r/GCode/ThumbnailData.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
|
||||
#include "I18N.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
@@ -2626,11 +2627,20 @@ void PartPlate::set_plate_name(const std::string& name)
|
||||
if (boost::equals(m_name, name))
|
||||
return;
|
||||
|
||||
const std::string previous_name = m_name;
|
||||
m_name = name;
|
||||
if (m_print != nullptr)
|
||||
m_print->set_plate_name(name);
|
||||
|
||||
invalidate_plate_name_texture();
|
||||
|
||||
if (m_plater != nullptr && !m_plater->is_loading_project()) {
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = name;
|
||||
ctx.previous_name = previous_name;
|
||||
ctx.index = m_plate_index;
|
||||
fire_lifecycle_event(LifecycleEvent::PlateRenamed, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
//get the print's object, result and index
|
||||
@@ -4741,9 +4751,16 @@ int PartPlateList::create_plate(bool adjust_position)
|
||||
|
||||
if (m_plater) {
|
||||
// In GUI mode
|
||||
wxGetApp().obj_list()->on_plate_added(plate);
|
||||
wxGetApp().obj_list()->on_plate_added(plate);
|
||||
}
|
||||
|
||||
if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) {
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = plate->get_plate_name();
|
||||
ctx.index = new_index;
|
||||
fire_lifecycle_event(LifecycleEvent::PlateCreated, ctx);
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":created a new plate %1%") % new_index;
|
||||
return new_index;
|
||||
}
|
||||
@@ -4842,6 +4859,7 @@ int PartPlateList::delete_plate(int index)
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate %1%, has an invalid index %2%") % index % plate->get_index();
|
||||
return -1;
|
||||
}
|
||||
const std::string plate_name = plate->get_plate_name();
|
||||
|
||||
if (m_plater) {
|
||||
// In GUI mode
|
||||
@@ -4924,6 +4942,13 @@ int PartPlateList::delete_plate(int index)
|
||||
|
||||
delete plate;
|
||||
|
||||
if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) {
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = plate_name;
|
||||
ctx.index = index;
|
||||
fire_lifecycle_event(LifecycleEvent::PlateDeleted, ctx);
|
||||
}
|
||||
|
||||
// FIX: context of BackgroundSliceProcess and gcode preview need to be updated before ObjectList::reload_all_plates().
|
||||
#if 0
|
||||
if (m_plater != nullptr) {
|
||||
@@ -5032,6 +5057,7 @@ int PartPlateList::select_plate(int index)
|
||||
if (m_plate_list.empty() || index >= m_plate_list.size()) {
|
||||
return -1;
|
||||
}
|
||||
const int previous_index = m_current_plate;
|
||||
|
||||
// BBS: erase unnecessary snapshot
|
||||
if (get_curr_plate_index() != index && m_intialized) {
|
||||
@@ -5058,6 +5084,13 @@ int PartPlateList::select_plate(int index)
|
||||
//wxQueueEvent(m_plater, new SimpleEvent(EVT_GLCANVAS_PLATE_SELECT));
|
||||
}
|
||||
|
||||
if (previous_index != index && m_intialized && m_plater != nullptr && !m_plater->is_loading_project()) {
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = m_plate_list[index]->get_plate_name();
|
||||
ctx.index = index;
|
||||
fire_lifecycle_event(LifecycleEvent::PlateSelected, ctx);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
+105
-146
@@ -57,6 +57,7 @@
|
||||
#include <wx/aui/aui.h>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "libslic3r/Format/STL.hpp"
|
||||
#include "libslic3r/Format/DRC.hpp"
|
||||
#include "libslic3r/Format/STEP.hpp"
|
||||
@@ -87,7 +88,6 @@
|
||||
#ifdef __WXGTK__
|
||||
#include "LinuxDisplayBackend.hpp"
|
||||
#endif
|
||||
#include "AuiPaneLayout.hpp"
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "GUI_Factories.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
@@ -6749,14 +6749,6 @@ struct Plater::priv
|
||||
|
||||
// GUI elements
|
||||
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;
|
||||
wxPanel* current_panel{ nullptr };
|
||||
std::vector<wxPanel*> panels;
|
||||
@@ -6929,11 +6921,6 @@ struct Plater::priv
|
||||
void update_sidebar(bool force_update = false);
|
||||
void reset_window_layout();
|
||||
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(); }
|
||||
|
||||
@@ -7514,18 +7501,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
panel_3d->SetSizer(panel_sizer);
|
||||
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();
|
||||
|
||||
{
|
||||
@@ -8191,14 +8166,6 @@ 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) {
|
||||
notification_manager->set_sidebar_collapsed(sidebar.IsShown());
|
||||
m_aui_mgr.Update();
|
||||
@@ -8208,96 +8175,10 @@ void Plater::priv::update_sidebar(bool force_update) {
|
||||
void Plater::priv::reset_window_layout()
|
||||
{
|
||||
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;
|
||||
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() {
|
||||
if (!sidebar_layout.is_enabled) {
|
||||
return Sidebar::None;
|
||||
@@ -10264,7 +10145,14 @@ void Plater::priv::remove(size_t obj_idx)
|
||||
view3D->enable_layers_editing(false);
|
||||
|
||||
m_worker.cancel_all();
|
||||
std::string obj_name = (obj_idx < model.objects.size()) ? model.objects[obj_idx]->name : std::to_string(obj_idx);
|
||||
model.delete_object(obj_idx);
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = obj_name;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
|
||||
}
|
||||
//BBS: notify partplate the instance removed
|
||||
partplate_list.notify_instance_removed(obj_idx, -1);
|
||||
update();
|
||||
@@ -10297,7 +10185,14 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia
|
||||
if (obj->is_cut())
|
||||
sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx);
|
||||
|
||||
std::string obj_name = obj->name;
|
||||
model.delete_object(obj_idx);
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = obj_name;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
|
||||
}
|
||||
//BBS: notify partplate the instance removed
|
||||
partplate_list.notify_instance_removed(obj_idx, -1);
|
||||
|
||||
@@ -10343,10 +10238,22 @@ void Plater::priv::delete_all_objects_from_model()
|
||||
|
||||
void Plater::priv::reset(bool apply_presets_change)
|
||||
{
|
||||
// TakeSnapshot below and load_current_presets() further down each re-evaluate the
|
||||
// aggregate dirty flag against a baseline that hasn't been reset yet, so they can toggle
|
||||
// is_dirty() back and forth several times before it settles; coalesce those into one event.
|
||||
ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(dirty_state);
|
||||
|
||||
Plater::TakeSnapshot snapshot(q, _u8L("Reset Project"), UndoRedo::SnapshotType::ProjectSeparator);
|
||||
|
||||
clear_warnings();
|
||||
|
||||
const std::string closed_project_name = into_u8(get_project_filename());
|
||||
if (!closed_project_name.empty() || !model.objects.empty()) {
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = closed_project_name;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectClosed, ctx);
|
||||
}
|
||||
// A new project must not inherit the previous project's published selection (Feature A/B).
|
||||
m_has_pending_published = false;
|
||||
m_pending_published_keys.clear();
|
||||
@@ -12774,11 +12681,15 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
||||
notification_manager->set_slicing_progress_export_possible();
|
||||
|
||||
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
|
||||
const std::string lifecycle_job_name = this->background_process.fff_print() ?
|
||||
this->background_process.fff_print()->output_filename() : std::string();
|
||||
this->background_process.reset_export();
|
||||
// This bool stops showing export finished notification even when process_completed_with_error is false
|
||||
bool has_error = false;
|
||||
std::string lifecycle_error_msg;
|
||||
if (evt.error()) {
|
||||
auto message = evt.format_error_message();
|
||||
lifecycle_error_msg = message.first;
|
||||
if (evt.critical_error()) {
|
||||
if (q->m_tracking_popup_menu) {
|
||||
// We don't want to pop-up a message box when tracking a pop-up menu.
|
||||
@@ -12816,6 +12727,14 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
||||
is_finished = true;
|
||||
}
|
||||
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = lifecycle_job_name;
|
||||
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
|
||||
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
|
||||
}
|
||||
|
||||
//BBS: set the current plater's slice result to valid
|
||||
if (!this->background_process.empty())
|
||||
this->background_process.get_current_plate()->update_slice_result_valid_state(evt.success());
|
||||
@@ -15349,21 +15268,33 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_
|
||||
//get_partplate_list().reinit();
|
||||
//get_partplate_list().update_slice_context_to_current_plate(p->background_process);
|
||||
//p->preview->update_gcode_result(p->partplate_list.get_current_slice_result());
|
||||
reset(transfer_preset_changes);
|
||||
reset_project_dirty_after_save();
|
||||
reset_project_dirty_initial_presets();
|
||||
wxGetApp().update_saved_preset_from_current_preset();
|
||||
update_project_dirty_from_presets();
|
||||
if (!silent) {
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::NewProject, ctx);
|
||||
}
|
||||
{
|
||||
// Same rationale as in Plater::priv::reset(): the whole reset + preset-reload +
|
||||
// baseline-reset sequence below settles into its final dirty state only once it
|
||||
// completes, so hold notifications until then to avoid firing on transient flips.
|
||||
ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(p->dirty_state);
|
||||
|
||||
//reset project
|
||||
p->project.reset();
|
||||
//set project name
|
||||
if (project_name.empty())
|
||||
p->set_project_name(_L("Untitled"));
|
||||
else
|
||||
p->set_project_name(project_name);
|
||||
reset(transfer_preset_changes);
|
||||
reset_project_dirty_after_save();
|
||||
reset_project_dirty_initial_presets();
|
||||
wxGetApp().update_saved_preset_from_current_preset();
|
||||
update_project_dirty_from_presets();
|
||||
|
||||
Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator);
|
||||
//reset project
|
||||
p->project.reset();
|
||||
//set project name
|
||||
if (project_name.empty())
|
||||
p->set_project_name(_L("Untitled"));
|
||||
else
|
||||
p->set_project_name(project_name);
|
||||
|
||||
Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator);
|
||||
}
|
||||
|
||||
Model m;
|
||||
model().load_from(m); // new id avoid same path name
|
||||
@@ -15481,6 +15412,13 @@ void Plater::load_project(wxString const& filename2,
|
||||
p->set_project_name(_L("Untitled"));
|
||||
}
|
||||
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = into_u8(load_restore ? originfile : filename);
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectOpened, ctx);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (using_exported_file()) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename;
|
||||
@@ -15552,11 +15490,23 @@ int Plater::save_project(bool saveAs)
|
||||
if (full_pathnames) {
|
||||
save_strategy = save_strategy | SaveStrategy::FullPathSources;
|
||||
}
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = into_u8(filename);
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectBeforeSave, ctx);
|
||||
}
|
||||
if (export_3mf(into_path(filename), save_strategy) < 0) {
|
||||
MessageDialog(this, _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs have the project file open."),
|
||||
_L("Save project"), wxOK | wxICON_WARNING).ShowModal();
|
||||
return wxID_CANCEL;
|
||||
}
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = into_u8(filename);
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectAfterSave, ctx);
|
||||
}
|
||||
|
||||
Slic3r::remove_backup(model(), false);
|
||||
|
||||
@@ -17892,19 +17842,6 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_
|
||||
|
||||
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
|
||||
void Plater::select_curr_plate_all() { p->select_curr_plate_all(); }
|
||||
void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); }
|
||||
@@ -20889,6 +20826,14 @@ void Plater::changed_object(ModelObject &object){
|
||||
|
||||
// Check outside bed
|
||||
get_current_canvas3D()->requires_check_outside_state();
|
||||
|
||||
if (!is_loading_project()) {
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = object.name;
|
||||
ctx.id = std::to_string(object.id().id);
|
||||
ctx.source = "geometry";
|
||||
fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
void Plater::changed_object(int obj_idx)
|
||||
@@ -20925,6 +20870,20 @@ void Plater::changed_objects(const std::vector<size_t>& object_idxs)
|
||||
|
||||
// update print
|
||||
this->p->schedule_background_process();
|
||||
|
||||
if (!is_loading_project()) {
|
||||
for (size_t obj_idx : object_idxs) {
|
||||
if (obj_idx >= p->model.objects.size() || p->model.objects[obj_idx] == nullptr)
|
||||
continue;
|
||||
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = p->model.objects[obj_idx]->name;
|
||||
ctx.id = std::to_string(p->model.objects[obj_idx]->id().id);
|
||||
ctx.index = static_cast<int>(obj_idx);
|
||||
ctx.source = "geometry";
|
||||
fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Plater::schedule_background_process(bool schedule/* = true*/)
|
||||
|
||||
@@ -476,17 +476,6 @@ public:
|
||||
|
||||
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.
|
||||
// Update the UI based on the current preferences.
|
||||
void update_ui_from_settings();
|
||||
|
||||
@@ -1,23 +1,80 @@
|
||||
#include "WebDialog.hpp"
|
||||
#include "PluginWebDialog.hpp"
|
||||
|
||||
#include "slic3r/GUI/GUI.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/uri.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
WebDialog::WebDialog(wxWindow* parent,
|
||||
const wxString& title,
|
||||
const std::string& html,
|
||||
const wxSize& size,
|
||||
MessageHandler on_message,
|
||||
SubmitHandler on_submit,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed,
|
||||
long wx_style)
|
||||
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";
|
||||
|
||||
// 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 std::string& html,
|
||||
const wxSize& size,
|
||||
MessageHandler on_message,
|
||||
SubmitHandler on_submit,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed,
|
||||
long wx_style)
|
||||
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
|
||||
, m_html(html)
|
||||
, m_on_message(std::move(on_message))
|
||||
@@ -27,7 +84,7 @@ WebDialog::WebDialog(wxWindow* parent,
|
||||
{
|
||||
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
||||
// is swapped in via SetPage once the bootstrap finishes loading.
|
||||
create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240));
|
||||
create_webview("web/dialog/PluginWebDialog/blank.html", title, size, wxSize(320, 240));
|
||||
|
||||
// Paint the window/webview in the themed background so there is no white
|
||||
// flash before the (transparent) bootstrap page and plugin HTML render.
|
||||
@@ -39,22 +96,22 @@ WebDialog::WebDialog(wxWindow* parent,
|
||||
// create_webview() via add_user_scripts(); nothing to add here.
|
||||
// Swap in the plugin HTML once the bootstrap page settles. Bind ERROR too so a
|
||||
// missing/blocked bootstrap resource (e.g. a packaged build) still triggers it.
|
||||
Bind(wxEVT_WEBVIEW_LOADED, &WebDialog::on_bootstrap_event, this, wv->GetId());
|
||||
Bind(wxEVT_WEBVIEW_ERROR, &WebDialog::on_bootstrap_event, this, wv->GetId());
|
||||
Bind(wxEVT_WEBVIEW_NAVIGATED, &WebDialog::on_navigated, this, wv->GetId());
|
||||
Bind(wxEVT_WEBVIEW_LOADED, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
|
||||
Bind(wxEVT_WEBVIEW_ERROR, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
|
||||
Bind(wxEVT_WEBVIEW_NAVIGATED, &PluginWebDialog::on_navigated, this, wv->GetId());
|
||||
}
|
||||
Bind(wxEVT_CLOSE_WINDOW, &WebDialog::on_close_window, this);
|
||||
Bind(wxEVT_CLOSE_WINDOW, &PluginWebDialog::on_close_window, this);
|
||||
}
|
||||
|
||||
void WebDialog::add_user_scripts()
|
||||
void PluginWebDialog::add_user_scripts()
|
||||
{
|
||||
if (wxWebView* wv = browser()) {
|
||||
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
|
||||
wv->AddUserScript(wxString::FromUTF8(web_hosting::orca_bridge_script()));
|
||||
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
|
||||
wv->AddUserScript(ORCA_BRIDGE_JS);
|
||||
}
|
||||
}
|
||||
|
||||
WebDialog::~WebDialog()
|
||||
PluginWebDialog::~PluginWebDialog()
|
||||
{
|
||||
// Runs on every destruction path. Deliberately NOT a wxEVT_DESTROY handler:
|
||||
// that event is sent from the base ~wxDialog(), after this subclass's members
|
||||
@@ -64,19 +121,19 @@ WebDialog::~WebDialog()
|
||||
m_on_destroyed();
|
||||
}
|
||||
|
||||
void WebDialog::post_message(WebDialog* dialog, const nlohmann::json& data)
|
||||
void PluginWebDialog::post_message(PluginWebDialog* dialog, const nlohmann::json& data)
|
||||
{
|
||||
if (dialog != nullptr && dialog->is_open())
|
||||
dialog->push_message(data);
|
||||
}
|
||||
|
||||
void WebDialog::request_close(WebDialog* dialog)
|
||||
void PluginWebDialog::request_close(PluginWebDialog* dialog)
|
||||
{
|
||||
if (dialog != nullptr)
|
||||
dialog->Close();
|
||||
}
|
||||
|
||||
void WebDialog::destroy_silently(WebDialog* dialog)
|
||||
void PluginWebDialog::destroy_for_plugin(PluginWebDialog* dialog)
|
||||
{
|
||||
if (dialog == nullptr)
|
||||
return;
|
||||
@@ -90,42 +147,42 @@ void WebDialog::destroy_silently(WebDialog* dialog)
|
||||
dialog->Destroy();
|
||||
}
|
||||
|
||||
void WebDialog::on_bootstrap_event(wxWebViewEvent& event)
|
||||
void PluginWebDialog::on_bootstrap_event(wxWebViewEvent& event)
|
||||
{
|
||||
const bool loaded = event.GetEventType() == wxEVT_WEBVIEW_LOADED;
|
||||
// The first bootstrap load (or its error) triggers the swap to plugin HTML.
|
||||
if (!m_content_loaded)
|
||||
load_page_html();
|
||||
load_plugin_content();
|
||||
// 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
|
||||
// base URL and restores SetPage content itself, so nothing matches there.
|
||||
else if (web_hosting::is_content_url(event.GetURL())) {
|
||||
else if (is_content_url(event.GetURL())) {
|
||||
if (m_own_page_load)
|
||||
m_own_page_load = false;
|
||||
else if (loaded && m_content_navigated)
|
||||
load_page_html();
|
||||
load_plugin_content();
|
||||
}
|
||||
if (loaded)
|
||||
m_content_navigated = false;
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void WebDialog::on_navigated(wxWebViewEvent& event)
|
||||
void PluginWebDialog::on_navigated(wxWebViewEvent& event)
|
||||
{
|
||||
m_content_navigated = web_hosting::is_content_url(event.GetURL());
|
||||
m_content_navigated = is_content_url(event.GetURL());
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void WebDialog::load_page_html()
|
||||
void PluginWebDialog::load_plugin_content()
|
||||
{
|
||||
m_content_loaded = true;
|
||||
if (wxWebView* wv = browser()) {
|
||||
m_own_page_load = true;
|
||||
wv->SetPage(wxString::FromUTF8(m_html), web_hosting::content_base_url());
|
||||
wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
|
||||
}
|
||||
}
|
||||
|
||||
void WebDialog::on_script_message(const nlohmann::json& payload)
|
||||
void PluginWebDialog::on_script_message(const nlohmann::json& payload)
|
||||
{
|
||||
if (payload.value("channel", std::string()) == "orca") {
|
||||
const std::string kind = payload.value("kind", std::string());
|
||||
@@ -145,7 +202,7 @@ void WebDialog::on_script_message(const nlohmann::json& payload)
|
||||
handle_common_script_command(payload);
|
||||
}
|
||||
|
||||
void WebDialog::push_message(const nlohmann::json& data)
|
||||
void PluginWebDialog::push_message(const nlohmann::json& data)
|
||||
{
|
||||
if (!m_open)
|
||||
return;
|
||||
@@ -154,7 +211,7 @@ void WebDialog::push_message(const nlohmann::json& data)
|
||||
call_web_handler(envelope, wxT("__orcaDispatch"));
|
||||
}
|
||||
|
||||
void WebDialog::finish(bool submitted, const nlohmann::json& data)
|
||||
void PluginWebDialog::finish(bool submitted, const nlohmann::json& data)
|
||||
{
|
||||
if (!m_open)
|
||||
return;
|
||||
@@ -173,7 +230,7 @@ void WebDialog::finish(bool submitted, const nlohmann::json& data)
|
||||
Close();
|
||||
}
|
||||
|
||||
void WebDialog::on_close_window(wxCloseEvent&)
|
||||
void PluginWebDialog::on_close_window(wxCloseEvent&)
|
||||
{
|
||||
if (!m_open) {
|
||||
// finish() already dispatched submit/close and requested the close.
|
||||
@@ -193,7 +250,7 @@ void WebDialog::on_close_window(wxCloseEvent&)
|
||||
Destroy();
|
||||
}
|
||||
|
||||
void WebDialog::fire_submit(const nlohmann::json& data)
|
||||
void PluginWebDialog::fire_submit(const nlohmann::json& data)
|
||||
{
|
||||
if (m_on_submit) {
|
||||
SubmitHandler cb = std::move(m_on_submit);
|
||||
@@ -201,7 +258,7 @@ void WebDialog::fire_submit(const nlohmann::json& data)
|
||||
}
|
||||
}
|
||||
|
||||
void WebDialog::fire_close()
|
||||
void PluginWebDialog::fire_close()
|
||||
{
|
||||
if (m_close_fired)
|
||||
return;
|
||||
@@ -1,5 +1,5 @@
|
||||
#ifndef slic3r_GUI_WebDialog_hpp_
|
||||
#define slic3r_GUI_WebDialog_hpp_
|
||||
#ifndef slic3r_GUI_PluginWebDialog_hpp_
|
||||
#define slic3r_GUI_PluginWebDialog_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.
|
||||
//
|
||||
// Usable both modally (ShowModal -> read result()) and modelessly (Show()).
|
||||
class WebDialog : public Slic3r::GUI::WebViewHostDialog
|
||||
class PluginWebDialog : public Slic3r::GUI::WebViewHostDialog
|
||||
{
|
||||
public:
|
||||
using MessageHandler = std::function<void(const nlohmann::json& data)>;
|
||||
@@ -32,20 +32,20 @@ public:
|
||||
// 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
|
||||
// / no derived members).
|
||||
WebDialog(wxWindow* parent,
|
||||
const wxString& title,
|
||||
const std::string& html,
|
||||
const wxSize& size,
|
||||
MessageHandler on_message,
|
||||
SubmitHandler on_submit,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed,
|
||||
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
|
||||
~WebDialog() override;
|
||||
PluginWebDialog(wxWindow* parent,
|
||||
const wxString& title,
|
||||
const std::string& html,
|
||||
const wxSize& size,
|
||||
MessageHandler on_message,
|
||||
SubmitHandler on_submit,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed,
|
||||
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
|
||||
~PluginWebDialog() override;
|
||||
|
||||
static void post_message(WebDialog* dialog, const nlohmann::json& data);
|
||||
static void request_close(WebDialog* dialog);
|
||||
static void destroy_silently(WebDialog* dialog);
|
||||
static void post_message(PluginWebDialog* dialog, const nlohmann::json& data);
|
||||
static void request_close(PluginWebDialog* dialog);
|
||||
static void destroy_for_plugin(PluginWebDialog* dialog);
|
||||
|
||||
// Push a payload to the page; delivered to handlers registered via
|
||||
// window.orca.onMessage(). MAIN-THREAD ONLY (the plugin layer marshals).
|
||||
@@ -65,7 +65,7 @@ protected:
|
||||
private:
|
||||
void on_bootstrap_event(wxWebViewEvent& event);
|
||||
void on_navigated(wxWebViewEvent& event);
|
||||
void load_page_html();
|
||||
void load_plugin_content();
|
||||
void on_close_window(wxCloseEvent& event);
|
||||
void fire_submit(const nlohmann::json& data);
|
||||
void fire_close();
|
||||
@@ -86,4 +86,4 @@ private:
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_GUI_WebDialog_hpp_
|
||||
#endif // slic3r_GUI_PluginWebDialog_hpp_
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "MainFrame.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
@@ -17,13 +18,23 @@ namespace GUI {
|
||||
|
||||
void ProjectDirtyStateManager::update_from_undo_redo_stack(bool dirty)
|
||||
{
|
||||
const bool was_dirty = is_dirty();
|
||||
m_plater_dirty = dirty;
|
||||
notify_dirty_change(was_dirty, "undo_redo");
|
||||
if (const Plater *plater = wxGetApp().plater(); plater && wxGetApp().initialized())
|
||||
wxGetApp().mainframe->update_title();
|
||||
}
|
||||
|
||||
void ProjectDirtyStateManager::set_plater_dirty(bool is_dirty)
|
||||
{
|
||||
const bool was_dirty = this->is_dirty();
|
||||
m_plater_dirty = is_dirty;
|
||||
notify_dirty_change(was_dirty, "plater");
|
||||
}
|
||||
|
||||
void ProjectDirtyStateManager::update_from_presets()
|
||||
{
|
||||
const bool was_dirty = is_dirty();
|
||||
m_presets_dirty = false;
|
||||
// check switching of the presets only for exist/loaded project, but not for new
|
||||
GUI_App &app = wxGetApp();
|
||||
@@ -45,18 +56,53 @@ void ProjectDirtyStateManager::update_from_presets()
|
||||
}
|
||||
m_presets_dirty |= app.has_unsaved_preset_changes();
|
||||
m_project_config_dirty = m_initial_project_config != app.preset_bundle->project_config;
|
||||
notify_dirty_change(was_dirty, "presets");
|
||||
app.mainframe->update_title();
|
||||
}
|
||||
|
||||
void ProjectDirtyStateManager::reset_after_save()
|
||||
{
|
||||
const bool was_dirty = is_dirty();
|
||||
this->reset_initial_presets();
|
||||
m_plater_dirty = false;
|
||||
m_presets_dirty = false;
|
||||
m_project_config_dirty = false;
|
||||
notify_dirty_change(was_dirty, "save");
|
||||
wxGetApp().mainframe->update_title();
|
||||
}
|
||||
|
||||
void ProjectDirtyStateManager::notify_dirty_change(bool was_dirty, const char *source)
|
||||
{
|
||||
if (m_suppress_depth > 0)
|
||||
return;
|
||||
|
||||
const bool dirty = is_dirty();
|
||||
if (was_dirty == dirty)
|
||||
return;
|
||||
|
||||
LifecycleEventContext ctx;
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
ctx.dirty = dirty;
|
||||
ctx.source = source;
|
||||
fire_lifecycle_event(LifecycleEvent::ProjectDirtyChanged, ctx);
|
||||
}
|
||||
|
||||
void ProjectDirtyStateManager::begin_suppress_notifications()
|
||||
{
|
||||
if (m_suppress_depth == 0)
|
||||
m_suppress_entry_dirty = is_dirty();
|
||||
++m_suppress_depth;
|
||||
}
|
||||
|
||||
void ProjectDirtyStateManager::end_suppress_notifications()
|
||||
{
|
||||
assert(m_suppress_depth > 0);
|
||||
if (m_suppress_depth > 0)
|
||||
--m_suppress_depth;
|
||||
if (m_suppress_depth == 0)
|
||||
notify_dirty_change(m_suppress_entry_dirty, "batch");
|
||||
}
|
||||
|
||||
void ProjectDirtyStateManager::reset_initial_presets()
|
||||
{
|
||||
m_initial_presets.fill(std::string{});
|
||||
@@ -168,4 +214,3 @@ void ProjectDirtyStateManager::render_debug_window() const
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
|
||||
@@ -14,21 +14,42 @@ public:
|
||||
void reset_after_save();
|
||||
void reset_initial_presets();
|
||||
|
||||
void set_plater_dirty(bool is_dirty) { m_plater_dirty = is_dirty; }
|
||||
void set_plater_dirty(bool is_dirty);
|
||||
bool is_dirty() const { return m_plater_dirty || m_project_config_dirty || m_presets_dirty; }
|
||||
bool is_presets_dirty() const { return m_presets_dirty; }
|
||||
|
||||
// RAII guard coalescing dirty-state updates: while any guard is alive, ProjectDirtyChanged
|
||||
// notifications are held back; when the outermost guard is destroyed, at most one
|
||||
// notification fires, reflecting only the net change across the whole guarded scope.
|
||||
class NotificationSuppressor
|
||||
{
|
||||
public:
|
||||
explicit NotificationSuppressor(ProjectDirtyStateManager &owner) : m_owner(owner) { m_owner.begin_suppress_notifications(); }
|
||||
~NotificationSuppressor() { m_owner.end_suppress_notifications(); }
|
||||
NotificationSuppressor(const NotificationSuppressor &) = delete;
|
||||
NotificationSuppressor &operator=(const NotificationSuppressor &) = delete;
|
||||
private:
|
||||
ProjectDirtyStateManager &m_owner;
|
||||
};
|
||||
|
||||
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
|
||||
void render_debug_window() const;
|
||||
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
|
||||
|
||||
private:
|
||||
void notify_dirty_change(bool was_dirty, const char *source);
|
||||
void begin_suppress_notifications();
|
||||
void end_suppress_notifications();
|
||||
|
||||
// Does the Undo / Redo stack indicate the project is dirty?
|
||||
bool m_plater_dirty { false };
|
||||
// Do the presets indicate the project is dirty?
|
||||
bool m_presets_dirty { false };
|
||||
// Is the project config dirty?
|
||||
bool m_project_config_dirty { false };
|
||||
// NotificationSuppressor nesting depth and the dirty state observed when the outermost guard began.
|
||||
int m_suppress_depth { 0 };
|
||||
bool m_suppress_entry_dirty { false };
|
||||
// Keeps track of preset names selected at the time of last project save.
|
||||
std::array<std::string, Preset::TYPE_COUNT> m_initial_presets;
|
||||
DynamicPrintConfig m_initial_project_config;
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
#include "slic3r/Utils/PresetUpdater.hpp"
|
||||
#include "slic3r/plugin/PluginConfig.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "format.hpp"
|
||||
@@ -6964,6 +6965,20 @@ bool Tab::select_preset(
|
||||
}
|
||||
load_current_preset();
|
||||
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = preset_name;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
switch (m_type) {
|
||||
case Preset::TYPE_PRINT: ctx.msg = "print"; break;
|
||||
case Preset::TYPE_SLA_PRINT: ctx.msg = "sla_print"; break;
|
||||
case Preset::TYPE_FILAMENT: ctx.msg = "filament"; break;
|
||||
case Preset::TYPE_SLA_MATERIAL: ctx.msg = "sla_material"; break;
|
||||
case Preset::TYPE_PRINTER: ctx.msg = "printer"; break;
|
||||
default: break;
|
||||
}
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::PresetSelected, ctx);
|
||||
}
|
||||
|
||||
if (delete_third_printer) {
|
||||
wxGetApp().CallAfter([filament_presets, process_presets]() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "TaskManager.hpp"
|
||||
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "nlohmann/json.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
@@ -210,6 +211,13 @@ int TaskManager::schedule(TaskStateInfo* task)
|
||||
assert(task->state() == TaskState::TS_PENDING);
|
||||
task->set_state(TaskState::TS_SENDING);
|
||||
|
||||
LifecycleEventContext start_ctx;
|
||||
start_ctx.name = task->params().project_name;
|
||||
start_ctx.device_id = task->params().dev_id;
|
||||
start_ctx.job_id = std::to_string(task->task_info_id);
|
||||
start_ctx.source = "task_manager";
|
||||
fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx);
|
||||
|
||||
BOOST_LOG_TRIVIAL(trace) << "task_manager: schedule a task to dev_id = " << task->params().dev_id;
|
||||
boost::thread* new_sending_thread = new boost::thread();
|
||||
*new_sending_thread = Slic3r::create_thread(
|
||||
@@ -237,6 +245,16 @@ int TaskManager::schedule(TaskStateInfo* task)
|
||||
task->set_state(TaskState::TS_SEND_CANCELED);
|
||||
}
|
||||
}
|
||||
|
||||
LifecycleEventContext finish_ctx;
|
||||
finish_ctx.name = task->params().project_name;
|
||||
finish_ctx.device_id = task->params().dev_id;
|
||||
finish_ctx.job_id = std::to_string(task->task_info_id);
|
||||
finish_ctx.source = "task_manager";
|
||||
finish_ctx.code = result == 0 ? LifecycleEvtCode::Ok :
|
||||
(task->is_canceled() ? LifecycleEvtCode::Warn : LifecycleEvtCode::Error);
|
||||
finish_ctx.msg = result == 0 ? "" : (task->is_canceled() ? "cancelled" : "failed");
|
||||
fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx);
|
||||
|
||||
/* remove from sending task list */
|
||||
m_scedule_mutex.lock();
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
#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
|
||||
@@ -1,48 +0,0 @@
|
||||
#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
|
||||
@@ -1,69 +0,0 @@
|
||||
#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
|
||||
@@ -1,25 +0,0 @@
|
||||
#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,8 +75,6 @@ if(document.documentElement)
|
||||
|
||||
} // 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
|
||||
// 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().
|
||||
@@ -89,7 +87,7 @@ std::string WebViewHostDialog::theme_user_script()
|
||||
"if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);");
|
||||
}
|
||||
|
||||
std::string WebViewHostDialog::element_defaults_user_script()
|
||||
std::string WebViewHostDialog::plugin_defaults_user_script()
|
||||
{
|
||||
std::string css;
|
||||
css += "<style id=\"orca-plugin-defaults\">";
|
||||
|
||||
@@ -49,11 +49,9 @@ public:
|
||||
const std::string& prelude = {},
|
||||
const std::string& on_inject = {});
|
||||
|
||||
// Shared by WebPanel hosts and WebDialog.
|
||||
// Shared by modeless Pages tabs and PluginWebDialog.
|
||||
static std::string theme_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();
|
||||
static std::string plugin_defaults_user_script();
|
||||
|
||||
protected:
|
||||
wxWebView* browser() const { return m_browser; }
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "CrealityPrint.hpp"
|
||||
#include "../GUI/PrintHostDialogs.hpp"
|
||||
#include "../GUI/MainFrame.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
#include "Obico.hpp"
|
||||
#include "Flashforge.hpp"
|
||||
#include "SimplyPrint.hpp"
|
||||
@@ -360,12 +361,29 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job)
|
||||
{
|
||||
emit_progress(0); // Indicate the upload is starting
|
||||
|
||||
// Captured before upload_data is moved into upload() below.
|
||||
const std::string upload_filename = the_job.upload_data.source_path.filename().string();
|
||||
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = upload_filename;
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
fire_lifecycle_event(LifecycleEvent::UploadStarted, ctx);
|
||||
}
|
||||
|
||||
bool success = the_job.printhost->upload(std::move(the_job.upload_data),
|
||||
[this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); },
|
||||
[this](wxString error) { this->error_fn(std::move(error)); },
|
||||
[this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); }
|
||||
);
|
||||
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = upload_filename;
|
||||
ctx.code = success ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error;
|
||||
fire_lifecycle_event(LifecycleEvent::UploadFinished, ctx);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
emit_progress(100);
|
||||
if (the_job.switch_to_device_tab) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
@@ -46,6 +47,16 @@ void install_capability_resolver()
|
||||
});
|
||||
}
|
||||
|
||||
// Global libslic3r-side seam (Slic3r::fire_lifecycle_event, in libslic3r/LifecycleEvents.hpp):
|
||||
// broadcasts to every loaded, enabled capability regardless of type, unlike the SlicingPipeline
|
||||
// hook below which only targets picker-selected SlicingPipeline capabilities.
|
||||
void install_lifecycle_event_hook()
|
||||
{
|
||||
set_lifecycle_hook_fn([](LifecycleEvent event, const LifecycleEventContext& ctx) {
|
||||
PluginManager::instance().dispatch_lifecycle_event(event, ctx);
|
||||
});
|
||||
}
|
||||
|
||||
// Print::process() fires this hook at each pipeline seam on the slicing worker
|
||||
// thread; here we run the picker-selected SlicingPipeline capabilities. Per
|
||||
// capability we acquire the GIL, honor cancellation, and convert a plugin
|
||||
@@ -122,12 +133,14 @@ void install()
|
||||
{
|
||||
install_capability_resolver();
|
||||
install_slicing_pipeline_hook();
|
||||
install_lifecycle_event_hook();
|
||||
}
|
||||
|
||||
void uninstall()
|
||||
{
|
||||
ConfigBase::set_resolve_capability_fn(nullptr);
|
||||
Print::set_slicing_pipeline_hook_fn(nullptr);
|
||||
set_lifecycle_hook_fn(nullptr);
|
||||
}
|
||||
|
||||
} // namespace Slic3r::plugin_hooks
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "PluginManager.hpp"
|
||||
|
||||
#include <exception>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include <memory>
|
||||
#include <pybind11/embed.h>
|
||||
@@ -2089,4 +2090,24 @@ ExecutionResult PluginManager::run_script_capability(const std::string& plugin_k
|
||||
return result;
|
||||
}
|
||||
|
||||
void PluginManager::dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx) {
|
||||
for (const auto& cap : get_plugin_capabilities()) {
|
||||
if (!cap || !cap->is_enabled()) continue;
|
||||
try {
|
||||
cap->on_lifecycle_event(evt, ctx);
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name()
|
||||
<< "' on_lifecycle_event(" << lifecycle_event_to_string(evt) << ") threw: " << ex.what()
|
||||
<< " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code)
|
||||
<< ", msg='" << ctx.msg << "']";
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name()
|
||||
<< "' on_lifecycle_event(" << lifecycle_event_to_string(evt)
|
||||
<< ") threw a non-standard exception"
|
||||
<< " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code)
|
||||
<< ", msg='" << ctx.msg << "']";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <libslic3r/Config.hpp>
|
||||
#include <libslic3r/LifecycleEvents.hpp>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -20,7 +21,6 @@
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
#include "CloudPluginService.hpp"
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PluginDescriptor.hpp"
|
||||
#include "PluginLoader.hpp"
|
||||
#include "PluginConfig.hpp"
|
||||
@@ -210,6 +210,8 @@ public:
|
||||
|
||||
ExecutionResult run_script_capability(const std::string& plugin_key, const std::string& capability_name, std::string& error);
|
||||
|
||||
void dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx);
|
||||
|
||||
private:
|
||||
PluginManager() = default;
|
||||
PluginManager(const PluginManager&) = delete;
|
||||
|
||||
@@ -132,6 +132,11 @@ public:
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
|
||||
}
|
||||
|
||||
void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_lifecycle_event, event, ctx);
|
||||
}
|
||||
};
|
||||
|
||||
class PyPluginInterfaceTrampoline : public PyPluginCommonTrampoline<PluginCapabilityInterface>
|
||||
|
||||
@@ -377,6 +377,62 @@ void bind_python_api(pybind11::module_& m)
|
||||
.value("FatalError", PluginResult::FatalError)
|
||||
.export_values();
|
||||
|
||||
py::enum_<LifecycleEvent>(m, "LifecycleEvent", "Application lifecycle moment passed to on_lifecycle_event")
|
||||
.value("NewProject", LifecycleEvent::NewProject)
|
||||
.value("ProjectOpened", LifecycleEvent::ProjectOpened)
|
||||
.value("ProjectBeforeSave", LifecycleEvent::ProjectBeforeSave)
|
||||
.value("ProjectAfterSave", LifecycleEvent::ProjectAfterSave)
|
||||
.value("ProjectClosed", LifecycleEvent::ProjectClosed)
|
||||
.value("ProjectDirtyChanged", LifecycleEvent::ProjectDirtyChanged)
|
||||
.value("SliceStarted", LifecycleEvent::SliceStarted)
|
||||
.value("SliceGeometryFinished", LifecycleEvent::SliceGeometryFinished)
|
||||
.value("GCodeExportStarted", LifecycleEvent::GCodeExportStarted)
|
||||
.value("GCodeExportFinished", LifecycleEvent::GCodeExportFinished)
|
||||
.value("SlicingJobComplete", LifecycleEvent::SlicingJobComplete)
|
||||
.value("ObjectAdded", LifecycleEvent::ObjectAdded)
|
||||
.value("ObjectDeleted", LifecycleEvent::ObjectDeleted)
|
||||
.value("ObjectTransformed", LifecycleEvent::ObjectTransformed)
|
||||
.value("ObjectChanged", LifecycleEvent::ObjectChanged)
|
||||
.value("ObjectRenamed", LifecycleEvent::ObjectRenamed)
|
||||
.value("PlateCreated", LifecycleEvent::PlateCreated)
|
||||
.value("PlateDeleted", LifecycleEvent::PlateDeleted)
|
||||
.value("PlateSelected", LifecycleEvent::PlateSelected)
|
||||
.value("PlateRenamed", LifecycleEvent::PlateRenamed)
|
||||
.value("PresetSelected", LifecycleEvent::PresetSelected)
|
||||
.value("PresetSaved", LifecycleEvent::PresetSaved)
|
||||
.value("PrintStateChanged", LifecycleEvent::PrintStateChanged)
|
||||
.value("DeviceOnlineChanged", LifecycleEvent::DeviceOnlineChanged)
|
||||
.value("DeviceDiscovered", LifecycleEvent::DeviceDiscovered)
|
||||
.value("DeviceSelected", LifecycleEvent::DeviceSelected)
|
||||
.value("DeviceConnected", LifecycleEvent::DeviceConnected)
|
||||
.value("DeviceDisconnected", LifecycleEvent::DeviceDisconnected)
|
||||
.value("UploadStarted", LifecycleEvent::UploadStarted)
|
||||
.value("UploadFinished", LifecycleEvent::UploadFinished)
|
||||
.value("PrintJobStarted", LifecycleEvent::PrintJobStarted)
|
||||
.value("PrintJobFinished", LifecycleEvent::PrintJobFinished)
|
||||
.value("SendJobStarted", LifecycleEvent::SendJobStarted)
|
||||
.value("SendJobFinished", LifecycleEvent::SendJobFinished)
|
||||
.export_values();
|
||||
|
||||
py::enum_<LifecycleEvtCode>(m, "LifecycleEvtCode", "Outcome code accompanying a LifecycleEventContext")
|
||||
.value("Ok", LifecycleEvtCode::Ok)
|
||||
.value("Error", LifecycleEvtCode::Error)
|
||||
.value("Warn", LifecycleEvtCode::Warn)
|
||||
.export_values();
|
||||
|
||||
py::class_<LifecycleEventContext>(m, "LifecycleEventContext", "Payload accompanying a LifecycleEvent")
|
||||
.def(py::init<>())
|
||||
.def_readonly("name", &LifecycleEventContext::name)
|
||||
.def_readonly("code", &LifecycleEventContext::code)
|
||||
.def_readonly("msg", &LifecycleEventContext::msg)
|
||||
.def_readonly("id", &LifecycleEventContext::id)
|
||||
.def_readonly("previous_name", &LifecycleEventContext::previous_name)
|
||||
.def_readonly("device_id", &LifecycleEventContext::device_id)
|
||||
.def_readonly("job_id", &LifecycleEventContext::job_id)
|
||||
.def_readonly("source", &LifecycleEventContext::source)
|
||||
.def_readonly("index", &LifecycleEventContext::index)
|
||||
.def_readonly("dirty", &LifecycleEventContext::dirty);
|
||||
|
||||
py::class_<PluginContext>(m, "PluginContext", "Context shared with plugin entry points")
|
||||
.def(py::init<>())
|
||||
.def_readwrite("orca_version", &PluginContext::orca_version);
|
||||
@@ -401,6 +457,9 @@ void bind_python_api(pybind11::module_& m)
|
||||
.def("get_type", &PluginCapabilityInterface::get_type)
|
||||
.def("on_load", &PluginCapabilityInterface::on_load)
|
||||
.def("on_unload", &PluginCapabilityInterface::on_unload)
|
||||
.def("on_lifecycle_event", &PluginCapabilityInterface::on_lifecycle_event,
|
||||
"Override to react to an application lifecycle moment (LifecycleEvent) and its\n"
|
||||
"LifecycleEventContext payload. Available on every capability type.")
|
||||
.def("has_config_ui", &PluginCapabilityInterface::has_config_ui,
|
||||
"Override to return True to replace the host's default JSON editor with your own HTML\n"
|
||||
"UI, returned by get_config_ui(). Every capability is configurable and appears in the\n"
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
#include <libslic3r/LifecycleEvents.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
|
||||
@@ -167,6 +169,8 @@ public:
|
||||
virtual void on_unload() {}
|
||||
virtual void on_cancelled() {}
|
||||
|
||||
virtual void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) { (void) event; (void) ctx; }
|
||||
|
||||
// ── C++-only host state, never exposed to Python. Set by the loader at materialization. ──
|
||||
//
|
||||
// The capability owns its own identity and enable flag: they are read once under the GIL, live
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/MainFrame.hpp>
|
||||
#include <slic3r/GUI/MsgDialog.hpp>
|
||||
#include <slic3r/GUI/Plater.hpp>
|
||||
#include <slic3r/GUI/DockPanel.hpp>
|
||||
#include <slic3r/GUI/PluginProgressDialog.hpp>
|
||||
#include <slic3r/GUI/WebDialog.hpp>
|
||||
#include <slic3r/GUI/PluginWebDialog.hpp>
|
||||
#include <slic3r/GUI/NotificationManager.hpp>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
@@ -22,7 +20,6 @@
|
||||
#include <wx/defs.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <future>
|
||||
@@ -76,7 +73,7 @@ CallablePtr make_holder(py::object obj)
|
||||
|
||||
// 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).
|
||||
GUI::WebDialog::MessageHandler make_message_adapter(py::object on_message)
|
||||
GUI::PluginWebDialog::MessageHandler make_message_adapter(py::object on_message)
|
||||
{
|
||||
CallablePtr holder = make_holder(std::move(on_message));
|
||||
if (!holder)
|
||||
@@ -94,7 +91,7 @@ GUI::WebDialog::MessageHandler make_message_adapter(py::object on_message)
|
||||
};
|
||||
}
|
||||
|
||||
GUI::WebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
|
||||
GUI::PluginWebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
|
||||
{
|
||||
CallablePtr holder = make_holder(std::move(on_submit));
|
||||
if (!holder)
|
||||
@@ -112,25 +109,6 @@ GUI::WebDialog::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
|
||||
// owning plugin so all of a plugin's UI can be torn down on unload.
|
||||
@@ -372,13 +350,28 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
||||
if (!UiRegistry::instance().is_open(new_id))
|
||||
return;
|
||||
|
||||
GUI::WebDialog::CloseHandler on_close = make_close_adapter(close_holder);
|
||||
// Plugin's on_close: fired only on a user/JS-initiated close (not forced
|
||||
// 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.
|
||||
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
||||
|
||||
auto* dlg = new GUI::WebDialog(ui_parent(), wxString::FromUTF8(title), html,
|
||||
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
|
||||
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
|
||||
auto* dlg = new GUI::PluginWebDialog(ui_parent(), wxString::FromUTF8(title), html,
|
||||
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
|
||||
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
|
||||
UiRegistry::instance().bind(new_id, dlg, plugin_key);
|
||||
if (modal) {
|
||||
dlg->ShowModal();
|
||||
@@ -394,69 +387,14 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
||||
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)
|
||||
{
|
||||
if (wxTheApp == nullptr)
|
||||
return;
|
||||
json j = py_to_json(data); // GIL held (binding body)
|
||||
GUI::wxGetApp().CallAfter([id, j = std::move(j)]() {
|
||||
auto* window = UiRegistry::instance().get_as<wxWindow>(id);
|
||||
if (auto* panel = dynamic_cast<GUI::DockPanel*>(window))
|
||||
panel->push_message(j);
|
||||
else
|
||||
GUI::WebDialog::post_message(dynamic_cast<GUI::WebDialog*>(window), j);
|
||||
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
|
||||
GUI::PluginWebDialog::post_message(d, j);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -465,23 +403,8 @@ void handle_close(int id)
|
||||
if (wxTheApp == nullptr)
|
||||
return;
|
||||
GUI::wxGetApp().CallAfter([id]() {
|
||||
auto* window = UiRegistry::instance().get_as<wxWindow>(id);
|
||||
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);
|
||||
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
|
||||
GUI::PluginWebDialog::request_close(d);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -640,31 +563,6 @@ 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) "
|
||||
"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.")
|
||||
.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)
|
||||
@@ -732,10 +630,8 @@ 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
|
||||
// forced teardown (intended); the resource destructor still cleans the registry.
|
||||
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
|
||||
if (auto* dialog = dynamic_cast<GUI::WebDialog*>(window))
|
||||
GUI::WebDialog::destroy_silently(dialog);
|
||||
else if (auto* panel = dynamic_cast<GUI::DockPanel*>(window))
|
||||
panel->destroy_silently();
|
||||
if (auto* dialog = dynamic_cast<GUI::PluginWebDialog*>(window))
|
||||
GUI::PluginWebDialog::destroy_for_plugin(dialog);
|
||||
else if (window != nullptr)
|
||||
window->Destroy();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
#include "PluginPages.hpp"
|
||||
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/Notebook.hpp"
|
||||
#include "slic3r/GUI/GUI_App.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/plugin/PluginManager.hpp"
|
||||
|
||||
#include <libslic3r/Utils.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
@@ -61,11 +66,27 @@ constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS(
|
||||
} // namespace
|
||||
|
||||
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
|
||||
: GUI::WebPanel(parent, PLUGIN_PAGE_BRIDGE_JS)
|
||||
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
|
||||
, m_cap(std::move(capability))
|
||||
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
|
||||
{
|
||||
browser()->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this);
|
||||
auto* topsizer = new wxBoxSizer(wxVERTICAL);
|
||||
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;
|
||||
m_cap->set_message_sender([lifetime](const std::string& message) {
|
||||
@@ -96,54 +117,89 @@ void PluginPage::detach_capability()
|
||||
m_cap.reset();
|
||||
}
|
||||
|
||||
std::optional<std::string> PluginPage::page_html()
|
||||
wxString PluginPage::web_base_url() const
|
||||
{
|
||||
if (m_cap == nullptr)
|
||||
return std::nullopt;
|
||||
const auto path = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
|
||||
return wxString("file://") + GUI::from_u8(path) + "/";
|
||||
}
|
||||
|
||||
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 {
|
||||
return m_cap->get_ui();
|
||||
m_browser->SetPage(wxString::FromUTF8(m_cap->get_ui()), web_base_url());
|
||||
} catch (const std::exception& error) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "': " << error.what();
|
||||
detach_capability();
|
||||
} catch (...) {
|
||||
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)
|
||||
{
|
||||
const wxString url = event.GetURL();
|
||||
if (!url.empty())
|
||||
browser()->LoadURL(url);
|
||||
if (!url.empty() && m_browser != nullptr)
|
||||
m_browser->LoadURL(url);
|
||||
event.Veto();
|
||||
}
|
||||
|
||||
bool PluginPage::on_page_message(const std::string& kind, const nlohmann::json& data)
|
||||
void PluginPage::on_script_message(wxWebViewEvent& event)
|
||||
{
|
||||
if (kind != "message")
|
||||
return false;
|
||||
if (!m_cap)
|
||||
return true;
|
||||
return;
|
||||
|
||||
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 {
|
||||
m_cap->on_message(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
|
||||
m_cap->on_message(data == root.end()
|
||||
? "null"
|
||||
: data->dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
|
||||
} catch (const std::exception& error) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "': " << error.what();
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "'";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
// non-JSON payload needs wrapping as a string literal.
|
||||
post_to_page(nlohmann::json::accept(message)
|
||||
? message
|
||||
: nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
|
||||
const std::string payload = nlohmann::json::accept(message)
|
||||
? message
|
||||
: 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()
|
||||
@@ -212,6 +268,10 @@ bool PluginPages::create_page(const PluginCapabilityId& id)
|
||||
}
|
||||
|
||||
auto* page = new PluginPage(m_parent, std::move(capability));
|
||||
if (!page->is_valid()) {
|
||||
page->Destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!icon.empty()) {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <slic3r/GUI/WebPanel.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp>
|
||||
|
||||
@@ -12,13 +11,14 @@
|
||||
#include <vector>
|
||||
|
||||
#include <wx/bitmap.h>
|
||||
#include <wx/panel.h>
|
||||
#include <wx/webview.h>
|
||||
|
||||
class Notebook;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PluginPage : public GUI::WebPanel
|
||||
class PluginPage : public wxPanel
|
||||
{
|
||||
public:
|
||||
PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability);
|
||||
@@ -26,20 +26,24 @@ public:
|
||||
|
||||
PluginPage() = delete;
|
||||
|
||||
bool is_valid() const { return m_browser != nullptr && m_cap != nullptr; }
|
||||
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 set_icon(const wxBitmap& icon) { m_icon = 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:
|
||||
void on_new_window(wxWebViewEvent& event);
|
||||
void load_plugin_content();
|
||||
wxString bootstrap_url() const;
|
||||
wxString web_base_url() const;
|
||||
|
||||
wxWebView* m_browser{nullptr};
|
||||
std::shared_ptr<PagesPluginCapability> m_cap;
|
||||
std::shared_ptr<std::atomic<PluginPage*>> m_lifetime;
|
||||
bool m_content_loaded{false};
|
||||
wxBitmap m_icon;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
#include <libslic3r/Model.hpp>
|
||||
#include <libslic3r/PresetBundle.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 "plugin_test_utils.hpp"
|
||||
#include "python_test_support.hpp"
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
@@ -145,8 +141,6 @@ 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_MODAL").cast<long>() == 1);
|
||||
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
|
||||
// exist here; they must fail cleanly with a clear error, not crash.
|
||||
@@ -157,74 +151,6 @@ TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app i
|
||||
CHECK(error.matches(PyExc_RuntimeError));
|
||||
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]")
|
||||
|
||||
Reference in New Issue
Block a user