From 04b67f0cb0ae647933e855bed9dc36b92e206413 Mon Sep 17 00:00:00 2001 From: Alessandro Ranellucci Date: Tue, 1 Jul 2014 16:40:56 +0200 Subject: [PATCH] Refactored OptionsGroup class for cleaner OOP model and cleaner event model --- lib/Slic3r/GUI.pm | 1 + lib/Slic3r/GUI/BedShapeDialog.pm | 79 +- lib/Slic3r/GUI/MainFrame.pm | 86 +- lib/Slic3r/GUI/OptionsGroup.pm | 743 ++++++--------- lib/Slic3r/GUI/OptionsGroup/Field.pm | 405 +++++++++ lib/Slic3r/GUI/Plater.pm | 110 ++- lib/Slic3r/GUI/Plater/ObjectCutDialog.pm | 111 ++- .../GUI/Plater/OverrideSettingsPanel.pm | 27 +- lib/Slic3r/GUI/Preferences.pm | 94 +- lib/Slic3r/GUI/SimpleTab.pm | 276 +++--- lib/Slic3r/GUI/Tab.pm | 845 ++++++++++-------- xs/src/PrintConfig.cpp | 7 +- 12 files changed, 1609 insertions(+), 1175 deletions(-) create mode 100644 lib/Slic3r/GUI/OptionsGroup/Field.pm diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index f152c4154..65ba7a36a 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -20,6 +20,7 @@ use Slic3r::GUI::Plater::OverrideSettingsPanel; use Slic3r::GUI::Preferences; use Slic3r::GUI::ProgressStatusBar; use Slic3r::GUI::OptionsGroup; +use Slic3r::GUI::OptionsGroup::Field; use Slic3r::GUI::SimpleTab; use Slic3r::GUI::Tab; diff --git a/lib/Slic3r/GUI/BedShapeDialog.pm b/lib/Slic3r/GUI/BedShapeDialog.pm index 010b9d0f7..6e3805716 100644 --- a/lib/Slic3r/GUI/BedShapeDialog.pm +++ b/lib/Slic3r/GUI/BedShapeDialog.pm @@ -65,48 +65,51 @@ sub new { $sbsizer->Add($self->{shape_options_book}); $self->{optgroups} = []; - $self->_init_shape_options_page('Rectangular', [ - { - opt_key => 'rect_size', + { + my $optgroup = $self->_init_shape_options_page('Rectangular'); + $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'rect_size', type => 'point', label => 'Size', tooltip => 'Size in X and Y of the rectangular plate.', default => [200,200], - }, - { - opt_key => 'rect_origin', + )); + $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'rect_origin', type => 'select', label => 'Origin', tooltip => 'Position of the 0,0 point.', labels => ['Front left corner','Center'], values => ['corner','center'], default => 'corner', - }, - ]); - $self->_init_shape_options_page('Circular', [ - { - opt_key => 'diameter', + )); + $optgroup->on_change->($_) for qw(rect_size rect_origin); # set defaults + } + { + my $optgroup = $self->_init_shape_options_page('Circular'); + $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'diameter', type => 'f', label => 'Diameter', tooltip => 'Diameter of the print bed. It is assumed that origin (0,0) is located in the center.', sidetext => 'mm', default => 200, - }, - ]); - - my $custom = sub { - my ($parent) = @_; - - my $btn = Wx::Button->new($parent, -1, "Load shape from STL...", wxDefaultPosition, wxDefaultSize); - EVT_BUTTON($self, $btn, sub { $self->_load_stl }); - return $btn; - }; - $self->_init_shape_options_page('Custom', [], [ - { - full_width => 1, - widget => $custom, - }, - ]); + )); + $optgroup->on_change->($_) for qw(diameter); # set defaults + } + { + my $optgroup = $self->_init_shape_options_page('Custom'); + $optgroup->append_line(Slic3r::GUI::OptionsGroup::Line->new( + full_width => 1, + widget => sub { + my ($parent) = @_; + + my $btn = Wx::Button->new($parent, -1, "Load shape from STL...", wxDefaultPosition, wxDefaultSize); + EVT_BUTTON($self, $btn, sub { $self->_load_stl }); + return $btn; + } + )); + } EVT_CHOICEBOOK_PAGE_CHANGED($self, -1, sub { $self->_update_shape; @@ -229,26 +232,24 @@ sub _update_preview { } sub _init_shape_options_page { - my ($self, $title, $options, $lines) = @_; - - my $on_change = sub { - my ($opt_key, $value) = @_; - $self->{"_$opt_key"} = $value; - $self->_update_shape; - }; + my ($self, $title) = @_; my $panel = Wx::Panel->new($self->{shape_options_book}); - push @{$self->{optgroups}}, my $optgroup = Slic3r::GUI::OptionsGroup->new( + my $optgroup; + push @{$self->{optgroups}}, $optgroup = Slic3r::GUI::OptionsGroup->new( parent => $panel, title => 'Settings', - options => $options, - $lines ? (lines => $lines) : (), - on_change => $on_change, label_width => 100, + on_change => sub { + my ($opt_id) = @_; + $self->{"_$opt_id"} = $optgroup->get_value($opt_id); + $self->_update_shape; + }, ); - $on_change->($_->{opt_key}, $_->{default}) for @$options; # populate with defaults $panel->SetSizerAndFit($optgroup->sizer); $self->{shape_options_book}->AddPage($panel, $title); + + return $optgroup; } sub _load_stl { diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index 1b047ee4f..103b6c160 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -69,8 +69,9 @@ sub _init_tabpanel { $self->{tabpanel} = my $panel = Wx::Notebook->new($self, -1, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL); - $panel->AddPage($self->{plater} = Slic3r::GUI::Plater->new($panel), "Plater") - unless $self->{no_plater}; + if (!$self->{no_plater}) { + $panel->AddPage($self->{plater} = Slic3r::GUI::Plater->new($panel), "Plater"); + } $self->{options_tabs} = {}; my $simple_config; @@ -82,31 +83,44 @@ sub _init_tabpanel { my $class_prefix = $self->{mode} eq 'simple' ? "Slic3r::GUI::SimpleTab::" : "Slic3r::GUI::Tab::"; for my $tab_name (qw(print filament printer)) { my $tab; - $tab = $self->{options_tabs}{$tab_name} = ($class_prefix . ucfirst $tab_name)->new( - $panel, - on_value_change => sub { - $self->{plater}->on_config_change(@_) if $self->{plater}; # propagate config change events to the plater - if ($self->{loaded}) { # don't save while loading for the first time - if ($self->{mode} eq 'simple') { - # save config - $self->config->save("$Slic3r::GUI::datadir/simple.ini"); - - # save a copy into each preset section - # so that user gets the config when switching to expert mode - $tab->config->save(sprintf "$Slic3r::GUI::datadir/%s/%s.ini", $tab->name, 'Simple Mode'); - $Slic3r::GUI::Settings->{presets}{$tab->name} = 'Simple Mode.ini'; - wxTheApp->save_settings; - } - $self->config->save($Slic3r::GUI::autosave) if $Slic3r::GUI::autosave; + $tab = $self->{options_tabs}{$tab_name} = ($class_prefix . ucfirst $tab_name)->new($panel); + $tab->on_value_change(sub { + my $config = $tab->config; + $self->{plater}->on_config_change($config) if $self->{plater}; # propagate config change events to the plater + if ($self->{loaded}) { # don't save while loading for the first time + if ($self->{mode} eq 'simple') { + # save config + $self->config->save("$Slic3r::GUI::datadir/simple.ini"); + + # save a copy into each preset section + # so that user gets the config when switching to expert mode + $config->save(sprintf "$Slic3r::GUI::datadir/%s/%s.ini", $tab->name, 'Simple Mode'); + $Slic3r::GUI::Settings->{presets}{$tab->name} = 'Simple Mode.ini'; + wxTheApp->save_settings; } - }, - on_presets_changed => sub { - $self->{plater}->update_presets($tab_name, @_) if $self->{plater}; - }, - ); + $self->config->save($Slic3r::GUI::autosave) if $Slic3r::GUI::autosave; + } + }); + $tab->on_presets_changed(sub { + if ($self->{plater}) { + $self->{plater}->update_presets($tab_name, @_); + $self->{plater}->on_config_change($tab->config); + } + }); + $tab->load_presets; $panel->AddPage($tab, $tab->title); $tab->load_config($simple_config) if $simple_config; } + + if ($self->{plater}) { + $self->{plater}->on_select_preset(sub { + my ($group, $preset) = @_; + $self->{options_tabs}{$group}->select_preset($preset); + }); + + # load initial config + $self->{plater}->on_config_change($self->config); + } } sub _init_menubar { @@ -155,11 +169,11 @@ sub _init_menubar { $fileMenu->AppendSeparator(); $self->_append_menu_item($fileMenu, "Preferences…", 'Application preferences', sub { Slic3r::GUI::Preferences->new($self)->ShowModal; - }); + }, wxID_PREFERENCES); $fileMenu->AppendSeparator(); $self->_append_menu_item($fileMenu, "&Quit", 'Quit Slic3r', sub { $self->Close(0); - }); + }, wxID_EXIT); } # Plater menu @@ -547,7 +561,7 @@ sub load_config { my ($config) = @_; foreach my $tab (values %{$self->{options_tabs}}) { - $tab->set_value($_, $config->$_) for @{$config->get_keys}; + $tab->load_config($config); } } @@ -636,6 +650,11 @@ This method collects all config values from the tabs and merges them into a sing sub config { my $self = shift; + return Slic3r::Config->new_from_defaults + if !exists $self->{options_tabs}{print} + || !exists $self->{options_tabs}{filament} + || !exists $self->{options_tabs}{printer}; + # retrieve filament presets and build a single config object for them my $filament_config; if (!$self->{plater} || $self->{plater}->filament_presets == 1 || $self->{mode} eq 'simple') { @@ -682,17 +701,6 @@ sub config { return $config; } -sub set_value { - my $self = shift; - my ($opt_key, $value) = @_; - - my $changed = 0; - foreach my $tab (values %{$self->{options_tabs}}) { - $changed = 1 if $tab->set_value($opt_key, $value); - } - return $changed; -} - sub check_unsaved_changes { my $self = shift; @@ -713,9 +721,9 @@ sub select_tab { } sub _append_menu_item { - my ($self, $menu, $string, $description, $cb) = @_; + my ($self, $menu, $string, $description, $cb, $id) = @_; - my $id = &Wx::NewId(); + $id //= &Wx::NewId(); my $item = $menu->Append($id, $string, $description); EVT_MENU($self, $id, $cb); return $item; diff --git a/lib/Slic3r/GUI/OptionsGroup.pm b/lib/Slic3r/GUI/OptionsGroup.pm index 53ef59fcf..6febca49c 100644 --- a/lib/Slic3r/GUI/OptionsGroup.pm +++ b/lib/Slic3r/GUI/OptionsGroup.pm @@ -5,61 +5,19 @@ use List::Util qw(first); use Wx qw(:combobox :font :misc :sizer :systemsettings :textctrl wxTheApp); use Wx::Event qw(EVT_CHECKBOX EVT_COMBOBOX EVT_SPINCTRL EVT_TEXT EVT_KILL_FOCUS EVT_SLIDER); -=head1 NAME - -Slic3r::GUI::OptionsGroup - pre-filled Wx::StaticBoxSizer wrapper containing one or more options - -=head1 SYNOPSIS - - my $optgroup = Slic3r::GUI::OptionsGroup->new( - parent => $self->parent, - title => 'Layers', - options => [ - { - opt_key => 'layer_height', # mandatory - type => 'f', # mandatory - label => 'Layer height', - tooltip => 'This setting controls the height (and thus the total number) of the slices/layers.', - sidetext => 'mm', - width => 200, - full_width => 0, - height => 50, - min => 0, - max => 100, - labels => [], - values => [], - default => 0.4, # mandatory - readonly => 0, - on_change => sub { print "new value is $_[0]\n" }, - }, - ], - on_change => sub { print "new value for $_[0] is $_[1]\n" }, - no_labels => 0, - label_width => 180, - extra_column => sub { ... }, - ); - $sizer->Add($optgroup->sizer); - -=cut - has 'parent' => (is => 'ro', required => 1); has 'title' => (is => 'ro', required => 1); -has 'options' => (is => 'ro', required => 1, trigger => 1); -has 'lines' => (is => 'lazy'); -has 'on_change' => (is => 'ro', default => sub { sub {} }); -has 'no_labels' => (is => 'ro', default => sub { 0 }); +has 'on_change' => (is => 'rw', default => sub { sub {} }); has 'staticbox' => (is => 'ro', default => sub { 1 }); -has 'label_width' => (is => 'ro', default => sub { 180 }); -has 'extra_column' => (is => 'ro'); -has 'label_font' => (is => 'ro'); -has 'sidetext_font' => (is => 'ro', default => sub { Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT) }); -has 'ignore_on_change_return' => (is => 'ro', default => sub { 1 }); - +has 'label_width' => (is => 'rw', default => sub { 180 }); +has 'extra_column' => (is => 'rw', default => sub { undef }); +has 'label_font' => (is => 'rw'); +has 'sidetext_font' => (is => 'rw', default => sub { Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT) }); has 'sizer' => (is => 'rw'); -has '_triggers' => (is => 'ro', default => sub { {} }); -has '_setters' => (is => 'ro', default => sub { {} }); - -sub _trigger_options {} +has '_disabled' => (is => 'rw', default => sub { 0 }); +has '_grid_sizer' => (is => 'rw'); +has '_options' => (is => 'ro', default => sub { {} }); +has '_fields' => (is => 'ro', default => sub { {} }); sub BUILD { my $self = shift; @@ -71,487 +29,354 @@ sub BUILD { $self->sizer(Wx::BoxSizer->new(wxVERTICAL)); } - my $num_columns = $self->extra_column ? 3 : 2; - my $grid_sizer = Wx::FlexGridSizer->new(scalar(@{$self->options}), $num_columns, 0, 0); - $grid_sizer->SetFlexibleDirection(wxHORIZONTAL); - $grid_sizer->AddGrowableCol($self->no_labels ? 0 : 1); + my $num_columns = 1; + ++$num_columns if $self->label_width != 0; + ++$num_columns if $self->extra_column; + $self->_grid_sizer(Wx::FlexGridSizer->new(0, $num_columns, 0, 0)); + $self->_grid_sizer->SetFlexibleDirection(wxHORIZONTAL); + $self->_grid_sizer->AddGrowableCol($self->label_width != 0); # TODO: border size may be related to wxWidgets 2.8.x vs. 2.9.x instead of wxMAC specific - $self->sizer->Add($grid_sizer, 0, wxEXPAND | wxALL, &Wx::wxMAC ? 0 : 5); + $self->sizer->Add($self->_grid_sizer, 0, wxEXPAND | wxALL, &Wx::wxMAC ? 0 : 5); +} + +# this method accepts a Slic3r::GUI::OptionsGroup::Line object +sub append_line { + my ($self, $line) = @_; - foreach my $line (@{$self->lines}) { - # build default callbacks in case we don't call _build_line() below - foreach my $opt_key (@{$line->{options}}) { - my $opt = first { $_->{opt_key} eq $opt_key } @{$self->options}; - $self->_setters->{$opt_key} //= sub {}; - $self->_triggers->{$opt_key} = $opt->{on_change} || sub { return 1 }; - } - - if ($line->{sizer}) { - $self->sizer->Add($line->{sizer}, 0, wxEXPAND | wxALL, &Wx::wxMAC ? 0 : 15); - } elsif ($line->{widget} && $line->{full_width}) { - my $sizer = $line->{widget}->($self->parent); - $self->sizer->Add($sizer, 0, wxEXPAND | wxALL, &Wx::wxMAC ? 0 : 15); - } else { - $self->_build_line($line, $grid_sizer); - } + if ($line->sizer || ($line->widget && $line->full_width)) { + # full-width widgets are appended *after* the grid sizer, so after all the non-full-width lines + my $sizer = $line->sizer // $line->widget->($self->parent); + $self->sizer->Add($sizer, 0, wxEXPAND | wxALL, &Wx::wxMAC ? 0 : 15); + return; } -} - -# default behavior: one option per line -sub _build_lines { - my $self = shift; - my $lines = []; - foreach my $opt (@{$self->options}) { - push @$lines, { - label => $opt->{label}, - sidetext => $opt->{sidetext}, - full_width => $opt->{full_width}, - options => [$opt->{opt_key}], - }; - } - return $lines; -} - -sub single_option_line { - my $class = shift; - my ($opt_key) = @_; - - return { - label => $Slic3r::Config::Options->{$opt_key}{label}, - sidetext => $Slic3r::Config::Options->{$opt_key}{sidetext}, - options => [$opt_key], - }; -} - -sub _build_line { - my $self = shift; - my ($line, $grid_sizer) = @_; + my $grid_sizer = $self->_grid_sizer; + # if we have an extra column, build it if ($self->extra_column) { if (defined (my $item = $self->extra_column->($line))) { $grid_sizer->Add($item, 0, wxALIGN_CENTER_VERTICAL, 0); } else { + # if the callback provides no sizer for the extra cell, put a spacer $grid_sizer->AddSpacer(1); } } + # build label if we have it my $label; - if (!$self->no_labels) { - $label = Wx::StaticText->new($self->parent, -1, $line->{label} ? "$line->{label}:" : "", wxDefaultPosition, [$self->label_width, -1]); + if ($self->label_width != 0) { + $label = Wx::StaticText->new($self->parent, -1, $line->label ? $line->label . ":" : "", wxDefaultPosition, [$self->label_width, -1]); $label->SetFont($self->label_font) if $self->label_font; $label->Wrap($self->label_width) ; # needed to avoid Linux/GTK bug $grid_sizer->Add($label, 0, wxALIGN_CENTER_VERTICAL, 0); - $label->SetToolTipString($line->{tooltip}) if $line->{tooltip}; + $label->SetToolTipString($line->label_tooltip) if $line->label_tooltip; } - my @fields = (); - my @field_labels = (); - foreach my $opt_key (@{$line->{options}}) { - my $opt = first { $_->{opt_key} eq $opt_key } @{$self->options}; - push @fields, $self->_build_field($opt) unless $line->{widget}; - push @field_labels, $opt->{label}; + # if we have a widget, add it to the sizer + if ($line->widget) { + my $widget_sizer = $line->widget->($self->parent); + $grid_sizer->Add($widget_sizer, 0, wxEXPAND | wxALL, &Wx::wxMAC ? 0 : 15); + return; } - if (@fields > 1 || $line->{widget} || $line->{sidetext}) { - my $sizer = Wx::BoxSizer->new(wxHORIZONTAL); - for my $i (0 .. $#fields) { - if (@fields > 1 && $field_labels[$i]) { - my $field_label = Wx::StaticText->new($self->parent, -1, "$field_labels[$i]:", wxDefaultPosition, wxDefaultSize); - $field_label->SetFont($self->sidetext_font); - $sizer->Add($field_label, 0, wxALIGN_CENTER_VERTICAL, 0); - } - $sizer->Add($fields[$i], 0, wxALIGN_CENTER_VERTICAL, 0); + + # if we have a single option with no sidetext just add it directly to the grid sizer + my @options = @{$line->get_options}; + $self->_options->{$_->opt_id} = $_ for @options; + if (@options == 1 && !$options[0]->sidetext) { + my $option = $options[0]; + my $field = $self->_build_field($option); + $grid_sizer->Add($field, 0, ($option->full_width ? wxEXPAND : 0) | wxALIGN_CENTER_VERTICAL, 0); + return; + } + + # if we're here, we have more than one option or a single option with sidetext + # so we need a horizontal sizer to arrange these things + my $sizer = Wx::BoxSizer->new(wxHORIZONTAL); + $grid_sizer->Add($sizer, 0, 0, 0); + + foreach my $option (@options) { + # add label if any + if ($option->label) { + my $field_label = Wx::StaticText->new($self->parent, -1, $option->label . ":", wxDefaultPosition, wxDefaultSize); + $field_label->SetFont($self->sidetext_font); + $sizer->Add($field_label, 0, wxALIGN_CENTER_VERTICAL, 0); } - if ($line->{widget}) { - my $widget_sizer = $line->{widget}->($self->parent); - $sizer->Add($widget_sizer, 0, wxEXPAND | wxALL, &Wx::wxMAC ? 0 : 15); - } elsif ($line->{sidetext}) { - my $sidetext = Wx::StaticText->new($self->parent, -1, $line->{sidetext}, wxDefaultPosition, wxDefaultSize); + + # add field + my $field = $self->_build_field($option); + $sizer->Add($field, 0, wxALIGN_CENTER_VERTICAL, 0); + + # add sidetext if any + if ($option->sidetext) { + my $sidetext = Wx::StaticText->new($self->parent, -1, $option->sidetext, wxDefaultPosition, wxDefaultSize); $sidetext->SetFont($self->sidetext_font); $sizer->Add($sidetext, 0, wxLEFT | wxALIGN_CENTER_VERTICAL , 4); } - $grid_sizer->Add($sizer); - } else { - $grid_sizer->Add($fields[0], 0, ($line->{full_width} ? wxEXPAND : 0) | wxALIGN_CENTER_VERTICAL, 0); } } +sub append_single_option_line { + my ($self, $option) = @_; + + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => $option->label, + label_tooltip => $option->tooltip, + ); + $option->label(""); + $line->append_option($option); + $self->append_line($line); + + return $line; +} + sub _build_field { my $self = shift; my ($opt) = @_; - my $opt_key = $opt->{opt_key}; - + my $opt_id = $opt->opt_id; + my $on_change = sub { + my ($opt_id, $value) = @_; + $self->_on_change($opt_id, $value) + unless $self->_disabled; + }; my $on_kill_focus = sub { - my ($s, $event) = @_; - - # Without this, there will be nasty focus bugs on Windows. - # Also, docs for wxEvent::Skip() say "In general, it is recommended to skip all - # non-command events to allow the default handling to take place." - $event->Skip(1); - - $self->on_kill_focus($opt_key); + my ($opt_id) = @_; + $self->_on_kill_focus($opt_id); }; - my $field; - my $tooltip = $opt->{tooltip}; my $type = $opt->{gui_type} || $opt->{type}; - if ($type =~ /^(i|i_enum_open|i_enum_closed|f|s|s@|percent|slider)$/) { - my $style = 0; - $style = wxTE_MULTILINE if $opt->{multiline}; - # default width on Windows is too large - my $size = Wx::Size->new($opt->{width} || 60, $opt->{height} || -1); - - my $on_change = sub { - my $value = $field->GetValue; - $value ||= 0 if $type =~ /^(i|i_enum_open|i_enum_closed|f|percent)$/; # prevent crash trying to pass empty strings to Config - $self->_on_change($opt_key, $value); - }; - if ($type eq 'i') { - $field = Wx::SpinCtrl->new($self->parent, -1, $opt->{default}, wxDefaultPosition, $size, $style, $opt->{min} || 0, $opt->{max} || 2147483647, $opt->{default}); - $self->_setters->{$opt_key} = sub { $field->SetValue($_[0]) }; - EVT_SPINCTRL ($self->parent, $field, $on_change); - EVT_TEXT ($self->parent, $field, $on_change); - EVT_KILL_FOCUS($field, $on_kill_focus); - } elsif ($type eq 'i_enum_open' || $type eq 'i_enum_closed') { - $field = Wx::ComboBox->new($self->parent, -1, $opt->{default}, wxDefaultPosition, $size, $opt->{labels} || $opt->{values}); - $self->_setters->{$opt_key} = sub { - my ($value) = @_; - - if ($opt->{gui_flags} =~ /\bshow_value\b/) { - $field->SetValue($value); - return; - } - - if ($opt->{values}) { - # check whether we have a value index - my $value_idx = first { $opt->{values}[$_] eq $value } 0..$#{$opt->{values}}; - if (defined $value_idx) { - $field->SetSelection($value_idx); - return; - } - } - if ($opt->{labels} && $value <= $#{$opt->{labels}}) { - $field->SetValue($opt->{labels}[$value]); - return; - } - $field->SetValue($value); - }; - $self->_setters->{$opt_key}->($opt->{default}); # use label if any - EVT_COMBOBOX($self->parent, $field, sub { - # Without CallAfter, the field text is not populated on Windows. - wxTheApp->CallAfter(sub { - my $value = $field->GetSelection; - my $label; - - if ($opt->{values}) { - $label = $value = $opt->{values}[$value]; - } elsif ($value <= $#{$opt->{labels}}) { - $label = $opt->{labels}[$value]; - } else { - $label = $value; - } - - $field->SetValue($label); - $self->_on_change($opt_key, $value); - }); - }); - EVT_TEXT($self->parent, $field, sub { - my ($s, $event) = @_; - $event->Skip; - wxTheApp->CallAfter(sub { - my $label = $field->GetValue; - if (defined (my $value = first { $opt->{labels}[$_] eq $label } 0..$#{$opt->{labels}})) { - if ($opt->{values}) { - $value = $opt->{values}[$value]; - } - $self->_on_change($opt_key, $value); - } elsif ($label !~ /^[0-9]+$/) { - # if typed text is not numeric, select the default value - my $default = 0; - if ($opt->{values}) { - $default = $opt->{values}[0]; - } - $self->_setters->{$opt_key}->($default); - $self->_on_change($opt_key, $default); - } else { - $self->_on_change($opt_key, $label); - } - }); - }); - } elsif ($type eq 'slider') { - my $scale = 10; - $field = Wx::BoxSizer->new(wxHORIZONTAL); - my $slider = Wx::Slider->new($self->parent, -1, ($opt->{default} // $opt->{min})*$scale, ($opt->{min} // 0)*$scale, ($opt->{max} // 100)*$scale, wxDefaultPosition, $size); - my $statictext = Wx::StaticText->new($self->parent, -1, $slider->GetValue/$scale); - $field->Add($_, 0, wxALIGN_CENTER_VERTICAL, 0) for $slider, $statictext; - $self->_setters->{$opt_key} = sub { - $field->SetValue($_[0]*$scale); - }; - EVT_SLIDER($self->parent, $slider, sub { - my $value = $slider->GetValue/$scale; - $statictext->SetLabel($value); - $self->_on_change($opt_key, $value); - }); - } else { - $field = Wx::TextCtrl->new($self->parent, -1, $opt->{default}, wxDefaultPosition, $size, $style); - # value supplied to the setter callback might be undef in case user loads a config - # that has empty string for multi-value options like 'wipe' - $self->_setters->{$opt_key} = sub { $field->ChangeValue($_[0]) if defined $_[0] }; - EVT_TEXT($self->parent, $field, $on_change); - EVT_KILL_FOCUS($field, $on_kill_focus); - } - $field->Disable if $opt->{readonly}; - $tooltip .= " (default: " . $opt->{default} . ")" if ($opt->{default}); - } elsif ($type eq 'bool') { - $field = Wx::CheckBox->new($self->parent, -1, ""); - $field->SetValue($opt->{default}); - $field->Disable if $opt->{readonly}; - EVT_CHECKBOX($self->parent, $field, sub { $self->_on_change($opt_key, $field->GetValue); }); - $self->_setters->{$opt_key} = sub { $field->SetValue($_[0]) }; - $tooltip .= " (default: " . ($opt->{default} ? 'yes' : 'no') . ")" if defined($opt->{default}); - } elsif ($type eq 'point') { - $field = Wx::BoxSizer->new(wxHORIZONTAL); - my $field_size = Wx::Size->new(40, -1); - my @items = ( - Wx::StaticText->new($self->parent, -1, "x:"), - my $x_field = Wx::TextCtrl->new($self->parent, -1, $opt->{default}->[0], wxDefaultPosition, $field_size), - Wx::StaticText->new($self->parent, -1, " y:"), - my $y_field = Wx::TextCtrl->new($self->parent, -1, $opt->{default}->[1], wxDefaultPosition, $field_size), + + my $field; + if ($type eq 'bool') { + $field = Slic3r::GUI::OptionsGroup::Field::Checkbox->new( + parent => $self->parent, + option => $opt, + ); + } elsif ($type eq 'i') { + $field = Slic3r::GUI::OptionsGroup::Field::SpinCtrl->new( + parent => $self->parent, + option => $opt, + ); + } elsif ($type =~ /^(f|s|s@|percent)$/) { + $field = Slic3r::GUI::OptionsGroup::Field::TextCtrl->new( + parent => $self->parent, + option => $opt, ); - $field->Add($_, 0, wxALIGN_CENTER_VERTICAL, 0) for @items; - if ($tooltip) { - $_->SetToolTipString( - $tooltip . " (default: " . join(",", @{$opt->{default}}) . ")" - ) for @items; - } - foreach my $field ($x_field, $y_field) { - EVT_TEXT($self->parent, $field, sub { $self->_on_change($opt_key, [ $x_field->GetValue, $y_field->GetValue ]) }); - EVT_KILL_FOCUS($field, $on_kill_focus); - } - $self->_setters->{$opt_key} = sub { - $x_field->SetValue($_[0][0]); - $y_field->SetValue($_[0][1]); - }; } elsif ($type eq 'select') { - $field = Wx::ComboBox->new($self->parent, -1, "", wxDefaultPosition, wxDefaultSize, $opt->{labels} || $opt->{values}, wxCB_READONLY); - EVT_COMBOBOX($self->parent, $field, sub { - $self->_on_change($opt_key, $opt->{values}[$field->GetSelection]); - }); - $self->_setters->{$opt_key} = sub { - $field->SetSelection(grep $opt->{values}[$_] eq $_[0], 0..$#{$opt->{values}}); - }; - $self->_setters->{$opt_key}->($opt->{default}); - - $tooltip .= " (default: " - . $opt->{labels}[ first { $opt->{values}[$_] eq $opt->{default} } 0..$#{$opt->{values}} ] - . ")" if ($opt->{default}); - } else { - die "Unsupported option type: " . $type; + $field = Slic3r::GUI::OptionsGroup::Field::Choice->new( + parent => $self->parent, + option => $opt, + ); + } elsif ($type eq 'f_enum_open' || $type eq 'i_enum_open' || $type eq 'i_enum_closed') { + $field = Slic3r::GUI::OptionsGroup::Field::NumericChoice->new( + parent => $self->parent, + option => $opt, + ); + } elsif ($type eq 'point') { + $field = Slic3r::GUI::OptionsGroup::Field::Point->new( + parent => $self->parent, + option => $opt, + ); + } elsif ($type eq 'slider') { + $field = Slic3r::GUI::OptionsGroup::Field::Slider->new( + parent => $self->parent, + option => $opt, + ); } - if ($tooltip && $field->can('SetToolTipString')) { - $field->SetToolTipString($tooltip); - } - return $field; + return undef if !$field; + + $field->on_change($on_change); + $field->on_kill_focus($on_kill_focus); + $self->_fields->{$opt_id} = $field; + + return $field->isa('Slic3r::GUI::OptionsGroup::Field::wxWindow') + ? $field->wxWindow + : $field->wxSizer; } -sub _option { - my $self = shift; - my ($opt_key) = @_; +sub get_value { + my ($self, $opt_id) = @_; - return first { $_->{opt_key} eq $opt_key } @{$self->options}; + return if !exists $self->_fields->{$opt_id}; + return $self->_fields->{$opt_id}->get_value; +} + +sub set_value { + my ($self, $opt_id, $value) = @_; + + return if !exists $self->_fields->{$opt_id}; + $self->_fields->{$opt_id}->set_value($value); } sub _on_change { - my $self = shift; - my ($opt_key, $value) = @_; - - return if $self->sizer->GetStaticBox->GetParent->{disabled}; - $self->_triggers->{$opt_key}->($value) or $self->ignore_on_change_return or return; - $self->on_change->($opt_key, $value); + my ($self, $opt_id) = @_; + $self->on_change->($opt_id); } -=head2 set_value - -This method accepts an option key and a value. If this option group contains the supplied -option key, its field will be updated with the new value and the method will return a true -value, otherwise it will return false. - -=cut - -sub set_value { - my $self = shift; - my ($opt_key, $value) = @_; - - if ($self->_setters->{$opt_key}) { - $self->_setters->{$opt_key}->($value); - $self->_on_change($opt_key, $value); - return 1; - } - - return 0; +sub _on_kill_focus { + my ($self, $opt_id) = @_; + # nothing } -sub on_kill_focus {} + +package Slic3r::GUI::OptionsGroup::Line; +use Moo; + +has 'label' => (is => 'rw', default => sub { "" }); +has 'full_width' => (is => 'rw', default => sub { 0 }); +has 'label_tooltip' => (is => 'rw', default => sub { "" }); +has 'sizer' => (is => 'rw'); +has 'widget' => (is => 'rw'); +has '_options' => (is => 'ro', default => sub { [] }); + +# this method accepts a Slic3r::GUI::OptionsGroup::Option object +sub append_option { + my ($self, $option) = @_; + push @{$self->_options}, $option; +} + +sub get_options { + my ($self) = @_; + return [ @{$self->_options} ]; +} + + +package Slic3r::GUI::OptionsGroup::Option; +use Moo; + +has 'opt_id' => (is => 'rw', required => 1); +has 'type' => (is => 'rw', required => 1); +has 'default' => (is => 'rw', required => 1); +has 'gui_type' => (is => 'rw', default => sub { undef }); +has 'gui_flags' => (is => 'rw', default => sub { "" }); +has 'label' => (is => 'rw', default => sub { "" }); +has 'sidetext' => (is => 'rw', default => sub { "" }); +has 'tooltip' => (is => 'rw', default => sub { "" }); +has 'multiline' => (is => 'rw', default => sub { 0 }); +has 'full_width' => (is => 'rw', default => sub { 0 }); +has 'width' => (is => 'rw', default => sub { undef }); +has 'height' => (is => 'rw', default => sub { undef }); +has 'min' => (is => 'rw', default => sub { undef }); +has 'max' => (is => 'rw', default => sub { undef }); +has 'labels' => (is => 'rw', default => sub { [] }); +has 'values' => (is => 'rw', default => sub { [] }); +has 'readonly' => (is => 'rw', default => sub { 0 }); + package Slic3r::GUI::ConfigOptionsGroup; use Moo; extends 'Slic3r::GUI::OptionsGroup'; +has 'config' => (is => 'ro', required => 1); +has 'full_labels' => (is => 'ro', default => sub { 0 }); +has '_opt_map' => (is => 'ro', default => sub { {} }); -=head1 NAME - -Slic3r::GUI::ConfigOptionsGroup - pre-filled Wx::StaticBoxSizer wrapper containing one or more config options - -=head1 SYNOPSIS - - my $optgroup = Slic3r::GUI::ConfigOptionsGroup->new( - parent => $self->parent, - title => 'Layers', - config => $config, - options => ['layer_height'], - on_change => sub { print "new value for $_[0] is $_[1]\n" }, - no_labels => 0, - label_width => 180, +sub get_option { + my ($self, $opt_key, $opt_index) = @_; + + $opt_index //= -1; + + if (!$self->config->has($opt_key)) { + die "No $opt_key in ConfigOptionsGroup config"; + } + + my $opt_id = ($opt_index == -1 ? $opt_key : "${opt_key}#${opt_index}"); + $self->_opt_map->{$opt_id} = [ $opt_key, $opt_index ]; + + my $optdef = $Slic3r::Config::Options->{$opt_key}; # we should access this from $self->config + my $default_value = $self->_get_config_value($opt_key, $opt_index, $optdef->{gui_flags} =~ /\bserialized\b/); + + return Slic3r::GUI::OptionsGroup::Option->new( + opt_id => $opt_id, + type => $optdef->{type}, + default => $default_value, + gui_type => $optdef->{gui_type}, + gui_flags => $optdef->{gui_flags}, + label => ($self->full_labels && defined $optdef->{full_label}) ? $optdef->{full_label} : $optdef->{label}, + sidetext => $optdef->{sidetext}, + tooltip => $optdef->{tooltip} . " (default: " . $default_value . ")", + multiline => $optdef->{multiline}, + width => $optdef->{width}, + min => $optdef->{min}, + max => $optdef->{max}, + labels => $optdef->{labels}, + values => $optdef->{values}, + readonly => $optdef->{readonly}, ); - $sizer->Add($optgroup->sizer); - -=cut - -use List::Util qw(first); - -has 'config' => (is => 'ro', required => 1); -has 'full_labels' => (is => 'ro', default => sub {0}); -has '+ignore_on_change_return' => (is => 'ro', default => sub { 0 }); - -sub _trigger_options { - my $self = shift; - - $self->SUPER::_trigger_options; - @{$self->options} = map { - my $opt = $_; - if (ref $opt ne 'HASH') { - my $full_key = $opt; - my ($opt_key, $index) = $self->_split_key($full_key); - my $config_opt = $Slic3r::Config::Options->{$opt_key}; - - $opt = { - opt_key => $full_key, - config => 1, - label => ($self->full_labels && defined $config_opt->{full_label}) ? $config_opt->{full_label} : $config_opt->{label}, - (map { $_ => $config_opt->{$_} } qw(type gui_type gui_flags tooltip sidetext width height full_width min max labels values multiline readonly)), - default => $self->_get_config($opt_key, $index), - on_change => sub { return $self->_set_config($opt_key, $index, $_[0]) }, - }; - } - $opt; - } @{$self->options}; } -sub _option { - my $self = shift; - my ($opt_key) = @_; +sub append_single_option_line { + my ($self, $opt_key, $opt_index) = @_; - return first { $_->{opt_key} =~ /^\Q$opt_key\E(#.+)?$/ } @{$self->options}; -} - -sub set_value { - my $self = shift; - my ($opt_key, $value) = @_; - - my $opt = $self->_option($opt_key) or return 0; - - # if user is setting a non-config option, forward the call to the parent - if (!$opt->{config}) { - return $self->SUPER::set_value($opt_key, $value); + my $option; + if (ref($opt_key)) { + $option = $opt_key; + } else { + $option = $self->get_option($opt_key, $opt_index); } + return $self->SUPER::append_single_option_line($option); +} + +sub reload_config { + my ($self) = @_; - my $changed = 0; - foreach my $full_key (keys %{$self->_setters}) { - my ($key, $index) = $self->_split_key($full_key); + foreach my $opt_id (keys %{ $self->_opt_map }) { + my ($opt_key, $opt_index) = @{ $self->_opt_map->{$opt_id} }; + my $option = $self->_options->{$opt_id}; + $self->set_value($opt_id, $self->_get_config_value($opt_key, $opt_index, $option->gui_flags =~ /\bserialized\b/)); + } +} + +sub _get_config_value { + my ($self, $opt_key, $opt_index, $deserialize) = @_; + + if ($deserialize) { + die "Can't deserialize option indexed value" if $opt_index != -1; + return $self->config->serialize($opt_key); + } else { + return $opt_index == -1 + ? $self->config->get($opt_key) + : $self->config->get_at($opt_key, $opt_index); + } +} + +sub _on_change { + my ($self, $opt_id) = @_; + + if (exists $self->_opt_map->{$opt_id}) { + my ($opt_key, $opt_index) = @{ $self->_opt_map->{$opt_id} }; + my $option = $self->_options->{$opt_id}; - if ($key eq $opt_key) { - $self->config->set($key, $value); - $self->SUPER::set_value($full_key, $self->_get_config($key, $index)); - return 1; + # get value + my $field_value = $self->get_value($opt_id); + if ($option->gui_flags =~ /\bserialized\b/) { + die "Can't set serialized option indexed value" if $opt_index != -1; + $self->config->set_deserialize($opt_key, $field_value); + } else { + if ($opt_index == -1) { + $self->config->set($opt_key, $field_value); + } else { + my $value = $self->config->get($opt_key); + $value->[$opt_index] = $field_value; + $self->config->set($opt_key, $value); + } } } - # if we're here, we know this option but we found no setter, so we just propagate it - if ($self->config->has($opt_key)) { - $self->config->set($opt_key, $value); - $self->SUPER::set_value($opt_key, $value); - return 1; - } - return 0; + $self->SUPER::_on_change($opt_id); } -sub on_kill_focus { - my ($self, $full_key) = @_; +sub _on_kill_focus { + my ($self, $opt_id) = @_; # when a field loses focus, reapply the config value to it # (thus discarding any invalid input and reverting to the last # accepted value) - my ($key, $index) = $self->_split_key($full_key); - $self->SUPER::set_value($full_key, $self->_get_config($key, $index)); + $self->reload_config; } -sub _split_key { - my $self = shift; - my ($opt_key) = @_; - - my $index; - $opt_key =~ s/#(\d+)$// and $index = $1; - return ($opt_key, $index); -} - -sub _get_config { - my $self = shift; - my ($opt_key, $index, $config) = @_; - - my ($get_m, $serialized) = $self->_config_methods($opt_key, $index); - my $value = ($config // $self->config)->$get_m($opt_key); - if (defined $index) { - $value->[$index] //= $value->[0]; #/ - $value = $value->[$index]; - } - return $value; -} - -sub _set_config { - my $self = shift; - my ($opt_key, $index, $value) = @_; - - my ($get_m, $serialized) = $self->_config_methods($opt_key, $index); - if (defined $index) { - my $values = $self->config->$get_m($opt_key); - $values->[$index] = $value; - - # ignore set() return value - $self->config->set($opt_key, $values); - } else { - if ($serialized) { - # ignore set_deserialize() return value - return $self->config->set_deserialize($opt_key, $value); - } else { - # ignore set() return value - return $self->config->set($opt_key, $value); - } - } -} - -sub _config_methods { - my $self = shift; - my ($opt_key, $index) = @_; - - # if it's an array type but no index was specified, use the serialized version - return ($Slic3r::Config::Options->{$opt_key}{type} =~ /\@$/ && !defined $index) - ? qw(serialize 1) - : qw(get 0); -} - -package Slic3r::GUI::OptionsGroup::StaticTextLine; +package Slic3r::GUI::OptionsGroup::StaticText; use Wx qw(:misc :systemsettings); use base 'Wx::StaticText'; diff --git a/lib/Slic3r/GUI/OptionsGroup/Field.pm b/lib/Slic3r/GUI/OptionsGroup/Field.pm new file mode 100644 index 000000000..ca81d1fd4 --- /dev/null +++ b/lib/Slic3r/GUI/OptionsGroup/Field.pm @@ -0,0 +1,405 @@ +package Slic3r::GUI::OptionsGroup::Field; +use Moo; + +# This is a base class for option fields. + +has 'parent' => (is => 'ro', required => 1); +has 'option' => (is => 'ro', required => 1); # Slic3r::GUI::OptionsGroup::Option +has 'on_change' => (is => 'rw', default => sub { sub {} }); +has 'on_kill_focus' => (is => 'rw', default => sub { sub {} }); +has 'wxSsizer' => (is => 'rw'); # alternatively, wxSizer object +has 'disable_change_event' => (is => 'rw', default => sub { 0 }); + +# This method should not fire the on_change event +sub set_value { + my ($self, $value) = @_; + die "Method not implemented"; +} + +sub get_value { + my ($self) = @_; + die "Method not implemented"; +} + +sub set_tooltip { + my ($self, $tooltip) = @_; + + $self->SetToolTipString($tooltip) + if $tooltip && $self->can('SetToolTipString'); +} + +sub _on_change { + my ($self, $opt_id) = @_; + + $self->on_change->($opt_id) + unless $self->disable_change_event; +} + +sub _on_kill_focus { + my ($self, $opt_id, $s, $event) = @_; + + # Without this, there will be nasty focus bugs on Windows. + # Also, docs for wxEvent::Skip() say "In general, it is recommended to skip all + # non-command events to allow the default handling to take place." + $event->Skip(1); + + $self->on_kill_focus->($opt_id); +} + + +package Slic3r::GUI::OptionsGroup::Field::wxWindow; +use Moo; +extends 'Slic3r::GUI::OptionsGroup::Field'; + +has 'wxWindow' => (is => 'rw', trigger => 1); # wxWindow object + +sub _default_size { + my ($self) = @_; + + # default width on Windows is too large + return Wx::Size->new($self->option->width || 60, $self->option->height || -1); +} + +sub _trigger_wxWindow { + my ($self) = @_; + + $self->wxWindow->SetToolTipString($self->option->tooltip) + if $self->option->tooltip && $self->wxWindow->can('SetToolTipString'); +} + +sub set_value { + my ($self, $value) = @_; + + $self->disable_change_event(1); + $self->wxWindow->SetValue($value); + $self->disable_change_event(0); +} + +sub get_value { + my ($self) = @_; + return $self->wxWindow->GetValue; +} + + +package Slic3r::GUI::OptionsGroup::Field::Checkbox; +use Moo; +extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow'; + +use Wx qw(:misc); +use Wx::Event qw(EVT_CHECKBOX); + +sub BUILD { + my ($self) = @_; + + my $field = Wx::CheckBox->new($self->parent, -1, ""); + $self->wxWindow($field); + $field->SetValue($self->option->default); + $field->Disable if $self->option->readonly; + + EVT_CHECKBOX($self->parent, $field, sub { + $self->_on_change($self->option->opt_id); + }); +} + + +package Slic3r::GUI::OptionsGroup::Field::SpinCtrl; +use Moo; +extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow'; + +use Wx qw(:misc); +use Wx::Event qw(EVT_SPINCTRL EVT_TEXT EVT_KILL_FOCUS); + +sub BUILD { + my ($self) = @_; + + my $field = Wx::SpinCtrl->new($self->parent, -1, $self->option->default, wxDefaultPosition, $self->_default_size, + 0, $self->option->min || 0, $self->option->max || 2147483647, $self->option->default); + $self->wxWindow($field); + + EVT_SPINCTRL($self->parent, $field, sub { + $self->_on_change($self->option->opt_id); + }); + EVT_TEXT($self->parent, $field, sub { + $self->_on_change($self->option->opt_id); + }); + EVT_KILL_FOCUS($field, sub { + $self->_on_kill_focus($self->option->opt_id, @_); + }); +} + + +package Slic3r::GUI::OptionsGroup::Field::TextCtrl; +use Moo; +extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow'; + +use Wx qw(:misc :textctrl); +use Wx::Event qw(EVT_TEXT EVT_KILL_FOCUS); + +sub BUILD { + my ($self) = @_; + + my $style = 0; + $style = wxTE_MULTILINE if $self->option->multiline; + my $field = Wx::TextCtrl->new($self->parent, -1, $self->option->default, wxDefaultPosition, + $self->_default_size, $style); + $self->wxWindow($field); + + # TODO: test loading a config that has empty string for multi-value options like 'wipe' + + EVT_TEXT($self->parent, $field, sub { + $self->_on_change($self->option->opt_id); + }); + EVT_KILL_FOCUS($field, sub { + $self->_on_kill_focus($self->option->opt_id, @_); + }); +} + + +package Slic3r::GUI::OptionsGroup::Field::Choice; +use Moo; +extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow'; + +use List::Util qw(first); +use Wx qw(:misc :combobox); +use Wx::Event qw(EVT_COMBOBOX); + +sub BUILD { + my ($self) = @_; + + my $field = Wx::ComboBox->new($self->parent, -1, "", wxDefaultPosition, $self->_default_size, + $self->option->labels || $self->option->values, wxCB_READONLY); + $self->wxWindow($field); + + EVT_COMBOBOX($self->parent, $field, sub { + $self->_on_change($self->option->opt_id); + }); +} + +sub set_value { + my ($self, $value) = @_; + + my $idx = first { $self->option->values->[$_] eq $value } 0..$#{$self->option->values}; + + $self->disable_change_event(1); + $self->wxWindow->SetSelection($idx); + $self->disable_change_event(0); +} + +sub get_value { + my ($self) = @_; + return $self->option->values->[$self->wxWindow->GetSelection]; +} + + +package Slic3r::GUI::OptionsGroup::Field::NumericChoice; +use Moo; +extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow'; + +use List::Util qw(first); +use Wx qw(:misc :combobox); +use Wx::Event qw(EVT_COMBOBOX EVT_TEXT); + +sub BUILD { + my ($self) = @_; + + my $field = Wx::ComboBox->new($self->parent, -1, $self->option->default, wxDefaultPosition, $self->_default_size, + $self->option->labels || $self->option->values); + $self->wxWindow($field); + + $self->set_value($self->option->default); + + EVT_COMBOBOX($self->parent, $field, sub { + my $disable_change_event = $self->disable_change_event; + $self->disable_change_event(1); + + my $value = $field->GetSelection; + my $label; + + if ($self->option->values) { + $label = $value = $self->option->values->[$value]; + } elsif ($value <= $#{$self->option->labels}) { + $label = $self->option->labels->[$value]; + } else { + $label = $value; + } + + $field->SetValue($label); + + $self->disable_change_event($disable_change_event); + $self->_on_change($self->option->opt_id); + }); + EVT_TEXT($self->parent, $field, sub { + $self->_on_change($self->option->opt_id); + }); +} + +sub set_value { + my ($self, $value) = @_; + + $self->disable_change_event(1); + + my $field = $self->wxWindow; + if ($self->option->gui_flags =~ /\bshow_value\b/) { + $field->SetValue($value); + } else { + if ($self->option->values) { + # check whether we have a value index + my $value_idx = first { $self->option->values->[$_] eq $value } 0..$#{$self->option->values}; + if (defined $value_idx) { + $field->SetSelection($value_idx); + $self->disable_change_event(0); + return; + } + } + if ($self->option->labels && $value <= $#{$self->option->labels}) { + $field->SetValue($self->option->labels->[$value]); + $self->disable_change_event(0); + return; + } + $field->SetValue($value); + } + + $self->disable_change_event(0); +} + +sub get_value { + my ($self) = @_; + + my $label = $self->wxWindow->GetValue; + my $value_idx = first { $self->option->labels->[$_] eq $label } 0..$#{$self->option->labels}; + if (defined $value_idx) { + if ($self->option->values) { + return $self->option->values->[$value_idx]; + } + return $value_idx; + } + return $label; +} + + +package Slic3r::GUI::OptionsGroup::Field::wxSizer; +use Moo; +extends 'Slic3r::GUI::OptionsGroup::Field'; + +has 'wxSizer' => (is => 'rw'); # wxSizer object + + +package Slic3r::GUI::OptionsGroup::Field::Point; +use Moo; +extends 'Slic3r::GUI::OptionsGroup::Field::wxSizer'; + +has 'x_textctrl' => (is => 'rw'); +has 'y_textctrl' => (is => 'rw'); + +use Slic3r::Geometry qw(X Y); +use Wx qw(:misc :sizer); +use Wx::Event qw(EVT_TEXT); + +sub BUILD { + my ($self) = @_; + + my $sizer = Wx::BoxSizer->new(wxHORIZONTAL); + $self->wxSizer($sizer); + + my $field_size = Wx::Size->new(40, -1); + $self->x_textctrl(Wx::TextCtrl->new($self->parent, -1, $self->option->default->[X], wxDefaultPosition, $field_size)); + $self->y_textctrl(Wx::TextCtrl->new($self->parent, -1, $self->option->default->[Y], wxDefaultPosition, $field_size)); + + my @items = ( + Wx::StaticText->new($self->parent, -1, "x:"), + $self->x_textctrl, + Wx::StaticText->new($self->parent, -1, " y:"), + $self->y_textctrl, + ); + $sizer->Add($_, 0, wxALIGN_CENTER_VERTICAL, 0) for @items; + + if ($self->option->tooltip) { + foreach my $item (@items) { + $item->SetToolTipString($self->option->tooltip) + if $item->can('SetToolTipString'); + } + } + + EVT_TEXT($self->parent, $_, sub { + $self->_on_change($self->option->opt_id); + }) for $self->x_textctrl, $self->y_textctrl; +} + +sub set_value { + my ($self, $value) = @_; + + $self->disable_change_event(1); + $self->x_textctrl->SetValue($value->[X]); + $self->y_textctrl->SetValue($value->[Y]); + $self->disable_change_event(0); +} + +sub get_value { + my ($self) = @_; + + return [ + $self->x_textctrl->GetValue, + $self->y_textctrl->GetValue, + ]; +} + + +package Slic3r::GUI::OptionsGroup::Field::Slider; +use Moo; +extends 'Slic3r::GUI::OptionsGroup::Field::wxSizer'; + +has 'scale' => (is => 'rw', default => sub { 10 }); +has 'slider' => (is => 'rw'); +has 'statictext' => (is => 'rw'); + +use Slic3r::Geometry qw(X Y); +use Wx qw(:misc :sizer); +use Wx::Event qw(EVT_SLIDER); + +sub BUILD { + my ($self) = @_; + + my $sizer = Wx::BoxSizer->new(wxHORIZONTAL); + $self->wxSizer($sizer); + + my $slider = Wx::Slider->new( + $self->parent, -1, + ($self->option->default // $self->option->min) * $self->scale, + ($self->option->min // 0) * $self->scale, + ($self->option->max // 100) * $self->scale, + wxDefaultPosition, + [ $self->option->width // -1, $self->option->height // -1 ], + ); + $self->slider($slider); + + my $statictext = Wx::StaticText->new($self->parent, -1, $slider->GetValue/$self->scale); + $self->statictext($statictext); + + $sizer->Add($_, 0, wxALIGN_CENTER_VERTICAL, 0) for $slider, $statictext; + + EVT_SLIDER($self->parent, $slider, sub { + $self->_update_statictext; + $self->_on_change($self->option->opt_id); + }); +} + +sub set_value { + my ($self, $value) = @_; + + $self->disable_change_event(1); + $self->slider->SetValue($value); + $self->_update_statictext; + $self->disable_change_event(0); +} + +sub get_value { + my ($self) = @_; + return $self->slider->GetValue/$self->scale; +} + +sub _update_statictext { + my ($self) = @_; + $self->statictext->SetLabel($self->get_value); +} + +1; diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index 2d43c1a62..3345c4df7 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -270,7 +270,7 @@ sub new { my $choice = Wx::Choice->new($self, -1, wxDefaultPosition, [140, -1], []); $choice->SetFont($Slic3r::GUI::small_font); $self->{preset_choosers}{$group} = [$choice]; - EVT_CHOICE($choice, $choice, sub { $self->on_select_preset($group, @_) }); + EVT_CHOICE($choice, $choice, sub { $self->_on_select_preset($group, @_) }); $self->{preset_choosers_sizers}{$group} = Wx::BoxSizer->new(wxVERTICAL); $self->{preset_choosers_sizers}{$group}->Add($choice, 0, wxEXPAND | wxBOTTOM, FILAMENT_CHOOSERS_SPACING); @@ -345,13 +345,21 @@ sub new { $sizer->SetSizeHints($self); $self->SetSizer($sizer); } + return $self; } +# sets the callback sub on_select_preset { + my ($self, $cb) = @_; + $self->{on_select_preset} = $cb; +} + +sub _on_select_preset { my $self = shift; my ($group, $choice) = @_; + # if user changed filament preset, don't propagate this to the tabs if ($group eq 'filament' && @{$self->{preset_choosers}{filament}} > 1) { my @filament_presets = $self->filament_presets; $Slic3r::GUI::Settings->{presets}{filament} = $choice->GetString($filament_presets[0]) . ".ini"; @@ -360,7 +368,11 @@ sub on_select_preset { wxTheApp->save_settings; return; } - $self->GetFrame->{options_tabs}{$group}->select_preset($choice->GetSelection); + $self->{on_select_preset}->($group, $choice->GetSelection) + if $self->{on_select_preset}; + + # get new config and generate on_config_change() event for updating plater and other things + $self->on_config_change($self->GetFrame->config); } sub GetFrame { @@ -590,28 +602,28 @@ sub rotate { $angle = 0 - $angle; # rotate clockwise (be consistent with button icon) } - { - if ($axis == Z) { - my $new_angle = $model_instance->rotation + $angle; - $_->set_rotation($new_angle) for @{ $model_object->instances }; - $object->transform_thumbnail($self->{model}, $obj_idx); - } else { - # rotation around X and Y needs to be performed on mesh - # so we first apply any Z rotation - if ($model_instance->rotation != 0) { - $model_object->rotate(deg2rad($model_instance->rotation), Z); - $_->set_rotation(0) for @{ $model_object->instances }; - } - $model_object->rotate(deg2rad($angle), $axis); - $self->make_thumbnail($obj_idx); + $self->stop_background_process; + + if ($axis == Z) { + my $new_angle = $model_instance->rotation + $angle; + $_->set_rotation($new_angle) for @{ $model_object->instances }; + $object->transform_thumbnail($self->{model}, $obj_idx); + } else { + # rotation around X and Y needs to be performed on mesh + # so we first apply any Z rotation + if ($model_instance->rotation != 0) { + $model_object->rotate(deg2rad($model_instance->rotation), Z); + $_->set_rotation(0) for @{ $model_object->instances }; } - $model_object->update_bounding_box; - - # update print and start background processing - $self->stop_background_process; - $self->{print}->add_model_object($model_object, $obj_idx); - $self->schedule_background_process; + $model_object->rotate(deg2rad($angle), $axis); + $self->make_thumbnail($obj_idx); } + + $model_object->update_bounding_box; + # update print and start background processing + $self->{print}->add_model_object($model_object, $obj_idx); + $self->schedule_background_process; + $self->selection_changed; # refresh info (size etc.) $self->update; $self->{canvas}->Refresh; @@ -799,8 +811,9 @@ sub async_apply_config { if ($invalidated) { # kill current thread if any $self->stop_background_process; + $self->resume_background_process; } else { - # TODO: restore process thread + $self->resume_background_process; } # schedule a new process thread in case it wasn't running @@ -810,8 +823,6 @@ sub async_apply_config { sub start_background_process { my ($self) = @_; - $self->resume_background_process; - return if !$Slic3r::have_threads; return if !@{$self->{objects}}; return if $self->{process_thread}; @@ -1147,29 +1158,33 @@ sub update { $self->{canvas}->Refresh; } +sub on_extruders_change { + my ($self, $num_extruders) = @_; + + my $choices = $self->{preset_choosers}{filament}; + while (@$choices < $num_extruders) { + my @presets = $choices->[0]->GetStrings; + push @$choices, Wx::Choice->new($self, -1, wxDefaultPosition, [150, -1], [@presets]); + $choices->[-1]->SetFont($Slic3r::GUI::small_font); + $self->{preset_choosers_sizers}{filament}->Add($choices->[-1], 0, wxEXPAND | wxBOTTOM, FILAMENT_CHOOSERS_SPACING); + EVT_CHOICE($choices->[-1], $choices->[-1], sub { $self->_on_select_preset('filament', @_) }); + my $i = first { $choices->[-1]->GetString($_) eq ($Slic3r::GUI::Settings->{presets}{"filament_" . $#$choices} || '') } 0 .. $#presets; + $choices->[-1]->SetSelection($i || 0); + } + while (@$choices > $num_extruders) { + $self->{preset_choosers_sizers}{filament}->Remove(-1); + $choices->[-1]->Destroy; + pop @$choices; + } + $self->Layout; +} + sub on_config_change { my $self = shift; - my ($opt_key, $value) = @_; + my ($config) = @_; - if ($opt_key eq 'extruders_count' && defined $value) { - my $choices = $self->{preset_choosers}{filament}; - while (@$choices < $value) { - my @presets = $choices->[0]->GetStrings; - push @$choices, Wx::Choice->new($self, -1, wxDefaultPosition, [150, -1], [@presets]); - $choices->[-1]->SetFont($Slic3r::GUI::small_font); - $self->{preset_choosers_sizers}{filament}->Add($choices->[-1], 0, wxEXPAND | wxBOTTOM, FILAMENT_CHOOSERS_SPACING); - EVT_CHOICE($choices->[-1], $choices->[-1], sub { $self->on_select_preset('filament', @_) }); - my $i = first { $choices->[-1]->GetString($_) eq ($Slic3r::GUI::Settings->{presets}{"filament_" . $#$choices} || '') } 0 .. $#presets; - $choices->[-1]->SetSelection($i || 0); - } - while (@$choices > $value) { - $self->{preset_choosers_sizers}{filament}->Remove(-1); - $choices->[-1]->Destroy; - pop @$choices; - } - $self->Layout; - } elsif ($self->{config}->has($opt_key)) { - $self->{config}->set($opt_key, $value); + foreach my $opt_key (@{$self->{config}->diff($config)}) { + $self->{config}->set($opt_key, $config->get($opt_key)); if ($opt_key eq 'bed_shape') { $self->{canvas}->update_bed_size; $self->update; @@ -1252,6 +1267,7 @@ sub object_settings_dialog { object => $self->{objects}[$obj_idx], model_object => $model_object, ); + $self->suspend_background_process; $dlg->ShowModal; # update thumbnail since parts may have changed @@ -1261,8 +1277,12 @@ sub object_settings_dialog { # update print if ($dlg->PartsChanged || $dlg->PartSettingsChanged) { + $self->stop_background_process; + $self->resume_background_process; $self->{print}->reload_object($obj_idx); $self->schedule_background_process; + } else { + $self->resume_background_process; } } diff --git a/lib/Slic3r/GUI/Plater/ObjectCutDialog.pm b/lib/Slic3r/GUI/Plater/ObjectCutDialog.pm index 4d9819a50..6ff76b70d 100644 --- a/lib/Slic3r/GUI/Plater/ObjectCutDialog.pm +++ b/lib/Slic3r/GUI/Plater/ObjectCutDialog.pm @@ -23,73 +23,66 @@ sub new { keep_lower => 1, rotate_lower => 1, }; - my $cut_button_sizer = Wx::BoxSizer->new(wxVERTICAL); - { - $self->{btn_cut} = Wx::Button->new($self, -1, "Perform cut", wxDefaultPosition, wxDefaultSize); - $cut_button_sizer->Add($self->{btn_cut}, 0, wxALIGN_RIGHT | wxALL, 10); - } - my $optgroup = Slic3r::GUI::OptionsGroup->new( - parent => $self, - title => 'Cut', - options => [ - { - opt_key => 'z', - type => 'slider', - label => 'Z', - default => $self->{cut_options}{z}, - min => 0, - max => $self->{model_object}->bounding_box->size->z, - }, - { - opt_key => 'keep_upper', - type => 'bool', - label => 'Upper part', - default => $self->{cut_options}{keep_upper}, - }, - { - opt_key => 'keep_lower', - type => 'bool', - label => 'Lower part', - default => $self->{cut_options}{keep_lower}, - }, - { - opt_key => 'rotate_lower', - type => 'bool', - label => '', - tooltip => 'If enabled, the lower part will be rotated by 180° so that the flat cut surface lies on the print bed.', - default => $self->{cut_options}{rotate_lower}, - }, - ], - lines => [ - { - label => 'Z', - options => [qw(z)], - }, - { - label => 'Keep', - options => [qw(keep_upper keep_lower)], - }, - { - label => 'Rotate lower part', - options => [qw(rotate_lower)], - }, - { - sizer => $cut_button_sizer, - }, - ], - on_change => sub { - my ($opt_key, $value) = @_; + + my $optgroup; + $optgroup = Slic3r::GUI::OptionsGroup->new( + parent => $self, + title => 'Cut', + on_change => sub { + my ($opt_id) = @_; - $self->{cut_options}{$opt_key} = $value; - if ($opt_key eq 'z') { + my $value = $optgroup->get_value($opt_id); + $self->{cut_options}{$opt_id} = $value; + if ($opt_id eq 'z') { if ($self->{canvas}) { $self->{canvas}->SetCuttingPlane($value); $self->{canvas}->Render; } } }, - label_width => 120, + label_width => 120, ); + $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'z', + type => 'slider', + label => 'Z', + default => $self->{cut_options}{z}, + min => 0, + max => $self->{model_object}->bounding_box->size->z, + )); + { + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Keep', + ); + $line->append_option(Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'keep_upper', + type => 'bool', + label => 'Upper part', + default => $self->{cut_options}{keep_upper}, + )); + $line->append_option(Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'keep_lower', + type => 'bool', + label => 'Lower part', + default => $self->{cut_options}{keep_lower}, + )); + $optgroup->append_line($line); + } + $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'rotate_lower', + label => 'Rotate lower part upwards', + type => 'bool', + tooltip => 'If enabled, the lower part will be rotated by 180° so that the flat cut surface lies on the print bed.', + default => $self->{cut_options}{rotate_lower}, + )); + { + my $cut_button_sizer = Wx::BoxSizer->new(wxVERTICAL); + $self->{btn_cut} = Wx::Button->new($self, -1, "Perform cut", wxDefaultPosition, wxDefaultSize); + $cut_button_sizer->Add($self->{btn_cut}, 0, wxALIGN_RIGHT | wxALL, 10); + $optgroup->append_line(Slic3r::GUI::OptionsGroup::Line->new( + sizer => $cut_button_sizer, + )); + } # left pane with tree my $left_sizer = Wx::BoxSizer->new(wxVERTICAL); diff --git a/lib/Slic3r/GUI/Plater/OverrideSettingsPanel.pm b/lib/Slic3r/GUI/Plater/OverrideSettingsPanel.pm index a6b59bf84..29448db81 100644 --- a/lib/Slic3r/GUI/Plater/OverrideSettingsPanel.pm +++ b/lib/Slic3r/GUI/Plater/OverrideSettingsPanel.pm @@ -38,7 +38,7 @@ sub new { my $id = &Wx::NewId(); $menu->Append($id, $self->{option_labels}{$opt_key}); EVT_MENU($menu, $id, sub { - $self->{config}->apply($self->{default_config}->get($opt_key)); + $self->{config}->set($opt_key, $self->{default_config}->get($opt_key)); $self->update_optgroup; $self->{on_change}->() if $self->{on_change}; }); @@ -102,18 +102,18 @@ sub update_optgroup { } foreach my $category (sort keys %categories) { my $optgroup = Slic3r::GUI::ConfigOptionsGroup->new( - parent => $self, - title => $category, - config => $self->{config}, - options => [ sort @{$categories{$category}} ], - full_labels => 1, - label_font => $Slic3r::GUI::small_font, - sidetest_font => $Slic3r::GUI::small_font, - label_width => 120, - on_change => sub { $self->{on_change}->() if $self->{on_change} }, - extra_column => sub { + parent => $self, + title => $category, + config => $self->{config}, + full_labels => 1, + label_font => $Slic3r::GUI::small_font, + sidetext_font => $Slic3r::GUI::small_font, + label_width => 120, + on_change => sub { $self->{on_change}->() if $self->{on_change} }, + extra_column => sub { my ($line) = @_; - my ($opt_key) = @{$line->{options}}; # we assume that we have one option per line + + my $opt_key = $line->get_options->[0]->opt_id; # we assume that we have one option per line # disallow deleting fixed options return undef if $self->{fixed_options}{$opt_key}; @@ -128,6 +128,9 @@ sub update_optgroup { return $btn; }, ); + foreach my $opt_key (sort @{$categories{$category}}) { + $optgroup->append_single_option_line($opt_key); + } $self->{options_sizer}->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM, 0); } $self->Layout; diff --git a/lib/Slic3r/GUI/Preferences.pm b/lib/Slic3r/GUI/Preferences.pm index f9d36bbb5..8bb64fda1 100644 --- a/lib/Slic3r/GUI/Preferences.pm +++ b/lib/Slic3r/GUI/Preferences.pm @@ -4,58 +4,60 @@ use Wx::Event qw(EVT_BUTTON EVT_TEXT_ENTER); use base 'Wx::Dialog'; sub new { - my $class = shift; - my ($parent, %params) = @_; + my ($class, $parent) = @_; my $self = $class->SUPER::new($parent, -1, "Preferences", wxDefaultPosition, [500,200]); - $self->{values}; + $self->{values} = {}; - my $optgroup = Slic3r::GUI::OptionsGroup->new( + my $optgroup; + $optgroup = Slic3r::GUI::OptionsGroup->new( parent => $self, title => 'General', - options => [ - { - opt_key => 'mode', - type => 'select', - label => 'Mode', - tooltip => 'Choose between a simpler, basic mode and an expert mode with more options and more complicated interface.', - labels => ['Simple','Expert'], - values => ['simple','expert'], - default => $Slic3r::GUI::Settings->{_}{mode}, - }, - { - opt_key => 'version_check', - type => 'bool', - label => 'Check for updates', - tooltip => 'If this is enabled, Slic3r will check for updates daily and display a reminder if a newer version is available.', - default => $Slic3r::GUI::Settings->{_}{version_check} // 1, - readonly => !wxTheApp->have_version_check, - }, - { - opt_key => 'remember_output_path', - type => 'bool', - label => 'Remember output directory', - tooltip => 'If this is enabled, Slic3r will prompt the last output directory instead of the one containing the input files.', - default => $Slic3r::GUI::Settings->{_}{remember_output_path}, - }, - { - opt_key => 'autocenter', - type => 'bool', - label => 'Auto-center parts', - tooltip => 'If this is enabled, Slic3r will auto-center objects around the configured print center.', - default => $Slic3r::GUI::Settings->{_}{autocenter}, - }, - { - opt_key => 'background_processing', - type => 'bool', - label => 'Background processing', - tooltip => 'If this is enabled, Slic3r will pre-process objects as soon as they\'re loaded in order to save time when exporting G-code.', - default => $Slic3r::GUI::Settings->{_}{background_processing}, - readonly => !$Slic3r::have_threads, - }, - ], - on_change => sub { $self->{values}{$_[0]} = $_[1] }, + on_change => sub { + my ($opt_id) = @_; + $self->{values}{$opt_id} = $optgroup->get_value($opt_id); + }, label_width => 100, ); + $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'mode', + type => 'select', + label => 'Mode', + tooltip => 'Choose between a simpler, basic mode and an expert mode with more options and more complicated interface.', + labels => ['Simple','Expert'], + values => ['simple','expert'], + default => $Slic3r::GUI::Settings->{_}{mode}, + )); + $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'version_check', + type => 'bool', + label => 'Check for updates', + tooltip => 'If this is enabled, Slic3r will check for updates daily and display a reminder if a newer version is available.', + default => $Slic3r::GUI::Settings->{_}{version_check} // 1, + readonly => !wxTheApp->have_version_check, + )); + $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'remember_output_path', + type => 'bool', + label => 'Remember output directory', + tooltip => 'If this is enabled, Slic3r will prompt the last output directory instead of the one containing the input files.', + default => $Slic3r::GUI::Settings->{_}{remember_output_path}, + )); + $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'autocenter', + type => 'bool', + label => 'Auto-center parts', + tooltip => 'If this is enabled, Slic3r will auto-center objects around the configured print center.', + default => $Slic3r::GUI::Settings->{_}{autocenter}, + )); + $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'background_processing', + type => 'bool', + label => 'Background processing', + tooltip => 'If this is enabled, Slic3r will pre-process objects as soon as they\'re loaded in order to save time when exporting G-code.', + default => $Slic3r::GUI::Settings->{_}{background_processing}, + readonly => !$Slic3r::have_threads, + )); + my $sizer = Wx::BoxSizer->new(wxVERTICAL); $sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10); diff --git a/lib/Slic3r/GUI/SimpleTab.pm b/lib/Slic3r/GUI/SimpleTab.pm index 42599f8ac..93f4b3830 100644 --- a/lib/Slic3r/GUI/SimpleTab.pm +++ b/lib/Slic3r/GUI/SimpleTab.pm @@ -13,8 +13,6 @@ sub new { my $class = shift; my ($parent, %params) = @_; my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); - $self->{options} = []; # array of option names handled by this tab - $self->{$_} = $params{$_} for qw(on_value_change); $self->SetScrollbars(1, 1, 1, 1); @@ -34,29 +32,26 @@ sub new { return $self; } -sub append_optgroup { - my $self = shift; - my %params = @_; +sub init_config_options { + my ($self, @opt_keys) = @_; + $self->{config}->apply(Slic3r::Config->new_from_defaults(@opt_keys)); +} + +sub new_optgroup { + my ($self, $title, %params) = @_; - # apply default values - { - my @options = @{$params{options}}; - $_ =~ s/#.+// for @options; - my $config = Slic3r::Config->new_from_defaults(@options); - $self->{config}->apply($config); - } - - my $class = $params{class} || 'Slic3r::GUI::ConfigOptionsGroup'; - my $optgroup = $class->new( - parent => $self, - config => $self->{config}, - label_width => 200, - on_change => sub { $self->on_value_change(@_) }, - %params, + my $optgroup = Slic3r::GUI::ConfigOptionsGroup->new( + parent => $self, + title => $title, + config => $self->{config}, + label_width => $params{label_width} // 200, + on_change => sub { $self->_on_value_change(@_) }, ); push @{$self->{optgroups}}, $optgroup; - ($params{sizer} || $self->{vsizer})->Add($optgroup->sizer, 0, wxEXPAND | wxALL, 10); + $self->{vsizer}->Add($optgroup->sizer, 0, wxEXPAND | wxALL, 10); + + return $optgroup; } sub load_config_file { @@ -71,29 +66,27 @@ sub load_config { my $self = shift; my ($config) = @_; - foreach my $opt_key (grep $self->{config}->has($_), @{$config->get_keys}) { - my $value = $config->get($opt_key); - $self->{config}->set($opt_key, $value); - $_->set_value($opt_key, $value) for @{$self->{optgroups}}; + foreach my $opt_key (@{$self->{config}->get_keys}) { + next unless $config->has($opt_key); + $self->{config}->set($opt_key, $config->get($opt_key)); } + $_->reload_config for @{$self->{optgroups}}; } -sub set_value { - my $self = shift; - my ($opt_key, $value) = @_; - - my $changed = 0; - foreach my $optgroup (@{$self->{optgroups}}) { - $changed = 1 if $optgroup->set_value($opt_key, $value); - } - return $changed; -} +sub load_presets {} sub is_dirty { 0 } sub config { $_[0]->{config}->clone } -# propagate event to the parent sub on_value_change { + my ($self, $cb) = @_; + $self->{on_value_change} = $cb; +} + +sub on_presets_changed {} + +# propagate event to the parent +sub _on_value_change { my $self = shift; $self->{on_value_change}->(@_) if $self->{on_value_change}; } @@ -109,50 +102,63 @@ sub title { 'Print Settings' } sub build { my $self = shift; - $self->append_optgroup( - title => 'General', - options => [qw(layer_height perimeters top_solid_layers bottom_solid_layers)], - lines => [ - Slic3r::GUI::OptionsGroup->single_option_line('layer_height'), - Slic3r::GUI::OptionsGroup->single_option_line('perimeters'), - { - label => 'Solid layers', - options => [qw(top_solid_layers bottom_solid_layers)], - }, - ], - ); + $self->init_config_options(qw( + layer_height perimeters top_solid_layers bottom_solid_layers + fill_density fill_pattern support_material support_material_spacing raft_layers + perimeter_speed infill_speed travel_speed + brim_width + complete_objects extruder_clearance_radius extruder_clearance_height + )); - $self->append_optgroup( - title => 'Infill', - options => [qw(fill_density fill_pattern)], - ); + { + my $optgroup = $self->new_optgroup('General'); + $optgroup->append_single_option_line('layer_height'); + $optgroup->append_single_option_line('perimeters'); + + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Solid layers', + ); + $line->append_option($optgroup->get_option('top_solid_layers')); + $line->append_option($optgroup->get_option('bottom_solid_layers')); + $optgroup->append_line($line); + } - $self->append_optgroup( - title => 'Support material', - options => [qw(support_material support_material_spacing raft_layers)], - ); + { + my $optgroup = $self->new_optgroup('Infill'); + $optgroup->append_single_option_line('fill_density'); + $optgroup->append_single_option_line('fill_pattern'); + } - $self->append_optgroup( - title => 'Speed', - options => [qw(perimeter_speed infill_speed travel_speed)], - ); + { + my $optgroup = $self->new_optgroup('Support material'); + $optgroup->append_single_option_line('support_material'); + $optgroup->append_single_option_line('support_material_spacing'); + $optgroup->append_single_option_line('raft_layers'); + } - $self->append_optgroup( - title => 'Brim', - options => [qw(brim_width)], - ); + { + my $optgroup = $self->new_optgroup('Speed'); + $optgroup->append_single_option_line('perimeter_speed'); + $optgroup->append_single_option_line('infill_speed'); + $optgroup->append_single_option_line('travel_speed'); + } - $self->append_optgroup( - title => 'Sequential printing', - options => [qw(complete_objects extruder_clearance_radius extruder_clearance_height)], - lines => [ - Slic3r::GUI::OptionsGroup->single_option_line('complete_objects'), - { - label => 'Extruder clearance (mm)', - options => [qw(extruder_clearance_radius extruder_clearance_height)], - }, - ], - ); + { + my $optgroup = $self->new_optgroup('Brim'); + $optgroup->append_single_option_line('brim_width'); + } + + { + my $optgroup = $self->new_optgroup('Sequential printing'); + $optgroup->append_single_option_line('complete_objects'); + + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Extruder clearance (mm)', + ); + $line->append_option($optgroup->get_option('extruder_clearance_radius')); + $line->append_option($optgroup->get_option('extruder_clearance_height')); + $optgroup->append_line($line); + } } package Slic3r::GUI::SimpleTab::Filament; @@ -164,25 +170,38 @@ sub title { 'Filament Settings' } sub build { my $self = shift; - $self->append_optgroup( - title => 'Filament', - options => ['filament_diameter#0', 'extrusion_multiplier#0'], - ); + $self->init_config_options(qw( + filament_diameter extrusion_multiplier + temperature first_layer_temperature bed_temperature first_layer_bed_temperature + )); - $self->append_optgroup( - title => 'Temperature (°C)', - options => ['temperature#0', 'first_layer_temperature#0', qw(bed_temperature first_layer_bed_temperature)], - lines => [ - { - label => 'Extruder', - options => ['first_layer_temperature#0', 'temperature#0'], - }, - { - label => 'Bed', - options => [qw(first_layer_bed_temperature bed_temperature)], - }, - ], - ); + { + my $optgroup = $self->new_optgroup('Filament'); + $optgroup->append_single_option_line('filament_diameter', 0); + $optgroup->append_single_option_line('extrusion_multiplier', 0); + } + + { + my $optgroup = $self->new_optgroup('Temperature (°C)'); + + { + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Extruder', + ); + $line->append_option($optgroup->get_option('first_layer_temperature', 0)); + $line->append_option($optgroup->get_option('temperature', 0)); + $optgroup->append_line($line); + } + + { + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Bed', + ); + $line->append_option($optgroup->get_option('first_layer_bed_temperature')); + $line->append_option($optgroup->get_option('bed_temperature')); + $optgroup->append_line($line); + } + } } package Slic3r::GUI::SimpleTab::Printer; @@ -194,37 +213,56 @@ sub title { 'Printer Settings' } sub build { my $self = shift; - $self->append_optgroup( - title => 'Size and coordinates', - options => [qw(bed_size z_offset)], - ); + $self->init_config_options(qw( + z_offset + gcode_flavor + nozzle_diameter + retract_length retract_lift + start_gcode + end_gcode + )); - $self->append_optgroup( - title => 'Firmware', - options => [qw(gcode_flavor)], - ); + { + my $optgroup = $self->new_optgroup('Size and coordinates'); + # TODO: add bed_shape + $optgroup->append_single_option_line('z_offset'); + } - $self->append_optgroup( - title => 'Extruder', - options => ['nozzle_diameter#0'], - ); + { + my $optgroup = $self->new_optgroup('Firmware'); + $optgroup->append_single_option_line('gcode_flavor'); + } - $self->append_optgroup( - title => 'Retraction', - options => ['retract_length#0', 'retract_lift#0'], - ); + { + my $optgroup = $self->new_optgroup('Extruder'); + $optgroup->append_single_option_line('nozzle_diameter', 0); + } - $self->append_optgroup( - title => 'Start G-code', - no_labels => 1, - options => [qw(start_gcode)], - ); + { + my $optgroup = $self->new_optgroup('Retraction'); + $optgroup->append_single_option_line('retract_length', 0); + $optgroup->append_single_option_line('retract_lift', 0); + } - $self->append_optgroup( - title => 'End G-code', - no_labels => 1, - options => [qw(end_gcode)], - ); + { + my $optgroup = $self->new_optgroup('Start G-code', + label_width => 0, + ); + my $option = $optgroup->get_option('start_gcode'); + $option->full_width(1); + $option->height(150); + $optgroup->append_single_option_line($option); + } + + { + my $optgroup = $self->new_optgroup('End G-code', + label_width => 0, + ); + my $option = $optgroup->get_option('end_gcode'); + $option->full_width(1); + $option->height(150); + $optgroup->append_single_option_line($option); + } } 1; diff --git a/lib/Slic3r/GUI/Tab.pm b/lib/Slic3r/GUI/Tab.pm index baf4bfeb3..7952f3500 100644 --- a/lib/Slic3r/GUI/Tab.pm +++ b/lib/Slic3r/GUI/Tab.pm @@ -14,8 +14,6 @@ sub new { my $class = shift; my ($parent, %params) = @_; my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); - $self->{options} = []; # array of option names handled by this tab - $self->{$_} = $params{$_} for qw(on_value_change on_presets_changed); # horizontal sizer $self->{sizer} = Wx::BoxSizer->new(wxHORIZONTAL); @@ -82,7 +80,7 @@ sub new { EVT_CHOICE($parent, $self->{presets_choice}, sub { $self->on_select_preset; - $self->on_presets_changed; + $self->_on_presets_changed; }); EVT_BUTTON($self, $self->{btn_save_preset}, sub { $self->save_preset }); @@ -100,16 +98,14 @@ sub new { $self->{presets_choice}->Delete($i); $self->{presets_choice}->SetSelection(0); $self->on_select_preset; - $self->on_presets_changed; + $self->_on_presets_changed; }); $self->{config} = Slic3r::Config->new; $self->build; if ($self->hidden_options) { $self->{config}->apply(Slic3r::Config->new_from_defaults($self->hidden_options)); - push @{$self->{options}}, $self->hidden_options; } - $self->load_presets; return $self; } @@ -151,17 +147,30 @@ sub save_preset { $self->load_presets; $self->{presets_choice}->SetSelection(first { basename($self->{presets}[$_]{file}) eq $name . ".ini" } 1 .. $#{$self->{presets}}); $self->on_select_preset; - $self->on_presets_changed; + $self->_on_presets_changed; } -# propagate event to the parent sub on_value_change { - my $self = shift; - $self->{on_value_change}->(@_) if $self->{on_value_change}; + my ($self, $cb) = @_; + $self->{on_value_change} = $cb; } sub on_presets_changed { + my ($self, $cb) = @_; + $self->{on_presets_changed} = $cb; +} + +# propagate event to the parent +sub _on_value_change { my $self = shift; + + $self->set_dirty(1); + $self->{on_value_change}->(@_) if $self->{on_value_change}; +} + +sub _on_presets_changed { + my $self = shift; + $self->{on_presets_changed}->([$self->{presets_choice}->GetStrings], $self->{presets_choice}->GetSelection) if $self->{on_presets_changed}; } @@ -184,7 +193,7 @@ sub select_preset { sub on_select_preset { my $self = shift; - if (defined $self->{dirty}) { + if ($self->{dirty}) { my $name = $self->{dirty} == 0 ? 'Default preset' : "Preset \"$self->{presets}[$self->{dirty}]{name}\""; my $confirm = Wx::MessageDialog->new($self, "$name has unsaved changes. Discard changes and continue anyway?", 'Unsaved Changes', wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION); @@ -199,7 +208,7 @@ sub on_select_preset { my $preset_config = $self->get_preset_config($preset); eval { local $SIG{__WARN__} = Slic3r::GUI::warning_catcher($self); - foreach my $opt_key (@{$self->{options}}) { + foreach my $opt_key (@{$self->{config}->get_keys}) { $self->{config}->set($opt_key, $preset_config->get($opt_key)) if $preset_config->has($opt_key); } @@ -208,7 +217,7 @@ sub on_select_preset { : $self->{btn_delete_preset}->Enable; $self->on_preset_loaded; - $self->reload_values; + $self->reload_config; # use CallAfter because some field triggers schedule on_change calls using CallAfter, # and we don't want them to be called after this set_dirty(0) as they would mark the @@ -232,7 +241,7 @@ sub get_preset_config { my ($preset) = @_; if ($preset->{default}) { - return Slic3r::Config->new_from_defaults(@{$self->{options}}); + return Slic3r::Config->new_from_defaults(@{$self->{config}->get_keys}); } else { if (!-e $preset->{file}) { Slic3r::GUI::show_error($self, "The selected preset does not exist anymore ($preset->{file})."); @@ -243,12 +252,17 @@ sub get_preset_config { my $external_config = Slic3r::Config->load($preset->{file}); my $config = Slic3r::Config->new; $config->set($_, $external_config->get($_)) - for grep $external_config->has($_), @{$self->{options}}; + for grep $external_config->has($_), @{$self->{config}->get_keys}; return $config; } } +sub init_config_options { + my ($self, @opt_keys) = @_; + $self->{config}->apply(Slic3r::Config->new_from_defaults(@opt_keys)); +} + sub add_options_page { my $self = shift; my ($title, $icon, %params) = @_; @@ -259,23 +273,7 @@ sub add_options_page { $self->{iconcount}++; } - { - # get all config options being added to the current page; remove indexes; associate defaults - my @options = map { $_ =~ s/#.+//; $_ } grep !ref($_), map @{$_->{options}}, @{$params{optgroups}}; - my %defaults_to_set = map { $_ => 1 } @options; - - # apply default values for the options we don't have already - delete $defaults_to_set{$_} for @{$self->{options}}; - $self->{config}->apply(Slic3r::Config->new_from_defaults(keys %defaults_to_set)) if %defaults_to_set; - - # append such options to our list - push @{$self->{options}}, @options; - } - - my $page = Slic3r::GUI::Tab::Page->new($self, $title, $self->{iconcount}, %params, on_change => sub { - $self->on_value_change(@_); - $self->set_dirty(1); - }); + my $page = Slic3r::GUI::Tab::Page->new($self, $title, $self->{iconcount}); $page->Hide; $self->{sizer}->Add($page, 1, wxEXPAND | wxLEFT, 5); push @{$self->{pages}}, $page; @@ -283,22 +281,9 @@ sub add_options_page { return $page; } -sub set_value { +sub reload_config { my $self = shift; - my ($opt_key, $value) = @_; - - my $changed = 0; - foreach my $page (@{$self->{pages}}) { - $changed = 1 if $page->set_value($opt_key, $value); - } - return $changed; -} - -sub reload_values { - my $self = shift; - - $self->set_value($_, $self->{config}->get($_)) - for @{$self->{config}->get_keys}; + $_->reload_config for @{$self->{pages}}; } sub update_tree { @@ -338,7 +323,7 @@ sub set_dirty { $self->{presets_choice}->SetString($i, $text); $self->{presets_choice}->SetSelection($selection); # http://trac.wxwidgets.org/ticket/13769 } - $self->on_presets_changed; + $self->_on_presets_changed; } sub is_dirty { @@ -370,7 +355,7 @@ sub load_presets { $self->{presets_choice}->SetSelection($i || 0); $self->on_select_preset; } - $self->on_presets_changed; + $self->_on_presets_changed; } sub load_config_file { @@ -391,7 +376,18 @@ sub load_config_file { } $self->{presets_choice}->SetSelection($i); $self->on_select_preset; - $self->on_presets_changed; + $self->_on_presets_changed; +} + +sub load_config { + my $self = shift; + my ($config) = @_; + + foreach my $opt_key (@{$self->{config}->diff($config)}) { + $self->{config}->set($opt_key, $config->get($opt_key)); + $self->set_dirty(1); + } + $self->reload_config; } package Slic3r::GUI::Tab::Print; @@ -403,165 +399,268 @@ sub title { 'Print Settings' } sub build { my $self = shift; - $self->add_options_page('Layers and perimeters', 'layers.png', optgroups => [ - { - title => 'Layer height', - options => [qw(layer_height first_layer_height)], - }, - { - title => 'Vertical shells', - options => [qw(perimeters spiral_vase)], - }, - { - title => 'Horizontal shells', - options => [qw(top_solid_layers bottom_solid_layers)], - lines => [ - { - label => 'Solid layers', - options => [qw(top_solid_layers bottom_solid_layers)], - }, - ], - }, - { - title => 'Quality (slower slicing)', - options => [qw(extra_perimeters avoid_crossing_perimeters thin_walls overhangs)], - lines => [ - Slic3r::GUI::OptionsGroup->single_option_line('extra_perimeters'), - Slic3r::GUI::OptionsGroup->single_option_line('avoid_crossing_perimeters'), - Slic3r::GUI::OptionsGroup->single_option_line('thin_walls'), - Slic3r::GUI::OptionsGroup->single_option_line('overhangs'), - ], - }, - { - title => 'Advanced', - options => [qw(seam_position external_perimeters_first)], - }, - ]); + $self->init_config_options(qw( + layer_height first_layer_height + perimeters spiral_vase + top_solid_layers bottom_solid_layers + extra_perimeters avoid_crossing_perimeters thin_walls overhangs + seam_position external_perimeters_first + fill_density fill_pattern solid_fill_pattern + infill_every_layers infill_only_where_needed + solid_infill_every_layers fill_angle solid_infill_below_area + only_retract_when_crossing_perimeters infill_first + perimeter_speed small_perimeter_speed external_perimeter_speed infill_speed + solid_infill_speed top_solid_infill_speed support_material_speed + support_material_interface_speed bridge_speed gap_fill_speed + travel_speed + first_layer_speed + perimeter_acceleration infill_acceleration bridge_acceleration + first_layer_acceleration default_acceleration + skirts skirt_distance skirt_height min_skirt_length + brim_width + support_material support_material_threshold support_material_enforce_layers + raft_layers + support_material_pattern support_material_spacing support_material_angle + support_material_interface_layers support_material_interface_spacing + dont_support_bridges + notes + complete_objects extruder_clearance_radius extruder_clearance_height + gcode_comments output_filename_format + post_process + perimeter_extruder infill_extruder support_material_extruder support_material_interface_extruder + ooze_prevention standby_temperature_delta + interface_shells + extrusion_width first_layer_extrusion_width perimeter_extrusion_width + external_perimeter_extrusion_width infill_extrusion_width solid_infill_extrusion_width + top_infill_extrusion_width support_material_extrusion_width + bridge_flow_ratio + xy_size_compensation threads resolution + )); - $self->add_options_page('Infill', 'shading.png', optgroups => [ + { + my $page = $self->add_options_page('Layers and perimeters', 'layers.png'); { - title => 'Infill', - options => [qw(fill_density fill_pattern solid_fill_pattern)], - }, + my $optgroup = $page->new_optgroup('Layer height'); + $optgroup->append_single_option_line('layer_height'); + $optgroup->append_single_option_line('first_layer_height'); + } { - title => 'Reducing printing time', - options => [qw(infill_every_layers infill_only_where_needed)], - }, + my $optgroup = $page->new_optgroup('Vertical shells'); + $optgroup->append_single_option_line('perimeters'); + $optgroup->append_single_option_line('spiral_vase'); + } { - title => 'Advanced', - options => [qw(solid_infill_every_layers fill_angle - solid_infill_below_area only_retract_when_crossing_perimeters infill_first)], - }, - ]); + my $optgroup = $page->new_optgroup('Horizontal shells'); + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Solid layers', + ); + $line->append_option($optgroup->get_option('top_solid_layers')); + $line->append_option($optgroup->get_option('bottom_solid_layers')); + $optgroup->append_line($line); + } + { + my $optgroup = $page->new_optgroup('Quality (slower slicing)'); + $optgroup->append_single_option_line('extra_perimeters'); + $optgroup->append_single_option_line('avoid_crossing_perimeters'); + $optgroup->append_single_option_line('thin_walls'); + $optgroup->append_single_option_line('overhangs'); + } + { + my $optgroup = $page->new_optgroup('Advanced'); + $optgroup->append_single_option_line('seam_position'); + $optgroup->append_single_option_line('external_perimeters_first'); + } + } - $self->add_options_page('Speed', 'time.png', optgroups => [ + { + my $page = $self->add_options_page('Infill', 'shading.png'); { - title => 'Speed for print moves', - options => [qw(perimeter_speed small_perimeter_speed external_perimeter_speed infill_speed solid_infill_speed top_solid_infill_speed support_material_speed support_material_interface_speed bridge_speed gap_fill_speed)], - }, + my $optgroup = $page->new_optgroup('Infill'); + $optgroup->append_single_option_line('fill_density'); + $optgroup->append_single_option_line('fill_pattern'); + $optgroup->append_single_option_line('solid_fill_pattern'); + } { - title => 'Speed for non-print moves', - options => [qw(travel_speed)], - }, + my $optgroup = $page->new_optgroup('Reducing printing time'); + $optgroup->append_single_option_line('infill_every_layers'); + $optgroup->append_single_option_line('infill_only_where_needed'); + } { - title => 'Modifiers', - options => [qw(first_layer_speed)], - }, - { - title => 'Acceleration control (advanced)', - options => [qw(perimeter_acceleration infill_acceleration bridge_acceleration first_layer_acceleration default_acceleration)], - }, - ]); + my $optgroup = $page->new_optgroup('Advanced'); + $optgroup->append_single_option_line('solid_infill_every_layers'); + $optgroup->append_single_option_line('fill_angle'); + $optgroup->append_single_option_line('solid_infill_below_area'); + $optgroup->append_single_option_line('only_retract_when_crossing_perimeters'); + $optgroup->append_single_option_line('infill_first'); + } + } - $self->add_options_page('Skirt and brim', 'box.png', optgroups => [ + { + my $page = $self->add_options_page('Speed', 'time.png'); { - title => 'Skirt', - options => [qw(skirts skirt_distance skirt_height min_skirt_length)], - }, + my $optgroup = $page->new_optgroup('Speed for print moves'); + $optgroup->append_single_option_line('perimeter_speed'); + $optgroup->append_single_option_line('small_perimeter_speed'); + $optgroup->append_single_option_line('external_perimeter_speed'); + $optgroup->append_single_option_line('infill_speed'); + $optgroup->append_single_option_line('solid_infill_speed'); + $optgroup->append_single_option_line('top_solid_infill_speed'); + $optgroup->append_single_option_line('support_material_speed'); + $optgroup->append_single_option_line('support_material_interface_speed'); + $optgroup->append_single_option_line('bridge_speed'); + $optgroup->append_single_option_line('gap_fill_speed'); + } { - title => 'Brim', - options => [qw(brim_width)], - }, - ]); + my $optgroup = $page->new_optgroup('Speed for non-print moves'); + $optgroup->append_single_option_line('travel_speed'); + } + { + my $optgroup = $page->new_optgroup('Modifiers'); + $optgroup->append_single_option_line('first_layer_speed'); + } + { + my $optgroup = $page->new_optgroup('Acceleration control (advanced)'); + $optgroup->append_single_option_line('perimeter_acceleration'); + $optgroup->append_single_option_line('infill_acceleration'); + $optgroup->append_single_option_line('bridge_acceleration'); + $optgroup->append_single_option_line('first_layer_acceleration'); + $optgroup->append_single_option_line('default_acceleration'); + } + } - $self->add_options_page('Support material', 'building.png', optgroups => [ + { + my $page = $self->add_options_page('Skirt and brim', 'box.png'); { - title => 'Support material', - options => [qw(support_material support_material_threshold support_material_enforce_layers)], - }, + my $optgroup = $page->new_optgroup('Skirt'); + $optgroup->append_single_option_line('skirts'); + $optgroup->append_single_option_line('skirt_distance'); + $optgroup->append_single_option_line('skirt_height'); + $optgroup->append_single_option_line('min_skirt_length'); + } { - title => 'Raft', - options => [qw(raft_layers)], - }, - { - title => 'Options for support material and raft', - options => [qw(support_material_pattern support_material_spacing support_material_angle - support_material_interface_layers support_material_interface_spacing - dont_support_bridges)], - }, - ]); + my $optgroup = $page->new_optgroup('Brim'); + $optgroup->append_single_option_line('brim_width'); + } + } - $self->add_options_page('Notes', 'note.png', optgroups => [ + { + my $page = $self->add_options_page('Support material', 'building.png'); { - title => 'Notes', - no_labels => 1, - options => [qw(notes)], - }, - ]); + my $optgroup = $page->new_optgroup('Support material'); + $optgroup->append_single_option_line('support_material'); + $optgroup->append_single_option_line('support_material_threshold'); + $optgroup->append_single_option_line('support_material_enforce_layers'); + } + { + my $optgroup = $page->new_optgroup('Raft'); + $optgroup->append_single_option_line('raft_layers'); + } + { + my $optgroup = $page->new_optgroup('Options for support material and raft'); + $optgroup->append_single_option_line('support_material_pattern'); + $optgroup->append_single_option_line('support_material_spacing'); + $optgroup->append_single_option_line('support_material_angle'); + $optgroup->append_single_option_line('support_material_interface_layers'); + $optgroup->append_single_option_line('support_material_interface_spacing'); + $optgroup->append_single_option_line('dont_support_bridges'); + } + } - $self->add_options_page('Output options', 'page_white_go.png', optgroups => [ + { + my $page = $self->add_options_page('Notes', 'note.png'); { - title => 'Sequential printing', - options => [qw(complete_objects extruder_clearance_radius extruder_clearance_height)], - lines => [ - Slic3r::GUI::OptionsGroup->single_option_line('complete_objects'), - { - label => 'Extruder clearance (mm)', - options => [qw(extruder_clearance_radius extruder_clearance_height)], - }, - ], - }, - { - title => 'Output file', - options => [qw(gcode_comments output_filename_format)], - }, - { - title => 'Post-processing scripts', - no_labels => 1, - options => [qw(post_process)], - }, - ]); + my $optgroup = $page->new_optgroup('Notes', + label_width => 0, + ); + my $option = $optgroup->get_option('notes'); + $option->full_width(1); + $option->height(250); + $optgroup->append_single_option_line($option); + } + } - $self->add_options_page('Multiple Extruders', 'funnel.png', optgroups => [ + { + my $page = $self->add_options_page('Output options', 'page_white_go.png'); { - title => 'Extruders', - options => [qw(perimeter_extruder infill_extruder support_material_extruder support_material_interface_extruder)], - }, + my $optgroup = $page->new_optgroup('Sequential printing'); + $optgroup->append_single_option_line('complete_objects'); + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Extruder clearance (mm)', + ); + foreach my $opt_key (qw(extruder_clearance_radius extruder_clearance_height)) { + my $option = $optgroup->get_option($opt_key); + $option->width(60); + $line->append_option($option); + } + $optgroup->append_line($line); + } { - title => 'Ooze prevention', - options => [qw(ooze_prevention standby_temperature_delta)], - }, + my $optgroup = $page->new_optgroup('Output file'); + $optgroup->append_single_option_line('gcode_comments'); + + { + my $option = $optgroup->get_option('output_filename_format'); + $option->full_width(1); + $optgroup->append_single_option_line($option); + } + } { - title => 'Advanced', - options => [qw(interface_shells)], - }, - ]); + my $optgroup = $page->new_optgroup('Post-processing scripts', + label_width => 0, + ); + my $option = $optgroup->get_option('post_process'); + $option->full_width(1); + $option->height(50); + $optgroup->append_single_option_line($option); + } + } - $self->add_options_page('Advanced', 'wrench.png', optgroups => [ + { + my $page = $self->add_options_page('Multiple Extruders', 'funnel.png'); { - title => 'Extrusion width', - label_width => 180, - options => [qw(extrusion_width first_layer_extrusion_width perimeter_extrusion_width external_perimeter_extrusion_width infill_extrusion_width solid_infill_extrusion_width top_infill_extrusion_width support_material_extrusion_width)], - }, + my $optgroup = $page->new_optgroup('Extruders'); + $optgroup->append_single_option_line('perimeter_extruder'); + $optgroup->append_single_option_line('infill_extruder'); + $optgroup->append_single_option_line('support_material_extruder'); + $optgroup->append_single_option_line('support_material_interface_extruder'); + } { - title => 'Flow', - options => [qw(bridge_flow_ratio)], - }, + my $optgroup = $page->new_optgroup('Ooze prevention'); + $optgroup->append_single_option_line('ooze_prevention'); + $optgroup->append_single_option_line('standby_temperature_delta'); + } { - title => 'Other', - options => [qw(xy_size_compensation), ($Slic3r::have_threads ? qw(threads) : ()), qw(resolution)], - }, - ]); + my $optgroup = $page->new_optgroup('Advanced'); + $optgroup->append_single_option_line('interface_shells'); + } + } + + { + my $page = $self->add_options_page('Advanced', 'wrench.png'); + { + my $optgroup = $page->new_optgroup('Extrusion width', + label_width => 180, + ); + $optgroup->append_single_option_line('extrusion_width'); + $optgroup->append_single_option_line('first_layer_extrusion_width'); + $optgroup->append_single_option_line('perimeter_extrusion_width'); + $optgroup->append_single_option_line('external_perimeter_extrusion_width'); + $optgroup->append_single_option_line('infill_extrusion_width'); + $optgroup->append_single_option_line('solid_infill_extrusion_width'); + $optgroup->append_single_option_line('top_infill_extrusion_width'); + $optgroup->append_single_option_line('support_material_extrusion_width'); + } + { + my $optgroup = $page->new_optgroup('Flow'); + $optgroup->append_single_option_line('bridge_flow_ratio'); + } + { + my $optgroup = $page->new_optgroup('Other'); + $optgroup->append_single_option_line('xy_size_compensation'); + $optgroup->append_single_option_line('threads') if $Slic3r::have_threads; + $optgroup->append_single_option_line('resolution'); + } + } } sub hidden_options { !$Slic3r::have_threads ? qw(threads) : () } @@ -575,62 +674,88 @@ sub title { 'Filament Settings' } sub build { my $self = shift; - $self->add_options_page('Filament', 'spool.png', optgroups => [ - { - title => 'Filament', - options => ['filament_diameter#0', 'extrusion_multiplier#0'], - }, - { - title => 'Temperature (°C)', - options => ['temperature#0', 'first_layer_temperature#0', qw(bed_temperature first_layer_bed_temperature)], - lines => [ - { - label => 'Extruder', - options => ['first_layer_temperature#0', 'temperature#0'], - }, - { - label => 'Bed', - options => [qw(first_layer_bed_temperature bed_temperature)], - }, - ], - }, - ]); + $self->init_config_options(qw( + filament_diameter extrusion_multiplier + temperature first_layer_temperature bed_temperature first_layer_bed_temperature + fan_always_on cooling + min_fan_speed max_fan_speed bridge_fan_speed disable_fan_first_layers + fan_below_layer_time slowdown_below_layer_time min_print_speed + )); - $self->add_options_page('Cooling', 'hourglass.png', optgroups => [ + { + my $page = $self->add_options_page('Filament', 'spool.png'); { - title => 'Enable', - options => [qw(fan_always_on cooling)], - lines => [ - Slic3r::GUI::OptionsGroup->single_option_line('fan_always_on'), - Slic3r::GUI::OptionsGroup->single_option_line('cooling'), - { - label => '', - full_width => 1, - widget => sub { - my ($parent) = @_; - return $self->{description_line} = Slic3r::GUI::OptionsGroup::StaticTextLine->new($parent); - }, + my $optgroup = $page->new_optgroup('Filament'); + $optgroup->append_single_option_line('filament_diameter', 0); + $optgroup->append_single_option_line('extrusion_multiplier', 0); + } + + { + my $optgroup = $page->new_optgroup('Temperature (°C)'); + + { + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Extruder', + ); + $line->append_option($optgroup->get_option('first_layer_temperature', 0)); + $line->append_option($optgroup->get_option('temperature', 0)); + $optgroup->append_line($line); + } + + { + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Bed', + ); + $line->append_option($optgroup->get_option('first_layer_bed_temperature')); + $line->append_option($optgroup->get_option('bed_temperature')); + $optgroup->append_line($line); + } + } + } + + { + my $page = $self->add_options_page('Cooling', 'hourglass.png'); + { + my $optgroup = $page->new_optgroup('Enable'); + $optgroup->append_single_option_line('fan_always_on'); + $optgroup->append_single_option_line('cooling'); + + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => '', + full_width => 1, + widget => sub { + my ($parent) = @_; + return $self->{description_line} = Slic3r::GUI::OptionsGroup::StaticText->new($parent); }, - ], - }, + ); + $optgroup->append_line($line); + } { - title => 'Fan settings', - options => [qw(min_fan_speed max_fan_speed bridge_fan_speed disable_fan_first_layers)], - lines => [ - { - label => 'Fan speed', - options => [qw(min_fan_speed max_fan_speed)], - }, - Slic3r::GUI::OptionsGroup->single_option_line('bridge_fan_speed'), - Slic3r::GUI::OptionsGroup->single_option_line('disable_fan_first_layers'), - ], - }, + my $optgroup = $page->new_optgroup('Fan settings'); + + { + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Fan speed', + ); + $line->append_option($optgroup->get_option('min_fan_speed')); + $line->append_option($optgroup->get_option('max_fan_speed')); + $optgroup->append_line($line); + } + + $optgroup->append_single_option_line('bridge_fan_speed'); + $optgroup->append_single_option_line('disable_fan_first_layers'); + } { - title => 'Cooling thresholds', - label_width => 250, - options => [qw(fan_below_layer_time slowdown_below_layer_time min_print_speed)], - }, - ]); + my $optgroup = $page->new_optgroup('Cooling thresholds', + label_width => 250, + ); + $optgroup->append_single_option_line('fan_below_layer_time'); + $optgroup->append_single_option_line('slowdown_below_layer_time'); + $optgroup->append_single_option_line('min_print_speed'); + } + } + + $self->_update_description; } sub _update_description { @@ -662,10 +787,10 @@ sub _update_description { $self->{description_line}->SetText($msg); } -sub on_value_change { +sub _on_value_change { my $self = shift; my ($opt_key) = @_; - $self->SUPER::on_value_change(@_); + $self->SUPER::_on_value_change(@_); $self->_update_description; } @@ -681,7 +806,15 @@ sub title { 'Printer Settings' } sub build { my $self = shift; - $self->{extruders_count} = 1; + $self->init_config_options(qw( + bed_shape z_offset + gcode_flavor use_relative_e_distances + use_firmware_retraction vibration_limit + start_gcode end_gcode layer_gcode toolchange_gcode + nozzle_diameter extruder_offset + retract_length retract_lift retract_speed retract_restart_extra retract_before_travel retract_layer_change wipe + retract_length_toolchange retract_restart_extra_toolchange + )); my $bed_shape_widget = sub { my ($parent) = @_; @@ -701,73 +834,99 @@ sub build { my $value = $dlg->GetValue; $self->{config}->set('bed_shape', $value); $self->set_dirty(1); - $self->on_value_change('bed_shape', $value); + $self->_on_value_change('bed_shape', $value); } }); return $sizer; }; - $self->add_options_page('General', 'printer_empty.png', optgroups => [ - { - title => 'Size and coordinates', - options => [qw(bed_shape z_offset)], - lines => [ - { - label => 'Bed shape', - widget => $bed_shape_widget, - options => ['bed_shape'], - }, - Slic3r::GUI::OptionsGroup->single_option_line('z_offset'), - ], - }, - { - title => 'Firmware', - options => [qw(gcode_flavor use_relative_e_distances)], - }, - { - class => 'Slic3r::GUI::OptionsGroup', - title => 'Capabilities', - options => [ - { - opt_key => 'extruders_count', - label => 'Extruders', - tooltip => 'Number of extruders of the printer.', - type => 'i', - min => 1, - default => 1, - on_change => sub { $self->{extruders_count} = $_[0] }, - }, - ], - }, - { - title => 'Advanced', - options => [qw(use_firmware_retraction vibration_limit)], - }, - ]); + $self->{extruders_count} = 1; - $self->add_options_page('Custom G-code', 'cog.png', optgroups => [ + { + my $page = $self->add_options_page('General', 'printer_empty.png'); { - title => 'Start G-code', - no_labels => 1, - options => [qw(start_gcode)], - }, + my $optgroup = $page->new_optgroup('Size and coordinates'); + + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Bed shape', + widget => $bed_shape_widget, + ); + $optgroup->append_line($line); + + $optgroup->append_single_option_line('z_offset'); + } { - title => 'End G-code', - no_labels => 1, - options => [qw(end_gcode)], - }, + my $optgroup = $page->new_optgroup('Firmware'); + $optgroup->append_single_option_line('gcode_flavor'); + $optgroup->append_single_option_line('use_relative_e_distances'); + } { - title => 'Layer change G-code', - no_labels => 1, - options => [qw(layer_gcode)], - }, + my $optgroup = $page->new_optgroup('Capabilities'); + { + my $option = Slic3r::GUI::OptionsGroup::Option->new( + opt_id => 'extruders_count', + type => 'i', + default => 1, + label => 'Extruders', + tooltip => 'Number of extruders of the printer.', + min => 1, + ); + $optgroup->append_single_option_line($option); + } + $optgroup->on_change(sub { + my ($opt_id) = @_; + if ($opt_id eq 'extruders_count') { + $self->{extruders_count} = $optgroup->get_value('extruders_count'); + $self->_build_extruder_pages; + } + }); + } { - title => 'Tool change G-code', - no_labels => 1, - options => [qw(toolchange_gcode)], - }, - ]); + my $optgroup = $page->new_optgroup('Advanced'); + $optgroup->append_single_option_line('use_firmware_retraction'); + $optgroup->append_single_option_line('vibration_limit'); + } + } + { + my $page = $self->add_options_page('Custom G-code', 'cog.png'); + { + my $optgroup = $page->new_optgroup('Start G-code', + label_width => 0, + ); + my $option = $optgroup->get_option('start_gcode'); + $option->full_width(1); + $option->height(150); + $optgroup->append_single_option_line($option); + } + { + my $optgroup = $page->new_optgroup('End G-code', + label_width => 0, + ); + my $option = $optgroup->get_option('end_gcode'); + $option->full_width(1); + $option->height(150); + $optgroup->append_single_option_line($option); + } + { + my $optgroup = $page->new_optgroup('Layer change G-code', + label_width => 0, + ); + my $option = $optgroup->get_option('layer_gcode'); + $option->full_width(1); + $option->height(150); + $optgroup->append_single_option_line($option); + } + { + my $optgroup = $page->new_optgroup('Tool change G-code', + label_width => 0, + ); + my $option = $optgroup->get_option('toolchange_gcode'); + $option->full_width(1); + $option->height(150); + $optgroup->append_single_option_line($option); + } + } $self->{extruder_pages} = []; $self->_build_extruder_pages; @@ -797,30 +956,26 @@ sub _build_extruder_pages { } # build page - $self->{extruder_pages}[$extruder_idx] = $self->add_options_page("Extruder " . ($extruder_idx + 1), 'funnel.png', optgroups => [ - { - title => 'Size', - options => ['nozzle_diameter#' . $extruder_idx], - }, - { - title => 'Position (for multi-extruder printers)', - options => ['extruder_offset#' . $extruder_idx], - }, - { - title => 'Retraction', - options => [ - map "${_}#${extruder_idx}", - qw(retract_length retract_lift retract_speed retract_restart_extra retract_before_travel retract_layer_change wipe) - ], - }, - { - title => 'Retraction when tool is disabled (advanced settings for multi-extruder setups)', - options => [ - map "${_}#${extruder_idx}", - qw(retract_length_toolchange retract_restart_extra_toolchange) - ], - }, - ]); + my $page = $self->{extruder_pages}[$extruder_idx] = $self->add_options_page("Extruder " . ($extruder_idx + 1), 'funnel.png'); + { + my $optgroup = $page->new_optgroup('Size'); + $optgroup->append_single_option_line('nozzle_diameter', $extruder_idx); + } + { + my $optgroup = $page->new_optgroup('Position (for multi-extruder printers)'); + $optgroup->append_single_option_line('extruder_offset', $extruder_idx); + } + { + my $optgroup = $page->new_optgroup('Retraction'); + $optgroup->append_single_option_line($_, $extruder_idx) + for qw(retract_length retract_lift retract_speed retract_restart_extra retract_before_travel retract_layer_change wipe); + } + { + my $optgroup = $page->new_optgroup('Retraction when tool is disabled (advanced settings for multi-extruder setups)'); + $optgroup->append_single_option_line($_, $extruder_idx) + for qw(retract_length_toolchange retract_restart_extra_toolchange); + } + $self->{extruder_pages}[$extruder_idx]{disabled} = 0; } @@ -842,33 +997,20 @@ sub _build_extruder_pages { (grep $_->{title} !~ /^Extruder \d+/, @{$self->{pages}}), @{$self->{extruder_pages}}[ 0 .. $self->{extruders_count}-1 ], ); -} - -sub on_value_change { - my $self = shift; - my ($opt_key) = @_; - $self->SUPER::on_value_change(@_); - - if ($opt_key eq 'extruders_count') { - # add extra pages or remove unused - $self->_build_extruder_pages; - - # update page list and select first page (General) - $self->update_tree(0); - } + $self->update_tree(0); } # this gets executed after preset is loaded and before GUI fields are updated sub on_preset_loaded { my $self = shift; - + return; # update the extruders count field { # update the GUI field according to the number of nozzle diameters supplied $self->set_value('extruders_count', scalar @{ $self->{config}->nozzle_diameter }); # update extruder page list - $self->on_value_change('extruders_count'); + $self->_on_value_change('extruders_count'); } } @@ -889,7 +1031,7 @@ use base 'Wx::ScrolledWindow'; sub new { my $class = shift; - my ($parent, $title, $iconID, %params) = @_; + my ($parent, $title, $iconID) = @_; my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); $self->{optgroups} = []; $self->{title} = $title; @@ -900,30 +1042,29 @@ sub new { $self->{vsizer} = Wx::BoxSizer->new(wxVERTICAL); $self->SetSizer($self->{vsizer}); - if ($params{optgroups}) { - $self->append_optgroup( - %$_, - config => $parent->{config}, - on_change => $params{on_change}, - ) for @{$params{optgroups}}; - } - return $self; } -sub append_optgroup { - my $self = shift; - my %params = @_; +sub new_optgroup { + my ($self, $title, %params) = @_; - my $class = $params{class} || 'Slic3r::GUI::ConfigOptionsGroup'; - my $optgroup = $class->new( - parent => $self, - config => $self->GetParent->{config}, - label_width => 200, - %params, + my $optgroup = Slic3r::GUI::ConfigOptionsGroup->new( + parent => $self, + title => $title, + config => $self->GetParent->{config}, + label_width => $params{label_width} // 200, + on_change => sub { $self->GetParent->_on_value_change(@_) }, ); - $self->{vsizer}->Add($optgroup->sizer, 0, wxEXPAND | wxALL, 5); + push @{$self->{optgroups}}, $optgroup; + $self->{vsizer}->Add($optgroup->sizer, 0, wxEXPAND | wxALL, 10); + + return $optgroup; +} + +sub reload_config { + my ($self) = @_; + $_->reload_config for @{$self->{optgroups}}; } sub set_value { diff --git a/xs/src/PrintConfig.cpp b/xs/src/PrintConfig.cpp index b114837f0..7417a7742 100644 --- a/xs/src/PrintConfig.cpp +++ b/xs/src/PrintConfig.cpp @@ -17,7 +17,6 @@ PrintConfigDef::build_def() { Options["bed_temperature"].type = coInt; Options["bed_temperature"].label = "Other layers"; Options["bed_temperature"].tooltip = "Bed temperature for layers after the first one. Set this to zero to disable bed temperature control commands in the output."; - Options["bed_temperature"].sidetext = "°C"; Options["bed_temperature"].cli = "bed-temperature=i"; Options["bed_temperature"].full_label = "Bed temperature"; Options["bed_temperature"].min = 0; @@ -224,7 +223,7 @@ PrintConfigDef::build_def() { Options["fill_angle"].max = 359; Options["fill_density"].type = coPercent; - Options["fill_density"].gui_type = "i_enum_open"; + Options["fill_density"].gui_type = "f_enum_open"; Options["fill_density"].gui_flags = "show_value"; Options["fill_density"].label = "Fill density"; Options["fill_density"].category = "Infill"; @@ -293,7 +292,6 @@ PrintConfigDef::build_def() { Options["first_layer_bed_temperature"].type = coInt; Options["first_layer_bed_temperature"].label = "First layer"; Options["first_layer_bed_temperature"].tooltip = "Heated build plate temperature for the first layer. Set this to zero to disable bed temperature control commands in the output."; - Options["first_layer_bed_temperature"].sidetext = "°C"; Options["first_layer_bed_temperature"].cli = "first-layer-bed-temperature=i"; Options["first_layer_bed_temperature"].max = 0; Options["first_layer_bed_temperature"].max = 300; @@ -322,7 +320,6 @@ PrintConfigDef::build_def() { Options["first_layer_temperature"].type = coInts; Options["first_layer_temperature"].label = "First layer"; Options["first_layer_temperature"].tooltip = "Extruder temperature for first layer. If you want to control temperature manually during print, set this to zero to disable temperature control commands in the output file."; - Options["first_layer_temperature"].sidetext = "°C"; Options["first_layer_temperature"].cli = "first-layer-temperature=i@"; Options["first_layer_temperature"].min = 0; Options["first_layer_temperature"].max = 400; @@ -551,6 +548,7 @@ PrintConfigDef::build_def() { Options["post_process"].label = "Post-processing scripts"; Options["post_process"].tooltip = "If you want to process the output G-code through custom scripts, just list their absolute paths here. Separate multiple scripts with a semicolon. Scripts will be passed the absolute path to the G-code file as the first argument, and they can access the Slic3r config settings by reading environment variables."; Options["post_process"].cli = "post-process=s@"; + Options["post_process"].gui_flags = "serialized"; Options["post_process"].multiline = true; Options["post_process"].full_width = true; Options["post_process"].height = 60; @@ -857,7 +855,6 @@ PrintConfigDef::build_def() { Options["temperature"].type = coInts; Options["temperature"].label = "Other layers"; Options["temperature"].tooltip = "Extruder temperature for layers after the first one. Set this to zero to disable temperature control commands in the output."; - Options["temperature"].sidetext = "°C"; Options["temperature"].cli = "temperature=i@"; Options["temperature"].full_label = "Temperature"; Options["temperature"].max = 0;