Allow dirty presets, and many more improvements including remembering the preset dialog position and closing it with the Esc key

This commit is contained in:
Alessandro Ranellucci 2017-03-22 19:47:23 +01:00
parent 9a9597df83
commit 4ca5f9ee16
11 changed files with 497 additions and 359 deletions

View File

@ -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}; }
my @presets = ();
sub load_presets {
my ($self) = @_;
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;
for my $group (qw(printer filament print)) {
my $presets = $self->{presets}{$group};
@presets = sort { $a->name cmp $b->name }
@presets,
(grep -e $_->file, @{$self->{external_presets}});
# keep external or dirty presets
@$presets = grep { ($_->external && $_->file_exists) || $_->dirty } @$presets;
if ($force_default || !@presets) {
unshift @presets, Slic3r::GUI::Preset->new(
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;
@$presets = sort { $a->name cmp $b->name } @$presets;
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;

View File

@ -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;
}

View File

@ -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")};
}

View File

@ -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);
});
});

View File

@ -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;

View File

@ -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;
return !$self->_config->empty;
}
# 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;
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 . "\"";
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";
}
return $config;
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;

View File

@ -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);
}
foreach my $preset (@$presets) {
$self->{presets_choice}->Append($preset->dropdown_name);
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}};
# 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);

View File

@ -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;
}

View File

@ -532,6 +532,11 @@ DynamicConfig::clear() {
this->options.clear();
}
bool
DynamicConfig::empty() const {
return this->options.empty();
}
void
DynamicConfig::read_cli(const std::vector<std::string> &tokens, t_config_option_keys* extra)
{

View File

@ -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<std::string> &tokens, t_config_option_keys* extra);
void read_cli(const int argc, const char **argv, t_config_option_keys* extra);

View File

@ -40,6 +40,7 @@
%name{get_keys} std::vector<std::string> keys();
void erase(t_config_option_key opt_key);
void clear();
bool empty();
void normalize();
%name{setenv} void setenv_();
double min_object_distance();