mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 21:42:43 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
173706750d | ||
|
|
ffb2946b20 | ||
|
|
e89a05b1e4 | ||
|
|
02a8011b97 |
@@ -684,6 +684,18 @@ jobs:
|
||||
name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
|
||||
path: './build/src/Release/OrcaSlicer_profile_validator'
|
||||
|
||||
# generate_system_cache is what scripts/build_preset_cache.sh bakes the
|
||||
# <vendor>.opc caches with; it was already built by the "Build system
|
||||
# preset cache (Linux)" step above. The .opc format is 64-bit
|
||||
# little-endian native, i.e. identical across every platform Orca ships,
|
||||
# so only the Linux binary is published.
|
||||
- name: Upload generate_system_cache Ubuntu
|
||||
if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: generate_system_cache_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
|
||||
path: './build/src/dev-utils/Release/generate_system_cache'
|
||||
|
||||
- name: Deploy Ubuntu release
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
@@ -714,6 +726,17 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1
|
||||
|
||||
- name: Deploy Ubuntu generate_system_cache release
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: ./build/src/dev-utils/Release/generate_system_cache
|
||||
asset_name: generate_system_cache_Linux${{ env.ubuntu-ver-str }}_nightly
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1
|
||||
|
||||
- name: Deploy orca_custom_preset_tests
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
name: Post-merge profiles
|
||||
|
||||
# Push-triggered counterpart to check_profiles.yml (which only gates PRs). When a
|
||||
# profile change lands on main or a release branch, rebuild the affected vendors'
|
||||
# binary preset caches (<vendor>.opc) and publish each as a versioned ZIP asset on
|
||||
# a per-Orca-version release of the profiles repo. From there OrcaCloud's OTA
|
||||
# Manager lists the asset, a maintainer attaches a changelog and hits Publish, and
|
||||
# only then does it become a live OTA update - this workflow does none of that
|
||||
# last part (no changelog, no R2, no webhook).
|
||||
#
|
||||
# Asset contract expected by OrcaCloud's release scanner:
|
||||
# ^(\d+\.\d+\.\d+)_([^_]+)_(\d+(?:\.\d+){3})_(\d{12})\.zip$
|
||||
# <orca_ver>_<vendor>_<profile_version>_<UTC yyyymmddHHMM>.zip (zip root: <vendor>.opc)
|
||||
#
|
||||
# Setup (App + secrets): docs/ota/post-merge-profiles-setup.md
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
paths:
|
||||
- 'resources/profiles/**'
|
||||
- '.github/workflows/post_merge_profiles.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# One run per branch; let a run finish rather than cancel it, since it publishes.
|
||||
concurrency:
|
||||
group: post-merge-profiles-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
# generate_system_cache is published to this repo's own nightly-builds release
|
||||
# by build_orca.yml's Linux leg. The job guard pins github.repository to
|
||||
# OrcaSlicer/OrcaSlicer, so this resolves there.
|
||||
TOOL_REPO: ${{ github.repository }}
|
||||
TOOL_ASSET: generate_system_cache_Linux_Ubuntu2404_nightly
|
||||
# Where per-vendor ZIP assets are published; OrcaCloud's OTA reads this repo.
|
||||
PROFILES_OWNER: OrcaSlicer
|
||||
PROFILES_REPO: orcaslicer-profiles
|
||||
|
||||
jobs:
|
||||
publish_profile_caches:
|
||||
name: Publish profile caches
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
|
||||
# FOLDER_MERGERS is an environment-scoped variable, shared with the PR
|
||||
# merge bot. Keep this environment free of protection rules so this
|
||||
# push-triggered job does not wait for a reviewer.
|
||||
environment: merge-delegation
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
# Enough history to reach github.event.before for the changed-vendor
|
||||
# diff on a normal push; deeper pushes fall back to HEAD^..HEAD in the
|
||||
# step below. fetch-depth: 0 would clone all of OrcaSlicer's history.
|
||||
fetch-depth: 50
|
||||
|
||||
- name: Resolve changed vendors
|
||||
id: vendors
|
||||
shell: bash
|
||||
env:
|
||||
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base='${{ github.event.before }}'
|
||||
head='${{ github.sha }}'
|
||||
# Zero SHA (branch created / force push) or manual dispatch: fall back
|
||||
# to this commit's own diff.
|
||||
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
|
||||
base="$head^"
|
||||
fi
|
||||
echo "Diffing $base..$head"
|
||||
|
||||
mapfile -t candidates < <(
|
||||
git diff --name-only "$base" "$head" -- resources/profiles \
|
||||
| sed -nE 's#^resources/profiles/([^/]+)/.*#\1#p; s#^resources/profiles/([^/]+)\.json$#\1#p' \
|
||||
| sort -u
|
||||
)
|
||||
|
||||
vendors=()
|
||||
for v in "${candidates[@]:-}"; do
|
||||
[ -n "$v" ] || continue
|
||||
json="resources/profiles/$v.json"
|
||||
# A vendor has a manifest plus either a preset directory or a version
|
||||
# field; this drops non-vendor files such as blacklist.json.
|
||||
if [ -f "$json" ] && { [ -d "resources/profiles/$v" ] || jq -e '.version' "$json" >/dev/null 2>&1; }; then
|
||||
vendors+=("$v")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#vendors[@]}" -eq 0 ]; then
|
||||
echo "No changed vendor profiles in this push; nothing to publish."
|
||||
echo "vendors=" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# A vendor is eligible only when both the profile directory and its
|
||||
# sibling bundle JSON are covered by at least one FOLDER_MERGERS
|
||||
# grant. The account part is intentionally ignored here: this is a
|
||||
# post-merge safety check, not an authorization check for a command.
|
||||
grants=()
|
||||
while IFS= read -r raw_line; do
|
||||
line="${raw_line#"${raw_line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
[ -n "$line" ] || continue
|
||||
[[ "$line" == \#* ]] && continue
|
||||
[[ "$line" == *:* ]] || continue
|
||||
|
||||
grant="${line#*:}"
|
||||
grant="${grant#"${grant%%[![:space:]]*}"}"
|
||||
grant="${grant%"${grant##*[![:space:]]}"}"
|
||||
while [[ "$grant" == */ ]]; do grant="${grant%/}"; done
|
||||
grants+=("$grant")
|
||||
done <<< "${FOLDER_MERGERS:-}"
|
||||
|
||||
is_granted() {
|
||||
local path="$1"
|
||||
local grant
|
||||
for grant in "${grants[@]:-}"; do
|
||||
if [[ "$path" == "$grant" || "$path" == "$grant/"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
unauthorized=()
|
||||
for v in "${vendors[@]}"; do
|
||||
if ! is_granted "resources/profiles/$v" || ! is_granted "resources/profiles/$v.json"; then
|
||||
unauthorized+=("$v")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#unauthorized[@]}" -ne 0 ]; then
|
||||
echo "Changed vendor profiles are not covered by FOLDER_MERGERS: ${unauthorized[*]}"
|
||||
echo "No profile caches will be published for this push."
|
||||
echo "vendors=" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf 'Changed vendors: %s\n' "${vendors[*]}"
|
||||
echo "vendors=${vendors[*]}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Resolve Orca version
|
||||
id: orca
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
raw="$(sed -nE 's/^set\(SoftFever_VERSION "([^"]+)".*/\1/p' version.inc | head -1)"
|
||||
[ -n "$raw" ] || { echo "::error::could not read SoftFever_VERSION from version.inc"; exit 1; }
|
||||
orca_ver="${raw%%-*}"
|
||||
if ! [[ "$orca_ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::Orca version '$orca_ver' (from '$raw') is not X.Y.Z"; exit 1
|
||||
fi
|
||||
# release_tag is what the desktop client sends as orca_version and what
|
||||
# OrcaCloud keys R2 on; orca_ver (X.Y.Z) is the asset-name prefix.
|
||||
echo "release_tag=$raw" >> "$GITHUB_OUTPUT"
|
||||
echo "orca_ver=$orca_ver" >> "$GITHUB_OUTPUT"
|
||||
echo "Orca version: release_tag=$raw asset_prefix=$orca_ver"
|
||||
|
||||
- name: Validate profile versions
|
||||
id: pver
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
: > "$RUNNER_TEMP/pver.tsv"
|
||||
for v in ${{ steps.vendors.outputs.vendors }}; do
|
||||
pv="$(jq -r '.version // empty' "resources/profiles/$v.json")"
|
||||
if ! [[ "$pv" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::vendor $v version '${pv:-<none>}' must be 4 numeric parts (A.B.C.D) for the OTA asset name; fix resources/profiles/$v.json"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\t%s\n' "$v" "$pv" >> "$RUNNER_TEMP/pver.tsv"
|
||||
echo "$v -> $pv"
|
||||
done
|
||||
|
||||
- name: Download generate_system_cache
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
env:
|
||||
# gh (with the default token) rather than an unauthenticated curl: keeps
|
||||
# working if TOOL_REPO is ever private and avoids anonymous rate limits.
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release download nightly-builds --repo "$TOOL_REPO" \
|
||||
--pattern "$TOOL_ASSET" --output generate_system_cache --clobber
|
||||
chmod +x generate_system_cache
|
||||
|
||||
- name: Build caches and package assets
|
||||
id: pkg
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# One timestamp for the whole run so a multi-vendor merge groups together.
|
||||
ts="$(date -u +%Y%m%d%H%M)"
|
||||
orca_ver='${{ steps.orca.outputs.orca_ver }}'
|
||||
out="$RUNNER_TEMP/assets"
|
||||
mkdir -p "$out"
|
||||
|
||||
for v in ${{ steps.vendors.outputs.vendors }}; do
|
||||
./generate_system_cache -p "$GITHUB_WORKSPACE/resources/profiles" -v "$v" -l 2
|
||||
opc="resources/profiles/$v.opc"
|
||||
[ -f "$opc" ] || { echo "::error::$opc was not generated"; exit 1; }
|
||||
pv="$(awk -F'\t' -v v="$v" '$1==v{print $2}' "$RUNNER_TEMP/pver.tsv")"
|
||||
name="${orca_ver}_${v}_${pv}_${ts}.zip"
|
||||
( cd resources/profiles && zip -q -j "$out/$name" "$v.opc" )
|
||||
echo "packaged $name"
|
||||
done
|
||||
echo "dir=$out" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Mint profiles-repo token
|
||||
id: token
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.PROFILES_APP_ID }}
|
||||
private-key: ${{ secrets.PROFILES_APP_PRIVATE_KEY }}
|
||||
owner: ${{ env.PROFILES_OWNER }}
|
||||
repositories: ${{ env.PROFILES_REPO }}
|
||||
|
||||
- name: Publish assets to profiles release
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.token.outputs.token }}
|
||||
RELEASE_TAG: ${{ steps.orca.outputs.release_tag }}
|
||||
ASSET_DIR: ${{ steps.pkg.outputs.dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
repo="$PROFILES_OWNER/$PROFILES_REPO"
|
||||
if ! gh release view "$RELEASE_TAG" --repo "$repo" >/dev/null 2>&1; then
|
||||
echo "Creating release $RELEASE_TAG on $repo"
|
||||
gh release create "$RELEASE_TAG" --repo "$repo" \
|
||||
--title "$RELEASE_TAG" --notes "Profile cache assets for Orca $RELEASE_TAG." \
|
||||
--latest=false
|
||||
fi
|
||||
# Asset names are timestamp-unique; a clash means a bug, so don't --clobber.
|
||||
gh release upload "$RELEASE_TAG" --repo "$repo" "$ASSET_DIR"/*.zip
|
||||
|
||||
{
|
||||
echo "### Published to \`$repo\` release \`$RELEASE_TAG\`"
|
||||
for f in "$ASSET_DIR"/*.zip; do echo "- \`$(basename "$f")\`"; done
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -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",
|
||||
dock="right",
|
||||
width=320,
|
||||
height=480,
|
||||
on_message=self.on_message,
|
||||
on_close=self.on_close,
|
||||
)
|
||||
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)
|
||||
@@ -23,7 +23,8 @@ int main(int argc, char* argv[])
|
||||
#else
|
||||
("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory")
|
||||
#endif
|
||||
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)");
|
||||
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)")
|
||||
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional; generate the cache for this vendor only (the Orca filament library is always included as the inheritance base). All vendors if not specified.");
|
||||
// clang-format on
|
||||
|
||||
po::variables_map vm;
|
||||
@@ -38,6 +39,7 @@ int main(int argc, char* argv[])
|
||||
|
||||
const std::string profiles_path = vm["path"].as<std::string>();
|
||||
const int log_level = vm["log_level"].as<int>();
|
||||
const std::string vendor = vm["vendor"].as<std::string>();
|
||||
|
||||
if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) {
|
||||
std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n";
|
||||
@@ -59,8 +61,12 @@ int main(int argc, char* argv[])
|
||||
preset_bundle->set_is_validation_mode(true);
|
||||
preset_bundle->set_default_suppressed(true);
|
||||
preset_bundle->set_generate_vendor_caches(true);
|
||||
// Empty == every vendor. Otherwise only this vendor (plus the always-loaded
|
||||
// Orca filament library) is parsed, so only its <vendor>.opc is written.
|
||||
preset_bundle->set_vendor_to_validate(vendor);
|
||||
|
||||
std::cout << "Loading system presets from: " << profiles_path << "\n";
|
||||
std::cout << "Loading system presets from: " << profiles_path
|
||||
<< (vendor.empty() ? "" : " (vendor: " + vendor + ")") << "\n";
|
||||
|
||||
try {
|
||||
// In validation mode data_dir() is the profiles directory set above, so the
|
||||
@@ -71,6 +77,14 @@ int main(int argc, char* argv[])
|
||||
return 1;
|
||||
}
|
||||
|
||||
// A specific vendor must have produced its own cache; the always-loaded
|
||||
// filament library alone would otherwise mask a misspelt or removed name.
|
||||
if (!vendor.empty() && !fs::exists(fs::path(profiles_path) / (vendor + ".opc"))) {
|
||||
std::cerr << "No cache was generated for vendor '" << vendor << "' under " << profiles_path
|
||||
<< " - check the vendor name.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t cache_count = 0;
|
||||
for (auto& entry : fs::directory_iterator(profiles_path))
|
||||
if (boost::iends_with(entry.path().string(), ".opc"))
|
||||
|
||||
@@ -139,8 +139,6 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/PluginProgressDialog.hpp
|
||||
GUI/PluginWebDialog.cpp
|
||||
GUI/PluginWebDialog.hpp
|
||||
GUI/PluginDockPanel.cpp
|
||||
GUI/PluginDockPanel.hpp
|
||||
GUI/DragCanvas.cpp
|
||||
GUI/DragCanvas.hpp
|
||||
GUI/EditGCodeDialog.cpp
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
#ifdef __WXGTK__
|
||||
#include "LinuxDisplayBackend.hpp"
|
||||
#endif
|
||||
#include "PluginDockPanel.hpp"
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "GUI_Factories.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
@@ -6721,14 +6720,6 @@ struct Plater::priv
|
||||
|
||||
// GUI elements
|
||||
AuiMgr m_aui_mgr;
|
||||
// Live plugin panes. `on_close` runs when the user closes one from its close button; `shown` is
|
||||
// what the plugin asked for.
|
||||
struct PluginPane
|
||||
{
|
||||
std::function<void()> on_close;
|
||||
bool shown{true};
|
||||
};
|
||||
std::map<wxWindow*, PluginPane> m_plugin_panes;
|
||||
wxString m_default_window_layout;
|
||||
wxPanel* current_panel{ nullptr };
|
||||
std::vector<wxPanel*> panels;
|
||||
@@ -6900,11 +6891,6 @@ struct Plater::priv
|
||||
void update_sidebar(bool force_update = false);
|
||||
void reset_window_layout();
|
||||
Sidebar::DockingState get_sidebar_docking_state();
|
||||
void add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||
const wxSize& size, std::function<void()> on_close);
|
||||
void remove_plugin_pane(wxWindow* window);
|
||||
void show_plugin_pane(wxWindow* window, bool show);
|
||||
bool plugin_pane_visible(const PluginPane& plugin_pane, const wxAuiPaneInfo& pane) const;
|
||||
|
||||
bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); }
|
||||
|
||||
@@ -7485,18 +7471,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_plugin_panes.find(pane->window) : m_plugin_panes.end();
|
||||
if (it != m_plugin_panes.end()) {
|
||||
const std::function<void()> on_close = std::move(it->second.on_close);
|
||||
m_plugin_panes.erase(it);
|
||||
if (on_close)
|
||||
on_close();
|
||||
}
|
||||
evt.Skip();
|
||||
});
|
||||
|
||||
m_default_window_layout = m_aui_mgr.SavePerspective();
|
||||
|
||||
{
|
||||
@@ -8167,14 +8141,6 @@ void Plater::priv::update_sidebar(bool force_update) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [window, plugin_pane] : m_plugin_panes) {
|
||||
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
|
||||
if (pane.IsOk() && pane.IsShown() != plugin_pane_visible(plugin_pane, pane)) {
|
||||
pane.Show(!pane.IsShown());
|
||||
needs_update = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needs_update) {
|
||||
notification_manager->set_sidebar_collapsed(sidebar.IsShown());
|
||||
m_aui_mgr.Update();
|
||||
@@ -8184,95 +8150,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 hides every pane it does not list, and the default layout lists no plugin panes.
|
||||
for (const auto& [window, plugin_pane] : m_plugin_panes)
|
||||
if (wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window); pane.IsOk())
|
||||
pane.Show(plugin_pane.shown);
|
||||
sidebar_layout.is_collapsed = false;
|
||||
update_sidebar(true);
|
||||
}
|
||||
|
||||
bool Plater::priv::plugin_pane_visible(const PluginPane& plugin_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 plugin_pane.shown && (!pane.IsFloating() || sidebar_layout.show);
|
||||
}
|
||||
|
||||
void Plater::priv::add_plugin_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 = plugin_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 PluginPane& plugin_pane = m_plugin_panes[window] = PluginPane{std::move(on_close)};
|
||||
info.Show(plugin_pane_visible(plugin_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_plugin_pane(wxWindow* window)
|
||||
{
|
||||
m_plugin_panes.erase(window);
|
||||
if (m_aui_mgr.DetachPane(window))
|
||||
m_aui_mgr.Update();
|
||||
window->Destroy();
|
||||
}
|
||||
|
||||
void Plater::priv::show_plugin_pane(wxWindow* window, bool show)
|
||||
{
|
||||
const auto it = m_plugin_panes.find(window);
|
||||
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
|
||||
if (it == m_plugin_panes.end() || !pane.IsOk())
|
||||
return;
|
||||
it->second.shown = show;
|
||||
if (pane.IsShown() == plugin_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;
|
||||
@@ -17829,14 +17710,6 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_
|
||||
|
||||
void Plater::reset_window_layout() { p->reset_window_layout(); }
|
||||
|
||||
void Plater::add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||
const wxSize& size, std::function<void()> on_close)
|
||||
{
|
||||
p->add_plugin_pane(window, name, caption, dock, size, std::move(on_close));
|
||||
}
|
||||
void Plater::remove_plugin_pane(wxWindow* window) { p->remove_plugin_pane(window); }
|
||||
void Plater::show_plugin_pane(wxWindow* window, bool show) { p->show_plugin_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(); }
|
||||
|
||||
@@ -475,14 +475,6 @@ public:
|
||||
|
||||
void reset_window_layout();
|
||||
|
||||
// Plugin panes dock 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_plugin_pane() destroys it without calling on_close.
|
||||
void add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||
const wxSize& size, std::function<void()> on_close);
|
||||
void remove_plugin_pane(wxWindow* window);
|
||||
void show_plugin_pane(wxWindow* window, bool show);
|
||||
|
||||
// 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,170 +0,0 @@
|
||||
#include "PluginDockPanel.hpp"
|
||||
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "PluginWebDialog.hpp"
|
||||
#include "Widgets/WebView.hpp"
|
||||
#include "Widgets/WebViewHostDialog.hpp"
|
||||
|
||||
#include <libslic3r/Utils.hpp>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <wx/sizer.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;
|
||||
}
|
||||
|
||||
std::string plugin_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 {};
|
||||
}
|
||||
|
||||
PluginDockPanel::PluginDockPanel(wxWindow* parent,
|
||||
const std::string& html,
|
||||
MessageHandler on_message,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed)
|
||||
: wxPanel(parent, wxID_ANY)
|
||||
, m_html(html)
|
||||
, m_on_message(std::move(on_message))
|
||||
, m_on_close(std::move(on_close))
|
||||
, m_on_destroyed(std::move(on_destroyed))
|
||||
{
|
||||
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
SetSizer(sizer);
|
||||
|
||||
const std::string bootstrap = (boost::filesystem::path(resources_dir()) / PluginWebDialog::BOOTSTRAP_PAGE).make_preferred().string();
|
||||
m_browser = WebView::CreateWebView(this, wxString("file://") + from_u8(bootstrap));
|
||||
if (m_browser == nullptr) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Could not create the web view for a plugin dock panel";
|
||||
return;
|
||||
}
|
||||
|
||||
m_browser->SetBackgroundColour(GetBackgroundColour());
|
||||
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
|
||||
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
|
||||
m_browser->AddUserScript(PluginWebDialog::bridge_user_script());
|
||||
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginDockPanel::on_bootstrap_event, this);
|
||||
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginDockPanel::on_bootstrap_event, this);
|
||||
m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, [](wxWebViewEvent& event) { event.Veto(); });
|
||||
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginDockPanel::on_script_message, this);
|
||||
m_browser->Bind(EVT_WEBVIEW_RECREATED, &PluginDockPanel::on_webview_recreated, this);
|
||||
sizer->Add(m_browser, 1, wxEXPAND);
|
||||
}
|
||||
|
||||
PluginDockPanel::~PluginDockPanel()
|
||||
{
|
||||
if (m_on_destroyed)
|
||||
m_on_destroyed();
|
||||
}
|
||||
|
||||
void PluginDockPanel::on_bootstrap_event(wxWebViewEvent& event)
|
||||
{
|
||||
if (!m_content_loaded) {
|
||||
m_content_loaded = true;
|
||||
m_browser->SetPage(wxString::FromUTF8(m_html), PluginWebDialog::content_base_url());
|
||||
}
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void PluginDockPanel::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 (kind == "message") {
|
||||
if (m_on_message)
|
||||
m_on_message(payload.contains("data") ? payload["data"] : nlohmann::json());
|
||||
} else if (kind == "close") {
|
||||
request_close();
|
||||
}
|
||||
}
|
||||
|
||||
void PluginDockPanel::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.
|
||||
WebView::RunScript(m_browser, wxString::FromUTF8(WebViewHostDialog::theme_apply_script()));
|
||||
}
|
||||
|
||||
void PluginDockPanel::push_message(const nlohmann::json& data)
|
||||
{
|
||||
if (m_browser == nullptr || m_closing)
|
||||
return;
|
||||
|
||||
nlohmann::json envelope;
|
||||
envelope["data"] = data;
|
||||
// The page may still be loading, so wait briefly for the bridge to define __orcaDispatch.
|
||||
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"
|
||||
"})(%s, 0);",
|
||||
wxString::FromUTF8(envelope.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace))));
|
||||
}
|
||||
|
||||
void PluginDockPanel::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 PluginDockPanel::request_close()
|
||||
{
|
||||
if (m_closing)
|
||||
return;
|
||||
fire_close();
|
||||
// A close requested by the page arrives inside the web view's own script-message callback,
|
||||
// which must return before the web view is destroyed.
|
||||
CallAfter([this]() { remove_pane(); });
|
||||
}
|
||||
|
||||
void PluginDockPanel::destroy_for_plugin()
|
||||
{
|
||||
m_closing = true;
|
||||
m_on_close = nullptr;
|
||||
remove_pane();
|
||||
}
|
||||
|
||||
void PluginDockPanel::remove_pane()
|
||||
{
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->remove_plugin_pane(this);
|
||||
else
|
||||
Destroy();
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -1,63 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <wx/panel.h>
|
||||
#include <wx/webview.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// Name of a plugin's pane in the Plater's dock manager. It stays the same across sessions, so the
|
||||
// saved window layout can put the pane back, and it never contains a wxAuiManager layout delimiter.
|
||||
std::string plugin_pane_name(const std::string& plugin_key, const std::string& title);
|
||||
|
||||
// The pane part saved for `pane_name` in a wxAuiManager layout string, in the form
|
||||
// wxAuiManager::LoadPaneInfo() takes, or empty when the layout has no such pane.
|
||||
std::string plugin_pane_layout_entry(const std::string& layout, const std::string& pane_name);
|
||||
|
||||
// Plugin-supplied HTML hosted in a pane of the Plater's dock manager, bridged to the plugin through
|
||||
// the same window.orca API as PluginWebDialog. Python-agnostic for the same reason: it can be
|
||||
// destroyed on the main thread without the GIL, so its hooks must not capture pybind11 objects.
|
||||
class PluginDockPanel : public wxPanel
|
||||
{
|
||||
public:
|
||||
using MessageHandler = std::function<void(const nlohmann::json& data)>;
|
||||
using CloseHandler = std::function<void()>;
|
||||
|
||||
// on_close fires once, on a user or page initiated close. on_destroyed runs from the destructor
|
||||
// on every path and must touch host-side state only.
|
||||
PluginDockPanel(wxWindow* parent,
|
||||
const std::string& html,
|
||||
MessageHandler on_message,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed);
|
||||
~PluginDockPanel() 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 firing on_close, for plugin unload.
|
||||
void destroy_for_plugin();
|
||||
// Fires on_close at most once. The Plater calls it when the pane's own close button is used.
|
||||
void fire_close();
|
||||
|
||||
private:
|
||||
void on_bootstrap_event(wxWebViewEvent& event);
|
||||
void on_script_message(wxWebViewEvent& event);
|
||||
void on_webview_recreated(wxCommandEvent& event);
|
||||
void remove_pane();
|
||||
|
||||
wxWebView* m_browser{nullptr};
|
||||
std::string m_html;
|
||||
bool m_content_loaded{false};
|
||||
bool m_closing{false};
|
||||
MessageHandler m_on_message;
|
||||
CloseHandler m_on_close;
|
||||
CloseHandler m_on_destroyed;
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -57,10 +57,6 @@ wxString web_base_url()
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* PluginWebDialog::bridge_user_script() { return ORCA_BRIDGE_JS; }
|
||||
|
||||
wxString PluginWebDialog::content_base_url() { return web_base_url(); }
|
||||
|
||||
PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
||||
const wxString& title,
|
||||
const std::string& html,
|
||||
@@ -79,7 +75,7 @@ PluginWebDialog::PluginWebDialog(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(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.
|
||||
|
||||
@@ -47,12 +47,6 @@ public:
|
||||
static void request_close(PluginWebDialog* dialog);
|
||||
static void destroy_for_plugin(PluginWebDialog* dialog);
|
||||
|
||||
// Shared with PluginDockPanel: the bundled blank page a plugin web view loads before the plugin
|
||||
// HTML is swapped in, the window.orca bridge, and the base URL the plugin HTML is loaded against.
|
||||
static constexpr const char* BOOTSTRAP_PAGE = "web/dialog/PluginWebDialog/blank.html";
|
||||
static const char* bridge_user_script();
|
||||
static wxString content_base_url();
|
||||
|
||||
// Push a payload to the page; delivered to handlers registered via
|
||||
// window.orca.onMessage(). MAIN-THREAD ONLY (the plugin layer marshals).
|
||||
void push_message(const nlohmann::json& data);
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -52,8 +52,6 @@ public:
|
||||
// Shared by modeless Pages tabs and PluginWebDialog.
|
||||
static std::string theme_user_script();
|
||||
static std::string plugin_defaults_user_script();
|
||||
// Re-themes an already-loaded page in place, for web views hosted outside a dialog.
|
||||
static std::string theme_apply_script();
|
||||
|
||||
protected:
|
||||
wxWebView* browser() const { return m_browser; }
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/MainFrame.hpp>
|
||||
#include <slic3r/GUI/MsgDialog.hpp>
|
||||
#include <slic3r/GUI/Plater.hpp>
|
||||
#include <slic3r/GUI/PluginDockPanel.hpp>
|
||||
#include <slic3r/GUI/PluginProgressDialog.hpp>
|
||||
#include <slic3r/GUI/PluginWebDialog.hpp>
|
||||
#include <slic3r/GUI/NotificationManager.hpp>
|
||||
@@ -22,7 +20,6 @@
|
||||
#include <wx/defs.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <future>
|
||||
@@ -112,25 +109,6 @@ GUI::PluginWebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
|
||||
};
|
||||
}
|
||||
|
||||
// The plugin's on_close: fired only on a user/JS-initiated close (not forced teardown), while the
|
||||
// window is alive. Empty if the plugin passed None.
|
||||
std::function<void()> make_close_adapter(const CallablePtr& holder)
|
||||
{
|
||||
if (!holder)
|
||||
return nullptr;
|
||||
return [holder]() {
|
||||
PythonGILState gil;
|
||||
if (!gil)
|
||||
return;
|
||||
try {
|
||||
holder->fn();
|
||||
} catch (py::error_already_set& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
|
||||
PyErr_Clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Registry of live plugin UI resources. Keyed by an opaque id; tracks the
|
||||
// owning plugin so all of a plugin's UI can be torn down on unload.
|
||||
@@ -372,7 +350,22 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
||||
if (!UiRegistry::instance().is_open(new_id))
|
||||
return;
|
||||
|
||||
GUI::PluginWebDialog::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); };
|
||||
|
||||
@@ -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, const std::string& dock, int width,
|
||||
int height, py::object on_message, py::object on_close)
|
||||
{
|
||||
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::PluginDockPanel(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_plugin_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::PluginDockPanel*>(window))
|
||||
panel->push_message(j);
|
||||
else
|
||||
GUI::PluginWebDialog::post_message(dynamic_cast<GUI::PluginWebDialog*>(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::PluginDockPanel*>(window))
|
||||
panel->request_close();
|
||||
else
|
||||
GUI::PluginWebDialog::request_close(dynamic_cast<GUI::PluginWebDialog*>(window));
|
||||
});
|
||||
}
|
||||
|
||||
void handle_show(int id, bool show)
|
||||
{
|
||||
if (wxTheApp == nullptr)
|
||||
return;
|
||||
GUI::wxGetApp().CallAfter([id, show]() {
|
||||
auto* panel = UiRegistry::instance().get_as<GUI::PluginDockPanel>(id);
|
||||
GUI::Plater* plater = GUI::wxGetApp().plater();
|
||||
if (panel != nullptr && plater != nullptr)
|
||||
plater->show_plugin_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("dock") = "right", py::arg("width") = 320, py::arg("height") = 480,
|
||||
py::arg("on_message") = py::none(), py::arg("on_close") = py::none(),
|
||||
"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 the "
|
||||
"saved window layout remembers both. 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)
|
||||
@@ -734,8 +632,6 @@ void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
|
||||
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
|
||||
if (auto* dialog = dynamic_cast<GUI::PluginWebDialog*>(window))
|
||||
GUI::PluginWebDialog::destroy_for_plugin(dialog);
|
||||
else if (auto* panel = dynamic_cast<GUI::PluginDockPanel*>(window))
|
||||
panel->destroy_for_plugin();
|
||||
else if (window != nullptr)
|
||||
window->Destroy();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <libslic3r/Model.hpp>
|
||||
#include <libslic3r/PresetBundle.hpp>
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
#include <slic3r/GUI/PluginDockPanel.hpp>
|
||||
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
||||
|
||||
#include "python_test_support.hpp"
|
||||
@@ -142,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.
|
||||
@@ -154,45 +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);
|
||||
}
|
||||
|
||||
// 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 plugin pane's saved layout entry is found by pane name", "[PluginHost]")
|
||||
{
|
||||
using Slic3r::GUI::plugin_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(plugin_pane_layout_entry(layout, "plugin:demo:Scene") == plugin);
|
||||
CHECK(plugin_pane_layout_entry(layout, "sidebar") == sidebar);
|
||||
CHECK(plugin_pane_layout_entry(layout, "plugin:demo").empty());
|
||||
CHECK(plugin_pane_layout_entry("", "plugin:demo:Scene").empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
|
||||
|
||||
Reference in New Issue
Block a user