Tab.pm 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. package Slic3r::GUI::Tab;
  2. use strict;
  3. use warnings;
  4. use utf8;
  5. use File::Basename qw(basename);
  6. use List::Util qw(first);
  7. use Wx qw(:bookctrl :dialog :keycode :icon :id :misc :panel :sizer :treectrl :window);
  8. use Wx::Event qw(EVT_BUTTON EVT_CHOICE EVT_KEY_DOWN EVT_TREE_SEL_CHANGED);
  9. use base 'Wx::Panel';
  10. sub new {
  11. my $class = shift;
  12. my ($parent, %params) = @_;
  13. my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL);
  14. $self->{options} = []; # array of option names handled by this tab
  15. $self->{$_} = $params{$_} for qw(plater on_value_change);
  16. # horizontal sizer
  17. $self->{sizer} = Wx::BoxSizer->new(wxHORIZONTAL);
  18. $self->{sizer}->SetSizeHints($self);
  19. $self->SetSizer($self->{sizer});
  20. # left vertical sizer
  21. my $left_sizer = Wx::BoxSizer->new(wxVERTICAL);
  22. $self->{sizer}->Add($left_sizer, 0, wxEXPAND | wxLEFT | wxTOP | wxBOTTOM, 3);
  23. my $left_col_width = 150;
  24. # preset chooser
  25. {
  26. # choice menu
  27. $self->{presets_choice} = Wx::Choice->new($self, -1, wxDefaultPosition, [$left_col_width, -1], []);
  28. $self->{presets_choice}->SetFont($Slic3r::GUI::small_font);
  29. # buttons
  30. $self->{btn_save_preset} = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new("$Slic3r::var/disk.png", wxBITMAP_TYPE_PNG));
  31. $self->{btn_delete_preset} = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new("$Slic3r::var/delete.png", wxBITMAP_TYPE_PNG));
  32. $self->{btn_save_preset}->SetToolTipString("Save current " . lc($self->title));
  33. $self->{btn_delete_preset}->SetToolTipString("Delete this preset");
  34. $self->{btn_delete_preset}->Disable;
  35. ### These cause GTK warnings:
  36. ###my $box = Wx::StaticBox->new($self, -1, "Presets:", wxDefaultPosition, [$left_col_width, 50]);
  37. ###my $hsizer = Wx::StaticBoxSizer->new($box, wxHORIZONTAL);
  38. my $hsizer = Wx::BoxSizer->new(wxHORIZONTAL);
  39. $left_sizer->Add($hsizer, 0, wxEXPAND | wxBOTTOM, 5);
  40. $hsizer->Add($self->{presets_choice}, 1, wxRIGHT | wxALIGN_CENTER_VERTICAL, 3);
  41. $hsizer->Add($self->{btn_save_preset}, 0, wxALIGN_CENTER_VERTICAL);
  42. $hsizer->Add($self->{btn_delete_preset}, 0, wxALIGN_CENTER_VERTICAL);
  43. }
  44. # tree
  45. $self->{treectrl} = Wx::TreeCtrl->new($self, -1, wxDefaultPosition, [$left_col_width, -1], wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_SUNKEN | wxWANTS_CHARS);
  46. $left_sizer->Add($self->{treectrl}, 1, wxEXPAND);
  47. $self->{icons} = Wx::ImageList->new(16, 16, 1);
  48. $self->{treectrl}->AssignImageList($self->{icons});
  49. $self->{iconcount} = -1;
  50. $self->{treectrl}->AddRoot("root");
  51. $self->{pages} = [];
  52. $self->{treectrl}->SetIndent(0);
  53. EVT_TREE_SEL_CHANGED($parent, $self->{treectrl}, sub {
  54. my $page = first { $_->{title} eq $self->{treectrl}->GetItemText($self->{treectrl}->GetSelection) } @{$self->{pages}}
  55. or return;
  56. $_->Hide for @{$self->{pages}};
  57. $page->Show;
  58. $self->{sizer}->Layout;
  59. $self->Refresh;
  60. });
  61. EVT_KEY_DOWN($self->{treectrl}, sub {
  62. my ($treectrl, $event) = @_;
  63. if ($event->GetKeyCode == WXK_TAB) {
  64. $treectrl->Navigate($event->ShiftDown ? &Wx::wxNavigateBackward : &Wx::wxNavigateForward);
  65. } else {
  66. $event->Skip;
  67. }
  68. });
  69. EVT_CHOICE($parent, $self->{presets_choice}, sub {
  70. $self->on_select_preset;
  71. $self->sync_presets;
  72. });
  73. EVT_BUTTON($self, $self->{btn_save_preset}, sub {
  74. # since buttons (and choices too) don't get focus on Mac, we set focus manually
  75. # to the treectrl so that the EVT_* events are fired for the input field having
  76. # focus currently. is there anything better than this?
  77. $self->{treectrl}->SetFocus;
  78. my $preset = $self->current_preset;
  79. my $default_name = $preset->{default} ? 'Untitled' : basename($preset->{name});
  80. $default_name =~ s/\.ini$//i;
  81. my $dlg = Slic3r::GUI::SavePresetWindow->new($self,
  82. title => lc($self->title),
  83. default => $default_name,
  84. values => [ map { my $name = $_->{name}; $name =~ s/\.ini$//i; $name } @{$self->{presets}} ],
  85. );
  86. return unless $dlg->ShowModal == wxID_OK;
  87. my $file = sprintf "$Slic3r::GUI::datadir/%s/%s.ini", $self->name, $dlg->get_name;
  88. $self->config->save($file);
  89. $self->set_dirty(0);
  90. $self->load_presets;
  91. $self->{presets_choice}->SetSelection(first { basename($self->{presets}[$_]{file}) eq $dlg->get_name . ".ini" } 1 .. $#{$self->{presets}});
  92. $self->on_select_preset;
  93. $self->sync_presets;
  94. });
  95. EVT_BUTTON($self, $self->{btn_delete_preset}, sub {
  96. my $i = $self->{presets_choice}->GetSelection;
  97. return if $i == 0; # this shouldn't happen but let's trap it anyway
  98. my $res = Wx::MessageDialog->new($self, "Are you sure you want to delete the selected preset?", 'Delete Preset', wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION)->ShowModal;
  99. return unless $res == wxID_YES;
  100. if (-e $self->{presets}[$i]{file}) {
  101. unlink $self->{presets}[$i]{file};
  102. }
  103. splice @{$self->{presets}}, $i, 1;
  104. $self->set_dirty(0);
  105. $self->{presets_choice}->Delete($i);
  106. $self->{presets_choice}->SetSelection(0);
  107. $self->on_select_preset;
  108. $self->sync_presets;
  109. });
  110. $self->{config} = Slic3r::Config->new;
  111. $self->build;
  112. if ($self->hidden_options) {
  113. $self->{config}->apply(Slic3r::Config->new_from_defaults($self->hidden_options));
  114. push @{$self->{options}}, $self->hidden_options;
  115. }
  116. $self->load_presets;
  117. return $self;
  118. }
  119. sub current_preset {
  120. my $self = shift;
  121. return $self->{presets}[ $self->{presets_choice}->GetSelection ];
  122. }
  123. sub get_preset {
  124. my $self = shift;
  125. return $self->{presets}[ $_[0] ];
  126. }
  127. # propagate event to the parent
  128. sub on_value_change {
  129. my $self = shift;
  130. $self->{on_value_change}->(@_) if $self->{on_value_change};
  131. }
  132. sub on_preset_loaded {}
  133. sub hidden_options {}
  134. sub config { $_[0]->{config}->clone }
  135. sub select_default_preset {
  136. my $self = shift;
  137. $self->{presets_choice}->SetSelection(0);
  138. }
  139. sub select_preset {
  140. my $self = shift;
  141. $self->{presets_choice}->SetSelection($_[0]);
  142. $self->on_select_preset;
  143. }
  144. sub on_select_preset {
  145. my $self = shift;
  146. if (defined $self->{dirty}) {
  147. my $name = $self->{dirty} == 0 ? 'Default preset' : "Preset \"$self->{presets}[$self->{dirty}]{name}\"";
  148. my $confirm = Wx::MessageDialog->new($self, "$name has unsaved changes. Discard changes and continue anyway?",
  149. 'Unsaved Changes', wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION);
  150. if ($confirm->ShowModal == wxID_NO) {
  151. $self->{presets_choice}->SetSelection($self->{dirty});
  152. return;
  153. }
  154. $self->set_dirty(0);
  155. }
  156. my $preset = $self->current_preset;
  157. my $preset_config = $self->get_preset_config($preset);
  158. eval {
  159. local $SIG{__WARN__} = Slic3r::GUI::warning_catcher($self);
  160. foreach my $opt_key (@{$self->{options}}) {
  161. $self->{config}->set($opt_key, $preset_config->get($opt_key))
  162. if $preset_config->has($opt_key);
  163. }
  164. };
  165. Slic3r::GUI::catch_error($self);
  166. ($preset->{default} || $preset->{external})
  167. ? $self->{btn_delete_preset}->Disable
  168. : $self->{btn_delete_preset}->Enable;
  169. $self->on_preset_loaded;
  170. $self->reload_values;
  171. $self->set_dirty(0);
  172. $Slic3r::GUI::Settings->{presets}{$self->name} = $preset->{file} ? basename($preset->{file}) : '';
  173. Slic3r::GUI->save_settings;
  174. }
  175. sub get_preset_config {
  176. my $self = shift;
  177. my ($preset) = @_;
  178. if ($preset->{default}) {
  179. return Slic3r::Config->new_from_defaults(@{$self->{options}});
  180. } else {
  181. if (!-e $preset->{file}) {
  182. Slic3r::GUI::show_error($self, "The selected preset does not exist anymore ($preset->{file}).");
  183. return;
  184. }
  185. # apply preset values on top of defaults
  186. my $external_config = Slic3r::Config->load($preset->{file});
  187. my $config = Slic3r::Config->new;
  188. $config->set($_, $external_config->get($_))
  189. for grep $external_config->has($_), @{$self->{options}};
  190. return $config;
  191. }
  192. }
  193. sub add_options_page {
  194. my $self = shift;
  195. my ($title, $icon, %params) = @_;
  196. if ($icon) {
  197. my $bitmap = Wx::Bitmap->new("$Slic3r::var/$icon", wxBITMAP_TYPE_PNG);
  198. $self->{icons}->Add($bitmap);
  199. $self->{iconcount}++;
  200. }
  201. {
  202. # get all config options being added to the current page; remove indexes; associate defaults
  203. my @options = map { $_ =~ s/#.+//; $_ } grep !ref($_), map @{$_->{options}}, @{$params{optgroups}};
  204. my %defaults_to_set = map { $_ => 1 } @options;
  205. # apply default values for the options we don't have already
  206. delete $defaults_to_set{$_} for @{$self->{options}};
  207. $self->{config}->apply(Slic3r::Config->new_from_defaults(keys %defaults_to_set)) if %defaults_to_set;
  208. # append such options to our list
  209. push @{$self->{options}}, @options;
  210. }
  211. my $page = Slic3r::GUI::Tab::Page->new($self, $title, $self->{iconcount}, %params, on_change => sub {
  212. $self->on_value_change(@_);
  213. $self->set_dirty(1);
  214. $self->sync_presets;
  215. });
  216. $page->Hide;
  217. $self->{sizer}->Add($page, 1, wxEXPAND | wxLEFT, 5);
  218. push @{$self->{pages}}, $page;
  219. $self->update_tree;
  220. return $page;
  221. }
  222. sub set_value {
  223. my $self = shift;
  224. my ($opt_key, $value) = @_;
  225. my $changed = 0;
  226. foreach my $page (@{$self->{pages}}) {
  227. $changed = 1 if $page->set_value($opt_key, $value);
  228. }
  229. return $changed;
  230. }
  231. sub reload_values {
  232. my $self = shift;
  233. $self->set_value($_, $self->{config}->get($_)) for keys %{$self->{config}};
  234. }
  235. sub update_tree {
  236. my $self = shift;
  237. my ($select) = @_;
  238. $select //= 0; #/
  239. my $rootItem = $self->{treectrl}->GetRootItem;
  240. $self->{treectrl}->DeleteChildren($rootItem);
  241. foreach my $page (@{$self->{pages}}) {
  242. my $itemId = $self->{treectrl}->AppendItem($rootItem, $page->{title}, $page->{iconID});
  243. $self->{treectrl}->SelectItem($itemId) if $self->{treectrl}->GetChildrenCount($rootItem) == $select + 1;
  244. }
  245. }
  246. sub set_dirty {
  247. my $self = shift;
  248. my ($dirty) = @_;
  249. my $selection = $self->{presets_choice}->GetSelection;
  250. my $i = $self->{dirty} // $selection; #/
  251. my $text = $self->{presets_choice}->GetString($i);
  252. if ($dirty) {
  253. $self->{dirty} = $i;
  254. if ($text !~ / \(modified\)$/) {
  255. $self->{presets_choice}->SetString($i, "$text (modified)");
  256. $self->{presets_choice}->SetSelection($selection); # http://trac.wxwidgets.org/ticket/13769
  257. }
  258. } else {
  259. $self->{dirty} = undef;
  260. $text =~ s/ \(modified\)$//;
  261. $self->{presets_choice}->SetString($i, $text);
  262. $self->{presets_choice}->SetSelection($selection); # http://trac.wxwidgets.org/ticket/13769
  263. }
  264. $self->sync_presets;
  265. }
  266. sub is_dirty {
  267. my $self = shift;
  268. return (defined $self->{dirty});
  269. }
  270. sub load_presets {
  271. my $self = shift;
  272. $self->{presets} = [{
  273. default => 1,
  274. name => '- default -',
  275. }];
  276. opendir my $dh, "$Slic3r::GUI::datadir/" . $self->name or die "Failed to read directory $Slic3r::GUI::datadir/" . $self->name . " (errno: $!)\n";
  277. foreach my $file (sort grep /\.ini$/i, readdir $dh) {
  278. my $name = basename($file);
  279. $name =~ s/\.ini$//;
  280. push @{$self->{presets}}, {
  281. file => "$Slic3r::GUI::datadir/" . $self->name . "/$file",
  282. name => $name,
  283. };
  284. }
  285. closedir $dh;
  286. $self->{presets_choice}->Clear;
  287. $self->{presets_choice}->Append($_->{name}) for @{$self->{presets}};
  288. {
  289. # load last used preset
  290. my $i = first { basename($self->{presets}[$_]{file}) eq ($Slic3r::GUI::Settings->{presets}{$self->name} || '') } 1 .. $#{$self->{presets}};
  291. $self->{presets_choice}->SetSelection($i || 0);
  292. $self->on_select_preset;
  293. }
  294. $self->sync_presets;
  295. }
  296. sub load_external_config {
  297. my $self = shift;
  298. my ($file) = @_;
  299. # look for the loaded config among the existing menu items
  300. my $i = first { $self->{presets}[$_]{file} eq $file && $self->{presets}[$_]{external} } 1..$#{$self->{presets}};
  301. if (!$i) {
  302. my $preset_name = basename($file); # keep the .ini suffix
  303. push @{$self->{presets}}, {
  304. file => $file,
  305. name => $preset_name,
  306. external => 1,
  307. };
  308. $self->{presets_choice}->Append($preset_name);
  309. $i = $#{$self->{presets}};
  310. }
  311. $self->{presets_choice}->SetSelection($i);
  312. $self->on_select_preset;
  313. $self->sync_presets;
  314. }
  315. sub sync_presets {
  316. my $self = shift;
  317. $self->{plater}->update_presets($self->name, [$self->{presets_choice}->GetStrings], $self->{presets_choice}->GetSelection);
  318. }
  319. package Slic3r::GUI::Tab::Print;
  320. use base 'Slic3r::GUI::Tab';
  321. sub name { 'print' }
  322. sub title { 'Print Settings' }
  323. sub build {
  324. my $self = shift;
  325. $self->add_options_page('Layers and perimeters', 'layers.png', optgroups => [
  326. {
  327. title => 'Layer height',
  328. options => [qw(layer_height first_layer_height)],
  329. },
  330. {
  331. title => 'Vertical shells',
  332. options => [qw(perimeters randomize_start extra_perimeters)],
  333. },
  334. {
  335. title => 'Horizontal shells',
  336. options => [qw(top_solid_layers bottom_solid_layers)],
  337. lines => [
  338. {
  339. label => 'Solid layers',
  340. options => [qw(top_solid_layers bottom_solid_layers)],
  341. },
  342. ],
  343. },
  344. ]);
  345. $self->add_options_page('Infill', 'shading.png', optgroups => [
  346. {
  347. title => 'Infill',
  348. options => [qw(fill_density fill_pattern solid_fill_pattern)],
  349. },
  350. {
  351. title => 'Advanced',
  352. options => [qw(infill_every_layers solid_infill_every_layers fill_angle solid_infill_below_area only_retract_when_crossing_perimeters)],
  353. },
  354. ]);
  355. $self->add_options_page('Speed', 'time.png', optgroups => [
  356. {
  357. title => 'Speed for print moves',
  358. options => [qw(perimeter_speed small_perimeter_speed external_perimeter_speed infill_speed solid_infill_speed top_solid_infill_speed bridge_speed)],
  359. },
  360. {
  361. title => 'Speed for non-print moves',
  362. options => [qw(travel_speed)],
  363. },
  364. {
  365. title => 'Modifiers',
  366. options => [qw(first_layer_speed)],
  367. },
  368. ]);
  369. $self->add_options_page('Skirt and brim', 'box.png', optgroups => [
  370. {
  371. title => 'Skirt',
  372. options => [qw(skirts skirt_distance skirt_height min_skirt_length)],
  373. },
  374. {
  375. title => 'Brim',
  376. options => [qw(brim_width)],
  377. },
  378. ]);
  379. $self->add_options_page('Support material', 'building.png', optgroups => [
  380. {
  381. title => 'Support material',
  382. options => [qw(support_material support_material_threshold support_material_pattern support_material_spacing support_material_angle)],
  383. },
  384. ]);
  385. $self->add_options_page('Notes', 'note.png', optgroups => [
  386. {
  387. title => 'Notes',
  388. no_labels => 1,
  389. options => [qw(notes)],
  390. },
  391. ]);
  392. $self->add_options_page('Output options', 'page_white_go.png', optgroups => [
  393. {
  394. title => 'Sequential printing',
  395. options => [qw(complete_objects extruder_clearance_radius extruder_clearance_height)],
  396. lines => [
  397. Slic3r::GUI::OptionsGroup->single_option_line('complete_objects'),
  398. {
  399. label => 'Extruder clearance (mm)',
  400. options => [qw(extruder_clearance_radius extruder_clearance_height)],
  401. },
  402. ],
  403. },
  404. {
  405. title => 'Output file',
  406. options => [qw(gcode_comments output_filename_format)],
  407. },
  408. {
  409. title => 'Post-processing scripts',
  410. no_labels => 1,
  411. options => [qw(post_process)],
  412. },
  413. ]);
  414. $self->add_options_page('Multiple Extruders', 'funnel.png', optgroups => [
  415. {
  416. title => 'Extruders',
  417. options => [qw(perimeter_extruder infill_extruder support_material_extruder)],
  418. },
  419. ]);
  420. $self->add_options_page('Advanced', 'wrench.png', optgroups => [
  421. {
  422. title => 'Extrusion width',
  423. label_width => 180,
  424. options => [qw(extrusion_width first_layer_extrusion_width perimeter_extrusion_width infill_extrusion_width support_material_extrusion_width)],
  425. },
  426. {
  427. title => 'Flow',
  428. options => [qw(bridge_flow_ratio)],
  429. },
  430. $Slic3r::have_threads ? {
  431. title => 'Other',
  432. options => [qw(threads)],
  433. } : (),
  434. ]);
  435. }
  436. sub hidden_options { !$Slic3r::have_threads ? qw(threads) : () }
  437. package Slic3r::GUI::Tab::Filament;
  438. use base 'Slic3r::GUI::Tab';
  439. sub name { 'filament' }
  440. sub title { 'Filament Settings' }
  441. sub build {
  442. my $self = shift;
  443. $self->add_options_page('Filament', 'spool.png', optgroups => [
  444. {
  445. title => 'Filament',
  446. options => ['filament_diameter#0', 'extrusion_multiplier#0'],
  447. },
  448. {
  449. title => 'Temperature (°C)',
  450. options => ['temperature#0', 'first_layer_temperature#0', qw(bed_temperature first_layer_bed_temperature)],
  451. lines => [
  452. {
  453. label => 'Extruder',
  454. options => ['first_layer_temperature#0', 'temperature#0'],
  455. },
  456. {
  457. label => 'Bed',
  458. options => [qw(first_layer_bed_temperature bed_temperature)],
  459. },
  460. ],
  461. },
  462. ]);
  463. $self->add_options_page('Cooling', 'hourglass.png', optgroups => [
  464. {
  465. title => 'Enable',
  466. options => [qw(cooling)],
  467. lines => [
  468. Slic3r::GUI::OptionsGroup->single_option_line('cooling'),
  469. {
  470. label => '',
  471. widget => ($self->{description_line} = Slic3r::GUI::OptionsGroup::StaticTextLine->new),
  472. },
  473. ],
  474. },
  475. {
  476. title => 'Fan settings',
  477. options => [qw(min_fan_speed max_fan_speed bridge_fan_speed disable_fan_first_layers fan_always_on)],
  478. lines => [
  479. {
  480. label => 'Fan speed',
  481. options => [qw(min_fan_speed max_fan_speed)],
  482. },
  483. Slic3r::GUI::OptionsGroup->single_option_line('bridge_fan_speed'),
  484. Slic3r::GUI::OptionsGroup->single_option_line('disable_fan_first_layers'),
  485. Slic3r::GUI::OptionsGroup->single_option_line('fan_always_on'),
  486. ],
  487. },
  488. {
  489. title => 'Cooling thresholds',
  490. label_width => 250,
  491. options => [qw(fan_below_layer_time slowdown_below_layer_time min_print_speed)],
  492. },
  493. ]);
  494. }
  495. sub _update_description {
  496. my $self = shift;
  497. my $config = $self->config;
  498. my $msg = "";
  499. if ($config->cooling) {
  500. $msg = sprintf "If estimated layer time is below ~%ds, fan will run at 100%% and print speed will be reduced so that no less than %ds are spent on that layer (however, speed will never be reduced below %dmm/s).",
  501. $config->slowdown_below_layer_time, $config->slowdown_below_layer_time, $config->min_print_speed;
  502. if ($config->fan_below_layer_time > $config->slowdown_below_layer_time) {
  503. $msg .= sprintf "\nIf estimated layer time is greater, but still below ~%ds, fan will run at a proportionally decreasing speed between %d%% and %d%%.",
  504. $config->fan_below_layer_time, $config->max_fan_speed, $config->min_fan_speed;
  505. }
  506. if ($config->fan_always_on) {
  507. $msg .= sprintf "\nDuring the other layers, fan will always run at %d%%.", $config->min_fan_speed;
  508. } else {
  509. $msg .= "\nDuring the other layers, fan will be turned off."
  510. }
  511. }
  512. $self->{description_line}->SetText($msg);
  513. }
  514. sub on_value_change {
  515. my $self = shift;
  516. my ($opt_key) = @_;
  517. $self->SUPER::on_value_change(@_);
  518. $self->_update_description;
  519. }
  520. package Slic3r::GUI::Tab::Printer;
  521. use base 'Slic3r::GUI::Tab';
  522. sub name { 'printer' }
  523. sub title { 'Printer Settings' }
  524. sub build {
  525. my $self = shift;
  526. $self->{extruders_count} = 1;
  527. $self->add_options_page('General', 'printer_empty.png', optgroups => [
  528. {
  529. title => 'Size and coordinates',
  530. options => [qw(bed_size print_center z_offset)],
  531. },
  532. {
  533. title => 'Firmware',
  534. options => [qw(gcode_flavor use_relative_e_distances)],
  535. },
  536. {
  537. title => 'Capabilities',
  538. options => [
  539. {
  540. opt_key => 'extruders_count',
  541. label => 'Extruders',
  542. tooltip => 'Number of extruders of the printer.',
  543. type => 'i',
  544. min => 1,
  545. default => 1,
  546. on_change => sub { $self->{extruders_count} = $_[0] },
  547. },
  548. ],
  549. },
  550. ]);
  551. $self->add_options_page('Custom G-code', 'cog.png', optgroups => [
  552. {
  553. title => 'Start G-code',
  554. no_labels => 1,
  555. options => [qw(start_gcode)],
  556. },
  557. {
  558. title => 'End G-code',
  559. no_labels => 1,
  560. options => [qw(end_gcode)],
  561. },
  562. {
  563. title => 'Layer change G-code',
  564. no_labels => 1,
  565. options => [qw(layer_gcode)],
  566. },
  567. ]);
  568. $self->{extruder_pages} = [];
  569. $self->_build_extruder_pages;
  570. }
  571. sub _extruder_options { qw(nozzle_diameter extruder_offset retract_length retract_lift retract_speed retract_restart_extra retract_before_travel
  572. retract_length_toolchange retract_restart_extra_toolchange) }
  573. sub config {
  574. my $self = shift;
  575. my $config = $self->SUPER::config(@_);
  576. # remove all unused values
  577. foreach my $opt_key ($self->_extruder_options) {
  578. splice @{ $config->{$opt_key} }, $self->{extruders_count};
  579. }
  580. return $config;
  581. }
  582. sub _build_extruder_pages {
  583. my $self = shift;
  584. foreach my $extruder_idx (0 .. $self->{extruders_count}-1) {
  585. # build page if it doesn't exist
  586. $self->{extruder_pages}[$extruder_idx] ||= $self->add_options_page("Extruder " . ($extruder_idx + 1), 'funnel.png', optgroups => [
  587. {
  588. title => 'Size',
  589. options => ['nozzle_diameter#' . $extruder_idx],
  590. },
  591. {
  592. title => 'Position (for multi-extruder printers)',
  593. options => ['extruder_offset#' . $extruder_idx],
  594. },
  595. {
  596. title => 'Retraction',
  597. options => [
  598. map "${_}#${extruder_idx}",
  599. qw(retract_length retract_lift retract_speed retract_restart_extra retract_before_travel)
  600. ],
  601. },
  602. {
  603. title => 'Retraction when tool is disabled (advanced settings for multi-extruder setups)',
  604. options => [
  605. map "${_}#${extruder_idx}",
  606. qw(retract_length_toolchange retract_restart_extra_toolchange)
  607. ],
  608. },
  609. ]);
  610. $self->{extruder_pages}[$extruder_idx]{disabled} = 0;
  611. }
  612. # rebuild page list
  613. @{$self->{pages}} = (
  614. (grep $_->{title} !~ /^Extruder \d+/, @{$self->{pages}}),
  615. @{$self->{extruder_pages}}[ 0 .. $self->{extruders_count}-1 ],
  616. );
  617. }
  618. sub on_value_change {
  619. my $self = shift;
  620. my ($opt_key) = @_;
  621. $self->SUPER::on_value_change(@_);
  622. if ($opt_key eq 'extruders_count') {
  623. # remove unused pages from list
  624. my @unused_pages = @{ $self->{extruder_pages} }[$self->{extruders_count} .. $#{$self->{extruder_pages}}];
  625. for my $page (@unused_pages) {
  626. @{$self->{pages}} = grep $_ ne $page, @{$self->{pages}};
  627. $page->{disabled} = 1;
  628. }
  629. # add extra pages
  630. $self->_build_extruder_pages;
  631. # update page list and select first page (General)
  632. $self->update_tree(0);
  633. }
  634. }
  635. # this gets executed after preset is loaded and before GUI fields are updated
  636. sub on_preset_loaded {
  637. my $self = shift;
  638. # update the extruders count field
  639. {
  640. # update the GUI field according to the number of nozzle diameters supplied
  641. $self->set_value('extruders_count', scalar @{ $self->{config}->nozzle_diameter });
  642. # update extruder page list
  643. $self->on_value_change('extruders_count');
  644. }
  645. }
  646. sub load_external_config {
  647. my $self = shift;
  648. $self->SUPER::load_external_config(@_);
  649. Slic3r::GUI::warning_catcher($self)->(
  650. "Your configuration was imported. However, Slic3r is currently only able to import settings "
  651. . "for the first defined filament. We recommend you don't use exported configuration files "
  652. . "for multi-extruder setups and rely on the built-in preset management system instead.")
  653. if @{ $self->{config}->nozzle_diameter } > 1;
  654. }
  655. package Slic3r::GUI::Tab::Page;
  656. use Wx qw(:misc :panel :sizer);
  657. use base 'Wx::ScrolledWindow';
  658. sub new {
  659. my $class = shift;
  660. my ($parent, $title, $iconID, %params) = @_;
  661. my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
  662. $self->{optgroups} = [];
  663. $self->{title} = $title;
  664. $self->{iconID} = $iconID;
  665. $self->SetScrollbars(1, 1, 1, 1);
  666. $self->{vsizer} = Wx::BoxSizer->new(wxVERTICAL);
  667. $self->SetSizer($self->{vsizer});
  668. if ($params{optgroups}) {
  669. $self->append_optgroup(
  670. %$_,
  671. config => $parent->{config},
  672. on_change => $params{on_change},
  673. ) for @{$params{optgroups}};
  674. }
  675. return $self;
  676. }
  677. sub append_optgroup {
  678. my $self = shift;
  679. my %params = @_;
  680. my $class = $params{class} || 'Slic3r::GUI::ConfigOptionsGroup';
  681. my $optgroup = $class->new(
  682. parent => $self,
  683. config => $self->GetParent->{config},
  684. label_width => 200,
  685. %params,
  686. );
  687. $self->{vsizer}->Add($optgroup->sizer, 0, wxEXPAND | wxALL, 5);
  688. push @{$self->{optgroups}}, $optgroup;
  689. }
  690. sub set_value {
  691. my $self = shift;
  692. my ($opt_key, $value) = @_;
  693. my $changed = 0;
  694. foreach my $optgroup (@{$self->{optgroups}}) {
  695. $changed = 1 if $optgroup->set_value($opt_key, $value);
  696. }
  697. return $changed;
  698. }
  699. package Slic3r::GUI::SavePresetWindow;
  700. use Wx qw(:combobox :dialog :id :misc :sizer);
  701. use Wx::Event qw(EVT_BUTTON EVT_TEXT_ENTER);
  702. use base 'Wx::Dialog';
  703. sub new {
  704. my $class = shift;
  705. my ($parent, %params) = @_;
  706. my $self = $class->SUPER::new($parent, -1, "Save preset", wxDefaultPosition, wxDefaultSize);
  707. my $text = Wx::StaticText->new($self, -1, "Save " . lc($params{title}) . " as:", wxDefaultPosition, wxDefaultSize);
  708. $self->{combo} = Wx::ComboBox->new($self, -1, $params{default}, wxDefaultPosition, wxDefaultSize, $params{values},
  709. wxTE_PROCESS_ENTER);
  710. my $buttons = $self->CreateStdDialogButtonSizer(wxOK | wxCANCEL);
  711. my $sizer = Wx::BoxSizer->new(wxVERTICAL);
  712. $sizer->Add($text, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 10);
  713. $sizer->Add($self->{combo}, 0, wxEXPAND | wxLEFT | wxRIGHT, 10);
  714. $sizer->Add($buttons, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
  715. EVT_BUTTON($self, wxID_OK, \&accept);
  716. EVT_TEXT_ENTER($self, $self->{combo}, \&accept);
  717. $self->SetSizer($sizer);
  718. $sizer->SetSizeHints($self);
  719. return $self;
  720. }
  721. sub accept {
  722. my ($self, $event) = @_;
  723. if (($self->{chosen_name} = $self->{combo}->GetValue)) {
  724. if ($self->{chosen_name} =~ /^[^<>:\/\\|?*\"]+$/i) {
  725. $self->EndModal(wxID_OK);
  726. } else {
  727. Slic3r::GUI::show_error($self, "The supplied name is not valid; the following characters are not allowed: <>:/\|?*\"");
  728. }
  729. }
  730. }
  731. sub get_name {
  732. my $self = shift;
  733. return $self->{chosen_name};
  734. }
  735. 1;