diff --git a/MatterControl.Printing/Settings/SettingsKey.cs b/MatterControl.Printing/Settings/SettingsKey.cs index 90a2a7405..76dc738d2 100644 --- a/MatterControl.Printing/Settings/SettingsKey.cs +++ b/MatterControl.Printing/Settings/SettingsKey.cs @@ -152,5 +152,15 @@ namespace MatterHackers.MatterControl.SlicerConfiguration public const string z_probe_z_offset = nameof(z_probe_z_offset); public const string z_servo_depolyed_angle = nameof(z_servo_depolyed_angle); public const string z_servo_retracted_angle = nameof(z_servo_retracted_angle); + public const string small_perimeter_speed = nameof(small_perimeter_speed); + public const string bridge_speed = nameof(bridge_speed); + public const string air_gap_speed = nameof(air_gap_speed); + public const string external_perimeter_speed = nameof(external_perimeter_speed); + public const string infill_speed = nameof(infill_speed); + public const string perimeter_speed = nameof(perimeter_speed); + public const string solid_infill_speed = nameof(solid_infill_speed); + public const string support_material_speed = nameof(support_material_speed); + public const string travel_speed = nameof(travel_speed); + public const string retract_speed = nameof(retract_speed); } } diff --git a/MatterControlLib/ApplicationView/ApplicationController.cs b/MatterControlLib/ApplicationView/ApplicationController.cs index d853bf8c6..b464ae29e 100644 --- a/MatterControlLib/ApplicationView/ApplicationController.cs +++ b/MatterControlLib/ApplicationView/ApplicationController.cs @@ -2376,10 +2376,17 @@ If you experience adhesion problems, please re-run leveling." // Project to newline separated Error/Details/Location string var formattedErrors = errors.Select(err => { + string location = null; + + if (err is SettingsValidationError settingsError) + { + location = settingsError.Location; + } + // Conditionally combine Error/Details/Location when not empty return err.Error + ((string.IsNullOrWhiteSpace(err.Details)) ? "" : $"\n\n{err.Details}") + - ((string.IsNullOrWhiteSpace(err.Location)) ? "" : $"\n\n{err.Location}"); + ((string.IsNullOrWhiteSpace(location)) ? "" : $"\n\n{location}"); }).ToArray(); StyledMessageBox.ShowMessageBox( diff --git a/MatterControlLib/ApplicationView/SettingsValidation.cs b/MatterControlLib/ApplicationView/SettingsValidation.cs index 9ca2deabf..93d2fda25 100644 --- a/MatterControlLib/ApplicationView/SettingsValidation.cs +++ b/MatterControlLib/ApplicationView/SettingsValidation.cs @@ -55,24 +55,23 @@ namespace MatterHackers.MatterControl settings.GetValue(SettingsKey.nozzle_diameter)); errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.layer_height) { Error = "{0} must be less than or equal to the {1}.".Localize().FormatWith( GetSettingsName(SettingsKey.layer_height), GetSettingsName(SettingsKey.nozzle_diameter)), Details = details, - Location = GetSettingsLocation(SettingsKey.layer_height) }); } else if (settings.GetValue(SettingsKey.layer_height) <= 0) { errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.layer_height) { Error = "{0} must be greater than 0.".Localize().FormatWith(GetSettingsName(SettingsKey.layer_height)), - Location = GetSettingsLocation(SettingsKey.layer_height) }); } - else if (settings.GetValue(SettingsKey.first_layer_height) > settings.GetValue(SettingsKey.nozzle_diameter)) + + if (settings.GetValue(SettingsKey.first_layer_height) > settings.GetValue(SettingsKey.nozzle_diameter)) { var details = "{0} = {1}\n{2} = {3}".FormatWith( GetSettingsName(SettingsKey.first_layer_height), @@ -81,13 +80,12 @@ namespace MatterHackers.MatterControl settings.GetValue(SettingsKey.nozzle_diameter)); errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.first_layer_height) { Error = "{0} must be less than or equal to the {1}.".Localize().FormatWith( GetSettingsName(SettingsKey.layer_height), GetSettingsName(SettingsKey.nozzle_diameter)), Details = details, - Location = GetSettingsLocation(SettingsKey.first_layer_height) }); } } @@ -102,22 +100,20 @@ namespace MatterHackers.MatterControl if (startGCodeLine.StartsWith("G29")) { errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.start_gcode) { Error = "Start G-Code cannot contain G29 if Print Recovery is enabled.".Localize(), Details = "Your Start G-Code should not contain a G29 if you are planning on using Print Recovery. Change your start G-Code or turn off Print Recovery.".Localize(), - Location = GetSettingsLocation(SettingsKey.start_gcode) }); } if (startGCodeLine.StartsWith("G30")) { errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.start_gcode) { Error = "Start G-Code cannot contain G30 if Print Leveling is enabled.".Localize(), Details = "Your Start G-Code should not contain a G30 if you are planning on using Print Recovery. Change your start G-Code or turn off Print Recovery.".Localize(), - Location = GetSettingsLocation(SettingsKey.start_gcode) }); } } @@ -131,22 +127,20 @@ namespace MatterHackers.MatterControl if (startGCodeLine.StartsWith("G29")) { errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.start_gcode) { Error = "Start G-Code cannot contain G29 if Print Leveling is enabled.".Localize(), Details = "Your Start G-Code should not contain a G29 if you are planning on using print leveling. Change your start G-Code or turn off print leveling.".Localize(), - Location = GetSettingsLocation(SettingsKey.start_gcode) }); } if (startGCodeLine.StartsWith("G30")) { errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.start_gcode) { Error = "Start G-Code cannot contain G30 if Print Leveling is enabled.".Localize(), Details = "Your Start G-Code should not contain a G30 if you are planning on using print leveling. Change your start G-Code or turn off print leveling.".Localize(), - Location = GetSettingsLocation(SettingsKey.start_gcode) }); } } @@ -165,8 +159,10 @@ namespace MatterHackers.MatterControl new ValidationError() { Error = "Z Offset is too large.".Localize(), - Details = "The Z Offset for your printer, sometimes called Baby Stepping, is greater than 2mm and invalid. Clear the value and re-level the bed.".Localize(), - Location = location + Details = string.Format( + "{0}\n\n{1}", + "The Z Offset for your printer, sometimes called Baby Stepping, is greater than 2mm and invalid. Clear the value and re-level the bed.".Localize(), + location) }); } @@ -179,27 +175,25 @@ namespace MatterHackers.MatterControl settings.GetValue(SettingsKey.nozzle_diameter)); errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.first_layer_extrusion_width) { Error = "{0} must be less than or equal to the {1} * 4.".Localize().FormatWith( GetSettingsName(SettingsKey.first_layer_extrusion_width), GetSettingsName(SettingsKey.nozzle_diameter)), - Details = details, - Location = GetSettingsLocation(SettingsKey.first_layer_extrusion_width) + Details = details }); } if (settings.GetValue(SettingsKey.first_layer_extrusion_width) <= 0) { errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.first_layer_extrusion_width) { Error = "{0} must be greater than 0.".Localize().FormatWith( GetSettingsName(SettingsKey.first_layer_extrusion_width)), Details = "{0} = {1}".FormatWith( GetSettingsName(SettingsKey.first_layer_extrusion_width), settings.GetValue(SettingsKey.first_layer_extrusion_width)), - Location = GetSettingsLocation(SettingsKey.first_layer_extrusion_width) }); } @@ -212,13 +206,12 @@ namespace MatterHackers.MatterControl settings.GetValue(SettingsKey.nozzle_diameter)); errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.external_perimeter_extrusion_width) { Error = "{0} must be less than or equal to the {1} * 4.".Localize().FormatWith( GetSettingsName(SettingsKey.external_perimeter_extrusion_width), GetSettingsName(SettingsKey.nozzle_diameter)), Details = details, - Location = GetSettingsLocation(SettingsKey.external_perimeter_extrusion_width) }); } @@ -226,66 +219,61 @@ namespace MatterHackers.MatterControl { errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.external_perimeter_extrusion_width) { Error = "{0} must be greater than 0.".Localize().FormatWith( GetSettingsName(SettingsKey.external_perimeter_extrusion_width)), Details = "{0} = {1}".FormatWith( GetSettingsName(SettingsKey.external_perimeter_extrusion_width), settings.GetValue(SettingsKey.external_perimeter_extrusion_width)), - Location = GetSettingsLocation(SettingsKey.external_perimeter_extrusion_width) }); } if (settings.GetValue(SettingsKey.min_fan_speed) > 100) { errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.min_fan_speed) { Error = "The {0} can only go as high as 100%.".Localize().FormatWith( GetSettingsName(SettingsKey.min_fan_speed)), Details = "It is currently set to {0}.".Localize().FormatWith( settings.GetValue(SettingsKey.min_fan_speed)), - Location = GetSettingsLocation(SettingsKey.min_fan_speed) }); } if (settings.GetValue(SettingsKey.max_fan_speed) > 100) { errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.max_fan_speed) { Error = "The {0} can only go as high as 100%.".Localize().FormatWith( GetSettingsName(SettingsKey.max_fan_speed)), Details = "It is currently set to {0}.".Localize().FormatWith( settings.GetValue(SettingsKey.max_fan_speed)), - Location = GetSettingsLocation(SettingsKey.max_fan_speed) }); } if (settings.GetValue(SettingsKey.extruder_count) < 1) { errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.extruder_count) { Error = "The {0} must be at least 1.".Localize().FormatWith( GetSettingsName(SettingsKey.extruder_count)), Details= "It is currently set to {0}.".Localize().FormatWith( settings.GetValue(SettingsKey.extruder_count)), - Location = GetSettingsLocation(SettingsKey.extruder_count) }); } if (settings.GetValue(SettingsKey.fill_density) < 0 || settings.GetValue(SettingsKey.fill_density) > 1) { errors.Add( - new ValidationError() + new SettingsValidationError(SettingsKey.fill_density) { Error = "The {0} must be between 0 and 1.".Localize().FormatWith( GetSettingsName(SettingsKey.fill_density)), Details = "It is currently set to {0}.".Localize().FormatWith( settings.GetValue(SettingsKey.fill_density)), - Location = GetSettingsLocation(SettingsKey.filament_density) }); } @@ -299,19 +287,18 @@ namespace MatterHackers.MatterControl ValidateGCodeLinesShortEnough(SettingsKey.start_gcode, printer, errors); // If the given speed is part of the current slice engine then check that it is greater than 0. - ValidateGoodSpeedSettingGreaterThan0("bridge_speed", printer, errors); - ValidateGoodSpeedSettingGreaterThan0("air_gap_speed", printer, errors); - ValidateGoodSpeedSettingGreaterThan0("external_perimeter_speed", printer, errors); + ValidateGoodSpeedSettingGreaterThan0(SettingsKey.bridge_speed, printer, errors); + ValidateGoodSpeedSettingGreaterThan0(SettingsKey.air_gap_speed, printer, errors); + ValidateGoodSpeedSettingGreaterThan0(SettingsKey.external_perimeter_speed, printer, errors); ValidateGoodSpeedSettingGreaterThan0(SettingsKey.first_layer_speed, printer, errors); - ValidateGoodSpeedSettingGreaterThan0("infill_speed", printer, errors); - ValidateGoodSpeedSettingGreaterThan0("perimeter_speed", printer, errors); - ValidateGoodSpeedSettingGreaterThan0("small_perimeter_speed", printer, errors); - ValidateGoodSpeedSettingGreaterThan0("solid_infill_speed", printer, errors); - ValidateGoodSpeedSettingGreaterThan0("support_material_speed", printer, errors); + ValidateGoodSpeedSettingGreaterThan0(SettingsKey.infill_speed, printer, errors); + ValidateGoodSpeedSettingGreaterThan0(SettingsKey.perimeter_speed, printer, errors); + ValidateGoodSpeedSettingGreaterThan0(SettingsKey.small_perimeter_speed, printer, errors); + ValidateGoodSpeedSettingGreaterThan0(SettingsKey.solid_infill_speed, printer, errors); + ValidateGoodSpeedSettingGreaterThan0(SettingsKey.support_material_speed, printer, errors); ValidateGoodSpeedSettingGreaterThan0(SettingsKey.top_solid_infill_speed, printer, errors); - ValidateGoodSpeedSettingGreaterThan0("travel_speed", printer, errors); - - ValidateGoodSpeedSettingGreaterThan0("retract_speed", printer, errors); + ValidateGoodSpeedSettingGreaterThan0(SettingsKey.travel_speed, printer, errors); + ValidateGoodSpeedSettingGreaterThan0(SettingsKey.retract_speed, printer, errors); } catch (Exception e) { @@ -326,53 +313,30 @@ namespace MatterHackers.MatterControl return errors; } - private static string GetSettingsLocation(string settingsKey) - { - var settingData = SettingsOrganizer.Instance.GetSettingsData(settingsKey); - var setingsSectionName = settingData.OrganizerSubGroup.Group.Category.SettingsSection.Name; - var rootLevel = SettingsOrganizer.Instance.UserLevels[setingsSectionName]; - var subGroup = rootLevel.GetContainerForSetting(settingsKey); - var category = subGroup.Group.Category; - - if (setingsSectionName == "Advanced") - { - setingsSectionName = "Slice Settings"; - } - - return "Location".Localize() + ":" - + "\n" + setingsSectionName.Localize() - + "\n • " + category.Name.Localize() - + "\n • " + subGroup.Group.Name.Localize() - + "\n • " + settingData.PresentationName.Localize(); - } - private static string GetSettingsName(string settingsKey) { var settingData = SettingsOrganizer.Instance.GetSettingsData(settingsKey); return settingData.PresentationName.Localize(); } - private static bool ValidateGCodeLinesShortEnough(string gCodeSetting, PrinterConfig printer, List errors) + private static bool ValidateGCodeLinesShortEnough(string settingsKey, PrinterConfig printer, List errors) { - string[] gCodeString = printer.Settings.GetValue(SettingsKey.start_gcode).Replace("\\n", "\n").Split('\n'); - // make sure the custom gcode does not have lines too long to print - foreach (string line in gCodeString) + foreach (string line in printer.Settings.GetValue(settingsKey).Replace("\\n", "\n").Split('\n')) { var trimedLine = line.Split(';')[0].Trim(); var length = trimedLine.Length; if (length > 100) { - SliceSettingData data = SettingsOrganizer.Instance.GetSettingsData(gCodeSetting); + SliceSettingData data = SettingsOrganizer.Instance.GetSettingsData(settingsKey); if (data != null) { var details = "Found a line that is {0} characters long.\n{1}...".Localize().FormatWith(length, trimedLine.Substring(0, 20)); errors.Add( - new ValidationError() + new SettingsValidationError(settingsKey) { Error = "All G-Code lines mush be shorter than 100 characters (excluding comments).".Localize().FormatWith(data.PresentationName), Details = details, - Location = GetSettingsLocation(gCodeSetting) }); } @@ -383,9 +347,9 @@ namespace MatterHackers.MatterControl return true; } - private static void ValidateGoodSpeedSettingGreaterThan0(string speedSetting, PrinterConfig printer, List errors) + private static void ValidateGoodSpeedSettingGreaterThan0(string settingsKey, PrinterConfig printer, List errors) { - var actualSpeedValueString = printer.Settings.GetValue(speedSetting); + var actualSpeedValueString = printer.Settings.GetValue(settingsKey); var speedValueString = actualSpeedValueString; if (speedValueString.EndsWith("%")) { @@ -400,18 +364,17 @@ namespace MatterHackers.MatterControl } if (!valueWasNumber - || (printer.EngineMappingsMatterSlice.MapContains(speedSetting) + || (printer.EngineMappingsMatterSlice.MapContains(settingsKey) && speedToCheck <= 0)) { - SliceSettingData data = SettingsOrganizer.Instance.GetSettingsData(speedSetting); + SliceSettingData data = SettingsOrganizer.Instance.GetSettingsData(settingsKey); if (data != null) { errors.Add( - new ValidationError() + new SettingsValidationError(settingsKey) { Error = "The {0} must be greater than 0.".Localize().FormatWith(data.PresentationName), Details = "It is currently set to {0}.".Localize().FormatWith(actualSpeedValueString), - Location = GetSettingsLocation(speedSetting) }); } } diff --git a/MatterControlLib/ApplicationView/SettingsValidationError.cs b/MatterControlLib/ApplicationView/SettingsValidationError.cs new file mode 100644 index 000000000..0a44e1ece --- /dev/null +++ b/MatterControlLib/ApplicationView/SettingsValidationError.cs @@ -0,0 +1,66 @@ +/* +Copyright (c) 2019, John Lewin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. +*/ + +using MatterHackers.Localizations; +using MatterHackers.MatterControl.SlicerConfiguration; + +namespace MatterHackers.MatterControl +{ + public class SettingsValidationError : ValidationError + { + public SettingsValidationError(string settingsName) + { + this.CanonicalSettingsName = settingsName; + } + + public string CanonicalSettingsName { get; } + + public string Location => SettingsLocation(this.CanonicalSettingsName); + + private static string SettingsLocation(string settingsKey) + { + var settingData = SettingsOrganizer.Instance.GetSettingsData(settingsKey); + var setingsSectionName = settingData.OrganizerSubGroup.Group.Category.SettingsSection.Name; + var rootLevel = SettingsOrganizer.Instance.UserLevels[setingsSectionName]; + var subGroup = rootLevel.GetContainerForSetting(settingsKey); + var category = subGroup.Group.Category; + + if (setingsSectionName == "Advanced") + { + setingsSectionName = "Slice Settings"; + } + + return "Location".Localize() + ":" + + "\n" + setingsSectionName.Localize() + + "\n • " + category.Name.Localize() + + "\n • " + subGroup.Group.Name.Localize() + + "\n • " + settingData.PresentationName.Localize(); + } + } +} \ No newline at end of file diff --git a/MatterControlLib/ApplicationView/ValidationError.cs b/MatterControlLib/ApplicationView/ValidationError.cs index fbf80be1b..4c0707ca9 100644 --- a/MatterControlLib/ApplicationView/ValidationError.cs +++ b/MatterControlLib/ApplicationView/ValidationError.cs @@ -27,6 +27,9 @@ of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. */ +using MatterHackers.Localizations; +using MatterHackers.MatterControl.SlicerConfiguration; + namespace MatterHackers.MatterControl { public class ValidationError @@ -34,11 +37,5 @@ namespace MatterHackers.MatterControl public string Error { get; set; } public string Details { get; set; } - - public string Source { get; set; } - - public string SourceName { get; set; } - - public string Location { get; set; } } } \ No newline at end of file diff --git a/MatterControlLib/SlicerConfiguration/SliceSettingsRow.cs b/MatterControlLib/SlicerConfiguration/SliceSettingsRow.cs index e7b1f21c6..2e47b960a 100644 --- a/MatterControlLib/SlicerConfiguration/SliceSettingsRow.cs +++ b/MatterControlLib/SlicerConfiguration/SliceSettingsRow.cs @@ -1,5 +1,5 @@ /* -Copyright (c) 2018, Lars Brubaker, John Lewin +Copyright (c) 2019, Lars Brubaker, John Lewin All rights reserved. Redistribution and use in source and binary forms, with or without @@ -32,6 +32,8 @@ using System.Collections.Generic; using System.Linq; using Markdig.Agg; using MatterHackers.Agg; +using MatterHackers.Agg.Image; +using MatterHackers.Agg.Platform; using MatterHackers.Agg.UI; using MatterHackers.Localizations; using MatterHackers.MatterControl.CustomWidgets; @@ -42,6 +44,8 @@ namespace MatterHackers.MatterControl.SlicerConfiguration { public class SliceSettingsRow : SettingsRow { + private IEnumerable validationErrors; + private static Dictionary> extendedInfo = new Dictionary>() { #if DEBUG @@ -86,6 +90,7 @@ namespace MatterHackers.MatterControl.SlicerConfiguration private Popover popoverBubble = null; private SystemWindow systemWindow = null; + private ValidationWrapper validationWrapper; public SliceSettingsRow(PrinterConfig printer, SettingsContext settingsContext, SliceSettingData settingData, ThemeConfig theme, bool fullRowSelect = false) : base (settingData.PresentationName.Localize(), settingData.HelpText.Localize(), theme, fullRowSelect: fullRowSelect) @@ -156,6 +161,23 @@ namespace MatterHackers.MatterControl.SlicerConfiguration this.PerformLayout(); } + public void UpdateValidationState(List errors) + { + var fieldErrors = errors.OfType().Where(e => e.CanonicalSettingsName == this.settingData.SlicerConfigName); + if (fieldErrors.Any()) + { + validationErrors = fieldErrors; + this.ContentValid = false; + } + else + { + validationErrors = Enumerable.Empty(); + this.ContentValid = true; + } + } + + public SettingsValidationError ValidationEror { get; set; } + public Color HighlightColor { get => overrideIndicator.BackgroundColor; @@ -168,6 +190,22 @@ namespace MatterHackers.MatterControl.SlicerConfiguration } } + public bool ContentValid + { + get => validationWrapper?.ContentValid ?? true; + set + { + if (validationWrapper != null + && validationWrapper.ContentValid != value) + { + validationWrapper.ContentValid = value; + + // ShowPopever performs bounds validation and should have no effect if mouse is not in bounds + this.ShowPopover(this); + } + } + } + public override void OnClick(MouseEventArgs mouseEvent) { if (ActionWidget != null @@ -241,17 +279,20 @@ namespace MatterHackers.MatterControl.SlicerConfiguration public ArrowDirection ArrowDirection { get; set; } = ArrowDirection.Right; + public UIField UIField { get; internal set; } + public override void OnMouseEnterBounds(MouseEventArgs mouseEvent) { - if (systemWindow == null) - { - return; - } + this.ShowPopover(this); - var settingsRow = this; + base.OnMouseEnterBounds(mouseEvent); + } + private void ShowPopover(SliceSettingsRow settingsRow) + { // Only display popovers when we're the active widget, exit if we're not first under mouse - if (!this.ContainsFirstUnderMouseRecursive()) + if (systemWindow == null + || !this.ContainsFirstUnderMouseRecursive()) { return; } @@ -333,6 +374,33 @@ namespace MatterHackers.MatterControl.SlicerConfiguration } } + if (validationErrors.Any()) + { + var errorsPanel = new FlowLayoutWidget(FlowDirection.TopToBottom) + { + Padding = theme.DefaultContainerPadding / 2, + HAnchor = HAnchor.Stretch, + VAnchor = VAnchor.Fit + }; + popover.AddChild(errorsPanel); + + foreach (var item in validationErrors) + { + var errorPanel = new FlowLayoutWidget(FlowDirection.TopToBottom) + { + Margin = new BorderDouble(0, 5) + }; + errorsPanel.AddChild(errorPanel); + + errorsPanel.AddChild( + new WrappedTextWidget(item.Error, pointSize: theme.DefaultFontSize - 1, textColor: Color.Red) + { + Margin = new BorderDouble(bottom: 3) + }); + errorsPanel.AddChild(new WrappedTextWidget(item.Details, pointSize: theme.DefaultFontSize - 1, textColor: Color.Red)); + } + } + #if DEBUG popover.AddChild(new TextWidget(settingData.SlicerConfigName + mapsTo, pointSize: theme.DefaultFontSize - 1, textColor: AppContext.Theme.TextColor) { @@ -359,8 +427,6 @@ namespace MatterHackers.MatterControl.SlicerConfiguration secondsToClose: closeSeconds); popoverBubble = popover; - - base.OnMouseEnterBounds(mouseEvent); } public override void OnMouseLeaveBounds(MouseEventArgs mouseEvent) @@ -476,7 +542,59 @@ namespace MatterHackers.MatterControl.SlicerConfiguration public void AddContent(GuiWidget content) { - dataArea.AddChild(content); + validationWrapper = new ValidationWrapper(); + dataArea.AddChild(validationWrapper); + + validationWrapper.AddChild(content); + } + + /// + /// Wraps UIFields and conditionally displays validation error hints when validation errors occur + /// + private class ValidationWrapper : GuiWidget + { + private bool _contentValid = true; + private ImageBuffer exclamation; + + public ValidationWrapper() + { + this.VAnchor = VAnchor.Fit; + this.HAnchor = HAnchor.Fit; + this.Padding = new BorderDouble(left: 5); + + exclamation = AggContext.StaticData.LoadIcon("exclamation.png"); + + this.Border = new BorderDouble(bottom: 1); + } + + public bool ContentValid + { + get => _contentValid; + set + { + if (_contentValid != value) + { + _contentValid = value; + this.Invalidate(); + } + } + } + + public override Color BorderColor + { + get => (this.ContentValid) ? base.BorderColor : Color.Red; + set => base.BorderColor = value; + } + + public override void OnDraw(Graphics2D graphics2D) + { + base.OnDraw(graphics2D); + + if (!this.ContentValid) + { + graphics2D.Render(exclamation, this.LocalBounds.Left, this.LocalBounds.Top - exclamation.Height); + } + } } } } diff --git a/MatterControlLib/SlicerConfiguration/SliceSettingsWidget.cs b/MatterControlLib/SlicerConfiguration/SliceSettingsWidget.cs index 98989f0ac..c780edfb8 100644 --- a/MatterControlLib/SlicerConfiguration/SliceSettingsWidget.cs +++ b/MatterControlLib/SlicerConfiguration/SliceSettingsWidget.cs @@ -208,6 +208,8 @@ namespace MatterHackers.MatterControl.SlicerConfiguration allUiFields = new Dictionary(); + var errors = printer.ValidateSettings(); + // Loop over categories creating a tab for each foreach (var category in userLevel.Categories) { @@ -236,7 +238,7 @@ namespace MatterHackers.MatterControl.SlicerConfiguration this.CreateOemProfileInfoRow()); } - var groupSection = this.CreateGroupSection(group); + var groupSection = this.CreateGroupSection(group, errors); groupSection.Name = group.Name + " Panel"; @@ -376,7 +378,7 @@ namespace MatterHackers.MatterControl.SlicerConfiguration { "Fan", "enable_fan" }, }; - public SectionWidget CreateGroupSection(SettingsOrganizer.Group group) + public SectionWidget CreateGroupSection(SettingsOrganizer.Group group, List errors) { var groupPanel = new FlowLayoutWidget(FlowDirection.TopToBottom) { @@ -420,7 +422,7 @@ namespace MatterHackers.MatterControl.SlicerConfiguration if (printer.EngineMappingsMatterSlice.MapContains(settingData.SlicerConfigName) && settingShouldBeShown) { - settingsRow = CreateItemRow(settingData); + settingsRow = CreateItemRow(settingData, errors); if (firstRow) { @@ -600,12 +602,12 @@ namespace MatterHackers.MatterControl.SlicerConfiguration return dataArea; } - internal GuiWidget CreateItemRow(SliceSettingData settingData) + internal GuiWidget CreateItemRow(SliceSettingData settingData, List errors) { - return CreateItemRow(settingData, settingsContext, printer, theme, ref tabIndexForItem, allUiFields); + return CreateItemRow(settingData, settingsContext, printer, theme, ref tabIndexForItem, allUiFields, errors); } - public static GuiWidget CreateItemRow(SliceSettingData settingData, SettingsContext settingsContext, PrinterConfig printer, ThemeConfig theme, ref int tabIndexForItem, Dictionary fieldCache = null) + public static GuiWidget CreateItemRow(SliceSettingData settingData, SettingsContext settingsContext, PrinterConfig printer, ThemeConfig theme, ref int tabIndexForItem, Dictionary fieldCache = null, List errors = null) { string sliceSettingValue = settingsContext.GetValue(settingData.SlicerConfigName); @@ -819,6 +821,14 @@ namespace MatterHackers.MatterControl.SlicerConfiguration } } + settingsRow.UIField = uiField; + uiField.Row = settingsRow; + + if (errors?.Any() == true) + { + settingsRow.UpdateValidationState(errors); + } + // Invoke the UpdateStyle implementation settingsRow.UpdateStyle(); @@ -895,6 +905,8 @@ namespace MatterHackers.MatterControl.SlicerConfiguration private void Printer_SettingChanged(object s, StringEventArgs stringEvent) { + var errors = printer.ValidateSettings(); + if (stringEvent != null) { string settingsKey = stringEvent.Data; @@ -908,6 +920,8 @@ namespace MatterHackers.MatterControl.SlicerConfiguration currentValue, userInitiated: false); } + + uifield.Row.UpdateValidationState(errors); } } } diff --git a/MatterControlLib/SlicerConfiguration/UIFields/UIField.cs b/MatterControlLib/SlicerConfiguration/UIFields/UIField.cs index 7106029ff..653b0f64e 100644 --- a/MatterControlLib/SlicerConfiguration/UIFields/UIField.cs +++ b/MatterControlLib/SlicerConfiguration/UIFields/UIField.cs @@ -62,6 +62,8 @@ namespace MatterHackers.MatterControl.SlicerConfiguration public string Name { get; set; } + public SliceSettingsRow Row { get; internal set; } + protected virtual string ConvertValue(string newValue) { return newValue; diff --git a/StaticData/Icons/exclamation.png b/StaticData/Icons/exclamation.png new file mode 100644 index 000000000..714256f49 Binary files /dev/null and b/StaticData/Icons/exclamation.png differ