Downloader feature - Downloads from Printables.com

Custom URL Registration:
 - Windows - writes to registers.
 - Linux - desktop integration file.
 - Macos - info.plist.in creates registration and is controlled only via app config.
Registration is first made in Config Wizard. Or is triggered from Preferences. Path to downloads folder can be set.
URL link starts new instance of PS which sends data to running instance via SingleInstance structures if exists.
New progress notification is introduced with pause and stop buttons.
Downloader writes downloaded data by chunks.
Support for zip files is introduced. Zip files can be opened, downloaded or drag'n'droped in PS. Archive dialog is opened. Then if more than 1 project is selected, only geometry is loaded.
Opening of 3mf project now supports openning project in new PS instance.
This commit is contained in:
David Kocik 2023-01-05 15:00:37 +01:00
parent e70c4849ba
commit ce38e57ec4
37 changed files with 9616 additions and 6577 deletions

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.0"
id="close_window"
x="0px"
y="0px"
viewBox="0 0 100 100"
enable-background="new 0 0 100 100"
xml:space="preserve"
sodipodi:docname="notification_open.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"><metadata
id="metadata19"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs17" /><sodipodi:namedview
inkscape:document-rotation="0"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1369"
id="namedview15"
showgrid="false"
inkscape:zoom="10.08"
inkscape:cx="50"
inkscape:cy="50"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="close_window" />
<g
id="g4">
<path
fill="#ED6B21"
d="M80,92.83H20c-7.08,0-12.83-5.76-12.83-12.84V20c0-7.08,5.76-12.83,12.83-12.83h60 c7.08,0,12.84,5.76,12.84,12.83v60C92.83,87.08,87.08,92.83,80,92.83z M20,12.83c-3.95,0-7.17,3.21-7.17,7.17v60 c0,3.95,3.21,7.17,7.17,7.17h60c3.95,0,7.17-3.21,7.17-7.17V20c0-3.95-3.21-7.17-7.17-7.17H20z"
id="path2" />
</g>
<g
id="open"
transform="matrix(4.05,0,0,4.05,15.4645,15.277381)"
style="fill:#ed6b21;fill-opacity:1"><path
id="path2-7"
d="M 1.22,14 V 3 c 0,0 0,-1 1,-1 1,0 4,0 5,0 1,0 1,2 2,2 1,0 4,0 4,0 0,0 1,0 1,1 v 2 h -1 c 0,0 0,0 0,-1 0,-1 -1,-1 -1,-1 h -3.5 c -1,0 -1,-2 -2,-2 -1,0 -3.5,0 -3.5,0 -1,0 -1,1 -1,1 v 9 1 h 1 v 1 c 0,0 0,0 -1,0 -1,0 -1,-1 -1,-1 z"
fill="#808080"
style="fill:#ed6b21;fill-opacity:1" /><path
id="path4"
d="M 5,6 C 4.45,6 3.86,6.43 3.68,6.95 l -2.37,7.1 C 1.14,14.57 1.45,15 2,15 h 10 c 0.55,0 1.14,-0.43 1.32,-0.95 l 2.37,-7.1 C 15.86,6.43 15.55,6 15,6 Z"
fill="#ed6b21"
style="fill:#ed6b21;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.0"
id="close_window"
x="0px"
y="0px"
viewBox="0 0 100 100"
enable-background="new 0 0 100 100"
xml:space="preserve"
sodipodi:docname="notification_open_hover.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"><metadata
id="metadata19"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs17" /><sodipodi:namedview
inkscape:document-rotation="0"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1369"
id="namedview15"
showgrid="false"
inkscape:zoom="10.08"
inkscape:cx="50"
inkscape:cy="50"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="close_window" />
<g
transform="matrix(1.15,0,0,1.15,-7.50075,-7.5)"
id="g4">
<path
fill="#ed6b21"
d="M 80,92.83 H 20 C 12.92,92.83 7.17,87.07 7.17,79.99 V 20 C 7.17,12.92 12.93,7.17 20,7.17 h 60 c 7.08,0 12.84,5.76 12.84,12.83 V 80 C 92.83,87.08 87.08,92.83 80,92.83 Z m -60,-80 c -3.95,0 -7.17,3.21 -7.17,7.17 v 60 c 0,3.95 3.21,7.17 7.17,7.17 h 60 c 3.95,0 7.17,-3.21 7.17,-7.17 V 20 c 0,-3.95 -3.21,-7.17 -7.17,-7.17 z"
id="path2" />
</g>
<g
id="open"
transform="matrix(4.6575,0,0,4.6575,10.313937,10.113631)"
style="fill:#ed6b21;fill-opacity:1"><path
id="path2-7"
d="M 1.22,14 V 3 c 0,0 0,-1 1,-1 1,0 4,0 5,0 1,0 1,2 2,2 1,0 4,0 4,0 0,0 1,0 1,1 v 2 h -1 c 0,0 0,0 0,-1 0,-1 -1,-1 -1,-1 h -3.5 c -1,0 -1,-2 -2,-2 -1,0 -3.5,0 -3.5,0 -1,0 -1,1 -1,1 v 9 1 h 1 v 1 c 0,0 0,0 -1,0 -1,0 -1,-1 -1,-1 z"
fill="#808080"
style="fill:#ed6b21;fill-opacity:1" /><path
id="path4"
d="M 5,6 C 4.45,6 3.86,6.43 3.68,6.95 l -2.37,7.1 C 1.14,14.57 1.45,15 2,15 h 10 c 0.55,0 1.14,-0.43 1.32,-0.95 l 2.37,-7.1 C 15.86,6.43 15.55,6 15,6 Z"
fill="#ed6b21"
style="fill:#ed6b21;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="notification_pause.svg"
xml:space="preserve"
style="enable-background:new 0 0 800 800;"
viewBox="0 0 800 800"
y="0px"
x="0px"
id="Layer_1"
version="1.1"><metadata
id="metadata15"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs13" /><sodipodi:namedview
inkscape:current-layer="Layer_1"
inkscape:window-maximized="1"
inkscape:window-y="-8"
inkscape:window-x="-8"
inkscape:cy="459.92063"
inkscape:cx="400"
inkscape:zoom="1.26"
showgrid="false"
id="namedview11"
inkscape:window-height="1369"
inkscape:window-width="2560"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:document-rotation="0" />
<style
id="style2"
type="text/css">
.st0{fill:#ED6B21;}
</style>
<path
id="path4"
d="m 317.95173,562.36128 h -60.775 v -330.565 h 60.775 v 330.565 m 48.195,27.88 v -386.24 c 0,-11.22 -9.095,-20.315 -20.315,-20.315 h -116.535 c -11.22,0 -20.315,9.095 -20.315,20.315 v 386.24 c 0,11.22 9.095,20.315 20.315,20.315 h 116.45 c 11.22,0 20.4,-9.095 20.4,-20.315 z"
class="st0"
style="stroke-width:0.85" />
<g
transform="matrix(0.9775,0,0,0.9775,53.547,53.54775)"
id="g4">
<path
d="M 597.2,701.3 H 110.6 C 53.2,701.3 6.5,654.6 6.5,597.2 V 110.6 C 6.5,53.2 53.2,6.5 110.6,6.5 h 486.6 c 57.4,0 104.1,46.7 104.1,104.1 v 486.6 c 0,57.4 -46.7,104.1 -104.1,104.1 z M 110.6,52.4 c -32,0 -58.2,26 -58.2,58.2 v 486.6 c 0,32 26,58.2 58.2,58.2 h 486.6 c 32,0 58.2,-26 58.2,-58.2 V 110.6 c 0,-32 -26,-58.2 -58.2,-58.2 z"
class="st0"
id="path2" />
</g>
<path
id="path17"
d="m 150.65676,738.12999 c -12.4717,-1.39663 -26.66772,-5.94192 -37.84321,-12.11671 -17.754551,-9.80992 -33.768844,-26.68981 -42.418124,-44.71089 -5.985061,-12.4701 -8.760227,-23.35456 -9.821918,-38.52249 -0.48061,-6.8663 -0.640464,-87.42616 -0.497289,-250.61508 0.195544,-222.88027 0.294923,-240.94223 1.356742,-246.58759 4.2349,-22.51562 13.68014,-40.62012 29.200931,-55.97194 14.237938,-14.082924 31.958648,-23.427941 52.602238,-27.739791 5.87892,-1.227937 14.00696,-1.268146 256.3492,-1.268146 h 250.27778 l 7.08334,1.561512 c 21.30688,4.697075 36.90336,13.216072 51.96052,28.381502 14.67865,14.784203 23.1932,30.350373 27.76125,50.752683 l 1.56791,7.00271 v 250.95239 c 0,242.72256 -0.0418,251.15149 -1.26428,257.0238 -9.30592,44.69034 -45.18963,77.43352 -89.75566,81.90028 -9.17898,0.92002 -488.33076,0.87927 -496.55943,-0.0425 z M 652.87275,692.49 c 19.93824,-6.17834 34.6922,-21.42493 40.00111,-41.33675 l 1.51306,-5.67494 V 399.58544 153.69259 l -1.52571,-5.73412 c -5.66288,-21.28292 -21.4158,-36.89778 -42.2051,-41.83523 -5.63965,-1.33941 -7.66026,-1.3488 -253.17948,-1.17613 l -247.49447,0.17405 -4.72222,1.5953 c -18.05932,6.10093 -31.7315,19.23923 -37.4918,36.0278 -1.04762,3.05333 -2.22128,7.52472 -2.60813,9.93642 -0.47859,2.9836 -0.705,81.91876 -0.70847,246.99889 -0.005,218.14117 0.10226,243.1829 1.05916,248.25397 4.27172,22.63802 22.24346,40.86392 44.80877,45.4425 3.58848,0.72811 49.16893,0.87009 250.95237,0.78171 l 246.56747,-0.10801 z"
style="fill:#ed6b21;fill-opacity:1;stroke-width:0.674603" /><path
id="path21"
d="m 210.06357,199.0892 c 1.10499,-4.08272 3.30912,-7.53117 6.63124,-10.37477 5.42019,-4.63948 2.10678,-4.43387 71.33297,-4.42657 l 62.44927,0.007 3.44194,1.60268 c 4.05635,1.88877 7.75734,5.3977 9.8769,9.36436 l 1.52243,2.84915 v 199.00794 199.00793 l -1.52243,2.84915 c -2.11956,3.96665 -5.82055,7.47559 -9.8769,9.36436 l -3.44194,1.60267 -62.44927,0.007 c -69.78764,0.008 -65.98231,0.26152 -71.72146,-4.79028 -1.69319,-1.4904 -3.87229,-4.37627 -4.9283,-6.52672 l -1.89304,-3.85513 -0.12602,-196.30953 c -0.0987,-153.67069 0.0544,-196.97613 0.70461,-199.37859 z m 77.67539,363.64446 H 318.4334 V 397.11858 231.5035 H 287.73896 257.0445 v 165.61508 165.61508 z"
style="fill:#ed6b21;fill-opacity:1;stroke-width:0.674603" /><path
style="fill:#ed6b21;stroke-width:0.85"
class="st0"
d="m 533.78507,563.71023 h -60.775 v -330.565 h 60.775 v 330.565 m 48.195,27.88 v -386.24 c 0,-11.22 -9.095,-20.315 -20.315,-20.315 h -116.535 c -11.22,0 -20.315,9.095 -20.315,20.315 v 386.24 c 0,11.22 9.095,20.315 20.315,20.315 h 116.45 c 11.22,0 20.4,-9.095 20.4,-20.315 z"
id="path4-3" /><path
style="fill:#ed6b21;fill-opacity:1;stroke-width:0.674603"
d="m 425.89691,200.43815 c 1.10499,-4.08272 3.30912,-7.53117 6.63124,-10.37477 5.42019,-4.63948 2.10678,-4.43387 71.33297,-4.42657 l 62.44927,0.007 3.44194,1.60268 c 4.05635,1.88877 7.75734,5.3977 9.8769,9.36436 l 1.52243,2.84915 v 199.00794 199.00793 l -1.52243,2.84915 c -2.11956,3.96665 -5.82055,7.47559 -9.8769,9.36436 l -3.44194,1.60267 -62.44927,0.007 c -69.78764,0.008 -65.98231,0.26152 -71.72146,-4.79028 -1.69319,-1.4904 -3.87229,-4.37627 -4.9283,-6.52672 l -1.89304,-3.85513 -0.12602,-196.30953 c -0.0987,-153.67069 0.0544,-196.97613 0.70461,-199.37859 z m 77.67539,363.64446 h 30.69444 V 398.46753 232.85245 H 503.5723 472.87784 v 165.61508 165.61508 z"
id="path21-9" /></svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 800 800"
style="enable-background:new 0 0 800 800;"
xml:space="preserve"
sodipodi:docname="notification_pause_hover.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"><metadata
id="metadata15"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs13" /><sodipodi:namedview
inkscape:document-rotation="0"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1369"
id="namedview11"
showgrid="false"
inkscape:zoom="1.26"
inkscape:cx="400"
inkscape:cy="459.92063"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<style
type="text/css"
id="style2">
.st0{fill:#ED6B21;}
</style>
<path
style="stroke-width:0.9775"
class="st0"
d="M 322.50986,587.14728 H 252.61861 V 206.99753 h 69.89125 v 380.14975 m 55.42424,32.062 v -444.176 c 0,-12.903 -10.45925,-23.36225 -23.36225,-23.36225 H 220.55661 c -12.903,0 -23.36225,10.45925 -23.36225,23.36225 v 444.176 c 0,12.903 10.45925,23.36225 23.36225,23.36225 H 354.4741 c 12.90301,0 23.46,-10.45925 23.46,-23.36225 z"
id="path4" />
<g
id="g4"
transform="matrix(1.124125,0,0,1.124125,1.6564125,1.6571625)">
<path
id="path2"
class="st0"
d="M 597.2,701.3 H 110.6 C 53.2,701.3 6.5,654.6 6.5,597.2 V 110.6 C 6.5,53.2 53.2,6.5 110.6,6.5 h 486.6 c 57.4,0 104.1,46.7 104.1,104.1 v 486.6 c 0,57.4 -46.7,104.1 -104.1,104.1 z M 110.6,52.4 c -32,0 -58.2,26 -58.2,58.2 v 486.6 c 0,32 26,58.2 58.2,58.2 h 486.6 c 32,0 58.2,-26 58.2,-58.2 V 110.6 c 0,-32 -26,-58.2 -58.2,-58.2 z" />
</g>
<path
style="fill:#ed6b21;fill-opacity:1;stroke-width:0.775793"
d="M 113.38564,788.89154 C 99.043181,787.28541 82.717758,782.05833 69.865944,774.95732 49.44821,763.67591 31.031774,744.26404 21.085102,723.5398 14.202281,709.19918 11.01084,696.68205 9.7898958,679.23893 9.2371943,671.34269 9.0533622,578.69885 9.2180135,391.03159 9.4428891,134.71928 9.5571749,113.94803 10.778267,107.45586 15.648402,81.5629 26.510428,60.742725 44.359337,43.088132 60.732966,26.892769 81.111783,16.146 104.85191,11.187372 111.61267,9.7752445 120.95992,9.7290042 399.65349,9.7290042 h 287.81945 l 8.14584,1.7957388 c 24.50291,5.401636 42.43886,15.198483 59.7546,32.638727 16.88044,17.001834 26.67218,34.902929 31.92543,58.36559 l 1.8031,8.05311 v 288.59525 c 0,279.13094 -0.0481,288.82421 -1.45392,295.57737 -10.70181,51.39389 -51.96808,89.04855 -103.21901,94.18532 -10.55583,1.05803 -561.58037,1.01116 -571.04334,-0.0489 z m 577.54838,-52.48599 c 22.92898,-7.10509 39.89603,-24.63867 46.00128,-47.53726 l 1.74002,-6.52619 V 399.5653 116.78853 l -1.75457,-6.59424 c -6.51231,-24.475359 -24.62817,-42.432448 -48.53586,-48.110516 -6.4856,-1.540321 -8.8093,-1.55112 -291.1564,-1.352549 l -284.61864,0.200157 -5.43056,1.834595 c -20.768215,7.01607 -36.491222,22.125115 -43.115567,41.431973 -1.204763,3.51133 -2.554472,8.65343 -2.99935,11.42688 -0.550378,3.43114 -0.81075,94.20657 -0.81474,284.04872 -0.0058,250.86235 0.117599,279.66034 1.218034,285.49207 4.912478,26.03372 25.579979,46.99351 51.530083,52.25887 4.12675,0.83733 56.54427,1.00061 288.59523,0.89897 l 283.55259,-0.12421 z"
id="path17" /><path
style="fill:#ed6b21;fill-opacity:1;stroke-width:0.775793"
d="m 198.47436,169.38465 c 1.27074,-4.69513 3.80549,-8.66084 7.62593,-11.93098 6.23322,-5.33541 2.4228,-5.09896 82.03292,-5.09056 l 71.81666,0.008 3.95823,1.84308 c 4.6648,2.17209 8.92094,6.20736 11.35843,10.76902 l 1.7508,3.27652 v 228.85913 228.85912 l -1.7508,3.27652 c -2.43749,4.56165 -6.69363,8.59693 -11.35843,10.76902 l -3.95823,1.84307 -71.81666,0.008 c -80.25579,0.009 -75.87966,0.30074 -82.47968,-5.50883 -1.94717,-1.71396 -4.45314,-5.03271 -5.66755,-7.50572 l -2.17699,-4.4334 -0.14493,-225.75596 c -0.1135,-176.7213 0.0626,-226.52255 0.8103,-229.28538 z m 89.3267,418.19113 h 35.29861 V 397.11844 206.6611 h -35.29861 -35.29863 v 190.45734 190.45734 z"
id="path21" /><path
id="path4-3"
d="M 538.3432,588.49623 H 468.45195 V 208.34648 h 69.89125 v 380.14975 m 55.42425,32.062 v -444.176 c 0,-12.903 -10.45925,-23.36225 -23.36225,-23.36225 H 436.38995 c -12.903,0 -23.36225,10.45925 -23.36225,23.36225 v 444.176 c 0,12.903 10.45925,23.36225 23.36225,23.36225 h 133.9175 c 12.903,0 23.46,-10.45925 23.46,-23.36225 z"
class="st0"
style="fill:#ed6b21;stroke-width:0.9775" /><path
id="path21-9"
d="m 414.3077,170.7336 c 1.27074,-4.69513 3.80549,-8.66084 7.62593,-11.93098 6.23322,-5.33541 2.4228,-5.09896 82.03292,-5.09056 l 71.81666,0.008 3.95823,1.84308 c 4.6648,2.17209 8.92094,6.20736 11.35843,10.76902 l 1.7508,3.27652 v 228.85913 228.85912 l -1.7508,3.27652 c -2.43749,4.56165 -6.69363,8.59693 -11.35843,10.76902 l -3.95823,1.84307 -71.81666,0.008 c -80.25579,0.009 -75.87966,0.30074 -82.47968,-5.50883 -1.94717,-1.71396 -4.45314,-5.03271 -5.66755,-7.50572 l -2.17699,-4.4334 -0.14493,-225.75596 c -0.1135,-176.7213 0.0626,-226.52255 0.8103,-229.28538 z m 89.3267,418.19113 h 35.29861 V 398.46739 208.01005 H 503.6344 468.33577 v 190.45734 190.45734 z"
style="fill:#ed6b21;fill-opacity:1;stroke-width:0.775793" /></svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="notification_play.svg"
xml:space="preserve"
style="enable-background:new 0 0 800 800;"
viewBox="0 0 800 800"
y="0px"
x="0px"
id="Layer_1"
version="1.1"><metadata
id="metadata15"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs13" /><sodipodi:namedview
inkscape:current-layer="Layer_1"
inkscape:window-maximized="1"
inkscape:window-y="-8"
inkscape:window-x="-8"
inkscape:cy="407.44757"
inkscape:cx="291.98172"
inkscape:zoom="1.7819091"
showgrid="false"
id="namedview11"
inkscape:window-height="1369"
inkscape:window-width="2560"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:document-rotation="0" />
<style
id="style2"
type="text/css">
.st0{fill:#ED6B21;}
</style>
<path
id="path6"
d="M 501.14231,400.1595 345.84732,517.11951 v -233.75 l 155.29499,116.79 m 66.63999,0 c 0,-6.12 -2.72,-12.155 -8.15999,-16.235 L 330.12233,211.2895 c -13.43,-10.115 -32.555,-0.51 -32.555,16.235 v 345.35501 c 0,16.745 19.125,26.35 32.555,16.235 l 229.49998,-172.805 c 5.44,-3.995 8.16,-10.03 8.15999,-16.15 z"
class="st0"
style="stroke-width:0.85" />
<g
transform="matrix(0.9775,0,0,0.9775,53.547,53.54776)"
id="g4">
<path
d="M 597.2,701.3 H 110.6 C 53.2,701.3 6.5,654.6 6.5,597.2 V 110.6 C 6.5,53.2 53.2,6.5 110.6,6.5 h 486.6 c 57.4,0 104.1,46.7 104.1,104.1 v 486.6 c 0,57.4 -46.7,104.1 -104.1,104.1 z M 110.6,52.4 c -32,0 -58.2,26 -58.2,58.2 v 486.6 c 0,32 26,58.2 58.2,58.2 h 486.6 c 32,0 58.2,-26 58.2,-58.2 V 110.6 c 0,-32 -26,-58.2 -58.2,-58.2 z"
class="st0"
id="path2" />
</g>
<path
id="path17"
d="m 150.65676,738.13001 c -12.4717,-1.39665 -26.66772,-5.9419 -37.84321,-12.1167 C 95.05898,716.20336 79.0447,699.32348 70.39542,681.3024 64.41036,668.8323 61.63518,657.94784 60.5735,642.7799 c -0.48062,-6.86631 -0.64046,-87.42616 -0.4973,-250.61509 0.1956,-222.88026 0.29494,-240.94222 1.35676,-246.58758 4.2349,-22.51562 13.68014,-40.62012 29.20092,-55.971935 14.23795,-14.08294 31.95867,-23.42796 52.60225,-27.7398 5.87892,-1.22794 14.00696,-1.26814 256.3492,-1.26814 h 250.27778 l 7.08334,1.5615 c 21.30688,4.69708 36.90336,13.21608 51.96053,28.3815 14.67865,14.784215 23.1932,30.350375 27.76125,50.752695 l 1.5679,7.0027 v 250.95241 c 0,242.72256 -0.042,251.15147 -1.2643,257.0238 -9.3059,44.69035 -45.18961,77.4335 -89.75565,81.9003 -9.17897,0.92 -488.33076,0.87925 -496.55942,-0.0425 z M 652.87274,692.49 c 19.93824,-6.17835 34.6922,-21.42494 40.00112,-41.33676 l 1.51306,-5.67491 V 399.58544 153.69258 l -1.52572,-5.7341 c -5.66288,-21.28292 -21.4158,-36.89778 -42.2051,-41.83524 -5.63964,-1.3394 -7.66026,-1.3488 -253.17948,-1.17612 l -247.49446,0.174 -4.72222,1.5953 c -18.05932,6.10094 -31.7315,19.23924 -37.4918,36.02779 -1.04762,3.05335 -2.22128,7.52472 -2.60814,9.93642 -0.47858,2.98361 -0.705,81.91877 -0.70846,246.99891 -0.004,218.14116 0.1022,243.18289 1.05916,248.25395 4.27172,22.63803 22.24346,40.86393 44.80876,45.44251 3.58848,0.72811 49.16894,0.8701 250.95238,0.7817 l 246.56746,-0.10799 z"
style="fill:#ed6b21;fill-opacity:1;stroke-width:0.6746" /><path
id="path19"
d="m 300.17824,218.44362 c 2.06443,-4.13128 4.83026,-6.86894 9.40917,-9.3133 3.43647,-1.83451 12.82007,-1.79344 16.52778,0.0724 3.22403,1.6224 232.7236,174.17375 235.79013,177.281 3.35767,3.4022 4.88323,7.0012 5.23218,12.34324 0.27168,4.159 0.0718,5.32914 -1.5138,8.86232 -1.00471,2.23886 -2.78731,4.97342 -3.96129,6.07682 -4.28583,4.02812 -232.27571,175.25818 -235.54572,176.9048 -4.6586,2.34584 -12.1025,2.3876 -16.52928,0.0928 -3.87486,-2.00878 -7.80538,-5.7435 -9.67029,-9.18862 l -1.46069,-2.69842 -0.1736,-178.1462 -0.1736,-178.14618 z m 45.66908,298.67589 c 1.00351,-0.0612 154.99009,-116.48775 154.99009,-117.18534 0,-0.71184 -154.02593,-116.84804 -154.99009,-116.86318 -0.27827,-0.004 -0.50595,52.66168 -0.50595,117.03568 0,64.37401 0.22768,117.0298 0.50595,117.01284 z"
style="fill:#ed6b21;fill-opacity:1;stroke-width:0.6746" /><path
style="fill:#ed6b21;stroke-width:0.85"
class="st0"
d="M 501.14231,400.15949 345.84732,517.1195 v -233.75 l 155.29499,116.79 m 66.63999,0 c 0,-6.12 -2.72,-12.155 -8.15999,-16.235 L 330.12233,211.28949 c -13.43,-10.115 -32.555,-0.51 -32.555,16.235 V 572.8795 c 0,16.745 19.125,26.35 32.555,16.235 l 229.49998,-172.805 c 5.44,-3.995 8.16,-10.03 8.15999,-16.15 z"
id="path6-2" /><path
style="fill:#ed6b21;fill-opacity:1;stroke-width:0.6746"
d="m 300.17824,218.44361 c 2.06443,-4.13128 4.83026,-6.86894 9.40917,-9.3133 3.43647,-1.83451 12.82007,-1.79344 16.52778,0.0724 3.22403,1.6224 232.7236,174.17375 235.79013,177.281 3.35767,3.4022 4.88323,7.0012 5.23218,12.34324 0.27168,4.159 0.0718,5.32914 -1.5138,8.86232 -1.00471,2.23886 -2.78731,4.97342 -3.96129,6.07682 -4.28583,4.02812 -232.27571,175.25818 -235.54572,176.9048 -4.6586,2.34584 -12.1025,2.3876 -16.52928,0.0928 -3.87486,-2.00878 -7.80538,-5.7435 -9.67029,-9.18862 l -1.46069,-2.69842 -0.1736,-178.1462 -0.1736,-178.14618 z m 45.66908,298.67589 c 1.00351,-0.0612 154.99009,-116.48775 154.99009,-117.18534 0,-0.71184 -154.02593,-116.84804 -154.99009,-116.86318 -0.27827,-0.004 -0.50595,52.66168 -0.50595,117.03568 0,64.37401 0.22768,117.0298 0.50595,117.01284 z"
id="path19-3" /></svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 800 800"
style="enable-background:new 0 0 800 800;"
xml:space="preserve"
sodipodi:docname="notification_play_hover.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"><metadata
id="metadata15"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs13" /><sodipodi:namedview
inkscape:document-rotation="0"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1369"
id="namedview11"
showgrid="false"
inkscape:zoom="0.89095455"
inkscape:cx="422.22291"
inkscape:cy="611.44958"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<style
type="text/css"
id="style2">
.st0{fill:#ED6B21;}
</style>
<path
style="stroke-width:0.9775"
class="st0"
d="M 511.41243,400.15312 332.8232,534.65714 v -268.8125 l 178.58923,134.3085 m 76.63599,0 c 0,-7.038 -3.128,-13.97825 -9.38399,-18.67025 L 314.73946,182.95262 c -15.4445,-11.63225 -37.43825,-0.5865 -37.43825,18.67025 v 397.15827 c 0,19.25675 21.99375,30.3025 37.43825,18.67025 L 578.66443,418.72564 c 6.256,-4.59425 9.384,-11.5345 9.38399,-18.5725 z"
id="path6" />
<g
id="g4"
transform="matrix(1.124125,0,0,1.124125,1.6564125,1.6571725)">
<path
id="path2"
class="st0"
d="M 597.2,701.3 H 110.6 C 53.2,701.3 6.5,654.6 6.5,597.2 V 110.6 C 6.5,53.2 53.2,6.5 110.6,6.5 h 486.6 c 57.4,0 104.1,46.7 104.1,104.1 v 486.6 c 0,57.4 -46.7,104.1 -104.1,104.1 z M 110.6,52.4 c -32,0 -58.2,26 -58.2,58.2 v 486.6 c 0,32 26,58.2 58.2,58.2 h 486.6 c 32,0 58.2,-26 58.2,-58.2 V 110.6 c 0,-32 -26,-58.2 -58.2,-58.2 z" />
</g>
<path
style="fill:#ed6b21;fill-opacity:1;stroke-width:0.77579"
d="M 113.38564,788.89156 C 99.043182,787.28541 82.717759,782.05837 69.865945,774.95735 49.44819,763.67591 31.031768,744.26405 21.085096,723.53981 14.202277,709.19919 11.01082,696.68206 9.7898876,679.23893 9.2371746,671.34268 9.0533586,578.69885 9.2179926,391.03158 9.4429326,134.71928 9.5571736,113.94803 10.778267,107.45586 15.648402,81.562898 26.510428,60.742723 44.359325,43.088136 60.732967,26.892755 81.111795,16.145982 104.85191,11.187366 111.61267,9.7752352 120.95992,9.7290052 399.65349,9.7290052 h 287.81945 l 8.14584,1.7957248 c 24.50291,5.401642 42.43886,15.198492 59.75461,32.638725 16.88045,17.001847 26.67218,34.902931 31.92544,58.365595 l 1.80308,8.05311 v 288.59527 c 0,279.13094 -0.0483,288.82419 -1.45394,295.57737 -10.70179,51.3939 -51.96805,89.04853 -103.219,94.18535 -10.55582,1.058 -561.58037,1.01113 -571.04333,-0.0489 z m 577.54837,-52.48601 c 22.92898,-7.10511 39.89603,-24.63868 46.00129,-47.53728 l 1.74002,-6.52614 V 399.5653 116.78851 l -1.75458,-6.59421 c -6.51231,-24.475359 -24.62817,-42.432448 -48.53586,-48.110527 -6.48559,-1.54031 -8.8093,-1.55112 -291.1564,-1.352538 l -284.61863,0.2001 -5.43056,1.834595 c -20.768214,7.016081 -36.491221,22.125126 -43.115566,41.43196 -1.204763,3.51135 -2.554472,8.65343 -2.999361,11.42688 -0.550367,3.43115 -0.81075,94.20659 -0.814729,284.04875 -0.0046,250.86233 0.11753,279.66032 1.218034,285.49204 4.912478,26.03373 25.579979,46.99352 51.530072,52.25889 4.12675,0.83732 56.54428,1.00061 288.59524,0.89895 l 283.55258,-0.12419 z"
id="path17" /><path
style="fill:#ed6b21;fill-opacity:1;stroke-width:0.77579"
d="m 280.30386,191.19243 c 2.37409,-4.75097 5.55479,-7.89928 10.82054,-10.7103 3.95194,-2.10968 14.74308,-2.06245 19.00695,0.0833 3.70763,1.86576 267.63214,200.29982 271.15865,203.87315 3.86132,3.91253 5.61571,8.05138 6.017,14.19473 0.31244,4.78285 0.0826,6.12851 -1.74087,10.19167 -1.15541,2.57469 -3.2054,5.71943 -4.55548,6.98834 -4.9287,4.63234 -267.11707,201.54691 -270.87758,203.44052 -5.35739,2.69772 -13.91787,2.74574 -19.00867,0.10672 -4.45609,-2.3101 -8.97619,-6.60503 -11.12083,-10.56691 l -1.6798,-3.10319 -0.19964,-204.86813 -0.19964,-204.8681 z M 332.8233,534.6697 c 1.15403,-0.0704 178.2386,-133.96091 178.2386,-134.76314 0,-0.81862 -177.12982,-134.37525 -178.2386,-134.39266 -0.32001,-0.005 -0.58185,60.56094 -0.58185,134.59104 0,74.03011 0.26184,134.58427 0.58185,134.56476 z"
id="path19" /><path
id="path6-2"
d="M 511.41243,400.15311 332.8232,534.65713 v -268.8125 l 178.58923,134.3085 m 76.63599,0 c 0,-7.038 -3.128,-13.97825 -9.38399,-18.67025 L 314.73946,182.95261 c -15.4445,-11.63225 -37.43825,-0.5865 -37.43825,18.67025 v 397.15827 c 0,19.25675 21.99375,30.3025 37.43825,18.67025 L 578.66443,418.72563 c 6.256,-4.59425 9.384,-11.5345 9.38399,-18.5725 z"
class="st0"
style="fill:#ed6b21;stroke-width:0.9775" /><path
id="path19-3"
d="m 280.30386,191.19242 c 2.37409,-4.75097 5.55479,-7.89928 10.82054,-10.7103 3.95194,-2.10968 14.74308,-2.06245 19.00695,0.0833 3.70763,1.86576 267.63214,200.29982 271.15865,203.87315 3.86132,3.91253 5.61571,8.05138 6.017,14.19473 0.31244,4.78285 0.0826,6.12851 -1.74087,10.19167 -1.15541,2.57469 -3.2054,5.71943 -4.55548,6.98834 -4.9287,4.63234 -267.11707,201.54691 -270.87758,203.44052 -5.35739,2.69772 -13.91787,2.74574 -19.00867,0.10672 -4.45609,-2.3101 -8.97619,-6.60503 -11.12083,-10.56691 l -1.6798,-3.10319 -0.19964,-204.86813 -0.19964,-204.8681 z m 52.51944,343.47727 c 1.15403,-0.0704 178.2386,-133.96091 178.2386,-134.76314 0,-0.81862 -177.12982,-134.37525 -178.2386,-134.39266 -0.32001,-0.005 -0.58185,60.56094 -0.58185,134.59104 0,74.03011 0.26184,134.58427 0.58185,134.56476 z"
style="fill:#ed6b21;fill-opacity:1;stroke-width:0.77579" /></svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -113,6 +113,9 @@ int CLI::run(int argc, char **argv)
std::find(m_transforms.begin(), m_transforms.end(), "cut") == m_transforms.end() &&
std::find(m_transforms.begin(), m_transforms.end(), "cut_x") == m_transforms.end() &&
std::find(m_transforms.begin(), m_transforms.end(), "cut_y") == m_transforms.end();
bool start_downloader = false;
bool delete_after_load = false;
std::string download_url;
bool start_as_gcodeviewer =
#ifdef _WIN32
false;
@ -221,6 +224,11 @@ int CLI::run(int argc, char **argv)
}
if (!start_as_gcodeviewer) {
for (const std::string& file : m_input_files) {
if (boost::starts_with(file, "prusaslicer://")) {
start_downloader = true;
download_url = file;
continue;
}
if (!boost::filesystem::exists(file)) {
boost::nowide::cerr << "No such file: " << file << std::endl;
exit(1);
@ -478,6 +486,9 @@ int CLI::run(int argc, char **argv)
// Models are repaired by default.
//for (auto &model : m_models)
// model.repair();
} else if (opt_key == "delete-after-load") {
delete_after_load = true;
} else {
boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl;
return 1;
@ -663,9 +674,12 @@ int CLI::run(int argc, char **argv)
params.extra_config = std::move(m_extra_config);
params.input_files = std::move(m_input_files);
params.start_as_gcodeviewer = start_as_gcodeviewer;
params.start_downloader = start_downloader;
params.download_url = download_url;
params.delete_after_load = delete_after_load;
#if ENABLE_GL_CORE_PROFILE
params.opengl_version = opengl_version;
#if ENABLE_OPENGL_DEBUG_OPTION
params.opengl_version = opengl_version;
params.opengl_debug = opengl_debug;
#endif // ENABLE_OPENGL_DEBUG_OPTION
#endif // ENABLE_GL_CORE_PROFILE

View File

@ -158,6 +158,12 @@ namespace ImGui
const wchar_t SliderFloatEditBtnIcon = 0x2604;
const wchar_t SliderFloatEditBtnPressedIcon = 0x2605;
const wchar_t ClipboardBtnIcon = 0x2606;
const wchar_t PlayButton = 0x2618;
const wchar_t PlayHoverButton = 0x2619;
const wchar_t PauseButton = 0x261A;
const wchar_t PauseHoverButton = 0x261B;
const wchar_t OpenButton = 0x261C;
const wchar_t OpenHoverButton = 0x261D;
const wchar_t LegendTravel = 0x2701;
const wchar_t LegendWipe = 0x2702;
@ -173,8 +179,8 @@ namespace ImGui
const wchar_t LegendToolMarker = 0x2712;
const wchar_t WarningMarkerSmall = 0x2713;
const wchar_t ExpandBtn = 0x2714;
const wchar_t CollapseBtn = 0x2715;
const wchar_t InfoMarkerSmall = 0x2716;
const wchar_t CollapseBtn = 0x2715;
// void MyFunction(const char* name, const MyMatrix44& v);
}

View File

@ -4632,6 +4632,10 @@ CLITransformConfigDef::CLITransformConfigDef()
def->label = L("Scale to Fit");
def->tooltip = L("Scale to fit the given volume.");
def->set_default_value(new ConfigOptionPoint3(Vec3d(0,0,0)));
def = this->add("delete-after-load", coString);
def->label = L("Delete files after loading");
def->tooltip = L("Delete files after loading.");
}
CLIMiscConfigDef::CLIMiscConfigDef()

View File

@ -110,6 +110,17 @@
<string>Alternate</string>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>PrusaSlicer Downloads</string>
<key>CFBundleURLSchemes</key>
<array>
<string>prusaslicer</string>
</array>
</dict>
</array>
<key>LSMinimumSystemVersion</key>
<string>10.12</string>
<key>NSPrincipalClass</key>

View File

@ -232,6 +232,12 @@ set(SLIC3R_GUI_SOURCES
GUI/DesktopIntegrationDialog.hpp
GUI/HintNotification.cpp
GUI/HintNotification.hpp
GUI/FileArchiveDialog.cpp
GUI/FileArchiveDialog.hpp
GUI/Downloader.cpp
GUI/Downloader.hpp
GUI/DownloaderFileGet.cpp
GUI/DownloaderFileGet.hpp
Utils/AppUpdater.cpp
Utils/AppUpdater.hpp
Utils/Http.cpp

File diff suppressed because it is too large Load Diff

View File

@ -392,10 +392,56 @@ struct PageUpdate: ConfigWizardPage
{
bool version_check;
bool preset_update;
wxTextCtrl* path_text_ctrl;
PageUpdate(ConfigWizard *parent);
};
namespace DownloaderUtils {
wxString get_downloads_path();
class Worker : public wxBoxSizer
{
wxWindow* m_parent {nullptr};
wxTextCtrl* m_input_path {nullptr};
bool downloader_checked {false};
#ifdef __linux__
bool perform_registration_linux { false };
#endif // __linux__
bool perform_register();
void deregister();
public:
Worker(wxWindow* parent);
~Worker(){}
void allow(bool allow_) { downloader_checked = allow_; }
bool is_checked() const { return downloader_checked; }
wxString path_name() const { return m_input_path ? m_input_path->GetValue() : wxString(); }
void set_path_name(wxString name);
void set_path_name(const std::string& name);
bool on_finish();
#ifdef __linux__
bool get_perform_registration_linux() { return perform_registration_linux; }
#endif // __linux__
};
}
struct PageDownloader : ConfigWizardPage
{
DownloaderUtils::Worker* downloader{ nullptr };
PageDownloader(ConfigWizard* parent);
bool on_finish_downloader() const ;
};
struct PageReloadFromDisk : ConfigWizardPage
{
bool full_pathnames;
@ -583,7 +629,8 @@ struct ConfigWizard::priv
PageMaterials *page_filaments = nullptr;
PageMaterials *page_sla_materials = nullptr;
PageCustom *page_custom = nullptr;
PageUpdate *page_update = nullptr;
PageUpdate* page_update = nullptr;
PageDownloader* page_downloader = nullptr;
PageReloadFromDisk *page_reload_from_disk = nullptr;
#ifdef _WIN32
PageFilesAssociation* page_files_association = nullptr;
@ -631,9 +678,9 @@ struct ConfigWizard::priv
bool apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes);
// #ys_FIXME_alise
void update_presets_in_config(const std::string& section, const std::string& alias_key, bool add);
#ifdef __linux__
void perform_desktop_integration() const;
#endif
//#ifdef __linux__
// void perform_desktop_integration() const;
//#endif
bool check_fff_selected(); // Used to decide whether to display Filaments page
bool check_sla_selected(); // Used to decide whether to display SLA Materials page

View File

@ -218,10 +218,9 @@ bool DesktopIntegrationDialog::integration_possible()
{
return true;
}
void DesktopIntegrationDialog::perform_desktop_integration()
void DesktopIntegrationDialog::perform_desktop_integration(bool perform_downloader)
{
BOOST_LOG_TRIVIAL(debug) << "performing desktop integration";
BOOST_LOG_TRIVIAL(debug) << "performing desktop integration. With downloader integration: " << perform_downloader;
// Path to appimage
const char *appimage_env = std::getenv("APPIMAGE");
std::string excutable_path;
@ -287,7 +286,7 @@ void DesktopIntegrationDialog::perform_desktop_integration()
std::string target_dir_icons;
std::string target_dir_desktop;
// slicer icon
// iterate thru target_candidates to find icons folder
for (size_t i = 0; i < target_candidates.size(); ++i) {
@ -300,20 +299,20 @@ void DesktopIntegrationDialog::perform_desktop_integration()
break; // success
else
target_dir_icons.clear(); // copying failed
// if all failed - try creating default home folder
if (i == target_candidates.size() - 1) {
// create $HOME/.local/share
create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/icons" + icon_theme_dirs);
// copy icon
target_dir_icons = GUI::format("%1%/.local/share",wxFileName::GetHomeDir());
std::string icon_path = GUI::format("%1%/icons/PrusaSlicer.png",resources_dir());
std::string dest_path = GUI::format("%1%/icons/%2%PrusaSlicer%3%.png", target_dir_icons, icon_theme_path, version_suffix);
if (!contains_path_dir(target_dir_icons, "icons")
|| !copy_icon(icon_path, dest_path)) {
// every attempt failed - icon wont be present
target_dir_icons.clear();
}
}
}
// if all failed - try creating default home folder
if (i == target_candidates.size() - 1) {
// create $HOME/.local/share
create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/icons" + icon_theme_dirs);
// copy icon
target_dir_icons = GUI::format("%1%/.local/share",wxFileName::GetHomeDir());
std::string icon_path = GUI::format("%1%/icons/PrusaSlicer.png",resources_dir());
std::string dest_path = GUI::format("%1%/icons/%2%PrusaSlicer%3%.png", target_dir_icons, icon_theme_path, version_suffix);
if (!contains_path_dir(target_dir_icons, "icons")
|| !copy_icon(icon_path, dest_path)) {
// every attempt failed - icon wont be present
target_dir_icons.clear();
}
}
}
if(target_dir_icons.empty()) {
@ -324,25 +323,25 @@ void DesktopIntegrationDialog::perform_desktop_integration()
// desktop file
// iterate thru target_candidates to find applications folder
for (size_t i = 0; i < target_candidates.size(); ++i)
{
std::string desktop_file = GUI::format(
"[Desktop Entry]\n"
"Name=PrusaSlicer%1%\n"
"GenericName=3D Printing Software\n"
"Icon=PrusaSlicer%2%\n"
"Exec=\"%3%\" %%F\n"
"Terminal=false\n"
"Type=Application\n"
"MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;application/x-amf;\n"
"Categories=Graphics;3DGraphics;Engineering;\n"
"Keywords=3D;Printing;Slicer;slice;3D;printer;convert;gcode;stl;obj;amf;SLA\n"
"StartupNotify=false\n"
"StartupWMClass=prusa-slicer\n", name_suffix, version_suffix, excutable_path);
for (size_t i = 0; i < target_candidates.size(); ++i) {
if (contains_path_dir(target_candidates[i], "applications")) {
target_dir_desktop = target_candidates[i];
// Write slicer desktop file
std::string desktop_file = GUI::format(
"[Desktop Entry]\n"
"Name=PrusaSlicer%1%\n"
"GenericName=3D Printing Software\n"
"Icon=PrusaSlicer%2%\n"
"Exec=\"%3%\" %%F\n"
"Terminal=false\n"
"Type=Application\n"
"MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;application/x-amf;\n"
"Categories=Graphics;3DGraphics;Engineering;\n"
"Keywords=3D;Printing;Slicer;slice;3D;printer;convert;gcode;stl;obj;amf;SLA\n"
"StartupNotify=false\n"
"StartupWMClass=prusa-slicer\n", name_suffix, version_suffix, excutable_path);
std::string path = GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix);
if (create_desktop_file(path, desktop_file)){
BOOST_LOG_TRIVIAL(debug) << "PrusaSlicer.desktop file installation success.";
@ -352,24 +351,24 @@ void DesktopIntegrationDialog::perform_desktop_integration()
BOOST_LOG_TRIVIAL(debug) << "Attempt to PrusaSlicer.desktop file installation failed. failed path: " << target_candidates[i];
target_dir_desktop.clear();
}
// if all failed - try creating default home folder
if (i == target_candidates.size() - 1) {
// create $HOME/.local/share
create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/applications");
// create desktop file
target_dir_desktop = GUI::format("%1%/.local/share",wxFileName::GetHomeDir());
std::string path = GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix);
if (contains_path_dir(target_dir_desktop, "applications")) {
if (!create_desktop_file(path, desktop_file)) {
// Desktop file not written - end desktop integration
BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not create desktop file";
return;
}
} else {
// Desktop file not written - end desktop integration
BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed because the application directory was not found.";
}
// if all failed - try creating default home folder
if (i == target_candidates.size() - 1) {
// create $HOME/.local/share
create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/applications");
// create desktop file
target_dir_desktop = GUI::format("%1%/.local/share",wxFileName::GetHomeDir());
std::string path = GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix);
if (contains_path_dir(target_dir_desktop, "applications")) {
if (!create_desktop_file(path, desktop_file)) {
// Desktop file not written - end desktop integration
BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not create desktop file";
return;
}
} else {
// Desktop file not written - end desktop integration
BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed because the application directory was not found.";
return;
}
}
}
@ -398,7 +397,7 @@ void DesktopIntegrationDialog::perform_desktop_integration()
}
// Desktop file
std::string desktop_file = GUI::format(
std::string desktop_file_viewer = GUI::format(
"[Desktop Entry]\n"
"Name=Prusa Gcode Viewer%1%\n"
"GenericName=3D Printing Software\n"
@ -410,9 +409,8 @@ void DesktopIntegrationDialog::perform_desktop_integration()
"Categories=Graphics;3DGraphics;\n"
"Keywords=3D;Printing;Slicer;\n"
"StartupNotify=false\n", name_suffix, version_suffix, excutable_path);
std::string desktop_path = GUI::format("%1%/applications/PrusaSlicerGcodeViewer%2%.desktop", target_dir_desktop, version_suffix);
if (create_desktop_file(desktop_path, desktop_file))
if (create_desktop_file(desktop_path, desktop_file_viewer))
// save path to desktop file
app_config->set("desktop_integration_app_viewer_path", desktop_path);
else {
@ -421,6 +419,37 @@ void DesktopIntegrationDialog::perform_desktop_integration()
}
}
if (perform_downloader)
{
std::string desktop_file_downloader = GUI::format(
"[Desktop Entry]\n"
"Name=PrusaSlicer URL Protocol%1%\n"
"Exec=\"%3%\" --single-instance %%u\n"
"Icon=PrusaSlicer%4%\n"
"Terminal=false\n"
"Type=Application\n"
"MimeType=x-scheme-handler/prusaslicer;\n"
"StartupNotify=false\n"
, name_suffix, version_suffix, excutable_path, version_suffix);
// desktop file for downloader as part of main app
std::string desktop_path = GUI::format("%1%/applications/PrusaSlicerURLProtocol%2%.desktop", target_dir_desktop, version_suffix);
if (create_desktop_file(desktop_path, desktop_file_downloader)) {
// save path to desktop file
app_config->set("desktop_integration_URL_path", desktop_path);
// finish registration on mime type
std::string command = GUI::format("xdg-mime default PrusaSlicerURLProtocol%1%.desktop x-scheme-handler/prusaslicer", version_suffix);
BOOST_LOG_TRIVIAL(debug) << "system command: " << command;
int r = system(command.c_str());
BOOST_LOG_TRIVIAL(debug) << "system result: " << r;
} else {
BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not create URL Protocol desktop file";
show_error(nullptr, _L("Performing desktop integration failed - could not create URL Protocol desktop file."));
return;
}
}
wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationSuccess);
}
void DesktopIntegrationDialog::undo_desktop_intgration()
@ -453,9 +482,26 @@ void DesktopIntegrationDialog::undo_desktop_intgration()
std::remove(path.c_str());
}
}
// URL Protocol
path = std::string(app_config->get("desktop_integration_URL_path"));
if (!path.empty()) {
BOOST_LOG_TRIVIAL(debug) << "removing " << path;
std::remove(path.c_str());
}
wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::UndoDesktopIntegrationSuccess);
}
void DesktopIntegrationDialog::undo_downloader_registration()
{
const AppConfig *app_config = wxGetApp().app_config;
std::string path = std::string(app_config->get("desktop_integration_URL_path"));
if (!path.empty()) {
BOOST_LOG_TRIVIAL(debug) << "removing " << path;
std::remove(path.c_str());
}
// There is no need to undo xdg-mime default command. It is done automatically when desktop file is deleted.
}
DesktopIntegrationDialog::DesktopIntegrationDialog(wxWindow *parent)
: wxDialog(parent, wxID_ANY, _(L("Desktop Integration")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER)
{
@ -481,7 +527,7 @@ DesktopIntegrationDialog::DesktopIntegrationDialog(wxWindow *parent)
wxButton *btn_perform = new wxButton(this, wxID_ANY, _L("Perform"));
btn_szr->Add(btn_perform, 0, wxALL, 10);
btn_perform->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { DesktopIntegrationDialog::perform_desktop_integration(); EndModal(wxID_ANY); });
btn_perform->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { DesktopIntegrationDialog::perform_desktop_integration(false); EndModal(wxID_ANY); });
if (can_undo){
wxButton *btn_undo = new wxButton(this, wxID_ANY, _L("Undo"));

View File

@ -26,9 +26,14 @@ public:
// Creates Desktop files and icons for both PrusaSlicer and GcodeViewer.
// Stores paths into App Config.
// Rewrites if files already existed.
static void perform_desktop_integration();
// if perform_downloader:
// Creates Destktop files for PrusaSlicer downloader feature
// Regiters PrusaSlicer to start on prusaslicer:// URL
static void perform_desktop_integration(bool perform_downloader);
// Deletes Desktop files and icons for both PrusaSlicer and GcodeViewer at paths stored in App Config.
static void undo_desktop_intgration();
static void undo_downloader_registration();
private:
};

