diff options
author | Tiago Conceição <Tiago_caza@hotmail.com> | 2020-09-22 06:29:11 +0300 |
---|---|---|
committer | Tiago Conceição <Tiago_caza@hotmail.com> | 2020-09-22 06:29:11 +0300 |
commit | 732d27e647b51a65c2736320d4f0ee590acb0d12 (patch) | |
tree | bc7d6b7074d0f4d6826707e59285ecf3aac2aca4 | |
parent | 0ce27a1f9c61b6f852f44efa02a1b7fc834c9efe (diff) |
WPF progress
-rw-r--r-- | UVtools.Core/FileFormats/FileFormat.cs | 3 | ||||
-rw-r--r-- | UVtools.Core/FileFormats/IFileFormat.cs | 8 | ||||
-rw-r--r-- | UVtools.Core/FileFormats/PHZFile.cs | 1 | ||||
-rw-r--r-- | UVtools.Core/Operations/OperationProgress.cs | 71 | ||||
-rw-r--r-- | UVtools.GUI/Forms/FrmLoading.cs | 2 | ||||
-rw-r--r-- | UVtools.WPF/App.axaml | 1 | ||||
-rw-r--r-- | UVtools.WPF/App.axaml.cs | 16 | ||||
-rw-r--r-- | UVtools.WPF/AppSettings.cs | 2 | ||||
-rw-r--r-- | UVtools.WPF/Controls/AdvancedImageBox.cs | 13 | ||||
-rw-r--r-- | UVtools.WPF/LayerCache.cs | 16 | ||||
-rw-r--r-- | UVtools.WPF/MainWindow.axaml | 204 | ||||
-rw-r--r-- | UVtools.WPF/MainWindow.axaml.cs | 194 | ||||
-rw-r--r-- | UVtools.WPF/Program.cs | 15 | ||||
-rw-r--r-- | UVtools.WPF/Structures/LogItem.cs | 61 | ||||
-rw-r--r-- | UVtools.WPF/Structures/PixelPicker.cs | 46 | ||||
-rw-r--r-- | UVtools.WPF/UVtools.WPF.csproj | 1 | ||||
-rw-r--r-- | UVtools.WPF/Windows/ProgressWindow.axaml | 19 | ||||
-rw-r--r-- | UVtools.WPF/Windows/ProgressWindow.axaml.cs | 61 |
18 files changed, 644 insertions, 90 deletions
diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index 375d315..7c72f15 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -321,6 +321,8 @@ namespace UVtools.Core.FileFormats set => _haveModifiedLayers = value; } // => LayerManager.IsModified; + public Size Resolution => new Size((int) ResolutionX, (int) ResolutionY); + public abstract uint ResolutionX { get; set; } public abstract uint ResolutionY { get; set; } @@ -363,6 +365,7 @@ namespace UVtools.Core.FileFormats public StringBuilder GCode { get; set; } + public bool HaveGCode => !(GCode is null); public abstract object[] Configs { get; } public bool IsValid => !ReferenceEquals(FileFullPath, null); diff --git a/UVtools.Core/FileFormats/IFileFormat.cs b/UVtools.Core/FileFormats/IFileFormat.cs index 0ca1b84..e166b55 100644 --- a/UVtools.Core/FileFormats/IFileFormat.cs +++ b/UVtools.Core/FileFormats/IFileFormat.cs @@ -7,6 +7,7 @@ */ using System; +using System.Drawing; using System.Text; using Emgu.CV; using UVtools.Core.Operations; @@ -82,6 +83,8 @@ namespace UVtools.Core.FileFormats /// </summary> LayerManager LayerManager { get; set; } + Size Resolution { get; } + /// <summary> /// Gets the image width resolution /// </summary> @@ -208,6 +211,11 @@ namespace UVtools.Core.FileFormats StringBuilder GCode { get; set; } /// <summary> + /// Gets if this file have available gcode + /// </summary> + bool HaveGCode { get; } + + /// <summary> /// Get all configuration objects with properties and values /// </summary> object[] Configs { get; } diff --git a/UVtools.Core/FileFormats/PHZFile.cs b/UVtools.Core/FileFormats/PHZFile.cs index b331ace..8a68196 100644 --- a/UVtools.Core/FileFormats/PHZFile.cs +++ b/UVtools.Core/FileFormats/PHZFile.cs @@ -978,6 +978,7 @@ namespace UVtools.Core.FileFormats public override void Decode(string fileFullPath, OperationProgress progress = null) { base.Decode(fileFullPath, progress); + if(progress is null) progress = new OperationProgress(OperationProgress.StatusGatherLayers, LayerCount); using (var inputFile = new FileStream(fileFullPath, FileMode.Open, FileAccess.Read)) { diff --git a/UVtools.Core/Operations/OperationProgress.cs b/UVtools.Core/Operations/OperationProgress.cs index f34d339..3ffce1f 100644 --- a/UVtools.Core/Operations/OperationProgress.cs +++ b/UVtools.Core/Operations/OperationProgress.cs @@ -6,11 +6,14 @@ * of this license document, but changing it is not allowed. */ using System; +using System.Diagnostics; using System.Threading; +using UVtools.Core.Extensions; +using UVtools.Core.Objects; namespace UVtools.Core.Operations { - public sealed class OperationProgress + public sealed class OperationProgress : BindableBase { public const string StatusDecodeThumbnails = "Decoded Thumbnails"; public const string StatusGatherLayers = "Gathered Layers"; @@ -34,6 +37,11 @@ namespace UVtools.Core.Operations public CancellationToken Token => TokenSource.Token; private bool _canCancel = true; + private string _title = "Operation"; + private string _itemName = StatusDecodeLayers; + private uint _processedItems; + private uint _itemCount; + public OperationProgress() @@ -50,6 +58,8 @@ namespace UVtools.Core.Operations _canCancel = canCancel; } + public Stopwatch StopWatch { get; } = new Stopwatch(); + /// <summary> /// Gets or sets if operation can be cancelled /// </summary> @@ -60,35 +70,74 @@ namespace UVtools.Core.Operations if (!_canCancel) return _canCancel; return !Token.IsCancellationRequested && Token.CanBeCanceled && _canCancel; } - set => _canCancel = value; + set => SetProperty(ref _canCancel, value); + } + + /// <summary> + /// Gets or sets the item name for the operation + /// </summary> + public string Title + { + get => _title; + set => SetProperty(ref _title, value); } + public string ElapsedTimeStr => $"{StopWatch.Elapsed.Minutes}m {StopWatch.Elapsed.Seconds}s {StopWatch.Elapsed.Milliseconds}ms"; + /// <summary> /// Gets or sets the item name for the operation /// </summary> - public string ItemName { get; set; } = StatusDecodeLayers; + public string ItemName + { + get => _itemName; + set => SetProperty(ref _itemName, value); + } /// <summary> /// Gets or sets the number of processed items /// </summary> - public uint ProcessedItems { get; set; } + public uint ProcessedItems + { + get => _processedItems; + set + { + //_processedItems = value; + SetProperty(ref _processedItems, value); + OnPropertyChanged(nameof(ProgressPercent)); + OnPropertyChanged(nameof(Description)); + } + } /// <summary> /// Gets or sets the total of item count on this operation /// </summary> - public uint ItemCount { get; set; } + public uint ItemCount + { + get => _itemCount; + set + { + SetProperty(ref _itemCount, value); + OnPropertyChanged(nameof(IsIndeterminate)); + OnPropertyChanged(nameof(ProgressPercent)); + OnPropertyChanged(nameof(Description)); + } + } /// <summary> /// Gets the remaining items to be processed /// </summary> public uint RemainingItems => ItemCount - ProcessedItems; - public int ProgressStep => (int) Math.Min(ProcessedItems * 100 / ItemCount, 100); + public int ProgressStep => (int)ProgressPercent; + + public string Description => ToString(); + + public bool IsIndeterminate => ItemCount == 0; /// <summary> /// Gets the progress from 0 to 100% /// </summary> - public double ProgressPercent => Math.Round(ProcessedItems * 100.0 / ItemCount, 2); + public double ProgressPercent => ItemCount == 0 ? 0 : Math.Round(ProcessedItems * 100.0 / ItemCount, 2).Clamp(0, 100); public static OperationProgress operator +(OperationProgress progress, uint value) { @@ -122,5 +171,13 @@ namespace UVtools.Core.Operations $"{ProcessedItems}/? {ItemName}" : $"{ProcessedItems}/{ItemCount} {ItemName} | {ProgressPercent:0.00}%"; } + + public void TriggerRefresh() + { + OnPropertyChanged(nameof(ElapsedTimeStr)); + OnPropertyChanged(nameof(CanCancel)); + //OnPropertyChanged(nameof(ProgressPercent)); + //OnPropertyChanged(nameof(Description)); + } } } diff --git a/UVtools.GUI/Forms/FrmLoading.cs b/UVtools.GUI/Forms/FrmLoading.cs index 61cd510..8806c57 100644 --- a/UVtools.GUI/Forms/FrmLoading.cs +++ b/UVtools.GUI/Forms/FrmLoading.cs @@ -11,7 +11,7 @@ namespace UVtools.GUI.Forms public partial class FrmLoading : Form { public Stopwatch StopWatch { get; } = new Stopwatch(); - public OperationProgress Progress { get; set; } + public OperationProgress Progress { get; private set; } private LogItem OperationLog; diff --git a/UVtools.WPF/App.axaml b/UVtools.WPF/App.axaml index 9b64e5d..3eb218e 100644 --- a/UVtools.WPF/App.axaml +++ b/UVtools.WPF/App.axaml @@ -4,6 +4,7 @@ <Application.Styles> <StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/> <StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/> + <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml"/> <StyleInclude Source="avares://ThemeEditor.Controls.ColorPicker/ColorPicker.xaml"/> </Application.Styles> </Application> diff --git a/UVtools.WPF/App.axaml.cs b/UVtools.WPF/App.axaml.cs index 9e0b688..f22ddb1 100644 --- a/UVtools.WPF/App.axaml.cs +++ b/UVtools.WPF/App.axaml.cs @@ -6,6 +6,8 @@ * of this license document, but changing it is not allowed. */ +using System; +using System.Diagnostics; using System.Globalization; using System.Threading; using Avalonia; @@ -49,7 +51,19 @@ namespace UVtools.WPF } #region Utilities - + public static void NewInstance(string filePath) + { + try + { + var info = new ProcessStartInfo("UVtools.exe", $"\"{filePath}\""); + Process.Start(info)?.Dispose(); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + + } #endregion } diff --git a/UVtools.WPF/AppSettings.cs b/UVtools.WPF/AppSettings.cs index 9b35e1f..162d76c 100644 --- a/UVtools.WPF/AppSettings.cs +++ b/UVtools.WPF/AppSettings.cs @@ -27,7 +27,7 @@ namespace UVtools.WPF /// <summary> /// Returns the zoom level that will be used for autozoom actions /// </summary> - public const ushort LockedZoomLevel = 1200; + public static int LockedZoomLevel => ZoomLevels[UserSettings.Instance.LayerPreview.ZoomLockLevelIndex + ZoomLevelSkipCount]; /// <summary> diff --git a/UVtools.WPF/Controls/AdvancedImageBox.cs b/UVtools.WPF/Controls/AdvancedImageBox.cs index 513e11d..0922927 100644 --- a/UVtools.WPF/Controls/AdvancedImageBox.cs +++ b/UVtools.WPF/Controls/AdvancedImageBox.cs @@ -408,8 +408,17 @@ namespace UVtools.WPF.Controls set { _image = value; - SizedContainer.Width = _image.Size.Width; - SizedContainer.Height = _image.Size.Height; + if (_image is null) + { + SizedContainer.Width = 0; + SizedContainer.Height = 0; + } + else + { + SizedContainer.Width = _image.Size.Width; + SizedContainer.Height = _image.Size.Height; + } + InvalidateVisual(); } } diff --git a/UVtools.WPF/LayerCache.cs b/UVtools.WPF/LayerCache.cs index 53660fc..e46d4e7 100644 --- a/UVtools.WPF/LayerCache.cs +++ b/UVtools.WPF/LayerCache.cs @@ -10,28 +10,35 @@ using System; using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Util; +using UVtools.Core; namespace UVtools.WPF { public sealed class LayerCache { + private Layer _layer; private Mat _image; private Array _layerHierarchyJagged; private VectorOfVectorOfPoint _layerContours; private Mat _layerHierarchy; - public Mat Image + public bool IsCached => !ReferenceEquals(_layer, null); + + public Layer Layer { - get => _image; + get => _layer; set { Clear(); - _image = value; + _layer = value; + _image = _layer.LayerMat; ImageBgr = new Mat(); - CvInvoke.CvtColor(value, ImageBgr, ColorConversion.Gray2Bgr); + CvInvoke.CvtColor(_image, ImageBgr, ColorConversion.Gray2Bgr); } } + public Mat Image => _image; + public Mat ImageBgr { get; set; } public VectorOfVectorOfPoint LayerContours @@ -81,6 +88,7 @@ namespace UVtools.WPF /// </summary> public void Clear() { + _layer = null; _image?.Dispose(); ImageBgr?.Dispose(); _layerContours?.Dispose(); diff --git a/UVtools.WPF/MainWindow.axaml b/UVtools.WPF/MainWindow.axaml index 35058e9..76f60c1 100644 --- a/UVtools.WPF/MainWindow.axaml +++ b/UVtools.WPF/MainWindow.axaml @@ -11,27 +11,42 @@ Icon="/Assets/Icons/UVtools.ico" MinWidth="1024" MinHeight="600" + DragDrop.AllowDrop="True" > <DockPanel> <Menu DockPanel.Dock="Top"> <MenuItem Name="MainMenu.File" Header="_File"> - <MenuItem Name="MainMenu.File.Open" Header="_Open" HotKey="Ctrl+O" InputGesture="Ctrl+O" Command="{Binding MenuFileOpenClicked}"> + <MenuItem + Name="MainMenu.File.Open" + Header="_Open" + HotKey="Ctrl+O" InputGesture="Ctrl+O" + Command="{Binding MenuFileOpenClicked}"> <MenuItem.Icon> <Image Source="\Assets\Icons\open-16x16.png"/> </MenuItem.Icon> </MenuItem> - <MenuItem Name="MainMenu.File.OpenNewWindow" Header="Open in _new window" HotKey="Ctrl+Shift+O" InputGesture="Ctrl+Shift+O" Command="{Binding ButtonClicked}"> + <MenuItem Name="MainMenu.File.OpenNewWindow" Header="Open in _new window" HotKey="Ctrl+Shift+O" InputGesture="Ctrl+Shift+O" Command="{Binding MenuFileOpenNewWindowClicked}"> <MenuItem.Icon> <Image Source="\Assets\Icons\open-16x16.png"/> </MenuItem.Icon> </MenuItem> - <MenuItem Name="MainMenu.File.Reload" Header="_Reload" HotKey="Ctrl+F5" InputGesture="Ctrl+F5" IsEnabled="{Binding IsFileLoaded}"> + <MenuItem + Name="MainMenu.File.Reload" + Header="_Reload" + HotKey="Ctrl+F5" InputGesture="Ctrl+F5" + IsEnabled="{Binding IsFileLoaded}" + Command="{Binding ReloadFile}"> <MenuItem.Icon> <Image Source="\Assets\Icons\file-refresh-16x16.png"/> </MenuItem.Icon> </MenuItem> - <MenuItem Name="MainMenu.File.Save" Header="_Save" HotKey="Ctrl+S" InputGesture="Ctrl+S"> + <MenuItem + Name="MainMenu.File.Save" + Header="_Save" + HotKey="Ctrl+S" InputGesture="Ctrl+S" + IsEnabled="{Binding CanSave}" + > <MenuItem.Icon> <Image Source="\Assets\Icons\save-16x16.png"/> </MenuItem.Icon> @@ -41,7 +56,12 @@ <Image Source="\Assets\Icons\save-as-16x16.png"/> </MenuItem.Icon> </MenuItem> - <MenuItem Name="MainMenu.File.Close" Header="_Close" HotKey="Ctrl+W" InputGesture="Ctrl+W" IsEnabled="{Binding IsFileLoaded}"> + <MenuItem + Name="MainMenu.File.Close" + Header="_Close" + HotKey="Ctrl+W" InputGesture="Ctrl+W" + IsEnabled="{Binding IsFileLoaded}" + Command="{Binding CloseFile}"> <MenuItem.Icon> <Image Source="\Assets\Icons\file-close-16x16.png"/> </MenuItem.Icon> @@ -102,35 +122,106 @@ </MenuItem> </Menu> - <TabControl IsEnabled="{Binding IsFileLoaded}" DockPanel.Dock="Left" Width="300"> - <TabItem Header="Information" VerticalContentAlignment="Center"> - <TextBlock Text="I am in the circle page !" HorizontalAlignment="Left" VerticalAlignment="Center"/> - </TabItem> + <TabControl + IsEnabled="{Binding IsFileLoaded}" + DockPanel.Dock="Left" + Width="400" + SelectedIndex="3"> <TabItem> <TabItem.Header> - <TextBlock VerticalAlignment="Center">Issues</TextBlock> + <StackPanel VerticalAlignment="Center" Orientation="Horizontal"> + <Image Source="/Assets/Icons/button-info-16x16.png" Width="16"/> + <TextBlock Margin="5,0,0,0">Information</TextBlock> + </StackPanel> + </TabItem.Header> + </TabItem> + + <TabItem IsVisible="{Binding SlicerFile.HaveGCode}"> + <TabItem.Header> + <StackPanel VerticalAlignment="Center" Orientation="Horizontal"> + <Image Source="/Assets/Icons/code-16x16.png" Width="16"/> + <TextBlock Margin="5,0,0,0">Gcode</TextBlock> + </StackPanel> </TabItem.Header> - <StackPanel> - <TextBlock Text="I am in the triangle page ! I'll put a button to show you that each page contains what you want." HorizontalAlignment="Left" VerticalAlignment="Center"/> - <Button>A button in the triangle page !</Button> - </StackPanel> </TabItem> + + <TabItem> <TabItem.Header> - <TextBlock VerticalAlignment="Center">GCode</TextBlock> + <StackPanel VerticalAlignment="Center" Orientation="Horizontal"> + <Image Source="/Assets/Icons/warning-16x16.png" Width="16"/> + <TextBlock Margin="5,0,0,0">Issues</TextBlock> + </StackPanel> </TabItem.Header> - <StackPanel Orientation="Horizontal"> - <TextBlock Text="Square : " HorizontalAlignment="Left" VerticalAlignment="Center"/> - <Rectangle Fill="Blue" Width="63" Height="41"/> - </StackPanel> + + </TabItem> + <TabItem> <TabItem.Header> - <TextBlock VerticalAlignment="Center">Log</TextBlock> + <StackPanel VerticalAlignment="Center" Orientation="Horizontal"> + <Image Source="/Assets/Icons/log-16x16.png" Width="16"/> + <TextBlock Margin="5,0,0,0">Log</TextBlock> + </StackPanel> </TabItem.Header> - <StackPanel Orientation="Horizontal"> - <TextBlock Text="Square : " HorizontalAlignment="Left" VerticalAlignment="Center"/> - <Rectangle Fill="Blue" Width="63" Height="41"/> + + <StackPanel Orientation="Vertical"> + + <Grid ColumnDefinitions="Auto,Auto,*"> + <Button + Grid.Column="0" + Command="{Binding Logs.Clear}"> + <StackPanel Orientation="Horizontal"> + <Image Source="/Assets/Icons/trash-16x16.png" Width="16"/> + <TextBlock Margin="5,0,0,0">Clear</TextBlock> + </StackPanel> + </Button> + + <CheckBox + Grid.Column="1" + VerticalAlignment="Center" + ToolTip.Tip="Shows extra information usefull to debug problems." + IsChecked="{Binding IsVerbose}" + Content="Verbose" + Margin="5,0,0,0" + /> + + <TextBlock + Grid.Column="2" + HorizontalAlignment="Right" + VerticalAlignment="Center" + Text="{Binding Logs.Count, StringFormat=Operations: \{0\}}"/> + </Grid> + + + <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" Orientation="Horizontal"> + + + + </StackPanel> + <DataGrid Items="{Binding Logs}" + CanUserReorderColumns="True" + CanUserResizeColumns="True" + CanUserSortColumns="True" + GridLinesVisibility="All" + IsReadOnly="True" + ClipboardCopyMode="IncludeHeader" + > + <DataGrid.Columns> + <DataGridTextColumn Header="#" + Binding="{Binding Index}" + Width="Auto" /> + <DataGridTextColumn Header="Started" + Binding="{Binding StartTime}" + Width="Auto" /> + <DataGridTextColumn Header="Time(s)" + Binding="{Binding ElapsedTime}" + Width="Auto" /> + <DataGridTextColumn Header="Description" + Binding="{Binding Description}" + Width="Auto" /> + </DataGrid.Columns> + </DataGrid> </StackPanel> </TabItem> </TabControl> @@ -287,7 +378,7 @@ <ToggleButton IsChecked="{Binding ShowLayerImageIssues}" - ToolTip.Tip="Highlight Issues on current layer.\nValid only if Issues are calculated." + ToolTip.Tip="Highlight Issues on current layer. Valid only if Issues are calculated." > <StackPanel Orientation="Horizontal"> <Image Source="/Assets/Icons/warning-16x16.png"/> @@ -349,8 +440,69 @@ Name="Layer.Image" /> - <StackPanel HorizontalAlignment="Left" Grid.Row="2" Orientation="Horizontal" Spacing="1"> - <TextBlock Text="{Binding ShowLayerRenderMs}ms"/> + <StackPanel Margin="0,5,0,0" Grid.Row="2" Orientation="Horizontal" Spacing="5"> + <StackPanel + ToolTip.Tip="Number of pixels to cure on this layer image and the percetange of them against total lcd pixels" + VerticalAlignment="Center" Orientation="Horizontal" Spacing="5"> + <Image Source="/Assets/Icons/pixel-16x16.png"/> + <TextBlock Text="{Binding LayerPixelCountStr, StringFormat=Pixels: \{0\}}"/> + </StackPanel> + + <Button + ToolTip.Tip="Object volume bounds for current layer, position and size. Click: go to region" + > + <StackPanel VerticalAlignment="Center" Orientation="Horizontal" Spacing="5"> + <Image Source="/Assets/Icons/expand-16x16.png"/> + <TextBlock Text="{Binding LayerBoundsStr, StringFormat=Bounds: \{0\}}"/> + </StackPanel> + </Button> + + <Button + ToolTip.Tip="Region of interest selection over layer. (NS): Not selected + Click: go to region | SHIFT + click: Clear ROI" + > + <StackPanel VerticalAlignment="Center" Orientation="Horizontal" Spacing="5"> + <Image Source="/Assets/Icons/object-group-16x16.png"/> + <TextBlock Text="{Binding LayerROIStr, StringFormat=ROI: \{0\}}"/> + </StackPanel> + </Button> + + </StackPanel> + + <StackPanel Margin="0,5,0,0" VerticalAlignment="Center" HorizontalAlignment="Right" Grid.Row="2" Orientation="Horizontal" Spacing="5"> + + <Button + IsEnabled="{Binding LayerPixelPicker.IsSet}" + ToolTip.Tip="Pixel picker: Use CONTROL and over a pixel to get his position and brightness. Click: Center at position" > + <StackPanel VerticalAlignment="Center" Orientation="Horizontal" Spacing="5"> + <Image Source="/Assets/Icons/map-marker-16x16.png"/> + <TextBlock Text="{Binding LayerPixelPicker}"/> + </StackPanel> + </Button> + + <Button + ToolTip.Tip="Layer image zoom level, use mouse scroll to zoom in/out into image. Ctrl + 0 OR double right click to scale to fit" + > + <StackPanel VerticalAlignment="Center" Orientation="Horizontal" Spacing="5"> + <Image Source="/Assets/Icons/search-16x16.png"/> + <TextBlock Text="{Binding LayerZoomStr, StringFormat=Zoom: [ \{0\} ]}"/> + </StackPanel> + </Button> + + <Button + ToolTip.Tip="Layer Resolution. Click: Zoom to fit" + Command="{Binding ZoomToFit}" + > + <StackPanel VerticalAlignment="Center" Orientation="Horizontal" Spacing="5"> + <Image Source="/Assets/Icons/expand-16x16.png"/> + <TextBlock Text="{Binding LayerResolutionStr}"/> + </StackPanel> + </Button> + + + <TextBlock + ToolTip.Tip="Layer preview computation time." + VerticalAlignment="Center" Text="{Binding ShowLayerRenderMs, StringFormat=\{0\}ms}"/> </StackPanel> </Grid> diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index b61abb5..39c9101 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -6,22 +6,27 @@ * of this license document, but changing it is not allowed. */ using System; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.IO; +using System.Linq; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using Avalonia; using Avalonia.Collections; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Markup.Xaml; +using Avalonia.Threading; using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; -using Emgu.CV.Util; using UVtools.Core; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; +using UVtools.Core.PixelEditor; using UVtools.WPF.Controls; using UVtools.WPF.Extensions; using UVtools.WPF.Structures; @@ -87,15 +92,28 @@ namespace UVtools.WPF #endregion #region Controls + + public ProgressWindow ProgressWindow = new ProgressWindow(); + + //private ProgressWindow _progressWindow; public AdvancedImageBox LayerImageBox; public SliderEx LayerSlider; public Panel LayerNavigationTooltipPanel; public Border LayerNavigationTooltipBorder; + + #region DataSets + public ObservableCollection<LogItem> Logs { get; } = new ObservableCollection<LogItem>(); + public ObservableCollection<LayerIssue> Issues { get; } = new ObservableCollection<LayerIssue>(); + public ObservableCollection<PixelOperation> Drawings { get; } = new ObservableCollection<PixelOperation>(); + #endregion + #endregion #region Members private uint _actualLayer; private bool _isGUIEnabled = true; + private bool _canSave; + private bool _isVerbose; private bool _showLayerImageRotated; private bool _showLayerImageDifference; private bool _showLayerImageIssues = true; @@ -113,7 +131,22 @@ namespace UVtools.WPF public bool IsGUIEnabled { get => _isGUIEnabled; - set => SetProperty(ref _isGUIEnabled, value); + set + { + if (!SetProperty(ref _isGUIEnabled, value)) return; + if (!_isGUIEnabled) return; + if (ProgressWindow.IsActive) + { + Dispatcher.UIThread.InvokeAsync(() => + { + ProgressWindow.Close(); + ProgressWindow.Dispose(); + ProgressWindow = new ProgressWindow(); + }); + } + + + } } public bool IsFileLoaded @@ -122,6 +155,18 @@ namespace UVtools.WPF set => OnPropertyChanged(); } + public bool CanSave + { + get => _canSave; + set => SetProperty(ref _canSave, value); + } + + public bool IsVerbose + { + get => _isVerbose; + set => SetProperty(ref _isVerbose, value); + } + public bool ShowLayerImageRotated { get => _showLayerImageRotated; @@ -233,6 +278,21 @@ namespace UVtools.WPF public bool CanGoUp => _actualLayer < SliderMaximumValue; public bool CanGoDown => _actualLayer > 0; + public string LayerPixelCountStr + { + get + { + if (!LayerCache.IsCached) return "0"; + var pixelPercent = + Math.Round( + LayerCache.Layer.NonZeroPixelCount * 100.0 / (SlicerFile.ResolutionX * SlicerFile.ResolutionY), 2); + return $"{LayerCache.Layer.NonZeroPixelCount} ({pixelPercent}%)"; + } + } + + public string LayerBoundsStr => LayerCache.Layer is null ? "NS" : LayerCache.Layer.BoundingRectangle.ToString(); + public string LayerROIStr => LayerCache.Layer is null ? "NS" : "TODO"; + public long ShowLayerRenderMs { get => _showLayerRenderMs; @@ -240,7 +300,13 @@ namespace UVtools.WPF } #endregion - private uint ActualLayer + public PixelPicker LayerPixelPicker { get; } = new PixelPicker(); + + public string LayerZoomStr => $"{LayerImageBox.Zoom / 100}x"+ + (AppSettings.LockedZoomLevel == LayerImageBox.Zoom ? "🔒" : string.Empty); + public string LayerResolutionStr => SlicerFile?.Resolution.ToString() ?? "Unloaded"; + + public uint ActualLayer { get => _actualLayer; set @@ -250,6 +316,8 @@ namespace UVtools.WPF OnPropertyChanged(nameof(CanGoUp)); OnPropertyChanged(nameof(ActualLayerTooltip)); OnPropertyChanged(nameof(LayerNavigationTooltipMargin)); + OnPropertyChanged(nameof(LayerPixelCountStr)); + OnPropertyChanged(nameof(LayerBoundsStr)); ShowLayer(); } } @@ -259,7 +327,7 @@ namespace UVtools.WPF get { double top = 0; - if (LayerSlider != null) + if (LayerSlider?.Track != null) { double trackerPos = LayerSlider.Track.Thumb.Bounds.Height / 2 + LayerSlider.Track.Thumb.Bounds.Top; double halfTooltipHeight = LayerNavigationTooltipBorder.Bounds.Height / 2; @@ -279,19 +347,12 @@ namespace UVtools.WPF public LayerCache LayerCache = new LayerCache(); - #endregion #region Constructors public MainWindow() { - _showLayerImageDifference = Settings.LayerPreview.ShowLayerDifference; - _showLayerOutlinePrintVolumeBoundary = Settings.LayerPreview.VolumeBoundsOutline; - _showLayerOutlineLayerBoundary = Settings.LayerPreview.LayerBoundsOutline; - _showLayerOutlineHollowAreas = Settings.LayerPreview.HollowOutline; - - DataContext = this; InitializeComponent(); #if DEBUG //this.AttachDevTools(); @@ -301,6 +362,12 @@ namespace UVtools.WPF LayerSlider = this.FindControl<SliderEx>("Layer.Navigation.Slider"); LayerNavigationTooltipPanel = this.FindControl<Panel>("Layer.Navigation.Tooltip.Panel"); LayerNavigationTooltipBorder = this.FindControl<Border>("Layer.Navigation.Tooltip.Border"); + + _showLayerImageDifference = Settings.LayerPreview.ShowLayerDifference; + _showLayerOutlinePrintVolumeBoundary = Settings.LayerPreview.VolumeBoundsOutline; + _showLayerOutlineLayerBoundary = Settings.LayerPreview.LayerBoundsOutline; + _showLayerOutlineHollowAreas = Settings.LayerPreview.HollowOutline; + /*LayerSlider.PropertyChanged += (sender, args) => { Debug.WriteLine(args.Property.Name); @@ -311,8 +378,22 @@ namespace UVtools.WPF } };*/ PropertyChanged += OnPropertyChanged; + DataContext = this; + AddHandler(DragDrop.DropEvent, (sender, e) => + { + ProcessFiles(e.Data.GetFileNames().ToArray()); + }); + + AddLog($"{About.Software} start"); } + public override void Show() + { + base.Show(); + ProcessFiles(Program.Args); + } + + private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { Debug.WriteLine(e.PropertyName); @@ -332,14 +413,28 @@ namespace UVtools.WPF #region Events - public async void MenuFileOpenClicked() + public void MenuFileOpenClicked() => OpenFile(); + public void MenuFileOpenNewWindowClicked() => OpenFile(true); + + public async void OpenFile(bool newWindow = false) { var dialog = new OpenFileDialog { - AllowMultiple = false, + AllowMultiple = true, + }; var files = await dialog.ShowAsync(this); - ProcessFiles(files); + ProcessFiles(files, newWindow); + } + + public void CloseFile() + { + if (SlicerFile is null) return; + SlicerFile?.Dispose(); + App.SlicerFile = null; + LayerCache.Clear(); + LayerImageBox.Image = null; + ResetDataContext(); } public async void MenuFileSettingsClicked() @@ -347,6 +442,7 @@ namespace UVtools.WPF SettingsWindow settingsWindow = new SettingsWindow(); await settingsWindow.ShowDialog(this); } + #endregion #region Methods @@ -361,58 +457,63 @@ namespace UVtools.WPF ProcessFile(files[i]); continue; } + + App.NewInstance(files[i]); + } } - void ReloadFile(uint actualLayer = 0) + void ReloadFile() => ReloadFile(_actualLayer); + + void ReloadFile(uint actualLayer) { if (App.SlicerFile is null) return; - ProcessFile(App.SlicerFile.FileFullPath, actualLayer); + ProcessFile(SlicerFile.FileFullPath, _actualLayer); } - void ProcessFile(string fileName, uint actualLayer = 0) + async void ProcessFile(string fileName, uint actualLayer = 0) { + if (!File.Exists(fileName)) return; + CloseFile(); var fileNameOnly = Path.GetFileName(fileName); App.SlicerFile = FileFormat.FindByExtension(fileName, true, true); - if (App.SlicerFile is null) return; + if (SlicerFile is null) return; + ProgressWindow.SetTitle($"Opening: {fileNameOnly}"); IsGUIEnabled = false; - var task = Task.Factory.StartNew(() => { try { - App.SlicerFile.Decode(fileName); + SlicerFile.Decode(fileName, ProgressWindow.RestartProgress()); } catch (OperationCanceledException) { - App.SlicerFile.Clear(); + SlicerFile.Clear(); } finally { - /* Invoke((MethodInvoker)delegate - { - // Running on the UI thread - EnableGUI(true); - });*/ + IsGUIEnabled = true; } }); //ProgressWindow progressWindow = new ProgressWindow(); //progressWindow.ShowDialog(this); - task.Wait(); + await ProgressWindow.ShowDialog(this); - var mat = App.SlicerFile[0].LayerMat; + /*var mat = App.SlicerFile[0].LayerMat; var matRgb = App.SlicerFile[0].BrgMat; Debug.WriteLine("4K grayscale - BGRA convertion:"); var bitmap = mat.ToBitmap(); Debug.WriteLine("4K BGR - BGRA convertion:"); var bitmapRgb = matRgb.ToBitmap(); - LayerImageBox.Image = bitmapRgb; + LayerImageBox.Image = bitmapRgb;*/ + + _actualLayer = actualLayer; + ShowLayer(); - IsGUIEnabled = true; ResetDataContext(); } @@ -444,6 +545,11 @@ namespace UVtools.WPF ActualLayer = SliderMaximumValue; } + public void ZoomToFit() + { + LayerImageBox.ZoomToFit(); + } + /// <summary> /// Shows a layer number /// </summary> @@ -453,17 +559,11 @@ namespace UVtools.WPF Debug.WriteLine($"Showing layer: {_actualLayer}"); - - //AddLogVerbose($"Show Layer: {layerNum}"); - - Stopwatch watch = Stopwatch.StartNew(); - var layer = SlicerFile[_actualLayer]; + LayerCache.Layer = SlicerFile[_actualLayer]; try { - LayerCache.Image = layer.LayerMat; - //var imageSpan = LayerCache.Image.GetPixelSpan<byte>(); //var imageBgrSpan = LayerCache.ImageBgr.GetPixelSpan<byte>(); @@ -805,6 +905,7 @@ namespace UVtools.WPF watch.Stop(); ShowLayerRenderMs = watch.ElapsedMilliseconds; + AddLogVerbose($"Show Layer: {_actualLayer}", watch.Elapsed.TotalSeconds); } catch (Exception e) { @@ -812,6 +913,25 @@ namespace UVtools.WPF } } + + public void AddLog(LogItem log) + { + log.Index = Logs.Count; + Logs.Insert(0, log); + //lbLogOperations.Text = $"Operations: {count}"; + } + + public void AddLog(string description, double elapsedTime = 0) => + AddLog(new LogItem(Logs.Count, description, elapsedTime)); + + + public void AddLogVerbose(string description, double elapsedTime = 0) + { + if (!_isVerbose) return; + AddLog(description, elapsedTime); + Debug.WriteLine(description); + } + #endregion } } diff --git a/UVtools.WPF/Program.cs b/UVtools.WPF/Program.cs index ea81577..b3f91bb 100644 --- a/UVtools.WPF/Program.cs +++ b/UVtools.WPF/Program.cs @@ -1,14 +1,21 @@ -using Avalonia; +using System; +using Avalonia; namespace UVtools.WPF { - class Program + public static class Program { + public static string[] Args; // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + [STAThread] + public static void Main(string[] args) + { + Args = args; + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() diff --git a/UVtools.WPF/Structures/LogItem.cs b/UVtools.WPF/Structures/LogItem.cs new file mode 100644 index 0000000..4dc6872 --- /dev/null +++ b/UVtools.WPF/Structures/LogItem.cs @@ -0,0 +1,61 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using UVtools.Core.Objects; + +namespace UVtools.WPF.Structures +{ + public sealed class LogItem : BindableBase + { + private int _index; + private string _startTime; + private double _elapsedTime; + private string _description; + + public int Index + { + get => _index; + set => SetProperty(ref _index, value); + } + + public string StartTime + { + get => _startTime; + set => SetProperty(ref _startTime, value); + } + + public double ElapsedTime + { + get => _elapsedTime; + set => SetProperty(ref _elapsedTime, Math.Round(value, 2)); + } + + public string Description + { + get => _description; + set => SetProperty(ref _description, value); + } + + public LogItem(int index, string description, double elapsedTime = 0) + { + _index = index; + _description = description; + ElapsedTime = elapsedTime; + _startTime = DateTime.Now.ToString("HH:mm:ss"); + } + + public LogItem(string description = null, uint elapsedTime = 0) : this(0, description, elapsedTime) + { } + + public override string ToString() + { + return Description; + } + } +} diff --git a/UVtools.WPF/Structures/PixelPicker.cs b/UVtools.WPF/Structures/PixelPicker.cs new file mode 100644 index 0000000..f011954 --- /dev/null +++ b/UVtools.WPF/Structures/PixelPicker.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; +using Avalonia; +using ReactiveUI; + +namespace UVtools.WPF.Structures +{ + public class PixelPicker : ReactiveObject + { + private bool _isSet; + private Point _location = new Point(0,0); + private byte _brightness; + + public bool IsSet + { + get => _isSet; + private set => this.RaiseAndSetIfChanged(ref _isSet, value); + } + + public Point Location + { + get => _location; + private set => this.RaiseAndSetIfChanged(ref _location, value); + } + + public byte Brightness + { + get => _brightness; + private set => this.RaiseAndSetIfChanged(ref _brightness, value); + } + + public void Set(Point location, byte brightness) + { + Location = location; + Brightness = brightness; + IsSet = true; + } + + public override string ToString() + { + return $"{{X={_location.X}, Y={_location.Y}, B={_brightness}}}"; + } + } +} diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj index 6d7eac4..8e47665 100644 --- a/UVtools.WPF/UVtools.WPF.csproj +++ b/UVtools.WPF/UVtools.WPF.csproj @@ -22,6 +22,7 @@ <ItemGroup> <PackageReference Include="Avalonia" Version="0.10.0-preview5" /> <PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" /> + <PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.0-preview5" /> <PackageReference Include="Avalonia.Desktop" Version="0.10.0-preview5" /> <PackageReference Include="Avalonia.ThemeManager" Version="0.10.0-preview3" /> <PackageReference Include="Dock.Avalonia" Version="0.10.0-preview5" /> diff --git a/UVtools.WPF/Windows/ProgressWindow.axaml b/UVtools.WPF/Windows/ProgressWindow.axaml index fbcc4a2..435bd20 100644 --- a/UVtools.WPF/Windows/ProgressWindow.axaml +++ b/UVtools.WPF/Windows/ProgressWindow.axaml @@ -14,12 +14,21 @@ Icon="/Assets/Icons/UVtools.ico" > <StackPanel Orientation="Vertical"> - <TextBlock Margin="10" Text="Title"/> - <TextBlock Margin="10,0,10,10" Text="Elapsed Time: "/> - <TextBlock Margin="10,0,10,10" Text="Progress" HorizontalAlignment="Center"/> + <TextBlock Margin="10" Text="{Binding Title}"/> + <TextBlock Margin="10,0,10,10" Text="{Binding ElapsedTimeStr, StringFormat=Elapsed Time: \{0\}}"/> + <TextBlock Margin="10,0,10,10" Text="{Binding Description}" HorizontalAlignment="Center"/> <Grid RowDefinitions="30" ColumnDefinitions="*,Auto"> - <ProgressBar Grid.Column="0" ShowProgressText="True"/> - <Button Grid.Column="1" Padding="30,0" IsCancel="True">Cancel</Button> + <ProgressBar + Minimum="0" + Maximum="100" + IsIndeterminate="{Binding IsIndeterminate}" + Value="{Binding ProgressPercent}" Grid.Column="0" ShowProgressText="True"/> + <Button + IsEnabled="{Binding CanCancel}" + Command="{Binding TokenSource.Cancel}" + Grid.Column="1" + Padding="30,0" + IsCancel="True">Cancel</Button> </Grid> </StackPanel> </Window> diff --git a/UVtools.WPF/Windows/ProgressWindow.axaml.cs b/UVtools.WPF/Windows/ProgressWindow.axaml.cs index b9744e6..953340a 100644 --- a/UVtools.WPF/Windows/ProgressWindow.axaml.cs +++ b/UVtools.WPF/Windows/ProgressWindow.axaml.cs @@ -5,22 +5,79 @@ * Everyone is permitted to copy and distribute verbatim copies * of this license document, but changing it is not allowed. */ + +using System; +using System.Diagnostics; +using System.Timers; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Threading; +using UVtools.Core.Operations; +using UVtools.WPF.Structures; namespace UVtools.WPF.Windows { - public class ProgressWindow : Window + public class ProgressWindow : Window, IDisposable { + public Stopwatch StopWatch { get; } = new Stopwatch(); + public OperationProgress Progress { get; } = new OperationProgress(); + + private LogItem _logItem = new LogItem(); + + private Timer _timer; + + public bool CanCancel => Progress?.CanCancel ?? false; + public ProgressWindow() { InitializeComponent(); + DataContext = Progress; + _timer = new Timer(100) {AutoReset = true}; + _timer.Elapsed += (sender, args) => + { + Progress.TriggerRefresh(); + }; } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - + } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + _timer.Stop(); + Progress.StopWatch.Stop(); + _logItem.ElapsedTime = Math.Round(Progress.StopWatch.Elapsed.TotalSeconds, 2); + App.MainWindow.AddLog(_logItem); + } + + public void SetTitle(string title) + { + Progress.Title = title; + _logItem.Description = title; + } + + public OperationProgress RestartProgress(bool canCancel = true) + { + Progress.CanCancel = canCancel; + Progress.StopWatch.Restart(); + + if (!_timer.Enabled) + { + _timer.Enabled = true; + _timer.Start(); + } + + Dispatcher.UIThread.InvokeAsync(() => DataContext = Progress); + return Progress; + } + + public void Dispose() + { + _timer.Close(); + _timer.Dispose(); } } } |