From c800f887d2d50f9f34500c323f5cc8c750ef258b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Mon, 16 May 2022 01:25:21 +0100 Subject: [PATCH] v3.4.2 - **Core:** - (Add) Getter `FileFormat.DisplayPixelCount` Gets the display total number of pixels (ResolutionX * ResolutionY) - (Add) Getter `Layer.NonZeroPixelRatio` Gets the ratio between non zero pixels and display number of pixels - (Add) Getter `Layer.NonZeroPixelPercentage` Gets the percentage of non zero pixels relative to the display number of pixels - (Add) Getter `Layer.PreviousHeightLayer()` Gets the previous layer with a different height from the current, returns null if no previous layer - (Add) Getter `Layer.NextHeightLayer()` Gets the next layer with a different height from the current, returns null if no next layer - (Add) Method `Layer.GetPreviousLayerWithAtLeastPixelCountOf()` Gets the previous layer matching at least a number of pixels, returns null if no previous layer - (Add) Method `Layer.GetNextLayerWithAtLeastPixelCountOf()` Gets the next layer matching at least a number of pixels, returns null if no next layer - (Add) Method `Operation.GetRoiOrVolumeBounds()` returns the selected ROI rectangle or model volume bounds rectangle - (Add) Documentation around `Operation` methods - (Fix) Open files in partial mode when the resolution is not defined would cause a `NullPointerException` (#474) - **Suggestion: Wait time before cure** - (Add) Proportional maximum time change: Sets the maximum allowed time difference relative to the previous layer (#471) - (Add) Proportional mass get modes: Previous, Average and Maximum relative to a defined height (#471) - (Change) Proportional set type sets fallback time to the first layer - (Fix) Proportional set type was taking current layer mass instead of looking to the previous cured layer (#471) - **Tools:** - **Edit print parameters:** - (Change) Incorporate the unit label into the numeric input box - (Change) Allow TSMC speeds to be 0 as minimum value (#472) - (Fix) PCB Exposure: The thumbnail has random noise around the image - **Settings:** - (Add) Tools: "Always prompt for confirmation before execute the operation" - (Fix) Changing layer compression method when no file is loaded would cause a error - **UI:** - (Add) Holding Shift key while drag and drop a .uvtop file will try to execute the operation without showing the window or prompt - (Add) Drag and drop a .cs or .csx file into UVtools will load and show the scripting dialog with the file selected - (Add) Errors that crash application will now show an report window with the crash information and able to fast report them - (Add) "Version" key and value on registry to tell the current installed version (Windows MSI only) - (Upgrade) AvaloniaUI from 0.10.13 to 0.10.14 - (Upgrade) .NET from 6.0.4 to 6.0.5 --- CHANGELOG.md | 34 +++ RELEASE_NOTES.md | 34 ++- .../UVtools.AvaloniaControls.csproj | 2 +- UVtools.Core/Extensions/EmguExtensions.cs | 2 +- UVtools.Core/FileFormats/CWSFile.cs | 1 - UVtools.Core/FileFormats/FileFormat.cs | 31 ++- .../FileFormats/PhotonWorkshopFile.cs | 16 +- UVtools.Core/Layers/Layer.cs | 89 +++++++ UVtools.Core/Operations/Operation.cs | 222 +++++++++++++++++- .../Operations/OperationPCBExposure.cs | 6 +- UVtools.Core/Operations/OperationScripting.cs | 3 +- .../SuggestionWaitTimeBeforeCure.cs | 171 ++++++++++++-- UVtools.Core/SystemOS/SystemAware.cs | 10 + UVtools.Core/UVtools.Core.csproj | 6 +- UVtools.Installer/Code/Product.wxs | 1 + UVtools.InstallerMM/UVtools.InstallerMM.wxs | 6 +- UVtools.WPF/App.axaml.cs | 142 +++++++---- UVtools.WPF/ConsoleArguments.cs | 2 +- .../SuggestionWaitTimeBeforeCureControl.axaml | 114 +++++++-- .../Tools/ToolEditParametersControl.axaml | 8 +- .../Tools/ToolEditParametersControl.axaml.cs | 22 +- .../Tools/ToolScriptingControl.axaml.cs | 2 - UVtools.WPF/Controls/WindowEx.cs | 66 +++++- UVtools.WPF/ErrorLog.cs | 1 + UVtools.WPF/Extensions/WindowExtensions.cs | 2 +- UVtools.WPF/MainWindow.LayerPreview.cs | 3 +- UVtools.WPF/MainWindow.axaml.cs | 65 ++++- UVtools.WPF/Program.cs | 63 +++-- UVtools.WPF/UVtools.WPF.csproj | 12 +- UVtools.WPF/UserSettings.cs | 8 + UVtools.WPF/Windows/AboutWindow.axaml.cs | 18 ++ UVtools.WPF/Windows/CantRunWindow.axaml | 4 +- UVtools.WPF/Windows/CantRunWindow.axaml.cs | 7 +- UVtools.WPF/Windows/MessageWindow.axaml | 90 +++++++ UVtools.WPF/Windows/MessageWindow.axaml.cs | 141 +++++++++++ UVtools.WPF/Windows/SettingsWindow.axaml | 3 + UVtools.WPF/Windows/ToolWindow.axaml.cs | 5 +- 37 files changed, 1196 insertions(+), 216 deletions(-) create mode 100644 UVtools.WPF/Windows/MessageWindow.axaml create mode 100644 UVtools.WPF/Windows/MessageWindow.axaml.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1797a49..06bbe51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +## 16/05/2022 - v3.4.2 + +- **Core:** + - (Add) Getter `FileFormat.DisplayPixelCount` Gets the display total number of pixels (ResolutionX * ResolutionY) + - (Add) Getter `Layer.NonZeroPixelRatio` Gets the ratio between non zero pixels and display number of pixels + - (Add) Getter `Layer.NonZeroPixelPercentage` Gets the percentage of non zero pixels relative to the display number of pixels + - (Add) Getter `Layer.PreviousHeightLayer()` Gets the previous layer with a different height from the current, returns null if no previous layer + - (Add) Getter `Layer.NextHeightLayer()` Gets the next layer with a different height from the current, returns null if no next layer + - (Add) Method `Layer.GetPreviousLayerWithAtLeastPixelCountOf()` Gets the previous layer matching at least a number of pixels, returns null if no previous layer + - (Add) Method `Layer.GetNextLayerWithAtLeastPixelCountOf()` Gets the next layer matching at least a number of pixels, returns null if no next layer + - (Add) Method `Operation.GetRoiOrVolumeBounds()` returns the selected ROI rectangle or model volume bounds rectangle + - (Add) Documentation around `Operation` methods + - (Fix) Open files in partial mode when the resolution is not defined would cause a `NullPointerException` (#474) +- **Suggestion: Wait time before cure** + - (Add) Proportional maximum time change: Sets the maximum allowed time difference relative to the previous layer (#471) + - (Add) Proportional mass get modes: Previous, Average and Maximum relative to a defined height (#471) + - (Change) Proportional set type sets fallback time to the first layer + - (Fix) Proportional set type was taking current layer mass instead of looking to the previous cured layer (#471) +- **Tools:** + - **Edit print parameters:** + - (Change) Incorporate the unit label into the numeric input box + - (Change) Allow TSMC speeds to be 0 as minimum value (#472) + - (Fix) PCB Exposure: The thumbnail has random noise around the image +- **Settings:** + - (Add) Tools: "Always prompt for confirmation before execute the operation" + - (Fix) Changing layer compression method when no file is loaded would cause a error +- **UI:** + - (Add) Holding Shift key while drag and drop a .uvtop file will try to execute the operation without showing the window or prompt + - (Add) Drag and drop a .cs or .csx file into UVtools will load and show the scripting dialog with the file selected +- (Add) Errors that crash application will now show an report window with the crash information and able to fast report them +- (Add) "Version" key and value on registry to tell the current installed version (Windows MSI only) +- (Upgrade) AvaloniaUI from 0.10.13 to 0.10.14 +- (Upgrade) .NET from 6.0.4 to 6.0.5 +- ## 02/05/2022 - v3.4.1 - (Add) Suggestion - Wait time before cure: Allow to set the number of layers to smooth transition from bottom to normal wait time (Defaults to 8) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 516ea10..d889774 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,32 @@ -- (Add) Suggestion - Wait time before cure: Allow to set the number of layers to smooth transition from bottom to normal wait time (Defaults to 8) -- (Fix) Tool - PCB Exposure: Pixels per millimeter was been set to fixed value instead of use printer lcd pitch, causing wrong dimentions on different from 50µm pitch -- (Fix) Tool - PCB Exposure: Unable to run the tool when the display size information isn't available +- **Core:** + - (Add) Getter `FileFormat.DisplayPixelCount` Gets the display total number of pixels (ResolutionX * ResolutionY) + - (Add) Getter `Layer.NonZeroPixelRatio` Gets the ratio between non zero pixels and display number of pixels + - (Add) Getter `Layer.NonZeroPixelPercentage` Gets the percentage of non zero pixels relative to the display number of pixels + - (Add) Getter `Layer.PreviousHeightLayer()` Gets the previous layer with a different height from the current, returns null if no previous layer + - (Add) Getter `Layer.NextHeightLayer()` Gets the next layer with a different height from the current, returns null if no next layer + - (Add) Method `Layer.GetPreviousLayerWithAtLeastPixelCountOf()` Gets the previous layer matching at least a number of pixels, returns null if no previous layer + - (Add) Method `Layer.GetNextLayerWithAtLeastPixelCountOf()` Gets the next layer matching at least a number of pixels, returns null if no next layer + - (Add) Method `Operation.GetRoiOrVolumeBounds()` returns the selected ROI rectangle or model volume bounds rectangle + - (Add) Documentation around `Operation` methods + - (Fix) Open files in partial mode when the resolution is not defined would cause a `NullPointerException` (#474) +- **Suggestion: Wait time before cure** + - (Add) Proportional maximum time change: Sets the maximum allowed time difference relative to the previous layer (#471) + - (Add) Proportional mass get modes: Previous, Average and Maximum relative to a defined height (#471) + - (Change) Proportional set type sets fallback time to the first layer + - (Fix) Proportional set type was taking current layer mass instead of looking to the previous cured layer (#471) +- **Tools:** + - **Edit print parameters:** + - (Change) Incorporate the unit label into the numeric input box + - (Change) Allow TSMC speeds to be 0 as minimum value (#472) + - (Fix) PCB Exposure: The thumbnail has random noise around the image +- **Settings:** + - (Add) Tools: "Always prompt for confirmation before execute the operation" + - (Fix) Changing layer compression method when no file is loaded would cause a error +- **UI:** + - (Add) Holding Shift key while drag and drop a .uvtop file will try to execute the operation without showing the window or prompt + - (Add) Drag and drop a .cs or .csx file into UVtools will load and show the scripting dialog with the file selected +- (Add) Errors that crash application will now show an report window with the crash information and able to fast report them +- (Upgrade) AvaloniaUI from 0.10.13 to 0.10.14 +- (Upgrade) .NET from 6.0.4 to 6.0.5 +- diff --git a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj index a3f8722..45106a3 100644 --- a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj +++ b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj @@ -38,7 +38,7 @@ - + diff --git a/UVtools.Core/Extensions/EmguExtensions.cs b/UVtools.Core/Extensions/EmguExtensions.cs index 5c506ae..833fd8d 100644 --- a/UVtools.Core/Extensions/EmguExtensions.cs +++ b/UVtools.Core/Extensions/EmguExtensions.cs @@ -489,7 +489,7 @@ public static class EmguExtensions using var roi = src.Size == rect.Size ? src.Roi(src.Size) : src.Roi(rect); var numberOfChannels = roi.NumberOfChannels; - var cropped = new Mat(roi.Rows + margin.Height * 2, roi.Cols + margin.Width * 2, roi.Depth, numberOfChannels); + var cropped = InitMat(new Size(roi.Width + margin.Width * 2, roi.Height + margin.Height * 2), numberOfChannels, roi.Depth); using var dest = new Mat(cropped, new Rectangle(margin.Width, margin.Height, roi.Width, roi.Height)); roi.CopyTo(dest); diff --git a/UVtools.Core/FileFormats/CWSFile.cs b/UVtools.Core/FileFormats/CWSFile.cs index 8fb5f6a..b32d673 100644 --- a/UVtools.Core/FileFormats/CWSFile.cs +++ b/UVtools.Core/FileFormats/CWSFile.cs @@ -18,7 +18,6 @@ using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; using System.Xml.Serialization; using UVtools.Core.Converters; using UVtools.Core.Extensions; diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index 69ec05a..b62099d 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -172,7 +172,7 @@ public abstract class FileFormat : BindableBase, IDisposable, IEquatable - /// Gets the bounding rectangle of the object + /// Gets the bounding rectangle of the model /// public Rectangle BoundingRectangle { @@ -1492,6 +1492,11 @@ public abstract class FileFormat : BindableBase, IDisposable, IEquatable public abstract uint ResolutionY { get; set; } + /// + /// Gets the display total number of pixels ( * ) + /// + public uint DisplayPixelCount => ResolutionX * ResolutionY; + /// /// Gets the size of display in millimeters /// @@ -5320,7 +5325,7 @@ public abstract class FileFormat : BindableBase, IDisposable, IEquatable imageLength) { image.Dispose(); - throw new FileLoadException("Error image ran off the end"); + throw new FileLoadException("Image ran off the end."); } } } @@ -763,6 +763,12 @@ public class PhotonWorkshopFile : FileFormat //color &= 0xff; + if (pixelPos + repeat > imageLength) + { + mat.Dispose(); + throw new FileLoadException($"Image ran off the end: {pixelPos}+ {repeat} = {pixelPos + repeat}, expecting: {imageLength}"); + } + // We only need to set the non-zero pixels mat.FillSpan(ref pixelPos, repeat, color); @@ -772,18 +778,12 @@ public class PhotonWorkshopFile : FileFormat //i++; break; } - - if (pixelPos > imageLength) - { - mat.Dispose(); - throw new FileLoadException($"Error image ran off the end: {pixelPos - repeat}({repeat}) of {imageLength}"); - } } if (pixelPos > 0 && pixelPos != imageLength) { mat.Dispose(); - throw new FileLoadException($"Error image ended short: {pixelPos} of {imageLength}"); + throw new FileLoadException($"Image ended short: {pixelPos}, expecting: {imageLength}"); } return mat; diff --git a/UVtools.Core/Layers/Layer.cs b/UVtools.Core/Layers/Layer.cs index e0b5683..6493a53 100644 --- a/UVtools.Core/Layers/Layer.cs +++ b/UVtools.Core/Layers/Layer.cs @@ -98,12 +98,40 @@ public class Layer : BindableBase, IEquatable, IEquatable internal set { if (!RaiseAndSetIfChanged(ref _nonZeroPixelCount, value)) return; + RaisePropertyChanged(nameof(NonZeroPixelRatio)); + RaisePropertyChanged(nameof(NonZeroPixelPercentage)); RaisePropertyChanged(nameof(Area)); RaisePropertyChanged(nameof(Volume)); MaterialMilliliters = -1; // Recalculate } } + /// + /// Gets the ratio between non zero pixels and display number of pixels + /// + public double NonZeroPixelRatio + { + get + { + var displayPixelCount = SlicerFile.DisplayPixelCount; + if (displayPixelCount == 0) return double.NaN; + return (double)_nonZeroPixelCount / displayPixelCount; + } + } + + /// + /// Gets the percentage of non zero pixels relative to the display number of pixels + /// + public double NonZeroPixelPercentage + { + get + { + var pixelRatio = NonZeroPixelRatio; + if (double.IsNaN(pixelRatio)) return double.NaN; + return pixelRatio * 100.0; + } + } + /// /// Gets if this layer is empty/all black pixels /// @@ -214,7 +242,37 @@ public class Layer : BindableBase, IEquatable, IEquatable if (IsFirstLayer || _index > SlicerFile.Count) return null; return SlicerFile[_index - 1]; } + } + /// + /// Gets the previous layer with a different height from the current, returns null if no previous layer + /// + public Layer? PreviousHeightLayer + { + get + { + if (IsFirstLayer || _index > SlicerFile.Count) return null; + for (int i = (int)_index - 1; i >= 0; i--) + { + if (SlicerFile[i].PositionZ < _positionZ) return SlicerFile[i]; + } + + return null; + } + } + + /// + /// Gets the previous layer matching at least pixels, returns null if no previous layer + /// + public Layer? GetPreviousLayerWithAtLeastPixelCountOf(uint numberOfPixels) + { + if (IsFirstLayer || _index > SlicerFile.Count) return null; + for (int i = (int)_index - 1; i >= 0; i--) + { + if (SlicerFile[i].NonZeroPixelCount >= numberOfPixels) return SlicerFile[i]; + } + + return null; } /// @@ -229,6 +287,37 @@ public class Layer : BindableBase, IEquatable, IEquatable } } + /// + /// Gets the next layer with a different height from the current, returns null if no next layer + /// + public Layer? NextHeightLayer + { + get + { + if (_index >= SlicerFile.LastLayerIndex) return null; + for (var i = _index + 1; i < SlicerFile.LayerCount; i++) + { + if (SlicerFile[i].PositionZ > _positionZ) return SlicerFile[i]; + } + + return null; + } + } + + /// + /// Gets the next layer matching at least pixels, returns null if no next layer + /// + public Layer? GetNextLayerWithAtLeastPixelCountOf(uint numberOfPixels) + { + if (_index >= SlicerFile.LastLayerIndex) return null; + for (var i = _index + 1; i < SlicerFile.LayerCount; i++) + { + if (SlicerFile[i].NonZeroPixelCount >= numberOfPixels) return SlicerFile[i]; + } + + return null; + } + /// /// Gets the layer index /// diff --git a/UVtools.Core/Operations/Operation.cs b/UVtools.Core/Operations/Operation.cs index 0bafa08..d1b50b4 100644 --- a/UVtools.Core/Operations/Operation.cs +++ b/UVtools.Core/Operations/Operation.cs @@ -66,6 +66,9 @@ public abstract class Operation : BindableBase, IDisposable set => RaiseAndSetIfChanged(ref _importedFrom, value); } + /// + /// Gets or sets the parent + /// [XmlIgnore] public FileFormat SlicerFile { @@ -78,6 +81,9 @@ public abstract class Operation : BindableBase, IDisposable } } + /// + /// Gets the bounding rectangle of the model, preserved from any change during and after execution + /// [XmlIgnore] public Rectangle OriginalBoundingRectangle { @@ -85,6 +91,9 @@ public abstract class Operation : BindableBase, IDisposable private set => RaiseAndSetIfChanged(ref _originalBoundingRectangle, value); } + /// + /// Gets or sets any object which is not used internally + /// [XmlIgnore] public object? Tag { get; set; } @@ -93,6 +102,9 @@ public abstract class Operation : BindableBase, IDisposable /// public string Id => GetType().Name.Remove(0, ClassNameLength); + /// + /// Gets the starting layer selection + /// public virtual LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.All; /// @@ -104,6 +116,9 @@ public abstract class Operation : BindableBase, IDisposable set => RaiseAndSetIfChanged(ref _layerRangeSelection, value); } + /// + /// Gets a string representing the layer range, used with profiles + /// public virtual string LayerRangeString { get @@ -228,9 +243,19 @@ public abstract class Operation : BindableBase, IDisposable } } + /// + /// Gets if any bottom layer is included in the selected layer range + /// public bool LayerRangeHaveBottoms => LayerIndexStart < (SlicerFile.FirstNormalLayer?.Index ?? 0); + + /// + /// Gets if any normal layer is included in the selected layer range + /// public bool LayerRangeHaveNormals => LayerIndexEnd >= (SlicerFile.FirstNormalLayer?.Index ?? 0); + /// + /// Gets the number of selected layers + /// public uint LayerRangeCount => (uint)Math.Max(0, (int)LayerIndexEnd - LayerIndexStart + 1); /// @@ -242,6 +267,9 @@ public abstract class Operation : BindableBase, IDisposable set => RaiseAndSetIfChanged(ref _profileName, value); } + /// + /// Gets if this profile is the default to load + /// public bool ProfileIsDefault { get => _profileIsDefault; @@ -262,6 +290,9 @@ public abstract class Operation : BindableBase, IDisposable } } + /// + /// Gets if there is an ROI associated + /// public bool HaveROI => !ROI.IsEmpty; /// @@ -279,8 +310,14 @@ public abstract class Operation : BindableBase, IDisposable } } + /// + /// Gets if there is masks associated + /// public bool HaveMask => _maskPoints is not null && _maskPoints.Length > 0; + /// + /// Gets if there is roi or masks associated + /// public bool HaveROIorMask => HaveROI || HaveMask; /// @@ -310,6 +347,9 @@ public abstract class Operation : BindableBase, IDisposable #region Methods + /// + /// Gets if the operation can spawn + /// public bool CanSpawn => string.IsNullOrWhiteSpace(ValidateSpawn()); /// @@ -326,6 +366,10 @@ public abstract class Operation : BindableBase, IDisposable return string.IsNullOrWhiteSpace(message); } + /// + /// Validates the operation, return null or empty if validates + /// + /// public virtual string? ValidateInternally() { if (!ValidateSpawn(out var message)) @@ -345,11 +389,18 @@ public abstract class Operation : BindableBase, IDisposable return ValidateInternally(); } + /// + /// Gets if the operation is able to execute + /// + /// public bool CanValidate() { return string.IsNullOrWhiteSpace(Validate()); } + /// + /// Selects all layers from first to last layer + /// public void SelectAllLayers() { LayerIndexStart = 0; @@ -357,12 +408,19 @@ public abstract class Operation : BindableBase, IDisposable LayerRangeSelection = LayerRangeSelection.All; } + /// + /// Selects one layer + /// + /// Layer index to select public void SelectCurrentLayer(uint layerIndex) { LayerIndexStart = LayerIndexEnd = layerIndex; LayerRangeSelection = LayerRangeSelection.Current; } + /// + /// Selects all bottom layers + /// public void SelectBottomLayers() { LayerIndexStart = 0; @@ -370,6 +428,9 @@ public abstract class Operation : BindableBase, IDisposable LayerRangeSelection = LayerRangeSelection.Bottom; } + /// + /// Selects all normal layers + /// public void SelectNormalLayers() { LayerIndexStart = SlicerFile.FirstNormalLayer?.Index ?? 0; @@ -377,30 +438,49 @@ public abstract class Operation : BindableBase, IDisposable LayerRangeSelection = LayerRangeSelection.Normal; } + /// + /// Select the first layer (0) + /// public void SelectFirstLayer() { LayerIndexStart = LayerIndexEnd = 0; LayerRangeSelection = LayerRangeSelection.First; } + /// + /// Select the last layer + /// public void SelectLastLayer() { LayerIndexStart = LayerIndexEnd = SlicerFile.LastLayerIndex; LayerRangeSelection = LayerRangeSelection.Last; } + /// + /// Selects from first to a layer index + /// + /// To layer index to select public void SelectFirstToCurrentLayer(uint currentLayerIndex) { LayerIndexStart = 0; LayerIndexEnd = Math.Min(currentLayerIndex, SlicerFile.LastLayerIndex); } + /// + /// Selects from a layer index to the last layer + /// + /// From layer index to select public void SelectCurrentToLastLayer(uint currentLayerIndex) { LayerIndexStart = Math.Min(currentLayerIndex, SlicerFile.LastLayerIndex); LayerIndexEnd = SlicerFile.LastLayerIndex; } + /// + /// Selects layer given a range type + /// + /// + /// public void SelectLayers(LayerRangeSelection range) { switch (range) @@ -436,67 +516,141 @@ public abstract class Operation : BindableBase, IDisposable /// public virtual void InitWithSlicerFile() { } + /// + /// Clears the ROI and set to empty + /// public void ClearROI() { ROI = Rectangle.Empty; } + /// + /// Clear and + /// public void ClearROIandMasks() { ClearROI(); ClearMasks(); } + /// + /// Set only if not set already + /// + /// ROI to set public void SetROIIfEmpty(Rectangle roi) { if (HaveROI) return; ROI = roi; } + /// + /// Gets the size, but if empty returns the file resolution size instead + /// + /// public Size GetRoiSizeOrDefault() => GetRoiSizeOrDefault(SlicerFile.Resolution); - public Size GetRoiSizeOrDefault(Mat? defaultMat) => defaultMat is null ? GetRoiSizeOrDefault() : GetRoiSizeOrDefault(defaultMat.Size); + + /// + /// Gets the size, but if empty returns size instead + /// + /// + /// + public Size GetRoiSizeOrDefault(Mat? src) => src is null ? GetRoiSizeOrDefault() : GetRoiSizeOrDefault(src.Size); + + /// + /// Gets the size, but if empty returns the size from instead + /// + /// + /// public Size GetRoiSizeOrDefault(Rectangle fallbackRectangle) => GetRoiSizeOrDefault(fallbackRectangle.Size); + + /// + /// Gets the size, but if empty returns the instead + /// + /// + /// public Size GetRoiSizeOrDefault(Size fallbackSize) { return HaveROI ? _roi.Size : fallbackSize; } + /// + /// Gets the size, but if empty returns the model volume bounds size instead + /// + /// public Size GetRoiSizeOrVolumeSize() => GetRoiSizeOrVolumeSize(_originalBoundingRectangle.Size); + /// + /// Gets the size, but if empty returns the instead + /// + /// + /// public Size GetRoiSizeOrVolumeSize(Size fallbackSize) { return HaveROI ? _roi.Size : fallbackSize; } - - public Mat GetRoiOrDefault(Mat defaultMat) + /// + /// Gets a cropped shared from by the , but if empty return the instead + /// + /// + /// + public Mat GetRoiOrDefault(Mat src) { - return HaveROI && defaultMat.Size != _roi.Size ? defaultMat.Roi(_roi) : defaultMat; + return HaveROI && src.Size != _roi.Size ? src.Roi(_roi) : src; } - public Mat GetRoiOrDefault(Mat defaultMat, Rectangle fallbackRoi) + /// + /// Gets a cropped shared from by the , but if empty crop by + /// + /// + /// + /// + public Mat GetRoiOrDefault(Mat src, Rectangle fallbackRoi) { - if (HaveROI && defaultMat.Size != _roi.Size) return defaultMat.Roi(_roi); - if (fallbackRoi.IsEmpty) return defaultMat; - return defaultMat.Size != fallbackRoi.Size ? defaultMat.Roi(fallbackRoi) : defaultMat; + if (HaveROI && src.Size != _roi.Size) return src.Roi(_roi); + if (fallbackRoi.IsEmpty) return src; + return src.Size != fallbackRoi.Size ? src.Roi(fallbackRoi) : src; } + /// + /// Gets a cropped shared from by the , but if empty crop by + /// + /// + /// public Mat GetRoiOrVolumeBounds(Mat defaultMat) { return GetRoiOrDefault(defaultMat, _originalBoundingRectangle); } + /// + /// Gets the , but if empty returns + /// + /// + public Rectangle GetRoiOrVolumeBounds() => HaveROI ? _roi : _originalBoundingRectangle; + + /// + /// Clears all masks + /// public void ClearMasks() { MaskPoints = null; } + /// + /// Sets masks only if they are empty + /// + /// public void SetMasksIfEmpty(Point[][] points) { if (HaveMask) return; MaskPoints = points; } + /// + /// Returns a mask given + /// + /// + /// public Mat? GetMask(Mat mat) => GetMask(_maskPoints, mat); public Mat? GetMask(Point[][]? points, Mat mat) @@ -507,6 +661,12 @@ public abstract class Operation : BindableBase, IDisposable return GetRoiOrDefault(mask); } + /// + /// Apply a mask to a mat + /// + /// Original untouched mat + /// Mat to modify and apply the mask + /// Mask public void ApplyMask(Mat original, Mat result, Mat? mask) { if (mask is null) return; @@ -538,12 +698,23 @@ public abstract class Operation : BindableBase, IDisposable ApplyMask(original, result, mask); } - + /// + /// Execute the operation internally, to be override by class + /// + /// + /// + /// protected virtual bool ExecuteInternally(OperationProgress progress) { throw new NotImplementedException(); } + /// + /// Execute the operation + /// + /// + /// + /// public bool Execute(OperationProgress? progress = null) { if (_slicerFile is null) throw new InvalidOperationException($"{Title} can't execute due the lacking of a file parent."); @@ -566,6 +737,13 @@ public abstract class Operation : BindableBase, IDisposable public Task ExecuteAsync(OperationProgress? progress = null) => Task.Run(() => Execute(progress), progress?.Token ?? default); + /// + /// Execute the operation on a given + /// + /// + /// + /// + /// public virtual bool Execute(Mat mat, params object[]? arguments) { throw new NotImplementedException(); @@ -597,12 +775,21 @@ public abstract class Operation : BindableBase, IDisposable operation.MaskPoints = MaskPoints; } + /// + /// Serialize class to XML file + /// + /// + /// public void Serialize(string path, bool indent = false) { if(indent) XmlExtensions.SerializeToFile(this, path, XmlExtensions.SettingsIndent); else XmlExtensions.SerializeToFile(this, path); } + /// + /// Clone object + /// + /// public virtual Operation Clone() { var operation = MemberwiseClone() as Operation; @@ -623,6 +810,11 @@ public abstract class Operation : BindableBase, IDisposable #region Static Methods + /// + /// Deserialize from a XML file + /// + /// XML file path + /// public static Operation? Deserialize(string path) { if (!File.Exists(path)) return null; @@ -641,6 +833,12 @@ public abstract class Operation : BindableBase, IDisposable return Deserialize(path, type); } + /// + /// Deserialize from a XML file + /// + /// XML file path + /// + /// public static Operation Deserialize(string path, Type type) { var serializer = new XmlSerializer(type); @@ -650,6 +848,12 @@ public abstract class Operation : BindableBase, IDisposable return operation; } + /// + /// Deserialize from a XML file + /// + /// XML file path + /// + /// public static Operation Deserialize(string path, Operation operation) => Deserialize(path, operation.GetType()); #endregion diff --git a/UVtools.Core/Operations/OperationPCBExposure.cs b/UVtools.Core/Operations/OperationPCBExposure.cs index d8c721d..3177263 100644 --- a/UVtools.Core/Operations/OperationPCBExposure.cs +++ b/UVtools.Core/Operations/OperationPCBExposure.cs @@ -195,9 +195,7 @@ public class OperationPCBExposure : Operation protected override bool ExecuteInternally(OperationProgress progress) { using var mat = GetMat(); - var layer = new Layer(mat, SlicerFile); - layer.SetNoDelays(); - + SlicerFile.SuppressRebuildPropertiesWork(() => { SlicerFile.LayerHeight = (float) _layerHeight; @@ -207,7 +205,7 @@ public class OperationPCBExposure : Operation SlicerFile.LiftHeightTotal = 0; SlicerFile.SetNoDelays(); - SlicerFile.Layers = new[] { layer }; + SlicerFile.Layers = new[] { new Layer(mat, SlicerFile) }; }, true); diff --git a/UVtools.Core/Operations/OperationScripting.cs b/UVtools.Core/Operations/OperationScripting.cs index ce6835f..0ec7e6e 100644 --- a/UVtools.Core/Operations/OperationScripting.cs +++ b/UVtools.Core/Operations/OperationScripting.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; using System; +using System.Diagnostics; using System.IO; using System.Xml.Serialization; using UVtools.Core.FileFormats; @@ -176,7 +177,7 @@ public sealed class OperationScripting : Operation _scriptState = CSharpScript.RunAsync(_scriptText, ScriptOptions.Default.AddReferences(typeof(About).Assembly).WithAllowUnsafe(true), ScriptGlobals).Result; - + var result = _scriptState.ContinueWithAsync("ScriptInit();").Result; RaisePropertyChanged(nameof(CanExecute)); diff --git a/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs b/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs index fa818de..5c84694 100644 --- a/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs +++ b/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs @@ -18,7 +18,7 @@ namespace UVtools.Core.Suggestions; public sealed class SuggestionWaitTimeBeforeCure : Suggestion { #region Enums - public enum SuggestionWaitTimeBeforeCureSetType + public enum SuggestionWaitTimeBeforeCureSetType : byte { [Description("Fixed: Use a fixed time")] Fixed, @@ -27,10 +27,21 @@ public sealed class SuggestionWaitTimeBeforeCure : Suggestion [Description("Proportional to layer area")] ProportionalLayerArea, } + + public enum SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom : byte + { + [Description("Previous mass")] + Previous, + [Description("Average of previous masses")] + Average, + [Description("Maximum of previous masses")] + Maximum, + } #endregion #region Members private SuggestionWaitTimeBeforeCureSetType _setType = SuggestionWaitTimeBeforeCureSetType.Fixed; + private decimal _bottomHeight = 1; private decimal _fixedBottomWaitTimeBeforeCure = 20; private decimal _fixedWaitTimeBeforeCure = 2; @@ -41,12 +52,16 @@ public sealed class SuggestionWaitTimeBeforeCure : Suggestion private uint _proportionalLayerPixels = 1000000; private uint _proportionalBottomLayerArea = 1000; private uint _proportionalLayerArea = 1000; + private decimal _proportionalBottomWaitTimeBeforeCureMaximumDifference = 1; + private decimal _proportionalWaitTimeBeforeCureMaximumDifference = 1; + private SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom _proportionalCalculateMassFrom = SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Previous; + private decimal _proportionalMassRelativeHeight = 0.2m; private decimal _minimumBottomWaitTimeBeforeCure = 5; private decimal _minimumWaitTimeBeforeCure = 1; private decimal _maximumBottomWaitTimeBeforeCure = 120; private decimal _maximumWaitTimeBeforeCure = 12; private bool _createEmptyFirstLayer = true; - + #endregion #region Properties @@ -154,11 +169,29 @@ public sealed class SuggestionWaitTimeBeforeCure : Suggestion public bool IsSetTypeFixed => _setType == SuggestionWaitTimeBeforeCureSetType.Fixed; public bool IsSetTypeProportionalLayerPixels => _setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels; public bool IsSetTypeProportionalLayerArea => _setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerArea; - + + public SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom ProportionalCalculateMassFrom + { + get => _proportionalCalculateMassFrom; + set + { + if (!RaiseAndSetIfChanged(ref _proportionalCalculateMassFrom, value)) return; + RaisePropertyChanged(nameof(IsProportionalCalculateMassFromPrevious)); + } + } + + public decimal ProportionalMassRelativeHeight + { + get => _proportionalMassRelativeHeight; + set => RaiseAndSetIfChanged(ref _proportionalMassRelativeHeight, Math.Max(0, Math.Round(value, Layer.HeightPrecision))); + } + + public bool IsProportionalCalculateMassFromPrevious => _proportionalCalculateMassFrom == SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Previous; + public decimal BottomHeight { get => _bottomHeight; - set => RaiseAndSetIfChanged(ref _bottomHeight, value); + set => RaiseAndSetIfChanged(ref _bottomHeight, Math.Max(0, value)); } public decimal FixedBottomWaitTimeBeforeCure @@ -166,7 +199,7 @@ public sealed class SuggestionWaitTimeBeforeCure : Suggestion get => _fixedBottomWaitTimeBeforeCure; set { - if(!RaiseAndSetIfChanged(ref _fixedBottomWaitTimeBeforeCure, Math.Round(value, 2))) return; + if(!RaiseAndSetIfChanged(ref _fixedBottomWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2))) return; RaisePropertyChanged(nameof(WaitTimeBeforeCureTransitionDecrement)); } } @@ -176,7 +209,7 @@ public sealed class SuggestionWaitTimeBeforeCure : Suggestion get => _fixedWaitTimeBeforeCure; set { - if (!RaiseAndSetIfChanged(ref _fixedWaitTimeBeforeCure, Math.Round(value, 2))) return; + if (!RaiseAndSetIfChanged(ref _fixedWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2))) return; RaisePropertyChanged(nameof(WaitTimeBeforeCureTransitionDecrement)); } } @@ -196,13 +229,13 @@ public sealed class SuggestionWaitTimeBeforeCure : Suggestion public decimal ProportionalBottomWaitTimeBeforeCure { get => _proportionalBottomWaitTimeBeforeCure; - set => RaiseAndSetIfChanged(ref _proportionalBottomWaitTimeBeforeCure, Math.Round(value, 2)); + set => RaiseAndSetIfChanged(ref _proportionalBottomWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2)); } public decimal ProportionalWaitTimeBeforeCure { get => _proportionalWaitTimeBeforeCure; - set => RaiseAndSetIfChanged(ref _proportionalWaitTimeBeforeCure, Math.Round(value, 2)); + set => RaiseAndSetIfChanged(ref _proportionalWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2)); } @@ -230,28 +263,40 @@ public sealed class SuggestionWaitTimeBeforeCure : Suggestion set => RaiseAndSetIfChanged(ref _proportionalLayerArea, Math.Max(1, value)); } + public decimal ProportionalBottomWaitTimeBeforeCureMaximumDifference + { + get => _proportionalBottomWaitTimeBeforeCureMaximumDifference; + set => RaiseAndSetIfChanged(ref _proportionalBottomWaitTimeBeforeCureMaximumDifference, Math.Round(Math.Max(0, value), 2)); + } + + public decimal ProportionalWaitTimeBeforeCureMaximumDifference + { + get => _proportionalWaitTimeBeforeCureMaximumDifference; + set => RaiseAndSetIfChanged(ref _proportionalWaitTimeBeforeCureMaximumDifference, Math.Round(Math.Max(0, value), 2)); + } + public decimal MinimumBottomWaitTimeBeforeCure { get => _minimumBottomWaitTimeBeforeCure; - set => RaiseAndSetIfChanged(ref _minimumBottomWaitTimeBeforeCure, Math.Round(value, 2)); + set => RaiseAndSetIfChanged(ref _minimumBottomWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2)); } public decimal MinimumWaitTimeBeforeCure { get => _minimumWaitTimeBeforeCure; - set => RaiseAndSetIfChanged(ref _minimumWaitTimeBeforeCure, Math.Round(value, 2)); + set => RaiseAndSetIfChanged(ref _minimumWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2)); } public decimal MaximumBottomWaitTimeBeforeCure { get => _maximumBottomWaitTimeBeforeCure; - set => RaiseAndSetIfChanged(ref _maximumBottomWaitTimeBeforeCure, Math.Round(value, 2)); + set => RaiseAndSetIfChanged(ref _maximumBottomWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2)); } public decimal MaximumWaitTimeBeforeCure { get => _maximumWaitTimeBeforeCure; - set => RaiseAndSetIfChanged(ref _maximumWaitTimeBeforeCure, Math.Round(value, 2)); + set => RaiseAndSetIfChanged(ref _maximumWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2)); } public bool CreateEmptyFirstLayer @@ -410,23 +455,97 @@ public sealed class SuggestionWaitTimeBeforeCure : Suggestion } if (layer.NonZeroPixelCount <= 1) return 0; // Empty layer, don't need wait time - - return _setType switch + + float mass = 0; + if (layer.Index > 0 && _proportionalCalculateMassFrom != SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Previous && _proportionalMassRelativeHeight > 0) { - SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels => (float)Math.Round( - (isBottomLayer - ? layer.NonZeroPixelCount * _proportionalBottomWaitTimeBeforeCure / _proportionalBottomLayerPixels - : layer.NonZeroPixelCount * _proportionalWaitTimeBeforeCure / _proportionalLayerPixels).Clamp( - isBottomLayer ? _minimumBottomWaitTimeBeforeCure : _minimumWaitTimeBeforeCure, - isBottomLayer ? _maximumBottomWaitTimeBeforeCure : _maximumWaitTimeBeforeCure), 2), - SuggestionWaitTimeBeforeCureSetType.ProportionalLayerArea => (float) Math.Round( - (isBottomLayer - ? (decimal)layer.GetArea() * _proportionalBottomWaitTimeBeforeCure / _proportionalBottomLayerArea - : (decimal)layer.GetArea() * _proportionalWaitTimeBeforeCure / _proportionalLayerArea).Clamp( - isBottomLayer ? _minimumBottomWaitTimeBeforeCure : _minimumWaitTimeBeforeCure, - isBottomLayer ? _maximumBottomWaitTimeBeforeCure : _maximumWaitTimeBeforeCure), 2), + //var previousLayer = layer.GetPreviousLayerWithAtLeastPixelCountOf(2); // Skip all previous empty layer + //if (previousLayer is not null) layer = previousLayer; + uint count = 0; + + Layer? previousLayer = layer; + + while ((previousLayer = previousLayer!.PreviousLayer) is not null && (layer.PositionZ - previousLayer.PositionZ) <= (float)_proportionalMassRelativeHeight) + { + if(previousLayer.NonZeroPixelCount < 2) continue; // Skip empty layers + + count++; + switch (_proportionalCalculateMassFrom) + { + case SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Average: + mass += _setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels + ? previousLayer.NonZeroPixelCount + : previousLayer.GetArea(); + break; + case SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Maximum: + mass = Math.Max(_setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels + ? previousLayer.NonZeroPixelCount + : previousLayer.GetArea(), mass); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + if (_proportionalCalculateMassFrom == SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Average && mass > 0 && count > 0) + { + mass /= count; + } + } + + if (mass <= 0) + { + var previousLayer = layer.GetPreviousLayerWithAtLeastPixelCountOf(2); // Skip all previous empty layer + if (previousLayer is null) + { + return isBottomLayer ? (float)_fixedBottomWaitTimeBeforeCure : (float)_fixedWaitTimeBeforeCure; + } + mass = _setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels + ? previousLayer.NonZeroPixelCount + : previousLayer.GetArea(); + } + + float value = _setType switch + { + SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels => (float) (isBottomLayer + ? (decimal)mass * _proportionalBottomWaitTimeBeforeCure / _proportionalBottomLayerPixels + : (decimal)mass * _proportionalWaitTimeBeforeCure / _proportionalLayerPixels), + SuggestionWaitTimeBeforeCureSetType.ProportionalLayerArea => (float) (isBottomLayer + ? (decimal)mass * _proportionalBottomWaitTimeBeforeCure / _proportionalBottomLayerArea + : (decimal)mass * _proportionalWaitTimeBeforeCure / _proportionalLayerArea), _ => throw new ArgumentOutOfRangeException() }; + + if (isBottomLayer) + { + if (_proportionalBottomWaitTimeBeforeCureMaximumDifference > 0) + { + var previousLayer = layer.GetPreviousLayerWithAtLeastPixelCountOf(2); + if(previousLayer is not null) + { + value = value.Clamp( + Math.Max(0, previousLayer.WaitTimeBeforeCure - (float)_proportionalBottomWaitTimeBeforeCureMaximumDifference), + previousLayer.WaitTimeBeforeCure + (float)_proportionalBottomWaitTimeBeforeCureMaximumDifference); + } + } + } + else + { + if (_proportionalWaitTimeBeforeCureMaximumDifference > 0) + { + var previousLayer = layer.GetPreviousLayerWithAtLeastPixelCountOf(2); + if (previousLayer is not null) + { + value = value.Clamp( + Math.Max(0, previousLayer.WaitTimeBeforeCure - (float)_proportionalWaitTimeBeforeCureMaximumDifference), + previousLayer.WaitTimeBeforeCure + (float)_proportionalWaitTimeBeforeCureMaximumDifference); + } + } + } + + return (float)Math.Round((decimal)value, 2).Clamp( + isBottomLayer ? _minimumBottomWaitTimeBeforeCure : _minimumWaitTimeBeforeCure, + isBottomLayer ? _maximumBottomWaitTimeBeforeCure : _maximumWaitTimeBeforeCure); } public float CalculateWaitTime(Layer layer) => CalculateWaitTime(false, layer); diff --git a/UVtools.Core/SystemOS/SystemAware.cs b/UVtools.Core/SystemOS/SystemAware.cs index a7d7573..2b89217 100644 --- a/UVtools.Core/SystemOS/SystemAware.cs +++ b/UVtools.Core/SystemOS/SystemAware.cs @@ -255,6 +255,16 @@ public static class SystemAware } } + public static void StartThisApplication(string? arguments = null) + { + var executable = Environment.ProcessPath; + + if (File.Exists(executable)) // Direct execute + { + StartProcess(executable, arguments); + } + } + public static string GetExecutableName(string executable) { return OperatingSystem.IsWindows() ? $"{executable}.exe" : executable; diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index f9417e3..7c95a6a 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -10,7 +10,7 @@ https://github.com/sn4k3/UVtools https://github.com/sn4k3/UVtools MSLA/DLP, file analysis, calibration, repair, conversion and manipulation - 3.4.1 + 3.4.2 Copyright © 2020 PTRTECH UVtools.png AnyCPU;x64 @@ -66,11 +66,11 @@ - + - + diff --git a/UVtools.Installer/Code/Product.wxs b/UVtools.Installer/Code/Product.wxs index d392cc8..b14c5f7 100644 --- a/UVtools.Installer/Code/Product.wxs +++ b/UVtools.Installer/Code/Product.wxs @@ -35,6 +35,7 @@ + diff --git a/UVtools.InstallerMM/UVtools.InstallerMM.wxs b/UVtools.InstallerMM/UVtools.InstallerMM.wxs index e28de76..5e61ad8 100644 --- a/UVtools.InstallerMM/UVtools.InstallerMM.wxs +++ b/UVtools.InstallerMM/UVtools.InstallerMM.wxs @@ -2,7 +2,7 @@ - + @@ -1554,8 +1554,8 @@ - - + + diff --git a/UVtools.WPF/App.axaml.cs b/UVtools.WPF/App.axaml.cs index 163df81..0558025 100644 --- a/UVtools.WPF/App.axaml.cs +++ b/UVtools.WPF/App.axaml.cs @@ -12,6 +12,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Web; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -25,7 +26,6 @@ using UVtools.Core; using UVtools.Core.FileFormats; using UVtools.Core.Managers; using UVtools.Core.SystemOS; -using UVtools.WPF.Extensions; using UVtools.WPF.Structures; using UVtools.WPF.Windows; using Bitmap = Avalonia.Media.Imaging.Bitmap; @@ -180,10 +180,6 @@ public class App : Application UserSettings.Load(); UserSettings.SetVersion(); - MaterialManager.Load(); - OperationProfiles.Load(); - SuggestionManager.Load(); - /*ThemeSelector = ThemeSelector.Create(Path.Combine(ApplicationPath, "Assets", "Themes")); ThemeSelector.LoadSelectedTheme(Path.Combine(UserSettings.SettingsFolder, "selected.theme")); if (ThemeSelector.SelectedTheme.Name == "UVtoolsDark" || ThemeSelector.SelectedTheme.Name == "Light") @@ -208,9 +204,92 @@ public class App : Application ApplyTheme(); } - try + if (Program.IsCrashReport) { - if (CvInvoke.Init()) + //Program.Args = new[] {"--crash-report", "test2", "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." }; + if (string.IsNullOrWhiteSpace(Program.Args[1])) return; + if(string.IsNullOrWhiteSpace(Program.Args[2])) return; + var category = Program.Args[1]; + var message = $"{Program.Args[2]}\nCategory: {category}"; + + var bugReportMessageMk = $"# Report\n```\n{message}\n```"; + + try + { + var append = $"\n\n# System\n{AboutWindow.GetEssentialInformationStatic()}"; + message += append; + bugReportMessageMk += append; + } + catch + { + // ignored + } + + var append2 = $"\n\nMachine date time: {DateTime.Now}\n UTC date time: {DateTime.UtcNow}"; + message += append2; + bugReportMessageMk += $"{append2}\n\n# Additional information and Workflow\nComplete with additional information and the workflow that caused this crash."; + + try + { + Current?.Clipboard?.SetTextAsync(bugReportMessageMk); + } + catch + { + // ignored + } + + + var reportButton = MessageWindow.CreateButton("Report", "fas fa-bug"); + reportButton.Click += (sender, e) => + { + Current?.Clipboard?.SetTextAsync(bugReportMessageMk); + using var reader = new StringReader(message); + SystemAware.OpenBrowser($"https://github.com/sn4k3/UVtools/issues/new?assignees=sn4k3&labels=&template=bug_report.md&title={HttpUtility.UrlEncode($"[Crash] {reader.ReadLine()}")}&body={HttpUtility.UrlEncode("")}"); + e.Handled = true; + }; + + var helpButton = MessageWindow.CreateButton("Help", "fas fa-question"); + helpButton.Click += (sender, e) => + { + Current?.Clipboard?.SetTextAsync(bugReportMessageMk); + SystemAware.OpenBrowser("https://github.com/sn4k3/UVtools/discussions/categories/q-a"); + e.Handled = true; + }; + + var restartButton = MessageWindow.CreateButton("Restart", "fas fa-redo-alt"); + restartButton.Click += (sender, e) => + { + SystemAware.StartThisApplication(); + }; + + desktop.MainWindow = new MessageWindow($"{About.SoftwareWithVersion} - Crash report", + "far fa-frown", + $"{About.Software} crashed due an unexpected {category.ToLowerInvariant()} error.\nYou can report this error if you find necessary.\nFind more details below:\n", + message, + new[] + { + reportButton, + helpButton, + restartButton, + MessageWindow.CreateCloseButton("fas fa-sign-out-alt") + }); + } + else + { + try + { + if (!CvInvoke.Init()) + { + desktop.MainWindow = new CantRunWindow(); + } + } + catch (Exception e) + { + desktop.MainWindow = new CantRunWindow(); + Console.WriteLine(e.ToString()); + } + + if (desktop.MainWindow is null) { if (Design.IsDesignMode) { @@ -226,56 +305,15 @@ public class App : Application }; } - MainWindow = new MainWindow(); - if (UserSettings.Instance.General.StartMaximized) - { - MainWindow.WindowState = WindowState.Maximized; - } - else - { - if (UserSettings.Instance.General.RestoreWindowLastSize) - { - MainWindow.Width = UserSettings.Instance.General.LastWindowBounds.Width; - MainWindow.Height = UserSettings.Instance.General.LastWindowBounds.Height; - } + MaterialManager.Load(); + OperationProfiles.Load(); + SuggestionManager.Load(); - if (UserSettings.Instance.General.RestoreWindowLastPosition) - { - MainWindow.Position = new PixelPoint(UserSettings.Instance.General.LastWindowBounds.Location.X, UserSettings.Instance.General.LastWindowBounds.Location.Y); - } - } + MainWindow = new MainWindow(); desktop.MainWindow = MainWindow; } - else - { - desktop.MainWindow = new CantRunWindow(); - } - } - catch (Exception e) - { - desktop.MainWindow = new CantRunWindow(); - Console.WriteLine(e.ToString()); } - - /*try - { - if(!CvInvoke.Init()) - await MainWindow.MessageBoxError("UVtools can not init OpenCV library\n" + - "Please build or install this dependencies in order to run UVtools\n" + - "Check manual or page at 'Requirements' section for help", - "UVtools can not run"); - } - catch (Exception e) - { - await MainWindow.MessageBoxError("UVtools can not run due lack of dependencies from cvextern/OpenCV\n" + - "Please build or install this dependencies in order to run UVtools\n" + - "Check manual or page at 'Requirements' section for help\n\n" + - "Additional information:\n" + - $"{e}", "UVtools can not run"); - return; - }*/ - //desktop.Exit += (sender, e) => ThemeSelector.SaveSelectedTheme(Path.Combine(UserSettings.SettingsFolder, "selected.theme")); } diff --git a/UVtools.WPF/ConsoleArguments.cs b/UVtools.WPF/ConsoleArguments.cs index 788b104..552b500 100644 --- a/UVtools.WPF/ConsoleArguments.cs +++ b/UVtools.WPF/ConsoleArguments.cs @@ -25,7 +25,7 @@ public static class ConsoleArguments /// True if is a valid argument, otherwise false public static bool ParseArgs(string[] args) { - if(args is null || args.Length == 0) return false; + if(args.Length == 0) return false; if (args[0] is "--cmd" && args.Length > 1) { diff --git a/UVtools.WPF/Controls/Suggestions/SuggestionWaitTimeBeforeCureControl.axaml b/UVtools.WPF/Controls/Suggestions/SuggestionWaitTimeBeforeCureControl.axaml index 596e1e4..47d0c07 100644 --- a/UVtools.WPF/Controls/Suggestions/SuggestionWaitTimeBeforeCureControl.axaml +++ b/UVtools.WPF/Controls/Suggestions/SuggestionWaitTimeBeforeCureControl.axaml @@ -2,9 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="600" x:Class="UVtools.WPF.Controls.Suggestions.SuggestionWaitTimeBeforeCureControl"> - - + + - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - @@ -77,12 +77,6 @@ - typeof(Window); public DialogResults DialogResult { get; set; } = DialogResults.Unknown; @@ -78,6 +89,21 @@ public class WindowEx : Window, INotifyPropertyChanged, IStyleable public double WindowMaxHeight => this.GetScreenWorkingArea().Height - UserSettings.Instance.General.WindowsVerticalMargin; + public Size WindowMaxSize + { + get + { + var size = this.GetScreenWorkingArea(); + return new Size(size.Width - UserSettings.Instance.General.WindowsHorizontalMargin, + this.GetScreenWorkingArea().Height - UserSettings.Instance.General.WindowsVerticalMargin); + } + } + + public WindowConstrainsMaxSizeType WindowConstrainMaxSize { get; set; } = WindowConstrainsMaxSizeType.UserSettings; + + public double WindowsWidthMaxSizeRatio { get; set; } = 1; + public double WindowsHeightMaxSizeRatio { get; set; } = 1; + public UserSettings Settings => UserSettings.Instance; public virtual FileFormat SlicerFile @@ -97,10 +123,42 @@ public class WindowEx : Window, INotifyPropertyChanged, IStyleable protected override void OnOpened(EventArgs e) { base.OnOpened(e); - if (!CanResize && WindowState == WindowState.Normal) + AutoConstainsWindowMaxSize(); + } + + /*protected override Size MeasureOverride(Size availableSize) + { + var result = base.MeasureOverride(availableSize); + if (SizeToContent == SizeToContent.Manual) return result; + + if (MaxWidth > 0 && MaxWidth < result.Width) { - MaxWidth = WindowMaxWidth; - MaxHeight = WindowMaxHeight; + result = result.WithWidth(MaxWidth); + } + if (MaxHeight > 0 && MaxHeight < result.Height) + { + result = result.WithHeight(MaxHeight); + } + + return result; + }*/ + + public void AutoConstainsWindowMaxSize() + { + if (!CanResize && WindowState == WindowState.Normal && WindowConstrainMaxSize != WindowConstrainsMaxSizeType.None) + { + switch (WindowConstrainMaxSize) + { + case WindowConstrainsMaxSizeType.UserSettings: + MaxWidth = WindowMaxWidth; + MaxHeight = WindowMaxHeight; + break; + case WindowConstrainsMaxSizeType.Ratio: + var size = this.GetScreenWorkingArea(); + if (WindowsWidthMaxSizeRatio is > 0 and < 1) MaxWidth = size.Width * WindowsWidthMaxSizeRatio; + if (WindowsHeightMaxSizeRatio is > 0 and < 1) MaxHeight = size.Height * WindowsHeightMaxSizeRatio; + break; + } } } diff --git a/UVtools.WPF/ErrorLog.cs b/UVtools.WPF/ErrorLog.cs index 0955aed..1dd6775 100644 --- a/UVtools.WPF/ErrorLog.cs +++ b/UVtools.WPF/ErrorLog.cs @@ -20,6 +20,7 @@ public static class ErrorLog } catch (Exception exception) { + Console.WriteLine(exception); Debug.WriteLine(exception); } } diff --git a/UVtools.WPF/Extensions/WindowExtensions.cs b/UVtools.WPF/Extensions/WindowExtensions.cs index 76cbc30..8a67220 100644 --- a/UVtools.WPF/Extensions/WindowExtensions.cs +++ b/UVtools.WPF/Extensions/WindowExtensions.cs @@ -95,7 +95,7 @@ public static class WindowExtensions public static Screen GetCurrentScreen(this Window window) { return //window.Screens.ScreenFromVisual(window) ?? - window.Screens.ScreenFromVisual(App.MainWindow) ?? + window.Screens.ScreenFromVisual(App.MainWindow ?? window) ?? window.Screens.Primary ?? window.Screens.All[0]; } diff --git a/UVtools.WPF/MainWindow.LayerPreview.cs b/UVtools.WPF/MainWindow.LayerPreview.cs index ff0db6f..37e697e 100644 --- a/UVtools.WPF/MainWindow.LayerPreview.cs +++ b/UVtools.WPF/MainWindow.LayerPreview.cs @@ -491,8 +491,7 @@ public partial class MainWindow get { if (!LayerCache.IsCached) return "Pixels: 0"; - var pixelPercent = Math.Round(LayerCache.Layer.NonZeroPixelCount * 100.0 / (SlicerFile.ResolutionX * SlicerFile.ResolutionY), 2); - var text = $"Pixels: {LayerCache.Layer.NonZeroPixelCount} ({pixelPercent}%)"; + var text = $"Pixels: {LayerCache.Layer.NonZeroPixelCount} ({LayerCache.Layer.NonZeroPixelPercentage:F2}%)"; var volume = LayerCache.Layer.Volume; if (volume > 0) { diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index 1c24f95..aa65ff6 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -22,6 +22,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Org.BouncyCastle.Operators; using UVtools.AvaloniaControls; using UVtools.Core; using UVtools.Core.Extensions; @@ -250,6 +251,30 @@ public partial class MainWindow : WindowEx public MainWindow() { + if (Settings.General.StartMaximized) + { + WindowState = WindowState.Maximized; + } + else + { + if (Settings.General.RestoreWindowLastPosition) + { + Position = new PixelPoint(Settings.General.LastWindowBounds.Location.X, Settings.General.LastWindowBounds.Location.Y); + } + + if (Settings.General.RestoreWindowLastSize) + { + Width = Settings.General.LastWindowBounds.Width; + Height = Settings.General.LastWindowBounds.Height; + } + + var windowSize = this.GetScreenWorkingArea(); + if (Width >= windowSize.Width || Height >= windowSize.Height) + { + WindowState = WindowState.Maximized; + } + } + InitializeComponent(); //App.ThemeSelector?.EnableThemes(this); @@ -703,7 +728,6 @@ public partial class MainWindow : WindowEx protected override void OnOpened(EventArgs e) { base.OnOpened(e); - var windowSize = this.GetScreenWorkingArea(); var clientSizeObs = this.GetObservable(ClientSizeProperty); clientSizeObs.Subscribe(size => @@ -726,11 +750,6 @@ public partial class MainWindow : WindowEx ProcessFiles(args.Data.GetFileNames()?.ToArray()); }); - if (!Settings.General.StartMaximized && (ClientSize.Width >= windowSize.Width || ClientSize.Height >= windowSize.Height)) - { - WindowState = WindowState.Maximized; - } - AddLog($"{About.Software} start", Program.ProgramStartupTime.Elapsed.TotalSeconds); if (Settings.General.CheckForUpdatesOnStartup) @@ -1067,7 +1086,7 @@ public partial class MainWindow : WindowEx App.ApplyTheme(); } - if (oldLayerCompressionCodec != Settings.General.LayerCompressionCodec) + if (oldLayerCompressionCodec != Settings.General.LayerCompressionCodec && IsFileLoaded) { IsGUIEnabled = false; ShowProgressWindow($"Changing layers compression codec from {oldLayerCompressionCodec.ToString().ToUpper()} to {Settings.General.LayerCompressionCodec.ToString().ToUpper()}"); @@ -1210,7 +1229,7 @@ public partial class MainWindow : WindowEx private void UpdateTitle() { var title = $"{About.Software} "; - + if (IsFileLoaded) { title += $"File: {SlicerFile.Filename} ({Math.Round(LastStopWatch.ElapsedMilliseconds / 1000m, 2)}s) "; @@ -1254,17 +1273,39 @@ public partial class MainWindow : WindowEx try { var operation = Operation.Deserialize(files[i]); - await ShowRunOperation(operation); + operation.SlicerFile = SlicerFile; + if ((_globalModifiers & KeyModifiers.Shift) != 0) await RunOperation(operation); + else await ShowRunOperation(operation); } catch (Exception e) { Debug.WriteLine(e); - throw; } continue; } + if (files[i].EndsWith(".cs") || files[i].EndsWith(".csx")) + { + if (!IsFileLoaded) continue; + try + { + var operation = new OperationScripting(SlicerFile); + operation.ReloadScriptFromFile(files[i]); + if (operation.CanExecute) + { + if ((_globalModifiers & KeyModifiers.Shift) != 0) await RunOperation(operation); + else await ShowRunOperation(operation); + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + + continue; + } + if (i == 0 && !openNewWindow && (_globalModifiers & KeyModifiers.Shift) == 0) { @@ -1293,7 +1334,7 @@ public partial class MainWindow : WindowEx var fileNameOnly = Path.GetFileName(fileName); SlicerFile = FileFormat.FindByExtensionOrFilePath(fileName, true); if (SlicerFile is null) return; - + IsGUIEnabled = false; ShowProgressWindow($"Opening: {fileNameOnly}"); @@ -2024,6 +2065,8 @@ public partial class MainWindow : WindowEx public async Task RunOperation(Operation baseOperation) { if (baseOperation is null) return false; + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + baseOperation.SlicerFile ??= SlicerFile; switch (baseOperation) { diff --git a/UVtools.WPF/Program.cs b/UVtools.WPF/Program.cs index 2e71d42..b72d439 100644 --- a/UVtools.WPF/Program.cs +++ b/UVtools.WPF/Program.cs @@ -1,17 +1,12 @@ using System; using System.Diagnostics; -using System.Drawing; using System.Globalization; -using System.Runtime.ExceptionServices; +using System.Threading.Tasks; using Avalonia; -using Emgu.CV; -using Emgu.CV.Structure; using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.FontAwesome; using Projektanker.Icons.Avalonia.MaterialDesign; -using UVtools.Core.Extensions; -using UVtools.Core.Gerber; -using UVtools.WPF.Extensions; +using UVtools.Core.SystemOS; namespace UVtools.WPF; @@ -19,7 +14,8 @@ namespace UVtools.WPF; public static class Program { - public static string[] Args = null!; + public static string[] Args = Array.Empty(); + public static bool IsCrashReport; public static Stopwatch ProgramStartupTime = null!; // Initialization code. Don't use any Avalonia, third-party APIs or any @@ -43,6 +39,11 @@ public static class Program return; } + if (Args.Length > 2 && Args[0] == "--crash-report") + { + IsCrashReport = true; + } + /*Slicer slicer = new(Size.Empty, SizeF.Empty, "D:\\Cube100x100x100.stl"); var slices = slicer.SliceModel(0.05f); @@ -60,33 +61,43 @@ public static class Program //var machinesText = Machine.GenerateMachinePresetsFromPrusaSlicer(); // Add the event handler for handling non-UI thread exceptions to the event. - AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException; + AppDomain.CurrentDomain.UnhandledException += (sender, e) => HandleUnhandledException("Non-UI", (Exception)e.ExceptionObject); + TaskScheduler.UnobservedTaskException += (sender, e) => HandleUnhandledException("Task", e.Exception); //AppDomain.CurrentDomain.FirstChanceException += CurrentDomainOnFirstChanceException; - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + try + { + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + catch (Exception e) + { + HandleUnhandledException("Application", e); + } + // Closing } - private static void CurrentDomainOnFirstChanceException(object? sender, FirstChanceExceptionEventArgs e) + private static void HandleUnhandledException(string category, Exception ex) { - ErrorLog.AppendLine("First chance exception", e.Exception.ToString()); - } + ErrorLog.AppendLine($"Fatal {category} Error", ex.ToString()); - - private static async void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs e) - { - var ex = (Exception)e.ExceptionObject; - ErrorLog.AppendLine("Fatal Non-UI Error", ex.ToString()); - - try + if (!IsCrashReport) { - var errorMsg = $"An application error occurred. Please contact the administrator with the following information:\n\n{ex}"; - await App.MainWindow.MessageBoxError(errorMsg, "Fatal Non-UI Error"); - } - catch (Exception exception) - { - Debug.WriteLine(exception); + try + { + SystemAware.StartThisApplication($"--crash-report \"{category}\" \"{ex}\""); + //var errorMsg = $"An application error occurred. Please contact the administrator with the following information:\n\n{ex}"; + //await App.MainWindow.MessageBoxError(errorMsg, "Fatal Non-UI Error"); + } + catch (Exception exception) + { + Debug.WriteLine(exception); + Console.WriteLine(exception); + } } + + Environment.Exit(-1); } // Avalonia configuration, don't remove; also used by visual designer. diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj index 32d4b43..3be333b 100644 --- a/UVtools.WPF/UVtools.WPF.csproj +++ b/UVtools.WPF/UVtools.WPF.csproj @@ -12,7 +12,7 @@ LICENSE https://github.com/sn4k3/UVtools Git - 3.4.1 + 3.4.2 AnyCPU;x64 UVtools.png README.md @@ -39,15 +39,15 @@ 1701;1702; - - - - + + + + - + diff --git a/UVtools.WPF/UserSettings.cs b/UVtools.WPF/UserSettings.cs index 37adc53..69c45d6 100644 --- a/UVtools.WPF/UserSettings.cs +++ b/UVtools.WPF/UserSettings.cs @@ -12,6 +12,7 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.Drawing; using System.IO; +using System.Reactive.Disposables; using System.Xml.Serialization; using Avalonia.Media; using JetBrains.Annotations; @@ -1394,6 +1395,7 @@ public sealed class UserSettings : BindableBase public sealed class ToolsUserSettings : BindableBase { private bool _expandDescriptions = true; + private bool _promptForConfirmation = true; private bool _restoreLastUsedSettings; private bool _lastUsedSettingsKeepOnCloseFile = true; private bool _lastUsedSettingsPriorityOverDefaultProfile = true; @@ -1404,6 +1406,12 @@ public sealed class UserSettings : BindableBase set => RaiseAndSetIfChanged(ref _expandDescriptions, value); } + public bool PromptForConfirmation + { + get => _promptForConfirmation; + set => RaiseAndSetIfChanged(ref _promptForConfirmation, value); + } + public bool RestoreLastUsedSettings { get => _restoreLastUsedSettings; diff --git a/UVtools.WPF/Windows/AboutWindow.axaml.cs b/UVtools.WPF/Windows/AboutWindow.axaml.cs index 9fa0726..420f8f0 100644 --- a/UVtools.WPF/Windows/AboutWindow.axaml.cs +++ b/UVtools.WPF/Windows/AboutWindow.axaml.cs @@ -132,6 +132,24 @@ public class AboutWindow : WindowEx public void OpenLicense() => SystemAware.OpenBrowser(About.LicenseUrl); + public static string GetEssentialInformationStatic() + { + var message = new StringBuilder(); + message.AppendLine($"{About.SoftwareWithVersionArch}"); + message.AppendLine($"Operative system: {OSDescription}"); + message.AppendLine($"Processor: {ProcessorName}"); + message.AppendLine($"Processor cores: {ProcessorCount}"); + message.AppendLine($"Memory RAM: {MemoryRAMDescription}"); + message.AppendLine($"Runtime: {RuntimeDescription}"); + message.AppendLine($"Framework: {FrameworkDescription}"); + message.AppendLine($"AvaloniaUI: {AvaloniaUIDescription}"); + message.AppendLine($"OpenCV: {OpenCVVersion}"); + message.AppendLine(); + message.AppendLine($"Path: {App.ApplicationPath}"); + message.AppendLine($"Executable: {App.AppExecutable}"); + return message.ToString(); + } + private string GetEssentialInformation() { var message = new StringBuilder(); diff --git a/UVtools.WPF/Windows/CantRunWindow.axaml b/UVtools.WPF/Windows/CantRunWindow.axaml index 18e5307..870251c 100644 --- a/UVtools.WPF/Windows/CantRunWindow.axaml +++ b/UVtools.WPF/Windows/CantRunWindow.axaml @@ -1,4 +1,4 @@ - - + diff --git a/UVtools.WPF/Windows/CantRunWindow.axaml.cs b/UVtools.WPF/Windows/CantRunWindow.axaml.cs index d3cb4db..c3612ca 100644 --- a/UVtools.WPF/Windows/CantRunWindow.axaml.cs +++ b/UVtools.WPF/Windows/CantRunWindow.axaml.cs @@ -6,21 +6,18 @@ * of this license document, but changing it is not allowed. */ using Avalonia; -using Avalonia.Controls; using Avalonia.Markup.Xaml; using UVtools.Core.SystemOS; +using UVtools.WPF.Controls; namespace UVtools.WPF.Windows { - public partial class CantRunWindow : Window + public partial class CantRunWindow : WindowEx { public CantRunWindow() { InitializeComponent(); DataContext = this; -#if DEBUG - this.AttachDevTools(); -#endif } private void InitializeComponent() diff --git a/UVtools.WPF/Windows/MessageWindow.axaml b/UVtools.WPF/Windows/MessageWindow.axaml new file mode 100644 index 0000000..3215c96 --- /dev/null +++ b/UVtools.WPF/Windows/MessageWindow.axaml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UVtools.WPF/Windows/MessageWindow.axaml.cs b/UVtools.WPF/Windows/MessageWindow.axaml.cs new file mode 100644 index 0000000..407fd80 --- /dev/null +++ b/UVtools.WPF/Windows/MessageWindow.axaml.cs @@ -0,0 +1,141 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Markup.Xaml; +using UVtools.WPF.Controls; + +namespace UVtools.WPF.Windows +{ + public partial class MessageWindow : WindowEx + { + private string? _headerIcon; + private ushort _headerIconSize = 64; + private string _headerTitle; + private bool _aboutButtonIsVisible = true; + private string _message; + + private readonly StackPanel _buttonsRightPanel; + + public string? HeaderIcon + { + get => _headerIcon; + set + { + if (!RaiseAndSetIfChanged(ref _headerIcon, value)) return; + RaisePropertyChanged(nameof(HeaderIsVisible)); + } + } + + public ushort HeaderIconSize + { + get => _headerIconSize; + set => RaiseAndSetIfChanged(ref _headerIconSize, value); + } + + public string? HeaderTitle + { + get => _headerTitle; + set + { + if(!RaiseAndSetIfChanged(ref _headerTitle, value)) return; + RaisePropertyChanged(nameof(HeaderIsVisible)); + } + } + + public bool HeaderIsVisible => !string.IsNullOrWhiteSpace(HeaderIcon) || !string.IsNullOrWhiteSpace(HeaderTitle); + + public string Message + { + get => _message; + set => RaiseAndSetIfChanged(ref _message, value); + } + + public bool AboutButtonIsVisible + { + get => _aboutButtonIsVisible; + set => RaiseAndSetIfChanged(ref _aboutButtonIsVisible, value); + } + + public MessageWindow() + { + InitializeComponent(); + + CanResize = Settings.General.WindowsCanResize; + if (WindowStartupLocation == WindowStartupLocation.CenterOwner && Owner is null) + { + WindowStartupLocation = WindowStartupLocation.CenterScreen; + } + + AutoConstainsWindowMaxSize(); + + _buttonsRightPanel = this.FindControl("ButtonsRightPanel"); + DataContext = this; + } + + public MessageWindow(string title, string? headerIcon, string? headerTitle, string message, ButtonWithIcon[]? buttons = null) : this() + { + Title = title; + HeaderIcon = headerIcon; + HeaderTitle = headerTitle; + Message = message; + + if (buttons is not null && buttons.Length > 0) + { + _buttonsRightPanel.Children.Clear(); + _buttonsRightPanel.Children.AddRange(buttons); + + foreach (var button in buttons) + { + button.Click += (sender, e) => + { + if (e.Handled) return; + Close(button); + e.Handled = true; + }; + } + } + } + + public MessageWindow(string title, string message, ButtonWithIcon[]? buttons = null) : this(title, null, null, message, buttons) { } + + /*protected override void OnOpened(EventArgs e) + { + base.OnOpened(e); + }*/ + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public async void OpenAboutWindow() + { + await new AboutWindow().ShowDialog(this); + } + + public static ButtonWithIcon CreateButton(string? text, string? icon, int padding = 10) => + new() + { + Icon = icon, + Text = text, + VerticalAlignment = VerticalAlignment.Center, + Padding = new Thickness(padding) + }; + + public static ButtonWithIcon CreateButton(string? text, int padding = 10) => CreateButton(text, null, padding); + + public static ButtonWithIcon CreateCancelButton(string? icon = null, int padding = 10) + { + var btn = CreateButton("Cancel", icon, padding); + btn.IsCancel = true; + return btn; + } + + public static ButtonWithIcon CreateCloseButton(string? icon = null, int padding = 10) + { + var btn = CreateButton("Close", icon, padding); + btn.IsCancel = true; + return btn; + } + } +} diff --git a/UVtools.WPF/Windows/SettingsWindow.axaml b/UVtools.WPF/Windows/SettingsWindow.axaml index 88c8e5d..cda1778 100644 --- a/UVtools.WPF/Windows/SettingsWindow.axaml +++ b/UVtools.WPF/Windows/SettingsWindow.axaml @@ -1886,6 +1886,9 @@ ToolTip.Tip="Disable this if you want to gain some vertical space for windows, you can still toggle the descriptions and read them if required." Content="Expand and show descriptions by default"/> + + diff --git a/UVtools.WPF/Windows/ToolWindow.axaml.cs b/UVtools.WPF/Windows/ToolWindow.axaml.cs index fea7a28..0dc2542 100644 --- a/UVtools.WPF/Windows/ToolWindow.axaml.cs +++ b/UVtools.WPF/Windows/ToolWindow.axaml.cs @@ -815,10 +815,9 @@ public class ToolWindow : WindowEx ToolControl.BaseOperation.SetMasksIfEmpty(Masks); if (!await ToolControl.ValidateForm()) return; - if (!string.IsNullOrEmpty(ToolControl.BaseOperation.ConfirmationText)) + if (Settings.Tools.PromptForConfirmation && !string.IsNullOrEmpty(ToolControl.BaseOperation.ConfirmationText)) { - var result = - await this.MessageBoxQuestion( + var result = await this.MessageBoxQuestion( $"Are you sure you want to {ToolControl.BaseOperation.ConfirmationText}"); if (result != ButtonResult.Yes) return; }