View File

@ -0,0 +1,245 @@
#include "Downloader.hpp"
#include "GUI_App.hpp"
#include "NotificationManager.hpp"
#include <boost/algorithm/string.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace GUI {
namespace {
void open_folder(const std::string& path)
{
// Code taken from NotificationManager.cpp
// Execute command to open a file explorer, platform dependent.
// FIXME: The const_casts aren't needed in wxWidgets 3.1, remove them when we upgrade.
#ifdef _WIN32
const wxString widepath = from_u8(path);
const wchar_t* argv[] = { L"explorer", widepath.GetData(), nullptr };
::wxExecute(const_cast<wchar_t**>(argv), wxEXEC_ASYNC, nullptr);
#elif __APPLE__
const char* argv[] = { "open", path.data(), nullptr };
::wxExecute(const_cast<char**>(argv), wxEXEC_ASYNC, nullptr);
#else
const char* argv[] = { "xdg-open", path.data(), nullptr };
// Check if we're running in an AppImage container, if so, we need to remove AppImage's env vars,
// because they may mess up the environment expected by the file manager.
// Mostly this is about LD_LIBRARY_PATH, but we remove a few more too for good measure.
if (wxGetEnv("APPIMAGE", nullptr)) {
// We're running from AppImage
wxEnvVariableHashMap env_vars;
wxGetEnvMap(&env_vars);
env_vars.erase("APPIMAGE");
env_vars.erase("APPDIR");
env_vars.erase("LD_LIBRARY_PATH");
env_vars.erase("LD_PRELOAD");
env_vars.erase("UNION_PRELOAD");
wxExecuteEnv exec_env;
exec_env.env = std::move(env_vars);
wxString owd;
if (wxGetEnv("OWD", &owd)) {
// This is the original work directory from which the AppImage image was run,
// set it as CWD for the child process:
exec_env.cwd = std::move(owd);
}
::wxExecute(const_cast<char**>(argv), wxEXEC_ASYNC, nullptr, &exec_env);
}
else {
// Looks like we're NOT running from AppImage, we'll make no changes to the environment.
::wxExecute(const_cast<char**>(argv), wxEXEC_ASYNC, nullptr, nullptr);
}
#endif
}
std::string filename_from_url(const std::string& url)
{
// TODO: can it be done with curl?
size_t slash = url.find_last_of("/");
if (slash == std::string::npos && slash != url.size() - 1)
return std::string();
return url.substr(slash + 1, url.size() - slash + 1);
}
}
Download::Download(int ID, std::string url, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder)
: m_id(ID)
, m_filename(filename_from_url(url))
, m_dest_folder(dest_folder)
{
assert(boost::filesystem::is_directory(dest_folder));
m_final_path = dest_folder / m_filename;
m_file_get = std::make_shared<FileGet>(ID, std::move(url), m_filename, evt_handler, dest_folder);
}
void Download::start()
{
m_state = DownloadState::DownloadOngoing;
m_file_get->get();
}
void Download::cancel()
{
m_state = DownloadState::DownloadStopped;
m_file_get->cancel();
}
void Download::pause()
{
//assert(m_state == DownloadState::DownloadOngoing);
// if instead of assert - it can happen that user clicks on pause several times before the pause happens
if (m_state != DownloadState::DownloadOngoing)
return;
m_state = DownloadState::DownloadPaused;
m_file_get->pause();
}
void Download::resume()
{
//assert(m_state == DownloadState::DownloadPaused);
if (m_state != DownloadState::DownloadPaused)
return;
m_state = DownloadState::DownloadOngoing;
m_file_get->resume();
}
Downloader::Downloader()
: wxEvtHandler()
{
//Bind(EVT_DWNLDR_FILE_COMPLETE, [](const wxCommandEvent& evt) {});
//Bind(EVT_DWNLDR_FILE_PROGRESS, [](const wxCommandEvent& evt) {});
//Bind(EVT_DWNLDR_FILE_ERROR, [](const wxCommandEvent& evt) {});
//Bind(EVT_DWNLDR_FILE_NAME_CHANGE, [](const wxCommandEvent& evt) {});
Bind(EVT_DWNLDR_FILE_COMPLETE, &Downloader::on_complete, this);
Bind(EVT_DWNLDR_FILE_PROGRESS, &Downloader::on_progress, this);
Bind(EVT_DWNLDR_FILE_ERROR, &Downloader::on_error, this);
Bind(EVT_DWNLDR_FILE_NAME_CHANGE, &Downloader::on_name_change, this);
Bind(EVT_DWNLDR_FILE_PAUSED, &Downloader::on_paused, this);
Bind(EVT_DWNLDR_FILE_CANCELED, &Downloader::on_canceled, this);
}
void Downloader::start_download(const std::string& full_url)
{
assert(m_initialized);
// TODO: There is a misterious slash appearing in recieved msg on windows
#ifdef _WIN32
if (!boost::starts_with(full_url, "prusaslicer://open/?file=")) {
#else
if (!boost::starts_with(full_url, "prusaslicer://open?file=")) {
#endif
BOOST_LOG_TRIVIAL(error) << "Could not start download due to wrong URL: " << full_url;
// TODO: show error?
return;
}
size_t id = get_next_id();
// TODO: still same mistery
#ifdef _WIN32
std::string escaped_url = FileGet::escape_url(full_url.substr(25));
#else
std::string escaped_url = FileGet::escape_url(full_url.substr(24));
#endif
// TODO: enable after testing
/*
if (!boost::starts_with(escaped_url, "https://media.printables.com/")) {
BOOST_LOG_TRIVIAL(error) << "Download won't start. Download URL doesn't point to https://media.printables.com : " << escaped_url;
// TODO: show error?
return;
}
*/
std::string text(escaped_url);
m_downloads.emplace_back(std::make_unique<Download>(id, std::move(escaped_url), this, m_dest_folder));
NotificationManager* ntf_mngr = wxGetApp().notification_manager();
ntf_mngr->push_download_URL_progress_notification(id, m_downloads.back()->get_filename(), std::bind(&Downloader::user_action_callback, this, std::placeholders::_1, std::placeholders::_2));
m_downloads.back()->start();
BOOST_LOG_TRIVIAL(debug) << "started download";
}
void Downloader::on_progress(wxCommandEvent& event)
{
size_t id = event.GetInt();
float percent = (float)std::stoi(boost::nowide::narrow(event.GetString())) / 100.f;
//BOOST_LOG_TRIVIAL(error) << "progress " << id << ": " << percent;
NotificationManager* ntf_mngr = wxGetApp().notification_manager();
BOOST_LOG_TRIVIAL(trace) << "Download "<< id << ": " << percent;
ntf_mngr->set_download_URL_progress(id, percent);
}
void Downloader::on_error(wxCommandEvent& event)
{
size_t id = event.GetInt();
set_download_state(event.GetInt(), DownloadState::DownloadError);
BOOST_LOG_TRIVIAL(error) << "Download error: " << event.GetString();
NotificationManager* ntf_mngr = wxGetApp().notification_manager();
ntf_mngr->set_download_URL_error(id, boost::nowide::narrow(event.GetString()));
}
void Downloader::on_complete(wxCommandEvent& event)
{
// TODO: is this always true? :
// here we open the file itself, notification should get 1.f progress from on progress.
set_download_state(event.GetInt(), DownloadState::DownloadDone);
wxArrayString paths;
paths.Add(event.GetString());
wxGetApp().plater()->load_files(paths);
}
bool Downloader::user_action_callback(DownloaderUserAction action, int id)
{
for (size_t i = 0; i < m_downloads.size(); ++i) {
if (m_downloads[i]->get_id() == id) {
switch (action) {
case DownloadUserCanceled:
m_downloads[i]->cancel();
return true;
case DownloadUserPaused:
m_downloads[i]->pause();
return true;
case DownloadUserContinued:
m_downloads[i]->resume();
return true;
case DownloadUserOpenedFolder:
open_folder(m_downloads[i]->get_dest_folder());
return true;
default:
return false;
}
}
}
return false;
}
void Downloader::on_name_change(wxCommandEvent& event)
{
}
void Downloader::on_paused(wxCommandEvent& event)
{
size_t id = event.GetInt();
NotificationManager* ntf_mngr = wxGetApp().notification_manager();
ntf_mngr->set_download_URL_paused(id);
}
void Downloader::on_canceled(wxCommandEvent& event)
{
size_t id = event.GetInt();
NotificationManager* ntf_mngr = wxGetApp().notification_manager();
ntf_mngr->set_download_URL_canceled(id);
}
void Downloader::set_download_state(int id, DownloadState state)
{
for (size_t i = 0; i < m_downloads.size(); ++i) {
if (m_downloads[i]->get_id() == id) {
m_downloads[i]->set_state(state);
return;
}
}
}
}
}

