Compare commits

..
Author SHA1 Message Date
SoftFever 60ebbf7daa Stop estimating a prime tower for a single used filament
The no-tower case was gated on there being no purge volume, but the SEMM flush matrix reads every configured slot and is nonzero even when only one filament is used, so the plater preview drew a tower the print would never contain.
2026-09-19 00:38:22 +08: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
Lam Wei Lun 4ea50a33e4 Testing macOS fixes 2026-09-18 19:07:03 +08:00
SoftFever 1159ca5f7f Merge branch 'main' into weilun/speed_dial 2026-09-18 17:28:24 +08:00
Lam Wei Lun 8f4e3dde55 Merge main 2026-09-18 15:59: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
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
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
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
131 changed files with 73806 additions and 3639 deletions
+5 -2
View File
@@ -449,10 +449,13 @@ jobs:
# 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
# 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 \
! -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"
- name: Upload flatpak test asset
uses: actions/upload-artifact@v7
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
+4
View File
@@ -125,6 +125,7 @@ src/slic3r/GUI/ThermalPreconditioningDialog.hpp
src/slic3r/GUI/Jobs/SLAImportJob.cpp
src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp
src/slic3r/GUI/AboutDialog.cpp
src/slic3r/GUI/ActionRegistry.cpp
src/slic3r/GUI/AMSMaterialsSetting.cpp
src/slic3r/GUI/ExtrusionCalibration.cpp
src/slic3r/GUI/AmsMappingPopup.cpp
@@ -159,6 +160,7 @@ src/slic3r/GUI/SelectMachinePop.cpp
src/slic3r/GUI/StatusPanel.cpp
src/slic3r/GUI/Monitor.cpp
src/slic3r/GUI/MsgDialog.cpp
src/slic3r/GUI/NativeCommands.cpp
src/slic3r/GUI/NotificationManager.hpp
src/slic3r/GUI/NotificationManager.cpp
src/slic3r/GUI/ObjectDataViewModel.cpp
@@ -179,6 +181,8 @@ src/slic3r/GUI/PublishDialog.cpp
src/slic3r/GUI/PublishSettingsDialog.cpp
src/slic3r/GUI/SavePresetDialog.cpp
src/slic3r/GUI/Search.cpp
src/slic3r/GUI/SettingsIndex.cpp
src/slic3r/GUI/SpeedDialDialog.cpp
src/slic3r/GUI/Selection.cpp
src/slic3r/GUI/SelectMachine.cpp
src/slic3r/GUI/PrePrintChecker.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",
"version": "02.04.00.07",
"version": "02.04.00.08",
"force_update": "0",
"description": "Orca Filament Library",
"filament_list": [
@@ -304,6 +304,14 @@
"name": "Generic PA-CF @System",
"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",
"sub_path": "filament/Bambu/Bambu PC @base.json"
@@ -436,6 +444,10 @@
"name": "GreenGate3D PETG @base",
"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",
"sub_path": "filament/JAYO/JAYO PETG @base.json"
@@ -636,6 +648,10 @@
"name": "Generic PLA-CF @System",
"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",
"sub_path": "filament/JAYO/JAYO PLA @base.json"
@@ -1116,6 +1132,30 @@
"name": "Fiberon PA612-CF @System",
"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",
"sub_path": "filament/Bambu/Bambu PC @System.json"
@@ -1312,6 +1352,18 @@
"name": "GreenGate3D PETG @System",
"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",
"sub_path": "filament/JAYO/BBL/JAYO PETG @BBL A1 0.4 nozzle.json"
@@ -1700,6 +1752,18 @@
"name": "Generic PLA Matte @System",
"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",
"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">
<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>
<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>
<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>
@@ -27,6 +30,7 @@
<div class="dial-count" id="count" hidden></div>
</div>
<div class="dial-list" id="list"></div>
<div class="dial-detail" id="detail" hidden></div>
</div>
</body>
</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");
const duplicateActions = [
{ id: "0123456789abcdef", title: "Repair", source: "Mesh Tools" },
{ id: "fedcba9876543210", title: "Repair", source: "Mesh Tools" }
{ id: "0123456789abcdef", title: "Repair", source: "Mesh Tools" },
{ id: "fedcba9876543210", title: "Repair", source: "Mesh Tools" }
];
assert.equal(
ctx.actionLabel(duplicateActions[0], duplicateActions),
"Repair from Mesh Tools (0123456789abcdef)",
"duplicate labels should use the opaque id without interpreting its contents"
ctx.actionLabel(duplicateActions[0], duplicateActions),
"Repair from Mesh Tools (0123456789abcdef)",
"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, "whitespace-only 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 shows the recents+pool list");
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(
ctx.selectedActionId({ zone: "list", i: 0 }, duplicateActions, [], ""),
null,
"Enter with an empty query must not resolve to an action the blank list never showed"
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), []),
"0123456789abcdef",
"Enter with an empty query resolves the first action in the recents+pool list"
);
assert.equal(
ctx.selectedActionId({ zone: "list", i: 0 }, duplicateActions, [], "rep"),
"0123456789abcdef",
"a typed query resolves the list selection"
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), []),
"0123456789abcdef",
"a typed query resolves the list selection"
);
assert.equal(
ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""),
"fedcba9876543210",
"favourites stay runnable with an empty query - the fav bar is always visible"
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [duplicateActions[0]], ""), []),
"0123456789abcdef",
"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");
+509 -185
View File
@@ -1,225 +1,549 @@
* { box-sizing: border-box; }
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;
* {
box-sizing: border-box;
}
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 {
display: flex;
flex-direction: column;
background: var(--panel, var(--orca-bg, #fff));
border: 1px solid var(--border, var(--orca-border, #ddd));
overflow: hidden;
/* why: no height cap here - the launcher reports its true natural height to C++, which
display: flex;
flex-direction: column;
background: var(--panel, var(--orca-bg, #fff));
border: 1px solid var(--border, var(--orca-border, #ddd));
border-radius: 7px;
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
bounds growth; capping the launcher would make the measurement circular. */
}
.fav-bar {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 8px;
padding: 9px 10px;
border-bottom: 1px solid var(--border, var(--orca-border, #ddd));
overflow-x: auto; /* scroll horizontally once favourites overflow the row */
scrollbar-width: thin;
/* why: keep scrollIntoView (arrow-nav) from scrolling the first/last tile flush to the edge,
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 8px;
padding: 9px 10px;
border-bottom: 1px solid var(--border, var(--orca-border, #ddd));
overflow-x: auto;
/* scroll horizontally once favourites overflow the row */
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. */
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. */
.fav-eyebrow { flex: 0 0 auto; padding: 8px 10px 0; }
.fav-eyebrow {
flex: 0 0 auto;
padding: 8px 10px 0;
}
.fav-tile {
flex: 0 0 auto; /* keep tiles full-size; don't shrink to fit - scroll instead */
width: 30px;
height: 30px;
border: 0;
border-radius: 8px;
cursor: pointer;
color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%));
font-weight: 700;
/* why: mirror .tile centering - tileCode can be 2-3 chars (e.g. "EA1") on collision, and a
bare <button> inherits 13px + UA padding, clipping the code (e.g. "AE1"). inline-flex + 12px + pad:0 fits it. */
font-size: 12px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: var(--speed-tile-bg, #f0f0f0);
border: 1px solid var(--speed-tile-border, #d8d8d8);
flex: 0 0 auto;
/* keep tiles full-size; don't shrink to fit - scroll instead */
width: 30px;
height: 30px;
border: 0;
border-radius: 8px;
cursor: pointer;
/* why: mirror .tile centering - icons/placeholder are 16px, and a bare <button> inherits
13px + UA padding, which would clip them. inline-flex + pad:0 + overflow:hidden fits them. */
font-size: 12px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: var(--speed-tile-bg, #f0f0f0);
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 {
position: fixed;
z-index: 10;
min-width: 120px;
padding: 4px;
background: var(--panel, var(--orca-bg, #fff));
border: 1px solid var(--border, var(--orca-border, #ddd));
border-radius: 7px;
box-shadow: 0 4px 16px rgba(0,0,0,.18);
position: fixed;
z-index: 10;
min-width: 120px;
padding: 4px;
background: var(--panel, var(--orca-bg, #fff));
border: 1px solid var(--border, var(--orca-border, #ddd));
border-radius: 7px;
box-shadow: 0 4px 16px rgba(0, 0, 0, .18);
}
.ctx-menu[hidden] { display: none; }
.ctx-menu[hidden] {
display: none;
}
.ctx-item {
display: block;
width: 100%;
padding: 6px 10px;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--text, var(--orca-fg, #1b1c1e));
font: inherit;
text-align: left;
cursor: pointer;
display: block;
width: 100%;
padding: 6px 10px;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--text, var(--orca-fg, #1b1c1e));
font: inherit;
text-align: left;
cursor: pointer;
}
.ctx-item:hover { background: var(--row-hover, rgba(127,127,127,.16)); }
.ctx-item:disabled { opacity: .4; cursor: default; }
.ctx-item:disabled:hover { background: transparent; }
.dial-head { flex: 0 0 auto; padding: 8px; }
.ctx-item:hover {
background: var(--row-hover, rgba(127, 127, 127, .16));
}
.ctx-item:disabled {
opacity: .4;
cursor: default;
}
.ctx-item:disabled:hover {
background: transparent;
}
.dial-head {
flex: 0 0 auto;
padding: 8px;
}
.plugin-search {
display: flex;
align-items: center;
gap: 4px;
height: 30px;
padding: 0 8px;
background: var(--panel, var(--orca-bg, #fff));
border: 1px solid var(--border, var(--orca-border, #ddd));
border-radius: 7px;
display: flex;
align-items: center;
gap: 4px;
height: 30px;
padding: 0 8px;
background: var(--panel, var(--orca-bg, #fff));
border: 1px solid var(--border, var(--orca-border, #ddd));
border-radius: 7px;
}
.plugin-search:focus-within {
border-color: var(--main-color, var(--orca-accent, #009688));
box-shadow: 0 0 0 2px rgba(0,150,136,.25);
border-color: var(--main-color, var(--orca-accent, #009688));
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 {
flex: 1;
min-width: 0;
background: transparent;
border: 0;
outline: 0;
color: var(--text, var(--orca-fg, #1b1c1e));
font: inherit;
flex: 1;
min-width: 0;
padding: 0;
background: transparent;
border: 0;
outline: 0;
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 {
display: inline-flex;
align-items: center;
justify-content: center;
width: 12px;
height: 12px;
padding: 0;
border: 0;
border-radius: 50%;
color: var(--muted, var(--orca-muted, #6b7280));
cursor: pointer;
background: rgba(127,127,127,.20);
display: inline-flex;
align-items: center;
justify-content: center;
width: 12px;
height: 12px;
padding: 0;
border: 0;
border-radius: 50%;
color: var(--muted, var(--orca-muted, #6b7280));
cursor: pointer;
background: rgba(127, 127, 127, .20);
}
.plugin-search-clear[hidden] { display: none; }
.plugin-search-clear[hidden] {
display: none;
}
.dial-count {
padding: 4px 4px 0;
text-align: left;
font-size: 11px;
color: var(--muted, var(--orca-muted, #6b7280));
padding: 4px 4px 0;
text-align: left;
font-size: 11px;
color: var(--muted, var(--orca-muted, #6b7280));
}
.dial-count[hidden] { display: none; }
.dial-count[hidden] {
display: none;
}
.dial-list {
flex: 0 1 auto;
min-height: 0;
/* ADJUST HEIGHT HERE. The popup auto-resizes to the content (HTML is the source of truth), so
flex: 0 1 auto;
min-height: 0;
/* 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
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). */
--row-h: 44px;
--rows: 5;
max-height: calc(var(--rows) * var(--row-h) + 0.3 * var(--row-h) + 4px);
overflow-y: auto;
padding: 4px 4px 6px;
scrollbar-width: thin;
--row-h: 44px;
--rows: 5;
max-height: calc(var(--rows) * var(--row-h) + 0.3 * var(--row-h) + 4px);
overflow-y: auto;
padding: 4px 4px 6px;
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 {
display: flex;
align-items: center;
gap: 10px;
min-height: 44px;
padding: 6px 8px;
border-radius: 7px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
min-height: 44px;
padding: 6px 8px;
border-radius: 7px;
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 {
flex: 0 0 auto;
width: 26px;
height: 26px;
border-radius: 6px;
color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%));
font-weight: 700;
font-size: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--speed-tile-bg, #f0f0f0);
border: 1px solid var(--speed-tile-border, #d8d8d8);
flex: 0 0 auto;
width: 26px;
height: 26px;
border-radius: 6px;
display: inline-flex;
align-items: center;
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 {
font-size: 10px;
line-height: 1.3;
color: var(--muted, var(--orca-muted, #6b7280));
opacity: .85;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 10px;
line-height: 1.3;
color: var(--muted, var(--orca-muted, #6b7280));
opacity: .85;
overflow: hidden;
text-overflow: ellipsis;
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-name mark, .row-eyebrow mark { background: var(--plugin-status-warn-bg); color: var(--plugin-status-warn); border-radius: 2px; }
.row-sc { flex: 0 0 auto; display: inline-flex; gap: 3px; }
kbd {
display: inline-flex;
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;
.row-line {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
}
.star {
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;
.row-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.star svg { display: block; }
.row:hover .star:not(.on),
.row.sel .star:not(.on) { opacity: .5; }
.star.on { opacity: 1; color: var(--main-color, var(--orca-accent, #009688)); }
.star:hover { background: rgba(127,127,127,.18); }
.row-name mark,
.row-eyebrow mark {
background: var(--plugin-status-warn-bg);
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 {
min-height: 64px;
padding: 18px 10px;
display: flex;
align-items: center;
justify-content: center;
color: var(--muted, var(--orca-muted, #6b7280));
text-align: center;
min-height: 64px;
padding: 18px 10px;
display: flex;
align-items: center;
justify-content: center;
color: var(--muted, var(--orca-muted, #6b7280));
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-tile-bg: #efefef;
--speed-tile-border: #d8d8d8;
--speed-tile-text-s: 74%;
--speed-tile-text-l: 34%;
}
:root[data-orca-theme="dark"] {
@@ -95,8 +93,6 @@
/* Speed dial icon tile treatment */
--speed-tile-bg: #34343b;
--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
+72
View File
@@ -7,6 +7,10 @@
// Fold per-character so matched offsets stay in ORIGINAL string coordinates (highlighting slices the
// original text; a separately-folded string would desync offsets).
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
}
@@ -15,6 +19,74 @@ function Norm(ch, caseSensitive) {
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) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
+15 -1
View File
@@ -5,7 +5,7 @@ const vm = require("vm"), assert = require("assert"), fs = require("fs");
const ctx = {};
vm.createContext(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.
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.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");
+18
View File
@@ -286,6 +286,8 @@ void AppConfig::set_defaults()
// 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_SPEED_DIAL_RECENT_COUNT, std::to_string(get_speed_dial_recent_count()));
if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty())
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);
}
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> result;
+8
View File
@@ -46,6 +46,11 @@ using namespace nlohmann;
#define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5
#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)
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09"
#else
@@ -394,6 +399,9 @@ public:
// dropdown on the last tab.
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;
void add_skipped_network_version(const std::string& version);
bool is_network_version_skipped(const std::string& version) const;
+5 -46
View File
@@ -8,7 +8,6 @@
#include "I18N.hpp"
#include "GCode.hpp"
#include "Exception.hpp"
#include "LifecycleEvents.hpp"
#include "ExtrusionEntity.hpp"
#include "EdgeGrid.hpp"
#include "Geometry/ConvexHull.hpp"
@@ -2477,14 +2476,6 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
m_writer.set_is_bbl_machine(print->is_BBL_printer());
print->set_started(psGCodeExport);
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx);
}
// check if any custom gcode contains keywords used by the gcode processor to
// produce time estimation and gcode toolpaths
std::vector<std::pair<std::string, std::string>> validation_res = DoExport::validate_custom_gcode(*print);
@@ -2520,22 +2511,12 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
m_processor.set_print(print);
GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor);
if (! file.is_open()) {
std::string err_msg = std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n";
BOOST_LOG_TRIVIAL(error) << err_msg << std::endl;
BOOST_LOG_TRIVIAL(error) << std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n" << std::endl;
if (!fs::exists(folder)) {
//fs::create_directory(folder);
std::string add_err_msg = "the parent path " + folder.string() +" is not there!!!";
BOOST_LOG_TRIVIAL(error) << add_err_msg << std::endl;
err_msg += add_err_msg;
BOOST_LOG_TRIVIAL(error) << "the parent path " + folder.string() +" is not there!!!" << std::endl;
}
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + err_msg;
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError(err_msg);
throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n");
}
try {
@@ -2546,18 +2527,11 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
boost::nowide::remove(path_tmp.c_str());
throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n");
}
} catch (std::exception &ex) {
} catch (std::exception & /* ex */) {
// Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown.
// Close and remove the file.
file.close();
boost::nowide::remove(path_tmp.c_str());
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + ex.what();
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw;
}
file.close();
@@ -2663,13 +2637,6 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
std::error_code ret = rename_file(path_tmp, path);
if (ret) {
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError(
std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "error code " + ret.message() + '\n' +
"Is " + path_tmp + " locked?" + '\n');
@@ -2680,15 +2647,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
BOOST_LOG_TRIVIAL(info) << "Exporting G-code finished" << log_memory_info();
print->set_done(psGCodeExport);
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
// Orca: label_object_enabled reflects whether objects are labeled in the g-code (EXCLUDE_OBJECT /
// M486), which is driven by exclude_object for every printer
if(result != nullptr)
+1
View File
@@ -710,6 +710,7 @@ private:
// Always check gcode placeholders when building in debug mode.
#if !defined(NDEBUG)
#undef ORCA_CHECK_GCODE_PLACEHOLDERS
#define ORCA_CHECK_GCODE_PLACEHOLDERS 1
#endif
+10 -9
View File
@@ -107,6 +107,12 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeT
// normalize_fdm_2 clearing enable_prime_tower. Its mixed-filament case is not modelled.
const bool need_wipe_tower = smooth_timelapse || wrapping;
// Fewer than two filaments cannot make a tool change, so only wrapping detection or smooth
// timelapse print a tower then. The flush volume is no proof of one: it is read from the
// matrix of every configured slot, nonzero even when a single one of them is used.
if (filaments_cnt < 2 && !need_wipe_tower)
return footprint;
// A tower printed for one of the reasons above has no tool change to purge for; both
// planners give it the idle depth below and nothing more.
const size_t purge_count = filaments_cnt > 1 ? (dual_nozzle ? filaments_cnt : filaments_cnt - 1) : 0;
@@ -151,14 +157,6 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeT
purges[longest_ramming].filament_change_length = float(float_at("filament_change_length", filament_ids[longest_ramming], 0.) * double(nozzles.size() - 1));
}
// Both wall types decide this together: over-reserving only wastes bed area, but reporting
// no tower for one that is built collapses the validation hull to a point.
// A tool change is a reason on its own (see the base commit); Type1 already reserves
// per filament, Type2 has only the volume, which can resolve to zero.
const bool has_purge = type1 ? !purges.empty() : volume > EPSILON;
if (!has_purge && filaments_cnt < 2 && !need_wipe_tower)
return footprint;
const double min_depth = WipeTower::get_limit_depth_by_height(float(max_object_height));
const float perimeter_width = float(nozzle_diameter) * 1.25f; // Width_To_Nozzle_Ratio
// With nothing to purge, plan_tower_new sizes the tower for wrapping detection or the
@@ -171,7 +169,10 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeT
if (!purges.empty())
side = WipeTower::estimate_rib_tower_bbox_side(purges, float(width), float(layer_height), float(nozzle_diameter), float(extra_spacing), float(rib_width), float(extra_rib_length), float(max_object_height));
else {
const double square = has_purge ? std::sqrt(volume / layer_height * extra_spacing) : idle_depth;
// Type2 squares the tower from its purge volume; Type1 with no purge list (a lone
// filament kept for timelapse or wrapping) sizes for the idle depth.
const bool has_purge = !type1 && volume > EPSILON;
const double square = has_purge ? std::sqrt(volume / layer_height * extra_spacing) : idle_depth;
side = WipeTower::rib_footprint_side(float(square), float(square), float(rib_width), float(extra_rib_length), float(max_object_height));
}
footprint.width = footprint.depth = side;
-181
View File
@@ -1,181 +0,0 @@
#pragma once
// LifecycleEvents.hpp
// --------------------
// Application lifecycle events (project, slicing, plate editing, preset, printer connection, and
// job activity) that other subsystems -- chiefly the plugin layer above libslic3r -- may want to
// observe. Lives in libslic3r rather than the plugin layer because some events fire from inside
// the slicing engine itself; see fire_lifecycle_event() below.
#include <functional>
#include <string>
#include <utility>
namespace Slic3r
{
enum class LifecycleEvent {
// Project (3mf)
NewProject,
ProjectOpened,
ProjectBeforeSave,
ProjectAfterSave,
ProjectClosed,
ProjectDirtyChanged,
// Slicing pipeline
SliceStarted,
SliceGeometryFinished,
GCodeExportStarted,
GCodeExportFinished,
SlicingJobComplete,
// Plate/model editing
ObjectAdded,
ObjectDeleted,
ObjectTransformed,
ObjectChanged,
ObjectRenamed,
PlateCreated,
PlateDeleted,
PlateSelected,
PlateRenamed,
// Preset
PresetSelected,
PresetSaved,
// Printer/device
PrintStateChanged,
DeviceOnlineChanged,
DeviceDiscovered,
DeviceSelected,
DeviceConnected,
DeviceDisconnected,
UploadStarted,
UploadFinished,
// Print/send jobs
PrintJobStarted,
PrintJobFinished,
SendJobStarted,
SendJobFinished,
};
// Scoped so callers must qualify (LifecycleEvtCode::Error, not ERROR) -- ERROR/OK collide with
// Windows macros (wingdi.h) as unqualified names.
enum class LifecycleEvtCode { Ok, Error, Warn };
struct LifecycleEventContext
{
// The primary subject identifier for the event. This is event-specific (for example, a
// project/output path, preset name, device id, or object name), must not contain status or
// prose, and may be empty when the event has no single subject.
std::string name;
// Outcome of the operation represented by the event. For state-change and start events,
// Ok means that the event occurred; it does not imply that a future operation succeeded.
LifecycleEvtCode code = LifecycleEvtCode::Ok;
// Optional human-readable detail or diagnostic text. It is not a stable parsing contract;
// machine-readable data should be represented by a dedicated field or event instead.
std::string msg;
// Stable subject/object identifier, when the source model provides one.
std::string id;
// Previous value for rename and other before/after events.
std::string previous_name;
// Device identifier for printer and job events.
std::string device_id;
// Job identifier when the originating queue/task provides one.
std::string job_id;
// Source subsystem or operation detail, suitable for filtering but not guaranteed to be
// exhaustive across versions.
std::string source;
// Plate, object, or volume index when the source uses an index rather than a stable id.
int index = -1;
// Aggregate project dirty state for ProjectDirtyChanged.
bool dirty = false;
};
inline std::string lifecycle_event_to_string(LifecycleEvent event)
{
switch (event) {
case LifecycleEvent::NewProject: return "NewProject";
case LifecycleEvent::ProjectOpened: return "ProjectOpened";
case LifecycleEvent::ProjectBeforeSave: return "ProjectBeforeSave";
case LifecycleEvent::ProjectAfterSave: return "ProjectAfterSave";
case LifecycleEvent::ProjectClosed: return "ProjectClosed";
case LifecycleEvent::ProjectDirtyChanged: return "ProjectDirtyChanged";
case LifecycleEvent::SliceStarted: return "SliceStarted";
case LifecycleEvent::SliceGeometryFinished: return "SliceGeometryFinished";
case LifecycleEvent::GCodeExportStarted: return "GCodeExportStarted";
case LifecycleEvent::GCodeExportFinished: return "GCodeExportFinished";
case LifecycleEvent::SlicingJobComplete: return "SlicingJobComplete";
case LifecycleEvent::ObjectAdded: return "ObjectAdded";
case LifecycleEvent::ObjectDeleted: return "ObjectDeleted";
case LifecycleEvent::ObjectTransformed: return "ObjectTransformed";
case LifecycleEvent::ObjectChanged: return "ObjectChanged";
case LifecycleEvent::ObjectRenamed: return "ObjectRenamed";
case LifecycleEvent::PlateCreated: return "PlateCreated";
case LifecycleEvent::PlateDeleted: return "PlateDeleted";
case LifecycleEvent::PlateSelected: return "PlateSelected";
case LifecycleEvent::PlateRenamed: return "PlateRenamed";
case LifecycleEvent::PresetSelected: return "PresetSelected";
case LifecycleEvent::PresetSaved: return "PresetSaved";
case LifecycleEvent::PrintStateChanged: return "PrintStateChanged";
case LifecycleEvent::DeviceOnlineChanged: return "DeviceOnlineChanged";
case LifecycleEvent::DeviceDiscovered: return "DeviceDiscovered";
case LifecycleEvent::DeviceSelected: return "DeviceSelected";
case LifecycleEvent::DeviceConnected: return "DeviceConnected";
case LifecycleEvent::DeviceDisconnected: return "DeviceDisconnected";
case LifecycleEvent::UploadStarted: return "UploadStarted";
case LifecycleEvent::UploadFinished: return "UploadFinished";
case LifecycleEvent::PrintJobStarted: return "PrintJobStarted";
case LifecycleEvent::PrintJobFinished: return "PrintJobFinished";
case LifecycleEvent::SendJobStarted: return "SendJobStarted";
case LifecycleEvent::SendJobFinished: return "SendJobFinished";
default: return "Unknown";
}
}
inline std::string lifecycle_evt_code_to_string(LifecycleEvtCode code)
{
switch (code) {
case LifecycleEvtCode::Ok: return "Ok";
case LifecycleEvtCode::Error: return "Error";
case LifecycleEvtCode::Warn: return "Warn";
default: return "Unknown";
}
}
// Global cross-layer seam (mirrors ConfigBase::set_resolve_capability_fn): any libslic3r code can
// fire a lifecycle event without depending on the plugin layer above it, which installs the
// dispatcher here at startup. Not tied to Print/GCode specifically, since nothing here should
// require callers to hold a Print& just to report an event.
using LifecycleHookFn = std::function<void(LifecycleEvent, const LifecycleEventContext&)>;
inline LifecycleHookFn& lifecycle_hook_fn()
{
static LifecycleHookFn fn;
return fn;
}
inline void set_lifecycle_hook_fn(LifecycleHookFn fn) { lifecycle_hook_fn() = std::move(fn); }
inline void fire_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx)
{
if (const LifecycleHookFn& fn = lifecycle_hook_fn(); fn)
fn(event, ctx);
}
}
-10
View File
@@ -47,7 +47,6 @@
#include <boost/log/trivial.hpp>
#include "libslic3r.h"
#include "LifecycleEvents.hpp"
#include "Utils.hpp"
#include "Time.hpp"
#include "PlaceholderParser.hpp"
@@ -2973,7 +2972,6 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
// 1) Find the preset with a new_name or create a new one,
// initialize it with the edited config.
auto it = this->find_preset_internal(new_name);
const bool preset_existed = (it != m_presets.end() && it->name == new_name);
if (it != m_presets.end() && it->name == new_name) {
// Preset with the same name found.
Preset &preset = *it;
@@ -3081,14 +3079,6 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
this->get_selected_preset().save(&(parent_preset->config));
else
this->get_selected_preset().save(nullptr);
{
LifecycleEventContext ctx;
ctx.name = new_name;
ctx.msg = preset_existed ? "overwrite" : "new";
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::PresetSaved, ctx);
}
}
// A detached standalone preset for the Full Publish receiver: create a user preset holding
+1 -39
View File
@@ -14,7 +14,6 @@
#include "Flow.hpp"
#include "Geometry/ConvexHull.hpp"
#include "I18N.hpp"
#include "LifecycleEvents.hpp"
#include "ShortestPath.hpp"
#include "Thread.hpp"
#include "Time.hpp"
@@ -2695,13 +2694,6 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (m_objects.empty())
return;
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
}
for (PrintObject *obj : m_objects)
obj->clear_shared_object();
@@ -3320,13 +3312,6 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
BOOST_LOG_TRIVIAL(info) << "Slicing process finished." << log_memory_info();
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
}
}
// G-code export process, running at a background thread.
@@ -4926,14 +4911,6 @@ void Print::set_gcode_file_invalidated()
//BBS: add gcode file preload logic
void Print::export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb)
{
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx);
}
try {
GCodeProcessor processor;
GCodeProcessor::s_IsBBLPrinter = is_BBL_printer();
@@ -4953,28 +4930,13 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
*result = std::move(processor.extract_result());
result->filament_change_sequence = filament_seq_loaded;
result->nozzle_change_sequence = nozzle_seq_loaded;
} catch (std::exception &ex) {
} catch (std::exception & /* ex */) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = file + "\n" + ex.what();
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError(
std::string("Failed to process the G-code file ") + file + " from previous 3mf\n");
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": process the G-code file %1% successfully")%file.c_str();
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
}
std::tuple<float, float> Print::object_skirt_offset(double margin_height) const
+4 -2
View File
@@ -14,8 +14,6 @@ set(SLIC3R_GUI_SOURCES
GUI/2DBed.hpp
GUI/3DBed.cpp
GUI/3DBed.hpp
GUI/Widgets/StaticGroup.cpp
GUI/Widgets/StaticGroup.hpp
GUI/3DScene.cpp
GUI/3DScene.hpp
GUI/Widgets/FilamentLoad.cpp
@@ -129,6 +127,8 @@ set(SLIC3R_GUI_SOURCES
GUI/SpeedDialDialog.hpp
GUI/ActionRegistry.cpp
GUI/ActionRegistry.hpp
GUI/NativeCommands.cpp
GUI/NativeCommands.hpp
GUI/PluginsConfigDialog.cpp
GUI/PluginsConfigDialog.hpp
GUI/ProcessRunner.cpp
@@ -476,6 +476,8 @@ set(SLIC3R_GUI_SOURCES
GUI/SkipPartCanvas.hpp
GUI/Search.cpp
GUI/Search.hpp
GUI/SettingsIndex.cpp
GUI/SettingsIndex.hpp
GUI/Selection.cpp
GUI/Selection.hpp
GUI/SelectMachine.cpp
+523 -60
View File
@@ -3,20 +3,48 @@
#include "GUI.hpp"
#include "GUI_App.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 <libslic3r/AppConfig.hpp>
#include <libslic3r/Config.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <wx/thread.h>
#include <boost/filesystem.hpp>
#include <boost/nowide/convert.hpp>
#include <algorithm>
#include <cmath>
#include <ctime>
#include <exception>
#include <iterator>
#include <string>
#include <unordered_map>
#include <unordered_set>
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 {
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)
{
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)
{
wxGetApp().app_config->set(kConfigSection, key, j.dump());
}
void write_section(const char* key, const nlohmann::json& j) { wxGetApp().app_config->set(kConfigSection, key, j.dump()); }
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)
return 0.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);
}
@@ -79,26 +102,25 @@ struct PluginScriptAction : AppAction
// The id an action for (plugin_key, capability) would have - lets refresh_capability
// remove a gone capability without materialising the action.
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)
: AppAction(kIdPrefix,
capability_in.empty() ? plugin_key_in : capability_in, // title
plugin_key_in, // source_key
std::move(source_name)),
plugin_key(std::move(plugin_key_in)), capability(std::move(capability_in))
capability_in.empty() ? plugin_key_in : capability_in, // title
plugin_key_in, // source_key
std::move(source_name))
, 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;
const ExecutionResult result = PluginManager::instance().run_script_capability(plugin_key, capability, error);
if (!error.empty())
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.");
return {skipped ? AppActionRunResult::Level::Info : AppActionRunResult::Level::Success,
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,
// enabled script capability.
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability,
const std::string& source_name)
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability, const std::string& source_name)
{
PluginManager& manager = PluginManager::instance();
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);
}
// ---- 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
ActionRegistry::~ActionRegistry() = default;
void ActionRegistry::init()
{
assert(wxThread::IsMain());
@@ -151,18 +315,12 @@ void ActionRegistry::init()
// Subscribe before enumerating so a concurrent load cannot land between the initial
// snapshot and callback registration. Duplicate notifications are safe: upsert is by
// id and the m_actions scan in refresh_source is idempotent.
manager.subscribe_on_load_callback(
[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_load_callback([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_capability_load_callback(
[on_capability](const PluginCapabilityId& capability) {
on_capability(capability, ActionChange::Added);
});
[on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Added); });
manager.subscribe_on_capability_unload_callback(
[on_capability](const PluginCapabilityId& capability) {
on_capability(capability, ActionChange::Removed);
});
[on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Removed); });
// enumerate current script capabilities
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)) {
if (!capability)
continue;
const std::string& key = capability->audit_plugin_key();
auto it = source_names.find(key);
const std::string& key = capability->audit_plugin_key();
auto it = source_names.find(key);
const std::string& source_name = it == source_names.end() ? key : it->second;
if (auto action = make_action(key, capability->name(), source_name))
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)
@@ -198,7 +384,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
if (change == ActionChange::Removed)
return;
PluginManager& manager = PluginManager::instance();
PluginManager& manager = PluginManager::instance();
const std::string source_name = find_loaded_source_name(manager, plugin_key);
for (const auto& capability : manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Script)) {
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,
ActionChange change)
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change)
{
assert(wxThread::IsMain());
@@ -233,7 +418,7 @@ void ActionRegistry::upsert(std::unique_ptr<AppAction> action)
return;
seed_state(*action);
std::string id = action->id();
std::string id = action->id();
std::shared_ptr<AppAction> stored = std::move(action);
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
{
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();
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()) {
a.count = it->value("count", 0);
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 -----------------------------------------------------------
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();
}
AppAction* ActionRegistry::find(const std::string& id)
{
return const_cast<AppAction*>(by_id(id));
}
AppAction* ActionRegistry::find(const std::string& id) { return const_cast<AppAction*>(by_id(id)); }
// ---- dispatch + write-through ----------------------------------------------
AppActionRunResult ActionRegistry::run(const std::string& id)
AppActionRunResult ActionRegistry::run(const std::string& id, const std::string& param)
{
assert(wxThread::IsMain());
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
// keep-alive preserves the action until run returns.
std::shared_ptr<AppAction> keep = it->second;
AppActionRunResult o = keep->run();
AppActionRunResult o = keep->run(param);
if (o.level == AppActionRunResult::Level::Busy)
return o;
// Bump stats (write-through). Re-read to avoid clobbering a concurrent field.
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();
nlohmann::json& e = stats[id];
if (!e.is_object())
@@ -300,22 +492,37 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
e["count"] = e.value("count", 0) + 1;
e["last"] = (long long) std::time(nullptr);
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;
}
void ActionRegistry::set_favourite(const std::string& id, bool on)
bool ActionRegistry::set_favourite(const std::string& id, bool on)
{
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);
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);
}
if (!on && it != favs.end())
favs.erase(it);
write_section("favourite_actions", nlohmann::json(favs));
if (AppAction* live = find(id))
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)
@@ -325,16 +532,184 @@ void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
std::vector<std::string> next;
// keep the requested order, but only ids that are actually favourites (guard a bad payload)
for (const auto& id : ids)
if (std::find(cur.begin(), cur.end(), id) != cur.end() &&
std::find(next.begin(), next.end(), id) == next.end())
if (std::find(cur.begin(), cur.end(), id) != cur.end() && std::find(next.begin(), next.end(), id) == next.end())
next.push_back(id);
// why: don't drop favourites the page omitted (e.g. pins with no live action hidden from the bar)
for (const auto& id : cur)
if (std::find(next.begin(), next.end(), id) == next.end())
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));
}
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
{
assert(wxThread::IsMain());
@@ -351,11 +726,31 @@ void ActionRegistry::suppress_ask(const std::string& id)
write_section("ask_suppressed", nlohmann::json(arr));
}
// ---- snapshot ---------------------------------------------------------------
nlohmann::json ActionRegistry::snapshot() const
bool ActionRegistry::tooltip_expanded() const
{
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;
sorted.reserve(m_actions.size());
for (const auto& entry : m_actions)
@@ -374,17 +769,85 @@ nlohmann::json ActionRegistry::snapshot() const
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();
for (const AppAction* a : sorted)
actions.push_back({{"id", a->id()},
{"title", a->title()},
{"source", a->source_name()},
{"shortcut", ""}});
actions.push_back(action_to_json(a));
// 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
// reorder the favourites bar). The page (js) filters out ids with no live action itself.
nlohmann::json favourites(read_string_array("favourite_actions"));
return {{"actions", std::move(actions)}, {"favourites", std::move(favourites)}};
// reorder the favourites bar). Drop pins with no live action (an option hidden by the
// current mode, an unloaded plugin, a gone plate/project) and persist the pruned list, so
// 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
+137 -25
View File
@@ -2,10 +2,13 @@
#include <nlohmann/json.hpp>
#include <libslic3r/Config.hpp>
#include <wx/string.h>
#include <wx/thread.h>
#include <cassert>
#include <cstddef>
#include <memory>
#include <string>
#include <string_view>
@@ -18,14 +21,25 @@ namespace Slic3r { namespace GUI {
// How a source's action set changed. Drives the registry's refresh handlers.
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
// actions translate their runner-specific result into this generic shape.
struct AppActionRunResult
{
enum class Level { Success, Info, Error, Busy };
Level level = Level::Info;
wxString message; // empty = "nothing worth showing"
Level level = Level::Info;
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.
@@ -52,12 +66,35 @@ struct AppAction
}
// seeded from AppConfig for the snapshot / sort:
bool favourite = false;
int count = 0;
long long last = 0; // epoch seconds
bool favourite = false;
int count = 0;
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 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:
// 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
// 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)
: m_id(compose_id(prefix, title, source_key)),
m_title(std::move(title)),
m_source_key(std::move(source_key)),
m_source_name(std::move(source_name)) {}
: m_id(compose_id(prefix, title, source_key))
, m_title(std::move(title))
, m_source_key(std::move(source_key))
, 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:
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
};
// 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.
//
// Workflow:
// 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the
// current script capabilities into actions.
// 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the current script
// capabilities into actions, then materialises the static built-ins from the NativeCommands catalog.
// 2. Loader load/unload callbacks route through refresh_source()/refresh_capability(),
// which upsert()/remove() actions. The registry keeps the only action list and
// 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
// than behind a polymorphic source interface.
// note: the static catalog lives in NativeCommands; the registry owns the pool, persistence and
// dispatch, and materialises the dynamic families inline rather than behind a source interface.
class ActionRegistry
{
public:
~ActionRegistry();
// 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
// updates together.
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
// the action with the same id. A null action is ignored.
void upsert(std::unique_ptr<AppAction> action);
@@ -105,31 +176,72 @@ public:
void remove(const std::string& id);
// 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).
AppActionRunResult run(const std::string& id); // runs + bumps stats
void set_favourite(const std::string& id, bool on);
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
AppActionRunResult run(const std::string& id, const std::string& param = {}); // runs + bumps stats
// Pin/unpin. Returns false when `on` would exceed kFavLimit (the bar is full) so the
// 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").
bool should_ask(const std::string& id) const;
void suppress_ask(const std::string& id);
// Flat, frecency-sorted snapshot for the webview: {actions:[...], favourites:[...]}.
nlohmann::json snapshot() const;
// Footer expand/collapse preference. Global (applies to every action) and persisted; absent
// 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:
void seed_state(AppAction& a) const; // favourite/stats from config
AppAction* find(const std::string& id);
void seed_state(AppAction& a) const; // favourite/stats from config
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
// one plugin's whole action set; refresh_capability touches a single capability.
void refresh_source(const std::string& plugin_key, 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
std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock
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
};
}} // namespace Slic3r::GUI
+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_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());
e.Skip();
});
+2 -25
View File
@@ -10,7 +10,6 @@
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Time.hpp"
@@ -356,10 +355,7 @@ namespace Slic3r
obj->bind_state = "free";
obj->last_alive = Slic3r::Utils::get_current_time_utc();
// Route through set_online_state() (rather than writing m_is_online directly) so the
// DeviceOnlineChanged lifecycle event fires consistently; same effective value/behavior
// here since the object was already online in the common case.
obj->set_online_state(true);
obj->m_is_online = true;
obj->set_dev_name(dev_name);
/* if (!obj->dev_ip.empty()) {
Slic3r::GUI::wxGetApp().app_config->set_str("ip_address", obj->dev_id, obj->dev_ip);
@@ -377,10 +373,6 @@ namespace Slic3r
obj->bind_sec_link = sec_link;
obj->dev_connection_name = connection_name;
obj->bind_ssdp_version = ssdp_version;
// Discovery establishes the initial reachability state. Do not report it as an
// online transition; DeviceDiscovered below is the lifecycle event for a new
// device. Subsequent updates route through set_online_state(), so a known device
// still emits DeviceOnlineChanged when its reachability actually changes.
obj->m_is_online = true;
//load access code
@@ -397,15 +389,6 @@ namespace Slic3r
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " New Machine, dev_id= " << dev_id
<< ", ip = " << dev_ip <<", printer_name = " << dev_name
<< ", con_type= " << connect_type <<", signal= " << printer_signal << ", bind_state= " << bind_state;
// First discovery of a genuinely new device (not a periodic SSDP/heartbeat update to
// an already-known one, which is handled in the branch above).
{
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceDiscovered, ctx);
}
}
update_local_machine(*obj);
}
@@ -1001,14 +984,8 @@ namespace Slic3r
}
void DeviceManager::OnSelectedMachineChanged(const std::string& /*pre_dev_id*/,
const std::string& new_dev_id)
const std::string& /*new_dev_id*/)
{
{
LifecycleEventContext ctx;
ctx.name = new_dev_id; // empty string is a valid deselection
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceSelected, ctx);
}
if (MachineObject* obj_ = get_selected_machine()) {
GUI::wxGetApp().sidebar().update_sync_status(obj_);
if(m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription)
+1 -27
View File
@@ -5,7 +5,6 @@
#include "libslic3r/Time.hpp"
#include "libslic3r/Thread.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "GuiColor.hpp"
@@ -2627,15 +2626,7 @@ void MachineObject::reset()
void MachineObject::set_print_state(std::string status)
{
const bool changed = (print_status != status);
print_status = status;
if (changed) {
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = print_status;
fire_lifecycle_event(LifecycleEvent::PrintStateChanged, ctx);
}
}
int MachineObject::connect(bool use_openssl)
@@ -2657,10 +2648,6 @@ int MachineObject::connect(bool use_openssl)
int MachineObject::disconnect()
{
if (m_agent) {
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx);
return m_agent->disconnect_printer();
}
return -1;
@@ -2691,16 +2678,8 @@ bool MachineObject::is_connecting()
void MachineObject::set_online_state(bool on_off)
{
const bool changed = (m_is_online != on_off);
m_is_online = on_off;
if (!on_off) m_active_state = NotActive;
if (changed) {
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = on_off ? "online" : "offline";
fire_lifecycle_event(LifecycleEvent::DeviceOnlineChanged, ctx);
}
}
bool MachineObject::is_info_ready(bool check_version) const
@@ -4622,13 +4601,8 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
try {
if (j.contains("event")) {
if (j["event"].contains("event")) {
if (j["event"]["event"].get<std::string>() == "client.disconnected") {
if (j["event"]["event"].get<std::string>() == "client.disconnected")
set_online_state(false);
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx);
}
else if (j["event"]["event"].get<std::string>() == "client.connected")
set_online_state(true);
}
+3 -34
View File
@@ -42,7 +42,6 @@
#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
#include "slic3r/Utils/MacDarkMode.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include <slic3r/GUI/GUI_Utils.hpp>
@@ -1043,7 +1042,6 @@ wxDEFINE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, 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_INSTANCE_MOVED, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent);
@@ -3583,11 +3581,6 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
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':
{
@@ -5209,16 +5202,8 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
//BBS: nofity object list to update
wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items();
if (object_moved) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "moved";
if (done.size() == 1)
ctx.name = m_model->objects[done.begin()->first]->name;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
if (object_moved)
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED));
}
// BBS: support wipe-tower for multi-plates
for (int plate_id = 0; plate_id < wipe_tower_origins.size(); plate_id++) {
@@ -5339,16 +5324,8 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type)
//BBS: nofity object list to update
wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items();
if (!done.empty()) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "rotated";
if (done.size() == 1)
ctx.name = m_model->objects[done.begin()->first]->name;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
if (!done.empty())
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED));
}
m_dirty = true;
}
@@ -5439,16 +5416,8 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type)
//BBS: notify object info update
wxGetApp().plater()->show_object_info();
if (!done.empty()) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "scaled";
if (done.size() == 1)
ctx.name = m_model->objects[done.begin()->first]->name;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
if (!done.empty())
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED));
}
m_dirty = true;
}
-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_ALL, 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_INSTANCE_MOVED, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
+88 -22
View File
@@ -2179,11 +2179,6 @@ void GUI_App::init_networking_callbacks()
obj->command_get_access_code();
if (m_agent)
m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer());
LifecycleEventContext ctx;
ctx.name = obj->get_dev_id();
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceConnected, ctx);
}
});
});
@@ -2222,11 +2217,6 @@ void GUI_App::init_networking_callbacks()
obj->command_get_version();
event.SetInt(0);
event.SetString(obj->get_dev_id());
LifecycleEventContext ctx;
ctx.name = obj->get_dev_id();
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceConnected, ctx);
} else if (state == ConnectStatus::ConnectStatusFailed) {
// Orca: only update status if same device id
if (m_device_manager->selected_machine != dev_id) return;
@@ -2242,22 +2232,10 @@ void GUI_App::init_networking_callbacks()
wxGetApp().show_dialog(text);
}
event.SetInt(-1);
{
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx);
}
} else if (state == ConnectStatus::ConnectStatusLost) {
m_device_manager->set_selected_machine("");
event.SetInt(-1);
BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = lost";
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx);
} else {
event.SetInt(-1);
BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = " << state;
@@ -2646,6 +2624,10 @@ void GUI_App::init_app_config()
}
#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")));
}
@@ -4625,6 +4607,12 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI enter";
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();
ProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE);
@@ -8193,6 +8181,22 @@ void GUI_App::save_mode(const /*ConfigOptionMode*/int 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
void GUI_App::update_mode()
{
@@ -8341,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()
{
// Reached from the plugins dialog's webview ("open_terminal" command), i.e. from
@@ -8466,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();
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)
+8
View File
@@ -599,6 +599,11 @@ public:
std::string get_saved_mode_str();
std::string get_mode_str();
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_internal_development();
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_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());
// 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_speed_dial();
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;
}
// 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
wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeType type) {
auto sub_menu = new wxMenu;
// 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 and
// avoids repeating the label strings (and the value-vs-pointer comparison pitfalls that come
// with that). Labels are wrapped in L() so they are picked up for translation.
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);
const std::vector<HandyModel>& models = handy_models();
for (std::size_t i = 0; i < models.size(); ++i) {
append_menu_item(sub_menu, wxID_ANY, _(models[i].label), "",
[i](wxCommandEvent&) { MenuFactory::load_handy_model(i); }, "", 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) {
auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) {
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);
}
}
};
auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) { add_volume_with_gizmo(gizmo_type, type); };
if (type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER ||
type == ModelVolumeType::INVALID // cannot use gizmo without selected object
+18
View File
@@ -4,6 +4,7 @@
#include <map>
#include <vector>
#include <array>
#include <cstddef>
#include <wx/bitmap.h>
@@ -51,6 +52,23 @@ public:
static std::vector<wxBitmap> get_text_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() = default;
+1 -39
View File
@@ -1,6 +1,5 @@
#include "libslic3r/libslic3r.h"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "GUI_ObjectList.hpp"
#include "GUI_Factories.hpp"
//#include "GUI_ObjectLayers.hpp"
@@ -10,7 +9,6 @@
#include "BitmapComboBox.hpp"
#include "MainFrame.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "OptionsGroup.hpp"
#include "Tab.hpp"
@@ -1201,39 +1199,17 @@ void ObjectList::update_name_in_model(const wxDataViewItem& item) const
if (m_objects_model->GetItemType(item) & itObject) {
std::string name = m_objects_model->GetName(item).ToUTF8().data();
if (obj->name != name) {
const std::string previous_name = obj->name;
obj->name = name;
// if object has just one volume, rename this volume too
if (obj->volumes.size() == 1)
obj->volumes[0]->name = obj->name;
Slic3r::save_object_mesh(*obj);
LifecycleEventContext ctx;
ctx.name = name;
ctx.previous_name = previous_name;
ctx.id = std::to_string(obj->id().id);
ctx.index = obj_idx;
ctx.source = "object";
fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx);
}
return;
}
if (volume_id < 0) return;
std::string name = m_objects_model->GetName(item).ToUTF8().data();
if (obj->volumes[volume_id]->name == name)
return;
const std::string previous_name = obj->volumes[volume_id]->name;
obj->volumes[volume_id]->name = name;
LifecycleEventContext ctx;
ctx.name = name;
ctx.previous_name = previous_name;
ctx.id = std::to_string(obj->volumes[volume_id]->id().id);
ctx.index = volume_id;
ctx.source = "volume";
fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx);
obj->volumes[volume_id]->name = m_objects_model->GetName(item).ToUTF8().data();
}
void ObjectList::update_name_in_list(int obj_idx, int vol_idx) const
@@ -3572,14 +3548,7 @@ void ObjectList::delete_all_connectors_for_object(int obj_idx)
obj->delete_connectors();
if (obj->volumes.empty() || !obj->has_solid_mesh()) {
const std::string deleted_obj_name = obj->name;
model.delete_object(idx);
{
Slic3r::LifecycleEventContext ctx;
ctx.name = deleted_obj_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
}
m_objects_model->Delete(m_objects_model->GetItemById(idx));
continue;
}
@@ -4148,13 +4117,6 @@ void ObjectList::add_object_to_list(size_t obj_idx, bool call_selection_changed,
const auto item = m_objects_model->AddObject(model_object, warning_bitmap, model_object->is_cut());
Expand(m_objects_model->GetParent(item));
{
Slic3r::LifecycleEventContext ctx;
ctx.name = model_object->name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectAdded, ctx);
}
if (!do_info_update)
return;
+2
View File
@@ -478,6 +478,8 @@ int get_dpi_for_window(const wxWindow *window);
#ifdef __WXOSX__
void dataview_remove_insets(wxDataViewCtrl* dv);
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
#ifdef __WXGTK__
+17
View File
@@ -1,5 +1,6 @@
#include <unistd.h>
#include <sys/sysctl.h>
#import <Cocoa/Cocoa.h>
#import <wx/osx/cocoa/dataview.h>
#import "GUI_Utils.hpp"
@@ -21,6 +22,22 @@ void staticbox_remove_margin(wxStaticBox* sb) {
[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()
// Returns true if the current process is being debugged (either
// running under the debugger or has a debugger attached post facto).
-8
View File
@@ -13,7 +13,6 @@
#include "slic3r/GUI/NotificationManager.hpp"
#include "slic3r/GUI/format.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "libnest2d/common.hpp"
@@ -699,13 +698,6 @@ void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) {
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":arrange m_unprintable: name: %4%, bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale<double>(ap.translation(X)) % unscale<double>(ap.translation(Y)) % ap.name;
}
{
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "arranged";
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
}
m_plater->update();
// BBS
//wxGetApp().obj_manipul()->set_dirty();
-8
View File
@@ -6,7 +6,6 @@
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "libnest2d/common.hpp"
#include <numeric>
@@ -348,13 +347,6 @@ void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr)
m_plater->arrange();
}
m_plater->update();
{
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "arranged";
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
}
}
m_plater->mark_plate_toolbar_image_dirty();
-7
View File
@@ -5,7 +5,6 @@
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/NotificationManager.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "libslic3r/PresetBundle.hpp"
@@ -255,12 +254,6 @@ void OrientJob::finalize(bool canceled, std::exception_ptr &eptr)
mesh.apply();
}
if (!m_selected.empty()) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "auto_oriented";
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
}
m_plater->update();
-22
View File
@@ -1,5 +1,4 @@
#include "PrintJob.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "libslic3r/MTUtils.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/PresetBundle.hpp"
@@ -134,13 +133,6 @@ wxString PrintJob::get_http_error_msg(unsigned int status, std::string body)
void PrintJob::process(Ctl &ctl)
{
LifecycleEventContext start_ctx;
start_ctx.name = m_project_name;
start_ctx.device_id = m_dev_id;
start_ctx.source = "print_job";
fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx);
m_lifecycle_started = true;
/* display info */
std::string msg;
int curr_percent = 10;
@@ -694,7 +686,6 @@ void PrintJob::process(Ctl &ctl)
}
wxQueueEvent(m_plater, evt);
m_job_finished = true;
m_lifecycle_success = true;
}
}
@@ -707,19 +698,6 @@ void PrintJob::finalize(bool canceled, std::exception_ptr &eptr) {
eptr = std::current_exception();
}
if (m_lifecycle_started && !m_lifecycle_finished) {
LifecycleEventContext finish_ctx;
finish_ctx.name = m_project_name;
finish_ctx.device_id = m_dev_id;
finish_ctx.source = "print_job";
finish_ctx.code = canceled ? LifecycleEvtCode::Warn :
(eptr || !m_lifecycle_success ? LifecycleEvtCode::Error : LifecycleEvtCode::Ok);
finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" :
(m_lifecycle_success ? "" : "failed"));
fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx);
m_lifecycle_finished = true;
}
if (canceled || eptr)
return;
}
-3
View File
@@ -43,9 +43,6 @@ class PrintJob : public Job
std::function<void()> m_success_fun{nullptr};
std::string m_dev_id;
bool m_job_finished{ false };
bool m_lifecycle_started{ false };
bool m_lifecycle_finished{ false };
bool m_lifecycle_success{ false };
int m_print_job_completed_id = 0;
wxString m_completed_evt_data;
std::function<void()> m_enter_ip_address_fun_fail{ nullptr };
-21
View File
@@ -1,5 +1,4 @@
#include "SendJob.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "libslic3r/MTUtils.hpp"
@@ -149,13 +148,6 @@ void SendJob::process(Ctl &ctl)
}
}
LifecycleEventContext start_ctx;
start_ctx.name = m_project_name;
start_ctx.device_id = m_dev_id;
start_ctx.source = "send_job";
fire_lifecycle_event(LifecycleEvent::SendJobStarted, start_ctx);
m_lifecycle_started = true;
int total_plate_num = m_plater->get_partplate_list().get_plate_count();
PartPlate* plate = m_plater->get_partplate_list().get_plate(job_data.plate_idx);
@@ -432,19 +424,6 @@ void SendJob::finalize(bool canceled, std::exception_ptr &eptr)
eptr = std::current_exception();
}
if (m_lifecycle_started && !m_lifecycle_finished) {
LifecycleEventContext finish_ctx;
finish_ctx.name = m_project_name;
finish_ctx.device_id = m_dev_id;
finish_ctx.source = "send_job";
finish_ctx.code = canceled ? LifecycleEvtCode::Warn :
(eptr ? LifecycleEvtCode::Error : (m_job_finished ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error));
finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" :
(m_job_finished ? "" : "failed"));
fire_lifecycle_event(LifecycleEvent::SendJobFinished, finish_ctx);
m_lifecycle_finished = true;
}
if (canceled || eptr)
return;
}
-2
View File
@@ -22,8 +22,6 @@ class SendJob : public Job
PrintPrepareData job_data;
std::string m_dev_id;
bool m_job_finished{ false };
bool m_lifecycle_started{ false };
bool m_lifecycle_finished{ false };
int m_print_job_completed_id = 0;
bool m_is_check_mode{false};
bool m_check_and_continue{false};
+3 -2
View File
@@ -198,6 +198,9 @@ void KBShortcutsDialog::fill_shortcuts()
// 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
#ifdef __APPLE__
{"fn+⌫", L("Delete Selected")},
@@ -268,8 +271,6 @@ void KBShortcutsDialog::fill_shortcuts()
{ "O", L("Zoom out") },
{ "V", L("Toggle printable for object/part") },
{ 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 });
+144 -93
View File
@@ -1,6 +1,7 @@
#include "MainFrame.hpp"
#include <wx/panel.h>
#include <wx/textentry.h>
#include <wx/notebook.h>
#include <wx/listbook.h>
#include <wx/simplebook.h>
@@ -46,7 +47,9 @@
// BBS
#include "PartPlate.hpp"
#include "Preferences.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/ProgressDialog.hpp"
#include "Widgets/StaticBox.hpp"
#include "BindDialog.hpp"
#include "../Utils/MacDarkMode.hpp"
#include "../Utils/NetworkAgentFactory.hpp"
@@ -107,6 +110,31 @@ enum class ERescaleTarget
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__
// A thin transparent panel placed at a window edge to handle resize.
// Works regardless of underlying content (GLCanvas3D, wxWebView, etc.)
@@ -702,6 +730,22 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
}
return;}
#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.ShiftDown() && evt.GetKeyCode() == 'G') {
m_plater->apply_background_progress();
@@ -3344,6 +3388,11 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().open_preferences();
},
"", 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);
#endif
// Help menu
@@ -3368,7 +3417,13 @@ void MainFrame::init_menubar_as_editor()
auto top_menu = m_topbar->GetTopMenu();
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", "",
[this](wxCommandEvent &) {
// 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
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Temperature"), _L("Temperature Calibration"),
[this](wxCommandEvent&) {
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](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Max Volumetric Speed
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Max flowrate"), _L("Max flowrate"),
[this](wxCommandEvent&) {
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](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Pressure Advance
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Pressure advance"), _L("Pressure advance"),
[this](wxCommandEvent&) {
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](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Flow rate (Wizard Dialog)
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"),
[this](wxCommandEvent&) {
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](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Retraction
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Retraction"), _L("Retraction"),
[this](wxCommandEvent&) {
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](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Cornering
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Cornering"), _L("Cornering calibration"),
[this](wxCommandEvent&) {
auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Input Shaping (with submenu)
auto input_shaping_menu = new wxMenu();
append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"),
[this](wxCommandEvent&) {
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); },
"", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"),
[this](wxCommandEvent&) {
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); },
"", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
m_topbar->GetCalibMenu()->AppendSubMenu(input_shaping_menu, _L("Input Shaping"));
// VFA
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("VFA"), _L("VFA"),
[this](wxCommandEvent&) {
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](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// help
@@ -3506,6 +3524,10 @@ void MainFrame::init_menubar_as_editor()
#else
// On Mac, the Apple menu ignores non-standard custom items, so add Preset Bundle to the File menu
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(
fileMenu, wxID_ANY, _L("Preset Bundle"), "",
[this](wxCommandEvent&) {
@@ -3552,89 +3574,52 @@ void MainFrame::init_menubar_as_editor()
// Temperature
append_menu_item(calib_menu, wxID_ANY, _L("Temperature"), _L("Temperature"),
[this](wxCommandEvent&) {
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](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Max Volumetric Speed
append_menu_item(calib_menu, wxID_ANY, _L("Max flowrate"), _L("Max flowrate"),
[this](wxCommandEvent&) {
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](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Pressure Advance
append_menu_item(calib_menu, wxID_ANY, _L("Pressure advance"), _L("Pressure advance"),
[this](wxCommandEvent&) {
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](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Flowrate (with submenu)
// ORCA: Flow rate (Wizard Dialog)
append_menu_item(calib_menu, wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"),
[this](wxCommandEvent&) {
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](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Retraction
append_menu_item(calib_menu, wxID_ANY, _L("Retraction"), _L("Retraction"),
[this](wxCommandEvent&) {
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](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Cornering
append_menu_item(calib_menu, wxID_ANY, _L("Cornering"), _L("Cornering calibration"),
[this](wxCommandEvent&) {
auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Input Shaping (with submenu)
auto input_shaping_menu = new wxMenu();
append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"),
[this](wxCommandEvent&) {
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); },
"", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"),
[this](wxCommandEvent&) {
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); },
"", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
calib_menu->AppendSubMenu(input_shaping_menu, _L("Input Shaping"));
// VFA
append_menu_item(calib_menu, wxID_ANY, _L("VFA"), _L("VFA"),
[this](wxCommandEvent&) {
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](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// help
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"));
}
// 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.
+19
View File
@@ -113,6 +113,20 @@ protected:
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
{
#ifdef __APPLE__
@@ -360,6 +374,11 @@ public:
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
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 <wx/button.h>
#include <wx/dcclient.h>
#include <wx/sizer.h>
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 */)
{
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);
if (bmp_name.empty() && bmp.IsOk())
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);
//BBS set size for button
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);
m_pageButtons.insert(m_pageButtons.begin() + n, btn);
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->SetCols(m_buttons_sizer->GetCols() + 1);
m_sizer->Layout();
@@ -209,6 +221,7 @@ void ButtonsListCtrl::RemovePage(size_t n)
Button* btn = m_pageButtons[n];
m_pageButtons.erase(m_pageButtons.begin() + n);
m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA
m_pageIcons.erase(m_pageIcons.begin() + n);
m_buttons_sizer->Remove(n);
#if __WXOSX__
RemoveChild(btn);
@@ -245,7 +258,7 @@ void ButtonsListCtrl::SetCompact(size_t n, bool compact)
int em = em_unit(this);
Button* btn = m_pageButtons[n];
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
@@ -254,6 +267,12 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const
return btn->GetLabel();
}
// ORCA
wxString ButtonsListCtrl::GetPageLabel(size_t n) const
{
return n < m_pageLabels.size() ? m_pageLabels[n] : wxString();
}
// ORCA
void ButtonsListCtrl::SetOverflowButton(wxWindow* button)
{
+23
View File
@@ -33,6 +33,14 @@ public:
void SetPageText(size_t n, const wxString& strText);
void SetCompact(size_t n, bool compact); // ORCA
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
// 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.
@@ -47,6 +55,7 @@ private:
int m_btn_margin;
int m_line_margin;
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
};
@@ -241,6 +250,20 @@ public:
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
{
return false;
+21 -1
View File
@@ -1,6 +1,7 @@
#include "OptionsGroup.hpp"
#include "ConfigExceptions.hpp"
#include "Plater.hpp"
#include "SettingsIndex.hpp"
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "OG_CustomCtrl.hpp"
@@ -244,6 +245,25 @@ void OptionsGroup::append_line(const Line& 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()))
return;
@@ -650,7 +670,7 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index
m_opt_map.emplace(opt_id, pair);
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);
}
+5 -1
View File
@@ -250,6 +250,10 @@ protected:
virtual void back_to_initial_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:
static wxString get_url(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) {}
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; }
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,
wxDefaultPosition,
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);
Bind(wxEVT_SCROLL_CHANGED, [this](auto &e) {
+1 -34
View File
@@ -29,7 +29,6 @@
#include "libslic3r/Tesselate.hpp"
#include "libslic3r/GCode/ThumbnailData.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
@@ -2637,20 +2636,11 @@ void PartPlate::set_plate_name(const std::string& name)
if (boost::equals(m_name, name))
return;
const std::string previous_name = m_name;
m_name = name;
if (m_print != nullptr)
m_print->set_plate_name(name);
invalidate_plate_name_texture();
if (m_plater != nullptr && !m_plater->is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = name;
ctx.previous_name = previous_name;
ctx.index = m_plate_index;
fire_lifecycle_event(LifecycleEvent::PlateRenamed, ctx);
}
}
//get the print's object, result and index
@@ -4761,16 +4751,9 @@ int PartPlateList::create_plate(bool adjust_position)
if (m_plater) {
// In GUI mode
wxGetApp().obj_list()->on_plate_added(plate);
wxGetApp().obj_list()->on_plate_added(plate);
}
if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = plate->get_plate_name();
ctx.index = new_index;
fire_lifecycle_event(LifecycleEvent::PlateCreated, ctx);
}
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":created a new plate %1%") % new_index;
return new_index;
}
@@ -4869,7 +4852,6 @@ int PartPlateList::delete_plate(int index)
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate %1%, has an invalid index %2%") % index % plate->get_index();
return -1;
}
const std::string plate_name = plate->get_plate_name();
if (m_plater) {
// In GUI mode
@@ -4952,13 +4934,6 @@ int PartPlateList::delete_plate(int index)
delete plate;
if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = plate_name;
ctx.index = index;
fire_lifecycle_event(LifecycleEvent::PlateDeleted, ctx);
}
// FIX: context of BackgroundSliceProcess and gcode preview need to be updated before ObjectList::reload_all_plates().
#if 0
if (m_plater != nullptr) {
@@ -5067,7 +5042,6 @@ int PartPlateList::select_plate(int index)
if (m_plate_list.empty() || index >= m_plate_list.size()) {
return -1;
}
const int previous_index = m_current_plate;
// BBS: erase unnecessary snapshot
if (get_curr_plate_index() != index && m_intialized) {
@@ -5094,13 +5068,6 @@ int PartPlateList::select_plate(int index)
//wxQueueEvent(m_plater, new SimpleEvent(EVT_GLCANVAS_PLATE_SELECT));
}
if (previous_index != index && m_intialized && m_plater != nullptr && !m_plater->is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = m_plate_list[index]->get_plate_name();
ctx.index = index;
fire_lifecycle_event(LifecycleEvent::PlateSelected, ctx);
}
return 0;
}
+145 -208
View File
@@ -57,7 +57,6 @@
#include <wx/aui/aui.h>
#include "libslic3r/libslic3r.h"
#include "libslic3r/LifecycleEvents.hpp"
#include "libslic3r/Format/STL.hpp"
#include "libslic3r/Format/DRC.hpp"
#include "libslic3r/Format/STEP.hpp"
@@ -149,7 +148,6 @@
#include "Widgets/RadioGroup.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/StaticGroup.hpp"
#include "GUI_ObjectTable.hpp"
#include "libslic3r/Thread.hpp"
@@ -476,14 +474,9 @@ enum class ActionButtonType : int {
abSendGCode
};
// Background for the extruder-group title chip and its edit buttons, matching the StaticGroup
// interior. macOS keeps a lighter #F7F7F7 tint in light mode; dark mode uses the mapped colour.
// Background for the extruder-group title chip and its edit buttons
static wxColour extruder_group_chip_bg()
{
#ifdef __WXOSX__
if (!wxGetApp().dark_mode())
return wxColour("#F7F7F7");
#endif
return StateColor::darkModeColorFor(*wxWHITE);
}
@@ -498,38 +491,39 @@ public:
SetBackgroundColour(extruder_group_chip_bg());
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->SetFont(Label::Body_13);
m_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
m_label->SetFont(Label::Body_12);
m_label->SetForegroundColour(label_color);
m_brace_left = new wxStaticText(this, wxID_ANY, "(", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
m_brace_left->SetFont(Label::Body_13);
m_brace_left->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
m_brace_left->SetFont(Label::Body_12);
m_brace_left->SetForegroundColour(label_color);
m_brace_left->Hide();
m_count = new wxStaticText(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
m_count->SetFont(Label::Body_13.Bold());
m_count->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
m_count->SetFont(Label::Body_12.Bold());
m_count->SetForegroundColour(label_color);
m_count->Hide();
m_brace_right = new wxStaticText(this, wxID_ANY, ")", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
m_brace_right->SetFont(Label::Body_13);
m_brace_right->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
m_brace_right->SetFont(Label::Body_12);
m_brace_right->SetForegroundColour(label_color);
m_brace_right->Hide();
m_hover_btn = new ScalableButton(this, wxID_ANY, "dot");
m_hover_btn->SetMinSize(wxSize(FromDIP(25), -1));
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->SetBackgroundColour(extruder_group_chip_bg());
m_hover_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
if (m_enabled && m_hover_on_click)
m_hover_on_click();
});
sizer->Add(m_label, 0, wxALIGN_CENTER_VERTICAL);
sizer->Add(m_brace_left, 0, wxALIGN_CENTER_VERTICAL);
sizer->Add(m_count, 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_count , 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,
// which outranks best size in sizer allocation, so once the count is shown any ancestor
@@ -541,7 +535,7 @@ public:
void EnableEdit(bool 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); }
@@ -552,11 +546,13 @@ public:
m_count->Hide();
m_brace_left->Hide();
m_brace_right->Hide();
m_hover_btn->Hide();
} else {
m_count->SetLabel(wxString::Format("%d", count));
m_count->Show();
m_brace_left->Show();
m_brace_right->Show();
m_hover_btn->Show();
}
UpdateSizing();
}
@@ -567,15 +563,19 @@ public:
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).
void sys_color_changed()
{
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})
t->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
t->SetForegroundColour(label_color);
m_hover_btn->SetBackgroundColour(extruder_group_chip_bg());
Refresh();
}
@@ -602,11 +602,12 @@ private:
bool m_enabled{false};
};
struct ExtruderGroup : StaticGroup
struct ExtruderGroup : StaticBox
{
ExtruderGroup(wxWindow * parent, int index, wxString const &title);
wxStaticBoxSizer *sizer = nullptr;
wxBoxSizer * sizer = nullptr;
HoverLabel * hover_label = nullptr;
wxStaticText* ams_label{nullptr};
ScalableButton * btn_edit = nullptr;
ComboBox * combo_diameter = nullptr;
ComboBox * combo_flow = nullptr;
@@ -646,8 +647,10 @@ struct ExtruderGroup : StaticGroup
{
if (hover_label)
hover_label->Rescale();
if (btn_edit)
if (btn_edit){
btn_edit->msw_rescale();
btn_edit->SetMinSize(ams_label->GetSize());
}
btn_up->msw_rescale();
btn_down->msw_rescale();
combo_diameter->Rescale();
@@ -862,9 +865,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual)
// double
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->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
// update_extruder_separator_icon). Created hidden; a click re-shows the ready/not-ready tip.
@@ -879,7 +882,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual)
}
// 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);
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()));
@@ -1257,7 +1262,7 @@ public:
Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {
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.DrawRoundedRectangle(0, 0, GetSize().x, GetSize().y, 0);
});
@@ -1311,29 +1316,25 @@ public:
};
ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title)
: StaticGroup(parent, wxID_ANY, wxString())
: StaticBox(parent)
{
SetFont(Label::Body_10);
SetForegroundColour(wxColour("#CECECE"));
SetBorderColor(wxColour("#EEEEEE"));
SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); // ORCA match radius with other boxes
ShowBadge(true);
SetTopMargin(FromDIP(7)); // ORCA
// The title lives in an interactive row inside the card (with the nozzle-count badge and its edit
// button) instead of being painted on the border by StaticGroup.
// The title lives in an interactive row inside the card (with the nozzle-count badge and its edit button)
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
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);
this->combo_diameter = combo_diameter;
wxStaticText *label_flow = new wxStaticText(this, wxID_ANY, _L("Flow"));
label_flow->SetFont(Label::Body_14);
label_flow->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
if (index >= 0) label_flow->SetMinSize({FromDIP(80), -1});
combo_diameter->SetToolTip(_L("Diameter"));
// Flow
auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
combo_flow->GetDropDown().SetUseContentWidth(true);
combo_flow->Bind(wxEVT_COMBOBOX, [index, combo_flow](wxCommandEvent &evt) {
@@ -1350,51 +1351,75 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
}
});
this->combo_flow = combo_flow;
combo_flow->SetToolTip(_L("Flow"));
// AMS
wxStaticText *label_ams = new wxStaticText(this, wxID_ANY, _L("AMS"));
label_ams->SetFont(Label::Body_14);
label_ams->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
//label_ams->SetMinSize({FromDIP(70), -1});
auto ams_panel = new wxPanel(this, wxID_ANY);
ams_panel->SetBackgroundColour(*wxWHITE);
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) {
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->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);
auto size = GetSize();
auto pos = ClientToScreen({0, size.y + 12});
auto size = GetSize();
auto pos = ClientToScreen({0, size.y - FromDIP(8) - combo_diameter->GetSize().y});
size.SetWidth(size.GetWidth() + FromDIP(10));
window->Position(pos, {0, 0});
window->Popup();
});
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}) {
w->Bind(wxEVT_ENTER_WINDOW, [w, hovered, this](wxMouseEvent &evt) { *hovered = w; btn_edit->SetBitmap_("edit"); });
w->Bind(wxEVT_LEAVE_WINDOW, [w, hovered, this](wxMouseEvent &evt) { if (*hovered == w) { btn_edit->SetBitmap_("dot"); *hovered = nullptr; } });
for (wxWindow *w : std::initializer_list<wxWindow *>{this, btn_edit, ams_not_installed_msg, ams_label, ams_panel}) {
// ORCA using CallAfter fixes crash on linux while clicking edit button
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
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();
}
hsizer_ams = new wxBoxSizer(wxHORIZONTAL);
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)
hsizer_ams->Add(btn_edit, 0, wxLEFT | wxALIGN_CENTER, FromDIP(2));
hsizer_ams->Add(ams_not_installed_msg, 0, wxALIGN_CENTER);
hsizer_ams->Add(btn_edit, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(5));
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->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
if (page_cur > 0)
@@ -1402,7 +1427,7 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
update_ams();
});
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->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
if (page_cur + 1 < page_num)
@@ -1411,31 +1436,24 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
});
btn_down->Hide();
wxBoxSizer *hsizer_diameter = new wxBoxSizer(wxHORIZONTAL);
hsizer_diameter->Add(label_diameter, 0, wxALIGN_CENTER);
hsizer_diameter->Add(combo_diameter, 1, wxEXPAND);
wxBoxSizer * hsizer_nozzle = new wxBoxSizer(wxHORIZONTAL);
hsizer_nozzle->Add(label_flow, 0, wxALIGN_CENTER);
hsizer_nozzle->Add(combo_flow, 1, wxEXPAND);
wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL);
wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL);
hsizer->Add(combo_diameter, 1, wxRIGHT, FromDIP(5));
hsizer->Add(combo_flow , 1);
vsizer->AddSpacer(FromDIP(16)); // spacing for title and control
if (index < 0) {
label_ams->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;
ams_panel->Hide();
} else {
wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL);
vsizer->Add(hover_label, 0, wxLEFT | wxALL, 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(ams_panel, 0, wxEXPAND | wxLEFT | wxRIGHT , FromDIP(5));
vsizer->AddSpacer(FromDIP(2));
}
vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5));
SetSizer(vsizer);
Layout();
AMSCountPopupWindow::UpdateAMSCount(index < 0 ? 0 : index, this);
}
@@ -1506,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)
@@ -6423,6 +6441,11 @@ Search::OptionsSearcher& Sidebar::get_searcher()
return p->searcher;
}
Search::SettingsIndex& Sidebar::settings_index()
{
return p->searcher.index();
}
std::string& Sidebar::get_search_line()
{
return p->searcher.search_string();
@@ -7572,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_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)
{ 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(); });
@@ -10124,14 +10143,7 @@ void Plater::priv::remove(size_t obj_idx)
view3D->enable_layers_editing(false);
m_worker.cancel_all();
std::string obj_name = (obj_idx < model.objects.size()) ? model.objects[obj_idx]->name : std::to_string(obj_idx);
model.delete_object(obj_idx);
{
Slic3r::LifecycleEventContext ctx;
ctx.name = obj_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
}
//BBS: notify partplate the instance removed
partplate_list.notify_instance_removed(obj_idx, -1);
update();
@@ -10164,14 +10176,7 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia
if (obj->is_cut())
sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx);
std::string obj_name = obj->name;
model.delete_object(obj_idx);
{
Slic3r::LifecycleEventContext ctx;
ctx.name = obj_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
}
//BBS: notify partplate the instance removed
partplate_list.notify_instance_removed(obj_idx, -1);
@@ -10217,22 +10222,10 @@ void Plater::priv::delete_all_objects_from_model()
void Plater::priv::reset(bool apply_presets_change)
{
// TakeSnapshot below and load_current_presets() further down each re-evaluate the
// aggregate dirty flag against a baseline that hasn't been reset yet, so they can toggle
// is_dirty() back and forth several times before it settles; coalesce those into one event.
ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(dirty_state);
Plater::TakeSnapshot snapshot(q, _u8L("Reset Project"), UndoRedo::SnapshotType::ProjectSeparator);
clear_warnings();
const std::string closed_project_name = into_u8(get_project_filename());
if (!closed_project_name.empty() || !model.objects.empty()) {
Slic3r::LifecycleEventContext ctx;
ctx.name = closed_project_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectClosed, ctx);
}
// A new project must not inherit the previous project's published selection (Feature A/B).
m_has_pending_published = false;
m_pending_published_keys.clear();
@@ -12660,15 +12653,11 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
notification_manager->set_slicing_progress_export_possible();
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
const std::string lifecycle_job_name = this->background_process.fff_print() ?
this->background_process.fff_print()->output_filename() : std::string();
this->background_process.reset_export();
// This bool stops showing export finished notification even when process_completed_with_error is false
bool has_error = false;
std::string lifecycle_error_msg;
if (evt.error()) {
auto message = evt.format_error_message();
lifecycle_error_msg = message.first;
if (evt.critical_error()) {
if (q->m_tracking_popup_menu) {
// We don't want to pop-up a message box when tracking a pop-up menu.
@@ -12706,14 +12695,6 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
is_finished = true;
}
{
Slic3r::LifecycleEventContext ctx;
ctx.name = lifecycle_job_name;
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
}
//BBS: set the current plater's slice result to valid
if (!this->background_process.empty())
this->background_process.get_current_plate()->update_slice_result_valid_state(evt.success());
@@ -12870,17 +12851,8 @@ void Plater::priv::on_action_add(SimpleEvent&)
//BBS: add plate from toolbar
void Plater::priv::on_action_add_plate(SimpleEvent&)
{
if (q != nullptr) {
take_snapshot("add partplate");
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;
}
if (q != nullptr)
q->add_plate();
}
//BBS: remove plate from toolbar
@@ -15256,33 +15228,21 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_
//get_partplate_list().reinit();
//get_partplate_list().update_slice_context_to_current_plate(p->background_process);
//p->preview->update_gcode_result(p->partplate_list.get_current_slice_result());
if (!silent) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::NewProject, ctx);
}
{
// Same rationale as in Plater::priv::reset(): the whole reset + preset-reload +
// baseline-reset sequence below settles into its final dirty state only once it
// completes, so hold notifications until then to avoid firing on transient flips.
ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(p->dirty_state);
reset(transfer_preset_changes);
reset_project_dirty_after_save();
reset_project_dirty_initial_presets();
wxGetApp().update_saved_preset_from_current_preset();
update_project_dirty_from_presets();
reset(transfer_preset_changes);
reset_project_dirty_after_save();
reset_project_dirty_initial_presets();
wxGetApp().update_saved_preset_from_current_preset();
update_project_dirty_from_presets();
//reset project
p->project.reset();
//set project name
if (project_name.empty())
p->set_project_name(_L("Untitled"));
else
p->set_project_name(project_name);
//reset project
p->project.reset();
//set project name
if (project_name.empty())
p->set_project_name(_L("Untitled"));
else
p->set_project_name(project_name);
Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator);
}
Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator);
Model m;
model().load_from(m); // new id avoid same path name
@@ -15400,13 +15360,6 @@ void Plater::load_project(wxString const& filename2,
p->set_project_name(_L("Untitled"));
}
{
Slic3r::LifecycleEventContext ctx;
ctx.name = into_u8(load_restore ? originfile : filename);
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectOpened, ctx);
}
} else {
if (using_exported_file()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename;
@@ -15478,23 +15431,11 @@ int Plater::save_project(bool saveAs)
if (full_pathnames) {
save_strategy = save_strategy | SaveStrategy::FullPathSources;
}
{
Slic3r::LifecycleEventContext ctx;
ctx.name = into_u8(filename);
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectBeforeSave, ctx);
}
if (export_3mf(into_path(filename), save_strategy) < 0) {
MessageDialog(this, _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs have the project file open."),
_L("Save project"), wxOK | wxICON_WARNING).ShowModal();
return wxID_CANCEL;
}
{
Slic3r::LifecycleEventContext ctx;
ctx.name = into_u8(filename);
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectAfterSave, ctx);
}
Slic3r::remove_backup(model(), false);
@@ -20814,14 +20755,6 @@ void Plater::changed_object(ModelObject &object){
// Check outside bed
get_current_canvas3D()->requires_check_outside_state();
if (!is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = object.name;
ctx.id = std::to_string(object.id().id);
ctx.source = "geometry";
fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx);
}
}
void Plater::changed_object(int obj_idx)
@@ -20858,20 +20791,6 @@ void Plater::changed_objects(const std::vector<size_t>& object_idxs)
// update print
this->p->schedule_background_process();
if (!is_loading_project()) {
for (size_t obj_idx : object_idxs) {
if (obj_idx >= p->model.objects.size() || p->model.objects[obj_idx] == nullptr)
continue;
LifecycleEventContext ctx;
ctx.name = p->model.objects[obj_idx]->name;
ctx.id = std::to_string(p->model.objects[obj_idx]->id().id);
ctx.index = static_cast<int>(obj_idx);
ctx.source = "geometry";
fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx);
}
}
}
void Plater::schedule_background_process(bool schedule/* = true*/)
@@ -22005,6 +21924,24 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi
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 index = plate_index, ret;
+4
View File
@@ -283,6 +283,7 @@ public:
std::vector<std::string>& types,
std::vector<size_t>* config_indices = nullptr);
Search::OptionsSearcher& get_searcher();
Search::SettingsIndex& settings_index();
std::string& get_search_line();
void update_printer_thumbnail();
@@ -781,6 +782,9 @@ public:
void apply_background_progress();
//BBS: select the plate by hover_id
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
int delete_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();
}
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)
{
descriptor.plugin_root.clear();
@@ -150,41 +141,6 @@ PluginDescriptor as_cloud_only_descriptor(PluginDescriptor 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);
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
// ── 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)
: WebViewHostDialog(parent, id, _L("Plugins"), pos, size, style)
{ 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())
return false;
BOOST_LOG_TRIVIAL(info) << "Installing local plugin package from path: " << package_path;
std::string error;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Installing local plugin package from path: " << 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();
std::transform(extension.begin(), extension.end(), extension.begin(),
[](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");
// The shared helper reports a user-cancelled overwrite with an empty message: stay silent.
if (message.IsEmpty()) {
send_plugins();
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) {
BOOST_LOG_TRIVIAL(error) << "Plugin package installation failed for " << package_path << " error=" << error;
show_status(_L("Failed to install plugin package. See the log for details."), "warn");
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to install plugin package.";
show_status(message, "warn");
send_plugins();
return false;
}
BOOST_LOG_TRIVIAL(info) << "Plugin package installed successfully from " << package_path;
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
show_status(wxString::Format(_L("Installed \"%s\"."), installed_name), "success");
refresh_plugin_metadata_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudMeta);
show_status(message, "success");
prompt_for_missing_plugins();
send_plugins();
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));
}
void PluginsDialog::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::open_plugin_hub() { Slic3r::GUI::open_plugin_hub(); }
void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin)
{
+183 -124
View File
@@ -25,6 +25,8 @@
#include <wx/string.h>
#include <wx/timer.h>
#include <boost/filesystem.hpp>
class wxTimer;
namespace Slic3r {
@@ -35,6 +37,184 @@ enum class PluginCapabilityType;
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
{
public:
@@ -111,53 +291,8 @@ private:
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE, // | wxPD_CAN_ABORT for cancel button
bool finish_after_dialog_destroyed = false)
{
const auto alive = m_alive;
ProgressDialog* progress = new ProgressDialog(title, message, maximum, this, style);
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();
detail::run_off_thread_with_progress(std::forward<Run>(run), std::forward<OnFinish>(on_finish), this, title, message, maximum, style,
m_alive, finish_after_dialog_destroyed, [this] { restore_z_order(); });
}
template<typename Run>
@@ -167,83 +302,7 @@ private:
int maximum = 100,
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
{
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_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);
}
return detail::run_wait_with_progress(std::forward<Run>(run), this, title, message, maximum, style, m_alive, [this] { restore_z_order(); });
}
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)"));
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
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 "
+1 -46
View File
@@ -6,7 +6,6 @@
#include "MainFrame.hpp"
#include "I18N.hpp"
#include "Plater.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include <boost/algorithm/string/predicate.hpp>
@@ -18,23 +17,13 @@ namespace GUI {
void ProjectDirtyStateManager::update_from_undo_redo_stack(bool dirty)
{
const bool was_dirty = is_dirty();
m_plater_dirty = dirty;
notify_dirty_change(was_dirty, "undo_redo");
if (const Plater *plater = wxGetApp().plater(); plater && wxGetApp().initialized())
wxGetApp().mainframe->update_title();
}
void ProjectDirtyStateManager::set_plater_dirty(bool is_dirty)
{
const bool was_dirty = this->is_dirty();
m_plater_dirty = is_dirty;
notify_dirty_change(was_dirty, "plater");
}
void ProjectDirtyStateManager::update_from_presets()
{
const bool was_dirty = is_dirty();
m_presets_dirty = false;
// check switching of the presets only for exist/loaded project, but not for new
GUI_App &app = wxGetApp();
@@ -56,53 +45,18 @@ void ProjectDirtyStateManager::update_from_presets()
}
m_presets_dirty |= app.has_unsaved_preset_changes();
m_project_config_dirty = m_initial_project_config != app.preset_bundle->project_config;
notify_dirty_change(was_dirty, "presets");
app.mainframe->update_title();
}
void ProjectDirtyStateManager::reset_after_save()
{
const bool was_dirty = is_dirty();
this->reset_initial_presets();
m_plater_dirty = false;
m_presets_dirty = false;
m_project_config_dirty = false;
notify_dirty_change(was_dirty, "save");
wxGetApp().mainframe->update_title();
}
void ProjectDirtyStateManager::notify_dirty_change(bool was_dirty, const char *source)
{
if (m_suppress_depth > 0)
return;
const bool dirty = is_dirty();
if (was_dirty == dirty)
return;
LifecycleEventContext ctx;
ctx.code = LifecycleEvtCode::Ok;
ctx.dirty = dirty;
ctx.source = source;
fire_lifecycle_event(LifecycleEvent::ProjectDirtyChanged, ctx);
}
void ProjectDirtyStateManager::begin_suppress_notifications()
{
if (m_suppress_depth == 0)
m_suppress_entry_dirty = is_dirty();
++m_suppress_depth;
}
void ProjectDirtyStateManager::end_suppress_notifications()
{
assert(m_suppress_depth > 0);
if (m_suppress_depth > 0)
--m_suppress_depth;
if (m_suppress_depth == 0)
notify_dirty_change(m_suppress_entry_dirty, "batch");
}
void ProjectDirtyStateManager::reset_initial_presets()
{
m_initial_presets.fill(std::string{});
@@ -214,3 +168,4 @@ void ProjectDirtyStateManager::render_debug_window() const
} // namespace GUI
} // namespace Slic3r
+1 -22
View File
@@ -14,42 +14,21 @@ public:
void reset_after_save();
void reset_initial_presets();
void set_plater_dirty(bool is_dirty);
void set_plater_dirty(bool is_dirty) { m_plater_dirty = is_dirty; }
bool is_dirty() const { return m_plater_dirty || m_project_config_dirty || m_presets_dirty; }
bool is_presets_dirty() const { return m_presets_dirty; }
// RAII guard coalescing dirty-state updates: while any guard is alive, ProjectDirtyChanged
// notifications are held back; when the outermost guard is destroyed, at most one
// notification fires, reflecting only the net change across the whole guarded scope.
class NotificationSuppressor
{
public:
explicit NotificationSuppressor(ProjectDirtyStateManager &owner) : m_owner(owner) { m_owner.begin_suppress_notifications(); }
~NotificationSuppressor() { m_owner.end_suppress_notifications(); }
NotificationSuppressor(const NotificationSuppressor &) = delete;
NotificationSuppressor &operator=(const NotificationSuppressor &) = delete;
private:
ProjectDirtyStateManager &m_owner;
};
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
void render_debug_window() const;
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
private:
void notify_dirty_change(bool was_dirty, const char *source);
void begin_suppress_notifications();
void end_suppress_notifications();
// Does the Undo / Redo stack indicate the project is dirty?
bool m_plater_dirty { false };
// Do the presets indicate the project is dirty?
bool m_presets_dirty { false };
// Is the project config dirty?
bool m_project_config_dirty { false };
// NotificationSuppressor nesting depth and the dirty state observed when the outermost guard began.
int m_suppress_depth { 0 };
bool m_suppress_entry_dirty { false };
// Keeps track of preset names selected at the time of last project save.
std::array<std::string, Preset::TYPE_COUNT> m_initial_presets;
DynamicPrintConfig m_initial_project_config;

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