/* * GNU AFFERO GENERAL PUBLIC LICENSE * Version 3, 19 November 2007 * Copyright (C) 2007 Free Software Foundation, Inc. * Everyone is permitted to copy and distribute verbatim copies * of this license document, but changing it is not allowed. */ using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Threading; using MessageBox.Avalonia.Enums; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using UVtools.AvaloniaControls; using UVtools.Core; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; using UVtools.Core.Managers; using UVtools.Core.Network; using UVtools.Core.Objects; using UVtools.Core.Operations; using UVtools.Core.SystemOS; using UVtools.WPF.Controls; using UVtools.WPF.Controls.Calibrators; using UVtools.WPF.Controls.Tools; using UVtools.WPF.Extensions; using UVtools.WPF.Structures; using UVtools.WPF.Windows; using Helpers = UVtools.WPF.Controls.Helpers; using Path = System.IO.Path; using Point = Avalonia.Point; namespace UVtools.WPF; public partial class MainWindow : WindowEx { #region Redirects public AppVersionChecker VersionChecker => App.VersionChecker; public static ClipboardManager Clipboard => ClipboardManager.Instance; #endregion #region Controls //public ProgressWindow ProgressWindow = new (); public static MenuItem[] MenuTools { get; } = { new() { Tag = new OperationEditParameters()}, new() { Tag = new OperationRepairLayers()}, new() { Tag = new OperationMove()}, new() { Tag = new OperationResize()}, new() { Tag = new OperationFlip()}, new() { Tag = new OperationRotate()}, new() { Tag = new OperationSolidify()}, new() { Tag = new OperationMorph()}, new() { Tag = new OperationRaftRelief()}, new() { Tag = new OperationRedrawModel()}, /*new() { Tag = new OperationThreshold()},*/ new() { Tag = new OperationLayerArithmetic()}, new() { Tag = new OperationPixelArithmetic()}, new() { Tag = new OperationMask()}, /*new() { Tag = new OperationPixelDimming()},*/ new() { Tag = new OperationLightBleedCompensation()}, new() { Tag = new OperationInfill()}, new() { Tag = new OperationBlur()}, new() { Tag = new OperationPattern()}, new() { Tag = new OperationFadeExposureTime()}, new() { Tag = new OperationDoubleExposure()}, new() { Tag = new OperationDynamicLifts()}, new() { Tag = new OperationDynamicLayerHeight()}, new() { Tag = new OperationLayerReHeight()}, new() { Tag = new OperationRaiseOnPrintFinish()}, new() { Tag = new OperationChangeResolution()}, new() { Tag = new OperationTimelapse()}, new() { Tag = new OperationLithophane()}, new() { Tag = new OperationPCBExposure()}, new() { Tag = new OperationScripting()}, new() { Tag = new OperationCalculator()}, }; public static MenuItem[] MenuCalibration { get; } = { new() { Tag = new OperationCalibrateExposureFinder()}, new() { Tag = new OperationCalibrateElephantFoot()}, new() { Tag = new OperationCalibrateXYZAccuracy()}, new() { Tag = new OperationCalibrateLiftHeight()}, new() { Tag = new OperationCalibrateBloomingEffect()}, new() { Tag = new OperationCalibrateTolerance()}, new() { Tag = new OperationCalibrateGrayscale()}, new() { Tag = new OperationCalibrateStressTower()}, new() { Tag = new OperationCalibrateExternalTests()}, }; public static MenuItem[] LayerActionsMenu { get; } = { new() { Tag = new OperationLayerImport()}, new() { Tag = new OperationLayerClone()}, new() { Tag = new OperationLayerRemove()}, new() { Tag = new OperationLayerExportImage()}, new() { Tag = new OperationLayerExportGif()}, new() { Tag = new OperationLayerExportHtml()}, new() { Tag = new OperationLayerExportSkeleton()}, new() { Tag = new OperationLayerExportHeatMap()}, new() { Tag = new OperationLayerExportMesh()}, }; #endregion #region Members public Stopwatch LastStopWatch = new(); private bool _isGUIEnabled = true; private uint _savesCount; private bool _canSave; private readonly MenuItem _menuFileSendTo; private IEnumerable _menuFileOpenRecentItems; private IEnumerable _menuFileSendToItems; private IEnumerable _menuFileConvertItems; private PointerEventArgs _globalPointerEventArgs; private PointerPoint _globalPointerPoint; private KeyModifiers _globalModifiers = KeyModifiers.None; private TabItem _selectedTabItem; private TabItem _lastSelectedTabItem; #endregion #region GUI Models public bool IsGUIEnabled { get => _isGUIEnabled; set { if (!RaiseAndSetIfChanged(ref _isGUIEnabled, value)) return; if (!_isGUIEnabled) { DragDrop.SetAllowDrop(this, false); //ProgressWindow = new ProgressWindow(); return; } DragDrop.SetAllowDrop(this, true); LastStopWatch = Progress.StopWatch; ProgressFinish(); //ProgressWindow.Close(DialogResults.OK); //ProgressWindow.Dispose(); /*if (Dispatcher.UIThread.CheckAccess()) { ProgressWindow.Close(); ProgressWindow.Dispose(); } else { Dispatcher.UIThread.InvokeAsync(() => { ProgressWindow.Close(); ProgressWindow.Dispose(); }); }*/ } } public bool IsFileLoaded { get => SlicerFile is not null; set => RaisePropertyChanged(); } public TabItem TabInformation { get; } public TabItem TabGCode { get; } public TabItem TabIssues { get; } public TabItem TabPixelEditor { get; } public TabItem TabLog { get; } public TabItem SelectedTabItem { get => _selectedTabItem; set { var lastTab = _selectedTabItem; if (!RaiseAndSetIfChanged(ref _selectedTabItem, value)) return; LastSelectedTabItem = lastTab; if (_firstTimeOnIssues) { _firstTimeOnIssues = false; if (ReferenceEquals(_selectedTabItem, TabIssues) && Settings.Issues.ComputeIssuesOnClickTab) { Dispatcher.UIThread.InvokeAsync(async () => await OnClickDetectIssues()); } } } } public TabItem LastSelectedTabItem { get => _lastSelectedTabItem; set => RaiseAndSetIfChanged(ref _lastSelectedTabItem, value); } #endregion public uint SavesCount { get => _savesCount; set => RaiseAndSetIfChanged(ref _savesCount, value); } public bool CanSave { get => IsFileLoaded && _canSave; set => RaiseAndSetIfChanged(ref _canSave, value); } public IEnumerable MenuFileOpenRecentItems { get => _menuFileOpenRecentItems; set => RaiseAndSetIfChanged(ref _menuFileOpenRecentItems, value); } public IEnumerable MenuFileSendToItems { get => _menuFileSendToItems; set => RaiseAndSetIfChanged(ref _menuFileSendToItems, value); } public IEnumerable MenuFileConvertItems { get => _menuFileConvertItems; set => RaiseAndSetIfChanged(ref _menuFileConvertItems, value); } #region Constructors 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); InitProgress(); InitInformation(); InitIssues(); InitPixelEditor(); InitClipboardLayers(); InitLayerPreview(); InitSuggestions(); RefreshRecentFiles(true); TabInformation = this.FindControl("TabInformation"); TabGCode = this.FindControl("TabGCode"); TabIssues = this.FindControl("TabIssues"); TabPixelEditor = this.FindControl("TabPixelEditor"); TabLog = this.FindControl("TabLog"); foreach (var menuItem in new[] { MenuTools, MenuCalibration, LayerActionsMenu }) { foreach (var menuTool in menuItem) { if (menuTool.Tag is not Operation operation) continue; if (!string.IsNullOrWhiteSpace(operation.IconClass)) menuTool.Icon = new Projektanker.Icons.Avalonia.Icon{Value = operation.IconClass}; menuTool.Header = operation.Title; menuTool.Click += async (sender, args) => await ShowRunOperation(operation.GetType()); } } /*LayerSlider.PropertyChanged += (sender, args) => { Debug.WriteLine(args.Property.Name); if (args.Property.Name == nameof(LayerSlider.Value)) { LayerNavigationTooltipPanel.Margin = LayerNavigationTooltipMargin; return; } };*/ //PropertyChanged += OnPropertyChanged; UpdateTitle(); DataContext = this; _menuFileSendTo = this.FindControl("MainMenu.File.SendTo"); this.FindControl("MainMenu.File").SubmenuOpened += (sender, e) => { if (!IsFileLoaded) return; var menuItems = new List(); var drives = DriveInfo.GetDrives(); if (drives.Length > 0) { foreach (var drive in drives) { if (drive.DriveType != DriveType.Removable || !drive.IsReady) continue; // Not our target, skip if (SlicerFile.FileFullPath.StartsWith(drive.Name)) continue; // File already on this device, skip var header = drive.Name; if (!string.IsNullOrWhiteSpace(drive.VolumeLabel)) { header += $" {drive.VolumeLabel}"; } header += $" ({SizeExtensions.SizeSuffix(drive.AvailableFreeSpace)}) [{drive.DriveFormat}]"; var menuItem = new MenuItem { Header = header, Tag = drive, Icon = new Projektanker.Icons.Avalonia.Icon{ Value = "fa-brands fa-usb" } }; menuItem.Click += FileSendToItemClick; menuItems.Add(menuItem); } } if (Settings.General.SendToCustomLocations is not null && Settings.General.SendToCustomLocations.Count > 0) { foreach (var location in Settings.General.SendToCustomLocations) { if(!location.IsEnabled) continue; if (!string.IsNullOrWhiteSpace(location.CompatibleExtensions)) { var extensions = location.CompatibleExtensions.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var found = false; foreach (var ext in extensions) { found = SlicerFile.FileEndsWith($".{ext}"); if (found) break; } if(!found) continue; } var menuItem = new MenuItem { Header = location.ToString(), Tag = location, Icon = new Projektanker.Icons.Avalonia.Icon { Value = "fa-solid fa-folder" } }; menuItem.Click += FileSendToItemClick; menuItems.Add(menuItem); } } if (Settings.Network.RemotePrinters is not null && Settings.Network.RemotePrinters.Count > 0) { foreach (var remotePrinter in Settings.Network.RemotePrinters) { if(!remotePrinter.IsEnabled || !remotePrinter.IsValid) continue; if (!string.IsNullOrWhiteSpace(remotePrinter.CompatibleExtensions)) { var extensions = remotePrinter.CompatibleExtensions.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var found = false; foreach (var ext in extensions) { found = SlicerFile.FileEndsWith($".{ext}"); if (found) break; } if (!found) continue; } var menuItem = new MenuItem { Header = remotePrinter.ToString(), Tag = remotePrinter, Icon = new Projektanker.Icons.Avalonia.Icon { Value = "fa-solid fa-network-wired" } }; menuItem.Click += FileSendToItemClick; menuItems.Add(menuItem); } } if (Settings.General.SendToProcess is not null && Settings.General.SendToProcess.Count > 0) { foreach (var application in Settings.General.SendToProcess) { if (!application.IsEnabled ) continue; if (!string.IsNullOrWhiteSpace(application.CompatibleExtensions)) { var extensions = application.CompatibleExtensions.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var found = false; foreach (var ext in extensions) { found = SlicerFile.FileEndsWith($".{ext}"); if (found) break; } if (!found) continue; } var menuItem = new MenuItem { Header = application.Name, Tag = application, Icon = new Projektanker.Icons.Avalonia.Icon { Value = "fa-solid fa-cog" } }; menuItem.Click += FileSendToItemClick; menuItems.Add(menuItem); } } MenuFileSendToItems = menuItems; _menuFileSendTo.IsVisible = _menuFileSendTo.IsEnabled = menuItems.Count > 0; }; } private async void FileSendToItemClick(object? sender, RoutedEventArgs e) { if (sender is not MenuItem menuItem) return; string path; if (menuItem.Tag is DriveInfo drive) { if (!drive.IsReady) { await this.MessageBoxError($"The device {drive.Name} is not ready/available at this time.", "Unable to copy the file"); return; } path = drive.Name; } else if (menuItem.Tag is MappedDevice device) { path = device.Path; } else if (menuItem.Tag is RemotePrinter preRemotePrinter) { path = preRemotePrinter.HostUrl; } else if (menuItem.Tag is MappedProcess process) { path = process.Name; } else { return; } if (CanSave) { switch (await this.MessageBoxQuestion("There are unsaved changes. Do you want to save the current file before copy it over?\n\n" + "Yes: Save the current file and copy it over.\n" + "No: Copy the file without current modifications.\n" + "Cancel: Abort the operation.", "Send to - Unsaved changes", ButtonEnum.YesNoCancel)) { case ButtonResult.Yes: await SaveFile(true); break; case ButtonResult.No: break; default: return; } } ShowProgressWindow($"Sending: {SlicerFile.Filename} to {path}"); Progress.ItemName = "Sending"; if (menuItem.Tag is RemotePrinter remotePrinter) { var startPrint = (_globalModifiers & KeyModifiers.Shift) != 0 && remotePrinter.RequestPrintFile is not null && remotePrinter.RequestPrintFile.IsValid; if (startPrint) { if (await this.MessageBoxQuestion( "If supported, you are about to send the file and start the print with it.\n" + "Keep in mind there is no guarantee that the file will start to print.\n" + "Are you sure you want to continue?\n\n" + "Yes: Send file and print it.\n" + "No: Cancel file sending and print.", "Send and print the file?") != ButtonResult.Yes) return; } HttpResponseMessage response = null; try { await using var stream = File.OpenRead(SlicerFile.FileFullPath); using var httpContent = new StreamContent(stream); Progress.ItemCount = (uint)(stream.Length / 1000000); bool isCopying = true; try { var task = new Task(() => { while (isCopying) { Progress.ProcessedItems = (uint)(stream.Position / 1000000); Thread.Sleep(200); } }); } catch (Exception) { // ignored } response = await remotePrinter.RequestUploadFile.SendRequest(remotePrinter, Progress, SlicerFile.Filename, httpContent); isCopying = false; if (!response.IsSuccessStatusCode) { await this.MessageBoxError(response.ToString(), "Send to printer"); } } catch (OperationCanceledException) { } catch (Exception ex) { await this.MessageBoxError(ex.Message, "Send to printer"); } if ( response is not null && response.IsSuccessStatusCode && startPrint) { response.Dispose(); Progress.Title = "Waiting 2 seconds..."; await Task.Delay(2000); try { response = await remotePrinter.RequestPrintFile.SendRequest(remotePrinter, Progress, SlicerFile.Filename); if (!response.IsSuccessStatusCode) { await this.MessageBoxError(response.ToString(), "Unable to send the print command"); } /*else { await this.MessageBoxInfo(response.ToString(), "Print send command report"); }*/ } catch (OperationCanceledException) { } catch (Exception ex) { await this.MessageBoxError(ex.Message, "Unable to send the print command"); } } } else if (menuItem.Tag is MappedProcess process) { Progress.ItemName = "Waiting for completion"; try { await process.StartProcess(SlicerFile, Progress.Token); } catch (OperationCanceledException){} catch (Exception ex) { await this.MessageBoxError(ex.Message, $"Unable to start the process {process.Name}"); } } else { /*var copyResult = await Task.Factory.StartNew(() => { try { var fileDest = Path.Combine(path, SlicerFile.Filename); //File.Copy(SlicerFile.FileFullPath, $"{Path.Combine(path, SlicerFile.Filename)}", true); var buffer = new byte[1024 * 1024]; // 1MB buffer using var source = File.OpenRead(SlicerFile.FileFullPath); using var dest = new FileStream(fileDest, FileMode.Create, FileAccess.Write); //long totalBytes = 0; //int currentBlockSize; Progress.Reset("Megabyte(s)", (uint)(source.Length / 1000000)); var copyProgress = new Progress(copiedBytes => Progress.ProcessedItems = (uint)(copiedBytes / 1000000)); source.CopyToAsync(dest, 0, copyProgress, Progress.Token).ConfigureAwait(false); /*while ((currentBlockSize = source.Read(buffer)) > 0) { totalBytes += currentBlockSize; dest.Write(buffer, 0, currentBlockSize); if (Progress.Token.IsCancellationRequested) { // Delete dest file here dest.Dispose(); File.Delete(fileDest); return false; } Progress.ProcessedItems = (uint)(totalBytes / 1000000); }*/ /* return true; } catch (OperationCanceledException) { } catch (Exception exception) { Dispatcher.UIThread.InvokeAsync(async () => await this.MessageBoxError(exception.ToString(), "Unable to copy the file")); } return false; });*/ bool copyResult = false; var fileDest = Path.Combine(path, SlicerFile.Filename); try { await using var source = File.OpenRead(SlicerFile.FileFullPath); await using var dest = new FileStream(fileDest, FileMode.Create, FileAccess.Write); Progress.Reset("Megabyte(s)", (uint)(source.Length / 1000000)); var copyProgress = new Progress(copiedBytes => Progress.ProcessedItems = (uint)(copiedBytes / 1000000)); await source.CopyToAsync(dest, copyProgress, Progress.Token); copyResult = true; } catch (OperationCanceledException) { try { if (File.Exists(fileDest)) File.Delete(fileDest); } catch (Exception ex) { Debug.WriteLine(ex); } } catch (Exception exception) { await this.MessageBoxError(exception.Message, "Unable to copy the file"); } if(copyResult && menuItem.Tag is DriveInfo removableDrive && OperatingSystem.IsWindows() && Settings.General.SendToPromptForRemovableDeviceEject) { if (await this.MessageBoxQuestion( $"File '{SlicerFile.Filename}' has copied successfully into {removableDrive.Name}\n" + $"Do you want to eject the {removableDrive.Name} drive now?", "Copied ok, eject the drive?") == ButtonResult.Yes) { Progress.ResetAll($"Ejecting {removableDrive.Name}"); var ejectResult = await Task.Factory.StartNew(() => { try { return Core.SystemOS.Windows.USB.USBEject(removableDrive.Name); } catch (OperationCanceledException) { } catch (Exception exception) { Dispatcher.UIThread.InvokeAsync(async () => await this.MessageBoxError(exception.Message, $"Unable to eject the drive {removableDrive.Name}")); } return false; }); if (!ejectResult) { await this.MessageBoxError($"Unable to eject the drive {removableDrive.Name}\n\n" + "Possible causes:\n" + "- Drive may be busy or locked\n" + "- Drive was already ejected\n" + "- No permission to eject the drive\n" + "- Another error while trying to eject the drive\n\n" + "Please try to eject the drive manually.", $"Unable to eject the drive {removableDrive.Name}"); } } } } IsGUIEnabled = true; } protected override void OnOpened(EventArgs e) { base.OnOpened(e); var clientSizeObs = this.GetObservable(ClientSizeProperty); clientSizeObs.Subscribe(size => { Settings.General._lastWindowBounds.Width = (int)size.Width; Settings.General._lastWindowBounds.Height = (int)size.Height; UpdateLayerTrackerHighlightIssues(); }); var windowStateObs = this.GetObservable(WindowStateProperty); windowStateObs.Subscribe(windowsState => UpdateLayerTrackerHighlightIssues()); PositionChanged += (sender, args) => { Settings.General._lastWindowBounds.X = Math.Max(0, Position.X); Settings.General._lastWindowBounds.Y = Math.Max(0, Position.Y); }; AddHandler(DragDrop.DropEvent, (sender, args) => { if (!_isGUIEnabled) return; ProcessFiles(args.Data.GetFileNames()?.ToArray()); }); AddLog($"{About.Software} start", Program.ProgramStartupTime.Elapsed.TotalSeconds); if (Settings.General.CheckForUpdatesOnStartup) { Task.Factory.StartNew(() => VersionChecker.Check()); } ProcessFiles(Program.Args); if (!IsFileLoaded && Settings.General.LoadLastRecentFileOnStartup) { RecentFiles.Load(); if (RecentFiles.Instance.Count > 0) { ProcessFile(Path.Combine(App.ApplicationPath, RecentFiles.Instance[0])); } } if (!IsFileLoaded && Settings.General.LoadDemoFileOnStartup) { ProcessFile(Path.Combine(App.ApplicationPath, About.DemoFile)); } DispatcherTimer.Run(() => { UpdateTitle(); return true; }, TimeSpan.FromSeconds(1)); Program.ProgramStartupTime.Stop(); /*if (About.IsBirthday) { this.MessageBoxInfo($"Age: {About.AgeStr}\n" + $"This message will only show today, see you in next year!\n" + $"Thank you for using {About.Software}.", $"Today it's the {About.Software} birthday!").ConfigureAwait(false); }*/ } protected override void OnClosed(EventArgs e) { base.OnClosed(e); if (!UserSettings.Instance.General.StartMaximized && (UserSettings.Instance.General.RestoreWindowLastPosition || UserSettings.Instance.General.RestoreWindowLastSize)) { UserSettings.Save(); } } private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { Debug.WriteLine(e.PropertyName); /*if (e.PropertyName == nameof(ActualLayer)) { LayerSlider.Value = ActualLayer; ShowLayer(); return; }*/ } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } #endregion #region Overrides protected override void OnPointerMoved(PointerEventArgs e) { base.OnPointerMoved(e); _globalPointerEventArgs = e; _globalModifiers = e.KeyModifiers; } protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); _globalPointerPoint = e.GetCurrentPoint(this); } protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); _globalPointerPoint = e.GetCurrentPoint(this); } protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); _globalModifiers = e.KeyModifiers; if (e.Handled || !IsFileLoaded || LayerImageBox.IsPanning || LayerImageBox.TrackerImage is not null || LayerImageBox.Cursor == StaticControls.CrossCursor || LayerImageBox.Cursor == StaticControls.HandCursor || LayerImageBox.SelectionMode == AdvancedImageBox.SelectionModes.Rectangle ) return; var imageBoxMousePosition = _globalPointerEventArgs?.GetPosition(LayerImageBox) ?? new Point(-1, -1); if (imageBoxMousePosition.X < 0 || imageBoxMousePosition.Y < 0) return; // Pixel Edit is active, Shift is down, and the cursor is over the image region. if (e.KeyModifiers == KeyModifiers.Shift) { if (IsPixelEditorActive) { LayerImageBox.AutoPan = false; LayerImageBox.Cursor = StaticControls.CrossCursor; TooltipOverlayText = "Pixel editing is on (SHIFT):\n" + "» Click over a pixel to draw\n" + "» Hold CTRL to clear pixels"; UpdatePixelEditorCursor(); } else { LayerImageBox.Cursor = StaticControls.CrossCursor; LayerImageBox.SelectionMode = AdvancedImageBox.SelectionModes.Rectangle; TooltipOverlayText = "ROI & Mask selection mode (SHIFT):\n" + "» Left-click drag to select a fixed ROI region\n" + "» Left-click + ALT drag to select specific objects ROI\n" + "» Right-click on a specific object to select it ROI\n" + "» Right-click + ALT on a specific object to select it as a Mask\n" + "» Right-click + ALT + CTRL on a specific object to select all it enclosing areas as a Mask\n" + "Press Esc to clear the ROI & Masks"; } IsTooltipOverlayVisible = Settings.LayerPreview.TooltipOverlay; e.Handled = true; return; } if (e.KeyModifiers == KeyModifiers.Control) { LayerImageBox.Cursor = StaticControls.HandCursor; LayerImageBox.AutoPan = false; TooltipOverlayText = "Issue selection mode:\n" + "» Click over an issue to select it"; IsTooltipOverlayVisible = Settings.LayerPreview.TooltipOverlay; e.Handled = true; return; } } protected override void OnKeyUp(KeyEventArgs e) { _globalModifiers = e.KeyModifiers; if ((e.Key is Key.LeftShift or Key.RightShift || (e.KeyModifiers & KeyModifiers.Shift) != 0) && (e.KeyModifiers & KeyModifiers.Control) != 0 && e.Key == Key.Z) { e.Handled = true; ClipboardUndoAndRerun(true); return; } if (e.Key is Key.LeftShift or Key.RightShift || (e.KeyModifiers & KeyModifiers.Shift) == 0 || (e.KeyModifiers & KeyModifiers.Control) == 0) { LayerImageBox.TrackerImage = null; LayerImageBox.Cursor = StaticControls.ArrowCursor; LayerImageBox.AutoPan = true; LayerImageBox.SelectionMode = AdvancedImageBox.SelectionModes.None; IsTooltipOverlayVisible = false; e.Handled = true; return; } base.OnKeyUp(e); } public void OpenContextMenu(string name) { var menu = this.FindControl($"{name}ContextMenu"); if (menu is null) return; var parent = this.FindControl