View File

@ -0,0 +1,99 @@
#ifndef slic3r_Downloader_hpp_
#define slic3r_Downloader_hpp_
#include "DownloaderFileGet.hpp"
#include <boost/filesystem/path.hpp>
#include <wx/wx.h>
namespace Slic3r {
namespace GUI {
class NotificationManager;
enum DownloadState
{
DownloadPending = 0,
DownloadOngoing,
DownloadStopped,
DownloadDone,
DownloadError,
DownloadPaused,
DownloadStateUnknown
};
enum DownloaderUserAction
{
DownloadUserCanceled,
DownloadUserPaused,
DownloadUserContinued,
DownloadUserOpenedFolder
};
class Download {
public:
Download(int ID, std::string url, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder);
void start();
void cancel();
void pause();
void resume();
int get_id() const { return m_id; }
boost::filesystem::path get_final_path() const { return m_final_path; }
std::string get_filename() const { return m_filename; }
DownloadState get_state() const { return m_state; }
void set_state(DownloadState state) { m_state = state; }
std::string get_dest_folder() { return m_dest_folder.string(); }
private:
const int m_id;
std::string m_filename;
boost::filesystem::path m_final_path;
boost::filesystem::path m_dest_folder;
std::shared_ptr<FileGet> m_file_get;
DownloadState m_state { DownloadState::DownloadPending };
};
class Downloader : public wxEvtHandler {
public:
Downloader();
bool get_initialized() { return m_initialized; }
void init(const boost::filesystem::path& dest_folder)
{
m_dest_folder = dest_folder;
m_initialized = true;
}
void start_download(const std::string& full_url);
// cancel = false -> just pause
bool user_action_callback(DownloaderUserAction action, int id);
private:
bool m_initialized { false };
std::vector<std::unique_ptr<Download>> m_downloads;
boost::filesystem::path m_dest_folder;
size_t m_next_id { 0 };
size_t get_next_id() { return ++m_next_id; }
void on_progress(wxCommandEvent& event);
void on_error(wxCommandEvent& event);
void on_complete(wxCommandEvent& event);
void on_name_change(wxCommandEvent& event);
void on_paused(wxCommandEvent& event);
void on_canceled(wxCommandEvent& event);
void set_download_state(int id, DownloadState state);
/*
bool is_in_state(int id, DownloadState state) const;
DownloadState get_download_state(int id) const;
bool cancel_download(int id);
bool pause_download(int id);
bool resume_download(int id);
bool delete_download(int id);
wxString get_path_of(int id) const;
wxString get_folder_path_of(int id) const;
*/
};
}
}
#endif

