diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index 88e8b072a..eb5a7bd01 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -5,7 +5,9 @@ use utf8; use File::Basename qw(basename); use FindBin; -use List::Util qw(first); +use List::Util qw(first any); +use Slic3r::Geometry qw(X Y); + use Slic3r::GUI::2DBed; use Slic3r::GUI::AboutDialog; use Slic3r::GUI::BedShapeDialog; @@ -98,7 +100,7 @@ sub OnInit { Slic3r::debugf "wxWidgets version %s, Wx version %s\n", &Wx::wxVERSION_STRING, $Wx::VERSION; $self->{notifier} = Slic3r::GUI::Notifier->new; - $self->{external_presets} = []; + $self->{presets} = { print => [], filament => [], printer => [] }; # locate or create data directory # Unix: ~/.Slic3r @@ -142,6 +144,8 @@ sub OnInit { unlink "$enc_datadir/simple.ini"; } + $self->load_presets; + # application frame Wx::Image::AddHandler(Wx::PNGHandler->new); $self->{mainframe} = my $frame = Slic3r::GUI::MainFrame->new( @@ -283,47 +287,64 @@ sub save_settings { Slic3r::Config->write_ini("$datadir/slic3r.ini", $Settings); } -sub presets { - my ($self, $section, $force_default) = @_; +sub presets { return $_[0]->{presets}; } + +sub load_presets { + my ($self) = @_; - my @presets = (); + for my $group (qw(printer filament print)) { + my $presets = $self->{presets}{$group}; + + # keep external or dirty presets + @$presets = grep { ($_->external && $_->file_exists) || $_->dirty } @$presets; + + my $dir = "$Slic3r::GUI::datadir/$group"; + opendir my $dh, Slic3r::encode_path($dir) + or die "Failed to read directory $dir (errno: $!)\n"; + foreach my $file (grep /\.ini$/i, readdir $dh) { + $file = Slic3r::decode_path($file); + my $name = basename($file); + $name =~ s/\.ini$//i; + + # skip if we already have it + next if any { $_->name eq $name } @$presets; + + push @$presets, Slic3r::GUI::Preset->new( + group => $group, + name => $name, + file => "$dir/$file", + ); + } + closedir $dh; - opendir my $dh, Slic3r::encode_path("$Slic3r::GUI::datadir/$section") - or die "Failed to read directory $Slic3r::GUI::datadir/$section (errno: $!)\n"; - foreach my $file (grep /\.ini$/i, readdir $dh) { - $file = Slic3r::decode_path($file); - my $name = basename($file); - $name =~ s/\.ini$//i; - push @presets, Slic3r::GUI::Preset->new( - name => $name, - file => "$Slic3r::GUI::datadir/$section/$file", - ); - } - closedir $dh; + @$presets = sort { $a->name cmp $b->name } @$presets; - @presets = sort { $a->name cmp $b->name } - @presets, - (grep -e $_->file, @{$self->{external_presets}}); - - if ($force_default || !@presets) { - unshift @presets, Slic3r::GUI::Preset->new( + unshift @$presets, Slic3r::GUI::Preset->new( + group => $group, default => 1, name => '- default -', ); } - - return @presets; } sub add_external_preset { my ($self, $file) = @_; - push @{$self->{external_presets}}, my $preset = Slic3r::GUI::Preset->new( - name => basename($file), # keep .ini suffix - file => $file, - external => 1, - ); - return $preset; + my $name = basename($file); # keep .ini suffix + for my $group (qw(printer filament print)) { + my $presets = $self->{presets}{$group}; + + # remove any existing preset with the same name + @$presets = grep { $_->name ne $name } @$presets; + + push @$presets, Slic3r::GUI::Preset->new( + group => $group, + name => $name, + file => $file, + external => 1, + ); + } + return $name; } sub have_version_check { @@ -416,4 +437,29 @@ sub set_menu_item_icon { } } +sub save_window_pos { + my ($self, $window, $name) = @_; + + $Settings->{_}{"${name}_pos"} = join ',', $window->GetScreenPositionXY; + $Settings->{_}{"${name}_size"} = join ',', $window->GetSizeWH; + $Settings->{_}{"${name}_maximized"} = $window->IsMaximized; + $self->save_settings; +} + +sub restore_window_pos { + my ($self, $window, $name) = @_; + + if (defined $Settings->{_}{"${name}_pos"}) { + my $size = [ split ',', $Settings->{_}{"${name}_size"}, 2 ]; + $window->SetSize($size); + + my $display = Wx::Display->new->GetClientArea(); + my $pos = [ split ',', $Settings->{_}{"${name}_pos"}, 2 ]; + if (($pos->[X] + $size->[X]/2) < $display->GetRight && ($pos->[Y] + $size->[Y]/2) < $display->GetBottom) { + $window->Move($pos); + } + $window->Maximize(1) if $Settings->{_}{"${name}_maximized"}; + } +} + 1; diff --git a/lib/Slic3r/GUI/Controller.pm b/lib/Slic3r/GUI/Controller.pm index 7ebf3491d..87b85fde2 100644 --- a/lib/Slic3r/GUI/Controller.pm +++ b/lib/Slic3r/GUI/Controller.pm @@ -47,7 +47,7 @@ sub new { delete $presets{$_} for map $_->printer_name, @panels; foreach my $preset_name (sort keys %presets) { - my $config = $presets{$preset_name}->load_config; + my $config = $presets{$preset_name}->dirty_config; next if !$config->serial_port; my $id = &Wx::NewId(); @@ -101,7 +101,7 @@ sub OnActivate { # get all available presets my %presets = (); { - my %all = map { $_->name => $_ } wxTheApp->presets('printer'); + my %all = map { $_->name => $_ } @{wxTheApp->presets->{printer}}; my %configs = map { my $name = $_; $name => $all{$name}->load_config } keys %all; %presets = map { $_ => $configs{$_} } grep $configs{$_}->serial_port, keys %all; } diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index 9a6707b91..7b21d64c3 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -56,19 +56,8 @@ sub new { $self->SetSizer($sizer); $self->Fit; $self->SetMinSize([760, 490]); - if (defined $Slic3r::GUI::Settings->{_}{main_frame_size}) { - my $size = [ split ',', $Slic3r::GUI::Settings->{_}{main_frame_size}, 2 ]; - $self->SetSize($size); - - my $display = Wx::Display->new->GetClientArea(); - my $pos = [ split ',', $Slic3r::GUI::Settings->{_}{main_frame_pos}, 2 ]; - if (($pos->[X] + $size->[X]/2) < $display->GetRight && ($pos->[Y] + $size->[Y]/2) < $display->GetBottom) { - $self->Move($pos); - } - $self->Maximize(1) if $Slic3r::GUI::Settings->{_}{main_frame_maximized}; - } else { - $self->SetSize($self->GetMinSize); - } + $self->SetSize($self->GetMinSize); + wxTheApp->restore_window_pos($self, "main_frame"); $self->Show; $self->Layout; } @@ -78,23 +67,23 @@ sub new { my (undef, $event) = @_; if ($event->CanVeto) { - my $veto = 0; + if (!$self->{plater}->prompt_unsaved_changes) { + $event->Veto; + return; + } + if ($self->{controller} && $self->{controller}->printing) { my $confirm = Wx::MessageDialog->new($self, "You are currently printing. Do you want to stop printing and continue anyway?", 'Unfinished Print', wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); - $veto = 1 if $confirm->ShowModal == wxID_YES; - } - if ($veto) { - $event->Veto; - return; + if ($confirm->ShowModal == wxID_NO) { + $event->Veto; + return; + } } } # save window size - $Slic3r::GUI::Settings->{_}{main_frame_pos} = join ',', $self->GetScreenPositionXY; - $Slic3r::GUI::Settings->{_}{main_frame_size} = join ',', $self->GetSizeWH; - $Slic3r::GUI::Settings->{_}{main_frame_maximized} = $self->IsMaximized; - wxTheApp->save_settings; + wxTheApp->save_window_pos($self, "main_frame"); # propagate event $event->Skip; @@ -507,9 +496,9 @@ sub load_config_file { wxTheApp->save_settings; $last_config = $file; - my $preset = wxTheApp->add_external_preset($file); + my $name = wxTheApp->add_external_preset($file); $self->{plater}->load_presets; - $self->{plater}->select_preset_by_name($preset->name, $_) for qw(print filament printer); + $self->{plater}->select_preset_by_name($name, $_) for qw(print filament printer); } sub export_configbundle { @@ -536,8 +525,9 @@ sub export_configbundle { $ini->{presets} = $Slic3r::GUI::Settings->{presets}; foreach my $section (qw(print filament printer)) { - my @presets = wxTheApp->presets($section); + my @presets = @{wxTheApp->presets->{$section}}; foreach my $preset (@presets) { + next if $preset->default || $preset->external; $ini->{"$section:" . $preset->name} = $preset->load_config->as_ini->{_}; } } @@ -581,10 +571,10 @@ sub load_configbundle { next if $skip_no_id && !$config->get($section . "_settings_id"); { - my @current_presets = Slic3r::GUI->presets($section); + my @current_presets = @{wxTheApp->presets->{$section}}; my %current_ids = map { $_ => 1 } grep $_, - map $_->load_config->get($section . "_settings_id"), + map $_->dirty_config->get($section . "_settings_id"), @current_presets; next INI_BLOCK if exists $current_ids{$config->get($section . "_settings_id")}; } diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index 21c98bf78..f586c7b43 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -368,6 +368,7 @@ sub new { printer => 'Printer', ); $self->{preset_choosers} = {}; + $self->{preset_choosers_names} = {}; # wxChoice* => [] for my $group (qw(print filament printer)) { # label my $text = Wx::StaticText->new($self, -1, "$group_labels{$group}:", wxDefaultPosition, wxDefaultSize, wxALIGN_RIGHT); @@ -380,7 +381,7 @@ sub new { EVT_COMBOBOX($choice, $choice, sub { my ($choice) = @_; wxTheApp->CallAfter(sub { - $self->_on_select_preset($group); + $self->_on_change_combobox($group, $choice); }); }); @@ -512,11 +513,52 @@ sub new { return $self; } +sub prompt_unsaved_changes { + my ($self) = @_; + + foreach my $group (qw(printer filament print)) { + foreach my $choice (@{$self->{preset_choosers}{$group}}) { + my $pp = $self->{preset_choosers_names}{$choice}; + for my $i (0..$#$pp) { + my $preset = first { $_->name eq $pp->[$i] } @{wxTheApp->presets->{$group}}; + if (!$preset->prompt_unsaved_changes($self)) { + # Restore the previous one + $choice->SetSelection($i); + return 0; + } + } + } + } + return 1; +} + +sub _on_change_combobox { + my ($self, $group, $choice) = @_; + + if (0) { + # This code is disabled because wxPerl doesn't provide GetCurrentSelection + my $current_name = $self->{preset_choosers_names}{$choice}[$choice->GetCurrentSelection]; + my $current = first { $_->name eq $current_name } @{wxTheApp->presets->{$group}}; + if (!$current->prompt_unsaved_changes($self)) { + # Restore the previous one + $choice->SetSelection($choice->GetCurrentSelection); + return; + } + } else { + return 0 if !$self->prompt_unsaved_changes; + } + wxTheApp->CallAfter(sub { + $self->_on_select_preset($group); + + # This will remove the "(modified)" mark from any dirty preset handled here. + $self->load_presets; + }); +} + sub _on_select_preset { my ($self, $group) = @_; - my @presets = map $self->presets->{$group}[scalar $_->GetSelection], - @{$self->{preset_choosers}{$group}}; + my @presets = $self->selected_presets($group); $Slic3r::GUI::Settings->{presets}{$group} = $presets[0]->name; $Slic3r::GUI::Settings->{presets}{"${group}_${_}"} = $presets[$_]->name @@ -565,17 +607,16 @@ sub GetFrame { sub load_presets { my ($self) = @_; - $self->presets({}); - my $selected_printer_name; foreach my $group (qw(printer filament print)) { - my @presets = wxTheApp->presets($group); + my @presets = @{wxTheApp->presets->{$group}}; + + # Skip presets not compatible with the selected printer, if they + # have other compatible printers configured (and at least one of them exists). if ($group eq 'filament' || $group eq 'print') { - my %printer_names = map { $_->name => 1 } @{ $self->presets->{printer} }; - # Skip presets not compatible with the selected printer, if they - # have other compatible printers configured (and at least one of them exists). + my %printer_names = map { $_->name => 1 } @{ wxTheApp->presets->{printer} }; for (my $i = 0; $i <= $#presets; ++$i) { - my $config = $presets[$i]->config; + my $config = $presets[$i]->dirty_config; next if !$config->has('compatible_printers'); my @compat = @{$config->compatible_printers}; if (@compat @@ -586,14 +627,11 @@ sub load_presets { } } } - if (!@presets) { - unshift @presets, Slic3r::GUI::Preset->new( - default => 1, - name => '- default -', - ); - } - $self->presets->{$group} = [@presets]; + # Only show the default presets if we have no other presets. + if (@presets > 1) { + @presets = grep { !$_->default } @presets; + } # get the wxChoice objects for this group my @choosers = @{ $self->{preset_choosers}{$group} }; @@ -613,11 +651,12 @@ sub load_presets { # populate the wxChoice objects foreach my $choice (@choosers) { $choice->Clear; + $self->{preset_choosers_names}{$choice} = []; foreach my $preset (@presets) { # load/generate the proper icon my $bitmap; if ($group eq 'filament') { - my $config = $preset->config; + my $config = $preset->dirty_config; if ($preset->default || !$config->has('filament_colour')) { $bitmap = Wx::Bitmap->new($Slic3r::var->("spool.png"), wxBITMAP_TYPE_PNG); } else { @@ -634,7 +673,8 @@ sub load_presets { } elsif ($group eq 'printer') { $bitmap = Wx::Bitmap->new($Slic3r::var->("printer_empty.png"), wxBITMAP_TYPE_PNG); } - $choice->AppendString($preset->name, $bitmap); + $choice->AppendString($preset->dropdown_name, $bitmap); + push @{$self->{preset_choosers_names}{$choice}}, $preset->name; } my $selected = shift @sel; @@ -643,14 +683,13 @@ sub load_presets { # won't be picked up as the visible string $choice->SetSelection($selected); - my $preset_name = $choice->GetString($selected); + my $preset_name = $self->{preset_choosers_names}{$choice}[$selected]; $self->{print}->placeholder_parser->set("${group}_preset", $preset_name); + # TODO: populate other filament preset placeholders $selected_printer_name = $preset_name if $group eq 'printer'; } } } - - #$self->_on_select_preset($_) for qw(printer filament print); } sub select_preset_by_name { @@ -658,11 +697,12 @@ sub select_preset_by_name { # $n is optional - my $presets = $self->presets->{$group}; - my $i = first { $presets->[$_]->name eq $name } 0..$#$presets; + my $presets = wxTheApp->presets->{$group}; + my $choosers = $self->{preset_choosers}{$group}; + my $names = $self->{preset_choosers_names}{$choosers->[0]}; + my $i = first { $names->[$_] eq $name } 0..$#$names; return if !defined $i; - my $choosers = $self->{preset_choosers}{$group}; if (defined $n && $n <= $#$choosers) { $choosers->[$n]->SetSelection($i); } else { @@ -674,10 +714,16 @@ sub select_preset_by_name { sub selected_presets { my ($self, $group) = @_; - my %presets; + my %presets = (); foreach my $group (qw(printer filament print)) { - my @i = map scalar($_->GetSelection), @{ $self->{preset_choosers}{$group} }; - $presets{$group} = [ @{$self->presets->{$group}}[@i] ]; + $presets{$group} = []; + foreach my $choice (@{$self->{preset_choosers}{$group}}) { + my $sel = $choice->GetSelection; + $sel = 0 if $sel == -1; + push @{ $presets{$group} }, + grep { $_->name eq $self->{preset_choosers_names}{$choice}[$sel] } + @{wxTheApp->presets->{$group}}; + } } return $group ? @{$presets{$group}} : %presets; } @@ -697,7 +743,7 @@ sub show_preset_editor { # Select the preset that was last selected in the editor. $self->select_preset_by_name - ($dlg->preset_editor->get_current_preset->name, $group, $i, 1); + ($dlg->preset_editor->current_preset->name, $group, $i, 1); } # Returns the current config by merging the selected presets and the overrides. @@ -715,16 +761,13 @@ sub config { qw(print filament printer); my %presets = $self->selected_presets; - $config->apply($_->config([ $classes{printer}->options ])) - for @{ $presets{printer} }; + $config->apply($_->dirty_config) for @{ $presets{printer} }; if (@{ $presets{filament} }) { - my @opt_keys = ($classes{filament}->options, $classes{filament}->overriding_options); - my $filament_config = shift(@{ $presets{filament} }) - ->config(\@opt_keys); + my $filament_config = $presets{filament}[0]->dirty_config; - my $i = 1; - for my $preset (@{ $presets{filament} }) { - my $config = $preset->config(\@opt_keys); + for my $i (1..$#{ $presets{filament} }) { + my $preset = $presets{filament}[$i]; + my $config = $preset->dirty_config; foreach my $opt_key (@{$config->get_keys}) { if ($filament_config->has($opt_key)) { my $value = $filament_config->get($opt_key); @@ -733,14 +776,11 @@ sub config { $filament_config->set($opt_key, $value); } } - ++$i; } $config->apply($filament_config); } - $config->apply($_->config([ $classes{print}->options ])) - for @{ $presets{print} }; - + $config->apply($_->dirty_config) for @{ $presets{print} }; $config->apply($self->{settings_override_config}); return $config; @@ -1896,7 +1936,7 @@ sub on_extruders_change { EVT_COMBOBOX($choice, $choice, sub { my ($choice) = @_; wxTheApp->CallAfter(sub { - $self->_on_select_preset('filament'); + $self->_on_change_combobox('filament', $choice); }); }); diff --git a/lib/Slic3r/GUI/Plater/OverrideSettingsPanel.pm b/lib/Slic3r/GUI/Plater/OverrideSettingsPanel.pm index 9549807a8..781e0fb6d 100644 --- a/lib/Slic3r/GUI/Plater/OverrideSettingsPanel.pm +++ b/lib/Slic3r/GUI/Plater/OverrideSettingsPanel.pm @@ -71,7 +71,7 @@ sub new { EVT_MENU($menu, $id, sub { $self->{config}->set($opt_key, $self->{default_config}->get($opt_key)); $self->update_optgroup; - $self->{on_change}->() if $self->{on_change}; + $self->{on_change}->($opt_key) if $self->{on_change}; }); } $self->PopupMenu($menu, $btn->GetPosition); @@ -155,7 +155,10 @@ sub update_optgroup { 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} }, + on_change => sub { + my ($opt_key) = @_; + $self->{on_change}->($opt_key) if $self->{on_change}; + }, extra_column => sub { my ($line) = @_; @@ -169,7 +172,7 @@ sub update_optgroup { wxDefaultPosition, wxDefaultSize, Wx::wxBORDER_NONE); EVT_BUTTON($self, $btn, sub { $self->{config}->erase($opt_key); - $self->{on_change}->() if $self->{on_change}; + $self->{on_change}->($opt_key) if $self->{on_change}; wxTheApp->CallAfter(sub { $self->update_optgroup }); }); return $btn; diff --git a/lib/Slic3r/GUI/Preset.pm b/lib/Slic3r/GUI/Preset.pm index d855c3c60..5bdd0f20e 100644 --- a/lib/Slic3r/GUI/Preset.pm +++ b/lib/Slic3r/GUI/Preset.pm @@ -1,11 +1,16 @@ package Slic3r::GUI::Preset; use Moo; -use Unicode::Normalize; +use Unicode::Normalize; +use Wx qw(:dialog :icon :id wxTheApp); + +has 'group' => (is => 'ro', required => 1); has 'default' => (is => 'ro', default => sub { 0 }); has 'external' => (is => 'ro', default => sub { 0 }); has 'name' => (is => 'rw', required => 1); has 'file' => (is => 'rw'); +has '_config' => (is => 'rw', default => sub { Slic3r::Config->new }); +has '_dirty_config' => (is => 'ro', default => sub { Slic3r::Config->new }); sub BUILD { my ($self) = @_; @@ -13,40 +18,193 @@ sub BUILD { $self->name(Unicode::Normalize::NFC($self->name)); } -sub config { - my ($self, $keys, $extra_keys) = @_; +sub _loaded { + my ($self) = @_; - if ($self->default) { - return Slic3r::Config->new_from_defaults(@$keys); - } else { - if (!-e Slic3r::encode_path($self->file)) { - Slic3r::GUI::show_error(undef, "The selected preset does not exist anymore (" . $self->file . ")."); - return undef; - } - my $external_config = $self->load_config; - if (!$keys) { - return $external_config; - } else { - # apply preset values on top of defaults - my $config = Slic3r::Config->new_from_defaults(@$keys); - $config->set($_, $external_config->get($_)) - for grep $external_config->has($_), @$keys; - - # For extra_keys we don't populate defaults. - if ($extra_keys && !$self->external) { - $config->set($_, $external_config->get($_)) - for grep $external_config->has($_), @$extra_keys; - } + return !$self->_config->empty; +} + +sub dirty_options { + my ($self) = @_; + + my @dirty = (); + + # Options present in both configs with different values: + push @dirty, @{$self->_config->diff($self->_dirty_config)}; + + # Overrides added to the dirty config: + my @extra = $self->_group_class->overriding_options; + push @dirty, grep { !$self->_config->has($_) && $self->_dirty_config->has($_) } @extra; + # Overrides removed from the dirty config: + push @dirty, grep { $self->_config->has($_) && !$self->_dirty_config->has($_) } @extra; + + return @dirty; +} + +sub dirty { + my ($self) = @_; + + return !!$self->dirty_options; +} + +sub dropdown_name { + my ($self) = @_; + + my $name = $self->name; + $name .= " (modified)" if $self->dirty; + return $name; +} + +sub file_exists { + my ($self) = @_; + + die "Can't call file_exists() on a non-file preset" if !$self->file; + return -e Slic3r::encode_path($self->file); +} + +sub rename { + my ($self, $name) = @_; + + $self->name($name); + $self->file(sprintf "$Slic3r::GUI::datadir/%s/%s.ini", $self->group, $name); +} + +sub prompt_unsaved_changes { + my ($self, $parent) = @_; + + if ($self->dirty) { + my $name = $self->default ? 'Default preset' : "Preset \"" . $self->name . "\""; - return $config; + my $opts = ''; + foreach my $opt_key ($self->dirty_options) { + my $opt = $Slic3r::Config::Options->{$opt_key}; + my $name = $opt->{full_label} // $opt->{label}; + if ($opt->{category}) { + $name = $opt->{category} . " > $name"; + } + $opts .= "- $name\n"; + } + + my $msg = sprintf "%s has unsaved changes:\n%s\nDo you want to save them?", $name, $opts; + my $confirm = Wx::MessageDialog->new($parent, $msg, + 'Unsaved Changes', wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxICON_QUESTION); + $confirm->SetYesNoCancelLabels('Save', 'Discard', 'Cancel'); + my $res = $confirm->ShowModal; + + if ($res == wxID_CANCEL) { + return 0; + } elsif ($res == wxID_YES) { + return $self->save($self->default ? undef : $self->name); + } elsif ($res == wxID_NO) { + $self->dismiss_changes; + return 1; } } + + return 1; +} + +sub save { + my ($self, $name, $parent) = @_; + + if (!$name) { + my $default_name = $self->default ? 'Untitled' : $self->name; + $default_name =~ s/\.ini$//i; + + my $dlg = Slic3r::GUI::SavePresetWindow->new($parent, + default => $default_name, + values => [ map $_->name, grep !$_->default && !$_->external, @{wxTheApp->presets->{$self->name}} ], + ); + return 0 unless $dlg->ShowModal == wxID_OK; + $name = $dlg->get_name; + } + + $self->rename($name); + + if (!$self->file) { + die "Calling save() without setting filename"; + } + + $self->_config->clear; + $self->_config->apply($self->_dirty_config); + $self->_config->save($self->file); + wxTheApp->load_presets; + + return 1; +} + +sub dismiss_changes { + my ($self) = @_; + + $self->_dirty_config->clear; + $self->_dirty_config->apply($self->_config); +} + +sub delete { + my ($self) = @_; + + die "Default config can't be deleted" if $self->default; + die "External configs can't be deleted" if $self->external; + + # Otherwise wxTheApp->load_presets() will keep it + $self->dismiss_changes; + + if ($self->file) { + unlink Slic3r::encode_path($self->file) if $self->file_exists; + $self->file(undef); + } +} + +# This returns the loaded config with the dirty options applied. +sub dirty_config { + my ($self) = @_; + + $self->load_config if !$self->_loaded; + + return $self->_dirty_config->clone; } sub load_config { my ($self) = @_; - return Slic3r::Config->load($self->file); + my @keys = $self->_group_class->options; + my @extra_keys = $self->_group_class->overriding_options; + + if ($self->default) { + $self->_config(Slic3r::Config->new_from_defaults(@keys)); + } elsif ($self->file) { + if (!$self->file_exists) { + Slic3r::GUI::show_error(undef, "The selected preset does not exist anymore (" . $self->file . ")."); + return undef; + } + my $external_config = Slic3r::Config->load($self->file); + if (!@keys) { + $self->_config($external_config); + } else { + # apply preset values on top of defaults + my $config = Slic3r::Config->new_from_defaults(@keys); + $config->set($_, $external_config->get($_)) + for grep $external_config->has($_), @keys; + + # For extra_keys we don't populate defaults. + if (@extra_keys && !$self->external) { + $config->set($_, $external_config->get($_)) + for grep $external_config->has($_), @extra_keys; + } + + $self->_config($config); + } + } + + $self->_dirty_config->apply($self->_config); + + return $self->_config; +} + +sub _group_class { + my ($self) = @_; + + return "Slic3r::GUI::PresetEditor::".ucfirst $self->group; } 1; diff --git a/lib/Slic3r/GUI/PresetEditor.pm b/lib/Slic3r/GUI/PresetEditor.pm index 63bc52981..758810490 100644 --- a/lib/Slic3r/GUI/PresetEditor.pm +++ b/lib/Slic3r/GUI/PresetEditor.pm @@ -10,13 +10,15 @@ use Wx qw(:bookctrl :dialog :keycode :icon :id :misc :panel :sizer :treectrl :wi use Wx::Event qw(EVT_BUTTON EVT_CHOICE EVT_KEY_DOWN EVT_TREE_SEL_CHANGED); use base qw(Wx::Panel Class::Accessor); -__PACKAGE__->mk_accessors(qw(current_preset)); +__PACKAGE__->mk_accessors(qw(current_preset config)); sub new { my $class = shift; my ($parent, %params) = @_; my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); + $self->{presets} = wxTheApp->presets->{$self->name}; + # horizontal sizer $self->{sizer} = Wx::BoxSizer->new(wxHORIZONTAL); #$self->{sizer}->SetSizeHints($self); @@ -91,78 +93,39 @@ sub new { EVT_BUTTON($self, $self->{btn_save_preset}, sub { $self->save_preset }); EVT_BUTTON($self, $self->{btn_delete_preset}, sub { - my $i = $self->current_preset; - return if $i == 0; # this shouldn't happen but let's trap it anyway my $res = Wx::MessageDialog->new($self, "Are you sure you want to delete the selected preset?", 'Delete Preset', wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION)->ShowModal; return unless $res == wxID_YES; - if (-e $self->{presets}[$i]->file) { - unlink $self->{presets}[$i]->file; - } - splice @{$self->{presets}}, $i, 1; - $self->{presets_choice}->Delete($i); + + $self->current_preset->delete; $self->current_preset(undef); - $self->select_preset(0); + wxTheApp->load_presets; + $self->load_presets; + $self->select_preset(0, 1); }); - # C++ instance DynamicPrintConfig - $self->{config} = Slic3r::Config->new; - # Initialize the DynamicPrintConfig by default keys/values. - $self->{config}->apply(Slic3r::Config->new_from_defaults($self->options)); + $self->config(Slic3r::Config->new_from_defaults($self->options)); - - - # Possible %params keys: no_controller - $self->build(%params); + $self->build; $self->update_tree; + $self->load_presets; $self->_update; - if ($self->hidden_options) { - $self->{config}->apply(Slic3r::Config->new_from_defaults($self->hidden_options)); - } return $self; } -sub get_current_preset { - my $self = shift; - return $self->get_preset($self->current_preset); -} - -sub get_current_preset_config { - my ($self) = @_; - - return $self->get_preset_config($self->get_current_preset); -} - -sub get_preset { - my ($self, $i) = @_; - return $self->{presets}[$i]; -} - +# This is called by the save button. sub save_preset { - my ($self, $name) = @_; + my ($self) = @_; # since buttons (and choices too) don't get focus on Mac, we set focus manually # to the treectrl so that the EVT_* events are fired for the input field having # focus currently. is there anything better than this? $self->{treectrl}->SetFocus; - if (!defined $name) { - my $preset = $self->get_current_preset; - my $default_name = $preset->default ? 'Untitled' : $preset->name; - $default_name =~ s/\.ini$//i; - - my $dlg = Slic3r::GUI::SavePresetWindow->new($self, - title => lc($self->title), - default => $default_name, - values => [ map $_->name, grep !$_->default && !$_->external, @{$self->{presets}} ], - ); - return 0 unless $dlg->ShowModal == wxID_OK; - $name = $dlg->get_name; - } - - $self->config->save(sprintf "$Slic3r::GUI::datadir/%s/%s.ini", $self->name, $name); + my $preset = $self->current_preset; + $preset->save(undef, $self); $self->load_presets; - $self->select_preset_by_name($name); + $self->select_preset_by_name($preset->name); return 1; } @@ -176,17 +139,17 @@ sub on_value_change { # or changed by user (so also when a preset is loaded). # propagate event to the parent sub _on_value_change { - my $self = shift; + my ($self, $opt_key) = @_; - $self->{on_value_change}->(@_) if $self->{on_value_change}; + $self->current_preset->_dirty_config->apply($self->config); + $self->{on_value_change}->($opt_key) if $self->{on_value_change}; + $self->load_presets; $self->_update; } sub _update {} sub on_preset_loaded {} -sub hidden_options {} -sub config { $_[0]->{config}->clone } sub select_preset { my ($self, $i, $force) = @_; @@ -198,62 +161,54 @@ sub select_preset { sub select_preset_by_name { my ($self, $name, $force) = @_; - my $i = first { $self->{presets}[$_]->name eq $name } 0 .. $#{$self->{presets}}; - return if !defined $i; - $self->select_preset($i, $force); + my $presets = wxTheApp->presets->{$self->name}; + my $i = first { $presets->[$_]->name eq $name } 0..$#$presets; + $self->{presets_choice}->SetSelection($i); + $self->on_select_preset($force); } sub prompt_unsaved_changes { my ($self) = @_; - if ($self->is_dirty) { - my $old_preset = $self->get_current_preset; - my $name = $old_preset->default ? 'Default preset' : "Preset \"" . $old_preset->name . "\""; - - my @option_names = (); - foreach my $opt_key (@{$self->dirty_options}) { - my $opt = $Slic3r::Config::Options->{$opt_key}; - my $name = $opt->{full_label} // $opt->{label}; - if ($opt->{category}) { - $name = $opt->{category} . " > $name"; - } - push @option_names, $name; - } - - my $changes = join "\n", map "- $_", @option_names; - my $confirm = Wx::MessageDialog->new($self, "$name has unsaved changes:\n$changes\n\nDo you want to save them?", - 'Unsaved Changes', wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxICON_QUESTION); - $confirm->SetYesNoCancelLabels('Save', 'Discard', 'Cancel'); - my $res = $confirm->ShowModal; - - if ($res == wxID_CANCEL) { - return 0; - } elsif ($res == wxID_YES) { - return $self->save_preset($old_preset->default ? undef : $old_preset->name); - } - } - - return 1; + return 1 if !$self->current_preset; + return $self->current_preset->prompt_unsaved_changes($self); } sub on_select_preset { my ($self, $force) = @_; - my $i = $self->{presets_choice}->GetSelection; - return if defined $self->current_preset && $i == $self->current_preset; + # This method is called: + # - upon first initialization; + # - whenever user selects a preset from the dropdown; + # - whenever select_preset() or select_preset_by_name() are called. - if (!$force && defined $self->current_preset && !$self->prompt_unsaved_changes) { - $self->{presets_choice}->SetSelection($self->current_preset); + # Get the selected name. + my $preset = wxTheApp->presets->{$self->name}->[$self->{presets_choice}->GetSelection]; + + # If selection didn't change, do nothing. + return if defined $self->current_preset && $preset->name eq $self->current_preset->name; + + # If we have unsaved changes, prompt user. + if (!$force && !$self->prompt_unsaved_changes) { + # User decided not to save the current changes, so we restore the previous selection. + my $presets = wxTheApp->presets->{$self->name}; + my $i = first { $presets->[$_]->name eq $self->current_preset->name } 0..$#$presets; + $self->{presets_choice}->SetSelection($i); return; } - $self->current_preset($i); - my $preset = $self->get_current_preset; - my $preset_config = $self->get_preset_config($preset); + $self->current_preset($preset); + + # We reload presets in order to remove the "(modified)" suffix in case user was + # prompted and chose to discard changes. + $self->load_presets; + + $preset->load_config if !$preset->_loaded; + $self->config->clear; + $self->config->apply($preset->dirty_config); + eval { local $SIG{__WARN__} = Slic3r::GUI::warning_catcher($self); - $self->{config}->clear; - $self->{config}->apply($preset_config); ($preset->default || $preset->external) ? $self->{btn_delete_preset}->Disable : $self->{btn_delete_preset}->Enable; @@ -266,14 +221,6 @@ sub on_select_preset { $@ = "I was unable to load the selected config file: $@"; Slic3r::GUI::catch_error($self); } - - # use CallAfter because some field triggers schedule on_change calls using CallAfter, - # and we don't want them to be called after this update_dirty() as they would mark the - # preset dirty again - # (not sure this is true anymore now that update_dirty is idempotent) - wxTheApp->CallAfter(sub { - $self->update_dirty; - }); } sub add_options_page { @@ -323,89 +270,35 @@ sub update_tree { } } -sub update_dirty { - my $self = shift; - - foreach my $i (0..$#{$self->{presets}}) { - my $preset = $self->get_preset($i); - if ($i == $self->current_preset && $self->is_dirty) { - $self->{presets_choice}->SetString($i, $preset->name . " (modified)"); - } else { - $self->{presets_choice}->SetString($i, $preset->name); - } - } - $self->{presets_choice}->SetSelection($self->current_preset); # http://trac.wxwidgets.org/ticket/13769 -} - -sub is_dirty { - my $self = shift; - return @{$self->dirty_options} > 0; -} - -sub dirty_options { - my $self = shift; - - return [] if !defined $self->current_preset; # happens during initialization - - my $preset_config = $self->get_current_preset_config; - return [ - (grep !$preset_config->has($_), @{$self->{config}->get_keys}), - (grep !$self->{config}->has($_), @{$preset_config->get_keys}), - @{ $self->{config}->diff($self->get_current_preset_config) }, - ]; -} - sub load_presets { my $self = shift; - $self->{presets} = [ wxTheApp->presets($self->name, 1) ]; - $self->current_preset(undef); + my $presets = wxTheApp->presets->{$self->name}; $self->{presets_choice}->Clear; - $self->{presets_choice}->Append($_->name) for @{$self->{presets}}; - $self->select_preset(0); -} - -sub load_config_file { - my $self = shift; - my ($file) = @_; - - # look for the loaded config among the existing menu items - my $i = first { $self->{presets}[$_]{file} eq $file && $self->{presets}[$_]{external} } 1..$#{$self->{presets}}; - if (!$i) { - my $preset_name = basename($file); # keep the .ini suffix - push @{$self->{presets}}, Slic3r::GUI::Preset->new( - file => $file, - name => $preset_name, - external => 1, - ); - $self->{presets_choice}->Append($preset_name); - $i = $#{$self->{presets}}; + foreach my $preset (@$presets) { + $self->{presets_choice}->Append($preset->dropdown_name); + + # Preserve selection. + if ($self->current_preset && $self->current_preset->name eq $preset->name) { + $self->{presets_choice}->SetSelection($self->{presets_choice}->GetCount-1); + } } - $self->{presets_choice}->SetSelection($i); - $self->on_select_preset; } -sub load_config { +# This is called internally whenever we make automatic adjustments to configuration +# based on user actions. +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->update_dirty; - } + my $diff = $self->config->diff($config); + $self->config->set($_, $config->get($_)) for @$diff; + # First apply all changes, then call all the _on_value_change triggers. + $self->_on_value_change($_) for @$diff; $self->reload_config; $self->_update; } -sub get_preset_config { - my ($self, $preset) = @_; - - return $preset->config( - [ $self->options ], - [ $self->overriding_options ], - ); -} - sub get_field { my ($self, $opt_key, $opt_index) = @_; @@ -452,16 +345,15 @@ sub _compatible_printers_widget { "Compatible printers", \@presets); my @selections = (); - foreach my $preset_name (@{ $self->{config}->get('compatible_printers') }) { + foreach my $preset_name (@{ $self->config->get('compatible_printers') }) { push @selections, first { $presets[$_] eq $preset_name } 0..$#presets; } $dlg->SetSelections(@selections); if ($dlg->ShowModal == wxID_OK) { my $value = [ @presets[$dlg->GetSelections] ]; - $self->{config}->set('compatible_printers', $value); - $self->update_dirty; - $self->_on_value_change('compatible_printers', $value); + $self->config->set('compatible_printers', $value); + $self->_on_value_change('compatible_printers'); } }); @@ -554,16 +446,15 @@ sub build { [ map $options{$_}, @opt_keys ]); my @selections = (); - foreach my $opt_key (@{ $self->{config}->get('overridable') }) { + foreach my $opt_key (@{ $self->config->get('overridable') }) { push @selections, first { $opt_keys[$_] eq $opt_key } 0..$#opt_keys; } $dlg->SetSelections(@selections); if ($dlg->ShowModal == wxID_OK) { my $value = [ @opt_keys[$dlg->GetSelections] ]; - $self->{config}->set('overridable', $value); - $self->update_dirty; - $self->_on_value_change('overridable', $value); + $self->config->set('overridable', $value); + $self->_on_value_change('overridable'); } }); @@ -850,11 +741,11 @@ sub _update { $new_conf->set("top_solid_layers", 0); $new_conf->set("fill_density", 0); $new_conf->set("support_material", 0); - $self->load_config($new_conf); + $self->_load_config($new_conf); } else { my $new_conf = Slic3r::Config->new; $new_conf->set("spiral_vase", 0); - $self->load_config($new_conf); + $self->_load_config($new_conf); } } @@ -880,7 +771,7 @@ sub _update { $new_conf->set("support_material", 0); $self->{support_material_overhangs_queried} = 0; } - $self->load_config($new_conf); + $self->_load_config($new_conf); } } } else { @@ -900,7 +791,7 @@ sub _update { } else { $new_conf->set("fill_density", 40); } - $self->load_config($new_conf); + $self->_load_config($new_conf); } my $have_perimeters = $config->perimeters > 0; @@ -975,8 +866,6 @@ sub _update { for qw(standby_temperature_delta); } -sub hidden_options { !$Slic3r::have_threads ? qw(threads) : () } - package Slic3r::GUI::PresetEditor::Filament; use base 'Slic3r::GUI::PresetEditor'; @@ -1150,9 +1039,11 @@ sub build { $self->{overrides_panel} = my $panel = Slic3r::GUI::Plater::OverrideSettingsPanel->new($parent, size => [-1, 300], on_change => sub { - $self->{config}->erase($_) for @overridable; - $self->{config}->apply($self->{overrides_config}); - $self->update_dirty; + my ($opt_key) = @_; + $self->config->erase($_) for @overridable; + $self->current_preset->_dirty_config->erase($_) for @overridable; + $self->config->apply($self->{overrides_config}); + $self->_on_value_change($opt_key); }); $panel->set_editable(1); $panel->set_default_config($self->{overrides_default_config}); @@ -1172,8 +1063,8 @@ sub reload_config { $self->{overrides_config}->clear; foreach my $opt_key (@{$self->{overrides_default_config}->get_keys}) { - if ($self->{config}->has($opt_key)) { - $self->{overrides_config}->set($opt_key, $self->{config}->get($opt_key)); + if ($self->config->has($opt_key)) { + $self->{overrides_config}->set($opt_key, $self->config->get($opt_key)); } } $self->{overrides_panel}->update_optgroup; @@ -1186,10 +1077,10 @@ sub _update { $self->_update_description; - my $cooling = $self->{config}->cooling; + my $cooling = $self->config->cooling; $self->get_field($_)->toggle($cooling) for qw(max_fan_speed fan_below_layer_time slowdown_below_layer_time min_print_speed); - $self->get_field($_)->toggle($cooling || $self->{config}->fan_always_on) + $self->get_field($_)->toggle($cooling || $self->config->fan_always_on) for qw(min_fan_speed disable_fan_first_layers); } @@ -1269,12 +1160,11 @@ sub build { label => 'Bed shape', ); $line->append_button("Set…", "cog.png", sub { - my $dlg = Slic3r::GUI::BedShapeDialog->new($self, $self->{config}->bed_shape); + my $dlg = Slic3r::GUI::BedShapeDialog->new($self, $self->config->bed_shape); if ($dlg->ShowModal == wxID_OK) { my $value = $dlg->GetValue; - $self->{config}->set('bed_shape', $value); - $self->update_dirty; - $self->_on_value_change('bed_shape', $value); + $self->config->set('bed_shape', $value); + $self->_on_value_change('bed_shape'); } }); $optgroup->append_line($line); @@ -1301,7 +1191,6 @@ sub build { wxTheApp->CallAfter(sub { $self->_extruders_count_changed($optgroup->get_value('extruders_count')); }); - $self->update_dirty; } }); } @@ -1328,8 +1217,8 @@ sub build { $line->append_button("Test", "wrench.png", sub { my $sender = Slic3r::GCode::Sender->new; my $res = $sender->connect( - $self->{config}->serial_port, - $self->{config}->serial_speed, + $self->config->serial_port, + $self->config->serial_speed, ); if ($res && $sender->wait_connected) { Slic3r::GUI::show_info($self, "Connection to printer works correctly.", "Success!"); @@ -1355,10 +1244,8 @@ sub build { my $dlg = Slic3r::GUI::BonjourBrowser->new($self, $entries); if ($dlg->ShowModal == wxID_OK) { my $value = $dlg->GetValue . ":" . $dlg->GetPort; - $self->{config}->set('octoprint_host', $value); - $self->update_dirty; - $self->_on_value_change('octoprint_host', $value); - $self->reload_config; + $self->config->set('octoprint_host', $value); + $self->_on_value_change('octoprint_host'); } } else { Wx::MessageDialog->new($self, 'No Bonjour device found', 'Device Browser', wxOK | wxICON_INFORMATION)->ShowModal; @@ -1369,8 +1256,8 @@ sub build { $ua->timeout(10); my $res = $ua->get( - "http://" . $self->{config}->octoprint_host . "/api/version", - 'X-Api-Key' => $self->{config}->octoprint_apikey, + "http://" . $self->config->octoprint_host . "/api/version", + 'X-Api-Key' => $self->config->octoprint_apikey, ); if ($res->is_success) { Slic3r::GUI::show_info($self, "Connection to OctoPrint works correctly.", "Success!"); @@ -1462,7 +1349,7 @@ sub _extruders_count_changed { $self->{extruders_count} = $extruders_count; $self->_build_extruder_pages; - $self->_on_value_change('extruders_count', $extruders_count); + $self->_on_value_change('extruders_count'); $self->_update; } @@ -1477,7 +1364,7 @@ sub _build_extruder_pages { foreach my $extruder_idx (@{$self->{extruder_pages}} .. $self->{extruders_count}-1) { # extend options foreach my $opt_key ($self->_extruder_options) { - my $values = $self->{config}->get($opt_key); + my $values = $self->config->get($opt_key); if (!defined $values) { $values = [ $default_config->get_at($opt_key, 0) ]; } else { @@ -1485,7 +1372,7 @@ sub _build_extruder_pages { my $last_value = $values->[-1]; $values->[$extruder_idx] //= $last_value; } - $self->{config}->set($opt_key, $values) + $self->config->set($opt_key, $values) or die "Unable to extend $opt_key"; } @@ -1531,9 +1418,9 @@ sub _build_extruder_pages { # remove extra config values foreach my $opt_key ($self->_extruder_options) { - my $values = $self->{config}->get($opt_key); + my $values = $self->config->get($opt_key); splice @$values, $self->{extruders_count} if $self->{extruders_count} <= $#$values; - $self->{config}->set($opt_key, $values) + $self->config->set($opt_key, $values) or die "Unable to truncate $opt_key"; } @@ -1610,7 +1497,7 @@ sub _update { } else { $new_conf->set("use_firmware_retraction", 0); } - $self->load_config($new_conf); + $self->_load_config($new_conf); } $self->get_field('retract_length_toolchange', $i)->toggle($have_multiple_extruders); @@ -1628,7 +1515,7 @@ sub on_preset_loaded { # update the extruders count field { # update the GUI field according to the number of nozzle diameters supplied - my $extruders_count = scalar @{ $self->{config}->nozzle_diameter }; + my $extruders_count = scalar @{ $self->config->nozzle_diameter }; $self->set_value('extruders_count', $extruders_count); $self->_extruders_count_changed($extruders_count); } @@ -1642,7 +1529,7 @@ sub load_config_file { "Your configuration was imported. However, Slic3r is currently only able to import settings " . "for the first defined filament. We recommend you don't use exported configuration files " . "for multi-extruder setups and rely on the built-in preset management system instead.") - if @{ $self->{config}->nozzle_diameter } > 1; + if @{ $self->config->nozzle_diameter } > 1; } package Slic3r::GUI::PresetEditor::Page; @@ -1676,8 +1563,7 @@ sub new_optgroup { on_change => sub { my ($opt_key, $value) = @_; wxTheApp->CallAfter(sub { - $self->GetParent->update_dirty; - $self->GetParent->_on_value_change($opt_key, $value); + $self->GetParent->_on_value_change($opt_key); }); }, ); @@ -1726,7 +1612,7 @@ sub new { my @values = @{$params{values}}; - my $text = Wx::StaticText->new($self, -1, "Save " . lc($params{title}) . " as:", wxDefaultPosition, wxDefaultSize); + my $text = Wx::StaticText->new($self, -1, "Save profile as:", wxDefaultPosition, wxDefaultSize); $self->{combo} = Wx::ComboBox->new($self, -1, $params{default}, wxDefaultPosition, wxDefaultSize, \@values, wxTE_PROCESS_ENTER); my $buttons = $self->CreateStdDialogButtonSizer(wxOK | wxCANCEL); diff --git a/lib/Slic3r/GUI/PresetEditorDialog.pm b/lib/Slic3r/GUI/PresetEditorDialog.pm index d706622a3..6385844c5 100644 --- a/lib/Slic3r/GUI/PresetEditorDialog.pm +++ b/lib/Slic3r/GUI/PresetEditorDialog.pm @@ -1,8 +1,8 @@ package Slic3r::GUI::PresetEditorDialog; use strict; use warnings; -use Wx qw(:dialog :id :misc :sizer :button :icon wxTheApp); -use Wx::Event qw(EVT_CLOSE); +use Wx qw(:dialog :id :misc :sizer :button :icon wxTheApp WXK_ESCAPE); +use Wx::Event qw(EVT_CLOSE EVT_CHAR_HOOK); use base qw(Wx::Dialog Class::Accessor); use utf8; @@ -15,7 +15,6 @@ sub new { $self->preset_editor($self->preset_editor_class->new($self)); $self->SetTitle($self->preset_editor->title); - $self->preset_editor->load_presets; my $sizer = Wx::BoxSizer->new(wxVERTICAL); $sizer->Add($self->preset_editor, 1, wxEXPAND); @@ -24,23 +23,32 @@ sub new { #$sizer->SetSizeHints($self); if (0) { - # This does not call the EVT_CLOSE below my $buttons = $self->CreateStdDialogButtonSizer(wxCLOSE); $sizer->Add($buttons, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10); } + wxTheApp->restore_window_pos($self, "preset_editor"); + EVT_CLOSE($self, sub { my (undef, $event) = @_; - if ($event->CanVeto && !$self->preset_editor->prompt_unsaved_changes) { - $event->Veto; - return; - } + # save window size + wxTheApp->save_window_pos($self, "preset_editor"); # propagate event $event->Skip; }); + EVT_CHAR_HOOK($self, sub { + my (undef, $event) = @_; + + if ($event->GetKeyCode == WXK_ESCAPE) { + $self->Close; + } else { + $event->Skip; + } + }); + return $self; } diff --git a/xs/src/libslic3r/Config.cpp b/xs/src/libslic3r/Config.cpp index b5565ea76..f18e4e025 100644 --- a/xs/src/libslic3r/Config.cpp +++ b/xs/src/libslic3r/Config.cpp @@ -532,6 +532,11 @@ DynamicConfig::clear() { this->options.clear(); } +bool +DynamicConfig::empty() const { + return this->options.empty(); +} + void DynamicConfig::read_cli(const std::vector &tokens, t_config_option_keys* extra) { diff --git a/xs/src/libslic3r/Config.hpp b/xs/src/libslic3r/Config.hpp index 45ba8a3ba..41d3cc668 100644 --- a/xs/src/libslic3r/Config.hpp +++ b/xs/src/libslic3r/Config.hpp @@ -678,6 +678,7 @@ class DynamicConfig : public virtual ConfigBase t_config_option_keys keys() const; void erase(const t_config_option_key &opt_key); void clear(); + bool empty() const; void read_cli(const std::vector &tokens, t_config_option_keys* extra); void read_cli(const int argc, const char **argv, t_config_option_keys* extra); diff --git a/xs/xsp/Config.xsp b/xs/xsp/Config.xsp index 25da96477..6744a1e10 100644 --- a/xs/xsp/Config.xsp +++ b/xs/xsp/Config.xsp @@ -40,6 +40,7 @@ %name{get_keys} std::vector keys(); void erase(t_config_option_key opt_key); void clear(); + bool empty(); void normalize(); %name{setenv} void setenv_(); double min_object_distance();