Compare commits

...
Author SHA1 Message Date
Kris Austin e0410db22e fix: hide the CAD gizmos unless the experimental CAD feature is enabled (#15762) 2026-09-18 18:38:55 -03:00
Kris Austin 8eac7aafdb ci: fix the flatpak dependency cache broken by #14709 (#15763) 2026-09-18 17:47:28 -03:00
Ian Bassi f956914a11 Update OrcaSlicer_de.po (#15766) 2026-09-18 16:12:11 -03:00
packerlschupfer f9e9cff53a calib: defensive guards in find_optimal_PA_speed (fix CLI pa-tower SIGSEGV) (#14414) 2026-09-18 14:26:14 -03:00
Ian Bassi 8a17df4a89 Fix some overhang detection (#15694) 2026-09-18 14:23:38 -03:00
Kiss Lorandandyw4z e573fc4680 Align Publish 3MF dialog styling and fix tab-switch flicker (#15695)
* Fix Publish 3MF dialog styling

Use Orca's shared checkbox widget in the Publish 3MF dialog. Keep checkbox labels clickable, preserve toggle event propagation and disabled-row state, and use Windows-only double buffering on the tab page hosts to reduce flicker during page switches.

Also align the dialog's guide-link color with existing Orca dialogs.

* Fix Publish 3MF dialog styling

Use Orca's shared checkbox widget in the Publish 3MF dialog. Keep checkbox labels clickable, preserve toggle event propagation and disabled-row state, and use Windows-only double buffering on the tab page hosts to reduce flicker during page switches.

Also align the dialog's guide-link color with existing Orca dialogs.

* match font size and left margins

---------

Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-09-18 19:25:53 +03:00
yw4zandNoisyfox c5f9257878 Compact bbl nozzle UI (#15083)
* init

* drop usage of StaticGroup for ExtruderGroup

* completely remove StaticGroup from project

* fix alignment of "Not installed" text

* fix crash on linux while clicking edit button

* Fix background color on macOS

---------

Co-authored-by: Noisyfox <timemanager.rick@gmail.com>
2026-09-18 23:43:42 +08:00
inslogic3d 9bd4432f27 Add INSLOGIC filament profiles to OrcaFilamentLibrary (#15398)
Merged by /bot merge on behalf of @inslogic3d (id 321604763).
Grants: resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC, resources/profiles/OrcaFilamentLibrary.json
Head: 22a51b32f0
2026-09-18 15:43:33 +00:00
SoftFever 3f001489bf Speed Dial Enhancements (#15562) 2026-09-18 23:21:19 +08:00
Kris Austin 7472b87b67 build: fix the macro redefinition that fails clang Debug builds (#15759)
libslic3r_version.h defines ORCA_CHECK_GCODE_PLACEHOLDERS from the CMake
option, 0 by default, and GCode.hpp then forces it to 1 whenever NDEBUG
is undefined. Clang reports the second definition under
-Wmacro-redefined, which is on by default, so since -Werror (#15660)
every clang Debug build fails at the libslic3r files that include
GCode.hpp. Release and RelWithDebInfo define NDEBUG and never compile
the override, which is why no CI configuration sees it. The override
dates from #3861 and has warned in every Debug build since.

Undefine the macro before overriding it. Debug builds get the same
value the redefinition already produced, so nothing else changes.

Follow-up to #15749, refs #15748.
2026-09-18 11:56:44 -03:00
CliffordandClaude Opus 5 83e729d20e fix(build): drop two unused lambda captures that break non-Release clang builds (#15749)
fix(build): drop two unused lambda captures that break non-Release builds

Both handlers capture this and never use it. They sit inside
#if !BBL_RELEASE_TO_PUBLIC, which CMakeLists defines as $<CONFIG:Release>, so
the code compiles in Debug and RelWithDebInfo but not in Release. Clang warns
on an unused capture under -Wall, and since every warning became an error the
two captures fail any clang build that is not Release - an Xcode scheme on its
default Debug configuration, for instance. The CI matrix builds Release, where
the block does not exist, and GCC does not implement the warning at all, so
nothing in CI can see it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 11:42:31 -03:00
Ian Chua 080f27f602 fix: windows unsubscribed loaded plugin not allowed (#15751)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->
On windows, after installing and loading a plugin, if you try to
unsubscribe from a loaded plugin, and on refresh, it will be an orphaned
plugin.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
On unsubscribing from a loaded cloud plugin
<img width="1418" height="862" alt="image"
src="https://github.com/user-attachments/assets/9ba8353c-1645-4b21-86ff-2ed235f4d259"
/>

On plugin refresh
<img width="1418" height="862" alt="image"
src="https://github.com/user-attachments/assets/fd58c144-7970-4448-ba3b-e72e52980467"
/>

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-18 21:32:57 +08:00
Lam Wei Lun 4ea50a33e4 Testing macOS fixes 2026-09-18 19:07:03 +08:00
Ian Chua 23c77f15cf feat: add CI to generate OPC for OTA workflow (#15624)
# Description

Adds the CI half of the profile OTA pipeline: a push-triggered workflow
that
rebuilds a vendor's binary preset cache (`<vendor>.opc`) whenever its
profile
changes on `main` / `release/*`, and publishes it as a versioned release
asset
for OrcaCloud's OTA Manager to pick up.

### `.github/workflows/post_merge_profiles.yml` (new)

Push-triggered counterpart to `check_profiles.yml` (which only gates
PRs):

- Diffs the push to find which vendors under `resources/profiles/**`
changed.
- Reads the Orca version from `version.inc` and each vendor's 4-part
`version`
from `resources/profiles/<vendor>.json` (fails the run if it isn't
`A.B.C.D`).
- Downloads the prebuilt `generate_system_cache` from this repo's
`nightly-builds` release and builds one `<vendor>.opc` per changed
vendor.
- Packages each as
`<orca_ver>_<vendor>_<profile_version>_<UTCyyyymmddHHMM>.zip` (zip root
`<vendor>.opc`) — the asset-name contract OrcaCloud's release scanner
expects.
- Uploads them to a per-Orca-version release on the profiles repo via a
scoped
  GitHub App token.

It stops there: no changelog, no R2, no OTA webhook — a maintainer still
publishes from the OTA Manager. Job is guarded to
`OrcaSlicer/OrcaSlicer`;
workflow permissions are `contents: read` (the cross-repo write uses the
App
token only).

### `.github/workflows/build_orca.yml`

Two steps on the Linux leg: upload `generate_system_cache` as a CI
artifact,
and (on `main`) deploy it to the `nightly-builds` release as
`generate_system_cache_Linux_Ubuntu2404_nightly` so the workflow above
has a
tool to download. `.opc` is 64-bit little-endian and
platform-independent, so
only the Linux binary is shipped.

### `src/dev-utils/generate_system_cache.cpp`

New `-v` / `--vendor` option to generate the cache for a single vendor
(plus the
always-loaded Orca filament library) instead of all vendors, and an
error if the
named vendor produced no `.opc` (catches typos). Reuses
`PresetBundle::set_vendor_to_validate()` from #14217.

## Operational prerequisites

- Repo secrets `PROFILES_APP_ID` / `PROFILES_APP_PRIVATE_KEY` for a
GitHub App
  with `contents: write` on the target profiles repo.
- `env.PROFILES_OWNER` / `env.PROFILES_REPO` in
`post_merge_profiles.yml` must
  point at the production profiles repo OrcaCloud reads.
- `generate_system_cache_Linux_Ubuntu2404_nightly` only appears after
the first
post-merge nightly `build_orca` run; a profile-only push before then
fails at
  the download step.

## Screenshots/Recordings/Graphs

n/a — CI only.

## Tests

Exercised end-to-end in a staging environment: per-vendor `.opc` zips
published
in the expected `<orca_ver>_<vendor>_<profile_version>_<timestamp>.zip`
layout,
and `generate_system_cache -v <vendor>` confirmed to emit only that
vendor's
`.opc`.

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-18 18:53:26 +08:00
Ian Chua 00f78c18cf Merge branch 'main' into fix/windows-unsub-plugin 2026-09-18 18:51:50 +08:00
SoftFever 1159ca5f7f Merge branch 'main' into weilun/speed_dial 2026-09-18 17:28:24 +08:00
SoftFever 2dd3ef7cff fix build errors on mac 2026-09-18 17:24:47 +08:00
peachismomo 2bfab589ee fix: windows unsubsribe loaded plugin not allowed 2026-09-18 16:07:25 +08:00
Lam Wei Lun 8f4e3dde55 Merge main 2026-09-18 15:59:00 +08:00
Ian Chua 4a72a3bba2 Merge branch 'main' into feat/ota-opc-ci 2026-09-18 14:56:00 +08:00
Lam Wei Lun 988108c8ef Fix flatpak test 2026-09-16 18:31:39 +08:00
Lam Wei Lun 075a84093f Fixes issue with showing hidden settings in speed dial. 2026-09-16 16:02:39 +08:00
Lam Wei Lun b01d18bba3 Tab autocomplete. Better search integration of categories and action names 2026-09-16 13:06:16 +08:00
Lam Wei Lun f76e4b1e02 Fixed centering of icons. Disabled zooming in/out on Windows. Added default icons 2026-09-16 12:15:27 +08:00
Lam Wei Lun 435336a3cf Merge branch 'main' into weilun/speed_dial 2026-09-16 10:20:13 +08:00
Ian Chua 173706750d Merge branch 'main' into feat/ota-opc-ci 2026-09-15 15:19:43 +08:00
Lam Wei Lun e1d3c90030 Fixes for window rounding 2026-09-14 18:08:34 +08:00
Lam Wei Lun 729638cd95 Merge from main 2026-09-14 17:02:07 +08:00
Lam Wei Lun f070f493e1 Merge branch 'main' into weilun/speed_dial 2026-09-14 16:35:48 +08:00
Lam Wei Lun 9ed8537343 Rounded corner for the dialog 2026-09-14 15:32:51 +08:00
Lam Wei Lun 0131533ae0 Get speed dial to work with translations 2026-09-14 14:59:06 +08:00
Lam Wei Lun 87e278c4b3 Remove unused capture 2026-09-14 12:02:06 +08:00
Lam Wei Lun 1d4b920b30 Merge branch 'main' into weilun/speed_dial
# Conflicts:
#	src/slic3r/GUI/Tab.cpp
2026-09-14 10:37:16 +08:00
Lam Wei Lun 50daf5abf3 Merge main 2026-09-11 17:31:56 +08:00
Lam Wei Lun 966e029b95 Code refactoring for better maintainability 2026-09-11 17:15:57 +08:00
Lam Wei Lun c57b74731e Tooltip bottom bar for process/filament/printer settings 2026-09-11 16:38:14 +08:00
Lam Wei Lun 40693a4c94 merge main and fix conflicts 2026-09-11 15:46:20 +08:00
Lam Wei Lun 70a2b3a814 Code cleanup, dedup, update unit tests 2026-09-11 15:43:46 +08:00
Lam Wei Lun 81458dae86 Add category headers. Alphabetical sorting when no search query. Recent count adjustments 2026-09-11 14:56:59 +08:00
Lam Wei Lun 7c2991d00c Update translations. Code dedup and cleanup 2026-09-11 13:05:39 +08:00
Lam Wei Lun 42bee12481 Speed Dial: replace tile monograms with native SVG icons
Tiles previously rendered a colored monogram (title/source initials plus an ordinal) with a per-id hue. Show the matching native SVG icon instead:

- AppAction/NativeCommand gain an `icon` field; commands get a curated key->icon table and settings inherit their group header's icon.
- Notebook tracks each page's resource icon name and reports it in tab_options(), so the tab picker can show it too.
- Searcher records the group icon so settings keep it through search.
- Web tile rendering swaps monogramFor/hue for an <img>; drop the now-unused hue/text CSS vars. Add a test asserting every non-empty icon resolves to a shipped SVG.
2026-09-11 11:36:24 +08:00
Lam Wei Lun 792ab183e4 Merge branch 'main' into weilun/speed_dial 2026-09-11 11:14:19 +08:00
Ian Chua ffb2946b20 feat: compare with environment variable FOLDER_MERGERS for verified folders 2026-09-10 20:09:37 +08:00
Lam Wei Lun 8bbce371b2 Add Help actions. Add Toggle Developer action. Add warning popup when switching between process mode. Add primitives/handy models actions 2026-09-10 18:34:05 +08:00
Lam Wei Lun f520af9901 Merge branch 'main' into weilun/speed_dial 2026-09-10 16:39:47 +08:00
Ian Chua e89a05b1e4 fix: repo name 2026-09-10 16:38:03 +08:00
Ian Chua 02a8011b97 feat: add CI to generate OPC for OTA workflow 2026-09-10 16:27:23 +08:00
Lam Wei Lun 1d44320f30 Updated unit tests 2026-09-10 13:29:57 +08:00
Lam Wei Lun 16d285fea6 Cleanup comments and formatting 2026-09-10 13:00:23 +08:00
Lam Wei Lun 9fc6c45770 Fixes potential focus bug 2026-09-10 12:52:10 +08:00
Lam Wei Lun b4beae4be3 Fixed potential UB 2026-09-10 12:47:43 +08:00
Lam Wei Lun a0ba8e12e5 Change to a pin/bookmark icon.
Added shortcut (Ctrl/Cmd+B) to pin/unpin actions.

Clear unresolved pinned actions when switching between modes.

Fixed scroll-back issue when scrolling to pin action
2026-09-10 12:07:36 +08:00
Lam Wei Lun 2346b5c259 Merge branch 'weilun/speed_dial' of https://github.com/lamweilun/OrcaSlicer into weilun/speed_dial 2026-09-10 12:06:48 +08:00
SoftFever 0c07ec9c3c Merge branch 'main' into weilun/speed_dial 2026-09-10 11:54:13 +08:00
Lam Wei Lun 4c6ce75e78 Merge branch 'main' into weilun/speed_dial 2026-09-10 09:35:43 +08:00
Lam Wei Lun 0fb87f05b1 Remove printer connect/disconnect for now 2026-09-09 18:05:49 +08:00
Lam Wei Lun 0709c9931e Slight refactor for opening Plugins in speed dial 2026-09-09 17:36:21 +08:00
Lam Wei Lun 3985200672 Search improvements. Code refactoring so its easier to maintain. Experimental features for speed dial: connect/disconnect from printer. 2026-09-09 16:53:58 +08:00
Lam Wei Lun c3a21fa0d4 Switched back to jump to setting instead 2026-09-09 12:26:09 +08:00
Lam Wei Lun cae23a933e Plate controls 2026-09-09 11:29:18 +08:00
Lam Wei Lun a1ee7f099f Merge branch 'main' into weilun/speed_dial 2026-09-09 10:06:18 +08:00
Lam Wei Lun 846a95374f Fixed unit test issue. Added basic object manipulation to actions. Added calibration wizards to actions. Added View controls to actions 2026-09-08 18:23:51 +08:00
Lam Wei Lun 2dce0ad24f Back to spacebar for speed dial shortcut. Integrated inline process settings editing. Optimized saerch results 2026-09-08 16:46:56 +08:00
Lam Wei Lun b32f28e712 Initial Commit of speed dial improvement 2026-09-07 14:00:28 +08:00
119 changed files with 74261 additions and 2816 deletions
+9 -8
View File
@@ -330,12 +330,13 @@ jobs:
# sources are unchanged, so a re-run of the same commit would skip the # sources are unchanged, so a re-run of the same commit would skip the
# OrcaSlicer module and ship no test asset. A per-run value in that module's # OrcaSlicer module and ship no test asset. A per-run value in that module's
# env keeps it rebuilding; orca_deps stays cached, and the compiler cache # env keeps it rebuilding; orca_deps stays cached, and the compiler cache
# still serves the rebuild. # still serves the rebuild. run-tests on the same module builds the test
- name: Inject commit hash and flatpak-builder cache buster into Flatpak manifest # binaries.
- name: Inject commit hash, run-tests and cache buster into Flatpak manifest
env: env:
flatpak_builder_cache_buster: ${{ github.run_id }}-${{ github.run_attempt }} flatpak_builder_cache_buster: ${{ github.run_id }}-${{ github.run_attempt }}
run: | run: |
sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n build-options:\n env:\n flatpak_builder_cache_buster: \"$flatpak_builder_cache_buster\"\n git_commit_hash: \"$git_commit_hash\"|}" \ sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n run-tests: true\n build-options:\n env:\n flatpak_builder_cache_buster: \"$flatpak_builder_cache_buster\"\n git_commit_hash: \"$git_commit_hash\"|}" \
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
shell: bash shell: bash
# flatpak-builder's --ccache only wraps cc and gcc, and the manifest builds # flatpak-builder's --ccache only wraps cc and gcc, and the manifest builds
@@ -381,9 +382,6 @@ jobs:
save-cache: false save-cache: false
arch: ${{ matrix.variant.arch }} arch: ${{ matrix.variant.arch }}
upload-artifact: false upload-artifact: false
# run-tests fires the module's build-only test-commands; keep-build-dirs
# retains the binaries for the packaging step below.
run-tests: true
keep-build-dirs: true keep-build-dirs: true
# The build has just touched everything it can use, so an object untouched # The build has just touched everything it can use, so an object untouched
# for a week is dead, usually orphaned by a flag change. # for a week is dead, usually orphaned by a flag change.
@@ -449,10 +447,13 @@ jobs:
# the bounds checks are compiled in, so a stripped exe still catches them. # the bounds checks are compiled in, so a stripped exe still catches them.
find "$d/build_flatpak/tests" -type f -perm -u+x -exec strip --strip-unneeded {} + 2>/dev/null || true find "$d/build_flatpak/tests" -type f -perm -u+x -exec strip --strip-unneeded {} + 2>/dev/null || true
# At runtime the tests read tests/ (TEST_DATA_DIR), scripts/, and under # At runtime the tests read tests/ (TEST_DATA_DIR), scripts/, and under
# resources/ the shipped profiles (PROFILES_DIR) and the printers/ maps. # resources/ the shipped profiles (PROFILES_DIR), the printers/ maps, and
# the icon SVGs (NativeCommands icon names are checked against them).
find "$d" -mindepth 1 -maxdepth 1 -type d \ find "$d" -mindepth 1 -maxdepth 1 -type d \
! -name tests ! -name build_flatpak ! -name scripts ! -name resources -exec rm -rf {} + ! -name tests ! -name build_flatpak ! -name scripts ! -name resources -exec rm -rf {} +
find "$d/resources" -mindepth 1 -maxdepth 1 ! -name profiles ! -name printers -exec rm -rf {} + find "$d/resources" -mindepth 1 -maxdepth 1 ! -name profiles ! -name printers ! -name images -exec rm -rf {} +
# Only the SVGs are read; the png/ico/icns/gif assets are ~35MB of dead weight.
find "$d/resources/images" -mindepth 1 -maxdepth 1 ! -name '*.svg' -exec rm -rf {} + 2>/dev/null || true
tar -cf flatpak-test-asset.tar flatpak_app "$d" tar -cf flatpak-test-asset.tar flatpak_app "$d"
- name: Upload flatpak test asset - name: Upload flatpak test asset
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
+23
View File
@@ -685,6 +685,18 @@ jobs:
name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }} name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/Release/OrcaSlicer_profile_validator' 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 - name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0 uses: WebFreak001/deploy-nightly@v3.2.0
@@ -715,6 +727,17 @@ jobs:
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
max_releases: 1 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 - 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' }} 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 uses: WebFreak001/deploy-nightly@v3.2.0
+253
View File
@@ -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"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5
View File
@@ -125,6 +125,7 @@ src/slic3r/GUI/ThermalPreconditioningDialog.hpp
src/slic3r/GUI/Jobs/SLAImportJob.cpp src/slic3r/GUI/Jobs/SLAImportJob.cpp
src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp
src/slic3r/GUI/AboutDialog.cpp src/slic3r/GUI/AboutDialog.cpp
src/slic3r/GUI/ActionRegistry.cpp
src/slic3r/GUI/AMSMaterialsSetting.cpp src/slic3r/GUI/AMSMaterialsSetting.cpp
src/slic3r/GUI/ExtrusionCalibration.cpp src/slic3r/GUI/ExtrusionCalibration.cpp
src/slic3r/GUI/AmsMappingPopup.cpp src/slic3r/GUI/AmsMappingPopup.cpp
@@ -159,6 +160,7 @@ src/slic3r/GUI/SelectMachinePop.cpp
src/slic3r/GUI/StatusPanel.cpp src/slic3r/GUI/StatusPanel.cpp
src/slic3r/GUI/Monitor.cpp src/slic3r/GUI/Monitor.cpp
src/slic3r/GUI/MsgDialog.cpp src/slic3r/GUI/MsgDialog.cpp
src/slic3r/GUI/NativeCommands.cpp
src/slic3r/GUI/NotificationManager.hpp src/slic3r/GUI/NotificationManager.hpp
src/slic3r/GUI/NotificationManager.cpp src/slic3r/GUI/NotificationManager.cpp
src/slic3r/GUI/ObjectDataViewModel.cpp src/slic3r/GUI/ObjectDataViewModel.cpp
@@ -179,6 +181,8 @@ src/slic3r/GUI/PublishDialog.cpp
src/slic3r/GUI/PublishSettingsDialog.cpp src/slic3r/GUI/PublishSettingsDialog.cpp
src/slic3r/GUI/SavePresetDialog.cpp src/slic3r/GUI/SavePresetDialog.cpp
src/slic3r/GUI/Search.cpp src/slic3r/GUI/Search.cpp
src/slic3r/GUI/SettingsIndex.cpp
src/slic3r/GUI/SpeedDialDialog.cpp
src/slic3r/GUI/Selection.cpp src/slic3r/GUI/Selection.cpp
src/slic3r/GUI/SelectMachine.cpp src/slic3r/GUI/SelectMachine.cpp
src/slic3r/GUI/PrePrintChecker.cpp src/slic3r/GUI/PrePrintChecker.cpp
@@ -292,5 +296,6 @@ src/slic3r/GUI/AMSDryControl.cpp
src/slic3r/GUI/AMSDryControl.hpp src/slic3r/GUI/AMSDryControl.hpp
src/libslic3r/PresetBundle.cpp src/libslic3r/PresetBundle.cpp
src/slic3r/GUI/CAD/DesignPanel.cpp src/slic3r/GUI/CAD/DesignPanel.cpp
src/slic3r/GUI/CAD/SketchInlineEditor.cpp
src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp
src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M14.5,1.5v12a1,1,0,0,1-1,1H1.5a1,1,0,0,1-1-1V1.5a1,1,0,0,1,1-1h12a1,1,0,0,1,1,1Z" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><polyline points="4,5 6.5,7.5 4,10" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/><line x1="8" y1="10" x2="11" y2="10" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 524 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><path d="M11.5,5.5v5c0,.55-.45,1-1,1H1.5c-.55,0-1-.45-1-1V1.5C.5.95.95.5,1.5.5h5M3.5,6.5v2h2l5.59-5.59c.55-.55.55-1.45,0-2s-1.45-.55-2,0L3.5,6.5Z" style="fill:none; stroke:#949494; stroke-linecap:square; stroke-linejoin:round;"/></svg>

After

Width:  |  Height:  |  Size: 340 B

+65 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "OrcaFilamentLibrary", "name": "OrcaFilamentLibrary",
"version": "02.04.00.07", "version": "02.04.00.08",
"force_update": "0", "force_update": "0",
"description": "Orca Filament Library", "description": "Orca Filament Library",
"filament_list": [ "filament_list": [
@@ -304,6 +304,14 @@
"name": "Generic PA-CF @System", "name": "Generic PA-CF @System",
"sub_path": "filament/Generic PA-CF @System.json" "sub_path": "filament/Generic PA-CF @System.json"
}, },
{
"name": "INSLOGIC PA6-66 @base",
"sub_path": "filament/INSLOGIC/INSLOGIC PA6-66 @base.json"
},
{
"name": "INSLOGIC PA6-CF @base",
"sub_path": "filament/INSLOGIC/INSLOGIC PA6-CF @base.json"
},
{ {
"name": "Bambu PC @base", "name": "Bambu PC @base",
"sub_path": "filament/Bambu/Bambu PC @base.json" "sub_path": "filament/Bambu/Bambu PC @base.json"
@@ -436,6 +444,10 @@
"name": "GreenGate3D PETG @base", "name": "GreenGate3D PETG @base",
"sub_path": "filament/GreenGate3D/GreenGate3D PETG @base.json" "sub_path": "filament/GreenGate3D/GreenGate3D PETG @base.json"
}, },
{
"name": "INSLOGIC PETG Pro @base",
"sub_path": "filament/INSLOGIC/INSLOGIC PETG Pro @base.json"
},
{ {
"name": "JAYO PETG @base", "name": "JAYO PETG @base",
"sub_path": "filament/JAYO/JAYO PETG @base.json" "sub_path": "filament/JAYO/JAYO PETG @base.json"
@@ -636,6 +648,10 @@
"name": "Generic PLA-CF @System", "name": "Generic PLA-CF @System",
"sub_path": "filament/Generic PLA-CF @System.json" "sub_path": "filament/Generic PLA-CF @System.json"
}, },
{
"name": "INSLOGIC PLA Pro @base",
"sub_path": "filament/INSLOGIC/INSLOGIC PLA Pro @base.json"
},
{ {
"name": "JAYO PLA @base", "name": "JAYO PLA @base",
"sub_path": "filament/JAYO/JAYO PLA @base.json" "sub_path": "filament/JAYO/JAYO PLA @base.json"
@@ -1116,6 +1132,30 @@
"name": "Fiberon PA612-CF @System", "name": "Fiberon PA612-CF @System",
"sub_path": "filament/Polymaker/Fiberon PA612-CF @System.json" "sub_path": "filament/Polymaker/Fiberon PA612-CF @System.json"
}, },
{
"name": "INSLOGIC PA6-66 @BBL P2S",
"sub_path": "filament/INSLOGIC/BBL/INSLOGIC PA6-66 @BBL P2S.json"
},
{
"name": "INSLOGIC PA6-66 @P1-X1",
"sub_path": "filament/INSLOGIC/BBL/INSLOGIC PA6-66 @P1-X1.json"
},
{
"name": "INSLOGIC PA6-66 @Prusa CORE One",
"sub_path": "filament/INSLOGIC/Prusa/INSLOGIC PA6-66 @Prusa CORE One.json"
},
{
"name": "INSLOGIC PA6-CF @BBL P2S",
"sub_path": "filament/INSLOGIC/BBL/INSLOGIC PA6-CF @BBL P2S.json"
},
{
"name": "INSLOGIC PA6-CF @P1-X1",
"sub_path": "filament/INSLOGIC/BBL/INSLOGIC PA6-CF @P1-X1.json"
},
{
"name": "INSLOGIC PA6-CF @Prusa CORE One",
"sub_path": "filament/INSLOGIC/Prusa/INSLOGIC PA6-CF @Prusa CORE One.json"
},
{ {
"name": "Bambu PC @System", "name": "Bambu PC @System",
"sub_path": "filament/Bambu/Bambu PC @System.json" "sub_path": "filament/Bambu/Bambu PC @System.json"
@@ -1312,6 +1352,18 @@
"name": "GreenGate3D PETG @System", "name": "GreenGate3D PETG @System",
"sub_path": "filament/GreenGate3D/GreenGate3D PETG @System.json" "sub_path": "filament/GreenGate3D/GreenGate3D PETG @System.json"
}, },
{
"name": "INSLOGIC PETG Pro @BBL P2S",
"sub_path": "filament/INSLOGIC/BBL/INSLOGIC PETG Pro @BBL P2S.json"
},
{
"name": "INSLOGIC PETG Pro @P1-X1",
"sub_path": "filament/INSLOGIC/BBL/INSLOGIC PETG Pro @P1-X1.json"
},
{
"name": "INSLOGIC PETG Pro @Prusa CORE One",
"sub_path": "filament/INSLOGIC/Prusa/INSLOGIC PETG Pro @Prusa CORE One.json"
},
{ {
"name": "JAYO PETG @BBL A1 0.4 nozzle", "name": "JAYO PETG @BBL A1 0.4 nozzle",
"sub_path": "filament/JAYO/BBL/JAYO PETG @BBL A1 0.4 nozzle.json" "sub_path": "filament/JAYO/BBL/JAYO PETG @BBL A1 0.4 nozzle.json"
@@ -1700,6 +1752,18 @@
"name": "Generic PLA Matte @System", "name": "Generic PLA Matte @System",
"sub_path": "filament/Generic PLA Matte @System.json" "sub_path": "filament/Generic PLA Matte @System.json"
}, },
{
"name": "INSLOGIC PLA Pro @BBL P2S",
"sub_path": "filament/INSLOGIC/BBL/INSLOGIC PLA Pro @BBL P2S.json"
},
{
"name": "INSLOGIC PLA Pro @P1-X1",
"sub_path": "filament/INSLOGIC/BBL/INSLOGIC PLA Pro @P1-X1.json"
},
{
"name": "INSLOGIC PLA Pro @Prusa CORE One",
"sub_path": "filament/INSLOGIC/Prusa/INSLOGIC PLA Pro @Prusa CORE One.json"
},
{ {
"name": "JAYO PLA @BBL A1 0.4 nozzle", "name": "JAYO PLA @BBL A1 0.4 nozzle",
"sub_path": "filament/JAYO/BBL/JAYO PLA @BBL A1 0.4 nozzle.json" "sub_path": "filament/JAYO/BBL/JAYO PLA @BBL A1 0.4 nozzle.json"
@@ -0,0 +1,275 @@
{
"type": "filament",
"name": "INSLOGIC PA6-66 @BBL P2S",
"inherits": "INSLOGIC PA6-66 @base",
"from": "system",
"setting_id": "UHHdgZgE7e8KR2GV",
"instantiation": "true",
"activate_air_filtration": [
"1"
],
"additional_cooling_fan_speed": [
"70"
],
"close_fan_the_first_x_layers": [
"1"
],
"close_additional_fan_first_x_layers": [
"3"
],
"complete_print_exhaust_fan_speed": [
"70"
],
"cool_plate_temp": [
"0"
],
"cool_plate_temp_initial_layer": [
"0"
],
"during_print_exhaust_fan_speed": [
"70"
],
"eng_plate_temp": [
"50"
],
"eng_plate_temp_initial_layer": [
"50"
],
"fan_cooling_layer_time": [
"20"
],
"fan_max_speed": [
"100"
],
"fan_min_speed": [
"60"
],
"filament_adaptive_volumetric_speed": [
"0"
],
"filament_extruder_compatibility": [
"0"
],
"filament_cooling_before_tower": [
"10",
"10"
],
"filament_preheat_temperature_delta": [
"0"
],
"filament_tower_interface_pre_extrusion_dist": [
"10"
],
"filament_tower_interface_pre_extrusion_length": [
"0"
],
"filament_tower_ironing_area": [
"4"
],
"filament_tower_interface_purge_volume": [
"20"
],
"filament_tower_interface_print_temp": [
"-1"
],
"filament_density": [
"1.10"
],
"filament_dev_ams_drying_ams_limitations": [
"1"
],
"filament_dev_ams_drying_heat_distortion_temperature": [
"90"
],
"filament_dev_ams_drying_temperature": [
"65",
"80",
"65",
"80"
],
"filament_dev_ams_drying_time": [
"12",
"12",
"12",
"12"
],
"filament_dev_chamber_drying_bed_temperature": [
"90.0"
],
"filament_dev_chamber_drying_time": [
"18"
],
"filament_dev_drying_cooling_temperature": [
"65"
],
"filament_dev_drying_softening_temperature": [
"85"
],
"filament_extruder_variant": [
"Direct Drive Standard",
"Direct Drive High Flow"
],
"filament_flow_ratio": [
"0.98",
"0.98"
],
"filament_flush_temp": [
"0"
],
"filament_flush_temp_fast": [
"0"
],
"filament_flush_volumetric_speed": [
"0"
],
"filament_is_support": [
"0"
],
"filament_max_volumetric_speed": [
"12",
"12"
],
"filament_minimal_purge_on_wipe_tower": [
"15"
],
"filament_pre_cooling_temperature": [
"0"
],
"filament_pre_cooling_temperature_nc": [
"0"
],
"filament_prime_volume": [
"45"
],
"filament_prime_volume_nc": [
"60"
],
"filament_printable": [
"3"
],
"filament_ramming_travel_time_nc": [
"0"
],
"filament_ramming_volumetric_speed": [
"-1"
],
"filament_ramming_volumetric_speed_nc": [
"-1"
],
"filament_scarf_seam_type": [
"none"
],
"filament_shrink": [
"100%"
],
"filament_soluble": [
"0"
],
"full_fan_speed_layer": [
"0"
],
"additional_fan_full_speed_layer": [
"0"
],
"filament_scarf_height": [
"10%"
],
"filament_scarf_gap": [
"0%"
],
"filament_scarf_length": [
"10"
],
"filament_ramming_travel_time": [
"0",
"0"
],
"filament_retract_length_nc": [
"14"
],
"filament_change_length": [
"10"
],
"hot_plate_temp": [
"50"
],
"hot_plate_temp_initial_layer": [
"50"
],
"long_retractions_when_ec": [
"0",
"0"
],
"nozzle_temperature": [
"255",
"255"
],
"nozzle_temperature_initial_layer": [
"255",
"255"
],
"nozzle_temperature_range_high": [
"280"
],
"nozzle_temperature_range_low": [
"250"
],
"overhang_fan_speed": [
"95"
],
"overhang_fan_threshold": [
"10%"
],
"reduce_fan_stop_start_freq": [
"0"
],
"required_nozzle_HRC": [
"3"
],
"retraction_distances_when_ec": [
"0",
"0"
],
"slow_down_for_layer_cooling": [
"1"
],
"slow_down_layer_time": [
"6"
],
"slow_down_min_speed": [
"10"
],
"supertack_plate_temp": [
"0"
],
"supertack_plate_temp_initial_layer": [
"0"
],
"temperature_vitrification": [
"74.2"
],
"textured_plate_temp": [
"50"
],
"textured_plate_temp_initial_layer": [
"50"
],
"volumetric_speed_coefficients": [
"0 0 0 0 0 0"
],
"filament_start_gcode": [
"; Filament gcode\nM145 P0 ; set airduct mode to cooling mode for cooling\nM106 P2 S255 ; turn on auxiliary fan for cooling\nM106 P3 S127 ; turn on chamber fan for cooling\nM1002 gcode_claim_action : 29\nM191 S0 ; wait for chamber temp\nM106 P2 S102 ; turn on chamber cooling fan\nM106 P10 S0 ; turn off left aux fan\nM142 P6 R30 S40 U0.3 V0.8 ; set PETG exhaust chamber autocooling\n"
],
"filament_end_gcode": [
"; filament end gcode \n\n"
],
"filament_adhesiveness_category": [
"400"
],
"idle_temperature": [
"0"
],
"compatible_printers": [
"Bambu Lab P2S 0.4 nozzle"
]
}
@@ -0,0 +1,210 @@
{
"type": "filament",
"name": "INSLOGIC PA6-66 @P1-X1",
"inherits": "INSLOGIC PA6-66 @base",
"from": "system",
"setting_id": "X6EcSgHdfuxoY1lL",
"instantiation": "true",
"cool_plate_temp": [
"0"
],
"eng_plate_temp": [
"40"
],
"hot_plate_temp": [
"50"
],
"textured_plate_temp": [
"50"
],
"cool_plate_temp_initial_layer": [
"0"
],
"eng_plate_temp_initial_layer": [
"40"
],
"hot_plate_temp_initial_layer": [
"50"
],
"textured_plate_temp_initial_layer": [
"50"
],
"overhang_fan_threshold": [
"95%"
],
"overhang_fan_speed": [
"100"
],
"slow_down_for_layer_cooling": [
"1"
],
"close_fan_the_first_x_layers": [
"3"
],
"filament_end_gcode": [
"; filament end gcode \n"
],
"filament_flow_ratio": [
"0.98",
"0.98"
],
"reduce_fan_stop_start_freq": [
"1"
],
"fan_cooling_layer_time": [
"15"
],
"filament_density": [
"1.2"
],
"filament_deretraction_speed": [
"nil",
"nil"
],
"filament_max_volumetric_speed": [
"12",
"12"
],
"filament_minimal_purge_on_wipe_tower": [
"15"
],
"filament_retraction_minimum_travel": [
"nil",
"nil"
],
"filament_retract_before_wipe": [
"nil",
"nil"
],
"filament_retract_when_changing_layer": [
"nil",
"nil"
],
"filament_retraction_length": [
"1",
"1"
],
"filament_z_hop": [
"nil",
"nil"
],
"filament_z_hop_types": [
"nil",
"nil"
],
"filament_retract_restart_extra": [
"nil",
"nil"
],
"filament_retraction_speed": [
"nil",
"nil"
],
"filament_soluble": [
"0"
],
"filament_wipe": [
"nil",
"nil"
],
"filament_wipe_distance": [
"nil",
"nil"
],
"nozzle_temperature_initial_layer": [
"255",
"255"
],
"full_fan_speed_layer": [
"2"
],
"fan_max_speed": [
"100"
],
"fan_min_speed": [
"60"
],
"slow_down_min_speed": [
"10"
],
"slow_down_layer_time": [
"2"
],
"filament_start_gcode": [
"; Filament gcode\n"
],
"nozzle_temperature": [
"255",
"255"
],
"temperature_vitrification": [
"70.4"
],
"nozzle_temperature_range_low": [
"250",
"250"
],
"nozzle_temperature_range_high": [
"280",
"280"
],
"filament_flush_temp": [
"0",
"0"
],
"filament_flush_volumetric_speed": [
"0",
"0"
],
"filament_long_retractions_when_cut": [
"0"
],
"filament_ramming_volumetric_speed": [
"-1",
"-1"
],
"filament_retraction_distances_when_cut": [
"nil",
"nil"
],
"filament_extruder_variant": [
"Direct Drive Standard",
"Direct Drive High Flow"
],
"filament_pre_cooling_temperature": [
"0",
"0"
],
"filament_ramming_travel_time": [
"0",
"0"
],
"filament_adaptive_volumetric_speed": [
"0",
"0"
],
"long_retractions_when_ec": [
"0",
"0"
],
"retraction_distances_when_ec": [
"0",
"0"
],
"volumetric_speed_coefficients": [
"0 0 0 0 0 0",
"0 0 0 0 0 0"
],
"idle_temperature": [
"0"
],
"additional_cooling_fan_speed": [
"70"
],
"compatible_printers": [
"Bambu Lab X1 0.4 nozzle",
"Bambu Lab X1 Carbon 0.4 nozzle",
"Bambu Lab P1S 0.4 nozzle",
"Bambu Lab X1E 0.4 nozzle"
]
}
@@ -0,0 +1,275 @@
{
"type": "filament",
"name": "INSLOGIC PA6-CF @BBL P2S",
"inherits": "INSLOGIC PA6-CF @base",
"from": "system",
"setting_id": "Km9iCy4A8kukE0AA",
"instantiation": "true",
"activate_air_filtration": [
"1"
],
"additional_cooling_fan_speed": [
"70"
],
"close_fan_the_first_x_layers": [
"1"
],
"close_additional_fan_first_x_layers": [
"3"
],
"complete_print_exhaust_fan_speed": [
"70"
],
"cool_plate_temp": [
"0"
],
"cool_plate_temp_initial_layer": [
"0"
],
"during_print_exhaust_fan_speed": [
"70"
],
"eng_plate_temp": [
"65"
],
"eng_plate_temp_initial_layer": [
"65"
],
"fan_cooling_layer_time": [
"20"
],
"fan_max_speed": [
"100"
],
"fan_min_speed": [
"60"
],
"filament_adaptive_volumetric_speed": [
"0"
],
"filament_extruder_compatibility": [
"0"
],
"filament_cooling_before_tower": [
"10",
"10"
],
"filament_preheat_temperature_delta": [
"0"
],
"filament_tower_interface_pre_extrusion_dist": [
"10"
],
"filament_tower_interface_pre_extrusion_length": [
"0"
],
"filament_tower_ironing_area": [
"4"
],
"filament_tower_interface_purge_volume": [
"20"
],
"filament_tower_interface_print_temp": [
"-1"
],
"filament_density": [
"1.12"
],
"filament_dev_ams_drying_ams_limitations": [
"1"
],
"filament_dev_ams_drying_heat_distortion_temperature": [
"90"
],
"filament_dev_ams_drying_temperature": [
"65",
"80",
"65",
"80"
],
"filament_dev_ams_drying_time": [
"12",
"12",
"12",
"12"
],
"filament_dev_chamber_drying_bed_temperature": [
"90.0"
],
"filament_dev_chamber_drying_time": [
"18"
],
"filament_dev_drying_cooling_temperature": [
"65"
],
"filament_dev_drying_softening_temperature": [
"85"
],
"filament_extruder_variant": [
"Direct Drive Standard",
"Direct Drive High Flow"
],
"filament_flow_ratio": [
"0.98",
"0.98"
],
"filament_flush_temp": [
"0"
],
"filament_flush_temp_fast": [
"0"
],
"filament_flush_volumetric_speed": [
"0"
],
"filament_is_support": [
"0"
],
"filament_max_volumetric_speed": [
"12",
"12"
],
"filament_minimal_purge_on_wipe_tower": [
"15"
],
"filament_pre_cooling_temperature": [
"0"
],
"filament_pre_cooling_temperature_nc": [
"0"
],
"filament_prime_volume": [
"45"
],
"filament_prime_volume_nc": [
"60"
],
"filament_printable": [
"3"
],
"filament_ramming_travel_time_nc": [
"0"
],
"filament_ramming_volumetric_speed": [
"-1"
],
"filament_ramming_volumetric_speed_nc": [
"-1"
],
"filament_scarf_seam_type": [
"none"
],
"filament_shrink": [
"100%"
],
"filament_soluble": [
"0"
],
"full_fan_speed_layer": [
"0"
],
"additional_fan_full_speed_layer": [
"0"
],
"filament_scarf_height": [
"10%"
],
"filament_scarf_gap": [
"0%"
],
"filament_scarf_length": [
"10"
],
"filament_ramming_travel_time": [
"0",
"0"
],
"filament_retract_length_nc": [
"14"
],
"filament_change_length": [
"10"
],
"hot_plate_temp": [
"65"
],
"hot_plate_temp_initial_layer": [
"65"
],
"long_retractions_when_ec": [
"0",
"0"
],
"nozzle_temperature": [
"275",
"275"
],
"nozzle_temperature_initial_layer": [
"280",
"280"
],
"nozzle_temperature_range_high": [
"290"
],
"nozzle_temperature_range_low": [
"270"
],
"overhang_fan_speed": [
"95"
],
"overhang_fan_threshold": [
"10%"
],
"reduce_fan_stop_start_freq": [
"0"
],
"required_nozzle_HRC": [
"3"
],
"retraction_distances_when_ec": [
"0",
"0"
],
"slow_down_for_layer_cooling": [
"1"
],
"slow_down_layer_time": [
"6"
],
"slow_down_min_speed": [
"10"
],
"supertack_plate_temp": [
"0"
],
"supertack_plate_temp_initial_layer": [
"0"
],
"temperature_vitrification": [
"74.2"
],
"textured_plate_temp": [
"65"
],
"textured_plate_temp_initial_layer": [
"65"
],
"volumetric_speed_coefficients": [
"0 0 0 0 0 0"
],
"filament_start_gcode": [
"; Filament gcode\nM145 P0 ; set airduct mode to cooling mode for cooling\nM106 P2 S255 ; turn on auxiliary fan for cooling\nM106 P3 S127 ; turn on chamber fan for cooling\nM1002 gcode_claim_action : 29\nM191 S0 ; wait for chamber temp\nM106 P2 S102 ; turn on chamber cooling fan\nM106 P10 S0 ; turn off left aux fan\nM142 P6 R30 S40 U0.3 V0.8 ; set PETG exhaust chamber autocooling\n"
],
"filament_end_gcode": [
"; filament end gcode \n\n"
],
"filament_adhesiveness_category": [
"400"
],
"idle_temperature": [
"0"
],
"compatible_printers": [
"Bambu Lab P2S 0.4 nozzle"
]
}
@@ -0,0 +1,210 @@
{
"type": "filament",
"name": "INSLOGIC PA6-CF @P1-X1",
"inherits": "INSLOGIC PA6-CF @base",
"from": "system",
"setting_id": "ky3k6Q9Fyi3QwCHL",
"instantiation": "true",
"cool_plate_temp": [
"0"
],
"eng_plate_temp": [
"40"
],
"hot_plate_temp": [
"65"
],
"textured_plate_temp": [
"65"
],
"cool_plate_temp_initial_layer": [
"0"
],
"eng_plate_temp_initial_layer": [
"40"
],
"hot_plate_temp_initial_layer": [
"65"
],
"textured_plate_temp_initial_layer": [
"65"
],
"overhang_fan_threshold": [
"95%"
],
"overhang_fan_speed": [
"100"
],
"slow_down_for_layer_cooling": [
"1"
],
"close_fan_the_first_x_layers": [
"3"
],
"filament_end_gcode": [
"; filament end gcode \n"
],
"filament_flow_ratio": [
"0.98",
"0.98"
],
"reduce_fan_stop_start_freq": [
"1"
],
"fan_cooling_layer_time": [
"15"
],
"filament_density": [
"1.2"
],
"filament_deretraction_speed": [
"nil",
"nil"
],
"filament_max_volumetric_speed": [
"12",
"12"
],
"filament_minimal_purge_on_wipe_tower": [
"15"
],
"filament_retraction_minimum_travel": [
"nil",
"nil"
],
"filament_retract_before_wipe": [
"nil",
"nil"
],
"filament_retract_when_changing_layer": [
"nil",
"nil"
],
"filament_retraction_length": [
"1",
"1"
],
"filament_z_hop": [
"nil",
"nil"
],
"filament_z_hop_types": [
"nil",
"nil"
],
"filament_retract_restart_extra": [
"nil",
"nil"
],
"filament_retraction_speed": [
"nil",
"nil"
],
"filament_soluble": [
"0"
],
"filament_wipe": [
"nil",
"nil"
],
"filament_wipe_distance": [
"nil",
"nil"
],
"nozzle_temperature_initial_layer": [
"280",
"280"
],
"full_fan_speed_layer": [
"2"
],
"fan_max_speed": [
"100"
],
"fan_min_speed": [
"60"
],
"slow_down_min_speed": [
"10"
],
"slow_down_layer_time": [
"2"
],
"filament_start_gcode": [
"; Filament gcode\n"
],
"nozzle_temperature": [
"275",
"275"
],
"temperature_vitrification": [
"70.4"
],
"nozzle_temperature_range_low": [
"270",
"270"
],
"nozzle_temperature_range_high": [
"290",
"290"
],
"filament_flush_temp": [
"0",
"0"
],
"filament_flush_volumetric_speed": [
"0",
"0"
],
"filament_long_retractions_when_cut": [
"0"
],
"filament_ramming_volumetric_speed": [
"-1",
"-1"
],
"filament_retraction_distances_when_cut": [
"nil",
"nil"
],
"filament_extruder_variant": [
"Direct Drive Standard",
"Direct Drive High Flow"
],
"filament_pre_cooling_temperature": [
"0",
"0"
],
"filament_ramming_travel_time": [
"0",
"0"
],
"filament_adaptive_volumetric_speed": [
"0",
"0"
],
"long_retractions_when_ec": [
"0",
"0"
],
"retraction_distances_when_ec": [
"0",
"0"
],
"volumetric_speed_coefficients": [
"0 0 0 0 0 0",
"0 0 0 0 0 0"
],
"idle_temperature": [
"0"
],
"additional_cooling_fan_speed": [
"70"
],
"compatible_printers": [
"Bambu Lab X1 0.4 nozzle",
"Bambu Lab X1 Carbon 0.4 nozzle",
"Bambu Lab P1S 0.4 nozzle",
"Bambu Lab X1E 0.4 nozzle"
]
}
@@ -0,0 +1,328 @@
{
"type": "filament",
"name": "INSLOGIC PETG Pro @BBL P2S",
"inherits": "INSLOGIC PETG Pro @base",
"from": "system",
"setting_id": "6iAMtvQjja2ZKenA",
"instantiation": "true",
"cool_plate_temp": [
"0"
],
"cool_plate_temp_initial_layer": [
"0"
],
"eng_plate_temp": [
"70"
],
"eng_plate_temp_initial_layer": [
"70"
],
"fan_cooling_layer_time": [
"25"
],
"fan_max_speed": [
"100"
],
"fan_min_speed": [
"60"
],
"filament_flow_ratio": [
"0.98",
"0.98"
],
"filament_max_volumetric_speed": [
"12",
"12"
],
"hot_plate_temp": [
"70"
],
"hot_plate_temp_initial_layer": [
"70"
],
"nozzle_temperature_range_high": [
"260"
],
"overhang_fan_speed": [
"100"
],
"overhang_fan_threshold": [
"95%"
],
"slow_down_layer_time": [
"8"
],
"slow_down_min_speed": [
"20",
"20"
],
"textured_plate_temp": [
"70"
],
"textured_plate_temp_initial_layer": [
"70"
],
"filament_deretraction_speed": [
"nil",
"nil"
],
"filament_flush_temp": [
"0"
],
"filament_flush_volumetric_speed": [
"0"
],
"filament_long_retractions_when_cut": [
"0"
],
"filament_ramming_volumetric_speed": [
"-1"
],
"filament_ramming_volumetric_speed_nc": [
"-1"
],
"filament_retract_before_wipe": [
"nil",
"nil"
],
"filament_retract_restart_extra": [
"nil",
"nil"
],
"filament_retract_when_changing_layer": [
"nil",
"nil"
],
"filament_retraction_distances_when_cut": [
"nil",
"nil"
],
"filament_retraction_length": [
"nil",
"nil"
],
"filament_retraction_minimum_travel": [
"nil",
"nil"
],
"filament_retraction_speed": [
"nil",
"nil"
],
"filament_wipe": [
"nil",
"nil"
],
"filament_wipe_distance": [
"nil",
"nil"
],
"filament_z_hop": [
"nil",
"nil"
],
"filament_z_hop_types": [
"nil",
"nil"
],
"filament_extruder_variant": [
"Direct Drive Standard",
"Direct Drive High Flow"
],
"first_x_layer_fan_speed": [
"0"
],
"filament_pre_cooling_temperature": [
"0"
],
"filament_pre_cooling_temperature_nc": [
"0"
],
"filament_ramming_travel_time": [
"0",
"0"
],
"filament_ramming_travel_time_nc": [
"0"
],
"filament_adaptive_volumetric_speed": [
"0"
],
"long_retractions_when_ec": [
"0",
"0"
],
"nozzle_temperature": [
"245",
"245"
],
"nozzle_temperature_initial_layer": [
"245",
"245"
],
"retraction_distances_when_ec": [
"0",
"0"
],
"volumetric_speed_coefficients": [
"0 0 0 0 0 0"
],
"activate_air_filtration": [
"0"
],
"additional_cooling_fan_speed": [
"70"
],
"close_fan_the_first_x_layers": [
"1"
],
"close_additional_fan_first_x_layers": [
"3"
],
"complete_print_exhaust_fan_speed": [
"70"
],
"during_print_exhaust_fan_speed": [
"70"
],
"filament_extruder_compatibility": [
"0"
],
"filament_cooling_before_tower": [
"10",
"10"
],
"filament_preheat_temperature_delta": [
"0"
],
"filament_tower_interface_pre_extrusion_dist": [
"10"
],
"filament_tower_interface_pre_extrusion_length": [
"0"
],
"filament_tower_ironing_area": [
"4"
],
"filament_tower_interface_purge_volume": [
"20"
],
"filament_tower_interface_print_temp": [
"-1"
],
"filament_density": [
"1.28"
],
"filament_dev_ams_drying_ams_limitations": [
"1",
"0"
],
"filament_dev_ams_drying_heat_distortion_temperature": [
"75"
],
"filament_dev_ams_drying_temperature": [
"65",
"65",
"55",
"55"
],
"filament_dev_ams_drying_time": [
"12",
"12",
"12",
"12"
],
"filament_dev_chamber_drying_bed_temperature": [
"80"
],
"filament_dev_chamber_drying_time": [
"12.0"
],
"filament_dev_drying_cooling_temperature": [
"55"
],
"filament_dev_drying_softening_temperature": [
"60"
],
"filament_is_support": [
"0"
],
"filament_minimal_purge_on_wipe_tower": [
"15"
],
"filament_prime_volume": [
"45"
],
"filament_prime_volume_nc": [
"60"
],
"filament_printable": [
"3"
],
"filament_scarf_seam_type": [
"none"
],
"filament_shrink": [
"100%"
],
"filament_soluble": [
"0"
],
"full_fan_speed_layer": [
"0"
],
"additional_fan_full_speed_layer": [
"0"
],
"filament_scarf_height": [
"10%"
],
"filament_scarf_gap": [
"0%"
],
"filament_scarf_length": [
"10"
],
"filament_retract_length_nc": [
"14"
],
"filament_change_length": [
"10"
],
"nozzle_temperature_range_low": [
"240"
],
"reduce_fan_stop_start_freq": [
"1"
],
"required_nozzle_HRC": [
"3"
],
"slow_down_for_layer_cooling": [
"1"
],
"supertack_plate_temp": [
"70"
],
"supertack_plate_temp_initial_layer": [
"70"
],
"temperature_vitrification": [
"81"
],
"filament_start_gcode": [
"; Filament gcode\nM145 P0 ; set airduct mode to cooling mode for cooling\nM106 P2 S255 ; turn on auxiliary fan for cooling\nM106 P3 S127 ; turn on chamber fan for cooling\nM1002 gcode_claim_action : 29\nM191 S0 ; wait for chamber temp\nM106 P2 S102 ; turn on chamber cooling fan\nM106 P10 S0 ; turn off left aux fan\nM142 P6 R30 S40 U0.3 V0.8 ; set PETG exhaust chamber autocooling\n"
],
"filament_end_gcode": [
"; filament end gcode \n\n"
],
"filament_adhesiveness_category": [
"300"
],
"idle_temperature": [
"0"
],
"compatible_printers": [
"Bambu Lab P2S 0.4 nozzle"
]
}
@@ -0,0 +1,210 @@
{
"type": "filament",
"name": "INSLOGIC PETG Pro @P1-X1",
"inherits": "INSLOGIC PETG Pro @base",
"from": "system",
"setting_id": "miOBX0bGzziXI6SE",
"instantiation": "true",
"cool_plate_temp": [
"0"
],
"eng_plate_temp": [
"40"
],
"hot_plate_temp": [
"70"
],
"textured_plate_temp": [
"70"
],
"cool_plate_temp_initial_layer": [
"0"
],
"eng_plate_temp_initial_layer": [
"40"
],
"hot_plate_temp_initial_layer": [
"70"
],
"textured_plate_temp_initial_layer": [
"70"
],
"overhang_fan_threshold": [
"95%"
],
"overhang_fan_speed": [
"100"
],
"slow_down_for_layer_cooling": [
"1"
],
"close_fan_the_first_x_layers": [
"3"
],
"filament_end_gcode": [
"; filament end gcode \n"
],
"filament_flow_ratio": [
"0.98",
"0.98"
],
"reduce_fan_stop_start_freq": [
"1"
],
"fan_cooling_layer_time": [
"15"
],
"filament_density": [
"1.2"
],
"filament_deretraction_speed": [
"nil",
"nil"
],
"filament_max_volumetric_speed": [
"12",
"12"
],
"filament_minimal_purge_on_wipe_tower": [
"15"
],
"filament_retraction_minimum_travel": [
"nil",
"nil"
],
"filament_retract_before_wipe": [
"nil",
"nil"
],
"filament_retract_when_changing_layer": [
"nil",
"nil"
],
"filament_retraction_length": [
"1",
"1"
],
"filament_z_hop": [
"nil",
"nil"
],
"filament_z_hop_types": [
"nil",
"nil"
],
"filament_retract_restart_extra": [
"nil",
"nil"
],
"filament_retraction_speed": [
"nil",
"nil"
],
"filament_soluble": [
"0"
],
"filament_wipe": [
"nil",
"nil"
],
"filament_wipe_distance": [
"nil",
"nil"
],
"nozzle_temperature_initial_layer": [
"245",
"245"
],
"full_fan_speed_layer": [
"2"
],
"fan_max_speed": [
"100"
],
"fan_min_speed": [
"60"
],
"slow_down_min_speed": [
"10"
],
"slow_down_layer_time": [
"2"
],
"filament_start_gcode": [
"; Filament gcode\n"
],
"nozzle_temperature": [
"245",
"245"
],
"temperature_vitrification": [
"70.4"
],
"nozzle_temperature_range_low": [
"240",
"240"
],
"nozzle_temperature_range_high": [
"260",
"260"
],
"filament_flush_temp": [
"0",
"0"
],
"filament_flush_volumetric_speed": [
"0",
"0"
],
"filament_long_retractions_when_cut": [
"0"
],
"filament_ramming_volumetric_speed": [
"-1",
"-1"
],
"filament_retraction_distances_when_cut": [
"nil",
"nil"
],
"filament_extruder_variant": [
"Direct Drive Standard",
"Direct Drive High Flow"
],
"filament_pre_cooling_temperature": [
"0",
"0"
],
"filament_ramming_travel_time": [
"0",
"0"
],
"filament_adaptive_volumetric_speed": [
"0",
"0"
],
"long_retractions_when_ec": [
"0",
"0"
],
"retraction_distances_when_ec": [
"0",
"0"
],
"volumetric_speed_coefficients": [
"0 0 0 0 0 0",
"0 0 0 0 0 0"
],
"idle_temperature": [
"0"
],
"additional_cooling_fan_speed": [
"70"
],
"compatible_printers": [
"Bambu Lab X1 0.4 nozzle",
"Bambu Lab X1 Carbon 0.4 nozzle",
"Bambu Lab P1S 0.4 nozzle",
"Bambu Lab X1E 0.4 nozzle"
]
}
@@ -0,0 +1,190 @@
{
"type": "filament",
"name": "INSLOGIC PLA Pro @BBL P2S",
"inherits": "INSLOGIC PLA Pro @base",
"from": "system",
"setting_id": "UAd1QXW7vm9gwoEc",
"instantiation": "true",
"filament_flow_ratio": [
"0.98",
"0.98"
],
"filament_max_volumetric_speed": [
"12",
"12"
],
"filament_scarf_seam_type": [
"all"
],
"filament_scarf_height": [
"10%"
],
"filament_scarf_gap": [
"0%"
],
"filament_scarf_length": [
"10"
],
"filament_change_length": [
"5"
],
"filament_prime_volume": [
"30"
],
"filament_start_gcode": [
"; filament start gcode\n{if (bed_temperature[current_extruder] >55)||(bed_temperature_initial_layer[current_extruder] >55)}M106 P3 S200\n{elsif(bed_temperature[current_extruder] >50)||(bed_temperature_initial_layer[current_extruder] >50)}M106 P3 S150\n{elsif(bed_temperature[current_extruder] >45)||(bed_temperature_initial_layer[current_extruder] >45)}M106 P3 S50\n{endif}\n\n{if activate_air_filtration[current_extruder] && support_air_filtration}\nM106 P3 S{during_print_exhaust_fan_speed_num[current_extruder]} \n{endif}"
],
"eng_plate_temp": [
"55"
],
"eng_plate_temp_initial_layer": [
"55"
],
"filament_deretraction_speed": [
"nil",
"nil"
],
"filament_flush_temp": [
"0",
"0"
],
"filament_flush_volumetric_speed": [
"0",
"0"
],
"filament_long_retractions_when_cut": [
"nil",
"nil"
],
"filament_ramming_volumetric_speed": [
"-1",
"-1"
],
"filament_ramming_volumetric_speed_nc": [
"-1",
"-1"
],
"filament_retract_before_wipe": [
"nil",
"nil"
],
"filament_retract_restart_extra": [
"nil",
"nil"
],
"filament_retract_when_changing_layer": [
"nil",
"nil"
],
"filament_retraction_distances_when_cut": [
"10",
"10"
],
"filament_retraction_length": [
"1",
"1"
],
"filament_retraction_minimum_travel": [
"nil",
"nil"
],
"filament_retraction_speed": [
"nil",
"nil"
],
"filament_wipe": [
"nil",
"nil"
],
"filament_wipe_distance": [
"nil",
"nil"
],
"filament_z_hop": [
"nil",
"nil"
],
"filament_z_hop_types": [
"nil",
"nil"
],
"filament_extruder_variant": [
"Direct Drive Standard",
"Direct Drive High Flow"
],
"first_x_layer_fan_speed": [
"40"
],
"filament_pre_cooling_temperature": [
"0",
"0"
],
"filament_pre_cooling_temperature_nc": [
"0",
"0"
],
"filament_ramming_travel_time": [
"0",
"0"
],
"filament_ramming_travel_time_nc": [
"0",
"0"
],
"filament_adaptive_volumetric_speed": [
"0",
"0"
],
"long_retractions_when_ec": [
"0",
"0"
],
"nozzle_temperature": [
"215",
"215"
],
"nozzle_temperature_initial_layer": [
"215",
"215"
],
"retraction_distances_when_ec": [
"0",
"0"
],
"volumetric_speed_coefficients": [
"0 0 0 0 0 0",
"0 0 0 0 0 0"
],
"nozzle_temperature_range_low": [
"205",
"205"
],
"nozzle_temperature_range_high": [
"245",
"245"
],
"hot_plate_temp": [
"55"
],
"hot_plate_temp_initial_layer": [
"55"
],
"textured_plate_temp": [
"55"
],
"textured_plate_temp_initial_layer": [
"55"
],
"fan_min_speed": [
"60"
],
"fan_max_speed": [
"100"
],
"additional_cooling_fan_speed": [
"70"
],
"compatible_printers": [
"Bambu Lab P2S 0.4 nozzle"
]
}
@@ -0,0 +1,210 @@
{
"type": "filament",
"name": "INSLOGIC PLA Pro @P1-X1",
"inherits": "INSLOGIC PLA Pro @base",
"from": "system",
"setting_id": "InmXr3LoNKKDfnQu",
"instantiation": "true",
"cool_plate_temp": [
"0"
],
"eng_plate_temp": [
"40"
],
"hot_plate_temp": [
"55"
],
"textured_plate_temp": [
"55"
],
"cool_plate_temp_initial_layer": [
"0"
],
"eng_plate_temp_initial_layer": [
"40"
],
"hot_plate_temp_initial_layer": [
"55"
],
"textured_plate_temp_initial_layer": [
"55"
],
"overhang_fan_threshold": [
"95%"
],
"overhang_fan_speed": [
"100"
],
"slow_down_for_layer_cooling": [
"1"
],
"close_fan_the_first_x_layers": [
"3"
],
"filament_end_gcode": [
"; filament end gcode \n"
],
"filament_flow_ratio": [
"0.98",
"0.98"
],
"reduce_fan_stop_start_freq": [
"1"
],
"fan_cooling_layer_time": [
"15"
],
"filament_density": [
"1.2"
],
"filament_deretraction_speed": [
"nil",
"nil"
],
"filament_max_volumetric_speed": [
"12",
"12"
],
"filament_minimal_purge_on_wipe_tower": [
"15"
],
"filament_retraction_minimum_travel": [
"nil",
"nil"
],
"filament_retract_before_wipe": [
"nil",
"nil"
],
"filament_retract_when_changing_layer": [
"nil",
"nil"
],
"filament_retraction_length": [
"1",
"1"
],
"filament_z_hop": [
"nil",
"nil"
],
"filament_z_hop_types": [
"nil",
"nil"
],
"filament_retract_restart_extra": [
"nil",
"nil"
],
"filament_retraction_speed": [
"nil",
"nil"
],
"filament_soluble": [
"0"
],
"filament_wipe": [
"nil",
"nil"
],
"filament_wipe_distance": [
"nil",
"nil"
],
"nozzle_temperature_initial_layer": [
"215",
"215"
],
"full_fan_speed_layer": [
"2"
],
"fan_max_speed": [
"100"
],
"fan_min_speed": [
"60"
],
"slow_down_min_speed": [
"10"
],
"slow_down_layer_time": [
"2"
],
"filament_start_gcode": [
"; Filament gcode\n"
],
"nozzle_temperature": [
"215",
"215"
],
"temperature_vitrification": [
"70.4"
],
"nozzle_temperature_range_low": [
"205",
"205"
],
"nozzle_temperature_range_high": [
"245",
"245"
],
"filament_flush_temp": [
"0",
"0"
],
"filament_flush_volumetric_speed": [
"0",
"0"
],
"filament_long_retractions_when_cut": [
"0"
],
"filament_ramming_volumetric_speed": [
"-1",
"-1"
],
"filament_retraction_distances_when_cut": [
"nil",
"nil"
],
"filament_extruder_variant": [
"Direct Drive Standard",
"Direct Drive High Flow"
],
"filament_pre_cooling_temperature": [
"0",
"0"
],
"filament_ramming_travel_time": [
"0",
"0"
],
"filament_adaptive_volumetric_speed": [
"0",
"0"
],
"long_retractions_when_ec": [
"0",
"0"
],
"retraction_distances_when_ec": [
"0",
"0"
],
"volumetric_speed_coefficients": [
"0 0 0 0 0 0",
"0 0 0 0 0 0"
],
"idle_temperature": [
"0"
],
"additional_cooling_fan_speed": [
"70"
],
"compatible_printers": [
"Bambu Lab X1 0.4 nozzle",
"Bambu Lab X1 Carbon 0.4 nozzle",
"Bambu Lab P1S 0.4 nozzle",
"Bambu Lab X1E 0.4 nozzle"
]
}
@@ -0,0 +1,36 @@
{
"type": "filament",
"name": "INSLOGIC PA6-66 @base",
"inherits": "fdm_filament_pa",
"from": "system",
"filament_id": "OF02mnRc",
"instantiation": "false",
"filament_vendor": [
"INSLOGIC"
],
"filament_type": [
"PA"
],
"filament_density": [
"1.08"
],
"filament_diameter": [
"1.75"
],
"filament_max_volumetric_speed": [
"12"
],
"filament_soluble": [
"0"
],
"nozzle_temperature_range_low": [
"250"
],
"nozzle_temperature_range_high": [
"280"
],
"filament_notes": [
"Inslogic Nylon PA6/66 TDS (20.01.2025). Nozzle sizes 0.2/0.4/0.6mm. Nozzle 250-280C. Bed 30-50C. Printing speed 50-150mm/s. Cooling fan 100%. Textured PEI Sheet. Drying 80C for 48h or 110C for 3h. TDS linear speed is not converted to filament_max_volumetric_speed."
],
"compatible_printers": []
}
@@ -0,0 +1,39 @@
{
"type": "filament",
"name": "INSLOGIC PA6-CF @base",
"inherits": "fdm_filament_pa",
"from": "system",
"filament_id": "OFDkHRsW",
"instantiation": "false",
"filament_vendor": [
"INSLOGIC"
],
"filament_type": [
"PA-CF"
],
"filament_density": [
"1.20"
],
"filament_diameter": [
"1.75"
],
"filament_max_volumetric_speed": [
"12"
],
"filament_soluble": [
"0"
],
"nozzle_temperature_range_low": [
"270"
],
"nozzle_temperature_range_high": [
"290"
],
"filament_notes": [
"Inslogic PA6-CF TDS (20.01.2025). Nozzle sizes 0.4/0.6mm. Nozzle 270-290C at 50-100mm/s. Bed 50-70C. Cooling fan 30%. Textured PEI Sheet. Drying 90C for 12h or 110C for 4h. Carbon-fiber reinforced; abrasive filament. TDS linear speed is not converted to filament_max_volumetric_speed."
],
"compatible_printers": [],
"required_nozzle_HRC": [
"3"
]
}
@@ -0,0 +1,36 @@
{
"type": "filament",
"name": "INSLOGIC PETG Pro @base",
"inherits": "fdm_filament_pet",
"from": "system",
"filament_id": "OFBdBfg1",
"instantiation": "false",
"filament_vendor": [
"INSLOGIC"
],
"filament_type": [
"PETG"
],
"filament_density": [
"1.26"
],
"filament_diameter": [
"1.75"
],
"filament_max_volumetric_speed": [
"12"
],
"filament_soluble": [
"0"
],
"nozzle_temperature_range_low": [
"230"
],
"nozzle_temperature_range_high": [
"270"
],
"filament_notes": [
"Inslogic PETG Pro TDS (12.02.2024). Nozzle sizes 0.2/0.4/0.6mm. 230-240C at 50-100mm/s; 240-255C at 100-300mm/s; 255-270C at 300-600mm/s. Bed 60-70C. Cooling fan 100%. Bed: Smooth PEI Sheet / High Temperature Plate. Drying 50C for 4h. TDS linear speed is not converted to filament_max_volumetric_speed."
],
"compatible_printers": []
}
@@ -0,0 +1,36 @@
{
"type": "filament",
"name": "INSLOGIC PLA Pro @base",
"inherits": "fdm_filament_pla",
"from": "system",
"filament_id": "OFvPeNbA",
"instantiation": "false",
"filament_vendor": [
"INSLOGIC"
],
"filament_type": [
"PLA"
],
"filament_density": [
"1.20"
],
"filament_diameter": [
"1.75"
],
"filament_max_volumetric_speed": [
"12"
],
"filament_soluble": [
"0"
],
"nozzle_temperature_range_low": [
"195"
],
"nozzle_temperature_range_high": [
"220"
],
"filament_notes": [
"Inslogic PLA Pro TDS (12.02.2024). Nozzle sizes 0.2/0.4/0.6mm. 195-205C at 50-100mm/s; 205-220C at 100-300mm/s. Bed 50-60C. Cooling fan 100%. Bed: Textured PEI Sheet / Cool Plate. Drying 50C for 4h. TDS linear speed is not converted to filament_max_volumetric_speed."
],
"compatible_printers": []
}
@@ -0,0 +1,165 @@
{
"type": "filament",
"name": "INSLOGIC PA6-66 @Prusa CORE One",
"inherits": "INSLOGIC PA6-66 @base",
"from": "system",
"setting_id": "Kuv4zCw8jhxYOzhB",
"instantiation": "true",
"hot_plate_temp": [
"40"
],
"hot_plate_temp_initial_layer": [
"40"
],
"textured_plate_temp": [
"40"
],
"textured_plate_temp_initial_layer": [
"40"
],
"enable_overhang_bridge_fan": [
"1"
],
"overhang_fan_speed": [
"100"
],
"chamber_minimal_temperature": [
"0"
],
"chamber_temperature": [
"0"
],
"slow_down_for_layer_cooling": [
"1"
],
"close_fan_the_first_x_layers": [
"0"
],
"filament_end_gcode": [
"; Filament-specific end gcode"
],
"filament_flow_ratio": [
"0.99"
],
"fan_cooling_layer_time": [
"100"
],
"filament_cooling_final_speed": [
"3.5"
],
"filament_cooling_initial_speed": [
"10"
],
"filament_cooling_moves": [
"2"
],
"filament_deretraction_speed": [
"nil"
],
"filament_loading_speed": [
"10"
],
"filament_loading_speed_start": [
"50"
],
"filament_max_volumetric_speed": [
"12"
],
"filament_minimal_purge_on_wipe_tower": [
"15"
],
"filament_multitool_ramming": [
"0"
],
"filament_multitool_ramming_flow": [
"10"
],
"filament_multitool_ramming_volume": [
"10"
],
"filament_ramming_parameters": [
"250 100 40.1613 40.3548 40.4516 40.3548 40.2581| 0.05 40.1483 0.45 40.3419 0.95 40.3419 1.45 40.3419 1.95 40.3419 2.45 40.3419 2.95 40.3419 3.45 40.3419 3.95 40.3419 4.45 40.3419 4.95 40.3419"
],
"filament_retraction_minimum_travel": [
"nil"
],
"filament_retract_before_wipe": [
"nil"
],
"filament_retract_when_changing_layer": [
"nil"
],
"filament_retraction_length": [
"nil"
],
"filament_retract_length_toolchange": [
"nil"
],
"filament_z_hop": [
"nil"
],
"filament_retract_lift_above": [
"nil"
],
"filament_retract_lift_below": [
"nil"
],
"filament_retract_restart_extra": [
"nil"
],
"filament_retract_restart_extra_toolchange": [
"nil"
],
"filament_retraction_speed": [
"nil"
],
"filament_stamping_distance": [
"45"
],
"filament_stamping_loading_speed": [
"29"
],
"filament_toolchange_delay": [
"0"
],
"filament_unloading_speed": [
"100"
],
"filament_unloading_speed_start": [
"100"
],
"filament_wipe": [
"nil"
],
"nozzle_temperature_initial_layer": [
"265"
],
"full_fan_speed_layer": [
"0"
],
"idle_temperature": [
"170"
],
"fan_max_speed": [
"100"
],
"fan_min_speed": [
"100"
],
"slow_down_min_speed": [
"20"
],
"slow_down_layer_time": [
"15"
],
"filament_start_gcode": [
""
],
"nozzle_temperature": [
"265"
],
"compatible_printers": [
"Prusa CORE One 0.4 nozzle",
"Prusa CORE One L 0.4 nozzle"
]
}
@@ -0,0 +1,165 @@
{
"type": "filament",
"name": "INSLOGIC PA6-CF @Prusa CORE One",
"inherits": "INSLOGIC PA6-CF @base",
"from": "system",
"setting_id": "ZCYSMR6VCj0IrYAF",
"instantiation": "true",
"hot_plate_temp": [
"60"
],
"hot_plate_temp_initial_layer": [
"60"
],
"textured_plate_temp": [
"60"
],
"textured_plate_temp_initial_layer": [
"60"
],
"enable_overhang_bridge_fan": [
"1"
],
"overhang_fan_speed": [
"30"
],
"chamber_minimal_temperature": [
"0"
],
"chamber_temperature": [
"0"
],
"slow_down_for_layer_cooling": [
"1"
],
"close_fan_the_first_x_layers": [
"0"
],
"filament_end_gcode": [
"; Filament-specific end gcode"
],
"filament_flow_ratio": [
"0.99"
],
"fan_cooling_layer_time": [
"100"
],
"filament_cooling_final_speed": [
"3.5"
],
"filament_cooling_initial_speed": [
"10"
],
"filament_cooling_moves": [
"2"
],
"filament_deretraction_speed": [
"nil"
],
"filament_loading_speed": [
"10"
],
"filament_loading_speed_start": [
"50"
],
"filament_max_volumetric_speed": [
"12"
],
"filament_minimal_purge_on_wipe_tower": [
"15"
],
"filament_multitool_ramming": [
"0"
],
"filament_multitool_ramming_flow": [
"10"
],
"filament_multitool_ramming_volume": [
"10"
],
"filament_ramming_parameters": [
"250 100 40.1613 40.3548 40.4516 40.3548 40.2581| 0.05 40.1483 0.45 40.3419 0.95 40.3419 1.45 40.3419 1.95 40.3419 2.45 40.3419 2.95 40.3419 3.45 40.3419 3.95 40.3419 4.45 40.3419 4.95 40.3419"
],
"filament_retraction_minimum_travel": [
"nil"
],
"filament_retract_before_wipe": [
"nil"
],
"filament_retract_when_changing_layer": [
"nil"
],
"filament_retraction_length": [
"nil"
],
"filament_retract_length_toolchange": [
"nil"
],
"filament_z_hop": [
"nil"
],
"filament_retract_lift_above": [
"nil"
],
"filament_retract_lift_below": [
"nil"
],
"filament_retract_restart_extra": [
"nil"
],
"filament_retract_restart_extra_toolchange": [
"nil"
],
"filament_retraction_speed": [
"nil"
],
"filament_stamping_distance": [
"45"
],
"filament_stamping_loading_speed": [
"29"
],
"filament_toolchange_delay": [
"0"
],
"filament_unloading_speed": [
"100"
],
"filament_unloading_speed_start": [
"100"
],
"filament_wipe": [
"nil"
],
"nozzle_temperature_initial_layer": [
"280"
],
"full_fan_speed_layer": [
"0"
],
"idle_temperature": [
"170"
],
"fan_max_speed": [
"30"
],
"fan_min_speed": [
"30"
],
"slow_down_min_speed": [
"20"
],
"slow_down_layer_time": [
"15"
],
"filament_start_gcode": [
""
],
"nozzle_temperature": [
"280"
],
"compatible_printers": [
"Prusa CORE One 0.4 nozzle",
"Prusa CORE One L 0.4 nozzle"
]
}
@@ -0,0 +1,165 @@
{
"type": "filament",
"name": "INSLOGIC PETG Pro @Prusa CORE One",
"inherits": "INSLOGIC PETG Pro @base",
"from": "system",
"setting_id": "yHUGQJhN0HUa7A5V",
"instantiation": "true",
"hot_plate_temp": [
"65"
],
"hot_plate_temp_initial_layer": [
"65"
],
"textured_plate_temp": [
"65"
],
"textured_plate_temp_initial_layer": [
"65"
],
"enable_overhang_bridge_fan": [
"1"
],
"overhang_fan_speed": [
"100"
],
"chamber_minimal_temperature": [
"0"
],
"chamber_temperature": [
"0"
],
"slow_down_for_layer_cooling": [
"1"
],
"close_fan_the_first_x_layers": [
"0"
],
"filament_end_gcode": [
"; Filament-specific end gcode"
],
"filament_flow_ratio": [
"0.99"
],
"fan_cooling_layer_time": [
"100"
],
"filament_cooling_final_speed": [
"3.5"
],
"filament_cooling_initial_speed": [
"10"
],
"filament_cooling_moves": [
"2"
],
"filament_deretraction_speed": [
"nil"
],
"filament_loading_speed": [
"10"
],
"filament_loading_speed_start": [
"50"
],
"filament_max_volumetric_speed": [
"12"
],
"filament_minimal_purge_on_wipe_tower": [
"15"
],
"filament_multitool_ramming": [
"0"
],
"filament_multitool_ramming_flow": [
"10"
],
"filament_multitool_ramming_volume": [
"10"
],
"filament_ramming_parameters": [
"250 100 40.1613 40.3548 40.4516 40.3548 40.2581| 0.05 40.1483 0.45 40.3419 0.95 40.3419 1.45 40.3419 1.95 40.3419 2.45 40.3419 2.95 40.3419 3.45 40.3419 3.95 40.3419 4.45 40.3419 4.95 40.3419"
],
"filament_retraction_minimum_travel": [
"nil"
],
"filament_retract_before_wipe": [
"nil"
],
"filament_retract_when_changing_layer": [
"nil"
],
"filament_retraction_length": [
"nil"
],
"filament_retract_length_toolchange": [
"nil"
],
"filament_z_hop": [
"nil"
],
"filament_retract_lift_above": [
"nil"
],
"filament_retract_lift_below": [
"nil"
],
"filament_retract_restart_extra": [
"nil"
],
"filament_retract_restart_extra_toolchange": [
"nil"
],
"filament_retraction_speed": [
"nil"
],
"filament_stamping_distance": [
"45"
],
"filament_stamping_loading_speed": [
"29"
],
"filament_toolchange_delay": [
"0"
],
"filament_unloading_speed": [
"100"
],
"filament_unloading_speed_start": [
"100"
],
"filament_wipe": [
"nil"
],
"nozzle_temperature_initial_layer": [
"250"
],
"full_fan_speed_layer": [
"0"
],
"idle_temperature": [
"170"
],
"fan_max_speed": [
"100"
],
"fan_min_speed": [
"100"
],
"slow_down_min_speed": [
"20"
],
"slow_down_layer_time": [
"15"
],
"filament_start_gcode": [
""
],
"nozzle_temperature": [
"250"
],
"compatible_printers": [
"Prusa CORE One 0.4 nozzle",
"Prusa CORE One L 0.4 nozzle"
]
}
@@ -0,0 +1,165 @@
{
"type": "filament",
"name": "INSLOGIC PLA Pro @Prusa CORE One",
"inherits": "INSLOGIC PLA Pro @base",
"from": "system",
"setting_id": "Qkscha5uuuFXbvbB",
"instantiation": "true",
"hot_plate_temp": [
"55"
],
"hot_plate_temp_initial_layer": [
"55"
],
"textured_plate_temp": [
"55"
],
"textured_plate_temp_initial_layer": [
"55"
],
"enable_overhang_bridge_fan": [
"1"
],
"overhang_fan_speed": [
"100"
],
"chamber_minimal_temperature": [
"0"
],
"chamber_temperature": [
"0"
],
"slow_down_for_layer_cooling": [
"1"
],
"close_fan_the_first_x_layers": [
"0"
],
"filament_end_gcode": [
"; Filament-specific end gcode"
],
"filament_flow_ratio": [
"0.99"
],
"fan_cooling_layer_time": [
"100"
],
"filament_cooling_final_speed": [
"3.5"
],
"filament_cooling_initial_speed": [
"10"
],
"filament_cooling_moves": [
"2"
],
"filament_deretraction_speed": [
"nil"
],
"filament_loading_speed": [
"10"
],
"filament_loading_speed_start": [
"50"
],
"filament_max_volumetric_speed": [
"12"
],
"filament_minimal_purge_on_wipe_tower": [
"15"
],
"filament_multitool_ramming": [
"0"
],
"filament_multitool_ramming_flow": [
"10"
],
"filament_multitool_ramming_volume": [
"10"
],
"filament_ramming_parameters": [
"250 100 40.1613 40.3548 40.4516 40.3548 40.2581| 0.05 40.1483 0.45 40.3419 0.95 40.3419 1.45 40.3419 1.95 40.3419 2.45 40.3419 2.95 40.3419 3.45 40.3419 3.95 40.3419 4.45 40.3419 4.95 40.3419"
],
"filament_retraction_minimum_travel": [
"nil"
],
"filament_retract_before_wipe": [
"nil"
],
"filament_retract_when_changing_layer": [
"nil"
],
"filament_retraction_length": [
"nil"
],
"filament_retract_length_toolchange": [
"nil"
],
"filament_z_hop": [
"nil"
],
"filament_retract_lift_above": [
"nil"
],
"filament_retract_lift_below": [
"nil"
],
"filament_retract_restart_extra": [
"nil"
],
"filament_retract_restart_extra_toolchange": [
"nil"
],
"filament_retraction_speed": [
"nil"
],
"filament_stamping_distance": [
"45"
],
"filament_stamping_loading_speed": [
"29"
],
"filament_toolchange_delay": [
"0"
],
"filament_unloading_speed": [
"100"
],
"filament_unloading_speed_start": [
"100"
],
"filament_wipe": [
"nil"
],
"nozzle_temperature_initial_layer": [
"210"
],
"full_fan_speed_layer": [
"0"
],
"idle_temperature": [
"170"
],
"fan_max_speed": [
"100"
],
"fan_min_speed": [
"100"
],
"slow_down_min_speed": [
"20"
],
"slow_down_layer_time": [
"15"
],
"filament_start_gcode": [
""
],
"nozzle_temperature": [
"210"
],
"compatible_printers": [
"Prusa CORE One 0.4 nozzle",
"Prusa CORE One L 0.4 nozzle"
]
}
+5 -1
View File
@@ -19,7 +19,10 @@
<span class="plugin-search-icon" aria-hidden="true"> <span class="plugin-search-icon" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/></svg>
</span> </span>
<input class="plugin-search-input" id="q" type="text" placeholder="Search actions" spellcheck="false" autocomplete="off" aria-label="Search actions" /> <span class="plugin-search-field">
<span class="search-ghost" id="ghost" hidden aria-hidden="true"><span class="search-ghost-typed" id="ghostTyped"></span><span class="search-ghost-suffix" id="ghostSuffix"></span></span>
<input class="plugin-search-input" id="q" type="text" placeholder="Search actions" spellcheck="false" autocomplete="off" aria-label="Search actions" />
</span>
<button class="plugin-search-clear" id="clear" type="button" title="Clear" aria-label="Clear search" hidden> <button class="plugin-search-clear" id="clear" type="button" title="Clear" aria-label="Clear search" hidden>
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><line x1="5" y1="5" x2="11" y2="11"/><line x1="11" y1="5" x2="5" y2="11"/></svg> <svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><line x1="5" y1="5" x2="11" y2="11"/><line x1="11" y1="5" x2="5" y2="11"/></svg>
</button> </button>
@@ -27,6 +30,7 @@
<div class="dial-count" id="count" hidden></div> <div class="dial-count" id="count" hidden></div>
</div> </div>
<div class="dial-list" id="list"></div> <div class="dial-list" id="list"></div>
<div class="dial-detail" id="detail" hidden></div>
</div> </div>
</body> </body>
</html> </html>
File diff suppressed because it is too large Load Diff
+410 -16
View File
@@ -13,33 +13,427 @@ vm.runInContext(fs.readFileSync(__dirname + "/speeddial.js", "utf8"), ctx);
assert.equal(typeof ctx.parseId, "undefined", "opaque action ids must never be parsed"); assert.equal(typeof ctx.parseId, "undefined", "opaque action ids must never be parsed");
const duplicateActions = [ const duplicateActions = [
{ id: "0123456789abcdef", title: "Repair", source: "Mesh Tools" }, { id: "0123456789abcdef", title: "Repair", source: "Mesh Tools" },
{ id: "fedcba9876543210", title: "Repair", source: "Mesh Tools" } { id: "fedcba9876543210", title: "Repair", source: "Mesh Tools" }
]; ];
assert.equal( assert.equal(
ctx.actionLabel(duplicateActions[0], duplicateActions), ctx.actionLabel(duplicateActions[0], duplicateActions),
"Repair from Mesh Tools (0123456789abcdef)", "Repair from Mesh Tools (0123456789abcdef)",
"duplicate labels should use the opaque id without interpreting its contents" "duplicate labels should use the opaque id without interpreting its contents"
); );
assert.equal(ctx.shouldRenderActionList(""), false, "an empty search keeps the action list hidden"); assert.equal(ctx.shouldRenderActionList(""), false, "an empty search shows the recents+pool list");
assert.equal(ctx.shouldRenderActionList(" "), false, "whitespace-only search keeps the action list hidden"); assert.equal(ctx.shouldRenderActionList(" "), false, "whitespace-only search shows the recents+pool list");
assert.equal(ctx.shouldRenderActionList("r"), true, "typing starts rendering matching actions"); assert.equal(ctx.shouldRenderActionList("r"), true, "typing starts rendering matching actions");
// commandList: an empty query shows recents first, then every other action; a typed query filters all.
assert.deepEqual(ctx.commandList(duplicateActions, [], ""), duplicateActions,
"empty query + no recents shows the whole action pool");
assert.deepEqual(ctx.commandList(duplicateActions, [duplicateActions[0]], ""),
[duplicateActions[0], duplicateActions[1]],
"empty query shows the recent first, then the remaining actions");
assert.deepEqual(ctx.commandList(duplicateActions, [], "rep"), duplicateActions,
"a typed query filters actions (both identical titles match) instead of showing recents");
// filterTabs (tab phase): an empty query keeps the whole list; a typed query filters by title/id.
const tabOptions = [
{ id: "home", title: "Home" },
{ id: "prepare", title: "Prepare" },
{ id: "monitor", title: "Device" },
{ id: "project", title: "Project" }
];
assert.deepEqual(ctx.filterTabs(tabOptions, ""), tabOptions,
"empty query keeps the whole tab list");
assert.equal(ctx.filterTabs(tabOptions, "prep").length, 1,
"a typed query filters tabs by title");
assert.equal(ctx.filterTabs(tabOptions, "Device").length, 1,
"a typed query matches a tab title");
assert.deepEqual(ctx.filterTabs(tabOptions, "zzz"), [],
"a typed query with no match returns an empty list");
// tabTitle: pages added with an empty title (e.g. MainFrame's Home tab) fall back to the id.
assert.equal(ctx.tabTitle({ id: "home", title: "" }), "Home",
"an empty title falls back to the title-cased id");
assert.equal(ctx.tabTitle({ id: "home" }), "Home",
"a missing title falls back to the title-cased id");
assert.equal(ctx.tabTitle({ id: "prepare", title: "Prepare" }), "Prepare",
"a populated title is kept as-is");
assert.equal(ctx.tabTitle({ id: "prepare", title: " Prepare" }), "Prepare",
"a stray leading space in a tab title is trimmed so the label shows cleanly");
assert.equal(ctx.filterTabs([{ id: "home", title: "" }], "home").length, 1,
"an untitled tab still matches a typed query via the id/title fallback");
assert.equal(ctx.filterTabs([{ id: "prepare", title: " Prepare" }], "prepare").length, 1,
"a leading-space tab title still matches a typed query");
// The main phase is ONE pool: commands/plugins/settings are all actions, ranked by relevance
// (no group headers, no actions-vs-settings discrimination).
const pool = [
{ id: "c1", title: "Layer Height", source: "Quality", group: "Quality : Layers", input: "" },
{ id: "s1", title: "Go to layer (percent)", source: "OrcaSlicer", group: "Commands", input: "percent" },
{ id: "c2", title: "Top Surface Layers", source: "Quality", group: "Quality : Layers", input: "" }
];
assert.deepEqual(ctx.searchActions(pool, ""), pool, "an empty query returns the pool unchanged");
assert.equal(ctx.searchActions(pool, "zzz").length, 0, "a query with no match returns nothing");
// "layer" matches multiple; the exact-titled action ranks above the loosely-matching command.
assert.equal(ctx.searchActions(pool, "layer")[0].id, "c1",
"a title-exact match ranks above a partial match");
assert.equal(ctx.searchActions(pool, "layer").length >= 2, true,
"both a setting and a command match the same query in the same list");
assert.equal(ctx.searchActions(pool, "surface")[0].id, "c2",
"a later-but-precise match still ranks by relevance, not by pool type");
// A perfect match (the needle as one contiguous run) outranks a fuzzy match of the same field - and a
// contiguous GROUP/header hit ("Recent Projects") beats a scattered fuzzy TITLE hit ("Retraction Length"),
// which is what the old flat title-bonus ranking got backwards.
const perfectPool = [
{ id: "set", title: "Retraction Length", source: "Process : Quality : Retraction", group: "", input: "" },
{ id: "recent", title: "myproject.3mf", source: "/home/me/projects/myproject.3mf", group: "Recent Projects", input: "" }
];
assert.equal(ctx.searchActions(perfectPool, "recent")[0].id, "recent",
"a contiguous header/group match ranks above a scattered fuzzy title match");
// Within a perfect match, the row-name (title) outranks the header (group): the action whose TITLE
// contains the needle perfectly beats the action whose GROUP does, both being contiguous matches.
const titleFirstPool = [
{ id: "grp", title: "Delete Selected", source: "OrcaSlicer", group: "Object", input: "" },
{ id: "t", title: "Object Preview", source: "OrcaSlicer", group: "View", input: "" }
];
assert.equal(ctx.searchActions(titleFirstPool, "object")[0].id, "t",
"a perfect title match ranks above an equally-perfect group match");
// Highlighting: the needle is matched as a whole word / most-contiguous run, so "orient" lights up the
// whole word in "Auto-Orient" instead of the stray "o" of "Auto" plus "rient" (greedy-leftmost).
const orientPool = [
{ id: "ao", title: "Auto-Orient", source: "OrcaSlicer", group: "Object", input: "" }
];
ctx.searchActions(orientPool, "orient");
assert.deepEqual(ctx.matchIndex.ao.title, [[5, 11]],
"a whole-word match highlights the full word, not a scattered fuzzy pick");
// A fuzzy source-only match with a very late start still counts, even though its score is negative;
// the old `score < 0` sentinel mistook it for "no field matched" and dropped the action.
const negativePool = [
{ id: "n", title: "Unrelated", source: "o" + "x".repeat(200) + "rnt", group: "", input: "" }
];
assert.equal(ctx.searchActions(negativePool, "ornt").length, 1,
"a low-score fuzzy match is not mistaken for no match");
// A setting whose displayed title is the page row label keeps the descriptive ConfigOptionDef name as
// a search-only alias, so the old wording still finds it without being shown.
const aliasPool = [
{ id: "rev", title: "Reverse on even", full_label: "Overhang reversal", source: "Process : Quality : Overhangs", group: "", input: "" }
];
assert.deepEqual(ctx.searchActions(aliasPool, "overhang reversal").map(function (a) { return a.id; }), ["rev"],
"the descriptive full_label is searchable even though the title shows the row label");
assert.equal(ctx.matchIndex.rev.title, null,
"an alias-only match does not highlight the displayed title");
assert.deepEqual(ctx.searchActions(aliasPool, "reversal").map(function (a) { return a.id; }), ["rev"],
"a token that exists only in the full_label still matches");
// Multi-token cross-field search: each whitespace-separated word must match SOME searchable field,
// but different words may match different fields. "inner" is the title while "speed" and
// "acceleration" live in the source breadcrumb, so the query as a whole is never contiguous in one
// field - the old single-needle match found nothing for this.
const crossPool = [
{ id: "acc", title: "Inner wall", source: "Process : Speed : Acceleration", group: "", input: "" },
{ id: "spd", title: "Inner wall", source: "Process : Speed : Other layers speed", group: "", input: "" },
{ id: "other", title: "Outer wall", source: "Process : Quality : Walls", group: "", input: "" }
];
assert.deepEqual(
ctx.searchActions(crossPool, "speed acceleration inner").map(function (a) { return a.id; }),
["acc"],
"words may match different fields and every word is required"
);
assert.deepEqual(
ctx.searchActions(crossPool, "speed inner").map(function (a) { return a.id; }).sort(),
["acc", "spd"],
"a two-word title+source query matches every setting under Speed"
);
assert.deepEqual(
ctx.searchActions(crossPool, "quality inner").map(function (a) { return a.id; }),
[],
"an action is dropped when any one word matches no field"
);
// Highlighting merges the per-field ranges the tokens produced.
ctx.searchActions(crossPool, "speed acceleration inner");
assert.deepEqual(ctx.matchIndex.acc.title, [[0, 5]], "the title token highlights in the title");
assert.deepEqual(ctx.matchIndex.acc.source, [[10, 15], [18, 30]], "each path token highlights in the breadcrumb");
assert.deepEqual(ctx.queryTokens(" Speed Acceleration "), ["speed", "acceleration"],
"a query splits into normalized whitespace-separated tokens");
assert.deepEqual(ctx.queryTokens(""), [], "an empty query has no tokens");
// Inline completion: the LAST token is completed to a word in the top-ranked result, name first then
// breadcrumb. Pure - the caller appends `suffix`.
const compPool = [
{ id: "acc", title: "Inner wall", source: "Process : Speed : Acceleration", group: "", input: "" },
{ id: "smooth", title: "Smooth", source: "Process : Speed : Other layers speed", group: "", input: "" }
];
assert.equal(ctx.completionFor("speed acc", ctx.searchActions(compPool, "speed acc")).suffix, "eleration",
"the last token completes to the next word in the breadcrumb");
assert.equal(ctx.completionFor("inner w", ctx.searchActions(compPool, "inner w")).suffix, "all",
"the title is preferred over the breadcrumb for completion");
assert.equal(ctx.completionFor("inner wall", ctx.searchActions(compPool, "inner wall")), null,
"an already-complete word has nothing to add");
assert.equal(ctx.completionFor("", compPool), null, "an empty query has no completion");
assert.equal(ctx.completionFor("zzz", ctx.searchActions(compPool, "zzz")), null,
"a query with no match has no completion");
// actionCategory: a command/dynamic action's group is its category; a setting uses the top-level
// source segment; every plugin shares one header; a category-less action falls back to "Other".
assert.equal(ctx.actionCategory({ id: "c", group: "Help", source: "OrcaSlicer", kind: "command" }), "Help",
"a command's group is its category");
assert.equal(ctx.actionCategory({ id: "s", group: "", source: "Process : Quality : Layers", kind: "command" }), "Process",
"a setting's category is the top-level source segment");
assert.equal(ctx.actionCategory({ id: "s", group: "", source: "Filament : Cooling", kind: "command" }), "Filament",
"a Filament setting groups under Filament");
assert.equal(ctx.actionCategory({ id: "plugin_script_action:Foo:bar.py", group: "", source: "Gcode Optimizer", kind: "plugin" }), "Plugins",
"every plugin shares one Plugins header");
assert.equal(ctx.actionCategory({ id: "x", group: "", source: "", kind: "command" }), "Other",
"a category-less action falls back to Other");
// groupActions: bucket by category, order the groups alphabetically, keep the incoming order within
// each group (the pool arrives frecency-sorted).
const groupPool = [
{ id: "q1", title: "Q1", source: "Quality", group: "Quality", kind: "command" },
{ id: "h1", title: "H1", source: "OrcaSlicer", group: "Help", kind: "command" },
{ id: "p1", title: "P1", source: "Process : A", group: "", kind: "command" },
{ id: "h2", title: "H2", source: "OrcaSlicer", group: "Help", kind: "command" }
];
assert.deepEqual(ctx.groupActions(groupPool).map(function (a) { return a.id; }), ["h1", "h2", "p1", "q1"],
"groups are alphabetical and each group keeps its incoming order");
// commandList (the main-phase list) delegates to the ranked search for a typed query and returns
// recents + the category-grouped pool for an empty query.
const mixed = [
{ id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", kind: "command", input: "" },
{ id: "set", title: "Sparse Infill Density", source: "Quality", group: "Quality", kind: "command", input: "" }
];
assert.equal(ctx.commandList(mixed, [], "sli")[0].id, "cmd",
"a typed query keeps the relevance-ranked action list (best match first)");
assert.deepEqual(ctx.commandList(mixed, mixed.slice(0, 1), "").map(function (a) { return a.id; }), ["cmd", "set"],
"an empty query shows the recents first and de-dupes them out of the tail");
assert.deepEqual(ctx.commandList(mixed, [], "").map(function (a) { return a.id; }), ["cmd", "set"],
"empty query + no recents shows the whole action pool");
assert.deepEqual(ctx.commandList(mixed, [mixed[1]], "").map(function (a) { return a.id; }), ["set", "cmd"],
"the recent is hoisted above the alphabetically-ordered groups");
// commandSections: "Recent" (when recents exist) plus one header per category in the grouped list;
// a typed query or an empty list yields no headers. Uses a computed grouped list so the recents
// hoist and the category ordering are exercised together.
const sectionPool = [
{ id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", kind: "command" },
{ id: "help", title: "Shortcuts", source: "OrcaSlicer", group: "Help", kind: "command" },
{ id: "set", title: "Infill", source: "Quality", group: "Quality", kind: "command" }
];
const sectionList = ctx.commandList(sectionPool, [sectionPool[0]], "");
assert.deepEqual(sectionList.map(function (a) { return a.id; }), ["cmd", "help", "set"],
"recents are hoisted, then the rest is grouped alphabetically (Commands, Help, Quality)");
assert.deepEqual(ctx.commandSections(sectionList, 1, ""), { 0: "Recent", 1: "Help", 2: "Quality" },
"recents + grouped actions get one header per category");
assert.deepEqual(ctx.commandSections([sectionPool[0]], 1, ""), { 0: "Recent" },
"a list that is all recents gets only the Recent header");
assert.deepEqual(ctx.commandSections(ctx.commandList(sectionPool, [], ""), 0, ""),
{ 0: "Commands", 1: "Help", 2: "Quality" },
"with no recents the grouped list still gets category headers");
assert.equal(ctx.commandSections(sectionList, 1, "sli"), null,
"a typed query has no section headers");
assert.equal(ctx.commandSections([], 0, ""), null,
"an empty list has no section headers");
// selectedActionId: resolves the active list (recents+pool for an empty query, filtered list otherwise).
assert.equal( assert.equal(
ctx.selectedActionId({ zone: "list", i: 0 }, duplicateActions, [], ""), ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), []),
null, "0123456789abcdef",
"Enter with an empty query must not resolve to an action the blank list never showed" "Enter with an empty query resolves the first action in the recents+pool list"
); );
assert.equal( assert.equal(
ctx.selectedActionId({ zone: "list", i: 0 }, duplicateActions, [], "rep"), ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), []),
"0123456789abcdef", "0123456789abcdef",
"a typed query resolves the list selection" "a typed query resolves the list selection"
); );
assert.equal( assert.equal(
ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""), ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [duplicateActions[0]], ""), []),
"fedcba9876543210", "0123456789abcdef",
"favourites stay runnable with an empty query - the fav bar is always visible" "Enter with an empty query resolves the recent entry"
);
assert.equal(
ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"]),
"fedcba9876543210",
"favourites stay runnable with an empty query - the fav bar is always visible"
); );
// Fav quick-launch slots: digit 1..9 -> index 0..8, digit 0 -> index 9 (the 10th), else -1.
assert.equal(ctx.favIndexForDigit("1"), 0, "digit 1 maps to the 1st favourite slot");
assert.equal(ctx.favIndexForDigit("9"), 8, "digit 9 maps to the 9th favourite slot");
assert.equal(ctx.favIndexForDigit("0"), 9, "digit 0 maps to the 10th (last) favourite slot");
assert.equal(ctx.favIndexForDigit("x"), -1, "non-digit keys are not a slot");
assert.equal(ctx.favIndexForDigit(""), -1, "an empty key is not a slot");
// Badge label per 0-based index: 0..8 -> "1".."9", index 9 -> "0", out of range -> null.
assert.equal(ctx.favSlotForIndex(0), "1", "index 0 shows badge 1");
assert.equal(ctx.favSlotForIndex(8), "9", "index 8 shows badge 9");
assert.equal(ctx.favSlotForIndex(9), "0", "index 9 shows badge 0 (the 10th slot)");
assert.equal(ctx.favSlotForIndex(10), null, "index 10 is beyond the cap");
assert.equal(ctx.favSlotForIndex(-1), null, "negative index is not a slot");
assert.equal(ctx.K_FAV_LIMIT, 10, "the slot count matches the quick-launch cap");
// favDigitFromEvent: prefer the physical code (so macOS Option+digit still maps even though e.key
// is the composed symbol), and fall back to e.key for keyboards/synthetic events without a code.
assert.equal(ctx.favDigitFromEvent({ code: "Digit1", key: "¡" }), "1", "Digit1 wins over a composed key");
assert.equal(ctx.favDigitFromEvent({ code: "Digit0", key: "0" }), "0", "Digit0 is a physical digit");
assert.equal(ctx.favDigitFromEvent({ code: "Numpad7", key: "7" }), "7", "numpad digits count");
assert.equal(ctx.favDigitFromEvent({ code: "", key: "3" }), "3", "a missing code falls back to key");
assert.equal(ctx.favDigitFromEvent({ key: "a" }), "a", "non-digit input is passed through (maps to -1)");
assert.equal(ctx.favDigitFromEvent(null), "", "a null event yields no digit");
// nextSel: arrow-nav wrapping. Down wraps at the list bottom to the first row; Up wraps at the
// list top to the last row ONLY when there's no fav bar above (else it goes to the fav bar).
assert.deepEqual(ctx.nextSel({ zone: "list", i: 2 }, "ArrowDown", 3, 0), { zone: "list", i: 0 },
"ArrowDown at the last row wraps to the first row");
assert.deepEqual(ctx.nextSel({ zone: "list", i: 1 }, "ArrowDown", 3, 0), { zone: "list", i: 2 },
"ArrowDown in the middle advances by one");
assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 3, 0), { zone: "list", i: 2 },
"ArrowUp at the first row with no fav bar wraps to the last row");
assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 3, 2), { zone: "fav", i: 0 },
"ArrowUp at the first row with a fav bar goes to the fav bar (unchanged)");
assert.deepEqual(ctx.nextSel({ zone: "list", i: 2 }, "ArrowUp", 3, 0), { zone: "list", i: 1 },
"ArrowUp in the middle moves up by one");
assert.deepEqual(ctx.nextSel({ zone: "fav", i: 1 }, "ArrowDown", 3, 2), { zone: "list", i: 0 },
"ArrowDown from the fav bar lands on the first list row");
assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowDown", 1, 0), { zone: "list", i: 0 },
"a single-row list never wraps off the end");
assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 1, 0), { zone: "list", i: 0 },
"ArrowUp on the only row stays put");
// Windowed list reveal: how many rows must be materialized to cover `fromIndex` plus `size` more,
// clamped to the total. Drives the "render the next window on scroll / arrow-nav" append.
assert.equal(ctx.revealTarget(100, 0, 100), 100, "covers the whole list when the window reaches the end");
assert.equal(ctx.revealTarget(100, 60, 100), 100, "clamps to the total at the tail");
assert.equal(ctx.revealTarget(30, 5, 100), 30, "a short list is fully covered");
assert.equal(ctx.revealTarget(100, 5, 50), 55, "reveals exactly fromIndex + size");
assert.equal(ctx.revealTarget(100, -5, 50), 50, "negative start is clamped to the first row");
assert.equal(ctx.revealTarget(200, 50, 100), 150, "a scroll viewpoint reveals a window past the current rows");
assert.equal(ctx.revealTarget(10, 0, 50), 10, "a list shorter than one window stays fully materialized");
// spacerHeight: un-rendered rows (44px) plus un-rendered section headers (30px), never negative.
assert.equal(ctx.spacerHeight(100, 50, 0, 0), 50 * 44, "the tail rows reserve their full height");
assert.equal(ctx.spacerHeight(100, 100, 0, 0), 0, "a fully-rendered list needs no spacer");
assert.equal(ctx.spacerHeight(100, 50, 3, 1), 50 * 44 + 2 * 30, "pending section headers reserve their height too");
assert.equal(ctx.spacerHeight(10, 0, 2, 0), 10 * 44 + 2 * 30, "a short list still reserves its headers");
assert.equal(ctx.spacerHeight(0, 0, 0, 0), 0, "an empty list has no spacer");
assert.equal(ctx.spacerHeight(10, 20, 0, 5), 0, "over-rendered counters clamp to zero");
// visibleFavourites: the quick-bar drops pins whose action no longer exists (plugin unloaded,
// command removed) and collapses duplicate ids, keeping the persisted pin order.
assert.deepEqual(ctx.visibleFavourites(["a", "b", "c"], [{ id: "a" }, { id: "b" }]),
["a", "b"], "a pin with no live action is dropped from the quick-bar");
assert.deepEqual(ctx.visibleFavourites(["b", "a", "b"], [{ id: "a" }, { id: "b" }]),
["b", "a"], "duplicate pins collapse to the first occurrence");
assert.deepEqual(ctx.visibleFavourites([], [{ id: "a" }]), [], "no pins renders an empty quick-bar");
assert.deepEqual(ctx.visibleFavourites(["a"], []), [], "a stale config with no actions renders nothing");
// actionIcon: the SVG base name for a tile's pictogram, or "" when the action has none (blank tile).
assert.equal(ctx.actionIcon({ id: "x", title: "Slice", icon: "media_play" }), "media_play",
"an action's icon base name is returned verbatim");
assert.equal(ctx.actionIcon({ id: "x", title: "Go to tab...", icon: "" }), "",
"an empty icon renders a blank tile");
assert.equal(ctx.actionIcon({ id: "x", title: "Plugin action" }), "",
"a missing icon field renders a blank tile");
assert.equal(ctx.actionIcon(null), "",
"a null action (tab row) renders a blank tile");
// tileIcon: the base name a tile renders - the action's own icon when present, else the placeholder.
assert.equal(ctx.tileIcon({ id: "x", title: "Slice", icon: "media_play" }), "media_play",
"an action with an icon keeps it");
assert.equal(ctx.tileIcon({ id: "x", title: "Go to tab...", icon: "" }), "action_default",
"an empty icon falls back to the placeholder");
assert.equal(ctx.tileIcon({ id: "x", title: "Plugin action" }), "action_default",
"a missing icon falls back to the placeholder");
assert.equal(ctx.DEFAULT_ICON, "action_default", "the placeholder is the dedicated default glyph");
assert.ok(fs.existsSync(__dirname + "/../../../images/action_default.svg"),
"the placeholder SVG ships alongside the page's other icons");
// needsModeSwitch: a setting is gated only when its required mode outranks the user's current mode.
assert.equal(ctx.needsModeSwitch({ mode: "advanced" }, "simple"), true, "Advanced is gated in Simple mode");
assert.equal(ctx.needsModeSwitch({ mode: "expert" }, "simple"), true, "Expert is gated in Simple mode");
assert.equal(ctx.needsModeSwitch({ mode: "expert" }, "advanced"), true, "Expert is gated in Advanced mode");
assert.equal(ctx.needsModeSwitch({ mode: "develop" }, "expert"), true, "Developer is gated in Expert mode");
assert.equal(ctx.needsModeSwitch({ mode: "simple" }, "simple"), false, "a Simple setting is not gated");
assert.equal(ctx.needsModeSwitch({ mode: "advanced" }, "advanced"), false, "an Advanced setting is not gated in Advanced mode");
assert.equal(ctx.needsModeSwitch({ mode: "develop" }, "develop"), false, "a Developer setting is not gated in Developer mode");
assert.equal(ctx.needsModeSwitch({}, "simple"), false, "a command with no mode is never gated");
// MODE_RANK must match the C++ ConfigOptionMode order (comSimple < comAdvanced < comExpert < comDevelop).
assert.deepEqual(ctx.MODE_RANK, { simple: 0, advanced: 1, expert: 2, develop: 3 },
"mode rank matches the C++ ConfigOptionMode order");
// modeBadge: the tag text for gated settings, empty once the setting is available.
assert.equal(ctx.modeBadge({ mode: "advanced" }, "simple"), "Advanced", "Advanced badge text");
assert.equal(ctx.modeBadge({ mode: "expert" }, "simple"), "Expert", "Expert badge text");
assert.equal(ctx.modeBadge({ mode: "develop" }, "simple"), "Developer", "Developer badge text");
assert.equal(ctx.modeBadge({ mode: "advanced" }, "advanced"), "", "no badge when the mode already matches");
// modeFilterFromQuery: whole-word, case-insensitive mode keywords -> internal mode values. "developer"
// maps to the internal "develop"; "simple" is deliberately not a keyword; a prefix is not a match.
assert.deepEqual(ctx.modeFilterFromQuery("advanced"), ["advanced"], "Advanced is a mode keyword");
assert.deepEqual(ctx.modeFilterFromQuery("Expert"), ["expert"], "the keyword is case-insensitive");
assert.deepEqual(ctx.modeFilterFromQuery("developer"), ["develop"], "Developer maps to the develop value");
assert.deepEqual(ctx.modeFilterFromQuery("develop"), ["develop"], "the internal develop spelling also works");
assert.deepEqual(ctx.modeFilterFromQuery("expert retraction"), ["expert"], "a keyword is found among other text");
assert.deepEqual(ctx.modeFilterFromQuery("advanced expert"), ["advanced", "expert"], "multiple keywords are deduped in order");
assert.deepEqual(ctx.modeFilterFromQuery("advanced advanced"), ["advanced"], "a repeated keyword is deduped");
assert.deepEqual(ctx.modeFilterFromQuery("simple"), [], "Simple is not a mode keyword");
assert.deepEqual(ctx.modeFilterFromQuery("advance"), [], "a mode-word prefix is not a whole-word match");
assert.deepEqual(ctx.modeFilterFromQuery(""), [], "an empty query names no mode");
// searchActions mode union: a mode keyword keeps the normal text matches AND appends every setting
// requiring that mode. Not a filter - a Simple setting literally named "Advanced..." still shows, and
// commands (mode "simple") are never pulled in by a keyword.
const modePool = [
{ id: "a1", title: "Top Surface Layers", source: "Quality", group: "Quality : Layers", mode: "advanced" },
{ id: "a2", title: "Advanced Detection", source: "Quality", group: "Quality", mode: "simple" },
{ id: "a3", title: "Retraction Length", source: "Process", group: "Process : Quality", mode: "expert" },
{ id: "a4", title: "Slice", source: "OrcaSlicer", group: "Commands", mode: "simple" }
];
var adv = ctx.searchActions(modePool, "advanced");
assert.deepEqual(adv.map(function (a) { return a.id; }), ["a2", "a1"],
"a text match (a2) ranks above the mode-only setting (a1), and no expert/command leaks in");
var expert = ctx.searchActions(modePool, "expert");
assert.deepEqual(expert.map(function (a) { return a.id; }), ["a3"], "the expert keyword pulls in the expert setting");
assert.deepEqual(ctx.searchActions(modePool, "developer"), [], "no developer settings means no mode extras");
assert.equal(ctx.searchActions(modePool, "retraction")[0].id, "a3",
"a query with no mode keyword is unaffected by the mode union");
assert.equal(ctx.searchActions([{ id: "both", title: "Advanced", source: "Quality", group: "", mode: "advanced" }], "advanced").length,
1, "a setting that both matches text and requires the mode appears exactly once");
// actionHasWiki: the footer's wiki link/F1 path is offered only when the action carries a wiki flag.
assert.equal(ctx.actionHasWiki({ id: "x", wiki: true }), true, "a wiki-flagged setting offers the wiki action");
assert.equal(ctx.actionHasWiki({ id: "x", wiki: false }), false, "a setting without a wiki path offers nothing");
assert.equal(ctx.actionHasWiki({ id: "x" }), false, "a missing wiki field offers nothing");
assert.equal(ctx.actionHasWiki(null), false, "no action selected offers nothing");
// actionHasDetail: the footer strip appears only when the highlighted action has a description or
// wiki link; selecting a plain command hides it.
assert.equal(ctx.actionHasDetail({ id: "a", desc: "Layer height" }), true, "a description shows the footer");
assert.equal(ctx.actionHasDetail({ id: "a", wiki: true }), true, "a wiki link shows the footer");
assert.equal(ctx.actionHasDetail({ id: "a", desc: "Layer height", wiki: true }), true, "both show the footer");
assert.equal(ctx.actionHasDetail({ id: "a", desc: "" }), false, "an empty description hides the footer");
assert.equal(ctx.actionHasDetail({ id: "a", desc: "", wiki: false }), false, "empty description and false wiki hide the footer");
assert.equal(ctx.actionHasDetail({ id: "a" }), false, "an action with neither hides the footer");
assert.equal(ctx.actionHasDetail(null), false, "no selected action hides the footer");
// detailToggleVisible: the expand/collapse arrow is offered only when there is a description to
// toggle. Collapse is a global preference, so the control stays for short tooltips too.
assert.equal(ctx.detailToggleVisible({ id: "a", desc: "Layer height" }), true, "a description offers the toggle");
assert.equal(ctx.detailToggleVisible({ id: "a", desc: "" }), false, "an empty description offers no toggle");
assert.equal(ctx.detailToggleVisible({ id: "a", wiki: true }), false, "a wiki-only action has nothing to collapse");
assert.equal(ctx.detailToggleVisible({ id: "a" }), false, "an action with no description offers no toggle");
assert.equal(ctx.detailToggleVisible(null), false, "no selected action offers no toggle");
// stateFromPayload: the footer expansion is a persisted global and defaults to expanded when the
// C++ payload omits it (first run / older config).
assert.equal(ctx.stateFromPayload({}).tooltipExpanded, true, "expansion defaults to true when absent");
assert.equal(ctx.stateFromPayload({ tooltip_expanded: false }).tooltipExpanded, false, "a collapsed payload is honored");
assert.equal(ctx.stateFromPayload({ tooltip_expanded: true }).tooltipExpanded, true, "an expanded payload is honored");
console.log("ok"); console.log("ok");
+509 -185
View File
@@ -1,225 +1,549 @@
* { box-sizing: border-box; } * {
html, body { margin: 0; } box-sizing: border-box;
body {
font-family: var(--orca-font, "Segoe UI", sans-serif);
font-size: 13px;
color: var(--text, var(--orca-fg, #1b1c1e));
background: var(--bg, var(--orca-bg, #fff));
overflow: hidden;
user-select: none;
} }
html,
body {
margin: 0;
}
body {
font-family: var(--orca-font, "Segoe UI", sans-serif);
font-size: 13px;
color: var(--text, var(--orca-fg, #1b1c1e));
background: var(--bg, var(--orca-bg, #fff));
overflow: hidden;
user-select: none;
}
.launcher { .launcher {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--panel, var(--orca-bg, #fff)); background: var(--panel, var(--orca-bg, #fff));
border: 1px solid var(--border, var(--orca-border, #ddd)); border: 1px solid var(--border, var(--orca-border, #ddd));
overflow: hidden; border-radius: 7px;
/* why: no height cap here - the launcher reports its true natural height to C++, which overflow: hidden;
/* why: no height cap here - the launcher reports its true natural height to C++, which
sizes the popup to match (HTML is source of truth). The list's own max-height is what sizes the popup to match (HTML is source of truth). The list's own max-height is what
bounds growth; capping the launcher would make the measurement circular. */ bounds growth; capping the launcher would make the measurement circular. */
} }
.fav-bar { .fav-bar {
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 9px 10px; padding: 9px 10px;
border-bottom: 1px solid var(--border, var(--orca-border, #ddd)); border-bottom: 1px solid var(--border, var(--orca-border, #ddd));
overflow-x: auto; /* scroll horizontally once favourites overflow the row */ overflow-x: auto;
scrollbar-width: thin; /* scroll horizontally once favourites overflow the row */
/* why: keep scrollIntoView (arrow-nav) from scrolling the first/last tile flush to the edge, scrollbar-width: thin;
/* why: keep scrollIntoView (arrow-nav) from scrolling the first/last tile flush to the edge,
which would clip its selected outline. Matches the 10px horizontal padding. */ which would clip its selected outline. Matches the 10px horizontal padding. */
scroll-padding-inline: 10px; scroll-padding-inline: 10px;
} }
.fav-bar[hidden] { display: none; }
.fav-bar[hidden] {
display: none;
}
/* Name of the selected favourite, above the bar; left-aligned to the tiles' 10px inset. */ /* Name of the selected favourite, above the bar; left-aligned to the tiles' 10px inset. */
.fav-eyebrow { flex: 0 0 auto; padding: 8px 10px 0; } .fav-eyebrow {
flex: 0 0 auto;
padding: 8px 10px 0;
}
.fav-tile { .fav-tile {
flex: 0 0 auto; /* keep tiles full-size; don't shrink to fit - scroll instead */ flex: 0 0 auto;
width: 30px; /* keep tiles full-size; don't shrink to fit - scroll instead */
height: 30px; width: 30px;
border: 0; height: 30px;
border-radius: 8px; border: 0;
cursor: pointer; border-radius: 8px;
color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%)); cursor: pointer;
font-weight: 700; /* why: mirror .tile centering - icons/placeholder are 16px, and a bare <button> inherits
/* why: mirror .tile centering - tileCode can be 2-3 chars (e.g. "EA1") on collision, and a 13px + UA padding, which would clip them. inline-flex + pad:0 + overflow:hidden fits them. */
bare <button> inherits 13px + UA padding, clipping the code (e.g. "AE1"). inline-flex + 12px + pad:0 fits it. */ font-size: 12px;
font-size: 12px; padding: 0;
padding: 0; display: inline-flex;
display: inline-flex; align-items: center;
align-items: center; justify-content: center;
justify-content: center; overflow: hidden;
overflow: hidden; background: var(--speed-tile-bg, #f0f0f0);
background: var(--speed-tile-bg, #f0f0f0); border: 1px solid var(--speed-tile-border, #d8d8d8);
border: 1px solid var(--speed-tile-border, #d8d8d8);
} }
.fav-tile.sel { outline: 2px solid var(--main-color, var(--orca-accent, #009688)); outline-offset: 2px; }
.fav-tile.sel {
outline: 2px solid var(--main-color, var(--orca-accent, #009688));
outline-offset: 2px;
}
/* Hover-revealed remove button in the tile's corner; sits inside the tile bounds so overflow:hidden clips it. */
.fav-tile {
position: relative;
}
.fav-unpin {
position: absolute;
top: 1px;
right: 1px;
width: 13px;
height: 13px;
padding: 0;
border: 0;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--muted, var(--orca-muted, #6b7280));
background: rgba(127, 127, 127, .35);
cursor: pointer;
opacity: 0;
}
.fav-tile:hover .fav-unpin,
.fav-tile.sel .fav-unpin {
opacity: 1;
}
.fav-unpin:hover {
background: rgba(127, 127, 127, .6);
}
/* Numbered quick-launch slot (Alt/Option+digit), top-left corner of the fav tile. */
.fav-slot {
position: absolute;
top: 1px;
left: 1px;
min-width: 11px;
height: 11px;
padding: 0 2px;
border-radius: 3px;
font-size: 8px;
font-weight: 600;
line-height: 11px;
text-align: center;
color: var(--muted, var(--orca-muted, #6b7280));
background: rgba(127, 127, 127, .22);
}
/* Transient flash banner pinned to the top of the launcher (e.g. "favourites are full"). */
.dial-flash {
flex: 0 0 auto;
padding: 4px 10px;
font-size: 11px;
color: var(--plugin-status-warn, #b45309);
background: var(--plugin-status-warn-bg, rgba(255, 193, 7, .22));
animation: dial-flash-fade 2.5s ease forwards;
}
@keyframes dial-flash-fade {
0% {
opacity: 0;
}
10% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.ctx-menu { .ctx-menu {
position: fixed; position: fixed;
z-index: 10; z-index: 10;
min-width: 120px; min-width: 120px;
padding: 4px; padding: 4px;
background: var(--panel, var(--orca-bg, #fff)); background: var(--panel, var(--orca-bg, #fff));
border: 1px solid var(--border, var(--orca-border, #ddd)); border: 1px solid var(--border, var(--orca-border, #ddd));
border-radius: 7px; border-radius: 7px;
box-shadow: 0 4px 16px rgba(0,0,0,.18); box-shadow: 0 4px 16px rgba(0, 0, 0, .18);
} }
.ctx-menu[hidden] { display: none; }
.ctx-menu[hidden] {
display: none;
}
.ctx-item { .ctx-item {
display: block; display: block;
width: 100%; width: 100%;
padding: 6px 10px; padding: 6px 10px;
border: 0; border: 0;
border-radius: 5px; border-radius: 5px;
background: transparent; background: transparent;
color: var(--text, var(--orca-fg, #1b1c1e)); color: var(--text, var(--orca-fg, #1b1c1e));
font: inherit; font: inherit;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
} }
.ctx-item:hover { background: var(--row-hover, rgba(127,127,127,.16)); }
.ctx-item:disabled { opacity: .4; cursor: default; } .ctx-item:hover {
.ctx-item:disabled:hover { background: transparent; } background: var(--row-hover, rgba(127, 127, 127, .16));
.dial-head { flex: 0 0 auto; padding: 8px; } }
.ctx-item:disabled {
opacity: .4;
cursor: default;
}
.ctx-item:disabled:hover {
background: transparent;
}
.dial-head {
flex: 0 0 auto;
padding: 8px;
}
.plugin-search { .plugin-search {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
height: 30px; height: 30px;
padding: 0 8px; padding: 0 8px;
background: var(--panel, var(--orca-bg, #fff)); background: var(--panel, var(--orca-bg, #fff));
border: 1px solid var(--border, var(--orca-border, #ddd)); border: 1px solid var(--border, var(--orca-border, #ddd));
border-radius: 7px; border-radius: 7px;
} }
.plugin-search:focus-within { .plugin-search:focus-within {
border-color: var(--main-color, var(--orca-accent, #009688)); border-color: var(--main-color, var(--orca-accent, #009688));
box-shadow: 0 0 0 2px rgba(0,150,136,.25); box-shadow: 0 0 0 2px rgba(0, 150, 136, .25);
} }
.plugin-search-icon { display: inline-flex; color: var(--muted, var(--orca-muted, #6b7280)); }
.plugin-search-icon {
display: inline-flex;
color: var(--muted, var(--orca-muted, #6b7280));
}
.plugin-search-input { .plugin-search-input {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
background: transparent; padding: 0;
border: 0; background: transparent;
outline: 0; border: 0;
color: var(--text, var(--orca-fg, #1b1c1e)); outline: 0;
font: inherit; color: var(--text, var(--orca-fg, #1b1c1e));
font: inherit;
} }
/* Wraps the input so the ghost completion can sit exactly at the caret. The ghost mirrors the typed
text in an invisible run (reserving its width) followed by the muted suggested suffix. */
.plugin-search-field {
position: relative;
flex: 1;
min-width: 0;
display: flex;
align-items: center;
}
.search-ghost {
position: absolute;
inset: 0;
display: flex;
align-items: center;
overflow: hidden;
white-space: pre;
font: inherit;
pointer-events: none;
}
.search-ghost[hidden] {
display: none;
}
.search-ghost-typed {
visibility: hidden;
}
.search-ghost-suffix {
color: var(--muted, var(--orca-muted, #6b7280));
opacity: .7;
}
.plugin-search-clear { .plugin-search-clear {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 12px; width: 12px;
height: 12px; height: 12px;
padding: 0; padding: 0;
border: 0; border: 0;
border-radius: 50%; border-radius: 50%;
color: var(--muted, var(--orca-muted, #6b7280)); color: var(--muted, var(--orca-muted, #6b7280));
cursor: pointer; cursor: pointer;
background: rgba(127,127,127,.20); background: rgba(127, 127, 127, .20);
} }
.plugin-search-clear[hidden] { display: none; }
.plugin-search-clear[hidden] {
display: none;
}
.dial-count { .dial-count {
padding: 4px 4px 0; padding: 4px 4px 0;
text-align: left; text-align: left;
font-size: 11px; font-size: 11px;
color: var(--muted, var(--orca-muted, #6b7280)); color: var(--muted, var(--orca-muted, #6b7280));
} }
.dial-count[hidden] { display: none; }
.dial-count[hidden] {
display: none;
}
.dial-list { .dial-list {
flex: 0 1 auto; flex: 0 1 auto;
min-height: 0; min-height: 0;
/* ADJUST HEIGHT HERE. The popup auto-resizes to the content (HTML is the source of truth), so /* ADJUST HEIGHT HERE. The popup auto-resizes to the content (HTML is the source of truth), so
this list cap drives the whole window height: --rows full rows + a ~30% peek of the next row this list cap drives the whole window height: --rows full rows + a ~30% peek of the next row
as a "scroll for more" affordance. Bump --rows to show more rows; change 0.3 for a bigger/ as a "scroll for more" affordance. Bump --rows to show more rows; change 0.3 for a bigger/
smaller peek. --row-h MUST match .row min-height (44px). */ smaller peek. --row-h MUST match .row min-height (44px). */
--row-h: 44px; --row-h: 44px;
--rows: 5; --rows: 5;
max-height: calc(var(--rows) * var(--row-h) + 0.3 * var(--row-h) + 4px); max-height: calc(var(--rows) * var(--row-h) + 0.3 * var(--row-h) + 4px);
overflow-y: auto; overflow-y: auto;
padding: 4px 4px 6px; padding: 4px 4px 6px;
scrollbar-width: thin; scrollbar-width: thin;
} }
.dial-list.empty { overflow-y: hidden; }
.dial-list.empty {
overflow-y: hidden;
}
/* Section header in the empty-query list ("Recent" then one per category). Its 30px height MUST match
SECTION_H in speeddial.js, which reserves header space in the windowed-list bottom spacer. */
.dial-section {
height: 30px;
display: flex;
align-items: center;
padding: 0 8px;
font-size: 10px;
font-weight: 600;
letter-spacing: .04em;
text-transform: uppercase;
color: var(--muted, var(--orca-muted, #6b7280));
opacity: .85;
}
/* Bottom spacer for the windowed command list: fills the un-rendered tail so the scrollbar reflects
the full match count, while only a near-viewport window of rows exists in the DOM. */
.dial-spacer-bottom {
flex: 0 0 auto;
}
.row { .row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
min-height: 44px; min-height: 44px;
padding: 6px 8px; padding: 6px 8px;
border-radius: 7px; border-radius: 7px;
cursor: pointer; cursor: pointer;
} }
.row:hover { background: var(--row-hover, rgba(127,127,127,.16)); }
.row.sel { background: var(--row-selected, rgba(0,150,136,.18)); } .row:hover {
background: var(--row-hover, rgba(127, 127, 127, .16));
}
.row.sel {
background: var(--row-selected, rgba(0, 150, 136, .18));
}
.tile { .tile {
flex: 0 0 auto; flex: 0 0 auto;
width: 26px; width: 26px;
height: 26px; height: 26px;
border-radius: 6px; border-radius: 6px;
color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%)); display: inline-flex;
font-weight: 700; align-items: center;
font-size: 12px; justify-content: center;
display: inline-flex; background: var(--speed-tile-bg, #f0f0f0);
align-items: center; border: 1px solid var(--speed-tile-border, #d8d8d8);
justify-content: center;
background: var(--speed-tile-bg, #f0f0f0);
border: 1px solid var(--speed-tile-border, #d8d8d8);
} }
.row-left { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
/* Native SVG pictogram in a tile. The Orca icon grid draws 1px strokes from 0.5..14.5 of the 16
viewBox, so the visible glyph is 0..15 and, centered as-is, leaves one extra pixel on the right/
bottom (the "Save Project leans left" look). Shift by half a pixel to center the visible artwork. */
.tile-icon {
width: 16px;
height: 16px;
display: block;
pointer-events: none;
transform: translate(.5px, .5px);
}
/* Tab-strip glyphs are drawn white for the dark tab bar; recolor to the shared #949494 gray so
they read on both the light and dark tile backgrounds. */
.tile-icon.tab-mono {
filter: brightness(0) invert(.58);
}
.row-left {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
}
.row-eyebrow { .row-eyebrow {
font-size: 10px; font-size: 10px;
line-height: 1.3; line-height: 1.3;
color: var(--muted, var(--orca-muted, #6b7280)); color: var(--muted, var(--orca-muted, #6b7280));
opacity: .85; opacity: .85;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.row-line { display: flex; align-items: center; gap: 7px; min-width: 0; }
.row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .row-line {
.row-name mark, .row-eyebrow mark { background: var(--plugin-status-warn-bg); color: var(--plugin-status-warn); border-radius: 2px; } display: flex;
.row-sc { flex: 0 0 auto; display: inline-flex; gap: 3px; } align-items: center;
kbd { gap: 7px;
display: inline-flex; min-width: 0;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
font-size: 11px;
color: var(--muted, var(--orca-muted, #6b7280));
background: rgba(127,127,127,.12);
border: 1px solid var(--border, var(--orca-border, #ddd));
border-radius: 4px;
} }
.star {
flex: 0 0 auto; .row-name {
width: 24px; min-width: 0;
height: 24px; overflow: hidden;
border: 0; text-overflow: ellipsis;
border-radius: 5px; white-space: nowrap;
background: transparent;
color: var(--muted, var(--orca-muted, #6b7280));
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0;
} }
.star svg { display: block; }
.row:hover .star:not(.on), .row-name mark,
.row.sel .star:not(.on) { opacity: .5; } .row-eyebrow mark {
.star.on { opacity: 1; color: var(--main-color, var(--orca-accent, #009688)); } background: var(--plugin-status-warn-bg);
.star:hover { background: rgba(127,127,127,.18); } color: var(--plugin-status-warn);
border-radius: 2px;
}
/* Mode tag on settings above the user's current mode (Advanced/Expert/Developer). */
.row-mode {
flex: 0 0 auto;
padding: 1px 6px;
font-size: 10px;
line-height: 1.4;
border-radius: 8px;
color: var(--plugin-status-warn);
background: var(--plugin-status-warn-bg);
white-space: nowrap;
}
.pin {
flex: 0 0 auto;
width: 24px;
height: 24px;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--muted, var(--orca-muted, #6b7280));
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0;
}
.pin svg {
display: block;
}
.row:hover .pin:not(.on),
.row.sel .pin:not(.on) {
opacity: .5;
}
.pin.on {
opacity: 1;
color: var(--main-color, var(--orca-accent, #009688));
}
.pin:hover {
background: rgba(127, 127, 127, .18);
}
.dial-empty { .dial-empty {
min-height: 64px; min-height: 64px;
padding: 18px 10px; padding: 18px 10px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--muted, var(--orca-muted, #6b7280)); color: var(--muted, var(--orca-muted, #6b7280));
text-align: center; text-align: center;
}
/* Footer detail strip: the selected action's description, its wiki link and the expand/collapse
arrow. Auto-sizes to the content; the description is clamped to 6 lines. */
.dial-detail {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border-top: 1px solid var(--border, var(--orca-border, #ddd));
overflow: hidden;
}
.dial-detail[hidden] {
display: none;
}
.detail-desc {
flex: 1 1 auto;
min-width: 0;
font-size: 11px;
line-height: 1.4;
color: var(--muted, var(--orca-muted, #6b7280));
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 6;
overflow: hidden;
overflow-wrap: anywhere;
}
.detail-wiki {
flex: 0 0 auto;
border: 0;
padding: 0;
background: transparent;
font: inherit;
font-size: 11px;
color: var(--main-color, var(--orca-accent, #009688));
cursor: pointer;
white-space: nowrap;
}
.detail-wiki:hover {
text-decoration: underline;
}
/* Expand/collapse arrow for the tooltip. Pinned right so it stays put whether or not the
description/wiki are visible; the chevron points up (collapse) when expanded. */
.detail-toggle {
flex: 0 0 auto;
margin-left: auto;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
padding: 2px;
background: transparent;
color: var(--muted, var(--orca-muted, #6b7280));
cursor: pointer;
}
.detail-toggle:hover {
color: var(--text, var(--orca-fg, #1b1c1e));
}
.detail-toggle svg {
transition: transform .12s ease;
}
.detail-toggle[aria-expanded="true"] svg {
transform: rotate(180deg);
} }
-4
View File
@@ -55,8 +55,6 @@
/* Speed dial icon tile treatment */ /* Speed dial icon tile treatment */
--speed-tile-bg: #efefef; --speed-tile-bg: #efefef;
--speed-tile-border: #d8d8d8; --speed-tile-border: #d8d8d8;
--speed-tile-text-s: 74%;
--speed-tile-text-l: 34%;
} }
:root[data-orca-theme="dark"] { :root[data-orca-theme="dark"] {
@@ -95,8 +93,6 @@
/* Speed dial icon tile treatment */ /* Speed dial icon tile treatment */
--speed-tile-bg: #34343b; --speed-tile-bg: #34343b;
--speed-tile-border: #50505a; --speed-tile-border: #50505a;
--speed-tile-text-s: 82%;
--speed-tile-text-l: 84%;
} }
/* Re-theme the shared common.css chrome through variables (replaces dark.css's /* Re-theme the shared common.css chrome through variables (replaces dark.css's
+72
View File
@@ -7,6 +7,10 @@
// Fold per-character so matched offsets stay in ORIGINAL string coordinates (highlighting slices the // Fold per-character so matched offsets stay in ORIGINAL string coordinates (highlighting slices the
// original text; a separately-folded string would desync offsets). // original text; a separately-folded string would desync offsets).
function FoldChar(ch) { function FoldChar(ch) {
// why: fast path - ASCII is already NFD-stable and diacritic-free, so the normalize/regex below are
// no-ops. This skip is the hot cost in the Speed Dial search (thousands of settings per keystroke).
if (ch.length === 1 && ch.charCodeAt(0) < 0x80)
return ch;
return ch.normalize("NFD").replace(/\p{Diacritic}/gu, ""); // accents always folded return ch.normalize("NFD").replace(/\p{Diacritic}/gu, ""); // accents always folded
} }
@@ -15,6 +19,74 @@ function Norm(ch, caseSensitive) {
return caseSensitive ? folded : folded.toLowerCase(); // case-sensitivity is the only toggle return caseSensitive ? folded : folded.toLowerCase(); // case-sensitivity is the only toggle
} }
// Pre-normalize a whole haystack with a length-preserving fold, so a caller can match it repeatedly
// against one cached string. One input UTF-16 code unit always maps to one output code unit, so
// indices stay aligned to the ORIGINAL text - the highlight ranges that FuzzyRangesNorm returns slice
// the original correctly. Iterate by UTF-16 code unit (not Array.from code point) to mirror
// FuzzyRanges' own indexing exactly.
function NormText(text, caseSensitive) {
const src = text || "";
let out = "";
for (let i = 0; i < src.length; i++)
out += NormStable(src[i], caseSensitive);
return out;
}
// Length-preserving variant of Norm: NFD can expand a code unit (Hangul syllables become Jamo) or
// drop it (combining diacritics), and toLowerCase can expand one too (U+0130). Any of those would
// desync highlight offsets, so fall back to the original unit whenever the fold is not 1:1.
function NormStable(ch, caseSensitive) {
const folded = FoldChar(ch);
const stable = folded.length === 1 ? folded : ch;
if (caseSensitive)
return stable;
const lower = stable.toLowerCase();
return lower.length === 1 ? lower : stable;
}
// Match a PRE-normalized haystack against a PRE-normalized needle (both produced by NormText with the
// same caseSensitive flag). Skipping the per-character fold makes repeated matching (per keystroke over a
// cached pool) cheap. Returns ranges in original coordinates, or null on no match.
// Prefers the most-contiguous (smallest-span) occurrence over greedy-leftmost: a scattered match that
// spans a stray earlier character is worse than a tight run later, so "orient" against a normalized
// "auto-orient" returns [[5,11]] (the word) not [[3,4],[6,11]]. A fully contiguous run is the optimum
// and short-circuits early.
function FuzzyRangesNorm(haystackNorm, needleNorm) {
const t = haystackNorm || "";
const needle = needleNorm || "";
if (!needle)
return null;
const n = t.length, nl = needle.length;
let best = null; // {ranges, span, start}
for (let start = 0; start < n; start++) {
if (t[start] !== needle[0])
continue;
let qi = 0;
const ranges = [];
let lastEnd = start;
for (let i = start; i < n && qi < nl; i++) {
if (t[i] === needle[qi]) {
const last = ranges[ranges.length - 1];
if (last && last[1] === i)
last[1] = i + 1;
else
ranges.push([i, i + 1]);
lastEnd = i + 1;
qi++;
}
}
if (qi !== nl)
continue;
const span = lastEnd - start;
if (!best || span < best.span || (span === best.span && start < best.start)) {
best = { ranges, span, start };
if (span === nl)
return ranges; // can't beat a fully contiguous run
}
}
return best ? best.ranges : null;
}
function EscapeRegExp(value) { function EscapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
} }
+15 -1
View File
@@ -5,7 +5,7 @@ const vm = require("vm"), assert = require("assert"), fs = require("fs");
const ctx = {}; const ctx = {};
vm.createContext(ctx); vm.createContext(ctx);
vm.runInContext(fs.readFileSync(__dirname + "/fuzzy-search.js", "utf8"), ctx); vm.runInContext(fs.readFileSync(__dirname + "/fuzzy-search.js", "utf8"), ctx);
const { FoldChar, Norm, EscapeRegExp, FuzzyRanges, WholeWordRanges } = ctx; const { FoldChar, Norm, NormText, EscapeRegExp, FuzzyRanges, WholeWordRanges, FuzzyRangesNorm } = ctx;
// FoldChar / Norm: accents fold, case only folds when case-insensitive. // FoldChar / Norm: accents fold, case only folds when case-insensitive.
assert.equal(FoldChar("é"), "e"); assert.equal(FoldChar("é"), "e");
@@ -34,4 +34,18 @@ assert.deepEqual(WholeWordRanges("a.b", "a", false), [[0, 1]]); // '.' is a
assert.equal(EscapeRegExp("a.b*"), "a\\.b\\*"); assert.equal(EscapeRegExp("a.b*"), "a\\.b\\*");
assert.deepEqual(WholeWordRanges("c++ tool", "c", false), [[0, 1]]); // '+' would be a regex error unescaped assert.deepEqual(WholeWordRanges("c++ tool", "c", false), [[0, 1]]); // '+' would be a regex error unescaped
// FuzzyRangesNorm: prefers the most-contiguous (smallest-span) occurrence over greedy-leftmost, so a
// stray earlier character does not steal the highlight from a later tight word.
assert.deepEqual(FuzzyRangesNorm(NormText("Auto-Orient", false), NormText("orient", false)), [[5, 11]]);
// A contiguous run inside a larger word is also preferred over a scattered greedy pick.
assert.deepEqual(FuzzyRangesNorm(NormText("AutoOriented", false), NormText("orient", false)), [[4, 10]]);
assert.equal(FuzzyRangesNorm(NormText("Measure", false), NormText("xyz", false)), null); // no subsequence
// NormText stays 1:1 with the original even when NFD expands (Hangul syllables) or drops (combining
// diacritics, U+0130) a code unit, so highlight ranges remain aligned to the original string.
assert.equal(NormText("각x", false).length, "각x".length);
assert.equal(NormText("e\u0301x", false).length, "e\u0301x".length);
assert.equal(NormText("\u0130x", false).length, "\u0130x".length);
assert.equal(FuzzyRangesNorm(NormText("각abcdeXfgh", false), NormText("X", false))[0][0], 6);
console.log("ok"); console.log("ok");
@@ -384,7 +384,7 @@ modules:
- cmake --build build_flatpak --target generate_system_cache -j$FLATPAK_BUILDER_N_JOBS - cmake --build build_flatpak --target generate_system_cache -j$FLATPAK_BUILDER_N_JOBS
- ./scripts/build_preset_cache.sh -n -b build_flatpak /app/share/OrcaSlicer/profiles - ./scripts/build_preset_cache.sh -n -b build_flatpak /app/share/OrcaSlicer/profiles
# Built (not run) here via the action's run-tests, then shipped to a separate # Built (not run) here when CI injects run-tests, then shipped to a separate
# test job. Only the test sources compile; nothing installs to /app. # test job. Only the test sources compile; nothing installs to /app.
test-commands: test-commands:
- cmake . -B build_flatpak -DBUILD_TESTS=ON - cmake . -B build_flatpak -DBUILD_TESTS=ON
+16 -2
View File
@@ -23,7 +23,8 @@ int main(int argc, char* argv[])
#else #else
("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory") ("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory")
#endif #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 // clang-format on
po::variables_map vm; 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 std::string profiles_path = vm["path"].as<std::string>();
const int log_level = vm["log_level"].as<int>(); 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)) { if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) {
std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n"; 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_is_validation_mode(true);
preset_bundle->set_default_suppressed(true); preset_bundle->set_default_suppressed(true);
preset_bundle->set_generate_vendor_caches(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 { try {
// In validation mode data_dir() is the profiles directory set above, so the // 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; 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; size_t cache_count = 0;
for (auto& entry : fs::directory_iterator(profiles_path)) for (auto& entry : fs::directory_iterator(profiles_path))
if (boost::iends_with(entry.path().string(), ".opc")) if (boost::iends_with(entry.path().string(), ".opc"))
+18
View File
@@ -286,6 +286,8 @@ void AppConfig::set_defaults()
// The getter already defaults, parses and clamps; write back what it resolves to. // The getter already defaults, parses and clamps; write back what it resolves to.
set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(get_plugin_pages_visible_count())); set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(get_plugin_pages_visible_count()));
set(SETTING_SPEED_DIAL_RECENT_COUNT, std::to_string(get_speed_dial_recent_count()));
if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty()) if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty())
set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false); set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false);
@@ -1684,6 +1686,22 @@ int AppConfig::get_plugin_pages_visible_count() const
return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX); return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX);
} }
int AppConfig::get_speed_dial_recent_count() const
{
std::string value = get(SETTING_SPEED_DIAL_RECENT_COUNT);
if (value.empty())
return SPEED_DIAL_RECENT_COUNT_DEFAULT;
int recent_count = SPEED_DIAL_RECENT_COUNT_DEFAULT;
try {
recent_count = std::stoi(value);
}
catch (...) {
return SPEED_DIAL_RECENT_COUNT_DEFAULT;
}
return std::clamp(recent_count, SPEED_DIAL_RECENT_COUNT_MIN, SPEED_DIAL_RECENT_COUNT_MAX);
}
std::vector<std::string> AppConfig::get_skipped_network_versions() const std::vector<std::string> AppConfig::get_skipped_network_versions() const
{ {
std::vector<std::string> result; std::vector<std::string> result;
+8
View File
@@ -46,6 +46,11 @@ using namespace nlohmann;
#define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5 #define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5
#define PLUGIN_PAGES_VISIBLE_COUNT_MAX 10 #define PLUGIN_PAGES_VISIBLE_COUNT_MAX 10
#define SETTING_SPEED_DIAL_RECENT_COUNT "speed_dial_recent_count"
#define SPEED_DIAL_RECENT_COUNT_MIN 0
#define SPEED_DIAL_RECENT_COUNT_DEFAULT 5
#define SPEED_DIAL_RECENT_COUNT_MAX 10
#if defined(_WIN32) || defined(_WIN64) #if defined(_WIN32) || defined(_WIN64)
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09" #define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09"
#else #else
@@ -394,6 +399,9 @@ public:
// dropdown on the last tab. // dropdown on the last tab.
int get_plugin_pages_visible_count() const; int get_plugin_pages_visible_count() const;
// Number of recently launched actions shown at the top of the Speed Dial; 0 hides them.
int get_speed_dial_recent_count() const;
std::vector<std::string> get_skipped_network_versions() const; std::vector<std::string> get_skipped_network_versions() const;
void add_skipped_network_version(const std::string& version); void add_skipped_network_version(const std::string& version);
bool is_network_version_skipped(const std::string& version) const; bool is_network_version_skipped(const std::string& version) const;
+3 -1
View File
@@ -776,7 +776,9 @@ double ConfigBase::get_abs_value(const t_config_option_key &opt_key, double rati
{ {
// Get stored option value. // Get stored option value.
const ConfigOption *raw_opt = this->option(opt_key); const ConfigOption *raw_opt = this->option(opt_key);
assert(raw_opt != nullptr); // Mirror the single-arg overload — assert() is a no-op under NDEBUG.
if (raw_opt == nullptr)
throw ConfigurationError("ConfigBase::get_abs_value(): \"" + opt_key + "\" is not defined");
if (raw_opt->type() != coFloatOrPercent) if (raw_opt->type() != coFloatOrPercent)
throw ConfigurationError("ConfigBase::get_abs_value(): opt_key is not of coFloatOrPercent"); throw ConfigurationError("ConfigBase::get_abs_value(): opt_key is not of coFloatOrPercent");
// Compute absolute value. // Compute absolute value.
+1
View File
@@ -710,6 +710,7 @@ private:
// Always check gcode placeholders when building in debug mode. // Always check gcode placeholders when building in debug mode.
#if !defined(NDEBUG) #if !defined(NDEBUG)
#undef ORCA_CHECK_GCODE_PLACEHOLDERS
#define ORCA_CHECK_GCODE_PLACEHOLDERS 1 #define ORCA_CHECK_GCODE_PLACEHOLDERS 1
#endif #endif
+7 -3
View File
@@ -120,9 +120,6 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe
fill_no_overlap fill_no_overlap
); );
if (this->layer()->lower_layer != nullptr)
// Cummulative sum of polygons over all the regions.
g.lower_slices = &this->layer()->lower_layer->lslices;
if (this->layer()->upper_layer != NULL) if (this->layer()->upper_layer != NULL)
g.upper_slices = &this->layer()->upper_layer->lslices; g.upper_slices = &this->layer()->upper_layer->lslices;
@@ -135,6 +132,13 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe
g.overhang_flow = this->bridging_flow(frPerimeter, object_config.thick_bridges); g.overhang_flow = this->bridging_flow(frPerimeter, object_config.thick_bridges);
g.solid_infill_flow = this->flow(frSolidInfill); g.solid_infill_flow = this->flow(frSolidInfill);
// Cumulative sum of polygons over all the regions, less what the lower layer could not print.
ExPolygons lower_slices;
if (this->layer()->lower_layer != nullptr) {
lower_slices = g.printable_slices(this->layer()->lower_layer->lslices);
g.lower_slices = &lower_slices;
}
if (this->layer()->object()->config().wall_generator.value == PerimeterGeneratorType::Arachne && !spiral_mode) if (this->layer()->object()->config().wall_generator.value == PerimeterGeneratorType::Arachne && !spiral_mode)
g.process_arachne(); g.process_arachne();
else else
+15
View File
@@ -2885,6 +2885,21 @@ bool PerimeterGeneratorLoop::is_internal_contour() const
return true; return true;
} }
// ORCA: Arachne drops features below min_feature_size, classic builds nothing thinner than a third of the
// nozzle. Both describe the layer below, a union of regions sharing neither nozzle nor generator, so every
// ambiguity resolves low: it may keep a sliver that was never printed, but it never drops one that was.
ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const
{
double min_width = *std::min_element(print_config->nozzle_diameter.values.begin(),
print_config->nozzle_diameter.values.end()) / 3.;
if (object_config->wall_generator.value == PerimeterGeneratorType::Arachne) {
const double min_feature_size = Arachne::make_paths_params(layer_id, *object_config, *print_config).min_feature_size;
// Spiral vase can put a classic layer under an Arachne one, so there both limits apply.
min_width = print_config->spiral_mode ? std::min(min_width, min_feature_size) : min_feature_size;
}
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices;
}
std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width) std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width)
{ {
float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1); float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
+2
View File
@@ -149,6 +149,8 @@ public:
//BBS //BBS
double smaller_width_ext_mm3_per_mm() const { return m_ext_mm3_per_mm_smaller_width; } double smaller_width_ext_mm3_per_mm() const { return m_ext_mm3_per_mm_smaller_width; }
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; } Polygons lower_slices_polygons() const { return m_lower_slices_polygons; }
// ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support.
ExPolygons printable_slices(const ExPolygons &slices) const;
private: private:
std::vector<Polygons> generate_lower_polygons_series(float width); std::vector<Polygons> generate_lower_polygons_series(float width);
+14 -3
View File
@@ -11,12 +11,23 @@ namespace Slic3r {
float CalibPressureAdvance::find_optimal_PA_speed(const DynamicPrintConfig &config, double line_width, double layer_height, int extruder_id, int filament_idx) float CalibPressureAdvance::find_optimal_PA_speed(const DynamicPrintConfig &config, double line_width, double layer_height, int extruder_id, int filament_idx)
{ {
const double general_suggested_min_speed = 100.0; const double general_suggested_min_speed = 100.0;
double filament_max_volumetric_speed = config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->get_at(filament_idx); // Read defensively — CLI callers may hand us a config missing optional keys.
auto vector_at = [&config](const char *key, int idx) -> double {
if (const auto *o = config.option<ConfigOptionFloats>(key)) return o->get_at(idx);
const ConfigOptionDef *d = config.def() ? config.def()->get(key) : nullptr;
return (d && d->default_value) ? d->get_default_value<ConfigOptionFloats>()->get_at(idx) : 0.0;
};
auto nullable_at = [&config](const char *key, int idx) -> double {
if (const auto *o = config.option<ConfigOptionFloatsNullable>(key)) return o->get_at(idx);
const ConfigOptionDef *d = config.def() ? config.def()->get(key) : nullptr;
return (d && d->default_value) ? d->get_default_value<ConfigOptionFloatsNullable>()->get_at(idx) : 0.0;
};
double filament_max_volumetric_speed = vector_at("filament_max_volumetric_speed", filament_idx);
// todo multi_extruders: // todo multi_extruders:
const float nozzle_diameter = config.option<ConfigOptionFloats>("nozzle_diameter")->get_at(extruder_id); const float nozzle_diameter = vector_at("nozzle_diameter", extruder_id);
if (line_width <= 0.) line_width = Flow::auto_extrusion_width(frPerimeter, nozzle_diameter); if (line_width <= 0.) line_width = Flow::auto_extrusion_width(frPerimeter, nozzle_diameter);
Flow pattern_line = Flow(line_width, layer_height, nozzle_diameter); Flow pattern_line = Flow(line_width, layer_height, nozzle_diameter);
auto pa_speed = std::min(std::max(general_suggested_min_speed, config.option<ConfigOptionFloatsNullable>("outer_wall_speed")->get_at(extruder_id)), auto pa_speed = std::min(std::max(general_suggested_min_speed, nullable_at("outer_wall_speed", extruder_id)),
filament_max_volumetric_speed / pattern_line.mm3_per_mm()); filament_max_volumetric_speed / pattern_line.mm3_per_mm());
return std::floor(pa_speed); return std::floor(pa_speed);
+4 -2
View File
@@ -14,8 +14,6 @@ set(SLIC3R_GUI_SOURCES
GUI/2DBed.hpp GUI/2DBed.hpp
GUI/3DBed.cpp GUI/3DBed.cpp
GUI/3DBed.hpp GUI/3DBed.hpp
GUI/Widgets/StaticGroup.cpp
GUI/Widgets/StaticGroup.hpp
GUI/3DScene.cpp GUI/3DScene.cpp
GUI/3DScene.hpp GUI/3DScene.hpp
GUI/Widgets/FilamentLoad.cpp GUI/Widgets/FilamentLoad.cpp
@@ -129,6 +127,8 @@ set(SLIC3R_GUI_SOURCES
GUI/SpeedDialDialog.hpp GUI/SpeedDialDialog.hpp
GUI/ActionRegistry.cpp GUI/ActionRegistry.cpp
GUI/ActionRegistry.hpp GUI/ActionRegistry.hpp
GUI/NativeCommands.cpp
GUI/NativeCommands.hpp
GUI/PluginsConfigDialog.cpp GUI/PluginsConfigDialog.cpp
GUI/PluginsConfigDialog.hpp GUI/PluginsConfigDialog.hpp
GUI/ProcessRunner.cpp GUI/ProcessRunner.cpp
@@ -476,6 +476,8 @@ set(SLIC3R_GUI_SOURCES
GUI/SkipPartCanvas.hpp GUI/SkipPartCanvas.hpp
GUI/Search.cpp GUI/Search.cpp
GUI/Search.hpp GUI/Search.hpp
GUI/SettingsIndex.cpp
GUI/SettingsIndex.hpp
GUI/Selection.cpp GUI/Selection.cpp
GUI/Selection.hpp GUI/Selection.hpp
GUI/SelectMachine.cpp GUI/SelectMachine.cpp
+523 -60
View File
@@ -3,20 +3,48 @@
#include "GUI.hpp" #include "GUI.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include "MainFrame.hpp"
#include "NativeCommands.hpp"
#include "Notebook.hpp"
#include "OptionsGroup.hpp"
#include "Plater.hpp"
#include "SettingsIndex.hpp"
#include "Tab.hpp"
#include "slic3r/plugin/PluginManager.hpp" #include "slic3r/plugin/PluginManager.hpp"
#include <libslic3r/AppConfig.hpp> #include <libslic3r/AppConfig.hpp>
#include <libslic3r/Config.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp> #include <slic3r/plugin/PythonPluginInterface.hpp>
#include <wx/thread.h> #include <wx/thread.h>
#include <boost/filesystem.hpp>
#include <boost/nowide/convert.hpp>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <ctime> #include <ctime>
#include <exception>
#include <iterator>
#include <string>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
namespace Slic3r { namespace GUI { namespace Slic3r { namespace GUI {
std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit)
{
std::vector<std::string> out;
out.reserve(std::min(ids.size(), limit));
for (const auto& id : ids) {
if (out.size() >= limit)
break;
if (std::find(out.begin(), out.end(), id) == out.end())
out.push_back(id);
}
return out;
}
namespace { namespace {
constexpr const char* kConfigSection = "speed_dial"; constexpr const char* kConfigSection = "speed_dial";
@@ -28,14 +56,9 @@ nlohmann::json parse_config_json(const std::string& value, nlohmann::json fallba
} }
nlohmann::json read_section(const char* key, nlohmann::json fallback) nlohmann::json read_section(const char* key, nlohmann::json fallback)
{ { return parse_config_json(wxGetApp().app_config->get(kConfigSection, key), std::move(fallback)); }
return parse_config_json(wxGetApp().app_config->get(kConfigSection, key), std::move(fallback));
}
void write_section(const char* key, const nlohmann::json& j) void write_section(const char* key, const nlohmann::json& j) { wxGetApp().app_config->set(kConfigSection, key, j.dump()); }
{
wxGetApp().app_config->set(kConfigSection, key, j.dump());
}
std::vector<std::string> read_string_array(const char* key) std::vector<std::string> read_string_array(const char* key)
{ {
@@ -53,7 +76,7 @@ double frecency_score(int count, long long last, long long now)
if (count <= 0) if (count <= 0)
return 0.0; return 0.0;
constexpr double HALF_LIFE_DAYS = 30.0; constexpr double HALF_LIFE_DAYS = 30.0;
double age = std::max(0.0, double(now - last) / 86400.0); double age = std::max(0.0, double(now - last) / 86400.0);
return count * std::pow(2.0, -age / HALF_LIFE_DAYS); return count * std::pow(2.0, -age / HALF_LIFE_DAYS);
} }
@@ -79,26 +102,25 @@ struct PluginScriptAction : AppAction
// The id an action for (plugin_key, capability) would have - lets refresh_capability // The id an action for (plugin_key, capability) would have - lets refresh_capability
// remove a gone capability without materialising the action. // remove a gone capability without materialising the action.
static std::string id_for(const std::string& plugin_key, const std::string& capability) static std::string id_for(const std::string& plugin_key, const std::string& capability)
{ { return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key); }
return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key);
}
PluginScriptAction(std::string plugin_key_in, std::string capability_in, std::string source_name) PluginScriptAction(std::string plugin_key_in, std::string capability_in, std::string source_name)
: AppAction(kIdPrefix, : AppAction(kIdPrefix,
capability_in.empty() ? plugin_key_in : capability_in, // title capability_in.empty() ? plugin_key_in : capability_in, // title
plugin_key_in, // source_key plugin_key_in, // source_key
std::move(source_name)), std::move(source_name))
plugin_key(std::move(plugin_key_in)), capability(std::move(capability_in)) , plugin_key(std::move(plugin_key_in))
, capability(std::move(capability_in))
{} {}
AppActionRunResult run() const override AppActionRunResult run(const std::string& /*param*/) const override
{ {
std::string error; std::string error;
const ExecutionResult result = PluginManager::instance().run_script_capability(plugin_key, capability, error); const ExecutionResult result = PluginManager::instance().run_script_capability(plugin_key, capability, error);
if (!error.empty()) if (!error.empty())
return {AppActionRunResult::Level::Error, from_u8(error)}; return {AppActionRunResult::Level::Error, from_u8(error)};
const bool skipped = result.status == PluginResult::Skipped; const bool skipped = result.status == PluginResult::Skipped;
const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished."); const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
return {skipped ? AppActionRunResult::Level::Info : AppActionRunResult::Level::Success, return {skipped ? AppActionRunResult::Level::Info : AppActionRunResult::Level::Success,
result.message.empty() ? fallback : from_u8(result.message)}; result.message.empty() ? fallback : from_u8(result.message)};
@@ -107,8 +129,7 @@ struct PluginScriptAction : AppAction
// Builds an action for a capability, or nullptr if it is not a currently-loaded, // Builds an action for a capability, or nullptr if it is not a currently-loaded,
// enabled script capability. // enabled script capability.
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability, std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability, const std::string& source_name)
const std::string& source_name)
{ {
PluginManager& manager = PluginManager::instance(); PluginManager& manager = PluginManager::instance();
if (!manager.is_plugin_loaded(plugin_key)) if (!manager.is_plugin_loaded(plugin_key))
@@ -119,8 +140,151 @@ std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std:
return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name); return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name);
} }
// ---- built-in command actions (the speed dial "commands" section) ------
constexpr const char* kSettingPrefix = "orca_setting";
constexpr const char* kPlateGotoPrefix = "orca_plate_goto";
constexpr const char* kRecentProjectPrefix = "orca_recent_project";
// Display context for a setting action's eyebrow, e.g. the "Process" in "Process : Quality : Layers".
// Keyed by the option's preset type so the palette reads like the settings sidebar tabs.
std::string setting_type_context(Preset::Type type)
{
switch (type) {
case Preset::TYPE_FILAMENT:
case Preset::TYPE_SLA_MATERIAL: return _u8L("Filament");
case Preset::TYPE_PRINTER: return _u8L("Printer");
case Preset::TYPE_PRINT:
case Preset::TYPE_SLA_PRINT:
default: return _u8L("Process");
}
}
// Stable, non-localized mode token for the webview, which maps it to a badge ("Developer" etc.).
const char* mode_key(ConfigOptionMode mode)
{
switch (mode) {
case comAdvanced: return "advanced";
case comExpert: return "expert";
case comDevelop: return "develop";
default: return "simple";
}
}
// A config setting exposed as a first-class action: selecting it jumps the sidebar to the option.
// The id is keyed by opt_key+type (NOT the display label), so renaming/localizing never re-keys
// the action; title/group/source are purely for display + search. run() performs the jump, and
// the generic registry run() bumps stats so a jump shows up in "recents" like any other action.
struct SettingAction : AppAction
{
std::string opt_key;
Preset::Type type;
std::wstring category; // English category, forwarded to jump_to_option (it localizes)
static std::string id_for(const std::string& opt_key, Preset::Type type)
{ return std::string(kSettingPrefix) + ":" + opt_key + ":" + std::to_string(int(type)); }
SettingAction(std::string opt_key_in,
Preset::Type type_in,
std::string title,
std::string group,
std::wstring category_in,
std::string source_name,
ConfigOptionMode mode_in)
: AppAction(AppActionId{id_for(opt_key_in, type_in)}, std::move(title), kOrcaSourceKey, std::move(source_name))
, opt_key(std::move(opt_key_in))
, type(type_in)
, category(std::move(category_in))
{
// A setting is a single-phase command: activating it jumps the sidebar to the option
// (like the sidebar's own settings search), then the dial closes. run() performs the jump.
this->kind = AppActionKind::Command;
this->group = std::move(group);
this->required_mode = mode_in;
}
AppActionRunResult run(const std::string& /*param*/) const override
{
wxGetApp().sidebar().jump_to_option(opt_key, type, category);
return {AppActionRunResult::Level::Success};
}
};
// Seed one action's persisted state (favourite flag + frecency counters) from an already-parsed
// stats blob and capped favourite list. Shared by the dynamic materialisers.
void seed_from(const nlohmann::json& stats, const std::vector<std::string>& favs, const std::string& id, AppAction& a)
{
a.favourite = std::find(favs.begin(), favs.end(), id) != favs.end();
if (auto it = stats.find(id); it != stats.end() && it->is_object()) {
a.count = it->value("count", 0);
a.last = it->value("last", 0LL);
}
}
// Drop actions whose id starts with `prefix` but that were not seen in this pass (a stale materialisation).
void drop_stale(std::unordered_map<std::string, std::shared_ptr<AppAction>>& actions, const char* prefix,
const std::unordered_set<std::string>& seen)
{
for (auto it = actions.begin(); it != actions.end();) {
if (it->first.rfind(prefix, 0) == 0 && !seen.count(it->first))
it = actions.erase(it);
else
++it;
}
}
// A dynamic "Go to Plate N" action, one per live plate, rebuilt on every snapshot() (so a
// rename/move immediately shows up). id is keyed by plate index, NOT the display title, so
// renaming a plate never re-keys it - the same contract as SettingAction. A pinned "Go to
// Plate N" whose plate is deleted simply stops resolving (visibleFavourites drops dead pins).
struct PlateAction : AppAction
{
int plate_index;
static std::string id_for(int index) { return AppAction::compose_id(kPlateGotoPrefix, std::to_string(index), kOrcaSourceKey); }
PlateAction(int index, std::string title, std::string source_name)
: AppAction(AppActionId{id_for(index)}, std::move(title), kOrcaSourceKey, std::move(source_name)), plate_index(index)
{
this->kind = AppActionKind::Command;
this->group = _u8L("Plate");
}
AppActionRunResult run(const std::string& /*param*/) const override
{ return NativeCommands::run("plate_goto", std::to_string(plate_index)); }
};
// A dynamic "Open recent project <name>" action, one per recent project file, rebuilt on every
// snapshot() (like PlateAction) so the list always reflects the current recents. The id is keyed
// by the file PATH, NOT the display title - the same contract as SettingAction/PlateAction, so a
// rename of a project (or a reordered recents list) never re-keys the action. A pinned recent whose
// file is deleted simply stops resolving (visibleFavourites drops dead pins). run() loads the
// project through MainFrame::open_recent_project so the existing missing-file handling is reused.
struct RecentProjectAction : AppAction
{
std::string file_path;
static std::string id_for(const std::string& path) { return AppAction::compose_id(kRecentProjectPrefix, path, kOrcaSourceKey); }
RecentProjectAction(std::string path, std::string title, std::string source)
: AppAction(AppActionId{id_for(path)}, std::move(title), kOrcaSourceKey, std::move(source)), file_path(std::move(path))
{
this->kind = AppActionKind::Command;
this->group = _u8L("Recent Projects");
}
AppActionRunResult run(const std::string& /*param*/) const override
{
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->open_recent_project(size_t(-1), wxString::FromUTF8(file_path));
return {AppActionRunResult::Level::Success};
}
};
} // namespace } // namespace
ActionRegistry::~ActionRegistry() = default;
void ActionRegistry::init() void ActionRegistry::init()
{ {
assert(wxThread::IsMain()); assert(wxThread::IsMain());
@@ -151,18 +315,12 @@ void ActionRegistry::init()
// Subscribe before enumerating so a concurrent load cannot land between the initial // Subscribe before enumerating so a concurrent load cannot land between the initial
// snapshot and callback registration. Duplicate notifications are safe: upsert is by // snapshot and callback registration. Duplicate notifications are safe: upsert is by
// id and the m_actions scan in refresh_source is idempotent. // id and the m_actions scan in refresh_source is idempotent.
manager.subscribe_on_load_callback( manager.subscribe_on_load_callback([on_source](const std::string& key) { on_source(key, ActionChange::Added); });
[on_source](const std::string& key) { on_source(key, ActionChange::Added); }); manager.subscribe_on_unload_callback([on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
manager.subscribe_on_unload_callback(
[on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
manager.subscribe_on_capability_load_callback( manager.subscribe_on_capability_load_callback(
[on_capability](const PluginCapabilityId& capability) { [on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Added); });
on_capability(capability, ActionChange::Added);
});
manager.subscribe_on_capability_unload_callback( manager.subscribe_on_capability_unload_callback(
[on_capability](const PluginCapabilityId& capability) { [on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Removed); });
on_capability(capability, ActionChange::Removed);
});
// enumerate current script capabilities // enumerate current script capabilities
std::unordered_map<std::string, std::string> source_names; std::unordered_map<std::string, std::string> source_names;
@@ -173,12 +331,40 @@ void ActionRegistry::init()
for (const auto& capability : manager.get_plugin_capabilities("", PluginCapabilityType::Script)) { for (const auto& capability : manager.get_plugin_capabilities("", PluginCapabilityType::Script)) {
if (!capability) if (!capability)
continue; continue;
const std::string& key = capability->audit_plugin_key(); const std::string& key = capability->audit_plugin_key();
auto it = source_names.find(key); auto it = source_names.find(key);
const std::string& source_name = it == source_names.end() ? key : it->second; const std::string& source_name = it == source_names.end() ? key : it->second;
if (auto action = make_action(key, capability->name(), source_name)) if (auto action = make_action(key, capability->name(), source_name))
upsert(std::move(action)); upsert(std::move(action));
} }
// Built-in palette commands (Save/Load, Preferences, Mode switch, Slice/Preview, Go to layer).
// Register after plugins so the plugin ids win on any (unlikely) id collision - ids are distinct
// by prefix, so this is order-independent. The catalog (and its thin AppAction adapter) lives in
// NativeCommands; the registry only stores and dispatches the result.
for (const NativeCommand& c : NativeCommands::catalog())
upsert(NativeCommands::make_action(c));
}
void ActionRegistry::relocalize_builtins()
{
assert(wxThread::IsMain());
if (!m_started)
return;
// Drop only the built-in commands; plugins and the dynamically materialised families are
// either unlocalized or rebuilt per snapshot. remove() only erases the map entry, and upsert()
// re-seeds favourites/stats from config, so key-based ids keep their pinned state.
std::vector<std::string> stale;
for (const auto& [id, action] : m_actions)
if (action->source_key() == kOrcaSourceKey && action->kind == AppActionKind::Command)
stale.push_back(id);
for (const std::string& id : stale)
remove(id);
NativeCommands::rebuild_catalog();
for (const NativeCommand& c : NativeCommands::catalog())
upsert(NativeCommands::make_action(c));
} }
void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change) void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change)
@@ -198,7 +384,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
if (change == ActionChange::Removed) if (change == ActionChange::Removed)
return; return;
PluginManager& manager = PluginManager::instance(); PluginManager& manager = PluginManager::instance();
const std::string source_name = find_loaded_source_name(manager, plugin_key); const std::string source_name = find_loaded_source_name(manager, plugin_key);
for (const auto& capability : manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Script)) { for (const auto& capability : manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Script)) {
if (!capability) if (!capability)
@@ -208,8 +394,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
} }
} }
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability, void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change)
ActionChange change)
{ {
assert(wxThread::IsMain()); assert(wxThread::IsMain());
@@ -233,7 +418,7 @@ void ActionRegistry::upsert(std::unique_ptr<AppAction> action)
return; return;
seed_state(*action); seed_state(*action);
std::string id = action->id(); std::string id = action->id();
std::shared_ptr<AppAction> stored = std::move(action); std::shared_ptr<AppAction> stored = std::move(action);
m_actions.insert_or_assign(std::move(id), std::move(stored)); m_actions.insert_or_assign(std::move(id), std::move(stored));
} }
@@ -246,11 +431,13 @@ void ActionRegistry::remove(const std::string& id)
void ActionRegistry::seed_state(AppAction& a) const void ActionRegistry::seed_state(AppAction& a) const
{ {
auto favs = read_string_array("favourite_actions"); // Favourites carry the quick-launch order, so the persisted list is the source of truth
// (not re-derived from the frecency sort). Cap it so stale configs can't exceed kFavLimit.
auto favs = favourite_ids();
a.favourite = std::find(favs.begin(), favs.end(), a.id()) != favs.end(); a.favourite = std::find(favs.begin(), favs.end(), a.id()) != favs.end();
nlohmann::json stats = read_section("stats", nlohmann::json::object()); nlohmann::json stats = read_section("stats", nlohmann::json::object());
auto it = stats.find(a.id()); auto it = stats.find(a.id());
if (it != stats.end() && it->is_object()) { if (it != stats.end() && it->is_object()) {
a.count = it->value("count", 0); a.count = it->value("count", 0);
a.last = it->value("last", 0LL); a.last = it->value("last", 0LL);
@@ -260,6 +447,14 @@ void ActionRegistry::seed_state(AppAction& a) const
} }
} }
void ActionRegistry::load_persisted(nlohmann::json& stats, std::vector<std::string>& favs) const
{
stats = read_section("stats", nlohmann::json::object());
if (!stats.is_object())
stats = nlohmann::json::object();
favs = favourite_ids();
}
// ---- read surface ----------------------------------------------------------- // ---- read surface -----------------------------------------------------------
const AppAction* ActionRegistry::by_id(const std::string& id) const const AppAction* ActionRegistry::by_id(const std::string& id) const
@@ -269,14 +464,11 @@ const AppAction* ActionRegistry::by_id(const std::string& id) const
return it == m_actions.end() ? nullptr : it->second.get(); return it == m_actions.end() ? nullptr : it->second.get();
} }
AppAction* ActionRegistry::find(const std::string& id) AppAction* ActionRegistry::find(const std::string& id) { return const_cast<AppAction*>(by_id(id)); }
{
return const_cast<AppAction*>(by_id(id));
}
// ---- dispatch + write-through ---------------------------------------------- // ---- dispatch + write-through ----------------------------------------------
AppActionRunResult ActionRegistry::run(const std::string& id) AppActionRunResult ActionRegistry::run(const std::string& id, const std::string& param)
{ {
assert(wxThread::IsMain()); assert(wxThread::IsMain());
auto it = m_actions.find(id); auto it = m_actions.find(id);
@@ -286,13 +478,13 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
// nested event loop; a queued source refresh can erase the entry while the // nested event loop; a queued source refresh can erase the entry while the
// keep-alive preserves the action until run returns. // keep-alive preserves the action until run returns.
std::shared_ptr<AppAction> keep = it->second; std::shared_ptr<AppAction> keep = it->second;
AppActionRunResult o = keep->run(); AppActionRunResult o = keep->run(param);
if (o.level == AppActionRunResult::Level::Busy) if (o.level == AppActionRunResult::Level::Busy)
return o; return o;
// Bump stats (write-through). Re-read to avoid clobbering a concurrent field. // Bump stats (write-through). Re-read to avoid clobbering a concurrent field.
nlohmann::json stats = read_section("stats", nlohmann::json::object()); nlohmann::json stats = read_section("stats", nlohmann::json::object());
if (!stats.is_object()) // corrupt (valid-JSON, non-object) value degrades to empty if (!stats.is_object()) // corrupt (valid-JSON, non-object) value degrades to empty
stats = nlohmann::json::object(); stats = nlohmann::json::object();
nlohmann::json& e = stats[id]; nlohmann::json& e = stats[id];
if (!e.is_object()) if (!e.is_object())
@@ -300,22 +492,37 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
e["count"] = e.value("count", 0) + 1; e["count"] = e.value("count", 0) + 1;
e["last"] = (long long) std::time(nullptr); e["last"] = (long long) std::time(nullptr);
write_section("stats", stats); write_section("stats", stats);
if (AppAction* live = find(id)) { live->count = e["count"]; live->last = e["last"]; } if (AppAction* live = find(id)) {
live->count = e["count"];
live->last = e["last"];
}
return o; return o;
} }
void ActionRegistry::set_favourite(const std::string& id, bool on) bool ActionRegistry::set_favourite(const std::string& id, bool on)
{ {
assert(wxThread::IsMain()); assert(wxThread::IsMain());
auto favs = read_string_array("favourite_actions"); // Start from the capped, deduped list so a persisted config can never be written back larger.
auto favs = favourite_ids();
auto it = std::find(favs.begin(), favs.end(), id); auto it = std::find(favs.begin(), favs.end(), id);
if (on && it == favs.end()) if (on && it == favs.end()) {
if (favs.size() >= kFavLimit)
return false; // bar is full - the caller surfaces a hint
favs.push_back(id); favs.push_back(id);
}
if (!on && it != favs.end()) if (!on && it != favs.end())
favs.erase(it); favs.erase(it);
write_section("favourite_actions", nlohmann::json(favs)); write_section("favourite_actions", nlohmann::json(favs));
if (AppAction* live = find(id)) if (AppAction* live = find(id))
live->favourite = on; live->favourite = on;
return true;
}
std::vector<std::string> ActionRegistry::favourite_ids() const
{
assert(wxThread::IsMain());
// Enforce the cap + dedupe on read so the persisted order can never grow past kFavLimit, even from an older config.
return cap_favourites(read_string_array("favourite_actions"), kFavLimit);
} }
void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids) void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
@@ -325,16 +532,184 @@ void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
std::vector<std::string> next; std::vector<std::string> next;
// keep the requested order, but only ids that are actually favourites (guard a bad payload) // keep the requested order, but only ids that are actually favourites (guard a bad payload)
for (const auto& id : ids) for (const auto& id : ids)
if (std::find(cur.begin(), cur.end(), id) != cur.end() && if (std::find(cur.begin(), cur.end(), id) != cur.end() && std::find(next.begin(), next.end(), id) == next.end())
std::find(next.begin(), next.end(), id) == next.end())
next.push_back(id); next.push_back(id);
// why: don't drop favourites the page omitted (e.g. pins with no live action hidden from the bar) // why: don't drop favourites the page omitted (e.g. pins with no live action hidden from the bar)
for (const auto& id : cur) for (const auto& id : cur)
if (std::find(next.begin(), next.end(), id) == next.end()) if (std::find(next.begin(), next.end(), id) == next.end())
next.push_back(id); next.push_back(id);
// never write the bar back larger than the quick-launch slots
next = cap_favourites(next, kFavLimit);
write_section("favourite_actions", nlohmann::json(next)); write_section("favourite_actions", nlohmann::json(next));
} }
void ActionRegistry::materialize_setting_actions()
{
assert(wxThread::IsMain());
// Reuse the Sidebar's live settings index: it's the only catalog whose group/category map is
// populated (Tab::add_key feeds it at build time), and it already mirrors the current
// configs/printer-technology. Use the all-modes view so the Speed Dial lists every setting,
// including those above the user's current mode, and can prompt to switch before jumping.
const std::vector<Search::Option>& options = wxGetApp().sidebar().settings_index().all_options();
// Load the persisted per-action state ONCE (not per-option) so a re-materialised setting keeps
// its recency/favourite; mirroring seed_state but amortised over the whole option set.
nlohmann::json stats;
std::vector<std::string> favs;
load_persisted(stats, favs);
std::unordered_set<std::string> seen;
for (const Search::Option& opt : options) {
// The row's live state drives both the hidden filter and the title (labels can change at
// runtime, e.g. brim_width -> "Brim ear radius"). Hidden rows are skipped, not marked seen.
Tab* tab = wxGetApp().get_tab(opt.type);
Tab::SettingRowState row;
if (tab)
row = tab->setting_row_state(opt.opt_key());
if (!row.visible)
continue;
const std::string id = SettingAction::id_for(opt.opt_key(), opt.type);
seen.insert(id);
// The page draws Line::label; the descriptive ConfigOptionDef name stays a search-only alias
// ("overhang reversal" still finds "Reverse on even").
const std::string search_label = boost::nowide::narrow(opt.label_local.empty() ? opt.label : opt.label_local);
std::string title = into_u8(Search::resolve_setting_title(from_u8(opt.display_label), row.label, row.multi));
if (title.empty())
title = search_label;
// Eyebrow/source = the full settings path "Process : Quality : Layers" (localized). The JS
// renders group || source and searches source + " " + group, so putting the whole path in
// source both displays it and makes it matchable by any segment (e.g. a "quality" query).
std::wstring path = boost::nowide::widen(setting_type_context(opt.type));
if (!opt.category_local.empty())
path += L" : " + opt.category_local;
if (!opt.group_local.empty())
path += L" : " + opt.group_local;
// title = the label the settings row draws; group stays empty so the source path (above) is
// the single display/search breadcrumb rather than being duplicated.
auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, title, std::string(), opt.category,
boost::nowide::narrow(path), opt.mode);
if (title != search_label)
action->full_label = search_label;
// Tile pictogram = the icon of the setting's own group header (e.g. Advanced -> param_advanced),
// the one shown next to it in the page. Fall back to the page/category icon for groups
// without one. Keys are the English titles the GUI registers.
action->icon = opt.group_icon;
if (action->icon.empty() && !opt.category.empty() && tab) {
const auto& icons = tab->get_category_icon_map();
auto it = icons.find(wxString(opt.category));
if (it != icons.end())
action->icon = it->second;
}
// Footer description + wiki affordance; only settings whose row declared a wiki path have one.
action->tooltip = opt.tooltip;
if (!opt.wiki_path.empty())
action->help_url = into_u8(OptionsGroup::get_url(opt.wiki_path));
seed_from(stats, favs, id, *action);
auto const action_id = action->id();
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
m_actions.insert_or_assign(action_id, app_action);
}
// Drop SettingActions whose option no longer exists in the current configs (e.g. the printer
// technology / UI mode changed). Non-setting actions are untouched.
drop_stale(m_actions, kSettingPrefix, seen);
}
void ActionRegistry::materialize_plate_actions()
{
assert(wxThread::IsMain());
// Plates are a filament (FFF) feature: SLA has a single plate and no plate UI, and gcode-only
// mode has no editable project - so no "Go to Plate N" actions are offered there.
Plater* plater = wxTheApp ? wxGetApp().plater() : nullptr;
if (!plater || plater->printer_technology() != ptFFF || plater->only_gcode_mode()) {
// Drop any stale plate actions (e.g. the printer technology switched to SLA).
drop_stale(m_actions, kPlateGotoPrefix, {});
return;
}
// Persisted per-action state, read ONCE (mirrors materialize_setting_actions) so a relisted
// "Go to Plate N" keeps its recency/favourite when the plate is renamed - the id is index-keyed.
nlohmann::json stats;
std::vector<std::string> favs;
load_persisted(stats, favs);
const std::vector<PartPlate*>& list = plater->get_partplate_list().get_plate_list();
std::unordered_set<std::string> seen;
for (size_t i = 0; i < list.size(); ++i) {
PartPlate* plate = list[i];
if (!plate)
continue;
const std::string id = PlateAction::id_for(int(i));
seen.insert(id);
// "Go to Plate N" + " (name)" when the plate is named, matching the object-list label.
std::string title(_u8L("Go to Plate"));
title += " " + std::to_string(i + 1);
const std::string name = plate->get_plate_name();
if (!name.empty())
title += " (" + name + ")";
auto action = std::make_unique<PlateAction>(int(i), title, kOrcaSourceName);
seed_from(stats, favs, id, *action);
auto const action_id = action->id();
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
m_actions.insert_or_assign(action_id, app_action);
}
// Drop plate actions whose index no longer exists (a plate was deleted / moved to the front).
drop_stale(m_actions, kPlateGotoPrefix, seen);
}
void ActionRegistry::materialize_recent_project_actions()
{
assert(wxThread::IsMain());
// Persisted per-action state, read ONCE (mirrors materialize_plate_actions) so a relisted recent
// project keeps its recency/favourite when the recents list reorders - the id is path-keyed.
nlohmann::json stats;
std::vector<std::string> favs;
load_persisted(stats, favs);
// app_config stores recents oldest-first; the palette shows newest-first.
std::vector<std::string> recents = wxGetApp().app_config->get_recent_projects();
std::reverse(recents.begin(), recents.end());
std::unordered_set<std::string> seen;
for (const std::string& path : recents) {
// Skip projects whose file is gone; the stale id is dropped below.
boost::system::error_code ec;
if (path.empty() || !boost::filesystem::exists(boost::filesystem::path(path), ec))
continue;
const std::string id = RecentProjectAction::id_for(path);
seen.insert(id);
// Title = file basename; source/eyebrow = the full path so search can match either.
boost::filesystem::path p(path);
std::string title = p.filename().string();
if (title.empty())
title = path;
auto action = std::make_unique<RecentProjectAction>(path, std::move(title), path);
seed_from(stats, favs, id, *action);
auto const action_id = action->id();
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
m_actions.insert_or_assign(action_id, app_action);
}
// Drop recent-project actions whose file no longer exists / was removed from the recents list.
drop_stale(m_actions, kRecentProjectPrefix, seen);
}
bool ActionRegistry::should_ask(const std::string& id) const bool ActionRegistry::should_ask(const std::string& id) const
{ {
assert(wxThread::IsMain()); assert(wxThread::IsMain());
@@ -351,11 +726,31 @@ void ActionRegistry::suppress_ask(const std::string& id)
write_section("ask_suppressed", nlohmann::json(arr)); write_section("ask_suppressed", nlohmann::json(arr));
} }
// ---- snapshot --------------------------------------------------------------- bool ActionRegistry::tooltip_expanded() const
nlohmann::json ActionRegistry::snapshot() const
{ {
assert(wxThread::IsMain()); assert(wxThread::IsMain());
const nlohmann::json j = read_section("tooltip_expanded", nlohmann::json(true));
return j.is_boolean() ? j.get<bool>() : true;
}
void ActionRegistry::set_tooltip_expanded(bool expanded)
{
assert(wxThread::IsMain());
write_section("tooltip_expanded", nlohmann::json(expanded));
}
// ---- snapshot ---------------------------------------------------------------
nlohmann::json ActionRegistry::snapshot()
{
assert(wxThread::IsMain());
// Settings and plates are first-class actions; make sure the current visible option set and the
// live plate list are materialised before we serialise the pool (tabs_list is built by the time
// the palette opens).
materialize_setting_actions();
materialize_plate_actions();
materialize_recent_project_actions();
std::vector<const AppAction*> sorted; std::vector<const AppAction*> sorted;
sorted.reserve(m_actions.size()); sorted.reserve(m_actions.size());
for (const auto& entry : m_actions) for (const auto& entry : m_actions)
@@ -374,17 +769,85 @@ nlohmann::json ActionRegistry::snapshot() const
return a->id() < b->id(); return a->id() < b->id();
}); });
auto action_to_json = [](const AppAction* a) {
return nlohmann::json({{"id", a->id()},
{"title", a->title()},
{"full_label", a->full_label},
{"source", a->source_name()},
{"group", a->group},
{"kind", a->kind == AppActionKind::Plugin ? "plugin" : "command"},
{"input", a->input},
{"icon", a->icon},
{"mode", mode_key(a->required_mode)},
{"desc", a->tooltip},
{"wiki", !a->help_url.empty()}});
};
nlohmann::json actions = nlohmann::json::array(); nlohmann::json actions = nlohmann::json::array();
for (const AppAction* a : sorted) for (const AppAction* a : sorted)
actions.push_back({{"id", a->id()}, actions.push_back(action_to_json(a));
{"title", a->title()},
{"source", a->source_name()},
{"shortcut", ""}});
// why: favourites is the ORDERED pin list - it must come from favourite_actions // why: favourites is the ORDERED pin list - it must come from favourite_actions
// as stored, not be re-derived from the frecency-sorted actions (that would // as stored, not be re-derived from the frecency-sorted actions (that would
// reorder the favourites bar). The page (js) filters out ids with no live action itself. // reorder the favourites bar). Drop pins with no live action (an option hidden by the
nlohmann::json favourites(read_string_array("favourite_actions")); // current mode, an unloaded plugin, a gone plate/project) and persist the pruned list, so
return {{"actions", std::move(actions)}, {"favourites", std::move(favourites)}}; // invisible pins can't silently fill the quick-launch cap. Order is preserved.
std::vector<std::string> favs = favourite_ids();
std::vector<std::string> live_favs;
live_favs.reserve(favs.size());
for (const auto& id : favs)
if (m_actions.count(id))
live_favs.push_back(id);
if (live_favs.size() != favs.size())
write_section("favourite_actions", nlohmann::json(live_favs));
nlohmann::json favourites(live_favs);
// Recent = the last-N launched actions by recency (only actions with a run history). N is a
// user preference; 0 hides recents without affecting the frecency order below.
const size_t recent_limit = size_t(wxGetApp().app_config->get_speed_dial_recent_count());
std::vector<const AppAction*> recent;
for (const auto& entry : m_actions)
if (entry.second->last > 0)
recent.push_back(entry.second.get());
std::sort(recent.begin(), recent.end(), [](const AppAction* a, const AppAction* b) {
if (a->last != b->last)
return a->last > b->last;
return a->id() < b->id();
});
if (recent.size() > recent_limit)
recent.resize(recent_limit);
nlohmann::json recent_json = nlohmann::json::array();
for (const AppAction* a : recent)
recent_json.push_back(action_to_json(a));
return {{"actions", std::move(actions)},
{"favourites", std::move(favourites)},
{"recent", std::move(recent_json)},
{"user_mode", mode_key(wxGetApp().get_mode())},
{"tooltip_expanded", tooltip_expanded()}};
}
// ---- tab options (enumerate the MainFrame notebook's current pages) ----------
nlohmann::json ActionRegistry::tab_options() const
{
assert(wxThread::IsMain());
nlohmann::json out = nlohmann::json::array();
if (!wxTheApp || wxGetApp().is_closing())
return out;
MainFrame* mf = wxGetApp().mainframe;
if (!mf || !mf->m_tabpanel)
return out;
Notebook* notebook = mf->m_tabpanel;
for (size_t i = 0; i < notebook->GetPageCount(); ++i) {
const wxString id = notebook->GetPageName(i);
if (id.empty())
continue;
out.push_back({{"id", id.ToStdString()},
{"title", notebook->GetPageLabel(i).ToStdString()},
{"icon", notebook->GetPageIcon(i)}});
}
return out;
} }
}} // namespace Slic3r::GUI }} // namespace Slic3r::GUI
+137 -25
View File
@@ -2,10 +2,13 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <libslic3r/Config.hpp>
#include <wx/string.h> #include <wx/string.h>
#include <wx/thread.h> #include <wx/thread.h>
#include <cassert> #include <cassert>
#include <cstddef>
#include <memory> #include <memory>
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -18,14 +21,25 @@ namespace Slic3r { namespace GUI {
// How a source's action set changed. Drives the registry's refresh handlers. // How a source's action set changed. Drives the registry's refresh handlers.
enum class ActionChange { Added, Removed }; enum class ActionChange { Added, Removed };
// What kind of runnable thing an action is. Drives the run-confirm gate (plugins ask, commands don't).
enum class AppActionKind { Plugin, Command };
// Result of running an AppAction, in the action layer's own vocabulary. Concrete // Result of running an AppAction, in the action layer's own vocabulary. Concrete
// actions translate their runner-specific result into this generic shape. // actions translate their runner-specific result into this generic shape.
struct AppActionRunResult struct AppActionRunResult
{ {
enum class Level { Success, Info, Error, Busy }; enum class Level { Success, Info, Error, Busy };
Level level = Level::Info; Level level = Level::Info;
wxString message; // empty = "nothing worth showing" wxString message; // empty = "nothing worth showing"
};
// Tag carrying a precomputed action id, used by the explicit-id ctor below. It exists so the
// id ctor and the compose-from-prefix ctor are NOT both reachable from a `const char*` first
// argument (which would make calls like AppAction("orca_command", ...) ambiguous).
struct AppActionId
{
std::string id;
}; };
// A speed-dial action: identity + user-state seeded from config + how to run itself. // A speed-dial action: identity + user-state seeded from config + how to run itself.
@@ -52,12 +66,35 @@ struct AppAction
} }
// seeded from AppConfig for the snapshot / sort: // seeded from AppConfig for the snapshot / sort:
bool favourite = false; bool favourite = false;
int count = 0; int count = 0;
long long last = 0; // epoch seconds long long last = 0; // epoch seconds
// Speed Dial presentation: Plugin keeps group empty (the UI falls back to the
// source name); Command sets a section label (e.g. "Commands", "Mode").
AppActionKind kind = AppActionKind::Plugin;
std::string group;
// Second-phase input descriptor for the palette: "percent" (jump to layer by a 0-100
// value) or "tab" (pick a notebook tab). Empty = run immediately on activation.
std::string input;
// Tile pictogram: SVG base name under resources/images; empty renders a blank tile (commands
// without a GUI icon, plugins). Set from NativeCommands / the setting's category icon.
std::string icon;
// Settings mode required to edit this action (SettingActions only). The palette prompts before
// running an action whose mode is above the user's current mode. comSimple for everything else.
ConfigOptionMode required_mode = comSimple;
// Description shown in the Speed Dial's footer strip (SettingActions: the localized tooltip).
std::string tooltip;
// Search-only alias when the title differs from the descriptive ConfigOptionDef name (e.g. title
// "Reverse on even", full_label "Overhang reversal"). Empty when the two agree.
std::string full_label;
// Full wiki URL, when the action has one (SettingActions whose row declared a label_path).
std::string help_url;
virtual ~AppAction() = default; virtual ~AppAction() = default;
virtual AppActionRunResult run() const = 0; // re-resolves + runs (UI thread) // Re-resolves + runs (UI thread). `param` carries an optional per-run argument for
// commands (e.g. a layer percentage); plugins ignore it.
virtual AppActionRunResult run(const std::string& param = {}) const = 0;
protected: protected:
// The definition is constructor-set and immutable. Refreshes replace an action // The definition is constructor-set and immutable. Refreshes replace an action
@@ -65,10 +102,17 @@ protected:
// why: source_key (not the display name) carries identity, so renaming the source's // why: source_key (not the display name) carries identity, so renaming the source's
// display name leaves the id - and its persisted stats/favourite - intact. // display name leaves the id - and its persisted stats/favourite - intact.
AppAction(std::string_view prefix, std::string title, std::string source_key, std::string source_name) AppAction(std::string_view prefix, std::string title, std::string source_key, std::string source_name)
: m_id(compose_id(prefix, title, source_key)), : m_id(compose_id(prefix, title, source_key))
m_title(std::move(title)), , m_title(std::move(title))
m_source_key(std::move(source_key)), , m_source_key(std::move(source_key))
m_source_name(std::move(source_name)) {} , m_source_name(std::move(source_name))
{}
// Explicit-id ctor: for actions whose id must NOT be derived from the display title
// (e.g. a setting action keyed by opt_key+type, so a rename/localization never re-keys it).
AppAction(AppActionId id, std::string title, std::string source_key, std::string source_name)
: m_id(std::move(id.id)), m_title(std::move(title)), m_source_key(std::move(source_key)), m_source_name(std::move(source_name))
{}
private: private:
std::string m_id; // <prefix>:<title>:<source_key> - stable identity + AppConfig key std::string m_id; // <prefix>:<title>:<source_key> - stable identity + AppConfig key
@@ -77,26 +121,53 @@ private:
std::string m_source_name; // display name of the action's source std::string m_source_name; // display name of the action's source
}; };
// Stable identity/display name of the built-in ("OrcaSlicer") action source. Shared by the native
// command catalog and the dynamically materialised setting/plate/recent actions so every built-in
// action re-keys together.
inline constexpr const char* kOrcaSourceKey = "orca";
inline constexpr const char* kOrcaSourceName = "OrcaSlicer";
// True when a setting at `setting_mode` cannot be edited in `current_mode` and the UI must switch
// first. Developer settings are handled as a separate prompt by the Speed Dial.
inline bool requires_mode_switch(ConfigOptionMode setting_mode, ConfigOptionMode current_mode)
{
return setting_mode > current_mode;
}
// Cap + dedupe a persisted favourite-id list, preserving first-occurrence order. A stale or
// hand-edited config must never grow the quick-launch bar past `limit`, and a duplicated id must collapse to its first pin.
std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit);
// Self-contained sink and single owner of runnable actions for the app session. // Self-contained sink and single owner of runnable actions for the app session.
// //
// Workflow: // Workflow:
// 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the // 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the current script
// current script capabilities into actions. // capabilities into actions, then materialises the static built-ins from the NativeCommands catalog.
// 2. Loader load/unload callbacks route through refresh_source()/refresh_capability(), // 2. Loader load/unload callbacks route through refresh_source()/refresh_capability(),
// which upsert()/remove() actions. The registry keeps the only action list and // which upsert()/remove() actions. The registry keeps the only action list and
// restores persisted user state as actions arrive. // restores persisted user state as actions arrive.
// 3. Consumers use by_id(), snapshot(), and run() without knowing the source. // 3. Dynamic built-in families (settings, plates, recent projects) are re-materialised at the top of
// snapshot(), because their membership follows live state (the current configs, plate list, recents).
// 4. Consumers use by_id(), snapshot(), and run() without knowing the source.
// //
// note: there is exactly one source (script plugins), so it lives inline here rather // note: the static catalog lives in NativeCommands; the registry owns the pool, persistence and
// than behind a polymorphic source interface. // dispatch, and materialises the dynamic families inline rather than behind a source interface.
class ActionRegistry class ActionRegistry
{ {
public: public:
~ActionRegistry();
// Subscribes to the plugin loader and enumerates its current actions. Call once // Subscribes to the plugin loader and enumerates its current actions. Call once
// on the UI thread after the plugin system is up; wires the initial list and live // on the UI thread after the plugin system is up; wires the initial list and live
// updates together. // updates together.
void init(); void init();
// Rebuilds the built-in command actions in the current UI locale. The command catalog copies
// translated titles/groups at construction, so after a live language switch the stored titles
// are stale until this runs. Ids are key-based and upsert re-seeds persisted state, so
// favourites/run history survive. UI thread only. No-op before init().
void relocalize_builtins();
// Takes ownership, seeds persisted state, then inserts the action or replaces // Takes ownership, seeds persisted state, then inserts the action or replaces
// the action with the same id. A null action is ignored. // the action with the same id. A null action is ignored.
void upsert(std::unique_ptr<AppAction> action); void upsert(std::unique_ptr<AppAction> action);
@@ -105,31 +176,72 @@ public:
void remove(const std::string& id); void remove(const std::string& id);
// Always-clean read surface. UI thread only. // Always-clean read surface. UI thread only.
const AppAction* by_id(const std::string& id) const; const AppAction* by_id(const std::string& id) const;
// Hard cap on the favourites bar: the numbered quick-launch slots (Alt/Option+1..9, 0).
static constexpr size_t kFavLimit = 10;
// Dispatch + write-through (registry is the only thing that touches AppConfig). // Dispatch + write-through (registry is the only thing that touches AppConfig).
AppActionRunResult run(const std::string& id); // runs + bumps stats AppActionRunResult run(const std::string& id, const std::string& param = {}); // runs + bumps stats
void set_favourite(const std::string& id, bool on); // Pin/unpin. Returns false when `on` would exceed kFavLimit (the bar is full) so the
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order // caller can surface a "favourites are full" hint instead of silently dropping the pin.
bool set_favourite(const std::string& id, bool on);
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
// Ordered pinned list (the source of truth), capped at kFavLimit and deduped, matching the
// visible bar the palette renders.
std::vector<std::string> favourite_ids() const;
// Run-confirm gate, keyed by action id (per-action "don't ask again"). // Run-confirm gate, keyed by action id (per-action "don't ask again").
bool should_ask(const std::string& id) const; bool should_ask(const std::string& id) const;
void suppress_ask(const std::string& id); void suppress_ask(const std::string& id);
// Flat, frecency-sorted snapshot for the webview: {actions:[...], favourites:[...]}. // Footer expand/collapse preference. Global (applies to every action) and persisted; absent
nlohmann::json snapshot() const; // means expanded, so a fresh config picks the richer default with no migration.
bool tooltip_expanded() const;
void set_tooltip_expanded(bool expanded);
// Flat, frecency-sorted snapshot for the webview:
// {actions:[...], favourites:[...], recent:[...]} (recent = last-N launched by recency).
nlohmann::json snapshot();
// "Go to tab..." Speed Dial helper: enumerate the MainFrame notebook's current pages
// as [{id,title,icon},...], using the page's real label (not the compact-blanked button text).
// Live by construction - built-in tabs (Home/Prepare/Preview/Device/Project/Calibration) and
// plugin tabs (plugin.<key>.<name>) are all Notebook pages, so a page appears/disappears with
// the notebook. Plugin tabs hidden in the overflow menu (many plugins) aren't separate pages and
// are not listed. Call on the UI thread; null-safe.
nlohmann::json tab_options() const;
private: private:
void seed_state(AppAction& a) const; // favourite/stats from config void seed_state(AppAction& a) const; // favourite/stats from config
AppAction* find(const std::string& id); AppAction* find(const std::string& id);
// Read the persisted stats blob + capped favourite list once for a materialisation pass.
void load_persisted(nlohmann::json& stats, std::vector<std::string>& favs) const;
// (Re)materialise the current visible config settings as SettingActions from the live
// searcher (respecting printer-tech + user-mode + visibility filtering), removing stale ones.
// Called at the top of snapshot() so the palette always reflects the current configs.
void materialize_setting_actions();
// (Re)materialise one "Go to Plate N" action per live plate, so the palette lists every plate
// directly on each spawn (no second-phase picker). FFF-editor only; SLA/gcode modes have no
// plate UI, so nothing is materialised and stale ids are dropped. Called at the top of snapshot().
void materialize_plate_actions();
// (Re)materialise one "Open recent project <name>" action per recent project file, so the
// palette lists every recent project and can load it by clicking. Keyed by file path (stable);
// files that no longer exist are skipped and their stale ids dropped. Called at the top of snapshot().
void materialize_recent_project_actions();
// Loader callbacks (marshalled to the UI thread) land here. refresh_source rebuilds // Loader callbacks (marshalled to the UI thread) land here. refresh_source rebuilds
// one plugin's whole action set; refresh_capability touches a single capability. // one plugin's whole action set; refresh_capability touches a single capability.
void refresh_source(const std::string& plugin_key, ActionChange change); void refresh_source(const std::string& plugin_key, ActionChange change);
void refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change); void refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change);
bool m_started = false; // init() runs exactly once; guards double-subscription bool m_started = false; // init() runs exactly once; guards double-subscription
std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock
}; };
}} // namespace Slic3r::GUI }} // namespace Slic3r::GUI
+10 -10
View File
@@ -403,7 +403,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// bind to their default mode; the other modes stay in the toolbar flyout. Registered here // bind to their default mode; the other modes stay in the toolbar flyout. Registered here
// where select_tool is in scope; the closures run at key-press time (members are live by then). // where select_tool is in scope; the closures run at key-press time (members are live by then).
auto sk_key = [this, select_tool](int ch, DesignSketchTool::Mode m, const wxString& h) { auto sk_key = [this, select_tool](int ch, DesignSketchTool::Mode m, const wxString& h) {
m_keys_sketch[ch] = [this, select_tool, m, h] { select_tool(m, h); }; m_keys_sketch[ch] = [select_tool, m, h] { select_tool(m, h); };
}; };
sk_key('L', DesignSketchTool::Mode::Line, _L("Line — click start, then end")); sk_key('L', DesignSketchTool::Mode::Line, _L("Line — click start, then end"));
sk_key('R', DesignSketchTool::Mode::CornerRect, _L("Rectangle — click two opposite corners")); sk_key('R', DesignSketchTool::Mode::CornerRect, _L("Rectangle — click two opposite corners"));
@@ -1445,16 +1445,16 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Choose FIRST, then open: open_tool() titles the card from m_dressup_type, so setting // Choose FIRST, then open: open_tool() titles the card from m_dressup_type, so setting
// it afterwards left the header reading "Fillet 1" over a chamfer. Nothing in the // it afterwards left the header reading "Fillet 1" over a chamfer. Nothing in the
// opener resets the combo, so this order is safe. // opener resets the combo, so this order is safe.
m_verb_actions["btn:dress#0"] = [this, open_feature, k_dress] { m_verb_actions["btn:dress#0"] = [this, open_feature] {
if (m_dressup_type) m_dressup_type->SetSelection(0); open_feature(k_dress); }; if (m_dressup_type) m_dressup_type->SetSelection(0); open_feature(k_dress); };
m_verb_actions["btn:dress#1"] = [this, open_feature, k_dress] { m_verb_actions["btn:dress#1"] = [this, open_feature] {
if (m_dressup_type) m_dressup_type->SetSelection(1); open_feature(k_dress); }; if (m_dressup_type) m_dressup_type->SetSelection(1); open_feature(k_dress); };
for (int op = 0; op < 3; ++op) for (int op = 0; op < 3; ++op)
m_verb_actions["btn:bool#" + std::to_string(op)] = [this, open_feature, k_bool, op] { m_verb_actions["btn:bool#" + std::to_string(op)] = [this, open_feature, op] {
open_feature(k_bool); open_feature(k_bool);
if (m_bool_op) { m_bool_op->SetSelection(op); refresh_preview(); } }; if (m_bool_op) { m_bool_op->SetSelection(op); refresh_preview(); } };
for (int t = 0; t < 2; ++t) for (int t = 0; t < 2; ++t)
m_verb_actions["btn:pat#" + std::to_string(t)] = [this, open_feature, k_pat, t] { m_verb_actions["btn:pat#" + std::to_string(t)] = [this, open_feature, t] {
open_feature(k_pat); if (m_pattern_type) m_pattern_type->SetSelection(t); }; open_feature(k_pat); if (m_pattern_type) m_pattern_type->SetSelection(t); };
auto* b_poly = icon_btn("design_polygon", _L("Polygon")); auto* b_poly = icon_btn("design_polygon", _L("Polygon"));
@@ -6625,7 +6625,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
mate_ghost = false; mate_ghost = false;
}; };
menu.Bind(wxEVT_MENU_HIGHLIGHT, menu.Bind(wxEVT_MENU_HIGHLIGHT,
[this, cs_a, cs_b, opts, mate_base, &mate_ghost, drop_ghost](wxMenuEvent& e) { [this, cs_a, cs_b, opts, &mate_ghost, drop_ghost](wxMenuEvent& e) {
const int i = e.GetMenuId() - (mate_base + 1); const int i = e.GetMenuId() - (mate_base + 1);
// Off the palette (a verb row, the header, or nothing) — a stale ghost from the row // Off the palette (a verb row, the header, or nothing) — a stale ghost from the row
// you just left is worse than none, so it goes as soon as the cursor does. // you just left is worse than none, so it goes as soon as the cursor does.
@@ -6635,7 +6635,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
m_viewport->repaint_now(); // synchronous: the popup owns the loop, a queued repaint is never serviced m_viewport->repaint_now(); // synchronous: the popup owns the loop, a queued repaint is never serviced
}); });
menu.Bind(wxEVT_MENU, [this, cs_a, cs_b, opts, mate_base, drop_ghost](wxCommandEvent& e) { menu.Bind(wxEVT_MENU, [this, cs_a, cs_b, opts, drop_ghost](wxCommandEvent& e) {
const int i = e.GetId() - (mate_base + 1); const int i = e.GetId() - (mate_base + 1);
if (i < 0 || i >= int(opts.size()) || !opts[i].viable) return; if (i < 0 || i >= int(opts.size()) || !opts[i].viable) return;
drop_ghost(); // the real bodies are about to become the ghost's pose drop_ghost(); // the real bodies are about to become the ghost's pose
@@ -6666,7 +6666,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
// status message on screen, and clicking one created nothing at all. The whole mate palette // status message on screen, and clicking one created nothing at all. The whole mate palette
// enumerated perfectly and fired nothing. Restricting the range keeps each half to its own ids // enumerated perfectly and fired nothing. Restricting the range keeps each half to its own ids
// regardless of bind order. // regardless of bind order.
menu.Bind(wxEVT_MENU_HIGHLIGHT, [this, &bound, base](wxMenuEvent& e) { menu.Bind(wxEVT_MENU_HIGHLIGHT, [this, &bound](wxMenuEvent& e) {
const int i = e.GetMenuId() - base; const int i = e.GetMenuId() - base;
if (i < 0 || i >= int(bound.size()) || bound[i] == nullptr || bound[i]->hint == nullptr) if (i < 0 || i >= int(bound.size()) || bound[i] == nullptr || bound[i]->hint == nullptr)
return; return;
@@ -6674,7 +6674,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
set_status(wxGetTranslation(wxString::FromUTF8(bound[i]->hint))); set_status(wxGetTranslation(wxString::FromUTF8(bound[i]->hint)));
m_status->Update(); // the popup owns the loop; without this the line repaints late m_status->Update(); // the popup owns the loop; without this the line repaints late
}, base, base + 499); // 499: the mate section starts at base + 500 (see mate_base) }, base, base + 499); // 499: the mate section starts at base + 500 (see mate_base)
menu.Bind(wxEVT_MENU, [this, &bound, base](wxCommandEvent& e) { menu.Bind(wxEVT_MENU, [this, &bound](wxCommandEvent& e) {
const int i = e.GetId() - base; const int i = e.GetId() - base;
if (i >= 0 && i < int(bound.size()) && bound[i]) if (i >= 0 && i < int(bound.size()) && bound[i])
run_offer_action(bound[i]->action); run_offer_action(bound[i]->action);
@@ -7935,7 +7935,7 @@ void DesignPanel::apply_live_constraint(SketchConstraintType type)
constraint_reject_text(plan.reason, type), int(sel.size()))); constraint_reject_text(plan.reason, type), int(sel.size())));
return; return;
case ConstraintPlan::Kind::AskValue: case ConstraintPlan::Kind::AskValue:
m_viewport->open_inline_value(plan.prefill, [this, plan, commit](double v) { m_viewport->open_inline_value(plan.prefill, [plan, commit](double v) {
commit(plan.defs, v); commit(plan.defs, v);
}); });
return; return;
+1 -1
View File
@@ -121,7 +121,7 @@ void SketchInlineEditor::do_commit()
// just sits there and the user has no idea what it wants. Say so in the title line and // just sits there and the user has no idea what it wants. Say so in the title line and
// keep editing. // keep editing.
ux_trace("refused", m_title, std::string("typed=") + m_buf); ux_trace("refused", m_title, std::string("typed=") + m_buf);
m_err = (m_buf[0] == '\0') ? into_u8(_L("Enter a number")) : into_u8(_L("Not a number")); m_err = (m_buf[0] == '\0') ? _u8L("Enter a number") : _u8L("Not a number");
m_focus_pending = true; m_focus_pending = true;
return; return;
} }
+1 -1
View File
@@ -82,7 +82,7 @@ CameraPopup::CameraPopup(wxWindow *parent)
top_sizer->Add(m_text_liveview_retry, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, FromDIP(5)); top_sizer->Add(m_text_liveview_retry, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, FromDIP(5));
top_sizer->Add(m_switch_liveview_retry, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, FromDIP(5)); top_sizer->Add(m_switch_liveview_retry, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, FromDIP(5));
m_switch_liveview_retry->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent &e) { m_switch_liveview_retry->Bind(wxEVT_TOGGLEBUTTON, [](wxCommandEvent &e) {
wxGetApp().app_config->set("liveview", "auto_retry", e.IsChecked()); wxGetApp().app_config->set("liveview", "auto_retry", e.IsChecked());
e.Skip(); e.Skip();
}); });
-6
View File
@@ -1042,7 +1042,6 @@ wxDEFINE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>); wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>);
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent);
@@ -3582,11 +3581,6 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
break; break;
} }
case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; } case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; }
case ' ': {
if (m_canvas_type == ECanvasType::CanvasView3D)
post_event(SimpleEvent(EVT_GLCANVAS_OPEN_SPEED_DIAL));
break;
}
case 'A': case 'A':
case 'a': case 'a':
{ {
-1
View File
@@ -169,7 +169,6 @@ wxDECLARE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>); // data: +1 => increase, -1 => decrease wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>); // data: +1 => increase, -1 => decrease
wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
+88
View File
@@ -2624,6 +2624,10 @@ void GUI_App::init_app_config()
} }
#endif // _WIN32 #endif // _WIN32
} }
// Speed Dial opens on a bare Space from any page by default. Seed the flag so Preferences and the
// MainFrame shortcut read the same value; an existing config (true or false) is left untouched.
if (app_config->get("enable_speed_dial").empty())
app_config->set_bool("enable_speed_dial", true);
set_logging_level(Slic3r::level_string_to_boost(app_config->get("log_severity_level"))); set_logging_level(Slic3r::level_string_to_boost(app_config->get("log_severity_level")));
} }
@@ -4603,6 +4607,12 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI enter"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI enter";
m_is_recreating_gui = true; m_is_recreating_gui = true;
// The palette injects its translated strings once, at creation; drop the cached dialog so the
// next open rebuilds it in the current locale (and can't outlive the old mainframe).
if (m_speed_dial_dialog) {
m_speed_dial_dialog->Destroy();
m_speed_dial_dialog = nullptr;
}
mainframe->shutdown(); mainframe->shutdown();
ProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); ProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE);
@@ -8171,6 +8181,22 @@ void GUI_App::save_mode(const /*ConfigOptionMode*/int mode)
update_mode(); update_mode();
} }
void GUI_App::set_mode(ConfigOptionMode mode)
{
const bool was_developer = app_config->get_bool("developer_mode");
if (was_developer)
app_config->set_bool("developer_mode", false);
save_mode(mode);
if (was_developer)
app_config->save();
}
void GUI_App::enable_developer_mode()
{
app_config->set_bool("developer_mode", true);
update_mode();
}
// Update view mode according to selected menu // Update view mode according to selected menu
void GUI_App::update_mode() void GUI_App::update_mode()
{ {
@@ -8319,6 +8345,65 @@ void GUI_App::open_plugins_dialog(size_t open_on_tab, const std::string& highlig
} }
} }
void GUI_App::refresh_plugins()
{
// The metadata refresh blocks on disc discovery and a cloud round-trip, so run it on a worker
// and report completion through the notification manager -- the speed dial needs no dialog.
std::thread([]() {
wxString error;
try {
refresh_plugin_metadata_blocking(/*fetch_cloud=*/true);
} catch (const std::exception& ex) {
error = from_u8(ex.what());
} catch (...) {
error = "Unknown error"; // plain literal: wx translation isn't safe off the UI thread
}
if (!wxTheApp)
return;
wxTheApp->CallAfter([error]() {
if (wxGetApp().is_closing())
return;
Plater* plater = wxGetApp().plater();
if (plater == nullptr)
return;
if (error.IsEmpty())
plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
into_u8(_L("Plugins refreshed.")));
else
plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
NotificationManager::NotificationLevel::ErrorNotificationLevel,
into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), error)));
});
}).detach();
}
void GUI_App::install_local_plugin()
{
if (mainframe == nullptr)
return;
wxFileDialog dialog(mainframe, _L("Select plugin package"), wxEmptyString, wxEmptyString, _L("Plugin files (*.py;*.whl)|*.py;*.whl"),
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (dialog.ShowModal() != wxID_OK)
return;
wxString message;
const bool ok = install_local_plugin_package(boost::filesystem::path(dialog.GetPath().ToUTF8().data()), mainframe, message);
if (message.IsEmpty())
return; // user cancelled the overwrite prompt
Plater* plater = this->plater();
if (plater == nullptr)
return;
plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
ok ? NotificationManager::NotificationLevel::RegularNotificationLevel : NotificationManager::NotificationLevel::ErrorNotificationLevel,
into_u8(message));
}
void GUI_App::open_terminal_dialog() void GUI_App::open_terminal_dialog()
{ {
// Reached from the plugins dialog's webview ("open_terminal" command), i.e. from // Reached from the plugins dialog's webview ("open_terminal" command), i.e. from
@@ -8444,6 +8529,9 @@ void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_
this->plater_->get_current_canvas3D()->force_set_focus(); this->plater_->get_current_canvas3D()->force_set_focus();
return; return;
} }
// Built-in Speed Dial command titles are copied from the catalog at init and don't follow a
// live locale switch; rebuild them in the new language before the GUI (and palette) rebuilds.
m_action_registry.relocalize_builtins();
} }
if (need_recreate_gui) if (need_recreate_gui)
+8
View File
@@ -599,6 +599,11 @@ public:
std::string get_saved_mode_str(); std::string get_saved_mode_str();
std::string get_mode_str(); std::string get_mode_str();
void save_mode(const /*ConfigOptionMode*/int mode) ; void save_mode(const /*ConfigOptionMode*/int mode) ;
// Switch to `mode` from the Speed Dial: a developer-mode override hides the saved mode
// (get_mode returns comDevelop), so clear it first and persist the choice.
void set_mode(ConfigOptionMode mode);
// Turn the developer-mode override on and refresh the UI (used before jumping to a Developer setting).
void enable_developer_mode();
void update_mode(); void update_mode();
void update_internal_development(); void update_internal_development();
void show_ip_address_enter_dialog(wxString title = wxEmptyString); void show_ip_address_enter_dialog(wxString title = wxEmptyString);
@@ -637,6 +642,9 @@ public:
void open_preferences(size_t open_on_tab = 0, const std::string& highlight_option = std::string()); void open_preferences(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
void open_presetbundledialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string()); void open_presetbundledialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
void open_plugins_dialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string()); void open_plugins_dialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
// Dialog-free plugin actions used by the speed dial: they never require the Plugins dialog to be open.
void refresh_plugins();
void install_local_plugin();
void open_terminal_dialog(); void open_terminal_dialog();
void open_speed_dial(); void open_speed_dial();
ActionRegistry& action_registry() { return m_action_registry; } ActionRegistry& action_registry() { return m_action_registry; }
+116 -98
View File
@@ -578,113 +578,131 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty
return sub_menu; return sub_menu;
} }
// Orca: handy models shipped under <resources>/handy_models. Defining everything in one table keeps
// the menu label, the files to load and the per-model behavior in a single place. Labels are wrapped
// in L() so they are picked up for translation. Shared with the command palette.
const std::vector<MenuFactory::HandyModel>& MenuFactory::handy_models()
{
static const std::vector<HandyModel> models = {
{"orca_cube", L("Orca Cube"), {"OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
{"orcasliced_combo", L("OrcaSliced Combo"), {"OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
{"orca_badge", L("Orca Badge"), {"OrcaBadge.3mf"}},
{"orca_tolerance_test", L("Orca Tolerance Test"), {"OrcaToleranceTest.drc"}},
{"3dbenchy", L("3DBenchy"), {"3DBenchy.drc"}},
{"cali_cat", L("Cali Cat"), {"calicat.drc"}},
{"autodesk_fdm_test", L("Autodesk FDM Test"), {"ksr_fdmtest_v4.drc"}},
{"voron_cube", L("Voron Cube"), {"Voron_Design_Cube_v7.drc"}},
{"stanford_bunny", L("Stanford Bunny"), {"Stanford_Bunny.drc"}},
{"orca_string_hell", L("Orca String Hell"), {"Orca_stringhell.drc"}, false, true},
};
return models;
}
void MenuFactory::load_handy_model(std::size_t index)
{
const std::vector<HandyModel>& models = handy_models();
if (index >= models.size())
return;
const HandyModel& model = models[index];
std::vector<boost::filesystem::path> input_files;
input_files.reserve(model.file_names.size());
for (const auto& file_name : model.file_names)
input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name));
Plater* pl = plater();
if (!pl)
return;
pl->load_files(input_files, LoadStrategy::LoadModel);
if (model.arrange_after_import) {
pl->set_prepare_state(Job::PREPARE_STATE_MENU);
pl->arrange();
}
// Suggest to change settings for stringhell
// This serves as mini tutorial for new users
if (model.is_stringhell) {
wxGetApp().CallAfter([=] {
DynamicPrintConfig* m_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
bool is_only_one_wall_top = m_config->opt_bool("only_one_wall_top");
auto min_width_top_surface = m_config->option<ConfigOptionFloatOrPercent>("min_width_top_surface")->value;
if (is_only_one_wall_top && min_width_top_surface > 0) {
wxString msg_text = _L("This model features text embossment on the top surface. For optimal results, it is "
"advisable to set the 'One Wall Threshold (min_width_top_surface)' "
"to 0 for the 'Only One Wall on Top Surfaces' to work best.\n"
"Yes - Change these settings automatically\n"
"No - Do not change these settings for me");
MessageDialog dialog(wxGetApp().plater(), msg_text, _L("Suggestion"), wxICON_WARNING | wxYES | wxNO);
if (dialog.ShowModal() == wxID_YES) {
m_config->set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(0, false));
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty();
wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config();
}
wxGetApp().plater()->update();
}
});
}
}
// Orca: add submenu for adding handy models // Orca: add submenu for adding handy models
wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeType type) { wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeType type) {
auto sub_menu = new wxMenu; auto sub_menu = new wxMenu;
// Orca: handy models shipped under <resources>/handy_models. Defining everything in one table const std::vector<HandyModel>& models = handy_models();
// keeps the menu label, the files to load and the per-model behavior in a single place and for (std::size_t i = 0; i < models.size(); ++i) {
// avoids repeating the label strings (and the value-vs-pointer comparison pitfalls that come append_menu_item(sub_menu, wxID_ANY, _(models[i].label), "",
// with that). Labels are wrapped in L() so they are picked up for translation. [i](wxCommandEvent&) { MenuFactory::load_handy_model(i); }, "", menu);
struct HandyModel
{
const char* label;
std::vector<std::string> file_names;
bool arrange_after_import = false;
bool is_stringhell = false;
};
static const std::vector<HandyModel> handy_models = {
{L("Orca Cube"), {"OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
{L("OrcaSliced Combo"), {"OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
{L("Orca Badge"), {"OrcaBadge.3mf"}},
{L("Orca Tolerance Test"), {"OrcaToleranceTest.drc"}},
{L("3DBenchy"), {"3DBenchy.drc"}},
{L("Cali Cat"), {"calicat.drc"}},
{L("Autodesk FDM Test"), {"ksr_fdmtest_v4.drc"}},
{L("Voron Cube"), {"Voron_Design_Cube_v7.drc"}},
{L("Stanford Bunny"), {"Stanford_Bunny.drc"}},
{L("Orca String Hell"), {"Orca_stringhell.drc"}, false, true},
};
for (const auto& model : handy_models) {
append_menu_item(
sub_menu, wxID_ANY, _(model.label), "",
[&model](wxCommandEvent&) {
std::vector<boost::filesystem::path> input_files;
input_files.reserve(model.file_names.size());
for (const auto& file_name : model.file_names)
input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name));
plater()->load_files(input_files, LoadStrategy::LoadModel);
if (model.arrange_after_import) {
plater()->set_prepare_state(Job::PREPARE_STATE_MENU);
plater()->arrange();
}
// Suggest to change settings for stringhell
// This serves as mini tutorial for new users
if (model.is_stringhell) {
wxGetApp().CallAfter([=] {
DynamicPrintConfig* m_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
bool is_only_one_wall_top = m_config->opt_bool("only_one_wall_top");
auto min_width_top_surface = m_config->option<ConfigOptionFloatOrPercent>("min_width_top_surface")->value;
if (is_only_one_wall_top && min_width_top_surface > 0) {
wxString msg_text = _L("This model features text embossment on the top surface. For optimal results, it is "
"advisable to set the 'One Wall Threshold (min_width_top_surface)' "
"to 0 for the 'Only One Wall on Top Surfaces' to work best.\n"
"Yes - Change these settings automatically\n"
"No - Do not change these settings for me");
MessageDialog dialog(wxGetApp().plater(), msg_text, _L("Suggestion"), wxICON_WARNING | wxYES | wxNO);
if (dialog.ShowModal() == wxID_YES) {
m_config->set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(0, false));
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty();
wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config();
}
wxGetApp().plater()->update();
}
});
}
},
"", menu);
} }
return sub_menu; return sub_menu;
} }
// Create a Text/SVG volume through the matching gizmo. `type == INVALID` means "create a new object".
// Shared by the add menu and the command palette.
static void add_volume_with_gizmo(GLGizmosManager::EType gizmo_type, ModelVolumeType type)
{
Plater* pl = plater();
if (!pl)
return;
const GLCanvas3D* canvas = pl->canvas3D();
if (!canvas)
return;
GLGizmoBase* gizmo_base = canvas->get_gizmos_manager().get_gizmo(gizmo_type);
if (!gizmo_base)
return;
ModelVolumeType volume_type = type;
// no selected object means create new object
if (volume_type == ModelVolumeType::INVALID)
volume_type = ModelVolumeType::MODEL_PART;
auto screen_position = canvas->get_popup_menu_position();
if (gizmo_type == GLGizmosManager::Emboss) {
auto* emboss = dynamic_cast<GLGizmoEmboss*>(gizmo_base);
if (emboss == nullptr)
return;
if (screen_position.has_value())
emboss->create_volume(volume_type, *screen_position);
else
emboss->create_volume(volume_type);
} else if (gizmo_type == GLGizmosManager::Svg) {
auto* svg = dynamic_cast<GLGizmoSVG*>(gizmo_base);
if (svg == nullptr)
return;
if (screen_position.has_value())
svg->create_volume(volume_type, *screen_position);
else
svg->create_volume(volume_type);
}
}
void MenuFactory::add_text_volume(ModelVolumeType type) { add_volume_with_gizmo(GLGizmosManager::Emboss, type); }
void MenuFactory::add_svg_volume(ModelVolumeType type) { add_volume_with_gizmo(GLGizmosManager::Svg, type); }
static void append_menu_itemm_add_(const wxString& name, GLGizmosManager::EType gizmo_type, wxMenu *menu, ModelVolumeType type, bool is_submenu_item) { static void append_menu_itemm_add_(const wxString& name, GLGizmosManager::EType gizmo_type, wxMenu *menu, ModelVolumeType type, bool is_submenu_item) {
auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) { auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) { add_volume_with_gizmo(gizmo_type, type); };
const GLCanvas3D *canvas = plater()->canvas3D();
const GLGizmosManager &mng = canvas->get_gizmos_manager();
GLGizmoBase *gizmo_base = mng.get_gizmo(gizmo_type);
ModelVolumeType volume_type = type;
// no selected object means create new object
if (volume_type == ModelVolumeType::INVALID)
volume_type = ModelVolumeType::MODEL_PART;
auto screen_position = canvas->get_popup_menu_position();
if (gizmo_type == GLGizmosManager::Emboss) {
auto emboss = dynamic_cast<GLGizmoEmboss *>(gizmo_base);
assert(emboss != nullptr);
if (emboss == nullptr) return;
if (screen_position.has_value()) {
emboss->create_volume(volume_type, *screen_position);
} else {
emboss->create_volume(volume_type);
}
} else if (gizmo_type == GLGizmosManager::Svg) {
auto svg = dynamic_cast<GLGizmoSVG *>(gizmo_base);
assert(svg != nullptr);
if (svg == nullptr) return;
if (screen_position.has_value()) {
svg->create_volume(volume_type, *screen_position);
} else {
svg->create_volume(volume_type);
}
}
};
if (type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER || if (type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER ||
type == ModelVolumeType::INVALID // cannot use gizmo without selected object type == ModelVolumeType::INVALID // cannot use gizmo without selected object
+18
View File
@@ -4,6 +4,7 @@
#include <map> #include <map>
#include <vector> #include <vector>
#include <array> #include <array>
#include <cstddef>
#include <wx/bitmap.h> #include <wx/bitmap.h>
@@ -51,6 +52,23 @@ public:
static std::vector<wxBitmap> get_text_volume_bitmaps(); static std::vector<wxBitmap> get_text_volume_bitmaps();
static std::vector<wxBitmap> get_svg_volume_bitmaps(); static std::vector<wxBitmap> get_svg_volume_bitmaps();
// Orca: handy models shipped under <resources>/handy_models. The menu and the command palette
// share this table so the model list and its per-model behavior live in one place.
struct HandyModel
{
const char* key;
const char* label;
std::vector<std::string> file_names;
bool arrange_after_import = false;
bool is_stringhell = false;
};
static const std::vector<HandyModel>& handy_models();
static void load_handy_model(std::size_t index);
// Add a Text/SVG volume through the Emboss/SVG gizmo. Shared by the add menu and the palette.
static void add_text_volume(ModelVolumeType type);
static void add_svg_volume(ModelVolumeType type);
MenuFactory(); MenuFactory();
~MenuFactory() = default; ~MenuFactory() = default;
+2
View File
@@ -478,6 +478,8 @@ int get_dpi_for_window(const wxWindow *window);
#ifdef __WXOSX__ #ifdef __WXOSX__
void dataview_remove_insets(wxDataViewCtrl* dv); void dataview_remove_insets(wxDataViewCtrl* dv);
void staticbox_remove_margin(wxStaticBox* sb); void staticbox_remove_margin(wxStaticBox* sb);
// Clip a top-level window (and its webview) to a rounded rect with a native layer.
void set_window_corner_radius(wxWindow* win, int radius);
#endif #endif
#ifdef __WXGTK__ #ifdef __WXGTK__
+17
View File
@@ -1,5 +1,6 @@
#include <unistd.h> #include <unistd.h>
#include <sys/sysctl.h> #include <sys/sysctl.h>
#import <Cocoa/Cocoa.h>
#import <wx/osx/cocoa/dataview.h> #import <wx/osx/cocoa/dataview.h>
#import "GUI_Utils.hpp" #import "GUI_Utils.hpp"
@@ -21,6 +22,22 @@ void staticbox_remove_margin(wxStaticBox* sb) {
[nativeBox setBorderWidth:0]; [nativeBox setBorderWidth:0];
} }
// wxOSX SetShape only clears the window background; it cannot clip to a region. Clipping the
// window's view layer to a rounded rect is what actually rounds the opaque webview inside.
void set_window_corner_radius(wxWindow* win, int radius) {
if (!win)
return;
NSView* view = (NSView*)win->GetHandle();
if (!view)
return;
NSWindow* window = [view window];
[window setOpaque:NO];
[window setBackgroundColor:[NSColor clearColor]];
[view setWantsLayer:YES];
[[view layer] setCornerRadius:radius];
[[view layer] setMasksToBounds:YES];
}
bool is_debugger_present() bool is_debugger_present()
// Returns true if the current process is being debugged (either // Returns true if the current process is being debugged (either
// running under the debugger or has a debugger attached post facto). // running under the debugger or has a debugger attached post facto).
+5 -6
View File
@@ -15,7 +15,6 @@
#endif #endif
#include <imgui/imgui_internal.h> #include <imgui/imgui_internal.h>
#define L(s) Slic3r::GUI::I18N::translate((s)).c_str()
#define UL(s) Slic3r::GUI::I18N::translate_utf8((s)).c_str() #define UL(s) Slic3r::GUI::I18N::translate_utf8((s)).c_str()
namespace Slic3r { namespace Slic3r {
@@ -374,15 +373,15 @@ void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit)
}; };
if (m_sp.is_pocket && has_sel) { if (m_sp.is_pocket && has_sel) {
if (btn(L("Pocket (Cut)"), ok)) apply_pocket(); if (btn(_u8L("Pocket (Cut)").c_str(), ok)) apply_pocket();
} else if (is_revolve) { } else if (is_revolve) {
if (btn(L("Revolve"), ok)) apply_revolve(); if (btn(_u8L("Revolve").c_str(), ok)) apply_revolve();
} else { } else {
if (btn(L("Extrude"), ok)) apply_extrude(); if (btn(_u8L("Extrude").c_str(), ok)) apply_extrude();
} }
if (ImGui::Button(L("Clear All"), {-1,0})) clear_all(); if (ImGui::Button(_u8L("Clear All").c_str(), {-1,0})) clear_all();
if (ImGui::Button(L("Close"), {-1,0})) m_parent.reset_all_gizmos(); if (ImGui::Button(_u8L("Close").c_str(), {-1,0})) m_parent.reset_all_gizmos();
GizmoImguiEnd(); GizmoImguiEnd();
ImGuiWrapper::pop_toolbar_style(); ImGuiWrapper::pop_toolbar_style();
+6 -3
View File
@@ -234,8 +234,10 @@ bool GLGizmosManager::init()
#ifdef SLIC3R_CAD #ifdef SLIC3R_CAD
// Registered last: Primitive and Sketch are the final entries before Undefined, so // Registered last: Primitive and Sketch are the final entries before Undefined, so
// omitting them leaves every preceding m_gizmos index (indexed by EType) untouched. // omitting them leaves every preceding m_gizmos index (indexed by EType) untouched.
m_gizmos.emplace_back(new GLGizmoPrimitive(m_parent, m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg", static_cast<unsigned int>(Primitive))); if (wxGetApp().is_enable_cad_feature()) {
m_gizmos.emplace_back(new GLGizmoSketch(m_parent, m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg", static_cast<unsigned int>(Sketch))); m_gizmos.emplace_back(new GLGizmoPrimitive(m_parent, m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg", static_cast<unsigned int>(Primitive)));
m_gizmos.emplace_back(new GLGizmoSketch(m_parent, m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg", static_cast<unsigned int>(Sketch)));
}
#endif #endif
//m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", sprite_id++)); //m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", sprite_id++));
//m_gizmos.emplace_back(new GLGizmoFaceDetector(m_parent, "face recognition.svg", sprite_id++)); //m_gizmos.emplace_back(new GLGizmoFaceDetector(m_parent, "face recognition.svg", sprite_id++));
@@ -1344,7 +1346,8 @@ GLGizmoBase* GLGizmosManager::get_current() const
GLGizmoBase* GLGizmosManager::get_gizmo(GLGizmosManager::EType type) const GLGizmoBase* GLGizmosManager::get_gizmo(GLGizmosManager::EType type) const
{ {
return ((type == Undefined) || m_gizmos.empty()) ? nullptr : m_gizmos[type].get(); // m_gizmos ends before the enum does when the CAD gizmos are not registered.
return type < m_gizmos.size() ? m_gizmos[type].get() : nullptr;
} }
GLGizmosManager::EType GLGizmosManager::get_gizmo_from_name(const std::string& gizmo_name) const GLGizmosManager::EType GLGizmosManager::get_gizmo_from_name(const std::string& gizmo_name) const
+3 -2
View File
@@ -198,6 +198,9 @@ void KBShortcutsDialog::fill_shortcuts()
// Switch table page // Switch table page
{ ctrl + L("Tab"), L("Switch table page")}, { ctrl + L("Tab"), L("Switch table page")},
// Open speed dial
{ L_CONTEXT("Space", "Keyboard Shortcut"), L("Open speed dial") },
{ alt + "1..9,0", L("Run a Speed Dial favourite (while the Speed Dial is open)") },
//DEL //DEL
#ifdef __APPLE__ #ifdef __APPLE__
{"fn+⌫", L("Delete Selected")}, {"fn+⌫", L("Delete Selected")},
@@ -268,8 +271,6 @@ void KBShortcutsDialog::fill_shortcuts()
{ "O", L("Zoom out") }, { "O", L("Zoom out") },
{ "V", L("Toggle printable for object/part") }, { "V", L("Toggle printable for object/part") },
{ L_CONTEXT("Tab", "Keyboard Shortcut"), L("Switch between Prepare/Preview") }, { L_CONTEXT("Tab", "Keyboard Shortcut"), L("Switch between Prepare/Preview") },
{ L_CONTEXT("Space", "Keyboard Shortcut"), L("Open actions speed dial") },
}; };
m_full_shortcuts.push_back({ { _L("Plater"), "" }, plater_shortcuts }); m_full_shortcuts.push_back({ { _L("Plater"), "" }, plater_shortcuts });
+144 -93
View File
@@ -1,6 +1,7 @@
#include "MainFrame.hpp" #include "MainFrame.hpp"
#include <wx/panel.h> #include <wx/panel.h>
#include <wx/textentry.h>
#include <wx/notebook.h> #include <wx/notebook.h>
#include <wx/listbook.h> #include <wx/listbook.h>
#include <wx/simplebook.h> #include <wx/simplebook.h>
@@ -46,7 +47,9 @@
// BBS // BBS
#include "PartPlate.hpp" #include "PartPlate.hpp"
#include "Preferences.hpp" #include "Preferences.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/ProgressDialog.hpp" #include "Widgets/ProgressDialog.hpp"
#include "Widgets/StaticBox.hpp"
#include "BindDialog.hpp" #include "BindDialog.hpp"
#include "../Utils/MacDarkMode.hpp" #include "../Utils/MacDarkMode.hpp"
#include "../Utils/NetworkAgentFactory.hpp" #include "../Utils/NetworkAgentFactory.hpp"
@@ -107,6 +110,31 @@ enum class ERescaleTarget
SettingsDialog SettingsDialog
}; };
namespace {
// Space opens the speed dial, but it is the activation key for buttons, checkboxes and other
// controls. CHAR_HOOK runs before the focused child, so only take Space when the focused window has
// no keyboard-activation meaning of its own. Canvases (GLCanvas3D) and panels are not controls and
// fall through to "open"; the Notebook itself does too, so Space still opens the dial on any page.
bool focus_keeps_space(wxWindow* focus)
{
if (!focus)
return false;
if (dynamic_cast<wxTextEntryBase*>(focus))
return true; // typing a space into a text field
if (dynamic_cast<wxWebView*>(focus))
return true; // web content scrolls and hosts its own text fields
if (dynamic_cast<::Button*>(focus))
return true; // custom button: Space clicks it (it is a wxWindow, not a wxControl)
if (dynamic_cast<StaticBox*>(focus))
return true; // custom composites (ComboBox, SpinInput, ...) activate with Space and are wxWindow
if (dynamic_cast<wxControl*>(focus) && !dynamic_cast<Notebook*>(focus))
return true; // stock button/checkbox/choice/list/etc. keep Space
return false;
}
} // namespace
#ifdef __WXGTK__ #ifdef __WXGTK__
// A thin transparent panel placed at a window edge to handle resize. // A thin transparent panel placed at a window edge to handle resize.
// Works regardless of underlying content (GLCanvas3D, wxWebView, etc.) // Works regardless of underlying content (GLCanvas3D, wxWebView, etc.)
@@ -702,6 +730,22 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
} }
return;} return;}
#endif #endif
// Orca: open the speed dial from any page with a bare Space. Only when no modifier is held (so
// editing shortcuts like Ctrl+Shift+Space in the canvas still reach it) and the focused window
// doesn't use Space to activate itself (buttons, checkboxes, list/choice controls, text fields),
// so a bare Space there still clicks/toggles instead of being hijacked. Gated by a preference
// (default on) so users can hand Space back to the focused control entirely.
if (wxGetApp().app_config->get_bool("enable_speed_dial") && !evt.CmdDown() && !evt.ShiftDown() &&
!evt.AltDown() && evt.GetKeyCode() == WXK_SPACE) {
if (focus_keeps_space(wxWindow::FindFocus())) {
evt.Skip(); // let the focused control keep Space
return;
}
// Defer out of the native key-event stack: open_speed_dial() may create a WebView and
// run script, the same window work the codebase avoids doing on native callbacks.
this->CallAfter([] { wxGetApp().open_speed_dial(); });
return;
}
if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; } if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; }
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') { if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') {
m_plater->apply_background_progress(); m_plater->apply_background_progress();
@@ -3344,6 +3388,11 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().open_preferences(); wxGetApp().open_preferences();
}, },
"", nullptr, []() { return true; }, this, 1); "", nullptr, []() { return true; }, this, 1);
parent_menu->AppendSeparator();
append_menu_item(
parent_menu, wxID_ANY, _L("Open speed dial...") + sep + "Space", "",
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
//parent_menu->Insert(1, preference_item); //parent_menu->Insert(1, preference_item);
#endif #endif
// Help menu // Help menu
@@ -3368,7 +3417,13 @@ void MainFrame::init_menubar_as_editor()
auto top_menu = m_topbar->GetTopMenu(); auto top_menu = m_topbar->GetTopMenu();
top_menu->AppendSeparator(); top_menu->AppendSeparator();
append_menu_item( append_menu_item(
top_menu, wxID_ANY, _L("Open speed dial...") + "\t" + "Space", "",
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
top_menu->AppendSeparator();
append_menu_item(
top_menu, wxID_ANY, _L("Preset Bundle") + "\t", "", top_menu, wxID_ANY, _L("Preset Bundle") + "\t", "",
[this](wxCommandEvent &) { [this](wxCommandEvent &) {
// Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit // Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit
@@ -3414,88 +3469,51 @@ void MainFrame::init_menubar_as_editor()
// Temperature // Temperature
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Temperature"), _L("Temperature Calibration"), append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Temperature"), _L("Temperature Calibration"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr,
if (!m_temp_calib_dlg)
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_temp_calib_dlg->ShowModal();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// Max Volumetric Speed // Max Volumetric Speed
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Max flowrate"), _L("Max flowrate"), append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Max flowrate"), _L("Max flowrate"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr,
if (!m_vol_test_dlg)
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_vol_test_dlg->ShowModal();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// Pressure Advance // Pressure Advance
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Pressure advance"), _L("Pressure advance"), append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Pressure advance"), _L("Pressure advance"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr,
if (!m_pa_calib_dlg)
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_pa_calib_dlg->ShowModal();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// Flow rate (Wizard Dialog) // Flow rate (Wizard Dialog)
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"), append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr,
if (!m_plater) return;
if (!m_flow_rate_calib_dlg)
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*)this, wxID_ANY, m_plater);
m_flow_rate_calib_dlg->ShowModal();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// Retraction // Retraction
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Retraction"), _L("Retraction"), append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Retraction"), _L("Retraction"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr,
if (!m_retraction_calib_dlg)
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_retraction_calib_dlg->ShowModal();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// Cornering // Cornering
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Cornering"), _L("Cornering calibration"), append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Cornering"), _L("Cornering calibration"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr,
auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// Input Shaping (with submenu) // Input Shaping (with submenu)
auto input_shaping_menu = new wxMenu(); auto input_shaping_menu = new wxMenu();
append_menu_item( append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"), input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); },
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
"", nullptr, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
append_menu_item( append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"), input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); },
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
"", nullptr, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
m_topbar->GetCalibMenu()->AppendSubMenu(input_shaping_menu, _L("Input Shaping")); m_topbar->GetCalibMenu()->AppendSubMenu(input_shaping_menu, _L("Input Shaping"));
// VFA // VFA
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("VFA"), _L("VFA"), append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("VFA"), _L("VFA"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr,
if (!m_vfa_test_dlg)
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_vfa_test_dlg->ShowModal();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// help // help
@@ -3506,6 +3524,10 @@ void MainFrame::init_menubar_as_editor()
#else #else
// On Mac, the Apple menu ignores non-standard custom items, so add Preset Bundle to the File menu // On Mac, the Apple menu ignores non-standard custom items, so add Preset Bundle to the File menu
fileMenu->AppendSeparator(); fileMenu->AppendSeparator();
append_menu_item(
fileMenu, wxID_ANY, _L("Open speed dial...") + sep + "Space", "",
[](wxCommandEvent&) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
append_menu_item( append_menu_item(
fileMenu, wxID_ANY, _L("Preset Bundle"), "", fileMenu, wxID_ANY, _L("Preset Bundle"), "",
[this](wxCommandEvent&) { [this](wxCommandEvent&) {
@@ -3552,89 +3574,52 @@ void MainFrame::init_menubar_as_editor()
// Temperature // Temperature
append_menu_item(calib_menu, wxID_ANY, _L("Temperature"), _L("Temperature"), append_menu_item(calib_menu, wxID_ANY, _L("Temperature"), _L("Temperature"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr,
if (!m_temp_calib_dlg)
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_temp_calib_dlg->ShowModal();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// Max Volumetric Speed // Max Volumetric Speed
append_menu_item(calib_menu, wxID_ANY, _L("Max flowrate"), _L("Max flowrate"), append_menu_item(calib_menu, wxID_ANY, _L("Max flowrate"), _L("Max flowrate"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr,
if (!m_vol_test_dlg)
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_vol_test_dlg->ShowModal();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// Pressure Advance // Pressure Advance
append_menu_item(calib_menu, wxID_ANY, _L("Pressure advance"), _L("Pressure advance"), append_menu_item(calib_menu, wxID_ANY, _L("Pressure advance"), _L("Pressure advance"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr,
if (!m_pa_calib_dlg)
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_pa_calib_dlg->ShowModal();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// Flowrate (with submenu) // Flowrate (with submenu)
// ORCA: Flow rate (Wizard Dialog) // ORCA: Flow rate (Wizard Dialog)
append_menu_item(calib_menu, wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"), append_menu_item(calib_menu, wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr,
if (!m_plater) return;
if (!m_flow_rate_calib_dlg)
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*)this, wxID_ANY, m_plater);
m_flow_rate_calib_dlg->ShowModal();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// Retraction // Retraction
append_menu_item(calib_menu, wxID_ANY, _L("Retraction"), _L("Retraction"), append_menu_item(calib_menu, wxID_ANY, _L("Retraction"), _L("Retraction"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr,
if (!m_retraction_calib_dlg)
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_retraction_calib_dlg->ShowModal();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// Cornering // Cornering
append_menu_item(calib_menu, wxID_ANY, _L("Cornering"), _L("Cornering calibration"), append_menu_item(calib_menu, wxID_ANY, _L("Cornering"), _L("Cornering calibration"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr,
auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// Input Shaping (with submenu) // Input Shaping (with submenu)
auto input_shaping_menu = new wxMenu(); auto input_shaping_menu = new wxMenu();
append_menu_item( append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"), input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); },
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
"", nullptr, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
append_menu_item( append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"), input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); },
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
"", nullptr, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
calib_menu->AppendSubMenu(input_shaping_menu, _L("Input Shaping")); calib_menu->AppendSubMenu(input_shaping_menu, _L("Input Shaping"));
// VFA // VFA
append_menu_item(calib_menu, wxID_ANY, _L("VFA"), _L("VFA"), append_menu_item(calib_menu, wxID_ANY, _L("VFA"), _L("VFA"),
[this](wxCommandEvent&) { [this](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr,
if (!m_vfa_test_dlg)
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_vfa_test_dlg->ShowModal();
}, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this); [this]() {return m_plater->is_view3D_shown();; }, this);
// help // help
append_menu_item(calib_menu, wxID_ANY, _L("Calibration Guide"), _L("Calibration Guide"), append_menu_item(calib_menu, wxID_ANY, _L("Calibration Guide"), _L("Calibration Guide"),
@@ -4408,6 +4393,72 @@ void MainFrame::technology_changed()
m_menubar->SetMenuLabel(id, pt == ptSLA ? _omitL("Material Settings") : _L("Filament settings")); m_menubar->SetMenuLabel(id, pt == ptSLA ? _omitL("Material Settings") : _L("Filament settings"));
} }
// Opens the calibration wizard for `calib_kind`. Single source of truth for the wizard lifecycle:
// the Calibration menu handlers and the Speed Dial native commands both call it. Most wizards are
// cached members reused across launches; cornering/input-shaping build a fresh transient dialog.
// Call while the Prepare (3D) panel is shown.
void MainFrame::run_calibration(CalibKind calib_kind)
{
switch (calib_kind) {
case CalibKind::Temperature: {
if (!m_temp_calib_dlg)
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_temp_calib_dlg->ShowModal();
break;
}
case CalibKind::MaxVolumetric: {
if (!m_vol_test_dlg)
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_vol_test_dlg->ShowModal();
break;
}
case CalibKind::PressureAdvance: {
if (!m_pa_calib_dlg)
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_pa_calib_dlg->ShowModal();
break;
}
case CalibKind::FlowRatio: {
if (!m_plater)
break;
if (!m_flow_rate_calib_dlg)
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*) this, wxID_ANY, m_plater);
m_flow_rate_calib_dlg->ShowModal();
break;
}
case CalibKind::Retraction: {
if (!m_retraction_calib_dlg)
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_retraction_calib_dlg->ShowModal();
break;
}
case CalibKind::Cornering: {
auto dlg = new Cornering_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
break;
}
case CalibKind::InputShapingFreq: {
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
break;
}
case CalibKind::InputShapingDamp: {
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
break;
}
case CalibKind::VFA: {
if (!m_vfa_test_dlg)
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_vfa_test_dlg->ShowModal();
break;
}
}
}
// //
// Called after the Preferences dialog is closed and the program settings are saved. // Called after the Preferences dialog is closed and the program settings are saved.
+19
View File
@@ -113,6 +113,20 @@ protected:
void on_dpi_changed(const wxRect& suggested_rect) override; void on_dpi_changed(const wxRect& suggested_rect) override;
}; };
// Calibration wizard identity, shared by MainFrame::run_calibration and the Speed Dial command runners.
enum class CalibKind : int
{
Temperature,
MaxVolumetric,
PressureAdvance,
FlowRatio,
Retraction,
Cornering,
InputShapingFreq,
InputShapingDamp,
VFA
};
class MainFrame : public DPIFrame class MainFrame : public DPIFrame
{ {
#ifdef __APPLE__ #ifdef __APPLE__
@@ -360,6 +374,11 @@ public:
void technology_changed(); void technology_changed();
// Opens the calibration wizard for `kind`. Single source of truth for the wizard lifecycle:
// the Calibration menu handlers and the Speed Dial native commands both call this. Most wizards
// are cached members; cornering/input-shaping are transient. Call while the Prepare (3D) panel
// is shown (menu items are gated on is_view3D_shown; the speed dial ensures it first).
void run_calibration(CalibKind calib_kind);
//BBS //BBS
void load_url(wxString url); void load_url(wxString url);
+668
View File
@@ -0,0 +1,668 @@
#include "NativeCommands.hpp"
#include "calib_dlg.hpp"
#include "Camera.hpp"
#include "DailyTips.hpp"
#include "GCodeViewer.hpp"
#include "GLCanvas3D.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "GUI_Factories.hpp"
#include "GUI_ObjectList.hpp"
#include "I18N.hpp"
#include "IMSlider.hpp"
#include "MainFrame.hpp"
#include "NetworkTestDialog.hpp"
#include "Plater.hpp"
#include "PluginsDialog.hpp"
#include "PlateSettingsDialog.hpp"
#include "DeviceCore/DevManager.h"
#include <libslic3r/Model.hpp>
#include <libslic3r/Utils.hpp>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <exception>
#include <memory>
#include <string>
#include <tuple>
#include <utility>
#include <wx/utils.h>
namespace Slic3r { namespace GUI {
namespace {
// Plate ops are an FFF feature: SLA has a single plate and no plate UI, gcode-only mode has no
// editable project - so gate every plate op on FFF + the normal editor.
bool is_fff_plater(Plater* plater) { return plater && plater->printer_technology() == ptFFF && !plater->only_gcode_mode(); }
AppActionRunResult plate_unavailable() { return {AppActionRunResult::Level::Info, _L("Plates are a filament (FFF) feature.")}; }
// Switch to the Prepare (3D) panel so object/calibration ops have a live canvas + selection, and
// the notebook page label matches. A no-op when the 3D panel is already shown.
void ensure_3d_view(Plater* plater)
{
if (plater && !plater->is_view3D_shown()) {
plater->select_view_3D("3D");
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->select_tab(TAB_ID_PREPARE);
}
}
// Object op guard + run: object ops read the Prepare canvas selection, so ensure that view first so
// a launch from another tab doesn't report a spuriously empty selection.
AppActionRunResult object_op(Plater* plater, bool (*ok)(Plater*), void (*op)(Plater*))
{
if (!plater)
return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")};
ensure_3d_view(plater);
if (!ok(plater))
return {AppActionRunResult::Level::Info, _L("Select an object first.")};
op(plater);
return {AppActionRunResult::Level::Success};
}
// Jump the preview to a layer selected by a 0-100 percent of the layer range. The caller has already
// switched to Preview (which may request a slice); if a slicer result is present the slider is
// repositioned immediately, otherwise the jump is a no-op until the user re-slices.
void go_to_layer(Plater* plater, const std::string& param)
{
if (!plater)
return;
double pct = 50.0;
try {
pct = std::stod(param);
} catch (const std::exception&) {}
pct = std::clamp(pct, 0.0, 100.0);
GLCanvas3D* canvas = plater->get_current_canvas3D();
if (!canvas)
return;
GCodeViewer& viewer = canvas->get_gcode_viewer();
IMSlider* layers = viewer.get_layers_slider();
IMSlider* moves = viewer.get_moves_slider();
if (!layers || layers->GetMaxValue() <= 0)
return;
const double max = double(layers->GetMaxValue());
const int target = int(std::lround(pct / 100.0 * max));
layers->SetHigherValue(target);
if (layers->is_one_layer())
layers->SetLowerValue(target);
layers->set_as_dirty();
if (moves) {
moves->SetHigherValue(moves->GetMaxValue());
moves->set_as_dirty();
}
}
// Select a named camera view. Plater::select_view dispatches to the current panel.
AppActionRunResult view_command(Plater* plater, const std::string& dir)
{
if (plater)
plater->select_view(dir);
return {AppActionRunResult::Level::Success};
}
// Calibration wizards. Routes through MainFrame::run_calibration, the same entry point as the
// Calibration menu (which caches most of the wizard dialogs).
AppActionRunResult calib_command(CalibKind kind)
{
MainFrame* mf = wxGetApp().mainframe;
if (!mf)
return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")};
ensure_3d_view(wxGetApp().plater());
mf->run_calibration(kind);
return {AppActionRunResult::Level::Success};
}
constexpr const char* kCommandPrefix = "orca_command";
// Thin AppAction wrapper for one catalog entry: identity and presentation come from the catalog,
// run() routes back to it. The id is keyed by the stable catalog key (not the display title), so a
// rename or a UI-language switch never re-keys the action.
struct CommandAction : AppAction
{
std::string command_key;
AppActionRunResult run(const std::string& param) const override { return NativeCommands::run(command_key, param); }
explicit CommandAction(const NativeCommand& c)
: AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName)
, command_key(c.key)
{
this->kind = AppActionKind::Command;
this->group = c.group;
this->input = c.input;
this->icon = c.icon;
}
};
std::vector<NativeCommand> build_command_catalog()
{
std::vector<NativeCommand> out;
auto add = [&](std::string key, std::string title, std::string group, std::function<AppActionRunResult(const std::string&)> runner,
std::string input = {}, std::string icon = {}) {
out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(icon), std::move(runner)});
};
// Presentation-first overload: keeps the tile icon next to the title/group it belongs to.
auto add_with_icon = [&](std::string key, std::string title, std::string group, std::string icon,
std::function<AppActionRunResult(const std::string&)> runner, std::string input = {}) {
add(std::move(key), std::move(title), std::move(group), std::move(runner), std::move(input), std::move(icon));
};
// ---- Slice & Export ----
add_with_icon("slice_and_preview", _u8L("Slice and Preview"), _u8L("Slice & Export"), "media_play", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (plater) {
plater->reslice();
plater->select_view_3D("Preview", false);
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->select_tab(TAB_ID_PREVIEW);
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon(
"go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), "height_range_layer",
[](const std::string& param) {
Plater* plater = wxGetApp().plater();
if (plater) {
plater->select_view_3D("Preview", false);
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->select_tab(TAB_ID_PREVIEW);
go_to_layer(plater, param);
}
return AppActionRunResult{AppActionRunResult::Level::Success};
},
"percent");
// "go_to_tab" is two-phase: the palette collects the tab after activating it, then hands the tab
// id back as `param` (same contract as go_to_layer's percent).
add(
"go_to_tab", _u8L("Go to tab..."), _u8L("Commands"),
[](const std::string& param) {
if (MainFrame* mf = wxGetApp().mainframe; mf && !param.empty())
mf->select_tab(from_u8(param));
return AppActionRunResult{AppActionRunResult::Level::Success};
},
"tab");
add_with_icon("load_project", _u8L("Load Project"), _u8L("Commands"), "menu_open", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->load_project();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("save_project", _u8L("Save Project"), _u8L("Commands"), "menu_save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->save_project(false);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("save_project_as", _u8L("Save Project As"), _u8L("Commands"), "menu_save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->save_project(true);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("open_preferences", _u8L("Preferences"), _u8L("Commands"), "cog", [](const std::string&) {
wxGetApp().open_preferences();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Mode ----
add_with_icon("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), "advanced", [](const std::string&) {
wxGetApp().set_mode(comSimple);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), "advanced", [](const std::string&) {
wxGetApp().set_mode(comAdvanced);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), "advanced", [](const std::string&) {
wxGetApp().set_mode(comExpert);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// Mirrors Preferences > Developer > Developer mode: flip the flag, persist, refresh the UI.
add_with_icon("toggle_developer_mode", _u8L("Toggle Developer Mode"), _u8L("Mode"), "advanced", [](const std::string&) {
GUI_App& app = wxGetApp();
const bool on = !app.app_config->get_bool("developer_mode");
app.app_config->set_bool("developer_mode", on);
app.app_config->save();
app.update_mode();
return AppActionRunResult{AppActionRunResult::Level::Success, on ? _L("Developer mode enabled.") : _L("Developer mode disabled.")};
});
// ---- Export pipeline ----
add_with_icon("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), "custom-gcode_gcode", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_gcode(false);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), "save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_stl();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export"), "menu_save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_core_3mf();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), "save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_gcode_3mf(false);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), "save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_gcode_3mf(true);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Calibration ----
// The tab-strip calib_sf glyph is drawn white for the dark tab bar and vanishes on the palette's
// light tile, so each wizard borrows the matching settings-group icon instead (gray + accent green).
add_with_icon("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), "param_temperature",
[](const std::string&) { return calib_command(CalibKind::Temperature); });
add_with_icon("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), "param_volumetric_speed",
[](const std::string&) { return calib_command(CalibKind::MaxVolumetric); });
add_with_icon("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance",
[](const std::string&) { return calib_command(CalibKind::PressureAdvance); });
add_with_icon("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance",
[](const std::string&) { return calib_command(CalibKind::FlowRatio); });
add_with_icon("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), "param_retraction",
[](const std::string&) { return calib_command(CalibKind::Retraction); });
add_with_icon("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), "param_precision",
[](const std::string&) { return calib_command(CalibKind::Cornering); });
add_with_icon("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), "param_resonance_avoidance",
[](const std::string&) { return calib_command(CalibKind::InputShapingFreq); });
add_with_icon("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), "param_resonance_avoidance",
[](const std::string&) { return calib_command(CalibKind::InputShapingDamp); });
add_with_icon("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), "param_speed", [](const std::string&) { return calib_command(CalibKind::VFA); });
// ---- View ----
// Titles are built with _u8L here (not via a variable) so xgettext can extract them.
for (auto [key, dir, title] :
std::initializer_list<std::tuple<const char*, const char*, std::string>>{{"view_top", "top", _u8L("View: Top")},
{"view_bottom", "bottom", _u8L("View: Bottom")},
{"view_front", "front", _u8L("View: Front")},
{"view_rear", "rear", _u8L("View: Rear")},
{"view_left", "left", _u8L("View: Left")},
{"view_right", "right", _u8L("View: Right")},
{"view_iso", "iso", _u8L("View: Isometric")}}) {
std::string k = key, d = dir;
add(k, title, _u8L("View"),
[d](const std::string&) { return view_command(wxGetApp().plater(), d); });
}
add("view_default", _u8L("View: Default"), _u8L("View"), [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (plater) {
plater->select_view("plate");
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
canvas->zoom_to_bed();
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("view_fit_bed", _u8L("Fit Bed to View"), _u8L("View"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
canvas->zoom_to_bed();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("view_toggle_perspective", _u8L("Toggle Perspective"), _u8L("View"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->get_camera().select_next_type();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View"), "toolbar_reset", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->reset_window_layout();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Object ----
add_with_icon("obj_delete", _u8L("Delete Selected"), _u8L("Object"), "delete", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); });
});
add_with_icon("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), "delete", [](const std::string&) {
return object_op(
wxGetApp().plater(), [](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); });
});
add_with_icon("obj_mirror_x", _u8L("Mirror X"), _u8L("Object"), "menu_mirror_x", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::X); });
});
add_with_icon("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object"), "menu_mirror_y", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Y); });
});
add_with_icon("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object"), "menu_mirror_z", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Z); });
});
add_with_icon("obj_split_objects", _u8L("Split to Objects"), _u8L("Object"), "menu_split_objects", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_objects(); }, [](Plater* p) { p->split_object(true); });
});
add_with_icon("obj_split_parts", _u8L("Split to Parts"), _u8L("Object"), "menu_split_parts", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_volumes(); }, [](Plater* p) { p->split_volume(); });
});
add("obj_center", _u8L("Center Selected on Plate"), _u8L("Object"), [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); });
});
add_with_icon("obj_drop", _u8L("Drop to Bed"), _u8L("Object"), "toolbar_flatten", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); });
});
add("obj_fit_volume", _u8L("Scale to Fit Print Volume"), _u8L("Object"), [](const std::string&) {
return object_op(
wxGetApp().plater(), [](Plater* p) { return p->can_scale_to_print_volume(); },
[](Plater* p) { p->scale_selection_to_fit_print_volume(); });
});
add_with_icon("obj_instances_up", _u8L("Increase Instances"), _u8L("Object"), "instance_add", [](const std::string&) {
return object_op(
wxGetApp().plater(), [](Plater* p) { return p->can_increase_instances(); }, [](Plater* p) { p->increase_instances(); });
});
add_with_icon("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"), "instance_remove", [](const std::string&) {
return object_op(
wxGetApp().plater(), [](Plater* p) { return p->can_decrease_instances(); }, [](Plater* p) { p->decrease_instances(); });
});
add_with_icon("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"), "toolbar_arrange", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->arrange(); });
});
add_with_icon("obj_orient", _u8L("Auto-Orient"), _u8L("Object"), "toolbar_orient", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); });
});
// ---- Add Primitive ---- (the Add > Add Primitive submenu; creates a new object)
auto add_primitive = [&](std::string key, std::string title, std::string icon, const char* type_name) {
add_with_icon(std::move(key), std::move(title), _u8L("Add Primitive"), std::move(icon), [type_name](const std::string&) {
Plater* plater = wxGetApp().plater();
if (plater) {
ensure_3d_view(plater);
if (ObjectList* list = wxGetApp().obj_list())
list->load_generic_subobject(type_name, ModelVolumeType::INVALID);
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
};
add_primitive("add_primitive_cube", _u8L("Cube"), "menu_obj_cube", "Cube");
add_primitive("add_primitive_cylinder", _u8L("Cylinder"), "menu_obj_cylinder", "Cylinder");
add_primitive("add_primitive_sphere", _u8L("Sphere"), "menu_obj_sphere", "Sphere");
add_primitive("add_primitive_cone", _u8L("Cone"), "menu_obj_cone", "Cone");
add_primitive("add_primitive_disc", _u8L("Disc"), "menu_obj_disc", "Disc");
add_primitive("add_primitive_torus", _u8L("Torus"), "menu_obj_torus", "Torus");
add_with_icon("add_primitive_text", _u8L("Text"), _u8L("Add Primitive"), "menu_obj_text", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (plater) {
ensure_3d_view(plater);
if (GLCanvas3D* canvas = plater->canvas3D())
canvas->clear_popup_menu_position();
MenuFactory::add_text_volume(ModelVolumeType::INVALID);
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("add_primitive_svg", _u8L("SVG"), _u8L("Add Primitive"), "menu_obj_svg", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (plater) {
ensure_3d_view(plater);
if (GLCanvas3D* canvas = plater->canvas3D())
canvas->clear_popup_menu_position();
MenuFactory::add_svg_volume(ModelVolumeType::INVALID);
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Add Handy models ---- (the Add > Add Handy models submenu)
const std::vector<MenuFactory::HandyModel>& handy = MenuFactory::handy_models();
for (std::size_t i = 0; i < handy.size(); ++i) {
add("add_handy_" + std::string(handy[i].key), Slic3r::GUI::I18N::translate_utf8(handy[i].label), _u8L("Add Handy models"),
[i](const std::string&) {
if (Plater* plater = wxGetApp().plater())
ensure_3d_view(plater);
MenuFactory::load_handy_model(i);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
}
// ---- Plate ----
add_with_icon("plate_add", _u8L("Add Plate"), _u8L("Plate"), "toolbar_add_plate", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
if (!plater->can_add_plate())
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot add another plate (maximum reached).")};
plater->add_plate();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate"), "menu_copy", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
if (!plater->can_add_plate())
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot duplicate a plate (maximum reached).")};
plater->duplicate_plate();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), "delete", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
if (!plater->can_delete_plate())
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot delete the only plate.")};
plater->delete_plate();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("plate_rename", _u8L("Rename Plate"), _u8L("Plate"), "plate_name_edit", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
PartPlate* curr = plater->get_partplate_list().get_curr_plate();
PlateNameEditDialog dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, _L("Edit Plate Name"));
dlg.set_plate_name(from_u8(curr->get_plate_name()));
if (dlg.ShowModal() == wxID_YES)
curr->set_plate_name(dlg.get_plate_name().ToUTF8().data());
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate"), "lock_normal", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
PartPlateList& plates = plater->get_partplate_list();
const int index = plates.get_curr_plate_index();
plater->take_snapshot("lock partplate");
plates.lock_plate(index, !plates.is_locked(index));
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("plate_goto", _u8L("Go to Plate"), _u8L("Plate"), "go_next_plate", [](const std::string& param) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
PartPlateList& plates = plater->get_partplate_list();
const int count = plates.get_plate_count();
if (count <= 0)
return AppActionRunResult{AppActionRunResult::Level::Info, _L("No plates available.")};
int index = 0;
try {
index = std::stoi(param);
} catch (const std::exception&) {}
index = std::clamp(index, 0, count - 1);
plater->select_plate(index, false);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Printer / device connection ----
add_with_icon("sync_ams", _u8L("Synchronize Filament List from AMS"), _u8L("Printer"), "ams_fila_sync", [](const std::string&) {
Plater* plater = wxGetApp().plater();
DeviceManager* dev = wxGetApp().getDeviceManager();
if (dev && dev->get_selected_machine() && plater) {
plater->sidebar().sync_ams_list();
return AppActionRunResult{AppActionRunResult::Level::Success};
}
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Connect a printer to synchronize the AMS filament list.")};
});
// ---- Presets / cloud ----
add_with_icon("preset_bundle", _u8L("Open Preset Bundle"), _u8L("Presets"), "menu_edit_preset", [](const std::string&) {
wxGetApp().open_presetbundledialog();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("sync_presets", _u8L("Sync Presets"), _u8L("Presets"), "printer_sync_ok", [](const std::string&) {
if (!wxGetApp().is_user_login())
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Sign in to sync presets.")};
wxGetApp().restart_sync_user_preset();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Import ----
add_with_icon("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), "menu_open", [](const std::string&) {
if (Plater* plater = wxGetApp().plater()) {
#ifdef __APPLE__
plater->add_model();
#else
plater->add_file();
#endif
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), "menu_open", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->import_zip_archive();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("import_configs", _u8L("Import Configs"), _u8L("Import"), "menu_open", [](const std::string&) {
if (MainFrame* mf = wxGetApp().mainframe)
mf->load_config_file();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Export extras ----
add_with_icon("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), "save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_stl(false, false, true);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), "save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_stl(false, false, false, FT_DRC);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), "save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_stl(false, false, true, FT_DRC);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), "custom-gcode_gcode", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_toolpaths_to_obj();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), "save", [](const std::string&) {
if (MainFrame* mf = wxGetApp().mainframe)
mf->export_config();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Help ---- (mirrors the top-bar Help menu, plus the wiki/YouTube links)
add("help_keyboard_shortcuts", _u8L("Keyboard Shortcuts"), _u8L("Help"), [](const std::string&) {
wxGetApp().keyboard_shortcuts();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_setup_wizard", _u8L("Setup Wizard"), _u8L("Help"), [](const std::string&) {
wxGetApp().ShowUserGuide();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), "open_project", [](const std::string&) {
Slic3r::GUI::desktop_open_datadir_folder();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_troubleshoot", _u8L("Troubleshoot Center"), _u8L("Help"), [](const std::string&) {
wxGetApp().troubleshoot();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_network_test", _u8L("Open Network Test"), _u8L("Help"), [](const std::string&) {
NetworkTestDialog dlg(wxGetApp().mainframe);
dlg.ShowModal();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("help_tip_of_the_day", _u8L("Show Tip of the Day"), _u8L("Help"), "info", [](const std::string&) {
if (Plater* plater = wxGetApp().plater()) {
plater->get_dailytips()->open();
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
canvas->set_as_dirty();
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), "refresh", [](const std::string&) {
wxGetApp().check_new_version_sf(true, 1);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), "OrcaSlicer_gradient_circle", [](const std::string&) {
Slic3r::GUI::about();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("open_wiki", _u8L("Open Wiki"), _u8L("Help"), "link_wiki_img", [](const std::string&) {
wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/", wxBROWSER_NEW_WINDOW);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("open_youtube", _u8L("Open YouTube Channel"), _u8L("Help"), [](const std::string&) {
wxLaunchDefaultBrowser("https://www.youtube.com/@OfficialOrcaSlicer/videos", wxBROWSER_NEW_WINDOW);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Plugins ----
add("open_plugins", _u8L("Open Plugins"), _u8L("Plugins"), [](const std::string&) {
wxGetApp().open_plugins_dialog();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("refresh_plugins", _u8L("Refresh Plugins"), _u8L("Plugins"), [](const std::string&) {
wxGetApp().refresh_plugins();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("install_plugin", _u8L("Install Plugin"), _u8L("Plugins"), [](const std::string&) {
open_plugin_hub();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("install_local_plugin", _u8L("Install Local Plugin"), _u8L("Plugins"), [](const std::string&) {
wxGetApp().install_local_plugin();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
return out;
}
std::vector<NativeCommand>& catalog_storage()
{
static std::vector<NativeCommand> commands = build_command_catalog();
return commands;
}
} // namespace
const std::vector<NativeCommand>& NativeCommands::catalog()
{
return catalog_storage();
}
void NativeCommands::rebuild_catalog()
{
// build_command_catalog() re-runs _u8L under the current locale, so replacing the storage
// refreshes every translated title/group after a language switch.
catalog_storage() = build_command_catalog();
}
std::unique_ptr<AppAction> NativeCommands::make_action(const NativeCommand& command)
{
return std::make_unique<CommandAction>(command);
}
AppActionRunResult NativeCommands::run(const std::string& key, const std::string& param)
{
GUI_App& app = wxGetApp();
if (app.is_closing())
return {};
for (const NativeCommand& c : catalog())
if (c.key == key)
return c.runner(param);
return {AppActionRunResult::Level::Info, _L("Unknown command.")};
}
}} // namespace Slic3r::GUI
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "ActionRegistry.hpp" // for AppAction / AppActionRunResult
namespace Slic3r { namespace GUI {
// A built-in speed-dial command: identity + how to run it. make_action() wraps a value as a thin
// AppAction for the registry, so this catalog is the single source of truth for the behaviour
// (runner => an owner method), the presentation (title/group/input), and the tile pictogram
// (icon = an SVG base name under resources/images, "" for no icon).
struct NativeCommand
{
std::string key;
std::string title;
std::string group;
std::string input; // "percent"/"tab" or "" for immediate run
std::string icon; // SVG base name, or "" to render a blank tile
std::function<AppActionRunResult(const std::string& param)> runner;
};
namespace NativeCommands {
// The full built-in command catalog. Built on first use and reused; call rebuild_catalog() after a
// live UI language switch so the translated titles/groups match the new locale. UI thread only.
const std::vector<NativeCommand>& catalog();
// Rebuilds the catalog in the current locale. UI thread only.
void rebuild_catalog();
// Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only.
AppActionRunResult run(const std::string& key, const std::string& param = {});
// Materialises one catalog entry as a runnable AppAction. Keeps the catalog's identity,
// presentation and behaviour as the single source of truth; ActionRegistry only stores and
// dispatches the result. UI thread only.
std::unique_ptr<AppAction> make_action(const NativeCommand& command);
} // namespace NativeCommands
}} // namespace Slic3r::GUI
+21 -2
View File
@@ -10,6 +10,7 @@
#include "Widgets/Label.hpp" #include "Widgets/Label.hpp"
#include <wx/button.h> #include <wx/button.h>
#include <wx/dcclient.h>
#include <wx/sizer.h> #include <wx/sizer.h>
wxDEFINE_EVENT(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, wxCommandEvent); wxDEFINE_EVENT(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, wxCommandEvent);
@@ -158,12 +159,22 @@ void ButtonsListCtrl::SetSelection(int sel)
bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */) bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */)
{ {
Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER); Button * btn = new Button(this, text, bmp_name, wxNO_BORDER);
btn->SetCornerRadius(0); btn->SetCornerRadius(0);
if (bmp_name.empty() && bmp.IsOk()) if (bmp_name.empty() && bmp.IsOk())
btn->SetIcon(bmp); btn->SetIcon(bmp);
// The label no longer carries a leading space, so widen the icon<->text gap to keep the
// original spacing between a tab's icon and its caption.
{
wxClientDC dc(btn);
dc.SetFont(btn->GetFont());
int space_w = 0;
dc.GetTextExtent(" ", &space_w, nullptr);
btn->SetIconSpacing(5 + space_w);
}
int em = em_unit(this); int em = em_unit(this);
//BBS set size for button //BBS set size for button
btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10}); btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10});
@@ -190,6 +201,7 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /*
Slic3r::GUI::wxGetApp().UpdateDarkUI(btn); Slic3r::GUI::wxGetApp().UpdateDarkUI(btn);
m_pageButtons.insert(m_pageButtons.begin() + n, btn); m_pageButtons.insert(m_pageButtons.begin() + n, btn);
m_pageLabels.insert(m_pageLabels.begin() + n, text); // ORCA m_pageLabels.insert(m_pageLabels.begin() + n, text); // ORCA
m_pageIcons.insert(m_pageIcons.begin() + n, bmp_name);
m_buttons_sizer->Insert(n, new wxSizerItem(btn)); m_buttons_sizer->Insert(n, new wxSizerItem(btn));
m_buttons_sizer->SetCols(m_buttons_sizer->GetCols() + 1); m_buttons_sizer->SetCols(m_buttons_sizer->GetCols() + 1);
m_sizer->Layout(); m_sizer->Layout();
@@ -209,6 +221,7 @@ void ButtonsListCtrl::RemovePage(size_t n)
Button* btn = m_pageButtons[n]; Button* btn = m_pageButtons[n];
m_pageButtons.erase(m_pageButtons.begin() + n); m_pageButtons.erase(m_pageButtons.begin() + n);
m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA
m_pageIcons.erase(m_pageIcons.begin() + n);
m_buttons_sizer->Remove(n); m_buttons_sizer->Remove(n);
#if __WXOSX__ #if __WXOSX__
RemoveChild(btn); RemoveChild(btn);
@@ -245,7 +258,7 @@ void ButtonsListCtrl::SetCompact(size_t n, bool compact)
int em = em_unit(this); int em = em_unit(this);
Button* btn = m_pageButtons[n]; Button* btn = m_pageButtons[n];
btn->SetMinSize({(compact ? 40 : 136) * em / 10, 36 * em / 10}); btn->SetMinSize({(compact ? 40 : 136) * em / 10, 36 * em / 10});
btn->SetLabel(compact ? "" : (" " + m_pageLabels[n])); btn->SetLabel(compact ? "" : m_pageLabels[n]);
} }
wxString ButtonsListCtrl::GetPageText(size_t n) const wxString ButtonsListCtrl::GetPageText(size_t n) const
@@ -254,6 +267,12 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const
return btn->GetLabel(); return btn->GetLabel();
} }
// ORCA
wxString ButtonsListCtrl::GetPageLabel(size_t n) const
{
return n < m_pageLabels.size() ? m_pageLabels[n] : wxString();
}
// ORCA // ORCA
void ButtonsListCtrl::SetOverflowButton(wxWindow* button) void ButtonsListCtrl::SetOverflowButton(wxWindow* button)
{ {
+23
View File
@@ -33,6 +33,14 @@ public:
void SetPageText(size_t n, const wxString& strText); void SetPageText(size_t n, const wxString& strText);
void SetCompact(size_t n, bool compact); // ORCA void SetCompact(size_t n, bool compact); // ORCA
wxString GetPageText(size_t n) const; wxString GetPageText(size_t n) const;
// ORCA: the full page label, unaffected by SetCompact() blanking the button text.
wxString GetPageLabel(size_t n) const;
// Resource name the page was inserted with (empty for plugin pages, which pass a wxBitmap).
const std::string& GetPageIcon(size_t n) const
{
static const std::string empty;
return n < m_pageIcons.size() ? m_pageIcons[n] : empty;
}
wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA
// ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g. // ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g.
// an overflow indicator. Pass nullptr to remove it; ownership stays with the caller. // an overflow indicator. Pass nullptr to remove it; ownership stays with the caller.
@@ -47,6 +55,7 @@ private:
int m_btn_margin; int m_btn_margin;
int m_line_margin; int m_line_margin;
std::vector<wxString> m_pageLabels; // ORCA std::vector<wxString> m_pageLabels; // ORCA
std::vector<std::string> m_pageIcons; // ORCA: resource icon name per page, plugin pages empty
wxWindow* m_overflow_button{nullptr}; // ORCA wxWindow* m_overflow_button{nullptr}; // ORCA
}; };
@@ -241,6 +250,20 @@ public:
return GetBtnsListCtrl()->GetPageText(n); return GetBtnsListCtrl()->GetPageText(n);
} }
// ORCA: the real page label. GetPageText() returns the button label, which SetCompact() blanks.
wxString GetPageLabel(size_t n) const
{
wxCHECK_MSG(n < GetPageCount(), wxString(), wxS("Invalid page"));
return GetBtnsListCtrl()->GetPageLabel(n);
}
// Resource icon name the page was inserted with; empty for pages added with a wxBitmap.
std::string GetPageIcon(size_t n) const
{
wxCHECK_MSG(n < GetPageCount(), std::string(), wxS("Invalid page"));
return GetBtnsListCtrl()->GetPageIcon(n);
}
virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override
{ {
return false; return false;
+21 -1
View File
@@ -1,6 +1,7 @@
#include "OptionsGroup.hpp" #include "OptionsGroup.hpp"
#include "ConfigExceptions.hpp" #include "ConfigExceptions.hpp"
#include "Plater.hpp" #include "Plater.hpp"
#include "SettingsIndex.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
#include "MainFrame.hpp" #include "MainFrame.hpp"
#include "OG_CustomCtrl.hpp" #include "OG_CustomCtrl.hpp"
@@ -244,6 +245,25 @@ void OptionsGroup::append_line(const Line& line)
{ {
m_lines.emplace_back(line); m_lines.emplace_back(line);
// Feed the searcher the row's wiki path (Line::label_path, for the Speed Dial's "open wiki"
// affordance) and the label the row actually draws, so a setting action is named like the page.
if (m_use_custom_ctrl) {
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
const Preset::Type type = static_cast<Preset::Type>(config_type());
const bool multi = line.get_options().size() > 1;
for (const auto& opt : line.get_options()) {
if (!line.label_path.empty())
index.set_path(opt.opt_id, type, line.label_path);
// Mirror the sub-label OG_CustomCtrl draws for a multi-option row, so the palette
// names each field like the page does.
const std::string& leaf_src = opt.opt.label;
const wxString leaf = (leaf_src == L_CONTEXT("Top", "Layers") || leaf_src == L_CONTEXT("Bottom", "Layers")) ?
_L_CONTEXT(leaf_src, "Layers") :
_(leaf_src);
index.set_line_label(opt.opt_id, type, Search::compose_display_label(line.label, leaf, multi));
}
}
if (line.full_width && (line.widget != nullptr || !line.get_extra_widgets().empty())) if (line.full_width && (line.widget != nullptr || !line.get_extra_widgets().empty()))
return; return;
@@ -650,7 +670,7 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index
m_opt_map.emplace(opt_id, pair); m_opt_map.emplace(opt_id, pair);
if (m_use_custom_ctrl) // fill group and category values just for options from Settings Tab if (m_use_custom_ctrl) // fill group and category values just for options from Settings Tab
wxGetApp().sidebar().get_searcher().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category()); wxGetApp().sidebar().settings_index().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category(), this->icon);
return Option(*m_config->def()->get(opt_key), opt_id); return Option(*m_config->def()->get(opt_key), opt_id);
} }
+5 -1
View File
@@ -250,6 +250,10 @@ protected:
virtual void back_to_initial_value(const std::string& opt_key) {} virtual void back_to_initial_value(const std::string& opt_key) {}
virtual void back_to_sys_value(const std::string& opt_key) {} virtual void back_to_sys_value(const std::string& opt_key) {}
// Preset::Type of a settings group; -1 for groups not tied to a preset. Used by append_line to
// register each option's wiki path with the searcher. Overridden by ConfigOptionsGroup.
virtual int config_type() const { return -1; }
public: public:
static wxString get_url(const std::string& path_end); static wxString get_url(const std::string& path_end);
static bool launch_browser(const std::string& path_end); static bool launch_browser(const std::string& path_end);
@@ -273,7 +277,7 @@ public:
OptionsGroup(parent, wxEmptyString, wxEmptyString, true, nullptr) {} OptionsGroup(parent, wxEmptyString, wxEmptyString, true, nullptr) {}
const wxString& config_category() const throw() { return m_config_category; } const wxString& config_category() const throw() { return m_config_category; }
int config_type() const throw() { return m_config_type; } int config_type() const throw() override { return m_config_type; }
const t_opt_map& opt_map() const throw() { return m_opt_map; } const t_opt_map& opt_map() const throw() { return m_opt_map; }
void set_config_category_and_type(const wxString &category, int type) { m_config_category = category; m_config_type = type; } void set_config_category_and_type(const wxString &category, int type) { m_config_category = category; m_config_type = type; }
+3 -1
View File
@@ -324,7 +324,9 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c
wxID_ANY, wxID_ANY,
wxDefaultPosition, wxDefaultPosition,
wxDefaultSize, wxDefaultSize,
wxVSCROLL) // hide hori-bar will cause hidden field mis-position wxVSCROLL // hide hori-bar will cause hidden field mis-position
| wxTAB_TRAVERSAL // Allows for traversal via tab key
)
{ {
// ShowScrollBar(GetHandle(), SB_BOTH, FALSE); // ShowScrollBar(GetHandle(), SB_BOTH, FALSE);
Bind(wxEVT_SCROLL_CHANGED, [this](auto &e) { Bind(wxEVT_SCROLL_CHANGED, [this](auto &e) {
+132 -103
View File
@@ -148,7 +148,6 @@
#include "Widgets/RadioGroup.hpp" #include "Widgets/RadioGroup.hpp"
#include "Widgets/CheckBox.hpp" #include "Widgets/CheckBox.hpp"
#include "Widgets/Button.hpp" #include "Widgets/Button.hpp"
#include "Widgets/StaticGroup.hpp"
#include "GUI_ObjectTable.hpp" #include "GUI_ObjectTable.hpp"
#include "libslic3r/Thread.hpp" #include "libslic3r/Thread.hpp"
@@ -475,14 +474,9 @@ enum class ActionButtonType : int {
abSendGCode abSendGCode
}; };
// Background for the extruder-group title chip and its edit buttons, matching the StaticGroup // Background for the extruder-group title chip and its edit buttons
// interior. macOS keeps a lighter #F7F7F7 tint in light mode; dark mode uses the mapped colour.
static wxColour extruder_group_chip_bg() static wxColour extruder_group_chip_bg()
{ {
#ifdef __WXOSX__
if (!wxGetApp().dark_mode())
return wxColour("#F7F7F7");
#endif
return StateColor::darkModeColorFor(*wxWHITE); return StateColor::darkModeColorFor(*wxWHITE);
} }
@@ -497,38 +491,39 @@ public:
SetBackgroundColour(extruder_group_chip_bg()); SetBackgroundColour(extruder_group_chip_bg());
auto sizer = new wxBoxSizer(wxHORIZONTAL); auto sizer = new wxBoxSizer(wxHORIZONTAL);
auto label_color = StateColor::darkModeColorFor(wxColour("#363636"));
m_label = new wxStaticText(this, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); m_label = new wxStaticText(this, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
m_label->SetFont(Label::Body_13); m_label->SetFont(Label::Body_12);
m_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B"))); m_label->SetForegroundColour(label_color);
m_brace_left = new wxStaticText(this, wxID_ANY, "(", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); m_brace_left = new wxStaticText(this, wxID_ANY, "(", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
m_brace_left->SetFont(Label::Body_13); m_brace_left->SetFont(Label::Body_12);
m_brace_left->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); m_brace_left->SetForegroundColour(label_color);
m_brace_left->Hide(); m_brace_left->Hide();
m_count = new wxStaticText(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); m_count = new wxStaticText(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
m_count->SetFont(Label::Body_13.Bold()); m_count->SetFont(Label::Body_12.Bold());
m_count->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); m_count->SetForegroundColour(label_color);
m_count->Hide(); m_count->Hide();
m_brace_right = new wxStaticText(this, wxID_ANY, ")", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); m_brace_right = new wxStaticText(this, wxID_ANY, ")", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
m_brace_right->SetFont(Label::Body_13); m_brace_right->SetFont(Label::Body_12);
m_brace_right->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); m_brace_right->SetForegroundColour(label_color);
m_brace_right->Hide(); m_brace_right->Hide();
m_hover_btn = new ScalableButton(this, wxID_ANY, "dot"); m_hover_btn = new ScalableButton(this, wxID_ANY, "edit_12px", wxEmptyString, FromDIP(wxSize(12,12)), wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 12);
m_hover_btn->SetMinSize(wxSize(FromDIP(25), -1));
m_hover_btn->SetBackgroundColour(extruder_group_chip_bg()); m_hover_btn->SetBackgroundColour(extruder_group_chip_bg());
m_hover_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) { m_hover_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
if (m_enabled && m_hover_on_click) if (m_enabled && m_hover_on_click)
m_hover_on_click(); m_hover_on_click();
}); });
sizer->Add(m_label, 0, wxALIGN_CENTER_VERTICAL); sizer->Add(m_label , 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(4));
sizer->Add(m_brace_left, 0, wxALIGN_CENTER_VERTICAL); sizer->Add(m_brace_left , 0, wxALIGN_CENTER_VERTICAL);
sizer->Add(m_count, 0, wxALIGN_CENTER_VERTICAL); sizer->Add(m_count , 0, wxALIGN_CENTER_VERTICAL);
sizer->Add(m_brace_right, 0, wxALIGN_CENTER_VERTICAL); sizer->Add(m_brace_right, 0, wxALIGN_CENTER_VERTICAL);
sizer->Add(m_hover_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(5)); sizer->Add(m_hover_btn , 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(4));
// No SetSizerAndFit: that would record the count-hidden width as an explicit min size, // No SetSizerAndFit: that would record the count-hidden width as an explicit min size,
// which outranks best size in sizer allocation, so once the count is shown any ancestor // which outranks best size in sizer allocation, so once the count is shown any ancestor
@@ -540,7 +535,7 @@ public:
void EnableEdit(bool enable) void EnableEdit(bool enable)
{ {
m_enabled = enable; m_enabled = enable;
m_hover_btn->SetBitmap_(enable ? "edit" : "dot"); //m_hover_btn->SetBitmap_(enable ? "edit_12px" : "dot"); // it causes crash if icon sizes not matches
} }
void SetOnHoverClick(std::function<void()> on_click) { m_hover_on_click = std::move(on_click); } void SetOnHoverClick(std::function<void()> on_click) { m_hover_on_click = std::move(on_click); }
@@ -551,11 +546,13 @@ public:
m_count->Hide(); m_count->Hide();
m_brace_left->Hide(); m_brace_left->Hide();
m_brace_right->Hide(); m_brace_right->Hide();
m_hover_btn->Hide();
} else { } else {
m_count->SetLabel(wxString::Format("%d", count)); m_count->SetLabel(wxString::Format("%d", count));
m_count->Show(); m_count->Show();
m_brace_left->Show(); m_brace_left->Show();
m_brace_right->Show(); m_brace_right->Show();
m_hover_btn->Show();
} }
UpdateSizing(); UpdateSizing();
} }
@@ -566,15 +563,19 @@ public:
UpdateSizing(); UpdateSizing();
} }
void Rescale() { m_hover_btn->msw_rescale(); } void Rescale() {
m_hover_btn->msw_rescale();
UpdateSizing();
}
// Re-apply the chip colours on a live light/dark switch (they are set once at construction). // Re-apply the chip colours on a live light/dark switch (they are set once at construction).
void sys_color_changed() void sys_color_changed()
{ {
SetBackgroundColour(extruder_group_chip_bg()); SetBackgroundColour(extruder_group_chip_bg());
m_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B"))); auto label_color = StateColor::darkModeColorFor(wxColour("#363636"));
m_label->SetForegroundColour(label_color);
for (wxStaticText *t : {m_brace_left, m_count, m_brace_right}) for (wxStaticText *t : {m_brace_left, m_count, m_brace_right})
t->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); t->SetForegroundColour(label_color);
m_hover_btn->SetBackgroundColour(extruder_group_chip_bg()); m_hover_btn->SetBackgroundColour(extruder_group_chip_bg());
Refresh(); Refresh();
} }
@@ -601,11 +602,12 @@ private:
bool m_enabled{false}; bool m_enabled{false};
}; };
struct ExtruderGroup : StaticGroup struct ExtruderGroup : StaticBox
{ {
ExtruderGroup(wxWindow * parent, int index, wxString const &title); ExtruderGroup(wxWindow * parent, int index, wxString const &title);
wxStaticBoxSizer *sizer = nullptr; wxBoxSizer * sizer = nullptr;
HoverLabel * hover_label = nullptr; HoverLabel * hover_label = nullptr;
wxStaticText* ams_label{nullptr};
ScalableButton * btn_edit = nullptr; ScalableButton * btn_edit = nullptr;
ComboBox * combo_diameter = nullptr; ComboBox * combo_diameter = nullptr;
ComboBox * combo_flow = nullptr; ComboBox * combo_flow = nullptr;
@@ -645,8 +647,10 @@ struct ExtruderGroup : StaticGroup
{ {
if (hover_label) if (hover_label)
hover_label->Rescale(); hover_label->Rescale();
if (btn_edit) if (btn_edit){
btn_edit->msw_rescale(); btn_edit->msw_rescale();
btn_edit->SetMinSize(ams_label->GetSize());
}
btn_up->msw_rescale(); btn_up->msw_rescale();
btn_down->msw_rescale(); btn_down->msw_rescale();
combo_diameter->Rescale(); combo_diameter->Rescale();
@@ -861,9 +865,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual)
// double // double
extruder_dual_sizer = new wxBoxSizer(wxHORIZONTAL); extruder_dual_sizer = new wxBoxSizer(wxHORIZONTAL);
extruder_dual_sizer->Add(left_extruder->sizer, 1, wxEXPAND, 0); extruder_dual_sizer->Add(left_extruder, 1, wxEXPAND, 0);
extruder_dual_sizer->AddSpacer(FromDIP(4)); extruder_dual_sizer->AddSpacer(FromDIP(4));
extruder_dual_sizer->Add(right_extruder->sizer, 1, wxEXPAND, 0); extruder_dual_sizer->Add(right_extruder, 1, wxEXPAND, 0);
// Filament Track Switch status icon, floated over the extruder AMS area (positioned in // Filament Track Switch status icon, floated over the extruder AMS area (positioned in
// update_extruder_separator_icon). Created hidden; a click re-shows the ready/not-ready tip. // update_extruder_separator_icon). Created hidden; a click re-shows the ready/not-ready tip.
@@ -878,7 +882,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual)
} }
// single // single
extruder_single_sizer = single_extruder->sizer; extruder_single_sizer = new wxBoxSizer(wxHORIZONTAL);
extruder_single_sizer->Add(single_extruder, 1, wxEXPAND, 0);
wxBoxSizer * extruder_sizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer * extruder_sizer = new wxBoxSizer(wxVERTICAL);
extruder_sizer->Add(extruder_dual_sizer , 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); extruder_sizer->Add(extruder_dual_sizer , 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin()));
extruder_sizer->Add(extruder_single_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); extruder_sizer->Add(extruder_single_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin()));
@@ -1256,7 +1262,7 @@ public:
Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) { Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {
wxPaintDC dc(this); wxPaintDC dc(this);
dc.SetPen(StateColor::darkModeColorFor(wxColour("#DBDBDB"))); // ORCA match popup border color dc.SetPen(StateColor::darkModeColorFor(wxColour("#009688"))); // ORCA match popup border color
dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRoundedRectangle(0, 0, GetSize().x, GetSize().y, 0); dc.DrawRoundedRectangle(0, 0, GetSize().x, GetSize().y, 0);
}); });
@@ -1310,29 +1316,25 @@ public:
}; };
ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title) ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title)
: StaticGroup(parent, wxID_ANY, wxString()) : StaticBox(parent)
{ {
SetFont(Label::Body_10); SetFont(Label::Body_10);
SetForegroundColour(wxColour("#CECECE")); SetForegroundColour(wxColour("#CECECE"));
SetBorderColor(wxColour("#EEEEEE")); SetBorderColor(wxColour("#EEEEEE"));
SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); // ORCA match radius with other boxes SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); // ORCA match radius with other boxes
ShowBadge(true); ShowBadge(true);
SetTopMargin(FromDIP(7)); // ORCA
// The title lives in an interactive row inside the card (with the nozzle-count badge and its edit // The title lives in an interactive row inside the card (with the nozzle-count badge and its edit button)
// button) instead of being painted on the border by StaticGroup.
hover_label = new HoverLabel(this, title); hover_label = new HoverLabel(this, title);
hover_label->SetPosition(wxPoint(FromDIP(PRINTER_PANEL_RADIUS), 0)); // position it without putting in a sizer so it will look like title
// Nozzle // Nozzle
wxStaticText *label_diameter = new wxStaticText(this, wxID_ANY, _L("Diameter"));
label_diameter->SetFont(Label::Body_14);
label_diameter->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
if (index >= 0) label_diameter->SetMinSize({FromDIP(80), -1});
auto combo_diameter = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); auto combo_diameter = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
this->combo_diameter = combo_diameter; this->combo_diameter = combo_diameter;
wxStaticText *label_flow = new wxStaticText(this, wxID_ANY, _L("Flow")); combo_diameter->SetToolTip(_L("Diameter"));
label_flow->SetFont(Label::Body_14);
label_flow->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); // Flow
if (index >= 0) label_flow->SetMinSize({FromDIP(80), -1});
auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
combo_flow->GetDropDown().SetUseContentWidth(true); combo_flow->GetDropDown().SetUseContentWidth(true);
combo_flow->Bind(wxEVT_COMBOBOX, [index, combo_flow](wxCommandEvent &evt) { combo_flow->Bind(wxEVT_COMBOBOX, [index, combo_flow](wxCommandEvent &evt) {
@@ -1349,51 +1351,75 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
} }
}); });
this->combo_flow = combo_flow; this->combo_flow = combo_flow;
combo_flow->SetToolTip(_L("Flow"));
// AMS // AMS
wxStaticText *label_ams = new wxStaticText(this, wxID_ANY, _L("AMS")); auto ams_panel = new wxPanel(this, wxID_ANY);
label_ams->SetFont(Label::Body_14); ams_panel->SetBackgroundColour(*wxWHITE);
label_ams->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
//label_ams->SetMinSize({FromDIP(70), -1}); ams_label = new wxStaticText(ams_panel, wxID_ANY, _L("AMS"));
ams_label->SetFont(Label::Body_14);
ams_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
// AMS not installed message
ams_not_installed_msg = new wxStaticText(ams_panel, wxID_ANY, _L("Not installed"));
ams_not_installed_msg->SetFont(Label::Body_14);
ams_not_installed_msg->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
if (index >= 0) { if (index >= 0) {
btn_edit = new ScalableButton(this, wxID_ANY, "dot"); btn_edit = new ScalableButton(ams_panel, wxID_ANY, "edit");
btn_edit->SetMinSize(ams_label->GetSize());
btn_edit->SetBackgroundColour(extruder_group_chip_bg()); btn_edit->SetBackgroundColour(extruder_group_chip_bg());
btn_edit->Hide(); btn_edit->Hide();
btn_edit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) { btn_edit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index, combo_diameter](auto &evt) {
PopupWindow *window = new AMSCountPopupWindow(this, index); PopupWindow *window = new AMSCountPopupWindow(this, index);
auto size = GetSize(); auto size = GetSize();
auto pos = ClientToScreen({0, size.y + 12}); auto pos = ClientToScreen({0, size.y - FromDIP(8) - combo_diameter->GetSize().y});
size.SetWidth(size.GetWidth() + FromDIP(10)); size.SetWidth(size.GetWidth() + FromDIP(10));
window->Position(pos, {0, 0}); window->Position(pos, {0, 0});
window->Popup(); window->Popup();
}); });
auto hovered = std::make_shared<wxWindow *>(); auto hovered = std::make_shared<wxWindow *>();
for (wxWindow *w : std::initializer_list<wxWindow *>{this, label_diameter, combo_diameter, label_flow, combo_flow, btn_edit, label_ams}) { for (wxWindow *w : std::initializer_list<wxWindow *>{this, btn_edit, ams_not_installed_msg, ams_label, ams_panel}) {
w->Bind(wxEVT_ENTER_WINDOW, [w, hovered, this](wxMouseEvent &evt) { *hovered = w; btn_edit->SetBitmap_("edit"); }); // ORCA using CallAfter fixes crash on linux while clicking edit button
w->Bind(wxEVT_LEAVE_WINDOW, [w, hovered, this](wxMouseEvent &evt) { if (*hovered == w) { btn_edit->SetBitmap_("dot"); *hovered = nullptr; } }); w->Bind(wxEVT_ENTER_WINDOW, [w, hovered, this](wxMouseEvent &evt) {
*hovered = w;
this->CallAfter([this]() {
btn_edit->Show();
ams_label->Hide();
hsizer_ams->Layout();
});
});
w->Bind(wxEVT_LEAVE_WINDOW, [w, hovered, this](wxMouseEvent &evt) {
if (*hovered == w) {
*hovered = nullptr;
this->CallAfter([this]() {
btn_edit->Hide();
ams_label->Show();
hsizer_ams->Layout();
});
}
});
} }
} }
// AMS not installed message
ams_not_installed_msg = new wxStaticText(this, wxID_ANY, _L("Not installed"));
ams_not_installed_msg->SetFont(Label::Body_14);
ams_not_installed_msg->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
// AMS group // AMS group
for (size_t i = 0; i < 4; ++i) { for (size_t i = 0; i < 4; ++i) {
ams[i] = new AMSPreview(this, wxID_ANY, AMSinfo(), AMSModel::GENERIC_AMS); ams[i] = new AMSPreview(ams_panel, wxID_ANY, AMSinfo(), AMSModel::GENERIC_AMS);
ams[i]->Close(); ams[i]->Close();
} }
hsizer_ams = new wxBoxSizer(wxHORIZONTAL); hsizer_ams = new wxBoxSizer(wxHORIZONTAL);
hsizer_ams->SetMinSize(0, ams[0]->GetMinHeight()); hsizer_ams->SetMinSize(0, ams[0]->GetMinHeight());
hsizer_ams->Add(label_ams, 0, wxALIGN_CENTER); hsizer_ams->Add(ams_label, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(5));
if (btn_edit) if (btn_edit)
hsizer_ams->Add(btn_edit, 0, wxLEFT | wxALIGN_CENTER, FromDIP(2)); hsizer_ams->Add(btn_edit, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(5));
hsizer_ams->Add(ams_not_installed_msg, 0, wxALIGN_CENTER); hsizer_ams->Add(ams_not_installed_msg, 1, wxALIGN_CENTER);
btn_up = new ScalableButton(this, wxID_ANY, "page_up", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14); ams_panel->SetSizer(hsizer_ams);
btn_up = new ScalableButton(ams_panel, wxID_ANY, "page_up", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14);
btn_up->SetBackgroundColour(*wxWHITE); btn_up->SetBackgroundColour(*wxWHITE);
btn_up->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) { btn_up->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
if (page_cur > 0) if (page_cur > 0)
@@ -1401,7 +1427,7 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
update_ams(); update_ams();
}); });
btn_up->Hide(); btn_up->Hide();
btn_down = new ScalableButton(this, wxID_ANY, "page_down", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14); btn_down = new ScalableButton(ams_panel, wxID_ANY, "page_down", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14);
btn_down->SetBackgroundColour(*wxWHITE); btn_down->SetBackgroundColour(*wxWHITE);
btn_down->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) { btn_down->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
if (page_cur + 1 < page_num) if (page_cur + 1 < page_num)
@@ -1410,31 +1436,24 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
}); });
btn_down->Hide(); btn_down->Hide();
wxBoxSizer *hsizer_diameter = new wxBoxSizer(wxHORIZONTAL); wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL);
hsizer_diameter->Add(label_diameter, 0, wxALIGN_CENTER); wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL);
hsizer_diameter->Add(combo_diameter, 1, wxEXPAND);
wxBoxSizer * hsizer_nozzle = new wxBoxSizer(wxHORIZONTAL); hsizer->Add(combo_diameter, 1, wxRIGHT, FromDIP(5));
hsizer_nozzle->Add(label_flow, 0, wxALIGN_CENTER); hsizer->Add(combo_flow , 1);
hsizer_nozzle->Add(combo_flow, 1, wxEXPAND);
vsizer->AddSpacer(FromDIP(16)); // spacing for title and control
if (index < 0) { if (index < 0) {
label_ams->Hide(); ams_panel->Hide();
ams_not_installed_msg->Hide();
wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL);
wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL);
hsizer->Add(hsizer_diameter, 1, wxEXPAND | wxTOP| wxBOTTOM, FromDIP(8));
hsizer->Add(hsizer_nozzle, 1, wxEXPAND | wxALL, FromDIP(8));
hsizer->AddSpacer(FromDIP(2)); // Avoid badge
vsizer->Add(hover_label, 0, wxLEFT | wxALL, FromDIP(2));
vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(2));
this->sizer = vsizer;
} else { } else {
wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL); vsizer->Add(ams_panel, 0, wxEXPAND | wxLEFT | wxRIGHT , FromDIP(5));
vsizer->Add(hover_label, 0, wxLEFT | wxALL, FromDIP(2)); vsizer->AddSpacer(FromDIP(2));
vsizer->Add(hsizer_ams, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(2));
vsizer->Add(hsizer_diameter, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(2));
vsizer->Add(hsizer_nozzle, 0, wxEXPAND | wxALL, FromDIP(2));
this->sizer = vsizer;
} }
vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5));
SetSizer(vsizer);
Layout();
AMSCountPopupWindow::UpdateAMSCount(index < 0 ? 0 : index, this); AMSCountPopupWindow::UpdateAMSCount(index < 0 ? 0 : index, this);
} }
@@ -1505,7 +1524,7 @@ void ExtruderGroup::update_ams()
} }
} }
sizer->Layout(); Layout();
} }
void ExtruderGroup::sync_ams(MachineObject const *obj, std::vector<DevAms *> const &ams4, std::vector<DevAms *> const &ams1) void ExtruderGroup::sync_ams(MachineObject const *obj, std::vector<DevAms *> const &ams4, std::vector<DevAms *> const &ams1)
@@ -6422,6 +6441,11 @@ Search::OptionsSearcher& Sidebar::get_searcher()
return p->searcher; return p->searcher;
} }
Search::SettingsIndex& Sidebar::settings_index()
{
return p->searcher.index();
}
std::string& Sidebar::get_search_line() std::string& Sidebar::get_search_line()
{ {
return p->searcher.search_string(); return p->searcher.search_string();
@@ -7571,10 +7595,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); }); view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); });
view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); }); view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
view3D_canvas->Bind(EVT_GLCANVAS_OPEN_SPEED_DIAL, [this](SimpleEvent&) {
if (this->q->is_view3D_shown())
wxGetApp().open_speed_dial();
});
view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int>& evt) view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int>& evt)
{ if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); }); { if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); });
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); });
@@ -12831,17 +12851,8 @@ void Plater::priv::on_action_add(SimpleEvent&)
//BBS: add plate from toolbar //BBS: add plate from toolbar
void Plater::priv::on_action_add_plate(SimpleEvent&) void Plater::priv::on_action_add_plate(SimpleEvent&)
{ {
if (q != nullptr) { if (q != nullptr)
take_snapshot("add partplate"); q->add_plate();
this->partplate_list.create_plate();
int new_plate = this->partplate_list.get_plate_count() - 1;
this->partplate_list.select_plate(new_plate);
update();
// BBS set default view
//q->get_camera().select_view("topfront");
q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE;
}
} }
//BBS: remove plate from toolbar //BBS: remove plate from toolbar
@@ -21913,6 +21924,24 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi
return ret; return ret;
} }
//BBS: add an empty plate and switch to it (mirrors the toolbar's Add Plate).
int Plater::add_plate()
{
if (!p->can_add_plate())
return -1;
take_snapshot("add partplate");
int new_plate = p->partplate_list.create_plate();
if (new_plate < 0)
return new_plate;
p->partplate_list.select_plate(new_plate);
update();
// BBS set default view
//get_camera().select_view("topfront");
p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE;
return new_plate;
}
int Plater::duplicate_plate(int plate_index) int Plater::duplicate_plate(int plate_index)
{ {
int index = plate_index, ret; int index = plate_index, ret;
+4
View File
@@ -283,6 +283,7 @@ public:
std::vector<std::string>& types, std::vector<std::string>& types,
std::vector<size_t>* config_indices = nullptr); std::vector<size_t>* config_indices = nullptr);
Search::OptionsSearcher& get_searcher(); Search::OptionsSearcher& get_searcher();
Search::SettingsIndex& settings_index();
std::string& get_search_line(); std::string& get_search_line();
void update_printer_thumbnail(); void update_printer_thumbnail();
@@ -781,6 +782,9 @@ public:
void apply_background_progress(); void apply_background_progress();
//BBS: select the plate by hover_id //BBS: select the plate by hover_id
int select_plate_by_hover_id(int hover_id, bool right_click = false, bool isModidyPlateName = false); int select_plate_by_hover_id(int hover_id, bool right_click = false, bool isModidyPlateName = false);
//BBS: add an empty plate and switch to it (the toolbar's Add Plate). Returns the new
// plate index, or -1 when the plate cap is reached.
int add_plate();
//BBS: delete the plate, index= -1 means the current plate //BBS: delete the plate, index= -1 means the current plate
int delete_plate(int plate_index = -1); int delete_plate(int plate_index = -1);
int duplicate_plate(int plate_index = -1); int duplicate_plate(int plate_index = -1);
+185 -122
View File
@@ -126,15 +126,6 @@ PluginCapabilityType primary_capability_type_of(PluginManager& manager, const st
return capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type(); return capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type();
} }
std::vector<PluginDescriptor> current_cloud_metadata_snapshot()
{
std::vector<PluginDescriptor> cloud_entries;
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
if (entry.is_cloud_plugin())
cloud_entries.push_back(entry);
return cloud_entries;
}
PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor) PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor)
{ {
descriptor.plugin_root.clear(); descriptor.plugin_root.clear();
@@ -150,41 +141,6 @@ PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor)
return descriptor; return descriptor;
} }
void refresh_plugin_metadata_blocking(bool fetch_cloud)
{
PluginManager& manager = PluginManager::instance();
std::vector<std::string> not_found, unauthorized;
const std::vector<PluginDescriptor> current_cloud_metadata = fetch_cloud ? std::vector<PluginDescriptor>{} :
current_cloud_metadata_snapshot();
manager.rescan_plugins();
if (!fetch_cloud) {
manager.update_cloud_metadata(current_cloud_metadata);
return;
}
manager.fetch_plugins_from_cloud(&not_found, &unauthorized);
wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() {
if (wxGetApp().is_closing())
return;
Plater* plater = wxGetApp().plater();
if (plater == nullptr)
return;
for (const auto& uuid : not_found)
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
format(_L("Plugin %s is no longer available."), uuid));
for (const auto& uuid : unauthorized)
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
format(_L("Plugin %s access is unauthorized."), uuid));
});
}
std::string to_string(PluginUpdateStatus status); std::string to_string(PluginUpdateStatus status);
nlohmann::json build_context_actions_payload(const PluginAvailableActions& available_actions); nlohmann::json build_context_actions_payload(const PluginAvailableActions& available_actions);
@@ -448,6 +404,176 @@ bool take_plugin_operation_result(const std::shared_ptr<PluginOperationState>& s
} }
} // namespace } // namespace
// ── Dialog-independent plugin actions (also used by the speed dial) ───────────────────────────
namespace {
// Snapshot of the currently-known cloud plugin descriptors, used to refresh metadata without a
// network round-trip (kUseCurrentCloudMeta).
std::vector<PluginDescriptor> current_cloud_metadata_snapshot()
{
std::vector<PluginDescriptor> cloud_entries;
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
if (entry.is_cloud_plugin())
cloud_entries.push_back(entry);
return cloud_entries;
}
} // namespace
void refresh_plugin_metadata_blocking(bool fetch_cloud)
{
PluginManager& manager = PluginManager::instance();
std::vector<std::string> not_found, unauthorized;
const std::vector<PluginDescriptor> current_cloud_metadata = fetch_cloud ? std::vector<PluginDescriptor>{} :
current_cloud_metadata_snapshot();
manager.rescan_plugins();
if (!fetch_cloud) {
manager.update_cloud_metadata(current_cloud_metadata);
return;
}
manager.fetch_plugins_from_cloud(&not_found, &unauthorized);
wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() {
if (wxGetApp().is_closing())
return;
Plater* plater = wxGetApp().plater();
if (plater == nullptr)
return;
for (const auto& uuid : not_found)
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
format(_L("Plugin %s is no longer available."), uuid));
for (const auto& uuid : unauthorized)
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
format(_L("Plugin %s access is unauthorized."), uuid));
});
}
void open_plugin_hub()
{
std::string cloud_base_url = "https://cloud.orcaslicer.com";
if (wxGetApp().getAgent()) {
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent());
if (orca_agent && !orca_agent->get_cloud_base_url().empty())
cloud_base_url = orca_agent->get_cloud_base_url();
}
while (!cloud_base_url.empty() && cloud_base_url.back() == '/')
cloud_base_url.pop_back();
if (cloud_base_url.empty())
cloud_base_url = "https://cloud.orcaslicer.com";
wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub"));
}
bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message)
{
message.clear();
if (package_file.empty())
return false;
// ---- pre-flight (main thread): validate + inspect + overwrite prompt ----
const wxString package_name = from_u8(package_file.filename().string());
std::string extension = package_file.extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
if (extension != ".py" && extension != ".whl") {
message = _L("Select a .py or .whl plugin package.");
return false;
}
PluginDescriptor plugin_descriptor;
bool existing_installation = false;
std::string error;
try {
if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error)) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << error;
message = _L("Failed to install plugin package. See the log for details.");
return false;
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << ex.what();
message = _L("Failed to install plugin package. See the log for details.");
return false;
} catch (...) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file;
message = _L("Failed to install plugin package. See the log for details.");
return false;
}
if (existing_installation) {
const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
wxMessageDialog dialog(parent,
wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."),
plugin_name),
kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel"));
if (dialog.ShowModal() != wxID_OK) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installation cancelled before overwrite. package=" << package_file
<< " plugin=" << plugin_descriptor.name;
return false; // cancelled: message stays empty so callers stay silent
}
}
// ---- install + refresh on a worker behind a modal progress dialog (keeps the UI live) ----
bool installed = false;
{
struct Result
{
std::mutex mutex;
bool ok = false;
std::string error;
};
auto state = std::make_shared<Result>();
detail::run_wait_with_progress(
[state, package_file]() {
std::string error;
bool ok = false;
try {
ok = PluginManager::instance().install_plugin(package_file, error);
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
if (ok) {
// Reflect the new package in discovery/cloud metadata without blocking the caller.
try { refresh_plugin_metadata_blocking(kUseCurrentCloudMeta); } catch (...) {}
}
std::lock_guard<std::mutex> lock(state->mutex);
state->ok = ok;
state->error = std::move(error);
},
parent, _L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100,
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME, /*alive=*/nullptr, /*restore=*/{});
std::lock_guard<std::mutex> lock(state->mutex);
installed = state->ok;
error = std::move(state->error);
}
if (!installed) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package installation failed for " << package_file << " error=" << error;
message = _L("Failed to install plugin package. See the log for details.");
return false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installed successfully from " << package_file;
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
message = wxString::Format(_L("Installed \"%s\"."), installed_name);
return true;
}
PluginsDialog::PluginsDialog(wxWindow* parent, wxWindowID id, const wxString&, const wxPoint& pos, const wxSize& size, long style) PluginsDialog::PluginsDialog(wxWindow* parent, wxWindowID id, const wxString&, const wxPoint& pos, const wxSize& size, long style)
: WebViewHostDialog(parent, id, _L("Plugins"), pos, size, style) : WebViewHostDialog(parent, id, _L("Plugins"), pos, size, style)
{ create_webview("web/dialog/PluginsDialog/index.html", _L("Plugins"), wxSize(900, 820), wxSize(760, 715)); } { create_webview("web/dialog/PluginsDialog/index.html", _L("Plugins"), wxSize(900, 820), wxSize(760, 715)); }
@@ -819,78 +945,31 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
{ {
if (package_path.empty()) if (package_path.empty())
return false; return false;
BOOST_LOG_TRIVIAL(info) << "Installing local plugin package from path: " << package_path; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Installing local plugin package from path: " << package_path;
std::string error;
const boost::filesystem::path package_file(package_path); const boost::filesystem::path package_file(package_path);
const wxString package_name = from_u8(package_file.filename().string()); wxString message;
const bool installed = install_local_plugin_package(package_file, this, message);
// The helper's overwrite prompt and progress dialog can push this webview behind; re-raise it
// once, after both have closed (the speed-dial path parents to the mainframe instead).
restore_z_order();
std::string extension = package_file.extension().string(); // The shared helper reports a user-cancelled overwrite with an empty message: stay silent.
std::transform(extension.begin(), extension.end(), extension.begin(), if (message.IsEmpty()) {
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
if (extension != ".py" && extension != ".whl") {
show_status(_L("Select a .py or .whl plugin package."), "info");
return false;
}
PluginDescriptor plugin_descriptor;
bool existing_installation = false;
auto report_inspection_failure = [&]() {
BOOST_LOG_TRIVIAL(error) << "Plugin package inspection failed for " << package_path << " error=" << error;
show_status(_L("Failed to install plugin package. See the log for details."), "warn");
send_plugins(); send_plugins();
return false; return false;
};
try {
if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error))
return report_inspection_failure();
} catch (const std::exception& ex) {
error = ex.what();
return report_inspection_failure();
} catch (...) {
error = "Unknown error";
return report_inspection_failure();
}
if (existing_installation) {
const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
wxMessageDialog dialog(
this,
wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."),
plugin_name),
kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel"));
const int overwrite_rc = dialog.ShowModal();
restore_z_order();
if (overwrite_rc != wxID_OK) {
BOOST_LOG_TRIVIAL(info) << "Plugin package installation cancelled before overwrite. package=" << package_path
<< " plugin=" << plugin_descriptor.name;
return false;
}
}
bool installed = false;
try {
installed = run_with_dialog_wait([package_file, &error]() { return PluginManager::instance().install_plugin(package_file, error); },
_L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100,
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME);
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
} }
if (!installed) { if (!installed) {
BOOST_LOG_TRIVIAL(error) << "Plugin package installation failed for " << package_path << " error=" << error; BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to install plugin package.";
show_status(_L("Failed to install plugin package. See the log for details."), "warn"); show_status(message, "warn");
send_plugins(); send_plugins();
return false; return false;
} }
BOOST_LOG_TRIVIAL(info) << "Plugin package installed successfully from " << package_path; show_status(message, "success");
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name); prompt_for_missing_plugins();
show_status(wxString::Format(_L("Installed \"%s\"."), installed_name), "success"); send_plugins();
refresh_plugin_metadata_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudMeta);
return true; return true;
} }
@@ -1086,23 +1165,7 @@ void PluginsDialog::open_plugin_on_cloud(const std::string& sharing_token)
wxLaunchDefaultBrowser(wxString::FromUTF8(orca_agent->get_cloud_base_url() + "/p/" + sharing_token)); wxLaunchDefaultBrowser(wxString::FromUTF8(orca_agent->get_cloud_base_url() + "/p/" + sharing_token));
} }
void PluginsDialog::open_plugin_hub() void PluginsDialog::open_plugin_hub() { Slic3r::GUI::open_plugin_hub(); }
{
std::string cloud_base_url = "https://cloud.orcaslicer.com";
if (wxGetApp().getAgent()) {
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent());
if (orca_agent && !orca_agent->get_cloud_base_url().empty())
cloud_base_url = orca_agent->get_cloud_base_url();
}
while (!cloud_base_url.empty() && cloud_base_url.back() == '/')
cloud_base_url.pop_back();
if (cloud_base_url.empty())
cloud_base_url = "https://cloud.orcaslicer.com";
wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub"));
}
void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin) void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin)
{ {
+183 -124
View File
@@ -25,6 +25,8 @@
#include <wx/string.h> #include <wx/string.h>
#include <wx/timer.h> #include <wx/timer.h>
#include <boost/filesystem.hpp>
class wxTimer; class wxTimer;
namespace Slic3r { namespace Slic3r {
@@ -35,6 +37,184 @@ enum class PluginCapabilityType;
namespace GUI { namespace GUI {
// Dialog-independent plugin-management actions, shared by the Plugins dialog and the speed dial:
// they never require the webview dialog to be open.
// Rescans local plugins and (optionally) re-fetches cloud metadata. Blocking: run off the UI
// thread. Used by PluginsDialog (behind its progress dialog) and GUI_App::refresh_plugins().
void refresh_plugin_metadata_blocking(bool fetch_cloud);
// Opens the Cloud plugin hub in the default browser. No dialog needed.
void open_plugin_hub();
// Synchronously installs a local plugin package (.py/.whl). Runs on the UI thread but keeps it
// responsive by performing the install on a worker behind a modal progress dialog. `parent` owns
// the overwrite prompt and the progress dialog. On success `message` carries the localized
// confirmation; on a user-cancelled overwrite it is empty; on failure it carries the reason.
bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message);
namespace detail {
// Shared worker + modal-progress machinery: pulse a progress dialog while `run` executes on a
// detached worker, then run `on_finish` back on the UI thread. `alive`, when non-null, gates both
// the pulse and `on_finish` so a worker outliving its dialog can't touch freed windows; pass null
// for a dialog-independent caller. `restore` runs after the progress dialog is destroyed and before
// `on_finish`, so a webview host can re-raise itself. `finish_after_dialog_destroyed` still calls
// `on_finish` (without touching the dialog) when the host died, so a waiting loop can exit.
template<typename Run, typename OnFinish>
void run_off_thread_with_progress(Run&& run,
OnFinish&& on_finish,
wxWindow* parent,
const wxString& title,
const wxString& message,
int maximum,
int style,
std::shared_ptr<std::atomic<bool>> alive,
bool finish_after_dialog_destroyed,
std::function<void()> restore)
{
wxProgressDialog* progress = new wxProgressDialog(title, message, maximum, parent, style);
wxTimer* timer = new wxTimer();
timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) {
if ((!alive || alive->load(std::memory_order_acquire)) && progress)
progress->Pulse(message);
});
timer->Start(100);
std::thread([alive,
progress,
timer,
run = std::forward<Run>(run),
on_finish = std::forward<OnFinish>(on_finish),
finish_after_dialog_destroyed,
restore = std::move(restore)]() mutable {
try {
run();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed: " << ex.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed with an unknown exception";
}
if (wxTheApp == nullptr)
return;
wxTheApp->CallAfter([alive,
progress,
timer,
on_finish = std::move(on_finish),
finish_after_dialog_destroyed,
restore = std::move(restore)]() mutable {
timer->Stop();
delete timer;
if (!alive || alive->load(std::memory_order_acquire)) {
progress->Destroy();
if (restore)
restore();
on_finish();
} else if (finish_after_dialog_destroyed) {
on_finish();
}
});
}).detach();
}
// Wait for a worker behind a progress dialog, returning its result (or rethrowing). The waiting
// loop stays responsive because it pumps the event loop the worker posts its completion into.
template<typename Run>
std::invoke_result_t<std::decay_t<Run>&> run_wait_with_progress(Run&& run,
wxWindow* parent,
const wxString& title,
const wxString& message,
int maximum,
int style,
std::shared_ptr<std::atomic<bool>> alive,
std::function<void()> restore)
{
using Result = std::invoke_result_t<std::decay_t<Run>&>;
bool finished = false;
wxEventLoop loop;
auto on_finish = [&finished, &loop]() {
finished = true;
if (loop.IsRunning())
loop.Exit();
};
if constexpr (std::is_void_v<Result>) {
struct WaitState
{
std::mutex mutex;
std::exception_ptr exception;
};
auto state = std::make_shared<WaitState>();
run_off_thread_with_progress(
[run = std::forward<Run>(run), state]() mutable {
try {
run();
} catch (...) {
std::lock_guard<std::mutex> lock(state->mutex);
state->exception = std::current_exception();
}
},
on_finish, parent, title, message, maximum, style, std::move(alive), /*finish_after_dialog_destroyed=*/true, std::move(restore));
if (!finished)
loop.Run();
std::exception_ptr exception;
{
std::lock_guard<std::mutex> lock(state->mutex);
exception = state->exception;
}
if (exception)
std::rethrow_exception(exception);
} else {
using StoredResult = std::decay_t<Result>;
struct WaitState
{
std::mutex mutex;
std::optional<StoredResult> result;
std::exception_ptr exception;
};
auto state = std::make_shared<WaitState>();
run_off_thread_with_progress(
[run = std::forward<Run>(run), state]() mutable {
try {
StoredResult result = run();
std::lock_guard<std::mutex> lock(state->mutex);
state->result.emplace(std::move(result));
} catch (...) {
std::lock_guard<std::mutex> lock(state->mutex);
state->exception = std::current_exception();
}
},
on_finish, parent, title, message, maximum, style, std::move(alive), /*finish_after_dialog_destroyed=*/true, std::move(restore));
if (!finished)
loop.Run();
std::optional<StoredResult> result;
std::exception_ptr exception;
{
std::lock_guard<std::mutex> lock(state->mutex);
if (state->result)
result.emplace(std::move(*state->result));
exception = state->exception;
}
if (exception)
std::rethrow_exception(exception);
return std::move(*result);
}
}
} // namespace detail
class PluginsDialog : public Slic3r::GUI::WebViewHostDialog class PluginsDialog : public Slic3r::GUI::WebViewHostDialog
{ {
public: public:
@@ -111,53 +291,8 @@ private:
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE, // | wxPD_CAN_ABORT for cancel button int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE, // | wxPD_CAN_ABORT for cancel button
bool finish_after_dialog_destroyed = false) bool finish_after_dialog_destroyed = false)
{ {
const auto alive = m_alive; detail::run_off_thread_with_progress(std::forward<Run>(run), std::forward<OnFinish>(on_finish), this, title, message, maximum, style,
ProgressDialog* progress = new ProgressDialog(title, message, maximum, this, style); m_alive, finish_after_dialog_destroyed, [this] { restore_z_order(); });
wxTimer* timer = new wxTimer();
timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) {
if (alive->load(std::memory_order_acquire) && progress)
progress->Pulse(message);
});
timer->Start(100);
std::thread([this,
alive,
progress,
timer,
run = std::forward<Run>(run),
on_finish = std::forward<OnFinish>(on_finish),
finish_after_dialog_destroyed]() mutable {
try {
run();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed: " << ex.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed with an unknown exception";
}
if (wxTheApp == nullptr)
return;
wxTheApp->CallAfter([this,
alive,
progress,
timer,
on_finish = std::move(on_finish),
finish_after_dialog_destroyed]() mutable {
timer->Stop();
delete timer;
if (alive->load(std::memory_order_acquire)) {
progress->Destroy();
restore_z_order();
on_finish();
} else if (finish_after_dialog_destroyed) {
on_finish();
}
});
}).detach();
} }
template<typename Run> template<typename Run>
@@ -167,83 +302,7 @@ private:
int maximum = 100, int maximum = 100,
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE) int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
{ {
using Result = std::invoke_result_t<std::decay_t<Run>&>; return detail::run_wait_with_progress(std::forward<Run>(run), this, title, message, maximum, style, m_alive, [this] { restore_z_order(); });
bool finished = false;
wxEventLoop loop;
auto on_finish = [&finished, &loop]() {
finished = true;
if (loop.IsRunning())
loop.Exit();
};
if constexpr (std::is_void_v<Result>) {
struct WaitState
{
std::mutex mutex;
std::exception_ptr exception;
};
auto state = std::make_shared<WaitState>();
run_with_dialog(
[run = std::forward<Run>(run), state]() mutable {
try {
run();
} catch (...) {
std::lock_guard<std::mutex> lock(state->mutex);
state->exception = std::current_exception();
}
},
on_finish, title, message, maximum, style, true);
if (!finished)
loop.Run();
std::exception_ptr exception;
{
std::lock_guard<std::mutex> lock(state->mutex);
exception = state->exception;
}
if (exception)
std::rethrow_exception(exception);
} else {
using StoredResult = std::decay_t<Result>;
struct WaitState
{
std::mutex mutex;
std::optional<StoredResult> result;
std::exception_ptr exception;
};
auto state = std::make_shared<WaitState>();
run_with_dialog(
[run = std::forward<Run>(run), state]() mutable {
try {
StoredResult result = run();
std::lock_guard<std::mutex> lock(state->mutex);
state->result.emplace(std::move(result));
} catch (...) {
std::lock_guard<std::mutex> lock(state->mutex);
state->exception = std::current_exception();
}
},
on_finish, title, message, maximum, style, true);
if (!finished)
loop.Run();
std::optional<StoredResult> result;
std::exception_ptr exception;
{
std::lock_guard<std::mutex> lock(state->mutex);
if (state->result)
result.emplace(std::move(*state->result));
exception = state->exception;
}
if (exception)
std::rethrow_exception(exception);
return std::move(*result);
}
} }
std::function<void()> m_open_terminal_dlg_fn; std::function<void()> m_open_terminal_dlg_fn;
+15
View File
@@ -1726,6 +1726,21 @@ void PreferencesDialog::create_items()
auto item_multi_machine = create_item_checkbox(_L("Multi device management"), _L("With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices."), "enable_multi_machine", _L("(Requires restart)")); auto item_multi_machine = create_item_checkbox(_L("Multi device management"), _L("With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices."), "enable_multi_machine", _L("(Requires restart)"));
g_sizer->Add(item_multi_machine); g_sizer->Add(item_multi_machine);
auto item_speed_dial = create_item_checkbox(_L("Open the Speed Dial with the Space key"),
_L("When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page."),
"enable_speed_dial");
g_sizer->Add(item_speed_dial);
auto item_speed_dial_recents = create_item_spinctrl(
_L("Recent actions"),
"",
_L("actions"),
_L("How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions."),
SETTING_SPEED_DIAL_RECENT_COUNT,
SPEED_DIAL_RECENT_COUNT_MIN,
SPEED_DIAL_RECENT_COUNT_MAX);
g_sizer->Add(item_speed_dial_recents);
#ifdef SLIC3R_CAD #ifdef SLIC3R_CAD
auto item_cad_feature = create_item_checkbox(_L("CAD feature (experimental)"), auto item_cad_feature = create_item_checkbox(_L("CAD feature (experimental)"),
_L("With this option enabled, the Design tab is shown, where models can be built and edited " _L("With this option enabled, the Design tab is shown, where models can be built and edited "
+102 -42
View File
@@ -8,6 +8,8 @@
#include "ConfigValueFormatter.hpp" #include "ConfigValueFormatter.hpp"
#include "FilamentBitmapUtils.hpp" #include "FilamentBitmapUtils.hpp"
#include "Widgets/Label.hpp" #include "Widgets/Label.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/HyperLink.hpp"
#include "Widgets/TextInput.hpp" #include "Widgets/TextInput.hpp"
#include "Widgets/DialogButtons.hpp" #include "Widgets/DialogButtons.hpp"
#include "Widgets/StaticLine.hpp" #include "Widgets/StaticLine.hpp"
@@ -39,6 +41,48 @@
namespace Slic3r { namespace GUI { namespace Slic3r { namespace GUI {
namespace { namespace {
// Orca's bitmap checkbox has the established teal checked state on every platform. Keep the
// label separate so it stays clickable like a native wxCheckBox, while the control itself
// remains accessible by keyboard.
wxStaticText* add_checkbox_label(wxWindow* parent,
wxBoxSizer* sizer,
::CheckBox* check,
const wxString& label,
const wxString& tooltip,
int label_width = 0)
{
check->SetToolTip(tooltip);
sizer->Add(check, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, parent->FromDIP(2));
auto* text = new wxStaticText(parent, wxID_ANY, label);
text->SetFont(Label::Body_14);
text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
if (label_width > 0) {
text->SetMinSize(wxSize(label_width, -1));
text->SetMaxSize(wxSize(label_width, -1));
text->Wrap(label_width);
}
text->SetToolTip(tooltip);
text->SetCursor(wxCURSOR_HAND);
const auto toggle = [check]() {
if (!check->IsEnabled())
return;
check->SetValue(!check->GetValue());
wxCommandEvent event(wxEVT_TOGGLEBUTTON, check->GetId());
event.SetEventObject(check);
check->GetEventHandler()->ProcessEvent(event);
};
text->Bind(wxEVT_LEFT_DOWN, [toggle](wxMouseEvent& event) {
if (!event.LeftDClick())
toggle();
});
text->Bind(wxEVT_LEFT_DCLICK, [toggle](wxMouseEvent&) {
toggle();
});
sizer->Add(text, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, parent->FromDIP(5));
return text;
}
// Menu ids for show_menu(): dedicated range so the popup cannot collide with application-level // Menu ids for show_menu(): dedicated range so the popup cannot collide with application-level
// bindings (e.g. MainFrame's recent-files wxID_FILE1.. range). // bindings (e.g. MainFrame's recent-files wxID_FILE1.. range).
enum { enum {
@@ -649,6 +693,9 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
m_outer_tabs->SetBackgroundColour(GetBackgroundColour()); m_outer_tabs->SetBackgroundColour(GetBackgroundColour());
m_outer_host = new wxPanel(this, wxID_ANY); m_outer_host = new wxPanel(this, wxID_ANY);
#ifdef __WINDOWS__
m_outer_host->SetDoubleBuffered(true);
#endif
m_outer_host->SetBackgroundColour(GetBackgroundColour()); m_outer_host->SetBackgroundColour(GetBackgroundColour());
m_outer_host_sizer = new wxBoxSizer(wxVERTICAL); m_outer_host_sizer = new wxBoxSizer(wxVERTICAL);
m_outer_host->SetSizer(m_outer_host_sizer); m_outer_host->SetSizer(m_outer_host_sizer);
@@ -688,24 +735,22 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
// Guide links, bottom-left, sharing the footer row with the OK/Cancel buttons (pushed right). // Guide links, bottom-left, sharing the footer row with the OK/Cancel buttons (pushed right).
auto make_link = [this](const wxString& label, const char* url) {
wxStaticText* link = new wxStaticText(this, wxID_ANY, label);
link->SetFont(Label::Body_13);
link->SetForegroundColour(wxColour(0x1F, 0x8E, 0xEA));
link->SetCursor(wxCURSOR_HAND);
link->Bind(wxEVT_LEFT_DOWN, [url](wxMouseEvent&) { wxLaunchDefaultBrowser(url, wxBROWSER_NEW_WINDOW); });
return link;
};
wxBoxSizer* links_sizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer* links_sizer = new wxBoxSizer(wxVERTICAL);
links_sizer->Add(make_link(_L("Publish 3MF Wiki"), "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html"), 0, wxALIGN_LEFT); auto* wiki_link = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html");
links_sizer->Add(make_link(_L("Publish 3MF Video Guide"), "https://www.youtube.com/watch?v=-xt1N29UIOg"), 0, auto* video_link = new HyperLink(this, _L("Video Guide"), "https://www.youtube.com/watch?v=-xt1N29UIOg");
wxTOP | wxALIGN_LEFT, FromDIP(4)); links_sizer->Add(wiki_link , 0, wxALIGN_LEFT);
links_sizer->Add(video_link, 0, wxTOP | wxALIGN_LEFT, FromDIP(4));
wxBoxSizer* footer = new wxBoxSizer(wxHORIZONTAL); wxBoxSizer* footer = new wxBoxSizer(wxHORIZONTAL);
footer->Add(links_sizer, 0, wxALIGN_CENTER_VERTICAL); footer->Add(links_sizer, 0, wxALIGN_CENTER_VERTICAL);
footer->AddStretchSpacer(); footer->AddStretchSpacer();
footer->Add(dlg_btns, 0, wxALIGN_CENTER_VERTICAL); footer->Add(dlg_btns, 0, wxALIGN_CENTER_VERTICAL);
w_sizer->Add(footer, 0, wxRIGHT | wxLEFT | wxBOTTOM | wxEXPAND, FromDIP(10)); auto* footer_line = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1));
footer_line->SetBackgroundColour(wxColour("#CECECE"));
footer_line->SetMinSize(wxSize(-1, 1));
footer_line->SetMaxSize(wxSize(-1, 1));
w_sizer->Add(footer_line, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10));
w_sizer->Add(footer, 0, wxRIGHT | wxLEFT | wxTOP | wxBOTTOM | wxEXPAND, FromDIP(10));
SetSizerAndFit(w_sizer); SetSizerAndFit(w_sizer);
fit_to_content(); // initial size only; the dialog is resizable fit_to_content(); // initial size only; the dialog is resizable
@@ -718,14 +763,14 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
// Size the window to its content: width follows the widest tab strip so no filament tab is // Size the window to its content: width follows the widest tab strip so no filament tab is
// hidden (TabCtrl::relayout hides overflowing buttons), height scales proportionally. Both // hidden (TabCtrl::relayout hides overflowing buttons), height scales proportionally. Both
// are floored at the 600x500 base and capped at hard DIP limits - deliberately not the whole // are floored at the 530x530 base and capped at hard DIP limits - deliberately not the whole
// display - with one last-resort clamp so the dialog can never open larger than the screen. // display - with one last-resort clamp so the dialog can never open larger than the screen.
// Also owns the resize floor: the window cannot be resized below what the tabs need, so // Also owns the resize floor: the window cannot be resized below what the tabs need, so
// shrinking never re-hides a filament tab. // shrinking never re-hides a filament tab.
void PublishSettingsDialog::fit_to_content() void PublishSettingsDialog::fit_to_content()
{ {
static const wxSize BASE{600, 500}; static const wxSize BASE{530, 530}; // base size in DIP, the minimum the dialog can shrink to
static const wxSize CAP{1300, 850}; static const wxSize CAP{1300, 850}; // hard cap in DIP, the maximum the dialog can grow to
int strip = m_outer_tabs->GetFullSize(); int strip = m_outer_tabs->GetFullSize();
for (const SectionGroup& section : m_sections) { for (const SectionGroup& section : m_sections) {
@@ -793,6 +838,8 @@ void PublishSettingsDialog::build_option_model()
return false; return false;
value = get_string_value(opt_id, full); value = get_string_value(opt_id, full);
unit = _(def->sidetext); unit = _(def->sidetext);
if (unit == "%" && value.EndsWith("%"))
unit.clear();
return true; return true;
}; };
@@ -1005,13 +1052,19 @@ void PublishSettingsDialog::build_option_model()
// stays valid even if the vector is reallocated later. // stays valid even if the vector is reallocated later.
for (size_t c = 0; c < m_categories.size(); ++c) for (size_t c = 0; c < m_categories.size(); ++c)
if (m_categories[c].enable_check != nullptr) if (m_categories[c].enable_check != nullptr)
m_categories[c].enable_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_enable_toggle(c); }); m_categories[c].enable_check->Bind(wxEVT_TOGGLEBUTTON, [this, c](wxCommandEvent& event) {
on_enable_toggle(c);
event.Skip();
});
// Wire the "Full Publish" checkboxes (physical slots): toggling one disables/enables the // Wire the "Full Publish" checkboxes (physical slots): toggling one disables/enables the
// material's rows. // material's rows.
for (size_t c = 0; c < m_categories.size(); ++c) for (size_t c = 0; c < m_categories.size(); ++c)
if (m_categories[c].full_check != nullptr) if (m_categories[c].full_check != nullptr)
m_categories[c].full_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_full_toggle(c); }); m_categories[c].full_check->Bind(wxEVT_TOGGLEBUTTON, [this, c](wxCommandEvent& event) {
on_full_toggle(c);
event.Skip();
});
// No filter is active at startup: every row matches until the user types. // No filter is active at startup: every row matches until the user types.
for (Row& row : m_rows) for (Row& row : m_rows)
@@ -1074,6 +1127,9 @@ size_t PublishSettingsDialog::section_group_for(Section kind)
section.mixed_tabs->Hide(); section.mixed_tabs->Hide();
} }
section.page_host = new wxPanel(section.page, wxID_ANY); section.page_host = new wxPanel(section.page, wxID_ANY);
#ifdef __WINDOWS__
section.page_host->SetDoubleBuffered(true);
#endif
section.page_host->SetBackgroundColour(GetBackgroundColour()); section.page_host->SetBackgroundColour(GetBackgroundColour());
section.page_host_sizer = new wxBoxSizer(wxVERTICAL); section.page_host_sizer = new wxBoxSizer(wxVERTICAL);
section.page_host->SetSizer(section.page_host_sizer); section.page_host->SetSizer(section.page_host_sizer);
@@ -1136,10 +1192,9 @@ size_t PublishSettingsDialog::category_index_for(
if (is_mixed) { if (is_mixed) {
// No chip/title: the lone "Enable" checkbox tops the page. // No chip/title: the lone "Enable" checkbox tops the page.
auto* enable_sizer = new wxBoxSizer(wxHORIZONTAL); auto* enable_sizer = new wxBoxSizer(wxHORIZONTAL);
category.enable_check = new wxCheckBox(category.page, wxID_ANY, _L("Enable")); category.enable_check = new ::CheckBox(category.page, wxID_ANY);
category.enable_check->SetFont(Label::Body_13); category.enable_label = add_checkbox_label(category.page, enable_sizer, category.enable_check, _L("Enable"),
category.enable_check->SetToolTip(_L("Publish this mixed filament and enable + Full Publish its component filaments")); _L("Publish this mixed filament and enable + Full Publish its component filaments"));
enable_sizer->Add(category.enable_check, 0, wxALIGN_CENTER_VERTICAL);
page_sizer->Add(enable_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6)); page_sizer->Add(enable_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6));
} else { } else {
// Line 1: [chip] [title] [Enable]. The Enable checkbox gates the whole slot: while // Line 1: [chip] [title] [Enable]. The Enable checkbox gates the whole slot: while
@@ -1152,19 +1207,19 @@ size_t PublishSettingsDialog::category_index_for(
category.title_label = new wxStaticText(category.page, wxID_ANY, title); category.title_label = new wxStaticText(category.page, wxID_ANY, title);
category.title_label->SetFont(Label::Head_14); category.title_label->SetFont(Label::Head_14);
header_sizer->Add(category.title_label, 0, wxALIGN_CENTER_VERTICAL); header_sizer->Add(category.title_label, 0, wxALIGN_CENTER_VERTICAL);
category.enable_check = new wxCheckBox(category.page, wxID_ANY, _L("Enable")); auto* enable_sizer = new wxBoxSizer(wxHORIZONTAL);
category.enable_check->SetFont(Label::Body_13); category.enable_check = new ::CheckBox(category.page, wxID_ANY);
category.enable_check->SetToolTip(_L("Publish this filament slot in the 3MF file")); category.enable_label = add_checkbox_label(category.page, enable_sizer, category.enable_check, _L("Enable"),
header_sizer->Add(category.enable_check, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10)); _L("Publish this filament slot in the 3MF file"));
header_sizer->Add(enable_sizer, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
page_sizer->Add(header_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6)); page_sizer->Add(header_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6));
// Line 2: the "Full Publish" toggle, on its own line below the title (hidden until // Line 2: the "Full Publish" toggle, on its own line below the title (hidden until
// the slot is enabled), aligned with the colour chip above it. // the slot is enabled), aligned with the colour chip above it.
auto* full_sizer = new wxBoxSizer(wxHORIZONTAL); auto* full_sizer = new wxBoxSizer(wxHORIZONTAL);
category.full_check = new wxCheckBox(category.page, wxID_ANY, _L("Full Publish")); category.full_check = new ::CheckBox(category.page, wxID_ANY);
category.full_check->SetFont(Label::Body_13); category.full_label = add_checkbox_label(category.page, full_sizer, category.full_check, _L("Full Publish"),
category.full_check->SetToolTip(_L("Embed the entire filament of this slot in the 3MF file")); _L("Embed the entire filament of this slot in the 3MF file"));
full_sizer->Add(category.full_check, 0, wxALIGN_CENTER_VERTICAL);
category.full_line_item = page_sizer->Add(full_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6)); category.full_line_item = page_sizer->Add(full_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6));
} }
} }
@@ -1180,7 +1235,7 @@ size_t PublishSettingsDialog::category_index_for(
category.info->SetFont(Label::Body_13); category.info->SetFont(Label::Body_13);
category.list_sizer->Add(category.info, 1, wxALIGN_CENTER_HORIZONTAL | wxALL, FromDIP(10)); category.list_sizer->Add(category.info, 1, wxALIGN_CENTER_HORIZONTAL | wxALL, FromDIP(10));
category.info->Hide(); category.info->Hide();
page_sizer->Add(category.scroll, 1, wxEXPAND | wxALL, FromDIP(4)); page_sizer->Add(category.scroll, 1, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(4));
// A material slot starts disabled: its rows (and its Full Publish line) stay hidden until // A material slot starts disabled: its rows (and its Full Publish line) stay hidden until
// "Enable" is checked. // "Enable" is checked.
if (section == Section::Material) if (section == Section::Material)
@@ -1241,7 +1296,7 @@ size_t PublishSettingsDialog::subcategory_index_for(size_t category_index, const
sub.header->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); sub.header->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
auto* wrap = new wxBoxSizer(wxVERTICAL); auto* wrap = new wxBoxSizer(wxVERTICAL);
wrap->Add(sub.header, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(6)); wrap->Add(sub.header, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(6));
sub.item = category.list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(22)); sub.item = category.list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5));
} }
category.subs.push_back(std::move(sub)); category.subs.push_back(std::move(sub));
return category.subs.size() - 1; return category.subs.size() - 1;
@@ -1271,15 +1326,18 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
const size_t row_index = m_rows.size(); const size_t row_index = m_rows.size();
m_rows.push_back(std::move(row)); m_rows.push_back(std::move(row));
Row& current = m_rows[row_index]; Row& current = m_rows[row_index];
current.check = new wxCheckBox(category.scroll, wxID_ANY, label); current.check = new ::CheckBox(category.scroll, wxID_ANY);
current.check->SetFont(Label::Body_13); current.check->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& event) {
current.check->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { refresh_tab_indicators(); }); refresh_tab_indicators();
event.Skip();
});
auto* row_sizer = new wxBoxSizer(wxHORIZONTAL); auto* row_sizer = new wxBoxSizer(wxHORIZONTAL);
row_sizer->Add(current.check, 0, wxALIGN_CENTER_VERTICAL); current.check_label = add_checkbox_label(category.scroll, row_sizer, current.check, label + ":", wxEmptyString,
24 * wxGetApp().em_unit());
// The value is read-only text (incl. the Type row: the published type is the slot's // The value is read-only text (incl. the Type row: the published type is the slot's
// normalized type, not author-editable). // normalized type, not author-editable).
current.value_label = new wxStaticText(category.scroll, wxID_ANY, value, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END); current.value_label = new wxStaticText(category.scroll, wxID_ANY, value, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
current.value_label->SetFont(Label::Body_13); current.value_label->SetFont(Label::Body_14);
current.value_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); current.value_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
current.value_label->SetToolTip(unit.IsEmpty() ? value : value + " " + unit); current.value_label->SetToolTip(unit.IsEmpty() ? value : value + " " + unit);
if (kind == RowKind::Color && !value.IsEmpty()) { if (kind == RowKind::Color && !value.IsEmpty()) {
@@ -1290,14 +1348,14 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
row_sizer->Add(current.color_chip, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); row_sizer->Add(current.color_chip, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
} }
} }
row_sizer->Add(current.value_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); row_sizer->Add(current.value_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
if (!unit.IsEmpty()) { if (!unit.IsEmpty()) {
current.unit_label = new wxStaticText(category.scroll, wxID_ANY, unit); current.unit_label = new wxStaticText(category.scroll, wxID_ANY, unit);
current.unit_label->SetFont(Label::Body_13); current.unit_label->SetFont(Label::Body_14);
current.unit_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B"))); current.unit_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
row_sizer->Add(current.unit_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); row_sizer->Add(current.unit_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4));
} }
current.item = category.list_sizer->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(38)); current.item = category.list_sizer->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5));
category.rows.push_back(row_index); category.rows.push_back(row_index);
category.subs[subcategory_index].rows.push_back(row_index); category.subs[subcategory_index].rows.push_back(row_index);
} }
@@ -1306,8 +1364,10 @@ void PublishSettingsDialog::on_full_toggle(size_t category_index)
{ {
Category& cat = m_categories[category_index]; Category& cat = m_categories[category_index];
const bool full = cat.full_check->GetValue(); const bool full = cat.full_check->GetValue();
for (size_t r : cat.rows) for (size_t r : cat.rows) {
m_rows[r].check->Enable(!full); m_rows[r].check->Enable(!full);
m_rows[r].check_label->Enable(!full);
}
refresh_tab_indicators(); refresh_tab_indicators();
} }
@@ -1501,7 +1561,7 @@ void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedV
void PublishSettingsDialog::set_row_bold(Row& row, bool bold) void PublishSettingsDialog::set_row_bold(Row& row, bool bold)
{ {
// Rebase on the dialog's body font so clearing bold restores the exact original font. // Rebase on the dialog's body font so clearing bold restores the exact original font.
row.check->SetFont(bold ? Label::Body_13.Bold() : Label::Body_13); row.check_label->SetFont(bold ? Label::Body_13.Bold() : Label::Body_13);
} }
void PublishSettingsDialog::save_scroll_position(Category& category) void PublishSettingsDialog::save_scroll_position(Category& category)
@@ -1749,7 +1809,7 @@ void PublishSettingsDialog::select_all(bool value)
for (Category& cat : m_categories) for (Category& cat : m_categories)
if (cat.section == Section::Material && cat.enable_check != nullptr) if (cat.section == Section::Material && cat.enable_check != nullptr)
cat.enable_check->SetValue(value); cat.enable_check->SetValue(value);
// wxCheckBox::SetValue does not emit wxEVT_CHECKBOX, so re-run the enable handlers to // CheckBox::SetValue does not emit wxEVT_TOGGLEBUTTON, so re-run the enable handlers to
// propagate mixed-slot components and refresh visibility as if the user had clicked. // propagate mixed-slot components and refresh visibility as if the user had clicked.
for (size_t c = 0; c < m_categories.size(); ++c) for (size_t c = 0; c < m_categories.size(); ++c)
if (m_categories[c].section == Section::Material) if (m_categories[c].section == Section::Material)
+7 -3
View File
@@ -18,6 +18,7 @@
// Widgets/StaticLine.hpp). // Widgets/StaticLine.hpp).
class TextInput; class TextInput;
class StaticLine; class StaticLine;
class CheckBox;
namespace Slic3r { namespace GUI { namespace Slic3r { namespace GUI {
@@ -97,7 +98,8 @@ private:
size_t inner_index{0}; size_t inner_index{0};
bool dirty{false}; // matches a dirty base key: pre-checked + bold bool dirty{false}; // matches a dirty base key: pre-checked + bold
bool matches_filter{false}; // survives the active filter (computed by apply_filter) bool matches_filter{false}; // survives the active filter (computed by apply_filter)
wxCheckBox* check{nullptr}; ::CheckBox* check{nullptr};
wxStaticText* check_label{nullptr};
wxStaticText* value_label{nullptr}; wxStaticText* value_label{nullptr};
wxStaticText* unit_label{nullptr}; wxStaticText* unit_label{nullptr};
wxStaticBitmap* color_chip{nullptr}; // Color rows only; swatch next to the value wxStaticBitmap* color_chip{nullptr}; // Color rows only; swatch next to the value
@@ -131,11 +133,13 @@ private:
// header row is hidden. For physical slots the Full Publish toggle sits on a second // header row is hidden. For physical slots the Full Publish toggle sits on a second
// line (full_line_item) visible only when enabled; for mixed slots Enable alone implies // line (full_line_item) visible only when enabled; for mixed slots Enable alone implies
// publishing the mix definition, so no Full Publish widget exists at all. // publishing the mix definition, so no Full Publish widget exists at all.
wxCheckBox* enable_check{nullptr}; ::CheckBox* enable_check{nullptr};
wxStaticText* enable_label{nullptr};
wxSizerItem* full_line_item{nullptr}; // sizer item of the Full Publish line (physical slots only) wxSizerItem* full_line_item{nullptr}; // sizer item of the Full Publish line (physical slots only)
// "Full Publish": while checked, the whole slot preset is serialized and its rows // "Full Publish": while checked, the whole slot preset is serialized and its rows
// (incl. Color/Type) are disabled. // (incl. Color/Type) are disabled.
wxCheckBox* full_check{nullptr}; ::CheckBox* full_check{nullptr};
wxStaticText* full_label{nullptr};
// True for a mixed-color filament slot: no Material/Retraction rows; Enable publishes // True for a mixed-color filament slot: no Material/Retraction rows; Enable publishes
// the slot's gradient/ratio definition as a whole. // the slot's gradient/ratio definition as a whole.
bool is_mixed{false}; bool is_mixed{false};
+6 -200
View File
@@ -62,95 +62,12 @@ static char marker_by_type(Preset::Type type, PrinterTechnology pt)
} }
} }
std::string Option::opt_key() const { return key.size() < 2 ? std::string() : into_u8(key).substr(2); }
void FoundOption::get_marked_label_and_tooltip(const char **label_, const char **tooltip_) const void FoundOption::get_marked_label_and_tooltip(const char **label_, const char **tooltip_) const
{ {
*label_ = marked_label.c_str(); *label_ = marked_label.c_str();
*tooltip_ = tooltip.c_str(); *tooltip_ = tooltip.c_str();
} }
template<class T>
// void change_opt_key(std::string& opt_key, DynamicPrintConfig* config)
void change_opt_key(std::string &opt_key, DynamicPrintConfig *config, int &cnt)
{
T *opt_cur = static_cast<T *>(config->option(opt_key));
cnt = opt_cur->values.size();
return;
if (opt_cur->values.size() > 0) opt_key += "#" + std::to_string(0);
}
static std::string get_key(const std::string &opt_key, Preset::Type type) { return std::to_string(int(type)) + ";" + opt_key; }
void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
{
auto emplace = [this, type](const std::string key, const wxString &label) {
const GroupAndCategory &gc = groups_and_categories[key];
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return;
wxString suffix;
wxString suffix_local;
if (gc.category == "Machine limits") {
//suffix = key.back() == '1' ? L("Stealth") : L("Normal");
suffix = key.back() == '1' ? wxEmptyString : wxEmptyString;
suffix_local = " " + _(suffix);
suffix = " " + suffix;
}
if (!label.IsEmpty())
options.emplace_back(Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(),
_(gc.group).ToStdWstring(), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring()});
};
for (std::string opt_key : config->keys()) {
const ConfigOptionDef &opt = config->def()->options.at(opt_key);
if (opt.mode > mode) continue;
int cnt = 0;
if ((type == Preset::TYPE_SLA_MATERIAL || type == Preset::TYPE_PRINTER || type == Preset::TYPE_PRINT) && opt_key != "printable_area")
switch (config->option(opt_key)->type()) {
case coInts: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
case coBools: change_opt_key<ConfigOptionBools>(opt_key, config, cnt); break;
case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break;
case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break;
case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break;
case coFloatsOrPercents: change_opt_key<ConfigOptionVector<FloatOrPercent>>(opt_key, config, cnt); break;
case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break;
// BBS
case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
default: break;
}
if (type == Preset::TYPE_FILAMENT && filament_options_with_variant.find(opt_key) != filament_options_with_variant.end())
opt_key += "#0";
wxString label = opt.full_label.empty() ? opt.label : opt.full_label;
std::string key = get_key(opt_key, type);
if (cnt == 0)
emplace(key, label);
else
for (int i = 0; i < cnt; ++i)
// ! It's very important to use "#". opt_key#n is a real option key used in GroupAndCategory
emplace(key + "#" + std::to_string(i), label);
}
}
inline void OptionsSearcher::sort_options()
{
std::sort(options.begin(), options.end(), [](const Option &o1, const Option &o2) { return o1.label < o2.label; });
Option * last = nullptr;
for (auto& opt : options) {
if (last && last->label == opt.label && last->group == opt.group && last->type == opt.type && last->category != opt.category) {
last->multi_category = true;
opt.multi_category = true;
}
last = &opt;
}
}
// Mark a string using ColorMarkerStart and ColorMarkerEnd symbols // Mark a string using ColorMarkerStart and ColorMarkerEnd symbols
static std::wstring mark_string(const std::wstring &str, const std::vector<uint16_t> &matches, Preset::Type type, PrinterTechnology pt) static std::wstring mark_string(const std::wstring &str, const std::vector<uint16_t> &matches, Preset::Type type, PrinterTechnology pt)
{ {
@@ -235,7 +152,8 @@ bool OptionsSearcher::search(const std::string &search, bool force /* = false*/,
return wxString(marker_by_type(opt.type, printer_technology)) + opt.category_local + sep + opt.group_local + sep + opt.label_local; return wxString(marker_by_type(opt.type, printer_technology)) + opt.category_local + sep + opt.group_local + sep + opt.label_local;
}; };
std::vector<uint16_t> matches, matches2; std::vector<uint16_t> matches, matches2;
const std::vector<Option> &options = m_index.options();
for (size_t i = 0; i < options.size(); i++) { for (size_t i = 0; i < options.size(); i++) {
const Option &opt = options[i]; const Option &opt = options[i];
if (full_list) { if (full_list) {
@@ -307,128 +225,21 @@ OptionsSearcher::~OptionsSearcher() {}
void OptionsSearcher::init(std::vector<InputInfo> input_values) void OptionsSearcher::init(std::vector<InputInfo> input_values)
{ {
options.clear(); m_index.init(std::move(input_values));
for (auto i : input_values) append_options(i.config, i.type, i.mode);
sort_options();
search(search_line, true, search_type); search(search_line, true, search_type);
} }
void OptionsSearcher::apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode) void OptionsSearcher::apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
{ {
if (options.empty()) return; if (m_index.apply(config, type, mode))
search(search_line, true, search_type);
options.erase(std::remove_if(options.begin(), options.end(), [type](Option opt) { return opt.type == type; }), options.end());
append_options(config, type, mode);
sort_options();
search(search_line, true, search_type);
} }
const Option &OptionsSearcher::get_option(size_t pos_in_filter) const const Option &OptionsSearcher::get_option(size_t pos_in_filter) const
{ {
assert(pos_in_filter != size_t(-1) && found[pos_in_filter].option_idx != size_t(-1)); assert(pos_in_filter != size_t(-1) && found[pos_in_filter].option_idx != size_t(-1));
return options[found[pos_in_filter].option_idx]; return m_index.option_at(found[pos_in_filter].option_idx);
}
const Option &OptionsSearcher::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const
{
auto not_found = [&variant_index]() -> const Option& {
static const Option empty_option;
variant_index = -2;
return empty_option;
};
variant_index = -1;
std::string opt_key2 = opt_key;
if (auto n = opt_key.find('#'); n != std::string::npos) {
variant_index = std::atoi(opt_key.c_str() + n + 1);
opt_key2 = opt_key.substr(0, n);
}
const std::wstring key = boost::nowide::widen(get_key(opt_key2, type));
auto it = std::lower_bound(options.begin(), options.end(), Option({key}));
if (it == options.end()) return not_found();
if (it->key == key) {
variant_index = -1;
} else {
const std::wstring prefix = key + L"#";
it = std::lower_bound(it, options.end(), Option({prefix}));
if (it == options.end() || it->key.compare(0, prefix.length(), prefix) != 0)
return not_found();
// Orca: Copy-parameters dialogs request the base key, without a vector index.
if (variant_index < 0) return *it;
const bool has_mode = type == Preset::TYPE_PRINTER && printer_options_with_variant_2.count(opt_key2) > 0;
const bool has_variant =
(type == Preset::TYPE_PRINT && print_options_with_variant.count(opt_key2) > 0) ||
(type == Preset::TYPE_FILAMENT && filament_options_with_variant.count(opt_key2) > 0) ||
(type == Preset::TYPE_PRINTER && printer_options_with_variant_1.count(opt_key2) > 0) || has_mode;
if (!has_variant || has_mode) {
// Orca: Machine limits store (Normal, Silent) pairs per variant; the UI registers only #0/#1.
const std::wstring indexed_key = has_mode ? prefix + std::to_wstring(variant_index % 2) :
boost::nowide::widen(get_key(opt_key, type));
it = std::lower_bound(it, options.end(), Option({indexed_key}));
if (it == options.end() || it->key != indexed_key)
return not_found();
if (!has_variant)
variant_index = -1;
}
}
return options[it - options.begin()];
}
static Option create_option(const std::string &opt_key, const wxString &label, Preset::Type type, const GroupAndCategory &gc)
{
wxString suffix;
wxString suffix_local;
if (gc.category == "Machine limits") {
//suffix = opt_key.back() == '1' ? L("Stealth") : L("Normal");
suffix = opt_key.back() == '1' ? wxEmptyString : wxEmptyString;
suffix_local = " " + _(suffix);
suffix = " " + suffix;
}
wxString category = gc.category;
if (type == Preset::TYPE_PRINTER && category.Contains("Extruder ")) {
std::string opt_idx = opt_key.substr(opt_key.find("#") + 1);
category = wxString::Format("%s %d", "Extruder", atoi(opt_idx.c_str()) + 1);
}
return Option{boost::nowide::widen(get_key(opt_key, type)),
type,
(label + suffix).ToStdWstring(),
(_(label) + suffix_local).ToStdWstring(),
gc.group.ToStdWstring(),
_(gc.group).ToStdWstring(),
gc.category.ToStdWstring(),
GUI::Tab::translate_category(category, type).ToStdWstring()};
}
Option OptionsSearcher::get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const
{
std::string key = get_key(opt_key, type);
auto it = std::lower_bound(options.begin(), options.end(), Option({boost::nowide::widen(key)}));
// BBS: return the 0th option when not found in searcher caused by mode difference
if (it == options.end()) return options[0];
if (it->key == boost::nowide::widen(key)) return options[it - options.begin()];
if (groups_and_categories.find(key) == groups_and_categories.end()) {
size_t pos = key.find('#');
if (pos == std::string::npos) return options[it - options.begin()];
std::string zero_opt_key = key.substr(0, pos + 1) + "0";
if (groups_and_categories.find(zero_opt_key) == groups_and_categories.end()) return options[it - options.begin()];
return create_option(opt_key, label, type, groups_and_categories.at(zero_opt_key));
}
const GroupAndCategory &gc = groups_and_categories.at(key);
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return options[it - options.begin()];
return create_option(opt_key, label, type, gc);
} }
void OptionsSearcher::show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow* ssearch_btn) void OptionsSearcher::show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow* ssearch_btn)
@@ -455,11 +266,6 @@ void OptionsSearcher::dlg_msw_rescale()
{ {
if (search_dialog) search_dialog->msw_rescale(); if (search_dialog) search_dialog->msw_rescale();
} }
void OptionsSearcher::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category)
{
groups_and_categories[get_key(opt_key, type)] = GroupAndCategory{group, category};
}
//------------------------------------------ //------------------------------------------
// SearchItem // SearchItem
//------------------------------------------ //------------------------------------------
+10 -54
View File
@@ -19,6 +19,7 @@
#include "wxExtensions.hpp" #include "wxExtensions.hpp"
#include "GUI_Utils.hpp" #include "GUI_Utils.hpp"
#include "libslic3r/Preset.hpp" #include "libslic3r/Preset.hpp"
#include "SettingsIndex.hpp"
#include "Widgets/ScrolledWindow.hpp" #include "Widgets/ScrolledWindow.hpp"
#include "Widgets/TextInput.hpp" #include "Widgets/TextInput.hpp"
#include "Widgets/PopupWindow.hpp" #include "Widgets/PopupWindow.hpp"
@@ -34,39 +35,6 @@ namespace Search {
class SearchDialog; class SearchDialog;
struct InputInfo
{
DynamicPrintConfig *config{nullptr};
Preset::Type type{Preset::TYPE_INVALID};
ConfigOptionMode mode{comSimple};
};
struct GroupAndCategory
{
wxString group;
wxString category;
};
struct Option
{
// bool operator<(const Option& other) const { return other.label > this->label; }
bool operator<(const Option &other) const { return other.key > this->key; }
// Fuzzy matching works at a character level. Thus matching with wide characters is a safer bet than with short characters,
// though for some languages (Chinese?) it may not work correctly.
std::wstring key;
Preset::Type type{Preset::TYPE_INVALID};
std::wstring label;
std::wstring label_local;
std::wstring group;
std::wstring group_local;
std::wstring category;
std::wstring category_local;
bool multi_category { false };
std::string opt_key() const;
};
struct FoundOption struct FoundOption
{ {
// UTF8 encoding, to be consumed by ImGUI by reference. // UTF8 encoding, to be consumed by ImGUI by reference.
@@ -90,25 +58,19 @@ struct OptionViewParameters
class OptionsSearcher class OptionsSearcher
{ {
std::string search_line; SettingsIndex m_index;
Preset::Type search_type = Preset::TYPE_INVALID;
std::map<std::string, GroupAndCategory> groups_and_categories; std::string search_line;
PrinterTechnology printer_technology; Preset::Type search_type = Preset::TYPE_INVALID;
PrinterTechnology printer_technology;
std::vector<Option> options{};
std::vector<FoundOption> found{}; std::vector<FoundOption> found{};
void append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
void sort_options();
void sort_found() void sort_found()
{ {
std::sort(found.begin(), found.end(), std::sort(found.begin(), found.end(),
[](const FoundOption &f1, const FoundOption &f2) { return f1.outScore > f2.outScore || (f1.outScore == f2.outScore && f1.label < f2.label); }); [](const FoundOption &f1, const FoundOption &f2) { return f1.outScore > f2.outScore || (f1.outScore == f2.outScore && f1.label < f2.label); });
}; };
size_t options_size() const { return options.size(); }
size_t found_size() const { return found.size(); } size_t found_size() const { return found.size(); }
public: public:
@@ -119,32 +81,26 @@ public:
OptionsSearcher(); OptionsSearcher();
~OptionsSearcher(); ~OptionsSearcher();
SettingsIndex & index() { return m_index; }
const SettingsIndex &index() const { return m_index; }
// Rebuild the catalog and re-run the current query so the cached results track it.
void init(std::vector<InputInfo> input_values); void init(std::vector<InputInfo> input_values);
void apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode); void apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
bool search(); bool search();
bool search(const std::string &search, bool force = false, Preset::Type type = Preset::TYPE_INVALID); bool search(const std::string &search, bool force = false, Preset::Type type = Preset::TYPE_INVALID);
void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category);
size_t size() const { return found_size(); } size_t size() const { return found_size(); }
const FoundOption &operator[](const size_t pos) const noexcept { return found[pos]; } const FoundOption &operator[](const size_t pos) const noexcept { return found[pos]; }
const Option & get_option(size_t pos_in_filter) const; const Option & get_option(size_t pos_in_filter) const;
const Option & get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const;
Option get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const;
const std::vector<FoundOption> &found_options() { return found; } const std::vector<FoundOption> &found_options() { return found; }
const GroupAndCategory & get_group_and_category(const std::string &opt_key) { return groups_and_categories[opt_key]; }
std::string & search_string() { return search_line; } std::string & search_string() { return search_line; }
void set_printer_technology(PrinterTechnology pt) { printer_technology = pt; } void set_printer_technology(PrinterTechnology pt) { printer_technology = pt; }
void sort_options_by_key()
{
std::sort(options.begin(), options.end(), [](const Option &o1, const Option &o2) { return o1.key < o2.key; });
}
void sort_options_by_label() { sort_options(); }
void show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow *ssearch_btn); void show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow *ssearch_btn);
void dlg_sys_color_changed(); void dlg_sys_color_changed();
void dlg_msw_rescale(); void dlg_msw_rescale();
+258
View File
@@ -0,0 +1,258 @@
#include "SettingsIndex.hpp"
#include <algorithm>
#include <cstddef>
#include <cstdlib>
#include <string>
#include <vector>
#include <boost/nowide/convert.hpp>
#include "GUI.hpp"
#include "I18N.hpp"
#include "Tab.hpp"
#include "libslic3r/PrintConfig.hpp"
namespace Slic3r {
using GUI::into_u8;
namespace Search {
static std::string get_key(const std::string &opt_key, Preset::Type type) { return std::to_string(int(type)) + ";" + opt_key; }
std::string Option::opt_key() const { return key.size() < 2 ? std::string() : into_u8(key).substr(2); }
template<class T>
// void change_opt_key(std::string& opt_key, DynamicPrintConfig* config)
void change_opt_key(std::string &opt_key, DynamicPrintConfig *config, int &cnt)
{
T *opt_cur = static_cast<T *>(config->option(opt_key));
cnt = opt_cur->values.size();
return;
if (opt_cur->values.size() > 0) opt_key += "#" + std::to_string(0);
}
// Single assembler for an indexed Option, shared by append_options() and create_option(), so a new
// Option field is only wired up in one place.
static Option make_option(const std::string &key, Preset::Type type, const wxString &label, const GroupAndCategory &gc,
ConfigOptionMode mode, const std::string &tooltip, bool rewrite_extruder_category)
{
wxString suffix;
wxString suffix_local;
if (gc.category == "Machine limits") {
//suffix = key.back() == '1' ? L("Stealth") : L("Normal");
suffix = key.back() == '1' ? wxEmptyString : wxEmptyString;
suffix_local = " " + _(suffix);
suffix = " " + suffix;
}
wxString category = gc.category;
if (rewrite_extruder_category && type == Preset::TYPE_PRINTER && category.Contains("Extruder ")) {
std::string opt_idx = key.substr(key.find("#") + 1);
category = wxString::Format("%s %d", "Extruder", atoi(opt_idx.c_str()) + 1);
}
Option option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(),
gc.group.ToStdWstring(), _(gc.group).ToStdWstring(), into_u8(gc.icon), gc.category.ToStdWstring(),
GUI::Tab::translate_category(category, type).ToStdWstring(), false, mode, tooltip, gc.path,
// The settings page draws Line::label; carrying it lets the Speed Dial name a setting
// the way the page does. `label`/`label_local` stay the search-oriented name.
into_u8(gc.line_label)};
return option;
}
void SettingsIndex::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
{
for (std::string opt_key : config->keys()) {
const ConfigOptionDef &opt = config->def()->options.at(opt_key);
const bool in_filtered = opt.mode <= mode;
int cnt = 0;
if ((type == Preset::TYPE_SLA_MATERIAL || type == Preset::TYPE_PRINTER || type == Preset::TYPE_PRINT) && opt_key != "printable_area")
switch (config->option(opt_key)->type()) {
case coInts: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
case coBools: change_opt_key<ConfigOptionBools>(opt_key, config, cnt); break;
case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break;
case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break;
case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break;
case coFloatsOrPercents: change_opt_key<ConfigOptionVector<FloatOrPercent>>(opt_key, config, cnt); break;
case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break;
// BBS
case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
default: break;
}
if (type == Preset::TYPE_FILAMENT && filament_options_with_variant.find(opt_key) != filament_options_with_variant.end())
opt_key += "#0";
wxString label = opt.full_label.empty() ? opt.label : opt.full_label;
std::string key = get_key(opt_key, type);
auto add = [&](const std::string &k) {
const GroupAndCategory &gc = m_groups_and_categories[k];
if (gc.group.IsEmpty() || gc.category.IsEmpty() || label.IsEmpty()) return;
const std::string tooltip = into_u8(_(opt.tooltip));
if (in_filtered)
m_options.emplace_back(make_option(k, type, label, gc, opt.mode, tooltip, false));
m_all_modes.emplace_back(make_option(k, type, label, gc, opt.mode, tooltip, false));
};
if (cnt == 0)
add(key);
else
for (int i = 0; i < cnt; ++i)
// ! It's very important to use "#". opt_key#n is a real option key used in GroupAndCategory
add(key + "#" + std::to_string(i));
}
}
void SettingsIndex::sort_options()
{
// Both views are label-sorted and multi_category-marked. They are separate consumers (the sidebar
// search and the Speed Dial); keeping them in sync here prevents the all-modes view from silently
// diverging in order or flags.
auto sort_and_mark = [](std::vector<Option> &v) {
std::sort(v.begin(), v.end(), [](const Option &o1, const Option &o2) { return o1.label < o2.label; });
Option *last = nullptr;
for (auto &opt : v) {
if (last && last->label == opt.label && last->group == opt.group && last->type == opt.type && last->category != opt.category) {
last->multi_category = true;
opt.multi_category = true;
}
last = &opt;
}
};
sort_and_mark(m_options);
sort_and_mark(m_all_modes);
}
void SettingsIndex::init(std::vector<InputInfo> input_values)
{
m_options.clear();
m_all_modes.clear();
for (auto i : input_values) append_options(i.config, i.type, i.mode);
sort_options();
}
bool SettingsIndex::apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
{
// m_all_modes is a separate consumer (the Speed Dial), so "nothing initialised yet" means both
// views are empty - the mode-filtered m_options can be empty while m_all_modes is not.
if (m_options.empty() && m_all_modes.empty()) return false;
m_options.erase(std::remove_if(m_options.begin(), m_options.end(), [type](Option opt) { return opt.type == type; }), m_options.end());
m_all_modes.erase(std::remove_if(m_all_modes.begin(), m_all_modes.end(), [type](Option opt) { return opt.type == type; }), m_all_modes.end());
append_options(config, type, mode);
sort_options();
return true;
}
const Option &SettingsIndex::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const
{
auto not_found = [&variant_index]() -> const Option & {
static const Option empty_option;
variant_index = -2;
return empty_option;
};
variant_index = -1;
std::string opt_key2 = opt_key;
if (auto n = opt_key.find('#'); n != std::string::npos) {
variant_index = std::atoi(opt_key.c_str() + n + 1);
opt_key2 = opt_key.substr(0, n);
}
const std::wstring key = boost::nowide::widen(get_key(opt_key2, type));
auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({key}));
if (it == m_options.end()) return not_found();
if (it->key == key) {
variant_index = -1;
} else {
const std::wstring prefix = key + L"#";
it = std::lower_bound(it, m_options.end(), Option({prefix}));
if (it == m_options.end() || it->key.compare(0, prefix.length(), prefix) != 0)
return not_found();
// Orca: Copy-parameters dialogs request the base key, without a vector index.
if (variant_index < 0) return *it;
const bool has_mode = type == Preset::TYPE_PRINTER && printer_options_with_variant_2.count(opt_key2) > 0;
const bool has_variant =
(type == Preset::TYPE_PRINT && print_options_with_variant.count(opt_key2) > 0) ||
(type == Preset::TYPE_FILAMENT && filament_options_with_variant.count(opt_key2) > 0) ||
(type == Preset::TYPE_PRINTER && printer_options_with_variant_1.count(opt_key2) > 0) || has_mode;
if (!has_variant || has_mode) {
// Orca: Machine limits store (Normal, Silent) pairs per variant; the UI registers only #0/#1.
const std::wstring indexed_key = has_mode ? prefix + std::to_wstring(variant_index % 2) :
boost::nowide::widen(get_key(opt_key, type));
it = std::lower_bound(it, m_options.end(), Option({indexed_key}));
if (it == m_options.end() || it->key != indexed_key)
return not_found();
if (!has_variant)
variant_index = -1;
}
}
return m_options[it - m_options.begin()];
}
static Option create_option(const std::string &opt_key, const wxString &label, Preset::Type type, const GroupAndCategory &gc)
{
return make_option(get_key(opt_key, type), type, label, gc, comSimple, std::string(), true);
}
Option SettingsIndex::get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const
{
std::string key = get_key(opt_key, type);
auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({boost::nowide::widen(key)}));
// BBS: return the 0th option when not found in searcher caused by mode difference
if (it == m_options.end()) return m_options[0];
if (it->key == boost::nowide::widen(key)) return m_options[it - m_options.begin()];
if (m_groups_and_categories.find(key) == m_groups_and_categories.end()) {
size_t pos = key.find('#');
if (pos == std::string::npos) return m_options[it - m_options.begin()];
std::string zero_opt_key = key.substr(0, pos + 1) + "0";
if (m_groups_and_categories.find(zero_opt_key) == m_groups_and_categories.end()) return m_options[it - m_options.begin()];
return create_option(opt_key, label, type, m_groups_and_categories.at(zero_opt_key));
}
const GroupAndCategory &gc = m_groups_and_categories.at(key);
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return m_options[it - m_options.begin()];
return create_option(opt_key, label, type, gc);
}
void SettingsIndex::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon)
{
// Update fields in place so a page rebuild (get_option after set_path) doesn't drop the
// previously recorded wiki path.
GroupAndCategory &gc = m_groups_and_categories[get_key(opt_key, type)];
gc.group = group;
gc.category = category;
gc.icon = icon;
}
void SettingsIndex::set_path(const std::string &opt_key, Preset::Type type, const std::string &path)
{
if (path.empty())
return;
m_groups_and_categories[get_key(opt_key, type)].path = path;
}
void SettingsIndex::set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label)
{
if (label.IsEmpty())
return;
m_groups_and_categories[get_key(opt_key, type)].line_label = label;
}
} // namespace Search
} // namespace Slic3r

Some files were not shown because too many files have changed in this diff Show More