View File

@ -0,0 +1,322 @@
#include "DownloaderFileGet.hpp"
#include <thread>
#include <curl/curl.h>
#include <boost/nowide/fstream.hpp>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
#include <iostream>
#include "format.hpp"
namespace Slic3r {
namespace GUI {
const size_t DOWNLOAD_MAX_CHUNK_SIZE = 10 * 1024 * 1024;
const size_t DOWNLOAD_SIZE_LIMIT = 1024 * 1024 * 1024;
std::string FileGet::escape_url(const std::string& unescaped)
{
std::string ret_val;
CURL* curl = curl_easy_init();
if (curl) {
int decodelen;
char* decoded = curl_easy_unescape(curl, unescaped.c_str(), unescaped.size(), &decodelen);
if (decoded) {
ret_val = std::string(decoded);
curl_free(decoded);
}
curl_easy_cleanup(curl);
}
return ret_val;
}
namespace {
unsigned get_current_pid()
{
#ifdef WIN32
return GetCurrentProcessId();
#else
return ::getpid();
#endif
}
}
// int = DOWNLOAD ID; string = file path
wxDEFINE_EVENT(EVT_DWNLDR_FILE_COMPLETE, wxCommandEvent);
// int = DOWNLOAD ID; string = error msg
wxDEFINE_EVENT(EVT_DWNLDR_FILE_ERROR, wxCommandEvent);
// int = DOWNLOAD ID; string = progress percent
wxDEFINE_EVENT(EVT_DWNLDR_FILE_PROGRESS, wxCommandEvent);
// int = DOWNLOAD ID; string = name
wxDEFINE_EVENT(EVT_DWNLDR_FILE_NAME_CHANGE, wxCommandEvent);
// int = DOWNLOAD ID;
wxDEFINE_EVENT(EVT_DWNLDR_FILE_PAUSED, wxCommandEvent);
// int = DOWNLOAD ID;
wxDEFINE_EVENT(EVT_DWNLDR_FILE_CANCELED, wxCommandEvent);
struct FileGet::priv
{
const int m_id;
std::string m_url;
std::string m_filename;
std::thread m_io_thread;
wxEvtHandler* m_evt_handler;
boost::filesystem::path m_dest_folder;
boost::filesystem::path m_tmp_path; // path when ongoing download
std::atomic_bool m_cancel { false };
std::atomic_bool m_pause { false };
std::atomic_bool m_stopped { false }; // either canceled or paused - download is not running
size_t m_written { 0 };
size_t m_absolute_size { 0 };
priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder);
void get_perform();
};
FileGet::priv::priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder)
: m_id(ID)
, m_url(std::move(url))
, m_filename(filename)
, m_evt_handler(evt_handler)
, m_dest_folder(dest_folder)
{
}
void FileGet::priv::get_perform()
{
assert(m_evt_handler);
assert(!m_url.empty());
assert(!m_filename.empty());
assert(boost::filesystem::is_directory(m_dest_folder));
m_stopped = false;
// open dest file
if (m_written == 0)
{
boost::filesystem::path dest_path = m_dest_folder / m_filename;
std::string extension = boost::filesystem::extension(dest_path);
std::string just_filename = m_filename.substr(0, m_filename.size() - extension.size());
std::string final_filename = just_filename;
size_t version = 0;
while (boost::filesystem::exists(m_dest_folder / (final_filename + extension)) || boost::filesystem::exists(m_dest_folder / (final_filename + extension + "." + std::to_string(get_current_pid()) + ".download")))
{
++version;
final_filename = just_filename + "(" + std::to_string(version) + ")";
}
m_filename = final_filename + extension;
m_tmp_path = m_dest_folder / (m_filename + "." + std::to_string(get_current_pid()) + ".download");
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE);
evt->SetString(boost::nowide::widen(m_filename));
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
}
boost::filesystem::path dest_path = m_dest_folder / m_filename;
wxString temp_path_wstring(m_tmp_path.wstring());
std::cout << "dest_path: " << dest_path.string() << std::endl;
std::cout << "m_tmp_path: " << m_tmp_path.string() << std::endl;
BOOST_LOG_TRIVIAL(info) << GUI::format("Starting download from %1% to %2%. Temp path is %3%",m_url, dest_path, m_tmp_path);
FILE* file;
// open file for writting
if (m_written == 0)
file = fopen(temp_path_wstring.c_str(), "wb");
else
file = fopen(temp_path_wstring.c_str(), "a");
assert(file != NULL);
std:: string range_string = std::to_string(m_written) + "-";
size_t written_previously = m_written;
size_t written_this_session = 0;
Http::get(m_url)
.size_limit(DOWNLOAD_SIZE_LIMIT) //more?
.set_range(range_string)
.on_progress([&](Http::Progress progress, bool& cancel) {
// to prevent multiple calls into following ifs (m_cancel / m_pause)
if (m_stopped){
cancel = true;
return;
}
if (m_cancel) {
m_stopped = true;
fclose(file);
// remove canceled file
std::remove(m_tmp_path.string().c_str());
m_written = 0;
cancel = true;
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_CANCELED);
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
return;
// TODO: send canceled event?
}
if (m_pause) {
m_stopped = true;
fclose(file);
cancel = true;
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PAUSED);
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
return;
}
if (m_absolute_size < progress.dltotal) {
m_absolute_size = progress.dltotal;
}
if (progress.dlnow != 0) {
if (progress.dlnow - written_this_session > DOWNLOAD_MAX_CHUNK_SIZE || progress.dlnow == progress.dltotal) {
try
{
std::string part_for_write = progress.buffer.substr(written_this_session, progress.dlnow);
fwrite(part_for_write.c_str(), 1, part_for_write.size(), file);
}
catch (const std::exception& e)
{
// fclose(file); do it?
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
evt->SetString(e.what());
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
cancel = true;
return;
}
written_this_session = progress.dlnow;
m_written = written_previously + written_this_session;
}
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PROGRESS);
int percent_total = (written_previously + progress.dlnow) * 100 / m_absolute_size;
evt->SetString(std::to_string(percent_total));
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
}
})
.on_error([&](std::string body, std::string error, unsigned http_status) {
if (file != NULL)
fclose(file);
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
evt->SetString(error);
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
})
.on_complete([&](std::string body, unsigned /* http_status */) {
// TODO: perform a body size check
//
//size_t body_size = body.size();
//if (body_size != expected_size) {
// return;
//}
try
{
/*
if (m_written < body.size())
{
// this code should never be entered. As there should be on_progress call after last bit downloaded.
std::string part_for_write = body.substr(m_written);
fwrite(part_for_write.c_str(), 1, part_for_write.size(), file);
}
*/
fclose(file);
boost::filesystem::rename(m_tmp_path, dest_path);
}
catch (const std::exception& /*e*/)
{
//TODO: report?
//error_message = GUI::format("Failed to write and move %1% to %2%", tmp_path, dest_path);
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
evt->SetString("Failed to write and move.");
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
return;
}
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_COMPLETE);
evt->SetString(dest_path.wstring());
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
})
.perform_sync();
}
FileGet::FileGet(int ID, std::string url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder)
: p(new priv(ID, std::move(url), filename, evt_handler, dest_folder))
{}
FileGet::FileGet(FileGet&& other) : p(std::move(other.p)) {}
FileGet::~FileGet()
{
if (p && p->m_io_thread.joinable()) {
p->m_cancel = true;
p->m_io_thread.join();
}
}
void FileGet::get()
{
assert(p);
if (p->m_io_thread.joinable()) {
// This will stop transfers being done by the thread, if any.
// Cancelling takes some time, but should complete soon enough.
p->m_cancel = true;
p->m_io_thread.join();
}
p->m_cancel = false;
p->m_pause = false;
p->m_io_thread = std::thread([this]() {
p->get_perform();
});
}
void FileGet::cancel()
{
if(p && p->m_stopped) {
if (p->m_io_thread.joinable()) {
p->m_cancel = true;
p->m_io_thread.join();
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_CANCELED);
evt->SetInt(p->m_id);
p->m_evt_handler->QueueEvent(evt);
}
}
if (p)
p->m_cancel = true;
}
void FileGet::pause()
{
if (p) {
p->m_pause = true;
}
}
void FileGet::resume()
{
assert(p);
if (p->m_io_thread.joinable()) {
// This will stop transfers being done by the thread, if any.
// Cancelling takes some time, but should complete soon enough.
p->m_cancel = true;
p->m_io_thread.join();
}
p->m_cancel = false;
p->m_pause = false;
p->m_io_thread = std::thread([this]() {
p->get_perform();
});
}
}
}

View File

@ -0,0 +1,44 @@
#ifndef slic3r_DownloaderFileGet_hpp_
#define slic3r_DownloaderFileGet_hpp_
#include "../Utils/Http.hpp"
#include <memory>
#include <string>
#include <wx/event.h>
#include <wx/frame.h>
#include <boost/filesystem.hpp>
namespace Slic3r {
namespace GUI {
class FileGet : public std::enable_shared_from_this<FileGet> {
private:
struct priv;
public:
FileGet(int ID, std::string url, const std::string& filename, wxEvtHandler* evt_handler,const boost::filesystem::path& dest_folder);
FileGet(FileGet&& other);
~FileGet();
void get();
void cancel();
void pause();
void resume();
static std::string escape_url(const std::string& url);
private:
std::unique_ptr<priv> p;
};
// int = DOWNLOAD ID; string = file path
wxDECLARE_EVENT(EVT_DWNLDR_FILE_COMPLETE, wxCommandEvent);
// int = DOWNLOAD ID; string = error msg
wxDECLARE_EVENT(EVT_DWNLDR_FILE_PROGRESS, wxCommandEvent);
// int = DOWNLOAD ID; string = progress percent
wxDECLARE_EVENT(EVT_DWNLDR_FILE_ERROR, wxCommandEvent);
// int = DOWNLOAD ID; string = name
wxDECLARE_EVENT(EVT_DWNLDR_FILE_NAME_CHANGE, wxCommandEvent);
// int = DOWNLOAD ID;
wxDECLARE_EVENT(EVT_DWNLDR_FILE_PAUSED, wxCommandEvent);
// int = DOWNLOAD ID;
wxDECLARE_EVENT(EVT_DWNLDR_FILE_CANCELED, wxCommandEvent);
}
}
#endif

View File

@ -0,0 +1,363 @@
#include "FileArchiveDialog.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
#include "GUI.hpp"
#include "MainFrame.hpp"
#include "ExtraRenderers.hpp"
#include "format.hpp"
#include <regex>
#include <boost/log/trivial.hpp>
#include <boost/nowide/convert.hpp>
namespace Slic3r {
namespace GUI {
ArchiveViewModel::ArchiveViewModel(wxWindow* parent)
:m_parent(parent)
{}
ArchiveViewModel::~ArchiveViewModel()
{}
std::shared_ptr<ArchiveViewNode> ArchiveViewModel::AddFile(std::shared_ptr<ArchiveViewNode> parent, const wxString& name, bool container)
{
std::shared_ptr<ArchiveViewNode> node = std::make_shared<ArchiveViewNode>(ArchiveViewNode(name));
node->set_container(container);
if (parent.get() != nullptr) {
parent->get_children().push_back(node);
node->set_parent(parent);
parent->set_is_folder(true);
} else {
m_top_children.emplace_back(node);
}
wxDataViewItem child = wxDataViewItem((void*)node.get());
wxDataViewItem parent_item= wxDataViewItem((void*)parent.get());
ItemAdded(parent_item, child);
if (parent)
m_ctrl->Expand(parent_item);
return node;
}
wxString ArchiveViewModel::GetColumnType(unsigned int col) const
{
if (col == 0)
return "bool";
return "string";//"DataViewBitmapText";
}
void ArchiveViewModel::Rescale()
{
// There should be no pictures rendered
}
void ArchiveViewModel::Delete(const wxDataViewItem& item)
{
assert(item.IsOk());
ArchiveViewNode* node = static_cast<ArchiveViewNode*>(item.GetID());
assert(node->get_parent() != nullptr);
for (std::shared_ptr<ArchiveViewNode> child : node->get_children())
{
Delete(wxDataViewItem((void*)child.get()));
}
delete [] node;
}
void ArchiveViewModel::Clear()
{
}
wxDataViewItem ArchiveViewModel::GetParent(const wxDataViewItem& item) const
{
assert(item.IsOk());
ArchiveViewNode* node = static_cast<ArchiveViewNode*>(item.GetID());
return wxDataViewItem((void*)node->get_parent().get());
}
unsigned int ArchiveViewModel::GetChildren(const wxDataViewItem& parent, wxDataViewItemArray& array) const
{
if (!parent.IsOk()) {
for (std::shared_ptr<ArchiveViewNode>child : m_top_children) {
array.push_back(wxDataViewItem((void*)child.get()));
}
return m_top_children.size();
}
ArchiveViewNode* node = static_cast<ArchiveViewNode*>(parent.GetID());
for (std::shared_ptr<ArchiveViewNode> child : node->get_children()) {
array.push_back(wxDataViewItem((void*)child.get()));
}
return node->get_children().size();
}
void ArchiveViewModel::GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned int col) const
{
assert(item.IsOk());
ArchiveViewNode* node = static_cast<ArchiveViewNode*>(item.GetID());
if (col == 0) {
variant = node->get_toggle();
} else {
variant = node->get_name();
}
}
void ArchiveViewModel::untoggle_folders(const wxDataViewItem& item)
{
assert(item.IsOk());
ArchiveViewNode* node = static_cast<ArchiveViewNode*>(item.GetID());
node->set_toggle(false);
if (node->get_parent().get() != nullptr)
untoggle_folders(wxDataViewItem((void*)node->get_parent().get()));
}
bool ArchiveViewModel::SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned int col)
{
assert(item.IsOk());
ArchiveViewNode* node = static_cast<ArchiveViewNode*>(item.GetID());
if (col == 0) {
node->set_toggle(variant.GetBool());
// if folder recursivelly check all children
for (std::shared_ptr<ArchiveViewNode> child : node->get_children()) {
SetValue(variant, wxDataViewItem((void*)child.get()), col);
}
if(!variant.GetBool() && node->get_parent())
untoggle_folders(wxDataViewItem((void*)node->get_parent().get()));
} else {
node->set_name(variant.GetString());
}
m_parent->Refresh();
return true;
}
bool ArchiveViewModel::IsEnabled(const wxDataViewItem& item, unsigned int col) const
{
// As of now, all items are always enabled.
// Returning false for col 1 would gray out text.
return true;
}
bool ArchiveViewModel::IsContainer(const wxDataViewItem& item) const
{
if(!item.IsOk())
return true;
ArchiveViewNode* node = static_cast<ArchiveViewNode*>(item.GetID());
return node->is_container();
}
ArchiveViewCtrl::ArchiveViewCtrl(wxWindow* parent, wxSize size)
: wxDataViewCtrl(parent, wxID_ANY, wxDefaultPosition, size, wxDV_VARIABLE_LINE_HEIGHT | wxDV_ROW_LINES
#ifdef _WIN32
| wxBORDER_SIMPLE
#endif
)
//, m_em_unit(em_unit(parent))
{
wxGetApp().UpdateDVCDarkUI(this);
m_model = new ArchiveViewModel(parent);
this->AssociateModel(m_model);
m_model->SetAssociatedControl(this);
}
ArchiveViewCtrl::~ArchiveViewCtrl()
{
if (m_model) {
m_model->Clear();
m_model->DecRef();
}
}
FileArchiveDialog::FileArchiveDialog(wxWindow* parent_window, mz_zip_archive* archive, std::vector<boost::filesystem::path>& selected_paths)
: DPIDialog(parent_window, wxID_ANY, _(L("Archive preview")), wxDefaultPosition,
wxSize(45 * wxGetApp().em_unit(), 40 * wxGetApp().em_unit()),
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxMAXIMIZE_BOX)
, m_selected_paths (selected_paths)
{
int em = em_unit();
wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL);
m_avc = new ArchiveViewCtrl(this, wxSize(60 * em, 30 * em));
m_avc->AppendToggleColumn(L"\u2714", 0, wxDATAVIEW_CELL_ACTIVATABLE, 6 * em);
m_avc->AppendTextColumn("filename", 1);
std::vector<std::shared_ptr<ArchiveViewNode>> stack;
std::function<void(std::vector<std::shared_ptr<ArchiveViewNode> >&, size_t)> reduce_stack = [] (std::vector<std::shared_ptr<ArchiveViewNode>>& stack, size_t size) {
if (size == 0) {
stack.clear();
return;
}
while (stack.size() > size)
stack.pop_back();
};
// recursively stores whole structure of file onto function stack and synchoronize with stack object.
std::function<size_t(const boost::filesystem::path&, std::vector<std::shared_ptr<ArchiveViewNode>>&)> adjust_stack = [&adjust_stack, &reduce_stack, &avc = m_avc](const boost::filesystem::path& const_file, std::vector<std::shared_ptr<ArchiveViewNode>>& stack)->size_t {
boost::filesystem::path file(const_file);
size_t struct_size = file.has_parent_path() ? adjust_stack(file.parent_path(), stack) : 0;
if (stack.size() > struct_size && (file.has_extension() || file.filename().string() != stack[struct_size]->get_name()))
{
reduce_stack(stack, struct_size);
}
if (!file.has_extension() && stack.size() == struct_size)
stack.push_back(avc->get_model()->AddFile((stack.empty() ? std::shared_ptr<ArchiveViewNode>(nullptr) : stack.back()), GUI::format_wxstr(file.filename().string()), true)); // filename string to wstring?
return struct_size + 1;
};
const std::regex pattern_drop(".*[.](stl|obj|amf|3mf|prusa|step|stp)", std::regex::icase);
mz_uint num_entries = mz_zip_reader_get_num_files(archive);
mz_zip_archive_file_stat stat;
std::vector<boost::filesystem::path> filtered_entries;
for (mz_uint i = 0; i < num_entries; ++i) {
if (mz_zip_reader_file_stat(archive, i, &stat)) {
wxString wname = boost::nowide::widen(stat.m_filename);
std::string name = GUI::format(wname);
//std::replace(name.begin(), name.end(), '\\', '/');
boost::filesystem::path path(name);
if (!path.has_extension())
continue;
// filter out MACOS specific hidden files
if (boost::algorithm::starts_with(path.string(), "__MACOSX"))
continue;
filtered_entries.emplace_back(std::move(path));
}
}
// sorting files will help adjust_stack function to not create multiple same folders
std::sort(filtered_entries.begin(), filtered_entries.end(), [](const boost::filesystem::path& p1, const boost::filesystem::path& p2){ return p1.string() > p2.string(); });
for (const boost::filesystem::path& path : filtered_entries)
{
std::shared_ptr<ArchiveViewNode> parent(nullptr);
adjust_stack(path, stack);
if (!stack.empty())
parent = stack.back();
if (std::regex_match(path.extension().string(), pattern_drop)) { // this leaves out non-compatible files
m_avc->get_model()->AddFile(parent, GUI::format_wxstr(path.filename().string()), false)->set_fullpath(/*std::move(path)*/path); // filename string to wstring?
}
}
wxBoxSizer* btn_sizer = new wxBoxSizer(wxHORIZONTAL);
wxButton* btn_all = new wxButton(this, wxID_ANY, "All");
btn_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { on_all_button(); });
btn_sizer->Add(btn_all, 0, wxLeft);
wxButton* btn_none = new wxButton(this, wxID_ANY, "None");
btn_none->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { on_none_button(); });
btn_sizer->Add(btn_none, 0, wxLeft);
btn_sizer->AddStretchSpacer();
wxButton* btn_run = new wxButton(this, wxID_OK, "Open");
btn_run->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { on_open_button(); });
btn_sizer->Add(btn_run, 0, wxRIGHT);
wxButton* cancel_btn = new wxButton(this, wxID_CANCEL, "Cancel");
cancel_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { this->EndModal(wxID_CANCEL); });
btn_sizer->Add(cancel_btn, 0, wxRIGHT);
topSizer->Add(m_avc, 1, wxEXPAND | wxALL, 10);
topSizer->Add(btn_sizer, 0, wxEXPAND | wxALL, 10);
this->SetMinSize(wxSize(80 * em, 30 * em));
this->SetSizer(topSizer);
}
void FileArchiveDialog::on_dpi_changed(const wxRect& suggested_rect)
{
int em = em_unit();
BOOST_LOG_TRIVIAL(error) << "on_dpi_changed";
//msw_buttons_rescale(this, em, { wxID_CANCEL, m_save_btn_id, m_move_btn_id, m_continue_btn_id });
//for (auto btn : { m_save_btn, m_transfer_btn, m_discard_btn })
// if (btn) btn->msw_rescale();
const wxSize& size = wxSize(70 * em, 30 * em);
SetMinSize(size);
//m_tree->Rescale(em);
Fit();
Refresh();
}
void FileArchiveDialog::on_open_button()
{
wxDataViewItemArray top_items;
m_avc->get_model()->GetChildren(wxDataViewItem(nullptr), top_items);
std::function<void(ArchiveViewNode*)> deep_fill = [&paths = m_selected_paths, &deep_fill](ArchiveViewNode* node){
if (node == nullptr)
return;
if (node->get_children().empty()) {
if (node->get_toggle())
paths.emplace_back(node->get_fullpath());
} else {
for (std::shared_ptr<ArchiveViewNode> child : node->get_children())
deep_fill(child.get());
}
};
for (const auto& item : top_items)
{
ArchiveViewNode* node = static_cast<ArchiveViewNode*>(item.GetID());
deep_fill(node);
}
this->EndModal(wxID_OK);
}
void FileArchiveDialog::on_all_button()
{
wxDataViewItemArray top_items;
m_avc->get_model()->GetChildren(wxDataViewItem(nullptr), top_items);
std::function<void(ArchiveViewNode*)> deep_fill = [&deep_fill](ArchiveViewNode* node) {
if (node == nullptr)
return;
node->set_toggle(true);
if (!node->get_children().empty()) {
for (std::shared_ptr<ArchiveViewNode> child : node->get_children())
deep_fill(child.get());
}
};
for (const auto& item : top_items)
{
ArchiveViewNode* node = static_cast<ArchiveViewNode*>(item.GetID());
deep_fill(node);
// Fix for linux, where Refresh or Update wont help to redraw toggle checkboxes.
// It should be enough to call ValueChanged for top items.
m_avc->get_model()->ValueChanged(item, 0);
}
Refresh();
}
void FileArchiveDialog::on_none_button()
{
wxDataViewItemArray top_items;
m_avc->get_model()->GetChildren(wxDataViewItem(nullptr), top_items);
std::function<void(ArchiveViewNode*)> deep_fill = [&deep_fill](ArchiveViewNode* node) {
if (node == nullptr)
return;
node->set_toggle(false);
if (!node->get_children().empty()) {
for (std::shared_ptr<ArchiveViewNode> child : node->get_children())
deep_fill(child.get());
}
};
for (const auto& item : top_items)
{
ArchiveViewNode* node = static_cast<ArchiveViewNode*>(item.GetID());
deep_fill(node);
// Fix for linux, where Refresh or Update wont help to redraw toggle checkboxes.
// It should be enough to call ValueChanged for top items.
m_avc->get_model()->ValueChanged(item, 0);
}
this->Refresh();
}
} // namespace GUI
} // namespace Slic3r

View File

@ -0,0 +1,118 @@
#ifndef slic3r_GUI_FileArchiveDialog_hpp_
#define slic3r_GUI_FileArchiveDialog_hpp_
#include "GUI_Utils.hpp"
#include "libslic3r/miniz_extension.hpp"
#include <boost/filesystem/path.hpp>
#include <wx/wx.h>
#include <wx/dataview.h>
#include "wxExtensions.hpp"
namespace Slic3r {
namespace GUI {
class ArchiveViewCtrl;
class ArchiveViewNode
{
public:
ArchiveViewNode(const wxString& name) : m_name(name) {}
std::vector<std::shared_ptr<ArchiveViewNode>>& get_children() { return m_children; }
void set_parent(std::shared_ptr<ArchiveViewNode> parent) { m_parent = parent; }
// On Linux, get_parent cannot just return size of m_children. ItemAdded would than crash.
std::shared_ptr<ArchiveViewNode> get_parent() const { return m_parent; }
bool is_container() const { return m_container; }
void set_container(bool is_container) { m_container = is_container; }
wxString get_name() const { return m_name; }
void set_name(const wxString& name) { m_name = name; }
bool get_toggle() const { return m_toggle; }
void set_toggle(bool toggle) { m_toggle = toggle; }
bool get_is_folder() const { return m_folder; }
void set_is_folder(bool is_folder) { m_folder = is_folder; }
void set_fullpath(boost::filesystem::path path) { m_fullpath = path; }
boost::filesystem::path get_fullpath() const { return m_fullpath; }
private:
wxString m_name;
std::shared_ptr<ArchiveViewNode> m_parent { nullptr };
std::vector<std::shared_ptr<ArchiveViewNode>> m_children;
bool m_toggle { false };
bool m_folder { false };
boost::filesystem::path m_fullpath;
bool m_container { false };
};
class ArchiveViewModel : public wxDataViewModel
{
public:
ArchiveViewModel(wxWindow* parent);
~ArchiveViewModel();
/* wxDataViewItem AddFolder(wxDataViewItem& parent, wxString name);
wxDataViewItem AddFile(wxDataViewItem& parent, wxString name);*/
std::shared_ptr<ArchiveViewNode> AddFile(std::shared_ptr<ArchiveViewNode> parent,const wxString& name, bool container);
wxString GetColumnType(unsigned int col) const override;
unsigned int GetColumnCount() const override { return 2; }
void Rescale();
void Delete(const wxDataViewItem& item);
void Clear();
wxDataViewItem GetParent(const wxDataViewItem& item) const override;
unsigned int GetChildren(const wxDataViewItem& parent, wxDataViewItemArray& array) const override;
void SetAssociatedControl(ArchiveViewCtrl* ctrl) { m_ctrl = ctrl; }
void GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned int col) const override;
bool SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned int col) override;
void untoggle_folders(const wxDataViewItem& item);
bool IsEnabled(const wxDataViewItem& item, unsigned int col) const override;
bool IsContainer(const wxDataViewItem& item) const override;
// Is the container just a header or an item with all columns
// In our case it is an item with all columns
bool HasContainerColumns(const wxDataViewItem& WXUNUSED(item)) const override { return true; }
protected:
wxWindow* m_parent { nullptr };
ArchiveViewCtrl* m_ctrl { nullptr };
std::vector<std::shared_ptr<ArchiveViewNode>> m_top_children;
};
class ArchiveViewCtrl : public wxDataViewCtrl
{
public:
ArchiveViewCtrl(wxWindow* parent, wxSize size);
~ArchiveViewCtrl();
ArchiveViewModel* get_model() const {return m_model; }
protected:
ArchiveViewModel* m_model;
};
class FileArchiveDialog : public DPIDialog
{
public:
FileArchiveDialog(wxWindow* parent_window, mz_zip_archive* archive, std::vector<boost::filesystem::path>& selected_paths);
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
void on_open_button();
void on_all_button();
void on_none_button();
std::vector<boost::filesystem::path>& m_selected_paths;
ArchiveViewCtrl* m_avc;
};
} // namespace GU
} // namespace Slic3r
#endif // slic3r_GUI_FileArchiveDialog_hpp_

File diff suppressed because it is too large Load Diff

View File

@ -46,6 +46,7 @@ class ObjectList;
class ObjectLayers;
class Plater;
class NotificationManager;
class Downloader;
struct GUI_InitParams;
class GalleryDialog;
@ -165,6 +166,7 @@ private:
std::unique_ptr <OtherInstanceMessageHandler> m_other_instance_message_handler;
std::unique_ptr <AppUpdater> m_app_updater;
std::unique_ptr <wxSingleInstanceChecker> m_single_instance_checker;
std::unique_ptr <Downloader> m_downloader;
std::string m_instance_hash_string;
size_t m_instance_hash_int;
@ -292,6 +294,7 @@ public:
void OSXStoreOpenFiles(const wxArrayString &files) override;
// wxWidgets override to get an event on open files.
void MacOpenFiles(const wxArrayString &fileNames) override;
void MacOpenURL(const wxString& url) override;
#endif /* __APPLE */
Sidebar& sidebar();
@ -304,6 +307,7 @@ public:
Model& model();
NotificationManager * notification_manager();
GalleryDialog * gallery_dialog();
Downloader* downloader();
// Parameters extracted from the command line to be passed to GUI after initialization.
GUI_InitParams* init_params { nullptr };
@ -358,6 +362,10 @@ public:
void associate_gcode_files();
#endif // __WXMSW__
// URL download - PrusaSlicer gets system call to open prusaslicer:// URL which should contain address of download
void start_download(std::string url);
private:
bool on_init_inner();
void init_app_config();
@ -380,6 +388,7 @@ private:
void app_version_check(bool from_user);
bool m_datadir_redefined { false };
};
DECLARE_APP(GUI_App)

View File

@ -30,6 +30,9 @@ struct GUI_InitParams
std::vector<std::string> input_files;
bool start_as_gcodeviewer;
bool start_downloader;
bool delete_after_load;
std::string download_url;
#if ENABLE_GL_CORE_PROFILE
std::pair<int, int> opengl_version;
#if ENABLE_OPENGL_DEBUG_OPTION

View File

@ -96,7 +96,12 @@ static const std::map<const wchar_t, std::string> font_icons_large = {
{ImGui::DocumentationButton , "notification_documentation" },
{ImGui::DocumentationHoverButton, "notification_documentation_hover"},
{ImGui::InfoMarker , "notification_info" },
{ImGui::PlayButton , "notification_play" },
{ImGui::PlayHoverButton , "notification_play_hover" },
{ImGui::PauseButton , "notification_pause" },
{ImGui::PauseHoverButton , "notification_pause_hover" },
{ImGui::OpenButton , "notification_open" },
{ImGui::OpenHoverButton , "notification_open_hover" },
};
static const std::map<const wchar_t, std::string> font_icons_extra_large = {

View File

@ -373,6 +373,7 @@ bool instance_check(int argc, char** argv, bool app_config_single_instance)
namespace GUI {
wxDEFINE_EVENT(EVT_LOAD_MODEL_OTHER_INSTANCE, LoadFromOtherInstanceEvent);
wxDEFINE_EVENT(EVT_START_DOWNLOAD_OTHER_INSTANCE, StartDownloadOtherInstanceEvent);
wxDEFINE_EVENT(EVT_INSTANCE_GO_TO_FRONT, InstanceGoToFrontEvent);
void OtherInstanceMessageHandler::init(wxEvtHandler* callback_evt_handler)
@ -501,12 +502,20 @@ void OtherInstanceMessageHandler::handle_message(const std::string& message)
}
std::vector<boost::filesystem::path> paths;
std::vector<std::string> downloads;
// Skip the first argument, it is the path to the slicer executable.
auto it = args.begin();
for (++ it; it != args.end(); ++ it) {
boost::filesystem::path p = MessageHandlerInternal::get_path(*it);
if (! p.string().empty())
paths.emplace_back(p);
// TODO: There is a misterious slash appearing in recieved msg on windows
#ifdef _WIN32
else if (it->rfind("prusaslicer://open/?file=", 0) == 0)
#else
else if (it->rfind("prusaslicer://open?file=", 0) == 0)
#endif
downloads.emplace_back(*it);
}
if (! paths.empty()) {
//wxEvtHandler* evt_handler = wxGetApp().plater(); //assert here?
@ -514,6 +523,10 @@ void OtherInstanceMessageHandler::handle_message(const std::string& message)
wxPostEvent(m_callback_evt_handler, LoadFromOtherInstanceEvent(GUI::EVT_LOAD_MODEL_OTHER_INSTANCE, std::vector<boost::filesystem::path>(std::move(paths))));
//}
}
if (!downloads.empty())
{
wxPostEvent(m_callback_evt_handler, StartDownloadOtherInstanceEvent(GUI::EVT_START_DOWNLOAD_OTHER_INSTANCE, std::vector<std::string>(std::move(downloads))));
}
}
#ifdef __APPLE__
@ -545,6 +558,9 @@ namespace MessageHandlerDBusInternal
" <method name=\"AnotherInstance\">"
" <arg name=\"data\" direction=\"in\" type=\"s\" />"
" </method>"
" <method name=\"Introspect\">"
" <arg name=\"data\" direction=\"out\" type=\"s\" />"
" </method>"
" </interface>"
" </node>";
@ -553,6 +569,7 @@ namespace MessageHandlerDBusInternal
dbus_connection_send(connection, reply, NULL);
dbus_message_unref(reply);
}
//method AnotherInstance receives message from another PrusaSlicer instance
static void handle_method_another_instance(DBusConnection *connection, DBusMessage *request)
{
@ -587,6 +604,9 @@ namespace MessageHandlerDBusInternal
} else if (0 == strcmp(our_interface.c_str(), interface_name) && 0 == strcmp("AnotherInstance", member_name)) {
handle_method_another_instance(connection, message);
return DBUS_HANDLER_RESULT_HANDLED;
} else if (0 == strcmp(our_interface.c_str(), interface_name) && 0 == strcmp("Introspect", member_name)) {
respond_to_introspect(connection, message);
return DBUS_HANDLER_RESULT_HANDLED;
}
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

View File

@ -43,8 +43,9 @@ class MainFrame;
#endif // __linux__
using LoadFromOtherInstanceEvent = Event<std::vector<boost::filesystem::path>>;
using StartDownloadOtherInstanceEvent = Event<std::vector<std::string>>;
wxDECLARE_EVENT(EVT_LOAD_MODEL_OTHER_INSTANCE, LoadFromOtherInstanceEvent);
wxDECLARE_EVENT(EVT_START_DOWNLOAD_OTHER_INSTANCE, StartDownloadOtherInstanceEvent);
using InstanceGoToFrontEvent = SimpleEvent;
wxDECLARE_EVENT(EVT_INSTANCE_GO_TO_FRONT, InstanceGoToFrontEvent);

View File

@ -947,6 +947,7 @@ void NotificationManager::ProgressBarNotification::render_bar(ImGuiWrapper& imgu
imgui.text(text.c_str());
}
}
//------ProgressBarWithCancelNotification----------------
void NotificationManager::ProgressBarWithCancelNotification::render_close_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
@ -1060,6 +1061,263 @@ void NotificationManager::ProgressBarWithCancelNotification::render_bar(ImGuiWra
ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? 0 : m_line_height / 4));
imgui.text(text.c_str());
}
//------URLDownloadNotification----------------
void NotificationManager::URLDownloadNotification::render_close_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
if (m_percentage < 0.f || m_percentage >= 1.f) {
render_close_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y);
if (m_percentage >= 1.f)
render_open_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y);
} else
render_pause_cancel_buttons_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y);
}
void NotificationManager::URLDownloadNotification::render_close_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
ImVec2 win_size(win_size_x, win_size_y);
ImVec2 win_pos(win_pos_x, win_pos_y);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f));
push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f));
std::string button_text;
button_text = ImGui::CloseNotifButton;
if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - win_size.x / 10.f, win_pos.y),
ImVec2(win_pos.x, win_pos.y + win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0)),
true))
{
button_text = ImGui::CloseNotifHoverButton;
}
ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str());
ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f);
ImGui::SetCursorPosX(win_size.x - m_line_height * 2.75f);
ImGui::SetCursorPosY(win_size.y / 2 - button_size.y);
if (imgui.button(button_text.c_str(), button_size.x, button_size.y))
{
close();
}
//invisible large button
ImGui::SetCursorPosX(win_size.x - m_line_height * 2.35f);
ImGui::SetCursorPosY(0);
if (imgui.button(" ", m_line_height * 2.125, win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0)))
{
close();
}
ImGui::PopStyleColor(5);
}
void NotificationManager::URLDownloadNotification::render_pause_cancel_buttons_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
render_cancel_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y);
render_pause_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y);
}
void NotificationManager::URLDownloadNotification::render_pause_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
ImVec2 win_size(win_size_x, win_size_y);
ImVec2 win_pos(win_pos_x, win_pos_y);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f));
push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f));
std::wstring button_text;
button_text = (m_download_paused ? ImGui::PlayButton : ImGui::PauseButton);
if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - m_line_height * 5.f, win_pos.y),
ImVec2(win_pos.x - m_line_height * 2.5f, win_pos.y + win_size.y),
true))
{
button_text = (m_download_paused ? ImGui::PlayHoverButton : ImGui::PauseHoverButton);
}
ImVec2 button_pic_size = ImGui::CalcTextSize(boost::nowide::narrow(button_text).c_str());
ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f);
ImGui::SetCursorPosX(win_size.x - m_line_height * 5.0f);
ImGui::SetCursorPosY(win_size.y / 2 - button_size.y);
if (imgui.button(button_text.c_str(), button_size.x, button_size.y))
{
trigger_user_action_callback(m_download_paused ? DownloaderUserAction::DownloadUserContinued : DownloaderUserAction::DownloadUserPaused);
}
//invisible large button
ImGui::SetCursorPosX(win_size.x - m_line_height * 4.625f);
ImGui::SetCursorPosY(0);
if (imgui.button(" ", m_line_height * 2.f, win_size.y))
{
trigger_user_action_callback(m_download_paused ? DownloaderUserAction::DownloadUserContinued : DownloaderUserAction::DownloadUserPaused);
}
ImGui::PopStyleColor(5);
}
void NotificationManager::URLDownloadNotification::render_open_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
ImVec2 win_size(win_size_x, win_size_y);
ImVec2 win_pos(win_pos_x, win_pos_y);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f));
push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f));
std::wstring button_text;
button_text = ImGui::OpenButton;
if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - m_line_height * 5.f, win_pos.y),
ImVec2(win_pos.x - m_line_height * 2.5f, win_pos.y + win_size.y),
true))
{
button_text = ImGui::OpenHoverButton;
}
ImVec2 button_pic_size = ImGui::CalcTextSize(boost::nowide::narrow(button_text).c_str());
ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f);
ImGui::SetCursorPosX(win_size.x - m_line_height * 5.0f);
ImGui::SetCursorPosY(win_size.y / 2 - button_size.y);
if (imgui.button(button_text.c_str(), button_size.x, button_size.y))
{
trigger_user_action_callback(DownloaderUserAction::DownloadUserOpenedFolder);
}
//invisible large button
ImGui::SetCursorPosX(win_size.x - m_line_height * 4.625f);
ImGui::SetCursorPosY(0);
if (imgui.button(" ", m_line_height * 2.f, win_size.y))
{
trigger_user_action_callback(DownloaderUserAction::DownloadUserOpenedFolder);
}
ImGui::PopStyleColor(5);
}
void NotificationManager::URLDownloadNotification::render_cancel_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
ImVec2 win_size(win_size_x, win_size_y);
ImVec2 win_pos(win_pos_x, win_pos_y);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f));
push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f));
std::string button_text;
button_text = ImGui::CancelButton;
if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - win_size.x / 10.f, win_pos.y),
ImVec2(win_pos.x, win_pos.y + win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0)),
true))
{
button_text = ImGui::CancelHoverButton;
}
ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str());
ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f);
ImGui::SetCursorPosX(win_size.x - m_line_height * 2.75f);
ImGui::SetCursorPosY(win_size.y / 2 - button_size.y);
if (imgui.button(button_text.c_str(), button_size.x, button_size.y))
{
trigger_user_action_callback(DownloaderUserAction::DownloadUserCanceled);
}
//invisible large button
ImGui::SetCursorPosX(win_size.x - m_line_height * 2.35f);
ImGui::SetCursorPosY(0);
if (imgui.button(" ", m_line_height * 2.125, win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0)))
{
trigger_user_action_callback(DownloaderUserAction::DownloadUserCanceled);
}
ImGui::PopStyleColor(5);
}
void NotificationManager::URLDownloadNotification::trigger_user_action_callback(DownloaderUserAction action)
{
if (m_user_action_callback) {
if (m_user_action_callback(action, m_download_id)) {}
}
}
void NotificationManager::URLDownloadNotification::render_bar(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
ProgressBarNotification::render_bar(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y);
std::string text;
if (m_percentage < 0.f) {
text = _u8L("ERROR") + ": " + m_error_message;
} else if (m_percentage >= 1.f) {
text = _u8L("COMPLETED");
} else {
std::stringstream stream;
stream << std::fixed << std::setprecision(2) << (int)(m_percentage * 100) << "%";
text = stream.str();
}
ImGui::SetCursorPosX(m_left_indentation);
ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? 0 : m_line_height / 4));
imgui.text(text.c_str());
}
void NotificationManager::URLDownloadNotification::count_spaces()
{
ProgressBarNotification::count_spaces();
m_window_width_offset = m_line_height * 6;
}
//------PrintHostUploadNotification----------------
void NotificationManager::PrintHostUploadNotification::init()
@ -2162,7 +2420,6 @@ void NotificationManager::upload_job_notification_show_error(int id, const std::
}
}
}
void NotificationManager::push_download_progress_notification(const std::string& text, std::function<bool()> cancel_callback)
{
// If already exists, change text and reset progress
@ -2194,6 +2451,81 @@ void NotificationManager::set_download_progress_percentage(float percentage)
}
}
void NotificationManager::push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback)
{
// If already exists
for (std::unique_ptr<PopNotification>& notification : m_pop_notifications) {
if (notification->get_type() == NotificationType::URLDownload && dynamic_cast<URLDownloadNotification*>(notification.get())->get_download_id() == id) {
return;
}
}
// push new one
NotificationData data{ NotificationType::URLDownload, NotificationLevel::ProgressBarNotificationLevel, 5, _utf8("Download:") + " " + text };
push_notification_data(std::make_unique<NotificationManager::URLDownloadNotification>(data, m_id_provider, m_evt_handler, id, user_action_callback), 0);
}
void NotificationManager::set_download_URL_progress(size_t id, float percentage)
{
for (std::unique_ptr<PopNotification>& notification : m_pop_notifications) {
if (notification->get_type() == NotificationType::URLDownload) {
URLDownloadNotification* ntf = dynamic_cast<URLDownloadNotification*>(notification.get());
if (ntf->get_download_id() != id)
continue;
// if this changes the percentage, it should be shown now
float percent_b4 = ntf->get_percentage();
ntf->set_percentage(percentage);
ntf->set_paused(false);
if (ntf->get_percentage() != percent_b4)
wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0);
return;
}
}
}
void NotificationManager::set_download_URL_paused(size_t id)
{
for (std::unique_ptr<PopNotification>& notification : m_pop_notifications) {
if (notification->get_type() == NotificationType::URLDownload) {
URLDownloadNotification* ntf = dynamic_cast<URLDownloadNotification*>(notification.get());
if (ntf->get_download_id() != id)
continue;
ntf->set_paused(true);
wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0);
return;
}
}
}
void NotificationManager::set_download_URL_canceled(size_t id)
{
for (std::unique_ptr<PopNotification>& notification : m_pop_notifications) {
if (notification->get_type() == NotificationType::URLDownload) {
URLDownloadNotification* ntf = dynamic_cast<URLDownloadNotification*>(notification.get());
if (ntf->get_download_id() != id)
continue;
ntf->close();
wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0);
return;
}
}
}
void NotificationManager::set_download_URL_error(size_t id, const std::string& text)
{
for (std::unique_ptr<PopNotification>& notification : m_pop_notifications) {
if (notification->get_type() == NotificationType::URLDownload) {
URLDownloadNotification* ntf = dynamic_cast<URLDownloadNotification*>(notification.get());
if (ntf->get_download_id() != id)
continue;
float percent_b4 = ntf->get_percentage();
ntf->set_percentage(-1.f);
ntf->set_error_message(text);
if (ntf->get_percentage() != percent_b4)
wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0);
return;
}
}
}
void NotificationManager::init_slicing_progress_notification(std::function<bool()> cancel_callback)
{
for (std::unique_ptr<PopNotification>& notification : m_pop_notifications) {

View File

@ -7,6 +7,7 @@
#include "Event.hpp"
#include "I18N.hpp"
#include "Jobs/ProgressIndicator.hpp"
#include "Downloader.hpp"
#include <libslic3r/ObjectID.hpp>
#include <libslic3r/Technologies.hpp>
@ -117,6 +118,8 @@ enum class NotificationType
NetfabbFinished,
// Short meesage to fill space between start and finish of export
ExportOngoing,
// Progressbar of download from prusaslicer:// url
URLDownload
};
class NotificationManager
@ -215,6 +218,12 @@ public:
// Download App progress
void push_download_progress_notification(const std::string& text, std::function<bool()> cancel_callback);
void set_download_progress_percentage(float percentage);
// Download URL progress notif
void push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback);
void set_download_URL_progress(size_t id, float percentage);
void set_download_URL_paused(size_t id);
void set_download_URL_canceled(size_t id);
void set_download_URL_error(size_t id, const std::string& text);
// slicing progress
void init_slicing_progress_notification(std::function<bool()> cancel_callback);
void set_slicing_progress_began();
@ -505,6 +514,62 @@ private:
long m_hover_time{ 0 };
};
class URLDownloadNotification : public ProgressBarNotification
{
public:
URLDownloadNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, size_t download_id, std::function<bool(DownloaderUserAction, int)> user_action_callback)
//: ProgressBarWithCancelNotification(n, id_provider, evt_handler, cancel_callback)
: ProgressBarNotification(n, id_provider, evt_handler)
, m_download_id(download_id)
, m_user_action_callback(user_action_callback)
{
}
void set_percentage(float percent) override
{
m_percentage = percent;
if (m_percentage >= 1.f) {
m_notification_start = GLCanvas3D::timestamp_now();
m_state = EState::Shown;
} else
m_state = EState::NotFading;
}
size_t get_download_id() { return m_download_id; }
void set_user_action_callback(std::function<bool(DownloaderUserAction, int)> user_action_callback) { m_user_action_callback = user_action_callback; }
void set_paused(bool paused) { m_download_paused = paused; }
void set_error_message(const std::string& message) { m_error_message = message; }
bool compare_text(const std::string& text) const override { return false; };
protected:
void render_close_button(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y) override;
void render_close_button_inner(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y);
void render_pause_cancel_buttons_inner(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y);
void render_open_button_inner(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y);
void render_cancel_button_inner(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y);
void render_pause_button_inner(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y);
void render_bar(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y) override;
void trigger_user_action_callback(DownloaderUserAction action);
void count_spaces() override;
size_t m_download_id;
std::function<bool(DownloaderUserAction, int)> m_user_action_callback;
bool m_download_paused {false};
std::string m_error_message;
};
class PrintHostUploadNotification : public ProgressBarNotification
{
public:
@ -819,7 +884,8 @@ private:
NotificationType::PlaterWarning,
NotificationType::ProgressBar,
NotificationType::PrintHostUpload,
NotificationType::SimplifySuggestion
NotificationType::SimplifySuggestion,
NotificationType::URLDownload
};
//prepared (basic) notifications
// non-static so its not loaded too early. If static, the translations wont load correctly.

View File

@ -27,6 +27,7 @@
#include <wx/numdlg.h>
#include <wx/debug.h>
#include <wx/busyinfo.h>
#include <wx/stdpaths.h>
#ifdef _WIN32
#include <wx/richtooltip.h>
#include <wx/custombgwin.h>
@ -50,6 +51,7 @@
#include "libslic3r/Utils.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/miniz_extension.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
@ -98,6 +100,7 @@
#include "ProjectDirtyStateManager.hpp"
#include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification
#include "Gizmos/GLGizmoCut.hpp"
#include "FileArchiveDialog.hpp"
#ifdef __APPLE__
#include "Gizmos/GLGizmosManager.hpp"
@ -2231,6 +2234,16 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
wxGetApp().mainframe->Raise();
this->q->load_files(input_files);
});
this->q->Bind(EVT_START_DOWNLOAD_OTHER_INSTANCE, [this](StartDownloadOtherInstanceEvent& evt) {
BOOST_LOG_TRIVIAL(trace) << "Received url from other instance event.";
wxGetApp().mainframe->Raise();
for (size_t i = 0; i < evt.data.size(); ++i) {
wxGetApp().start_download(evt.data[i]);
}
});
this->q->Bind(EVT_INSTANCE_GO_TO_FRONT, [this](InstanceGoToFrontEvent &) {
bring_instance_forward();
});
@ -2446,6 +2459,9 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
int answer_convert_from_meters = wxOK_DEFAULT;
int answer_convert_from_imperial_units = wxOK_DEFAULT;
bool in_temp = false;
const fs::path temp_path = wxStandardPaths::Get().GetTempDir().utf8_str().data();
size_t input_files_size = input_files.size();
for (size_t i = 0; i < input_files_size; ++i) {
#ifdef _WIN32
@ -2456,6 +2472,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
// Don't make a copy on Posix. Slash is a path separator, back slashes are not accepted as a substitute.
const auto &path = input_files[i];
#endif // _WIN32
in_temp = (path.parent_path() == temp_path);
const auto filename = path.filename();
if (progress_dlg) {
progress_dlg->Update(static_cast<int>(100.0f * static_cast<float>(i) / static_cast<float>(input_files.size())), _L("Loading file") + ": " + from_path(filename));
@ -2536,7 +2553,8 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
q->update_filament_colors_in_full_config();
is_project_file = true;
}
wxGetApp().app_config->update_config_dir(path.parent_path().string());
if (!in_temp)
wxGetApp().app_config->update_config_dir(path.parent_path().string());
}
}
else {
@ -2695,10 +2713,10 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end());
}
if (load_model) {
if (load_model && !in_temp) {
wxGetApp().app_config->update_skein_dir(input_files[input_files.size() - 1].parent_path().make_preferred().string());
// XXX: Plater.pm had @loaded_files, but didn't seem to fill them with the filenames...
// statusbar()->set_status_text(_L("Loaded"));
// statusbar()->set_status_text(_L("Loaded"));
}
// automatic selection of added objects
@ -2909,9 +2927,10 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type)
}
std::string out_dir = (boost::filesystem::path(output_file).parent_path()).string();
std::string temp_dir = wxStandardPaths::Get().GetTempDir().utf8_str().data();
wxFileDialog dlg(q, dlg_title,
is_shapes_dir(out_dir) ? from_u8(wxGetApp().app_config->get_last_dir()) : from_path(output_file.parent_path()), from_path(output_file.filename()),
out_dir == temp_dir ? from_u8(wxGetApp().app_config->get("last_output_path")) : (is_shapes_dir(out_dir) ? from_u8(wxGetApp().app_config->get_last_dir()) : from_path(output_file.parent_path())), from_path(output_file.filename()),
wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (dlg.ShowModal() != wxID_OK)
@ -4626,7 +4645,10 @@ void Plater::priv::set_project_filename(const wxString& filename)
m_project_filename = from_path(full_path);
wxGetApp().mainframe->update_title();
if (!filename.empty())
const fs::path temp_path = wxStandardPaths::Get().GetTempDir().utf8_str().data();
bool in_temp = (temp_path == full_path.parent_path().make_preferred());
if (!filename.empty() && !in_temp)
wxGetApp().mainframe->add_to_recent_projects(filename);
}
@ -5359,6 +5381,11 @@ Print& Plater::fff_print() { return p->fff_print; }
const SLAPrint& Plater::sla_print() const { return p->sla_print; }
SLAPrint& Plater::sla_print() { return p->sla_print; }
bool Plater::is_project_temp() const
{
return false;
}
void Plater::new_project()
{
if (int saved_project = p->save_project_if_dirty(_L("Creating a new project while the current project is modified.")); saved_project == wxID_CANCEL)
@ -5563,18 +5590,408 @@ std::vector<size_t> Plater::load_files(const std::vector<std::string>& input_fil
return p->load_files(paths, load_model, load_config, imperial_units);
}
enum class LoadType : unsigned char
class LoadProjectsDialog : public DPIDialog
{
Unknown,
OpenProject,
LoadGeometry,
LoadConfig
int m_action{ 0 };
bool m_all { false };
wxComboBox* m_combo_project { nullptr };
wxComboBox* m_combo_config { nullptr };
public:
enum class LoadProjectOption : unsigned char
{
Unknown,
AllGeometry,
AllNewWindow,
OneProject,
OneConfig
};
LoadProjectsDialog(const std::vector<fs::path>& paths);
int get_action() const { return m_action + 1; }
bool get_all() const { return m_all; }
int get_selected() const
{
if (m_combo_project && m_combo_project->IsEnabled())
return m_combo_project->GetSelection();
else if (m_combo_config && m_combo_config->IsEnabled())
return m_combo_config->GetSelection();
else
return -1;
}
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
};
LoadProjectsDialog::LoadProjectsDialog(const std::vector<fs::path>& paths)
: DPIDialog(static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY,
from_u8((boost::format(_utf8(L("%s - Multiple projects file"))) % SLIC3R_APP_NAME).str()), wxDefaultPosition,
wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
{
SetFont(wxGetApp().normal_font());
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
bool contains_projects = !paths.empty();
bool instances_allowed = wxGetApp().app_config->get("single_instance") != "1";
if (contains_projects)
main_sizer->Add(new wxStaticText(this, wxID_ANY,
get_wraped_wxString(_L("There are several files being loaded, including Project files.") + "\n" + _L("Select an action to apply to all files."))), 0, wxEXPAND | wxALL, 10);
else
main_sizer->Add(new wxStaticText(this, wxID_ANY,
get_wraped_wxString(_L("There are several files being loaded.") + "\n" + _L("Select an action to apply to all files."))), 0, wxEXPAND | wxALL, 10);
wxStaticBox* action_stb = new wxStaticBox(this, wxID_ANY, _L("Action"));
if (!wxOSX) action_stb->SetBackgroundStyle(wxBG_STYLE_PAINT);
action_stb->SetFont(wxGetApp().normal_font());
if (contains_projects) {
wxArrayString filenames;
for (const fs::path& path : paths) {
filenames.push_back(from_u8(path.filename().string()));
}
m_combo_project = new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, filenames, wxCB_READONLY);
m_combo_project->SetValue(filenames.front());
m_combo_project->Enable(false);
m_combo_config = new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, filenames, wxCB_READONLY);
m_combo_config->SetValue(filenames.front());
m_combo_config->Enable(false);
}
wxStaticBoxSizer* stb_sizer = new wxStaticBoxSizer(action_stb, wxVERTICAL);
int id = 0;
// all geometry
wxRadioButton* btn = new wxRadioButton(this, wxID_ANY, _L("Import geometry"), wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0);
btn->SetValue(id == m_action);
btn->Bind(wxEVT_RADIOBUTTON, [this, id, contains_projects](wxCommandEvent&) {
m_action = id;
if (contains_projects) {
m_combo_project->Enable(false);
m_combo_config->Enable(false);
}
});
stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5);
id++;
// all new window
if (instances_allowed) {
btn = new wxRadioButton(this, wxID_ANY, _L("Start new PrusaSlicer instance"), wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0);
btn->SetValue(id == m_action);
btn->Bind(wxEVT_RADIOBUTTON, [this, id, contains_projects](wxCommandEvent&) {
m_action = id;
if (contains_projects) {
m_combo_project->Enable(false);
m_combo_config->Enable(false);
}
});
stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5);
}
id++; // IMPORTANT TO ALWAYS UP THE ID EVEN IF OPTION IS NOT ADDED!
if (contains_projects) {
// one project
btn = new wxRadioButton(this, wxID_ANY, _L("Select one to load as project"), wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0);
btn->SetValue(false);
btn->Bind(wxEVT_RADIOBUTTON, [this, id](wxCommandEvent&) {
m_action = id;
m_combo_project->Enable(true);
m_combo_config->Enable(false);
});
stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5);
stb_sizer->Add(m_combo_project, 0, wxEXPAND | wxTOP, 5);
// one config
id++;
btn = new wxRadioButton(this, wxID_ANY, _L("Select one to load config only"), wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0);
btn->SetValue(id == m_action);
btn->Bind(wxEVT_RADIOBUTTON, [this, id, instances_allowed](wxCommandEvent&) {
m_action = id;
if (instances_allowed)
m_combo_project->Enable(false);
m_combo_config->Enable(true);
});
stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5);
stb_sizer->Add(m_combo_config, 0, wxEXPAND | wxTOP, 5);
}
main_sizer->Add(stb_sizer, 1, wxEXPAND | wxRIGHT | wxLEFT, 10);
wxBoxSizer* bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
bottom_sizer->Add(CreateStdDialogButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxLEFT, 5);
main_sizer->Add(bottom_sizer, 0, wxEXPAND | wxALL, 10);
SetSizer(main_sizer);
main_sizer->SetSizeHints(this);
// Update DarkUi just for buttons
wxGetApp().UpdateDlgDarkUI(this, true);
}
void LoadProjectsDialog::on_dpi_changed(const wxRect& suggested_rect)
{
const int em = em_unit();
SetMinSize(wxSize(65 * em, 30 * em));
Fit();
Refresh();
}
bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path)
{
//std::vector<fs::path> unzipped_paths;
std::vector<fs::path> non_project_paths;
std::vector<fs::path> project_paths;
try
{
mz_zip_archive archive;
mz_zip_zero_struct(&archive);
if (!open_zip_reader(&archive, archive_path.string())) {
std::string err_msg = GUI::format(_utf8("Loading of a zip archive on path %1% has failed."), archive_path.string());
throw Slic3r::FileIOError(err_msg);
}
mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
mz_zip_archive_file_stat stat;
std::vector<fs::path> selected_paths;
FileArchiveDialog dlg(static_cast<wxWindow*>(wxGetApp().mainframe), &archive, selected_paths);
if (dlg.ShowModal() == wxID_OK)
{
std::string archive_path_string = archive_path.string();
archive_path_string = archive_path_string.substr(0, archive_path_string.size() - 4);
fs::path archive_dir(wxStandardPaths::Get().GetTempDir().utf8_str().data());
for (mz_uint i = 0; i < num_entries; ++i) {
if (mz_zip_reader_file_stat(&archive, i, &stat)) {
wxString wname = boost::nowide::widen(stat.m_filename);
std::string name = GUI::format(wname);
fs::path archive_path(name);
for (const auto& path : selected_paths) {
if (path == archive_path) {
try
{
std::replace(name.begin(), name.end(), '\\', '/');
// rename if file exists
std::string filename = path.filename().string();
std::string extension = boost::filesystem::extension(path);
std::string just_filename = filename.substr(0, filename.size() - extension.size());
std::string final_filename = just_filename;
size_t version = 0;
while (fs::exists(archive_dir / (final_filename + extension)))
{
++version;
final_filename = just_filename + "(" + std::to_string(version) + ")";
}
filename = final_filename + extension;
fs::path final_path = archive_dir / filename;
std::string buffer((size_t)stat.m_uncomp_size, 0);
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0);
if (res == 0) {
wxString error_log = GUI::format_wxstr(_L("Failed to unzip file to %1%: %2% "), final_path.string(), mz_zip_get_error_string(mz_zip_get_last_error(&archive)));
BOOST_LOG_TRIVIAL(error) << error_log;
show_error(nullptr, error_log);
continue;
}
fs::fstream file(final_path, std::ios::out | std::ios::binary | std::ios::trunc);
file.write(buffer.c_str(), buffer.size());
file.close();
if (!fs::exists(final_path)) {
wxString error_log = GUI::format_wxstr(_L("Failed to find unzipped file at %1%. Unzipping of file has failed."), final_path.string());
BOOST_LOG_TRIVIAL(error) << error_log;
show_error(nullptr, error_log);
continue;
}
BOOST_LOG_TRIVIAL(info) << "Unzipped " << final_path;
if (!boost::algorithm::iends_with(filename, ".3mf") && !boost::algorithm::iends_with(filename, ".amf")) {
non_project_paths.emplace_back(final_path);
continue;
}
// if 3mf - read archive headers to find project file
if ((boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(final_path.string())) ||
(boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf"))) {
non_project_paths.emplace_back(final_path);
continue;
}
project_paths.emplace_back(final_path);
}
catch (const std::exception& e)
{
// ensure the zip archive is closed and rethrow the exception
close_zip_reader(&archive);
throw Slic3r::FileIOError(e.what());
}
}
}
}
}
close_zip_reader(&archive);
if (non_project_paths.size() + project_paths.size() != selected_paths.size())
BOOST_LOG_TRIVIAL(error) << "Decompresing of archive did not retrieve all files. Expected files: "
<< selected_paths.size()
<< " Decopressed files: "
<< non_project_paths.size() + project_paths.size();
} else {
close_zip_reader(&archive);
return false;
}
}
catch (const Slic3r::FileIOError& e) {
// zip reader should be already closed or not even opened
GUI::show_error(this, e.what());
return false;
}
// none selected
if (project_paths.empty() && non_project_paths.empty())
{
return false;
}
#if 0
// 1 project, 0 models - behave like drag n drop
if (project_paths.size() == 1 && non_project_paths.empty())
{
wxArrayString aux;
aux.Add(from_u8(project_paths.front().string()));
load_files(aux);
//load_files(project_paths, true, true);
boost::system::error_code ec;
fs::remove(project_paths.front(), ec);
if (ec)
BOOST_LOG_TRIVIAL(error) << ec.message();
return true;
}
// 1 model (or more and other instances are not allowed), 0 projects - open geometry
if (project_paths.empty() && (non_project_paths.size() == 1 || wxGetApp().app_config->get("single_instance") == "1"))
{
load_files(non_project_paths, true, false);
boost::system::error_code ec;
fs::remove(non_project_paths.front(), ec);
if (ec)
BOOST_LOG_TRIVIAL(error) << ec.message();
return true;
}
bool delete_after = true;
LoadProjectsDialog dlg(project_paths);
if (dlg.ShowModal() == wxID_OK) {
LoadProjectsDialog::LoadProjectOption option = static_cast<LoadProjectsDialog::LoadProjectOption>(dlg.get_action());
switch (option)
{
case LoadProjectsDialog::LoadProjectOption::AllGeometry: {
load_files(project_paths, true, false);
load_files(non_project_paths, true, false);
break;
}
case LoadProjectsDialog::LoadProjectOption::AllNewWindow: {
delete_after = false;
for (const fs::path& path : project_paths) {
wxString f = from_path(path);
start_new_slicer(&f, false);
}
for (const fs::path& path : non_project_paths) {
wxString f = from_path(path);
start_new_slicer(&f, false);
}
break;
}
case LoadProjectsDialog::LoadProjectOption::OneProject: {
int pos = dlg.get_selected();
assert(pos >= 0 && pos < project_paths.size());
if (wxGetApp().can_load_project())
load_project(from_path(project_paths[pos]));
project_paths.erase(project_paths.begin() + pos);
load_files(project_paths, true, false);
load_files(non_project_paths, true, false);
break;
}
case LoadProjectsDialog::LoadProjectOption::OneConfig: {
int pos = dlg.get_selected();
assert(pos >= 0 && pos < project_paths.size());
std::vector<fs::path> aux;
aux.push_back(project_paths[pos]);
load_files(aux, false, true);
project_paths.erase(project_paths.begin() + pos);
load_files(project_paths, true, false);
load_files(non_project_paths, true, false);
break;
}
case LoadProjectsDialog::LoadProjectOption::Unknown:
default:
assert(false);
break;
}
}
if (!delete_after)
return true;
#else
// 1 project file and some models - behave like drag n drop of 3mf and then load models
if (project_paths.size() == 1)
{
wxArrayString aux;
aux.Add(from_u8(project_paths.front().string()));
bool loaded3mf = load_files(aux, true);
load_files(non_project_paths, true, false);
boost::system::error_code ec;
if (loaded3mf) {
fs::remove(project_paths.front(), ec);
if (ec)
BOOST_LOG_TRIVIAL(error) << ec.message();
}
for (const fs::path& path : non_project_paths) {
// Delete file from temp file (path variable), it will stay only in app memory.
boost::system::error_code ec;
fs::remove(path, ec);
if (ec)
BOOST_LOG_TRIVIAL(error) << ec.message();
}
return true;
}
// load all projects and all models as geometry
load_files(project_paths, true, false);
load_files(non_project_paths, true, false);
#endif // 0
for (const fs::path& path : project_paths) {
// Delete file from temp file (path variable), it will stay only in app memory.
boost::system::error_code ec;
fs::remove(path, ec);
if (ec)
BOOST_LOG_TRIVIAL(error) << ec.message();
}
for (const fs::path& path : non_project_paths) {
// Delete file from temp file (path variable), it will stay only in app memory.
boost::system::error_code ec;
fs::remove(path, ec);
if (ec)
BOOST_LOG_TRIVIAL(error) << ec.message();
}
return true;
}
class ProjectDropDialog : public DPIDialog
{
int m_action { 0 };
public:
enum class LoadType : unsigned char
{
Unknown,
OpenProject,
LoadGeometry,
LoadConfig,
OpenWindow
};
ProjectDropDialog(const std::string& filename);
int get_action() const { return m_action + 1; }
@ -5590,17 +6007,21 @@ ProjectDropDialog::ProjectDropDialog(const std::string& filename)
{
SetFont(wxGetApp().normal_font());
bool single_instance_only = wxGetApp().app_config->get("single_instance") == "1";
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
const wxString choices[] = { _L("Open as project"),
_L("Import geometry only"),
_L("Import config only") };
wxArrayString choices;
choices.reserve(4);
choices.Add(_L("Open as project"));
choices.Add(_L("Import geometry only"));
choices.Add(_L("Import config only"));
if (!single_instance_only)
choices.Add(_L("Start new PrusaSlicer instance"));
main_sizer->Add(new wxStaticText(this, wxID_ANY,
get_wraped_wxString(_L("Select an action to apply to the file") + ": " + from_u8(filename))), 0, wxEXPAND | wxALL, 10);
m_action = std::clamp(std::stoi(wxGetApp().app_config->get("drop_project_action")),
static_cast<int>(LoadType::OpenProject), static_cast<int>(LoadType::LoadConfig)) - 1;
static_cast<int>(LoadType::OpenProject), single_instance_only? static_cast<int>(LoadType::LoadConfig) : static_cast<int>(LoadType::OpenWindow)) - 1;
wxStaticBox* action_stb = new wxStaticBox(this, wxID_ANY, _L("Action"));
if (!wxOSX) action_stb->SetBackgroundStyle(wxBG_STYLE_PAINT);
@ -5642,9 +6063,9 @@ void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect)
Refresh();
}
bool Plater::load_files(const wxArrayString& filenames)
bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/*=false*/)
{
const std::regex pattern_drop(".*[.](stl|obj|amf|3mf|prusa|step|stp)", std::regex::icase);
const std::regex pattern_drop(".*[.](stl|obj|amf|3mf|prusa|step|stp|zip)", std::regex::icase);
const std::regex pattern_gcode_drop(".*[.](gcode|g)", std::regex::icase);
std::vector<fs::path> paths;
@ -5688,53 +6109,61 @@ bool Plater::load_files(const wxArrayString& filenames)
for (std::vector<fs::path>::const_reverse_iterator it = paths.rbegin(); it != paths.rend(); ++it) {
std::string filename = (*it).filename().string();
if (boost::algorithm::iends_with(filename, ".3mf") || boost::algorithm::iends_with(filename, ".amf")) {
LoadType load_type = LoadType::Unknown;
ProjectDropDialog::LoadType load_type = ProjectDropDialog::LoadType::Unknown;
if (!model().objects.empty()) {
if ((boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(it->string())) ||
(boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf")))
load_type = LoadType::LoadGeometry;
load_type = ProjectDropDialog::LoadType::LoadGeometry;
else {
if (wxGetApp().app_config->get("show_drop_project_dialog") == "1") {
ProjectDropDialog dlg(filename);
if (dlg.ShowModal() == wxID_OK) {
int choice = dlg.get_action();
load_type = static_cast<LoadType>(choice);
load_type = static_cast<ProjectDropDialog::LoadType>(choice);
wxGetApp().app_config->set("drop_project_action", std::to_string(choice));
}
}
else
load_type = static_cast<LoadType>(std::clamp(std::stoi(wxGetApp().app_config->get("drop_project_action")),
static_cast<int>(LoadType::OpenProject), static_cast<int>(LoadType::LoadConfig)));
load_type = static_cast<ProjectDropDialog::LoadType>(std::clamp(std::stoi(wxGetApp().app_config->get("drop_project_action")),
static_cast<int>(ProjectDropDialog::LoadType::OpenProject), static_cast<int>(ProjectDropDialog::LoadType::LoadConfig)));
}
}
else
load_type = LoadType::OpenProject;
load_type = ProjectDropDialog::LoadType::OpenProject;
if (load_type == LoadType::Unknown)
if (load_type == ProjectDropDialog::LoadType::Unknown)
return false;
switch (load_type) {
case LoadType::OpenProject: {
case ProjectDropDialog::LoadType::OpenProject: {
if (wxGetApp().can_load_project())
load_project(from_path(*it));
break;
}
case LoadType::LoadGeometry: {
case ProjectDropDialog::LoadType::LoadGeometry: {
Plater::TakeSnapshot snapshot(this, _L("Import Object"));
load_files({ *it }, true, false);
break;
}
case LoadType::LoadConfig: {
case ProjectDropDialog::LoadType::LoadConfig: {
load_files({ *it }, false, true);
break;
}
case LoadType::Unknown : {
case ProjectDropDialog::LoadType::OpenWindow: {
wxString f = from_path(*it);
start_new_slicer(&f, false, delete_after_load);
return false; // did not load anything to this instance
}
case ProjectDropDialog::LoadType::Unknown : {
assert(false);
break;
}
}
return true;
} else if (boost::algorithm::iends_with(filename, ".zip")) {
return preview_zip_archive(*it);
}
}

View File

@ -152,6 +152,8 @@ public:
void render_project_state_debug_window() const;
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
bool is_project_temp() const;
Sidebar& sidebar();
const Model& model() const;
Model& model();
@ -175,9 +177,11 @@ public:
// To be called when providing a list of files to the GUI slic3r on command line.
std::vector<size_t> load_files(const std::vector<std::string>& input_files, bool load_model = true, bool load_config = true, bool imperial_units = false);
// to be called on drag and drop
bool load_files(const wxArrayString& filenames);
bool load_files(const wxArrayString& filenames, bool delete_after_load = false);
void check_selected_presets_visibility(PrinterTechnology loaded_printer_technology);
bool preview_zip_archive(const boost::filesystem::path& input_file);
const wxString& get_last_loaded_gcode() const { return m_last_loaded_gcode; }
void update();

View File

@ -10,6 +10,16 @@
#include "ButtonsDescription.hpp"
#include "OG_CustomCtrl.hpp"
#include "GLCanvas3D.hpp"
#include "ConfigWizard_private.hpp"
#include <boost/dll/runtime_symbol_info.hpp>
#ifdef WIN32
#include <wx/msw/registry.h>
#endif // WIN32
#ifdef __linux__
#include "DesktopIntegrationDialog.hpp"
#endif //__linux__
namespace Slic3r {
@ -80,6 +90,14 @@ void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::strin
m_use_custom_toolbar_size = get_app_config()->get("use_custom_toolbar_size") == "1";
if (wxGetApp().is_editor()) {
auto app_config = get_app_config();
downloader->set_path_name(app_config->get("url_downloader_dest"));
downloader->allow(!app_config->has("downloader_url_registered") || app_config->get("downloader_url_registered") == "1");
for (const std::string& opt_key : {"suppress_hyperlinks", "downloader_url_registered"})
m_optgroup_other->set_value(opt_key, app_config->get(opt_key) == "1");
// update colors for color pickers of the labels
update_color(m_sys_colour, wxGetApp().get_label_clr_sys());
update_color(m_mod_colour, wxGetApp().get_label_clr_modified());
@ -166,6 +184,25 @@ static void append_enum_option( std::shared_ptr<ConfigOptionsGroup> optgroup,
wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences"));
}
static void append_string_option(std::shared_ptr<ConfigOptionsGroup> optgroup,
const std::string& opt_key,
const std::string& label,
const std::string& tooltip,
const std::string& def_val,
ConfigOptionMode mode = comSimple)
{
ConfigOptionDef def = { opt_key, coString };
def.label = label;
def.tooltip = tooltip;
def.mode = mode;
def.set_default_value(new ConfigOptionString{ def_val });
Option option(def, opt_key);
optgroup->append_single_option_line(option);
// fill data to the Search Dialog
wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences"));
}
static void append_preferences_option_to_searcer(std::shared_ptr<ConfigOptionsGroup> optgroup,
const std::string& opt_key,
const wxString& label)
@ -423,9 +460,9 @@ void PreferencesDialog::build()
return;
}
if (opt_key == "suppress_hyperlinks")
/* if (opt_key == "suppress_hyperlinks")
m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "";
else
else*/
m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "0";
};
@ -440,14 +477,14 @@ void PreferencesDialog::build()
L("Show sidebar collapse/expand button"),
L("If enabled, the button for the collapse sidebar will be appeared in top right corner of the 3D Scene"),
app_config->get("show_collapse_button") == "1");
/*
append_bool_option(m_optgroup_gui, "suppress_hyperlinks",
L("Suppress to open hyperlink in browser"),
L("If enabled, PrusaSlicer will not open a hyperlinks in your browser."),
//L("If enabled, the descriptions of configuration parameters in settings tabs wouldn't work as hyperlinks. "
// "If disabled, the descriptions of configuration parameters in settings tabs will work as hyperlinks."),
app_config->get("suppress_hyperlinks") == "1");
*/
append_bool_option(m_optgroup_gui, "color_mapinulation_panel",
L("Use colors for axes values in Manipulation panel"),
L("If enabled, the axes names and axes values will be colorized according to the axes colors. "
@ -514,6 +551,37 @@ void PreferencesDialog::build()
create_settings_text_color_widget();
create_settings_mode_color_widget();
m_optgroup_other = create_options_tab(_L("Other"), tabs);
m_optgroup_other->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
if (auto it = m_values.find(opt_key); it != m_values.end() && opt_key != "url_downloader_dest") {
m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected
return;
}
if (opt_key == "suppress_hyperlinks")
m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "";
else
m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "0"; m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "0";
};
append_bool_option(m_optgroup_other, "suppress_hyperlinks",
L("Suppress to open hyperlink in browser"),
L("If enabled, PrusaSlicer will not open a hyperlinks in your browser."),
//L("If enabled, the descriptions of configuration parameters in settings tabs wouldn't work as hyperlinks. "
// "If disabled, the descriptions of configuration parameters in settings tabs will work as hyperlinks."),
app_config->get("suppress_hyperlinks") == "1");
append_bool_option(m_optgroup_other, "downloader_url_registered",
L("Allow downloads from Printables.com"),
L("If enabled, PrusaSlicer will allow to download from Printables.com"),
app_config->get("downloader_url_registered") == "1");
activate_options_tab(m_optgroup_other);
create_downloader_path_sizer();
#if ENABLE_ENVIRONMENT_MAP
// Add "Render" tab
m_optgroup_render = create_options_tab(L("Render"), tabs);
@ -587,7 +655,7 @@ std::vector<ConfigOptionsGroup*> PreferencesDialog::optgroups()
{
std::vector<ConfigOptionsGroup*> out;
out.reserve(4);
for (ConfigOptionsGroup* opt : { m_optgroup_general.get(), m_optgroup_camera.get(), m_optgroup_gui.get()
for (ConfigOptionsGroup* opt : { m_optgroup_general.get(), m_optgroup_camera.get(), m_optgroup_gui.get(), m_optgroup_other.get()
#ifdef _WIN32
, m_optgroup_dark_mode.get()
#endif // _WIN32
@ -614,6 +682,16 @@ void PreferencesDialog::update_ctrls_alignment()
void PreferencesDialog::accept(wxEvent&)
{
if (const auto it = m_values.find("downloader_url_registered"); it != m_values.end())
downloader->allow(it->second == "1");
if (!downloader->on_finish())
return;
#ifdef __linux__
if( downloader->get_perform_registration_linux())
DesktopIntegrationDialog::perform_desktop_integration(true);
#endif // __linux__
std::vector<std::string> options_to_recreate_GUI = { "no_defaults", "tabs_as_menu", "sys_menu_enabled" };
for (const std::string& option : options_to_recreate_GUI) {
@ -637,7 +715,7 @@ void PreferencesDialog::accept(wxEvent&)
}
}
auto app_config = get_app_config();
auto app_config = get_app_config();
m_seq_top_layer_only_changed = false;
if (auto it = m_values.find("seq_top_layer_only"); it != m_values.end())
@ -738,7 +816,7 @@ void PreferencesDialog::revert(wxEvent&)
continue;
}
for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui
for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui, m_optgroup_other
#ifdef _WIN32
, m_optgroup_dark_mode
#endif // _WIN32
@ -970,6 +1048,25 @@ void PreferencesDialog::create_settings_mode_color_widget()
append_preferences_option_to_searcer(m_optgroup_gui, opt_key, title);
}
void PreferencesDialog::create_downloader_path_sizer()
{
wxWindow* parent = m_optgroup_other->parent();
wxString title = L("Download path");
std::string opt_key = "url_downloader_dest";
m_blinkers[opt_key] = new BlinkingBitmap(parent);
downloader = new DownloaderUtils::Worker(parent);
auto sizer = new wxBoxSizer(wxHORIZONTAL);
sizer->Add(m_blinkers[opt_key], 0, wxRIGHT, 2);
sizer->Add(downloader, 1, wxALIGN_CENTER_VERTICAL);
m_optgroup_other->sizer->Add(sizer, 0, wxEXPAND | wxTOP, em_unit());
append_preferences_option_to_searcer(m_optgroup_other, opt_key, title);
}
void PreferencesDialog::init_highlighter(const t_config_option_key& opt_key)
{
if (m_blinkers.find(opt_key) != m_blinkers.end())
@ -978,7 +1075,7 @@ void PreferencesDialog::init_highlighter(const t_config_option_key& opt_key)
return;
}
for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui
for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui, m_optgroup_other
#ifdef _WIN32
, m_optgroup_dark_mode
#endif // _WIN32

View File

@ -28,19 +28,24 @@ namespace GUI {
class ConfigOptionsGroup;
class OG_CustomCtrl;
namespace DownloaderUtils {
class Worker;
}
class PreferencesDialog : public DPIDialog
{
std::map<std::string, std::string> m_values;
std::shared_ptr<ConfigOptionsGroup> m_optgroup_general;
std::shared_ptr<ConfigOptionsGroup> m_optgroup_camera;
std::shared_ptr<ConfigOptionsGroup> m_optgroup_gui;
std::shared_ptr<ConfigOptionsGroup> m_optgroup_other;
#ifdef _WIN32
std::shared_ptr<ConfigOptionsGroup> m_optgroup_dark_mode;
#endif //_WIN32
#if ENABLE_ENVIRONMENT_MAP
std::shared_ptr<ConfigOptionsGroup> m_optgroup_render;
#endif // ENABLE_ENVIRONMENT_MAP
wxSizer* m_icon_size_sizer;
wxSizer* m_icon_size_sizer {nullptr};
wxSlider* m_icon_size_slider {nullptr};
wxRadioButton* m_rb_old_settings_layout_mode {nullptr};
wxRadioButton* m_rb_new_settings_layout_mode {nullptr};
@ -54,6 +59,8 @@ class PreferencesDialog : public DPIDialog
wxColourPickerCtrl* m_mode_advanced { nullptr };
wxColourPickerCtrl* m_mode_expert { nullptr };
DownloaderUtils::Worker* downloader{ nullptr };
wxBookCtrlBase* tabs {nullptr};
bool isOSX {false};
@ -88,6 +95,7 @@ protected:
void create_settings_mode_widget();
void create_settings_text_color_widget();
void create_settings_mode_color_widget();
void create_downloader_path_sizer();
void init_highlighter(const t_config_option_key& opt_key);
std::vector<ConfigOptionsGroup*> optgroups();

View File

@ -144,6 +144,7 @@ struct Http::priv
void set_post_body(const fs::path &path);
void set_post_body(const std::string &body);
void set_put_body(const fs::path &path);
void set_range(const std::string& range);
std::string curl_error(CURLcode curlcode);
std::string body_size_error();
@ -225,7 +226,7 @@ int Http::priv::xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_o
bool cb_cancel = false;
if (self->progressfn) {
Progress progress(dltotal, dlnow, ultotal, ulnow);
Progress progress(dltotal, dlnow, ultotal, ulnow, self->buffer);
self->progressfn(progress, cb_cancel);
}
@ -313,6 +314,11 @@ void Http::priv::set_put_body(const fs::path &path)
}
}
void Http::priv::set_range(const std::string& range)
{
::curl_easy_setopt(curl, CURLOPT_RANGE, range.c_str());
}
std::string Http::priv::curl_error(CURLcode curlcode)
{
return (boost::format("%1%:\n%2%\n[Error %3%]")
@ -370,7 +376,7 @@ void Http::priv::http_perform()
if (res == CURLE_ABORTED_BY_CALLBACK) {
if (cancel) {
// The abort comes from the request being cancelled programatically
Progress dummyprogress(0, 0, 0, 0);
Progress dummyprogress(0, 0, 0, 0, std::string());
bool cancel = true;
if (progressfn) { progressfn(dummyprogress, cancel); }
} else {
@ -438,6 +444,12 @@ Http& Http::size_limit(size_t sizeLimit)
return *this;
}
Http& Http::set_range(const std::string& range)
{
if (p) { p->set_range(range); }
return *this;
}
Http& Http::header(std::string name, const std::string &value)
{
if (!p) { return * this; }

View File

@ -21,9 +21,10 @@ public:
size_t dlnow; // Bytes downloaded so far
size_t ultotal; // Total bytes to upload
size_t ulnow; // Bytes uploaded so far
const std::string& buffer; // reference to buffer containing all data
Progress(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) :
dltotal(dltotal), dlnow(dlnow), ultotal(ultotal), ulnow(ulnow)
Progress(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow, const std::string& buffer) :
dltotal(dltotal), dlnow(dlnow), ultotal(ultotal), ulnow(ulnow), buffer(buffer)
{}
};
@ -65,6 +66,8 @@ public:
// Sets a maximum size of the data that can be received.
// A value of zero sets the default limit, which is is 5MB.
Http& size_limit(size_t sizeLimit);
// range of donloaded bytes. example: curl_easy_setopt(curl, CURLOPT_RANGE, "0-199");
Http& set_range(const std::string& range);
// Sets a HTTP header field.
Http& header(std::string name, const std::string &value);
// Removes a header field.

View File

@ -33,7 +33,7 @@ enum class NewSlicerInstanceType {
// Start a new Slicer process instance either in a Slicer mode or in a G-code mode.
// Optionally load a 3MF, STL or a G-code on start.
static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance_type, const std::vector<wxString> paths_to_open, bool single_instance)
static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance_type, const std::vector<wxString> paths_to_open, bool single_instance, bool delete_after_load)
{
#ifdef _WIN32
wxString path;
@ -49,6 +49,9 @@ static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance
}
if (instance_type == NewSlicerInstanceType::Slicer && single_instance)
args.emplace_back(L"--single-instance");
if(delete_after_load && !paths_to_open.empty())
args.emplace_back(L"--delete-after-load=1");
args.emplace_back(nullptr);
BOOST_LOG_TRIVIAL(info) << "Trying to spawn a new slicer \"" << into_u8(path) << "\"";
// Don't call with wxEXEC_HIDE_CONSOLE, PrusaSlicer in GUI mode would just show the splash screen. It would not open the main window though, it would
@ -77,6 +80,8 @@ static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance
}
if (instance_type == NewSlicerInstanceType::Slicer && single_instance)
args.emplace_back("--single-instance");
if (delete_after_load && !paths_to_open.empty())
args.emplace_back("--delete-after-load=1");
boost::process::spawn(bin_path, args);
// boost::process::spawn() sets SIGCHLD to SIGIGN for the child process, thus if a child PrusaSlicer spawns another
// subprocess and the subrocess dies, the child PrusaSlicer will not receive information on end of subprocess
@ -121,6 +126,8 @@ static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance
}
if (instance_type == NewSlicerInstanceType::Slicer && single_instance)
args.emplace_back("--single-instance");
if (delete_after_load && !paths_to_open.empty())
args.emplace_back("--delete-after-load=1");
args.emplace_back(nullptr);
BOOST_LOG_TRIVIAL(info) << "Trying to spawn a new slicer \"" << args[0] << "\"";
if (wxExecute(const_cast<char**>(args.data()), wxEXEC_ASYNC | wxEXEC_MAKE_GROUP_LEADER) <= 0)
@ -129,26 +136,26 @@ static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance
#endif // Linux or Unix
#endif // Win32
}
static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance_type, const wxString* path_to_open, bool single_instance)
static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance_type, const wxString* path_to_open, bool single_instance, bool delete_after_load)
{
std::vector<wxString> paths;
if (path_to_open != nullptr)
paths.emplace_back(path_to_open->wc_str());
start_new_slicer_or_gcodeviewer(instance_type, paths, single_instance);
start_new_slicer_or_gcodeviewer(instance_type, paths, single_instance, delete_after_load);
}
void start_new_slicer(const wxString *path_to_open, bool single_instance)
void start_new_slicer(const wxString *path_to_open, bool single_instance/*=false*/, bool delete_after_load/*=false*/)
{
start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::Slicer, path_to_open, single_instance);
start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::Slicer, path_to_open, single_instance, delete_after_load);
}
void start_new_slicer(const std::vector<wxString>& files, bool single_instance)
void start_new_slicer(const std::vector<wxString>& files, bool single_instance/*=false*/, bool delete_after_load/*=false*/)
{
start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::Slicer, files, single_instance);
start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::Slicer, files, single_instance, delete_after_load);
}
void start_new_gcodeviewer(const wxString *path_to_open)
{
start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::GCodeViewer, path_to_open, false);
start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::GCodeViewer, path_to_open, false, false);
}
void start_new_gcodeviewer_open_file(wxWindow *parent)

View File

@ -11,8 +11,8 @@ namespace Slic3r {
namespace GUI {
// Start a new slicer instance, optionally with a file to open.
void start_new_slicer(const wxString *path_to_open = nullptr, bool single_instance = false);
void start_new_slicer(const std::vector<wxString>& files, bool single_instance = false);
void start_new_slicer(const wxString *path_to_open = nullptr, bool single_instance = false, bool delete_after_load = false);
void start_new_slicer(const std::vector<wxString>& files, bool single_instance = false, bool delete_after_load = false);
// Start a new G-code viewer instance, optionally with a file to open.
void start_new_gcodeviewer(const wxString *path_to_open = nullptr);