diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/GlobalSuppressions.cs b/KGySoft.Drawing.DebuggerVisualizers.Package/GlobalSuppressions.cs new file mode 100644 index 0000000..4a255e4 --- /dev/null +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'", Justification = "Decided individually")] diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/KGySoft.Drawing.DebuggerVisualizers.Package.csproj b/KGySoft.Drawing.DebuggerVisualizers.Package/KGySoft.Drawing.DebuggerVisualizers.Package.csproj index d52de91..9639991 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Package/KGySoft.Drawing.DebuggerVisualizers.Package.csproj +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/KGySoft.Drawing.DebuggerVisualizers.Package.csproj @@ -1,20 +1,14 @@  + 15.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) true - - true - - ..\KGySoft.snk - - - Debug AnyCPU 2.0 @@ -24,7 +18,7 @@ Properties KGySoft.Drawing.DebuggerVisualizers.Package KGySoft.Drawing.DebuggerVisualizers.Package - v4.6 + v4.5 true true true @@ -34,6 +28,8 @@ Program $(DevEnvDir)devenv.exe /rootsuffix Exp + latest + enable true @@ -53,6 +49,7 @@ 4 + True True @@ -67,6 +64,7 @@ + @@ -79,6 +77,7 @@ Designer + Designer @@ -101,11 +100,11 @@ False False - - ..\..\packages\KGySoft.CoreLibraries.5.4.0\lib\net45\KGySoft.CoreLibraries.dll + + ..\..\packages\KGySoft.CoreLibraries.5.6.1\lib\net45\KGySoft.CoreLibraries.dll - - ..\..\packages\KGySoft.Drawing.5.3.0\lib\net45\KGySoft.Drawing.dll + + ..\..\packages\KGySoft.Drawing.5.3.1\lib\net45\KGySoft.Drawing.dll ..\..\packages\VSSDK.GraphModel.11.0.4\lib\net45\Microsoft.VisualStudio.GraphModel.dll diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/Res.cs b/KGySoft.Drawing.DebuggerVisualizers.Package/Res.cs index 5725348..ed04656 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Package/Res.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/Res.cs @@ -21,6 +21,8 @@ #endregion +#nullable enable + namespace KGySoft.Drawing.DebuggerVisualizers.Package { #region Usings @@ -72,9 +74,9 @@ internal static class Res private static string Get(string id) => Resources.ResourceManager.GetString(id) ?? String.Format(CultureInfo.InvariantCulture, unavailableResource, id); - private static string Get(string format, params object[] args) => args == null ? format : SafeFormat(format, args); + private static string Get(string format, params object?[]? args) => args == null ? format : SafeFormat(format, args); - private static string SafeFormat(string format, object[] args) + private static string SafeFormat(string format, object?[] args) { try { @@ -83,10 +85,7 @@ private static string SafeFormat(string format, object[] args) { string nullRef = PublicResources.Null; for (; i < args.Length; i++) - { - if (args[i] == null) - args[i] = nullRef; - } + args[i] ??= nullRef; } return String.Format(LanguageSettings.FormattingLanguage, format, args); diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/_Attributes/PackageRegistrationAsyncAttribute.cs b/KGySoft.Drawing.DebuggerVisualizers.Package/_Attributes/PackageRegistrationAsyncAttribute.cs index a07d923..49a15b2 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Package/_Attributes/PackageRegistrationAsyncAttribute.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/_Attributes/PackageRegistrationAsyncAttribute.cs @@ -24,6 +24,8 @@ #endregion +#nullable enable + namespace KGySoft.Drawing.DebuggerVisualizers.Package { /// @@ -46,7 +48,7 @@ internal sealed class PackageRegistrationAsyncAttribute : RegistrationAttribute public override void Register(RegistrationContext context) { Type t = context.ComponentType; - Key packageKey = null; + Key? packageKey = null; try { packageKey = context.CreateKey(RegKeyName(context)); diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/_Attributes/ProvideAutoLoadAsyncAttribute.cs b/KGySoft.Drawing.DebuggerVisualizers.Package/_Attributes/ProvideAutoLoadAsyncAttribute.cs index ac675a5..a4cfb17 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Package/_Attributes/ProvideAutoLoadAsyncAttribute.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/_Attributes/ProvideAutoLoadAsyncAttribute.cs @@ -22,6 +22,8 @@ #endregion +#nullable enable + namespace KGySoft.Drawing.DebuggerVisualizers.Package { /// @@ -39,7 +41,7 @@ internal sealed class ProvideAutoLoadAsyncAttribute : RegistrationAttribute #region Properties - private string RegKeyName => $"AutoLoadPackages\\{new Guid(VSConstants.UICONTEXT.NoSolution_string):B}"; + private static string RegKeyName => $"AutoLoadPackages\\{new Guid(VSConstants.UICONTEXT.NoSolution_string):B}"; #endregion @@ -47,8 +49,8 @@ internal sealed class ProvideAutoLoadAsyncAttribute : RegistrationAttribute public override void Register(RegistrationContext context) { - using (Key childKey = context.CreateKey(RegKeyName)) - childKey.SetValue(context.ComponentType.GUID.ToString("B"), backgroundLoad); + using Key childKey = context.CreateKey(RegKeyName); + childKey.SetValue(context.ComponentType.GUID.ToString("B"), backgroundLoad); } public override void Unregister(RegistrationContext context) => context.RemoveValue(RegKeyName, context.ComponentType.GUID.ToString("B")); diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/DebuggerVisualizersPackage.cs b/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/DebuggerVisualizersPackage.cs index a6f49b3..12ebd56 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/DebuggerVisualizersPackage.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/DebuggerVisualizersPackage.cs @@ -31,6 +31,8 @@ #endregion +#nullable enable + namespace KGySoft.Drawing.DebuggerVisualizers.Package { #region Usings @@ -62,7 +64,7 @@ public sealed class DebuggerVisualizersPackage : Microsoft.VisualStudio.Shell.Pa /// public IVsTask Initialize(IAsyncServiceProvider pServiceProvider, IProfferAsyncService pProfferService, IAsyncProgressCallback pProgressCallback) { - return ThreadHelper.JoinableTaskFactory.RunAsync(async () => + return ThreadHelper.JoinableTaskFactory.RunAsync(async () => { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); var shellService = await GetServiceAsync(pServiceProvider, typeof(SVsShell)); @@ -83,8 +85,6 @@ protected override void Initialize() { base.Initialize(); -#pragma warning disable VSTHRD108, VSTHRD010 // Invoke single-threaded types on Main thread: Initialize is on Main thread (see also the assert and IAsyncLoadablePackageInitialize.Initialize) - // returning if async initialization is supported Debug.Assert(ThreadHelper.CheckAccess()); if (GetService(typeof(SAsyncServiceProvider)) is IAsyncServiceProvider) @@ -92,7 +92,6 @@ protected override void Initialize() var uiShellService = GetService(typeof(SVsShell)) as IVsShell; var menuCommandService = GetService(typeof(IMenuCommandService)) as IMenuCommandService; -#pragma warning restore VSTHRD010, VSTHRD010 // Invoke single-threaded types on Main thread DoInitialize(uiShellService, menuCommandService); } @@ -102,16 +101,17 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); if (!disposing) return; - DestroyCommands(); + ExecuteImagingToolsCommand.DestroyCommand(); + ManageDebuggerVisualizerInstallationsCommand.DestroyCommand(); } #endregion #region Private Methods - private async Task GetServiceAsync(IAsyncServiceProvider asyncServiceProvider, Type serviceType) where T : class + private async Task GetServiceAsync(IAsyncServiceProvider asyncServiceProvider, Type serviceType) where T : class { - T result = null; + T? result = null; await ThreadHelper.JoinableTaskFactory.RunAsync(async () => { Guid serviceTypeGuid = serviceType.GUID; @@ -123,7 +123,7 @@ await ThreadHelper.JoinableTaskFactory.RunAsync(async () => return result; } - private void DoInitialize(IVsShell shellService, IMenuCommandService menuCommandService) + private void DoInitialize(IVsShell? shellService, IMenuCommandService? menuCommandService) { if (initialized) return; @@ -133,7 +133,7 @@ private void DoInitialize(IVsShell shellService, IMenuCommandService menuCommand InitCommands(shellService, menuCommandService); } - private void InstallIfNeeded(IVsShell shellService) + private void InstallIfNeeded(IVsShell? shellService) { if (shellService == null) { @@ -150,29 +150,27 @@ private void InstallIfNeeded(IVsShell shellService) if (installedVersion.Installed && (installedVersion.Version == null || installedVersion.Version >= availableVersion.Version)) return; - InstallationManager.Install(targetPath, out string error, out string warning); + InstallationManager.Install(targetPath, out string? error, out string? warning); if (error != null) ShellDialogs.Error(this, Res.ErrorMessageFailedToInstall(targetPath, error)); else if (warning != null) ShellDialogs.Warning(this, Res.WarningMessageInstallationFinishedWithWarning(targetPath, warning)); else if (installedVersion.Installed && installedVersion.Version != null) - ShellDialogs.Info(this, Res.InfoMessageUpgradeFinished(installedVersion.Version, availableVersion.Version, targetPath)); + ShellDialogs.Info(this, Res.InfoMessageUpgradeFinished(installedVersion.Version, availableVersion.Version!, targetPath)); else - ShellDialogs.Info(this, Res.InfoMessageInstallationFinished(availableVersion.Version, targetPath)); + ShellDialogs.Info(this, Res.InfoMessageInstallationFinished(availableVersion.Version!, targetPath)); } - private void InitCommands(IVsShell shellService, IMenuCommandService menuCommandService) + private void InitCommands(IVsShell? shellService, IMenuCommandService? menuCommandService) { + // no menu point will be added + if (menuCommandService == null) + return; + menuCommandService.AddCommand(ExecuteImagingToolsCommand.GetCreateCommand(this)); menuCommandService.AddCommand(ManageDebuggerVisualizerInstallationsCommand.GetCreateCommand(this, shellService)); } - private void DestroyCommands() - { - ExecuteImagingToolsCommand.DestroyCommand(); - ManageDebuggerVisualizerInstallationsCommand.DestroyCommand(); - } - #endregion #endregion diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ExecuteImagingToolsCommand.cs b/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ExecuteImagingToolsCommand.cs index 35e2206..3f8fa26 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ExecuteImagingToolsCommand.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ExecuteImagingToolsCommand.cs @@ -26,16 +26,17 @@ #endregion +#nullable enable + namespace KGySoft.Drawing.DebuggerVisualizers.Package { internal static class ExecuteImagingToolsCommand { #region Fields - private static MenuCommand commandInstance; - private static IServiceProvider serviceProvider; - private static IViewModel imagingToolsViewModel; - private static IView imagingToolsView; + private static MenuCommand? commandInstance; + private static IServiceProvider? serviceProvider; + private static volatile IView? imagingToolsView; #endregion @@ -58,8 +59,6 @@ internal static void DestroyCommand() { imagingToolsView?.Dispose(); imagingToolsView = null; - imagingToolsViewModel?.Dispose(); - imagingToolsViewModel = null; } #endregion @@ -71,20 +70,15 @@ private static void OnExecuteImagingToolsCommand(object sender, EventArgs e) try { if (imagingToolsView == null || imagingToolsView.IsDisposed) - { - imagingToolsViewModel?.Dispose(); - imagingToolsViewModel = ViewModelFactory.CreateDefault(); - imagingToolsView = ViewFactory.CreateView(imagingToolsViewModel); - } - - imagingToolsView.Show(); + imagingToolsView = ViewHelper.CreateViewInNewThread(ViewModelFactory.CreateDefault); + else + imagingToolsView.Show(); } catch (Exception ex) { imagingToolsView?.Dispose(); - imagingToolsViewModel?.Dispose(); imagingToolsView = null; - ShellDialogs.Error(serviceProvider, Res.ErrorMessageUnexpectedError(ex.Message)); + ShellDialogs.Error(serviceProvider!, Res.ErrorMessageUnexpectedError(ex.Message)); } } diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/Ids.cs b/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/Ids.cs index 6c295fe..b343c4f 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/Ids.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/Ids.cs @@ -20,6 +20,8 @@ #endregion +#nullable enable + namespace KGySoft.Drawing.DebuggerVisualizers.Package { internal static class Ids @@ -30,7 +32,7 @@ internal static class Ids internal const string ResourceTitle = "110"; internal const string ResourceDetails = "112"; internal const int IconResourceId = 400; - internal const string Version = "2.3.0"; // Note: in .vsixmanifest it should be adjusted manually + internal const string Version = "2.4.0"; // Note: in .vsixmanifest it should be adjusted manually internal const int ExecuteImagingToolsCommandId = 0x0100; internal const int ManageDebuggerVisualizerInstallationsCommandId = 0x0101; diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ManageDebuggerVisualizerInstallationsCommand.cs b/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ManageDebuggerVisualizerInstallationsCommand.cs index ed91333..c4f67cf 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ManageDebuggerVisualizerInstallationsCommand.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ManageDebuggerVisualizerInstallationsCommand.cs @@ -27,17 +27,18 @@ #endregion +#nullable enable + namespace KGySoft.Drawing.DebuggerVisualizers.Package { internal static class ManageDebuggerVisualizerInstallationsCommand { #region Fields - private static MenuCommand commandInstance; - private static IServiceProvider serviceProvider; - private static IVsShell shellService; - private static IViewModel manageInstallationsViewModel; - private static IView manageInstallationsView; + private static MenuCommand? commandInstance; + private static IServiceProvider serviceProvider = default!; + private static IVsShell? shellService; + private static IView? manageInstallationsView; #endregion @@ -45,13 +46,13 @@ internal static class ManageDebuggerVisualizerInstallationsCommand #region Internal Methods - internal static MenuCommand GetCreateCommand(IServiceProvider package, IVsShell vsShell) + internal static MenuCommand GetCreateCommand(IServiceProvider package, IVsShell? vsShell) { if (commandInstance == null) { serviceProvider = package; shellService = vsShell; - commandInstance = new OleMenuCommand(OnExecuteImagingToolsCommand, new CommandID(Ids.CommandSet, Ids.ManageDebuggerVisualizerInstallationsCommandId)); + commandInstance = new OleMenuCommand(OnExecuteManageDebuggerVisualizerInstallationsCommand, new CommandID(Ids.CommandSet, Ids.ManageDebuggerVisualizerInstallationsCommandId)); } return commandInstance; @@ -61,34 +62,28 @@ internal static void DestroyCommand() { manageInstallationsView?.Dispose(); manageInstallationsView = null; - manageInstallationsViewModel?.Dispose(); - manageInstallationsViewModel = null; } #endregion #region Event handlers - private static void OnExecuteImagingToolsCommand(object sender, EventArgs e) + private static void OnExecuteManageDebuggerVisualizerInstallationsCommand(object sender, EventArgs e) { try { if (manageInstallationsView == null || manageInstallationsView.IsDisposed) { - manageInstallationsViewModel?.Dispose(); -#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread - invoked in UI thread. And ThreadHelper.ThrowIfNotOnUIThread() just emits another warning. - shellService.GetProperty((int)__VSSPROPID2.VSSPROPID_VisualStudioDir, out object documentsDirObj); -#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread - manageInstallationsViewModel = ViewModelFactory.CreateManageInstallations(documentsDirObj?.ToString()); - manageInstallationsView = ViewFactory.CreateView(manageInstallationsViewModel); + object? documentsDirObj = null; + shellService?.GetProperty((int)__VSSPROPID2.VSSPROPID_VisualStudioDir, out documentsDirObj); + manageInstallationsView = ViewHelper.CreateViewInNewThread(() => ViewModelFactory.CreateManageInstallations(documentsDirObj?.ToString())); } - - manageInstallationsView.Show(); + else + manageInstallationsView.Show(); } catch (Exception ex) { manageInstallationsView?.Dispose(); - manageInstallationsViewModel?.Dispose(); manageInstallationsView = null; ShellDialogs.Error(serviceProvider, Res.ErrorMessageUnexpectedError(ex.Message)); } diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ShellDialogs.cs b/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ShellDialogs.cs index 751bf12..a281a39 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ShellDialogs.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ShellDialogs.cs @@ -23,6 +23,8 @@ #endregion +#nullable enable + namespace KGySoft.Drawing.DebuggerVisualizers.Package { internal static class ShellDialogs diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ViewHelper.cs b/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ViewHelper.cs new file mode 100644 index 0000000..7f5016f --- /dev/null +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/_Classes/ViewHelper.cs @@ -0,0 +1,72 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: ViewHelper.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Threading; + +using KGySoft.Drawing.ImagingTools.View; +using KGySoft.Drawing.ImagingTools.ViewModel; + +#endregion + +#nullable enable + +namespace KGySoft.Drawing.DebuggerVisualizers.Package +{ + internal static class ViewHelper + { + #region Methods + + /// + /// Creates the view (along with the view model) in a new STA thread. This has two benefits: + /// 1.) The possible lagging of Visual Studio will not affect the view + /// 2.) Creation of the view model might change the display language of the current thread, which would be the thread of Visual Studio in this project. + /// + /// The view model factory. + /// + internal static IView CreateViewInNewThread(Func viewModelFactory) + { + IView? result = null; + using var created = new ManualResetEvent(false); + + // Creating a non-background STA thread for the view so the possible lagging of VisualStudio will not affect its performance + var t = new Thread(() => + { + using IViewModel viewModel = viewModelFactory.Invoke(); + result = ViewFactory.CreateView(viewModel); + + // ReSharper disable once AccessToDisposedClosure - disposed only after awaited + created.Set(); + + // Now the view is shown as a dialog and this thread is kept alive until it is closed. + // The caller method returns once the view is created and the result is also stored and can + // be re-used until closing the view and thus exiting the thread. + result.ShowDialog(); + result.Dispose(); + }); + + t.SetApartmentState(ApartmentState.STA); + t.IsBackground = false; + t.Start(); + created.WaitOne(); + return result!; + } + + #endregion + } +} diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/packages.config b/KGySoft.Drawing.DebuggerVisualizers.Package/packages.config index 63f9ab4..9a46723 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Package/packages.config +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/packages.config @@ -1,7 +1,7 @@  - - + + diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/readme.md b/KGySoft.Drawing.DebuggerVisualizers.Package/readme.md index 0417ae7..bbc53e8 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Package/readme.md +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/readme.md @@ -1,4 +1,4 @@ -## KGy SOFT Drawing Debugger Visualizers +## KGy SOFT Drawing Debugger Visualizers for Visual Studio 2008-2019 This package provides debugger visualizers for several `System.Drawing` types such as `Image`, `Bitmap`, `Metafile`, `Icon`, `BitmapData`, `Graphics`, `ColorPalette`, `Color`. It supports multi-page, multi-resolution and animated images as well as saving them in various formats. @@ -10,13 +10,17 @@ Either click the magnifier icon or choose a debugger visualizer from the drop do ![Debugging Graphics](https://kgysoft.net/images/DebugGraphics.png) -If an image or icon instance is debugged in a non read-only context, then it can be replaced and the palette entries of non read-only indexed bitmaps can be edited. +If an image or icon instance is debugged in a non read-only context, then it can be modified, replaced or cleared. + +![Changing pixel format with quantizing and dithering](https://kgysoft.net/images/Quantizing.png) + +Several modifications are allowed on non-read-only images such as rotating, resizing, changing pixel format with quantizing and dithering, adjusting brightness, contrast and gamma, or even editing the palette entries of indexed bitmaps. ![Debugging Palette](https://kgysoft.net/images/DebugPalette.png) ## Installing Debugger Visualizers -* For Visual Studio 2013 and above you can use this VSIX installer (tested with Visual Studio 2015, 2017, 2019). It will install the .NET 4.5 version. +* For Visual Studio 2013 and above you can use this VSIX package (tested with Visual Studio versions up to 2019). It will install the .NET 4.5 version, which works also for .NET Core projects. * For older Visual Studio versions and/or frameworks follow the [installation steps](https://github.com/koszeggy/KGySoft.Drawing.Tools#installing-debugger-visualizers) at the project site. ## Release Notes @@ -26,9 +30,9 @@ See the [change log](https://github.com/koszeggy/KGySoft.Drawing.Tools/blob/mast ## FAQ **Q:** Can I use the debugger visualizers for other Visual Studio versions? -
**A:** The VSIX installer supports Visual Studio 2013 and newer versions (tested until 2019). However, you can install the debugger visualizers manually for any version starting with Visual Studio 2008. See the [installation steps](https://github.com/koszeggy/KGySoft.Drawing.Tools#installing-debugger-visualizers) at the project site. +
**A:** The VSIX installer supports Visual Studio 2013 and newer versions (tested with versions up to 2019). However, you can install the debugger visualizers manually for any version starting with Visual Studio 2008. See the [installation steps](https://github.com/koszeggy/KGySoft.Drawing.Tools#installing-debugger-visualizers) at the project site. -**Q:** Is Visual Studio Core supported? +**Q:** Is Visual Studio Code supported?
**A:** As it has a completely different API, it is not supported yet. **Q:** I get an error message when I click the magnifier icon. @@ -37,8 +41,14 @@ See the [change log](https://github.com/koszeggy/KGySoft.Drawing.Tools/blob/mast **Q:** Are WPF image types supported?
**A:** No, these visualizers are for `System.Drawing` types. But the built-in Dependency Object visualizer is able to display image sources anyway. +**Q:** Where do I find the edited/downloaded resource files? Even my previously edited/downloaded resources have been disappeared. +
**A:** The _Visual Studio/Tools/KGy SOFT Drawing Debugger Visualizers_ and clicking the magnifier icon executes the Imaging Tools from different locations. If you edit the language resources at one place they will not be automatically applied at the other place. Therefore, the saved resources might be at different possible locations: +* If you execute a manually deployed version the resources will be in a `Resources` subfolder in the folder you executed Imaging Tools from +* During debugging the tool is executed from the debugger visualizers folder: `Documents\Visual Studio \Visualizers` +* If you launch the tool from the Visual Studio Tools menu, then it is located under `ProgramData\Microsoft\VisualStudio\Packages\...` + **Q:** I have removed the debugger visualizer extension, and it is still working. How can I remove it completely? -
**A:** When the extension is active it copies the visualizers into the `Visual Studio \Visualizers` folder if it is not there. Unlike an MSI installer the VSIX packages do not support uninstall actions so this copied content will not be removed automatically. However, the extension creates also a _KGy SOFT Drawing Debugger Visualizers/Manage Installations..._ menu item under the Tools menu where you can remove the installation from the Documents folder. So the proper way of a complete uninstall: +
**A:** When the extension is active it copies the visualizers into the `Documents\Visual Studio \Visualizers` folder if it is not there. Unlike an MSI installer the VSIX packages do not support uninstall actions so this copied content will not be removed automatically. However, the extension creates also a _KGy SOFT Drawing Debugger Visualizers/Manage Installations..._ menu item under the Tools menu where you can remove the installation from the Documents folder. So the proper way of a complete uninstall: 1. Click _Tools/Drawing Debugger Visualizers/Manage Installations..._ 2. Select the current Visual Studio version and click "Remove" 3. Now uninstall the extension from the _Tools/Extensions and Updates..._ (2019: _Extensions/Manage Extensions_) menu. Without this last step the debugger visualizers will be automatically reinstalled when you restart Visual Studio. diff --git a/KGySoft.Drawing.DebuggerVisualizers.Package/source.extension.vsixmanifest b/KGySoft.Drawing.DebuggerVisualizers.Package/source.extension.vsixmanifest index 62e6558..cfb65b8 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Package/source.extension.vsixmanifest +++ b/KGySoft.Drawing.DebuggerVisualizers.Package/source.extension.vsixmanifest @@ -1,7 +1,7 @@  - + KGy SOFT Drawing DebuggerVisualizers Debugger Visualizers for System.Drawing types such as Image (Bitmap and Metafile), Icon, Graphics, BitmapData, ColorPalette and Color. https://github.com/koszeggy/KGySoft.Drawing.Tools diff --git a/KGySoft.Drawing.DebuggerVisualizers.Test/GlobalSuppressions.cs b/KGySoft.Drawing.DebuggerVisualizers.Test/GlobalSuppressions.cs index 4c4658c..bfa5186 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Test/GlobalSuppressions.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Test/GlobalSuppressions.cs @@ -7,3 +7,4 @@ [assembly: SuppressMessage("Style", "IDE0063:Use simple 'using' statement", Justification = "Decided individually")] [assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "Decided individually")] +[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'", Justification = "Decided individually")] diff --git a/KGySoft.Drawing.DebuggerVisualizers.Test/KGySoft.Drawing.DebuggerVisualizers.Test.csproj b/KGySoft.Drawing.DebuggerVisualizers.Test/KGySoft.Drawing.DebuggerVisualizers.Test.csproj index f57228d..5ef8f4c 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Test/KGySoft.Drawing.DebuggerVisualizers.Test.csproj +++ b/KGySoft.Drawing.DebuggerVisualizers.Test/KGySoft.Drawing.DebuggerVisualizers.Test.csproj @@ -1,7 +1,7 @@  - net35;net40;net45;netcoreapp3.0 + net35;net40;net45;net5.0-windows false KGySoft.Drawing.DebuggerVisualizers.Test @@ -16,16 +16,22 @@ true WinExe app.manifest + enable + + + $(NoWarn);NETSDK1138 - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + NU1701 + @@ -33,7 +39,7 @@ - + diff --git a/KGySoft.Drawing.DebuggerVisualizers.Test/Program.cs b/KGySoft.Drawing.DebuggerVisualizers.Test/Program.cs index aa1f12a..8b4a7cd 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Test/Program.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Test/Program.cs @@ -17,7 +17,6 @@ #region Usings using System; -using System.Diagnostics.CodeAnalysis; using System.Windows.Forms; using KGySoft.Drawing.DebuggerVisualizers.Test.View; @@ -31,8 +30,6 @@ static class Program #region Methods [STAThread] - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "Would just cause double disposing because closing will dispose the form anyway.")] static void Main() { Application.EnableVisualStyles(); diff --git a/KGySoft.Drawing.DebuggerVisualizers.Test/Properties/AssemblyInfo.cs b/KGySoft.Drawing.DebuggerVisualizers.Test/Properties/AssemblyInfo.cs index 95cfbda..2d46710 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Test/Properties/AssemblyInfo.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Test/Properties/AssemblyInfo.cs @@ -35,6 +35,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.3.0.0")] -[assembly: AssemblyFileVersion("2.3.0.0")] -[assembly: AssemblyInformationalVersion("2.3.0")] +[assembly: AssemblyVersion("2.4.0")] +[assembly: AssemblyFileVersion("2.4.0")] +[assembly: AssemblyInformationalVersion("2.4.0")] diff --git a/KGySoft.Drawing.DebuggerVisualizers.Test/View/DebuggerTestForm.Designer.cs b/KGySoft.Drawing.DebuggerVisualizers.Test/View/DebuggerTestForm.Designer.cs index 2087f51..1da16a7 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Test/View/DebuggerTestForm.Designer.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Test/View/DebuggerTestForm.Designer.cs @@ -19,7 +19,7 @@ private void InitializeComponent() { this.btnViewDirect = new System.Windows.Forms.Button(); this.btnViewByDebugger = new System.Windows.Forms.Button(); - this.tbFile = new System.Windows.Forms.TextBox(); + this.txtFile = new System.Windows.Forms.TextBox(); this.rbFromFile = new System.Windows.Forms.RadioButton(); this.rbManagedIcon = new System.Windows.Forms.RadioButton(); this.rbHIcon = new System.Windows.Forms.RadioButton(); @@ -49,7 +49,7 @@ private void InitializeComponent() // this.btnViewDirect.Dock = System.Windows.Forms.DockStyle.Top; this.btnViewDirect.FlatStyle = System.Windows.Forms.FlatStyle.System; - this.btnViewDirect.Location = new System.Drawing.Point(0, 348); + this.btnViewDirect.Location = new System.Drawing.Point(0, 347); this.btnViewDirect.Name = "btnViewDirect"; this.btnViewDirect.Size = new System.Drawing.Size(196, 24); this.btnViewDirect.TabIndex = 14; @@ -60,22 +60,22 @@ private void InitializeComponent() // this.btnViewByDebugger.Dock = System.Windows.Forms.DockStyle.Top; this.btnViewByDebugger.FlatStyle = System.Windows.Forms.FlatStyle.System; - this.btnViewByDebugger.Location = new System.Drawing.Point(0, 372); + this.btnViewByDebugger.Location = new System.Drawing.Point(0, 371); this.btnViewByDebugger.Name = "btnViewByDebugger"; this.btnViewByDebugger.Size = new System.Drawing.Size(196, 24); this.btnViewByDebugger.TabIndex = 15; this.btnViewByDebugger.Text = "View by Debugger"; this.btnViewByDebugger.UseVisualStyleBackColor = true; // - // tbFile + // txtFile // - this.tbFile.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend; - this.tbFile.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.FileSystem; - this.tbFile.Dock = System.Windows.Forms.DockStyle.Top; - this.tbFile.Location = new System.Drawing.Point(3, 16); - this.tbFile.Name = "tbFile"; - this.tbFile.Size = new System.Drawing.Size(190, 20); - this.tbFile.TabIndex = 0; + this.txtFile.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend; + this.txtFile.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.FileSystem; + this.txtFile.Dock = System.Windows.Forms.DockStyle.Top; + this.txtFile.Location = new System.Drawing.Point(3, 16); + this.txtFile.Name = "txtFile"; + this.txtFile.Size = new System.Drawing.Size(190, 20); + this.txtFile.TabIndex = 0; // // rbFromFile // @@ -180,7 +180,7 @@ private void InitializeComponent() this.chbAsReadOnly.AutoSize = true; this.chbAsReadOnly.Dock = System.Windows.Forms.DockStyle.Top; this.chbAsReadOnly.FlatStyle = System.Windows.Forms.FlatStyle.System; - this.chbAsReadOnly.Location = new System.Drawing.Point(0, 330); + this.chbAsReadOnly.Location = new System.Drawing.Point(0, 329); this.chbAsReadOnly.Name = "chbAsReadOnly"; this.chbAsReadOnly.Size = new System.Drawing.Size(196, 18); this.chbAsReadOnly.TabIndex = 13; @@ -189,17 +189,16 @@ private void InitializeComponent() // // gbFile // - this.gbFile.AutoSize = true; this.gbFile.Controls.Add(this.rbAsIcon); this.gbFile.Controls.Add(this.rbAsMetafile); this.gbFile.Controls.Add(this.rbAsBitmap); this.gbFile.Controls.Add(this.rbAsImage); - this.gbFile.Controls.Add(this.tbFile); + this.gbFile.Controls.Add(this.txtFile); this.gbFile.Dock = System.Windows.Forms.DockStyle.Top; this.gbFile.Enabled = false; this.gbFile.Location = new System.Drawing.Point(0, 219); this.gbFile.Name = "gbFile"; - this.gbFile.Size = new System.Drawing.Size(196, 111); + this.gbFile.Size = new System.Drawing.Size(196, 110); this.gbFile.TabIndex = 12; this.gbFile.TabStop = false; this.gbFile.Text = "File Details"; @@ -367,7 +366,7 @@ private void InitializeComponent() private RadioButton rbHIcon; private RadioButton rbManagedIcon; private RadioButton rbFromFile; - private TextBox tbFile; + private TextBox txtFile; private Button btnViewByDebugger; private Button btnViewDirect; private PictureBox pictureBox; diff --git a/KGySoft.Drawing.DebuggerVisualizers.Test/View/DebuggerTestForm.cs b/KGySoft.Drawing.DebuggerVisualizers.Test/View/DebuggerTestForm.cs index 5d7be94..54b1f94 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Test/View/DebuggerTestForm.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Test/View/DebuggerTestForm.cs @@ -17,7 +17,6 @@ #region Usings using System; -using System.Diagnostics.CodeAnalysis; using System.Windows.Forms; using KGySoft.ComponentModel; @@ -33,8 +32,9 @@ public partial class DebuggerTestForm : Form private readonly CommandBindingsCollection commandBindings = new CommandBindingsCollection(); private readonly DebuggerTestFormViewModel viewModel = new DebuggerTestFormViewModel(); - private readonly Timer timer; - private string errorMessage; + private readonly Timer? timer; + + private string? errorMessage; #endregion @@ -43,6 +43,7 @@ public partial class DebuggerTestForm : Form public DebuggerTestForm() { InitializeComponent(); + gbFile.AutoSize = !OSUtils.IsMono; cmbPixelFormat.DataSource = viewModel.PixelFormats; commandBindings.AddPropertyBinding(chbAsImage, nameof(CheckBox.Checked), nameof(viewModel.AsImage), viewModel); @@ -60,7 +61,7 @@ public DebuggerTestForm() commandBindings.AddPropertyBinding(rbColor, nameof(RadioButton.Checked), nameof(viewModel.SingleColor), viewModel); commandBindings.AddPropertyBinding(rbFromFile, nameof(RadioButton.Checked), nameof(viewModel.ImageFromFile), viewModel); - commandBindings.AddPropertyBinding(tbFile, nameof(tbFile.Text), nameof(viewModel.FileName), viewModel); + commandBindings.AddPropertyBinding(txtFile, nameof(txtFile.Text), nameof(viewModel.FileName), viewModel); commandBindings.AddPropertyBinding(rbAsImage, nameof(RadioButton.Checked), nameof(viewModel.FileAsImage), viewModel); commandBindings.AddPropertyBinding(rbAsBitmap, nameof(RadioButton.Checked), nameof(viewModel.FileAsBitmap), viewModel); commandBindings.AddPropertyBinding(rbAsMetafile, nameof(RadioButton.Checked), nameof(viewModel.FileAsMetafile), viewModel); @@ -76,15 +77,21 @@ public DebuggerTestForm() commandBindings.AddPropertyBinding(viewModel, nameof(viewModel.PreviewImage), nameof(pictureBox.Image), pictureBox); commandBindings.Add(OnSelectFileCommand) - .AddSource(tbFile, nameof(tbFile.Click)) - .AddSource(tbFile, nameof(tbFile.DoubleClick)); + .AddSource(txtFile, nameof(txtFile.Click)) + .AddSource(txtFile, nameof(txtFile.DoubleClick)); commandBindings.Add(viewModel.DirectViewCommand).AddSource(btnViewDirect, nameof(btnViewDirect.Click)); commandBindings.Add(viewModel.DebugCommand).AddSource(btnViewByDebugger, nameof(btnViewByDebugger.Click)); viewModel.GetHwndCallback = () => Handle; viewModel.GetClipCallback = () => pictureBox.Bounds; - // Due to some strange issue on Linux the app crashes if we show a MessageBox while changing radio buttons + if (OSUtils.IsWindows) + { + viewModel.ErrorCallback = msg => MessageBox.Show(this, msg, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + // Due to some strange issue on Linux the app may crash if we show a MessageBox while changing radio buttons // so as a workaround we show error messages by using a timer. Another solution would be to show a custom dialog. timer = new Timer { Interval = 1 }; viewModel.ErrorCallback = message => @@ -126,19 +133,18 @@ protected override void Dispose(bool disposing) private void OnSelectFileCommand(ICommandSource source) { // simple click opens the file dialog only if text was empty - if (tbFile.Text.Length != 0 && source.TriggeringEvent == nameof(tbFile.Click)) + if (txtFile.Text.Length != 0 && source.TriggeringEvent == nameof(txtFile.Click)) return; - using (OpenFileDialog ofd = new OpenFileDialog { FileName = tbFile.Text }) + using (var ofd = new OpenFileDialog { FileName = txtFile.Text }) { if (ofd.ShowDialog() == DialogResult.OK) - tbFile.Text = ofd.FileName; + txtFile.Text = ofd.FileName; } } - [SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "This is just a test app")] private void OnShowErrorCommand() { - timer.Enabled = false; + timer!.Enabled = false; if (errorMessage != null) MessageBox.Show(this, errorMessage, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); errorMessage = null; diff --git a/KGySoft.Drawing.DebuggerVisualizers.Test/ViewModel/DebuggerTestFormViewModel.cs b/KGySoft.Drawing.DebuggerVisualizers.Test/ViewModel/DebuggerTestFormViewModel.cs index ad59478..3980a64 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Test/ViewModel/DebuggerTestFormViewModel.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Test/ViewModel/DebuggerTestFormViewModel.cs @@ -19,7 +19,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; @@ -62,7 +61,7 @@ internal class DebuggerTestFormViewModel : ObservableObjectBase }; private static readonly Dictionary debuggerVisualizers = Attribute.GetCustomAttributes(typeof(DebuggerHelper).Assembly, typeof(DebuggerVisualizerAttribute)) - .Cast().ToDictionary(a => a.Target, a => a); + .Cast().ToDictionary(a => a.Target!, a => a); #endregion @@ -70,7 +69,13 @@ internal class DebuggerTestFormViewModel : ObservableObjectBase internal bool AsImage { get => Get(); set => Set(value); } internal bool AsImageEnabled { get => Get(); set => Set(value); } - internal PixelFormat[] PixelFormats => Get(() => Enum.GetValues().Where(pf => pf.IsValidFormat()).OrderBy(pf => pf & PixelFormat.Max).ToArray()); + + internal PixelFormat[] PixelFormats => Get(() => Enum.GetValues() + .Where(pf => pf.IsValidFormat()) + // ReSharper disable once BitwiseOperatorOnEnumWithoutFlags + .OrderBy(pf => pf & PixelFormat.Max) + .ToArray()); + internal PixelFormat PixelFormat { get => Get(PixelFormat.Format32bppArgb); set => Set(value); } internal bool PixelFormatEnabled { get => Get(); set => Set(value); } @@ -85,7 +90,7 @@ internal class DebuggerTestFormViewModel : ObservableObjectBase internal bool SingleColor { get => Get(); set => Set(value); } internal bool ImageFromFile { get => Get(); set => Set(value); } - internal string FileName { get => Get(); set => Set(value); } + internal string? FileName { get => Get(); set => Set(value); } internal bool FileAsImage { get => Get(); set => Set(value); } internal bool FileAsBitmap { get => Get(); set => Set(value); } internal bool FileAsMetafile { get => Get(); set => Set(value); } @@ -95,17 +100,17 @@ internal class DebuggerTestFormViewModel : ObservableObjectBase internal bool AsReadOnlyEnabled { get => Get(); set => Set(value); } internal bool CanDebug { get => Get(); set => Set(value); } - internal Image PreviewImage { get => Get(); set => Set(value); } + internal Image? PreviewImage { get => Get(); set => Set(value); } - internal Action ErrorCallback { get => Get>(); set => Set(value); } - internal Func GetHwndCallback { get => Get>(); set => Set(value); } - internal Func GetClipCallback { get => Get>(); set => Set(value); } + internal Action? ErrorCallback { get => Get?>(); set => Set(value); } + internal Func? GetHwndCallback { get => Get?>(); set => Set(value); } + internal Func? GetClipCallback { get => Get?>(); set => Set(value); } internal ICommand DebugCommand => Get(() => new SimpleCommand(OnDebugCommand)); internal ICommand DirectViewCommand => Get(() => new SimpleCommand(OnViewDirectCommand)); - private object TestObject { get => Get(); set => Set(value); } - private Bitmap BitmapDataOwner { get => Get(); set => Set(value); } + private object? TestObject { get => Get(); set => Set(value); } + private Bitmap? BitmapDataOwner { get => Get(); set => Set(value); } #endregion @@ -113,7 +118,7 @@ internal class DebuggerTestFormViewModel : ObservableObjectBase #region Static Methods - private static Image FromPalette(IList palette) + private static Image? FromPalette(IList palette) { var size = palette.Count; if (size == 0) @@ -130,25 +135,31 @@ private static Metafile GenerateMetafile() //Set up reference Graphic Graphics refGraph = Graphics.FromHwnd(IntPtr.Zero); IntPtr hdc = refGraph.GetHdc(); - Metafile result = new Metafile(hdc, new Rectangle(0, 0, 100, 100), MetafileFrameUnit.Pixel, EmfType.EmfOnly, "Test"); + try + { + var result = new Metafile(hdc, new Rectangle(0, 0, 100, 100), MetafileFrameUnit.Pixel, EmfType.EmfOnly, "Test"); + + //Draw some silly drawing + using (var g = Graphics.FromImage(result)) + { + var r = new Rectangle(0, 0, 100, 100); + var leftEye = new Rectangle(20, 20, 20, 30); + var rightEye = new Rectangle(60, 20, 20, 30); + g.FillEllipse(Brushes.Yellow, r); + g.FillEllipse(Brushes.White, leftEye); + g.FillEllipse(Brushes.White, rightEye); + g.DrawEllipse(Pens.Black, leftEye); + g.DrawEllipse(Pens.Black, rightEye); + g.DrawBezier(Pens.Red, new Point(10, 50), new Point(10, 100), new Point(90, 100), new Point(90, 50)); + } - //Draw some silly drawing - using (var g = Graphics.FromImage(result)) + return result; + } + finally { - var r = new Rectangle(0, 0, 100, 100); - var leftEye = new Rectangle(20, 20, 20, 30); - var rightEye = new Rectangle(60, 20, 20, 30); - g.FillEllipse(Brushes.Yellow, r); - g.FillEllipse(Brushes.White, leftEye); - g.FillEllipse(Brushes.White, rightEye); - g.DrawEllipse(Pens.Black, leftEye); - g.DrawEllipse(Pens.Black, rightEye); - g.DrawBezier(Pens.Red, new Point(10, 50), new Point(10, 100), new Point(90, 100), new Point(90, 50)); + refGraph.ReleaseHdc(hdc); + refGraph.Dispose(); } - - refGraph.ReleaseHdc(hdc); //cleanup - refGraph.Dispose(); - return result; } #endregion @@ -160,9 +171,9 @@ private static Metafile GenerateMetafile() protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) { base.OnPropertyChanged(e); - if (e.NewValue is true && radioGroups.FirstOrDefault(g => g.Contains(e.PropertyName)) is HashSet group) + if (e.NewValue is true && radioGroups.FirstOrDefault(g => g.Contains(e.PropertyName!)) is HashSet group) { - AdjustRadioGroup(e.PropertyName, group); + AdjustRadioGroup(e.PropertyName!, group); if (group.Contains(nameof(Bitmap))) { AsImageEnabled = e.PropertyName.In(nameof(Bitmap), nameof(Metafile), nameof(HIcon), nameof(ManagedIcon)); @@ -184,7 +195,11 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) if (e.PropertyName == nameof(TestObject)) { - var obj = TestObject; + Image? preview = PreviewImage; + PreviewImage = null; + (preview as IDisposable)?.Dispose(); + + object? obj = TestObject; PreviewImage = GetPreviewImage(obj); CanDebug = obj != null; } @@ -212,36 +227,51 @@ private void AdjustRadioGroup(string propertyName, IEnumerable group) } } - private object GenerateObject() + private object? GenerateObject() { FreeTestObject(); try { - // actually the transient steps should be disposed, too... as this is just a test, now we rely on the destructor if (Bitmap) - return Icons.Shield.ExtractBitmap(0).ConvertPixelFormat(PixelFormat); + { + using Icon icon = Icons.Shield; + using Bitmap bmp = icon.ExtractBitmap(0)!; + return bmp.ConvertPixelFormat(PixelFormat); + } + if (Metafile) return GenerateMetafile(); if (HIcon) - return AsImage ? SystemIcons.Application.ToMultiResBitmap() : (object)SystemIcons.Application; + return AsImage ? SystemIcons.Application.ToMultiResBitmap() : SystemIcons.Application; + if (ManagedIcon) - return AsImage ? Icons.Application.ToMultiResBitmap() : (object)Icons.Application; + { + if (!AsImage) + return Icons.Application; + using Icon icon = Icons.Application; + return icon.ToMultiResBitmap(); + } + if (GraphicsBitmap) return GetBitmapGraphics(); if (GraphicsHwnd) return GetWindowGraphics(); if (BitmapData) return GetBitmapData(PixelFormat); + if (Palette) - using (var bmp = new Bitmap(1, 1, PixelFormat)) - return bmp.Palette; + { + using var bmp = new Bitmap(1, 1, PixelFormat); + return bmp.Palette; + } + if (SingleColor) return Color.Black; if (ImageFromFile) return FromFile(FileName); } - catch (Exception e) when (!(e is StackOverflowException)) + catch (Exception e) when (e is not StackOverflowException) { ErrorCallback?.Invoke($"Could not generate test object: {e.Message}"); return null; @@ -250,7 +280,7 @@ private object GenerateObject() return null; } - private Image GetPreviewImage(object obj) + private Image? GetPreviewImage(object? obj) { try { @@ -268,7 +298,7 @@ static Image ToSupportedFormat(Image image) => case Graphics graphics: return graphics.ToBitmap(false); case BitmapData _: - return ToSupportedFormat((Image)BitmapDataOwner.Clone()); + return ToSupportedFormat((Image)BitmapDataOwner!.Clone()); case ColorPalette palette: return FromPalette(palette.Entries); case Color color: @@ -277,18 +307,14 @@ static Image ToSupportedFormat(Image image) => return null; } } - catch (Exception e) when (!(e is StackOverflowException)) + catch (Exception e) when (e is not StackOverflowException) { ErrorCallback?.Invoke($"Could not generate preview image: {e.Message}"); return null; } } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the stream is passed to an image so must not be disposed")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", - Justification = "This is just a test application")] - private object FromFile(string fileName) + private object? FromFile(string? fileName) { try { @@ -298,14 +324,14 @@ private object FromFile(string fileName) if (FileAsIcon) return Icons.FromStream(stream); var image = Image.FromStream(stream); - if (FileAsBitmap && !(image is Bitmap)) + if (FileAsBitmap && image is not System.Drawing.Bitmap) { image.Dispose(); ErrorCallback?.Invoke("The file is not a Bitmap"); return null; } - if (FileAsMetafile && !(image is Metafile)) + if (FileAsMetafile && image is not System.Drawing.Imaging.Metafile) { image.Dispose(); ErrorCallback?.Invoke("The file is not a Metafile"); @@ -314,20 +340,22 @@ private object FromFile(string fileName) return image; } - catch (Exception e) when (!(e is StackOverflowException)) + catch (Exception e) when (e is not StackOverflowException) { ErrorCallback?.Invoke($"Could not open file: {e.Message}"); return null; } } - private Graphics GetBitmapGraphics() + private Graphics? GetBitmapGraphics() { try { - return Graphics.FromImage(Icons.Shield.ExtractBitmap(0).ConvertPixelFormat(PixelFormat)); + using Icon icon = Icons.Shield; + using Bitmap bmp = icon.ExtractBitmap(0)!; + return Graphics.FromImage(bmp.ConvertPixelFormat(PixelFormat)); } - catch (Exception e) when (!(e is StackOverflowException)) + catch (Exception e) when (e is not StackOverflowException) { ErrorCallback?.Invoke($"Could not create Graphics from a Bitmap with PixelFormat '{PixelFormat}': {e.Message}"); return null; @@ -346,7 +374,7 @@ private Graphics GetWindowGraphics() private BitmapData GetBitmapData(PixelFormat pixelFormat) { - var bitmap = Icons.Shield.ExtractBitmap(0); + Bitmap bitmap = Icons.Shield.ExtractBitmap(0)!; if (pixelFormat != bitmap.PixelFormat) bitmap = bitmap.ConvertPixelFormat(pixelFormat); BitmapDataOwner = bitmap; @@ -355,13 +383,18 @@ private BitmapData GetBitmapData(PixelFormat pixelFormat) private void FreeTestObject() { - switch (TestObject) + object? obj = TestObject; + if (obj == null) + return; + + TestObject = null; + switch (obj) { case IDisposable disposable: disposable.Dispose(); break; case BitmapData bitmapData: - var bitmap = BitmapDataOwner; + Bitmap bitmap = BitmapDataOwner!; bitmap.UnlockBits(bitmapData); bitmap.Dispose(); BitmapDataOwner = null; @@ -380,19 +413,19 @@ private void OnViewDirectCommand() case Image image: if (!ImageFromFile && AsImage || ImageFromFile && FileAsImage) { - Image newImage = DebuggerHelper.DebugImage(image, !AsReadOnly, hwnd); + Image? newImage = DebuggerHelper.DebugImage(image, !AsReadOnly, hwnd); if (newImage != image) TestObject = newImage; } else if (image is Metafile metafile) { - Metafile newMetafile = DebuggerHelper.DebugMetafile(metafile, !AsReadOnly, hwnd); + Metafile? newMetafile = DebuggerHelper.DebugMetafile(metafile, !AsReadOnly, hwnd); if (newMetafile != image) TestObject = newMetafile; } else if (image is Bitmap bitmap) { - Bitmap newBitmap = DebuggerHelper.DebugBitmap(bitmap, !AsReadOnly, hwnd); + Bitmap? newBitmap = DebuggerHelper.DebugBitmap(bitmap, !AsReadOnly, hwnd); if (newBitmap != bitmap) TestObject = newBitmap; } @@ -400,7 +433,7 @@ private void OnViewDirectCommand() break; case Icon icon: - Icon newIcon = DebuggerHelper.DebugIcon(icon, !AsReadOnly, hwnd); + Icon? newIcon = DebuggerHelper.DebugIcon(icon, !AsReadOnly, hwnd); if (newIcon != icon) TestObject = newIcon; break; @@ -414,7 +447,7 @@ private void OnViewDirectCommand() break; case ColorPalette palette: - ColorPalette newPalette = DebuggerHelper.DebugPalette(palette, !AsReadOnly, hwnd); + ColorPalette? newPalette = DebuggerHelper.DebugPalette(palette, !AsReadOnly, hwnd); if (newPalette != null) { TestObject = newPalette; @@ -431,10 +464,10 @@ private void OnViewDirectCommand() break; default: - throw new InvalidOperationException($"Unexpected object type: {TestObject.GetType()}"); + throw new InvalidOperationException($"Unexpected object type: {TestObject?.GetType()}"); } } - catch (Exception e) when (!(e is StackOverflowException)) + catch (Exception e) when (e is not StackOverflowException) { ErrorCallback?.Invoke($"Failed to view object: {e.Message}"); } @@ -442,14 +475,14 @@ private void OnViewDirectCommand() private void OnDebugCommand() { - object testObject = TestObject; + object? testObject = TestObject; if (testObject == null) return; Type targetType = testObject is Image && (!ImageFromFile && AsImage || ImageFromFile && FileAsImage) ? typeof(Image) : testObject.GetType(); - DebuggerVisualizerAttribute attr = debuggerVisualizers.GetValueOrDefault(targetType); + DebuggerVisualizerAttribute? attr = debuggerVisualizers.GetValueOrDefault(targetType); if (attr == null) { ErrorCallback?.Invoke($"No debugger visualizer found for type {targetType}"); @@ -460,13 +493,13 @@ private void OnDebugCommand() { var windowService = new TestWindowService(); var objectProvider = new TestObjectProvider(testObject) { IsObjectReplaceable = !AsReadOnly }; - DialogDebuggerVisualizer debugger = (DialogDebuggerVisualizer)Reflector.CreateInstance(Reflector.ResolveType(attr.VisualizerTypeName)); - objectProvider.Serializer = (VisualizerObjectSource)Reflector.CreateInstance(Reflector.ResolveType(attr.VisualizerObjectSourceTypeName)); + DialogDebuggerVisualizer debugger = (DialogDebuggerVisualizer)Reflector.CreateInstance(Reflector.ResolveType(attr.VisualizerTypeName)!); + objectProvider.Serializer = (VisualizerObjectSource)Reflector.CreateInstance(Reflector.ResolveType(attr.VisualizerObjectSourceTypeName!)!); Reflector.InvokeMethod(debugger, "Show", windowService, objectProvider); if (objectProvider.ObjectReplaced) TestObject = objectProvider.Object; } - catch (Exception e) when (!(e is StackOverflowException)) + catch (Exception e) when (e is not StackOverflowException) { ErrorCallback?.Invoke($"Failed to debug object: {e.Message}"); } diff --git a/KGySoft.Drawing.DebuggerVisualizers.Test/ViewModel/TestObjectProvider.cs b/KGySoft.Drawing.DebuggerVisualizers.Test/ViewModel/TestObjectProvider.cs index 52e7cb8..95be35d 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Test/ViewModel/TestObjectProvider.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Test/ViewModel/TestObjectProvider.cs @@ -26,6 +26,16 @@ #endregion +#region Suppressions + +#if NET5_0_OR_GREATER +#pragma warning disable SYSLIB0011 // Type or member is obsolete - must use BinaryFormatter to be compatible with the MS implementation +#pragma warning disable IDE0079 // Remove unnecessary suppression - must use BinaryFormatter to be compatible with the MS implementation +#pragma warning disable CS0618 // Use of obsolete symbol - as above +#endif + +#endregion + namespace KGySoft.Drawing.DebuggerVisualizers.Test.ViewModel { internal class TestObjectProvider : IVisualizerObjectProvider @@ -42,7 +52,7 @@ internal class TestObjectProvider : IVisualizerObjectProvider internal object Object { get; private set; } - internal VisualizerObjectSource Serializer { get; set; } + internal VisualizerObjectSource Serializer { get; set; } = default!; internal bool ObjectReplaced { get; private set; } @@ -60,7 +70,7 @@ internal class TestObjectProvider : IVisualizerObjectProvider public Stream GetData() { - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); Serializer.GetData(Object, ms); ms.Position = 0; return ms; @@ -76,11 +86,17 @@ public object GetObject() public void ReplaceObject(object newObject) { - Object = newObject.DeepClone(); + Object = newObject.DeepClone(null); + ObjectReplaced = true; + } + + public void ReplaceData(Stream newObjectData) + { + newObjectData.Position = 0L; + Object = Serializer.CreateReplacementObject(Object, newObjectData); ObjectReplaced = true; } - public void ReplaceData(Stream newObjectData) => throw new NotImplementedException(); public Stream TransferData(Stream outgoingData) => throw new NotImplementedException(); public object TransferObject(object outgoingObject) => throw new NotImplementedException(); diff --git a/KGySoft.Drawing.DebuggerVisualizers.Test/_Classes/OSUtils.cs b/KGySoft.Drawing.DebuggerVisualizers.Test/_Classes/OSUtils.cs index 7627c48..d67fd8a 100644 --- a/KGySoft.Drawing.DebuggerVisualizers.Test/_Classes/OSUtils.cs +++ b/KGySoft.Drawing.DebuggerVisualizers.Test/_Classes/OSUtils.cs @@ -17,7 +17,6 @@ #region Usings using System; -using System.Drawing; using KGySoft.CoreLibraries; @@ -30,12 +29,14 @@ internal static class OSUtils #region Fields private static bool? isWindows; + private static bool? isMono; #endregion #region Properties internal static bool IsWindows => isWindows ??= Environment.OSVersion.Platform.In(PlatformID.Win32NT, PlatformID.Win32Windows); + internal static bool IsMono => isMono ??= Type.GetType("Mono.Runtime") != null; #endregion } diff --git a/KGySoft.Drawing.DebuggerVisualizers/DebuggerHelper.cs b/KGySoft.Drawing.DebuggerVisualizers/DebuggerHelper.cs index 709bd39..01450d3 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/DebuggerHelper.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/DebuggerHelper.cs @@ -21,7 +21,6 @@ using System.Drawing; using System.Drawing.Imaging; -using KGySoft.Drawing.DebuggerVisualizers.Model; using KGySoft.Drawing.ImagingTools.Model; using KGySoft.Drawing.ImagingTools.View; using KGySoft.Drawing.ImagingTools.ViewModel; @@ -47,9 +46,9 @@ public static class DebuggerHelper /// If specified, then the created dialog will be owned by the window that has specified handle. This parameter is optional. ///
Default value: IntPtr.Zero. /// An that is returned by the debugger. If is , then this will be always the original . - public static Image DebugImage(Image image, bool isReplaceable = true, IntPtr ownerWindowHandle = default) + public static Image? DebugImage(Image? image, bool isReplaceable = true, IntPtr ownerWindowHandle = default) { - using (IViewModel vm = ViewModelFactory.FromImage(image, !isReplaceable)) + using (IViewModel vm = ViewModelFactory.FromImage(image, !isReplaceable)) { ViewFactory.ShowDialog(vm, ownerWindowHandle); return vm.IsModified ? vm.GetEditedModel() : image; @@ -64,9 +63,9 @@ public static Image DebugImage(Image image, bool isReplaceable = true, IntPtr ow /// If specified, then the created dialog will be owned by the window that has specified handle. This parameter is optional. ///
Default value: IntPtr.Zero. /// A that is returned by the debugger. If is , then this will be always the original . - public static Bitmap DebugBitmap(Bitmap bitmap, bool isReplaceable = true, IntPtr ownerWindowHandle = default) + public static Bitmap? DebugBitmap(Bitmap? bitmap, bool isReplaceable = true, IntPtr ownerWindowHandle = default) { - using (IViewModel vm = ViewModelFactory.FromBitmap(bitmap, !isReplaceable)) + using (IViewModel vm = ViewModelFactory.FromBitmap(bitmap, !isReplaceable)) { ViewFactory.ShowDialog(vm, ownerWindowHandle); return vm.IsModified ? vm.GetEditedModel() : bitmap; @@ -81,9 +80,9 @@ public static Bitmap DebugBitmap(Bitmap bitmap, bool isReplaceable = true, IntPt /// If specified, then the created dialog will be owned by the window that has specified handle. This parameter is optional. ///
Default value: IntPtr.Zero. /// A that is returned by the debugger. If is , then this will be always the original . - public static Metafile DebugMetafile(Metafile metafile, bool isReplaceable = true, IntPtr ownerWindowHandle = default) + public static Metafile? DebugMetafile(Metafile? metafile, bool isReplaceable = true, IntPtr ownerWindowHandle = default) { - using (IViewModel vm = ViewModelFactory.FromMetafile(metafile, !isReplaceable)) + using (IViewModel vm = ViewModelFactory.FromMetafile(metafile, !isReplaceable)) { ViewFactory.ShowDialog(vm, ownerWindowHandle); return vm.IsModified ? vm.GetEditedModel() : metafile; @@ -98,9 +97,9 @@ public static Metafile DebugMetafile(Metafile metafile, bool isReplaceable = tru /// If specified, then the created dialog will be owned by the window that has specified handle. This parameter is optional. ///
Default value: IntPtr.Zero. /// An that is returned by the debugger. If is , then this will be always the original . - public static Icon DebugIcon(Icon icon, bool isReplaceable = true, IntPtr ownerWindowHandle = default) + public static Icon? DebugIcon(Icon? icon, bool isReplaceable = true, IntPtr ownerWindowHandle = default) { - using (IViewModel vm = ViewModelFactory.FromIcon(icon, !isReplaceable)) + using (IViewModel vm = ViewModelFactory.FromIcon(icon, !isReplaceable)) { ViewFactory.ShowDialog(vm, ownerWindowHandle); return vm.IsModified ? vm.GetEditedModel() : icon; @@ -143,7 +142,7 @@ public static void DebugGraphics(Graphics graphics, IntPtr ownerWindowHandle = d /// If specified, then the created dialog will be owned by the window that has specified handle. This parameter is optional. ///
Default value: IntPtr.Zero. /// A non- instance, when the palette has been edited; otherwise, . - public static ColorPalette DebugPalette(ColorPalette palette, bool isReplaceable, IntPtr ownerWindowHandle = default) + public static ColorPalette? DebugPalette(ColorPalette palette, bool isReplaceable, IntPtr ownerWindowHandle = default) { if (palette == null) throw new ArgumentNullException(nameof(palette), PublicResources.ArgumentNull); @@ -186,25 +185,25 @@ public static ColorPalette DebugPalette(ColorPalette palette, bool isReplaceable #region Internal Methods - internal static ImageReference DebugImage(ImageInfo imageInfo, bool isReplaceable) + internal static ImageInfo? DebugImage(ImageInfo imageInfo, bool isReplaceable) { using (IViewModel viewModel = ViewModelFactory.FromImage(imageInfo, !isReplaceable)) return DebugImageInfo(viewModel, isReplaceable); } - internal static ImageReference DebugBitmap(ImageInfo bitmapInfo, bool isReplaceable) + internal static ImageInfo? DebugBitmap(ImageInfo bitmapInfo, bool isReplaceable) { using (IViewModel viewModel = ViewModelFactory.FromBitmap(bitmapInfo, !isReplaceable)) return DebugImageInfo(viewModel, isReplaceable); } - internal static ImageReference DebugMetafile(ImageInfo metafileInfo, bool isReplaceable) + internal static ImageInfo? DebugMetafile(ImageInfo metafileInfo, bool isReplaceable) { using (IViewModel viewModel = ViewModelFactory.FromMetafile(metafileInfo, !isReplaceable)) return DebugImageInfo(viewModel, isReplaceable); } - internal static ImageReference DebugIcon(ImageInfo iconInfo, bool isReplaceable) + internal static ImageInfo? DebugIcon(ImageInfo iconInfo, bool isReplaceable) { using (IViewModel viewModel = ViewModelFactory.FromIcon(iconInfo, !isReplaceable)) return DebugImageInfo(viewModel, isReplaceable); @@ -226,11 +225,11 @@ internal static void DebugGraphics(GraphicsInfo graphicsInfo) #region Private Methods - private static ImageReference DebugImageInfo(IViewModel viewModel, bool isReplaceable) + private static ImageInfo? DebugImageInfo(IViewModel viewModel, bool isReplaceable) { ViewFactory.ShowDialog(viewModel); if (isReplaceable && viewModel.IsModified) - return new ImageReference(viewModel.GetEditedModel()); + return viewModel.GetEditedModel(); return null; } diff --git a/KGySoft.Drawing.DebuggerVisualizers/GlobalSuppressions.cs b/KGySoft.Drawing.DebuggerVisualizers/GlobalSuppressions.cs index 7b6dae2..4e5b17e 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/GlobalSuppressions.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/GlobalSuppressions.cs @@ -7,3 +7,4 @@ [assembly: SuppressMessage("Style", "IDE0063:Use simple 'using' statement", Justification = "Decided individually")] [assembly: SuppressMessage("Style", "IDE0017:Simplify object initialization", Justification = "Decided individually")] +[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'", Justification = "Decided individually")] diff --git a/KGySoft.Drawing.DebuggerVisualizers/KGySoft.Drawing.DebuggerVisualizers.csproj b/KGySoft.Drawing.DebuggerVisualizers/KGySoft.Drawing.DebuggerVisualizers.csproj index dea5e4e..303d319 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/KGySoft.Drawing.DebuggerVisualizers.csproj +++ b/KGySoft.Drawing.DebuggerVisualizers/KGySoft.Drawing.DebuggerVisualizers.csproj @@ -1,7 +1,7 @@  - net35;net40;net45;netcoreapp3.0 + net35;net40;net45;net5.0-windows false KGySoft.Drawing.DebuggerVisualizers @@ -14,16 +14,22 @@ false LICENSE György Kőszeg + enable + + + $(NoWarn);NETSDK1138 - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + NU1701 + diff --git a/KGySoft.Drawing.DebuggerVisualizers/Model/ColorPaletteReference.cs b/KGySoft.Drawing.DebuggerVisualizers/Model/ColorPaletteReference.cs deleted file mode 100644 index 5c9d0ca..0000000 --- a/KGySoft.Drawing.DebuggerVisualizers/Model/ColorPaletteReference.cs +++ /dev/null @@ -1,74 +0,0 @@ -#region Copyright - -/////////////////////////////////////////////////////////////////////////////// -// File: ColorPaletteReference.cs -/////////////////////////////////////////////////////////////////////////////// -// Copyright (C) KGy SOFT, 2005-2020 - All Rights Reserved -// -// You should have received a copy of the LICENSE file at the top-level -// directory of this distribution. If not, then this file is considered as -// an illegal copy. -// -// Unauthorized copying of this file, via any medium is strictly prohibited. -/////////////////////////////////////////////////////////////////////////////// - -#endregion - -#region Usings - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Linq; -using System.Runtime.Serialization; -using System.Security; - -using KGySoft.Drawing.DebuggerVisualizers.Serialization; -using KGySoft.Drawing.ImagingTools.Model; - -#endregion - -namespace KGySoft.Drawing.DebuggerVisualizers.Model -{ - [Serializable] - internal sealed class ColorPaletteReference : IObjectReference - { - #region Fields - - private readonly byte[] rawData; - - #endregion - - #region Constructors - - internal ColorPaletteReference(ColorPalette palette) - { - if (palette == null) - return; - - using (var ms = new MemoryStream()) - { - SerializationHelper.SerializeColorPalette(palette, ms); - rawData = ms.ToArray(); - } - } - - #endregion - - #region Methods - - [SecurityCritical] - public object GetRealObject(StreamingContext context) - { - if (rawData == null) - return null; - - using MemoryStream ms = new MemoryStream(rawData); - return SerializationHelper.DeserializeColorPalette(ms); - } - - #endregion - } -} diff --git a/KGySoft.Drawing.DebuggerVisualizers/Model/ColorReference.cs b/KGySoft.Drawing.DebuggerVisualizers/Model/ColorReference.cs deleted file mode 100644 index 2b9ca92..0000000 --- a/KGySoft.Drawing.DebuggerVisualizers/Model/ColorReference.cs +++ /dev/null @@ -1,71 +0,0 @@ -#region Copyright - -/////////////////////////////////////////////////////////////////////////////// -// File: ColorReference.cs -/////////////////////////////////////////////////////////////////////////////// -// Copyright (C) KGy SOFT, 2005-2020 - All Rights Reserved -// -// You should have received a copy of the LICENSE file at the top-level -// directory of this distribution. If not, then this file is considered as -// an illegal copy. -// -// Unauthorized copying of this file, via any medium is strictly prohibited. -/////////////////////////////////////////////////////////////////////////////// - -#endregion - -#region Usings - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Linq; -using System.Runtime.Serialization; -using System.Security; - -using KGySoft.Drawing.DebuggerVisualizers.Serialization; -using KGySoft.Drawing.ImagingTools.Model; - -#endregion - -namespace KGySoft.Drawing.DebuggerVisualizers.Model -{ - [Serializable] - internal sealed class ColorReference : IObjectReference - { - #region Fields - - private readonly byte[] rawData; - - #endregion - - #region Constructors - - internal ColorReference(Color color) - { - using (var ms = new MemoryStream()) - { - SerializationHelper.SerializeColor(color, ms); - rawData = ms.ToArray(); - } - } - - #endregion - - #region Methods - - [SecurityCritical] - public object GetRealObject(StreamingContext context) - { - if (rawData == null) - return null; - - using MemoryStream ms = new MemoryStream(rawData); - return SerializationHelper.DeserializeColor(ms); - } - - #endregion - } -} diff --git a/KGySoft.Drawing.DebuggerVisualizers/Model/ImageReference.cs b/KGySoft.Drawing.DebuggerVisualizers/Model/ImageReference.cs deleted file mode 100644 index 2bbd8b2..0000000 --- a/KGySoft.Drawing.DebuggerVisualizers/Model/ImageReference.cs +++ /dev/null @@ -1,152 +0,0 @@ -#region Copyright - -/////////////////////////////////////////////////////////////////////////////// -// File: ImageReference.cs -/////////////////////////////////////////////////////////////////////////////// -// Copyright (C) KGy SOFT, 2005-2019 - All Rights Reserved -// -// You should have received a copy of the LICENSE file at the top-level -// directory of this distribution. If not, then this file is considered as -// an illegal copy. -// -// Unauthorized copying of this file, via any medium is strictly prohibited. -/////////////////////////////////////////////////////////////////////////////// - -#endregion - -#region Usings - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Runtime.Serialization; -using System.Security; - -using KGySoft.Drawing.DebuggerVisualizers.Serialization; -using KGySoft.Drawing.ImagingTools.Model; - -#endregion - -namespace KGySoft.Drawing.DebuggerVisualizers.Model -{ - [Serializable] - internal sealed class ImageReference : IObjectReference - { - #region Fields - - private readonly string fileName; - private readonly bool asIcon; - private readonly byte[] rawData; - - #endregion - - #region Constructors - - internal ImageReference(ImageInfo imageInfo) - { - if (imageInfo.Type == ImageInfoType.None) - return; - - asIcon = imageInfo.Type == ImageInfoType.Icon; - fileName = imageInfo.FileName; - if (fileName != null) - return; - - rawData = SerializeImage(imageInfo); - } - - #endregion - - #region Methods - - #region Static Methods - - private byte[] SerializeImage(ImageInfo imageInfo) - { - using (var ms = new MemoryStream()) - { - if (asIcon) - { - (imageInfo.Icon ?? imageInfo.GetCreateIcon()).SaveAsIcon(ms); - return ms.ToArray(); - } - - using (var bw = new BinaryWriter(ms)) - { - switch (imageInfo.Type) - { - case ImageInfoType.Pages: - case ImageInfoType.MultiRes: - // we must use an inner stream because image.Save (at least TIFF encoder) may overwrite - // the stream content before the original start position - using (var inner = new MemoryStream()) - { - if (imageInfo.Type == ImageInfoType.Pages) - imageInfo.Frames.Select(f => f.Image).SaveAsMultipageTiff(inner); - else - (imageInfo.Icon ?? imageInfo.GetCreateIcon()).SaveAsIcon(inner); - - bw.Write(true); // AsImage - bw.Write((int)inner.Length); - inner.WriteTo(ms); - } - - break; - - case ImageInfoType.Animation: - SerializationHelper.WriteImage(bw, imageInfo.GetCreateImage()); - break; - - default: - SerializationHelper.WriteImage(bw, imageInfo.Image); - break; - } - - return ms.ToArray(); - } - } - } - - #endregion - - #region Instance Methods - - [SecurityCritical] - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the icon is disposed by the container ImageInfo")] - public object GetRealObject(StreamingContext context) - { - if (fileName == null && rawData == null) - return null; - - if (fileName != null) - { - if (asIcon) - return new Icon(fileName); - - try - { - return Image.FromFile(fileName); - } - catch (Exception) - { - if (!fileName.EndsWith(".ico", StringComparison.OrdinalIgnoreCase)) - throw; - - // special handling for icon files: as a Bitmap icons may throw an exception - using (var info = new ImageInfo(new Icon(fileName))) - return info.GetCreateImage().Clone(); - } - } - - MemoryStream ms = new MemoryStream(rawData); - return asIcon ? (object)new Icon(ms) : SerializationHelper.ReadImage(new BinaryReader(ms)); - } - - #endregion - - #endregion - } -} diff --git a/KGySoft.Drawing.DebuggerVisualizers/Properties/AssemblyInfo.cs b/KGySoft.Drawing.DebuggerVisualizers/Properties/AssemblyInfo.cs index 9c000c0..bef6c77 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/Properties/AssemblyInfo.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/Properties/AssemblyInfo.cs @@ -40,9 +40,9 @@ // // You can specify all the values or you can default the Revision and Build Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("2.3.0.0")] -[assembly: AssemblyFileVersion("2.3.0.0")] -[assembly: AssemblyInformationalVersion("2.3.0")] +[assembly: AssemblyVersion("2.4.0")] +[assembly: AssemblyFileVersion("2.4.0")] +[assembly: AssemblyInformationalVersion("2.4.0")] // Image [assembly: DebuggerVisualizer(typeof(ImageDebuggerVisualizer), typeof(ImageSerializer), diff --git a/KGySoft.Drawing.DebuggerVisualizers/Serialization/BitmapDataSerializationInfo.cs b/KGySoft.Drawing.DebuggerVisualizers/Serialization/BitmapDataSerializationInfo.cs index bf9a6fa..966352e 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/Serialization/BitmapDataSerializationInfo.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/Serialization/BitmapDataSerializationInfo.cs @@ -17,7 +17,6 @@ #region Usings using System; -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; using System.IO; @@ -32,14 +31,12 @@ internal class BitmapDataSerializationInfo : IDisposable { #region Properties - internal BitmapDataInfo BitmapDataInfo { get; private set; } + internal BitmapDataInfo BitmapDataInfo { get; private set; } = default!; #endregion #region Constructors - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the stream must not be disposed and the leaveOpen parameter is not available on every targeted platform")] internal BitmapDataSerializationInfo(Stream stream) { ReadFrom(new BinaryReader(stream)); @@ -56,7 +53,7 @@ internal BitmapDataSerializationInfo(BitmapData bitmapData) #region Public Methods - public void Dispose() => BitmapDataInfo?.Dispose(); + public void Dispose() => BitmapDataInfo.Dispose(); #endregion @@ -65,10 +62,10 @@ internal BitmapDataSerializationInfo(BitmapData bitmapData) internal void Write(BinaryWriter bw) { // 1. Bitmap - SerializationHelper.WriteImage(bw, BitmapDataInfo.BackingImage); + SerializationHelper.WriteImage(bw, BitmapDataInfo.BackingImage!); // 2. Data - BitmapData data = BitmapDataInfo.BitmapData; + BitmapData data = BitmapDataInfo.BitmapData!; bw.Write(data.Width); bw.Write(data.Height); bw.Write(data.Stride); diff --git a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorPaletteSerializationInfo.cs b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorPaletteSerializationInfo.cs index 37caf82..d1d78d9 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorPaletteSerializationInfo.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorPaletteSerializationInfo.cs @@ -16,10 +16,10 @@ #region Usings -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; using System.IO; + using KGySoft.Reflection; #endregion @@ -30,7 +30,7 @@ internal sealed class ColorPaletteSerializationInfo { #region Properties - internal ColorPalette Palette { get; private set; } + internal ColorPalette Palette { get; private set; } = default!; #endregion @@ -41,8 +41,6 @@ internal ColorPaletteSerializationInfo(ColorPalette palette) Palette = palette; } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the stream must not be disposed and the leaveOpen parameter is not available on every targeted platform")] internal ColorPaletteSerializationInfo(Stream stream) { ReadFrom(new BinaryReader(stream)); diff --git a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorPaletteSerializer.cs b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorPaletteSerializer.cs index 8c214b3..45c125d 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorPaletteSerializer.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorPaletteSerializer.cs @@ -37,6 +37,11 @@ internal class ColorPaletteSerializer : VisualizerObjectSource /// public override void GetData(object target, Stream outgoingData) => SerializationHelper.SerializeColorPalette((ColorPalette)target, outgoingData); + /// + /// Called when the debugged object has been replaced + /// + public override object CreateReplacementObject(object target, Stream incomingData) => SerializationHelper.DeserializeColorPalette(incomingData); + #endregion } } \ No newline at end of file diff --git a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorSerializationInfo.cs b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorSerializationInfo.cs index 654c999..8420eb9 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorSerializationInfo.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorSerializationInfo.cs @@ -16,7 +16,6 @@ #region Usings -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.IO; @@ -39,8 +38,6 @@ internal ColorSerializationInfo(Color color) Color = color; } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the stream must not be disposed and the leaveOpen parameter is not available on every targeted platform")] internal ColorSerializationInfo(Stream stream) { ReadFrom(new BinaryReader(stream)); diff --git a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorSerializer.cs b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorSerializer.cs index 9a3137b..cfc6867 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorSerializer.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ColorSerializer.cs @@ -37,6 +37,11 @@ internal class ColorSerializer : VisualizerObjectSource /// public override void GetData(object target, Stream outgoingData) => SerializationHelper.SerializeColor((Color)target, outgoingData); + /// + /// Called when the debugged object has been replaced + /// + public override object CreateReplacementObject(object target, Stream incomingData) => SerializationHelper.DeserializeColor(incomingData); + #endregion } } \ No newline at end of file diff --git a/KGySoft.Drawing.DebuggerVisualizers/Serialization/GraphicsSerializationInfo.cs b/KGySoft.Drawing.DebuggerVisualizers/Serialization/GraphicsSerializationInfo.cs index 75ea6e6..399696c 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/Serialization/GraphicsSerializationInfo.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/Serialization/GraphicsSerializationInfo.cs @@ -17,7 +17,6 @@ #region Usings using System; -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Drawing2D; using System.IO; @@ -33,7 +32,7 @@ internal sealed class GraphicsSerializationInfo : IDisposable { #region Properties - internal GraphicsInfo GraphicsInfo { get; private set; } + internal GraphicsInfo GraphicsInfo { get; private set; } = default!; #endregion @@ -44,8 +43,6 @@ internal GraphicsSerializationInfo(Graphics graphics) GraphicsInfo = new GraphicsInfo(graphics); } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the stream must not be disposed and the leaveOpen parameter is not available on every targeted platform")] internal GraphicsSerializationInfo(Stream stream) { ReadFrom(new BinaryReader(stream)); @@ -57,7 +54,7 @@ internal GraphicsSerializationInfo(Stream stream) #region Public Methods - public void Dispose() => GraphicsInfo?.Dispose(); + public void Dispose() => GraphicsInfo.Dispose(); #endregion @@ -66,10 +63,10 @@ internal GraphicsSerializationInfo(Stream stream) internal void Write(BinaryWriter bw) { // 1. Bitmap - SerializationHelper.WriteImage(bw, GraphicsInfo.GraphicsImage); + SerializationHelper.WriteImage(bw, GraphicsInfo.GraphicsImage!); // 2. Transformation matrix - BinarySerializer.SerializeByWriter(bw, GraphicsInfo.Transform.Elements); + BinarySerializer.SerializeByWriter(bw, GraphicsInfo.Transform!.Elements); // 3. Meta bw.Write(GraphicsInfo.OriginalVisibleClipBounds.X); @@ -97,7 +94,7 @@ private void ReadFrom(BinaryReader br) result.GraphicsImage = (Bitmap)SerializationHelper.ReadImage(br); // 2. Transformation matrix - var elements = (float[])BinarySerializer.DeserializeByReader(br); + var elements = (float[])BinarySerializer.DeserializeByReader(br)!; result.Transform = new Matrix(elements[0], elements[1], elements[2], elements[3], elements[4], elements[5]); // 3. Meta diff --git a/KGySoft.Drawing.DebuggerVisualizers/Serialization/IconSerializer.cs b/KGySoft.Drawing.DebuggerVisualizers/Serialization/IconSerializer.cs index e4a4131..70435a7 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/Serialization/IconSerializer.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/Serialization/IconSerializer.cs @@ -37,6 +37,11 @@ internal class IconSerializer : VisualizerObjectSource /// public override void GetData(object target, Stream outgoingData) => SerializationHelper.SerializeIconInfo((Icon)target, outgoingData); + /// + /// Called when the debugged object has been replaced + /// + public override object? CreateReplacementObject(object target, Stream incomingData) => SerializationHelper.DeserializeReplacementIcon(incomingData); + #endregion } } diff --git a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ImageReplacementSerializationInfo.cs b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ImageReplacementSerializationInfo.cs new file mode 100644 index 0000000..e1b5aa9 --- /dev/null +++ b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ImageReplacementSerializationInfo.cs @@ -0,0 +1,177 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: ImageReplacementSerializationInfo.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; + +using KGySoft.Drawing.ImagingTools.Model; + +#endregion + +namespace KGySoft.Drawing.DebuggerVisualizers.Serialization +{ + internal sealed class ImageReplacementSerializationInfo : IDisposable + { + #region Fields + + private readonly ImageInfo? imageInfo; + private readonly Stream? stream; + + #endregion + + #region Constructors + + internal ImageReplacementSerializationInfo(ImageInfo imageInfo) => this.imageInfo = imageInfo; + + internal ImageReplacementSerializationInfo(Stream stream) => this.stream = stream; + + #endregion + + #region Methods + + #region Public Methods + + public void Dispose() + { + imageInfo?.Dispose(); + stream?.Dispose(); + } + + #endregion + + #region Internal Methods + + internal void Write(BinaryWriter bw) + { + // 1.) Image type + bw.Write((byte)imageInfo!.Type); + if (imageInfo.Type == ImageInfoType.None) + return; + + // 2.) File reference + string? fileName = imageInfo.FileName; + bw.Write(fileName != null); + if (fileName != null) + { + bw.Write(fileName); + return; + } + + // 3.) Image content + switch (imageInfo.Type) + { + case ImageInfoType.Icon: + case ImageInfoType.MultiRes: + SerializationHelper.WriteIcon(bw, imageInfo.GetCreateIcon()!); + return; + + case ImageInfoType.Pages: + // we must use an inner stream because image.Save (at least TIFF encoder) may overwrite + // the stream content before the original start position + using (var inner = new MemoryStream()) + { + imageInfo.Frames!.Select(f => f.Image!).SaveAsMultipageTiff(inner); + bw.Write((int)inner.Length); + bw.Flush(); + inner.WriteTo(bw.BaseStream); + } + + return; + + case ImageInfoType.Animation: + // Using GetCreateImage so in case of a changed animated GIF the possible exception is thrown + SerializationHelper.WriteImage(bw, imageInfo.GetCreateImage()!); + return; + + default: + SerializationHelper.WriteImage(bw, imageInfo.Image!); + return; + } + } + + internal object? GetReplacementObject() + { + var br = new BinaryReader(stream!); + + // 1.) Image type + ImageInfoType type = (ImageInfoType)br.ReadByte(); + if (type == ImageInfoType.None) + return null; + + // 2.) From file + string? fileName = br.ReadBoolean() ? br.ReadString() : null; + if (fileName != null) + { + if (type == ImageInfoType.Icon) + { + using FileStream fs = File.OpenRead(fileName); + return Icons.FromStream(fs); + } + + try + { + return Image.FromFile(fileName); + } + catch (Exception) + { + if (!fileName.EndsWith(".ico", StringComparison.OrdinalIgnoreCase)) + throw; + + // special handling for icon files: as a Bitmap icons may throw an exception + using FileStream fs = File.OpenRead(fileName); + using Icon? icon = Icons.FromStream(fs); + return icon?.ExtractNearestBitmap(new Size(UInt16.MaxValue, UInt16.MaxValue), PixelFormat.Format32bppArgb); + } + } + + // 3.) From stream + switch (type) + { + case ImageInfoType.Icon: + return SerializationHelper.ReadIcon(br); + + case ImageInfoType.MultiRes: + using (Icon? icon = SerializationHelper.ReadIcon(br)) + { + try + { + return icon?.ToMultiResBitmap(); + } + catch (Exception) + { + // special handling for icon files: as a Bitmap icons may throw an exception + return icon?.ExtractNearestBitmap(new Size(UInt16.MaxValue, UInt16.MaxValue), PixelFormat.Format32bppArgb); + } + } + + case ImageInfoType.Pages: + return Image.FromStream(new MemoryStream(br.ReadBytes(br.ReadInt32()))); + + default: + return SerializationHelper.ReadImage(br); + } + } + + #endregion + + #endregion + } +} diff --git a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ImageSerializationInfo.cs b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ImageSerializationInfo.cs index c05a0ab..5237faf 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ImageSerializationInfo.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ImageSerializationInfo.cs @@ -18,7 +18,6 @@ using System; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; using System.IO; @@ -34,7 +33,7 @@ internal sealed class ImageSerializationInfo : IDisposable { #region Properties - internal ImageInfo ImageInfo { get; private set; } + internal ImageInfo ImageInfo { get; private set; } = default!; #endregion @@ -42,16 +41,14 @@ internal sealed class ImageSerializationInfo : IDisposable internal ImageSerializationInfo(Image image) { - ImageInfo = new ImageInfo((Image)image?.Clone()); + ImageInfo = new ImageInfo(image); } internal ImageSerializationInfo(Icon icon) { - ImageInfo = new ImageInfo((Icon)icon?.Clone()); + ImageInfo = new ImageInfo(icon); } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "The stream must not be disposed and the leaveOpen parameter is not available in every targeted platform")] internal ImageSerializationInfo(Stream stream) { ReadFrom(new BinaryReader(stream)); @@ -98,7 +95,15 @@ private static void ReadMeta(BinaryReader br, ImageInfoBase imageInfo) #region Public Methods - public void Dispose() => ImageInfo?.Dispose(); + public void Dispose() + { + // image was not cloned so disposing only possibly created frames + if (!ImageInfo.HasFrames) + return; + + foreach (ImageFrameInfo frame in ImageInfo.Frames!) + frame.Dispose(); + } #endregion @@ -120,9 +125,9 @@ internal void Write(BinaryWriter bw) if (saveAsSingleImage) { if (ImageInfo.Type == ImageInfoType.Icon) - SerializationHelper.WriteIcon(bw, ImageInfo.GetCreateIcon()); + SerializationHelper.WriteIcon(bw, ImageInfo.GetCreateIcon()!); else - SerializationHelper.WriteImage(bw, ImageInfo.GetCreateImage()); + SerializationHelper.WriteImage(bw, ImageInfo.GetCreateImage()!); } // Meta is saved even for pages so we will have a general size, etc. @@ -131,11 +136,11 @@ internal void Write(BinaryWriter bw) // 4. Frames (if any) if (ImageInfo.HasFrames) { - bw.Write(ImageInfo.Frames.Length); + bw.Write(ImageInfo.Frames!.Length); foreach (ImageFrameInfo frame in ImageInfo.Frames) { if (!saveAsSingleImage) - SerializationHelper.WriteImage(bw, frame.Image); + SerializationHelper.WriteImage(bw, frame.Image!); WriteMeta(bw, frame); if (ImageInfo.Type == ImageInfoType.Animation) bw.Write(frame.Duration); @@ -169,14 +174,14 @@ private void ReadFrom(BinaryReader br) ReadMeta(br, ImageInfo); - if (imageType == ImageInfoType.SingleImage || imageType == ImageInfoType.Icon && ImageInfo.Icon.GetImagesCount() <= 1) + if (imageType == ImageInfoType.SingleImage || imageType == ImageInfoType.Icon && ImageInfo.Icon!.GetImagesCount() <= 1) return; // 4. Frames (if any) int len = br.ReadInt32(); var frames = new ImageFrameInfo[len]; - Bitmap[] frameImages = savedAsSingleImage - ? imageType == ImageInfoType.Icon ? ImageInfo.Icon.ExtractBitmaps() : ((Bitmap)ImageInfo.Image).ExtractBitmaps() + Bitmap?[] frameImages = savedAsSingleImage + ? imageType == ImageInfoType.Icon ? ImageInfo.Icon!.ExtractBitmaps() : ((Bitmap)ImageInfo.Image!).ExtractBitmaps() : new Bitmap[len]; Debug.Assert(frameImages.Length == frames.Length); for (int i = 0; i < len; i++) diff --git a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ImageSerializer.cs b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ImageSerializer.cs index c266ba2..616815c 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/Serialization/ImageSerializer.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/Serialization/ImageSerializer.cs @@ -37,6 +37,11 @@ internal class ImageSerializer : VisualizerObjectSource /// public override void GetData(object target, Stream outgoingData) => SerializationHelper.SerializeImageInfo((Image)target, outgoingData); + /// + /// Called when the debugged object has been replaced + /// + public override object? CreateReplacementObject(object target, Stream incomingData) => SerializationHelper.DeserializeReplacementImage(incomingData); + #endregion } } diff --git a/KGySoft.Drawing.DebuggerVisualizers/Serialization/SerializationHelper.cs b/KGySoft.Drawing.DebuggerVisualizers/Serialization/SerializationHelper.cs index f4c473d..db4c90c 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/Serialization/SerializationHelper.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/Serialization/SerializationHelper.cs @@ -17,7 +17,6 @@ #region Usings using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; using System.IO; @@ -37,56 +36,48 @@ internal static class SerializationHelper { #region Internal Methods - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the stream must not be disposed and the leaveOpen parameter is not available on every targeted platform")] internal static void SerializeImageInfo(Image image, Stream outgoingData) { using (var imageInfo = new ImageSerializationInfo(image)) imageInfo.Write(new BinaryWriter(outgoingData)); } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the stream must not be disposed and the leaveOpen parameter is not available on every targeted platform")] internal static void SerializeIconInfo(Icon icon, Stream outgoingData) { using (var iconInfo = new ImageSerializationInfo(icon)) iconInfo.Write(new BinaryWriter(outgoingData)); } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the stream must not be disposed and the leaveOpen parameter is not available on every targeted platform")] + internal static void SerializeReplacementImageInfo(ImageInfo imageInfo, Stream outgoingData) + { + using (var replacementInfo = new ImageReplacementSerializationInfo(imageInfo)) + replacementInfo.Write(new BinaryWriter(outgoingData)); + } + internal static void SerializeGraphicsInfo(Graphics g, Stream outgoingData) { using (var graphicsInfo = new GraphicsSerializationInfo(g)) graphicsInfo.Write(new BinaryWriter(outgoingData)); } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the stream must not be disposed and the leaveOpen parameter is not available on every targeted platform")] internal static void SerializeBitmapDataInfo(BitmapData bitmapData, Stream outgoingData) { using (var bitmapDataInfo = new BitmapDataSerializationInfo(bitmapData)) bitmapDataInfo.Write(new BinaryWriter(outgoingData)); } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the stream must not be disposed and the leaveOpen parameter is not available on every targeted platform")] internal static void SerializeColor(Color color, Stream outgoingData) => new ColorSerializationInfo(color).Write(new BinaryWriter(outgoingData)); - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the stream must not be disposed and the leaveOpen parameter is not available on every targeted platform")] internal static void SerializeColorPalette(ColorPalette palette, Stream outgoingData) => new ColorPaletteSerializationInfo(palette).Write(new BinaryWriter(outgoingData)); - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, disposing would dispose the return value")] internal static ImageInfo DeserializeImageInfo(Stream stream) => new ImageSerializationInfo(stream).ImageInfo; - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, disposing would dispose the return value")] + internal static Image? DeserializeReplacementImage(Stream stream) => (Image?)new ImageReplacementSerializationInfo(stream).GetReplacementObject(); + + internal static Icon? DeserializeReplacementIcon(Stream stream) => (Icon?)new ImageReplacementSerializationInfo(stream).GetReplacementObject(); + internal static BitmapDataInfo DeserializeBitmapDataInfo(Stream stream) => new BitmapDataSerializationInfo(stream).BitmapDataInfo; - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, disposing would dispose the return value")] internal static GraphicsInfo DeserializeGraphicsInfo(Stream stream) => new GraphicsSerializationInfo(stream).GraphicsInfo; internal static Color DeserializeColor(Stream stream) => new ColorSerializationInfo(stream).Color; @@ -95,14 +86,13 @@ internal static void SerializeBitmapDataInfo(BitmapData bitmapData, Stream outgo internal static void WriteImage(BinaryWriter bw, Image image) { - Debug.Assert(image != null, "Image should not be null here"); int bpp; // writing a decoder compatible stream if image is a metafile... bool asImage = image is Metafile // ... or is a TIFF with 48/64 BPP because saving as TIFF can preserve pixel format only if the raw format is also TIFF... || (bpp = image.GetBitsPerPixel()) > 32 && image.RawFormat.Guid == ImageFormat.Tiff.Guid - // ... or is an animated GIF, which always have 32 BPP pixel format + // ... or is an animated GIF, which always has 32 BPP pixel format || bpp == 32 && image.RawFormat.Guid == ImageFormat.Gif.Guid // ... or image is an icon - actually needed only for Windows XP to prevent error from LockBits when sizes are not recognized || image.RawFormat.Guid == ImageFormat.Icon.Guid; @@ -117,10 +107,13 @@ internal static void WriteImage(BinaryWriter bw, Image image) WriteRawBitmap((Bitmap)image, bw); } + internal static Image ReadImage(BinaryReader br) => br.ReadBoolean() + ? Image.FromStream(new MemoryStream(br.ReadBytes(br.ReadInt32()))) + : ReadRawBitmap(br); + internal static void WriteIcon(BinaryWriter bw, Icon icon) { - Debug.Assert(icon != null, "Icon should not be null here"); - using (MemoryStream ms = new MemoryStream()) + using (var ms = new MemoryStream()) { icon.SaveAsIcon(ms); bw.Write((int)ms.Length); @@ -128,13 +121,7 @@ internal static void WriteIcon(BinaryWriter bw, Icon icon) } } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "False alarm, the stream is passed to an image so must not be disposed")] - internal static Image ReadImage(BinaryReader br) => br.ReadBoolean() - ? Image.FromStream(new MemoryStream(br.ReadBytes(br.ReadInt32()))) - : ReadRawBitmap(br); - - internal static Icon ReadIcon(BinaryReader br) => new Icon(new MemoryStream(br.ReadBytes(br.ReadInt32()))); + internal static Icon? ReadIcon(BinaryReader br) => Icons.FromStream(new MemoryStream(br.ReadBytes(br.ReadInt32()))); #endregion @@ -144,7 +131,7 @@ private static void WriteAsImage(BinaryWriter bw, Image image) { // we must use an inner stream because image.Save (at least TIFF encoder) may overwrite // the stream content before the original start position - using (MemoryStream ms = new MemoryStream()) + using (var ms = new MemoryStream()) { if (image is Metafile metafile) metafile.Save(ms); @@ -196,7 +183,7 @@ private static Bitmap ReadRawBitmap(BinaryReader br) { var size = new Size(br.ReadInt32(), br.ReadInt32()); var pixelFormat = (PixelFormat)br.ReadInt32(); - Color[] palette = null; + Color[]? palette = null; if (pixelFormat.ToBitsPerPixel() <= 8) { palette = new Color[br.ReadInt32()]; diff --git a/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/BitmapDebuggerVisualizer.cs b/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/BitmapDebuggerVisualizer.cs index 727ff65..df0964a 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/BitmapDebuggerVisualizer.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/BitmapDebuggerVisualizer.cs @@ -17,8 +17,8 @@ #region Usings using System.Diagnostics.CodeAnalysis; +using System.IO; -using KGySoft.Drawing.DebuggerVisualizers.Model; using KGySoft.Drawing.DebuggerVisualizers.Serialization; using KGySoft.Drawing.ImagingTools.Model; @@ -41,12 +41,14 @@ internal sealed class BitmapDebuggerVisualizer : DialogDebuggerVisualizer /// The object provider. protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider) { - using (ImageInfo imageInfo = SerializationHelper.DeserializeImageInfo(objectProvider.GetData())) - { - ImageReference replacementObject = DebuggerHelper.DebugBitmap(imageInfo, objectProvider.IsObjectReplaceable); - if (objectProvider.IsObjectReplaceable && replacementObject != null) - objectProvider.ReplaceObject(replacementObject); - } + using ImageInfo imageInfo = SerializationHelper.DeserializeImageInfo(objectProvider.GetData()); + ImageInfo? replacementObject = DebuggerHelper.DebugBitmap(imageInfo, objectProvider.IsObjectReplaceable); + if (replacementObject == null) + return; + + using var ms = new MemoryStream(); + SerializationHelper.SerializeReplacementImageInfo(replacementObject, ms); + objectProvider.ReplaceData(ms); } #endregion diff --git a/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/ColorDebuggerVisualizer.cs b/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/ColorDebuggerVisualizer.cs index 72b8089..43d6df2 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/ColorDebuggerVisualizer.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/ColorDebuggerVisualizer.cs @@ -18,8 +18,8 @@ using System.Diagnostics.CodeAnalysis; using System.Drawing; +using System.IO; -using KGySoft.Drawing.DebuggerVisualizers.Model; using KGySoft.Drawing.DebuggerVisualizers.Serialization; using Microsoft.VisualStudio.DebuggerVisualizers; @@ -41,8 +41,12 @@ internal class ColorDebuggerVisualizer : DialogDebuggerVisualizer protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider) { Color? newColor = DebuggerHelper.DebugColor(SerializationHelper.DeserializeColor(objectProvider.GetData()), objectProvider.IsObjectReplaceable); - if (objectProvider.IsObjectReplaceable && newColor != null) - objectProvider.ReplaceObject(new ColorReference(newColor.Value)); + if (!objectProvider.IsObjectReplaceable || newColor == null) + return; + + using var ms = new MemoryStream(); + SerializationHelper.SerializeColor(newColor.Value, ms); + objectProvider.ReplaceData(ms); } #endregion diff --git a/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/ColorPaletteDebuggerVisualizer.cs b/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/ColorPaletteDebuggerVisualizer.cs index a3a1717..f9fbe18 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/ColorPaletteDebuggerVisualizer.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/ColorPaletteDebuggerVisualizer.cs @@ -18,7 +18,7 @@ using System.Diagnostics.CodeAnalysis; using System.Drawing.Imaging; -using KGySoft.Drawing.DebuggerVisualizers.Model; +using System.IO; using KGySoft.Drawing.DebuggerVisualizers.Serialization; using Microsoft.VisualStudio.DebuggerVisualizers; @@ -40,9 +40,13 @@ internal sealed class ColorPaletteDebuggerVisualizer : DialogDebuggerVisualizer /// The object provider. protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider) { - ColorPalette newPalette = DebuggerHelper.DebugPalette(SerializationHelper.DeserializeColorPalette(objectProvider.GetData()), objectProvider.IsObjectReplaceable); - if (objectProvider.IsObjectReplaceable && newPalette != null) - objectProvider.ReplaceObject(new ColorPaletteReference(newPalette)); + ColorPalette? newPalette = DebuggerHelper.DebugPalette(SerializationHelper.DeserializeColorPalette(objectProvider.GetData()), objectProvider.IsObjectReplaceable); + if (!objectProvider.IsObjectReplaceable || newPalette == null) + return; + + using var ms = new MemoryStream(); + SerializationHelper.SerializeColorPalette(newPalette, ms); + objectProvider.ReplaceData(ms); } #endregion diff --git a/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/IconDebuggerVisualizer.cs b/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/IconDebuggerVisualizer.cs index 4768a2b..54e942d 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/IconDebuggerVisualizer.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/IconDebuggerVisualizer.cs @@ -17,8 +17,8 @@ #region Usings using System.Diagnostics.CodeAnalysis; +using System.IO; -using KGySoft.Drawing.DebuggerVisualizers.Model; using KGySoft.Drawing.DebuggerVisualizers.Serialization; using KGySoft.Drawing.ImagingTools.Model; @@ -41,12 +41,14 @@ internal sealed class IconDebuggerVisualizer : DialogDebuggerVisualizer /// The object provider. protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider) { - using (ImageInfo iconInfo = SerializationHelper.DeserializeImageInfo(objectProvider.GetData())) - { - ImageReference replacementObject = DebuggerHelper.DebugIcon(iconInfo, objectProvider.IsObjectReplaceable); - if (objectProvider.IsObjectReplaceable && replacementObject != null) - objectProvider.ReplaceObject(replacementObject); - } + using ImageInfo iconInfo = SerializationHelper.DeserializeImageInfo(objectProvider.GetData()); + ImageInfo? replacementObject = DebuggerHelper.DebugIcon(iconInfo, objectProvider.IsObjectReplaceable); + if (replacementObject == null) + return; + + using var ms = new MemoryStream(); + SerializationHelper.SerializeReplacementImageInfo(replacementObject, ms); + objectProvider.ReplaceData(ms); } #endregion diff --git a/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/ImageDebuggerVisualizer.cs b/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/ImageDebuggerVisualizer.cs index 057f261..8da0238 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/ImageDebuggerVisualizer.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/ImageDebuggerVisualizer.cs @@ -17,8 +17,8 @@ #region Usings using System.Diagnostics.CodeAnalysis; +using System.IO; -using KGySoft.Drawing.DebuggerVisualizers.Model; using KGySoft.Drawing.DebuggerVisualizers.Serialization; using KGySoft.Drawing.ImagingTools.Model; @@ -41,12 +41,14 @@ internal sealed class ImageDebuggerVisualizer : DialogDebuggerVisualizer /// The object provider. protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider) { - using (ImageInfo imageInfo = SerializationHelper.DeserializeImageInfo(objectProvider.GetData())) - { - ImageReference replacementObject = DebuggerHelper.DebugImage(imageInfo, objectProvider.IsObjectReplaceable); - if (objectProvider.IsObjectReplaceable && replacementObject != null) - objectProvider.ReplaceObject(replacementObject); - } + using ImageInfo imageInfo = SerializationHelper.DeserializeImageInfo(objectProvider.GetData()); + ImageInfo? replacementObject = DebuggerHelper.DebugImage(imageInfo, objectProvider.IsObjectReplaceable); + if (replacementObject == null) + return; + + using var ms = new MemoryStream(); + SerializationHelper.SerializeReplacementImageInfo(replacementObject, ms); + objectProvider.ReplaceData(ms); } #endregion diff --git a/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/MetafileDebuggerVisualizer.cs b/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/MetafileDebuggerVisualizer.cs index 964c3bc..509b761 100644 --- a/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/MetafileDebuggerVisualizer.cs +++ b/KGySoft.Drawing.DebuggerVisualizers/_DebuggerVisualizers/MetafileDebuggerVisualizer.cs @@ -17,8 +17,8 @@ #region Usings using System.Diagnostics.CodeAnalysis; +using System.IO; -using KGySoft.Drawing.DebuggerVisualizers.Model; using KGySoft.Drawing.DebuggerVisualizers.Serialization; using KGySoft.Drawing.ImagingTools.Model; @@ -41,12 +41,14 @@ internal sealed class MetafileDebuggerVisualizer : DialogDebuggerVisualizer /// The object provider. protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider) { - using (ImageInfo imageInfo = SerializationHelper.DeserializeImageInfo(objectProvider.GetData())) - { - ImageReference replacementObject = DebuggerHelper.DebugMetafile(imageInfo, objectProvider.IsObjectReplaceable); - if (objectProvider.IsObjectReplaceable && replacementObject != null) - objectProvider.ReplaceObject(replacementObject); - } + using ImageInfo imageInfo = SerializationHelper.DeserializeImageInfo(objectProvider.GetData()); + ImageInfo? replacementObject = DebuggerHelper.DebugMetafile(imageInfo, objectProvider.IsObjectReplaceable); + if (replacementObject == null) + return; + + using var ms = new MemoryStream(); + SerializationHelper.SerializeReplacementImageInfo(replacementObject, ms); + objectProvider.ReplaceData(ms); } #endregion diff --git a/KGySoft.Drawing.ImagingTools/App.config b/KGySoft.Drawing.ImagingTools/App.config new file mode 100644 index 0000000..c76c674 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/GlobalSuppressions.cs b/KGySoft.Drawing.ImagingTools/GlobalSuppressions.cs index 4c4658c..df6aa49 100644 --- a/KGySoft.Drawing.ImagingTools/GlobalSuppressions.cs +++ b/KGySoft.Drawing.ImagingTools/GlobalSuppressions.cs @@ -7,3 +7,6 @@ [assembly: SuppressMessage("Style", "IDE0063:Use simple 'using' statement", Justification = "Decided individually")] [assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "Decided individually")] +[assembly: SuppressMessage("Style", "IDE0057:Use range operator", Justification = "Cannot be used because it is not supported in every targeted platform")] +[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'", Justification = "Decided individually")] +[assembly: SuppressMessage("Style", "IDE0042:Deconstruct variable declaration", Justification = "Decided individually")] diff --git a/KGySoft.Drawing.ImagingTools/KGySoft.Drawing.ImagingTools.Messages.resx b/KGySoft.Drawing.ImagingTools/KGySoft.Drawing.ImagingTools.Messages.resx index d2abd05..0993f67 100644 --- a/KGySoft.Drawing.ImagingTools/KGySoft.Drawing.ImagingTools.Messages.resx +++ b/KGySoft.Drawing.ImagingTools/KGySoft.Drawing.ImagingTools.Messages.resx @@ -120,8 +120,8 @@ Internal Error: {0} - - KGy SOFT Imaging Tools v{0} + + KGy SOFT Imaging Tools v{0} [{1}{2}] – {3} No Image @@ -138,6 +138,18 @@ Confirmation + + Open Image + + + Save Image As + + + Select Color + + + Browse For Folder + ; @@ -147,8 +159,8 @@ Size: {0}x{1} - - Palette Count: {0} + + Palette Color Count: {0} Visible Clip Bounds: {{X = {0}, Y = {1}, Size = {2}x{3}}} @@ -159,6 +171,9 @@ Color: {0} + + Edit Resources – {0} + files @@ -195,9 +210,27 @@ B: {0} + + Alpha color component (transparency) + + + Red color component + + + Green color component + + + Blue color component + + + Unsupported Language ({0}) + Color Count: {0:N0} + + Downloading... + Toggles whether the animation is handled as a single image. • When checked, animation will play and saving as GIF saves the whole animation. @@ -214,11 +247,10 @@ • When not checked, saving saves always the selected page only. - Toggles whether the metafile is displayed with anti aliasing enabled. + Smoothing Edges (Alt+S) - Toggles whether an enlarged image is rendered with smoothing interpolation. -A shrunk image is always displayed with smoothing. + Smoothing Resized Image (Alt+S) Auto @@ -226,9 +258,6 @@ A shrunk image is always displayed with smoothing. Counting colors... - - Operation has been canceled. - {{Width={0}, Height={1}}} @@ -248,7 +277,7 @@ Pixel Format: {3} Unknown format: {0} - Palette count: {0} + Palette color count: {0} Images: {0} @@ -324,6 +353,12 @@ Brightness: {5:F0}% The current installation is being executed, which cannot be removed + + Resource format string is invalid. + + + One or more placeholders are missing from the translated resource format string. + Could not save image due to an error: {0} @@ -379,6 +414,24 @@ Either select at least '{1}' or reduce the number of colors to {2}. Value must be greater than {0} + + Failed to save settings: {0} + + + Failed to regenerate resource file {0}: {1} + + + Failed to save resource file {0}: {1} + + + Failed to access online resources: {0} + + + Failed to download resource file {0}: {1} + + + Index '{0}' is invalid in the translated resource format string. + Could not create directory {0}: {1} @@ -405,11 +458,19 @@ then colors will be quantized to the 32 bit ARGB color space during the conversi The selected quantizer uses more colors than the selected pixel format '{0}' supports. Either select at least {1} pixel format or use another quantizer that uses no more colors than '{0}' can represent; otherwise, the result might not be optimal even with dithering. + + + {0} file(s) have been downloaded. + +The culture of one or more downloaded localizations is not supported on this platform. Those languages will not appear among the selectable languages. The selected quantizer supports partial transparency, which is not supported by ditherers, so partial transparent pixels will be blended with back color. + + This language is not supported on this platform or by the executing framework. + Are you sure you want to overwrite this installation? @@ -417,6 +478,17 @@ so partial transparent pixels will be blended with back color. The extension of the provided filename '{0}' does not match to the selected format ({1}). Are you sure you want to save the file with the provided extension? + + + Failed to read resource file {0}: {1} + +Do you want to try to regenerate it? The current file will be deleted. + + + The following files already exist: +{0} + +Do you want to overwrite them? Are you sure you want to remove this installation? @@ -427,6 +499,10 @@ Are you sure you want to save the file with the provided extension? There are unsaved modifications. Are sure to discard the changes? + + One or more selected items are for a different Imaging Tools version. +Are you sure you want to continue? + The palette contains no colors. Click OK to exit. @@ -444,6 +520,9 @@ It is possible that is has no effect. Without selecting a quantizer possible alpha pixels of the source image are blended with black. By selecting a quantizer you can specify a different back color. + + This item is for a different Imaging Tools version. + A quantizer has been auto selected for pixel format '{0}' using default settings. Use a specific instance to adjust parameters. @@ -465,6 +544,26 @@ Dithering may help to preserve more details. The selected pixel format represents a narrower set of colors than the original '{0}'. Dithering may help to preserve more details. + + + {0} file(s) have been downloaded. + + + About KGy SOFT Imaging Tools + +Version: v{0} +Author: György Kőszeg +Target Platform: {1} + +You are now using the compiled English resources. +Copyright © {2} KGy SOFT. All rights reserved. + + + Just a regular ToolStrip menu, eh? + +Now imagine every combination of target platforms (from .NET Framework 3.5 to .NET 5), operating systems (from Windows XP to Linux/Mono), different DPI settings, enabled/disabled visual styles and high contrast mode, right-to-left layout... + +Harmonizing visual elements for all possible environments is never a trivial task, but OMG, the ToolStrip wasn't a cakewalk. Would you believe that each and every combination had at least one rendering issue? My custom-zoomable ImageViewer control with the asynchronously generated resized interpolated images on multiple cores was an easy-peasy compared to that... N/A (KGySoft.Drawing.DebuggerVisualizers.dll is missing) @@ -482,25 +581,37 @@ Dithering may help to preserve more details. Debugger version: {0} - Debugger version: {0} - Runtime: {1} + Debugger version: {0} – Runtime: {1} - Debugger version: {0} - Target: {1} + Debugger version: {0} – Target: {1} Installed: {0} - Installed: {0} - Runtime: {1} + Installed: {0} – Runtime: {1} - Installed: {0} - Target: {1} + Installed: {0} – Target: {1} + + + Auto Zoom (Alt+Z) / Zoom Options + + + Auto Zoom - - Auto Zoom (Alt+Z) + + Zoom In + + + Zoom Out + + + Actual Size - Smooth Zooming + Smoothing Resized Image (Alt+S) Open... (Ctrl+O) @@ -554,20 +665,20 @@ Dithering may help to preserve more details. Next Image (Shift+Right) + Application Settings + + Manage Debugger Visualizer Installations... - + + Language Settings... + + Click to hide Back Color - - Open Image - - - Save Image As... - Default @@ -595,9 +706,6 @@ Dithering may help to preserve more details. Installable Version: - - - - Identified Visual Studio Versions: @@ -613,9 +721,6 @@ Dithering may help to preserve more details. Status: - - - - &Path: @@ -643,6 +748,9 @@ Dithering may help to preserve more details. &Cancel + + &Apply + &Close @@ -727,4 +835,165 @@ Dithering may help to preserve more details. Show Original + + Language Settings + + + Allow using resource files + + + When checked, allows using resources from .resx files directly + + + Use Operating System Language + + + When checked, uses the language of the operating system. The possibly non-existing resources will be automatically generated. + + + Show Languages Only With Existing Resources + + + When checked, enlists languages only with existing resource files. Uncheck to allow generating resources for any language. +Newly generated resources will contain English texts prefixed with "[T]". You can then edit the resources to translate these texts. + + + Display Language + + + &Edit Resources... + + + &Download Resources... + + + Resource File + + + Resource Entries + + + &Filter: + + + Filtering ignores case, accents and character width. +Tip: use '[T]' to filter untranslated texts. + + + Resource Key + + + Original Text + + + Translated Text + + + Original Text + + + Translated Text + + + Downloading Resources + + + Selected + + + Language + + + Author + + + Imaging Tools Version + + + Description + + + Download + + + About + + + Visit Web Site... + + + Visit GitHub Project Site... + + + Visit VisualStudio Marketplace... + + + Submit Language Resource Files... + + + About... + + + Oh boy, this was tough... + + + &OK + + + &Cancel + + + &Yes + + + &No + + + &Basic Colors: + + + Custo&m Colors: + + + &Define Custom Colors >> + + + Result + + + color + + + Hu&e: + + + &Sat: + + + &Lum: + + + &Red: + + + &Green: + + + &Blue: + + + &Add to Custom Colors + + + Select the target folder of the installation. + + + &Make New Folder + + + Folder: + + + Select Folder + \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/KGySoft.Drawing.ImagingTools.csproj b/KGySoft.Drawing.ImagingTools/KGySoft.Drawing.ImagingTools.csproj index 19d95ba..bf499e4 100644 --- a/KGySoft.Drawing.ImagingTools/KGySoft.Drawing.ImagingTools.csproj +++ b/KGySoft.Drawing.ImagingTools/KGySoft.Drawing.ImagingTools.csproj @@ -1,15 +1,13 @@  - net35;net40;net45;netcoreapp3.0 + net35;net40;net45;net5.0-windows false KGySoft.Drawing.ImagingTools bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml true ..\KGySoft.snk - - latest false LICENSE @@ -18,35 +16,56 @@ true WinExe app.manifest + enable + + + $(NoWarn);NETSDK1138 + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + ResXFileCodeGenerator + Resources.Designer.cs + - PublicResXFileCodeGenerator + True + True + Resources.resx - - + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + True + Settings.settings + + + KGySoft.Drawing.ImagingTools.Messages.resources - - AdjustColorsFormBase.resx - diff --git a/KGySoft.Drawing.ImagingTools/Model/AsyncTaskBase.cs b/KGySoft.Drawing.ImagingTools/Model/AsyncTaskBase.cs index 587eee0..d243705 100644 --- a/KGySoft.Drawing.ImagingTools/Model/AsyncTaskBase.cs +++ b/KGySoft.Drawing.ImagingTools/Model/AsyncTaskBase.cs @@ -28,70 +28,6 @@ namespace KGySoft.Drawing.ImagingTools.Model /// internal abstract class AsyncTaskBase : IDisposable { - #region Nested classes -#if NET35 - - private sealed class ManualResetEventSlim : IDisposable - { - #region Fields - - private readonly object syncRoot = new object(); - - private bool isSet; - private bool isDisposed; - - #endregion - - #region Methods - - #region Public Methods - - public void Dispose() - { - lock (syncRoot) - { - if (isDisposed) - return; - isDisposed = true; - isSet = true; - Monitor.PulseAll(syncRoot); - } - } - - #endregion - - #region Internal Methods - - internal void Set() - { - lock (syncRoot) - { - if (isDisposed) - return; - isSet = true; - Monitor.PulseAll(syncRoot); - } - } - - internal void Wait() - { - lock (syncRoot) - { - if (isDisposed) - return; - while (!isSet) - Monitor.Wait(syncRoot); - } - } - - #endregion - - #endregion - } - -#endif - #endregion - #region Fields #region Internal Fields diff --git a/KGySoft.Drawing.ImagingTools/Model/BitmapDataInfo.cs b/KGySoft.Drawing.ImagingTools/Model/BitmapDataInfo.cs index 19ef731..d3049d3 100644 --- a/KGySoft.Drawing.ImagingTools/Model/BitmapDataInfo.cs +++ b/KGySoft.Drawing.ImagingTools/Model/BitmapDataInfo.cs @@ -35,12 +35,12 @@ public sealed class BitmapDataInfo : IDisposable /// /// Gets or sets a that represents the content of the . /// - public Bitmap BackingImage { get; set; } + public Bitmap? BackingImage { get; set; } /// /// Gets or sets the bitmap data. /// - public BitmapData BitmapData { get; set; } + public BitmapData? BitmapData { get; set; } #endregion diff --git a/KGySoft.Drawing.ImagingTools/Model/CustomPropertiesObject.cs b/KGySoft.Drawing.ImagingTools/Model/CustomPropertiesObject.cs index 2b42017..374fc4a 100644 --- a/KGySoft.Drawing.ImagingTools/Model/CustomPropertiesObject.cs +++ b/KGySoft.Drawing.ImagingTools/Model/CustomPropertiesObject.cs @@ -58,7 +58,7 @@ internal CustomPropertiesObject(CustomPropertiesObject other, IEnumerable true; - protected override bool CanSetProperty(string propertyName, object value) => true; + protected override bool CanSetProperty(string propertyName, object? value) => true; #endregion @@ -66,21 +66,21 @@ internal CustomPropertiesObject(CustomPropertiesObject other, IEnumerable TypeDescriptor.GetAttributes(this, true); string ICustomTypeDescriptor.GetClassName() => TypeDescriptor.GetClassName(this, true); - string ICustomTypeDescriptor.GetComponentName() => TypeDescriptor.GetComponentName(this, true); + string? ICustomTypeDescriptor.GetComponentName() => TypeDescriptor.GetComponentName(this, true); TypeConverter ICustomTypeDescriptor.GetConverter() => TypeDescriptor.GetConverter(this, true); - EventDescriptor ICustomTypeDescriptor.GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true); - PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true); - object ICustomTypeDescriptor.GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true); + EventDescriptor? ICustomTypeDescriptor.GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true); + PropertyDescriptor? ICustomTypeDescriptor.GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true); + object? ICustomTypeDescriptor.GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true); EventDescriptorCollection ICustomTypeDescriptor.GetEvents() => TypeDescriptor.GetEvents(this, true); EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes) => TypeDescriptor.GetEvents(this, attributes, true); object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd) => this; PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() => propertyDescriptors; - PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes) + PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[]? attributes) => new PropertyDescriptorCollection(propertyDescriptors.Cast().Where(p => attributes == null || attributes.Any(a => p.Attributes.Contains(a))).ToArray()); void ICustomPropertiesProvider.ResetValue(string propertyName) => ResetProperty(propertyName); - void ICustomPropertiesProvider.SetValue(string propertyName, object value) => Set(value, true, propertyName); - object ICustomPropertiesProvider.GetValue(string propertyName, object defaultValue) => Get(defaultValue, propertyName); + void ICustomPropertiesProvider.SetValue(string propertyName, object? value) => Set(value, true, propertyName); + object? ICustomPropertiesProvider.GetValue(string propertyName, object? defaultValue) => Get(defaultValue, propertyName); #endregion diff --git a/KGySoft.Drawing.ImagingTools/Model/CustomPropertyDescriptor.cs b/KGySoft.Drawing.ImagingTools/Model/CustomPropertyDescriptor.cs index 6f05b57..4ceb647 100644 --- a/KGySoft.Drawing.ImagingTools/Model/CustomPropertyDescriptor.cs +++ b/KGySoft.Drawing.ImagingTools/Model/CustomPropertyDescriptor.cs @@ -43,13 +43,13 @@ private class PickValueConverter : TypeConverter #region Fields private readonly TypeConverter wrappedConverter; - private readonly object[] allowedValues; + private readonly object?[] allowedValues; #endregion #region Constructors - internal PickValueConverter(TypeConverter converter, object[] allowedValues) + internal PickValueConverter(TypeConverter converter, object?[] allowedValues) { wrappedConverter = converter; this.allowedValues = allowedValues; @@ -61,8 +61,8 @@ internal PickValueConverter(TypeConverter converter, object[] allowedValues) public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) => wrappedConverter.CanConvertFrom(context, sourceType); public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) => wrappedConverter.CanConvertTo(context, destinationType); - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) => wrappedConverter.ConvertFrom(context, culture, value); - public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) => wrappedConverter.ConvertTo(context, culture, value, destinationType); + public override object? ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) => wrappedConverter.ConvertFrom(context, culture, value); + public override object? ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) => wrappedConverter.ConvertTo(context, culture, value, destinationType); public override bool GetStandardValuesSupported(ITypeDescriptorContext context) => true; public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) => true; public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) => new StandardValuesCollection(allowedValues); @@ -78,9 +78,9 @@ internal PickValueConverter(TypeConverter converter, object[] allowedValues) private readonly HashSet attributes; - private AttributeCollection cachedAttributes; - private PickValueConverter converter; - private Type editor; + private AttributeCollection? cachedAttributes; + private PickValueConverter? converter; + private Type? editor; #endregion @@ -99,7 +99,7 @@ public override AttributeCollection Attributes } } - public override TypeConverter Converter => AllowedValues == null ? base.Converter : (converter ??= new PickValueConverter(base.Converter, AllowedValues)); + public override TypeConverter? Converter => AllowedValues == null || base.Converter == null ? base.Converter : converter ??= new PickValueConverter(base.Converter, AllowedValues); public override Type ComponentType => typeof(ICustomPropertiesProvider); public override bool IsReadOnly => false; public override Type PropertyType { get; } @@ -108,27 +108,29 @@ public override AttributeCollection Attributes #region Internal Properties - internal new string Category + internal new string? Category { get => base.Category; set { cachedAttributes = null; - attributes.Add(new CategoryAttribute(value)); + if (value != null) + attributes.Add(new CategoryAttribute(value)); } } - internal new string Description + internal new string? Description { get => base.Description; set { cachedAttributes = null; - attributes.Add(new DescriptionAttribute(value)); + if (value != null) + attributes.Add(new DescriptionAttribute(value)); } } - internal Type UITypeEditor + internal Type? UITypeEditor { get => editor ??= GetEditor(typeof(UITypeEditor))?.GetType(); set @@ -137,13 +139,14 @@ internal Type UITypeEditor return; cachedAttributes = null; editor = value; - attributes.Add(new EditorAttribute(value, typeof(UITypeEditor))); + if (value != null) + attributes.Add(new EditorAttribute(value, typeof(UITypeEditor))); } } - internal object DefaultValue { get; set; } - internal object[] AllowedValues { get; set; } - internal Func AdjustValue { get; set; } + internal object? DefaultValue { get; set; } + internal object?[]? AllowedValues { get; set; } + internal Func? AdjustValue { get; set; } #endregion @@ -174,16 +177,16 @@ public CustomPropertyDescriptor(CustomPropertyDescriptor other) : base(other) public override bool CanResetValue(object component) => ShouldSerializeValue(component); public override void ResetValue(object component) => ((ICustomPropertiesProvider)component).ResetValue(Name); public override void SetValue(object component, object value) => ((ICustomPropertiesProvider)component).SetValue(Name, DoAdjustValue(value)); - public override object GetValue(object component) => DoAdjustValue(((ICustomPropertiesProvider)component).GetValue(Name, DefaultValue)); + public override object? GetValue(object component) => DoAdjustValue(((ICustomPropertiesProvider)component).GetValue(Name, DefaultValue)); public override string ToString() => $"{Name}: {PropertyType}"; #endregion #region Private Methods - private object DoAdjustValue(object value) + private object? DoAdjustValue(object? value) => AdjustValue != null ? AdjustValue.Invoke(value) - : !AllowedValues.IsNullOrEmpty() && !value.In(AllowedValues) ? AllowedValues[0] + : !AllowedValues.IsNullOrEmpty() && !value.In(AllowedValues) ? AllowedValues![0] : value == null && PropertyType.IsValueType ? DefaultValue ?? Activator.CreateInstance(PropertyType) : value; diff --git a/KGySoft.Drawing.ImagingTools/Model/DesignDependencies.cs b/KGySoft.Drawing.ImagingTools/Model/DesignDependencies.cs index c35c00f..15736e6 100644 --- a/KGySoft.Drawing.ImagingTools/Model/DesignDependencies.cs +++ b/KGySoft.Drawing.ImagingTools/Model/DesignDependencies.cs @@ -26,8 +26,8 @@ internal static class DesignDependencies { #region Properties - internal static Type QuantizerThresholdEditor { get; set; } - internal static Type DithererStrengthEditor { get; set; } + internal static Type? QuantizerThresholdEditor { get; set; } + internal static Type? DithererStrengthEditor { get; set; } #endregion } diff --git a/KGySoft.Drawing.ImagingTools/Model/DithererDescriptor.cs b/KGySoft.Drawing.ImagingTools/Model/DithererDescriptor.cs index 5aedea3..ba0795d 100644 --- a/KGySoft.Drawing.ImagingTools/Model/DithererDescriptor.cs +++ b/KGySoft.Drawing.ImagingTools/Model/DithererDescriptor.cs @@ -18,14 +18,12 @@ using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; -using System.Linq; using System.Reflection; +#if NETFRAMEWORK using KGySoft.CoreLibraries; +#endif using KGySoft.Drawing.Imaging; -using KGySoft.Reflection; #endregion @@ -67,7 +65,7 @@ internal sealed class DithererDescriptor #region Constructors - internal DithererDescriptor(Type type, string propertyName) : this(type.GetProperty(propertyName)) + internal DithererDescriptor(Type type, string propertyName) : this(type.GetProperty(propertyName)!) { } @@ -83,11 +81,11 @@ internal DithererDescriptor(MemberInfo member) break; case PropertyInfo property: if (property.DeclaringType == typeof(OrderedDitherer)) - AddMethodChain(chain, parameters, typeof(OrderedDitherer).GetMethod(nameof(OrderedDitherer.ConfigureStrength))); + AddMethodChain(chain, parameters, typeof(OrderedDitherer).GetMethod(nameof(OrderedDitherer.ConfigureStrength))!); else if (property.DeclaringType == typeof(ErrorDiffusionDitherer)) { - AddMethodChain(chain, parameters, typeof(ErrorDiffusionDitherer).GetMethod(nameof(ErrorDiffusionDitherer.ConfigureProcessingDirection))); - AddMethodChain(chain, parameters, typeof(ErrorDiffusionDitherer).GetMethod(nameof(ErrorDiffusionDitherer.ConfigureErrorDiffusionMode))); + AddMethodChain(chain, parameters, typeof(ErrorDiffusionDitherer).GetMethod(nameof(ErrorDiffusionDitherer.ConfigureProcessingDirection))!); + AddMethodChain(chain, parameters, typeof(ErrorDiffusionDitherer).GetMethod(nameof(ErrorDiffusionDitherer.ConfigureErrorDiffusionMode))!); } break; @@ -108,7 +106,7 @@ internal DithererDescriptor(MemberInfo member) private static void AddParameters(List descriptors, ParameterInfo[] reflectedParameters) { foreach (ParameterInfo pi in reflectedParameters) - descriptors.Add(parametersMapping.GetValueOrDefault(pi.Name) ?? throw new InvalidOperationException(Res.InternalError($"Unexpected parameter: {pi.Name}"))); + descriptors.Add(parametersMapping.GetValueOrDefault(pi.Name!) ?? throw new InvalidOperationException(Res.InternalError($"Unexpected parameter: {pi.Name}"))); } private static void AddMethodChain(List chain, List parameters, MethodInfo method) @@ -124,20 +122,20 @@ private static void AddMethodChain(List chain, List InvokeChain[0] is ConstructorInfo ctor - ? ctor.DeclaringType.Name - : $"{InvokeChain[0].DeclaringType.Name}.{InvokeChain[0].Name}"; + ? ctor.DeclaringType!.Name + : $"{InvokeChain[0].DeclaringType!.Name}.{InvokeChain[0].Name}"; #endregion #region Internal Methods - internal object[] EvaluateParameters(ParameterInfo[] parameters, CustomPropertiesObject values) + internal object?[] EvaluateParameters(ParameterInfo[] parameters, CustomPropertiesObject values) { - var result = new object[parameters.Length]; + var result = new object?[parameters.Length]; for (int i = 0; i < result.Length; i++) { - string paramName = parameters[i].Name; - result[i] = Parameters.Find(d => d.Name == paramName).GetValue(values); + string paramName = parameters[i].Name!; + result[i] = Parameters.Find(d => d.Name == paramName)!.GetValue(values); } return result; diff --git a/KGySoft.Drawing.ImagingTools/Model/GraphicsInfo.cs b/KGySoft.Drawing.ImagingTools/Model/GraphicsInfo.cs index 1577a96..730d815 100644 --- a/KGySoft.Drawing.ImagingTools/Model/GraphicsInfo.cs +++ b/KGySoft.Drawing.ImagingTools/Model/GraphicsInfo.cs @@ -19,7 +19,6 @@ using System; using System.Drawing; using System.Drawing.Drawing2D; -using System.Text; #endregion @@ -36,12 +35,12 @@ public sealed class GraphicsInfo : IDisposable /// /// Gets or sets a that represents the content of the corresponding . /// - public Bitmap GraphicsImage { get; set; } + public Bitmap? GraphicsImage { get; set; } /// /// Gets or sets the transformation of the corresponding . /// - public Matrix Transform { get; set; } + public Matrix? Transform { get; set; } /// /// Gets or sets the original visible clip bounds in pixels, without applying any transformation. diff --git a/KGySoft.Drawing.ImagingTools/Model/ImageFrameInfo.cs b/KGySoft.Drawing.ImagingTools/Model/ImageFrameInfo.cs index d818bc6..869877f 100644 --- a/KGySoft.Drawing.ImagingTools/Model/ImageFrameInfo.cs +++ b/KGySoft.Drawing.ImagingTools/Model/ImageFrameInfo.cs @@ -41,11 +41,13 @@ public sealed class ImageFrameInfo : ImageInfoBase #region Constructors + #region Public Constructors + /// /// Initializes a new instance of the class from a . /// /// The bitmap that contains the image of the current frame. - public ImageFrameInfo(Bitmap bitmap) + public ImageFrameInfo(Bitmap? bitmap) { Image = bitmap; InitMeta(bitmap); @@ -53,6 +55,17 @@ public ImageFrameInfo(Bitmap bitmap) #endregion + #region Internal Constructors + + internal ImageFrameInfo(ImageFrameInfo other) : base(other) + { + Duration = other.Duration; + } + + #endregion + + #endregion + #region Methods /// diff --git a/KGySoft.Drawing.ImagingTools/Model/ImageInfo.cs b/KGySoft.Drawing.ImagingTools/Model/ImageInfo.cs index 7d3321a..73f3b18 100644 --- a/KGySoft.Drawing.ImagingTools/Model/ImageInfo.cs +++ b/KGySoft.Drawing.ImagingTools/Model/ImageInfo.cs @@ -50,19 +50,17 @@ public sealed class ImageInfo : ImageInfoBase /// /// Gets or sets an instance associated with this instance. /// - public Icon Icon { get => Get(); set => Set(value); } + public Icon? Icon { get => Get(); set => Set(value); } /// /// Gets or sets a file name associated with this instance. /// - public string FileName { get => Get(); set => Set(value); } + public string? FileName { get => Get(); set => Set(value); } /// /// If this instance represents a multi-frame image, then gets or sets the frames belong to the image. /// - [SuppressMessage("Performance", "CA1819:Properties should not return arrays", - Justification = "This is a descriptor class. It is expected that this property or the elements are set. The IsValid property gets whether this instance is valid, including frames.")] - public ImageFrameInfo[] Frames { get => Get(); set => Set(value); } + public ImageFrameInfo[]? Frames { get => Get(); set => Set(value); } /// /// Gets whether this instance represents a multi-frame image and has frames. @@ -99,7 +97,7 @@ public ImageInfo(ImageInfoType imageType) /// Initializes a new instance of the class from an . /// /// The image to be used for the initialization. - public ImageInfo(Image image) + public ImageInfo(Image? image) { InitFromImage(image); SetModified(false); @@ -109,7 +107,7 @@ public ImageInfo(Image image) /// Initializes a new instance of the class from an . /// /// The icon to be used for the initialization. - public ImageInfo(Icon icon) + public ImageInfo(Icon? icon) { InitFromIcon(icon); SetModified(false); @@ -128,9 +126,9 @@ public ImageInfo(Icon icon) /// An that represents the possible compound image of this instance. /// When a new image is created, then the return value will be the new value of the property as well. /// The object is in an invalid state (the property returns ). - public Image GetCreateImage() + public Image? GetCreateImage() { - Image image = Image; + Image? image = Image; if (image != null) return image; if (Type == ImageInfoType.None || !(Type == ImageInfoType.Icon || HasFrames)) @@ -145,9 +143,9 @@ public Image GetCreateImage() /// An that represents the possible icon of this instance. /// When a new icon is created, then the return value will be the new value of the property as well. /// The object is in an invalid state (the property returns ). - public Icon GetCreateIcon() + public Icon? GetCreateIcon() { - Icon icon = Icon; + Icon? icon = Icon; if (icon != null) return icon; if (Type == ImageInfoType.None) @@ -185,23 +183,23 @@ protected override ValidationResultsCollection DoValidation() return new ValidationResultsCollection(); ValidationResultsCollection result = base.DoValidation(); - ImageFrameInfo[] frames = Frames; + ImageFrameInfo[]? frames = Frames; if (Type.In(ImageInfoType.Pages, ImageInfoType.Animation, ImageInfoType.MultiRes)) { if (frames.IsNullOrEmpty()) result.AddError(nameof(Frames), PublicResources.CollectionEmpty); - else if (frames.Any(f => f.Image == null)) + else if (frames!.Any(f => f.Image == null)) result.AddError(nameof(Frames), PublicResources.ArgumentContainsNull); } bool hasFrames = HasFrames; if (Type == ImageInfoType.Icon) { - if (Icon == null && !hasFrames) - result.AddError(nameof(Icon), PublicResources.ArgumentNull); + if (Icon == null && Image == null && !hasFrames) + result.AddError(nameof(Icon), PublicResources.PropertyNull(nameof(Icon))); } else if (Image == null && !hasFrames) - result.AddError(nameof(Image), PublicResources.ArgumentNull); + result.AddError(nameof(Image), PublicResources.PropertyNull(nameof(Image))); return result; } @@ -227,14 +225,15 @@ protected override void Dispose(bool disposing) [SuppressMessage("ReSharper", "PossibleUnintendedReferenceComparison", Justification = "The dimension variable is compared with the references we set earlier")] - private void InitFromImage(Image image) + [SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "ReSharper issue")] + private void InitFromImage(Image? image) { if (image == null) return; InitMeta(image); Image = image; - Bitmap bmp = image as Bitmap; + Bitmap? bmp = image as Bitmap; ImageFrameInfo[] frames; // icon @@ -260,7 +259,7 @@ private void InitFromImage(Image image) } // other image: check if it has multiple frames - FrameDimension dimension = null; + FrameDimension? dimension = null; Guid[] dimensions = image.FrameDimensionsList; if (dimensions.Length > 0) { @@ -281,7 +280,7 @@ private void InitFromImage(Image image) } // multiple frames - byte[] times = null; + byte[]? times = null; Bitmap bitmap = (Bitmap)image; Type = dimension == FrameDimension.Time ? ImageInfoType.Animation : dimension == FrameDimension.Page ? ImageInfoType.Pages @@ -289,7 +288,7 @@ private void InitFromImage(Image image) // in case of animation there is a compound image if (dimension == FrameDimension.Time) - times = image.GetPropertyItem(0x5100).Value; + times = image.GetPropertyItem(0x5100)?.Value; frames = new ImageFrameInfo[frameCount]; Frames = frames; @@ -308,7 +307,7 @@ private void InitFromImage(Image image) image.SelectActiveFrame(dimension, 0); } - private void InitFromIcon(Icon icon) + private void InitFromIcon(Icon? icon) { #region Local Methods @@ -345,7 +344,7 @@ static void InitIconMeta(IconInfo iconInfo, ImageInfoBase imageInfo) return; } - Bitmap[] iconImages = icon.ExtractBitmaps(); + Bitmap?[] iconImages = icon.ExtractBitmaps(); Debug.Assert(iconInfo.Length == iconImages.Length); var frames = new ImageFrameInfo[iconInfo.Length]; for (int i = 0; i < frames.Length; i++) @@ -381,7 +380,7 @@ private Image GenerateImage() { case ImageInfoType.Pages: var ms = new MemoryStream(); - Frames.Select(f => f.Image).SaveAsMultipageTiff(ms); + Frames!.Select(f => f.Image!).SaveAsMultipageTiff(ms); ms.Position = 0; return new Bitmap(ms); @@ -389,12 +388,12 @@ private Image GenerateImage() case ImageInfoType.Icon: try { - return GetCreateIcon().ToMultiResBitmap(); + return GetCreateIcon()!.ToMultiResBitmap(); } catch (ArgumentException) { // In Windows XP it can happen that multi-res bitmap throws an exception even if PNG images are uncompressed - return GetCreateIcon().ExtractNearestBitmap(new Size(UInt16.MaxValue, UInt16.MaxValue), PixelFormat.Format32bppArgb); + return GetCreateIcon()!.ExtractNearestBitmap(new Size(UInt16.MaxValue, UInt16.MaxValue), PixelFormat.Format32bppArgb); } case ImageInfoType.Animation: @@ -415,12 +414,12 @@ private Icon GenerateIcon() throw new InvalidOperationException($"{error.PropertyName}: {error.Message}"); } - return !HasFrames ? Image.ToIcon() : Icons.Combine(Frames.Select(f => (Bitmap)f.Image)); + return !HasFrames ? Image!.ToIcon() : Icons.Combine(Frames!.Select(f => (Bitmap)f.Image!)); } private void FreeFrames() { - ImageFrameInfo[] frames = Frames; + ImageFrameInfo[]? frames = Frames; if (frames == null) return; foreach (ImageFrameInfo frame in frames) diff --git a/KGySoft.Drawing.ImagingTools/Model/ImageInfoBase.cs b/KGySoft.Drawing.ImagingTools/Model/ImageInfoBase.cs index e9dd2fe..d356f59 100644 --- a/KGySoft.Drawing.ImagingTools/Model/ImageInfoBase.cs +++ b/KGySoft.Drawing.ImagingTools/Model/ImageInfoBase.cs @@ -43,7 +43,7 @@ public abstract class ImageInfoBase : ValidatingObjectBase /// Gets or sets the image to be displayed or saved /// when debugging the corresponding or instance. /// - public Image Image { get => Get(); set => Set(value); } + public Image? Image { get => Get(); set => Set(value); } /// /// Gets or sets the horizontal resolution to be displayed @@ -73,7 +73,7 @@ public abstract class ImageInfoBase : ValidatingObjectBase /// Gets or sets the palette color entries to be displayed /// when debugging the corresponding or instance. /// - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a DTO class")] + [AllowNull] public Color[] Palette { get => Get(Reflector.EmptyArray()); set => Set(value ?? Reflector.EmptyArray()); } /// @@ -92,6 +92,31 @@ public abstract class ImageInfoBase : ValidatingObjectBase #endregion + #region Construction and Destruction + + #region Constructors + + private protected ImageInfoBase() + { + } + + private protected ImageInfoBase(ImageInfoBase other) + { + if (other == null) + throw new ArgumentNullException(nameof(other), PublicResources.ArgumentNull); + + if (other.Image is Image image) + Image = (Image)image.Clone(); + if (other.Palette.Length > 0) + Palette = (Color[])Palette.Clone(); + HorizontalRes = other.HorizontalRes; + VerticalRes = other.VerticalRes; + PixelFormat = other.PixelFormat; + RawFormat = other.RawFormat; + } + + #endregion + #region Destructor @@ -100,6 +125,8 @@ public abstract class ImageInfoBase : ValidatingObjectBase #endregion + #endregion + #region Methods #region Protected Methods @@ -129,7 +156,7 @@ protected override void Dispose(bool disposing) #region Private Protected Methods - private protected void InitMeta(Image image) + private protected void InitMeta(Image? image) { if (image == null) return; diff --git a/KGySoft.Drawing.ImagingTools/Model/InstallationInfo.cs b/KGySoft.Drawing.ImagingTools/Model/InstallationInfo.cs index fbfab06..0ee313d 100644 --- a/KGySoft.Drawing.ImagingTools/Model/InstallationInfo.cs +++ b/KGySoft.Drawing.ImagingTools/Model/InstallationInfo.cs @@ -18,13 +18,13 @@ using System; using System.Diagnostics; +#if NETFRAMEWORK using System.Diagnostics.CodeAnalysis; +#endif using System.IO; using System.Reflection; -using System.Runtime.Versioning; - -#if NETFRAMEWORK -using KGySoft.CoreLibraries; +#if !NET35 +using System.Runtime.Versioning; #endif #if NETCOREAPP using System.Runtime.Loader; @@ -33,6 +33,10 @@ using System.Security.Policy; #endif +#if NETFRAMEWORK +using KGySoft.CoreLibraries; +#endif + #endregion namespace KGySoft.Drawing.ImagingTools.Model @@ -98,14 +102,14 @@ internal SandboxContext(string path) : base(nameof(SandboxContext), isCollectibl #region Methods - protected override Assembly Load(AssemblyName name) + protected override Assembly? Load(AssemblyName name) { // ensuring that dependencies of the main assembly are also loaded into this context - string assemblyPath = resolver.ResolveAssemblyToPath(name); + string? assemblyPath = resolver.ResolveAssemblyToPath(name); if (assemblyPath == null) return null; - using var fs = File.OpenRead(assemblyPath); + using FileStream fs = File.OpenRead(assemblyPath); return LoadFromStream(fs); } @@ -131,26 +135,32 @@ protected override Assembly Load(AssemblyName name) /// Gets the version of an identified debugger visualizer installation. /// Can return  even if is , if the installed version could not be determined. /// - public Version Version { get; private set; } + public Version? Version { get; private set; } /// /// Gets the runtime version of an identified debugger visualizer installation. /// Can return  even if is , if the runtime version could not be determined. /// - public string RuntimeVersion { get; private set; } + public string? RuntimeVersion { get; private set; } /// /// Gets the target framework of an identified debugger visualizer installation. /// Can return  even if is , if the assembly does no contain target framework information /// (typically .NET 3.5 version). /// - public string TargetFramework { get; private set; } + // ReSharper disable once UnassignedGetOnlyAutoProperty + public string? TargetFramework + { + get; +#if !NET35 + private set; +#endif + } #endregion #region Constructors - [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "typeof().FullName will not be null")] internal InstallationInfo(string path) { Path = path; @@ -211,7 +221,8 @@ private void InitializeInfoByFileVersion(string path) { try { - Version = new Version(FileVersionInfo.GetVersionInfo(InstallationManager.GetDebuggerVisualizerFilePath(path)).FileVersion); + string? fileVersion = FileVersionInfo.GetVersionInfo(InstallationManager.GetDebuggerVisualizerFilePath(path)).FileVersion; + Version = fileVersion == null ? null : new Version(fileVersion); } catch (Exception e) when (!e.IsCritical()) { diff --git a/KGySoft.Drawing.ImagingTools/Model/LocalizationInfo.cs b/KGySoft.Drawing.ImagingTools/Model/LocalizationInfo.cs new file mode 100644 index 0000000..1a6e783 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/Model/LocalizationInfo.cs @@ -0,0 +1,99 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: LocalizationInfo.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.Model +{ + /// + /// Represents the metadata of localized resources. + /// + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global", Justification = "Setter accessors are needed for the deserializer")] + [SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "ReSharper issue")] + public class LocalizationInfo + { + #region Fields + + private Version? version; + + #endregion + + #region Properties + + #region Public Properties + + /// + /// Gets or sets the culture name of the localization that matches the property of the represented culture. + /// + public string CultureName { get; set; } = default!; + + /// + /// Gets or sets the description of the localization. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the version number of the KGySoft.Drawing.ImagingTools.exe assembly this localization belongs to. + /// + public string ImagingToolsVersion { get; set; } = default!; + + /// + /// Gets or sets the author of the localization. + /// + public string? Author { get; set; } + + /// + /// Gets or sets the class libraries whose resources are covered by this localization. + /// + public LocalizableLibraries ResourceSets { get; set; } = default!; + + #endregion + + #region Internal Properties + + // Note: Not a public property because XML deserialization may fail in very special circumstances + // (eg. executing as debugger visualizer in Windows 7 with VS2015). + internal Version Version + { + get + { + if (version is null) + { + try + { + version = new Version(ImagingToolsVersion); + } + catch (Exception e) when (!e.IsCritical()) + { + version = new Version(); + } + } + + return version; + } + } + + #endregion + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/Model/QuantizerDescriptor.cs b/KGySoft.Drawing.ImagingTools/Model/QuantizerDescriptor.cs index bfe393d..ec6ffb8 100644 --- a/KGySoft.Drawing.ImagingTools/Model/QuantizerDescriptor.cs +++ b/KGySoft.Drawing.ImagingTools/Model/QuantizerDescriptor.cs @@ -57,7 +57,12 @@ internal sealed class QuantizerDescriptor }, ["pixelFormat"] = new CustomPropertyDescriptor("pixelFormat", typeof(PixelFormat)) { - AllowedValues = Enum.GetValues().Where(pf => pf.IsValidFormat()).OrderBy(pf => pf & PixelFormat.Max).Select(pf => (object)pf).ToArray(), + AllowedValues = Enum.GetValues() + .Where(pf => pf.IsValidFormat()) + // ReSharper disable once BitwiseOperatorOnEnumWithoutFlags + .OrderBy(pf => pf & PixelFormat.Max) + .Select(pf => (object)pf) + .ToArray(), }, ["directMapping"] = new CustomPropertyDescriptor("directMapping", typeof(bool)) { DefaultValue = false }, ["maxColors"] = new CustomPropertyDescriptor("maxColors", typeof(int)) @@ -65,7 +70,7 @@ internal sealed class QuantizerDescriptor DefaultValue = 256, AdjustValue = value => { - if (!(value is int i)) + if (value is not int i) return 0; return i < 0 ? 0 @@ -87,17 +92,17 @@ internal sealed class QuantizerDescriptor #region Constructors - internal QuantizerDescriptor(Type type, string methodName) : this(type.GetMethod(methodName)) + internal QuantizerDescriptor(Type type, string methodName) : this(type.GetMethod(methodName)!) { } internal QuantizerDescriptor(MethodInfo method) { - this.Method = method; + Method = method; ParameterInfo[] methodParams = method.GetParameters(); Parameters = new CustomPropertyDescriptor[methodParams.Length]; for (int i = 0; i < Parameters.Length; i++) - Parameters[i] = parametersMapping.GetValueOrDefault(methodParams[i].Name) ?? throw new InvalidOperationException(Res.InternalError($"Unexpected parameter: {methodParams[i].Name}")); + Parameters[i] = parametersMapping.GetValueOrDefault(methodParams[i].Name!) ?? throw new InvalidOperationException(Res.InternalError($"Unexpected parameter: {methodParams[i].Name}")); } #endregion @@ -106,15 +111,15 @@ internal QuantizerDescriptor(MethodInfo method) #region Public Methods - public override string ToString() => $"{Method.DeclaringType.Name}.{Method.Name}"; + public override string ToString() => $"{Method.DeclaringType!.Name}.{Method.Name}"; #endregion #region Internal Methods - internal object[] EvaluateParameters(CustomPropertiesObject values) + internal object?[] EvaluateParameters(CustomPropertiesObject values) { - var result = new object[Parameters.Length]; + var result = new object?[Parameters.Length]; for (int i = 0; i < result.Length; i++) result[i] = Parameters[i].GetValue(values); return result; diff --git a/KGySoft.Drawing.ImagingTools/Model/ResourceEntry.cs b/KGySoft.Drawing.ImagingTools/Model/ResourceEntry.cs new file mode 100644 index 0000000..9b416f4 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/Model/ResourceEntry.cs @@ -0,0 +1,329 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: ResourceEntry.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; + +using KGySoft.ComponentModel; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.Model +{ + internal class ResourceEntry : ValidatingObjectBase + { + #region Nested Types + + private enum State + { + Text, + Index, + Padding, + Format + } + + #endregion + + #region Fields + + private int? placeholderCount; + + #endregion + + #region Properties + + public string Key { get; } + public string OriginalText { get; } + public string TranslatedText { get => Get(); init => Set(value); } + + #endregion + + #region Constructors + + internal ResourceEntry(string key, string originalText, string translatedText) + { + Key = key; + OriginalText = originalText; + TranslatedText = translatedText; + } + + #endregion + + #region Methods + + #region Protected Methods + + protected override ValidationResultsCollection DoValidation() + { + #region Local Methods + + static void AddFormatError(ValidationResultsCollection validationResults) => validationResults.AddError(nameof(TranslatedText), Res.ErrorMessageResourceFormatError); + + bool HandlePlaceholder(ValidationResultsCollection validationResults, int index, ref int used) + { + if (index > placeholderCount - 1) + { + validationResults.AddError(nameof(TranslatedText), Res.ErrorMessageResourcePlaceholderIndexInvalid(index)); + return false; + } + + used |= 1 << index; + return true; + } + + #endregion + + var result = new ValidationResultsCollection(); + EnsurePlaceholderCount(); + if (placeholderCount == 0) + return result; + + Debug.Assert(placeholderCount < 32, "No resource is expected to contain more than 32 placeholders in KGy SOFT Libraries"); + int usedPlaceholders = 0; + string value = TranslatedText; + var state = State.Text; + int currentIndex = 0; + using var reader = new StringReader(value); + while (reader.Read() is int c and >= 0) + { + switch (state) + { + // Out of placeholder part + case State.Text: + + switch (c) + { + // { - placeholder candidate: looking ahead one character + case '{': + switch (c = reader.Read()) + { + // {{ - escape + case '{': + continue; + + // 0..9 - an actual placeholder + case >= '0' and <= '9': + currentIndex = c - '0'; + state = State.Index; + continue; + + // other character or end of string (whitespace is not allowed here) + default: + AddFormatError(result); + return result; + } + + // } - only escape is allowed as we are not in placeholder now + case '}': + if (reader.Read() == '}') + continue; + AddFormatError(result); + return result; + + // anything else - staying in text + default: + continue; + } + + // Inside placeholder index + case State.Index: + + // more digits: staying in index + if (c is >= '0' and <= '9') + { + currentIndex *= 10; + currentIndex += c - '0'; + continue; + } + + // consuming possible spaces (no other whitespace is allowed) + while (c == ' ') + c = reader.Read(); + + switch (c) + { + // end of placeholder + case '}': + if (!HandlePlaceholder(result, currentIndex, ref usedPlaceholders)) + return result; + state = State.Text; + continue; + + // padding + case ',': + state = State.Padding; + continue; + + // format specifier + case ':': + state = State.Format; + continue; + + default: + AddFormatError(result); + return result; + } + + // Inside placeholder padding + case State.Padding: + + // consuming possible spaces before padding count + while (c == ' ') + c = reader.Read(); + + // consuming possible negative sign + if (c == '-') + c = reader.Read(); + + bool hasDigit = false; + + // consuming digits: staying in index + while (c is >= '0' and <= '9') + { + c = reader.Read(); + hasDigit = true; + } + + // consuming possible spaces after padding count + while (c == ' ') + c = reader.Read(); + + if (!hasDigit) + { + AddFormatError(result); + return result; + } + + switch (c) + { + // end of placeholder + case '}': + if (!HandlePlaceholder(result, currentIndex, ref usedPlaceholders)) + return result; + state = State.Text; + continue; + + // format specifier + case ':': + state = State.Format; + continue; + + default: + AddFormatError(result); + return result; + } + + // Inside placeholder format specifier + case State.Format: + switch (c) + { + // possible end of placeholder: looking ahead one character + case '}': + if (reader.Peek() == '}') + { + reader.Read(); + continue; + } + + if (!HandlePlaceholder(result, currentIndex, ref usedPlaceholders)) + return result; + state = State.Text; + continue; + + // in format specifier { is allowed only escaped as part of the format + case '{': + if (reader.Peek() == '{') + { + reader.Read(); + continue; + } + + AddFormatError(result); + return result; + + default: + continue; + } + } + } + + if (state != State.Text) + AddFormatError(result); + else if (usedPlaceholders != (1 << placeholderCount) - 1) + result.AddWarning(nameof(TranslatedText), Res.ErrorMessageResourcePlaceholderUnusedIndices); + return result; + } + + #endregion + + #region Private Methods + + private void EnsurePlaceholderCount() + { + if (placeholderCount.HasValue) + return; + + // Just a shortcut: in all KGy SOFT Libraries format string resources end with 'Format' + // Note: there are some resources that end with 'Format' and they are not format strings (eg. InfoMessage_SamePixelFormat) + // but they contain no '{' so there will be no misinterpretation. + if (!Key.EndsWith("Format", StringComparison.Ordinal)) + { + placeholderCount = 0; + return; + } + + // -2: a placeholder is at least 3 chars long + int len = OriginalText.Length - 2; + int max = -1; + for (int i = 0; i < len; i++) + { + if (OriginalText[i] != '{') + continue; + + if (OriginalText[i + 1] == '{') + { + i += 1; + continue; + } + + int posEnd = OriginalText.IndexOf('}', i + 2); + Debug.Assert(posEnd > 1, "Valid original formats are expected"); + int posPadding = OriginalText.IndexOf(',', i + 2); + if (posPadding < i || posPadding > posEnd) + posPadding = posEnd; + int posFormat = OriginalText.IndexOf(':', i + 2); + if (posFormat < i || posFormat > posEnd) + posFormat = posEnd; + + int indexLen = Math.Min(posEnd, Math.Min(posPadding, posFormat)) - 1 - i; + int index = Int32.Parse(OriginalText.Substring(i + 1, indexLen), NumberStyles.None, CultureInfo.InvariantCulture); + if (max < index) + max = index; + i = posEnd; + } + + placeholderCount = max + 1; + } + + #endregion + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/Model/_Enums/LocalizableLibraries.cs b/KGySoft.Drawing.ImagingTools/Model/_Enums/LocalizableLibraries.cs new file mode 100644 index 0000000..fc7d7b4 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/Model/_Enums/LocalizableLibraries.cs @@ -0,0 +1,51 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: LocalizableLibraries.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.Model +{ + /// + /// Represents the known class libraries with localizable resources. + /// + [Flags] + public enum LocalizableLibraries + { + /// + /// Represents none of the localizable libraries. + /// + None, + + /// + /// Represents the KGySoft.CoreLibraries.dll assembly. + /// + CoreLibraries = 1, + + /// + /// Represents the KGySoft.Drawing.dll assembly. + /// + DrawingLibraries = 1 << 1, + + /// + /// Represents the KGySoft.Drawing.ImagingTools.exe assembly. + /// + ImagingTools = 1 << 2 + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/Model/_Interfaces/ICustomPropertiesProvider.cs b/KGySoft.Drawing.ImagingTools/Model/_Interfaces/ICustomPropertiesProvider.cs index 23efedb..2467a7e 100644 --- a/KGySoft.Drawing.ImagingTools/Model/_Interfaces/ICustomPropertiesProvider.cs +++ b/KGySoft.Drawing.ImagingTools/Model/_Interfaces/ICustomPropertiesProvider.cs @@ -26,8 +26,8 @@ internal interface ICustomPropertiesProvider : ICustomTypeDescriptor { #region Methods - object GetValue(string propertyName, object defaultValue); - void SetValue(string propertyName, object value); + object? GetValue(string propertyName, object? defaultValue); + void SetValue(string propertyName, object? value); void ResetValue(string propertyName); #endregion diff --git a/KGySoft.Drawing.ImagingTools/Properties/AssemblyInfo.cs b/KGySoft.Drawing.ImagingTools/Properties/AssemblyInfo.cs index 7504347..8d3e50c 100644 --- a/KGySoft.Drawing.ImagingTools/Properties/AssemblyInfo.cs +++ b/KGySoft.Drawing.ImagingTools/Properties/AssemblyInfo.cs @@ -35,8 +35,8 @@ // // You can specify all the values or you can default the Revision and Build Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("2.3.0.0")] -[assembly: AssemblyFileVersion("2.3.0.0")] -[assembly: AssemblyInformationalVersion("2.3.0")] +[assembly: AssemblyVersion("2.4.0")] +[assembly: AssemblyFileVersion("2.4.0")] +[assembly: AssemblyInformationalVersion("2.4.0")] [assembly: NeutralResourcesLanguage("en")] diff --git a/KGySoft.Drawing.ImagingTools/Properties/DataSources/KGySoft.Drawing.ImagingTools.Model.ResourceEntry.datasource b/KGySoft.Drawing.ImagingTools/Properties/DataSources/KGySoft.Drawing.ImagingTools.Model.ResourceEntry.datasource new file mode 100644 index 0000000..b815936 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/Properties/DataSources/KGySoft.Drawing.ImagingTools.Model.ResourceEntry.datasource @@ -0,0 +1,10 @@ + + + + KGySoft.Drawing.ImagingTools.Model.ResourceEntry, KGySoft.Drawing.ImagingTools, Version=2.3.0.0, Culture=neutral, PublicKeyToken=b45eba277439ddfe + \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/Properties/DataSources/KGySoft.Drawing.ImagingTools.ViewModel.DownloadableResourceItem.datasource b/KGySoft.Drawing.ImagingTools/Properties/DataSources/KGySoft.Drawing.ImagingTools.ViewModel.DownloadableResourceItem.datasource new file mode 100644 index 0000000..3a8cc20 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/Properties/DataSources/KGySoft.Drawing.ImagingTools.ViewModel.DownloadableResourceItem.datasource @@ -0,0 +1,10 @@ + + + + KGySoft.Drawing.ImagingTools.ViewModel.DownloadableResourceItem, KGySoft.Drawing.ImagingTools, Version=2.3.0.0, Culture=neutral, PublicKeyToken=b45eba277439ddfe + \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/Properties/Resources.Designer.cs b/KGySoft.Drawing.ImagingTools/Properties/Resources.Designer.cs index 4acbb77..993abb9 100644 --- a/KGySoft.Drawing.ImagingTools/Properties/Resources.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace KGySoft.Drawing.ImagingTools.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -89,31 +89,27 @@ internal static System.Drawing.Icon Clear { return ((System.Drawing.Icon)(obj)); } } - + /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// - internal static System.Drawing.Icon Colors - { - get - { + internal static System.Drawing.Icon Colors { + get { object obj = ResourceManager.GetObject("Colors", resourceCulture); return ((System.Drawing.Icon)(obj)); } } - + /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// - internal static System.Drawing.Icon Compare - { - get - { + internal static System.Drawing.Icon Compare { + get { object obj = ResourceManager.GetObject("Compare", resourceCulture); return ((System.Drawing.Icon)(obj)); } } - + /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// @@ -124,6 +120,36 @@ internal static System.Drawing.Icon Crop { } } + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Edit { + get { + object obj = ResourceManager.GetObject("Edit", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon HandGrab { + get { + object obj = ResourceManager.GetObject("HandGrab", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon HandOpen { + get { + object obj = ResourceManager.GetObject("HandOpen", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// @@ -144,6 +170,16 @@ internal static System.Drawing.Icon ImagingTools { } } + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Language { + get { + object obj = ResourceManager.GetObject("Language", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// @@ -154,6 +190,36 @@ internal static System.Drawing.Icon Magnifier { } } + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Magnifier1 { + get { + object obj = ResourceManager.GetObject("Magnifier1", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon MagnifierMinus { + get { + object obj = ResourceManager.GetObject("MagnifierMinus", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon MagnifierPlus { + get { + object obj = ResourceManager.GetObject("MagnifierPlus", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// @@ -213,19 +279,47 @@ internal static System.Drawing.Icon Prev { return ((System.Drawing.Icon)(obj)); } } - + /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// - internal static System.Drawing.Icon Resize - { - get - { + internal static System.Drawing.Icon Quantize { + get { + object obj = ResourceManager.GetObject("Quantize", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Resize { + get { object obj = ResourceManager.GetObject("Resize", resourceCulture); return ((System.Drawing.Icon)(obj)); } } - + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon RotateLeft { + get { + object obj = ResourceManager.GetObject("RotateLeft", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon RotateRight { + get { + object obj = ResourceManager.GetObject("RotateRight", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// @@ -245,15 +339,13 @@ internal static System.Drawing.Icon Settings { return ((System.Drawing.Icon)(obj)); } } - + /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// - internal static System.Drawing.Icon Quantize - { - get - { - object obj = ResourceManager.GetObject("Quantize", resourceCulture); + internal static System.Drawing.Icon SmoothZoom { + get { + object obj = ResourceManager.GetObject("SmoothZoom", resourceCulture); return ((System.Drawing.Icon)(obj)); } } diff --git a/KGySoft.Drawing.ImagingTools/Properties/Resources.resx b/KGySoft.Drawing.ImagingTools/Properties/Resources.resx index eebbb70..7ca0005 100644 --- a/KGySoft.Drawing.ImagingTools/Properties/Resources.resx +++ b/KGySoft.Drawing.ImagingTools/Properties/Resources.resx @@ -139,15 +139,33 @@ ..\Resources\Edit.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\HandGrab.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\HandOpen.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\HighlightVisibleClip.ico;System.Drawing.Icon, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\ImagingTools.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\Language.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\Magnifier.ico;System.Drawing.Icon, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\Magnifier1.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\MagnifierMinus.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\MagnifierPlus.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\MultiPage.ico;System.Drawing.Icon, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/KGySoft.Drawing.ImagingTools/Properties/Settings.Designer.cs b/KGySoft.Drawing.ImagingTools/Properties/Settings.Designer.cs new file mode 100644 index 0000000..6bfee6b --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/Properties/Settings.Designer.cs @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace KGySoft.Drawing.ImagingTools.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool AllowResXResources { + get { + return ((bool)(this["AllowResXResources"])); + } + set { + this["AllowResXResources"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool UseOSLanguage { + get { + return ((bool)(this["UseOSLanguage"])); + } + set { + this["UseOSLanguage"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("(Default)")] + public global::System.Globalization.CultureInfo DisplayLanguage { + get { + return ((global::System.Globalization.CultureInfo)(this["DisplayLanguage"])); + } + set { + this["DisplayLanguage"] = value; + } + } + } +} diff --git a/KGySoft.Drawing.ImagingTools/Properties/Settings.settings b/KGySoft.Drawing.ImagingTools/Properties/Settings.settings new file mode 100644 index 0000000..7e9b5f1 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/Properties/Settings.settings @@ -0,0 +1,15 @@ + + + + + + False + + + False + + + (Default) + + + \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/Res.cs b/KGySoft.Drawing.ImagingTools/Res.cs index 393b57b..0d335b5 100644 --- a/KGySoft.Drawing.ImagingTools/Res.cs +++ b/KGySoft.Drawing.ImagingTools/Res.cs @@ -21,8 +21,11 @@ using System.Drawing; using System.Drawing.Imaging; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; +using System.Resources; +using System.Threading; using KGySoft.Collections; using KGySoft.CoreLibraries; @@ -50,13 +53,79 @@ internal static class Res UseLanguageSettings = true, }; + // Note: No need to use ThreadSafeCacheFactory here because used only from the UI thread when applying resources // ReSharper disable once CollectionNeverUpdated.Local - private static readonly Cache localizablePropertiesCache = new Cache(GetLocalizableProperties); + private static readonly Cache localizableStringPropertiesCache = new Cache(GetLocalizableStringProperties); + + private static string? resourcesDir; + private static CultureInfo displayLanguage; + private static EventHandler? displayLanguageChanged; + + #endregion + + #region Events + + /// + /// Occurs when property changed. + /// Similar to , which is not quite reliable when executing as a debugger visualizer because + /// Visual Studio resets the on every keystroke and other events. + /// + internal static event EventHandler? DisplayLanguageChanged + { + add => value.AddSafe(ref displayLanguageChanged); + remove => value.RemoveSafe(ref displayLanguageChanged); + } #endregion #region Properties + #region General + + internal static CultureInfo OSLanguage { get; } + internal static CultureInfo DefaultLanguage { get; } + internal static string ResourcesDir + { + get + { + if (resourcesDir == null) + { + string path = resourceManager.ResXResourcesDir; + resourcesDir = Path.IsPathRooted(path) ? Path.GetFullPath(path) : Path.GetFullPath(Path.Combine(Files.GetExecutingPath(), path)); + } + + return resourcesDir; + } + } + + /// + /// Represents the current display language. Similar to and + /// but this property is not thread-bounded and can be used reliably even when running as a debugger visualizer where Visual Studio always resets + /// the property on every keystroke. + /// + internal static CultureInfo DisplayLanguage + { + get + { + CultureInfo result = displayLanguage; + Thread.CurrentThread.CurrentUICulture = result; + return result; + } + set + { + // Always setting also the current thread because when running from debugger visualizer VS may change it independently from the this code base. + // It is also needed for DynamicResourceManager instances of the different KGy SOFT libraries to save content automatically on language change. + LanguageSettings.DisplayLanguage = value; + if (Equals(displayLanguage, value)) + return; + + displayLanguage = value; + OnDisplayLanguageChanged(); + } + } + + #endregion + #region Title Captions /// No Image @@ -74,6 +143,18 @@ internal static class Res /// Confirmation internal static string TitleConfirmation => Get("Title_Confirmation"); + /// Open Image + internal static string TitleOpenFileDialog => Get("Title_OpenFileDialog"); + + /// Save Image As + internal static string TitleSaveFileDialog => Get("Title_SaveFileDialog"); + + /// Color + internal static string TitleColorDialog => Get("Title_ColorDialog"); + + /// Browse For Folder + internal static string TitleFolderDialog => Get("Title_FolderDialog"); + #endregion #region Texts @@ -120,21 +201,23 @@ internal static class Res /// • When not checked, saving saves always the selected page only. internal static string TooltipTextCompoundMultiPage => Get("TooltipText_CompoundMultiPage"); - /// Toggles whether the metafile is displayed with anti aliasing enabled. + /// Smoothing Edges (Alt+S) internal static string TooltipTextSmoothMetafile => Get("TooltipText_SmoothMetafile"); - /// Toggles whether an enlarged image is rendered with smoothing interpolation. - /// A shrunk image is always displayed with smoothing. + /// Smooth Zooming (Alt+S) internal static string TooltipTextSmoothBitmap => Get("TooltipText_SmoothBitmap"); /// Auto internal static string TextAuto => Get("Text_Auto"); /// Counting colors... - internal static string TextCountingColors => Get("Text_CountingColors"); + internal static string TextCountingColorsId => "Text_CountingColors"; - /// Operation has been canceled. - internal static string TextOperationCanceled => Get("Text_OperationCanceled"); + /// Color Count: {0} + internal static string TextColorCountId => "Text_ColorCountFormat"; + + /// Downloading... + internal static string TextDownloading => Get("Text_Downloading"); #endregion @@ -151,18 +234,21 @@ internal static class Res #region Notifications /// The loaded metafile has been converted to Bitmap. To load it as a Metafile, choose the Image Debugger Visualizer instead. - internal static string NotificationMetafileAsBitmap => Get("Notification_MetafileAsBitmap"); + internal static string NotificationMetafileAsBitmapId => "Notification_MetafileAsBitmap"; /// The loaded image has been converted to Icon - internal static string NotificationImageAsIcon => Get("Notification_ImageAsIcon"); + internal static string NotificationImageAsIconId => "Notification_ImageAsIcon"; /// The palette of an indexed BitmapData cannot be reconstructed, therefore a default palette is used. You can change palette colors in the menu. - internal static string NotificationPaletteCannotBeRestored => Get("Notification_PaletteCannotBeRestored"); + internal static string NotificationPaletteCannotBeRestoredId => "Notification_PaletteCannotBeRestored"; #endregion #region Messages + /// Error: {0} + internal static string ErrorMessageId => "ErrorMessageFormat"; + /// Saving modifications as animated GIF is not supported internal static string ErrorMessageAnimGifNotSupported => Get("ErrorMessage_AnimGifNotSupported"); @@ -172,22 +258,37 @@ internal static class Res /// The current installation is being executed, which cannot be removed internal static string ErrorMessageInstallationCannotBeRemoved => Get("ErrorMessage_InstallationCannotBeRemoved"); + /// Resource format string is invalid. + internal static string ErrorMessageResourceFormatError => Get("ErrorMessage_ResourceFormatError"); + + /// One or more placeholders are missing from the translated resource format string. + internal static string ErrorMessageResourcePlaceholderUnusedIndices => Get("ErrorMessage_ResourcePlaceholderUnusedIndices"); + /// The selected quantizer supports partial transparency, which is not supported by ditherers, /// so partial transparent pixels will be blended with back color. internal static string WarningMessageDithererNoAlphaGradient => Get("WarningMessage_DithererNoAlphaGradient"); + /// This language is not supported on this platform or by the executing framework + internal static string WarningMessageUnsupportedCulture => Get("WarningMessage_UnsupportedCulture"); + /// Are you sure you want to overwrite this installation? internal static string ConfirmMessageOverwriteInstallation => Get("ConfirmMessage_OverwriteInstallation"); /// Are you sure you want to remove this installation? internal static string ConfirmMessageRemoveInstallation => Get("ConfirmMessage_RemoveInstallation"); +#if NETCOREAPP /// You are about to install the .NET Core version, which might not be supported by Visual Studio as a debugger visualizer. Are you sure? - internal static string ConfirmMessageNetCoreVersion => Get("ConfirmMessage_NetCoreVersion"); + internal static string ConfirmMessageNetCoreVersion => Get("ConfirmMessage_NetCoreVersion"); +#endif /// There are unsaved modifications. Are sure to discard the changes? internal static string ConfirmMessageDiscardChanges => Get("ConfirmMessage_DiscardChanges"); + /// One or more selected items are for a different Imaging Tools version. + /// Are you sure you want to continue? + internal static string ConfirmMessageResourceVersionMismatch => Get("ConfirmMessage_ResourceVersionMismatch"); + /// The palette contains no colors. Click OK to exit. internal static string InfoMessagePaletteEmpty => Get("InfoMessage_PaletteEmpty"); @@ -205,6 +306,16 @@ internal static class Res /// By selecting a quantizer you can specify a different back color. internal static string InfoMessageAlphaTurnsBlack => Get("InfoMessage_AlphaTurnsBlack"); + /// This item is for a different ImagingTools version. + internal static string InfoMessageResourceVersionMismatch => Get("InfoMessage_ResourceVersionMismatch"); + + /// Just a regular ToolStrip menu, eh? + /// + /// Now imagine every combination of target platforms (from .NET Framework 3.5 to .NET 5), operating systems (from Windows XP to Linux/Mono), different DPI settings, enabled/disabled visual styles and high contrast mode, right-to-left layout... + /// + /// Harmonizing visual elements for all possible environments is never a trivial task, but OMG, the ToolStrip wasn't a cakewalk. Would you believe that each and every combination had at least one rendering issue? My custom-zoomable ImageViewer control with the asynchronously generated resized interpolated images on multiple cores was an easy-peasy compared to that... + internal static string InfoMessageEasterEgg => Get("InfoMessage_EasterEgg"); + #endregion #region Installations @@ -225,27 +336,67 @@ internal static class Res #endregion + #region Constructors + + static Res() + { + OSLanguage = LanguageSettings.DisplayLanguage.GetClosestNeutralCulture(); + DefaultLanguage = (Attribute.GetCustomAttribute(typeof(Res).Assembly, typeof(NeutralResourcesLanguageAttribute)) is NeutralResourcesLanguageAttribute attr + ? CultureInfo.GetCultureInfo(attr.CultureName) + : CultureInfo.InvariantCulture).GetClosestNeutralCulture(); + DrawingModule.Initialize(); + + bool allowResXResources = Configuration.AllowResXResources; + displayLanguage = allowResXResources + ? Configuration.UseOSLanguage ? OSLanguage : Configuration.DisplayLanguage // here, allowing specific languages, too + : DefaultLanguage; + + if (Equals(displayLanguage, CultureInfo.InvariantCulture) || (!Equals(displayLanguage, DefaultLanguage) && !ResHelper.GetAvailableLanguages().Contains(displayLanguage))) + displayLanguage = DefaultLanguage; + DisplayLanguage = displayLanguage; + LanguageSettings.DynamicResourceManagersSource = allowResXResources ? ResourceManagerSources.CompiledAndResX : ResourceManagerSources.CompiledOnly; + } + + #endregion + #region Methods #region Internal Methods #region General - internal static string Get(string id) => resourceManager.GetString(id, LanguageSettings.DisplayLanguage) ?? String.Format(CultureInfo.InvariantCulture, unavailableResource, id); + /// + /// Just an empty method to be able to trigger the static constructor without running any code other than field initializations. + /// + internal static void EnsureInitialized() + { + } + + internal static void OnDisplayLanguageChanged() => displayLanguageChanged?.Invoke(null, EventArgs.Empty); + + internal static string? GetStringOrNull(string id) => resourceManager.GetString(id, DisplayLanguage); + + internal static string Get(string id) => GetStringOrNull(id) ?? String.Format(CultureInfo.InvariantCulture, unavailableResource, id); + + internal static string Get(string id, params object?[]? args) + { + string format = Get(id); + return args == null ? format : SafeFormat(format, args); + } internal static string Get(TEnum value) where TEnum : struct, Enum => Get($"{value.GetType().Name}.{Enum.ToString(value)}"); - internal static void ApplyResources(object target, string name) + internal static void ApplyStringResources(object target, string name) { // Unlike ComponentResourceManager we don't go by ResourceSet because that would kill resource fallback traversal // so we go by localizable properties - PropertyInfo[] properties = localizablePropertiesCache[target.GetType()]; + PropertyInfo[]? properties = localizableStringPropertiesCache[target.GetType()]; if (properties == null) return; foreach (PropertyInfo property in properties) { - string value = resourceManager.GetString(name + "." + property.Name, LanguageSettings.DisplayLanguage); + string? value = GetStringOrNull(name + "." + property.Name); if (value == null) continue; Reflector.SetProperty(target, property, value); @@ -253,15 +404,14 @@ internal static void ApplyResources(object target, string name) } /// Internal Error: {0} - /// Use this method to avoid CA1303 for using string literals in internal errors that never supposed to occur. internal static string InternalError(string msg) => Get("General_InternalErrorFormat", msg); #endregion #region Title Captions - /// KGy SOFT Imaging Tools v{0} - internal static string TitleAppNameAndVersion(Version version) => Get("Title_AppNameAndVersionFormat", version); + /// KGy SOFT Imaging Tools v{0} [{1}{2}] – {3} + internal static string TitleAppNameWithFileName(Version version, string fileName, string modifiedMark, string caption) => Get("Title_AppNameWithFileNameFormat", version, fileName, modifiedMark, caption); /// Type: {0} internal static string TitleType(string type) => Get("Title_TypeFormat", type); @@ -269,8 +419,8 @@ internal static void ApplyResources(object target, string name) /// Size: {0}x{1} internal static string TitleSize(Size size) => Get("Title_SizeFormat", size.Width, size.Height); - /// Palette Count: {0} - internal static string TitlePaletteCount(int count) => Get("Title_PaletteCountFormat", count); + /// Palette Color Count: {0} + internal static string TitlePaletteCount(int count) => Get("Title_ColorCountFormat", count); /// Visible Clip Bounds: {{X = {0}, Y = {1}, Size = {2}x{3}}} internal static string TitleVisibleClip(Rectangle rect) => Get("Title_VisibleClipFormat", rect.X, rect.Y, rect.Width, rect.Height); @@ -281,6 +431,9 @@ internal static void ApplyResources(object target, string name) /// Color: {0} internal static string TitleColor(Color color) => Get("Title_ColorFormat", color.Name); + /// Edit Resources – {0} + internal static string TitleEditResources(string langName) => Get("Title_EditResourcesFormat", langName); + #endregion #region Texts @@ -297,8 +450,8 @@ internal static void ApplyResources(object target, string name) /// B: {0} internal static string TextBlueValue(byte a) => Get("Text_BlueValueFormat", a); - /// Color Count: {0} - internal static string TextColorCount(int a) => Get("Text_ColorCountFormat", a); + /// Unsupported Language ({0}) + internal static string TextUnsupportedCulture(string cultureName) => Get("Text_UnsupportedCultureFormat", cultureName); #endregion @@ -321,7 +474,7 @@ internal static string InfoBitmapData(Size size, int stride, PixelFormat pixelFo /// Unknown format: {0} internal static string InfoUnknownFormat(Guid format) => Get("InfoText_UnknownFormat", format); - /// Palette count: {0} + /// Palette color count: {0} internal static string InfoPalette(int count) => Get("InfoText_PaletteFormat", count); /// Images: {0} @@ -373,9 +526,6 @@ internal static string InfoColor(int argb, string knownColors, string systemColo #region Messages - /// Error: {0} - internal static string ErrorMessage(string error) => Get("ErrorMessageFormat", error); - /// Could not load file due to an error: {0} internal static string ErrorMessageFailedToLoadFile(string error) => Get("ErrorMessage_FailedToLoadFileFormat", error); @@ -429,11 +579,30 @@ internal static string InfoColor(int argb, string knownColors, string systemColo internal static string ErrorMessageFailedToGeneratePreview(string message) => Get("ErrorMessage_FailedToGeneratePreviewFormat", message); /// Value must be between {0} and {1} - internal static string ErrorMessageValueMustBeBetween(T low, T high) => Get("ErrorMessage_ValueMustBeBetweenFormat", low, high); + internal static string ErrorMessageValueMustBeBetween(T low, T high) where T : struct => Get("ErrorMessage_ValueMustBeBetweenFormat", low, high); /// Value must be greater than {0} - internal static string ErrorMessageValueMustBeGreaterThan(T value) => Get("ErrorMessage_ValueMustBeGreaterThanFormat", value); + internal static string ErrorMessageValueMustBeGreaterThan(T value) where T : struct => Get("ErrorMessage_ValueMustBeGreaterThanFormat", value); + + /// Failed to save settings: {0} + internal static string ErrorMessageFailedToSaveSettings(string message) => Get("ErrorMessage_FailedToSaveSettingsFormat", message); + + /// Failed to regenerate resource file {0}: {1} + internal static string ErrorMessageFailedToRegenerateResource(string fileName, string message) => Get("ErrorMessage_FailedToRegenerateResourceFormat", fileName, message); + + /// Failed to save resource file {0}: {1} + internal static string ErrorMessageFailedToSaveResource(string fileName, string message) => Get("ErrorMessage_FailedToSaveResourceFormat", fileName, message); + + /// Failed to access online resources: {0} + internal static string ErrorMessageCouldNotAccessOnlineResources(string message) => Get("ErrorMessage_CouldNotAccessOnlineResourcesFormat", message); + /// Failed to download resource file {0}: {1} + internal static string ErrorMessageFailedToDownloadResource(string fileName, string message) => Get("ErrorMessage_FailedToDownloadResourceFormat", fileName, message); + + /// Index '{0}' is invalid in the translated resource format string. + internal static string ErrorMessageResourcePlaceholderIndexInvalid(int index) => Get("ErrorMessage_ResourcePlaceholderIndexInvalidFormat", index); + +#if NET45 /// Could not create directory {0}: {1} /// /// The debugger visualizer may will not work for .NET Core projects. @@ -447,7 +616,8 @@ internal static string InfoColor(int argb, string knownColors, string systemColo /// Could not copy file {0}: {1} /// /// The debugger visualizer may will not work for .NET Core projects. - internal static string WarningMessageCouldNotCopyFileNetCore(string path, string message) => Get("WarningMessage_CouldNotCopyFileNetCoreFormat", path, message); + internal static string WarningMessageCouldNotCopyFileNetCore(string path, string message) => Get("WarningMessage_CouldNotCopyFileNetCoreFormat", path, message); +#endif /// The installation finished with a warning: {0} internal static string WarningMessageInstallationWarning(string warning) => Get("WarningMessage_InstallationWarningFormat", warning); @@ -461,11 +631,27 @@ internal static string InfoColor(int argb, string knownColors, string systemColo /// otherwise, the result might not be optimal even with dithering. internal static string WarningMessageQuantizerTooWide(PixelFormat selectedPixelFormat, PixelFormat pixelFormatHint) => Get("WarningMessage_QuantizerTooWideFormat", selectedPixelFormat, pixelFormatHint); + /// {0} file(s) have been downloaded. + /// + /// The culture of one or more downloaded localizations is not supported on this platform. Those languages will not appear among the selectable languages. + internal static string WarningMessageDownloadCompletedWithUnsupportedCultures(int count) => Get("WarningMessage_DownloadCompletedWithUnsupportedCulturesFormat", count); + /// The extension of the provided filename '{0}' does not match to the selected format ({1}). /// /// Are you sure you want to save the file with the provided extension? internal static string ConfirmMessageSaveFileExtension(string fileName, string format) => Get("ConfirmMessage_SaveFileExtensionFormat", fileName, format); + /// Failed to read resource file {0}: {1} + /// + /// Do you want to try to regenerate it? The current file will be deleted. + internal static string ConfirmMessageTryRegenerateResource(string fileName, string message) => Get("ConfirmMessage_TryRegenerateResourceFormat", fileName, message); + + /// The following files already exist: + /// {0} + /// + /// Do you want to overwrite them? + internal static string ConfirmMessageOverwriteResources(string files) => Get("ConfirmMessage_MessageOverwriteResourcesFormat", files); + /// {0} is the lowest compatible pixel format, which still supports the selected quantizer. internal static string InfoMessagePixelFormatUnnecessarilyWide(PixelFormat pixelFormat) => Get("InfoMessage_PixelFormatUnnecessarilyWideFormat", pixelFormat); @@ -488,6 +674,20 @@ internal static string InfoColor(int argb, string knownColors, string systemColo /// The ditherer is ignored for pixel format '{0}' if there is no quantizer specified. internal static string InfoMessageDithererIgnored(PixelFormat pixelFormat) => Get("InfoMessage_DithererIgnoredFormat", pixelFormat); + /// {0} file(s) have been downloaded. + internal static string InfoMessageDownloadCompleted(int count) => Get("InfoMessage_DownloadCompletedFormat", count); + + /// About KGy SOFT Imaging Tools + /// + /// Version: v{0} + /// Author: György Kőszeg + /// Target Platform: {1} + /// + /// You are now using the compiled English resources. + /// Copyright © {2} KGy SOFT. All rights reserved. + /// + internal static string InfoMessageAbout(Version version, string platform, int year) => Get("InfoMessage_About", version, platform, year); + #endregion #region Installations @@ -495,19 +695,19 @@ internal static string InfoColor(int argb, string knownColors, string systemColo /// Debugger version: {0} internal static string InstallationAvailable(Version version) => Get("Installation_AvailableFormat", version); - /// Debugger version: {0} - Runtime: {1} + /// Debugger version: {0} – Runtime: {1} internal static string InstallationsAvailableWithRuntime(Version version, string runtimeVersion) => Get("Installation_AvailableWithRuntimeFormat", version, runtimeVersion); - /// Debugger version: {0} - Target: {1} + /// Debugger version: {0} – Target: {1} internal static string InstallationsAvailableWithTargetFramework(Version version, string targetFramework) => Get("Installation_AvailableWithTargetFrameworkFormat", version, targetFramework); /// Installed: {0} internal static string InstallationsStatusInstalled(Version version) => Get("Installations_StatusInstalledFormat", version); - /// Installed: {0} - Runtime: {1} + /// Installed: {0} – Runtime: {1} internal static string InstallationsStatusInstalledWithRuntime(Version version, string runtimeVersion) => Get("Installations_StatusInstalledWithRuntimeFormat", version, runtimeVersion); - /// Installed: {0} - Target: {1} + /// Installed: {0} – Target: {1} internal static string InstallationsStatusInstalledWithTargetFramework(Version version, string targetFramework) => Get("Installations_StatusInstalledWithTargetFrameworkFormat", version, targetFramework); #endregion @@ -516,13 +716,7 @@ internal static string InfoColor(int argb, string knownColors, string systemColo #region Private Methods - private static string Get(string id, params object[] args) - { - string format = Get(id); - return args == null ? format : SafeFormat(format, args); - } - - private static string SafeFormat(string format, object[] args) + private static string SafeFormat(string format, object?[] args) { try { @@ -531,10 +725,7 @@ private static string SafeFormat(string format, object[] args) { string nullRef = PublicResources.Null; for (; i < args.Length; i++) - { - if (args[i] == null) - args[i] = nullRef; - } + args[i] ??= nullRef; } return String.Format(LanguageSettings.FormattingLanguage, format, args); @@ -545,15 +736,26 @@ private static string SafeFormat(string format, object[] args) } } - private static PropertyInfo[] GetLocalizableProperties(Type type) + private static PropertyInfo[]? GetLocalizableStringProperties(Type type) { // Getting string properties only. The resource manager in this class works in safe mode anyway. var result = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.PropertyType == typeof(string) - && Attribute.GetCustomAttribute(p, typeof(LocalizableAttribute)) is LocalizableAttribute la && la.IsLocalizable).ToArray(); + && Attribute.GetCustomAttribute(p, typeof(LocalizableAttribute)) is LocalizableAttribute la && la.IsLocalizable).ToArray(); return result.Length == 0 ? null : result; } + private static CultureInfo GetClosestNeutralCulture(this CultureInfo culture) + { + if (CultureInfo.InvariantCulture.Equals(culture)) + return CultureInfo.GetCultureInfo("en"); + + while (!culture.IsNeutralCulture) + culture = culture.Parent; + + return culture; + } + #endregion #endregion diff --git a/KGySoft.Drawing.ImagingTools/Resources/HandGrab.ico b/KGySoft.Drawing.ImagingTools/Resources/HandGrab.ico new file mode 100644 index 0000000..dacadf1 Binary files /dev/null and b/KGySoft.Drawing.ImagingTools/Resources/HandGrab.ico differ diff --git a/KGySoft.Drawing.ImagingTools/Resources/HandOpen.ico b/KGySoft.Drawing.ImagingTools/Resources/HandOpen.ico new file mode 100644 index 0000000..cd565de Binary files /dev/null and b/KGySoft.Drawing.ImagingTools/Resources/HandOpen.ico differ diff --git a/KGySoft.Drawing.ImagingTools/Resources/Language.ico b/KGySoft.Drawing.ImagingTools/Resources/Language.ico new file mode 100644 index 0000000..47394ce Binary files /dev/null and b/KGySoft.Drawing.ImagingTools/Resources/Language.ico differ diff --git a/KGySoft.Drawing.ImagingTools/Resources/Magnifier1.ico b/KGySoft.Drawing.ImagingTools/Resources/Magnifier1.ico new file mode 100644 index 0000000..89efb29 Binary files /dev/null and b/KGySoft.Drawing.ImagingTools/Resources/Magnifier1.ico differ diff --git a/KGySoft.Drawing.ImagingTools/Resources/MagnifierMinus.ico b/KGySoft.Drawing.ImagingTools/Resources/MagnifierMinus.ico new file mode 100644 index 0000000..f8214aa Binary files /dev/null and b/KGySoft.Drawing.ImagingTools/Resources/MagnifierMinus.ico differ diff --git a/KGySoft.Drawing.ImagingTools/Resources/MagnifierPlus.ico b/KGySoft.Drawing.ImagingTools/Resources/MagnifierPlus.ico new file mode 100644 index 0000000..72b5856 Binary files /dev/null and b/KGySoft.Drawing.ImagingTools/Resources/MagnifierPlus.ico differ diff --git a/KGySoft.Drawing.ImagingTools/System/Diagnostics.CodeAnalysis/Attributes.cs b/KGySoft.Drawing.ImagingTools/System/Diagnostics.CodeAnalysis/Attributes.cs new file mode 100644 index 0000000..fac5114 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/System/Diagnostics.CodeAnalysis/Attributes.cs @@ -0,0 +1,29 @@ +#if NETFRAMEWORK || NETCOREAPP2_0 +// ReSharper disable once CheckNamespace +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class AllowNullAttribute : Attribute { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class MaybeNullAttribute : Attribute { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + // ReSharper disable once UnusedAutoPropertyAccessor.Global - used by the compiler + public bool ReturnValue { get; } + } +} + +#endif \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/System/MathF.cs b/KGySoft.Drawing.ImagingTools/System/MathF.cs new file mode 100644 index 0000000..1de5143 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/System/MathF.cs @@ -0,0 +1,14 @@ +#if NETFRAMEWORK +// ReSharper disable once CheckNamespace +namespace System +{ + internal static class MathF + { + #region Methods + + public static float Round(float x) => (float)Math.Round(x); + + #endregion + } +} +#endif \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/System/Runtime.CompilerServices/CallerMemberNameAttribute.cs b/KGySoft.Drawing.ImagingTools/System/Runtime.CompilerServices/CallerMemberNameAttribute.cs new file mode 100644 index 0000000..b6e70fb --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/System/Runtime.CompilerServices/CallerMemberNameAttribute.cs @@ -0,0 +1,10 @@ +#if NET35 || NET40 +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class CallerMemberNameAttribute : Attribute + { + } +} +#endif diff --git a/KGySoft.Drawing.ImagingTools/System/Runtime.CompilerServices/IsExternalInit.cs b/KGySoft.Drawing.ImagingTools/System/Runtime.CompilerServices/IsExternalInit.cs new file mode 100644 index 0000000..dc29d34 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/System/Runtime.CompilerServices/IsExternalInit.cs @@ -0,0 +1,8 @@ +#if NETFRAMEWORK || NETCOREAPP3_0 +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit { } +} + +#endif \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/System/Runtime.CompilerServices/TupleElementNamesAttribute.cs b/KGySoft.Drawing.ImagingTools/System/Runtime.CompilerServices/TupleElementNamesAttribute.cs new file mode 100644 index 0000000..3d74fe2 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/System/Runtime.CompilerServices/TupleElementNamesAttribute.cs @@ -0,0 +1,16 @@ +#if NET35 || NET40 || NET45 +using System.Collections.Generic; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue | AttributeTargets.Class | AttributeTargets.Struct)] + internal sealed class TupleElementNamesAttribute : Attribute + { + private readonly string[] transformNames; + public TupleElementNamesAttribute(string[] transformNames) => this.transformNames = transformNames ?? throw new ArgumentNullException(nameof(transformNames)); + public TupleElementNamesAttribute() => transformNames = new string[] { }; + public IList TransformNames => transformNames; + } +} +#endif \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/System/Threading/ManualResetEventSlim.cs b/KGySoft.Drawing.ImagingTools/System/Threading/ManualResetEventSlim.cs new file mode 100644 index 0000000..d514a2c --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/System/Threading/ManualResetEventSlim.cs @@ -0,0 +1,70 @@ +#if NET35 + +// ReSharper disable once CheckNamespace +namespace System.Threading +{ + internal sealed class ManualResetEventSlim : IDisposable + { + #region Fields + + private readonly object syncRoot = new object(); + + private bool isDisposed; + + #endregion + + #region Properties + + internal bool IsSet { get; private set; } + + #endregion + + #region Methods + + #region Public Methods + + public void Dispose() + { + lock (syncRoot) + { + if (isDisposed) + return; + isDisposed = true; + IsSet = true; + Monitor.PulseAll(syncRoot); + } + } + + #endregion + + #region Internal Methods + + internal void Set() + { + lock (syncRoot) + { + if (isDisposed) + return; + IsSet = true; + Monitor.PulseAll(syncRoot); + } + } + + internal void Wait() + { + lock (syncRoot) + { + if (isDisposed) + return; + while (!IsSet) + Monitor.Wait(syncRoot); + } + } + + #endregion + + #endregion + } +} + +#endif \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/System/ValueTuple.cs b/KGySoft.Drawing.ImagingTools/System/ValueTuple.cs new file mode 100644 index 0000000..33d8152 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/System/ValueTuple.cs @@ -0,0 +1,46 @@ +#if NET35 || NET40 || NET45 +// ReSharper disable NonReadonlyMemberInGetHashCode +// ReSharper disable FieldCanBeMadeReadOnly.Global +using System.Collections.Generic; + +// ReSharper disable once CheckNamespace +namespace System +{ + internal struct ValueTuple + { + internal static int CombineHashCodes(int h1, int h2) + { + uint num = (uint)((h1 << 5) | (h1 >> 27)); + return ((int)num + h1) ^ h2; + } + } + + [Serializable] + internal struct ValueTuple : IEquatable> + { + public T1 Item1; + public T2 Item2; + + public ValueTuple(T1 item1, T2 item2) + { + Item1 = item1; + Item2 = item2; + } + + public bool Equals(ValueTuple other) + => EqualityComparer.Default.Equals(Item1, other.Item1) + && EqualityComparer.Default.Equals(Item2, other.Item2); + + public override bool Equals(object obj) => obj is ValueTuple tuple && Equals(tuple); + + public override int GetHashCode() + => ValueTuple.CombineHashCodes(EqualityComparer.Default.GetHashCode(Item1), + EqualityComparer.Default.GetHashCode(Item2)); + + public override string ToString() => $"({Item1}, {Item2})"; + + public static bool operator ==(ValueTuple left, ValueTuple right) => left.Equals(right); + public static bool operator !=(ValueTuple left, ValueTuple right) => !left.Equals(right); + } +} +#endif diff --git a/KGySoft.Drawing.ImagingTools/View/Components/AdvancedToolStripSplitButton.cs b/KGySoft.Drawing.ImagingTools/View/Components/AdvancedToolStripSplitButton.cs new file mode 100644 index 0000000..8a8705d --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Components/AdvancedToolStripSplitButton.cs @@ -0,0 +1,139 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: AdvancedToolStripSplitButton.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Components +{ + /// + /// A whose button part can be checked and the default item can automatically be changed. + /// + // NOTE: The properly scaled arrow and the checked appearance is rendered by ScalingToolStripMenuRenderer, while + // the drop-down button size is adjusted in ScalingToolStrip for all ToolStripSplitButtons + internal class AdvancedToolStripSplitButton : ToolStripSplitButton + { + #region Fields + + private bool isChecked; + private bool autoChangeDefaultItem; + + #endregion + + #region Properties + + [DefaultValue(false)] + public bool CheckOnClick { get; set; } + + [DefaultValue(false)] + public bool Checked + { + get => isChecked; + set + { + if (value == isChecked) + return; + isChecked = value; + OnCheckedChanged(EventArgs.Empty); + Invalidate(); + } + } + + [DefaultValue(false)] + public bool AutoChangeDefaultItem + { + get => autoChangeDefaultItem; + set + { + if (value == autoChangeDefaultItem) + return; + autoChangeDefaultItem = value; + if (value && DropDownItems.Count > 0) + SetDefaultItem(DropDownItems[0]); + } + } + + #endregion + + #region Events + + public event EventHandler CheckedChanged + { + add => Events.AddHandler(nameof(CheckedChanged), value); + remove => Events.RemoveHandler(nameof(CheckedChanged), value); + } + + #endregion + + #region Methods + + #region Public Methods + + public override Size GetPreferredSize(Size constrainingSize) + { + if (Owner.Orientation == Orientation.Horizontal) + return base.GetPreferredSize(constrainingSize); + + // with vertical orientation the image is too small + Size result = base.GetPreferredSize(constrainingSize); + return new Size(result.Width + Owner.ScaleWidth(2), result.Height); + } + + #endregion + + #region Internal Methods + + internal void SetDefaultItem(ToolStripItem item) + { + DefaultItem = item; + Image = item.Image; + Text = item.Text; + } + + #endregion + + #region Protected Methods + + protected override void OnButtonClick(EventArgs e) + { + if (CheckOnClick) + Checked = !Checked; + if (OSUtils.IsMono) + DefaultItem?.PerformClick(); + else + base.OnButtonClick(e); + } + + protected virtual void OnCheckedChanged(EventArgs e) => (Events[nameof(CheckedChanged)] as EventHandler)?.Invoke(this, e); + + protected override void OnDropDownItemClicked(ToolStripItemClickedEventArgs e) + { + base.OnDropDownItemClicked(e); + if (autoChangeDefaultItem && DefaultItem != e.ClickedItem) + SetDefaultItem(e.ClickedItem); + } + + #endregion + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/View/Components/AdvancedToolTip.cs b/KGySoft.Drawing.ImagingTools/View/Components/AdvancedToolTip.cs new file mode 100644 index 0000000..1f57a6f --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Components/AdvancedToolTip.cs @@ -0,0 +1,85 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: AdvancedToolTip.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.ComponentModel; +using System.Windows.Forms; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Components +{ + /// + /// A ToolTip that supports RTL correctly + /// + /// + internal class AdvancedToolTip : ToolTip + { + #region Constructors + + public AdvancedToolTip() => Initialize(); + + public AdvancedToolTip(IContainer container) : base(container) => Initialize(); + + #endregion + + #region Methods + + #region Static Methods + + private static void AdvancedToolTip_Draw(object sender, DrawToolTipEventArgs e) => e.DrawToolTipAdvanced(); + + #endregion + + #region Instance Methods + + protected override void Dispose(bool disposing) + { + if (disposing) + { + Draw -= AdvancedToolTip_Draw; + Res.DisplayLanguageChanged -= Res_DisplayLanguageChanged; + } + + base.Dispose(disposing); + } + + #endregion + + #region Private Methods + + private void Initialize() + { + Res.DisplayLanguageChanged += Res_DisplayLanguageChanged; + Draw += AdvancedToolTip_Draw; + ResetOwnerDraw(); + } + + private void ResetOwnerDraw() => OwnerDraw = Res.DisplayLanguage.TextInfo.IsRightToLeft; + + #endregion + + #region Event Handlers + + private void Res_DisplayLanguageChanged(object? sender, EventArgs e) => ResetOwnerDraw(); + + #endregion + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/ScalingToolStripDropDownButton.cs b/KGySoft.Drawing.ImagingTools/View/Components/ScalingToolStripDropDownButton.cs similarity index 84% rename from KGySoft.Drawing.ImagingTools/View/Controls/ScalingToolStripDropDownButton.cs rename to KGySoft.Drawing.ImagingTools/View/Components/ScalingToolStripDropDownButton.cs index 749b59c..e3c0174 100644 --- a/KGySoft.Drawing.ImagingTools/View/Controls/ScalingToolStripDropDownButton.cs +++ b/KGySoft.Drawing.ImagingTools/View/Components/ScalingToolStripDropDownButton.cs @@ -21,7 +21,7 @@ #endregion -namespace KGySoft.Drawing.ImagingTools.View.Controls +namespace KGySoft.Drawing.ImagingTools.View.Components { /// /// A that can scale its arrow regardless of .NET version and app.config settings. @@ -64,7 +64,7 @@ internal Padding ArrowPadding { if (arrowPadding != Padding.Empty) return arrowPadding; - var scaled = Size.Round(Owner.ScaleSize(arrowPaddingUnscaled)); + Size scaled = Size.Round(Owner.ScaleSize(arrowPaddingUnscaled)); return arrowPadding = new Padding(scaled.Width, scaled.Height, scaled.Width, scaled.Height); } } @@ -73,8 +73,8 @@ internal Rectangle ArrowRectangle { get { - var padding = ArrowPadding; - var size = ArrowSize; + Padding padding = ArrowPadding; + Size size = ArrowSize; var bounds = new Rectangle(Point.Empty, Size); if (TextDirection == ToolStripTextDirection.Horizontal) { @@ -93,6 +93,8 @@ internal Rectangle ArrowRectangle #region Methods + #region Public Methods + public override Size GetPreferredSize(Size constrainingSize) { var showArrow = ShowDropDownArrow; @@ -109,5 +111,21 @@ public override Size GetPreferredSize(Size constrainingSize) } #endregion + + #region Internal Methods + +#if NETFRAMEWORK + internal void AdjustImageRectangle(ref Rectangle imageBounds) + { + if (RightToLeft == RightToLeft.Yes) + imageBounds.X = Width - 2 - imageBounds.Width; + else + imageBounds.X = 2; + } +#endif + + #endregion + + #endregion } } diff --git a/KGySoft.Drawing.ImagingTools/View/Components/ZoomSplitButton.cs b/KGySoft.Drawing.ImagingTools/View/Components/ZoomSplitButton.cs new file mode 100644 index 0000000..3198e0e --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Components/ZoomSplitButton.cs @@ -0,0 +1,116 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: ZoomSplitButton.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Components +{ + internal class ZoomSplitButton : AdvancedToolStripSplitButton + { + #region Properties + + #region Public Properties + + // Overridden just to prevent saving a fixed low-res image in the .resx file + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override Image Image { get => base.Image; set => base.Image = value; } + + #endregion + + #region Internal Properties + + internal ToolStripMenuItem AutoZoomMenuItem { get; } + internal ToolStripMenuItem IncreaseZoomMenuItem { get; } + internal ToolStripMenuItem DecreaseZoomMenuItem { get; } + internal ToolStripMenuItem ResetZoomMenuItem { get; } + + #endregion + + #endregion + + #region Constructors + + public ZoomSplitButton() + { + CheckOnClick = true; + Image = Images.Magnifier; + ToolStripItemCollection items = DropDownItems; + + AutoZoomMenuItem = new ToolStripMenuItem + { + Name = "miAutoZoom", + Image = Images.Magnifier, + CheckOnClick = true, + ShortcutKeys = Keys.Alt | Keys.Z + }; + AutoZoomMenuItem.CheckedChanged += (_, _) => Checked = AutoZoomMenuItem.Checked; + + IncreaseZoomMenuItem = new ToolStripMenuItem + { + Name = "miIncreaseZoom", + Image = Images.MagnifierPlus, + ShortcutKeys = Keys.Control | Keys.Add, + ShortcutKeyDisplayString = @"Ctrl++", + }; + + DecreaseZoomMenuItem = new ToolStripMenuItem + { + Name = "miDecreaseZoom", + Image = Images.MagnifierMinus, + ShortcutKeys = Keys.Control | Keys.Subtract, + ShortcutKeyDisplayString = @"Ctrl+-", + }; + + ResetZoomMenuItem = new ToolStripMenuItem + { + Name = "miResetZoom", + Image = Images.Magnifier1, + ShortcutKeys = Keys.Control | Keys.NumPad0, + ShortcutKeyDisplayString = @"Ctrl+0", + }; + + items.AddRange(new ToolStripItem[] { AutoZoomMenuItem, IncreaseZoomMenuItem, DecreaseZoomMenuItem, ResetZoomMenuItem }); + } + + protected override void OnParentChanged(ToolStrip? oldParent, ToolStrip? newParent) + { + base.OnParentChanged(oldParent, newParent); + + // Mono: without this the new parent's renderer will not be applied to the drop down menu strip + if (OSUtils.IsMono && newParent != null) + AutoZoomMenuItem.Owner.Renderer = newParent.Renderer; + } + + #endregion + + #region Methods + + protected override void OnCheckedChanged(EventArgs e) + { + base.OnCheckedChanged(e); + AutoZoomMenuItem.Checked = Checked; + } + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/AdvancedDataGridView.cs b/KGySoft.Drawing.ImagingTools/View/Controls/AdvancedDataGridView.cs new file mode 100644 index 0000000..40c6c4d --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Controls/AdvancedDataGridView.cs @@ -0,0 +1,357 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: AdvancedDataGridView.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +using KGySoft.ComponentModel; +using KGySoft.CoreLibraries; + +#endregion + +#region Suppressions + +#if NETCOREAPP3_0 +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - Columns items are never null +#pragma warning disable CS8602 // Dereference of a possibly null reference. - Columns items are never null +#endif + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Controls +{ + #region Usings + +#if NET35 || NET40 + using ValidationList = IList; +#else + using ValidationList = IReadOnlyList; +#endif + + #endregion + + /// + /// Just a DataGridView that + /// - provides some default style with a few fixed issues + /// - scales the columns automatically + /// - provides scaling error/warning/info icons, which appear also on Linux/Mono + /// + internal class AdvancedDataGridView : DataGridView + { + #region Fields + + private readonly DataGridViewCellStyle defaultDefaultCellStyle; + private readonly DataGridViewCellStyle defaultColumnHeadersDefaultCellStyle; + private readonly DataGridViewCellStyle defaultAlternatingRowsDefaultCellStyle; + + private bool isRightToLeft; + private Bitmap? errorIcon; + private Bitmap? warningIcon; + private Bitmap? infoIcon; + + #endregion + + #region Properties + + #region Public Properties + + // these are reintroduced just for the ShouldSerialize... methods + [AmbientValue(null)] + [AllowNull] + public new DataGridViewCellStyle DefaultCellStyle + { + get => base.DefaultCellStyle; + set => base.DefaultCellStyle = value; + } + + [AmbientValue(null)] + [AllowNull] + public new DataGridViewCellStyle ColumnHeadersDefaultCellStyle + { + get => base.ColumnHeadersDefaultCellStyle; + set => base.ColumnHeadersDefaultCellStyle = value; + } + + [AmbientValue(null)] + [AllowNull] + public new DataGridViewCellStyle AlternatingRowsDefaultCellStyle + { + get => base.AlternatingRowsDefaultCellStyle; + set => base.AlternatingRowsDefaultCellStyle = value; + } + + #endregion + + #region Private Properties + + private Bitmap ErrorIcon => errorIcon ??= Icons.SystemError.ToScaledBitmap(this.GetScale()); + private Bitmap WarningIcon => warningIcon ??= Icons.SystemWarning.ToScaledBitmap(this.GetScale()); + private Bitmap InfoIcon => infoIcon ??= Icons.SystemInformation.ToScaledBitmap(this.GetScale()); + + #endregion + + #endregion + + #region Constructors + + public AdvancedDataGridView() + { + DefaultCellStyle = defaultDefaultCellStyle = new DataGridViewCellStyle(DefaultCellStyle) + { + // Base default uses Window back color with ControlText fore color. Most cases it's not an issue unless Window/Control colors are close to inverted. + BackColor = SystemColors.Window, + ForeColor = SystemColors.WindowText, + }; + + ColumnHeadersDefaultCellStyle = defaultColumnHeadersDefaultCellStyle = new DataGridViewCellStyle + { + // Base default uses Control back color with WindowText fore color. Most cases it's not an issue unless Window/Control colors are close to inverted. + BackColor = SystemColors.Control, + ForeColor = SystemColors.ControlText, + }; + + AlternatingRowsDefaultCellStyle = defaultAlternatingRowsDefaultCellStyle = new DataGridViewCellStyle + { + BackColor = SystemColors.ControlLight, + ForeColor = SystemColors.ControlText, + }; + } + + #endregion + + #region Methods + + #region Static Methods + + private static string GetRowValidationText(ValidationResultsCollection validationResults) => validationResults.Count switch + { + 0 => String.Empty, + 1 => validationResults[0].Message, + _ => validationResults.Select(r => r.Message).Join(Environment.NewLine) + }; + + private static string GetCellValidationText(ValidationResultsCollection validationResults, string propertyName) + { + int len = validationResults.Count; + if (len == 0) + return String.Empty; + + for (var s = ValidationSeverity.Error; s >= ValidationSeverity.Information; s--) + { + for (int i = 0; i < len; i++) + { + if (validationResults[i].Severity == s && validationResults[i].PropertyName == propertyName) + return validationResults[i].Message; + } + } + + return String.Empty; + } + + #endregion + + #region Instance Methods + + #region Protected Methods + + protected override void OnParentChanged(EventArgs e) + { + base.OnParentChanged(e); + AdjustAlternatingRowsColors(); + } + + protected override void OnSystemColorsChanged(EventArgs e) + { + base.OnSystemColorsChanged(e); + AdjustAlternatingRowsColors(); + AlternatingRowsDefaultCellStyle = SystemInformation.HighContrast + ? null + : new DataGridViewCellStyle { BackColor = SystemColors.ControlLight, ForeColor = SystemColors.ControlText }; + } + + protected override void ScaleControl(SizeF factor, BoundsSpecified specified) + { + base.ScaleControl(factor, specified); + if (factor.Width.Equals(1f)) + return; + foreach (DataGridViewColumn column in Columns) + column.Width = (int)(column.Width * factor.Width); + } + + protected override void OnRightToLeftChanged(EventArgs e) + { + base.OnRightToLeftChanged(e); + isRightToLeft = RightToLeft == RightToLeft.Yes; + } + + protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e) + { + e.Paint(e.CellBounds, e.PaintParts & ~DataGridViewPaintParts.ErrorIcon); + if ((e.PaintParts & DataGridViewPaintParts.ErrorIcon) != DataGridViewPaintParts.None) + DrawValidationIcon(e); + + e.Handled = true; + } + + protected override void OnRowErrorTextNeeded(DataGridViewRowErrorTextNeededEventArgs e) + { + if (e.RowIndex < 0 || Rows[e.RowIndex].DataBoundItem is not IValidatingObject validatingObject) + return; + + e.ErrorText = GetRowValidationText(validatingObject.ValidationResults); + } + + protected override void OnCellErrorTextNeeded(DataGridViewCellErrorTextNeededEventArgs e) + { + if (e.RowIndex < 0 || Rows[e.RowIndex].DataBoundItem is not IValidatingObject validatingObject) + return; + + ValidationResultsCollection validationResults = validatingObject.ValidationResults; + e.ErrorText = validationResults.Count == 0 ? String.Empty : GetCellValidationText(validationResults, Columns[e.ColumnIndex].DataPropertyName); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + errorIcon?.Dispose(); + warningIcon?.Dispose(); + infoIcon?.Dispose(); + } + + base.Dispose(disposing); + } + + #endregion + + #region Private Methods + + private void DrawValidationIcon(DataGridViewCellPaintingEventArgs e) + { + Rectangle bounds = e.CellBounds; + bounds.Height -= 1; + bounds.Width -= 1; + + Bitmap? icon = GetCellIcon(e); + if (icon == null) + return; + + Size size = icon.Size; + Rectangle iconRect = new Rectangle(bounds.Left + (isRightToLeft ? 4 : bounds.Width - size.Width - 4), + bounds.Top + ((bounds.Height >> 1) - (size.Height >> 1)), + size.Width, size.Height); + + Rectangle iconBounds = Rectangle.Intersect(bounds, iconRect); + if (iconBounds.IsEmpty) + return; + + bool clip = iconRect != iconBounds; + if (clip) + e.Graphics.IntersectClip(iconBounds); + e.Graphics.DrawImage(icon, iconRect); + if (clip) + e.Graphics.ResetClip(); + } + + private Bitmap? GetCellIcon(DataGridViewCellPaintingEventArgs e) + { + #region Local Methods + + static bool HasMatch(ValidationList list, string name) + { + // not using LINQ and delegates for better performance + int len = list.Count; + for (int i = 0; i < len; i++) + { + if (list[i].PropertyName == name) + return true; + } + + return false; + } + + #endregion + + if (e.RowIndex < 0) + return null; + + // falling back to default error logic + if (Rows[e.RowIndex].DataBoundItem is not IValidatingObject validatingObject) + return String.IsNullOrEmpty(e.ErrorText) ? null : ErrorIcon; + + if (!OSUtils.IsWindows) + EnsureValidationText(e, validatingObject); + + ValidationResultsCollection validationResults = validatingObject.ValidationResults; + if (validationResults.Count == 0) + return null; + + // row header + if (e.ColumnIndex < 0) + { + return validationResults.HasErrors ? ErrorIcon + : validationResults.HasWarnings ? WarningIcon + : validationResults.HasInfos ? InfoIcon + : null; + } + + // cell + string propertyName = Columns[e.ColumnIndex].DataPropertyName; + return HasMatch(validationResults.Errors, propertyName) ? ErrorIcon + : HasMatch(validationResults.Warnings, propertyName) ? WarningIcon + : HasMatch(validationResults.Infos, propertyName) ? InfoIcon + : null; + } + + private void EnsureValidationText(DataGridViewCellPaintingEventArgs e, IValidatingObject validatingObject) + { + DataGridViewRow row = Rows[e.RowIndex]; + DataGridViewCell cell = e.ColumnIndex < 0 ? row.HeaderCell : row.Cells[e.ColumnIndex]; + ValidationResultsCollection validationResults = validatingObject.ValidationResults; + + cell.ErrorText = e.ColumnIndex < 0 + ? GetRowValidationText(validationResults) + : GetCellValidationText(validationResults, Columns[e.ColumnIndex].DataPropertyName); + } + + private void AdjustAlternatingRowsColors() + { + if (!Equals(AlternatingRowsDefaultCellStyle, defaultAlternatingRowsDefaultCellStyle)) + return; + + AlternatingRowsDefaultCellStyle = SystemInformation.HighContrast + ? null + : defaultAlternatingRowsDefaultCellStyle; + } + + private bool ShouldSerializeAlternatingRowsDefaultCellStyle() => !Equals(AlternatingRowsDefaultCellStyle, defaultAlternatingRowsDefaultCellStyle); + private bool ShouldSerializeColumnHeadersDefaultCellStyle() => !Equals(ColumnHeadersDefaultCellStyle, defaultColumnHeadersDefaultCellStyle); + private bool ShouldSerializeDefaultCellStyle() => !Equals(DefaultCellStyle, defaultDefaultCellStyle); + + #endregion + + #endregion + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/AdvancedToolStrip.cs b/KGySoft.Drawing.ImagingTools/View/Controls/AdvancedToolStrip.cs new file mode 100644 index 0000000..2bf9821 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Controls/AdvancedToolStrip.cs @@ -0,0 +1,581 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: AdvancedToolStrip.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2019 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +using KGySoft.Collections; +using KGySoft.CoreLibraries; +using KGySoft.Drawing.ImagingTools.View.Components; +using KGySoft.Drawing.ImagingTools.WinApi; +using KGySoft.Reflection; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Controls +{ + /// + /// A with some additional features: + /// - It can scale its content regardless of .NET version and app.config settings. + /// - Custom renderer for corrected checked button appearance, scaled and correctly colored arrows, fixed high contrast appearance and more. + /// - Tool tip supports right-to-left + /// - Clicking works even if the owner form was not active + /// + internal class AdvancedToolStrip : ToolStrip + { + #region AdvancedToolStripRenderer class + + private class AdvancedToolStripRenderer : ToolStripProfessionalRenderer + { + #region ButtonStyle enum + + [Flags] + private enum ButtonStyle : byte + { + None, + Selected = 1, + Pressed = 1 << 1, + Checked = 1 << 2, + Dropped = 1 << 3 + } + + #endregion + + #region Fields + + private static readonly Size referenceOffset = new Size(2, 2); + private static readonly Size referenceOffsetDouble = new Size(4, 4); + private static readonly Cache disabledImagesCache = new(CreateDisabledImage, 8) { DisposeDroppedValues = true }; + + #endregion + + #region Methods + + #region Static Methods + + private static void FillBackground(Graphics g, Rectangle rect, Color color1, Color color2) + { + if (color1.ToArgb() == color2.ToArgb()) + g.FillRectangle(color1.GetBrush(), rect); + else + { + using var brush = new LinearGradientBrush(rect, color1, color2, LinearGradientMode.Vertical); + g.FillRectangle(brush, rect); + } + } + + private static void DrawArrow(Graphics g, Color color, Rectangle bounds, ArrowDirection direction) + { + Point middle = new Point(bounds.Left + bounds.Width / 2, bounds.Top + bounds.Height / 2); + + Point[] arrow; + Size offset = g.ScaleSize(referenceOffset); + Size offsetDouble = g.ScaleSize(referenceOffsetDouble); + + switch (direction) + { + case ArrowDirection.Up: + arrow = new Point[] + { + new Point(middle.X - offset.Width - 1, middle.Y + 1), + new Point(middle.X + offset.Width + 1, middle.Y + 1), + new Point(middle.X, middle.Y - offset.Height - 1) + }; + break; + case ArrowDirection.Left: + arrow = new Point[] + { + new Point(middle.X + offset.Width, middle.Y - offsetDouble.Height), + new Point(middle.X + offset.Width, middle.Y + offsetDouble.Height), + new Point(middle.X - offset.Width, middle.Y) + }; + break; + case ArrowDirection.Right: + arrow = new Point[] + { + new Point(middle.X - offset.Width, middle.Y - offsetDouble.Height), + new Point(middle.X - offset.Width, middle.Y + offsetDouble.Height), + new Point(middle.X + offset.Width, middle.Y) + }; + break; + default: + arrow = new Point[] + { + new Point(middle.X - offset.Width, middle.Y - 1), + new Point(middle.X + offset.Width + (OSUtils.IsMono && OSUtils.IsLinux ? 2 : 1), middle.Y - 1), + new Point(middle.X, middle.Y + offset.Height) + }; + break; + } + + g.FillPolygon(color.GetBrush(), arrow); + } + + private static void DrawThemedButtonBackground(Graphics g, ProfessionalColorTable colorTable, Rectangle bounds, ButtonStyle style) + { + #region Local Methods + + static void RenderWithVisualStyles(Graphics g, ProfessionalColorTable colorTable, Rectangle bounds, ButtonStyle style) + { + Color backgroundStart; + Color backgroundEnd; + if ((style & ButtonStyle.Pressed) != 0 || (style & ButtonStyle.Selected) != 0 && (style & ButtonStyle.Checked) != 0) + { + backgroundStart = colorTable.ButtonPressedGradientBegin; + backgroundEnd = colorTable.ButtonPressedGradientEnd; + } + else if ((style & ButtonStyle.Selected) != 0) + { + backgroundStart = colorTable.ButtonSelectedGradientBegin; + backgroundEnd = colorTable.ButtonSelectedGradientEnd; + } + else if ((style & ButtonStyle.Checked) != 0) + { + backgroundStart = colorTable.ButtonCheckedGradientBegin is { IsEmpty: false } c1 ? c1 : colorTable.ButtonCheckedHighlight; + backgroundEnd = colorTable.ButtonCheckedGradientEnd is { IsEmpty: false } c2 ? c2 : colorTable.ButtonCheckedHighlight; + } + else + return; + + FillBackground(g, bounds, backgroundStart, backgroundEnd); + } + + static void RenderBasicTheme(Graphics g, ProfessionalColorTable colorTable, Rectangle bounds, ButtonStyle style) + { + Color backColor = (style & ButtonStyle.Pressed) != 0 || (style & ButtonStyle.Selected) != 0 && (style & ButtonStyle.Checked) != 0 ? colorTable.ButtonPressedHighlight + : (style & ButtonStyle.Selected) != 0 ? colorTable.ButtonSelectedHighlight + : (style & ButtonStyle.Checked) != 0 ? colorTable.ButtonCheckedHighlight + : Color.Empty; + g.FillRectangle(backColor.GetBrush(), bounds); + } + + #endregion + + if (style == ButtonStyle.None) + return; + if ((style & ButtonStyle.Dropped) != 0) + { + FillBackground(g, bounds, colorTable.MenuItemPressedGradientBegin, colorTable.MenuItemPressedGradientEnd); + g.DrawRectangle(colorTable.MenuBorder.GetPen(), bounds.X, bounds.Y, bounds.Width - 1, bounds.Height - 1); + return; + } + + if (Application.RenderWithVisualStyles) + RenderWithVisualStyles(g, colorTable, bounds, style); + else + RenderBasicTheme(g, colorTable, bounds, style); + g.DrawRectangle(colorTable.ButtonSelectedBorder.GetPen(), bounds.X, bounds.Y, bounds.Width - 1, bounds.Height - 1); + } + + private static void DrawHighContrastButtonBackground(Graphics g, Rectangle bounds, ButtonStyle style) + { + if ((style & ButtonStyle.Dropped) == 0 && (style & (ButtonStyle.Selected | ButtonStyle.Checked | ButtonStyle.Pressed)) != 0) + g.FillRectangle(SystemBrushes.Highlight, bounds); + + Color borderColor = (style & ButtonStyle.Dropped) != 0 ? SystemColors.ButtonHighlight + : (style & ButtonStyle.Pressed) == 0 && (style & (ButtonStyle.Checked | ButtonStyle.Selected)) is ButtonStyle.Checked or ButtonStyle.Selected ? SystemColors.ControlLight + : Color.Empty; + + if (!borderColor.IsEmpty) + g.DrawRectangle(borderColor.GetPen(), bounds.X, bounds.Y, bounds.Width - 1, bounds.Height - 1); + } + + #endregion + + #region Instance Methods + + /// + /// Changes to original: + /// - Fixed color + /// - Fixed scaling + /// - [Mono]: Ignoring ToolStripSplitButton because it is painted along the button just like in the MS world. + /// - [Mono]: Fixing menu item arrow position in high DPI mode + /// + protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e) + { + if (e.Item is ToolStripSplitButton) + return; + Rectangle bounds = e.Item is ScalingToolStripDropDownButton scalingButton ? scalingButton.ArrowRectangle + : OSUtils.IsMono && e.Item is ToolStripMenuItem mi ? new Rectangle(e.ArrowRectangle.Left, 0, e.ArrowRectangle.Width, mi.Height) + : e.ArrowRectangle; + Color color = !e.Item.Enabled ? SystemColors.ControlDark + : SystemInformation.HighContrast ? e.Item.Selected && !e.Item.Pressed ? SystemColors.HighlightText : e.ArrowColor + : e.Item is ToolStripDropDownItem ? SystemColors.ControlText + : e.ArrowColor; + + DrawArrow(e.Graphics, color, bounds, e.Direction); + } + + /// + /// Changes to original: + /// - [HighContrast]: Not drawing the highlighted background if the menu item is disabled (this is already fixed in Core) + /// - [HighContrast]: Fixed bounds of highlight rectangle (it was good in .NET Framework but is wrong in Core) + /// + protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e) + { + if (!SystemInformation.HighContrast || e.Item is not ToolStripMenuItem item) + { + base.OnRenderMenuItemBackground(e); + return; + } + + // Selected/pressed menu point in high contrast mode: drawing the background only if enabled + var bounds = new Rectangle(2, 0, item.Width - 3, item.Height); + if (item.Pressed || item.Selected && item.Enabled) + e.Graphics.FillRectangle(SystemBrushes.Highlight, bounds); + else if (item.Selected && !item.Enabled) + e.Graphics.DrawRectangle(SystemPens.Highlight, bounds.X, bounds.Y, bounds.Width - 1, bounds.Height - 1); + } + + /// + /// Changes to original: + /// - When a menu item is selected, then not using its possible custom colors + /// - [HighContrast]: Fixing text color on highlighted menu items + /// + protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) + { + if (e.Item is ToolStripMenuItem mi) + { + e.TextColor = !mi.Enabled ? SystemColors.GrayText + : SystemInformation.HighContrast ? mi.Selected || mi.Pressed ? SystemColors.HighlightText : SystemColors.ControlText + : mi.Selected || mi.Pressed ? SystemColors.ControlText + : e.Item.ForeColor; + } + + base.OnRenderItemText(e); + } + + /// + /// Changes to original: + /// - Background image is omitted + /// - Not selected checked background uses fallback color if current theme has transparent checked background + /// - [HighContrast]: Not drawing border if button is pressed and checked (this is how the .NET Core version also works) + /// + protected override void OnRenderButtonBackground(ToolStripItemRenderEventArgs e) + { + ToolStripButton button = (ToolStripButton)e.Item; + Rectangle bounds = new Rectangle(Point.Empty, button.Size); + ButtonStyle style = (button.Pressed ? ButtonStyle.Pressed : 0) + | (button.Checked ? ButtonStyle.Checked : 0) + | (button.Selected ? ButtonStyle.Selected : 0); + + if (SystemInformation.HighContrast) + DrawHighContrastButtonBackground(e.Graphics, bounds, style); + else if (button.Enabled && style != ButtonStyle.None) + DrawThemedButtonBackground(e.Graphics, ColorTable, bounds, style); + else if (button.Owner != null && button.BackColor != button.Owner.BackColor) + e.Graphics.FillRectangle(button.BackColor.GetBrush(), bounds); + } + + /// + /// Changes to original: + /// - [HighContrast]: Dropped border color matches the menu border color + /// + protected override void OnRenderDropDownButtonBackground(ToolStripItemRenderEventArgs e) + { + ToolStripDropDownButton button = (ToolStripDropDownButton)e.Item; + Rectangle bounds = new Rectangle(Point.Empty, button.Size); + ButtonStyle style = (button.Pressed && button.HasDropDownItems ? ButtonStyle.Dropped : 0) + | (button.Pressed ? ButtonStyle.Pressed : 0) + | (button.Selected ? ButtonStyle.Selected : 0); + + if (SystemInformation.HighContrast) + DrawHighContrastButtonBackground(e.Graphics, bounds, style); + else if (button.Enabled && style != ButtonStyle.None) + DrawThemedButtonBackground(e.Graphics, ColorTable, bounds, style); + else if (button.Owner != null && button.BackColor != button.Owner.BackColor) + e.Graphics.FillRectangle(button.BackColor.GetBrush(), bounds); + } + + protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventArgs e) + { + #region Local Methods + + // Changes to original: + // - Background image is omitted + // - Separator width is ignored + // - The separator placement matches with high contrast mode. On 100% DPI this means 1 pixel shift so the image area is perfectly rectangular + // - Supporting AdvancedToolStripSplitButton checked state (rendering the same way as OnRenderButtonBackground does it) + static void DrawThemed(ToolStripItemRenderEventArgs e, ProfessionalColorTable colorTable, ButtonStyle style) + { + var button = (ToolStripSplitButton)e.Item; + Rectangle bounds = new Rectangle(Point.Empty, button.Size); + + // common part + ButtonStyle commonStyle = style & (ButtonStyle.Dropped | ButtonStyle.Selected); + if (commonStyle != ButtonStyle.None) + DrawThemedButtonBackground(e.Graphics, colorTable, bounds, commonStyle); + else if (button.Owner != null && button.BackColor != button.Owner.BackColor) + e.Graphics.FillRectangle(button.BackColor.GetBrush(), bounds); + + // button part + if ((style & ButtonStyle.Pressed) != 0 + || (style & ButtonStyle.Checked) != 0 + || (style & ButtonStyle.Selected) != 0 && (style & ButtonStyle.Dropped) == 0) + { + bounds = button.ButtonBounds; + if (OSUtils.IsMono) + bounds.Location = Point.Empty; + bounds.Width += 2; + if (button.RightToLeft == RightToLeft.Yes) + bounds.X -= 2; + + DrawThemedButtonBackground(e.Graphics, colorTable, bounds, style & ~ButtonStyle.Dropped); + } + + // arrow + bounds = button.DropDownButtonBounds; + if (OSUtils.IsMono) + bounds.X -= button.ButtonBounds.Left; + + DrawArrow(e.Graphics, button.Enabled ? SystemColors.ControlText : SystemColors.ControlDark, bounds, ArrowDirection.Down); + } + + // Changes to original: + // - Fixed arrow color + // - Fixed border color when button is not dropped + // - Supporting AdvancedToolStripSplitButton checked state (rendering the same way as OnRenderButtonBackground does it) + static void DrawHighContrast(ToolStripItemRenderEventArgs e, ButtonStyle style) + { + var button = (ToolStripSplitButton)e.Item; + Rectangle bounds = new Rectangle(Point.Empty, button.Size); + Rectangle dropBounds = button.DropDownButtonBounds; + + // common part + ButtonStyle commonStyle = style & (ButtonStyle.Dropped | ButtonStyle.Selected); + if (commonStyle != ButtonStyle.None) + DrawHighContrastButtonBackground(e.Graphics, bounds, commonStyle); + + // button part + if ((style & ButtonStyle.Pressed) != 0 + || (style & ButtonStyle.Checked) != 0 + || (style & ButtonStyle.Selected) != 0 && (style & ButtonStyle.Dropped) == 0) + { + bounds = button.ButtonBounds; + bounds.Width += 2; + if (button.RightToLeft == RightToLeft.Yes) + bounds.X -= 2; + + DrawHighContrastButtonBackground(e.Graphics, bounds, style & ~ButtonStyle.Dropped); + } + + // drop down border + Color arrowColor = SystemColors.ControlText; + if ((style & ButtonStyle.Dropped) == 0 && (style & ButtonStyle.Selected) != 0) + { + e.Graphics.DrawRectangle(SystemPens.ControlLight, dropBounds.X, dropBounds.Y, dropBounds.Width - 1, dropBounds.Height - 1); + arrowColor = SystemColors.HighlightText; + } + + // arrow + DrawArrow(e.Graphics, arrowColor, button.DropDownButtonBounds, ArrowDirection.Down); + } + + #endregion + + var button = (ToolStripSplitButton)e.Item; + ButtonStyle style = (button.DropDownButtonPressed ? ButtonStyle.Dropped : 0) + | (button.ButtonPressed ? ButtonStyle.Pressed : 0) + | (button.Selected ? ButtonStyle.Selected : 0) + | (button is AdvancedToolStripSplitButton { Checked: true } ? ButtonStyle.Checked : 0); + + if (SystemInformation.HighContrast) + DrawHighContrast(e, style); + else + DrawThemed(e, ColorTable, style); + } + + /// + /// Changes to original: + /// - Not drawing the default (possibly unscaled) check image + /// - Drawing the check background also in high contrast mode + /// - When VisualStyles are enabled, using slightly different colors than the original + /// + protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e) + { + int size = e.Item.Height; + Rectangle bounds = new Rectangle(e.Item.RightToLeft == RightToLeft.Yes ? e.Item.Width - size - 1 : OSUtils.IsMono ? 1 : 2, 0, size, size); + if (SystemInformation.HighContrast) + DrawHighContrastButtonBackground(e.Graphics, bounds, ButtonStyle.Selected); + else + DrawThemedButtonBackground(e.Graphics, ColorTable, bounds, e.Item.Selected ? ButtonStyle.Pressed : ButtonStyle.Selected); + } + + /// + /// Changes to original: + /// - Unlike Windows' base implementation, not drawing the checked menu item background again, which is already done by OnRenderItemCheck + /// - [Mono]: Scaling menu item images + /// - [HighContrast]: Shifting also clicked ToolStripSplitButton images just like for buttons + /// + protected override void OnRenderItemImage(ToolStripItemImageRenderEventArgs e) + { + if (e.Image == null) + return; + Rectangle bounds = e.ImageRectangle; + + // Fixing image scaling in menu items on Mono + if (OSUtils.IsMono && e.Item is ToolStripMenuItem) + bounds.Size = e.Item.Owner.ScaleSize(referenceSize); + // In high contrast mode shifting the pressed buttons by 1 pixel, including ToolStripSplitButton + else if (SystemInformation.HighContrast && e.Item is ToolStripButton { Pressed: true } or ToolStripSplitButton { ButtonPressed: true }) + bounds.X += 1; +#if NETFRAMEWORK + else if (e.Item is ScalingToolStripDropDownButton btn) + btn.AdjustImageRectangle(ref bounds); +#endif + + // On ToolStripSplitButtons the image originally is not quite centered + if (e.Item is ToolStripSplitButton) + bounds.X += e.Item.RightToLeft == RightToLeft.Yes ? -1 : 1; + + Image image = e.Item.Enabled ? e.Image : disabledImagesCache[e.Image]; + if (e.Item.ImageScaling == ToolStripItemImageScaling.None) + e.Graphics.DrawImage(image, bounds, new Rectangle(Point.Empty, bounds.Size), GraphicsUnit.Pixel); + else + e.Graphics.DrawImage(image, bounds); + } + + #endregion + + #endregion + } + + #endregion + + #region Fields + + #region Static Fields + + private static readonly Size referenceSize = new Size(16, 16); + + #endregion + + #region Instance Fields + + private readonly ToolTip? toolTip; + + private DockStyle explicitDock = DockStyle.Top; + private bool isAdjustingRtl; + + #endregion + + #endregion + + #region Constructors + + public AdvancedToolStrip() + { + ImageScalingSize = Size.Round(this.ScaleSize(referenceSize)); + Renderer = new AdvancedToolStripRenderer(); + toolTip = Reflector.TryGetProperty(this, nameof(ToolTip), out object? result) ? (ToolTip)result! + : Reflector.TryGetField(this, "tooltip_window", out result) ? (ToolTip)result! + : null; + + if (toolTip == null) + return; + toolTip = (ToolTip)Reflector.GetProperty(this, nameof(ToolTip))!; + toolTip.AutoPopDelay = 10_000; + + // Effectively used only when RTL is true because OwnerDraw is enabled only in that case + toolTip.Draw += ToolTip_Draw; + } + + #endregion + + #region Methods + + #region Static Methods + + private static void ToolTip_Draw(object sender, DrawToolTipEventArgs e) => e.DrawToolTipAdvanced(); + + #endregion + + #region Instance Methods + + protected override void WndProc(ref Message m) + { + base.WndProc(ref m); + + // ensuring that items can be clicked even if the container form is not activated + if (m.Msg == Constants.WM_MOUSEACTIVATE && m.Result == (IntPtr)Constants.MA_ACTIVATEANDEAT) + m.Result = (IntPtr)Constants.MA_ACTIVATE; + } + + protected override void OnItemAdded(ToolStripItemEventArgs e) + { + if (e.Item is ToolStripSplitButton splitBtn) + splitBtn.DropDownButtonWidth = this.ScaleWidth(11); + + base.OnItemAdded(e); + } + + protected override void OnSizeChanged(EventArgs e) + { + base.OnSizeChanged(e); + + // Preventing double scaling in Mono + if (OSUtils.IsMono && Dock.In(DockStyle.Top, DockStyle.Bottom)) + Height = this.ScaleHeight(25); + } + + protected override void OnDockChanged(EventArgs e) + { + base.OnDockChanged(e); + if (isAdjustingRtl) + return; + explicitDock = Dock; + } + + protected override void OnRightToLeftChanged(EventArgs e) + { + base.OnRightToLeftChanged(e); + bool isRtl = RightToLeft == RightToLeft.Yes; + if (toolTip != null) + toolTip.OwnerDraw = isRtl; + + DockStyle dock = Dock; + if (dock is not (DockStyle.Left or DockStyle.Right)) + return; + + if (isRtl ^ dock == explicitDock) + return; + isAdjustingRtl = true; + Dock = isRtl + ? explicitDock == DockStyle.Left ? DockStyle.Right : DockStyle.Left + : explicitDock; + isAdjustingRtl = false; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (toolTip != null) + toolTip.Draw -= ToolTip_Draw; + } + + base.Dispose(disposing); + } + + #endregion + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/AutoMirrorPanel.cs b/KGySoft.Drawing.ImagingTools/View/Controls/AutoMirrorPanel.cs new file mode 100644 index 0000000..b63f51c --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Controls/AutoMirrorPanel.cs @@ -0,0 +1,81 @@ +#region Usings + +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +#endregion + +#region Suppressions + +#if NETCOREAPP3_0 +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - Controls items are never null +#pragma warning disable CS8602 // Dereference of a possibly null reference. - Controls items are never null +#endif + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Controls +{ + /// + /// Just for mirroring content for RTL languages. + /// In this project all relevant controls are docked so handling the Dock property only. + /// + internal class AutoMirrorPanel : Panel + { + #region Fields + + private readonly List toBeAdjusted = new List(); + + #endregion + + #region Methods + + protected override void OnControlAdded(ControlEventArgs e) + { + // there is no public IsLayoutSuspended property but in this project we can assume that controls are either added + // in suspended state or before setting RightToLeft + base.OnControlAdded(e); + toBeAdjusted.Add(e.Control); + } + + protected override void OnLayout(LayoutEventArgs levent) + { + base.OnLayout(levent); + if (toBeAdjusted.Count == 0) + return; + + if (RightToLeft == RightToLeft.Yes) + { + foreach (Control control in toBeAdjusted) + { + // Adjusting docking only + DockStyle dockStyle = control.Dock; + if (dockStyle == DockStyle.Left) + control.Dock = DockStyle.Right; + else if (dockStyle == DockStyle.Right) + control.Dock = DockStyle.Left; + } + } + + toBeAdjusted.Clear(); + } + + protected override void OnRightToLeftChanged(EventArgs e) + { + base.OnRightToLeftChanged(e); + toBeAdjusted.Clear(); + foreach (Control control in Controls) + { + // Adjusting docking only + DockStyle dockStyle = control.Dock; + if (dockStyle == DockStyle.Left) + control.Dock = DockStyle.Right; + else if (dockStyle == DockStyle.Right) + control.Dock = DockStyle.Left; + } + } + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/BaseControl.cs b/KGySoft.Drawing.ImagingTools/View/Controls/BaseControl.cs index 87bb8ac..56cae7a 100644 --- a/KGySoft.Drawing.ImagingTools/View/Controls/BaseControl.cs +++ b/KGySoft.Drawing.ImagingTools/View/Controls/BaseControl.cs @@ -27,6 +27,12 @@ namespace KGySoft.Drawing.ImagingTools.View.Controls { internal class BaseControl : Control { + #region Fields + + protected static readonly int MouseWheelScrollDelta = OSUtils.IsMono && OSUtils.IsWindows ? 120 : SystemInformation.MouseWheelScrollDelta; + + #endregion + #region Events internal event EventHandler MouseHWheel @@ -43,6 +49,19 @@ protected override void WndProc(ref Message m) { switch (m.Msg) { + case Constants.WM_PAINT: + try + { + base.WndProc(ref m); + } + catch (Exception e) when (!e.IsCritical()) + { + // In Mono sometimes an internal GDI+ exception happens here + Invalidate(); + } + + break; + // Horizontal scroll case Constants.WM_MOUSEHWHEEL: HandledMouseEventArgs args = new HandledMouseEventArgs(MouseButtons.None, 0, diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/CheckGroupBox.cs b/KGySoft.Drawing.ImagingTools/View/Controls/CheckGroupBox.cs index 4fc0c56..2cc6ebd 100644 --- a/KGySoft.Drawing.ImagingTools/View/Controls/CheckGroupBox.cs +++ b/KGySoft.Drawing.ImagingTools/View/Controls/CheckGroupBox.cs @@ -26,9 +26,18 @@ #endregion +#region Suppressions + +#if NETCOREAPP3_0 +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - Controls items are never null +#pragma warning disable CS8604 // Possible null reference argument. - Controls items are never null +#endif + +#endregion + namespace KGySoft.Drawing.ImagingTools.View.Controls { - internal partial class CheckGroupBox : GroupBox + internal partial class CheckGroupBox : GroupBox, ICustomLocalizable { #region Events @@ -76,27 +85,24 @@ public bool Checked #region Constructors - [SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Whitespace")] [SuppressMessage("ReSharper", "LocalizableElement", Justification = "Whitespace")] + [SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "ReSharper issue")] public CheckGroupBox() { InitializeComponent(); Controls.Add(checkBox); + checkBox.SizeChanged += CheckBox_SizeChanged; - // Left should be 10 at 100% but only 8 at 175%, etc. - checkBox.Left = Math.Max(1, 13 - (int)(this.GetScale().X * Padding.Left)); - - // Vista or later: using System FlayStyle so animation is enabled with theming and while text is not misplaced with classic themes + // Vista or later: using System FlayStyle so animation is enabled with theming and text is not misplaced with classic themes bool visualStylesEnabled = Application.RenderWithVisualStyles; - checkBox.FlatStyle = OSUtils.IsVistaOrLater ? FlatStyle.System + checkBox.FlatStyle = OSUtils.IsMono ? FlatStyle.Standard + : OSUtils.IsVistaOrLater ? FlatStyle.System // Windows XP: Using standard style with themes so CheckBox color can be set correctly, and using System with classic theme for good placement - : OSUtils.IsWindows ? visualStylesEnabled ? FlatStyle.Standard : FlatStyle.System - // Non-windows (eg. Mono/Linux): Standard for best placement - : FlatStyle.Standard; + : visualStylesEnabled ? FlatStyle.Standard : FlatStyle.System; // GroupBox.FlayStyle must be the same as CheckBox; otherwise, System appearance would be transparent FlatStyle = checkBox.FlatStyle; - checkBox.CheckedChanged += this.CheckBox_CheckedChanged; + checkBox.CheckedChanged += CheckBox_CheckedChanged; // making sure there is enough space before the CheckBox at every DPI base.Text = " "; @@ -123,11 +129,25 @@ protected override void OnControlAdded(ControlEventArgs e) return; // when not in design mode, adding custom controls to a panel so we can toggle its Enabled with preserving their original state - if (contentPanel.Parent == null) - contentPanel.Parent = this; + contentPanel.Parent ??= this; e.Control.Parent = contentPanel; } + protected virtual void OnCheckedChanged(EventArgs e) => (Events[nameof(CheckedChanged)] as EventHandler)?.Invoke(this, e); + + protected override void OnSizeChanged(EventArgs e) + { + base.OnSizeChanged(e); + if (RightToLeft == RightToLeft.Yes) + ResetCheckBoxLocation(); + } + + protected override void OnRightToLeftChanged(EventArgs e) + { + base.OnRightToLeftChanged(e); + ResetCheckBoxLocation(); + } + protected override void Dispose(bool disposing) { if (disposing) @@ -137,6 +157,7 @@ protected override void Dispose(bool disposing) } checkBox.CheckedChanged -= CheckBox_CheckedChanged; + checkBox.SizeChanged -= CheckBox_SizeChanged; base.Dispose(disposing); } @@ -144,19 +165,49 @@ protected override void Dispose(bool disposing) #region Private Methods - private void OnCheckedChanged(EventArgs e) => (Events[nameof(CheckedChanged)] as EventHandler)?.Invoke(this, e); + private void ResetCheckBoxLocation() + => checkBox.Left = RightToLeft == RightToLeft.No + ? (int)(10 * this.GetScale().X) + : Width - checkBox.Width - (int)(10 * this.GetScale().X); #endregion #region Event handlers - private void CheckBox_CheckedChanged(object sender, EventArgs e) + private void CheckBox_CheckedChanged(object? sender, EventArgs e) { // Toggling the Enabled state of the content. This method preserves the original Enabled state of the controls. contentPanel.Enabled = checkBox.Checked; OnCheckedChanged(EventArgs.Empty); } + private void CheckBox_SizeChanged(object? sender, EventArgs e) => ResetCheckBoxLocation(); + + #endregion + + #region Explicitly Implemented Interface Methods + + void ICustomLocalizable.ApplyStringResources(ToolTip? toolTip) + { + string? name = Name; + if (String.IsNullOrEmpty(name)) + return; + + // Self properties + Res.ApplyStringResources(this, name); + + // tool tip: forwarding to the check box + if (toolTip != null) + { + string? value = Res.GetStringOrNull(name + "." + ControlExtensions.ToolTipPropertyName); + toolTip.SetToolTip(checkBox, value); + } + + // children: only contentPanel controls so checkBox is skipped (otherwise, could be overwritten by checkbox.Name) + foreach (Control child in contentPanel.Controls) + child.ApplyStringResources(toolTip); + } + #endregion #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/DownloadProgressFooter.cs b/KGySoft.Drawing.ImagingTools/View/Controls/DownloadProgressFooter.cs new file mode 100644 index 0000000..9be461d --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Controls/DownloadProgressFooter.cs @@ -0,0 +1,60 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: DownloadProgressFooter.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2020 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System.Windows.Forms; +using KGySoft.Drawing.ImagingTools.Model; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Controls +{ + internal class DownloadProgressFooter : ProgressFooter<(int MaximumValue, int CurrentValue)> + { + #region Properties + + internal override bool ProgressVisible + { + get => base.ProgressVisible; + set + { + if (value) + ProgressText = Res.TextDownloading; + base.ProgressVisible = value; + } + } + + #endregion + + #region Methods + + protected override void UpdateProgress() + { + var progress = Progress; + if (progress.MaximumValue == 0) + ProgressStyle = ProgressBarStyle.Marquee; + else + { + ProgressStyle = ProgressBarStyle.Blocks; + Maximum = progress.MaximumValue; + Value = progress.CurrentValue; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/DrawingProgressFooter.cs b/KGySoft.Drawing.ImagingTools/View/Controls/DrawingProgressFooter.cs new file mode 100644 index 0000000..c20dfc1 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Controls/DrawingProgressFooter.cs @@ -0,0 +1,57 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: DrawingProgressFooter.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2020 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System.Windows.Forms; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Controls +{ + internal class DrawingProgressFooter : ProgressFooter + { + #region Fields + + private DrawingProgress? displayedProgress; + + #endregion + + #region Methods + + protected override void UpdateProgress() + { + DrawingProgress progress = Progress; + if (progress == displayedProgress) + return; + + if (displayedProgress?.OperationType != progress.OperationType) + ProgressText = Res.Get(progress.OperationType); + if (progress.MaximumValue == 0) + ProgressStyle = ProgressBarStyle.Marquee; + else + { + ProgressStyle = ProgressBarStyle.Blocks; + Maximum = progress.MaximumValue; + Value = progress.CurrentValue; + } + + displayedProgress = progress; + } + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/DrawingProgressStatusStrip.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Controls/DrawingProgressStatusStrip.Designer.cs deleted file mode 100644 index b86e2b3..0000000 --- a/KGySoft.Drawing.ImagingTools/View/Controls/DrawingProgressStatusStrip.Designer.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Windows.Forms; - -namespace KGySoft.Drawing.ImagingTools.View.Controls -{ - partial class DrawingProgressStatusStrip - { - private void InitializeComponent() - { - this.components = new System.ComponentModel.Container(); - this.lblProgress = new System.Windows.Forms.ToolStripStatusLabel(); - this.pbProgress = new System.Windows.Forms.ToolStripProgressBar(); - this.timer = new System.Windows.Forms.Timer(this.components); - this.SuspendLayout(); - // - // lblProgress - // - this.lblProgress.Name = "lblProgress"; - this.lblProgress.Size = new System.Drawing.Size(0, 17); - // - // pbProgress - // - this.pbProgress.AutoSize = false; - this.pbProgress.Name = "pbProgress"; - this.pbProgress.Size = new System.Drawing.Size(100, 16); - // - // timer - // - this.timer.Interval = 30; - // - // DrawingProgressStatusStrip - // - this.BackColor = System.Drawing.Color.Transparent; - this.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.lblProgress, - this.pbProgress}); - this.SizingGrip = false; - this.ResumeLayout(false); - - } - - private ToolStripStatusLabel lblProgress; - private ToolStripProgressBar pbProgress; - private Timer timer; - private System.ComponentModel.IContainer components; - } -} diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/DrawingProgressStatusStrip.cs b/KGySoft.Drawing.ImagingTools/View/Controls/DrawingProgressStatusStrip.cs deleted file mode 100644 index 4906d61..0000000 --- a/KGySoft.Drawing.ImagingTools/View/Controls/DrawingProgressStatusStrip.cs +++ /dev/null @@ -1,159 +0,0 @@ -#region Copyright - -/////////////////////////////////////////////////////////////////////////////// -// File: DrawingProgressStatusStrip.cs -/////////////////////////////////////////////////////////////////////////////// -// Copyright (C) KGy SOFT, 2005-2020 - All Rights Reserved -// -// You should have received a copy of the LICENSE file at the top-level -// directory of this distribution. If not, then this file is considered as -// an illegal copy. -// -// Unauthorized copying of this file, via any medium is strictly prohibited. -/////////////////////////////////////////////////////////////////////////////// - -#endregion - -#region Usings - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Windows.Forms; - -#endregion - -namespace KGySoft.Drawing.ImagingTools.View.Controls -{ - internal partial class DrawingProgressStatusStrip : StatusStrip - { - #region Fields - - private readonly bool visualStyles = Application.RenderWithVisualStyles; - private bool progressVisible = true; // so ctor change will have effect at run-time - private DrawingProgress? displayedProgress; - - #endregion - - #region Properties - - [SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Whitespace")] - [SuppressMessage("ReSharper", "LocalizableElement", Justification = "Whitespace")] - internal bool ProgressVisible - { - get => progressVisible; - set - { - if (progressVisible == value) - return; - progressVisible = value; - if (value) - UpdateProgress(default); - - // In Windows we don't make label invisible but changing text to a space to prevent status strip height change - if (OSUtils.IsWindows) - { - pbProgress.Visible = progressVisible; - if (!progressVisible) - lblProgress.Text = " "; - } - // On Linux we let the progress bar remain visible for the same reason and to prevent (sort of) appearing ugly thick black areas - else - { - lblProgress.Visible = progressVisible; - if (!progressVisible) - { - pbProgress.Style = ProgressBarStyle.Blocks; - pbProgress.Value = 0; - } - - AdjustSize(); - } - - timer.Enabled = value; - } - } - - internal DrawingProgress Progress { get; set; } - - #endregion - - #region Constructors - - public DrawingProgressStatusStrip() - { - InitializeComponent(); - if (DesignMode) - return; - this.FixAppearance(); - ProgressVisible = false; - SizeChanged += DrawingProgressStatusStrip_SizeChanged; - lblProgress.TextChanged += lblProgress_TextChanged; - lblProgress.VisibleChanged += lblProgress_VisibleChanged; - timer.Tick += timer_Tick; - } - - #endregion - - #region Methods - - #region Protected Methods - - protected override void Dispose(bool disposing) - { - if (disposing) - components?.Dispose(); - - SizeChanged -= DrawingProgressStatusStrip_SizeChanged; - lblProgress.TextChanged -= lblProgress_TextChanged; - lblProgress.VisibleChanged -= lblProgress_VisibleChanged; - timer.Tick -= timer_Tick; - base.Dispose(disposing); - } - - #endregion - - #region Private Methods - - private void AdjustSize() => - pbProgress.Width = Width - (lblProgress.Visible ? lblProgress.Width - lblProgress.Margin.Horizontal : 0) - pbProgress.Margin.Horizontal - 2; - - private void UpdateProgress(DrawingProgress progress) - { - if (progress == displayedProgress) - return; - - if (displayedProgress?.OperationType != progress.OperationType) - lblProgress.Text = Res.Get(progress.OperationType); - if (progress.MaximumValue == 0) - pbProgress.Style = ProgressBarStyle.Marquee; - else - { - pbProgress.Style = ProgressBarStyle.Blocks; - pbProgress.Maximum = progress.MaximumValue; - - // Workaround for progress bar on Vista and above where it advances very slow - if (OSUtils.IsVistaOrLater && visualStyles && progress.CurrentValue > pbProgress.Value && progress.CurrentValue < progress.MaximumValue) - pbProgress.Value = progress.CurrentValue + 1; - pbProgress.Value = progress.CurrentValue; - } - - displayedProgress = progress; - } - - #endregion - - #region Event handlers -#pragma warning disable IDE1006 // Naming Styles - - private void lblProgress_TextChanged(object sender, EventArgs e) => AdjustSize(); - private void lblProgress_VisibleChanged(object sender, EventArgs e) => AdjustSize(); - private void DrawingProgressStatusStrip_SizeChanged(object sender, EventArgs e) => AdjustSize(); - - private void timer_Tick(object sender, EventArgs e) => UpdateProgress(Progress); - -#pragma warning restore IDE1006 // Naming Styles - #endregion - - #endregion - } -} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/ImageViewer.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Controls/ImageViewer.Designer.cs index 60feeb1..ed25152 100644 --- a/KGySoft.Drawing.ImagingTools/View/Controls/ImageViewer.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/Controls/ImageViewer.Designer.cs @@ -11,16 +11,18 @@ private void InitializeComponent() this.sbVertical = new System.Windows.Forms.VScrollBar(); this.SuspendLayout(); // - // hScrollBar + // sbHorizontal // + this.sbHorizontal.Cursor = System.Windows.Forms.Cursors.Arrow; this.sbHorizontal.Location = new System.Drawing.Point(0, 0); this.sbHorizontal.Name = "sbHorizontal"; this.sbHorizontal.Size = new System.Drawing.Size(80, 17); this.sbHorizontal.TabIndex = 0; this.sbHorizontal.Visible = false; // - // vScrollBar + // sbVertical // + this.sbVertical.Cursor = System.Windows.Forms.Cursors.Arrow; this.sbVertical.Location = new System.Drawing.Point(0, 0); this.sbVertical.Name = "sbVertical"; this.sbVertical.Size = new System.Drawing.Size(17, 80); @@ -32,6 +34,7 @@ private void InitializeComponent() this.Controls.Add(this.sbHorizontal); this.Controls.Add(this.sbVertical); this.ResumeLayout(false); + } private HScrollBar sbHorizontal; diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/ImageViewer.DisplayImageGenerator.cs b/KGySoft.Drawing.ImagingTools/View/Controls/ImageViewer.DisplayImageGenerator.cs new file mode 100644 index 0000000..15221ab --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Controls/ImageViewer.DisplayImageGenerator.cs @@ -0,0 +1,709 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: ImageViewer.DisplayImageGenerator.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2020 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Threading; + +using KGySoft.CoreLibraries; +using KGySoft.Drawing.Imaging; +using KGySoft.Drawing.ImagingTools.Model; +using KGySoft.Reflection; + +#endregion + +#region Suppressions + +#pragma warning disable CS1690 // Accessing a member on a field of a marshal-by-reference class may cause a runtime exception - false alarm, ImageViewer is never a remote object. + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Controls +{ + internal partial class ImageViewer + { + #region PreviewGenerator class + + private sealed class DisplayImageGenerator : IDisposable + { + #region Nested classes + + #region GenerateDefaultImageTask class + + private sealed class GenerateDefaultImageTask : AsyncTaskBase + { + #region Fields + + internal Bitmap SourceBitmap = default!; + internal bool InvalidateOwner; + + #endregion + } + + #endregion + + #region GenerateResizedImageTask class + + private sealed class GenerateResizedImageTask : AsyncTaskBase + { + #region Fields + + internal Image SourceImage = default!; + internal Size Size; + + #endregion + } + + #endregion + + #endregion + + #region Constants + + private const int sizeThreshold = 1024; + + #endregion + + #region Fields + + #region Static Fields + + /// + /// These formats are not are not supported by Graphics even though a Bitmap can use them. + /// On Linux/Mono some formats are completely unsupported but they do not appear here. + /// + private static readonly PixelFormat[] unsupportedFormats = OSUtils.IsWindows + ? new[] { PixelFormat.Format16bppGrayScale } + : new[] { PixelFormat.Format16bppRgb555, PixelFormat.Format16bppRgb565 }; + + /// + /// These formats are so slow that it is still faster to generate a 32bpp clone first than display them directly. + /// + private static readonly PixelFormat[] slowFormats = OSUtils.IsWindows + ? new[] { PixelFormat.Format48bppRgb, PixelFormat.Format64bppArgb, PixelFormat.Format64bppArgb } + : Reflector.EmptyArray(); + + #endregion + + #region Instance Fields + + private readonly ImageViewer owner; + + private volatile bool disposed; + + /// + /// true if generator can generate new content. Turned off on low memory or by . Invalidating the image enables it again. + /// + private bool enabled; + + private GenerateDefaultImageTask? generateDefaultImageTask; + private GenerateResizedImageTask? generateResizedImageTask; + + /// + /// The default image to be displayed when no resized display image is needed or while its generation is in progress. + /// Set by . If is true, then contains + /// - A fast PARGB32 clone of the original image it that is a Bitmap + /// - A clone of the original image if that is a Metafile so the original image will not be blocked to generate resized images + /// Otherwise, it is the same reference as the owner.Image. + /// If is false, then may contain the original image even if it cannot be displayed. + /// + private volatile Image? defaultDisplayImage; + private volatile bool isDefaultImageCloned; + + /// + /// If not null, contains the last cached size-adjusted display image. + /// It is not disposed immediately when a new size () is started to be generated + /// so it can be re-used when toggling smooth zooming. + /// + private volatile Bitmap? resizedDisplayImage; + + /// + /// Just to cache .Size, + /// because accessing it on without locking can lead to "object is used elsewhere" error. + /// + private Size resizedDisplayImageSize; + + /// + /// The currently requested size of the size adjusted image. If it is the same as , + /// then can be displayed. + /// + private Size requestedSize; + + #endregion + + #endregion + + #region Properties + + // This is alright, this class is private. + internal object SyncRoot => this; + + #endregion + + #region Constructors + + internal DisplayImageGenerator(ImageViewer owner) => this.owner = owner; + + #endregion + + #region Methods + + #region Static Methods + + private static void CancelRunningGenerate(AsyncTaskBase? task) + { + if (task == null) + return; + task.IsCanceled = true; + } + + private static void WaitForPendingGenerate(AsyncTaskBase? task) + { + if (task == null) + return; + task.WaitForCompletion(); + task.Dispose(); + } + + #endregion + + #region Instance Methods + + #region Public Methods + + public void Dispose() + { + if (disposed) + return; + Free(); + disposed = true; + } + + #endregion + + #region Internal Methods + + internal void InvalidateImages() + { + // This cancels all tasks and disposes every generating resources + Free(); + Debug.Assert(generateDefaultImageTask?.IsCanceled != false && generateResizedImageTask?.IsCanceled != false); + + // (Re-)enabling generating images + enabled = true; + } + + internal void InvalidateDisplayImage() + { + // Just canceling possible running generate. Not even clearing the possible already generated image. + // A new task will be started if a new paint explicitly requires it, in which case the last image can be re-used if possible. + CancelRunningGenerate(generateResizedImageTask); + } + + internal (Image?, InterpolationMode) GetDisplayImage() + { + Debug.Assert(owner.image != null); + InterpolationMode interpolationMode = InterpolationMode.NearestNeighbor; + + // 1.) Returning with a size adjusted display image + if (owner.smoothZooming && resizedDisplayImageSize == owner.targetRectangle.Size) + return (resizedDisplayImage, interpolationMode); + + // 2.) Checking if there is an already available default image. It might have to be resized on painting. + Image? result = defaultDisplayImage; + + // Smoothing Bitmap: leaving NearestNeighbor if a resized image is expected to be generated; + // otherwise, using some interpolation to be applied during painting + if (!owner.isMetafile && owner.smoothZooming) + { + float zoom = owner.zoom; + Size size = owner.imageSize; + + // >4x zoom or shrunk image that is not greater than generating threshold: using HighQualityBicubic + if (zoom >= 4f || zoom < 1f && size.Width <= sizeThreshold && size.Height <= sizeThreshold) + interpolationMode = InterpolationMode.HighQualityBicubic; + // 1-4x zoom: HighQualityBilinear for large images to prevent heavy lagging; otherwise, HighQualityBicubic + else if (zoom > 1f) + interpolationMode = size.Width > sizeThreshold || size.Height > sizeThreshold ? InterpolationMode.HighQualityBilinear : InterpolationMode.HighQualityBicubic; + // Shrinking of larger images if generating is disabled: applying a hopefully-not-too-slow fallback interpolation + else if (!enabled && zoom < 1f) + interpolationMode = owner.targetRectangle.Width > sizeThreshold || owner.targetRectangle.Height > sizeThreshold ? InterpolationMode.Bilinear : InterpolationMode.Bicubic; + } + + // 3.) Starting to generate cached images if needed + if (enabled) + { + if (result == null) + BeginGenerateDefaultDisplayImageIfNeeded(); + + BeginGenerateResizedDisplayImageIfNeeded(); + } + + // 4.) Returning either a generated display or the original image + if (result != null) + // here we already have a default display image we can return with + return (result, interpolationMode); + + // Waiting for the display image to be generated if pixel format is not supported, + // or it is so slow (>= 48bpp) that it is faster to wait for the converted image and paint the existing one. + // Note: Not using async because this project targets also .NET 3.5 and the image is locked anyway also in paint + if (owner.pixelFormat.In(unsupportedFormats) || owner.pixelFormat.In(slowFormats)) + generateDefaultImageTask?.WaitForCompletion(); + + // Too low memory: turning off image generation and freeing up resources. + if (!enabled) + { + Free(); + + // Assigning by original image to defaultDisplayImage so even large >= 48bpp images will be drawn directly. + defaultDisplayImage = owner.image; + } + + // Unless a default image has been generated in the meantime we return with the original image, or null, if its pixel format is not supported. + result = defaultDisplayImage ?? owner.image; + if (ReferenceEquals(result, owner.image) && owner.pixelFormat.In(unsupportedFormats)) + result = null; + + return (result, interpolationMode); + } + + #endregion + + #region Private Methods + + private void Free() + { + // disabling to prevent starting new tasks while freeing resources + enabled = false; + CancelRunningGenerate(generateDefaultImageTask); + CancelRunningGenerate(generateResizedImageTask); + + WaitForPendingGenerate(generateDefaultImageTask); + WaitForPendingGenerate(generateResizedImageTask); + + requestedSize = default; + + lock (SyncRoot) + { + if (isDefaultImageCloned) + defaultDisplayImage?.Dispose(); + defaultDisplayImage = null; + isDefaultImageCloned = false; + + resizedDisplayImageSize = default; + resizedDisplayImage?.Dispose(); + resizedDisplayImage = null; + } + } + + private void BeginGenerateDefaultDisplayImageIfNeeded() + { + Debug.Assert(owner.image != null && owner.pixelFormat != default); + + // A task is already running or the display image is already generated. + if (isDefaultImageCloned || generateDefaultImageTask != null) + return; + + Image image = owner.image!; + Bitmap? bitmap = image as Bitmap; + + // Metafile: The default image is the same as the original. If anti-aliased images are required, a clone is created on demand from that task + // Bitmap: generating a new default image for unsupported formats, + bool isGenerateNeeded = bitmap != null && (owner.pixelFormat.In(unsupportedFormats) + // for non-PARGB32 images larger than 256x256 - note: leaving even slow formats unconverted below sizeThreshold / 4 + || owner.pixelFormat != PixelFormat.Format32bppPArgb && (owner.imageSize.Width > sizeThreshold >> 2 || owner.imageSize.Height > sizeThreshold >> 2) + // and for native icons: converting because icons are handled oddly by GDI+, for example, the first column has half pixel width + || bitmap.RawFormat.Guid == ImageFormat.Icon.Guid); + + if (!isGenerateNeeded) + { + // A generated default image is set from another thread so handling possible concurrency. + if (defaultDisplayImage == null) + Interlocked.CompareExchange(ref defaultDisplayImage, image, null); + return; + } + + var task = new GenerateDefaultImageTask + { + SourceBitmap = bitmap!, + InvalidateOwner = bitmap!.RawFormat.Guid == ImageFormat.Icon.Guid + }; + generateDefaultImageTask = task; + ThreadPool.QueueUserWorkItem(GenerateDefaultImage!, task); + } + + private void BeginGenerateResizedDisplayImageIfNeeded() + { + Debug.Assert(owner.image != null && owner.pixelFormat != default); + + // Metafile: If smoothing edges is enabled + // Bitmap: If smoothing resize is enabled, the image is shrunk and image size is larger than 1024x1024 + bool isGenerateNeeded = owner.smoothZooming && (owner.isMetafile + || owner.zoom < 1f && (owner.imageSize.Width > sizeThreshold || owner.imageSize.Height > sizeThreshold)); + + // Not canceling the possible generate task here. It will call an invalidate in the end and we can see whether we use the result. + Size size = owner.targetRectangle.Size; + if (!isGenerateNeeded || size.Width < 1 || size.Height < 1) + { + requestedSize = default; + return; + } + + requestedSize = size; + GenerateResizedImageTask? task = generateResizedImageTask; + if (task != null) + { + // If there is already a running generate task + if (!task.IsCanceled) + { + // It is generating the same size: we keep it + if (task.Size == size) + return; + + // We just initiate cancellation but not awaiting the completion. + task.IsCanceled = true; + } + + // We do not await the task (we are in a lock here that is used in the task, too). + // Instead, we invalidate the owner so another paint will be triggered some time later. Hopefully the task will have been finished by that time. + owner.Invalidate(); + return; + } + + Debug.Assert(generateResizedImageTask == null); + task = new GenerateResizedImageTask + { + SourceImage = owner.image!, + Size = size + }; + + generateResizedImageTask = task; + ThreadPool.QueueUserWorkItem(GenerateResizedImage!, task); + } + + private void GenerateDefaultImage(object state) + { + #region Local Methods + + static Bitmap? DoGenerateDefaultImage(GenerateDefaultImageTask task, ref bool enabled) + { + Size size = task.SourceBitmap.Size; + + // skipping generating clone if there is not enough memory and it would only serve performance + // x4: because we want to convert to 32bpp + long managedPressure = size.Width * size.Height * 4; + if (!MemoryHelper.CanAllocate(managedPressure) && !task.SourceBitmap.PixelFormat.In(unsupportedFormats)) + task.IsCanceled = true; + + Bitmap? result = null; + try + { + result = new Bitmap(size.Width, size.Height, PixelFormat.Format32bppPArgb); + using IReadableBitmapData src = task.SourceBitmap.GetReadableBitmapData(); + using IWritableBitmapData dst = result.GetWritableBitmapData(); + + // here allowing to use max parallelization as the original image is locked anyway + var cfg = new AsyncConfig { IsCancelRequestedCallback = () => task.IsCanceled, ThrowIfCanceled = false }; + + // Not using Task and await because we want to be compatible with .NET 3.5, too. + IAsyncResult asyncResult = src.BeginCopyTo(dst, asyncConfig: cfg); + + // As we are already on a pool thread the End... call does not block the UI. It's still not the same as CopyTo() due to cancellation support. + asyncResult.EndCopyTo(); + } + catch (Exception) + { + // Despite all of the preconditions the memory could not be allocated or some other error occurred (yes, we catch even OutOfMemoryException here) + // NOTE: practically we always can recover from here: we simply don't use a generated clone and the worker thread can be finished + task.IsCanceled = true; + enabled = false; + } + finally + { + if (task.IsCanceled) + { + result?.Dispose(); + result = null; + } + } + + return result; + } + + #endregion + + var task = (GenerateDefaultImageTask)state; + + try + { + // canceled or lost race + if (task.IsCanceled || isDefaultImageCloned || task.SourceBitmap != owner.image || !enabled || disposed) + return; + + Bitmap? result = null; + + // Locking on the image to avoid the possible "bitmap region is already locked" issue. + // Until the default image is generated, it is locked during the paint, too. + lock (task.SourceBitmap) + { + try + { + // Generating the actual result. IsCanceled might be true if the lock above could not be immediately acquired + if (!task.IsCanceled) + result = DoGenerateDefaultImage(task, ref enabled); + } + finally + { + task.SetCompleted(); + } + } + + if (result == null || task.IsCanceled) + return; + + defaultDisplayImage = result; + isDefaultImageCloned = true; + + // only for icons because otherwise the appearance is the same + if (task.InvalidateOwner) + owner.Invalidate(); + } + finally + { + task.Dispose(); + generateDefaultImageTask = null; + } + } + + private void GenerateResizedImage(object state) + { + #region Local Methods + + static Bitmap? GenerateResizedMetafile(GenerateResizedImageTask task, ref bool enabled) + { + // For the resizing large managed buffer of source.Height * target.Width of ColorF (16 bytes) is allocated internally. To be safe we count with the doubled sizes. + Size doubledSize = new Size(task.Size.Width << 1, task.Size.Height << 1); + long managedPressure = doubledSize.Width * doubledSize.Height * 16; + if (!MemoryHelper.CanAllocate(managedPressure)) + task.IsCanceled = true; + + if (task.IsCanceled) + return null; + + // MetafileExtensions.ToBitmap does the same if anti aliasing is requested but this way the process can be canceled + Bitmap? result = null; + Bitmap? doubled = null; + try + { + doubled = new Bitmap(task.SourceImage, task.Size.Width << 1, task.Size.Height << 1); + + if (!task.IsCanceled) + { + result = new Bitmap(task.Size.Width, task.Size.Height, PixelFormat.Format32bppPArgb); + using IReadableBitmapData src = doubled.GetReadableBitmapData(); + using IReadWriteBitmapData dst = result.GetReadWriteBitmapData(); + + // not using Task and await we want to be compatible with .NET 3.5 + IAsyncResult asyncResult = src.BeginDrawInto(dst, + new Rectangle(Point.Empty, doubled.Size), + new Rectangle(Point.Empty, task.Size), + asyncConfig: new AsyncConfig + { + IsCancelRequestedCallback = () => task.IsCanceled, + ThrowIfCanceled = false, + MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount - 2) + }); + + // As we are already on a pool thread this is not a UI blocking call + asyncResult.EndDrawInto(); + } + } + catch (Exception) + { + // Despite all of the preconditions the memory could not be allocated or some other error occurred (yes, we catch even OutOfMemoryException here) + // NOTE: practically we always can recover from here: we simply don't use a generated preview and the worker thread can be finished + task.IsCanceled = true; + enabled = false; + } + finally + { + doubled?.Dispose(); + if (task.IsCanceled) + { + result?.Dispose(); + result = null; + } + } + + return result; + } + + static Bitmap? GenerateResizedBitmap(GenerateResizedImageTask task, ref bool enabled) + { + // BitmapExtensions.Resize does the same but this way the process can be canceled + Bitmap? result = null; + try + { + result = new Bitmap(task.Size.Width, task.Size.Height, PixelFormat.Format32bppPArgb); + lock (task.SourceImage) + { + using IReadableBitmapData src = ((Bitmap)task.SourceImage).GetReadableBitmapData(); + using IReadWriteBitmapData dst = result.GetReadWriteBitmapData(); + + // not using Task and await we want to be compatible with .NET 3.5 + IAsyncResult asyncResult = src.BeginDrawInto(dst, + new Rectangle(Point.Empty, task.SourceImage!.Size), + new Rectangle(Point.Empty, task.Size), + asyncConfig: new AsyncConfig + { + IsCancelRequestedCallback = () => task.IsCanceled, + ThrowIfCanceled = false, + MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount - 2) + }); + + // As we are already on a pool thread the End... call does not block the UI. + asyncResult.EndDrawInto(); + } + } + catch (Exception) + { + // Despite all of the preconditions the memory could not be allocated or some other error occurred (yes, we catch even OutOfMemoryException here) + // NOTE: practically we always can recover from here: we simply don't use a generated preview and the worker thread can be finished + task.IsCanceled = true; + enabled = false; + } + finally + { + if (task.IsCanceled) + { + result?.Dispose(); + result = null; + } + } + + return result; + } + + #endregion + + var task = (GenerateResizedImageTask)state; + + try + { + // canceled or lost race + if (task.IsCanceled || task.SourceImage != owner.image || task.Size != requestedSize || !enabled || disposed) + return; + + // returning if we already have the result + if (task.Size == resizedDisplayImageSize) + { + owner.Invalidate(); + return; + } + + // Before creating the preview releasing previous cached result. It is important to free it here, before checking the free memory. + // The lock ensures that no disposed image is displayed + lock (SyncRoot) + { + resizedDisplayImageSize = default; + resizedDisplayImage?.Dispose(); + resizedDisplayImage = null; + } + + // 1.) If there is no cloned display image generating that one first so the UI can use that while the original image will be free to create the resized images from. + if (!isDefaultImageCloned) + { + // The clone is just being generated. Invalidating and returning to come back later. + if (defaultDisplayImage == null || generateDefaultImageTask != null) + { + owner.Invalidate(); + return; + } + + Debug.Assert(ReferenceEquals(owner.image, defaultDisplayImage), "If isDefaultImageCloned is false, then defaultDisplayImage is expected to be the original instance here."); + Debug.Assert(owner.isMetafile || owner.pixelFormat == PixelFormat.Format32bppPArgb, "Clone is expected to be missing for metafiles and 32bpp PARGB bitmaps only."); + Image clone; + + // This may block the UI in OnPaint but once the clone is created OnPaint will use that instead of the original image. + lock (task.SourceImage) + { + try + { + // we do not allow canceling this part because this would be started again and again + clone = owner.image is Bitmap bitmap + ? bitmap.ConvertPixelFormat(PixelFormat.Format32bppPArgb) + : (Image)owner.image.Clone(); + } + catch (Exception) + { + enabled = false; + return; + } + } + + defaultDisplayImage = clone; + isDefaultImageCloned = true; + } + + if (task.IsCanceled) + return; + + // 2.) Generating the size-adjusted display image + Bitmap? result = null; + try + { + if (!task.IsCanceled) + result = task.SourceImage is Metafile ? GenerateResizedMetafile(task, ref enabled) : GenerateResizedBitmap(task, ref enabled); + } + finally + { + task.SetCompleted(); + } + + // setting latest cache (even if the task has been canceled as we have a completed result) + if (result != null) + { + resizedDisplayImage = result; + resizedDisplayImageSize = task.Size; + } + + if (task.IsCanceled) + return; + + owner.Invalidate(); + } + finally + { + task.Dispose(); + generateResizedImageTask = null; + } + } + + #endregion + + #endregion + + #endregion + } + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/ImageViewer.cs b/KGySoft.Drawing.ImagingTools/View/Controls/ImageViewer.cs index 33b4f32..acd0afd 100644 --- a/KGySoft.Drawing.ImagingTools/View/Controls/ImageViewer.cs +++ b/KGySoft.Drawing.ImagingTools/View/Controls/ImageViewer.cs @@ -17,17 +17,12 @@ #region Usings using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Threading; using System.Windows.Forms; -using KGySoft.CoreLibraries; -using KGySoft.Drawing.Imaging; -using KGySoft.Drawing.ImagingTools.Model; using KGySoft.Drawing.ImagingTools.WinApi; #endregion @@ -39,8 +34,6 @@ namespace KGySoft.Drawing.ImagingTools.View.Controls /// internal partial class ImageViewer : BaseControl { - #region Nested types - #region InvalidateFlags enum [Flags] @@ -49,493 +42,44 @@ private enum InvalidateFlags None, Sizes = 1, DisplayImage = 1 << 1, - All = Sizes | DisplayImage + Image = 1 << 2, + All = Sizes | DisplayImage | Image } #endregion - #region PreviewGenerator class - - private sealed class PreviewGenerator : IDisposable - { - #region Nested classes - - private sealed class GenerateTask : AsyncTaskBase - { - #region Fields - - internal Image SourceImage; - internal Size Size; - - #endregion - } - - #endregion - - #region Fields - - private readonly ImageViewer owner; - private readonly object syncRootGenerate = new object(); - - private bool enabled; - private GenerateTask activeTask; - - private Image sourceClone; - private Image safeDefaultImage; // The default image displayed when no generated preview is needed or while generation is in progress - private bool isClonedSafeDefaultImage; - private Size requestedSize; - - [SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "False alarm, it is either equals safeDefaultImage or currentPreview")] - private volatile Image displayImage; // The actual displayed image. If not null, it is either equals safeDefaultImage or currentPreview. - private volatile Bitmap cachedDisplayImage; // The lastly generated display image. Can be unused but is cached until a next preview is generated. - private Size currentCachedDisplayImage; // just to cache cachedDisplayImage.Size, because accessing currentPreview can lead to "object is used elsewhere" error - - #endregion - - #region Constructors - - internal PreviewGenerator(ImageViewer owner) - { - this.owner = owner; - enabled = true; - } - - #endregion - - #region Methods -#pragma warning disable CS1690 // Accessing a member on a field of a marshal-by-reference class may cause a runtime exception - false alarm, owner is never a remote object - - #region Public Methods - - public void Dispose() => Free(); - - #endregion - - #region Internal Methods - - [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", Justification = "False alarm, image is not a remote object")] - internal Image GetDisplayImage(bool generateSyncIfNull) - { - Image result = displayImage; - if (result != null || !generateSyncIfNull) - return result; - - if (safeDefaultImage == null) - { - Image image = owner.image; - Debug.Assert(image != null, "Image is not expected to be null here"); - PixelFormat pixelFormat = image.PixelFormat; - - try - { - // Converting non supported or too slow pixel formats - if (pixelFormat.In(convertedFormats)) - { - // Locking on display image so if it is the same as the original image, which is also locked when accessing its bitmap data - // the "bitmap region is already locked" can be avoided. Important: this cannot be ensured without locking here internally because - // OnPaint can occur any time after invalidating. - isClonedSafeDefaultImage = true; - lock (image) - safeDefaultImage = pixelFormat == PixelFormat.Format16bppGrayScale - ? image.ConvertPixelFormat(PixelFormat.Format8bppIndexed, PredefinedColorsQuantizer.Grayscale()) - : image.ConvertPixelFormat(pixelFormat.HasAlpha() ? PixelFormat.Format32bppPArgb : PixelFormat.Format24bppRgb); - } - - // Raw icons: converting because icons are handled oddly by GDI+, for example, the first column has half pixel width - else if (image is Bitmap bmp && bmp.RawFormat.Guid == ImageFormat.Icon.Guid) - { - isClonedSafeDefaultImage = true; - lock (image) - safeDefaultImage = bmp.CloneCurrentFrame(); - } - else - safeDefaultImage = image; - } - catch (Exception e) when (!e.IsCriticalGdi()) - { - // It may happen if no clone could be created (maybe on low memory) - // If pixel format is not supported at all then we let rendering die; otherwise, it may work but slowly or with visual glitches - isClonedSafeDefaultImage = false; - safeDefaultImage = image; - } - } - - // it is possible that we have a displayImage now but if not we return the default - return displayImage ??= safeDefaultImage; - } - - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "False alarm, task is passed to DoGenerate")] - [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", Justification = "False alarm, image is not a remote object")] - internal void BeginGenerateDisplayImage() - { - CancelRunningGenerate(); - if (!enabled) - return; - - Image image = owner.image; - if (image == null) - { - Debug.Assert(cachedDisplayImage == null && displayImage == null); - return; - } - - Size size = owner.targetRectangle.Size; - bool isGenerateNeeded = owner.isMetafile - ? owner.smoothZooming - : owner.zoom < 1f && (owner.imageSize.Width >= generateThreshold || owner.imageSize.Height >= generateThreshold); - - if (!isGenerateNeeded || size.Width < 1 || size.Height < 1) - { - displayImage = safeDefaultImage; - return; - } - - requestedSize = size; - ThreadPool.QueueUserWorkItem(DoGenerate, new GenerateTask { SourceImage = sourceClone, Size = size }); - } - - internal void Free() - { - CancelRunningGenerate(); - WaitForPendingGenerate(); - requestedSize = default; - displayImage = null; - sourceClone?.Dispose(); - sourceClone = null; - if (isClonedSafeDefaultImage) - safeDefaultImage?.Dispose(); - safeDefaultImage = null; - isClonedSafeDefaultImage = false; - FreeCachedPreview(); - enabled = true; - } - - #endregion - - #region Private Methods - - private void CancelRunningGenerate() - { - GenerateTask runningTask = activeTask; - if (runningTask == null) - return; - runningTask.IsCanceled = true; - } - - private void WaitForPendingGenerate() - { - // In a non-UI thread it should be in a lock - GenerateTask runningTask = activeTask; - if (runningTask == null) - return; - runningTask.WaitForCompletion(); - runningTask.Dispose(); - activeTask = null; - } - - private bool TrySetPreview(Image reference, Size size) - { - if (sourceClone != null && reference != sourceClone) - { - Debug.Assert(cachedDisplayImage != displayImage, "If image has been replaced in owner, its display image is not expected to be cached here"); - FreeCachedPreview(); - return false; - } - - // we don't free generated preview here maybe it can be re-used later (eg. toggling metafile smooth zooming) - if (currentCachedDisplayImage != size) - return false; - - if (displayImage == cachedDisplayImage) - return true; - - Debug.WriteLine($"Re-using pregenerated preview of size {size.Width}x{size.Height}"); - displayImage = cachedDisplayImage; - owner.Invalidate(); - return true; - } - - [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", Justification = "False alarm, this is not a remote object and is not exposed publicly")] - private void FreeCachedPreview() - { - lock (this) // It is alright, this is a private class. ImageViewer also locks on this instance when obtains display image so this ensures that no disposed image is painted. - { - if (displayImage != null && displayImage == cachedDisplayImage) - { - displayImage = null; - owner.Invalidate(); - } - - Bitmap toFree = cachedDisplayImage; - cachedDisplayImage = null; - toFree?.Dispose(); - currentCachedDisplayImage = default; - } - } - - [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", Justification = "False alarm, task.ReferenceImage is not a remote object")] - private void DoGenerate(object state) - { - var task = (GenerateTask)state; - - // this is a fairly large lock ensuring that only one generate task is running at once - lock (syncRootGenerate) - { - // lost race - if (task.SourceImage != sourceClone || task.Size != requestedSize || !enabled) - { - task.Dispose(); - return; - } - - // checking if we already have the preview - if (!task.IsCanceled) - { - if (TrySetPreview(task.SourceImage, task.Size)) - { - task.Dispose(); - return; - } - - // Before creating the preview releasing previous cached result. It is important to free it here, before checking the free memory. - FreeCachedPreview(); - } - - if (task.SourceImage == null) - { - Debug.Assert(sourceClone == null && owner.image != null); - Image image = owner.image; - - // As OnPaint can occur any time in the UI thread we lock on it. See also PaintImage. - lock (image) - { - try - { - // A clone must be created to use the image without locking later on and getting an "object is used elsewhere" error from paint. - // This is created synchronously so it can be used as a reference in the generating tasks. - if (owner.isMetafile) - sourceClone = (Metafile)image.Clone(); - else - { - PixelFormat pixelFormat = image.PixelFormat; - var bmp = (Bitmap)image; - - // clone is tried to be compact, fast and compatible - sourceClone = pixelFormat.In(PixelFormat.Format32bppArgb, PixelFormat.Format32bppPArgb, PixelFormat.Format64bppArgb, PixelFormat.Format64bppPArgb) ? bmp.ConvertPixelFormat(PixelFormat.Format32bppPArgb) - : pixelFormat.In(PixelFormat.Format24bppRgb, PixelFormat.Format32bppRgb, PixelFormat.Format48bppRgb) ? bmp.ConvertPixelFormat(PixelFormat.Format24bppRgb) - : pixelFormat == PixelFormat.Format16bppGrayScale ? bmp.ConvertPixelFormat(PixelFormat.Format8bppIndexed, PredefinedColorsQuantizer.Grayscale()) - : pixelFormat.In(convertedFormats) ? bmp.ConvertPixelFormat(PixelFormat.Format32bppPArgb) - : bmp.CloneCurrentFrame(); - } - - task.SourceImage = sourceClone; - } - catch (Exception e) when (!e.IsCriticalGdi()) - { - // Disabling preview generation if we could not create the clone (eg. on low memory) - // It will be re-enabled when owner.Image is reset. - enabled = false; - sourceClone?.Dispose(); - sourceClone = null; - return; - } - } - } - - Debug.Assert(activeTask?.IsCanceled != false); - WaitForPendingGenerate(); - Debug.Assert(activeTask == null); - - // from now on the task can be canceled - activeTask = task; - - try - { - Bitmap result = null; - try - { - if (!task.IsCanceled) - result = task.SourceImage is Metafile ? GenerateMetafilePreview(task) : GenerateBitmapPreview(task); - } - finally - { - task.SetCompleted(); - } - - if (result != null) - { - // setting latest cache (even if the task has been canceled since the generating the completed result) - currentCachedDisplayImage = task.Size; - cachedDisplayImage = result; - } - - if (task.IsCanceled) - return; - - Debug.WriteLine("Applying generated result"); - Debug.Assert(displayImage == null || displayImage == safeDefaultImage, "Display image is not the same as the original one: dispose is necessary"); - - // not freeing the display image because it is always the original image here - displayImage = result; - owner.Invalidate(); - } - finally - { - task.Dispose(); - activeTask = null; - } - } - } - - private static Bitmap GenerateMetafilePreview(GenerateTask task) - { - // For the resizing large managed buffer of source.Height * target.Width of ColorF (16 bytes) is allocated internally. To be safe we count with the doubled sizes. - Size doubledSize = new Size(task.Size.Width << 1, task.Size.Height << 1); - long managedPressure = doubledSize.Width * doubledSize.Height * 16; - if (!MemoryHelper.CanAllocate(managedPressure)) - { - Debug.WriteLine($"Discarding task because there is no {managedPressure:N0} bytes of available managed memory"); - task.IsCanceled = true; - } - - if (task.IsCanceled) - return null; - // MetafileExtensions.ToBitmap does the same if anti aliasing is requested but this way the process can be canceled - Debug.WriteLine($"Generating anti aliased image {task.Size.Width}x{task.Size.Height} on thread #{Thread.CurrentThread.ManagedThreadId}"); - Bitmap result = null; - Bitmap doubled = null; - try - { - doubled = new Bitmap(task.SourceImage, task.Size.Width << 1, task.Size.Height << 1); - if (!task.IsCanceled) - { - result = new Bitmap(task.Size.Width, task.Size.Height, PixelFormat.Format32bppPArgb); - using IReadableBitmapData src = doubled.GetReadableBitmapData(); - using IReadWriteBitmapData dst = result.GetReadWriteBitmapData(); - - // not using Task and await, because this method's signature must match the WaitCallback delegate, and we want to be compatible with .NET 3.5, too - IAsyncResult asyncResult = src.BeginDrawInto(dst, new Rectangle(Point.Empty, doubled.Size), new Rectangle(Point.Empty, result.Size), - // ReSharper disable once AccessToModifiedClosure - intended, if IsCanceled is modified we need to return its modified value - asyncConfig: new AsyncConfig { IsCancelRequestedCallback = () => task.IsCanceled, ThrowIfCanceled = false }); - - // As we are already on a pool thread this is not a UI blocking call - // This will throw an exception if resizing failed (resizing also allocates a large amount of memory). - asyncResult.EndDrawInto(); - } - } - catch (Exception e) when (!e.IsCriticalGdi()) - { - // Despite all of the preconditions the memory could not be allocated or some other error occurred (yes, we catch even OutOfMemoryException here) - // NOTE: practically we always can recover from here: we simply don't use a generated preview and the worker thread can be finished - task.IsCanceled = true; - } - finally - { - doubled?.Dispose(); - if (task.IsCanceled) - { - result?.Dispose(); - result = null; - } - } - - return result; - } - - private static Bitmap GenerateBitmapPreview(GenerateTask task) - { - // BitmapExtensions.Resize does the same but this way the process can be canceled - Debug.WriteLine($"Generating smoothed image {task.Size.Width}x{task.Size.Height} on thread #{Thread.CurrentThread.ManagedThreadId}"); - - Bitmap result = null; - try - { - result = new Bitmap(task.Size.Width, task.Size.Height, PixelFormat.Format32bppPArgb); - using IReadableBitmapData src = ((Bitmap)task.SourceImage).GetReadableBitmapData(); - using IReadWriteBitmapData dst = result.GetReadWriteBitmapData(); - var cfg = new AsyncConfig { IsCancelRequestedCallback = () => task.IsCanceled, ThrowIfCanceled = false, MaxDegreeOfParallelism = Environment.ProcessorCount >> 1 }; - - // Not using Task and await, because this method's signature must match the WaitCallback delegate, and we want to be compatible with .NET 3.5, too. - // As we are already on a pool thread the End... call does not block the UI. - var srcRect = new Rectangle(Point.Empty, task.SourceImage.Size); - var dstRect = new Rectangle(Point.Empty, task.Size); - if (srcRect == dstRect) - { - IAsyncResult asyncResult = src.BeginCopyTo(dst, srcRect, Point.Empty, asyncConfig: cfg); - asyncResult.EndCopyTo(); - } - else - { - IAsyncResult asyncResult = src.BeginDrawInto(dst, srcRect, dstRect, asyncConfig: cfg); - asyncResult.EndDrawInto(); - } - } - catch (Exception e) when (!e.IsCriticalGdi()) - { - // Despite all of the preconditions the memory could not be allocated or some other error occurred (yes, we catch even OutOfMemoryException here) - // NOTE: practically we always can recover from here: we simply don't use a generated preview and the worker thread can be finished - task.IsCanceled = true; - } - finally - { - if (task.IsCanceled) - { - result?.Dispose(); - result = null; - } - } - - return result; - } - - #endregion - -#pragma warning restore CS1690 // Accessing a member on a field of a marshal-by-reference class may cause a runtime exception - #endregion - } - - #endregion - - #endregion - - #region Constants - - private const int generateThreshold = 1000; - - #endregion - #region Fields #region Static Fields - private static readonly PixelFormat[] convertedFormats = OSUtils.IsWindows - // Windows: these are either not supported by Graphics or are very slow - ? new[] { PixelFormat.Format16bppGrayScale, PixelFormat.Format48bppRgb, PixelFormat.Format64bppArgb, PixelFormat.Format64bppPArgb } - // Non Windows (eg. Mono/Linux): these are not supported by Graphics - : new[] { PixelFormat.Format16bppRgb555, PixelFormat.Format16bppRgb565 }; - private static readonly Size referenceScrollSize = new Size(32, 32); #endregion #region Instance Fields - private readonly PreviewGenerator previewGenerator; + private readonly DisplayImageGenerator displayImageGenerator; - private Image image; + private Image? image; private Rectangle targetRectangle; private Rectangle clientRectangle; - private bool smoothZooming; - private bool autoZoom; private float zoom = 1; private Size scrollbarSize; private Size imageSize; + private PixelFormat pixelFormat; + private bool isMetafile; + private bool smoothZooming; + private bool autoZoom; private bool sbHorizontalVisible; private bool sbVerticalVisible; + private bool isApplyingZoom; + private bool isDragging; + private int scrollFractionVertical; private int scrollFractionHorizontal; - private bool isApplyingZoom; + private Size draggingOrigin; + private Point scrollingOrigin; #endregion @@ -543,7 +87,13 @@ private static Bitmap GenerateBitmapPreview(GenerateTask task) #region Events - internal event EventHandler ZoomChanged + internal event EventHandler? AutoZoomChanged + { + add => Events.AddHandler(nameof(AutoZoomChanged), value); + remove => Events.RemoveHandler(nameof(AutoZoomChanged), value); + } + + internal event EventHandler? ZoomChanged { add => Events.AddHandler(nameof(ZoomChanged), value); remove => Events.RemoveHandler(nameof(ZoomChanged), value); @@ -555,7 +105,7 @@ internal event EventHandler ZoomChanged #region Internal Properties - internal Image Image + internal Image? Image { get => image; set @@ -570,16 +120,7 @@ internal Image Image internal bool AutoZoom { get => autoZoom; - set - { - if (autoZoom == value) - return; - autoZoom = value; - if (!autoZoom && !isMetafile) - SetZoom(1f); - - Invalidate(InvalidateFlags.Sizes | (autoZoom ? InvalidateFlags.DisplayImage : InvalidateFlags.None)); - } + set => SetAutoZoom(value, true); } internal float Zoom @@ -631,20 +172,69 @@ public ImageViewer() InitializeComponent(); SetStyle(ControlStyles.Selectable | ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); - scrollbarSize = new Size(SystemInformation.VerticalScrollBarWidth, SystemInformation.HorizontalScrollBarHeight); + scrollbarSize = OSUtils.IsMono ? new Size(16, 16).Scale(this.GetScale()) : new Size(SystemInformation.VerticalScrollBarWidth, SystemInformation.HorizontalScrollBarHeight); sbVertical.Width = scrollbarSize.Width; sbHorizontal.Height = scrollbarSize.Height; sbVertical.ValueChanged += ScrollbarValueChanged; sbHorizontal.ValueChanged += ScrollbarValueChanged; - previewGenerator = new PreviewGenerator(this); + displayImageGenerator = new DisplayImageGenerator(this); } #endregion #region Methods + #region Internal Methods + + /// + /// Should be called when image content is changed while image reference remains the same (eg. rotation, palette change) + /// + internal void UpdateImage() + { + if (image == null) + return; + + var flags = InvalidateFlags.Image | InvalidateFlags.DisplayImage; + Size newImageSize; + lock (image) + { + newImageSize = image.Size; + pixelFormat = image.PixelFormat; + } + + if (newImageSize != imageSize) + { + imageSize = newImageSize; + flags |= InvalidateFlags.Sizes; + } + + Invalidate(flags); + } + + internal void IncreaseZoom() + { + SetAutoZoom(false, false); + ApplyZoomChange(0.25f); + } + + internal void DecreaseZoom() + { + SetAutoZoom(false, false); + ApplyZoomChange(-0.25f); + } + + internal void ResetZoom() + { + if (zoom.Equals(1f)) + return; + AutoZoom = false; + Zoom = 1f; + } + + #endregion + #region Protected Methods protected override void OnSizeChanged(EventArgs e) @@ -670,16 +260,16 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData) switch (keyData) { case Keys.Up: - VerticalScroll(SystemInformation.MouseWheelScrollDelta); + VerticalScroll(MouseWheelScrollDelta); return true; case Keys.Down: - VerticalScroll(-SystemInformation.MouseWheelScrollDelta); + VerticalScroll(-MouseWheelScrollDelta); return true; case Keys.Left: - HorizontalScroll(SystemInformation.MouseWheelScrollDelta); + HorizontalScroll(MouseWheelScrollDelta); return true; case Keys.Right: - HorizontalScroll(-SystemInformation.MouseWheelScrollDelta); + HorizontalScroll(-MouseWheelScrollDelta); return true; default: return base.ProcessCmdKey(ref msg, keyData); @@ -690,6 +280,33 @@ protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); Focus(); + if (!(sbHorizontalVisible || sbVerticalVisible) || (e.Button & MouseButtons.Left) == MouseButtons.None) + return; + isDragging = true; + draggingOrigin = new Size(e.Location); + scrollingOrigin = new Point(sbHorizontal.Value, sbVertical.Value); + Cursor = Cursors.HandGrab; + } + + protected override void OnMouseUp(MouseEventArgs e) + { + base.OnMouseUp(e); + if ((e.Button & MouseButtons.Left) == MouseButtons.None) + return; + isDragging = false; + Cursor = sbHorizontalVisible || sbVerticalVisible ? Cursors.HandOpen : null; + } + + protected override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + if (!isDragging) + return; + Point distance = e.Location - draggingOrigin; + if (sbHorizontalVisible && distance.X != 0) + sbHorizontal.SetValueSafe(scrollingOrigin.X - distance.X); + if (sbVerticalVisible && distance.Y != 0) + sbVertical.SetValueSafe(scrollingOrigin.Y - distance.Y); } protected override void OnMouseWheel(MouseEventArgs e) @@ -700,8 +317,8 @@ protected override void OnMouseWheel(MouseEventArgs e) // zoom case Keys.Control: if (autoZoom) - return; - float delta = (float)e.Delta / SystemInformation.MouseWheelScrollDelta / 5; + SetAutoZoom(false, false); + float delta = (float)e.Delta / MouseWheelScrollDelta / 5; ApplyZoomChange(delta); break; @@ -721,6 +338,8 @@ protected override void OnMouseHWheel(HandledMouseEventArgs e) HorizontalScroll(-e.Delta); } + protected override void OnRightToLeftChanged(EventArgs e) => AdjustSizes(); + protected override void Dispose(bool disposing) { if (IsDisposed) @@ -730,7 +349,7 @@ protected override void Dispose(bool disposing) sbHorizontal.ValueChanged -= ScrollbarValueChanged; if (disposing) - previewGenerator.Dispose(); + displayImageGenerator.Dispose(); base.Dispose(disposing); if (disposing) @@ -739,33 +358,14 @@ protected override void Dispose(bool disposing) #endregion - #region Internal Methods - - /// - /// Should be called when image content is changed - /// - internal void UpdateImage() - { - if (image == null) - return; - - // can happen when image is rotated - if (image.Size != imageSize || !ReferenceEquals(image, previewGenerator.GetDisplayImage(false))) - SetImage(image); - else - Invalidate(); - } - - #endregion - #region Private Methods - private void SetImage(Image value) + private void SetImage(Image? value) { - previewGenerator.Free(); image = value; isMetafile = image is Metafile; imageSize = image?.Size ?? default; + pixelFormat = image?.PixelFormat ?? default; Invalidate(InvalidateFlags.All); // making sure image is not under or over-zoomed @@ -778,14 +378,9 @@ private void VerticalScroll(int delta) // When scrolling by mouse, delta is always +-120 so this will be a small change on the scrollbar. // But we collect the fractional changes caused by the touchpad scrolling so it will not be lost either. int totalDelta = scrollFractionVertical + delta * sbVertical.SmallChange; - scrollFractionVertical = totalDelta % SystemInformation.MouseWheelScrollDelta; - int newValue = sbVertical.Value - totalDelta / SystemInformation.MouseWheelScrollDelta; - if (newValue < sbVertical.Minimum) - newValue = sbVertical.Minimum; - else if (newValue > sbVertical.Maximum - sbVertical.LargeChange + 1) - newValue = sbVertical.Maximum - sbVertical.LargeChange + 1; - - sbVertical.Value = newValue; + scrollFractionVertical = totalDelta % MouseWheelScrollDelta; + int newValue = sbVertical.Value - totalDelta / MouseWheelScrollDelta; + sbVertical.SetValueSafe(newValue); } private void HorizontalScroll(int delta) @@ -793,14 +388,9 @@ private void HorizontalScroll(int delta) // When scrolling by mouse, delta is always +-120 so this will be a small change on the scrollbar. // But we collect the fractional changes caused by the touchpad scrolling so it will not be lost either. int totalDelta = scrollFractionHorizontal + delta * sbVertical.SmallChange; - scrollFractionHorizontal = totalDelta % SystemInformation.MouseWheelScrollDelta; - int newValue = sbHorizontal.Value - totalDelta / SystemInformation.MouseWheelScrollDelta; - if (newValue < sbHorizontal.Minimum) - newValue = sbHorizontal.Minimum; - else if (newValue > sbHorizontal.Maximum - sbHorizontal.LargeChange + 1) - newValue = sbHorizontal.Maximum - sbHorizontal.LargeChange + 1; - - sbHorizontal.Value = newValue; + scrollFractionHorizontal = totalDelta % MouseWheelScrollDelta; + int newValue = sbHorizontal.Value - totalDelta / MouseWheelScrollDelta; + sbHorizontal.SetValueSafe(newValue); } private void Invalidate(InvalidateFlags flags) @@ -808,8 +398,10 @@ private void Invalidate(InvalidateFlags flags) if ((flags & InvalidateFlags.Sizes) != InvalidateFlags.None) AdjustSizes(); - if ((flags & InvalidateFlags.DisplayImage) != InvalidateFlags.None) - previewGenerator.BeginGenerateDisplayImage(); + if ((flags & InvalidateFlags.Image) != InvalidateFlags.None) + displayImageGenerator.InvalidateImages(); + else if ((flags & InvalidateFlags.DisplayImage) != InvalidateFlags.None) + displayImageGenerator.InvalidateDisplayImage(); Invalidate(); } @@ -820,6 +412,7 @@ private void AdjustSizes() { sbHorizontal.Visible = sbVertical.Visible = sbHorizontalVisible = sbVerticalVisible = false; targetRectangle = Rectangle.Empty; + Cursor = null; return; } @@ -842,6 +435,7 @@ private void AdjustSizes() targetRectangle = new Rectangle(targetLocation, scaledSize); clientRectangle = new Rectangle(Point.Empty, clientSize); sbHorizontal.Visible = sbVertical.Visible = sbHorizontalVisible = sbVerticalVisible = false; + Cursor = null; return; } @@ -863,11 +457,11 @@ private void AdjustSizes() return; } + Point clientLocation = Point.Empty; targetLocation = new Point((clientSize.Width >> 1) - (scaledSize.Width >> 1), (clientSize.Height >> 1) - (scaledSize.Height >> 1)); - targetRectangle = new Rectangle(targetLocation, scaledSize); - clientRectangle = new Rectangle(Point.Empty, clientSize); + bool isRtl = RightToLeft == RightToLeft.Yes; // both scrollbars if (sbHorizontalVisible && sbVerticalVisible) @@ -875,8 +469,9 @@ private void AdjustSizes() sbHorizontal.Dock = sbVertical.Dock = DockStyle.None; sbHorizontal.Width = clientSize.Width; sbHorizontal.Top = clientSize.Height; + sbHorizontal.Left = isRtl ? scrollbarSize.Width : 0; sbVertical.Height = clientSize.Height; - sbVertical.Left = clientSize.Width; + sbVertical.Left = isRtl ? 0 : clientSize.Width; } // horizontal scrollbar else if (sbHorizontalVisible) @@ -886,14 +481,14 @@ private void AdjustSizes() // vertical scrollbar else if (sbVerticalVisible) { - sbVertical.Dock = DockStyle.Right; + sbVertical.Dock = isRtl ? DockStyle.Left : DockStyle.Right; } // adjust scrollbar values if (sbHorizontalVisible) { - sbHorizontal.Minimum = targetRectangle.X; - sbHorizontal.Maximum = targetRectangle.Right; + sbHorizontal.Minimum = targetLocation.X; + sbHorizontal.Maximum = targetLocation.X + scaledSize.Width; sbHorizontal.LargeChange = clientSize.Width; sbHorizontal.SmallChange = this.ScaleSize(referenceScrollSize).Width; sbHorizontal.Value = Math.Min(sbHorizontal.Value, sbHorizontal.Maximum - sbHorizontal.LargeChange); @@ -901,8 +496,14 @@ private void AdjustSizes() if (sbVerticalVisible) { - sbVertical.Minimum = targetRectangle.Y; - sbVertical.Maximum = targetRectangle.Bottom; + if (isRtl) + { + targetLocation.X += scrollbarSize.Width; + clientLocation.X = scrollbarSize.Width; + } + + sbVertical.Minimum = targetLocation.Y; + sbVertical.Maximum = targetLocation.Y + scaledSize.Height; sbVertical.LargeChange = clientSize.Height; sbVertical.SmallChange = this.ScaleSize(referenceScrollSize).Height; sbVertical.Value = Math.Min(sbVertical.Value, sbVertical.Maximum - sbVertical.LargeChange); @@ -910,12 +511,17 @@ private void AdjustSizes() sbHorizontal.Visible = sbHorizontalVisible; sbVertical.Visible = sbVerticalVisible; + Cursor = sbHorizontalVisible || sbVerticalVisible ? Cursors.HandOpen : null; + isDragging = false; - clientRectangle = new Rectangle(Point.Empty, clientSize); + clientRectangle = new Rectangle(clientLocation, clientSize); targetRectangle = new Rectangle(targetLocation, scaledSize); + if (!isRtl || !sbVerticalVisible) + return; + + clientRectangle.X = scrollbarSize.Width; } - [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", Justification = "False alarm, image is not a remote object")] private void PaintImage(Graphics g) { g.IntersectClip(clientRectangle); @@ -924,15 +530,22 @@ private void PaintImage(Graphics g) dest.X -= sbHorizontal.Value; if (sbVerticalVisible) dest.Y -= sbVertical.Value; - g.InterpolationMode = !isMetafile && (smoothZooming && zoom > 1f || zoom < 1f && imageSize.Width < generateThreshold && imageSize.Height < generateThreshold) ? InterpolationMode.HighQualityBicubic : InterpolationMode.NearestNeighbor; - g.PixelOffsetMode = PixelOffsetMode.HighQuality; - lock (previewGenerator) + // This lock ensures that no disposed image is painted. The generator also locks on it when frees the cached preview. + lock (displayImageGenerator.SyncRoot) { + (Image? toDraw, InterpolationMode interpolationMode) = displayImageGenerator.GetDisplayImage(); + + // happens if image format is not supported and generating compatible display images is disabled due to low memory + if (toDraw == null) + return; + + g.PixelOffsetMode = PixelOffsetMode.HighQuality; + g.InterpolationMode = interpolationMode; + // Locking on display image so if it is the same as the original image, which is also locked when accessing its bitmap data - // the "bitmap region is already locked" can be avoided. Important: this cannot be ensured without locking here internally because + // so the "bitmap region is already locked" can be avoided. Important: this cannot be ensured without locking here internally because // OnPaint can occur any time after invalidating. - Image toDraw = previewGenerator.GetDisplayImage(true); bool useLock = image == toDraw; if (useLock) Monitor.Enter(toDraw); @@ -962,6 +575,18 @@ private void ApplyZoomChange(float delta) SetZoom(zoom * delta); } + private void SetAutoZoom(bool value, bool resetIfBitmap) + { + if (autoZoom == value) + return; + autoZoom = value; + if (resetIfBitmap && !autoZoom && !isMetafile) + SetZoom(1f); + + Invalidate(InvalidateFlags.Sizes | (autoZoom ? InvalidateFlags.DisplayImage : InvalidateFlags.None)); + OnAutoZoomChanged(EventArgs.Empty); + } + private void SetZoom(float value) { if (autoZoom || isApplyingZoom) @@ -1011,13 +636,14 @@ private void SetZoom(float value) } } + private void OnAutoZoomChanged(EventArgs e) => Events.GetHandler(nameof(AutoZoomChanged))?.Invoke(this, e); private void OnZoomChanged(EventArgs e) => Events.GetHandler(nameof(ZoomChanged))?.Invoke(this, e); #endregion #region Event handlers - private void ScrollbarValueChanged(object sender, EventArgs e) => Invalidate(); + private void ScrollbarValueChanged(object? sender, EventArgs e) => Invalidate(); #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/NotificationLabel.cs b/KGySoft.Drawing.ImagingTools/View/Controls/NotificationLabel.cs index 4d0eb91..3dbfd55 100644 --- a/KGySoft.Drawing.ImagingTools/View/Controls/NotificationLabel.cs +++ b/KGySoft.Drawing.ImagingTools/View/Controls/NotificationLabel.cs @@ -18,7 +18,6 @@ using System; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; using System.Windows.Forms; @@ -55,7 +54,7 @@ public override string Text } [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public new Image Image + public new Image? Image { get => base.Image; set @@ -98,8 +97,6 @@ public NotificationLabel() #region Public Methods - [SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", - Justification = "Font measuring must not rely on a variable resource text")] public override Size GetPreferredSize(Size proposedSize) { // Workaround: Immediately after calculating preferred size (eg. Dock == Top), another request arrives with empty proposedSize, which ruins the constrained result. @@ -116,8 +113,6 @@ public override Size GetPreferredSize(Size proposedSize) lastProposedSize = proposedSize; } - //TextFormatFlags formatFlags = this.GetFormatFlags(); - Size padding = GetBordersAndPadding(); Size proposedTextSize = proposedSize - padding; diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/PalettePanel.cs b/KGySoft.Drawing.ImagingTools/View/Controls/PalettePanel.cs index 1249341..cfb2fb9 100644 --- a/KGySoft.Drawing.ImagingTools/View/Controls/PalettePanel.cs +++ b/KGySoft.Drawing.ImagingTools/View/Controls/PalettePanel.cs @@ -45,13 +45,16 @@ internal sealed partial class PalettePanel : BaseControl #region Instance Fields - private IList palette; + private readonly int scrollbarWidth; + + private IList? palette; private int selectedColorIndex = -1; private int firstVisibleColor; private int visibleRowCount; private int counter; private PointF scale = new PointF(1f, 1f); private int scrollFraction; + private bool isRightToLeft; #endregion @@ -59,7 +62,7 @@ internal sealed partial class PalettePanel : BaseControl #region Events - internal event EventHandler SelectedColorChanged + internal event EventHandler? SelectedColorChanged { add => Events.AddHandler(nameof(SelectedColorChanged), value); remove => Events.RemoveHandler(nameof(SelectedColorChanged), value); @@ -71,7 +74,7 @@ internal event EventHandler SelectedColorChanged #region Internal Properties - internal IList Palette + internal IList? Palette { set { @@ -80,7 +83,7 @@ internal IList Palette timerSelection.Enabled = false; if (value != null) SelectedColorIndex = 0; - Invalidate(); + ResetLayout(); } } @@ -111,6 +114,7 @@ internal int SelectedColorIndex timerSelection.Enabled = true; if (invalidateAll) { + ResetLayout(); sbPalette.Value = firstVisibleColor >> 4; Invalidate(); } @@ -155,7 +159,7 @@ internal Color SelectedColor #region Constructors - internal PalettePanel() + public PalettePanel() { InitializeComponent(); @@ -164,7 +168,8 @@ internal PalettePanel() DoubleBuffered = true; SetStyle(ControlStyles.Selectable, true); - sbPalette.Width = SystemInformation.VerticalScrollBarWidth; + scrollbarWidth = OSUtils.IsMono ? this.ScaleWidth(16) : SystemInformation.VerticalScrollBarWidth; + sbPalette.Width = scrollbarWidth; } #endregion @@ -185,16 +190,19 @@ protected override void Dispose(bool disposing) protected override void OnPaint(PaintEventArgs e) { - scale = e.Graphics.GetScale(); - if (CheckPaletteLayout()) - return; + PointF currentScale = e.Graphics.GetScale(); + if (currentScale != scale) + { + scale = currentScale; + ResetLayout(); + } base.OnPaint(e); if (ColorCount == 0) return; e.Graphics.PixelOffsetMode = PixelOffsetMode.Half; - int upper = Math.Min(palette.Count, firstVisibleColor + (visibleRowCount << 4)); + int upper = Math.Min(palette!.Count, firstVisibleColor + (visibleRowCount << 4)); // iterating through visible colors for (int i = firstVisibleColor; i < upper; i++) @@ -264,14 +272,23 @@ protected override void OnMouseDown(MouseEventArgs e) if (e.Button != MouseButtons.Left || ColorCount == 0) return; + Point location = e.Location; + if (isRightToLeft) + { + location.X -= scrollbarWidth; + location.X = (int)MathF.Round(((13 << 4) + 2) * scale.X) - location.X; + } + + // same as before (using the raw location because GetColorRect translates it) if (GetColorRect(selectedColorIndex).Contains(e.Location)) return; - if (!Rectangle.Round(new RectangleF(2 * scale.X, 2 * scale.Y, (13 << 4) * scale.X, 13 * visibleRowCount * scale.Y)).Contains(e.Location)) + // out of range + if (!Rectangle.Round(new RectangleF(2 * scale.X, 2 * scale.Y, (13 << 4) * scale.X, 13 * visibleRowCount * scale.Y)).Contains(location)) return; - int x = ((int)(e.X / scale.X) - 2) / 13; - int y = ((int)(e.Y / scale.Y) - 2) / 13; + int x = ((int)(location.X / scale.X) - 2) / 13; + int y = ((int)(location.Y / scale.Y) - 2) / 13; int index = firstVisibleColor + (y << 4) + x; if (index >= ColorCount) @@ -292,10 +309,11 @@ protected override void OnPreviewKeyDown(PreviewKeyDownEventArgs e) switch (e.KeyData) { case Keys.Right: - newIndex = Math.Min(selectedColorIndex + 1, ColorCount - 1); - break; case Keys.Left: - newIndex = Math.Max(selectedColorIndex - 1, 0); + if (e.KeyData == Keys.Right ^ isRightToLeft) + newIndex = Math.Min(selectedColorIndex + 1, ColorCount - 1); + else + newIndex = Math.Max(selectedColorIndex - 1, 0); break; case Keys.Down: newIndex = Math.Min(selectedColorIndex + 16, ColorCount - 1); @@ -333,8 +351,8 @@ protected override void OnMouseWheel(MouseEventArgs e) // When scrolling by mouse, delta is always +-120 so this will be 1 change on the scrollbar. // But we collect the fractional changes caused by the touchpad scrolling so it will not be lost either. int totalDelta = scrollFraction + e.Delta; - scrollFraction = totalDelta % SystemInformation.MouseWheelScrollDelta; - int newValue = sbPalette.Value - totalDelta / SystemInformation.MouseWheelScrollDelta; + scrollFraction = totalDelta % MouseWheelScrollDelta; + int newValue = sbPalette.Value - totalDelta / MouseWheelScrollDelta; if (newValue < 0) newValue = 0; else if (newValue > sbPalette.Maximum - sbPalette.LargeChange + 1) @@ -343,37 +361,50 @@ protected override void OnMouseWheel(MouseEventArgs e) sbPalette.Value = newValue; } + protected override void OnSizeChanged(EventArgs e) + { + base.OnSizeChanged(e); + ResetLayout(); + } + + protected override void OnRightToLeftChanged(EventArgs e) + { + base.OnRightToLeftChanged(e); + isRightToLeft = RightToLeft == RightToLeft.Yes; + sbPalette.Dock = isRightToLeft ? DockStyle.Left : DockStyle.Right; + Invalidate(); + } + #endregion #region Private Methods - /// - /// Checks the scrollbar and returns on layout change (which means invalidated graphics) - /// - private bool CheckPaletteLayout() + private void ResetLayout() { if (ColorCount == 0) { sbPalette.Visible = false; visibleRowCount = 0; - return false; + return; } + Invalidate(); + // calculating visible rows int maxRows = ((int)(Height / scale.Y) - 5) / 13; if (maxRows == visibleRowCount) - return false; + return; - Invalidate(); visibleRowCount = maxRows; - int colorRows = (int)Math.Ceiling((double)palette.Count / 16); + int colorRows = (int)Math.Ceiling((double)palette!.Count / 16); if (visibleRowCount >= colorRows) { // scrollbar is not needed firstVisibleColor = 0; sbPalette.Visible = false; timerSelection.Enabled = true; - return true; + visibleRowCount = colorRows; + return; } // scrollbar is needed @@ -382,18 +413,25 @@ private bool CheckPaletteLayout() if (firstVisibleColor + (visibleRowCount << 4) >= palette.Count + 16) firstVisibleColor = palette.Count - (visibleRowCount << 4); sbPalette.Value = firstVisibleColor >> 4; + sbPalette.Visible = true; timerSelection.Enabled = IsSelectedColorVisible(); - return true; } private Rectangle GetColorRect(int index) { - float left = (2 + (index % 16) * 13) * scale.X; + float left = index % 16; + if (isRightToLeft) + left = 15 - left; + left = (2 + left * 13) * scale.X; + if (isRightToLeft) + left += scrollbarWidth; + + //float left = (2 + (index % 16) * 13) * scale.X; float top = (2 + ((index - firstVisibleColor) >> 4) * 13) * scale.Y; - // ReSharper disable CompareOfFloatsByEqualityOperator - intended + + // ReSharper disable once CompareOfFloatsByEqualityOperator - intended return new Rectangle(left % 1 == 0 ? (int)left : (int)left + 1, top % 1 == 0 ? (int)top : (int)top + 1, (int)(13 * scale.X), (int)(13 * scale.Y)); - // ReSharper restore CompareOfFloatsByEqualityOperator } private bool IsSelectedColorVisible() @@ -408,14 +446,14 @@ private bool IsSelectedColorVisible() //ReSharper disable InconsistentNaming #pragma warning disable IDE1006 // Naming Styles - private void sbPalette_ValueChanged(object sender, EventArgs e) + private void sbPalette_ValueChanged(object? sender, EventArgs e) { firstVisibleColor = sbPalette.Value << 4; timerSelection.Enabled = IsSelectedColorVisible(); - Invalidate(); + ResetLayout(); } - private void timerSelection_Tick(object sender, EventArgs e) + private void timerSelection_Tick(object? sender, EventArgs e) { if (!IsSelectedColorVisible()) { diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/ProgressFooter.cs b/KGySoft.Drawing.ImagingTools/View/Controls/ProgressFooter.cs new file mode 100644 index 0000000..e29c852 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Controls/ProgressFooter.cs @@ -0,0 +1,180 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: ProgressFooter.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2020 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Windows.Forms; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Controls +{ + /// + /// A with a progress bar that can be updated from any thread. + /// + internal class ProgressFooter : AutoMirrorPanel + { + #region Fields + + private readonly object syncRoot = new object(); + private readonly bool visualStyles = Application.RenderWithVisualStyles; + private readonly Label lblProgress; + private readonly ProgressBar pbProgress; + private readonly Timer timer; + + private bool progressVisible = true; // so ctor change will have effect at run-time + private TProgress? progress; + + #endregion + + #region Properties + + #region Internal Properties + + [SuppressMessage("ReSharper", "LocalizableElement", Justification = "Whitespace")] + [SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "ReSharper issue")] + internal virtual bool ProgressVisible + { + get => progressVisible; + set + { + if (progressVisible == value) + return; + progressVisible = value; + if (value) + { + Progress = default; + UpdateProgress(); + } + + lblProgress.Visible = pbProgress.Visible = value; + timer.Enabled = value; + } + } + + internal TProgress? Progress + { + get + { + lock (syncRoot) + return progress; + } + set + { + lock (syncRoot) + progress = value; + } + } + + #endregion + + #region Protected Properties + + protected virtual void UpdateProgress() => throw new InvalidOperationException(Res.InternalError($"{nameof(UpdateProgress)} is not overridden")); + + protected string ProgressText { set => lblProgress.Text = value; } + protected ProgressBarStyle ProgressStyle { set => pbProgress.Style = value; } + protected int Maximum { set => pbProgress.Maximum = value; } + + protected int Value + { + set + { + // Workaround for progress bar on Vista and above where it advances very slow + if (OSUtils.IsVistaOrLater && visualStyles && value > pbProgress.Value && value < pbProgress.Maximum) + pbProgress.Value = value + 1; + pbProgress.Value = value; + } + } + + protected override Padding DefaultPadding => new Padding(3, 3, 8, 3); + + #endregion + + #endregion + + #region Constructors + + protected ProgressFooter() + { + Dock = DockStyle.Bottom; + lblProgress = new Label + { + Name = nameof(lblProgress), + Dock = DockStyle.Left, + TextAlign = ContentAlignment.MiddleLeft + }; + pbProgress = new ProgressBar + { + Name = nameof(pbProgress), + Dock = DockStyle.Fill, + RightToLeftLayout = true, + }; + Controls.AddRange(new Control[] { pbProgress, lblProgress }); + timer = new Timer { Interval = 30 }; + + if (DesignMode) + return; + + ProgressVisible = false; + lblProgress.TextChanged += lblProgress_TextChanged; + timer.Tick += timer_Tick; + } + + #endregion + + #region Methods + + #region Protected Methods + + protected override void OnSizeChanged(EventArgs e) + { + base.OnSizeChanged(e); + + // Fixing high DPI appearance on Mono + PointF scale; + if (OSUtils.IsMono && (scale = this.GetScale()) != new PointF(1f, 1f)) + Height = (int)(22 * scale.Y); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + timer.Dispose(); + + lblProgress.TextChanged -= lblProgress_TextChanged; + timer.Tick -= timer_Tick; + base.Dispose(disposing); + } + + #endregion + + #region Event handlers +#pragma warning disable IDE1006 // Naming Styles + + private void lblProgress_TextChanged(object? sender, EventArgs e) => lblProgress.Width = lblProgress.PreferredWidth; + + private void timer_Tick(object? sender, EventArgs e) => UpdateProgress(); + +#pragma warning restore IDE1006 // Naming Styles + #endregion + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/ScalingCheckBox.cs b/KGySoft.Drawing.ImagingTools/View/Controls/ScalingCheckBox.cs index 081fd96..6030985 100644 --- a/KGySoft.Drawing.ImagingTools/View/Controls/ScalingCheckBox.cs +++ b/KGySoft.Drawing.ImagingTools/View/Controls/ScalingCheckBox.cs @@ -1,11 +1,26 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: ScalingCheckBox.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2020 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + using System.Drawing; -using System.Linq; -using System.Text; using System.Windows.Forms; +#endregion + namespace KGySoft.Drawing.ImagingTools.View.Controls { /// @@ -13,6 +28,8 @@ namespace KGySoft.Drawing.ImagingTools.View.Controls /// internal class ScalingCheckBox : CheckBox { + #region Methods + public override Size GetPreferredSize(Size proposedSize) { var flatStyle = FlatStyle; @@ -25,8 +42,12 @@ public override Size GetPreferredSize(Size proposedSize) // The gap between the CheckBox and the text is 3px smaller with System at every DPI Size result = base.GetPreferredSize(proposedSize); -#if !NET35 - // The scaling is different in .NET 3.5 so there we don't subtract the padding difference +#if NET35 + // The scaling is different in .NET 3.5 so instead if subtracting a constant padding difference + // we need to add some based on scaling, but only when visual styles are not applied + if (!Application.RenderWithVisualStyles) + result.Width += this.ScaleWidth(2); +#else result.Width -= 3; #endif @@ -34,5 +55,7 @@ public override Size GetPreferredSize(Size proposedSize) ResumeLayout(); return result; } + + #endregion } -} +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Controls/ScalingToolStrip.cs b/KGySoft.Drawing.ImagingTools/View/Controls/ScalingToolStrip.cs deleted file mode 100644 index 2f051f5..0000000 --- a/KGySoft.Drawing.ImagingTools/View/Controls/ScalingToolStrip.cs +++ /dev/null @@ -1,178 +0,0 @@ -#region Copyright - -/////////////////////////////////////////////////////////////////////////////// -// File: ScalingToolStrip.cs -/////////////////////////////////////////////////////////////////////////////// -// Copyright (C) KGy SOFT, 2005-2019 - All Rights Reserved -// -// You should have received a copy of the LICENSE file at the top-level -// directory of this distribution. If not, then this file is considered as -// an illegal copy. -// -// Unauthorized copying of this file, via any medium is strictly prohibited. -/////////////////////////////////////////////////////////////////////////////// - -#endregion - -#region Usings - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Windows.Forms; - -using KGySoft.Drawing.ImagingTools.WinApi; - -#endregion - -namespace KGySoft.Drawing.ImagingTools.View.Controls -{ - /// - /// A that can scale its content regardless of .NET version and app.config settings. - /// - internal class ScalingToolStrip : ToolStrip - { - #region Nested classes - - #region ScalingToolStripMenuRenderer class - - private class ScalingToolStripMenuRenderer : ToolStripProfessionalRenderer - { - #region Fields - - private static readonly Size referenceOffset = new Size(2, 2); - private static readonly Size referenceOffsetDouble = new Size(4, 4); - - #endregion - - #region Methods - - protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e) - { - Graphics g = e.Graphics; - Rectangle dropDownRect = e.Item is ScalingToolStripDropDownButton scalingButton ? scalingButton.ArrowRectangle : e.ArrowRectangle; - using (Brush brush = new SolidBrush(e.ArrowColor)) - { - Point middle = new Point(dropDownRect.Left + dropDownRect.Width / 2, dropDownRect.Top + dropDownRect.Height / 2); - - Point[] arrow; - - var offset = g.ScaleSize(referenceOffset); - var offsetDouble = g.ScaleSize(referenceOffsetDouble); - - switch (e.Direction) - { - case ArrowDirection.Up: - arrow = new Point[] { - new Point(middle.X - offset.Width, middle.Y + 1), - new Point(middle.X + offset.Width + 1, middle.Y + 1), - new Point(middle.X, middle.Y - offset.Height)}; - break; - case ArrowDirection.Left: - arrow = new Point[] { - new Point(middle.X + offset.Width, middle.Y - offsetDouble.Height), - new Point(middle.X + offset.Width, middle.Y + offsetDouble.Height), - new Point(middle.X - offset.Width, middle.Y)}; - break; - case ArrowDirection.Right: - arrow = new Point[] { - new Point(middle.X - offset.Width, middle.Y - offsetDouble.Height), - new Point(middle.X - offset.Width, middle.Y + offsetDouble.Height), - new Point(middle.X + offset.Width, middle.Y)}; - break; - default: - arrow = new Point[] { - new Point(middle.X - offset.Width, middle.Y - 1), - new Point(middle.X + offset.Width + 1, middle.Y - 1), - new Point(middle.X, middle.Y + offset.Height) }; - break; - } - - g.FillPolygon(brush, arrow); - } - } - - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "False alarm, see the disposing at the end")] - protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e) - { - Rectangle imageRect = e.ImageRectangle; - Image image = e.Image; - if (imageRect == Rectangle.Empty || image == null) - return; - - bool disposeImage = false; - if (!e.Item.Enabled) - { - image = CreateDisabledImage(image); - disposeImage = true; - } - - // Draw the checkmark background (providing no image) - base.OnRenderItemCheck(new ToolStripItemImageRenderEventArgs(e.Graphics, e.Item, null, e.ImageRectangle)); - - // Draw the checkmark image scaled to the image rectangle - e.Graphics.DrawImage(image, imageRect, new Rectangle(Point.Empty, image.Size), GraphicsUnit.Pixel); - - if (disposeImage) - image.Dispose(); - } - - protected override void OnRenderButtonBackground(ToolStripItemRenderEventArgs e) - { - if (e.Item is ToolStripButton button && button.Checked && button.Enabled) - { - if (OSUtils.IsWindows) - e.Graphics.Clear(ProfessionalColors.ButtonSelectedGradientMiddle); - else - { - // In Mono without this clipping the whole tool strip container is cleared - GraphicsState state = e.Graphics.Save(); - Rectangle rect = e.Item.ContentRectangle; - rect.Inflate(1, 1); - e.Graphics.SetClip(rect); - e.Graphics.Clear(ProfessionalColors.ButtonSelectedGradientMiddle); - e.Graphics.Restore(state); - } - } - - base.OnRenderButtonBackground(e); - } - - #endregion - } - - #endregion - - #endregion - - #region Fields - - private static readonly Size referenceSize = new Size(16, 16); - - #endregion - - #region Constructors - - public ScalingToolStrip() - { - ImageScalingSize = Size.Round(this.ScaleSize(referenceSize)); - Renderer = new ScalingToolStripMenuRenderer(); - } - - #endregion - - #region Methods - - protected override void WndProc(ref Message m) - { - base.WndProc(ref m); - - // ensuring that items can be clicked even if the container form is not activated - if (m.Msg == Constants.WM_MOUSEACTIVATE && m.Result == (IntPtr)Constants.MA_ACTIVATEANDEAT) - m.Result = (IntPtr)Constants.MA_ACTIVATE; - } - - #endregion - } -} diff --git a/KGySoft.Drawing.ImagingTools/View/Cursors.cs b/KGySoft.Drawing.ImagingTools/View/Cursors.cs new file mode 100644 index 0000000..ad01ed3 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Cursors.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Windows.Forms; +using KGySoft.Collections; + +namespace KGySoft.Drawing.ImagingTools.View +{ + internal static class Cursors + { + private class CursorInfo + { + private readonly Icon icon; + + private readonly Dictionary createdCursors = new(); + + internal CursorInfo(Icon icon) => this.icon = icon; + + internal Cursor GetCreateCursor(Size desiredSize) + { + if (createdCursors.TryGetValue(desiredSize.Width, out var value)) + return value.Cursor; + + // extracting bitmap and not icon so any sizes should work on all platforms + using Bitmap image = icon.ExtractNearestBitmap(desiredSize, PixelFormat.Format32bppArgb); + CursorHandle handle = image.ToCursorHandle(new Point(image.Width >> 1, image.Height >> 1)); + Cursor result = new Cursor(handle); + createdCursors[desiredSize.Width] = (handle, result); + return result; + } + } + + private static readonly Size referenceSize = new Size(16, 16); + + private static readonly StringKeyedDictionary cursors = new StringKeyedDictionary(); + + + internal static Cursor HandOpen => GetCreateCursor() ?? System.Windows.Forms.Cursors.Hand; + internal static Cursor HandGrab => GetCreateCursor() ?? System.Windows.Forms.Cursors.NoMove2D; + + private static Cursor? GetCreateCursor([CallerMemberName]string resourceName = null!) + { + if (!OSUtils.IsWindows) + return null; + if (!cursors.TryGetValue(resourceName, out CursorInfo? info)) + cursors[resourceName] = info = new CursorInfo((Icon)Properties.Resources.ResourceManager.GetObject(resourceName, CultureInfo.InvariantCulture)!); + return info.GetCreateCursor(referenceSize.Scale(OSUtils.SystemScale)); + } + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Design/DithererStrengthEditor.cs b/KGySoft.Drawing.ImagingTools/View/Design/DithererStrengthEditor.cs index fe9a702..e829b2a 100644 --- a/KGySoft.Drawing.ImagingTools/View/Design/DithererStrengthEditor.cs +++ b/KGySoft.Drawing.ImagingTools/View/Design/DithererStrengthEditor.cs @@ -42,11 +42,11 @@ internal class DithererStrengthEditor : UITypeEditor public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) => UITypeEditorEditStyle.DropDown; - public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) + public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider? provider, object? value) { if (provider == null || value == null) return value; - IWindowsFormsEditorService editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService)); + var editorService = (IWindowsFormsEditorService?)provider.GetService(typeof(IWindowsFormsEditorService)); if (editorService == null) return value; diff --git a/KGySoft.Drawing.ImagingTools/View/Design/QuantizerThresholdEditor.cs b/KGySoft.Drawing.ImagingTools/View/Design/QuantizerThresholdEditor.cs index bf9c385..b79ba53 100644 --- a/KGySoft.Drawing.ImagingTools/View/Design/QuantizerThresholdEditor.cs +++ b/KGySoft.Drawing.ImagingTools/View/Design/QuantizerThresholdEditor.cs @@ -42,11 +42,11 @@ internal class QuantizerThresholdEditor : UITypeEditor public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) => UITypeEditorEditStyle.DropDown; - public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) + public override object? EditValue(ITypeDescriptorContext context, IServiceProvider? provider, object? value) { if (provider == null || value == null) return value; - IWindowsFormsEditorService editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService)); + var editorService = (IWindowsFormsEditorService?)provider.GetService(typeof(IWindowsFormsEditorService)); if (editorService == null) return value; diff --git a/KGySoft.Drawing.ImagingTools/View/Dialogs.cs b/KGySoft.Drawing.ImagingTools/View/Dialogs.cs index eff06a7..ea615df 100644 --- a/KGySoft.Drawing.ImagingTools/View/Dialogs.cs +++ b/KGySoft.Drawing.ImagingTools/View/Dialogs.cs @@ -17,20 +17,210 @@ #region Usings using System; +using System.Drawing; +using System.Runtime.InteropServices; using System.Windows.Forms; +using KGySoft.Drawing.ImagingTools.WinApi; + #endregion namespace KGySoft.Drawing.ImagingTools.View { internal static class Dialogs { + #region Nested Types + + #region DialogType enum + + private enum DialogType + { + SingleButtonMessageBox, + MultiButtonMessageBox, + ColorDialog, + FolderDialog + } + + #region EnumerationContext struct + + private struct DialogContext + { + #region Fields + + internal DialogType DialogType; + internal int CustomStaticId; + internal bool AllowCustomStaticLocalization; + + #endregion + } + + #endregion + + #endregion + + #endregion + + #region Fields + + // These delegates are stored as a field to prevent their possible garbage collection while used by P/Invoke call. + private static readonly HOOKPROC callWndRetProc = CallWndRetProc; + private static readonly EnumChildProc enumChildProc = EnumChildProc; + + private static DialogContext dialogContext; + private static ColorDialog? colorDialog; + private static FolderBrowserDialog? folderDialog; + + #endregion + #region Methods - public static void ErrorMessage(string message) => MessageBox.Show(message, Res.TitleError, MessageBoxButtons.OK, MessageBoxIcon.Error); - public static void InfoMessage(string message) => MessageBox.Show(message, Res.TitleInformation, MessageBoxButtons.OK, MessageBoxIcon.Information); - public static void WarningMessage(string message) => MessageBox.Show(message, Res.TitleWarning, MessageBoxButtons.OK, MessageBoxIcon.Warning); - public static bool ConfirmMessage(string message) => MessageBox.Show(message, Res.TitleConfirmation, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes; + #region Internal Methods + + internal static void ErrorMessage(string message) => ShowMessageBox(message, Res.TitleError, MessageBoxButtons.OK, MessageBoxIcon.Error); + internal static void InfoMessage(string message) => ShowMessageBox(message, Res.TitleInformation, MessageBoxButtons.OK, MessageBoxIcon.Information); + internal static void WarningMessage(string message) => ShowMessageBox(message, Res.TitleWarning, MessageBoxButtons.OK, MessageBoxIcon.Warning); + + internal static bool ConfirmMessage(string message, bool isYesDefault = true) + => ShowMessageBox(message, Res.TitleConfirmation, MessageBoxButtons.YesNo, MessageBoxIcon.Question, isYesDefault ? MessageBoxDefaultButton.Button1 : MessageBoxDefaultButton.Button2) == DialogResult.Yes; + + internal static bool? CancellableConfirmMessage(string message, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1) + => ShowMessageBox(message, Res.TitleConfirmation, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question, defaultButton) switch + { + DialogResult.Yes => true, + DialogResult.No => false, + _ => null + }; + + internal static Color? PickColor(Color? selectedColor = default) + { + colorDialog ??= new ColorDialog { /*AnyColor = true,*/ FullOpen = true }; + if (selectedColor.HasValue) + colorDialog.Color = selectedColor.Value; + + // On Windows hooking messages to be able to localize the dialog texts + IntPtr windowHook = IntPtr.Zero; + if (OSUtils.IsWindows && !OSUtils.IsMono) + { + windowHook = User32.HookCallWndRetProc(callWndRetProc); + dialogContext = new DialogContext + { + DialogType = DialogType.ColorDialog, + AllowCustomStaticLocalization = true + }; + } + + DialogResult result = colorDialog.ShowDialog(); + + if (windowHook != IntPtr.Zero) + User32.UnhookWindowsHook(windowHook); + + return result == DialogResult.OK ? colorDialog.Color : null; + } + + internal static string? SelectFolder(string? selectedPath = null) + { + folderDialog ??= new FolderBrowserDialog { ShowNewFolderButton = true }; + if (selectedPath != null) + folderDialog.SelectedPath = selectedPath; + + // On Windows hooking messages to be able to localize the dialog texts + IntPtr windowHook = IntPtr.Zero; + if (OSUtils.IsWindows && !OSUtils.IsMono) + { + windowHook = User32.HookCallWndRetProc(callWndRetProc); + dialogContext = new DialogContext + { + DialogType = DialogType.FolderDialog, + AllowCustomStaticLocalization = true + }; + } + + DialogResult result = folderDialog.ShowDialog(); + + if (windowHook != IntPtr.Zero) + User32.UnhookWindowsHook(windowHook); + + return result == DialogResult.OK ? folderDialog.SelectedPath : null; + } + + #endregion + + #region Private Methods + + private static DialogResult ShowMessageBox(string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1) + { + MessageBoxOptions options = Res.DisplayLanguage.TextInfo.IsRightToLeft ? MessageBoxOptions.RightAlign | MessageBoxOptions.RtlReading : default; + IntPtr windowHook = IntPtr.Zero; + + // On Windows hooking messages to be able to localize the buttons + if (OSUtils.IsWindows && !OSUtils.IsMono) + { + windowHook = User32.HookCallWndRetProc(callWndRetProc); + dialogContext = new DialogContext + { + DialogType = buttons == MessageBoxButtons.OK ? DialogType.SingleButtonMessageBox : DialogType.MultiButtonMessageBox + }; + } + + DialogResult result = MessageBox.Show(message, caption, buttons, icon, defaultButton, options); + + if (windowHook != IntPtr.Zero) + User32.UnhookWindowsHook(windowHook); + + return result; + } + + private static IntPtr CallWndRetProc(int nCode, IntPtr wParam, IntPtr lParam) + { + if (nCode >= 0) + { + var msg = (CWPRETSTRUCT)Marshal.PtrToStructure(lParam, typeof(CWPRETSTRUCT))!; + if (msg.message == Constants.WM_INITDIALOG) + { + string name = User32.GetClassName(msg.hwnd); + if (name == Constants.ClassNameDialogBox) + { + // Localizing non-MessageBox captions + if (dialogContext.DialogType == DialogType.ColorDialog) + User32.SetControlText(msg.hwnd, Res.TitleColorDialog); + else if (dialogContext.DialogType == DialogType.FolderDialog) + User32.SetControlText(msg.hwnd, Res.TitleFolderDialog); + + // Enumerating the child controls by another WinAPI call + User32.EnumChildWindows(msg.hwnd, enumChildProc); + } + } + } + + return User32.CallNextHook(nCode, wParam, lParam); + } + + private static bool EnumChildProc(IntPtr hWnd, IntPtr lParam) + { + string className = User32.GetClassName(hWnd); + int id = User32.GetDialogControlId(hWnd); + if (id == 0) + return true; + + // Controls with id 65535 may duplicate on some dialogs. Usually these contain custom message but on color dialog + // these are also constant labels so we assign incremental negative ids for them. + if (id == UInt16.MaxValue && className == Constants.ClassNameStatic) + { + if (!dialogContext.AllowCustomStaticLocalization) + return true; + id = --dialogContext.CustomStaticId; + } + // If there is a single OK button in a MessageBox it has the same id as a Cancel button. + else if (dialogContext.DialogType == DialogType.SingleButtonMessageBox && id == Constants.IDCANCEL && className == Constants.ClassNameButton) + id = Constants.IDOK; + + string? text = Res.GetStringOrNull($"{dialogContext.DialogType}.{className}.{id}") ?? Res.GetStringOrNull($"{className}.{id}"); + if (text != null) + User32.SetControlText(hWnd, text); + return true; + } + + #endregion #endregion } diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/AdjustBrightnessForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/AdjustBrightnessForm.cs index e92832d..0fa7e5e 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/AdjustBrightnessForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/AdjustBrightnessForm.cs @@ -38,7 +38,7 @@ internal AdjustBrightnessForm(AdjustBrightnessViewModel viewModel) #region Private Constructors - private AdjustBrightnessForm() : this(null) + private AdjustBrightnessForm() : this(null!) { // this ctor is just for the designer } diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/AdjustColorsFormBase.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/AdjustColorsFormBase.Designer.cs index 11d1dbe..57dc1fb 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/AdjustColorsFormBase.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/AdjustColorsFormBase.Designer.cs @@ -39,11 +39,13 @@ private void InitializeComponent() this.trackBar.Dock = System.Windows.Forms.DockStyle.Fill; this.trackBar.Location = new System.Drawing.Point(35, 25); this.trackBar.Name = "trackBar"; + this.trackBar.RightToLeftLayout = true; this.trackBar.Size = new System.Drawing.Size(161, 31); this.trackBar.TabIndex = 2; // // btnReset // + this.btnReset.AutoSize = true; this.btnReset.Dock = System.Windows.Forms.DockStyle.Right; this.btnReset.FlatStyle = System.Windows.Forms.FlatStyle.System; this.btnReset.Location = new System.Drawing.Point(196, 25); diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/AdjustColorsFormBase.cs b/KGySoft.Drawing.ImagingTools/View/Forms/AdjustColorsFormBase.cs index 6da23d3..a7b6862 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/AdjustColorsFormBase.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/AdjustColorsFormBase.cs @@ -16,8 +16,8 @@ #region Usings -using System.Globalization; - +using System; +using System.Drawing; using KGySoft.CoreLibraries; using KGySoft.Drawing.ImagingTools.ViewModel; @@ -48,7 +48,7 @@ protected AdjustColorsFormBase(AdjustColorsViewModelBase viewModel) #region Private Constructors - private AdjustColorsFormBase() : this(null) + private AdjustColorsFormBase() : this(null!) { // this ctor is just for the designer } @@ -61,6 +61,20 @@ private AdjustColorsFormBase() : this(null) #region Protected Methods + protected override void OnLoad(EventArgs e) + { + // Fixing high DPI appearance on Mono + PointF scale; + if (OSUtils.IsMono && (scale = this.GetScale()) != new PointF(1f, 1f)) + { + pnlCheckBoxes.Height = (int)(25 * scale.Y); + btnReset.Width = (int)(64 * scale.X); + lblValue.Width = (int)(35 * scale.X); + } + + base.OnLoad(e); + } + protected override void ApplyResources() { Icon = Properties.Resources.Colors; @@ -69,6 +83,8 @@ protected override void ApplyResources() protected override void ApplyViewModel() { + if (OSUtils.IsMono) + pnlCheckBoxes.Height = this.ScaleHeight(25); InitCommandBindings(); InitPropertyBindings(); base.ApplyViewModel(); @@ -101,26 +117,26 @@ private void InitPropertyBindings() // VM.ColorChannels <-> chbRed.Checked CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.ColorChannels), chbRed, nameof(chbRed.Checked), - channels => ((ColorChannels)channels).HasFlag(ColorChannels.R), + channels => ((ColorChannels)channels!).HasFlag(ColorChannels.R), flag => flag is true ? VM.ColorChannels | ColorChannels.R : VM.ColorChannels & ~ColorChannels.R); // VM.ColorChannels <-> chbGreen.Checked CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.ColorChannels), chbGreen, nameof(chbGreen.Checked), - channels => ((ColorChannels)channels).HasFlag(ColorChannels.G), + channels => ((ColorChannels)channels!).HasFlag(ColorChannels.G), flag => flag is true ? VM.ColorChannels | ColorChannels.G : VM.ColorChannels & ~ColorChannels.G); // VM.ColorChannels <-> chbBlue.Checked CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.ColorChannels), chbBlue, nameof(chbBlue.Checked), - channels => ((ColorChannels)channels).HasFlag(ColorChannels.B), + channels => ((ColorChannels)channels!).HasFlag(ColorChannels.B), flag => flag is true ? VM.ColorChannels | ColorChannels.B : VM.ColorChannels & ~ColorChannels.B); // VM.Value <-> trackBar.Value CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.Value), trackBar, nameof(trackBar.Value), - value => (int)((float)value * 100), - value => (int)value / 100f); + value => (int)((float)value! * 100), + value => (int)value! / 100f); // VM.Value -> lblValue.Text - CommandBindings.AddPropertyBinding(ViewModel, nameof(VM.Value), nameof(lblValue.Text), v => ((float)v).ToString("F2", CultureInfo.CurrentCulture), lblValue); + CommandBindings.AddPropertyBinding(ViewModel, nameof(VM.Value), nameof(lblValue.Text), v => ((float)v!).ToString("F2", LanguageSettings.FormattingLanguage), lblValue); } #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/AdjustContrastForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/AdjustContrastForm.cs index d7da903..ee96a91 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/AdjustContrastForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/AdjustContrastForm.cs @@ -38,7 +38,7 @@ internal AdjustContrastForm(AdjustContrastViewModel viewModel) #region Private Constructors - private AdjustContrastForm() : this(null) + private AdjustContrastForm() : this(null!) { // this ctor is just for the designer } diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/AdjustGammaForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/AdjustGammaForm.cs index 5359489..073e398 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/AdjustGammaForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/AdjustGammaForm.cs @@ -38,7 +38,7 @@ internal AdjustGammaForm(AdjustGammaViewModel viewModel) #region Private Constructors - private AdjustGammaForm() : this(null) + private AdjustGammaForm() : this(null!) { // this ctor is just for the designer } diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/AppMainForm.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/AppMainForm.Designer.cs index e4ce4dc..06f191a 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/AppMainForm.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/AppMainForm.Designer.cs @@ -19,9 +19,15 @@ private void InitializeComponent() // // txtInfo // - this.txtInfo.Location = new System.Drawing.Point(0, 188); + this.txtInfo.Location = new System.Drawing.Point(0, 153); this.txtInfo.Size = new System.Drawing.Size(484, 123); // + // okCancelButtons + // + this.okCancelButtons.Location = new System.Drawing.Point(0, 276); + this.okCancelButtons.Size = new System.Drawing.Size(484, 35); + this.okCancelButtons.Visible = false; + // // AppMainForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/AppMainForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/AppMainForm.cs index 2156c4a..73e59e4 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/AppMainForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/AppMainForm.cs @@ -17,10 +17,10 @@ #region Usings using System; -using System.Drawing; -using System.Drawing.Imaging; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Windows.Forms; + using KGySoft.Drawing.ImagingTools.ViewModel; #endregion @@ -29,16 +29,11 @@ namespace KGySoft.Drawing.ImagingTools.View.Forms { internal partial class AppMainForm : ImageVisualizerForm { - #region Fields - - private static readonly string title = Res.TitleAppNameAndVersion(typeof(Res).Assembly.GetName().Version); - - #endregion - #region Properties #region Public Properties + [AllowNull] public override string Text { // base has VM.TitleCaption -> Text binding so this solution makes possible to enrich it in a compatible way @@ -70,7 +65,7 @@ internal AppMainForm(DefaultViewModel viewModel) #region Private Constructors - private AppMainForm() : this(null) + private AppMainForm() : this(null!) { // this ctor is just for the designer } @@ -89,10 +84,21 @@ protected override void ApplyViewModel() base.ApplyViewModel(); } + protected override void ApplyStringResources() + { + base.ApplyStringResources(); + Text = ViewModel.TitleCaption; + } + protected override void OnFormClosing(FormClosingEventArgs e) { if (e.CloseReason == CloseReason.UserClosing && ViewModel.IsModified) + { e.Cancel = !ViewModel.ConfirmIfModified(); + if (e.Cancel) + DialogResult = DialogResult.None; + } + base.OnFormClosing(e); } @@ -111,15 +117,17 @@ private void InitPropertyBindings() { // Base updates Text when ViewModel.TitleCaption changes. // Here adding an update also for FileName and IsModified changes in a compatible way - CommandBindings.AddPropertyChangedHandler(() => Text = ViewModel.TitleCaption, ViewModel, + CommandBindings.AddPropertyChangedHandler(() => Text = ViewModel.TitleCaption!, ViewModel, nameof(ViewModel.FileName), nameof(ViewModel.IsModified)); } - private string FormatText(string value) + private string FormatText(string? value) { - string fileName = ViewModel.FileName; + if (value == null) + return String.Empty; + string? fileName = ViewModel.FileName; string name = fileName == null ? Res.TextUnnamed : Path.GetFileName(fileName); - return String.IsNullOrEmpty(value) ? title : $"{title} [{name}{(ViewModel.IsModified ? "*" : String.Empty)}] - {value}"; + return Res.TitleAppNameWithFileName(InstallationManager.ImagingToolsVersion, name, ViewModel.IsModified ? "*" : String.Empty, value); } #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/BaseForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/BaseForm.cs index 3e8da89..e1c0ee4 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/BaseForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/BaseForm.cs @@ -17,10 +17,15 @@ #region Usings using System; +#if !NET5_0_OR_GREATER +using System.Security; using System.Collections.Specialized; +using System.Diagnostics; using System.Drawing; using System.Reflection; -using System.Runtime.InteropServices; + +using KGySoft.Drawing.ImagingTools.WinApi; +#endif using System.Windows.Forms; using KGySoft.Reflection; @@ -34,36 +39,43 @@ namespace KGySoft.Drawing.ImagingTools.View.Forms /// internal class BaseForm : Form { - #region NativeMethods class - - private static class NativeMethods - { - #region Methods - - [DllImport("user32.dll")] - internal static extern bool ScreenToClient(IntPtr hWnd, ref Point lpPoint); - - #endregion - } - - #endregion - #region Constants +#if !NET5_0_OR_GREATER + // ReSharper disable once InconsistentNaming private const int WM_NCHITTEST = 0x0084; +#endif #endregion #region Fields - private static readonly BitVector32.Section formStateRenderSizeGrip = OSUtils.IsWindows ? (BitVector32.Section)Reflector.GetField(typeof(Form), "FormStateRenderSizeGrip") : default; - private static FieldAccessor formStateField; +#if !NET5_0_OR_GREATER + private static BitVector32.Section formStateRenderSizeGrip; + private static BitVector32 formStateFallback = default; + private static FieldAccessor? formStateField; +#endif #endregion #region Properties - private BitVector32 FormState => (BitVector32)(formStateField ??= FieldAccessor.GetAccessor(typeof(Form).GetField("formState", BindingFlags.Instance | BindingFlags.NonPublic))).Get(this); +#if !NET5_0_OR_GREATER + private BitVector32 FormState + { + get + { + Debug.Assert(OSUtils.IsWindows && !OSUtils.IsMono); + if (formStateField == null) + { + formStateRenderSizeGrip = Reflector.TryGetField(typeof(Form), "FormStateRenderSizeGrip", out object? value) && value is BitVector32.Section section ? section : default; + formStateField = FieldAccessor.GetAccessor(typeof(Form).GetField("formState", BindingFlags.Instance | BindingFlags.NonPublic) ?? typeof(BaseForm).GetField(nameof(formStateFallback), BindingFlags.NonPublic | BindingFlags.Static)!); + } + + return (BitVector32)formStateField.Get(this)!; + } + } +#endif #endregion @@ -71,14 +83,16 @@ private static class NativeMethods static BaseForm() { - Type dpiHelper = Reflector.ResolveType(typeof(Form).Assembly, "System.Windows.Forms.DpiHelper"); +#if NETFRAMEWORK + Type? dpiHelper = Reflector.ResolveType(typeof(Form).Assembly, "System.Windows.Forms.DpiHelper"); if (dpiHelper == null) return; // Turning off WinForms auto resize logic to prevent interferences. // Occurs when executed as visualizer debugger and devenv.exe.config contains some random DpiAwareness Reflector.TrySetField(dpiHelper, "isInitialized", true); - Reflector.TrySetField(dpiHelper, "enableHighDpi", false); + Reflector.TrySetField(dpiHelper, "enableHighDpi", false); +#endif } #endregion @@ -87,9 +101,10 @@ static BaseForm() #region Protected Methods +#if !NET5_0_OR_GREATER protected override void WndProc(ref Message m) { - if (!OSUtils.IsWindows) + if (!OSUtils.IsWindows || OSUtils.IsMono) { base.WndProc(ref m); return; @@ -105,6 +120,7 @@ protected override void WndProc(ref Message m) break; } } +#endif protected override void Dispose(bool disposing) { @@ -117,17 +133,19 @@ protected override void Dispose(bool disposing) #region Private Methods +#if !NET5_0_OR_GREATER /// /// Bugfix: When size grip is visible, and form is above and left of the primary monitor, form cannot be dragged anymore due to forced diagonal resizing. - /// Note: will be fixed in .NET Core (see also https://github.com/dotnet/winforms/issues/1504) + /// Note: Needed only below .NET 5.0 because I fixed this directly in WinForms repository: https://github.com/dotnet/winforms/pull/2032/commits /// + [SecuritySafeCritical] private void WmNCHitTest(ref Message m) { if (FormState[formStateRenderSizeGrip] != 0) { // Here is the bug in original code: LParam contains two shorts. Without the cast negative values are positive ints Point pt = new Point(m.LParam.GetSignedLoWord(), m.LParam.GetSignedHiWord()); - NativeMethods.ScreenToClient(Handle, ref pt); + User32.ScreenToClient(this, ref pt); Size clientSize = ClientSize; if (pt.X >= clientSize.Width - 16 && pt.Y >= clientSize.Height - 16 && clientSize.Height >= 16) { @@ -144,6 +162,7 @@ private void WmNCHitTest(ref Message m) m.Result = (IntPtr)18; } } +#endif #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/ColorSpaceForm.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/ColorSpaceForm.Designer.cs index c00f089..3849fff 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/ColorSpaceForm.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/ColorSpaceForm.Designer.cs @@ -105,7 +105,6 @@ private void InitializeComponent() this.ClientSize = new System.Drawing.Size(384, 421); this.MinimumSize = new System.Drawing.Size(400, 460); this.Name = "ColorSpaceForm"; - this.Padding = new System.Windows.Forms.Padding(3, 3, 3, 0); this.Text = "ColorSpaceForm"; this.pnlSettings.ResumeLayout(false); this.gbDitherer.ResumeLayout(false); diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/ColorSpaceForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/ColorSpaceForm.cs index 491157c..18a17f9 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/ColorSpaceForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/ColorSpaceForm.cs @@ -43,6 +43,11 @@ internal ColorSpaceForm(ColorSpaceViewModel viewModel) { InitializeComponent(); + // Mono/Windows: exiting because ToolTips throw an exception if set for an embedded control and + // since they don't appear for negative padding there is simply no place for them. + if (OSUtils.IsMono && OSUtils.IsWindows) + return; + ValidationMapping[nameof(viewModel.PixelFormat)] = gbPixelFormat.CheckBox; ValidationMapping[nameof(viewModel.QuantizerSelectorViewModel.Quantizer)] = gbQuantizer.CheckBox; ValidationMapping[nameof(viewModel.DithererSelectorViewModel.Ditherer)] = gbDitherer.CheckBox; @@ -58,7 +63,7 @@ internal ColorSpaceForm(ColorSpaceViewModel viewModel) #region Private Constructors - private ColorSpaceForm() : this(null) + private ColorSpaceForm() : this(null!) { // this ctor is just for the designer } diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/ColorVisualizerForm.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/ColorVisualizerForm.Designer.cs index 072b8c8..afd85d2 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/ColorVisualizerForm.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/ColorVisualizerForm.Designer.cs @@ -18,7 +18,8 @@ partial class ColorVisualizerForm /// private void InitializeComponent() { - this.ucColorVisualizer = new ColorVisualizerControl(); + this.ucColorVisualizer = new KGySoft.Drawing.ImagingTools.View.UserControls.ColorVisualizerControl(); + this.okCancelButtons = new KGySoft.Drawing.ImagingTools.View.UserControls.OkCancelButtons(); this.SuspendLayout(); // // ucColorVisualizer @@ -26,15 +27,25 @@ private void InitializeComponent() this.ucColorVisualizer.Dock = System.Windows.Forms.DockStyle.Fill; this.ucColorVisualizer.Location = new System.Drawing.Point(0, 0); this.ucColorVisualizer.Name = "ucColorVisualizer"; - this.ucColorVisualizer.Size = new System.Drawing.Size(244, 200); + this.ucColorVisualizer.Size = new System.Drawing.Size(244, 186); this.ucColorVisualizer.TabIndex = 1; // + // okCancelButtons + // + this.okCancelButtons.BackColor = System.Drawing.Color.Transparent; + this.okCancelButtons.Dock = System.Windows.Forms.DockStyle.Bottom; + this.okCancelButtons.Location = new System.Drawing.Point(0, 186); + this.okCancelButtons.Name = "okCancelButtons"; + this.okCancelButtons.Size = new System.Drawing.Size(244, 35); + this.okCancelButtons.TabIndex = 2; + // // ColorVisualizerForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(244, 200); + this.ClientSize = new System.Drawing.Size(244, 221); this.Controls.Add(this.ucColorVisualizer); + this.Controls.Add(this.okCancelButtons); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; this.MinimumSize = new System.Drawing.Size(260, 234); this.Name = "ColorVisualizerForm"; @@ -45,5 +56,6 @@ private void InitializeComponent() #endregion private ColorVisualizerControl ucColorVisualizer; + private OkCancelButtons okCancelButtons; } } \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/ColorVisualizerForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/ColorVisualizerForm.cs index 9891f66..273af91 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/ColorVisualizerForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/ColorVisualizerForm.cs @@ -17,6 +17,8 @@ #region Usings using System.Drawing; +using System.Windows.Forms; + using KGySoft.Drawing.ImagingTools.ViewModel; #endregion @@ -33,13 +35,15 @@ internal ColorVisualizerForm(ColorVisualizerViewModel viewModel) : base(viewModel) { InitializeComponent(); + AcceptButton = okCancelButtons.OKButton; + CancelButton = okCancelButtons.CancelButton; } #endregion #region Private Constructors - private ColorVisualizerForm() : this(null) + private ColorVisualizerForm() : this(null!) { // this ctor is just for the designer } @@ -65,6 +69,26 @@ protected override void ApplyViewModel() base.ApplyViewModel(); } + protected override void OnFormClosing(FormClosingEventArgs e) + { + if (DialogResult != DialogResult.OK) + ViewModel.SetModified(false); + base.OnFormClosing(e); + } + + protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + { + switch (keyData) + { + case Keys.Escape when ViewModel.ReadOnly: // if not ReadOnly, use the Cancel button + DialogResult = DialogResult.Cancel; + return true; + + default: + return base.ProcessCmdKey(ref msg, keyData); + } + } + protected override void Dispose(bool disposing) { if (disposing) @@ -79,18 +103,26 @@ protected override void Dispose(bool disposing) private void InitPropertyBindings() { + // !VM.ReadOnly -> okCancelButtons.Visible + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.ReadOnly), nameof(okCancelButtons.Visible), ro => ro is false, okCancelButtons); + // VM.ReadOnly -> ucColorVisualizer.ReadOnly CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.ReadOnly), nameof(ucColorVisualizer.ReadOnly), ucColorVisualizer); // VM.Color -> ucColorVisualizer.Color, Text CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Color), nameof(ucColorVisualizer.Color), ucColorVisualizer); - CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Color), nameof(Text), c => Res.TitleColor((Color)c), this); + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Color), nameof(Text), c => Res.TitleColor((Color)c!), this); + + // VM.IsModified -> OKButton.Enabled + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.IsModified), nameof(okCancelButtons.OKButton.Enabled), okCancelButtons.OKButton); } private void InitCommandBindings() { CommandBindings.Add(OnColorEditedCommand) .AddSource(ucColorVisualizer, nameof(ucColorVisualizer.ColorEdited)); + CommandBindings.Add(OnCancelCommand) + .AddSource(okCancelButtons.CancelButton, nameof(okCancelButtons.CancelButton.Click)); } #endregion @@ -104,6 +136,8 @@ private void OnColorEditedCommand() ViewModel.Color = ucColorVisualizer.Color; } + private void OnCancelCommand() => ViewModel.SetModified(false); + #endregion #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/CountColorsForm.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/CountColorsForm.Designer.cs index 7cacc14..0227f89 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/CountColorsForm.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/CountColorsForm.Designer.cs @@ -15,23 +15,23 @@ partial class CountColorsForm /// private void InitializeComponent() { - this.lblResult = new System.Windows.Forms.Label(); + this.lblCountColorsStatus = new System.Windows.Forms.Label(); this.pnlButton = new System.Windows.Forms.Panel(); this.btnClose = new System.Windows.Forms.Button(); - this.progress = new KGySoft.Drawing.ImagingTools.View.Controls.DrawingProgressStatusStrip(); + this.progress = new KGySoft.Drawing.ImagingTools.View.Controls.DrawingProgressFooter(); this.pnlButton.SuspendLayout(); this.SuspendLayout(); // - // lblResult + // lblCountColorsStatus // - this.lblResult.Dock = System.Windows.Forms.DockStyle.Fill; - this.lblResult.Location = new System.Drawing.Point(0, 0); - this.lblResult.Name = "lblResult"; - this.lblResult.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0); - this.lblResult.Size = new System.Drawing.Size(274, 39); - this.lblResult.TabIndex = 0; - this.lblResult.Text = "lblResult"; - this.lblResult.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + this.lblCountColorsStatus.Dock = System.Windows.Forms.DockStyle.Fill; + this.lblCountColorsStatus.Location = new System.Drawing.Point(0, 0); + this.lblCountColorsStatus.Name = "lblCountColorsStatus"; + this.lblCountColorsStatus.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0); + this.lblCountColorsStatus.Size = new System.Drawing.Size(274, 39); + this.lblCountColorsStatus.TabIndex = 0; + this.lblCountColorsStatus.Text = "lblCountColorsStatus"; + this.lblCountColorsStatus.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; // // pnlButton // @@ -59,7 +59,6 @@ private void InitializeComponent() this.progress.Location = new System.Drawing.Point(0, 68); this.progress.Name = "progress"; this.progress.Size = new System.Drawing.Size(274, 22); - this.progress.SizingGrip = false; this.progress.TabIndex = 2; this.progress.Text = "drawingProgressStatusStrip1"; // @@ -68,7 +67,7 @@ private void InitializeComponent() this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(274, 90); - this.Controls.Add(this.lblResult); + this.Controls.Add(this.lblCountColorsStatus); this.Controls.Add(this.pnlButton); this.Controls.Add(this.progress); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; @@ -84,8 +83,8 @@ private void InitializeComponent() #endregion - private Controls.DrawingProgressStatusStrip progress; - private System.Windows.Forms.Label lblResult; + private Controls.DrawingProgressFooter progress; + private System.Windows.Forms.Label lblCountColorsStatus; private System.Windows.Forms.Panel pnlButton; private System.Windows.Forms.Button btnClose; } diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/CountColorsForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/CountColorsForm.cs index 455844d..82a6610 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/CountColorsForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/CountColorsForm.cs @@ -16,7 +16,6 @@ #region Usings -using System; using System.Windows.Forms; using KGySoft.Drawing.ImagingTools.ViewModel; @@ -42,7 +41,7 @@ internal CountColorsForm(CountColorsViewModel viewModel) #region Private Constructors - private CountColorsForm() : this(null) + private CountColorsForm() : this(null!) { // this ctor is just for the designer } @@ -93,15 +92,14 @@ private void InitCommandBindings() private void InitPropertyBindings() { - // VM.DisplayText <-> lblResult.Text - CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.DisplayText), nameof(lblResult.Text), lblResult); + // VM.DisplayText <-> lblCountColorsStatus.Text + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.DisplayText), nameof(lblCountColorsStatus.Text), lblCountColorsStatus); // VM.IsProcessing -> progress.ProgressVisible CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.IsProcessing), nameof(progress.ProgressVisible), progress); - // VM.Progress -> progress.Progress (in lock because it is already running) - lock (ViewModel.ProgressSyncRoot) - CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Progress), nameof(progress.Progress), progress); + // VM.Progress -> progress.Progress + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Progress), nameof(progress.Progress), progress); } #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/DownloadResourcesForm.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/DownloadResourcesForm.Designer.cs new file mode 100644 index 0000000..209864c --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Forms/DownloadResourcesForm.Designer.cs @@ -0,0 +1,142 @@ + +namespace KGySoft.Drawing.ImagingTools.View.Forms +{ + partial class DownloadResourcesForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.bindingSource = new System.Windows.Forms.BindingSource(this.components); + this.okCancelButtons = new KGySoft.Drawing.ImagingTools.View.UserControls.OkCancelButtons(); + this.gridDownloadableResources = new KGySoft.Drawing.ImagingTools.View.Controls.AdvancedDataGridView(); + this.colSelected = new System.Windows.Forms.DataGridViewCheckBoxColumn(); + this.colLanguage = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colAuthor = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colImagingToolsVersion = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colDescription = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.progress = new KGySoft.Drawing.ImagingTools.View.Controls.DownloadProgressFooter(); + ((System.ComponentModel.ISupportInitialize)(this.bindingSource)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.gridDownloadableResources)).BeginInit(); + this.SuspendLayout(); + // + // bindingSource + // + this.bindingSource.DataSource = typeof(KGySoft.Drawing.ImagingTools.ViewModel.DownloadableResourceItem); + // + // okCancelButtons + // + this.okCancelButtons.Dock = System.Windows.Forms.DockStyle.Bottom; + this.okCancelButtons.Location = new System.Drawing.Point(3, 166); + this.okCancelButtons.Name = "okCancelButtons"; + this.okCancelButtons.Size = new System.Drawing.Size(358, 35); + this.okCancelButtons.TabIndex = 1; + // + // gridDownloadableResources + // + this.gridDownloadableResources.AllowUserToAddRows = false; + this.gridDownloadableResources.AllowUserToDeleteRows = false; + this.gridDownloadableResources.AutoGenerateColumns = false; + this.gridDownloadableResources.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.colSelected, + this.colLanguage, + this.colAuthor, + this.colImagingToolsVersion, + this.colDescription}); + this.gridDownloadableResources.DataSource = this.bindingSource; + this.gridDownloadableResources.Dock = System.Windows.Forms.DockStyle.Fill; + this.gridDownloadableResources.Location = new System.Drawing.Point(3, 3); + this.gridDownloadableResources.MultiSelect = false; + this.gridDownloadableResources.Name = "gridDownloadableResources"; + this.gridDownloadableResources.Size = new System.Drawing.Size(358, 163); + this.gridDownloadableResources.TabIndex = 0; + // + // colSelected + // + this.colSelected.DataPropertyName = "Selected"; + this.colSelected.HeaderText = "colSelected"; + this.colSelected.Name = "colSelected"; + this.colSelected.Width = 50; + // + // colLanguage + // + this.colLanguage.DataPropertyName = "Language"; + this.colLanguage.HeaderText = "colLanguage"; + this.colLanguage.Name = "colLanguage"; + this.colLanguage.ReadOnly = true; + // + // colAuthor + // + this.colAuthor.DataPropertyName = "Author"; + this.colAuthor.HeaderText = "colAuthor"; + this.colAuthor.Name = "colAuthor"; + this.colAuthor.ReadOnly = true; + // + // colImagingToolsVersion + // + this.colImagingToolsVersion.DataPropertyName = "ImagingToolsVersion"; + this.colImagingToolsVersion.HeaderText = "colImagingToolsVersion"; + this.colImagingToolsVersion.Name = "colImagingToolsVersion"; + this.colImagingToolsVersion.ReadOnly = true; + this.colImagingToolsVersion.Width = 60; + // + // colDescription + // + this.colDescription.DataPropertyName = "Description"; + this.colDescription.HeaderText = "colDescription"; + this.colDescription.Name = "colDescription"; + this.colDescription.ReadOnly = true; + this.colDescription.Width = 120; + // + // progress + // + this.progress.BackColor = System.Drawing.Color.Transparent; + this.progress.Location = new System.Drawing.Point(3, 201); + this.progress.Name = "progress"; + this.progress.Size = new System.Drawing.Size(358, 22); + this.progress.TabIndex = 2; + this.progress.Text = "drawingProgressStatusStrip1"; + // + // DownloadResourcesForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(364, 226); + this.Controls.Add(this.gridDownloadableResources); + this.Controls.Add(this.okCancelButtons); + this.Controls.Add(this.progress); + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(350, 250); + this.Name = "DownloadResourcesForm"; + this.Padding = new System.Windows.Forms.Padding(3); + this.Text = "DownloadResourcesForm"; + ((System.ComponentModel.ISupportInitialize)(this.bindingSource)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.gridDownloadableResources)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private Controls.DownloadProgressFooter progress; + private Controls.AdvancedDataGridView gridDownloadableResources; + private System.Windows.Forms.BindingSource bindingSource; + private UserControls.OkCancelButtons okCancelButtons; + private System.Windows.Forms.DataGridViewCheckBoxColumn colSelected; + private System.Windows.Forms.DataGridViewTextBoxColumn colLanguage; + private System.Windows.Forms.DataGridViewTextBoxColumn colAuthor; + private System.Windows.Forms.DataGridViewTextBoxColumn colImagingToolsVersion; + private System.Windows.Forms.DataGridViewTextBoxColumn colDescription; + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/DownloadResourcesForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/DownloadResourcesForm.cs new file mode 100644 index 0000000..176811b --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Forms/DownloadResourcesForm.cs @@ -0,0 +1,126 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: DownloadResourcesForm.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System.Windows.Forms; + +using KGySoft.Drawing.ImagingTools.ViewModel; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Forms +{ + internal partial class DownloadResourcesForm : MvvmBaseForm + { + #region Constructors + + #region Internal Constructors + + internal DownloadResourcesForm(DownloadResourcesViewModel viewModel) : base(viewModel) + { + InitializeComponent(); + okCancelButtons.OKButton.Name = okCancelButtons.OKButton.Text = @"btnDownload"; + okCancelButtons.OKButton.DialogResult = DialogResult.None; + AcceptButton = okCancelButtons.OKButton; + CancelButton = okCancelButtons.CancelButton; + } + + #endregion + + #region Private Constructors + + private DownloadResourcesForm() : this(null!) + { + // this ctor is just for the designer + } + + #endregion + + #endregion + + #region Methods + + #region Protected Methods + + protected override void ApplyResources() + { + Icon = Properties.Resources.Language; + base.ApplyResources(); + } + + protected override void ApplyViewModel() + { + InitCommandBindings(); + InitPropertyBindings(); + base.ApplyViewModel(); + } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + ViewModel.CancelIfRunning(); + base.OnFormClosing(e); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + components?.Dispose(); + + base.Dispose(disposing); + } + + #endregion + + #region Private Methods + + private void InitCommandBindings() + { + CommandBindings.Add(ViewModel.DownloadCommand, ViewModel.DownloadCommandState) + .AddSource(okCancelButtons.OKButton, nameof(okCancelButtons.OKButton.Click)); + CommandBindings.Add(ViewModel.CancelCommand) + .AddSource(okCancelButtons.CancelButton, nameof(okCancelButtons.CancelButton.Click)); + CommandBindings.Add(OnCellContentClickCommand) + .AddSource(gridDownloadableResources, nameof(gridDownloadableResources.CellContentClick)); + } + + private void InitPropertyBindings() + { + // VM.IsProcessing -> progress.ProgressVisible + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.IsProcessing), nameof(progress.ProgressVisible), progress); + + // VM.Progress -> progress.Progress + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Progress), nameof(progress.Progress), progress); + + // VM.Items -> bindingSource.DataSource + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Items), nameof(bindingSource.DataSource), bindingSource); + } + + #endregion + + #region Command Handlers + + private void OnCellContentClickCommand() + { + if (gridDownloadableResources.CurrentCell is DataGridViewCheckBoxCell { EditingCellValueChanged: true }) + gridDownloadableResources.CommitEdit(DataGridViewDataErrorContexts.Commit); + } + + #endregion + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/EditResourcesForm.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/EditResourcesForm.Designer.cs new file mode 100644 index 0000000..c2a9f4b --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Forms/EditResourcesForm.Designer.cs @@ -0,0 +1,313 @@ + +namespace KGySoft.Drawing.ImagingTools.View.Forms +{ + partial class EditResourcesForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle(); + this.gbResourceEntries = new System.Windows.Forms.GroupBox(); + this.gridResources = new KGySoft.Drawing.ImagingTools.View.Controls.AdvancedDataGridView(); + this.colResourceKey = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colOriginalText = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colTranslatedText = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.bindingSource = new System.Windows.Forms.BindingSource(this.components); + this.pnlFilter = new KGySoft.Drawing.ImagingTools.View.Controls.AutoMirrorPanel(); + this.txtFilter = new System.Windows.Forms.TextBox(); + this.lblFilter = new System.Windows.Forms.Label(); + this.gbResourceFile = new System.Windows.Forms.GroupBox(); + this.cmbResourceFiles = new System.Windows.Forms.ComboBox(); + this.splitterEditResources = new System.Windows.Forms.Splitter(); + this.pnlEditResourceEntry = new System.Windows.Forms.TableLayoutPanel(); + this.gbOriginalText = new System.Windows.Forms.GroupBox(); + this.txtOriginalText = new System.Windows.Forms.TextBox(); + this.gbTranslatedText = new System.Windows.Forms.GroupBox(); + this.txtTranslatedText = new System.Windows.Forms.TextBox(); + this.okCancelApplyButtons = new KGySoft.Drawing.ImagingTools.View.UserControls.OkCancelButtons(); + this.gbResourceEntries.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.gridResources)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.bindingSource)).BeginInit(); + this.pnlFilter.SuspendLayout(); + this.gbResourceFile.SuspendLayout(); + this.pnlEditResourceEntry.SuspendLayout(); + this.gbOriginalText.SuspendLayout(); + this.gbTranslatedText.SuspendLayout(); + this.SuspendLayout(); + // + // gbResourceEntries + // + this.gbResourceEntries.Controls.Add(this.gridResources); + this.gbResourceEntries.Controls.Add(this.pnlFilter); + this.gbResourceEntries.Dock = System.Windows.Forms.DockStyle.Fill; + this.gbResourceEntries.Location = new System.Drawing.Point(3, 49); + this.gbResourceEntries.Name = "gbResourceEntries"; + this.gbResourceEntries.Size = new System.Drawing.Size(578, 117); + this.gbResourceEntries.TabIndex = 2; + this.gbResourceEntries.TabStop = false; + this.gbResourceEntries.Text = "gbResourceEntries"; + // + // gridResources + // + this.gridResources.AllowUserToAddRows = false; + this.gridResources.AllowUserToDeleteRows = false; + this.gridResources.AutoGenerateColumns = false; + this.gridResources.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.gridResources.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.colResourceKey, + this.colOriginalText, + this.colTranslatedText}); + this.gridResources.DataSource = this.bindingSource; + dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; + dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Window; + dataGridViewCellStyle1.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238))); + dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.WindowText; + dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight; + dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText; + dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True; + this.gridResources.DefaultCellStyle = dataGridViewCellStyle1; + this.gridResources.Dock = System.Windows.Forms.DockStyle.Fill; + this.gridResources.Location = new System.Drawing.Point(3, 40); + this.gridResources.MultiSelect = false; + this.gridResources.Name = "gridResources"; + this.gridResources.Size = new System.Drawing.Size(572, 74); + this.gridResources.TabIndex = 3; + // + // colResourceKey + // + this.colResourceKey.DataPropertyName = "Key"; + this.colResourceKey.HeaderText = "colResourceKey"; + this.colResourceKey.Name = "colResourceKey"; + this.colResourceKey.ReadOnly = true; + // + // colOriginalText + // + this.colOriginalText.DataPropertyName = "OriginalText"; + this.colOriginalText.HeaderText = "colOriginalText"; + this.colOriginalText.Name = "colOriginalText"; + this.colOriginalText.ReadOnly = true; + this.colOriginalText.Width = 200; + // + // colTranslatedText + // + this.colTranslatedText.DataPropertyName = "TranslatedText"; + this.colTranslatedText.HeaderText = "colTranslatedText"; + this.colTranslatedText.Name = "colTranslatedText"; + this.colTranslatedText.Width = 200; + // + // bindingSource + // + this.bindingSource.DataSource = typeof(KGySoft.Drawing.ImagingTools.Model.ResourceEntry); + // + // pnlFilter + // + this.pnlFilter.Controls.Add(this.txtFilter); + this.pnlFilter.Controls.Add(this.lblFilter); + this.pnlFilter.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlFilter.Location = new System.Drawing.Point(3, 16); + this.pnlFilter.Name = "pnlFilter"; + this.pnlFilter.Padding = new System.Windows.Forms.Padding(0, 2, 0, 2); + this.pnlFilter.Size = new System.Drawing.Size(572, 24); + this.pnlFilter.TabIndex = 4; + // + // txtFilter + // + this.txtFilter.Dock = System.Windows.Forms.DockStyle.Fill; + this.txtFilter.Location = new System.Drawing.Point(39, 2); + this.txtFilter.Name = "txtFilter"; + this.txtFilter.Size = new System.Drawing.Size(533, 20); + this.txtFilter.TabIndex = 1; + // + // lblFilter + // + this.lblFilter.AutoSize = true; + this.lblFilter.Dock = System.Windows.Forms.DockStyle.Left; + this.lblFilter.Location = new System.Drawing.Point(0, 2); + this.lblFilter.Name = "lblFilter"; + this.lblFilter.Padding = new System.Windows.Forms.Padding(0, 2, 0, 0); + this.lblFilter.Size = new System.Drawing.Size(39, 15); + this.lblFilter.TabIndex = 0; + this.lblFilter.Text = "lblFilter"; + // + // gbResourceFile + // + this.gbResourceFile.Controls.Add(this.cmbResourceFiles); + this.gbResourceFile.Dock = System.Windows.Forms.DockStyle.Top; + this.gbResourceFile.Location = new System.Drawing.Point(3, 3); + this.gbResourceFile.Name = "gbResourceFile"; + this.gbResourceFile.Size = new System.Drawing.Size(578, 46); + this.gbResourceFile.TabIndex = 5; + this.gbResourceFile.TabStop = false; + this.gbResourceFile.Text = "gbResourceFile"; + // + // cmbResourceFiles + // + this.cmbResourceFiles.Dock = System.Windows.Forms.DockStyle.Fill; + this.cmbResourceFiles.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbResourceFiles.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.cmbResourceFiles.FormattingEnabled = true; + this.cmbResourceFiles.Location = new System.Drawing.Point(3, 16); + this.cmbResourceFiles.Name = "cmbResourceFiles"; + this.cmbResourceFiles.Size = new System.Drawing.Size(572, 21); + this.cmbResourceFiles.TabIndex = 2; + // + // splitterEditResources + // + this.splitterEditResources.Dock = System.Windows.Forms.DockStyle.Bottom; + this.splitterEditResources.Location = new System.Drawing.Point(3, 166); + this.splitterEditResources.MinExtra = 50; + this.splitterEditResources.MinSize = 50; + this.splitterEditResources.Name = "splitterEditResources"; + this.splitterEditResources.Size = new System.Drawing.Size(578, 3); + this.splitterEditResources.TabIndex = 4; + this.splitterEditResources.TabStop = false; + // + // pnlEditResourceEntry + // + this.pnlEditResourceEntry.ColumnCount = 2; + this.pnlEditResourceEntry.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); + this.pnlEditResourceEntry.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); + this.pnlEditResourceEntry.Controls.Add(this.gbOriginalText, 0, 0); + this.pnlEditResourceEntry.Controls.Add(this.gbTranslatedText, 1, 0); + this.pnlEditResourceEntry.Dock = System.Windows.Forms.DockStyle.Bottom; + this.pnlEditResourceEntry.Location = new System.Drawing.Point(3, 169); + this.pnlEditResourceEntry.Name = "pnlEditResourceEntry"; + this.pnlEditResourceEntry.RowCount = 1; + this.pnlEditResourceEntry.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); + this.pnlEditResourceEntry.Size = new System.Drawing.Size(578, 104); + this.pnlEditResourceEntry.TabIndex = 3; + // + // gbOriginalText + // + this.gbOriginalText.Controls.Add(this.txtOriginalText); + this.gbOriginalText.Dock = System.Windows.Forms.DockStyle.Fill; + this.gbOriginalText.Location = new System.Drawing.Point(3, 3); + this.gbOriginalText.Name = "gbOriginalText"; + this.gbOriginalText.Size = new System.Drawing.Size(283, 98); + this.gbOriginalText.TabIndex = 0; + this.gbOriginalText.TabStop = false; + this.gbOriginalText.Text = "gbOriginalText"; + // + // txtOriginalText + // + this.txtOriginalText.BackColor = System.Drawing.SystemColors.Control; + this.txtOriginalText.Dock = System.Windows.Forms.DockStyle.Fill; + this.txtOriginalText.ForeColor = System.Drawing.SystemColors.ControlText; + this.txtOriginalText.Location = new System.Drawing.Point(3, 16); + this.txtOriginalText.Multiline = true; + this.txtOriginalText.Name = "txtOriginalText"; + this.txtOriginalText.ReadOnly = true; + this.txtOriginalText.ScrollBars = System.Windows.Forms.ScrollBars.Both; + this.txtOriginalText.Size = new System.Drawing.Size(277, 79); + this.txtOriginalText.TabIndex = 0; + this.txtOriginalText.WordWrap = false; + // + // gbTranslatedText + // + this.gbTranslatedText.Controls.Add(this.txtTranslatedText); + this.gbTranslatedText.Dock = System.Windows.Forms.DockStyle.Fill; + this.gbTranslatedText.Location = new System.Drawing.Point(292, 3); + this.gbTranslatedText.Name = "gbTranslatedText"; + this.gbTranslatedText.Size = new System.Drawing.Size(283, 98); + this.gbTranslatedText.TabIndex = 1; + this.gbTranslatedText.TabStop = false; + this.gbTranslatedText.Text = "gbTranslatedText"; + // + // txtTranslatedText + // + this.txtTranslatedText.AcceptsTab = true; + this.txtTranslatedText.Dock = System.Windows.Forms.DockStyle.Fill; + this.txtTranslatedText.Location = new System.Drawing.Point(3, 16); + this.txtTranslatedText.Multiline = true; + this.txtTranslatedText.Name = "txtTranslatedText"; + this.txtTranslatedText.ScrollBars = System.Windows.Forms.ScrollBars.Both; + this.txtTranslatedText.Size = new System.Drawing.Size(277, 79); + this.txtTranslatedText.TabIndex = 1; + this.txtTranslatedText.WordWrap = false; + // + // okCancelApplyButtons + // + this.okCancelApplyButtons.ApplyButtonVisible = true; + this.okCancelApplyButtons.BackColor = System.Drawing.Color.Transparent; + this.okCancelApplyButtons.Dock = System.Windows.Forms.DockStyle.Bottom; + this.okCancelApplyButtons.Location = new System.Drawing.Point(3, 273); + this.okCancelApplyButtons.Name = "okCancelApplyButtons"; + this.okCancelApplyButtons.Size = new System.Drawing.Size(578, 35); + this.okCancelApplyButtons.TabIndex = 1; + // + // EditResourcesForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(584, 311); + this.Controls.Add(this.gbResourceEntries); + this.Controls.Add(this.gbResourceFile); + this.Controls.Add(this.splitterEditResources); + this.Controls.Add(this.pnlEditResourceEntry); + this.Controls.Add(this.okCancelApplyButtons); + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(300, 300); + this.Name = "EditResourcesForm"; + this.Padding = new System.Windows.Forms.Padding(3); + this.Text = "EditResourcesForm"; + this.gbResourceEntries.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.gridResources)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.bindingSource)).EndInit(); + this.pnlFilter.ResumeLayout(false); + this.pnlFilter.PerformLayout(); + this.gbResourceFile.ResumeLayout(false); + this.pnlEditResourceEntry.ResumeLayout(false); + this.gbOriginalText.ResumeLayout(false); + this.gbOriginalText.PerformLayout(); + this.gbTranslatedText.ResumeLayout(false); + this.gbTranslatedText.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private UserControls.OkCancelButtons okCancelApplyButtons; + private System.Windows.Forms.GroupBox gbResourceEntries; + private Controls.AdvancedDataGridView gridResources; + private System.Windows.Forms.TableLayoutPanel pnlEditResourceEntry; + private System.Windows.Forms.GroupBox gbOriginalText; + private System.Windows.Forms.GroupBox gbTranslatedText; + private System.Windows.Forms.Splitter splitterEditResources; + private System.Windows.Forms.TextBox txtOriginalText; + private System.Windows.Forms.TextBox txtTranslatedText; + private System.Windows.Forms.BindingSource bindingSource; + private System.Windows.Forms.GroupBox gbResourceFile; + private System.Windows.Forms.ComboBox cmbResourceFiles; + private System.Windows.Forms.DataGridViewTextBoxColumn colResourceKey; + private System.Windows.Forms.DataGridViewTextBoxColumn colOriginalText; + private System.Windows.Forms.DataGridViewTextBoxColumn colTranslatedText; + private Controls.AutoMirrorPanel pnlFilter; + private System.Windows.Forms.TextBox txtFilter; + private System.Windows.Forms.Label lblFilter; + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/EditResourcesForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/EditResourcesForm.cs new file mode 100644 index 0000000..f135388 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Forms/EditResourcesForm.cs @@ -0,0 +1,148 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: EditResourcesForm.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System.Collections.Generic; +using System.Windows.Forms; + +using KGySoft.Drawing.ImagingTools.Model; +using KGySoft.Drawing.ImagingTools.ViewModel; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Forms +{ + internal partial class EditResourcesForm : MvvmBaseForm + { + #region Constructors + + #region Internal Constructors + + internal EditResourcesForm(EditResourcesViewModel viewModel) : base(viewModel) + { + // Note: Not setting Accept/CancelButton because they would be very annoying during the editing + InitializeComponent(); + cmbResourceFiles.ValueMember = nameof(KeyValuePair.Key); + cmbResourceFiles.DisplayMember = nameof(KeyValuePair.Value); + ErrorProvider.SetIconAlignment(gbTranslatedText, ErrorIconAlignment.MiddleLeft); + WarningProvider.SetIconAlignment(gbTranslatedText, ErrorIconAlignment.MiddleLeft); + ValidationMapping[nameof(ResourceEntry.TranslatedText)] = gbTranslatedText; + + // For Linux/Mono adding an empty column in the middle so the error provider icon will not appear in a new row + if (!OSUtils.IsWindows) + { + pnlEditResourceEntry.ColumnCount = 3; + pnlEditResourceEntry.SetColumn(gbTranslatedText, 2); + pnlEditResourceEntry.ColumnStyles.Insert(1, new ColumnStyle(SizeType.AutoSize)); + } + } + + #endregion + + #region Private Constructors + + private EditResourcesForm() : this(null!) + { + // this ctor is just for the designer + } + + #endregion + + #endregion + + #region Methods + + #region Protected Methods + + protected override void ApplyResources() + { + base.ApplyResources(); + Icon = Properties.Resources.Language; + } + + protected override void ApplyViewModel() + { + InitPropertyBindings(); + InitCommandBindings(); + base.ApplyViewModel(); + } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + if (gridResources.IsCurrentCellInEditMode) + { + gridResources.CancelEdit(); + gridResources.EndEdit(); + } + + base.OnFormClosing(e); + } + + #endregion + + #region Private Methods + + private void InitPropertyBindings() + { + // VM.ResourceFiles -> cmbResourceFiles.DataSource + cmbResourceFiles.DataSource = ViewModel.ResourceFiles; + + // VM.TitleCaption -> Text + CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(ViewModel.TitleCaption), this, nameof(Text)); + + // VM.SelectedLibrary <-> cmbResourceFiles.SelectedValue + CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(ViewModel.SelectedLibrary), cmbResourceFiles, nameof(cmbResourceFiles.SelectedValue)); + + // txtFilter.Text -> VM.Filter + CommandBindings.AddPropertyBinding(txtFilter, nameof(txtFilter.Text), nameof(ViewModel.Filter), ViewModel); + + // VM.FilteredSet -> bindingSource.DataSource + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.FilteredSet), nameof(bindingSource.DataSource), bindingSource); + + // bindingSource.OriginalText -> txtOriginalText.Text + txtOriginalText.DataBindings.Add(nameof(txtOriginalText.Text), bindingSource, nameof(ResourceEntry.OriginalText), false, DataSourceUpdateMode.Never); + + // bindingSource.TranslatedText <-> txtTranslatedText.Text + txtTranslatedText.DataBindings.Add(nameof(txtTranslatedText.Text), bindingSource, nameof(ResourceEntry.TranslatedText), false, DataSourceUpdateMode.OnValidation); + } + + private void InitCommandBindings() + { + // ApplyButton.Click -> ViewModel.ApplyResourcesCommand + CommandBindings.Add(ViewModel.ApplyResourcesCommand, ViewModel.ApplyResourcesCommandState) + .AddSource(okCancelApplyButtons.ApplyButton, nameof(okCancelApplyButtons.ApplyButton.Click)); + + // OKButton.Click -> ViewModel.SaveResourcesCommand, and preventing closing the form if the command has executed with errors + CommandBindings.Add(ViewModel.SaveResourcesCommand) + .AddSource(okCancelApplyButtons.OKButton, nameof(okCancelApplyButtons.OKButton.Click)) + .Executed += (_, args) => DialogResult = args.State[EditResourcesViewModel.StateSaveExecutedWithError] is true ? DialogResult.None : DialogResult.OK; + + // CancelButton.Click -> ViewModel.CancelResourcesCommand + CommandBindings.Add(ViewModel.CancelEditCommand) + .AddSource(okCancelApplyButtons.CancelButton, nameof(okCancelApplyButtons.CancelButton.Click)); + + // View commands + CommandBindings.Add(ValidationResultsChangedCommand) + .AddSource(bindingSource, nameof(bindingSource.CurrentItemChanged)) + .WithParameter(() => (bindingSource.Current as ResourceEntry)?.ValidationResults); + } + + #endregion + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/GraphicsVisualizerForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/GraphicsVisualizerForm.cs index 5f949dc..5d4f673 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/GraphicsVisualizerForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/GraphicsVisualizerForm.cs @@ -16,7 +16,6 @@ #region Usings -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Windows.Forms; @@ -30,12 +29,7 @@ internal partial class GraphicsVisualizerForm : ImageVisualizerForm { #region Fields - [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", - Justification = "False alarm, added to tsMenu, which is disposed by base")] private readonly ToolStripButton btnCrop; - - [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", - Justification = "False alarm, added to tsMenu, which is disposed by base")] private readonly ToolStripButton btnHighlightClip; #endregion @@ -77,7 +71,7 @@ internal GraphicsVisualizerForm(GraphicsVisualizerViewModel viewModel) #region Private Constructors - private GraphicsVisualizerForm() : this(null) + private GraphicsVisualizerForm() : this(null!) { // this ctor is just for the designer } diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/ImageVisualizerForm.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/ImageVisualizerForm.Designer.cs index 091882a..32364ed 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/ImageVisualizerForm.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/ImageVisualizerForm.Designer.cs @@ -1,4 +1,5 @@ using System.Windows.Forms; +using KGySoft.Drawing.ImagingTools.View.Components; using KGySoft.Drawing.ImagingTools.View.Controls; namespace KGySoft.Drawing.ImagingTools.View.Forms @@ -22,26 +23,25 @@ private void InitializeComponent() this.dlgOpen = new System.Windows.Forms.OpenFileDialog(); this.dlgSave = new System.Windows.Forms.SaveFileDialog(); this.timerPlayer = new System.Windows.Forms.Timer(this.components); - this.toolTip = new System.Windows.Forms.ToolTip(this.components); this.imageViewer = new KGySoft.Drawing.ImagingTools.View.Controls.ImageViewer(); this.lblNotification = new KGySoft.Drawing.ImagingTools.View.Controls.NotificationLabel(); this.splitter = new System.Windows.Forms.Splitter(); - this.tsMenu = new KGySoft.Drawing.ImagingTools.View.Controls.ScalingToolStrip(); - this.btnAutoZoom = new System.Windows.Forms.ToolStripButton(); + this.tsMenu = new KGySoft.Drawing.ImagingTools.View.Controls.AdvancedToolStrip(); + this.btnZoom = new KGySoft.Drawing.ImagingTools.View.Components.ZoomSplitButton(); this.btnAntiAlias = new System.Windows.Forms.ToolStripButton(); this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); this.btnOpen = new System.Windows.Forms.ToolStripButton(); this.btnSave = new System.Windows.Forms.ToolStripButton(); this.btnClear = new System.Windows.Forms.ToolStripButton(); this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator(); - this.btnColorSettings = new KGySoft.Drawing.ImagingTools.View.Controls.ScalingToolStripDropDownButton(); + this.btnColorSettings = new KGySoft.Drawing.ImagingTools.View.Components.ScalingToolStripDropDownButton(); this.miBackColor = new System.Windows.Forms.ToolStripMenuItem(); this.miBackColorDefault = new System.Windows.Forms.ToolStripMenuItem(); this.miBackColorWhite = new System.Windows.Forms.ToolStripMenuItem(); this.miBackColorBlack = new System.Windows.Forms.ToolStripMenuItem(); this.miShowPalette = new System.Windows.Forms.ToolStripMenuItem(); this.miCountColors = new System.Windows.Forms.ToolStripMenuItem(); - this.btnEdit = new KGySoft.Drawing.ImagingTools.View.Controls.ScalingToolStripDropDownButton(); + this.btnEdit = new KGySoft.Drawing.ImagingTools.View.Components.ScalingToolStripDropDownButton(); this.miRotateLeft = new System.Windows.Forms.ToolStripMenuItem(); this.miRotateRight = new System.Windows.Forms.ToolStripMenuItem(); this.miResizeBitmap = new System.Windows.Forms.ToolStripMenuItem(); @@ -55,8 +55,20 @@ private void InitializeComponent() this.btnCompound = new System.Windows.Forms.ToolStripButton(); this.btnPrev = new System.Windows.Forms.ToolStripButton(); this.btnNext = new System.Windows.Forms.ToolStripButton(); - this.btnConfiguration = new System.Windows.Forms.ToolStripButton(); + this.toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator(); + this.btnAbout = new System.Windows.Forms.ToolStripSplitButton(); + this.miWebSite = new System.Windows.Forms.ToolStripMenuItem(); + this.miGitHub = new System.Windows.Forms.ToolStripMenuItem(); + this.miMarketplace = new System.Windows.Forms.ToolStripMenuItem(); + this.miSubmitResources = new System.Windows.Forms.ToolStripMenuItem(); + this.miSeparatorAbout = new System.Windows.Forms.ToolStripSeparator(); + this.miAbout = new System.Windows.Forms.ToolStripMenuItem(); + this.miEasterEgg = new System.Windows.Forms.ToolStripMenuItem(); + this.btnConfiguration = new KGySoft.Drawing.ImagingTools.View.Components.AdvancedToolStripSplitButton(); + this.miManageInstallations = new System.Windows.Forms.ToolStripMenuItem(); + this.miLanguageSettings = new System.Windows.Forms.ToolStripMenuItem(); this.txtInfo = new System.Windows.Forms.TextBox(); + this.okCancelButtons = new KGySoft.Drawing.ImagingTools.View.UserControls.OkCancelButtons(); this.tsMenu.SuspendLayout(); this.SuspendLayout(); // @@ -65,7 +77,7 @@ private void InitializeComponent() this.imageViewer.Dock = System.Windows.Forms.DockStyle.Fill; this.imageViewer.Location = new System.Drawing.Point(0, 49); this.imageViewer.Name = "imageViewer"; - this.imageViewer.Size = new System.Drawing.Size(334, 141); + this.imageViewer.Size = new System.Drawing.Size(364, 106); this.imageViewer.TabIndex = 1; this.imageViewer.TabStop = false; // @@ -80,24 +92,25 @@ private void InitializeComponent() this.lblNotification.Location = new System.Drawing.Point(0, 25); this.lblNotification.Name = "lblNotification"; this.lblNotification.Padding = new System.Windows.Forms.Padding(3, 3, 20, 3); - this.lblNotification.Size = new System.Drawing.Size(334, 24); + this.lblNotification.Size = new System.Drawing.Size(364, 24); this.lblNotification.TabIndex = 4; this.lblNotification.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // splitter // this.splitter.Dock = System.Windows.Forms.DockStyle.Bottom; - this.splitter.Location = new System.Drawing.Point(0, 190); + this.splitter.Location = new System.Drawing.Point(0, 155); this.splitter.MinExtra = 16; + this.splitter.MinSize = 50; this.splitter.Name = "splitter"; - this.splitter.Size = new System.Drawing.Size(334, 3); + this.splitter.Size = new System.Drawing.Size(364, 3); this.splitter.TabIndex = 3; this.splitter.TabStop = false; // // tsMenu // this.tsMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.btnAutoZoom, + this.btnZoom, this.btnAntiAlias, this.toolStripSeparator1, this.btnOpen, @@ -110,19 +123,21 @@ private void InitializeComponent() this.btnCompound, this.btnPrev, this.btnNext, + this.toolStripSeparator4, + this.btnAbout, this.btnConfiguration}); this.tsMenu.Location = new System.Drawing.Point(0, 0); this.tsMenu.Name = "tsMenu"; - this.tsMenu.Size = new System.Drawing.Size(334, 25); + this.tsMenu.Size = new System.Drawing.Size(364, 25); this.tsMenu.TabIndex = 2; // - // btnAutoZoom + // btnZoom // - this.btnAutoZoom.CheckOnClick = true; - this.btnAutoZoom.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; - this.btnAutoZoom.ImageTransparentColor = System.Drawing.Color.Magenta; - this.btnAutoZoom.Name = "btnAutoZoom"; - this.btnAutoZoom.Size = new System.Drawing.Size(23, 22); + this.btnZoom.CheckOnClick = true; + this.btnZoom.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; + this.btnZoom.ImageTransparentColor = System.Drawing.Color.Magenta; + this.btnZoom.Name = "btnZoom"; + this.btnZoom.Size = new System.Drawing.Size(32, 22); // // btnAntiAlias // @@ -323,6 +338,7 @@ private void InitializeComponent() this.btnPrev.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.btnPrev.ImageTransparentColor = System.Drawing.Color.Magenta; this.btnPrev.Name = "btnPrev"; + this.btnPrev.RightToLeftAutoMirrorImage = true; this.btnPrev.Size = new System.Drawing.Size(23, 22); // // btnNext @@ -330,38 +346,131 @@ private void InitializeComponent() this.btnNext.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.btnNext.ImageTransparentColor = System.Drawing.Color.Magenta; this.btnNext.Name = "btnNext"; + this.btnNext.RightToLeftAutoMirrorImage = true; this.btnNext.Size = new System.Drawing.Size(23, 22); // + // toolStripSeparator4 + // + this.toolStripSeparator4.Name = "toolStripSeparator4"; + this.toolStripSeparator4.Size = new System.Drawing.Size(6, 25); + // + // btnAbout + // + this.btnAbout.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right; + this.btnAbout.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; + this.btnAbout.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.miWebSite, + this.miGitHub, + this.miMarketplace, + this.miSubmitResources, + this.miSeparatorAbout, + this.miAbout, + this.miEasterEgg}); + this.btnAbout.ImageTransparentColor = System.Drawing.Color.Magenta; + this.btnAbout.Name = "btnAbout"; + this.btnAbout.Size = new System.Drawing.Size(16, 22); + this.btnAbout.Text = "btnAbout"; + // + // miWebSite + // + this.miWebSite.Name = "miWebSite"; + this.miWebSite.Size = new System.Drawing.Size(180, 22); + this.miWebSite.Text = "miWebSite"; + // + // miGitHub + // + this.miGitHub.Name = "miGitHub"; + this.miGitHub.Size = new System.Drawing.Size(180, 22); + this.miGitHub.Text = "miGitHub"; + // + // miMarketplace + // + this.miMarketplace.Name = "miMarketplace"; + this.miMarketplace.Size = new System.Drawing.Size(180, 22); + this.miMarketplace.Text = "miMarketplace"; + // + // miSubmitResources + // + this.miSubmitResources.Name = "miSubmitResources"; + this.miSubmitResources.Size = new System.Drawing.Size(180, 22); + this.miSubmitResources.Text = "miSubmitResources"; + // + // miSeparatorAbout + // + this.miSeparatorAbout.Name = "miSeparatorAbout"; + this.miSeparatorAbout.Size = new System.Drawing.Size(177, 6); + // + // miAbout + // + this.miAbout.Name = "miAbout"; + this.miAbout.ShortcutKeys = System.Windows.Forms.Keys.F1; + this.miAbout.Size = new System.Drawing.Size(180, 22); + this.miAbout.Text = "miAbout"; + // + // miEasterEgg + // + this.miEasterEgg.Name = "miEasterEgg"; + this.miEasterEgg.Size = new System.Drawing.Size(180, 22); + this.miEasterEgg.Visible = false; + // // btnConfiguration // + this.btnConfiguration.AutoChangeDefaultItem = true; this.btnConfiguration.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; + this.btnConfiguration.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.miManageInstallations, + this.miLanguageSettings}); this.btnConfiguration.ImageTransparentColor = System.Drawing.Color.Magenta; this.btnConfiguration.Name = "btnConfiguration"; - this.btnConfiguration.Size = new System.Drawing.Size(23, 22); + this.btnConfiguration.Size = new System.Drawing.Size(16, 22); + // + // miManageInstallations + // + this.miManageInstallations.Name = "miManageInstallations"; + this.miManageInstallations.Size = new System.Drawing.Size(194, 22); + this.miManageInstallations.Text = "miManageInstallations"; + // + // miLanguageSettings + // + this.miLanguageSettings.Name = "miLanguageSettings"; + this.miLanguageSettings.Size = new System.Drawing.Size(194, 22); + this.miLanguageSettings.Text = "miLanguageSettings"; // // txtInfo // + this.txtInfo.BackColor = System.Drawing.SystemColors.Control; this.txtInfo.Dock = System.Windows.Forms.DockStyle.Bottom; - this.txtInfo.Location = new System.Drawing.Point(0, 193); + this.txtInfo.ForeColor = System.Drawing.SystemColors.ControlText; + this.txtInfo.Location = new System.Drawing.Point(0, 158); this.txtInfo.Multiline = true; this.txtInfo.Name = "txtInfo"; this.txtInfo.ReadOnly = true; this.txtInfo.ScrollBars = System.Windows.Forms.ScrollBars.Both; - this.txtInfo.Size = new System.Drawing.Size(334, 123); + this.txtInfo.Size = new System.Drawing.Size(364, 123); this.txtInfo.TabIndex = 0; this.txtInfo.TabStop = false; this.txtInfo.WordWrap = false; // + // okCancelButtons + // + this.okCancelButtons.BackColor = System.Drawing.Color.Transparent; + this.okCancelButtons.Dock = System.Windows.Forms.DockStyle.Bottom; + this.okCancelButtons.Location = new System.Drawing.Point(0, 281); + this.okCancelButtons.Name = "okCancelButtons"; + this.okCancelButtons.Size = new System.Drawing.Size(364, 35); + this.okCancelButtons.TabIndex = 5; + // // ImageVisualizerForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(334, 316); + this.ClientSize = new System.Drawing.Size(364, 316); this.Controls.Add(this.imageViewer); this.Controls.Add(this.lblNotification); this.Controls.Add(this.splitter); this.Controls.Add(this.tsMenu); this.Controls.Add(this.txtInfo); + this.Controls.Add(this.okCancelButtons); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; this.MinimumSize = new System.Drawing.Size(200, 200); this.Name = "ImageVisualizerForm"; @@ -375,7 +484,7 @@ private void InitializeComponent() #endregion private KGySoft.Drawing.ImagingTools.View.Controls.ImageViewer imageViewer; - private System.Windows.Forms.ToolStripButton btnAutoZoom; + private ZoomSplitButton btnZoom; private System.Windows.Forms.ToolStripSeparator toolStripSeparator1; private System.Windows.Forms.ToolStripButton btnSave; private System.Windows.Forms.ToolStripButton btnOpen; @@ -388,18 +497,17 @@ private void InitializeComponent() private System.Windows.Forms.SaveFileDialog dlgSave; private System.Windows.Forms.Splitter splitter; private System.Windows.Forms.Timer timerPlayer; - private KGySoft.Drawing.ImagingTools.View.Controls.ScalingToolStripDropDownButton btnColorSettings; + private ScalingToolStripDropDownButton btnColorSettings; private System.Windows.Forms.ToolStripMenuItem miBackColor; private System.Windows.Forms.ToolStripMenuItem miBackColorDefault; private System.Windows.Forms.ToolStripMenuItem miBackColorWhite; private System.Windows.Forms.ToolStripMenuItem miBackColorBlack; private System.Windows.Forms.ToolStripMenuItem miShowPalette; - protected KGySoft.Drawing.ImagingTools.View.Controls.ScalingToolStrip tsMenu; + protected KGySoft.Drawing.ImagingTools.View.Controls.AdvancedToolStrip tsMenu; protected System.Windows.Forms.TextBox txtInfo; private KGySoft.Drawing.ImagingTools.View.Controls.NotificationLabel lblNotification; - private ToolTip toolTip; private ToolStripSeparator toolStripSeparator2; - protected ToolStripButton btnConfiguration; + protected AdvancedToolStripSplitButton btnConfiguration; private ToolStripButton btnAntiAlias; private ToolStripMenuItem miCountColors; private ScalingToolStripDropDownButton btnEdit; @@ -412,5 +520,17 @@ private void InitializeComponent() private ToolStripMenuItem miBrightness; private ToolStripMenuItem miContrast; private ToolStripMenuItem miGamma; + private ToolStripSeparator toolStripSeparator4; + private ToolStripMenuItem miManageInstallations; + private ToolStripMenuItem miLanguageSettings; + protected UserControls.OkCancelButtons okCancelButtons; + private ToolStripSplitButton btnAbout; + private ToolStripMenuItem miWebSite; + private ToolStripMenuItem miGitHub; + private ToolStripMenuItem miMarketplace; + private ToolStripMenuItem miSubmitResources; + private ToolStripSeparator miSeparatorAbout; + private ToolStripMenuItem miAbout; + private ToolStripMenuItem miEasterEgg; } } \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/ImageVisualizerForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/ImageVisualizerForm.cs index b0a45ec..0b1c876 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/ImageVisualizerForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/ImageVisualizerForm.cs @@ -26,6 +26,15 @@ #endregion +#region Suppressions + +#if NETCOREAPP3_0 +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - DropDownItems items are never null +#pragma warning disable CS8602 // Dereference of a possibly null reference. - DropDownItems items are never null +#endif + +#endregion + namespace KGySoft.Drawing.ImagingTools.View.Forms { internal partial class ImageVisualizerForm : MvvmBaseForm @@ -38,13 +47,15 @@ internal ImageVisualizerForm(ImageVisualizerViewModel viewModel) : base(viewModel) { InitializeComponent(); + AcceptButton = okCancelButtons.OKButton; + CancelButton = okCancelButtons.CancelButton; } #endregion #region Private Constructors - private ImageVisualizerForm() : this(null) + private ImageVisualizerForm() : this(null!) { // this ctor is just for the designer } @@ -81,39 +92,47 @@ private static Image GetCompoundViewIcon(ImageInfoType type) protected override void OnLoad(EventArgs e) { - tsMenu.FixAppearance(); base.OnLoad(e); + tsMenu.FixAppearance(); } protected override void ApplyResources() { - // applying static resources base.ApplyResources(); Icon = Properties.Resources.ImagingTools; - btnAutoZoom.Image = Images.Magnifier; + + btnAntiAlias.Image = Images.SmoothZoom; btnOpen.Image = Images.Open; btnSave.Image = Images.Save; btnClear.Image = Images.Clear; - btnColorSettings.Image = Images.Palette; - btnPrev.Image = Images.Prev; - btnNext.Image = Images.Next; - btnConfiguration.Image = Images.Settings; - btnAntiAlias.Image = Images.SmoothZoom; - btnEdit.Image = Images.Edit; - miShowPalette.Image = Images.Palette; + btnColorSettings.Image = Images.Palette; miBackColorDefault.Image = Images.Check; + miShowPalette.Image = Images.Palette; + + btnEdit.Image = Images.Edit; miRotateLeft.Image = Images.RotateLeft; miRotateRight.Image = Images.RotateRight; miResizeBitmap.Image = Images.Resize; miColorSpace.Image = Images.Quantize; miAdjustColors.Image = Images.Colors; - toolTip.SetToolTip(lblNotification, Res.Get($"{nameof(lblNotification)}.ToolTip")); + btnPrev.Image = Images.Prev; + btnNext.Image = Images.Next; + + miManageInstallations.Image = Images.Settings; + miLanguageSettings.Image = Images.Language; + btnConfiguration.SetDefaultItem(miManageInstallations); + + miEasterEgg.Image = Images.ImagingTools; + btnAbout.Image = miAbout.Image = Icons.SystemInformation.ToScaledBitmap(); + } - // base cannot handle these because components do not have names and dialogs are not even added to components field - dlgOpen.Title = Res.Get($"{nameof(dlgOpen)}.{nameof(dlgOpen.Title)}"); - dlgSave.Title = Res.Get($"{nameof(dlgSave)}.{nameof(dlgSave.Title)}"); + protected override void ApplyStringResources() + { + base.ApplyStringResources(); + dlgOpen.Title = Res.TitleOpenFileDialog; + dlgSave.Title = Res.TitleSaveFileDialog; } protected override void ApplyViewModel() @@ -138,20 +157,31 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData) case Keys.Control | Keys.Delete: btnClear.PerformClick(); return true; - case Keys.Alt | Keys.Z: - btnAutoZoom.PerformClick(); + case Keys.Alt | Keys.S: + btnAntiAlias.PerformClick(); return true; case Keys.Shift | Keys.Right: - btnNext.PerformClick(); + (RightToLeft == RightToLeft.Yes ? btnPrev : btnNext).PerformClick(); return true; case Keys.Shift | Keys.Left: - btnPrev.PerformClick(); + (RightToLeft == RightToLeft.Yes ? btnNext : btnPrev).PerformClick(); + return true; + case Keys.Escape when ViewModel.ReadOnly: // if not ReadOnly, use the Cancel button if available + DialogResult = DialogResult.Cancel; return true; + default: return base.ProcessCmdKey(ref msg, keyData); } } + protected override void OnFormClosing(FormClosingEventArgs e) + { + if (DialogResult == DialogResult.Cancel) + ViewModel.SetModified(false); + base.OnFormClosing(e); + } + protected override void Dispose(bool disposing) { if (disposing) @@ -178,6 +208,10 @@ private void InitViewModelDependencies() private void InitPropertyBindings() { + // not as binding because will not change and we don't need the buttons for main form + if (ViewModel.ReadOnly) + okCancelButtons.Visible = false; + // VM.Notification -> lblNotification.Text CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Notification), nameof(Label.Text), lblNotification); @@ -190,14 +224,14 @@ private void InitPropertyBindings() // VM.InfoText -> txtInfo.Text CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.InfoText), nameof(TextBox.Text), txtInfo); - // VM.AutoZoom -> btnAutoZoom.Checked, imageViewer.AutoZoom - CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.AutoZoom), nameof(btnAutoZoom.Checked), btnAutoZoom); - CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.AutoZoom), nameof(imageViewer.AutoZoom), imageViewer); + // imageViewer.AutoZoom <-> VM.AutoZoom -> btnZoom.Checked + CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(ViewModel.AutoZoom), imageViewer, nameof(imageViewer.AutoZoom)); + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.AutoZoom), nameof(btnZoom.Checked), btnZoom); // VM.Zoom <-> imageViewer.Zoom CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(ViewModel.Zoom), imageViewer, nameof(imageViewer.Zoom)); - // VM.SmoothZooming -> btnAntiAlias.Checked, imageViewer.SmoothZooming + // VM.SmoothZooming -> btnAntiAlias.Checked CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.SmoothZooming), nameof(btnAntiAlias.Checked), btnAntiAlias); CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.SmoothZooming), nameof(imageViewer.SmoothZooming), imageViewer); @@ -221,13 +255,23 @@ private void InitPropertyBindings() CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.SaveFileFilter), nameof(dlgSave.Filter), dlgSave); CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.SaveFileFilterIndex), nameof(dlgSave.FilterIndex), dlgSave); CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.SaveFileDefaultExtension), nameof(dlgSave.DefaultExt), dlgSave); + + // VM.IsModified -> OKButton.Enabled + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.IsModified), nameof(okCancelButtons.OKButton.Enabled), okCancelButtons.OKButton); } + private void InitCommandBindings() { // View CommandBindings.Add(ViewModel.SetAutoZoomCommand, ViewModel.SetAutoZoomCommandState) - .WithParameter(() => btnAutoZoom.Checked) - .AddSource(btnAutoZoom, nameof(btnAutoZoom.CheckedChanged)); + .WithParameter(() => btnZoom.Checked) + .AddSource(btnZoom, nameof(btnZoom.CheckedChanged)); + CommandBindings.Add(imageViewer.IncreaseZoom) + .AddSource(btnZoom.IncreaseZoomMenuItem, nameof(btnZoom.IncreaseZoomMenuItem.Click)); + CommandBindings.Add(imageViewer.DecreaseZoom) + .AddSource(btnZoom.DecreaseZoomMenuItem, nameof(btnZoom.DecreaseZoomMenuItem.Click)); + CommandBindings.Add(imageViewer.ResetZoom) + .AddSource(btnZoom.ResetZoomMenuItem, nameof(btnZoom.ResetZoomMenuItem.Click)); CommandBindings.Add(ViewModel.SetSmoothZoomingCommand, ViewModel.SetSmoothZoomingCommandState) .WithParameter(() => btnAntiAlias.Checked) .AddSource(btnAntiAlias, nameof(btnAntiAlias.CheckedChanged)); @@ -266,39 +310,61 @@ private void InitCommandBindings() CommandBindings.Add(ViewModel.AdjustGammaCommand, ViewModel.EditBitmapCommandState) .AddSource(miGamma, nameof(miGamma.Click)); - // Compound controls + // Compound images CommandBindings.Add(ViewModel.SetCompoundViewCommand, ViewModel.SetCompoundViewCommandState) .WithParameter(() => btnCompound.Checked) .AddSource(btnCompound, nameof(btnCompound.CheckedChanged)); - CommandBindings.Add(ViewModel.AdvanceAnimationCommand, ViewModel.AdvanceAnimationCommandState) - .AddSource(timerPlayer, nameof(timerPlayer.Tick)); CommandBindings.Add(ViewModel.PrevImageCommand, ViewModel.PrevImageCommandState) .AddSource(btnPrev, nameof(btnPrev.Click)); CommandBindings.Add(ViewModel.NextImageCommand, ViewModel.NextImageCommandState) .AddSource(btnNext, nameof(btnNext.Click)); - CommandBindings.Add(ViewModel.ManageInstallationsCommand) - .AddSource(btnConfiguration, nameof(btnConfiguration.Click)); + CommandBindings.Add(ViewModel.AdvanceAnimationCommand, ViewModel.AdvanceAnimationCommandState) + .AddSource(timerPlayer, nameof(timerPlayer.Tick)); CommandBindings.Add(ViewModel.ViewImagePreviewSizeChangedCommand) .AddSource(imageViewer, nameof(imageViewer.SizeChanged)) .AddSource(imageViewer, nameof(imageViewer.ZoomChanged)); + // Configuration + CommandBindings.Add(ViewModel.ManageInstallationsCommand) + .AddSource(miManageInstallations, nameof(miManageInstallations.Click)); + CommandBindings.Add(ViewModel.SetLanguageCommand) + .AddSource(miLanguageSettings, nameof(miLanguageSettings.Click)); + + // About + CommandBindings.Add(ViewModel.ShowAboutCommand) + .AddSource(btnAbout, nameof(btnAbout.ButtonClick)); + CommandBindings.Add(ViewModel.ShowAboutCommand) + .AddSource(miAbout, nameof(miAbout.Click)); + CommandBindings.Add(ViewModel.VisitWebSiteCommand) + .AddSource(miWebSite, nameof(miWebSite.Click)); + CommandBindings.Add(ViewModel.VisitGitHubCommand) + .AddSource(miGitHub, nameof(miGitHub.Click)); + CommandBindings.Add(ViewModel.VisitMarketplaceCommand) + .AddSource(miMarketplace, nameof(miMarketplace.Click)); + CommandBindings.Add(ViewModel.SubmitResourcesCommand) + .AddSource(miSubmitResources, nameof(miSubmitResources.Click)); + CommandBindings.Add(ViewModel.ShowEasterEggCommand) + .AddSource(miEasterEgg, nameof(miEasterEgg.Click)); + // View commands CommandBindings.Add(OnResizeCommand) .AddSource(this, nameof(Resize)); CommandBindings.Add(OnPreviewImageResizedCommand) .AddSource(imageViewer, nameof(imageViewer.SizeChanged)); + CommandBindings.Add(() => miEasterEgg.Visible |= ModifierKeys == (Keys.Shift | Keys.Control)) + .AddSource(miAbout, nameof(miAbout.MouseDown)); } private Rectangle GetScreenRectangle() => Screen.FromHandle(Handle).WorkingArea; - private string SelectFileToOpen() + private string? SelectFileToOpen() { if (dlgOpen.ShowDialog(this) != DialogResult.OK) return null; return dlgOpen.FileName; } - private string SelectFileToSave() + private string? SelectFileToSave() { if (dlgSave.ShowDialog(this) != DialogResult.OK) return null; @@ -311,7 +377,9 @@ private void AdjustSize() int minHeight = new Size(16, 16).Scale(this.GetScale()).Height + SystemInformation.HorizontalScrollBarHeight; if (imageViewer.Height >= minHeight) return; - txtInfo.Height = ClientSize.Height - tsMenu.Height - splitter.Height - minHeight; + int buttonsHeight = okCancelButtons.Visible ? okCancelButtons.Height : 0; + int notificationHeight = lblNotification.Visible ? lblNotification.Height : 0; + txtInfo.Height = ClientSize.Height - Padding.Vertical - tsMenu.Height - splitter.Height - buttonsHeight - notificationHeight - minHeight; PerformLayout(); } diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/LanguageSettingsForm.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/LanguageSettingsForm.Designer.cs new file mode 100644 index 0000000..417ec38 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Forms/LanguageSettingsForm.Designer.cs @@ -0,0 +1,196 @@ + +namespace KGySoft.Drawing.ImagingTools.View.Forms +{ + partial class LanguageSettingsForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.okCancelApplyButtons = new KGySoft.Drawing.ImagingTools.View.UserControls.OkCancelButtons(); + this.gbAllowResxResources = new KGySoft.Drawing.ImagingTools.View.Controls.CheckGroupBox(); + this.gbDisplayLanguage = new System.Windows.Forms.GroupBox(); + this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.btnDownloadResources = new System.Windows.Forms.Button(); + this.btnEditResources = new System.Windows.Forms.Button(); + this.cmbLanguages = new System.Windows.Forms.ComboBox(); + this.chbExistingResourcesOnly = new System.Windows.Forms.CheckBox(); + this.chbUseOSLanguage = new System.Windows.Forms.CheckBox(); + this.toolTip = new System.Windows.Forms.ToolTip(this.components); + this.gbAllowResxResources.SuspendLayout(); + this.gbDisplayLanguage.SuspendLayout(); + this.tableLayoutPanel1.SuspendLayout(); + this.SuspendLayout(); + // + // okCancelApplyButtons + // + this.okCancelApplyButtons.ApplyButtonVisible = true; + this.okCancelApplyButtons.Dock = System.Windows.Forms.DockStyle.Bottom; + this.okCancelApplyButtons.Location = new System.Drawing.Point(3, 137); + this.okCancelApplyButtons.Name = "okCancelApplyButtons"; + this.okCancelApplyButtons.Size = new System.Drawing.Size(328, 35); + this.okCancelApplyButtons.TabIndex = 1; + // + // gbAllowResxResources + // + this.gbAllowResxResources.Controls.Add(this.gbDisplayLanguage); + this.gbAllowResxResources.Controls.Add(this.chbExistingResourcesOnly); + this.gbAllowResxResources.Controls.Add(this.chbUseOSLanguage); + this.gbAllowResxResources.Dock = System.Windows.Forms.DockStyle.Fill; + this.gbAllowResxResources.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.gbAllowResxResources.Location = new System.Drawing.Point(3, 3); + this.gbAllowResxResources.Name = "gbAllowResxResources"; + this.gbAllowResxResources.Padding = new System.Windows.Forms.Padding(5); + this.gbAllowResxResources.Size = new System.Drawing.Size(328, 134); + this.gbAllowResxResources.TabIndex = 0; + this.gbAllowResxResources.TabStop = false; + this.gbAllowResxResources.Text = "gbAllowResxResources"; + // + // gbDisplayLanguage + // + this.gbDisplayLanguage.Controls.Add(this.tableLayoutPanel1); + this.gbDisplayLanguage.Dock = System.Windows.Forms.DockStyle.Fill; + this.gbDisplayLanguage.Location = new System.Drawing.Point(5, 54); + this.gbDisplayLanguage.Name = "gbDisplayLanguage"; + this.gbDisplayLanguage.Size = new System.Drawing.Size(318, 75); + this.gbDisplayLanguage.TabIndex = 2; + this.gbDisplayLanguage.TabStop = false; + this.gbDisplayLanguage.Text = "gbDisplayLanguage"; + // + // tableLayoutPanel1 + // + this.tableLayoutPanel1.ColumnCount = 2; + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.Controls.Add(this.btnDownloadResources, 0, 1); + this.tableLayoutPanel1.Controls.Add(this.btnEditResources, 1, 0); + this.tableLayoutPanel1.Controls.Add(this.cmbLanguages, 0, 0); + this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayoutPanel1.Location = new System.Drawing.Point(3, 16); + this.tableLayoutPanel1.Name = "tableLayoutPanel1"; + this.tableLayoutPanel1.RowCount = 2; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); + this.tableLayoutPanel1.Size = new System.Drawing.Size(312, 56); + this.tableLayoutPanel1.TabIndex = 0; + // + // btnDownloadResources + // + this.btnDownloadResources.AutoSize = true; + this.tableLayoutPanel1.SetColumnSpan(this.btnDownloadResources, 2); + this.btnDownloadResources.Dock = System.Windows.Forms.DockStyle.Top; + this.btnDownloadResources.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.btnDownloadResources.Location = new System.Drawing.Point(2, 30); + this.btnDownloadResources.Margin = new System.Windows.Forms.Padding(2, 2, 3, 3); + this.btnDownloadResources.Name = "btnDownloadResources"; + this.btnDownloadResources.Size = new System.Drawing.Size(307, 23); + this.btnDownloadResources.TabIndex = 2; + this.btnDownloadResources.Text = "btnDownloadResources"; + this.btnDownloadResources.UseVisualStyleBackColor = true; + // + // btnEditResources + // + this.btnEditResources.AutoSize = true; + this.btnEditResources.Dock = System.Windows.Forms.DockStyle.Top; + this.btnEditResources.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.btnEditResources.Location = new System.Drawing.Point(204, 2); + this.btnEditResources.Margin = new System.Windows.Forms.Padding(3, 2, 3, 3); + this.btnEditResources.Name = "btnEditResources"; + this.btnEditResources.Size = new System.Drawing.Size(105, 23); + this.btnEditResources.TabIndex = 1; + this.btnEditResources.Text = "btnEditResources"; + this.btnEditResources.UseVisualStyleBackColor = true; + // + // cmbLanguages + // + this.cmbLanguages.Dock = System.Windows.Forms.DockStyle.Top; + this.cmbLanguages.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbLanguages.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.cmbLanguages.FormattingEnabled = true; + this.cmbLanguages.Location = new System.Drawing.Point(3, 3); + this.cmbLanguages.Name = "cmbLanguages"; + this.cmbLanguages.Size = new System.Drawing.Size(195, 21); + this.cmbLanguages.TabIndex = 0; + // + // chbExistingResourcesOnly + // + this.chbExistingResourcesOnly.AutoSize = true; + this.chbExistingResourcesOnly.Dock = System.Windows.Forms.DockStyle.Top; + this.chbExistingResourcesOnly.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.chbExistingResourcesOnly.Location = new System.Drawing.Point(5, 36); + this.chbExistingResourcesOnly.Name = "chbExistingResourcesOnly"; + this.chbExistingResourcesOnly.Size = new System.Drawing.Size(318, 18); + this.chbExistingResourcesOnly.TabIndex = 1; + this.chbExistingResourcesOnly.Text = "chbExistingResourcesOnly"; + this.chbExistingResourcesOnly.UseVisualStyleBackColor = true; + // + // chbUseOSLanguage + // + this.chbUseOSLanguage.AutoSize = true; + this.chbUseOSLanguage.Dock = System.Windows.Forms.DockStyle.Top; + this.chbUseOSLanguage.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.chbUseOSLanguage.Location = new System.Drawing.Point(5, 18); + this.chbUseOSLanguage.Name = "chbUseOSLanguage"; + this.chbUseOSLanguage.Size = new System.Drawing.Size(318, 18); + this.chbUseOSLanguage.TabIndex = 0; + this.chbUseOSLanguage.Text = "chbUseOSLanguage"; + this.chbUseOSLanguage.UseVisualStyleBackColor = true; + // + // LanguageSettingsForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(334, 175); + this.Controls.Add(this.gbAllowResxResources); + this.Controls.Add(this.okCancelApplyButtons); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "LanguageSettingsForm"; + this.Padding = new System.Windows.Forms.Padding(3); + this.Text = "LanguageSettingsForm"; + this.gbAllowResxResources.ResumeLayout(false); + this.gbAllowResxResources.PerformLayout(); + this.gbDisplayLanguage.ResumeLayout(false); + this.tableLayoutPanel1.ResumeLayout(false); + this.tableLayoutPanel1.PerformLayout(); + this.ResumeLayout(false); + } + + #endregion + + private UserControls.OkCancelButtons okCancelApplyButtons; + private KGySoft.Drawing.ImagingTools.View.Controls.CheckGroupBox gbAllowResxResources; + private System.Windows.Forms.CheckBox chbExistingResourcesOnly; + private System.Windows.Forms.ToolTip toolTip; + private System.Windows.Forms.GroupBox gbDisplayLanguage; + private System.Windows.Forms.CheckBox chbUseOSLanguage; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.Button btnEditResources; + private System.Windows.Forms.ComboBox cmbLanguages; + private System.Windows.Forms.Button btnDownloadResources; + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/LanguageSettingsForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/LanguageSettingsForm.cs new file mode 100644 index 0000000..f69cf51 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Forms/LanguageSettingsForm.cs @@ -0,0 +1,152 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: LanguageSettingsForm.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Drawing; +using System.Globalization; +using System.Windows.Forms; + +using KGySoft.ComponentModel; +using KGySoft.Drawing.ImagingTools.ViewModel; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View.Forms +{ + internal partial class LanguageSettingsForm : MvvmBaseForm + { + #region Constructors + + #region Internal Constructors + + internal LanguageSettingsForm(LanguageSettingsViewModel viewModel) : base(viewModel) + { + InitializeComponent(); + btnEditResources.Height = cmbLanguages.Height + 2; // helps aligning better for higher DPIs + AcceptButton = okCancelApplyButtons.OKButton; + CancelButton = okCancelApplyButtons.CancelButton; + } + + #endregion + + #region Private Constructors + + private LanguageSettingsForm() : this(null!) + { + // this ctor is just for the designer + } + + #endregion + + #endregion + + #region Methods + + #region Static Methods + + private static void OnFormatCultureCommand(ICommandSource source) + { + var culture = (CultureInfo)source.EventArgs.ListItem; + source.EventArgs.Value = $"{culture.EnglishName} ({culture.NativeName})"; + } + + #endregion + + #region Instance Methods + + #region Protected Methods + + protected override void OnLoad(EventArgs e) + { + // Fixing high DPI appearance on Mono + PointF scale; + if (OSUtils.IsMono && (scale = this.GetScale()) != new PointF(1f, 1f)) + { + btnEditResources.Size = new Size(105, 23).Scale(scale); + btnDownloadResources.Height = (int)(23 * scale.Y); + } + + base.OnLoad(e); + } + + protected override void ApplyResources() + { + base.ApplyResources(); + Icon = Properties.Resources.Language; + } + + protected override void ApplyViewModel() + { + InitCommandBindings(); + InitPropertyBindings(); + base.ApplyViewModel(); + } + + #endregion + + #region Private Methods + + private void InitPropertyBindings() + { + // VM.AllowResXResources <-> gbAllowResxResources.Checked + CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(ViewModel.AllowResXResources), gbAllowResxResources, nameof(gbAllowResxResources.Checked)); + + // VM.UseOSLanguage <-> chbUseOSLanguage.Checked + CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(ViewModel.UseOSLanguage), chbUseOSLanguage, nameof(chbUseOSLanguage.Checked)); + + // VM.ExistingLanguagesOnly <-> chbExistingResourcesOnly.Checked + CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(ViewModel.ExistingLanguagesOnly), chbExistingResourcesOnly, nameof(chbExistingResourcesOnly.Checked)); + + // VM.UseOSLanguage -> !cmbLanguages.Enabled + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.UseOSLanguage), nameof(cmbLanguages.Enabled), b => !((bool)b!), cmbLanguages); + + // VM.Languages -> cmbLanguages.DataSource + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Languages), nameof(cmbLanguages.DataSource), cmbLanguages); + + // VM.CurrentLanguage -> cmbLanguages.SelectedItem (cannot use two-way for SelectedItem because there is no SelectedItemChanged event) + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.CurrentLanguage), nameof(cmbLanguages.SelectedItem), cmbLanguages); + + // cmbLanguages.SelectedValue -> VM.CurrentLanguage (cannot use two-way for SelectedValue because ValueMember is not set) + CommandBindings.AddPropertyBinding(cmbLanguages, nameof(cmbLanguages.SelectedValue), nameof(ViewModel.CurrentLanguage), ViewModel); + } + + private void InitCommandBindings() + { + CommandBindings.Add(OnFormatCultureCommand) + .AddSource(cmbLanguages, nameof(cmbLanguages.Format)); + + CommandBindings.Add(ViewModel.SaveConfigCommand) + .AddSource(okCancelApplyButtons.OKButton, nameof(okCancelApplyButtons.OKButton.Click)); + + CommandBindings.Add(ViewModel.ApplyCommand, ViewModel.ApplyCommandState) + .AddSource(okCancelApplyButtons.ApplyButton, nameof(okCancelApplyButtons.ApplyButton.Click)); + + CommandBindings.Add(ViewModel.EditResourcesCommand, ViewModel.EditResourcesCommandState) + .AddSource(btnEditResources, nameof(btnEditResources.Click)); + + CommandBindings.Add(ViewModel.DownloadResourcesCommand) + .AddSource(btnDownloadResources, nameof(btnDownloadResources.Click)); + } + + #endregion + + #endregion + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/ManageInstallationsForm.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/ManageInstallationsForm.Designer.cs index 4614acf..8cd9513 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/ManageInstallationsForm.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/ManageInstallationsForm.Designer.cs @@ -18,96 +18,58 @@ partial class ManageInstallationsForm private void InitializeComponent() { this.gbInstallation = new System.Windows.Forms.GroupBox(); - this.tblButtons = new System.Windows.Forms.TableLayoutPanel(); - this.btnRemove = new System.Windows.Forms.Button(); - this.btnInstall = new System.Windows.Forms.Button(); - this.pnlStatus = new System.Windows.Forms.Panel(); + this.pnlStatus = new KGySoft.Drawing.ImagingTools.View.Controls.AutoMirrorPanel(); this.lblStatusText = new System.Windows.Forms.Label(); this.lblStatus = new System.Windows.Forms.Label(); + this.pnlButtons = new System.Windows.Forms.FlowLayoutPanel(); + this.btnRemove = new System.Windows.Forms.Button(); + this.btnInstall = new System.Windows.Forms.Button(); this.tbPath = new System.Windows.Forms.TextBox(); this.lblPath = new System.Windows.Forms.Label(); this.gbVisualStudioVersions = new System.Windows.Forms.GroupBox(); - this.cbInstallations = new System.Windows.Forms.ComboBox(); + this.cmbInstallations = new System.Windows.Forms.ComboBox(); this.gbAvailableVersion = new System.Windows.Forms.GroupBox(); this.lblAvailableVersion = new System.Windows.Forms.Label(); this.gbInstallation.SuspendLayout(); - this.tblButtons.SuspendLayout(); this.pnlStatus.SuspendLayout(); + this.pnlButtons.SuspendLayout(); this.gbVisualStudioVersions.SuspendLayout(); this.gbAvailableVersion.SuspendLayout(); this.SuspendLayout(); // // gbInstallation // - this.gbInstallation.Controls.Add(this.tblButtons); this.gbInstallation.Controls.Add(this.pnlStatus); + this.gbInstallation.Controls.Add(this.pnlButtons); this.gbInstallation.Controls.Add(this.tbPath); this.gbInstallation.Controls.Add(this.lblPath); this.gbInstallation.Dock = System.Windows.Forms.DockStyle.Fill; this.gbInstallation.Location = new System.Drawing.Point(3, 89); this.gbInstallation.Name = "gbInstallation"; - this.gbInstallation.Size = new System.Drawing.Size(378, 105); + this.gbInstallation.Size = new System.Drawing.Size(378, 119); this.gbInstallation.TabIndex = 2; this.gbInstallation.TabStop = false; this.gbInstallation.Text = "gbInstallation"; // - // tblButtons - // - this.tblButtons.ColumnCount = 2; - this.tblButtons.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.tblButtons.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.tblButtons.Controls.Add(this.btnRemove, 1, 0); - this.tblButtons.Controls.Add(this.btnInstall, 0, 0); - this.tblButtons.Dock = System.Windows.Forms.DockStyle.Top; - this.tblButtons.Location = new System.Drawing.Point(3, 66); - this.tblButtons.Name = "tblButtons"; - this.tblButtons.RowCount = 1; - this.tblButtons.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.tblButtons.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 37F)); - this.tblButtons.Size = new System.Drawing.Size(372, 37); - this.tblButtons.TabIndex = 3; - // - // btnRemove - // - this.btnRemove.Anchor = System.Windows.Forms.AnchorStyles.None; - this.btnRemove.FlatStyle = System.Windows.Forms.FlatStyle.System; - this.btnRemove.Location = new System.Drawing.Point(241, 7); - this.btnRemove.Name = "btnRemove"; - this.btnRemove.Size = new System.Drawing.Size(75, 23); - this.btnRemove.TabIndex = 1; - this.btnRemove.Text = "btnRemove"; - this.btnRemove.UseVisualStyleBackColor = true; - // - // btnInstall - // - this.btnInstall.Anchor = System.Windows.Forms.AnchorStyles.None; - this.btnInstall.FlatStyle = System.Windows.Forms.FlatStyle.System; - this.btnInstall.Location = new System.Drawing.Point(55, 7); - this.btnInstall.Name = "btnInstall"; - this.btnInstall.Size = new System.Drawing.Size(75, 23); - this.btnInstall.TabIndex = 0; - this.btnInstall.Text = "btnInstall"; - this.btnInstall.UseVisualStyleBackColor = true; - // // pnlStatus // this.pnlStatus.Controls.Add(this.lblStatusText); this.pnlStatus.Controls.Add(this.lblStatus); - this.pnlStatus.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlStatus.Dock = System.Windows.Forms.DockStyle.Fill; this.pnlStatus.Location = new System.Drawing.Point(3, 49); this.pnlStatus.Name = "pnlStatus"; - this.pnlStatus.Size = new System.Drawing.Size(372, 17); + this.pnlStatus.Padding = new System.Windows.Forms.Padding(0, 3, 0, 0); + this.pnlStatus.Size = new System.Drawing.Size(372, 32); this.pnlStatus.TabIndex = 2; // // lblStatusText // - this.lblStatusText.AutoSize = true; - this.lblStatusText.Dock = System.Windows.Forms.DockStyle.Left; + this.lblStatusText.Dock = System.Windows.Forms.DockStyle.Fill; this.lblStatusText.FlatStyle = System.Windows.Forms.FlatStyle.System; this.lblStatusText.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(238))); - this.lblStatusText.Location = new System.Drawing.Point(47, 0); + this.lblStatusText.Location = new System.Drawing.Point(47, 3); this.lblStatusText.Name = "lblStatusText"; - this.lblStatusText.Size = new System.Drawing.Size(81, 13); + this.lblStatusText.Size = new System.Drawing.Size(325, 29); this.lblStatusText.TabIndex = 1; this.lblStatusText.Text = "lblStatusText"; // @@ -116,12 +78,48 @@ private void InitializeComponent() this.lblStatus.AutoSize = true; this.lblStatus.Dock = System.Windows.Forms.DockStyle.Left; this.lblStatus.FlatStyle = System.Windows.Forms.FlatStyle.System; - this.lblStatus.Location = new System.Drawing.Point(0, 0); + this.lblStatus.Location = new System.Drawing.Point(0, 3); this.lblStatus.Name = "lblStatus"; this.lblStatus.Size = new System.Drawing.Size(47, 13); this.lblStatus.TabIndex = 0; this.lblStatus.Text = "lblStatus"; // + // pnlButtons + // + this.pnlButtons.Controls.Add(this.btnRemove); + this.pnlButtons.Controls.Add(this.btnInstall); + this.pnlButtons.Dock = System.Windows.Forms.DockStyle.Bottom; + this.pnlButtons.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; + this.pnlButtons.Location = new System.Drawing.Point(3, 81); + this.pnlButtons.Name = "pnlButtons"; + this.pnlButtons.Padding = new System.Windows.Forms.Padding(3); + this.pnlButtons.Size = new System.Drawing.Size(372, 35); + this.pnlButtons.TabIndex = 3; + // + // btnRemove + // + this.btnRemove.Anchor = System.Windows.Forms.AnchorStyles.None; + this.btnRemove.AutoSize = true; + this.btnRemove.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.btnRemove.Location = new System.Drawing.Point(287, 6); + this.btnRemove.Name = "btnRemove"; + this.btnRemove.Size = new System.Drawing.Size(76, 23); + this.btnRemove.TabIndex = 1; + this.btnRemove.Text = "btnRemove"; + this.btnRemove.UseVisualStyleBackColor = true; + // + // btnInstall + // + this.btnInstall.Anchor = System.Windows.Forms.AnchorStyles.None; + this.btnInstall.AutoSize = true; + this.btnInstall.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.btnInstall.Location = new System.Drawing.Point(206, 6); + this.btnInstall.Name = "btnInstall"; + this.btnInstall.Size = new System.Drawing.Size(75, 23); + this.btnInstall.TabIndex = 0; + this.btnInstall.Text = "btnInstall"; + this.btnInstall.UseVisualStyleBackColor = true; + // // tbPath // this.tbPath.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend; @@ -135,18 +133,17 @@ private void InitializeComponent() // // lblPath // - this.lblPath.AutoSize = true; this.lblPath.Dock = System.Windows.Forms.DockStyle.Top; this.lblPath.FlatStyle = System.Windows.Forms.FlatStyle.System; this.lblPath.Location = new System.Drawing.Point(3, 16); this.lblPath.Name = "lblPath"; - this.lblPath.Size = new System.Drawing.Size(39, 13); + this.lblPath.Size = new System.Drawing.Size(372, 13); this.lblPath.TabIndex = 0; this.lblPath.Text = "lblPath"; // // gbVisualStudioVersions // - this.gbVisualStudioVersions.Controls.Add(this.cbInstallations); + this.gbVisualStudioVersions.Controls.Add(this.cmbInstallations); this.gbVisualStudioVersions.Dock = System.Windows.Forms.DockStyle.Top; this.gbVisualStudioVersions.Location = new System.Drawing.Point(3, 43); this.gbVisualStudioVersions.Name = "gbVisualStudioVersions"; @@ -155,16 +152,16 @@ private void InitializeComponent() this.gbVisualStudioVersions.TabStop = false; this.gbVisualStudioVersions.Text = "gbVisualStudioVersions"; // - // cbInstallations + // cmbInstallations // - this.cbInstallations.Dock = System.Windows.Forms.DockStyle.Top; - this.cbInstallations.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; - this.cbInstallations.FlatStyle = System.Windows.Forms.FlatStyle.System; - this.cbInstallations.FormattingEnabled = true; - this.cbInstallations.Location = new System.Drawing.Point(3, 16); - this.cbInstallations.Name = "cbInstallations"; - this.cbInstallations.Size = new System.Drawing.Size(372, 21); - this.cbInstallations.TabIndex = 0; + this.cmbInstallations.Dock = System.Windows.Forms.DockStyle.Top; + this.cmbInstallations.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbInstallations.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.cmbInstallations.FormattingEnabled = true; + this.cmbInstallations.Location = new System.Drawing.Point(3, 16); + this.cmbInstallations.Name = "cmbInstallations"; + this.cmbInstallations.Size = new System.Drawing.Size(372, 21); + this.cmbInstallations.TabIndex = 0; // // gbAvailableVersion // @@ -179,12 +176,11 @@ private void InitializeComponent() // // lblAvailableVersion // - this.lblAvailableVersion.AutoSize = true; - this.lblAvailableVersion.Dock = System.Windows.Forms.DockStyle.Left; + this.lblAvailableVersion.Dock = System.Windows.Forms.DockStyle.Fill; this.lblAvailableVersion.FlatStyle = System.Windows.Forms.FlatStyle.System; this.lblAvailableVersion.Location = new System.Drawing.Point(3, 16); this.lblAvailableVersion.Name = "lblAvailableVersion"; - this.lblAvailableVersion.Size = new System.Drawing.Size(95, 13); + this.lblAvailableVersion.Size = new System.Drawing.Size(372, 21); this.lblAvailableVersion.TabIndex = 0; this.lblAvailableVersion.Text = "lblAvailableVersion"; // @@ -192,7 +188,7 @@ private void InitializeComponent() // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(384, 197); + this.ClientSize = new System.Drawing.Size(384, 211); this.Controls.Add(this.gbInstallation); this.Controls.Add(this.gbVisualStudioVersions); this.Controls.Add(this.gbAvailableVersion); @@ -204,28 +200,28 @@ private void InitializeComponent() this.Text = "ManageInstallationsForm"; this.gbInstallation.ResumeLayout(false); this.gbInstallation.PerformLayout(); - this.tblButtons.ResumeLayout(false); this.pnlStatus.ResumeLayout(false); this.pnlStatus.PerformLayout(); + this.pnlButtons.ResumeLayout(false); + this.pnlButtons.PerformLayout(); this.gbVisualStudioVersions.ResumeLayout(false); this.gbAvailableVersion.ResumeLayout(false); - this.gbAvailableVersion.PerformLayout(); this.ResumeLayout(false); } #endregion private System.Windows.Forms.GroupBox gbInstallation; - private System.Windows.Forms.TableLayoutPanel tblButtons; + private System.Windows.Forms.FlowLayoutPanel pnlButtons; private System.Windows.Forms.Button btnRemove; private System.Windows.Forms.Button btnInstall; - private System.Windows.Forms.Panel pnlStatus; + private Controls.AutoMirrorPanel pnlStatus; private System.Windows.Forms.Label lblStatusText; private System.Windows.Forms.Label lblStatus; private System.Windows.Forms.TextBox tbPath; private System.Windows.Forms.Label lblPath; private GroupBox gbVisualStudioVersions; - private ComboBox cbInstallations; + private ComboBox cmbInstallations; private GroupBox gbAvailableVersion; private Label lblAvailableVersion; } diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/ManageInstallationsForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/ManageInstallationsForm.cs index 75987ac..4e3acc2 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/ManageInstallationsForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/ManageInstallationsForm.cs @@ -16,8 +16,9 @@ #region Usings +using System; using System.Collections.Generic; -using System.Windows.Forms; +using System.Drawing; using KGySoft.Drawing.ImagingTools.ViewModel; @@ -35,15 +36,15 @@ internal ManageInstallationsForm(ManageInstallationsViewModel viewModel) : base(viewModel) { InitializeComponent(); - cbInstallations.ValueMember = nameof(KeyValuePair.Key); - cbInstallations.DisplayMember = nameof(KeyValuePair.Value); + cmbInstallations.ValueMember = nameof(KeyValuePair.Key); + cmbInstallations.DisplayMember = nameof(KeyValuePair.Value); } #endregion #region Private Constructors - private ManageInstallationsForm() : this(null) + private ManageInstallationsForm() : this(null!) { // this ctor is just for the designer } @@ -56,6 +57,21 @@ private ManageInstallationsForm() : this(null) #region Protected Methods + protected override void OnLoad(EventArgs e) + { + // Fixing high DPI appearance on Mono + PointF scale; + if (OSUtils.IsMono && (scale = this.GetScale()) != new PointF(1f, 1f)) + { + pnlButtons.Height = (int)(35 * scale.Y); + var referenceButtonSize = new Size(75, 23); + btnInstall.Size = referenceButtonSize.Scale(scale); + btnRemove.Size = referenceButtonSize.Scale(scale); + } + + base.OnLoad(e); + } + protected override void ApplyResources() { base.ApplyResources(); @@ -88,11 +104,11 @@ private void InitViewModelDependencies() private void InitPropertyBindings() { - // will not change so not as an actual binding - cbInstallations.DataSource = ViewModel.Installations; + // VM.Installations -> cmbInstallations.DataSource + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Installations), nameof(cmbInstallations.DataSource), cmbInstallations); - // VM.SelectedInstallation <-> cbInstallations.SelectedValue - CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(ViewModel.SelectedInstallation), cbInstallations, nameof(cbInstallations.SelectedValue)); + // VM.SelectedInstallation <-> cmbInstallations.SelectedValue + CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(ViewModel.SelectedInstallation), cmbInstallations, nameof(cmbInstallations.SelectedValue)); // VM.CurrentPath <-> tbPath.Text CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(ViewModel.CurrentPath), tbPath, nameof(tbPath.Text)); @@ -114,11 +130,7 @@ private void InitCommandBindings() .AddSource(btnRemove, nameof(btnRemove.Click)); } - private string SelectFolder() - { - using (var dlg = new FolderBrowserDialog { SelectedPath = ViewModel.CurrentPath }) - return dlg.ShowDialog() != DialogResult.OK ? null : dlg.SelectedPath; - } + private string? SelectFolder() => Dialogs.SelectFolder(ViewModel.CurrentPath); #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/MvvmBaseForm.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/MvvmBaseForm.Designer.cs new file mode 100644 index 0000000..6ef7bce --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/Forms/MvvmBaseForm.Designer.cs @@ -0,0 +1,28 @@ +namespace KGySoft.Drawing.ImagingTools.View.Forms +{ + partial class MvvmBaseForm + { + private KGySoft.Drawing.ImagingTools.View.Components.AdvancedToolTip toolTip; + private System.ComponentModel.IContainer components; + + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.toolTip = new KGySoft.Drawing.ImagingTools.View.Components.AdvancedToolTip(this.components); + this.SuspendLayout(); + // + // toolTip + // + this.toolTip.AutoPopDelay = 10000; + this.toolTip.InitialDelay = 500; + this.toolTip.ReshowDelay = 100; + // + // MvvmBaseForm + // + this.ClientSize = new System.Drawing.Size(284, 261); + this.Name = "MvvmBaseForm"; + this.RightToLeftLayout = true; + this.ResumeLayout(false); + } + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/MvvmBaseForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/MvvmBaseForm.cs index cca6e0f..0d38a5c 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/MvvmBaseForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/MvvmBaseForm.cs @@ -17,6 +17,11 @@ #region Usings using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Linq; +using System.Threading; using System.Windows.Forms; using KGySoft.ComponentModel; @@ -26,12 +31,22 @@ namespace KGySoft.Drawing.ImagingTools.View.Forms { - internal class MvvmBaseForm : BaseForm, IView + internal partial class MvvmBaseForm : BaseForm, IView where TViewModel : IDisposable // BUG: Actually should be ViewModelBase but WinForms designer with derived forms dies from that { #region Fields - private bool isClosing; + private readonly int threadId; + private readonly ManualResetEventSlim handleCreated; + + private ErrorProvider? warningProvider; + private ErrorProvider? infoProvider; + private ErrorProvider? errorProvider; + private ICommand? validationResultsChangesCommand; + + private bool isLoaded; + private bool isRtlChanging; + private Point location; #endregion @@ -39,8 +54,15 @@ internal class MvvmBaseForm : BaseForm, IView #region Protected Properties - protected TViewModel ViewModel { get; } - protected CommandBindingsCollection CommandBindings { get; } + protected TViewModel ViewModel { get; } = default!; + protected CommandBindingsCollection CommandBindings { get; } = new WinFormsCommandBindingsCollection(); + + protected ErrorProvider ErrorProvider => errorProvider ??= CreateProvider(ValidationSeverity.Error); + protected ErrorProvider WarningProvider => warningProvider ??= CreateProvider(ValidationSeverity.Warning); + protected ErrorProvider InfoProvider => infoProvider ??= CreateProvider(ValidationSeverity.Information); + + protected Dictionary ValidationMapping { get; } = new Dictionary(); + protected ICommand ValidationResultsChangedCommand => validationResultsChangesCommand ??= new SimpleCommand(OnValidationResultsChangedCommand); #endregion @@ -59,28 +81,34 @@ internal class MvvmBaseForm : BaseForm, IView protected MvvmBaseForm(TViewModel viewModel) { + threadId = Thread.CurrentThread.ManagedThreadId; + handleCreated = new ManualResetEventSlim(); + ApplyRightToLeft(); + InitializeComponent(); + StartPosition = OSUtils.IsMono && OSUtils.IsWindows ? FormStartPosition.WindowsDefaultLocation : FormStartPosition.CenterParent; + + // occurs in design mode but DesignMode is false for grandchild forms - if (viewModel == null) + if (viewModel == null!) return; - ViewModel = viewModel; + ViewModel = viewModel; ViewModelBase vm = VM; vm.ShowInfoCallback = Dialogs.InfoMessage; vm.ShowWarningCallback = Dialogs.WarningMessage; vm.ShowErrorCallback = Dialogs.ErrorMessage; vm.ConfirmCallback = Dialogs.ConfirmMessage; + vm.CancellableConfirmCallback = (msg, btn) => Dialogs.CancellableConfirmMessage(msg, btn switch { 0 => MessageBoxDefaultButton.Button1, 1 => MessageBoxDefaultButton.Button2, _ => MessageBoxDefaultButton.Button3 }); vm.ShowChildViewCallback = ShowChildView; vm.CloseViewCallback = () => BeginInvoke(new Action(Close)); vm.SynchronizedInvokeCallback = InvokeIfRequired; - - CommandBindings = new WinformsCommandBindingsCollection(); } #endregion #region Private Constructors - private MvvmBaseForm() + private MvvmBaseForm() : this(default!) { // this ctor is just for the designer } @@ -95,28 +123,72 @@ private MvvmBaseForm() protected override void OnLoad(EventArgs e) { + // Not Using tool window appearance on Linux because looks bad an on high DPI the close is too small + if (OSUtils.IsMono && OSUtils.IsLinux && FormBorderStyle == FormBorderStyle.SizableToolWindow) + { + FormBorderStyle = FormBorderStyle.Sizable; + MinimizeBox = false; + } + base.OnLoad(e); - if (ViewModel == null) + + // Null VM occurs in design mode but DesignMode is false for grandchild forms + // Loaded can be true if handle was recreated + if (isLoaded || ViewModel == null!) + { + if (!isRtlChanging) + return; + + // dialog has been reopened after changing RTL + isRtlChanging = false; + Location = location; return; + } + + isLoaded = true; ApplyResources(); ApplyViewModel(); } - protected virtual void ApplyResources() => this.ApplyStaticStringResources(); + protected virtual void ApplyResources() => ApplyStringResources(); + + protected virtual void ApplyStringResources() => this.ApplyStringResources(toolTip); - protected virtual void ApplyViewModel() => VM.ViewLoaded(); + protected virtual void ApplyViewModel() + { + InitPropertyBindings(); + InitCommandBindings(); + VM.ViewLoaded(); + } + + protected override void OnHandleCreated(EventArgs e) + { + base.OnHandleCreated(e); + handleCreated.Set(); + } protected override void OnFormClosing(FormClosingEventArgs e) { - if (!e.Cancel) - isClosing = true; + // Changing RightToLeft causes the dialog close. We let it happen because the parent may also change, + // and if we cancel the closing here, then a dialog may turn a non-modal form. Reopen as a dialog is handled in IView.ShowDialog + if (isRtlChanging) + { + if (DialogResult == DialogResult.OK) + isRtlChanging = false; + else + location = Location; + } + base.OnFormClosing(e); } protected override void Dispose(bool disposing) { if (disposing) - CommandBindings?.Dispose(); + { + components?.Dispose(); + CommandBindings.Dispose(); + } base.Dispose(disposing); } @@ -125,18 +197,54 @@ protected override void Dispose(bool disposing) #region Private Methods - private void ShowChildView(IViewModel vm) => ViewFactory.ShowDialog(vm, Handle); + private void InitPropertyBindings() + { + if (ValidationMapping.Count != 0) + { + // this.RightToLeft -> errorProvider/warningProvider/infoProvider.RightToLeft + CommandBindings.AddPropertyBinding(this, nameof(RightToLeft), nameof(ErrorProvider.RightToLeft), + rtl => rtl is RightToLeft.Yes, ErrorProvider, WarningProvider, InfoProvider); + } + } + + private void InitCommandBindings() + { + CommandBindings.Add(OnDisplayLanguageChangedCommand) + .AddSource(typeof(Res), nameof(Res.DisplayLanguageChanged)); + } + + private ErrorProvider CreateProvider(ValidationSeverity level) => new ErrorProvider(components) + { + ContainerControl = this, + Icon = level switch + { + ValidationSeverity.Error => Icons.SystemError.ToScaledIcon(this.GetScale()), + ValidationSeverity.Warning => Icons.SystemWarning.ToScaledIcon(this.GetScale()), + ValidationSeverity.Information => Icons.SystemInformation.ToScaledIcon(this.GetScale()), + _ => null + } + }; + + private void ShowChildView(IViewModel vm) => ViewFactory.ShowDialog(vm, this); private void InvokeIfRequired(Action action) { - if (isClosing || Disposing || IsDisposed) + if (Disposing || IsDisposed) return; + try { - if (InvokeRequired) - Invoke(action); - else + // no invoke is required (not using InvokeRequired because that may return false if handle is not created yet) + if (threadId == Thread.CurrentThread.ManagedThreadId) + { action.Invoke(); + return; + } + + if (!handleCreated.IsSet) + handleCreated.Wait(); + + Invoke(action); } catch (ObjectDisposedException) { @@ -144,13 +252,71 @@ private void InvokeIfRequired(Action action) } } + private void ApplyRightToLeft() + { + RightToLeft rtl = Res.DisplayLanguage.TextInfo.IsRightToLeft ? RightToLeft.Yes : RightToLeft.No; + if (RightToLeft == rtl) + return; + + if (!OSUtils.IsMono && IsHandleCreated) + isRtlChanging = true; + + RightToLeft = rtl; + } + + #endregion + + #region Command Handlers + + private void OnDisplayLanguageChangedCommand() => InvokeIfRequired(() => + { + ApplyRightToLeft(); + ApplyStringResources(); + }); + + private void OnValidationResultsChangedCommand(ValidationResultsCollection? validationResults) + { + foreach (KeyValuePair mapping in ValidationMapping) + { + var propertyResults = validationResults?[mapping.Key]; // var is IList in .NET 3.5 and IReadOnlyList above + ValidationResult? error = propertyResults?.FirstOrDefault(vr => vr.Severity == ValidationSeverity.Error); + ValidationResult? warning = error == null ? propertyResults?.FirstOrDefault(vr => vr.Severity == ValidationSeverity.Warning) : null; + ValidationResult? info = error == null && warning == null ? propertyResults?.FirstOrDefault(vr => vr.Severity == ValidationSeverity.Information) : null; + ErrorProvider.SetError(mapping.Value, error?.Message); + WarningProvider.SetError(mapping.Value, warning?.Message); + InfoProvider.SetError(mapping.Value, info?.Message); + } + } + #endregion #region Explicit Interface Implementations - void IView.ShowDialog(IntPtr ownerHandle) => ShowDialog(ownerHandle == IntPtr.Zero ? null : new OwnerWindowHandle(ownerHandle)); + [SuppressMessage("CodeQuality", "IDE0002:Name can be simplified", + Justification = "Without the base qualifier executing in Mono causes StackOverflowException. See https://github.com/mono/mono/issues/21129")] + void IDisposable.Dispose() + { + isRtlChanging = false; + InvokeIfRequired(base.Dispose); + } + + void IView.ShowDialog(IntPtr ownerHandle) + { + do + { + ShowDialog(ownerHandle == IntPtr.Zero ? null : new OwnerWindowHandle(ownerHandle)); + } while (isRtlChanging); + } - void IView.Show() + void IView.ShowDialog(IView? owner) + { + do + { + ShowDialog(owner is IWin32Window window ? window : null); + } while (isRtlChanging); + } + + void IView.Show() => InvokeIfRequired(() => { if (!Visible) { @@ -162,7 +328,7 @@ void IView.Show() WindowState = FormWindowState.Normal; Activate(); BringToFront(); - } + }); #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/PaletteVisualizerForm.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/PaletteVisualizerForm.Designer.cs index 2b780db..205df90 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/PaletteVisualizerForm.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/PaletteVisualizerForm.Designer.cs @@ -19,9 +19,10 @@ partial class PaletteVisualizerForm private void InitializeComponent() { this.gbPalette = new System.Windows.Forms.GroupBox(); - this.pnlPalette = new PalettePanel(); + this.pnlPalette = new KGySoft.Drawing.ImagingTools.View.Controls.PalettePanel(); this.gbSelectedColor = new System.Windows.Forms.GroupBox(); - this.ucColorVisualizer = new ColorVisualizerControl(); + this.ucColorVisualizer = new KGySoft.Drawing.ImagingTools.View.UserControls.ColorVisualizerControl(); + this.okCancelButtons = new KGySoft.Drawing.ImagingTools.View.UserControls.OkCancelButtons(); this.gbPalette.SuspendLayout(); this.gbSelectedColor.SuspendLayout(); this.SuspendLayout(); @@ -30,9 +31,9 @@ private void InitializeComponent() // this.gbPalette.Controls.Add(this.pnlPalette); this.gbPalette.Dock = System.Windows.Forms.DockStyle.Fill; - this.gbPalette.Location = new System.Drawing.Point(0, 0); + this.gbPalette.Location = new System.Drawing.Point(3, 3); this.gbPalette.Name = "gbPalette"; - this.gbPalette.Size = new System.Drawing.Size(247, 234); + this.gbPalette.Size = new System.Drawing.Size(241, 233); this.gbPalette.TabIndex = 0; this.gbPalette.TabStop = false; // @@ -41,17 +42,16 @@ private void InitializeComponent() this.pnlPalette.Dock = System.Windows.Forms.DockStyle.Fill; this.pnlPalette.Location = new System.Drawing.Point(3, 16); this.pnlPalette.Name = "pnlPalette"; - this.pnlPalette.Size = new System.Drawing.Size(241, 215); + this.pnlPalette.Size = new System.Drawing.Size(235, 214); this.pnlPalette.TabIndex = 0; - this.pnlPalette.TabStop = true; // // gbSelectedColor // this.gbSelectedColor.Controls.Add(this.ucColorVisualizer); this.gbSelectedColor.Dock = System.Windows.Forms.DockStyle.Bottom; - this.gbSelectedColor.Location = new System.Drawing.Point(0, 234); + this.gbSelectedColor.Location = new System.Drawing.Point(3, 236); this.gbSelectedColor.Name = "gbSelectedColor"; - this.gbSelectedColor.Size = new System.Drawing.Size(247, 216); + this.gbSelectedColor.Size = new System.Drawing.Size(241, 216); this.gbSelectedColor.TabIndex = 1; this.gbSelectedColor.TabStop = false; // @@ -60,20 +60,30 @@ private void InitializeComponent() this.ucColorVisualizer.Dock = System.Windows.Forms.DockStyle.Fill; this.ucColorVisualizer.Location = new System.Drawing.Point(3, 16); this.ucColorVisualizer.Name = "ucColorVisualizer"; - this.ucColorVisualizer.Size = new System.Drawing.Size(241, 197); + this.ucColorVisualizer.Size = new System.Drawing.Size(235, 197); this.ucColorVisualizer.TabIndex = 0; // + // okCancelButtons + // + this.okCancelButtons.Dock = System.Windows.Forms.DockStyle.Bottom; + this.okCancelButtons.Location = new System.Drawing.Point(3, 452); + this.okCancelButtons.Name = "okCancelButtons"; + this.okCancelButtons.Size = new System.Drawing.Size(241, 35); + this.okCancelButtons.TabIndex = 2; + // // PaletteVisualizerForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(247, 450); + this.ClientSize = new System.Drawing.Size(247, 490); this.Controls.Add(this.gbPalette); this.Controls.Add(this.gbSelectedColor); + this.Controls.Add(this.okCancelButtons); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; - this.MaximumSize = new System.Drawing.Size(280, 32867); - this.MinimumSize = new System.Drawing.Size(255, 300); + this.MaximumSize = new System.Drawing.Size(280, 32767); + this.MinimumSize = new System.Drawing.Size(255, 335); this.Name = "PaletteVisualizerForm"; + this.Padding = new System.Windows.Forms.Padding(3); this.gbPalette.ResumeLayout(false); this.gbSelectedColor.ResumeLayout(false); this.ResumeLayout(false); @@ -86,5 +96,6 @@ private void InitializeComponent() private PalettePanel pnlPalette; private System.Windows.Forms.GroupBox gbSelectedColor; private ColorVisualizerControl ucColorVisualizer; + private OkCancelButtons okCancelButtons; } } \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/PaletteVisualizerForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/PaletteVisualizerForm.cs index 162c3e4..8bcacdb 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/PaletteVisualizerForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/PaletteVisualizerForm.cs @@ -16,6 +16,10 @@ #region Usings +using System; +using System.Drawing; +using System.Windows.Forms; + using KGySoft.Drawing.ImagingTools.ViewModel; #endregion @@ -32,13 +36,15 @@ internal PaletteVisualizerForm(PaletteVisualizerViewModel viewModel) : base(viewModel) { InitializeComponent(); + AcceptButton = okCancelButtons.OKButton; + CancelButton = okCancelButtons.CancelButton; } #endregion #region Private Constructors - private PaletteVisualizerForm() : this(null) + private PaletteVisualizerForm() : this(null!) { // this ctor is just for the designer } @@ -51,6 +57,19 @@ private PaletteVisualizerForm() : this(null) #region Protected Methods + protected override void OnLoad(EventArgs e) + { + // Fixing high DPI appearance on Mono + PointF scale; + if (OSUtils.IsMono && (scale = this.GetScale()) != new PointF(1f, 1f)) + { + MinimumSize = new Size(255, 335).Scale(scale); + MaximumSize = new Size((int)(280 * scale.X), Int16.MaxValue); + } + + base.OnLoad(e); + } + protected override void ApplyResources() { base.ApplyResources(); @@ -65,6 +84,26 @@ protected override void ApplyViewModel() base.ApplyViewModel(); } + protected override void OnFormClosing(FormClosingEventArgs e) + { + if (DialogResult != DialogResult.OK) + ViewModel.SetModified(false); + base.OnFormClosing(e); + } + + protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + { + switch (keyData) + { + case Keys.Escape when ViewModel.ReadOnly: // if not ReadOnly, use the Cancel button + DialogResult = DialogResult.Cancel; + return true; + + default: + return base.ProcessCmdKey(ref msg, keyData); + } + } + protected override void Dispose(bool disposing) { if (disposing) @@ -78,17 +117,23 @@ protected override void Dispose(bool disposing) private void InitPropertyBindings() { + // !VM.ReadOnly -> okCancelButtons.Visible + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.ReadOnly), nameof(okCancelButtons.Visible), ro => ro is false, okCancelButtons); + // VM.Palette -> pnlPalette.Palette CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Palette), nameof(pnlPalette.Palette), pnlPalette); // VM.Count -> Text (formatted) - CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Count), nameof(Text), c => Res.TitlePaletteCount((int)c), this); + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Count), nameof(Text), c => Res.TitlePaletteCount((int)c!), this); // VM.ReadOnly -> ucColorVisualizer.ReadOnly CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.ReadOnly), nameof(ucColorVisualizer.ReadOnly), ucColorVisualizer); // pnlPalette.SelectedColor -> ucColorVisualizer.Color CommandBindings.AddPropertyBinding(pnlPalette, nameof(pnlPalette.SelectedColor), nameof(ucColorVisualizer.Color), ucColorVisualizer); + + // VM.IsModified -> OKButton.Enabled + CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.IsModified), nameof(okCancelButtons.OKButton.Enabled), okCancelButtons.OKButton); } private void InitCommandBindings() @@ -97,6 +142,8 @@ private void InitCommandBindings() .AddSource(ucColorVisualizer, nameof(ucColorVisualizer.ColorEdited)); CommandBindings.Add(OnSelectedColorChangedCommand) .AddSource(pnlPalette, nameof(pnlPalette.SelectedColorChanged)); + CommandBindings.Add(OnCancelCommand) + .AddSource(okCancelButtons.CancelButton, nameof(okCancelButtons.CancelButton.Click)); } private void UpdateInfo() => ucColorVisualizer.SpecialInfo = Res.InfoSelectedIndex(pnlPalette.SelectedColorIndex); @@ -118,6 +165,8 @@ private void OnColorEditedCommand() private void OnSelectedColorChangedCommand() => UpdateInfo(); + private void OnCancelCommand() => ViewModel.SetModified(false); + #endregion #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/ResizeBitmapForm.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/ResizeBitmapForm.Designer.cs index 4d4ea7b..7a4d8f2 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/ResizeBitmapForm.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/ResizeBitmapForm.Designer.cs @@ -18,18 +18,18 @@ private void InitializeComponent() this.tblNewSize = new System.Windows.Forms.TableLayoutPanel(); this.chbMaintainAspectRatio = new System.Windows.Forms.CheckBox(); this.lblScalingMode = new System.Windows.Forms.Label(); - this.pnlHeightPx = new System.Windows.Forms.Panel(); + this.pnlHeightPx = new KGySoft.Drawing.ImagingTools.View.Controls.AutoMirrorPanel(); this.lblHeightPx = new System.Windows.Forms.Label(); this.txtHeightPx = new System.Windows.Forms.TextBox(); - this.pnlHeightPercent = new System.Windows.Forms.Panel(); + this.pnlHeightPercent = new KGySoft.Drawing.ImagingTools.View.Controls.AutoMirrorPanel(); this.lblHeightPercent = new System.Windows.Forms.Label(); this.txtHeightPercent = new System.Windows.Forms.TextBox(); - this.pnlWidthPx = new System.Windows.Forms.Panel(); + this.pnlWidthPx = new KGySoft.Drawing.ImagingTools.View.Controls.AutoMirrorPanel(); this.lblWidthPx = new System.Windows.Forms.Label(); this.txtWidthPx = new System.Windows.Forms.TextBox(); this.rbByPixels = new System.Windows.Forms.RadioButton(); this.rbByPercentage = new System.Windows.Forms.RadioButton(); - this.pnlWidthPercent = new System.Windows.Forms.Panel(); + this.pnlWidthPercent = new KGySoft.Drawing.ImagingTools.View.Controls.AutoMirrorPanel(); this.lblWidthPercent = new System.Windows.Forms.Label(); this.txtWidthPercent = new System.Windows.Forms.TextBox(); this.lblWidth = new System.Windows.Forms.Label(); @@ -47,7 +47,7 @@ private void InitializeComponent() // this.pnlSettings.Controls.Add(this.tblNewSize); this.pnlSettings.Padding = new System.Windows.Forms.Padding(5); - this.pnlSettings.Size = new System.Drawing.Size(314, 143); + this.pnlSettings.Size = new System.Drawing.Size(328, 143); // // tblNewSize // @@ -75,8 +75,8 @@ private void InitializeComponent() this.tblNewSize.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); this.tblNewSize.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); this.tblNewSize.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); - this.tblNewSize.Size = new System.Drawing.Size(304, 137); - this.tblNewSize.TabIndex = 1; + this.tblNewSize.Size = new System.Drawing.Size(318, 137); + this.tblNewSize.TabIndex = 0; // // chbMaintainAspectRatio // @@ -86,43 +86,42 @@ private void InitializeComponent() this.chbMaintainAspectRatio.FlatStyle = System.Windows.Forms.FlatStyle.System; this.chbMaintainAspectRatio.Location = new System.Drawing.Point(103, 3); this.chbMaintainAspectRatio.Name = "chbMaintainAspectRatio"; - this.chbMaintainAspectRatio.Size = new System.Drawing.Size(198, 18); - this.chbMaintainAspectRatio.TabIndex = 10; + this.chbMaintainAspectRatio.Size = new System.Drawing.Size(212, 18); + this.chbMaintainAspectRatio.TabIndex = 0; this.chbMaintainAspectRatio.Text = "chbMaintainAspectRatio"; this.chbMaintainAspectRatio.UseVisualStyleBackColor = true; // // lblScalingMode // - this.lblScalingMode.AutoSize = true; this.lblScalingMode.Dock = System.Windows.Forms.DockStyle.Fill; this.lblScalingMode.Location = new System.Drawing.Point(3, 108); this.lblScalingMode.Name = "lblScalingMode"; + this.lblScalingMode.Padding = new System.Windows.Forms.Padding(0, 3, 0, 0); this.lblScalingMode.Size = new System.Drawing.Size(94, 29); - this.lblScalingMode.TabIndex = 8; + this.lblScalingMode.TabIndex = 9; this.lblScalingMode.Text = "lblScalingMode"; - this.lblScalingMode.TextAlign = System.Drawing.ContentAlignment.MiddleRight; + this.lblScalingMode.TextAlign = System.Drawing.ContentAlignment.TopRight; // // pnlHeightPx // this.pnlHeightPx.Controls.Add(this.lblHeightPx); this.pnlHeightPx.Controls.Add(this.txtHeightPx); - this.pnlHeightPx.Dock = System.Windows.Forms.DockStyle.Fill; - this.pnlHeightPx.Location = new System.Drawing.Point(205, 84); + this.pnlHeightPx.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlHeightPx.Location = new System.Drawing.Point(209, 81); + this.pnlHeightPx.Margin = new System.Windows.Forms.Padding(0); this.pnlHeightPx.Name = "pnlHeightPx"; - this.pnlHeightPx.Size = new System.Drawing.Size(96, 21); - this.pnlHeightPx.TabIndex = 7; + this.pnlHeightPx.Size = new System.Drawing.Size(109, 21); + this.pnlHeightPx.TabIndex = 8; // // lblHeightPx // - this.lblHeightPx.AutoSize = true; this.lblHeightPx.Dock = System.Windows.Forms.DockStyle.Fill; this.lblHeightPx.Location = new System.Drawing.Point(62, 0); this.lblHeightPx.Name = "lblHeightPx"; - this.lblHeightPx.Padding = new System.Windows.Forms.Padding(0, 3, 0, 0); - this.lblHeightPx.Size = new System.Drawing.Size(60, 16); + this.lblHeightPx.Padding = new System.Windows.Forms.Padding(0, 2, 0, 0); + this.lblHeightPx.Size = new System.Drawing.Size(47, 21); this.lblHeightPx.TabIndex = 1; this.lblHeightPx.Text = "lblHeightPx"; - this.lblHeightPx.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // txtHeightPx // @@ -137,23 +136,23 @@ private void InitializeComponent() // this.pnlHeightPercent.Controls.Add(this.lblHeightPercent); this.pnlHeightPercent.Controls.Add(this.txtHeightPercent); - this.pnlHeightPercent.Dock = System.Windows.Forms.DockStyle.Fill; - this.pnlHeightPercent.Location = new System.Drawing.Point(103, 84); + this.pnlHeightPercent.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlHeightPercent.Location = new System.Drawing.Point(100, 81); + this.pnlHeightPercent.Margin = new System.Windows.Forms.Padding(0); this.pnlHeightPercent.Name = "pnlHeightPercent"; - this.pnlHeightPercent.Size = new System.Drawing.Size(96, 21); - this.pnlHeightPercent.TabIndex = 6; + this.pnlHeightPercent.Size = new System.Drawing.Size(109, 21); + this.pnlHeightPercent.TabIndex = 7; // // lblHeightPercent // this.lblHeightPercent.AutoSize = true; - this.lblHeightPercent.Dock = System.Windows.Forms.DockStyle.Fill; + this.lblHeightPercent.Dock = System.Windows.Forms.DockStyle.Left; this.lblHeightPercent.Location = new System.Drawing.Point(62, 0); this.lblHeightPercent.Name = "lblHeightPercent"; - this.lblHeightPercent.Padding = new System.Windows.Forms.Padding(0, 3, 0, 0); - this.lblHeightPercent.Size = new System.Drawing.Size(85, 16); + this.lblHeightPercent.Padding = new System.Windows.Forms.Padding(0, 2, 0, 0); + this.lblHeightPercent.Size = new System.Drawing.Size(85, 15); this.lblHeightPercent.TabIndex = 1; this.lblHeightPercent.Text = "lblHeightPercent"; - this.lblHeightPercent.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // txtHeightPercent // @@ -168,23 +167,22 @@ private void InitializeComponent() // this.pnlWidthPx.Controls.Add(this.lblWidthPx); this.pnlWidthPx.Controls.Add(this.txtWidthPx); - this.pnlWidthPx.Dock = System.Windows.Forms.DockStyle.Fill; - this.pnlWidthPx.Location = new System.Drawing.Point(205, 57); + this.pnlWidthPx.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlWidthPx.Location = new System.Drawing.Point(209, 54); + this.pnlWidthPx.Margin = new System.Windows.Forms.Padding(0); this.pnlWidthPx.Name = "pnlWidthPx"; - this.pnlWidthPx.Size = new System.Drawing.Size(96, 21); - this.pnlWidthPx.TabIndex = 4; + this.pnlWidthPx.Size = new System.Drawing.Size(109, 21); + this.pnlWidthPx.TabIndex = 5; // // lblWidthPx // - this.lblWidthPx.AutoSize = true; this.lblWidthPx.Dock = System.Windows.Forms.DockStyle.Fill; this.lblWidthPx.Location = new System.Drawing.Point(62, 0); this.lblWidthPx.Name = "lblWidthPx"; - this.lblWidthPx.Padding = new System.Windows.Forms.Padding(0, 3, 0, 0); - this.lblWidthPx.Size = new System.Drawing.Size(57, 16); + this.lblWidthPx.Padding = new System.Windows.Forms.Padding(0, 2, 0, 0); + this.lblWidthPx.Size = new System.Drawing.Size(47, 21); this.lblWidthPx.TabIndex = 1; this.lblWidthPx.Text = "lblWidthPx"; - this.lblWidthPx.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // txtWidthPx // @@ -197,26 +195,24 @@ private void InitializeComponent() // // rbByPixels // - this.rbByPixels.AutoSize = true; - this.rbByPixels.Dock = System.Windows.Forms.DockStyle.Top; + this.rbByPixels.Dock = System.Windows.Forms.DockStyle.Fill; this.rbByPixels.FlatStyle = System.Windows.Forms.FlatStyle.System; - this.rbByPixels.Location = new System.Drawing.Point(205, 30); + this.rbByPixels.Location = new System.Drawing.Point(212, 30); this.rbByPixels.Name = "rbByPixels"; - this.rbByPixels.Size = new System.Drawing.Size(96, 18); - this.rbByPixels.TabIndex = 1; + this.rbByPixels.Size = new System.Drawing.Size(103, 21); + this.rbByPixels.TabIndex = 2; this.rbByPixels.TabStop = true; this.rbByPixels.Text = "rbByPixels"; this.rbByPixels.UseVisualStyleBackColor = true; // // rbByPercentage // - this.rbByPercentage.AutoSize = true; - this.rbByPercentage.Dock = System.Windows.Forms.DockStyle.Top; + this.rbByPercentage.Dock = System.Windows.Forms.DockStyle.Fill; this.rbByPercentage.FlatStyle = System.Windows.Forms.FlatStyle.System; this.rbByPercentage.Location = new System.Drawing.Point(103, 30); this.rbByPercentage.Name = "rbByPercentage"; - this.rbByPercentage.Size = new System.Drawing.Size(96, 18); - this.rbByPercentage.TabIndex = 0; + this.rbByPercentage.Size = new System.Drawing.Size(103, 21); + this.rbByPercentage.TabIndex = 1; this.rbByPercentage.TabStop = true; this.rbByPercentage.Text = "rbByPercentage"; this.rbByPercentage.UseVisualStyleBackColor = true; @@ -225,23 +221,23 @@ private void InitializeComponent() // this.pnlWidthPercent.Controls.Add(this.lblWidthPercent); this.pnlWidthPercent.Controls.Add(this.txtWidthPercent); - this.pnlWidthPercent.Dock = System.Windows.Forms.DockStyle.Fill; - this.pnlWidthPercent.Location = new System.Drawing.Point(103, 57); + this.pnlWidthPercent.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlWidthPercent.Location = new System.Drawing.Point(100, 54); + this.pnlWidthPercent.Margin = new System.Windows.Forms.Padding(0); this.pnlWidthPercent.Name = "pnlWidthPercent"; - this.pnlWidthPercent.Size = new System.Drawing.Size(96, 21); - this.pnlWidthPercent.TabIndex = 3; + this.pnlWidthPercent.Size = new System.Drawing.Size(109, 21); + this.pnlWidthPercent.TabIndex = 4; // // lblWidthPercent // this.lblWidthPercent.AutoSize = true; - this.lblWidthPercent.Dock = System.Windows.Forms.DockStyle.Fill; + this.lblWidthPercent.Dock = System.Windows.Forms.DockStyle.Left; this.lblWidthPercent.Location = new System.Drawing.Point(62, 0); this.lblWidthPercent.Name = "lblWidthPercent"; - this.lblWidthPercent.Padding = new System.Windows.Forms.Padding(0, 3, 0, 0); - this.lblWidthPercent.Size = new System.Drawing.Size(82, 16); + this.lblWidthPercent.Padding = new System.Windows.Forms.Padding(0, 2, 0, 0); + this.lblWidthPercent.Size = new System.Drawing.Size(82, 15); this.lblWidthPercent.TabIndex = 1; this.lblWidthPercent.Text = "lblWidthPercent"; - this.lblWidthPercent.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // txtWidthPercent // @@ -254,25 +250,25 @@ private void InitializeComponent() // // lblWidth // - this.lblWidth.AutoSize = true; this.lblWidth.Dock = System.Windows.Forms.DockStyle.Fill; this.lblWidth.Location = new System.Drawing.Point(3, 54); this.lblWidth.Name = "lblWidth"; + this.lblWidth.Padding = new System.Windows.Forms.Padding(0, 2, 0, 0); this.lblWidth.Size = new System.Drawing.Size(94, 27); - this.lblWidth.TabIndex = 2; + this.lblWidth.TabIndex = 3; this.lblWidth.Text = "lblWidth"; - this.lblWidth.TextAlign = System.Drawing.ContentAlignment.MiddleRight; + this.lblWidth.TextAlign = System.Drawing.ContentAlignment.TopRight; // // lblHeight // - this.lblHeight.AutoSize = true; this.lblHeight.Dock = System.Windows.Forms.DockStyle.Fill; this.lblHeight.Location = new System.Drawing.Point(3, 81); this.lblHeight.Name = "lblHeight"; + this.lblHeight.Padding = new System.Windows.Forms.Padding(0, 2, 0, 0); this.lblHeight.Size = new System.Drawing.Size(94, 27); - this.lblHeight.TabIndex = 5; + this.lblHeight.TabIndex = 6; this.lblHeight.Text = "lblHeight"; - this.lblHeight.TextAlign = System.Drawing.ContentAlignment.MiddleRight; + this.lblHeight.TextAlign = System.Drawing.ContentAlignment.TopRight; // // cmbScalingMode // @@ -281,17 +277,19 @@ private void InitializeComponent() this.cmbScalingMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; this.cmbScalingMode.FlatStyle = System.Windows.Forms.FlatStyle.System; this.cmbScalingMode.FormattingEnabled = true; - this.cmbScalingMode.Location = new System.Drawing.Point(103, 111); + this.cmbScalingMode.Location = new System.Drawing.Point(100, 108); + this.cmbScalingMode.Margin = new System.Windows.Forms.Padding(0); this.cmbScalingMode.Name = "cmbScalingMode"; - this.cmbScalingMode.Size = new System.Drawing.Size(198, 21); - this.cmbScalingMode.TabIndex = 9; + this.cmbScalingMode.Size = new System.Drawing.Size(218, 21); + this.cmbScalingMode.TabIndex = 10; // // ResizeBitmapForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(314, 291); - this.MinimumSize = new System.Drawing.Size(330, 330); + this.ClientSize = new System.Drawing.Size(334, 291); + this.Location = new System.Drawing.Point(0, 0); + this.MinimumSize = new System.Drawing.Size(350, 330); this.Name = "ResizeBitmapForm"; this.Text = "ResizeBitmapForm"; this.pnlSettings.ResumeLayout(false); @@ -306,7 +304,6 @@ private void InitializeComponent() this.pnlWidthPercent.ResumeLayout(false); this.pnlWidthPercent.PerformLayout(); this.ResumeLayout(false); - this.PerformLayout(); } @@ -315,18 +312,18 @@ private void InitializeComponent() private System.Windows.Forms.TableLayoutPanel tblNewSize; private System.Windows.Forms.RadioButton rbByPixels; private System.Windows.Forms.RadioButton rbByPercentage; - private System.Windows.Forms.Panel pnlWidthPercent; + private Controls.AutoMirrorPanel pnlWidthPercent; private System.Windows.Forms.Label lblWidthPercent; private System.Windows.Forms.TextBox txtWidthPercent; private System.Windows.Forms.Label lblWidth; private System.Windows.Forms.Label lblHeight; - private System.Windows.Forms.Panel pnlHeightPx; + private Controls.AutoMirrorPanel pnlHeightPx; private System.Windows.Forms.Label lblHeightPx; private System.Windows.Forms.TextBox txtHeightPx; - private System.Windows.Forms.Panel pnlHeightPercent; + private Controls.AutoMirrorPanel pnlHeightPercent; private System.Windows.Forms.Label lblHeightPercent; private System.Windows.Forms.TextBox txtHeightPercent; - private System.Windows.Forms.Panel pnlWidthPx; + private Controls.AutoMirrorPanel pnlWidthPx; private System.Windows.Forms.Label lblWidthPx; private System.Windows.Forms.TextBox txtWidthPx; private System.Windows.Forms.Label lblScalingMode; diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/ResizeBitmapForm.cs b/KGySoft.Drawing.ImagingTools/View/Forms/ResizeBitmapForm.cs index 9cbb03e..f5a4d85 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/ResizeBitmapForm.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/ResizeBitmapForm.cs @@ -17,7 +17,9 @@ #region Usings using System; +using System.Drawing; using System.Globalization; +using System.Windows.Forms; using KGySoft.Drawing.ImagingTools.ViewModel; @@ -41,16 +43,15 @@ internal ResizeBitmapForm(ResizeBitmapViewModel viewModel) : base(viewModel) { InitializeComponent(); - - ValidationMapping[nameof(viewModel.Width)] = lblWidth; - ValidationMapping[nameof(viewModel.Height)] = lblHeight; + ValidationMapping[nameof(viewModel.Width)] = lblWidthPercent; + ValidationMapping[nameof(viewModel.Height)] = lblHeightPercent; } #endregion #region Private Constructors - private ResizeBitmapForm() : this(null) + private ResizeBitmapForm() : this(null!) { // this ctor is just for the designer } @@ -63,10 +64,10 @@ private ResizeBitmapForm() : this(null) #region Static Methods - private static object FormatPercentage(object value) => ((float)value * 100f).ToString("F0", CultureInfo.CurrentCulture); - private static object ParsePercentage(object value) => Single.TryParse((string)value, NumberStyles.Number, CultureInfo.CurrentCulture, out float result) ? result / 100f : 0f; - private static object FormatInteger(object value) => ((int)value).ToString("F0", CultureInfo.CurrentCulture); - private static object ParseInteger(object value) => Int32.TryParse((string)value, NumberStyles.Integer, CultureInfo.CurrentCulture, out int result) ? result : 0; + private static object FormatPercentage(object value) => value is 0f ? String.Empty : ((float)value * 100f).ToString("F0", LanguageSettings.FormattingLanguage); + private static object ParsePercentage(object value) => Single.TryParse((string)value, NumberStyles.Number, LanguageSettings.FormattingLanguage, out float result) ? result / 100f : 0f; + private static object FormatInteger(object value) => value is 0 ? String.Empty : ((int)value).ToString("F0", LanguageSettings.FormattingLanguage); + private static object ParseInteger(object value) => Int32.TryParse((string)value, NumberStyles.Integer, LanguageSettings.FormattingLanguage, out int result) ? result : 0; #endregion @@ -74,6 +75,16 @@ private ResizeBitmapForm() : this(null) #region Protected Methods + protected override void OnLoad(EventArgs e) + { + // Fixing high DPI appearance on Mono + PointF scale; + if (OSUtils.IsMono && (scale = this.GetScale()) != new PointF(1f, 1f)) + tblNewSize.ColumnStyles[0].Width = (int)(100 * scale.X); + + base.OnLoad(e); + } + protected override void ApplyResources() { Icon = Properties.Resources.Resize; @@ -119,21 +130,40 @@ private void InitPropertyBindings() CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.ByPixels), rbByPixels, nameof(rbByPixels.Checked)); CommandBindings.AddPropertyBinding(ViewModel, nameof(VM.ByPixels), nameof(Enabled), txtWidthPx, txtHeightPx); + // Regular WinForms binding behaves a bit better because it does not clear the currently edited text box on parse error + // but it fails to sync the other properties properly on Mono so using KGy SOFT binding in Mono systems. + // VM.WidthRatio <-> txtWidthPercent.Text - CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.WidthRatio), txtWidthPercent, nameof(txtWidthPercent.Text), - FormatPercentage, ParsePercentage); + if (OSUtils.IsMono) + CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.WidthRatio), txtWidthPercent, nameof(txtWidthPercent.Text), FormatPercentage!, ParsePercentage!); + else + AddWinFormsBinding(nameof(VM.WidthRatio), txtWidthPercent, nameof(txtWidthPercent.Text), FormatPercentage, ParsePercentage); // VM.HeightRatio <-> txtHeightPercent.Text - CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.HeightRatio), txtHeightPercent, nameof(txtHeightPercent.Text), - FormatPercentage, ParsePercentage); + if (OSUtils.IsMono) + CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.HeightRatio), txtHeightPercent, nameof(txtHeightPercent.Text), FormatPercentage!, ParsePercentage!); + else + AddWinFormsBinding(nameof(VM.HeightRatio), txtHeightPercent, nameof(txtHeightPercent.Text), FormatPercentage, ParsePercentage); // VM.Width <-> txtWidthPx.Text - CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.Width), txtWidthPx, nameof(txtWidthPx.Text), - FormatInteger, ParseInteger); + if (OSUtils.IsMono) + CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.Width), txtWidthPx, nameof(txtWidthPx.Text), FormatInteger!, ParseInteger!); + else + AddWinFormsBinding(nameof(VM.Width), txtWidthPx, nameof(txtWidthPx.Text), FormatInteger, ParseInteger); // VM.Height <-> txtHeightPx.Text - CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.Height), txtHeightPx, nameof(txtHeightPx.Text), - FormatInteger, ParseInteger); + if (OSUtils.IsMono) + CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(VM.Height), txtHeightPx, nameof(txtHeightPx.Text), FormatInteger!, ParseInteger!); + else + AddWinFormsBinding(nameof(VM.Height), txtHeightPx, nameof(txtHeightPx.Text), FormatInteger, ParseInteger); + } + + private void AddWinFormsBinding(string sourceName, IBindableComponent target, string propertyName, Func format, Func parse) + { + var binding = new Binding(propertyName, ViewModel, sourceName, true, DataSourceUpdateMode.OnPropertyChanged); + binding.Format += (_, e) => e.Value = format.Invoke(e.Value); + binding.Parse += (_, e) => e.Value = parse.Invoke(e.Value); + target.DataBindings.Add(binding); } #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/TransformBitmapFormBase.Designer.cs b/KGySoft.Drawing.ImagingTools/View/Forms/TransformBitmapFormBase.Designer.cs index cbc23d5..d900632 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/TransformBitmapFormBase.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/TransformBitmapFormBase.Designer.cs @@ -15,65 +15,47 @@ partial class TransformBitmapFormBase /// private void InitializeComponent() { - this.components = new System.ComponentModel.Container(); - this.progress = new KGySoft.Drawing.ImagingTools.View.Controls.DrawingProgressStatusStrip(); + this.progress = new KGySoft.Drawing.ImagingTools.View.Controls.DrawingProgressFooter(); this.okCancelButtons = new KGySoft.Drawing.ImagingTools.View.UserControls.OkCancelButtons(); this.previewImage = new KGySoft.Drawing.ImagingTools.View.UserControls.PreviewImageControl(); - this.pnlSettings = new System.Windows.Forms.Panel(); - this.errorProvider = new System.Windows.Forms.ErrorProvider(this.components); - this.warningProvider = new System.Windows.Forms.ErrorProvider(this.components); - this.infoProvider = new System.Windows.Forms.ErrorProvider(this.components); - ((System.ComponentModel.ISupportInitialize)(this.errorProvider)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.warningProvider)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.infoProvider)).BeginInit(); + this.pnlSettings = new KGySoft.Drawing.ImagingTools.View.Controls.AutoMirrorPanel(); this.SuspendLayout(); // // progress // this.progress.BackColor = System.Drawing.Color.Transparent; - this.progress.Location = new System.Drawing.Point(0, 189); + this.progress.Dock = System.Windows.Forms.DockStyle.Bottom; + this.progress.Location = new System.Drawing.Point(3, 189); this.progress.Name = "progress"; - this.progress.Size = new System.Drawing.Size(248, 22); - this.progress.SizingGrip = false; + this.progress.Size = new System.Drawing.Size(242, 22); this.progress.TabIndex = 3; this.progress.Text = "drawingProgressStatusStrip1"; // // okCancelButtons // + this.okCancelButtons.BackColor = System.Drawing.Color.Transparent; this.okCancelButtons.Dock = System.Windows.Forms.DockStyle.Bottom; - this.okCancelButtons.Location = new System.Drawing.Point(0, 149); + this.okCancelButtons.Location = new System.Drawing.Point(3, 154); this.okCancelButtons.Name = "okCancelButtons"; - this.okCancelButtons.Size = new System.Drawing.Size(248, 40); + this.okCancelButtons.Size = new System.Drawing.Size(242, 35); this.okCancelButtons.TabIndex = 2; // // previewImage // this.previewImage.Dock = System.Windows.Forms.DockStyle.Fill; - this.previewImage.Location = new System.Drawing.Point(0, 56); + this.previewImage.Location = new System.Drawing.Point(3, 56); this.previewImage.Name = "previewImage"; - this.previewImage.Size = new System.Drawing.Size(248, 93); + this.previewImage.Size = new System.Drawing.Size(242, 98); this.previewImage.TabIndex = 1; // // pnlSettings // this.pnlSettings.Dock = System.Windows.Forms.DockStyle.Top; - this.pnlSettings.Location = new System.Drawing.Point(0, 0); + this.pnlSettings.Location = new System.Drawing.Point(3, 0); this.pnlSettings.Name = "pnlSettings"; - this.pnlSettings.Size = new System.Drawing.Size(248, 56); + this.pnlSettings.Size = new System.Drawing.Size(242, 56); this.pnlSettings.TabIndex = 0; // - // errorProvider - // - this.errorProvider.ContainerControl = this; - // - // warningProvider - // - this.warningProvider.ContainerControl = this; - // - // infoProvider - // - this.infoProvider.ContainerControl = this; - // // TransformBitmapFormBase // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); @@ -85,23 +67,17 @@ private void InitializeComponent() this.Controls.Add(this.progress); this.MinimizeBox = false; this.Name = "TransformBitmapFormBase"; + this.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0); this.Text = "TransformBitmapFormBase"; - ((System.ComponentModel.ISupportInitialize)(this.errorProvider)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.warningProvider)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.infoProvider)).EndInit(); this.ResumeLayout(false); - this.PerformLayout(); } #endregion - private Controls.DrawingProgressStatusStrip progress; + private Controls.DrawingProgressFooter progress; private UserControls.OkCancelButtons okCancelButtons; private UserControls.PreviewImageControl previewImage; - protected System.Windows.Forms.Panel pnlSettings; - private System.Windows.Forms.ErrorProvider warningProvider; - private System.Windows.Forms.ErrorProvider infoProvider; - private System.Windows.Forms.ErrorProvider errorProvider; + protected Controls.AutoMirrorPanel pnlSettings; } } \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/Forms/TransformBitmapFormBase.cs b/KGySoft.Drawing.ImagingTools/View/Forms/TransformBitmapFormBase.cs index 09d6f55..95885b1 100644 --- a/KGySoft.Drawing.ImagingTools/View/Forms/TransformBitmapFormBase.cs +++ b/KGySoft.Drawing.ImagingTools/View/Forms/TransformBitmapFormBase.cs @@ -16,12 +16,9 @@ #region Usings -using System.Collections.Generic; -using System.Linq; +using System; using System.Windows.Forms; -using KGySoft.ComponentModel; -using KGySoft.CoreLibraries; using KGySoft.Drawing.ImagingTools.ViewModel; #endregion @@ -30,15 +27,6 @@ namespace KGySoft.Drawing.ImagingTools.View.Forms { internal partial class TransformBitmapFormBase : MvvmBaseForm { - #region Properties - - protected Dictionary ValidationMapping { get; } - protected ErrorProvider ErrorProvider => errorProvider; - protected ErrorProvider WarningProvider => warningProvider; - protected ErrorProvider InfoProvider => infoProvider; - - #endregion - #region Constructors #region Protected Constructors @@ -50,18 +38,15 @@ protected TransformBitmapFormBase(TransformBitmapViewModelBase viewModel) AcceptButton = okCancelButtons.OKButton; CancelButton = okCancelButtons.CancelButton; - errorProvider.SetIconAlignment(previewImage.ImageViewer, ErrorIconAlignment.MiddleLeft); - ValidationMapping = new Dictionary - { - [nameof(viewModel.PreviewImageViewModel.PreviewImage)] = previewImage.ImageViewer, - }; + ErrorProvider.SetIconAlignment(previewImage.ImageViewer, ErrorIconAlignment.MiddleLeft); + ValidationMapping[nameof(viewModel.PreviewImageViewModel.PreviewImage)] = previewImage.ImageViewer; } #endregion #region Private Constructors - private TransformBitmapFormBase() : this(null) + private TransformBitmapFormBase() : this(null!) { // this ctor is just for the designer } @@ -74,14 +59,6 @@ private TransformBitmapFormBase() : this(null) #region Protected Methods - protected override void ApplyResources() - { - base.ApplyResources(); - errorProvider.Icon = Icons.SystemError.ToScaledIcon(); - warningProvider.Icon = Icons.SystemWarning.ToScaledIcon(); - infoProvider.Icon = Icons.SystemInformation.ToScaledIcon(); - } - protected override void ApplyViewModel() { InitCommandBindings(); @@ -93,8 +70,8 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { switch (keyData) { - case Keys.Alt | Keys.Z: - previewImage.AutoZoom = !previewImage.AutoZoom; + case Keys.Alt | Keys.S: + previewImage.SmoothZooming = !previewImage.SmoothZooming; return true; default: return base.ProcessCmdKey(ref msg, keyData); @@ -129,8 +106,9 @@ private void InitCommandBindings() .AddSource(okCancelButtons.CancelButton, nameof(okCancelButtons.CancelButton.Click)); // View commands - CommandBindings.Add>(OnValidationResultsChangedCommand) - .AddSource(ViewModel, nameof(ViewModel.ValidationResultsChanged)); + CommandBindings.Add(ValidationResultsChangedCommand) + .AddSource(ViewModel, nameof(ViewModel.ValidationResultsChanged)) + .WithParameter(() => ViewModel.ValidationResults); } private void InitPropertyBindings() @@ -147,24 +125,6 @@ private void InitPropertyBindings() #endregion - #region Command handlers - - private void OnValidationResultsChangedCommand(ICommandSource> src) - { - foreach (KeyValuePair mapping in ValidationMapping) - { - var validationResults = src.EventArgs.EventData[mapping.Key]; - ValidationResult error = validationResults.FirstOrDefault(vr => vr.Severity == ValidationSeverity.Error); - ValidationResult warning = error == null ? validationResults.FirstOrDefault(vr => vr.Severity == ValidationSeverity.Warning) : null; - ValidationResult info = error == null && warning == null ? validationResults.FirstOrDefault(vr => vr.Severity == ValidationSeverity.Information) : null; - errorProvider.SetError(mapping.Value, error?.Message); - warningProvider.SetError(mapping.Value, warning?.Message); - infoProvider.SetError(mapping.Value, info?.Message); - } - } - - #endregion - #endregion } } diff --git a/KGySoft.Drawing.ImagingTools/View/Images.cs b/KGySoft.Drawing.ImagingTools/View/Images.cs index f3034b5..f5cdea0 100644 --- a/KGySoft.Drawing.ImagingTools/View/Images.cs +++ b/KGySoft.Drawing.ImagingTools/View/Images.cs @@ -29,47 +29,49 @@ internal static class Images { #region Fields - #region Private Fields - - private static Bitmap check; - private static Bitmap crop; - private static Bitmap highlightVisibleClip; - private static Bitmap magnifier; - private static Bitmap save; - private static Bitmap open; - private static Bitmap clear; - private static Bitmap prev; - private static Bitmap next; - private static Bitmap palette; - private static Bitmap settings; - private static Bitmap animation; - private static Bitmap multiSize; - private static Bitmap multiPage; - private static Bitmap smoothZoom; - private static Bitmap edit; - private static Bitmap rotateLeft; - private static Bitmap rotateRight; - private static Bitmap resize; - private static Bitmap quantize; - private static Bitmap colors; - private static Bitmap compare; - - #endregion - - #region Internal Fields - - internal static readonly Size ReferenceSize = new Size(16, 16); - - #endregion + private static readonly Size referenceSize = new Size(16, 16); + + private static Bitmap? imagingTools; + private static Bitmap? check; + private static Bitmap? crop; + private static Bitmap? highlightVisibleClip; + private static Bitmap? language; + private static Bitmap? magnifier; + private static Bitmap? magnifierPlus; + private static Bitmap? magnifierMinus; + private static Bitmap? magnifier1; + private static Bitmap? save; + private static Bitmap? open; + private static Bitmap? clear; + private static Bitmap? prev; + private static Bitmap? next; + private static Bitmap? palette; + private static Bitmap? settings; + private static Bitmap? animation; + private static Bitmap? multiSize; + private static Bitmap? multiPage; + private static Bitmap? smoothZoom; + private static Bitmap? edit; + private static Bitmap? rotateLeft; + private static Bitmap? rotateRight; + private static Bitmap? resize; + private static Bitmap? quantize; + private static Bitmap? colors; + private static Bitmap? compare; #endregion #region Properties + internal static Bitmap ImagingTools => imagingTools ??= GetResource(nameof(ImagingTools)); internal static Bitmap Check => check ??= GetResource(nameof(Check)); internal static Bitmap Crop => crop ??= GetResource(nameof(Crop)); internal static Bitmap HighlightVisibleClip => highlightVisibleClip ??= GetResource(nameof(HighlightVisibleClip)); + internal static Bitmap Language => language ??= GetResource(nameof(Language)); internal static Bitmap Magnifier => magnifier ??= GetResource(nameof(Magnifier)); + internal static Bitmap MagnifierPlus => magnifierPlus ??= GetResource(nameof(MagnifierPlus)); + internal static Bitmap MagnifierMinus => magnifierMinus ??= GetResource(nameof(MagnifierMinus)); + internal static Bitmap Magnifier1 => magnifier1 ??= GetResource(nameof(Magnifier1)); internal static Bitmap Save => save ??= GetResource(nameof(Save)); internal static Bitmap Open => open ??= GetResource(nameof(Open)); internal static Bitmap Clear => clear ??= GetResource(nameof(Clear)); @@ -95,31 +97,40 @@ internal static class Images #region Internal Methods - internal static Bitmap ToScaledBitmap(this Icon icon) + internal static Bitmap ToScaledBitmap(this Icon icon, PointF? scale = null) { if (icon == null) throw new ArgumentNullException(nameof(icon), PublicResources.ArgumentNull); - return icon.ExtractNearestBitmap(ReferenceSize.Scale(OSUtils.SystemScale), PixelFormat.Format32bppArgb); + using (icon) + return icon.ExtractNearestBitmap(referenceSize.Scale(scale ?? OSUtils.SystemScale), PixelFormat.Format32bppArgb); } - internal static Icon ToScaledIcon(this Icon icon, bool legacyScaling = true) + internal static Icon ToScaledIcon(this Icon icon, PointF? scale = null, bool div16Scaling = true) { if (icon == null) throw new ArgumentNullException(nameof(icon), PublicResources.ArgumentNull); - Size size = ReferenceSize.Scale(OSUtils.SystemScale); - Icon result = icon.ExtractNearestIcon(size, PixelFormat.Format32bppArgb); - int mod; - if (!legacyScaling || OSUtils.IsWindows8OrLater || !OSUtils.IsWindows || (mod = result.Width & 0xF) == 0) - return result; - - // Windows XP-Windows 7 with legacy scaling: we need to make sure that icon size is dividable by 16 - // so it will not be corrupted (eg. ErrorProvider) - using Bitmap iconImage = icon.ExtractBitmap(result.Size); - result.Dispose(); - - // returning a larger icon without scaling so apparently it will have the same size as the original one - return iconImage.ToIcon(size.Width + (16 - mod), ScalingMode.NoScaling); + using (icon) + { + Size size = referenceSize.Scale(scale ?? OSUtils.SystemScale); + Icon result = icon.ExtractNearestIcon(size, PixelFormat.Format32bppArgb); + int mod; + if (!div16Scaling || !OSUtils.IsWindows || (mod = result.Width & 0xF) == 0) + return result; + +#if !NET35 + if (OSUtils.IsWindows8OrLater) + return result; +#endif + + // .NET 3.5 or Windows XP-Windows 7 with legacy scaling: we need to make sure that icon size is dividable by 16 + // so it will not be corrupted (eg. ErrorProvider) + using Bitmap iconImage = icon.ExtractBitmap(result.Size)!; + result.Dispose(); + + // returning a larger icon without scaling so apparently it will have the same size as the original one + return iconImage.ToIcon(size.Width + (16 - mod), ScalingMode.NoScaling); + } } #endregion @@ -128,7 +139,7 @@ internal static Icon ToScaledIcon(this Icon icon, bool legacyScaling = true) private static Bitmap GetResource(string resourceName) { - var icon = (Icon)Properties.Resources.ResourceManager.GetObject(resourceName, CultureInfo.InvariantCulture); + var icon = (Icon)Properties.Resources.ResourceManager.GetObject(resourceName, CultureInfo.InvariantCulture)!; if (OSUtils.IsVistaOrLater) return icon.ToMultiResBitmap(); diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/ColorVisualizerControl.Designer.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/ColorVisualizerControl.Designer.cs index 352701a..14b2c28 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/ColorVisualizerControl.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/ColorVisualizerControl.Designer.cs @@ -32,10 +32,9 @@ private void InitializeComponent() this.lblAlpha = new System.Windows.Forms.Label(); this.pnlAlpha = new System.Windows.Forms.Panel(); this.tbAlpha = new System.Windows.Forms.TrackBar(); - this.tsMenu = new ScalingToolStrip(); + this.tsMenu = new KGySoft.Drawing.ImagingTools.View.Controls.AdvancedToolStrip(); this.btnSelectColor = new System.Windows.Forms.ToolStripButton(); this.txtColor = new System.Windows.Forms.TextBox(); - this.colorDialog = new System.Windows.Forms.ColorDialog(); this.pnlControls.SuspendLayout(); this.tblColor.SuspendLayout(); this.pnlRed.SuspendLayout(); @@ -109,11 +108,13 @@ private void InitializeComponent() // // tbRed // - this.tbRed.Dock = System.Windows.Forms.DockStyle.Fill; + this.tbRed.AutoSize = false; + this.tbRed.Dock = System.Windows.Forms.DockStyle.Top; this.tbRed.LargeChange = 64; this.tbRed.Location = new System.Drawing.Point(0, 0); this.tbRed.Maximum = 255; this.tbRed.Name = "tbRed"; + this.tbRed.RightToLeftLayout = true; this.tbRed.Size = new System.Drawing.Size(78, 18); this.tbRed.TabIndex = 0; this.tbRed.TickFrequency = 64; @@ -141,11 +142,13 @@ private void InitializeComponent() // // tbGreen // - this.tbGreen.Dock = System.Windows.Forms.DockStyle.Fill; + this.tbGreen.AutoSize = false; + this.tbGreen.Dock = System.Windows.Forms.DockStyle.Top; this.tbGreen.LargeChange = 64; this.tbGreen.Location = new System.Drawing.Point(0, 0); this.tbGreen.Maximum = 255; this.tbGreen.Name = "tbGreen"; + this.tbGreen.RightToLeftLayout = true; this.tbGreen.Size = new System.Drawing.Size(78, 18); this.tbGreen.TabIndex = 0; this.tbGreen.TickFrequency = 64; @@ -173,11 +176,13 @@ private void InitializeComponent() // // tbBlue // - this.tbBlue.Dock = System.Windows.Forms.DockStyle.Fill; + this.tbBlue.AutoSize = false; + this.tbBlue.Dock = System.Windows.Forms.DockStyle.Top; this.tbBlue.LargeChange = 64; this.tbBlue.Location = new System.Drawing.Point(0, 0); this.tbBlue.Maximum = 255; this.tbBlue.Name = "tbBlue"; + this.tbBlue.RightToLeftLayout = true; this.tbBlue.Size = new System.Drawing.Size(78, 18); this.tbBlue.TabIndex = 0; this.tbBlue.TickFrequency = 64; @@ -216,11 +221,13 @@ private void InitializeComponent() // // tbAlpha // - this.tbAlpha.Dock = System.Windows.Forms.DockStyle.Fill; + this.tbAlpha.AutoSize = false; + this.tbAlpha.Dock = System.Windows.Forms.DockStyle.Top; this.tbAlpha.LargeChange = 64; this.tbAlpha.Location = new System.Drawing.Point(0, 0); this.tbAlpha.Maximum = 255; this.tbAlpha.Name = "tbAlpha"; + this.tbAlpha.RightToLeftLayout = true; this.tbAlpha.Size = new System.Drawing.Size(78, 18); this.tbAlpha.TabIndex = 0; this.tbAlpha.TickFrequency = 64; @@ -244,7 +251,9 @@ private void InitializeComponent() // // txtColor // + this.txtColor.BackColor = System.Drawing.SystemColors.Control; this.txtColor.Dock = System.Windows.Forms.DockStyle.Fill; + this.txtColor.ForeColor = System.Drawing.SystemColors.ControlText; this.txtColor.Location = new System.Drawing.Point(0, 83); this.txtColor.Multiline = true; this.txtColor.Name = "txtColor"; @@ -254,34 +263,25 @@ private void InitializeComponent() this.txtColor.TabIndex = 1; this.txtColor.WordWrap = false; // - // colorDialog - // - this.colorDialog.AnyColor = true; - this.colorDialog.FullOpen = true; - // - // ucColorVisualizer + // ColorVisualizerControl // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Controls.Add(this.txtColor); this.Controls.Add(this.pnlControls); - this.Name = "ucColorVisualizer"; + this.Name = "ColorVisualizerControl"; this.Size = new System.Drawing.Size(247, 188); this.pnlControls.ResumeLayout(false); this.pnlControls.PerformLayout(); this.tblColor.ResumeLayout(false); this.tblColor.PerformLayout(); this.pnlRed.ResumeLayout(false); - this.pnlRed.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.tbRed)).EndInit(); this.pnlGreen.ResumeLayout(false); - this.pnlGreen.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.tbGreen)).EndInit(); this.pnlBlue.ResumeLayout(false); - this.pnlBlue.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.tbBlue)).EndInit(); this.pnlAlpha.ResumeLayout(false); - this.pnlAlpha.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.tbAlpha)).EndInit(); this.tsMenu.ResumeLayout(false); this.tsMenu.PerformLayout(); @@ -293,7 +293,7 @@ private void InitializeComponent() #endregion private System.Windows.Forms.Panel pnlControls; - private ScalingToolStrip tsMenu; + private AdvancedToolStrip tsMenu; private System.Windows.Forms.TableLayoutPanel tblColor; private System.Windows.Forms.Label lblRed; private System.Windows.Forms.Panel pnlRed; @@ -306,7 +306,6 @@ private void InitializeComponent() private System.Windows.Forms.Panel pnlAlpha; private System.Windows.Forms.ToolStripButton btnSelectColor; private System.Windows.Forms.TextBox txtColor; - private System.Windows.Forms.ColorDialog colorDialog; private System.Windows.Forms.TrackBar tbAlpha; private System.Windows.Forms.TrackBar tbRed; private System.Windows.Forms.TrackBar tbGreen; diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/ColorVisualizerControl.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/ColorVisualizerControl.cs index 8bd023e..fe61e96 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/ColorVisualizerControl.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/ColorVisualizerControl.cs @@ -18,8 +18,9 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; using System.Text; using System.Windows.Forms; @@ -35,8 +36,8 @@ internal partial class ColorVisualizerControl : BaseUserControl #region Static Fields - private static Dictionary knownColors; - private static Dictionary systemColors; + private static Dictionary? knownColors; + private static Dictionary? systemColors; #endregion @@ -44,8 +45,9 @@ internal partial class ColorVisualizerControl : BaseUserControl private bool readOnly; private Color color; - private TextureBrush alphaBrush; - private string specialInfo; + private Bitmap? alphaPattern; + private ImageAttributes? attrTiles; + private string? specialInfo; #endregion @@ -141,7 +143,7 @@ internal Color Color } } - internal string SpecialInfo + internal string? SpecialInfo { get => specialInfo; set @@ -185,21 +187,8 @@ public ColorVisualizerControl() #region Static Methods - private static string GetKnownColor(Color color) - { - if (KnownColors.TryGetValue(color.ToArgb(), out string name)) - return name; - - return "-"; - } - - private static string GetSystemColors(Color color) - { - if (SystemColors.TryGetValue(color.ToArgb(), out string name)) - return name; - - return "-"; - } + private static string GetKnownColor(Color color) => KnownColors.GetValueOrDefault(color.ToArgb(), "–")!; + private static string GetSystemColors(Color color) => SystemColors.GetValueOrDefault(color.ToArgb(), "–")!; #endregion @@ -207,15 +196,30 @@ private static string GetSystemColors(Color color) #region Protected Methods + protected override void OnLoad(EventArgs e) + { + // Fixing high DPI appearance on Mono + PointF scale; + if (OSUtils.IsMono && (scale = this.GetScale()) != new PointF(1f, 1f)) + { + tblColor.ColumnStyles[0].Width = (int)(50 * scale.X); + tblColor.ColumnStyles[1].Width = (int)(80 * scale.X); + } + + base.OnLoad(e); + } + protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); - alphaBrush?.Dispose(); + alphaPattern?.Dispose(); + attrTiles?.Dispose(); } - alphaBrush = null; + alphaPattern = null; + attrTiles = null; pnlAlpha.Paint -= pnlColor_Paint; pnlColor.Paint -= pnlColor_Paint; btnSelectColor.Click -= btnEdit_Click; @@ -259,28 +263,30 @@ private void ColorUpdated() private void UpdateInfo() { - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); if (!String.IsNullOrEmpty(SpecialInfo)) sb.AppendLine(SpecialInfo); sb.Append(Res.InfoColor(color.ToArgb(), GetKnownColor(color), GetSystemColors(color), color.GetHue(), color.GetSaturation() * 100f, color.GetBrightness() * 100f)); txtColor.Text = sb.ToString(); } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "False alarm, bmpPattern is passed to a brush")] - private void CreateAlphaBrush() + private void CreateAlphaPattern() { Size size = new Size(10, 10).Scale(this.GetScale()); var bmpPattern = new Bitmap(size.Width, size.Height); using (Graphics g = Graphics.FromImage(bmpPattern)) { g.Clear(Color.White); - Rectangle smallRect = new Rectangle(Point.Empty, new Size(bmpPattern.Width >> 1, bmpPattern.Height >> 1)); + var smallRect = new Rectangle(Point.Empty, new Size(bmpPattern.Width >> 1, bmpPattern.Height >> 1)); g.FillRectangle(Brushes.Silver, smallRect); smallRect.Offset(smallRect.Width, smallRect.Height); g.FillRectangle(Brushes.Silver, smallRect); } - alphaBrush = new TextureBrush(bmpPattern); + // Using a TextureBrush would be simpler but that is not supported on Mono + attrTiles = new ImageAttributes(); + attrTiles.SetWrapMode(WrapMode.Tile); + alphaPattern = bmpPattern; } private void OnColorEdited() => Events.GetHandler(nameof(ColorEdited))?.Invoke(this, EventArgs.Empty); @@ -296,10 +302,11 @@ private void pnlColor_Paint(object sender, PaintEventArgs e) // painting checked background if (color.A != 255) { - if (alphaBrush == null) - CreateAlphaBrush(); + if (alphaPattern == null) + CreateAlphaPattern(); - e.Graphics.FillRectangle(alphaBrush, e.ClipRectangle); + Size size = pnlColor.Size; + e.Graphics.DrawImage(alphaPattern!, new Rectangle(Point.Empty, size), 0, 0 , size.Width, size.Height, GraphicsUnit.Pixel, attrTiles); } Color backColor = sender == pnlAlpha ? Color.FromArgb(color.A, Color.White) : color; @@ -307,44 +314,44 @@ private void pnlColor_Paint(object sender, PaintEventArgs e) e.Graphics.FillRectangle(b, e.ClipRectangle); } - private void btnEdit_Click(object sender, EventArgs e) + private void btnEdit_Click(object? sender, EventArgs e) { if (readOnly) return; - colorDialog.Color = color; - if (colorDialog.ShowDialog(this) == DialogResult.OK) + Color? newColor = Dialogs.PickColor(color); + if (newColor.HasValue) { - Color = colorDialog.Color; + Color = newColor.Value; OnColorEdited(); } } - private void tbAlpha_Scroll(object sender, EventArgs e) + private void tbAlpha_Scroll(object? sender, EventArgs e) { Color = Color.FromArgb(tbAlpha.Value, color.R, color.G, color.B); OnColorEdited(); } - private void tbRed_Scroll(object sender, EventArgs e) + private void tbRed_Scroll(object? sender, EventArgs e) { Color = Color.FromArgb(color.A, tbRed.Value, color.G, color.B); OnColorEdited(); } - private void tbGreen_Scroll(object sender, EventArgs e) + private void tbGreen_Scroll(object? sender, EventArgs e) { Color = Color.FromArgb(color.A, color.R, tbGreen.Value, color.B); OnColorEdited(); } - private void tbBlue_Scroll(object sender, EventArgs e) + private void tbBlue_Scroll(object? sender, EventArgs e) { Color = Color.FromArgb(color.A, color.R, color.G, tbBlue.Value); OnColorEdited(); } - void ucColorVisualizer_SystemColorsChanged(object sender, EventArgs e) + void ucColorVisualizer_SystemColorsChanged(object? sender, EventArgs e) { systemColors = null; UpdateInfo(); diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/DithererSelectorControl.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/DithererSelectorControl.cs index aeab33f..80f57fa 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/DithererSelectorControl.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/DithererSelectorControl.cs @@ -59,14 +59,14 @@ protected override void Dispose(bool disposing) private void InitCommandBindings() { // not for ViewModel.Parameters.PropertyChanged because it is not triggered for expanded properties such as collection elements - CommandBindings.Add(ViewModel.ResetDitherer) + CommandBindings.Add(ViewModel!.ResetDitherer) .AddSource(pgParameters, nameof(pgParameters.PropertyValueChanged)); } private void InitPropertyBindings() { // will not change so not as an actual binding - cmbDitherer.DataSource = ViewModel.Ditherers; + cmbDitherer.DataSource = ViewModel!.Ditherers; // VM.Parameters -> pgParameters.SelectedObject CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Parameters), nameof(pgParameters.SelectedObject), pgParameters); diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/DithererStrengthEditorControl.Designer.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/DithererStrengthEditorControl.Designer.cs index fe3cd9b..6f718df 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/DithererStrengthEditorControl.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/DithererStrengthEditorControl.Designer.cs @@ -27,6 +27,7 @@ private void InitializeComponent() this.trackBar.Location = new System.Drawing.Point(35, 0); this.trackBar.Maximum = 100; this.trackBar.Name = "trackBar"; + this.trackBar.RightToLeftLayout = true; this.trackBar.Size = new System.Drawing.Size(147, 27); this.trackBar.TabIndex = 0; this.trackBar.TickFrequency = 10; @@ -36,7 +37,7 @@ private void InitializeComponent() this.okCancelButtons.Dock = System.Windows.Forms.DockStyle.Bottom; this.okCancelButtons.Location = new System.Drawing.Point(0, 27); this.okCancelButtons.Name = "okCancelButtons"; - this.okCancelButtons.Size = new System.Drawing.Size(182, 40); + this.okCancelButtons.Size = new System.Drawing.Size(182, 35); this.okCancelButtons.TabIndex = 1; // // lblValue diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/DithererStrengthEditorControl.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/DithererStrengthEditorControl.cs index b3461bb..70353b5 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/DithererStrengthEditorControl.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/DithererStrengthEditorControl.cs @@ -17,7 +17,6 @@ #region Usings using System; -using System.Globalization; using System.Windows.Forms; using System.Windows.Forms.Design; @@ -29,7 +28,7 @@ internal partial class DithererStrengthEditorControl : BaseUserControl { #region Fields - private readonly IWindowsFormsEditorService editorService; + private readonly IWindowsFormsEditorService? editorService; private readonly float originalValue; #endregion @@ -52,7 +51,7 @@ internal DithererStrengthEditorControl(IWindowsFormsEditorService editorService, trackBar.ValueChanged += TrackBar_ValueChanged; okCancelButtons.CancelButton.Click += CancelButton_Click; okCancelButtons.OKButton.Click += OKButton_Click; - okCancelButtons.ApplyStaticStringResources(); + okCancelButtons.ApplyStringResources(); originalValue = value; trackBar.Value = value >= 0f && value <= 1f ? (int)(value * 100) : 0; @@ -65,6 +64,7 @@ internal DithererStrengthEditorControl(IWindowsFormsEditorService editorService, private DithererStrengthEditorControl() { + RightToLeft = Res.DisplayLanguage.TextInfo.IsRightToLeft ? RightToLeft.Yes : RightToLeft.No; InitializeComponent(); } @@ -98,27 +98,27 @@ protected override bool ProcessDialogKey(Keys keyData) #region Private Methods - private void UpdateLabel() => lblValue.Text = Value <= 0f ? Res.TextAuto : Value.ToString("F2", CultureInfo.CurrentCulture); + private void UpdateLabel() => lblValue.Text = Value <= 0f ? Res.TextAuto : Value.ToString("F2", LanguageSettings.FormattingLanguage); #endregion #region Event handlers - private void TrackBar_ValueChanged(object sender, EventArgs e) + private void TrackBar_ValueChanged(object? sender, EventArgs e) { Value = trackBar.Value / 100f; UpdateLabel(); } - private void OKButton_Click(object sender, EventArgs e) + private void OKButton_Click(object? sender, EventArgs e) { - editorService.CloseDropDown(); + editorService?.CloseDropDown(); } - private void CancelButton_Click(object sender, EventArgs e) + private void CancelButton_Click(object? sender, EventArgs e) { Value = originalValue; - editorService.CloseDropDown(); + editorService?.CloseDropDown(); } #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/MvvmBaseUserControl.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/MvvmBaseUserControl.cs index cc321da..c361df5 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/MvvmBaseUserControl.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/MvvmBaseUserControl.cs @@ -17,6 +17,9 @@ #region Usings using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Windows.Forms; using KGySoft.ComponentModel; using KGySoft.Drawing.ImagingTools.ViewModel; @@ -25,12 +28,15 @@ namespace KGySoft.Drawing.ImagingTools.View.UserControls { - internal partial class MvvmBaseUserControl : BaseUserControl + internal class MvvmBaseUserControl : BaseUserControl where TViewModel : IDisposable // BUG: Actually should be ViewModelBase but WinForms designer with derived types dies from that { #region Fields - private TViewModel vm; + private readonly int threadId; + private readonly ManualResetEventSlim handleCreated; + + private TViewModel? vm; private bool isLoaded; #endregion @@ -39,6 +45,10 @@ internal partial class MvvmBaseUserControl : BaseUserControl #region Internal Properties + /// + /// Gets or sets the view model. Can be null before initializing. Not null if called from . + /// + [MaybeNull] internal TViewModel ViewModel { get => vm; @@ -63,7 +73,7 @@ internal TViewModel ViewModel #region Private Properties // this would not be needed if where TViewModel : ViewModelBase didn't conflict with WinForms designer - private ViewModelBase VM => (ViewModelBase)(object)ViewModel; + private ViewModelBase? VM => (ViewModelBase?)(object?)ViewModel; #endregion @@ -73,7 +83,9 @@ internal TViewModel ViewModel protected MvvmBaseUserControl() { - CommandBindings = new WinformsCommandBindingsCollection(); + CommandBindings = new WinFormsCommandBindingsCollection(); + threadId = Thread.CurrentThread.ManagedThreadId; + handleCreated = new ManualResetEventSlim(); } #endregion @@ -102,20 +114,27 @@ protected virtual void ApplyResources() protected virtual void ApplyViewModel() { - ViewModelBase vm = VM; - vm.ShowInfoCallback = Dialogs.InfoMessage; - vm.ShowWarningCallback = Dialogs.WarningMessage; - vm.ShowErrorCallback = Dialogs.ErrorMessage; - vm.ConfirmCallback = Dialogs.ConfirmMessage; - vm.SynchronizedInvokeCallback = InvokeIfRequired; - - VM.ViewLoaded(); + ViewModelBase vmb = VM!; + vmb.ShowInfoCallback = Dialogs.InfoMessage; + vmb.ShowWarningCallback = Dialogs.WarningMessage; + vmb.ShowErrorCallback = Dialogs.ErrorMessage; + vmb.ConfirmCallback = Dialogs.ConfirmMessage; + vmb.CancellableConfirmCallback = (msg, btn) => Dialogs.CancellableConfirmMessage(msg, btn switch { 0 => MessageBoxDefaultButton.Button1, 1 => MessageBoxDefaultButton.Button2, _ => MessageBoxDefaultButton.Button3 }); + vmb.SynchronizedInvokeCallback = InvokeIfRequired; + + vmb.ViewLoaded(); + } + + protected override void OnHandleCreated(EventArgs e) + { + base.OnHandleCreated(e); + handleCreated.Set(); } protected override void Dispose(bool disposing) { if (disposing) - CommandBindings?.Dispose(); + CommandBindings.Dispose(); base.Dispose(disposing); } @@ -126,10 +145,26 @@ protected override void Dispose(bool disposing) private void InvokeIfRequired(Action action) { - if (InvokeRequired) + if (Disposing || IsDisposed) + return; + + try + { + // no invoke is required (not using InvokeRequired because that may return false if handle is not created yet) + if (threadId == Thread.CurrentThread.ManagedThreadId) + { + action.Invoke(); + return; + } + + if (!handleCreated.IsSet) + handleCreated.Wait(); Invoke(action); - else - action.Invoke(); + } + catch (ObjectDisposedException) + { + // it can happen that actual Invoke is started to execute only after querying isClosing and when Disposing and IsDisposed both return false + } } #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/OkCancelButtons.Designer.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/OkCancelButtons.Designer.cs index 1c89482..b59080c 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/OkCancelButtons.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/OkCancelButtons.Designer.cs @@ -15,67 +15,86 @@ partial class OkCancelButtons /// private void InitializeComponent() { - this.pnlButtons = new System.Windows.Forms.TableLayoutPanel(); - this.btnOK = new System.Windows.Forms.Button(); + this.pnlButtons = new System.Windows.Forms.FlowLayoutPanel(); + this.btnApply = new System.Windows.Forms.Button(); this.btnCancel = new System.Windows.Forms.Button(); + this.btnOK = new System.Windows.Forms.Button(); this.pnlButtons.SuspendLayout(); this.SuspendLayout(); // // pnlButtons // - this.pnlButtons.ColumnCount = 2; - this.pnlButtons.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.pnlButtons.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.pnlButtons.Controls.Add(this.btnOK, 0, 0); - this.pnlButtons.Controls.Add(this.btnCancel, 1, 0); + this.pnlButtons.Controls.Add(this.btnApply); + this.pnlButtons.Controls.Add(this.btnCancel); + this.pnlButtons.Controls.Add(this.btnOK); this.pnlButtons.Dock = System.Windows.Forms.DockStyle.Fill; + this.pnlButtons.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; this.pnlButtons.Location = new System.Drawing.Point(0, 0); this.pnlButtons.Name = "pnlButtons"; - this.pnlButtons.RowCount = 1; - this.pnlButtons.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.pnlButtons.Size = new System.Drawing.Size(218, 40); + this.pnlButtons.Padding = new System.Windows.Forms.Padding(3); + this.pnlButtons.Size = new System.Drawing.Size(260, 35); this.pnlButtons.TabIndex = 0; // - // btnOK + // btnApply // - this.btnOK.Anchor = System.Windows.Forms.AnchorStyles.None; - this.btnOK.DialogResult = System.Windows.Forms.DialogResult.OK; - this.btnOK.FlatStyle = System.Windows.Forms.FlatStyle.System; - this.btnOK.Location = new System.Drawing.Point(17, 8); - this.btnOK.Name = "btnOK"; - this.btnOK.Size = new System.Drawing.Size(75, 23); - this.btnOK.TabIndex = 0; - this.btnOK.Text = "btnOK"; - this.btnOK.UseVisualStyleBackColor = true; + this.btnApply.Anchor = System.Windows.Forms.AnchorStyles.None; + this.btnApply.AutoSize = true; + this.btnApply.BackColor = System.Drawing.SystemColors.Control; + this.btnApply.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.btnApply.Location = new System.Drawing.Point(176, 6); + this.btnApply.Name = "btnApply"; + this.btnApply.Size = new System.Drawing.Size(75, 23); + this.btnApply.TabIndex = 2; + this.btnApply.Text = "btnApply"; + this.btnApply.UseVisualStyleBackColor = false; + this.btnApply.Visible = false; // // btnCancel // this.btnCancel.Anchor = System.Windows.Forms.AnchorStyles.None; + this.btnCancel.AutoSize = true; + this.btnCancel.BackColor = System.Drawing.SystemColors.Control; this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; this.btnCancel.FlatStyle = System.Windows.Forms.FlatStyle.System; - this.btnCancel.Location = new System.Drawing.Point(126, 8); + this.btnCancel.Location = new System.Drawing.Point(95, 6); this.btnCancel.Name = "btnCancel"; this.btnCancel.Size = new System.Drawing.Size(75, 23); this.btnCancel.TabIndex = 1; this.btnCancel.Text = "btnCancel"; - this.btnCancel.UseVisualStyleBackColor = true; + this.btnCancel.UseVisualStyleBackColor = false; + // + // btnOK + // + this.btnOK.Anchor = System.Windows.Forms.AnchorStyles.None; + this.btnOK.AutoSize = true; + this.btnOK.BackColor = System.Drawing.SystemColors.Control; + this.btnOK.DialogResult = System.Windows.Forms.DialogResult.OK; + this.btnOK.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.btnOK.Location = new System.Drawing.Point(14, 6); + this.btnOK.Name = "btnOK"; + this.btnOK.Size = new System.Drawing.Size(75, 23); + this.btnOK.TabIndex = 0; + this.btnOK.Text = "btnOK"; + this.btnOK.UseVisualStyleBackColor = false; // // OkCancelButtons // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.Color.Transparent; this.Controls.Add(this.pnlButtons); this.Name = "OkCancelButtons"; - this.Size = new System.Drawing.Size(218, 40); + this.Size = new System.Drawing.Size(260, 35); this.pnlButtons.ResumeLayout(false); + this.pnlButtons.PerformLayout(); this.ResumeLayout(false); } #endregion - - private System.Windows.Forms.TableLayoutPanel pnlButtons; private System.Windows.Forms.Button btnOK; private System.Windows.Forms.Button btnCancel; + private System.Windows.Forms.FlowLayoutPanel pnlButtons; + private System.Windows.Forms.Button btnApply; } } diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/OkCancelButtons.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/OkCancelButtons.cs index 41ea9e4..da890df 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/OkCancelButtons.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/OkCancelButtons.cs @@ -16,32 +16,75 @@ #region Usings +using System; +using System.ComponentModel; +using System.Drawing; using System.Windows.Forms; #endregion namespace KGySoft.Drawing.ImagingTools.View.UserControls { - internal partial class OkCancelButtons : BaseUserControl + internal sealed partial class OkCancelButtons : BaseUserControl { + #region Fields + + private bool isApplyVisible; + + #endregion + #region Properties + #region Public Properties + + [DefaultValue(false)] + public bool ApplyButtonVisible + { + get => isApplyVisible; + set + { + if (isApplyVisible == value) + return; + ApplyButton.Visible = isApplyVisible = value; + } + } + + #endregion + + #region Internal Properties + internal Button OKButton => btnOK; internal Button CancelButton => btnCancel; + internal Button ApplyButton => btnApply; + + #endregion #endregion #region Constructors - public OkCancelButtons() - { - InitializeComponent(); - } + public OkCancelButtons() => InitializeComponent(); #endregion #region Methods + protected override void OnLoad(EventArgs e) + { + // Fixing high DPI appearance on Mono + PointF scale; + if (OSUtils.IsMono && (scale = this.GetScale()) != new PointF(1f, 1f)) + { + Height = (int)(35 * scale.Y); + var referenceButtonSize = new Size(75, 23); + OKButton.Size = referenceButtonSize.Scale(scale); + CancelButton.Size = referenceButtonSize.Scale(scale); + ApplyButton.Size = referenceButtonSize.Scale(scale); + } + + base.OnLoad(e); + } + protected override void Dispose(bool disposing) { if (disposing) diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/PreviewImageControl.Designer.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/PreviewImageControl.Designer.cs index 835930c..83e4f56 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/PreviewImageControl.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/PreviewImageControl.Designer.cs @@ -1,4 +1,6 @@ -namespace KGySoft.Drawing.ImagingTools.View.UserControls +using KGySoft.Drawing.ImagingTools.View.Components; + +namespace KGySoft.Drawing.ImagingTools.View.UserControls { partial class PreviewImageControl { @@ -15,34 +17,43 @@ partial class PreviewImageControl /// private void InitializeComponent() { - this.scalingToolStrip1 = new KGySoft.Drawing.ImagingTools.View.Controls.ScalingToolStrip(); - this.btnAutoZoom = new System.Windows.Forms.ToolStripButton(); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(PreviewImageControl)); + this.ivPreview = new KGySoft.Drawing.ImagingTools.View.Controls.ImageViewer(); + this.tsMenu = new KGySoft.Drawing.ImagingTools.View.Controls.AdvancedToolStrip(); + this.btnZoom = new ZoomSplitButton(); this.btnAntiAlias = new System.Windows.Forms.ToolStripButton(); this.btnShowOriginal = new System.Windows.Forms.ToolStripButton(); - this.ivPreview = new KGySoft.Drawing.ImagingTools.View.Controls.ImageViewer(); - this.scalingToolStrip1.SuspendLayout(); + this.tsMenu.SuspendLayout(); this.SuspendLayout(); // - // scalingToolStrip1 + // ivPreview + // + this.ivPreview.Dock = System.Windows.Forms.DockStyle.Fill; + this.ivPreview.Location = new System.Drawing.Point(33, 0); + this.ivPreview.Name = "ivPreview"; + this.ivPreview.Size = new System.Drawing.Size(117, 150); + this.ivPreview.TabIndex = 1; // - this.scalingToolStrip1.Dock = System.Windows.Forms.DockStyle.Left; - this.scalingToolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.btnAutoZoom, + // tsMenu + // + this.tsMenu.Dock = System.Windows.Forms.DockStyle.Left; + this.tsMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.btnZoom, this.btnAntiAlias, this.btnShowOriginal}); - this.scalingToolStrip1.Location = new System.Drawing.Point(0, 0); - this.scalingToolStrip1.Name = "scalingToolStrip1"; - this.scalingToolStrip1.Size = new System.Drawing.Size(32, 150); - this.scalingToolStrip1.TabIndex = 0; - this.scalingToolStrip1.Text = "tsMenu"; + this.tsMenu.Location = new System.Drawing.Point(0, 0); + this.tsMenu.Name = "tsMenu"; + this.tsMenu.Size = new System.Drawing.Size(33, 150); + this.tsMenu.TabIndex = 0; + this.tsMenu.Text = "tsMenu"; // - // btnAutoZoom + // btnZoom // - this.btnAutoZoom.CheckOnClick = true; - this.btnAutoZoom.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; - this.btnAutoZoom.ImageTransparentColor = System.Drawing.Color.Magenta; - this.btnAutoZoom.Name = "btnAutoZoom"; - this.btnAutoZoom.Size = new System.Drawing.Size(29, 4); + this.btnZoom.CheckOnClick = true; + this.btnZoom.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; + this.btnZoom.ImageTransparentColor = System.Drawing.Color.Magenta; + this.btnZoom.Name = "btnZoom"; + this.btnZoom.Size = new System.Drawing.Size(30, 20); // // btnAntiAlias // @@ -50,32 +61,24 @@ private void InitializeComponent() this.btnAntiAlias.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.btnAntiAlias.ImageTransparentColor = System.Drawing.Color.Magenta; this.btnAntiAlias.Name = "btnAntiAlias"; - this.btnAntiAlias.Size = new System.Drawing.Size(29, 4); + this.btnAntiAlias.Size = new System.Drawing.Size(30, 4); // // btnShowOriginal // this.btnShowOriginal.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.btnShowOriginal.ImageTransparentColor = System.Drawing.Color.Magenta; this.btnShowOriginal.Name = "btnShowOriginal"; - this.btnShowOriginal.Size = new System.Drawing.Size(29, 4); - // - // ivPreview - // - this.ivPreview.Dock = System.Windows.Forms.DockStyle.Fill; - this.ivPreview.Location = new System.Drawing.Point(32, 0); - this.ivPreview.Name = "ivPreview"; - this.ivPreview.Size = new System.Drawing.Size(118, 150); - this.ivPreview.TabIndex = 1; + this.btnShowOriginal.Size = new System.Drawing.Size(30, 4); // // PreviewImageControl // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Controls.Add(this.ivPreview); - this.Controls.Add(this.scalingToolStrip1); + this.Controls.Add(this.tsMenu); this.Name = "PreviewImageControl"; - this.scalingToolStrip1.ResumeLayout(false); - this.scalingToolStrip1.PerformLayout(); + this.tsMenu.ResumeLayout(false); + this.tsMenu.PerformLayout(); this.ResumeLayout(false); this.PerformLayout(); @@ -83,8 +86,8 @@ private void InitializeComponent() #endregion - private Controls.ScalingToolStrip scalingToolStrip1; - private System.Windows.Forms.ToolStripButton btnAutoZoom; + private Controls.AdvancedToolStrip tsMenu; + private ZoomSplitButton btnZoom; private System.Windows.Forms.ToolStripButton btnAntiAlias; private Controls.ImageViewer ivPreview; private System.Windows.Forms.ToolStripButton btnShowOriginal; diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/PreviewImageControl.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/PreviewImageControl.cs index 379d31c..564fee9 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/PreviewImageControl.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/PreviewImageControl.cs @@ -17,9 +17,9 @@ #region Usings using System; -using System.ComponentModel; using System.Drawing; using System.Windows.Forms; + using KGySoft.Drawing.ImagingTools.View.Controls; using KGySoft.Drawing.ImagingTools.ViewModel; @@ -31,16 +31,34 @@ internal partial class PreviewImageControl : MvvmBaseUserControl ViewModel.PreviewImage; - set => ViewModel.PreviewImage = value; + get => ViewModel?.PreviewImage; + set + { + if (ViewModel is PreviewImageViewModel vm) + vm.PreviewImage = value; + } } internal bool AutoZoom { - get => ViewModel.AutoZoom; - set => ViewModel.AutoZoom = value; + get => ViewModel?.AutoZoom ?? false; + set + { + if (ViewModel is PreviewImageViewModel vm) + vm.AutoZoom = value; + } + } + + internal bool SmoothZooming + { + get => ViewModel?.SmoothZooming ?? false; + set + { + if (ViewModel is PreviewImageViewModel vm) + vm.SmoothZooming = value; + } } internal ImageViewer ImageViewer => ivPreview; @@ -60,6 +78,12 @@ public PreviewImageControl() #region Protected Methods + protected override void OnLoad(EventArgs e) + { + tsMenu.FixAppearance(); + base.OnLoad(e); + } + protected override void Dispose(bool disposing) { if (disposing) @@ -70,7 +94,6 @@ protected override void Dispose(bool disposing) protected override void ApplyResources() { - btnAutoZoom.Image = Images.Magnifier; btnAntiAlias.Image = Images.SmoothZoom; btnShowOriginal.Image = Images.Compare; base.ApplyResources(); @@ -89,30 +112,33 @@ protected override void ApplyViewModel() private void InitCommandBindings() { - CommandBindings.Add(() => ViewModel.ShowOriginal = true) + CommandBindings.Add(ivPreview.IncreaseZoom) + .AddSource(btnZoom.IncreaseZoomMenuItem, nameof(btnZoom.IncreaseZoomMenuItem.Click)); + CommandBindings.Add(ivPreview.DecreaseZoom) + .AddSource(btnZoom.DecreaseZoomMenuItem, nameof(btnZoom.DecreaseZoomMenuItem.Click)); + CommandBindings.Add(ivPreview.ResetZoom) + .AddSource(btnZoom.ResetZoomMenuItem, nameof(btnZoom.ResetZoomMenuItem.Click)); + CommandBindings.Add(() => ViewModel!.ShowOriginal = true) .AddSource(btnShowOriginal, nameof(btnShowOriginal.MouseDown)); - CommandBindings.Add(() => ViewModel.ShowOriginal = false) + CommandBindings.Add(() => ViewModel!.ShowOriginal = false) .AddSource(btnShowOriginal, nameof(btnShowOriginal.MouseUp)); } private void InitPropertyBindings() { // VM.DisplayImage -> ivPreview.Image - CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.DisplayImage), nameof(ivPreview.Image), ivPreview); - - // VM.ZoomEnabled -> btnAutoZoom/btnAntiAlias.Enabled - CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.ZoomEnabled), nameof(ToolStripItem.Enabled), btnAutoZoom, btnAntiAlias); + CommandBindings.AddPropertyBinding(ViewModel!, nameof(ViewModel.DisplayImage), nameof(ivPreview.Image), ivPreview); // VM.ShowOriginalEnabled -> btnShowOriginal.Enabled - CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.ShowOriginalEnabled), nameof(ToolStripItem.Enabled), btnShowOriginal); + CommandBindings.AddPropertyBinding(ViewModel!, nameof(ViewModel.ShowOriginalEnabled), nameof(ToolStripItem.Enabled), btnShowOriginal); - // btnAutoZoom.Checked <-> VM.AutoZoom -> ivPreview.AutoZoom - CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(ViewModel.AutoZoom), btnAutoZoom, nameof(btnAutoZoom.Checked)); - CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.AutoZoom), nameof(ivPreview.AutoZoom), ivPreview); + // btnAutoZoom.Checked <-> VM.AutoZoom <-> ivPreview.AutoZoom + CommandBindings.AddTwoWayPropertyBinding(ViewModel!, nameof(ViewModel.AutoZoom), btnZoom, nameof(btnZoom.Checked)); + CommandBindings.AddTwoWayPropertyBinding(ViewModel!, nameof(ViewModel.AutoZoom), ivPreview, nameof(ivPreview.AutoZoom)); // btnAntiAlias.Checked <-> VM.SmoothZooming -> ivPreview.SmoothZooming - CommandBindings.AddTwoWayPropertyBinding(ViewModel, nameof(ViewModel.SmoothZooming), btnAntiAlias, nameof(btnAntiAlias.Checked)); - CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.SmoothZooming), nameof(ivPreview.SmoothZooming), ivPreview); + CommandBindings.AddTwoWayPropertyBinding(ViewModel!, nameof(ViewModel.SmoothZooming), btnAntiAlias, nameof(btnAntiAlias.Checked)); + CommandBindings.AddPropertyBinding(ViewModel!, nameof(ViewModel.SmoothZooming), nameof(ivPreview.SmoothZooming), ivPreview); } #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/QuantizerSelectorControl.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/QuantizerSelectorControl.cs index 12f333e..64358e2 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/QuantizerSelectorControl.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/QuantizerSelectorControl.cs @@ -59,14 +59,14 @@ protected override void Dispose(bool disposing) private void InitCommandBindings() { // not for ViewModel.Parameters.PropertyChanged because it is not triggered for expanded properties such as collection elements - CommandBindings.Add(ViewModel.ResetQuantizer) + CommandBindings.Add(ViewModel!.ResetQuantizer) .AddSource(pgParameters, nameof(pgParameters.PropertyValueChanged)); } private void InitPropertyBindings() { // will not change so not as an actual binding - cmbQuantizer.DataSource = ViewModel.Quantizers; + cmbQuantizer.DataSource = ViewModel!.Quantizers; // VM.Parameters -> pgParameters.SelectedObject CommandBindings.AddPropertyBinding(ViewModel, nameof(ViewModel.Parameters), nameof(pgParameters.SelectedObject), pgParameters); diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/QuantizerThresholdEditorControl.Designer.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/QuantizerThresholdEditorControl.Designer.cs index 8f7c3e9..75588c6 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/QuantizerThresholdEditorControl.Designer.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/QuantizerThresholdEditorControl.Designer.cs @@ -28,6 +28,7 @@ private void InitializeComponent() this.trackBar.Location = new System.Drawing.Point(35, 0); this.trackBar.Maximum = 255; this.trackBar.Name = "trackBar"; + this.trackBar.RightToLeftLayout = true; this.trackBar.Size = new System.Drawing.Size(147, 27); this.trackBar.TabIndex = 0; this.trackBar.TickFrequency = 16; @@ -37,7 +38,7 @@ private void InitializeComponent() this.okCancelButtons.Dock = System.Windows.Forms.DockStyle.Bottom; this.okCancelButtons.Location = new System.Drawing.Point(0, 27); this.okCancelButtons.Name = "okCancelButtons"; - this.okCancelButtons.Size = new System.Drawing.Size(182, 40); + this.okCancelButtons.Size = new System.Drawing.Size(182, 35); this.okCancelButtons.TabIndex = 1; // // lblValue diff --git a/KGySoft.Drawing.ImagingTools/View/UserControls/QuantizerThresholdEditorControl.cs b/KGySoft.Drawing.ImagingTools/View/UserControls/QuantizerThresholdEditorControl.cs index 29bc0ef..5250ae7 100644 --- a/KGySoft.Drawing.ImagingTools/View/UserControls/QuantizerThresholdEditorControl.cs +++ b/KGySoft.Drawing.ImagingTools/View/UserControls/QuantizerThresholdEditorControl.cs @@ -17,7 +17,6 @@ #region Usings using System; -using System.Globalization; using System.Windows.Forms; using System.Windows.Forms.Design; @@ -29,7 +28,7 @@ internal partial class QuantizerThresholdEditorControl : BaseUserControl { #region Fields - private readonly IWindowsFormsEditorService editorService; + private readonly IWindowsFormsEditorService? editorService; private readonly byte originalValue; #endregion @@ -52,7 +51,7 @@ internal QuantizerThresholdEditorControl(IWindowsFormsEditorService editorServic trackBar.ValueChanged += TrackBar_ValueChanged; okCancelButtons.CancelButton.Click += CancelButton_Click; okCancelButtons.OKButton.Click += OKButton_Click; - okCancelButtons.ApplyStaticStringResources(); + okCancelButtons.ApplyStringResources(); trackBar.Value = originalValue = value; } @@ -63,6 +62,7 @@ internal QuantizerThresholdEditorControl(IWindowsFormsEditorService editorServic private QuantizerThresholdEditorControl() { + RightToLeft = Res.DisplayLanguage.TextInfo.IsRightToLeft ? RightToLeft.Yes : RightToLeft.No; InitializeComponent(); } @@ -96,21 +96,21 @@ protected override bool ProcessDialogKey(Keys keyData) #region Event handlers - private void TrackBar_ValueChanged(object sender, EventArgs e) + private void TrackBar_ValueChanged(object? sender, EventArgs e) { Value = (byte)trackBar.Value; - lblValue.Text = Value.ToString(CultureInfo.CurrentCulture); + lblValue.Text = Value.ToString(LanguageSettings.FormattingLanguage); } - private void OKButton_Click(object sender, EventArgs e) + private void OKButton_Click(object? sender, EventArgs e) { - editorService.CloseDropDown(); + editorService?.CloseDropDown(); } - private void CancelButton_Click(object sender, EventArgs e) + private void CancelButton_Click(object? sender, EventArgs e) { Value = originalValue; - editorService.CloseDropDown(); + editorService?.CloseDropDown(); } #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/ViewFactory.cs b/KGySoft.Drawing.ImagingTools/View/ViewFactory.cs index 70324d9..a5d0b93 100644 --- a/KGySoft.Drawing.ImagingTools/View/ViewFactory.cs +++ b/KGySoft.Drawing.ImagingTools/View/ViewFactory.cs @@ -17,6 +17,7 @@ #region Usings using System; + using KGySoft.Drawing.ImagingTools.Model; using KGySoft.Drawing.ImagingTools.View.Design; using KGySoft.Drawing.ImagingTools.View.Forms; @@ -53,35 +54,25 @@ public static IView CreateView(IViewModel viewModel) if (viewModel == null) throw new ArgumentNullException(nameof(viewModel), PublicResources.ArgumentNull); - switch (viewModel) + return viewModel switch { - case DefaultViewModel defaultViewModel: - return new AppMainForm(defaultViewModel); - case GraphicsVisualizerViewModel graphicsVisualizerViewModel: - return new GraphicsVisualizerForm(graphicsVisualizerViewModel); - case ImageVisualizerViewModel imageVisualizerViewModel: // also for BitmapData - return new ImageVisualizerForm(imageVisualizerViewModel); - case PaletteVisualizerViewModel paletteVisualizerViewModel: - return new PaletteVisualizerForm(paletteVisualizerViewModel); - case ColorVisualizerViewModel colorVisualizerViewModel: - return new ColorVisualizerForm(colorVisualizerViewModel); - case ManageInstallationsViewModel manageInstallationsViewModel: - return new ManageInstallationsForm(manageInstallationsViewModel); - case ResizeBitmapViewModel resizeBitmapViewModel: - return new ResizeBitmapForm(resizeBitmapViewModel); - case ColorSpaceViewModel colorSpaceViewModel: - return new ColorSpaceForm(colorSpaceViewModel); - case CountColorsViewModel countColorsViewModel: - return new CountColorsForm(countColorsViewModel); - case AdjustBrightnessViewModel adjustBrightnessViewModel: - return new AdjustBrightnessForm(adjustBrightnessViewModel); - case AdjustContrastViewModel adjustContrastViewModel: - return new AdjustContrastForm(adjustContrastViewModel); - case AdjustGammaViewModel adjustGammaViewModel: - return new AdjustGammaForm(adjustGammaViewModel); - default: - throw new InvalidOperationException(Res.InternalError($"Unexpected viewModel type: {viewModel.GetType()}")); - } + DefaultViewModel defaultViewModel => new AppMainForm(defaultViewModel), + GraphicsVisualizerViewModel graphicsVisualizerViewModel => new GraphicsVisualizerForm(graphicsVisualizerViewModel), + ImageVisualizerViewModel imageVisualizerViewModel => new ImageVisualizerForm(imageVisualizerViewModel), // also for BitmapData + PaletteVisualizerViewModel paletteVisualizerViewModel => new PaletteVisualizerForm(paletteVisualizerViewModel), + ColorVisualizerViewModel colorVisualizerViewModel => new ColorVisualizerForm(colorVisualizerViewModel), + ManageInstallationsViewModel manageInstallationsViewModel => new ManageInstallationsForm(manageInstallationsViewModel), + ResizeBitmapViewModel resizeBitmapViewModel => new ResizeBitmapForm(resizeBitmapViewModel), + ColorSpaceViewModel colorSpaceViewModel => new ColorSpaceForm(colorSpaceViewModel), + CountColorsViewModel countColorsViewModel => new CountColorsForm(countColorsViewModel), + AdjustBrightnessViewModel adjustBrightnessViewModel => new AdjustBrightnessForm(adjustBrightnessViewModel), + AdjustContrastViewModel adjustContrastViewModel => new AdjustContrastForm(adjustContrastViewModel), + AdjustGammaViewModel adjustGammaViewModel => new AdjustGammaForm(adjustGammaViewModel), + LanguageSettingsViewModel languageSettingsViewModel => new LanguageSettingsForm(languageSettingsViewModel), + EditResourcesViewModel editResourcesViewModel => new EditResourcesForm(editResourcesViewModel), + DownloadResourcesViewModel downloadResourcesViewModel => new DownloadResourcesForm(downloadResourcesViewModel), + _ => throw new InvalidOperationException(Res.InternalError($"Unexpected viewModel type: {viewModel.GetType()}")) + }; } /// @@ -94,8 +85,21 @@ public static IView CreateView(IViewModel viewModel) /// A view for the specified instance. public static void ShowDialog(IViewModel viewModel, IntPtr ownerWindowHandle = default) { - using (IView view = CreateView(viewModel)) - view.ShowDialog(ownerWindowHandle); + using IView view = CreateView(viewModel); + view.ShowDialog(ownerWindowHandle); + } + + /// + /// Shows an internally created view for the specified instance, + /// which will be discarded when the view is closed. + /// + /// The view model to create the view for. + /// If not , then the created dialog will be owned by the specified instance. + /// A view for the specified instance. + public static void ShowDialog(IViewModel viewModel, IView? owner) + { + using IView view = CreateView(viewModel); + view.ShowDialog(owner); } #endregion diff --git a/KGySoft.Drawing.ImagingTools/View/WinformsCommandBindingsCollection.cs b/KGySoft.Drawing.ImagingTools/View/WinformsCommandBindingsCollection.cs index 7dba35c..25d69db 100644 --- a/KGySoft.Drawing.ImagingTools/View/WinformsCommandBindingsCollection.cs +++ b/KGySoft.Drawing.ImagingTools/View/WinformsCommandBindingsCollection.cs @@ -1,7 +1,7 @@ #region Copyright /////////////////////////////////////////////////////////////////////////////// -// File: WinformsCommandBindingsCollection.cs +// File: WinFormsCommandBindingsCollection.cs /////////////////////////////////////////////////////////////////////////////// // Copyright (C) KGy SOFT, 2005-2020 - All Rights Reserved // @@ -30,11 +30,11 @@ namespace KGySoft.Drawing.ImagingTools.View /// By using this collection the properties (eg. but also any other added property) /// of the added bindings will be synced with the command sources. /// - internal class WinformsCommandBindingsCollection : CommandBindingsCollection + internal class WinFormsCommandBindingsCollection : CommandBindingsCollection { #region Methods - public override ICommandBinding Add(ICommand command, IDictionary initialState = null, bool disposeCommand = false) + public override ICommandBinding Add(ICommand command, IDictionary? initialState = null, bool disposeCommand = false) => base.Add(command, initialState, disposeCommand) .AddStateUpdater(PropertyCommandStateUpdater.Updater); diff --git a/KGySoft.Drawing.ImagingTools/View/_Extensions/ColorExtensions.cs b/KGySoft.Drawing.ImagingTools/View/_Extensions/ColorExtensions.cs new file mode 100644 index 0000000..3b4e96e --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/_Extensions/ColorExtensions.cs @@ -0,0 +1,57 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: ColorExtensions.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System.Drawing; + +using KGySoft.Collections; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View +{ + internal static class ColorExtensions + { + #region Fields + + private static readonly Cache penCache = new(c => new Pen(Color.FromArgb(c)), 4) + { + DisposeDroppedValues = true, + EnsureCapacity = true, + }; + + private static readonly Cache brushCache = new(c => new SolidBrush(Color.FromArgb(c)), 4) + { + DisposeDroppedValues = true, + EnsureCapacity = true, + }; + + #endregion + + #region Methods + + internal static Pen GetPen(this Color color) => color.IsSystemColor + ? SystemPens.FromSystemColor(color) + : penCache[color.ToArgb()]; + + internal static Brush GetBrush(this Color color) => color.IsSystemColor + ? SystemBrushes.FromSystemColor(color) + : brushCache[color.ToArgb()]; + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/_Extensions/ControlExtensions.cs b/KGySoft.Drawing.ImagingTools/View/_Extensions/ControlExtensions.cs index d4155cd..034d1f6 100644 --- a/KGySoft.Drawing.ImagingTools/View/_Extensions/ControlExtensions.cs +++ b/KGySoft.Drawing.ImagingTools/View/_Extensions/ControlExtensions.cs @@ -24,10 +24,26 @@ #endregion +#region Suppressions + +#if NETCOREAPP3_0 +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - Controls/Columns/DropDownItems never have null elements +#pragma warning disable CS8602 // Dereference of a possibly null reference. - Controls/Columns/DropDownItems never have null elements +#pragma warning disable CS8604 // Possible null reference argument. - Controls/Columns/DropDownItems never have null elements +#endif + +#endregion + namespace KGySoft.Drawing.ImagingTools.View { internal static class ControlExtensions { + #region Constants + + internal const string ToolTipPropertyName = "ToolTipText"; + + #endregion + #region Methods /// @@ -51,17 +67,28 @@ internal static PointF GetScale(this Control control) internal static Size ScaleSize(this Control control, Size size) => size.Scale(control.GetScale()); + internal static int ScaleWidth(this Control control, int width) => width.Scale(control.GetScale().X); + internal static int ScaleHeight(this Control control, int height) => height.Scale(control.GetScale().Y); + /// /// Applies fixed string resources (which do not change unless language is changed) to a control. /// - internal static void ApplyStaticStringResources(this Control control) + internal static void ApplyStringResources(this Control control, ToolTip? toolTip = null) { + #region Local Methods + + static void ApplyToolTip(Control control, string name, ToolTip toolTip) + { + string? value = Res.GetStringOrNull(name + "." + ToolTipPropertyName); + toolTip.SetToolTip(control, value); + } + static void ApplyToolStripResources(ToolStripItemCollection items) { foreach (ToolStripItem item in items) { // to self - Res.ApplyResources(item, item.Name); + Res.ApplyStringResources(item, item.Name); // to children if (item is ToolStripDropDownItem dropDownItem) @@ -69,34 +96,64 @@ static void ApplyToolStripResources(ToolStripItemCollection items) } } + #endregion + string name = control.Name; if (String.IsNullOrEmpty(name)) name = control.GetType().Name; + // custom localization + if (control is ICustomLocalizable customLocalizable) + { + customLocalizable.ApplyStringResources(toolTip); + return; + } + // to self - Res.ApplyResources(control, name); + Res.ApplyStringResources(control, name); - // to children - foreach (Control child in control.Controls) child.ApplyStaticStringResources(); + // applying tool tip + if (toolTip != null) + ApplyToolTip(control, name, toolTip); - // to non-control sub-components + // to children switch (control) { case ToolStrip toolStrip: ApplyToolStripResources(toolStrip.Items); break; + + case DataGridView dataGridView: + foreach (DataGridViewColumn item in dataGridView.Columns) + Res.ApplyStringResources(item, item.Name); + break; + + default: + foreach (Control child in control.Controls) + child.ApplyStringResources(toolTip); + break; } } internal static void FixAppearance(this ToolStrip toolStrip) { - static void FixItems(ToolStripItemCollection items, Color replacementColor) + static void FixItems(ToolStripItemCollection items, Color? replacementColor) { foreach (ToolStripItem item in items) { - // to self - if ((item is ToolStripMenuItem || item is ToolStripLabel || item is ToolStripSeparator || item is ToolStripProgressBar) && item.BackColor.ToArgb() == replacementColor.ToArgb()) - item.BackColor = replacementColor; + // fixing closing menu due to the appearing tool tip (only on Mono/Windows) + if (OSUtils.IsWindows && item is ToolStripDropDownButton or ToolStripSplitButton) + { + item.AutoToolTip = false; + item.ToolTipText = null; + } + + // fixing menu color + if (replacementColor.HasValue) + { + if ((item is ToolStripMenuItem || item is ToolStripLabel || item is ToolStripSeparator || item is ToolStripProgressBar) && item.BackColor.ToArgb() == replacementColor.Value.ToArgb()) + item.BackColor = replacementColor.Value; + } // to children if (item is ToolStripDropDownItem dropDownItem) @@ -104,12 +161,14 @@ static void FixItems(ToolStripItemCollection items, Color replacementColor) } } - if (OSUtils.IsWindows || SystemInformation.HighContrast) + if (!OSUtils.IsMono) return; - // fixing "dark on dark" menu issue on Linux - Color replacementColor = Color.FromArgb(ProfessionalColors.MenuStripGradientBegin.ToArgb()); - toolStrip.BackColor = replacementColor; + // fixing "dark on dark" menu issue on Mono/Linux + Color? replacementColor = OSUtils.IsLinux && !SystemInformation.HighContrast ? Color.FromArgb(ProfessionalColors.MenuStripGradientBegin.ToArgb()) : null; + if (replacementColor.HasValue) + toolStrip.BackColor = replacementColor.Value; + FixItems(toolStrip.Items, replacementColor); } diff --git a/KGySoft.Drawing.ImagingTools/View/_Extensions/DrawToolTipEventArgsExtensions.cs b/KGySoft.Drawing.ImagingTools/View/_Extensions/DrawToolTipEventArgsExtensions.cs new file mode 100644 index 0000000..079c363 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/_Extensions/DrawToolTipEventArgsExtensions.cs @@ -0,0 +1,46 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: DrawToolTipEventArgsExtensions.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System.Drawing; +using System.Windows.Forms; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View +{ + internal static class DrawToolTipEventArgsExtensions + { + #region Methods + + internal static void DrawToolTipAdvanced(this DrawToolTipEventArgs e) + { + // Same as DrawBackground but will not recreate the brush again and again + // Note: the background color of this tool tip may differ from default ToolTip but will be the same as the ones on Close/Minimize/Maximize buttons + e.Graphics.FillRectangle(SystemBrushes.Info, e.Bounds); + e.DrawBorder(); + + var flags = TextFormatFlags.HidePrefix | TextFormatFlags.VerticalCenter | TextFormatFlags.LeftAndRightPadding; + if (Res.DisplayLanguage.TextInfo.IsRightToLeft) + flags |= TextFormatFlags.RightToLeft | TextFormatFlags.Right; + + e.DrawText(flags); + } + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/_Extensions/EventHandlerExtensions.cs b/KGySoft.Drawing.ImagingTools/View/_Extensions/EventHandlerExtensions.cs index 6e08a6e..6bdc438 100644 --- a/KGySoft.Drawing.ImagingTools/View/_Extensions/EventHandlerExtensions.cs +++ b/KGySoft.Drawing.ImagingTools/View/_Extensions/EventHandlerExtensions.cs @@ -27,7 +27,7 @@ internal static class EventHandlerExtensions { #region Methods - internal static TDelegate GetHandler(this EventHandlerList handlers, object key) where TDelegate : Delegate => handlers?[key] as TDelegate; + internal static TDelegate? GetHandler(this EventHandlerList? handlers, object key) where TDelegate : Delegate => handlers?[key] as TDelegate; #endregion } diff --git a/KGySoft.Drawing.ImagingTools/View/_Extensions/IntPtrExtensions.cs b/KGySoft.Drawing.ImagingTools/View/_Extensions/IntPtrExtensions.cs index b9604bc..19f65bc 100644 --- a/KGySoft.Drawing.ImagingTools/View/_Extensions/IntPtrExtensions.cs +++ b/KGySoft.Drawing.ImagingTools/View/_Extensions/IntPtrExtensions.cs @@ -1,10 +1,34 @@ -using System; +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: IntPtrExtensions.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; + +#endregion namespace KGySoft.Drawing.ImagingTools.View { internal static class IntPtrExtensions { + #region Methods + internal static int GetSignedLoWord(this IntPtr p) => (short)(p.ToInt64() & 0xFFFF); internal static int GetSignedHiWord(this IntPtr p) => (short)((p.ToInt64() >> 16) & 0xFFFF); + + #endregion } -} +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/_Extensions/ScrollbarExtensions.cs b/KGySoft.Drawing.ImagingTools/View/_Extensions/ScrollbarExtensions.cs new file mode 100644 index 0000000..652a62e --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/_Extensions/ScrollbarExtensions.cs @@ -0,0 +1,41 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: ScrollbarExtensions.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System.Windows.Forms; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View +{ + internal static class ScrollbarExtensions + { + #region Methods + + internal static void SetValueSafe(this ScrollBar scrollBar, int value) + { + if (value < scrollBar.Minimum) + value = scrollBar.Minimum; + else if (value > scrollBar.Maximum - scrollBar.LargeChange + 1) + value = scrollBar.Maximum - scrollBar.LargeChange + 1; + + scrollBar.Value = value; + } + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/_Interfaces/ICustomLocalizable.cs b/KGySoft.Drawing.ImagingTools/View/_Interfaces/ICustomLocalizable.cs new file mode 100644 index 0000000..91f71fd --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/View/_Interfaces/ICustomLocalizable.cs @@ -0,0 +1,33 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: ICustomLocalizable.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System.Windows.Forms; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.View +{ + internal interface ICustomLocalizable + { + #region Methods + + void ApplyStringResources(ToolTip? toolTip); + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/View/_Interfaces/IView.cs b/KGySoft.Drawing.ImagingTools/View/_Interfaces/IView.cs index 712fc0c..def24a9 100644 --- a/KGySoft.Drawing.ImagingTools/View/_Interfaces/IView.cs +++ b/KGySoft.Drawing.ImagingTools/View/_Interfaces/IView.cs @@ -17,7 +17,6 @@ #region Usings using System; -using System.Windows.Forms; #endregion @@ -29,22 +28,37 @@ namespace KGySoft.Drawing.ImagingTools.View /// public interface IView : IDisposable { + #region Properties + + /// + /// Gets whether this view is disposed. + /// + public bool IsDisposed { get; } + + #endregion + + #region Methods + /// /// Shows the view as a modal dialog. + /// When using this overload, do not let the handle of the owner destroyed (some operations such as changing right-to-left may cause the handle recreated). /// /// If specified, then the created dialog will be owned by the window that has specified handle. This parameter is optional. ///
Default value: IntPtr.Zero. void ShowDialog(IntPtr ownerWindowHandle = default); /// - /// Gets whether this view is disposed. + /// Shows the view as a modal dialog. /// - bool IsDisposed { get; } + /// If not , then the created dialog will be owned by the specified instance. + void ShowDialog(IView? owner); /// /// Shows the view as a non-modal window. /// If the view was already shown, then makes it the active window. /// void Show(); + + #endregion } } \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/AdjustBrightnessViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/AdjustBrightnessViewModel.cs index 9f04228..0959f06 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/AdjustBrightnessViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/AdjustBrightnessViewModel.cs @@ -43,9 +43,9 @@ internal GenerateTask(float value, ColorChannels colorChannels) #region Methods internal override IAsyncResult BeginGenerate(AsyncConfig asyncConfig) - => BitmapData.BeginAdjustBrightness(Value, channels: ColorChannels, asyncConfig: asyncConfig); + => BitmapData!.BeginAdjustBrightness(Value, channels: ColorChannels, asyncConfig: asyncConfig); - internal override Bitmap EndGenerate(IAsyncResult asyncResult) + internal override Bitmap? EndGenerate(IAsyncResult asyncResult) { asyncResult.EndAdjustBrightness(); return base.EndGenerate(asyncResult); diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/AdjustColorsViewModelBase.cs b/KGySoft.Drawing.ImagingTools/ViewModel/AdjustColorsViewModelBase.cs index 5bdbc50..0fc7817 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/AdjustColorsViewModelBase.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/AdjustColorsViewModelBase.cs @@ -17,8 +17,8 @@ #region Usings using System; -using System.Diagnostics.CodeAnalysis; using System.Drawing; + using KGySoft.ComponentModel; using KGySoft.CoreLibraries; using KGySoft.Drawing.Imaging; @@ -35,14 +35,14 @@ protected abstract class AdjustColorsTaskBase : GenerateTaskBase { #region Fields - private Bitmap result; + private Bitmap? result; #endregion #region Properties #region Internal Properties - + internal float Value { get; } internal ColorChannels ColorChannels { get; } @@ -50,7 +50,7 @@ protected abstract class AdjustColorsTaskBase : GenerateTaskBase #region Protected Properties - protected IReadWriteBitmapData BitmapData { get; private set; } + protected IReadWriteBitmapData? BitmapData { get; private set; } #endregion @@ -70,8 +70,6 @@ protected AdjustColorsTaskBase(float value, ColorChannels colorChannels) #region Internal Methods - [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", - Justification = "False alarm, source is never a remote object")] internal override void Initialize(Bitmap source, bool isInUse) { lock (source) @@ -79,11 +77,11 @@ internal override void Initialize(Bitmap source, bool isInUse) BitmapData = result.GetReadWriteBitmapData(); } - internal override Bitmap EndGenerate(IAsyncResult asyncResult) + internal override Bitmap? EndGenerate(IAsyncResult asyncResult) { // If there was no exception returning result and clearing the field to prevent disposing. // The caller will take care of disposing if the operation was canceled and the result is discarded. - Bitmap bmp = result; + Bitmap? bmp = result; result = null; return bmp; } @@ -94,7 +92,7 @@ internal override void SetCompleted() BitmapData = null; base.SetCompleted(); } - + #endregion #region Protected Methods @@ -122,9 +120,9 @@ protected override void Dispose(bool disposing) #region Properties #region Internal Properties - + internal ColorChannels ColorChannels { get => Get(ColorChannels.Rgb); set => Set(value); } - internal float Value { get => Get(DefaultValue); set => Set(value); } + internal float Value { get => Get(DefaultValue); set => Set(value); } internal virtual float MinValue => -1f; internal virtual float MaxValue => 1f; diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/AdjustContrastViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/AdjustContrastViewModel.cs index 27bf3fb..6424d51 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/AdjustContrastViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/AdjustContrastViewModel.cs @@ -43,9 +43,9 @@ internal GenerateTask(float value, ColorChannels colorChannels) #region Methods internal override IAsyncResult BeginGenerate(AsyncConfig asyncConfig) - => BitmapData.BeginAdjustContrast(Value, channels: ColorChannels, asyncConfig: asyncConfig); + => BitmapData!.BeginAdjustContrast(Value, channels: ColorChannels, asyncConfig: asyncConfig); - internal override Bitmap EndGenerate(IAsyncResult asyncResult) + internal override Bitmap? EndGenerate(IAsyncResult asyncResult) { asyncResult.EndAdjustContrast(); return base.EndGenerate(asyncResult); diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/AdjustGammaViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/AdjustGammaViewModel.cs index d89e9a2..ec20891 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/AdjustGammaViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/AdjustGammaViewModel.cs @@ -43,9 +43,9 @@ internal GenerateTask(float value, ColorChannels colorChannels) #region Methods internal override IAsyncResult BeginGenerate(AsyncConfig asyncConfig) - => BitmapData.BeginAdjustGamma(Value, channels: ColorChannels, asyncConfig: asyncConfig); + => BitmapData!.BeginAdjustGamma(Value, channels: ColorChannels, asyncConfig: asyncConfig); - internal override Bitmap EndGenerate(IAsyncResult asyncResult) + internal override Bitmap? EndGenerate(IAsyncResult asyncResult) { asyncResult.EndAdjustGamma(); return base.EndGenerate(asyncResult); diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/BitmapDataVisualizerViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/BitmapDataVisualizerViewModel.cs index 2a096b5..2002bf8 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/BitmapDataVisualizerViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/BitmapDataVisualizerViewModel.cs @@ -32,7 +32,7 @@ internal class BitmapDataVisualizerViewModel : ImageVisualizerViewModel #region Internal Properties - internal BitmapDataInfo BitmapDataInfo { get => Get(); set => Set(value); } + internal BitmapDataInfo? BitmapDataInfo { get => Get(); init => Set(value); } #endregion @@ -60,17 +60,17 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) base.OnPropertyChanged(e); if (e.PropertyName == nameof(BitmapDataInfo)) { - var bitmapDataInfo = (BitmapDataInfo)e.NewValue; + var bitmapDataInfo = (BitmapDataInfo?)e.NewValue; Image = bitmapDataInfo?.BackingImage; if ((bitmapDataInfo?.BitmapData?.PixelFormat ?? PixelFormat.Format32bppArgb).ToBitsPerPixel() <= 8) - Notification = Res.NotificationPaletteCannotBeRestored; + SetNotification(Res.NotificationPaletteCannotBeRestoredId); } } protected override void UpdateInfo() { - BitmapDataInfo bitmapDataInfo = BitmapDataInfo; - BitmapData bitmapData = bitmapDataInfo?.BitmapData; + BitmapDataInfo? bitmapDataInfo = BitmapDataInfo; + BitmapData? bitmapData = bitmapDataInfo?.BitmapData; if (bitmapDataInfo?.BackingImage == null || bitmapData == null) { diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/ColorSpaceViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/ColorSpaceViewModel.cs index 6dc8241..ff40bda 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/ColorSpaceViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/ColorSpaceViewModel.cs @@ -18,7 +18,6 @@ using System; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; using System.Linq; @@ -40,7 +39,7 @@ private sealed class GenerateTask : GenerateTaskBase { #region Fields - private Bitmap sourceBitmap; + private Bitmap? sourceBitmap; private bool isSourceCloned; #endregion @@ -48,14 +47,14 @@ private sealed class GenerateTask : GenerateTaskBase #region Properties internal PixelFormat PixelFormat { get; } - internal IQuantizer Quantizer { get; } - internal IDitherer Ditherer { get; } + internal IQuantizer? Quantizer { get; } + internal IDitherer? Ditherer { get; } #endregion #region Constructors - internal GenerateTask(PixelFormat pixelFormat, IQuantizer quantizer, IDitherer ditherer) + internal GenerateTask(PixelFormat pixelFormat, IQuantizer? quantizer, IDitherer? ditherer) { PixelFormat = pixelFormat; Quantizer = quantizer; @@ -66,8 +65,6 @@ internal GenerateTask(PixelFormat pixelFormat, IQuantizer quantizer, IDitherer d #region Methods - [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", - Justification = "False alarm, source is never a remote object")] internal override void Initialize(Bitmap source, bool isInUse) { // Locking on source image to avoid "bitmap region is already locked" if the UI is painting the image when we clone it. @@ -90,19 +87,22 @@ internal override void Initialize(Bitmap source, bool isInUse) } internal override IAsyncResult BeginGenerate(AsyncConfig asyncConfig) - => sourceBitmap.BeginConvertPixelFormat(PixelFormat, Quantizer, Ditherer, asyncConfig); + => sourceBitmap!.BeginConvertPixelFormat(PixelFormat, Quantizer, Ditherer, asyncConfig); - internal override Bitmap EndGenerate(IAsyncResult asyncResult) => asyncResult.EndConvertPixelFormat(); + internal override Bitmap? EndGenerate(IAsyncResult asyncResult) => asyncResult.EndConvertPixelFormat(); internal override void SetCompleted() { - if (isSourceCloned) + if (sourceBitmap != null) { - sourceBitmap?.Dispose(); - sourceBitmap = null; + if (isSourceCloned) + { + sourceBitmap.Dispose(); + sourceBitmap = null; + } + else + Monitor.Exit(sourceBitmap); } - else - Monitor.Exit(sourceBitmap); base.SetCompleted(); } @@ -187,10 +187,10 @@ protected override ValidationResultsCollection DoValidation() bool useQuantizer = UseQuantizer; bool useDitherer = UseDitherer; PixelFormat pixelFormat = changePixelFormat ? PixelFormat : originalPixelFormat; - IQuantizer quantizer = useQuantizer ? QuantizerSelectorViewModel.Quantizer : null; - IDitherer ditherer = useDitherer ? DithererSelectorViewModel.Ditherer : null; - Exception quantizerError = useQuantizer ? QuantizerSelectorViewModel.CreateQuantizerError : null; - Exception dithererError = useDitherer ? DithererSelectorViewModel.CreateDithererError : null; + IQuantizer? quantizer = useQuantizer ? QuantizerSelectorViewModel.Quantizer : null; + IDitherer? ditherer = useDitherer ? DithererSelectorViewModel.Ditherer : null; + Exception? quantizerError = useQuantizer ? QuantizerSelectorViewModel.CreateQuantizerError : null; + Exception? dithererError = useDitherer ? DithererSelectorViewModel.CreateDithererError : null; int bpp = pixelFormat.ToBitsPerPixel(); int originalBpp = originalPixelFormat.ToBitsPerPixel(); int? bppHint = quantizer?.PixelFormatHint.ToBitsPerPixel(); @@ -202,7 +202,7 @@ protected override ValidationResultsCollection DoValidation() if (quantizerError != null) result.AddError(nameof(QuantizerSelectorViewModel.Quantizer), Res.ErrorMessageFailedToInitializeQuantizer(quantizerError.Message)); else if (bppHint <= 8 && bppHint > bpp) - result.AddError(nameof(QuantizerSelectorViewModel.Quantizer), Res.ErrorMessageQuantizerPaletteTooLarge(pixelFormat, quantizer.PixelFormatHint, 1 << bpp)); + result.AddError(nameof(QuantizerSelectorViewModel.Quantizer), Res.ErrorMessageQuantizerPaletteTooLarge(pixelFormat, quantizer!.PixelFormatHint, 1 << bpp)); if (dithererError != null) result.AddError(nameof(DithererSelectorViewModel.Ditherer), Res.ErrorMessageFailedToInitializeDitherer(dithererError.Message)); @@ -215,7 +215,7 @@ protected override ValidationResultsCollection DoValidation() result.AddWarning(nameof(PixelFormat), Res.WarningMessageWideConversionLoss(originalPixelFormat)); if (bppHint > bpp) - result.AddWarning(nameof(QuantizerSelectorViewModel.Quantizer), Res.WarningMessageQuantizerTooWide(pixelFormat, quantizer.PixelFormatHint)); + result.AddWarning(nameof(QuantizerSelectorViewModel.Quantizer), Res.WarningMessageQuantizerTooWide(pixelFormat, quantizer!.PixelFormatHint)); if (bppHint == 32 && ditherer != null) result.AddWarning(nameof(DithererSelectorViewModel.Ditherer), Res.WarningMessageDithererNoAlphaGradient); @@ -224,15 +224,15 @@ protected override ValidationResultsCollection DoValidation() if (changePixelFormat && pixelFormat == originalPixelFormat) result.AddInfo(nameof(PixelFormat), Res.InfoMessageSamePixelFormat); if (bppHint < bpp) - result.AddInfo(nameof(PixelFormat), Res.InfoMessagePixelFormatUnnecessarilyWide(quantizer.PixelFormatHint)); + result.AddInfo(nameof(PixelFormat), Res.InfoMessagePixelFormatUnnecessarilyWide(quantizer!.PixelFormatHint)); if (!useQuantizer) { if (bpp <= 8 && bpp < originalBpp) result.AddInfo(nameof(QuantizerSelectorViewModel.Quantizer), Res.InfoMessagePaletteAutoSelected(1 << bpp, pixelFormat)); - else if (ditherer != null && pixelFormat.CanBeDithered()) + else if (changePixelFormat && ditherer != null && pixelFormat.CanBeDithered() && !(bpp <= 8 && bpp >= originalBpp)) result.AddInfo(nameof(QuantizerSelectorViewModel.Quantizer), Res.InfoMessageQuantizerAutoSelected(pixelFormat)); - else if (!useDitherer && originalHasAlpha && !pixelFormat.HasAlpha()) + else if (changePixelFormat && !useDitherer && originalHasAlpha && !pixelFormat.HasAlpha()) result.AddInfo(nameof(QuantizerSelectorViewModel.Quantizer), Res.InfoMessageAlphaTurnsBlack); } else if (bppHint > originalBpp) @@ -240,9 +240,9 @@ protected override ValidationResultsCollection DoValidation() else if (bppHint == 32 && originalBpp >= 32 && !originalPixelFormat.HasAlpha()) result.AddInfo(nameof(QuantizerSelectorViewModel.Quantizer), Res.InfoMessageArgbQuantizerHasNoEffect); - if (bppHint < originalBpp && !useDitherer && quantizer.PixelFormatHint.CanBeDithered()) + if (bppHint < originalBpp && !useDitherer && quantizer!.PixelFormatHint.CanBeDithered()) { - if (QuantizerSelectorViewModel.SelectedQuantizer.Method.Name != nameof(PredefinedColorsQuantizer.Grayscale)) + if (QuantizerSelectorViewModel.SelectedQuantizer!.Method.Name != nameof(PredefinedColorsQuantizer.Grayscale)) result.AddInfo(nameof(DithererSelectorViewModel.Ditherer), Res.InfoMessageQuantizerCanBeDithered(originalPixelFormat)); } else if (bpp < originalBpp && !useDitherer && pixelFormat.CanBeDithered()) @@ -282,8 +282,8 @@ protected override void Dispose(bool disposing) if (disposing) { // These disposals remove every subscriptions as well - QuantizerSelectorViewModel?.Dispose(); - DithererSelectorViewModel?.Dispose(); + QuantizerSelectorViewModel.Dispose(); + DithererSelectorViewModel.Dispose(); } base.Dispose(disposing); @@ -297,7 +297,7 @@ protected override void Dispose(bool disposing) #region Event Handlers - private void Selector_PropertyChanged(object sender, PropertyChangedEventArgs e) + private void Selector_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName.In(nameof(QuantizerSelectorViewModel.Quantizer), nameof(QuantizerSelectorViewModel.CreateQuantizerError), nameof(DithererSelectorViewModel.Ditherer), nameof(DithererSelectorViewModel.CreateDithererError))) diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/ColorVisualizerViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/ColorVisualizerViewModel.cs index b30a8d0..a191432 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/ColorVisualizerViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/ColorVisualizerViewModel.cs @@ -17,6 +17,7 @@ #region Usings using System.Drawing; +using KGySoft.ComponentModel; #endregion @@ -33,8 +34,18 @@ internal class ColorVisualizerViewModel : ViewModelBase, IViewModel #region Methods + #region Public Methods + public Color GetEditedModel() => Color; #endregion + + #region Protected Methods + + protected override void ApplyDisplayLanguage() => OnPropertyChanged(new PropertyChangedExtendedEventArgs(Color, Color, nameof(Color))); + + #endregion + + #endregion } } \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/CountColorsViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/CountColorsViewModel.cs index 66f8ea4..5f2286c 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/CountColorsViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/CountColorsViewModel.cs @@ -17,7 +17,6 @@ #region Usings using System; -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Threading; @@ -39,7 +38,7 @@ private sealed class CountTask : AsyncTaskBase { #region Fields - internal Bitmap Bitmap; + internal Bitmap Bitmap = default!; #endregion } @@ -51,18 +50,20 @@ private sealed class CountTask : AsyncTaskBase #region Fields private readonly DrawingProgressManager drawingProgressManager; + private readonly Bitmap bitmap; - private volatile CountTask task; + private volatile CountTask? activeTask; private int? colorCount; + private string displayTextId = default!; + private object[]? displayTextArgs; #endregion #region Properties - internal object ProgressSyncRoot => drawingProgressManager; internal bool IsProcessing { get => Get(); set => Set(value); } internal DrawingProgress Progress { get => Get(); set => Set(value); } - internal string DisplayText { get => Get(() => Res.TextCountingColors); set => Set(value); } + internal string DisplayText { get => Get(); set => Set(value); } internal ICommand CancelCommand => Get(() => new SimpleCommand(OnCancelCommand)); @@ -72,14 +73,9 @@ private sealed class CountTask : AsyncTaskBase internal CountColorsViewModel(Bitmap bitmap) { - if (bitmap == null) - throw new ArgumentNullException(nameof(bitmap), PublicResources.ArgumentNull); - drawingProgressManager = new DrawingProgressManager(p => - { - lock (ProgressSyncRoot) - Progress = p; - }); - BeginCountColors(bitmap); + this.bitmap = bitmap ?? throw new ArgumentNullException(nameof(bitmap), PublicResources.ArgumentNull); + SetDisplayText(Res.TextCountingColorsId); + drawingProgressManager = new DrawingProgressManager(p => Progress = p); } #endregion @@ -90,7 +86,7 @@ internal CountColorsViewModel(Bitmap bitmap) public int? GetEditedModel() { - task.WaitForCompletion(); + activeTask?.WaitForCompletion(); return colorCount; } @@ -100,9 +96,13 @@ internal CountColorsViewModel(Bitmap bitmap) internal void CancelIfRunning() { - task.IsCanceled = true; + CountTask? t = activeTask; + if (t == null) + return; + + t.IsCanceled = true; SetModified(false); - task.WaitForCompletion(); + t.WaitForCompletion(); } #endregion @@ -111,68 +111,96 @@ internal void CancelIfRunning() protected override bool AffectsModifiedState(string propertyName) => false; + internal override void ViewLoaded() + { + base.ViewLoaded(); + BeginCountColors(); + } + + protected override void ApplyDisplayLanguage() => UpdateDisplayText(); + #endregion #region Private Methods - private void BeginCountColors(Bitmap bitmap) + private void BeginCountColors() { IsProcessing = true; - task = new CountTask { Bitmap = bitmap }; - ThreadPool.QueueUserWorkItem(DoCountColors); + activeTask = new CountTask { Bitmap = bitmap }; + ThreadPool.QueueUserWorkItem(DoCountColors, activeTask); } - [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", Justification = "False alarm, task.Bitmap is not a remote object")] - private void DoCountColors(object state) + private void DoCountColors(object? state) { - Exception error = null; + Exception? error = null; + var task = (CountTask)state!; - // We must lock on the image to avoid the possible "bitmap region is already in use" error from the Paint of main view's image viewer, - // which also locks on the image to help avoiding this error - lock (task.Bitmap) + try { - IReadableBitmapData bitmapData = null; - try + // We must lock on the image to avoid the possible "bitmap region is already in use" error from the Paint of main view's image viewer, + // which also locks on the image to help avoiding this error + lock (task.Bitmap) { - bitmapData = task.Bitmap.GetReadableBitmapData(); - IAsyncResult asyncResult = bitmapData.BeginGetColorCount(new AsyncConfig + IReadableBitmapData? bitmapData = null; + try { - IsCancelRequestedCallback = () => task.IsCanceled, - ThrowIfCanceled = false, - Progress = drawingProgressManager - }); - - // Waiting to be finished or canceled. As we are on a different thread blocking wait is alright - colorCount = asyncResult.EndGetColorCount(); - } - catch (Exception e) when (!e.IsCriticalGdi()) - { - error = e; - } - finally - { - bitmapData?.Dispose(); - task.SetCompleted(); + bitmapData = task.Bitmap.GetReadableBitmapData(); + IAsyncResult asyncResult = bitmapData.BeginGetColorCount(new AsyncConfig + { + IsCancelRequestedCallback = () => task.IsCanceled, + ThrowIfCanceled = false, + Progress = drawingProgressManager + }); + + // Waiting to be finished or canceled. As we are on a different thread blocking wait is alright + colorCount = asyncResult.EndGetColorCount(); + } + catch (Exception e) + { + error = e; + } + finally + { + bitmapData?.Dispose(); + task.SetCompleted(); + } } - } - if (task.IsCanceled) - colorCount = null; + if (task.IsCanceled) + colorCount = null; - SetModified(colorCount.HasValue); + // returning if task was canceled because cancel closes the UI + if (colorCount.HasValue) + SetModified(true); + else + return; - // the execution of this method will be marshaled back to the UI thread - void Action() + // applying result (or error) + TryInvokeSync(() => + { + if (error != null) + SetDisplayText(Res.ErrorMessageId, error.Message); + else + SetDisplayText(Res.TextColorCountId, colorCount.Value); + IsProcessing = false; + }); + } + finally { - DisplayText = error != null ? Res.ErrorMessage(error.Message) - : colorCount == null ? Res.TextOperationCanceled - : Res.TextColorCount(colorCount.Value); - IsProcessing = false; + task.Dispose(); + activeTask = null; } + } - SynchronizedInvokeCallback?.Invoke(Action); + private void SetDisplayText(string resourceId, params object[] args) + { + displayTextId = resourceId; + displayTextArgs = args; + UpdateDisplayText(); } + private void UpdateDisplayText() => DisplayText = Res.Get(displayTextId, displayTextArgs); + #endregion #region Command Handlers diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/DefaultViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/DefaultViewModel.cs index 5009112..c9cfd84 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/DefaultViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/DefaultViewModel.cs @@ -16,8 +16,8 @@ #region Usings -using System; using System.IO; + using KGySoft.CoreLibraries; using KGySoft.Drawing.ImagingTools.Model; @@ -31,8 +31,8 @@ internal class DefaultViewModel : ImageVisualizerViewModel #region Internal Properties - internal string[] CommandLineArguments { get => Get(); set => Set(value); } - internal string FileName { get => Get(); set => Set(value); } + internal string[]? CommandLineArguments { get => Get(); init => Set(value); } + internal string? FileName { get => Get(); set => Set(value); } #endregion @@ -44,21 +44,25 @@ internal class DefaultViewModel : ImageVisualizerViewModel #endregion + #region Constructors + + internal DefaultViewModel() => SmoothZooming = true; + + #endregion + #region Methods #region Internal Methods internal override void ViewLoaded() { - string[] args = CommandLineArguments; - if (!args.IsNullOrEmpty()) - ProcessArgs(CommandLineArguments); - else + string[]? args = CommandLineArguments; + if (args.IsNullOrEmpty() || !ProcessArgs(args!)) UpdateInfo(); base.ViewLoaded(); } - internal bool ConfirmIfModified() => !IsModified || Confirm(Res.ConfirmMessageDiscardChanges); + internal bool ConfirmIfModified() => !IsModified || Confirm(Res.ConfirmMessageDiscardChanges, false); #endregion @@ -125,15 +129,18 @@ protected override void Clear() #region Private Methods - private void ProcessArgs(string[] args) + private bool ProcessArgs(string[] args) { - if (args.IsNullOrEmpty()) - return; + if (args.Length == 0) + return false; string file = args[0]; if (!File.Exists(file)) + { ShowError(Res.ErrorMessageFileDoesNotExist(file)); - else - OpenFile(file); + return false; + } + + return OpenFile(file); } #endregion diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/DithererSelectorViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/DithererSelectorViewModel.cs index e8642fa..7d91efe 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/DithererSelectorViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/DithererSelectorViewModel.cs @@ -18,10 +18,9 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics; -using System.Drawing; using System.Reflection; + using KGySoft.ComponentModel; using KGySoft.Drawing.Imaging; using KGySoft.Drawing.ImagingTools.Model; @@ -37,10 +36,10 @@ internal class DithererSelectorViewModel : ViewModelBase // not a static property so always can be reinitialized with the current language internal IList Ditherers => Get(InitDitherers); - internal DithererDescriptor SelectedDitherer { get => Get(); private set => Set(value); } - internal CustomPropertiesObject Parameters { get => Get(); private set => Set(value); } - internal IDitherer Ditherer { get => Get(); private set => Set(value); } - internal Exception CreateDithererError { get => Get(); set => Set(value); } + internal DithererDescriptor? SelectedDitherer { get => Get(); private set => Set(value); } + internal CustomPropertiesObject? Parameters { get => Get(); private set => Set(value); } + internal IDitherer? Ditherer { get => Get(); private set => Set(value); } + internal Exception? CreateDithererError { get => Get(); set => Set(value); } #endregion @@ -70,8 +69,8 @@ private static IList InitDitherers() => new DithererDescriptor(typeof(ErrorDiffusionDitherer), nameof(ErrorDiffusionDitherer.StevensonArce)), new DithererDescriptor(typeof(ErrorDiffusionDitherer), nameof(ErrorDiffusionDitherer.Stucki)), - new DithererDescriptor(typeof(RandomNoiseDitherer).GetConstructor(new[] { typeof(float), typeof(int?) })), - new DithererDescriptor(typeof(InterleavedGradientNoiseDitherer).GetConstructor(new[] { typeof(float) })), + new DithererDescriptor(typeof(RandomNoiseDitherer).GetConstructor(new[] { typeof(float), typeof(int?) })!), + new DithererDescriptor(typeof(InterleavedGradientNoiseDitherer).GetConstructor(new[] { typeof(float) })!), }; #endregion @@ -82,16 +81,17 @@ private static IList InitDitherers() => internal void ResetDitherer() { - DithererDescriptor descriptor = SelectedDitherer; + DithererDescriptor? descriptor = SelectedDitherer; + CustomPropertiesObject? parameters = Parameters; CreateDithererError = null; - if (descriptor == null) + + if (descriptor == null || parameters == null) { Ditherer = null; return; } - IDitherer ditherer = null; - CustomPropertiesObject parameterValues = Parameters; + IDitherer? ditherer = null; try { foreach (MemberInfo memberInfo in descriptor.InvokeChain) @@ -100,17 +100,17 @@ internal void ResetDitherer() { case ConstructorInfo ctor: Debug.Assert(ditherer == null); - ditherer = (IDitherer)CreateInstanceAccessor.GetAccessor(ctor).CreateInstance(descriptor.EvaluateParameters(ctor.GetParameters(), parameterValues)); + ditherer = (IDitherer)CreateInstanceAccessor.GetAccessor(ctor).CreateInstance(descriptor.EvaluateParameters(ctor.GetParameters(), parameters)); break; case PropertyInfo property: - Debug.Assert(ditherer == null && property.GetGetMethod().IsStatic); - ditherer = (IDitherer)PropertyAccessor.GetAccessor(property).Get(null); + Debug.Assert(ditherer == null && property.GetGetMethod()!.IsStatic); + ditherer = (IDitherer)PropertyAccessor.GetAccessor(property).Get(null)!; break; case MethodInfo method: Debug.Assert(ditherer != null && !method.IsStatic); - ditherer = (IDitherer)MethodAccessor.GetAccessor(method).Invoke(ditherer, descriptor.EvaluateParameters(method.GetParameters(), parameterValues)); + ditherer = (IDitherer)MethodAccessor.GetAccessor(method).Invoke(ditherer, descriptor.EvaluateParameters(method.GetParameters(), parameters))!; break; default: @@ -134,19 +134,18 @@ internal void ResetDitherer() protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) { base.OnPropertyChanged(e); - if (e.PropertyName == nameof(SelectedDitherer)) + switch (e.PropertyName) { - CustomPropertiesObject previousParameters = Parameters; - Parameters = previousParameters == null - ? new CustomPropertiesObject(SelectedDitherer.Parameters) - : new CustomPropertiesObject(previousParameters, SelectedDitherer.Parameters); - return; - } - - if (e.PropertyName == nameof(Parameters)) - { - ResetDitherer(); - return; + case nameof(SelectedDitherer): + CustomPropertiesObject? previousParameters = Parameters; + Parameters = previousParameters == null + ? new CustomPropertiesObject(SelectedDitherer!.Parameters) + : new CustomPropertiesObject(previousParameters, SelectedDitherer!.Parameters); + return; + + case nameof(Parameters): + ResetDitherer(); + return; } } @@ -154,7 +153,6 @@ protected override void Dispose(bool disposing) { if (IsDisposed) return; - CustomPropertiesObject parameters = Parameters; base.Dispose(disposing); } diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/DownloadResourcesViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/DownloadResourcesViewModel.cs new file mode 100644 index 0000000..1510d73 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/ViewModel/DownloadResourcesViewModel.cs @@ -0,0 +1,456 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: DownloadResourcesViewModel.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Xml; + +using KGySoft.ComponentModel; +using KGySoft.CoreLibraries; +using KGySoft.Drawing.ImagingTools.Model; +using KGySoft.Resources; +using KGySoft.Serialization.Xml; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.ViewModel +{ + internal class DownloadResourcesViewModel : ViewModelBase, IViewModel> + { + #region Nested classes + + #region DownloadManifestTask class + + private class DownloadTask : AsyncTaskBase + { + #region Fields + + internal Uri Uri = default!; + + #endregion + } + + #endregion + + #region DownloadResourcesTask class + + private sealed class DownloadResourcesTask : DownloadTask + { + #region Fields + + internal List Files = default!; + internal bool Overwrite; + + #endregion + } + + #endregion + + #region DownloadInfo class + + private sealed class DownloadInfo + { + #region Fields + + private readonly string remotePath; + + #endregion + + #region Properties + + internal LocalizationInfo Info { get; } + internal string FileName { get; } + internal string RemoteUri => $"{remotePath}/{FileName}"; + internal string LocalPath => Path.Combine(Res.ResourcesDir, FileName); + + #endregion + + #region Constructors + + public DownloadInfo(LocalizationInfo info, LocalizableLibraries library) + { + Info = info; + FileName = info.CultureName == Res.DefaultLanguage.Name + ? ResHelper.GetBaseName(library) + ".resx" + : $"{ResHelper.GetBaseName(library)}.{info.CultureName}.resx"; + remotePath = $"{info.CultureName}_{info.Author}_{info.ImagingToolsVersion}"; + } + + #endregion + } + + #endregion + + #endregion + + #region Fields + + private readonly List availableResources = new List(); + private readonly HashSet downloadedCultures = new HashSet(); + + private volatile AsyncTaskBase? activeTask; + + #endregion + + #region Properties + + internal DownloadableResourceItemCollection? Items { get => Get(); set => Set(value); } + internal bool IsProcessing { get => Get(); set => Set(value); } + internal (int MaximumValue, int CurrentValue) Progress { get => Get<(int, int)>(); set => Set(value); } + + internal ICommand CancelCommand => Get(() => new SimpleCommand(OnCancelCommand)); + internal ICommand DownloadCommand => Get(() => new SimpleCommand(OnDownloadCommand)); + + internal ICommandState DownloadCommandState => Get(() => new CommandState { Enabled = false }); + + #endregion + + #region Methods + + #region Internal Methods + + internal void CancelIfRunning() + { + AsyncTaskBase? t = activeTask; + if (t == null) + return; + + t.IsCanceled = true; + SetModified(false); + t.WaitForCompletion(); + } + + #endregion + + #region Protected Methods + + protected override bool AffectsModifiedState(string propertyName) => false; + + internal override void ViewLoaded() + { + base.ViewLoaded(); + try + { + BeginDownloadManifest(); + } + catch (Exception e) when (!e.IsCritical()) + { + ShowError(Res.ErrorMessageCouldNotAccessOnlineResources(e.Message)); + CloseViewCallback?.Invoke(); + } + } + + protected override void Dispose(bool disposing) + { + if (IsDisposed) + return; + + if (disposing) + Items?.Dispose(); + + base.Dispose(disposing); + } + + #endregion + + #region Private Methods + + private void BeginDownloadManifest() + { + IsProcessing = true; + activeTask = new DownloadTask { Uri = new Uri(Configuration.BaseUri, "manifest.xml") }; + ThreadPool.QueueUserWorkItem(DoDownloadManifest!, activeTask); + } + + private void DoDownloadManifest(object state) + { + var task = (DownloadTask)state!; + try + { + if (task.IsCanceled) + return; + + byte[]? data = Download(task); + if (data == null) + return; + + using var reader = XmlReader.Create(new StreamReader(new MemoryStream(data), Encoding.UTF8)); + reader.ReadStartElement("manifest"); + List itemsList = availableResources; + XmlSerializer.DeserializeContent(reader, itemsList); + + TryInvokeSync(() => + { + ResetItems(); + IsProcessing = false; + }); + } + catch (Exception e) + { + TryInvokeSync(() => + { + ShowError(Res.ErrorMessageCouldNotAccessOnlineResources(e.Message)); + CloseViewCallback?.Invoke(); + }); + } + finally + { + task.Dispose(); + activeTask = null; + } + } + + private void ResetItems() + { + DownloadableResourceItemCollection? oldItems = Items; + var items = new DownloadableResourceItemCollection(availableResources); + if (oldItems?.IsSorted == true) + items.ApplySort(oldItems.SortProperty!, ((IBindingList)oldItems).SortDirection); + + items.ListChanged += Items_ListChanged; + Items = items; + DownloadCommandState.Enabled = false; + oldItems?.Dispose(); + } + + private void BeginDownloadResources(List toDownload, bool overwrite) + { + DownloadCommandState.Enabled = false; + IsProcessing = true; + activeTask = new DownloadResourcesTask { Files = toDownload, Overwrite = overwrite }; + ThreadPool.QueueUserWorkItem(DoDownloadResources!, activeTask); + } + + private void DoDownloadResources(object state) + { + var task = (DownloadResourcesTask)state!; + string current = null!; + try + { + if (task.IsCanceled) + return; + + // x3: 2 for download (retrieving response + downloading content), 1 for saving the file + Progress = (task.Files.Count * 3, 0); + int downloaded = 0; + + if (!Directory.Exists(Res.ResourcesDir)) + Directory.CreateDirectory(Res.ResourcesDir); + + foreach (DownloadInfo downloadInfo in task.Files) + { + current = downloadInfo.FileName; + if (task.IsCanceled) + return; + + if (!task.Overwrite && File.Exists(downloadInfo.LocalPath)) + { + IncrementProgress(3); + continue; + } + + // downloading in memory first + task.Uri = new Uri(Configuration.BaseUri, downloadInfo.RemoteUri); + byte[]? data = Download(task); + if (data == null) + return; + + // If there was no issue with downloading, then saving the file. + // Using StreamReader/Writer instead of File.WriteAllBytes so the newlines are adjusted to the current platform + using var reader = new StreamReader(new MemoryStream(data), Encoding.UTF8); + using var writer = File.CreateText(downloadInfo.LocalPath); + while (!reader.EndOfStream) + writer.WriteLine(reader.ReadLine()); + + downloadedCultures.Add(downloadInfo.Info); + IncrementProgress(); + downloaded += 1; + SetModified(true); + } + + TryInvokeSync(() => + { + IsProcessing = false; + if (downloadedCultures.Count > 0) + ApplyResources(); + if (downloadedCultures.All(i => ResHelper.TryGetCulture(i.CultureName, out var _))) + ShowInfo(Res.InfoMessageDownloadCompleted(downloaded)); + else + ShowWarning(Res.WarningMessageDownloadCompletedWithUnsupportedCultures(downloaded)); + CloseViewCallback?.Invoke(); + }); + } + catch (Exception e) + { + // not clearing the downloadedCultures because those files are removed + TryInvokeSync(() => + { + IsProcessing = false; + if (downloadedCultures.Count > 0) + ApplyResources(); + DownloadCommandState.Enabled = Items?.Any(i => i.Selected) == true; + ShowError(Res.ErrorMessageFailedToDownloadResource(current, e.Message)); + }); + } + finally + { + task.Dispose(); + activeTask = null; + } + } + + private void ApplyResources() + { + ResHelper.ReleaseAllResources(); + CultureInfo current = Res.DisplayLanguage; + if (downloadedCultures.All(c => c.CultureName != current.Name)) + return; + + // The current language is among the downloaded ones: applying it + LanguageSettings.DynamicResourceManagersSource = ResourceManagerSources.CompiledAndResX; + Res.OnDisplayLanguageChanged(); + } + + protected override void ApplyDisplayLanguage() => ResetItems(); + + /// + /// Returns the downloaded content, or null if task was canceled. + /// Always increments 2 in progress: 1 for response, 1 for the downloaded content. + /// + private byte[]? Download(DownloadTask task) + { + // Not using WebClient and its async methods because we are already on a separate thread + // and this way we can use the same thread for multiple files, too. + var request = WebRequest.Create(task.Uri); + using WebResponse response = request.GetResponse(); + if (task.IsCanceled) + return null; + + // We do not use the file size in the progress because + // 1.) We work with small files + // 2.) When we download more files we can't set the maximum value for all of the files + IncrementProgress(); + using Stream? src = response.GetResponseStream(); + if (src == null) + return null; + + int len = (int)response.ContentLength; + var result = new byte[len]; + int offset = 0; + int count; + while ((count = src.Read(result, offset, result.Length - offset)) > 0) + { + if (task.IsCanceled) + return null; + offset += count; + } + + IncrementProgress(); + return result; + } + + private void IncrementProgress(int value = 1) + { + var current = Progress; + Progress = (current.MaximumValue, current.CurrentValue + value); + } + + #endregion + + #region Explicitly Implemented Interface Methods + + ICollection IViewModel>.GetEditedModel() => downloadedCultures; + + #endregion + + #region Event Handlers + + private void Items_ListChanged(object sender, ListChangedEventArgs e) + { + var items = (DownloadableResourceItemCollection)sender; + if (e.ListChangedType != ListChangedType.ItemChanged || e.PropertyDescriptor?.Name != nameof(DownloadableResourceItem.Selected)) + return; + + DownloadCommandState.Enabled = items[e.NewIndex].Selected || items.Any(i => i.Selected); + } + + #endregion + + #region Command Handlers + + private void OnCancelCommand() + { + CancelIfRunning(); + CloseViewCallback?.Invoke(); + } + + private void OnDownloadCommand() + { + var toDownload = new List(); + var existingFiles = new List(); + bool ignoreVersionMismatch = false; + Version selfVersion = InstallationManager.ImagingToolsVersion; + + foreach (DownloadableResourceItem item in Items!) + { + if (!item.Selected) + continue; + + if (!ignoreVersionMismatch && !selfVersion.NormalizedEquals(item.Info.Version)) + { + if (Confirm(Res.ConfirmMessageResourceVersionMismatch, false)) + ignoreVersionMismatch = true; + else + return; + } + + LocalizationInfo info = item.Info; + foreach (LocalizableLibraries lib in info.ResourceSets.GetFlags(false)) + { + var file = new DownloadInfo(info, lib); + toDownload.Add(file); + if (File.Exists(file.LocalPath)) + existingFiles.Add(file.FileName); + } + } + + bool overwrite = false; + if (existingFiles.Count > 0) + { + bool? confirmResult = CancellableConfirm(Res.ConfirmMessageOverwriteResources(existingFiles.Join(Environment.NewLine)), 1); + if (confirmResult == null) + return; + + overwrite = confirmResult.Value; + } + + BeginDownloadResources(toDownload, overwrite); + } + + #endregion + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/DownloadableResourceItem.cs b/KGySoft.Drawing.ImagingTools/ViewModel/DownloadableResourceItem.cs new file mode 100644 index 0000000..f5dc6d2 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/ViewModel/DownloadableResourceItem.cs @@ -0,0 +1,92 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: DownloadableResourceItem.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System.Globalization; + +using KGySoft.ComponentModel; +using KGySoft.Drawing.ImagingTools.Model; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.ViewModel +{ + internal class DownloadableResourceItem : ObservableObjectBase, IValidatingObject + { + #region Fields + + private string? language; + private ValidationResultsCollection? validationResults; + + #endregion + + #region Properties + + #region Public Properties + + public bool Selected { get => Get(); set => Set(value); } + + // Note: this could be also observable and we could subscribe language change just to adjust unsupported culture + // on-the-fly but this will never happen unless using this class from API. Subscribing language change from every item + // just for updating the possible unsupported cultures is not worth it. + public string Language => language ??= ResHelper.TryGetCulture(CultureName, out CultureInfo? culture) + ? $"{culture.EnglishName} ({culture.NativeName})" + : Res.TextUnsupportedCulture(CultureName); + + public string? Author => Info.Author; + public string ImagingToolsVersion => Info.ImagingToolsVersion.ToString(); + public string? Description => Info.Description; + + // We could just derive from ValidatingObjectBase but we the validation does not depend on property change we provide a lightweight implementation + public bool IsValid => true; + public ValidationResultsCollection ValidationResults => validationResults ??= CreateValidationResults(); + + #endregion + + #region Internal Properties + + internal LocalizationInfo Info { get; } + + internal string CultureName => Info.CultureName; + + #endregion + + #endregion + + #region Constructors + + internal DownloadableResourceItem(LocalizationInfo info) => Info = info; + + #endregion + + #region Methods + + private ValidationResultsCollection CreateValidationResults() + { + var result = new ValidationResultsCollection(); + + if (!InstallationManager.ImagingToolsVersion.NormalizedEquals(Info.Version)) + result.AddInfo(nameof(ImagingToolsVersion), Res.InfoMessageResourceVersionMismatch); + if (!ResHelper.TryGetCulture(CultureName, out var _)) + result.AddWarning(nameof(Language), Res.WarningMessageUnsupportedCulture); + + return result; + } + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/DownloadableResourceItemCollection.cs b/KGySoft.Drawing.ImagingTools/ViewModel/DownloadableResourceItemCollection.cs new file mode 100644 index 0000000..9a06bd8 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/ViewModel/DownloadableResourceItemCollection.cs @@ -0,0 +1,79 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: DownloadableResourceItemCollection.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System.Collections.Generic; +using System.ComponentModel; + +using KGySoft.Collections; +using KGySoft.ComponentModel; +using KGySoft.Drawing.ImagingTools.Model; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.ViewModel +{ + internal class DownloadableResourceItemCollection : SortableBindingList + { + #region Fields + + private readonly StringKeyedDictionary> langGroups; + + #endregion + + #region Constructors + + internal DownloadableResourceItemCollection(ICollection collection) : base(new List(collection.Count)) + { + langGroups = new StringKeyedDictionary>(); + foreach (LocalizationInfo info in collection) + { + var item = new DownloadableResourceItem(info); + item.PropertyChanged += Item_PropertyChanged!; + + if (langGroups.TryGetValue(info.CultureName, out List? group)) + group.Add(item); + else + langGroups[info.CultureName] = new List(1) { item }; + + Add(item); + } + + ApplySort(nameof(DownloadableResourceItem.Language), ListSortDirection.Ascending); + } + + #endregion + + #region Methods + + private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + var changedItem = (DownloadableResourceItem)sender; + if (e.PropertyName != nameof(DownloadableResourceItem.Selected) || !changedItem.Selected) + return; + + // deselecting other items of the same language as if they belonged to the same radio group + foreach (DownloadableResourceItem item in langGroups[((DownloadableResourceItem)sender).CultureName]) + { + if (item != sender && item.Selected) + item.Selected = false; + } + } + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/EditResourcesViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/EditResourcesViewModel.cs new file mode 100644 index 0000000..8d538c5 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/ViewModel/EditResourcesViewModel.cs @@ -0,0 +1,417 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: EditResourcesViewModel.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + + +#region Used Namespaces + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Resources; + +using KGySoft.ComponentModel; +using KGySoft.CoreLibraries; +using KGySoft.Drawing.ImagingTools.Model; +using KGySoft.Reflection; +using KGySoft.Resources; + +#endregion + +#region Used Aliases + +using ResXResourceSet = KGySoft.Resources.ResXResourceSet; + +#endregion + +#endregion + +#region Suppressions + +#if NETCOREAPP3_0 +#pragma warning disable CS8605 // Unboxing a possibly null value. - false alarm for iterating through a non-generic dictionary +#endif + +#endregion + +namespace KGySoft.Drawing.ImagingTools.ViewModel +{ + internal class EditResourcesViewModel : ViewModelBase + { + #region Constants + + #region Internal Constants + + internal const string StateSaveExecutedWithError = nameof(StateSaveExecutedWithError); + + #endregion + + #region Private Constants + + private const CompareOptions cultureSpecificCompareOptions = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth; + + #endregion + + #endregion + + #region Fields + + private readonly CultureInfo culture; + private readonly bool useInvariant; + private readonly Dictionary ResourceSet, bool IsModified)> resources; + + #endregion + + #region Properties + + internal KeyValuePair[] ResourceFiles { get; } // get only because never changes + internal string TitleCaption { get => Get(); set => Set(value); } + internal LocalizableLibraries SelectedLibrary { get => Get(); set => Set(value); } + internal string Filter { get => Get(""); set => Set(value); } + internal IList? FilteredSet { get => Get?>(); set => Set(value); } + + internal ICommand ApplyResourcesCommand => Get(() => new SimpleCommand(OnApplyResourcesCommand)); + internal ICommand SaveResourcesCommand => Get(() => new SimpleCommand(OnSaveResourcesCommand)); + internal ICommand CancelEditCommand => Get(() => new SimpleCommand(OnCancelEditCommand)); + + internal ICommandState ApplyResourcesCommandState => Get(() => new CommandState()); + + internal Action? SaveConfigurationCallback { get; set; } + + #endregion + + #region Constructors + + internal EditResourcesViewModel(CultureInfo culture) + { + this.culture = culture ?? throw new ArgumentNullException(nameof(culture), PublicResources.ArgumentNull); + + // The default language is used as the invariant resource set. + // The invariant file name is preferred, unless only the language-specific file exists. + useInvariant = Equals(culture, Res.DefaultLanguage) && !File.Exists(ToFileNameWithPath(LocalizableLibraries.ImagingTools)); + resources = new Dictionary, bool)>(3, EnumComparer.Comparer); + Set(String.Empty, false, nameof(Filter)); + ResourceFiles = Enum.GetFlags().Select(lib => new KeyValuePair(lib, ToFileName(lib))).ToArray(); + ApplyResourcesCommandState.Enabled = !Equals(Res.DisplayLanguage, culture); + UpdateTitle(); + SelectedLibrary = LocalizableLibraries.ImagingTools; + } + + #endregion + + #region Methods + + #region Protected Methods + + protected override bool AffectsModifiedState(string propertyName) => false; // set explicitly + + protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) + { + base.OnPropertyChanged(e); + switch (e.PropertyName) + { + case nameof(SelectedLibrary): + case nameof(Filter): + ApplySelection(); + break; + } + } + + protected override void ApplyDisplayLanguage() + { + UpdateTitle(); + ApplyResourcesCommandState.Enabled = false; + } + + protected override void Dispose(bool disposing) + { + if (IsDisposed) + return; + + if (disposing) + { + resources.Values.ForEach(v => (v.ResourceSet as IDisposable)?.Dispose()); + (FilteredSet as IDisposable)?.Dispose(); + } + + SaveConfigurationCallback = null; + base.Dispose(disposing); + } + + #endregion + + #region Private Methods + + private void UpdateTitle() => TitleCaption = Res.TitleEditResources($"{culture.EnglishName} ({culture.NativeName})"); + + private string ToFileName(LocalizableLibraries library) => useInvariant + ? ResHelper.GetBaseName(library) + ".resx" + : $"{ResHelper.GetBaseName(library)}.{culture.Name}.resx"; + + private string ToFileNameWithPath(LocalizableLibraries library) => Path.Combine(Res.ResourcesDir, ToFileName(library)); + + private void ApplySelection() + { + LocalizableLibraries library = SelectedLibrary; + + if (resources.TryGetValue(library, out var value)) + { + ApplyFilter(value.ResourceSet); + return; + } + + if (!TryReadResources(library, out IList? set, out Exception? error)) + { + if (!Confirm(Res.ConfirmMessageTryRegenerateResource(ToFileName(library), error.Message))) + { + ApplyFilter(Reflector.EmptyArray()); + return; + } + + try + { + File.Delete(ToFileNameWithPath(library)); + } + catch (Exception e) when (!e.IsCritical()) + { + ShowError(Res.ErrorMessageFailedToRegenerateResource(ToFileName(library), error.Message)); + ApplyFilter(Reflector.EmptyArray()); + return; + } + + if (!TryReadResources(library, out set, out error)) + { + ShowError(Res.ErrorMessageFailedToRegenerateResource(ToFileName(library), error.Message)); + ApplyFilter(Reflector.EmptyArray()); + return; + } + } + + resources[library] = (set, false); + ApplyFilter(set); + } + +#if NETCOREAPP3_0_OR_GREATER + [SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", + Justification = "Cannot use Contains because it is not available on all targeted platforms")] +#endif + private void ApplyFilter(IList set) + { + if (FilteredSet is SortableBindingList oldSet) + oldSet.ListChanged -= FilteredSet_ListChanged; + + if (set.Count == 0) + { + FilteredSet = set; + return; + } + + Debug.Assert(set is SortableBindingList, "Non-empty set is expected to be a SortableBindingList"); + string filter = Filter.StripAccents(); + SortableBindingList newSet; + if (filter.Length == 0) + newSet = (SortableBindingList)set; + else + { + newSet = new SortableBindingList(new List()); + CompareInfo cultureSpecificInfo = culture.CompareInfo; + CompareInfo invariantInfo = CultureInfo.InvariantCulture.CompareInfo; + foreach (ResourceEntry entry in set) + { + // Using ordinal search for key, invariant for original text (to allow ignoring char width, for example), + // and both ordinal and culture-specific search for the translated text because culture specific fails to match some patterns, + // eg.: "Vissza" is not found with the search term "viss" using the Hungarian culture. + // Stripping is because IgnoreNonSpace fails with some accents, eg. "ö" matches "o" using German culture but does not match with Hungarian. + string strippedTranslated; + if (entry.Key.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0 + || invariantInfo.IndexOf(entry.OriginalText, filter, cultureSpecificCompareOptions) >= 0 + || (strippedTranslated = entry.TranslatedText.StripAccents()).IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0 + || cultureSpecificInfo.IndexOf(strippedTranslated, filter, cultureSpecificCompareOptions) >= 0) + { + newSet.Add(entry); + } + } + } + + newSet.ListChanged += FilteredSet_ListChanged; + ApplySort(newSet); + FilteredSet = newSet; + } + + private bool TryReadResources(LocalizableLibraries library, [MaybeNullWhen(false)]out IList set, [MaybeNullWhen(true)]out Exception error) + { + try + { + // Creating a local resource manager so we can generate the entries that currently found in the compiled resource set. + // Auto appending only the queried keys so we can add the missing ones since the last creation and also remove the possibly removed ones. + // Note that this will not generate any .resx files as we use the default AutoSave = None + using var resourceManger = new DynamicResourceManager(ResHelper.GetBaseName(library), ResHelper.GetAssembly(library)) + { + SafeMode = true, + Source = ResourceManagerSources.CompiledOnly, + AutoAppend = AutoAppendOptions.AppendFirstNeutralCulture, + }; + + ResourceSet compiled = resourceManger.GetResourceSet(CultureInfo.InvariantCulture, true, false)!; + resourceManger.Source = ResourceManagerSources.CompiledAndResX; + + // Note: this way we add even possibly missing entries that were added to compiled resources since last creation while removed entries will be skipped + var result = new SortableBindingList(); + foreach (DictionaryEntry entry in compiled) + result.Add(new ResourceEntry((string)entry.Key, + (string)entry.Value!, + resourceManger.GetString((string)entry.Key, culture) ?? LanguageSettings.UntranslatedResourcePrefix + (string)entry.Value!)); + + error = null; + set = result; + return true; + } + catch (Exception e) when (!e.IsCritical()) + { + error = e; + set = null; + return false; + } + } + + private void ApplySort(SortableBindingList set) + { + var hint = FilteredSet as IBindingList; + ListSortDirection direction = hint?.IsSorted == true ? hint.SortDirection : ListSortDirection.Ascending; + PropertyDescriptor? sortProperty = hint?.SortProperty; + + if (sortProperty != null) + set.ApplySort(sortProperty, direction); + else + set.ApplySort(nameof(ResourceEntry.Key), direction); + } + + private bool TrySaveResources(LocalizableLibraries library, IList set, [MaybeNullWhen(true)]out Exception error) + { + // Note: We do not use a DynamicResourceManager for saving. This works because we let the actual DRMs drop their content after saving. + try + { + using var resx = new ResXResourceSet(); + foreach (ResourceEntry res in set) + resx.SetObject(res.Key, res.TranslatedText); + + string fileName = ToFileNameWithPath(library); + string dirName = Path.GetDirectoryName(fileName)!; + if (!Directory.Exists(dirName)) + Directory.CreateDirectory(dirName); + resx.Save(fileName); + error = null; + return true; + } + catch (Exception e) when (!e.IsCritical()) + { + error = e; + return false; + } + } + + private bool TrySaveResources() + { + foreach (var set in resources) + { + if (!set.Value.IsModified) + continue; + if (TrySaveResources(set.Key, set.Value.ResourceSet, out Exception? error)) + continue; + + ShowError(Res.ErrorMessageFailedToSaveResource(ToFileName(set.Key), error.Message)); + return false; + } + + return true; + } + + private void ApplyResources() + { + // As a first step, we save the configuration explicitly. + // Otherwise, it would be possible to select a language without applying it, then editing the resources and applying the new language here, + // in which case the configuration may remain unsaved. + SaveConfigurationCallback?.Invoke(); + LanguageSettings.DynamicResourceManagersSource = ResourceManagerSources.CompiledAndResX; + if (Equals(Res.DisplayLanguage, culture)) + Res.OnDisplayLanguageChanged(); + else + Res.DisplayLanguage = culture; + SetModified(false); + } + + #endregion + + #region Event Handlers + + private void FilteredSet_ListChanged(object sender, ListChangedEventArgs e) + { + if (!IsViewLoaded || e.ListChangedType != ListChangedType.ItemChanged || e.PropertyDescriptor?.Name != nameof(ResourceEntry.TranslatedText)) + return; + + ApplyResourcesCommandState.Enabled = true; + LocalizableLibraries library = SelectedLibrary; + if (resources.TryGetValue(library, out var value) && !value.IsModified) + resources[library] = (value.ResourceSet, true); + + SetModified(true); + } + + + #endregion + + #region Command Handlers + + private void OnApplyResourcesCommand(ICommandState state) + { + Debug.Assert(IsModified || !Equals(culture, Res.DisplayLanguage)); + ResHelper.ReleaseAllResources(); + bool success = TrySaveResources(); + if (success) + ApplyResources(); + } + + private void OnSaveResourcesCommand(ICommandState state) + { + bool success = true; + if (IsModified) + { + ResHelper.ReleaseAllResources(); + success = TrySaveResources(); + if (success) + { + if (Equals(Res.DisplayLanguage, culture)) + ApplyResources(); + } + } + + state[StateSaveExecutedWithError] = !success; + } + + private void OnCancelEditCommand() => SetModified(false); + + #endregion + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/GraphicsVisualizerViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/GraphicsVisualizerViewModel.cs index c0dee60..449683d 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/GraphicsVisualizerViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/GraphicsVisualizerViewModel.cs @@ -31,18 +31,12 @@ namespace KGySoft.Drawing.ImagingTools.ViewModel { internal class GraphicsVisualizerViewModel : ImageVisualizerViewModel { - #region Fields - - private bool viewInitialized; - - #endregion - #region Properties - internal GraphicsInfo GraphicsInfo { get => Get(); set => Set(value); } + internal GraphicsInfo? GraphicsInfo { get => Get(); init => Set(value); } internal bool Crop { get => Get(); set => Set(value); } - internal bool HighlightVisibleClip { get => Get(true); set => Set(value); } - internal Action DrawFocusRectangleCallback { get => Get>(); set => Set(value); } + internal bool HighlightVisibleClip { get => Get(true); set => Set(value); } + internal Action? DrawFocusRectangleCallback { get => Get?>(); set => Set(value); } internal ICommandState CropCommandState => Get(() => new CommandState()); internal ICommandState HighlightVisibleClipCommandState => Get(() => new CommandState()); @@ -72,17 +66,10 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) UpdateImageAndCommands(); } - internal override void ViewLoaded() - { - viewInitialized = true; - UpdateGraphicImage(); - base.ViewLoaded(); - } - protected override void UpdateInfo() { - GraphicsInfo graphicsInfo = GraphicsInfo; - Matrix transform = graphicsInfo?.Transform; + GraphicsInfo? graphicsInfo = GraphicsInfo; + Matrix? transform = graphicsInfo?.Transform; if (graphicsInfo?.GraphicsImage == null || transform == null) { TitleCaption = Res.TitleNoImage; @@ -142,28 +129,26 @@ protected override void Dispose(bool disposing) private void UpdateImageAndCommands() { UpdateGraphicImage(); - GraphicsInfo graphicsInfo = GraphicsInfo; - Bitmap backingImage = graphicsInfo?.GraphicsImage; - bool commandsEnabled = backingImage != null && (backingImage.Size != graphicsInfo.OriginalVisibleClipBounds.Size || graphicsInfo.OriginalVisibleClipBounds.Location != Point.Empty); + GraphicsInfo? graphicsInfo = GraphicsInfo; + Bitmap? backingImage = graphicsInfo?.GraphicsImage; + bool commandsEnabled = backingImage != null && (backingImage.Size != graphicsInfo!.OriginalVisibleClipBounds.Size || graphicsInfo.OriginalVisibleClipBounds.Location != Point.Empty); CropCommandState.Enabled = HighlightVisibleClipCommandState.Enabled = commandsEnabled; } private void UpdateGraphicImage() { - if (!viewInitialized) - return; - GraphicsInfo graphicsInfo = GraphicsInfo; - Bitmap backingImage = graphicsInfo?.GraphicsImage; + GraphicsInfo? graphicsInfo = GraphicsInfo; + Bitmap? backingImage = graphicsInfo?.GraphicsImage; if (backingImage == null) return; - Rectangle visibleRect = graphicsInfo.OriginalVisibleClipBounds; + Rectangle visibleRect = graphicsInfo!.OriginalVisibleClipBounds; if (Crop && (visibleRect.Size != backingImage.Size || visibleRect.Location != Point.Empty)) { if (visibleRect.Width <= 0 || visibleRect.Height <= 0) return; - Bitmap newImage = new Bitmap(visibleRect.Width, visibleRect.Height); + var newImage = new Bitmap(visibleRect.Width, visibleRect.Height); using (Graphics g = Graphics.FromImage(newImage)) g.DrawImage(backingImage, new Rectangle(Point.Empty, visibleRect.Size), visibleRect, GraphicsUnit.Pixel); @@ -173,7 +158,7 @@ private void UpdateGraphicImage() if (HighlightVisibleClip && (visibleRect.Size != backingImage.Size || visibleRect.Location != Point.Empty)) { - Bitmap newImage = new Bitmap(backingImage); + var newImage = new Bitmap(backingImage); using (Graphics g = Graphics.FromImage(newImage)) { using (Brush b = new SolidBrush(Color.FromArgb(128, Color.Black))) diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/ImageVisualizerViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/ImageVisualizerViewModel.cs index 0750d09..ddc6d66 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/ImageVisualizerViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/ImageVisualizerViewModel.cs @@ -23,6 +23,10 @@ using System.Drawing.Imaging; using System.IO; using System.Linq; +using System.Reflection; +#if !NET35 +using System.Runtime.Versioning; +#endif using System.Text; using KGySoft.ComponentModel; @@ -34,7 +38,7 @@ namespace KGySoft.Drawing.ImagingTools.ViewModel { - internal class ImageVisualizerViewModel : ViewModelBase, IViewModel, IViewModel, IViewModel, IViewModel, IViewModel + internal class ImageVisualizerViewModel : ViewModelBase, IViewModel, IViewModel, IViewModel, IViewModel, IViewModel { #region Constants @@ -59,10 +63,12 @@ internal class ImageVisualizerViewModel : ViewModelBase, IViewModel, private readonly AllowedImageTypes imageTypes; private ImageInfo imageInfo = new ImageInfo(ImageInfoType.None); + private bool keepAliveImageInfo; private int currentFrame = -1; private bool isOpenFilterUpToDate; private Size currentResolution; - private bool deferSettingCompoundStateImage; + private bool deferUpdateInfo; + private string? notificationId; #endregion @@ -72,47 +78,48 @@ internal class ImageVisualizerViewModel : ViewModelBase, IViewModel, #region Internal Properties - internal Image Image + internal Image? Image { get => imageInfo.GetCreateImage(); set => SetImageInfo(new ImageInfo(value)); } - internal Icon Icon + internal Icon? Icon { get => imageInfo.Icon; set => SetImageInfo(new ImageInfo(value)); } + [AllowNull] internal ImageInfo ImageInfo { get => imageInfo; set => SetImageInfo(value ?? new ImageInfo(ImageInfoType.None)); } - internal Image PreviewImage { get => Get(); set => Set(value); } + internal Image? PreviewImage { get => Get(); set => Set(value); } internal bool ReadOnly { get => Get(); set => Set(value); } - internal string TitleCaption { get => Get(); set => Set(value); } - internal string InfoText { get => Get(); set => Set(value); } - internal string Notification { get => Get(); set => Set(value); } + internal string? TitleCaption { get => Get(); set => Set(value); } + internal string? InfoText { get => Get(); set => Set(value); } + internal string? Notification { get => Get(); private set => Set(value); } internal bool AutoZoom { get => Get(); set => Set(value); } internal float Zoom { get => Get(1f); set => Set(value); } internal bool SmoothZooming { get => Get(); set => Set(value); } internal bool IsCompoundView { get => Get(true); set => Set(value); } internal bool IsAutoPlaying { get => Get(); set => Set(value); } - internal string OpenFileFilter { get => Get(); set => Set(value); } - internal string SaveFileFilter { get => Get(); set => Set(value); } + internal string? OpenFileFilter { get => Get(); set => Set(value); } + internal string? SaveFileFilter { get => Get(); set => Set(value); } internal int SaveFileFilterIndex { get => Get(); set => Set(value); } - internal string SaveFileDefaultExtension { get => Get(); set => Set(value); } + internal string? SaveFileDefaultExtension { get => Get(); set => Set(value); } - internal Func GetScreenRectangleCallback { get => Get>(); set => Set(value); } - internal Func GetViewSizeCallback { get => Get>(); set => Set(value); } - internal Func GetImagePreviewSizeCallback { get => Get>(); set => Set(value); } - internal Action ApplyViewSizeCallback { get => Get>(); set => Set(value); } - internal Func SelectFileToOpenCallback { get => Get>(); set => Set(value); } - internal Func SelectFileToSaveCallback { get => Get>(); set => Set(value); } - internal Action UpdatePreviewImageCallback { get => Get(); set => Set(value); } - internal Func GetCompoundViewIconCallback { get => Get>(); set => Set(value); } + internal Func? GetScreenRectangleCallback { get => Get?>(); set => Set(value); } + internal Func? GetViewSizeCallback { get => Get?>(); set => Set(value); } + internal Func? GetImagePreviewSizeCallback { get => Get?>(); set => Set(value); } + internal Action? ApplyViewSizeCallback { get => Get?>(); set => Set(value); } + internal Func? SelectFileToOpenCallback { get => Get?>(); set => Set(value); } + internal Func? SelectFileToSaveCallback { get => Get?>(); set => Set(value); } + internal Action? UpdatePreviewImageCallback { get => Get(); set => Set(value); } + internal Func? GetCompoundViewIconCallback { get => Get?>(); set => Set(value); } internal ICommandState SetAutoZoomCommandState => Get(() => new CommandState()); internal ICommandState SetSmoothZoomingCommandState => Get(() => new CommandState()); @@ -139,6 +146,7 @@ internal ImageInfo ImageInfo internal ICommand NextImageCommand => Get(() => new SimpleCommand(OnNextImageCommand)); internal ICommand ShowPaletteCommand => Get(() => new SimpleCommand(OnShowPaletteCommand)); internal ICommand ManageInstallationsCommand => Get(() => new SimpleCommand(OnManageInstallationsCommand)); + internal ICommand SetLanguageCommand => Get(() => new SimpleCommand(OnSetLanguageCommand)); internal ICommand RotateLeftCommand => Get(() => new SimpleCommand(OnRotateLeftCommand)); internal ICommand RotateRightCommand => Get(() => new SimpleCommand(OnRotateRightCommand)); internal ICommand ResizeBitmapCommand => Get(() => new SimpleCommand(OnResizeBitmapCommand)); @@ -147,6 +155,12 @@ internal ImageInfo ImageInfo internal ICommand AdjustBrightnessCommand => Get(() => new SimpleCommand(OnAdjustBrightnessCommand)); internal ICommand AdjustContrastCommand => Get(() => new SimpleCommand(OnAdjustContrastCommand)); internal ICommand AdjustGammaCommand => Get(() => new SimpleCommand(OnAdjustGammaCommand)); + internal ICommand ShowAboutCommand => Get(() => new SimpleCommand(OnShowAboutCommand)); + internal ICommand VisitWebSiteCommand => Get(() => new SimpleCommand(() => Process.Start("https://kgysoft.net"))); + internal ICommand VisitGitHubCommand => Get(() => new SimpleCommand(() => Process.Start("https://github.com/koszeggy/KGySoft.Drawing.Tools"))); + internal ICommand VisitMarketplaceCommand => Get(() => new SimpleCommand(() => Process.Start("https://marketplace.visualstudio.com/items?itemName=KGySoft.drawing-debugger-visualizers"))); + internal ICommand SubmitResourcesCommand => Get(() => new SimpleCommand(() => Process.Start("https://github.com/koszeggy/KGySoft.Drawing.Tools/issues/new?assignees=&labels=&template=submit-resources.md&title=%5BRes%5D"))); + internal ICommand ShowEasterEggCommand => Get(() => new SimpleCommand(() => ShowInfo(Res.InfoMessageEasterEgg))); #endregion @@ -201,9 +215,7 @@ private static string RawFormatToString(Guid imageFormat) return Res.InfoUnknownFormat(imageFormat); } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", - Justification = "Not disposing reader because stream must be left open and the constructor with leaveOpen parameter is not available for every platform")] - private static bool TryLoadCustom(MemoryStream stream, out Image image) + private static bool TryLoadCustom(MemoryStream stream, [MaybeNullWhen(false)]out Image image) { const int bdatHeader = 0x54414442; // "BDAT" image = null; @@ -232,8 +244,13 @@ private static bool TryLoadCustom(MemoryStream stream, out Image image) internal override void ViewLoaded() { InitAutoZoom(true); - if (deferSettingCompoundStateImage && SetCompoundViewCommandState.GetValueOrDefault(stateVisible)) - SetCompoundViewCommandStateImage(); + if (deferUpdateInfo) + { + if (SetCompoundViewCommandState.GetValueOrDefault(stateVisible)) + SetCompoundViewCommandStateImage(); + UpdateIfMultiResImage(); + } + base.ViewLoaded(); } @@ -269,7 +286,7 @@ protected virtual void UpdateInfo() } ImageInfoBase currentImage = GetCurrentImage(); - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); sb.Append(Res.TitleType(GetTypeName())); if (!imageInfo.IsMetafile) { @@ -299,14 +316,20 @@ protected virtual void UpdateInfo() protected virtual void OpenFile() { SetOpenFilter(); - string fileName = SelectFileToOpenCallback?.Invoke(); + string? fileName = SelectFileToOpenCallback?.Invoke(); if (fileName == null) return; - Notification = null; + SetNotification(null); OpenFile(fileName); } + protected void SetNotification(string? resourceId) + { + notificationId = resourceId; + UpdateNotification(); + } + protected virtual bool OpenFile(string path) { try @@ -324,45 +347,45 @@ protected virtual bool OpenFile(string path) protected virtual bool SaveFile(string fileName, string selectedFormat) { - ImageCodecInfo encoder = encoderCodecs.FirstOrDefault(e => e.FilenameExtension.Equals(selectedFormat, StringComparison.OrdinalIgnoreCase)); + ImageCodecInfo? encoder = encoderCodecs.FirstOrDefault(e => selectedFormat.Equals(e.FilenameExtension, StringComparison.OrdinalIgnoreCase)); try { // BMP if (encoder?.FormatID == ImageFormat.Bmp.Guid) - GetCurrentImage().Image.SaveAsBmp(fileName); + GetCurrentImage().Image!.SaveAsBmp(fileName); // JPEG else if (encoder?.FormatID == ImageFormat.Jpeg.Guid) - GetCurrentImage().Image.SaveAsJpeg(fileName, 95); + GetCurrentImage().Image!.SaveAsJpeg(fileName, 95); // GIF else if (encoder?.FormatID == ImageFormat.Gif.Guid) - GetCurrentImage().Image.SaveAsGif(fileName); + GetCurrentImage().Image!.SaveAsGif(fileName); // Tiff else if (encoder?.FormatID == ImageFormat.Tiff.Guid) { if (imageInfo.HasFrames && IsCompoundView) { using (Stream stream = File.Create(fileName)) - imageInfo.Frames.Select(f => f.Image).SaveAsMultipageTiff(stream); + imageInfo.Frames!.Select(f => f.Image!).SaveAsMultipageTiff(stream); } else - GetCurrentImage().Image.SaveAsTiff(fileName); + GetCurrentImage().Image!.SaveAsTiff(fileName); } // PNG else if (encoder?.FormatID == ImageFormat.Png.Guid) - GetCurrentImage().Image.SaveAsPng(fileName); + GetCurrentImage().Image!.SaveAsPng(fileName); // icon else if (selectedFormat == "*.ico") SaveIcon(fileName); // windows metafile else if (selectedFormat == "*.wmf" && imageInfo.IsMetafile) - ((Metafile)imageInfo.Image).SaveAsWmf(fileName); + ((Metafile)imageInfo.Image!).SaveAsWmf(fileName); // enhanced metafile else if (selectedFormat == "*.emf" && imageInfo.IsMetafile) - ((Metafile)imageInfo.Image).SaveAsEmf(fileName); + ((Metafile)imageInfo.Image!).SaveAsEmf(fileName); // Some unrecognized encoder - we assume it can handle every pixel format else if (encoder != null) - GetCurrentImage().Image.Save(fileName, encoder, null); + GetCurrentImage().Image!.Save(fileName, encoder, null); else if (selectedFormat == "*.bdat") SaveBitmapData(fileName); else @@ -383,9 +406,19 @@ protected virtual void Clear() SetModified(IsDebuggerVisualizer); } + protected override void ApplyDisplayLanguage() + { + isOpenFilterUpToDate = false; + UpdateSmoothZoomingTooltip(); + UpdateNotification(); + UpdateInfo(); + if (imageInfo.HasFrames) + UpdateCompoundToolTip(); + } + protected override void Dispose(bool disposing) { - if (disposing) + if (disposing && !keepAliveImageInfo) imageInfo.Dispose(); base.Dispose(disposing); } @@ -396,7 +429,6 @@ protected override void Dispose(bool disposing) private void SetImageInfo(ImageInfo value) { - Debug.Assert(value != null); ValidateImageInfo(value); currentResolution = Size.Empty; @@ -443,23 +475,24 @@ private void InitSingleImage() private void InitMultiImage() { - SetCompoundViewCommandState[stateToolTipText] = imageInfo.Type switch - { - ImageInfoType.Pages => Res.TooltipTextCompoundMultiPage, - ImageInfoType.Animation => Res.TooltipTextCompoundAnimation, - _ => Res.TooltipTextCompoundMultiSize - }; - + UpdateCompoundToolTip(); SetCompoundViewCommandStateImage(); SetCompoundViewCommandState[stateVisible] = true; ResetCompoundState(); } + private void UpdateCompoundToolTip() => SetCompoundViewCommandState[stateToolTipText] = imageInfo.Type switch + { + ImageInfoType.Pages => Res.TooltipTextCompoundMultiPage, + ImageInfoType.Animation => Res.TooltipTextCompoundAnimation, + _ => Res.TooltipTextCompoundMultiSize + }; + private ImageInfoBase GetCurrentImage() { if (!imageInfo.HasFrames || currentFrame < 0 || IsAutoPlaying) return imageInfo; - return imageInfo.Frames[currentFrame]; + return imageInfo.Frames![currentFrame]; } private bool IsSingleImageShown() => imageInfo.Type != ImageInfoType.None && !imageInfo.HasFrames @@ -467,10 +500,10 @@ private bool IsSingleImageShown() => imageInfo.Type != ImageInfoType.None && !im private void SetCompoundViewCommandStateImage() { - Func callback = GetCompoundViewIconCallback; - deferSettingCompoundStateImage = callback == null; + Func? callback = GetCompoundViewIconCallback; + deferUpdateInfo |= callback == null; if (callback != null) - SetCompoundViewCommandState[stateImage] = callback?.Invoke(imageInfo.Type); + SetCompoundViewCommandState[stateImage] = callback.Invoke(imageInfo.Type); } private void ImageChanged() @@ -499,14 +532,14 @@ private string GetFrameInfo(bool singleLine) if (!imageInfo.HasFrames) return String.Empty; - StringBuilder result = new StringBuilder(); + var result = new StringBuilder(); if (singleLine) result.Append("; "); if (currentFrame != -1 && !IsAutoPlaying) - result.Append(Res.InfoCurrentFrame(currentFrame + 1, imageInfo.Frames.Length)); + result.Append(Res.InfoCurrentFrame(currentFrame + 1, imageInfo.Frames!.Length)); else - result.Append(Res.InfoFramesCount(imageInfo.Frames.Length)); + result.Append(Res.InfoFramesCount(imageInfo.Frames!.Length)); if (!singleLine) result.AppendLine(); @@ -523,18 +556,17 @@ private Size GetSize() private string GetTypeName() { if (imageInfo.Type == ImageInfoType.Icon) - return typeof(Icon).Name; - Image img = GetCurrentImage().Image; - return img?.GetType().Name ?? typeof(Bitmap).Name; + return nameof(System.Drawing.Icon); + Image? img = GetCurrentImage().Image; + return img?.GetType().Name ?? nameof(Bitmap); } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Stream is passed to an Image/Icon")] private void FromFile(string fileName) { var stream = new MemoryStream(File.ReadAllBytes(fileName)); bool appearsIcon = Path.GetExtension(fileName).Equals(".ico", StringComparison.OrdinalIgnoreCase); - Icon icon = null; + Icon? icon = null; // icon is allowed and the content seems to be an icon // (this block is needed only for Windows XP: Icon Bitmap with PNG throws an exception but initializing from icon will succeed) @@ -554,7 +586,8 @@ private void FromFile(string fileName) } } - Image image = null; + Image? image = null; + string? openedFileName = fileName; // bitmaps and metafiles are both allowed if ((imageTypes & (AllowedImageTypes.Bitmap | AllowedImageTypes.Metafile)) == (AllowedImageTypes.Bitmap | AllowedImageTypes.Metafile)) @@ -594,8 +627,8 @@ private void FromFile(string fileName) if (image.RawFormat.Guid == ImageFormat.MemoryBmp.Guid) { - Notification = Res.NotificationMetafileAsBitmap; - fileName = null; + SetNotification(Res.NotificationMetafileAsBitmapId); + openedFileName = null; } } @@ -622,10 +655,9 @@ private void FromFile(string fileName) Bitmap iconImage = image as Bitmap ?? new Bitmap(image); icon = iconImage.ToIcon(); iconImage.Dispose(); - Notification = Res.NotificationImageAsIcon; - fileName = null; + SetNotification(Res.NotificationImageAsIconId); + openedFileName = null; } - } if (icon != null) @@ -637,12 +669,12 @@ private void FromFile(string fileName) Image = image; // null will be assigned if the image has been converted (see notifications) - imageInfo.FileName = fileName; + imageInfo.FileName = openedFileName; } private Image LoadImage(MemoryStream stream) { - if (TryLoadCustom(stream, out Image image)) + if (TryLoadCustom(stream, out Image? image)) return image; // bitmaps and metafiles are both allowed @@ -666,7 +698,7 @@ private void ResetCompoundState() IsAutoPlaying = false; NextImageCommandState.Enabled = true; PrevImageCommandState.Enabled = false; - PreviewImage = imageInfo.Frames[0].Image; + PreviewImage = imageInfo.Frames![0].Image; ImageChanged(); return; } @@ -676,7 +708,7 @@ private void ResetCompoundState() bool autoPlaying = imageInfo.Type == ImageInfoType.Animation; ICommandState timerState = AdvanceAnimationCommandState; IsAutoPlaying = autoPlaying; - PreviewImage = imageInfo.Frames[0].Image; + PreviewImage = imageInfo.Frames![0].Image; if (autoPlaying) { currentFrame = 0; @@ -697,7 +729,11 @@ private void UpdateMultiResImage() if (!imageInfo.IsMultiRes || currentFrame != -1) return; Size origSize = currentResolution; - Size clientSize = GetImagePreviewSizeCallback?.Invoke() ?? default; + Func? callback = GetImagePreviewSizeCallback; + deferUpdateInfo |= callback == null; + if (callback == null) + return; + Size clientSize = callback.Invoke(); int desiredSize = Math.Min(clientSize.Width, clientSize.Height); if (desiredSize < 1 && !origSize.IsEmpty) return; @@ -706,12 +742,22 @@ private void UpdateMultiResImage() // but that requires always a new bitmap and does not work in Windows XP desiredSize = Math.Max(desiredSize, 1); float zoom = AutoZoom ? 1f : Zoom; - ImageFrameInfo desiredImage = imageInfo.Frames.Aggregate((acc, i) => i.Size == acc.Size && i.BitsPerPixel > acc.BitsPerPixel || Math.Abs(i.Size.Width * zoom - desiredSize) < Math.Abs(acc.Size.Width * zoom - desiredSize) ? i : acc); + ImageFrameInfo desiredImage = imageInfo.Frames!.Aggregate((acc, i) + => i.Size == acc.Size && i.BitsPerPixel > acc.BitsPerPixel + || Math.Abs(i.Size.Width * zoom - desiredSize) < Math.Abs(acc.Size.Width * zoom - desiredSize) ? i : acc); currentResolution = desiredImage.Size; if (PreviewImage != desiredImage.Image) PreviewImage = desiredImage.Image; } + private void UpdateIfMultiResImage() + { + if (!imageInfo.IsMultiRes || currentFrame != -1) + return; + UpdateMultiResImage(); + UpdateInfo(); + } + private void SaveIcon(string fileName) { using (Stream stream = File.Create(fileName)) @@ -730,13 +776,13 @@ private void SaveIcon(Stream stream) // multi image icon without raw data else if (imageInfo.HasFrames) { - using (Icon i = Icons.Combine(imageInfo.Frames.Select(f => (Bitmap)f.Image).ToArray())) + using (Icon i = Icons.Combine(imageInfo.Frames!.Select(f => (Bitmap)f.Image!).ToArray())) i.Save(stream); } // single image icon without raw data else { - using (Icon i = Icons.Combine((Bitmap)imageInfo.Image)) + using (Icon i = Icons.Combine((Bitmap)imageInfo.Image!)) i.Save(stream); } @@ -747,23 +793,22 @@ private void SaveIcon(Stream stream) // saving a single icon image if (imageInfo.Icon != null) { - using (Icon i = imageInfo.Icon.ExtractIcon(currentFrame)) + using (Icon i = imageInfo.Icon.ExtractIcon(currentFrame)!) i.Save(stream); } else { - using (Icon i = Icons.Combine((Bitmap)GetCurrentImage().Image)) + using (Icon i = Icons.Combine((Bitmap)GetCurrentImage().Image!)) i.Save(stream); } stream.Flush(); } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "False alarm, bmp is disposed if it differs from image")] private void SaveBitmapData(string fileName) { using Stream stream = File.Create(fileName); - Image image = IsCompoundView ? imageInfo.GetCreateImage() : GetCurrentImage().Image; + Image image = IsCompoundView ? imageInfo.GetCreateImage()! : GetCurrentImage().Image!; Bitmap bmp = image as Bitmap ?? new Bitmap(image); try { @@ -777,8 +822,6 @@ private void SaveBitmapData(string fileName) } } - [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", - Justification = "This is not normalization, we want to display the extensions in lowercase")] private void SetOpenFilter() { if (isOpenFilterUpToDate || imageTypes == AllowedImageTypes.None) @@ -793,10 +836,10 @@ private void SetOpenFilter() if (sb.Length != 0) sb.Append('|'); - sb.Append($"{codecInfo.FormatDescription} {Res.TextFiles}|{codecInfo.FilenameExtension.ToLowerInvariant()}"); + sb.Append($"{codecInfo.FormatDescription} {Res.TextFiles}|{codecInfo.FilenameExtension?.ToLowerInvariant()}"); if (sbImages.Length != 0) sbImages.Append(';'); - sbImages.Append(codecInfo.FilenameExtension.ToLowerInvariant()); + sbImages.Append(codecInfo.FilenameExtension?.ToLowerInvariant()); } if ((imageTypes & AllowedImageTypes.Bitmap) != AllowedImageTypes.None) @@ -809,10 +852,6 @@ private void SetOpenFilter() isOpenFilterUpToDate = true; } - [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", - Justification = "This is not normalization, we want to set the default extension in lowercase")] - [SuppressMessage("Globalization", "CA1307:Specify StringComparison", - Justification = "IndexOf(char) uses ordinal lookup and the StringComparison overload is not available on every platform")] private void SetSaveFilter() { #region Local Methods @@ -828,12 +867,12 @@ static string GetFirstExtension(string extensions) #endregion // enlisting encoders - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); foreach (ImageCodecInfo codecInfo in encoderCodecs) { if (sb.Length != 0) sb.Append('|'); - sb.Append($"{codecInfo.FormatDescription} {Res.TextFileFormat}|{codecInfo.FilenameExtension.ToLowerInvariant()}"); + sb.Append($"{codecInfo.FormatDescription} {Res.TextFileFormat}|{codecInfo.FilenameExtension?.ToLowerInvariant()}"); } bool isEmf = false; @@ -850,7 +889,7 @@ static string GetFirstExtension(string extensions) string filter = sb.ToString(); // selecting appropriate format - string ext = null; + string? ext = null; if (imageInfo.IsMultiRes) ext = "ico"; else if (imageInfo.IsMetafile) @@ -864,7 +903,7 @@ static string GetFirstExtension(string extensions) { if (encoder.FormatID == imageInfo.RawFormat) { - ext = GetFirstExtension(encoder.FilenameExtension); + ext = GetFirstExtension(encoder.FilenameExtension ?? String.Empty); found = true; break; } @@ -877,7 +916,7 @@ static string GetFirstExtension(string extensions) if (!found) { ext = isPngSupported ? "png" - : encoderCodecs.Length > 0 ? GetFirstExtension(encoderCodecs[0].FilenameExtension) + : encoderCodecs.Length > 0 ? GetFirstExtension(encoderCodecs[0].FilenameExtension ?? String.Empty) : "ico"; } } @@ -889,18 +928,14 @@ static string GetFirstExtension(string extensions) private void InitAutoZoom(bool viewLoading) { + UpdateSmoothZoomingTooltip(); if (imageInfo.Type == ImageInfoType.None) { SetAutoZoomCommandState.Enabled = AutoZoom = false; - SetSmoothZoomingCommandState.Enabled = SmoothZooming = false; - SetSmoothZoomingCommandState[stateToolTipText] = null; return; } SetAutoZoomCommandState.Enabled = SetSmoothZoomingCommandState.Enabled = true; - SetSmoothZoomingCommandState[stateToolTipText] = imageInfo.IsMetafile - ? Res.TooltipTextSmoothMetafile - : Res.TooltipTextSmoothBitmap; // metafile: we always turn on auto zoom and preserve current smooth zooming if (imageInfo.IsMetafile) @@ -950,21 +985,21 @@ private void InvalidateImage() imageInfo.Icon = null; } - UpdatePreviewImageCallback.Invoke(); + UpdatePreviewImageCallback?.Invoke(); } private bool CheckSaveExtension(string fileName) { - string actualExt = Path.GetExtension(fileName)?.ToUpperInvariant(); - string[] filters = SaveFileFilter.Split('|'); + string actualExt = Path.GetExtension(fileName).ToUpperInvariant(); + string[] filters = SaveFileFilter!.Split('|'); int filterIndex = SaveFileFilterIndex; string suggestedExt = filters[((filterIndex - 1) << 1) + 1].ToUpperInvariant(); if (suggestedExt.Split(';').Contains('*' + actualExt)) return true; - return Confirm(Res.ConfirmMessageSaveFileExtension(Path.GetFileName(fileName), filters[(filterIndex - 1) << 1])); + return Confirm(Res.ConfirmMessageSaveFileExtension(Path.GetFileName(fileName), filters[(filterIndex - 1) << 1]), false); } - private void SetCurrentImage(Bitmap image) + private void SetCurrentImage(Bitmap? image) { // replacing the whole image (non-compound one) if (GetCurrentImage() == imageInfo) @@ -979,7 +1014,7 @@ private void SetCurrentImage(Bitmap image) else { Debug.Assert(currentFrame >= 0 && !IsAutoPlaying); - ImageFrameInfo[] frames = imageInfo.Frames; + ImageFrameInfo[] frames = imageInfo.Frames!; ImageFrameInfo origFrame = frames[currentFrame]; frames[currentFrame] = new ImageFrameInfo(image) { Duration = origFrame.Duration }; if (!ReferenceEquals(origFrame.Image, image)) @@ -991,14 +1026,14 @@ private void SetCurrentImage(Bitmap image) ImageChanged(); } - private void EditBitmap(Func> createViewModel) + private void EditBitmap(Func> createViewModel) { Debug.Assert(imageInfo.Type != ImageInfoType.None && !imageInfo.IsMetafile, "Non-metafile image is expected"); ImageInfoBase image = GetCurrentImage(); Debug.Assert(image.Image is Bitmap, "Existing bitmap image is expected"); - using (IViewModel viewModel = createViewModel.Invoke((Bitmap)image.Image)) + using (IViewModel viewModel = createViewModel.Invoke((Bitmap)image.Image!)) { ShowChildViewCallback?.Invoke(viewModel); if (viewModel.IsModified) @@ -1006,7 +1041,6 @@ private void EditBitmap(Func> createViewModel) } } - [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", Justification = "False alarm, image is not a remote object")] private void RotateBitmap(RotateFlipType direction) { Debug.Assert(imageInfo.Type != ImageInfoType.None && !imageInfo.IsMetafile, "Non-metafile image is expected"); @@ -1014,20 +1048,32 @@ private void RotateBitmap(RotateFlipType direction) Debug.Assert(image.Image is Bitmap, "Existing bitmap image is expected"); // must be in a lock because it can be in use in the UI (where it is also locked) - lock (image.Image) + lock (image.Image!) image.Image.RotateFlip(direction); SetCurrentImage((Bitmap)image.Image); } + private void UpdateSmoothZoomingTooltip() + => SetSmoothZoomingCommandState[stateToolTipText] = + imageInfo.Type == ImageInfoType.None ? null + : imageInfo.IsMetafile ? Res.TooltipTextSmoothMetafile + : Res.TooltipTextSmoothBitmap; + + private void UpdateNotification() => Notification = notificationId == null ? null : Res.Get(notificationId); + #endregion #region Explicitly Implemented Interface Methods - Image IViewModel.GetEditedModel() => (Image)Image?.Clone(); - Icon IViewModel.GetEditedModel() => Icon?.Clone() as Icon; - Bitmap IViewModel.GetEditedModel() => Image?.Clone() as Bitmap; - Metafile IViewModel.GetEditedModel() => Image?.Clone() as Metafile; - ImageInfo IViewModel.GetEditedModel() => imageTypes == AllowedImageTypes.Icon ? imageInfo.AsIcon() : imageInfo.AsImage(); + Image? IViewModel.GetEditedModel() => Image?.Clone() as Image; + Icon? IViewModel.GetEditedModel() => Icon?.Clone() as Icon; + Bitmap? IViewModel.GetEditedModel() => Image?.Clone() as Bitmap; + Metafile? IViewModel.GetEditedModel() => Image?.Clone() as Metafile; + ImageInfo IViewModel.GetEditedModel() + { + keepAliveImageInfo = true; + return imageTypes == AllowedImageTypes.Icon ? imageInfo.AsIcon() : imageInfo.AsImage(); + } #endregion @@ -1037,13 +1083,7 @@ private void RotateBitmap(RotateFlipType direction) private void OnSetSmoothZoomingCommand(bool newValue) => SmoothZooming = newValue; - private void OnViewImagePreviewSizeChangedCommand() - { - if (!imageInfo.IsMultiRes || currentFrame != -1) - return; - UpdateMultiResImage(); - UpdateInfo(); - } + private void OnViewImagePreviewSizeChangedCommand() => UpdateIfMultiResImage(); private void OnOpenFileCommand() => OpenFile(); @@ -1053,7 +1093,7 @@ private void OnSaveFileCommand() return; SetSaveFilter(); - string fileName; + string? fileName; do { fileName = SelectFileToSaveCallback?.Invoke(); @@ -1062,7 +1102,7 @@ private void OnSaveFileCommand() } while (!CheckSaveExtension(fileName)); int filterIndex = SaveFileFilterIndex; - string selectedFormat = SaveFileFilter.Split('|')[((filterIndex - 1) << 1) + 1]; + string selectedFormat = SaveFileFilter!.Split('|')[((filterIndex - 1) << 1) + 1]; SaveFile(fileName, selectedFormat); } @@ -1082,7 +1122,7 @@ private void OnAdvanceAnimationCommand() // playing with duration Debug.Assert(imageInfo.HasFrames); currentFrame++; - ImageFrameInfo[] frames = imageInfo.Frames; + ImageFrameInfo[] frames = imageInfo.Frames!; if (currentFrame >= frames.Length) currentFrame = 0; AdvanceAnimationCommandState[stateInterval] = frames[currentFrame].Duration; @@ -1094,7 +1134,7 @@ private void OnPrevImageCommand() if (!imageInfo.HasFrames || currentFrame <= 0) return; - PreviewImage = imageInfo.Frames[--currentFrame].Image; + PreviewImage = imageInfo.Frames![--currentFrame].Image; PrevImageCommandState.Enabled = currentFrame > 0; NextImageCommandState.Enabled = true; ImageChanged(); @@ -1102,7 +1142,7 @@ private void OnPrevImageCommand() private void OnNextImageCommand() { - ImageFrameInfo[] frames = imageInfo.Frames; + ImageFrameInfo[] frames = imageInfo.Frames!; if (!imageInfo.HasFrames || currentFrame >= frames.Length) return; @@ -1115,7 +1155,7 @@ private void OnNextImageCommand() private void OnShowPaletteCommand() { ImageInfoBase currentImage = GetCurrentImage(); - if (currentImage == null || currentImage.Palette.Length == 0) + if (currentImage.Palette.Length == 0) return; using (IViewModel vmPalette = ViewModelFactory.FromPalette(currentImage.Palette, IsPaletteReadOnly)) @@ -1125,7 +1165,7 @@ private void OnShowPaletteCommand() return; // apply changes - ColorPalette palette = currentImage.Image.Palette; + ColorPalette palette = currentImage.Image!.Palette; Color[] newPalette = vmPalette.GetEditedModel(); // even if the length of the palette is not edited it can happen that the preview image is ARGB32 @@ -1141,7 +1181,9 @@ private void OnShowPaletteCommand() for (int i = 0; i < newPalette.Length; i++) palette.Entries[i] = newPalette[i]; - currentImage.Image.Palette = palette; // the preview changes only if we apply the palette + // must be in a lock because it can be in use in the UI (where it is also locked) + lock (currentImage.Image) + currentImage.Image.Palette = palette; // the preview changes only if we apply the palette currentImage.Palette = palette.Entries; // the actual palette will be taken from here InvalidateImage(); } @@ -1160,7 +1202,7 @@ private void OnCountColorsCommand() ImageInfoBase image = GetCurrentImage(); Debug.Assert(image.Image is Bitmap, "Existing bitmap image is expected"); - using IViewModel viewModel = ViewModelFactory.CreateCountColors((Bitmap)image.Image); + using IViewModel viewModel = ViewModelFactory.CreateCountColors((Bitmap)image.Image!); ShowChildViewCallback?.Invoke(viewModel); // this prevents the viewModel from disposing until before the view is completely finished (on cancel, for example) @@ -1175,6 +1217,25 @@ private void OnCountColorsCommand() private void OnAdjustContrastCommand() => EditBitmap(ViewModelFactory.CreateAdjustContrast); private void OnAdjustGammaCommand() => EditBitmap(ViewModelFactory.CreateAdjustGamma); + private void OnSetLanguageCommand() + { + using IViewModel viewModel = ViewModelFactory.CreateLanguageSettings(); + ShowChildViewCallback?.Invoke(viewModel); + } + + private void OnShowAboutCommand() + { + Assembly asm = GetType().Assembly; + +#if NET35 + const string frameworkName = ".NET Framework 3.5"; +#else + TargetFrameworkAttribute attr = (TargetFrameworkAttribute)Attribute.GetCustomAttribute(asm, typeof(TargetFrameworkAttribute))!; + string frameworkName = attr.FrameworkDisplayName is { Length: > 0 } name ? name : attr.FrameworkName; +#endif + ShowInfo(Res.InfoMessageAbout(asm.GetName().Version!, frameworkName, DateTime.Now.Year)); + } + #endregion #endregion diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/LanguageSettingsViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/LanguageSettingsViewModel.cs new file mode 100644 index 0000000..42bd846 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/ViewModel/LanguageSettingsViewModel.cs @@ -0,0 +1,272 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: LanguageSettingsViewModel.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; + +using KGySoft.ComponentModel; +using KGySoft.Drawing.ImagingTools.Model; +using KGySoft.Resources; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.ViewModel +{ + internal class LanguageSettingsViewModel : ViewModelBase + { + #region Fields + + private List? neutralLanguages; + private HashSet? availableResXLanguages; + private List? selectableLanguages; + + #endregion + + #region Properties + + #region Internal Properties + + internal bool AllowResXResources { get => Get(); set => Set(value); } + internal bool UseOSLanguage { get => Get(); set => Set(value); } + internal bool ExistingLanguagesOnly { get => Get(true); set => Set(value); } + internal CultureInfo CurrentLanguage { get => Get(); set => Set(value); } + internal IList Languages { get => Get>(); set => Set(value); } + + internal ICommand ApplyCommand => Get(() => new SimpleCommand(OnApplyCommand)); + internal ICommand SaveConfigCommand => Get(() => new SimpleCommand(OnSaveConfigCommand)); + internal ICommand EditResourcesCommand => Get(() => new SimpleCommand(OnEditResourcesCommand)); + internal ICommand DownloadResourcesCommand => Get(() => new SimpleCommand(OnDownloadResourcesCommand)); + + internal ICommandState ApplyCommandState => Get(() => new CommandState()); + internal ICommandState EditResourcesCommandState => Get(() => new CommandState()); + + #endregion + + #region Private Properties + + private List NeutralLanguages + { + get + { + if (neutralLanguages == null) + { + CultureInfo[] result = CultureInfo.GetCultures(CultureTypes.NeutralCultures); + neutralLanguages = new List(result.Length - 1); + foreach (CultureInfo ci in result) + { + if (Equals(ci, CultureInfo.InvariantCulture)) + continue; + neutralLanguages.Add(ci); + } + } + + return neutralLanguages; + } + } + + private HashSet AvailableLanguages => availableResXLanguages ??= ResHelper.GetAvailableLanguages(); + + private List SelectableLanguages + { + get + { + if (selectableLanguages == null) + { + selectableLanguages = new List(AvailableLanguages); + if (!AvailableLanguages.Contains(Res.DefaultLanguage)) + selectableLanguages.Add(Res.DefaultLanguage); + } + + return selectableLanguages; + } + } + + #endregion + + #endregion + + #region Constructors + + internal LanguageSettingsViewModel() + { + CurrentLanguage = Res.DisplayLanguage; + AllowResXResources = Configuration.AllowResXResources; + UseOSLanguage = Configuration.UseOSLanguage; + ExistingLanguagesOnly = true; // could be the default value but this way we spare one reset when initializing binding + ResetLanguages(); + UpdateApplyCommandState(); + } + + #endregion + + #region Methods + + #region Protected Methods + + protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) + { + base.OnPropertyChanged(e); + switch (e.PropertyName) + { + case nameof(AllowResXResources): + EditResourcesCommandState.Enabled = e.NewValue is true; + ResetLanguages(); + UpdateApplyCommandState(); + break; + + case nameof(UseOSLanguage): + case nameof(ExistingLanguagesOnly): + ResetLanguages(); + UpdateApplyCommandState(); + break; + + case nameof(Languages): + if (e.OldValue is SortableBindingList sbl) + sbl.Dispose(); + break; + + case nameof(CurrentLanguage): + UpdateApplyCommandState(); + break; + } + } + + protected override void ApplyDisplayLanguage() => UpdateApplyCommandState(); + + protected override void Dispose(bool disposing) + { + if (IsDisposed) + return; + + if (disposing) + (Languages as SortableBindingList)?.Dispose(); + + base.Dispose(disposing); + } + + #endregion + + #region Private Methods + + private void ResetLanguages() + { + if (!AllowResXResources) + { + Languages = new[] { Res.DefaultLanguage }; + CurrentLanguage = Res.DefaultLanguage; + return; + } + + if (UseOSLanguage) + { + Languages = new[] { Res.OSLanguage }; + CurrentLanguage = Res.OSLanguage; + return; + } + + var result = new SortableBindingList(ExistingLanguagesOnly ? SelectableLanguages : NeutralLanguages); + result.ApplySort(nameof(CultureInfo.EnglishName), ListSortDirection.Ascending); + CultureInfo lastSelectedLanguage = CurrentLanguage; + Languages = result; + CurrentLanguage = result.Contains(lastSelectedLanguage) ? lastSelectedLanguage : Res.DefaultLanguage; + } + + private void UpdateApplyCommandState() + { + // Apply is enabled if current language is different than display language, + // or when turning on/off .resx resources for the default language matters because it also has a resource file + CultureInfo selected = CurrentLanguage; + ApplyCommandState.Enabled = !Equals(selected, Res.DisplayLanguage) + || (Equals(selected, Res.DefaultLanguage) + && (AllowResXResources ^ LanguageSettings.DynamicResourceManagersSource != ResourceManagerSources.CompiledOnly) + && AvailableLanguages.Contains(Res.DefaultLanguage)); + } + + private void ApplyAndSave() + { + if (!IsModified) + return; + + SaveConfiguration(); + + // Applying the current language + CultureInfo currentLanguage = CurrentLanguage; + LanguageSettings.DynamicResourceManagersSource = AllowResXResources ? ResourceManagerSources.CompiledAndResX : ResourceManagerSources.CompiledOnly; + + if (Equals(Res.DisplayLanguage, currentLanguage)) + Res.OnDisplayLanguageChanged(); + else + Res.DisplayLanguage = currentLanguage; + + // Note: Ensure is not really needed because main .resx is generated, while others are saved on demand in the editor, too + //ResHelper.EnsureResourcesGenerated(); // TODO If used, then add to EditResourcesVM.Save, too, to be consistent + ResHelper.SavePendingResources(); + availableResXLanguages = null; + selectableLanguages = null; + } + + private void SaveConfiguration() + { + Configuration.AllowResXResources = AllowResXResources; + Configuration.UseOSLanguage = UseOSLanguage; + Configuration.DisplayLanguage = CurrentLanguage; + try + { + Configuration.SaveSettings(); + } + catch (Exception e) when (!e.IsCritical()) + { + ShowError(Res.ErrorMessageFailedToSaveSettings(e.Message)); + } + } + + #endregion + + #region Command Handlers + + // Both Save and Apply do the same thing. + // The only difference is that Apply has an Enabled state and the View may bind Save to a button that closes the view. + private void OnApplyCommand() => ApplyAndSave(); + private void OnSaveConfigCommand() => ApplyAndSave(); + + private void OnEditResourcesCommand() + { + using IViewModel viewModel = ViewModelFactory.CreateEditResources(CurrentLanguage); + if (viewModel is EditResourcesViewModel vm) + vm.SaveConfigurationCallback = SaveConfiguration; + ShowChildViewCallback?.Invoke(viewModel); + availableResXLanguages = null; + selectableLanguages = null; + } + + private void OnDownloadResourcesCommand() + { + using IViewModel> viewModel = ViewModelFactory.CreateDownloadResources(); + ShowChildViewCallback?.Invoke(viewModel); + availableResXLanguages = null; + selectableLanguages = null; + ResetLanguages(); + } + + #endregion + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/ManageInstallationsViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/ManageInstallationsViewModel.cs index 14c0634..df8803d 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/ManageInstallationsViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/ManageInstallationsViewModel.cs @@ -22,7 +22,9 @@ using System.Linq; using KGySoft.ComponentModel; -using KGySoft.CoreLibraries; +#if NETFRAMEWORK +using KGySoft.CoreLibraries; +#endif using KGySoft.Drawing.ImagingTools.Model; #endregion @@ -41,7 +43,7 @@ internal class ManageInstallationsViewModel : ViewModelBase #region Fields - private InstallationInfo currentStatus; + private InstallationInfo currentStatus = default!; private bool isSelectingPath; #endregion @@ -49,12 +51,12 @@ internal class ManageInstallationsViewModel : ViewModelBase #region Properties internal IList> Installations { get => Get>>(); set => Set(value); } - internal string SelectedInstallation { get => Get(); set => Set(value); } - internal string CurrentPath { get => Get(); set => Set(value); } - internal string StatusText { get => Get("-"); set => Set(value); } - internal string AvailableVersionText { get => Get("-"); set => Set(value); } + internal string SelectedInstallation { get => Get(String.Empty); set => Set(value); } + internal string CurrentPath { get => Get(String.Empty); set => Set(value); } + internal string StatusText { get => Get("–"); set => Set(value); } + internal string AvailableVersionText { get => Get("–"); set => Set(value); } - internal Func SelectFolderCallback { get => Get>(); set => Set(value); } + internal Func? SelectFolderCallback { get => Get?>(); set => Set(value); } internal ICommandState SelectFolderCommandState => Get(() => new CommandState()); internal ICommandState InstallCommandState => Get(() => new CommandState()); @@ -68,7 +70,7 @@ internal class ManageInstallationsViewModel : ViewModelBase #region Constructors - internal ManageInstallationsViewModel(string hintPath) + internal ManageInstallationsViewModel(string? hintPath) { InitAvailableVersion(); InitInstallations(); @@ -86,12 +88,21 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) base.OnPropertyChanged(e); if (e.PropertyName == nameof(SelectedInstallation)) { - UpdatePath((string)e.NewValue); + UpdatePath((string)e.NewValue!); return; } if (e.PropertyName == nameof(CurrentPath)) - UpdateStatus((string)e.NewValue); + UpdateStatus((string)e.NewValue!); + } + + protected override void ApplyDisplayLanguage() + { + InitAvailableVersion(); + string currentPath = CurrentPath; + InitInstallations(); + CurrentPath = currentPath; + UpdateStatus(currentPath); } #endregion @@ -113,11 +124,11 @@ private void InitAvailableVersion() InstallCommandState.Enabled = available; } - private void TrySelectPath(string hintPath) + private void TrySelectPath(string? hintPath) { if (hintPath?.Contains(visualStudioName, StringComparison.Ordinal) == true) { - string preferredPath = Path.GetFileName(hintPath) == visualizersDir ? Path.GetDirectoryName(hintPath) : hintPath; + string preferredPath = Path.GetFileName(hintPath) == visualizersDir ? Path.GetDirectoryName(hintPath)! : hintPath; SelectInstallation(preferredPath); return; } @@ -177,6 +188,7 @@ private void UpdateStatus(string path) StatusText = currentStatus.TargetFramework != null ? Res.InstallationsStatusInstalledWithTargetFramework(currentStatus.Version, currentStatus.TargetFramework) : currentStatus.RuntimeVersion != null ? Res.InstallationsStatusInstalledWithRuntime(currentStatus.Version, currentStatus.RuntimeVersion) : Res.InstallationsStatusInstalled(currentStatus.Version); + RemoveCommandState.Enabled = currentStatus.Installed; } private void SelectFolder() @@ -199,14 +211,14 @@ private void OnSelectFolderCommand() private void OnInstallCommand() { - if (currentStatus.Installed && !Confirm(Res.ConfirmMessageOverwriteInstallation)) + if (currentStatus.Installed && !Confirm(Res.ConfirmMessageOverwriteInstallation, currentStatus.Version != null && InstallationManager.AvailableVersion.Version > currentStatus.Version)) return; #if NETCOREAPP - if (!Confirm(Res.ConfirmMessageNetCoreVersion)) + if (!Confirm(Res.ConfirmMessageNetCoreVersion, false)) return; #endif - InstallationManager.Install(currentStatus.Path, out string error, out string warning); + InstallationManager.Install(currentStatus.Path, out string? error, out string? warning); if (error != null) ShowError(Res.ErrorMessageInstallationFailed(error)); else if (warning != null) @@ -216,9 +228,9 @@ private void OnInstallCommand() private void OnRemoveCommand() { - if (!Confirm(Res.ConfirmMessageRemoveInstallation)) + if (!Confirm(Res.ConfirmMessageRemoveInstallation, false)) return; - InstallationManager.Uninstall(currentStatus.Path, out string error); + InstallationManager.Uninstall(currentStatus.Path, out string? error); if (error != null) ShowError(Res.ErrorMessageRemoveInstallationFailed(error)); UpdateStatus(currentStatus.Path); diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/PaletteVisualizerViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/PaletteVisualizerViewModel.cs index 5888b1a..0204264 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/PaletteVisualizerViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/PaletteVisualizerViewModel.cs @@ -16,11 +16,11 @@ #region Usings +using System; using System.Collections.Generic; using System.Drawing; using KGySoft.ComponentModel; -using KGySoft.CoreLibraries; #endregion @@ -32,7 +32,8 @@ internal class PaletteVisualizerViewModel : ViewModelBase, IViewModel #region Internal Properties - internal Color[] Palette { get => Get(); set => Set(value.Clone()); } + // ReSharper disable once ConstantConditionalAccessQualifier - not cloning if value is null + internal Color[] Palette { get => Get(); init => Set(value?.Clone() ?? throw new ArgumentNullException(nameof(value), PublicResources.ArgumentNull)); } internal int Count { get => Get(); set => Set(value); } internal bool ReadOnly { get => Get(); set => Set(value); } @@ -53,7 +54,7 @@ internal class PaletteVisualizerViewModel : ViewModelBase, IViewModel internal override void ViewLoaded() { base.ViewLoaded(); - if (Palette.IsNullOrEmpty()) + if (Palette.Length == 0) { ReadOnly = true; ShowInfo(Res.InfoMessagePaletteEmpty); @@ -72,8 +73,8 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) // Palette -> Count if (e.PropertyName == nameof(Palette)) { - var palette = (IList)e.NewValue; - Count = palette?.Count ?? 0; + var palette = (IList)e.NewValue!; + Count = palette.Count; } } diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/PreviewImageViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/PreviewImageViewModel.cs index 8ec966f..8af5350 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/PreviewImageViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/PreviewImageViewModel.cs @@ -28,13 +28,12 @@ internal class PreviewImageViewModel : ViewModelBase { #region Properties - internal Image OriginalImage { get => Get(); set => Set(value); } - internal Image PreviewImage { get => Get(); set => Set(value); } - internal Image DisplayImage { get => Get(); set => Set(value); } + internal Image? OriginalImage { get => Get(); set => Set(value); } + internal Image? PreviewImage { get => Get(); set => Set(value); } + internal Image? DisplayImage { get => Get(); set => Set(value); } internal bool AutoZoom { get => Get(true); set => Set(value); } internal bool SmoothZooming { get => Get(true); set => Set(value); } internal bool ShowOriginal { get => Get(); set => Set(value); } - internal bool ZoomEnabled { get => Get(); set => Set(value); } internal bool ShowOriginalEnabled { get => Get(true); set => Set(value); } #endregion @@ -56,9 +55,6 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) if (ShowOriginal) DisplayImage = PreviewImage; return; - case nameof(DisplayImage): - ZoomEnabled = e.NewValue != null; - return; case nameof(ShowOriginal): DisplayImage = e.NewValue is true ? OriginalImage : PreviewImage; return; diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/QuantizerSelectorViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/QuantizerSelectorViewModel.cs index e4c18f9..bb48d92 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/QuantizerSelectorViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/QuantizerSelectorViewModel.cs @@ -35,10 +35,10 @@ internal class QuantizerSelectorViewModel : ViewModelBase // not a static property so always can be reinitialized with the current language internal IList Quantizers => Get(InitQuantizers); - internal QuantizerDescriptor SelectedQuantizer { get => Get(); private set => Set(value); } - internal CustomPropertiesObject Parameters { get => Get(); private set => Set(value); } - internal IQuantizer Quantizer { get => Get(); private set => Set(value); } - internal Exception CreateQuantizerError { get => Get(); set => Set(value); } + internal QuantizerDescriptor? SelectedQuantizer { get => Get(); private set => Set(value); } + internal CustomPropertiesObject? Parameters { get => Get(); private set => Set(value); } + internal IQuantizer? Quantizer { get => Get(); private set => Set(value); } + internal Exception? CreateQuantizerError { get => Get(); set => Set(value); } #endregion @@ -51,7 +51,7 @@ private static IList InitQuantizers() => { //new QuantizerDescriptor(typeof(PredefinedColorsQuantizer), nameof(PredefinedColorsQuantizer.FromPixelFormat)), new QuantizerDescriptor(typeof(PredefinedColorsQuantizer), nameof(PredefinedColorsQuantizer.BlackAndWhite)), - new QuantizerDescriptor(typeof(PredefinedColorsQuantizer).GetMethod(nameof(PredefinedColorsQuantizer.FromCustomPalette), new[] { typeof(Color[]), typeof(Color), typeof(byte) })), + new QuantizerDescriptor(typeof(PredefinedColorsQuantizer).GetMethod(nameof(PredefinedColorsQuantizer.FromCustomPalette), new[] { typeof(Color[]), typeof(Color), typeof(byte) })!), new QuantizerDescriptor(typeof(PredefinedColorsQuantizer), nameof(PredefinedColorsQuantizer.Grayscale4)), new QuantizerDescriptor(typeof(PredefinedColorsQuantizer), nameof(PredefinedColorsQuantizer.Grayscale16)), new QuantizerDescriptor(typeof(PredefinedColorsQuantizer), nameof(PredefinedColorsQuantizer.Grayscale)), @@ -78,18 +78,19 @@ private static IList InitQuantizers() => internal void ResetQuantizer() { - QuantizerDescriptor descriptor = SelectedQuantizer; + QuantizerDescriptor? descriptor = SelectedQuantizer; + CustomPropertiesObject? parameters = Parameters; CreateQuantizerError = null; - if (descriptor == null) + if (descriptor == null || parameters == null) { Quantizer = null; return; } - object[] parameters = descriptor.EvaluateParameters(Parameters); + object?[] parameterValues = descriptor.EvaluateParameters(parameters); try { - Quantizer = (IQuantizer)MethodAccessor.GetAccessor(descriptor.Method).Invoke(null, parameters); + Quantizer = (IQuantizer)MethodAccessor.GetAccessor(descriptor.Method).Invoke(null, parameterValues)!; } catch (Exception e) when (!e.IsCritical()) { @@ -105,19 +106,18 @@ internal void ResetQuantizer() protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) { base.OnPropertyChanged(e); - if (e.PropertyName == nameof(SelectedQuantizer)) + switch (e.PropertyName) { - CustomPropertiesObject previousParameters = Parameters; - Parameters = previousParameters == null - ? new CustomPropertiesObject(SelectedQuantizer.Parameters) - : new CustomPropertiesObject(previousParameters, SelectedQuantizer.Parameters); - return; - } - - if (e.PropertyName == nameof(Parameters)) - { - ResetQuantizer(); - return; + case nameof(SelectedQuantizer): + CustomPropertiesObject? previousParameters = Parameters; + Parameters = previousParameters == null + ? new CustomPropertiesObject(SelectedQuantizer!.Parameters) + : new CustomPropertiesObject(previousParameters, SelectedQuantizer!.Parameters); + return; + + case nameof(Parameters): + ResetQuantizer(); + return; } } diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/ResizeBitmapViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/ResizeBitmapViewModel.cs index 3be9046..ea54cd5 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/ResizeBitmapViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/ResizeBitmapViewModel.cs @@ -17,7 +17,6 @@ #region Usings using System; -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; using System.Linq; @@ -39,11 +38,11 @@ private sealed class ResizeTask : GenerateTaskBase { #region Fields - private Bitmap sourceBitmap; - private Bitmap targetBitmap; + private Bitmap? sourceBitmap; + private Bitmap? targetBitmap; private bool isSourceCloned; - private IReadableBitmapData sourceBitmapData; - private IReadWriteBitmapData targetBitmapData; + private IReadableBitmapData? sourceBitmapData; + private IReadWriteBitmapData? targetBitmapData; #endregion @@ -68,8 +67,6 @@ internal ResizeTask(Size size, ScalingMode scalingMode) #region Internal Methods - [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", - Justification = "False alarm, source is never a remote object")] internal override void Initialize(Bitmap source, bool isInUse) { // this must be the first line to prevent disposing source if next lines fail @@ -102,17 +99,17 @@ internal override void Initialize(Bitmap source, bool isInUse) } internal override IAsyncResult BeginGenerate(AsyncConfig asyncConfig) - => sourceBitmapData.BeginDrawInto(targetBitmapData, new Rectangle(0, 0, sourceBitmapData.Width, sourceBitmapData.Height), - new Rectangle(0, 0, targetBitmapData.Width, targetBitmapData.Height), + => sourceBitmapData!.BeginDrawInto(targetBitmapData!, new Rectangle(0, 0, sourceBitmapData!.Width, sourceBitmapData.Height), + new Rectangle(0, 0, targetBitmapData!.Width, targetBitmapData.Height), scalingMode: ScalingMode, asyncConfig: asyncConfig); - internal override Bitmap EndGenerate(IAsyncResult asyncResult) + internal override Bitmap? EndGenerate(IAsyncResult asyncResult) { asyncResult.EndDrawInto(); // If there was no exception returning result and clearing the field to prevent disposing. // The caller will take care of disposing if the operation was canceled and the result is discarded. - Bitmap bmp = targetBitmap; + Bitmap? bmp = targetBitmap; targetBitmap = null; return bmp; } @@ -171,15 +168,21 @@ protected override void Dispose(bool disposing) #region Properties + #region Public Properties + + // The binding needs these to be public + public int Width { get => Get(originalSize.Width); set => Set(value); } + public int Height { get => Get(originalSize.Height); set => Set(value); } + public float WidthRatio { get => Get(1f); set => Set(value); } + public float HeightRatio { get => Get(1f); set => Set(value); } + + #endregion + #region Internal Properties internal bool KeepAspectRatio { get => Get(true); set => Set(value); } internal bool ByPercentage { get => Get(true); set => Set(value); } internal bool ByPixels { get => Get(false); set => Set(value); } - internal int Width { get => Get(originalSize.Width); set => Set(value); } - internal int Height { get => Get(originalSize.Height); set => Set(value); } - internal float WidthRatio { get => Get(1f); set => Set(value); } - internal float HeightRatio { get => Get(1f); set => Set(value); } internal ScalingMode[] ScalingModes => Get(Enum.GetValues); internal ScalingMode ScalingMode { get => Get(); set => Set(value); } @@ -231,7 +234,7 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) case nameof(Width): if (!ByPixels) break; - int width = (int)e.NewValue; + int width = (int)e.NewValue!; WidthRatio = width <= 0 ? 0f : (float)width / originalSize.Width; if (!KeepAspectRatio || adjustingWidth) break; @@ -250,7 +253,7 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) case nameof(WidthRatio): if (!ByPercentage) break; - float widthRatio = (float)e.NewValue; + float widthRatio = (float)e.NewValue!; Width = widthRatio <= 0f ? 0 : (int)(originalSize.Width * widthRatio); if (!KeepAspectRatio || adjustingWidth) break; @@ -269,7 +272,7 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) case nameof(Height): if (!ByPixels) break; - int height = (int)e.NewValue; + int height = (int)e.NewValue!; HeightRatio = height <= 0 ? 0f : (float)height / originalSize.Height; if (!KeepAspectRatio || adjustingHeight) break; @@ -288,7 +291,7 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) case nameof(HeightRatio): if (!ByPercentage) break; - float heightRatio = (float)e.NewValue; + float heightRatio = (float)e.NewValue!; Height = heightRatio <= 0f ? 0 : (int)(originalSize.Height * heightRatio); if (!KeepAspectRatio || adjustingHeight) break; diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/TransformBitmapViewModelBase.cs b/KGySoft.Drawing.ImagingTools/ViewModel/TransformBitmapViewModelBase.cs index dcb2509..b89c551 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/TransformBitmapViewModelBase.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/TransformBitmapViewModelBase.cs @@ -19,19 +19,17 @@ using System; using System.ComponentModel; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Threading; using KGySoft.ComponentModel; -using KGySoft.CoreLibraries; using KGySoft.Drawing.ImagingTools.Model; #endregion namespace KGySoft.Drawing.ImagingTools.ViewModel { - internal abstract class TransformBitmapViewModelBase : ViewModelBase, IViewModel, IValidatingObject + internal abstract class TransformBitmapViewModelBase : ViewModelBase, IViewModel, IValidatingObject { #region Nested Classes @@ -41,7 +39,7 @@ protected abstract class GenerateTaskBase : AsyncTaskBase internal abstract void Initialize(Bitmap source, bool isInUse); internal abstract IAsyncResult BeginGenerate(AsyncConfig asyncConfig); - internal abstract Bitmap EndGenerate(IAsyncResult asyncResult); + internal abstract Bitmap? EndGenerate(IAsyncResult asyncResult); #endregion } @@ -53,16 +51,15 @@ protected abstract class GenerateTaskBase : AsyncTaskBase private readonly Bitmap originalImage; private readonly object syncRoot = new object(); - private volatile GenerateTaskBase activeTask; - private bool initializing = true; + private volatile GenerateTaskBase? activeTask; private bool keepResult; - private DrawingProgressManager drawingProgressManager; + private DrawingProgressManager? drawingProgressManager; #endregion #region Events - internal event EventHandler> ValidationResultsChanged + internal event EventHandler? ValidationResultsChanged { add => ValidationResultsChangedHandler += value; remove => ValidationResultsChangedHandler -= value; @@ -102,8 +99,8 @@ internal event EventHandler> ValidationRe #region Private Properties - private Exception GeneratePreviewError { get => Get(); set => Set(value); } - private EventHandler> ValidationResultsChangedHandler { get => Get>>(); set => Set(value); } + private Exception? GeneratePreviewError { get => Get(); set => Set(value); } + private EventHandler? ValidationResultsChangedHandler { get => Get(); set => Set(value); } #endregion @@ -115,7 +112,7 @@ protected TransformBitmapViewModelBase(Bitmap image) { originalImage = image ?? throw new ArgumentNullException(nameof(image), PublicResources.ArgumentNull); PreviewImageViewModel previewImage = PreviewImageViewModel; - previewImage.PropertyChanged += PreviewImage_PropertyChanged; + previewImage.PropertyChanged += PreviewImage_PropertyChanged!; previewImage.PreviewImage = previewImage.OriginalImage = image; } @@ -128,8 +125,7 @@ protected TransformBitmapViewModelBase(Bitmap image) internal override void ViewLoaded() { // could be in constructor but we only need it when there is a view - drawingProgressManager = new DrawingProgressManager(p => Progress = p); - initializing = false; + drawingProgressManager = new DrawingProgressManager(TrySetProgress); base.ViewLoaded(); } @@ -143,9 +139,9 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) switch (e.PropertyName) { case nameof(ValidationResults): - var validationResults = (ValidationResultsCollection)e.NewValue; + var validationResults = (ValidationResultsCollection)e.NewValue!; IsValid = !validationResults.HasErrors; - ValidationResultsChangedHandler?.Invoke(this, new EventArgs(validationResults)); + ValidationResultsChangedHandler?.Invoke(this, EventArgs.Empty); return; case nameof(GeneratePreviewError): @@ -160,7 +156,7 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) return; default: - if (initializing || !AffectsPreview(e.PropertyName)) + if (!IsViewLoaded || !AffectsPreview(e.PropertyName!)) return; Validate(); ResetCommandState.Enabled = AreSettingsChanged; @@ -171,14 +167,14 @@ protected override void OnPropertyChanged(PropertyChangedExtendedEventArgs e) protected void Validate() { - if (initializing) + if (!IsViewLoaded) return; ValidationResults = DoValidation(); } protected virtual ValidationResultsCollection DoValidation() { - Exception error = GeneratePreviewError; + Exception? error = GeneratePreviewError; var result = new ValidationResultsCollection(); // errors @@ -195,7 +191,7 @@ protected virtual ValidationResultsCollection DoValidation() protected void BeginGeneratePreview() { - // sending cancel request to pending generate but the completion is awaited on a pool thread to prevent deadlocks + // sending cancel request to pending generate but the completion is awaited on a pool thread to prevent the UI from lagging CancelRunningGenerate(); GeneratePreviewError = null; @@ -207,8 +203,9 @@ protected void BeginGeneratePreview() return; } + // Not awaiting the canceled task here to prevent the UI from lagging. IsGenerating = true; - ThreadPool.QueueUserWorkItem(DoGenerate, CreateGenerateTask()); + ThreadPool.QueueUserWorkItem(DoGenerate!, CreateGenerateTask()); } protected abstract GenerateTaskBase CreateGenerateTask(); @@ -216,15 +213,23 @@ protected void BeginGeneratePreview() protected abstract bool MatchesOriginal(GenerateTaskBase task); protected virtual void ResetParameters() { } + protected override void ApplyDisplayLanguage() => Validate(); + protected override void Dispose(bool disposing) { if (IsDisposed) return; if (disposing) { - activeTask?.Dispose(); - Image preview = PreviewImageViewModel.PreviewImage; - PreviewImageViewModel?.Dispose(); + if (activeTask != null) + { + CancelRunningGenerate(); + WaitForPendingGenerate(); + } + + Debug.Assert(activeTask == null); + Image? preview = PreviewImageViewModel.PreviewImage; + PreviewImageViewModel.Dispose(); if (!ReferenceEquals(originalImage, preview) && !keepResult) preview?.Dispose(); @@ -239,27 +244,40 @@ protected override void Dispose(bool disposing) #region Private Methods - [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", Justification = "False alarm, originalImage is not a remote object")] private void DoGenerate(object state) { var task = (GenerateTaskBase)state; - // this is a fairly large lock ensuring that only one generate task is running at once - lock (syncRoot) + // This is a fairly large lock ensuring that only one generate task is running at once. + // Instead of this sync we could await the canceled task before queuing a new one but then the UI can freeze for some moments. + // (It wouldn't cause deadlocks because here every TryInvokeSync is after completing the task.) + // But many threads can be queued, which all stop here before acquiring the lock. To prevent spawning too many threads we + // don't use a regular lock here but a bit active spinning that can exit without taking the lock if the task gets outdated. + while (!Monitor.TryEnter(syncRoot, 1)) + { + + if (!IsDisposed && MatchesSettings(task)) + continue; + task.Dispose(); + return; + } + + try { // lost race - if (!MatchesSettings(task)) + if (IsDisposed || !MatchesSettings(task)) { task.Dispose(); return; } + // Awaiting the previous unfinished task. This could be also in BeginGeneratePreview but that may freeze the UI for some time. Debug.Assert(activeTask?.IsCanceled != false); WaitForPendingGenerate(); Debug.Assert(activeTask == null); // resetting possible previous progress - drawingProgressManager.Report(default); + drawingProgressManager?.Report(default); // from now on the task can be canceled activeTask = task; @@ -268,7 +286,8 @@ private void DoGenerate(object state) // original image if (MatchesOriginal(task)) { - SynchronizedInvokeCallback?.Invoke(() => + task.SetCompleted(); + TryInvokeSync(() => { SetPreview(originalImage); IsGenerating = false; @@ -281,10 +300,10 @@ private void DoGenerate(object state) { task.Initialize(originalImage, PreviewImageViewModel.PreviewImage == originalImage); } - catch (Exception e) when (!e.IsCriticalGdi()) + catch (Exception e) { task.SetCompleted(); - SynchronizedInvokeCallback?.Invoke(() => + TryInvokeSync(() => { GeneratePreviewError = e; SetPreview(null); @@ -293,8 +312,8 @@ private void DoGenerate(object state) return; } - Exception error = null; - Bitmap result = null; + Exception? error = null; + Bitmap? result = null; try { // starting generate: using Begin.../End... methods instead of await ...Async so it is compatible even with .NET 3.5 @@ -303,14 +322,13 @@ private void DoGenerate(object state) // ReSharper disable once AccessToDisposedClosure - false alarm, newTask.Dispose() is called only on error IsCancelRequestedCallback = () => task.IsCanceled, ThrowIfCanceled = false, - State = task, - Progress = drawingProgressManager + Progress = drawingProgressManager, }); // Waiting to be finished or canceled. As we are on a different thread blocking wait is alright result = task.EndGenerate(asyncResult); } - catch (Exception e) when (!e.IsCriticalGdi()) + catch (Exception e) { error = e; } @@ -326,7 +344,7 @@ private void DoGenerate(object state) } // applying result (or error) - SynchronizedInvokeCallback?.Invoke(() => + TryInvokeSync(() => { GeneratePreviewError = error; SetPreview(result); @@ -339,11 +357,29 @@ private void DoGenerate(object state) activeTask = null; } } + finally + { + Monitor.Exit(syncRoot); + } + } + + private void TrySetProgress(DrawingProgress progress) + { + if (IsDisposed) + return; + try + { + Progress = progress; + } + catch (ObjectDisposedException) + { + // lost race - just ignoring it + } } private void CancelRunningGenerate() { - GenerateTaskBase runningTask = activeTask; + GenerateTaskBase? runningTask = activeTask; if (runningTask == null) return; runningTask.IsCanceled = true; @@ -352,7 +388,7 @@ private void CancelRunningGenerate() private void WaitForPendingGenerate() { // In a non-UI thread it should be in a lock - GenerateTaskBase runningTask = activeTask; + GenerateTaskBase? runningTask = activeTask; if (runningTask == null) return; runningTask.WaitForCompletion(); @@ -360,10 +396,10 @@ private void WaitForPendingGenerate() activeTask = null; } - private void SetPreview(Bitmap image) + private void SetPreview(Bitmap? image) { PreviewImageViewModel preview = PreviewImageViewModel; - Image toDispose = preview.PreviewImage; + Image? toDispose = preview.PreviewImage; preview.PreviewImage = image; if (toDispose != null && toDispose != originalImage) toDispose.Dispose(); @@ -380,7 +416,7 @@ private void PreviewImage_PropertyChanged(object sender, PropertyChangedEventArg // preview image has been changed: updating IsModified accordingly if (e.PropertyName == nameof(vm.PreviewImage)) { - Image image = vm.PreviewImage; + Image? image = vm.PreviewImage; SetModified(image != null && originalImage != image); } } @@ -411,7 +447,7 @@ private void OnApplyCommand() #region Explicitly Implemented Interface Methods - Bitmap IViewModel.GetEditedModel() => PreviewImageViewModel.PreviewImage as Bitmap; + Bitmap? IViewModel.GetEditedModel() => PreviewImageViewModel.PreviewImage as Bitmap; #endregion diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/ViewModelBase.cs b/KGySoft.Drawing.ImagingTools/ViewModel/ViewModelBase.cs index 6fe8703..3d5f7ce 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/ViewModelBase.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/ViewModelBase.cs @@ -31,13 +31,30 @@ internal abstract class ViewModelBase : ObservableObjectBase, IViewModel { #region Properties - internal Action ShowErrorCallback { get => Get>(); set => Set(value); } - internal Action ShowWarningCallback { get => Get>(); set => Set(value); } - internal Action ShowInfoCallback { get => Get>(); set => Set(value); } - internal Func ConfirmCallback { get => Get>(); set => Set(value); } - internal Action ShowChildViewCallback { get => Get>(); set => Set(value); } - internal Action CloseViewCallback { get => Get(); set => Set(value); } - internal Action SynchronizedInvokeCallback { get => Get>(); set => Set(value); } + #region Internal Properties + + internal Action? ShowErrorCallback { get => Get?>(); set => Set(value); } + internal Action? ShowWarningCallback { get => Get?>(); set => Set(value); } + internal Action? ShowInfoCallback { get => Get?>(); set => Set(value); } + internal Func? ConfirmCallback { get => Get?>(); set => Set(value); } + internal Func? CancellableConfirmCallback { get => Get?>(); set => Set(value); } + internal Action? ShowChildViewCallback { get => Get?>(); set => Set(value); } + internal Action? CloseViewCallback { get => Get(); set => Set(value); } + internal Action? SynchronizedInvokeCallback { private get => Get?>(); set => Set(value); } + + #endregion + + #region Protected Properties + + protected bool IsViewLoaded { get; private set; } + + #endregion + + #endregion + + #region Constructors + + protected ViewModelBase() => Res.DisplayLanguageChanged += Res_DisplayLanguageChanged; #endregion @@ -48,13 +65,62 @@ internal abstract class ViewModelBase : ObservableObjectBase, IViewModel protected void ShowError(string message) => ShowErrorCallback?.Invoke(message); protected void ShowWarning(string message) => ShowWarningCallback?.Invoke(message); protected void ShowInfo(string message) => ShowInfoCallback?.Invoke(message); - protected bool Confirm(string message) => ConfirmCallback?.Invoke(message) ?? true; + protected bool Confirm(string message, bool isYesDefault = true) => ConfirmCallback?.Invoke(message, isYesDefault) ?? true; + protected bool? CancellableConfirm(string message, int defaultButton = 0) => CancellableConfirmCallback?.Invoke(message, defaultButton); + + protected bool TryInvokeSync(Action action) + { + if (IsDisposed) + return false; + + Action? callback; + try + { + callback = SynchronizedInvokeCallback; + } + catch (ObjectDisposedException) + { + return false; + } + + if (callback == null) + return false; + + callback.Invoke(action); + return true; + } + + protected virtual void ApplyDisplayLanguage() { } + + protected override void Dispose(bool disposing) + { + if (IsDisposed) + return; + + Res.DisplayLanguageChanged -= Res_DisplayLanguageChanged; + base.Dispose(disposing); + } #endregion #region Internal Methods - internal virtual void ViewLoaded() => SetModified(false); + internal virtual void ViewLoaded() + { + IsViewLoaded = true; + SetModified(false); + } + + #endregion + + #region Event Handlers + + private void Res_DisplayLanguageChanged(object? sender, EventArgs e) + { + // Trying to apply the new language in the thread of the corresponding view + if (!TryInvokeSync(ApplyDisplayLanguage)) + ApplyDisplayLanguage(); + } #endregion diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/ViewModelFactory.cs b/KGySoft.Drawing.ImagingTools/ViewModel/ViewModelFactory.cs index 8481bb3..b9b004b 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/ViewModelFactory.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/ViewModelFactory.cs @@ -16,9 +16,18 @@ #region Usings +#if NETFRAMEWORK +using System; +#endif +using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; +using System.Globalization; +#if NETFRAMEWORK +using KGySoft.ComponentModel; +using KGySoft.CoreLibraries; +#endif using KGySoft.Drawing.ImagingTools.Model; #endregion @@ -31,6 +40,18 @@ namespace KGySoft.Drawing.ImagingTools.ViewModel /// public static class ViewModelFactory { + #region Constructors + + static ViewModelFactory() + { + Res.EnsureInitialized(); +#if NETFRAMEWORK + typeof(Version).RegisterTypeConverter(); +#endif + } + + #endregion + #region Methods /// @@ -44,7 +65,7 @@ public static class ViewModelFactory /// /// The command line arguments. /// An instance that represents a view model for the specified command line arguments. - public static IViewModel FromCommandLineArguments(string[] args) => new DefaultViewModel { CommandLineArguments = args }; + public static IViewModel FromCommandLineArguments(string[]? args) => new DefaultViewModel { CommandLineArguments = args }; /// /// Creates a view model from an . @@ -52,7 +73,8 @@ public static class ViewModelFactory /// The image. /// , to create a read-only instance; otherwise, . /// An instance that represents a view model for an . - public static IViewModel FromImage(Image image, bool readOnly = false) => new ImageVisualizerViewModel { Image = (Image)image?.Clone(), ReadOnly = readOnly }; + public static IViewModel FromImage(Image? image, bool readOnly = false) + => new ImageVisualizerViewModel { Image = (Image?)image?.Clone(), ReadOnly = readOnly }; /// /// Creates a view model for an from arbitrary debug information. @@ -60,7 +82,8 @@ public static class ViewModelFactory /// The debug information for an image. /// , to create a read-only instance; otherwise, . /// An instance that represents a view model for an . - public static IViewModel FromImage(ImageInfo imageInfo, bool readOnly) => new ImageVisualizerViewModel { ImageInfo = imageInfo, ReadOnly = readOnly }; + public static IViewModel FromImage(ImageInfo? imageInfo, bool readOnly) + => new ImageVisualizerViewModel { ImageInfo = imageInfo, ReadOnly = readOnly }; /// /// Creates a view model from a . @@ -68,7 +91,8 @@ public static class ViewModelFactory /// The bitmap. /// , to create a read-only instance; otherwise, . /// An instance that represents a view model for a . - public static IViewModel FromBitmap(Bitmap bitmap, bool readOnly = false) => new ImageVisualizerViewModel(AllowedImageTypes.Bitmap | AllowedImageTypes.Icon) { Image = (Bitmap)bitmap?.Clone(), ReadOnly = readOnly }; + public static IViewModel FromBitmap(Bitmap? bitmap, bool readOnly = false) + => new ImageVisualizerViewModel(AllowedImageTypes.Bitmap | AllowedImageTypes.Icon) { Image = (Bitmap?)bitmap?.Clone(), ReadOnly = readOnly }; /// /// Creates a view model for a from arbitrary debug information. @@ -76,7 +100,8 @@ public static class ViewModelFactory /// The debug information for a bitmap. /// , to create a read-only instance; otherwise, . /// An instance that represents a view model for a . - public static IViewModel FromBitmap(ImageInfo bitmapInfo, bool readOnly) => new ImageVisualizerViewModel(AllowedImageTypes.Bitmap | AllowedImageTypes.Icon) { ImageInfo = bitmapInfo, ReadOnly = readOnly }; + public static IViewModel FromBitmap(ImageInfo? bitmapInfo, bool readOnly) + => new ImageVisualizerViewModel(AllowedImageTypes.Bitmap | AllowedImageTypes.Icon) { ImageInfo = bitmapInfo, ReadOnly = readOnly }; /// /// Creates a view model from a . @@ -84,7 +109,8 @@ public static class ViewModelFactory /// The metafile. /// , to create a read-only instance; otherwise, . /// An instance that represents a view model for a . - public static IViewModel FromMetafile(Metafile metafile, bool readOnly = false) => new ImageVisualizerViewModel(AllowedImageTypes.Metafile) { Image = (Metafile)metafile?.Clone(), ReadOnly = readOnly }; + public static IViewModel FromMetafile(Metafile? metafile, bool readOnly = false) + => new ImageVisualizerViewModel(AllowedImageTypes.Metafile) { Image = (Metafile?)metafile?.Clone(), ReadOnly = readOnly }; /// /// Creates a view model for a from arbitrary debug information. @@ -92,7 +118,8 @@ public static class ViewModelFactory /// The debug information for a metafile. /// , to create a read-only instance; otherwise, . /// An instance that represents a view model for a . - public static IViewModel FromMetafile(ImageInfo metafileInfo, bool readOnly) => new ImageVisualizerViewModel(AllowedImageTypes.Metafile) { ImageInfo = metafileInfo, ReadOnly = readOnly }; + public static IViewModel FromMetafile(ImageInfo? metafileInfo, bool readOnly) + => new ImageVisualizerViewModel(AllowedImageTypes.Metafile) { ImageInfo = metafileInfo, ReadOnly = readOnly }; /// /// Creates a view model from an . @@ -100,7 +127,8 @@ public static class ViewModelFactory /// The icon. /// , to create a read-only instance; otherwise, . /// An instance that represents a view model for an . - public static IViewModel FromIcon(Icon icon, bool readOnly = false) => new ImageVisualizerViewModel(AllowedImageTypes.Icon) { Icon = (Icon)icon?.Clone(), ReadOnly = readOnly }; + public static IViewModel FromIcon(Icon? icon, bool readOnly = false) + => new ImageVisualizerViewModel(AllowedImageTypes.Icon) { Icon = (Icon?)icon?.Clone(), ReadOnly = readOnly }; /// /// Creates a view model for an from arbitrary debug information. @@ -108,7 +136,8 @@ public static class ViewModelFactory /// The debug information for an icon. /// , to create a read-only instance; otherwise, . /// An instance that represents a view model for an . - public static IViewModel FromIcon(ImageInfo iconInfo, bool readOnly) => new ImageVisualizerViewModel(AllowedImageTypes.Icon) { ImageInfo = iconInfo, ReadOnly = readOnly }; + public static IViewModel FromIcon(ImageInfo? iconInfo, bool readOnly) + => new ImageVisualizerViewModel(AllowedImageTypes.Icon) { ImageInfo = iconInfo, ReadOnly = readOnly }; /// /// Creates a view model from a palette. @@ -132,35 +161,37 @@ public static class ViewModelFactory /// If the provided path is among the detected Visual Studio installations, then it will be preselected in the view. This parameter is optional. ///
Default value: . /// An instance that represents a view model for managing debugger visualizer installations. - public static IViewModel CreateManageInstallations(string hintPath = null) => new ManageInstallationsViewModel(hintPath); + public static IViewModel CreateManageInstallations(string? hintPath = null) => new ManageInstallationsViewModel(hintPath); /// /// Creates a view model from a . /// /// The bitmap data. /// An instance that represents a view model for a . - public static IViewModel FromBitmapData(BitmapData bitmapData) => new BitmapDataVisualizerViewModel { BitmapDataInfo = new BitmapDataInfo(bitmapData) }; + public static IViewModel FromBitmapData(BitmapData? bitmapData) + => new BitmapDataVisualizerViewModel { BitmapDataInfo = bitmapData == null ? null : new BitmapDataInfo(bitmapData) }; /// /// Creates a view model for a from arbitrary debug information. /// /// The debug information for a . /// An instance that represents a view model for a . - public static IViewModel FromBitmapData(BitmapDataInfo bitmapDataInfo) => new BitmapDataVisualizerViewModel { BitmapDataInfo = bitmapDataInfo }; + public static IViewModel FromBitmapData(BitmapDataInfo? bitmapDataInfo) => new BitmapDataVisualizerViewModel { BitmapDataInfo = bitmapDataInfo }; /// /// Creates a view model from a . /// /// The graphics. /// An instance that represents a view model for a . - public static IViewModel FromGraphics(Graphics graphics) => new GraphicsVisualizerViewModel { GraphicsInfo = new GraphicsInfo(graphics)}; + public static IViewModel FromGraphics(Graphics? graphics) + => new GraphicsVisualizerViewModel { GraphicsInfo = graphics == null ? null : new GraphicsInfo(graphics) }; /// /// Creates a view model for a from arbitrary debug information. /// /// The debug information for a . /// An instance that represents a view model for a . - public static IViewModel FromGraphics(GraphicsInfo graphicsInfo) => new GraphicsVisualizerViewModel { GraphicsInfo = graphicsInfo }; + public static IViewModel FromGraphics(GraphicsInfo? graphicsInfo) => new GraphicsVisualizerViewModel { GraphicsInfo = graphicsInfo }; /// /// Creates a view model for counting colors of a . @@ -174,35 +205,53 @@ public static class ViewModelFactory /// /// The bitmap to resize. /// An instance that represents a view model for resizing a . - public static IViewModel CreateResizeBitmap(Bitmap bitmap) => new ResizeBitmapViewModel(bitmap); + public static IViewModel CreateResizeBitmap(Bitmap bitmap) => new ResizeBitmapViewModel(bitmap); /// /// Creates a view model for adjusting of a with quantizing and dithering. /// /// The bitmap to transform. /// An instance that represents a view model for adjusting the colors of a . - public static IViewModel CreateAdjustColorSpace(Bitmap bitmap) => new ColorSpaceViewModel(bitmap); + public static IViewModel CreateAdjustColorSpace(Bitmap bitmap) => new ColorSpaceViewModel(bitmap); /// /// Creates a view model for adjusting the brightness of a . /// /// The bitmap to adjust. /// An instance that represents a view model for adjusting the brightness of a . - public static IViewModel CreateAdjustBrightness(Bitmap bitmap) => new AdjustBrightnessViewModel(bitmap); + public static IViewModel CreateAdjustBrightness(Bitmap bitmap) => new AdjustBrightnessViewModel(bitmap); /// /// Creates a view model for adjusting the contrast of a . /// /// The bitmap to adjust. /// An instance that represents a view model for adjusting the contrast of a . - public static IViewModel CreateAdjustContrast(Bitmap bitmap) => new AdjustContrastViewModel(bitmap); + public static IViewModel CreateAdjustContrast(Bitmap bitmap) => new AdjustContrastViewModel(bitmap); /// /// Creates a view model for adjusting the gamma of a . /// /// The bitmap to adjust. /// An instance that represents a view model for adjusting the gamma of a . - public static IViewModel CreateAdjustGamma(Bitmap bitmap) => new AdjustGammaViewModel(bitmap); + public static IViewModel CreateAdjustGamma(Bitmap bitmap) => new AdjustGammaViewModel(bitmap); + + /// + /// Creates a view model for managing language settings. + /// + /// An instance that represents a view model for managing language settings. + public static IViewModel CreateLanguageSettings() => new LanguageSettingsViewModel(); + + /// + /// Creates a view model for managing language settings. + /// + /// An instance that represents a view model for managing language settings. + public static IViewModel CreateEditResources(CultureInfo culture) => new EditResourcesViewModel(culture); + + /// + /// Creates a view model for downloading resources. + /// + /// An instance that represents a view model for managing language settings. + public static IViewModel> CreateDownloadResources() => new DownloadResourcesViewModel(); #endregion } diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/_Interfaces/IViewModel.cs b/KGySoft.Drawing.ImagingTools/ViewModel/_Interfaces/IViewModel.cs index aa824b6..1905c6c 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/_Interfaces/IViewModel.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/_Interfaces/IViewModel.cs @@ -28,5 +28,13 @@ namespace KGySoft.Drawing.ImagingTools.ViewModel ///
public interface IViewModel : IDisposable { + #region Properties + + /// + /// Gets whether the model instance that belongs this view model instance is modified. + /// + bool IsModified { get; } + + #endregion } } \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/ViewModel/_Interfaces/IViewModel`1.cs b/KGySoft.Drawing.ImagingTools/ViewModel/_Interfaces/IViewModel`1.cs index a7d0cfe..b3a3310 100644 --- a/KGySoft.Drawing.ImagingTools/ViewModel/_Interfaces/IViewModel`1.cs +++ b/KGySoft.Drawing.ImagingTools/ViewModel/_Interfaces/IViewModel`1.cs @@ -24,19 +24,10 @@ namespace KGySoft.Drawing.ImagingTools.ViewModel /// public interface IViewModel : IViewModel { - #region Properties - - /// - /// Gets whether the model instance that belongs this view model instance is modified. - /// - bool IsModified { get; } - - #endregion - #region Methods /// - /// If returns , then this method returns the edited model. + /// If returns , then this method returns the edited model. /// /// The edited model. TModel GetEditedModel(); diff --git a/KGySoft.Drawing.ImagingTools/WinApi/CWPRETSTRUCT.cs b/KGySoft.Drawing.ImagingTools/WinApi/CWPRETSTRUCT.cs new file mode 100644 index 0000000..9689961 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/WinApi/CWPRETSTRUCT.cs @@ -0,0 +1,40 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: CWPRETSTRUCT.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Runtime.InteropServices; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.WinApi +{ + [StructLayout(LayoutKind.Sequential)] + // ReSharper disable once InconsistentNaming + internal struct CWPRETSTRUCT + { + #region Fields + + internal IntPtr lResult; + internal IntPtr lParam; + internal IntPtr wParam; + internal uint message; + internal IntPtr hwnd; + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/WinApi/Constants.cs b/KGySoft.Drawing.ImagingTools/WinApi/Constants.cs index 4d7c200..0474e58 100644 --- a/KGySoft.Drawing.ImagingTools/WinApi/Constants.cs +++ b/KGySoft.Drawing.ImagingTools/WinApi/Constants.cs @@ -19,14 +19,28 @@ namespace KGySoft.Drawing.ImagingTools.WinApi internal static class Constants { #region Constants + // ReSharper disable InconsistentNaming internal const int WS_BORDER = 0x00800000; - internal const int WM_MOUSEHWHEEL = 0x020E; + + internal const int WM_PAINT = 0x0F; internal const int WM_MOUSEACTIVATE = 0x021; + internal const int WM_INITDIALOG = 0x0110; + internal const int WM_MOUSEHWHEEL = 0x020E; - internal const int MA_ACTIVATEANDEAT= 2; + internal const int MA_ACTIVATEANDEAT = 2; internal const int MA_ACTIVATE = 1; + internal const int WH_CALLWNDPROCRET = 12; + + internal const int IDOK = 1; + internal const int IDCANCEL = 2; + + internal const string ClassNameDialogBox = "#32770"; + internal const string ClassNameButton = "Button"; + internal const string ClassNameStatic = "Static"; + + // ReSharper restore InconsistentNaming #endregion } } \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/WinApi/EnumChildProc.cs b/KGySoft.Drawing.ImagingTools/WinApi/EnumChildProc.cs new file mode 100644 index 0000000..617769c --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/WinApi/EnumChildProc.cs @@ -0,0 +1,33 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: EnumChildProc.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.WinApi +{ + /// + /// An application-defined callback function used with the EnumChildWindows function. It receives the child window handles. + /// The WNDENUMPROC type defines a pointer to this callback function. EnumChildProc is a placeholder for the application-defined function name. + /// + /// A handle to a child window of the parent window specified in EnumChildWindows. + /// The application-defined value given in EnumChildWindows. + /// To continue enumeration, the callback function must return TRUE; to stop enumeration, it must return FALSE. + internal delegate bool EnumChildProc(IntPtr hWnd, IntPtr lParam); +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/WinApi/HOOKPROC.cs b/KGySoft.Drawing.ImagingTools/WinApi/HOOKPROC.cs new file mode 100644 index 0000000..f545df7 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/WinApi/HOOKPROC.cs @@ -0,0 +1,36 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: HOOKPROC.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; + +#endregion + +namespace KGySoft.Drawing.ImagingTools.WinApi +{ + /// + /// An application-defined or library-defined callback function used with the SetWindowsHookEx function. The system calls this function after the SendMessage function is called. + /// The hook procedure can examine the message; it cannot modify it. + /// The HOOKPROC type defines a pointer to this callback function. CallWndRetProc is a placeholder for the application-defined or library-defined function name. + /// + /// Specifies whether the hook procedure must process the message. If nCode is HC_ACTION, the hook procedure must process the message. + /// If nCode is less than zero, the hook procedure must pass the message to the CallNextHookEx function without further processing and must return the value returned by CallNextHookEx. + /// Specifies whether the message is sent by the current process. If the message is sent by the current process, it is nonzero; otherwise, it is NULL. + /// A pointer to a structure that contains details about the message. + /// + internal delegate IntPtr HOOKPROC(int nCode, IntPtr wParam, IntPtr lParam); +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/WinApi/Kernel32.cs b/KGySoft.Drawing.ImagingTools/WinApi/Kernel32.cs index 3e15f11..eb1b547 100644 --- a/KGySoft.Drawing.ImagingTools/WinApi/Kernel32.cs +++ b/KGySoft.Drawing.ImagingTools/WinApi/Kernel32.cs @@ -19,11 +19,13 @@ using System; using System.ComponentModel; using System.Runtime.InteropServices; +using System.Security; #endregion namespace KGySoft.Drawing.ImagingTools.WinApi { + [SecurityCritical] internal static class Kernel32 { #region NativeMethods class @@ -56,6 +58,13 @@ private static class NativeMethods [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer); + /// + /// Retrieves the thread identifier of the calling thread. + /// + /// The return value is the thread identifier of the calling thread. + [DllImport("kernel32.dll")] + internal static extern uint GetCurrentThreadId(); + #endregion } @@ -78,6 +87,8 @@ internal static long GetTotalMemory() return (long)status.ullTotalPhys; } + internal static uint GetCurrentThreadId() => NativeMethods.GetCurrentThreadId(); + #endregion } } diff --git a/KGySoft.Drawing.ImagingTools/WinApi/MEMORYSTATUSEX.cs b/KGySoft.Drawing.ImagingTools/WinApi/MEMORYSTATUSEX.cs index fbf64ac..b311259 100644 --- a/KGySoft.Drawing.ImagingTools/WinApi/MEMORYSTATUSEX.cs +++ b/KGySoft.Drawing.ImagingTools/WinApi/MEMORYSTATUSEX.cs @@ -23,6 +23,7 @@ namespace KGySoft.Drawing.ImagingTools.WinApi { [StructLayout(LayoutKind.Sequential)] + // ReSharper disable once InconsistentNaming internal struct MEMORYSTATUSEX { #region Fields diff --git a/KGySoft.Drawing.ImagingTools/WinApi/User32.cs b/KGySoft.Drawing.ImagingTools/WinApi/User32.cs new file mode 100644 index 0000000..fa7645a --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/WinApi/User32.cs @@ -0,0 +1,190 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: User32.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.ComponentModel; +#if !NET5_0_OR_GREATER +using System.Drawing; +#endif +using System.Runtime.InteropServices; +using System.Security; +#if !NET5_0_OR_GREATER +using System.Windows.Forms; +#endif + +#endregion + +namespace KGySoft.Drawing.ImagingTools.WinApi +{ + [SecurityCritical] + internal static class User32 + { + #region NativeMethods class + + private static class NativeMethods + { + #region Methods + +#if !NET5_0_OR_GREATER + /// + /// The ScreenToClient function converts the screen coordinates of a specified point on the screen to client-area coordinates. + /// + /// A handle to the window whose client area will be used for the conversion. + /// A pointer to a POINT structure that specifies the screen coordinates to be converted. + /// If the function succeeds, the return value is nonzero. + /// If the function fails, the return value is zero. + [DllImport("user32.dll")] + internal static extern bool ScreenToClient(IntPtr hWnd, ref Point lpPoint); +#endif + + /// + /// Installs an application-defined hook procedure into a hook chain. You would install a hook procedure to monitor the system for certain types of events. + /// These events are associated either with a specific thread or with all threads in the same desktop as the calling thread. + /// + /// The type of hook procedure to be installed. This parameter can be one of the WH_ values. + /// A pointer to the hook procedure. If the dwThreadId parameter is zero or specifies the identifier of a thread created by a different process, + /// the lpfn parameter must point to a hook procedure in a DLL. Otherwise, lpfn can point to a hook procedure in the code associated with the current process. + /// A handle to the DLL containing the hook procedure pointed to by the lpfn parameter. + /// The hMod parameter must be set to NULL if the dwThreadId parameter specifies a thread created by the current process and if the hook procedure is within + /// the code associated with the current process. + /// The identifier of the thread with which the hook procedure is to be associated. For desktop apps, if this parameter is zero, + /// the hook procedure is associated with all existing threads running in the same desktop as the calling thread. + /// If the function succeeds, the return value is the handle to the hook procedure. + /// If the function fails, the return value is NULL. To get extended error information, call GetLastError. + [DllImport("user32.dll", SetLastError = true)] + internal static extern IntPtr SetWindowsHookEx(int idHook, HOOKPROC lpfn, IntPtr hInstance, uint threadId); + + /// + /// Removes a hook procedure installed in a hook chain by the function. + /// + /// A handle to the hook to be removed. + /// This parameter is a hook handle obtained by a previous call to . + /// If the function succeeds, the return value is nonzero. + /// If the function fails, the return value is zero.To get extended error information, call GetLastError. + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool UnhookWindowsHookEx(IntPtr idHook); + + /// + /// Passes the hook information to the next hook procedure in the current hook chain. + /// A hook procedure can call this function either before or after processing the hook information. + /// + /// This parameter is ignored. + /// The hook code passed to the current hook procedure. + /// The next hook procedure uses this code to determine how to process the hook information. + /// The wParam value passed to the current hook procedure. + /// The meaning of this parameter depends on the type of hook associated with the current hook chain. + /// The lParam value passed to the current hook procedure. + /// The meaning of this parameter depends on the type of hook associated with the current hook chain. + /// This value is returned by the next hook procedure in the chain. The current hook procedure must also return this value. + /// The meaning of the return value depends on the hook type. For more information, see the descriptions of the individual hook procedures. + [DllImport("user32.dll")] + internal static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); + + /// + /// Retrieves the name of the class to which the specified window belongs. + /// + /// A handle to the window and, indirectly, the class to which the window belongs. + /// The class name string. + /// The length of the lpClassName buffer, in characters. + /// The buffer must be large enough to include the terminating null character; otherwise, the class name string is truncated to nMaxCount-1 characters. + /// If the function succeeds, the return value is the number of characters copied to the buffer, not including the terminating null character. + /// If the function fails, the return value is zero. To get extended error information, call GetLastError function. + [DllImport("user32.dll", SetLastError = true)] + internal static extern int GetClassName(IntPtr hWnd, [Out]char[] lpClassName, int nMaxCount); + + /// + /// Enumerates the child windows that belong to the specified parent window by passing the handle to each child window, in turn, to an application-defined callback function. + /// EnumChildWindows continues until the last child window is enumerated or the callback function returns FALSE. + /// + /// A handle to the parent window whose child windows are to be enumerated. If this parameter is NULL, this function is equivalent to EnumWindows. + /// A pointer to an application-defined callback function. For more information, see EnumChildProc. + /// An application-defined value to be passed to the callback function. + /// The return value is not used. + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EnumChildWindows(IntPtr hWndParent, EnumChildProc lpEnumFunc, IntPtr lParam); + + /// + /// Retrieves the identifier of the specified control. + /// + /// A handle to the control. + /// If the function succeeds, the return value is the identifier of the control. + /// If the function fails, the return value is zero.An invalid value for the hWnd parameter, for example, will cause the function to fail.To get extended error information, call GetLastError. + [DllImport("user32.dll", SetLastError = true)] + internal static extern int GetDlgCtrlID(IntPtr hWnd); + + /// + /// Changes the text of the specified window's title bar (if it has one). If the specified window is a control, the text of the control is changed. + /// However, SetWindowText cannot change the text of a control in another application. + /// + /// A handle to the window or control whose text is to be changed. + /// The new title or control text. + /// If the function succeeds, the return value is nonzero. + /// If the function fails, the return value is zero.To get extended error information, call GetLastError. + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool SetWindowText(IntPtr hWnd, string lpString); + + #endregion + } + + #endregion + + #region Fields + + /// + /// Used in , length is enough for the longest possible name with null terminator as described here: https://docs.microsoft.com/en-us/windows/win32/winmsg/about-window-classes + /// + private static readonly char[] classNameBuf = new char[11]; + + #endregion + + #region Methods + +#if !NET5_0_OR_GREATER + internal static void ScreenToClient(Control control, ref Point point) => NativeMethods.ScreenToClient(control.Handle, ref point); +#endif + + internal static IntPtr HookCallWndRetProc(HOOKPROC hookProc) + => NativeMethods.SetWindowsHookEx(Constants.WH_CALLWNDPROCRET, hookProc, IntPtr.Zero, Kernel32.GetCurrentThreadId()); + + internal static void UnhookWindowsHook(IntPtr hook) => NativeMethods.UnhookWindowsHookEx(hook); + + internal static IntPtr CallNextHook(int code, IntPtr wParam, IntPtr lParam) + => NativeMethods.CallNextHookEx(IntPtr.Zero, code, wParam, lParam); + + internal static string GetClassName(IntPtr handle) + { + if (handle == IntPtr.Zero) + throw new ArgumentNullException(nameof(handle), PublicResources.ArgumentNull); + int length = NativeMethods.GetClassName(handle, classNameBuf, classNameBuf.Length); + if (length == 0) + throw new Win32Exception(Marshal.GetLastWin32Error()); + return new String(classNameBuf, 0, length); + } + + internal static void EnumChildWindows(IntPtr handle, EnumChildProc enumProc) => NativeMethods.EnumChildWindows(handle, enumProc, IntPtr.Zero); + + internal static int GetDialogControlId(IntPtr handle) => NativeMethods.GetDlgCtrlID(handle); + + internal static void SetControlText(IntPtr handle, string text) => NativeMethods.SetWindowText(handle, text); + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/_Classes/Configuration.cs b/KGySoft.Drawing.ImagingTools/_Classes/Configuration.cs new file mode 100644 index 0000000..b92620b --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/_Classes/Configuration.cs @@ -0,0 +1,204 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: Configuration.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +#if NET35 +using System.ComponentModel; +#endif +using System.Configuration; +using System.Globalization; +#if NETFRAMEWORK +using System.Net; +#endif +using System.Reflection; +using System.Runtime.CompilerServices; + +#if NET35 +using KGySoft.CoreLibraries; +#endif +using KGySoft.Drawing.ImagingTools.Properties; + +#endregion + +namespace KGySoft.Drawing.ImagingTools +{ + internal static class Configuration + { + #region Nested classes + +#if NET35 + /// + /// This class is needed for .NET 3.5, which emits display name of a language instead of the property. + /// + private sealed class CultureInfoConverterFixed : CultureInfoConverter + { + #region Methods + + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (value == null) + return String.Empty; + if (value is not CultureInfo ci) + throw new ArgumentException(PublicResources.NotAnInstanceOfType(typeof(CultureInfo))); + + return destinationType == typeof(string) ? ci.Name : base.ConvertTo(context, culture, value, destinationType); + } + + #endregion + } +#endif + + #endregion + + #region Constants + + private const string defaultResourceRepositoryLocation = "https://koszeggy.github.io/KGySoft.Drawing.Tools/res/"; // same as "https://raw.githubusercontent.com/koszeggy/KGySoft.Drawing.Tools/pages/res/" + private const string fallbackResourceRepositoryLocation = "http://kgysoft.net/res/"; // "http://koszeggy.github.io/KGySoft.Drawing.Tools/res/" does not work on Win7/.NET 3.5 + + #endregion + + #region Fields + + private static readonly bool allowHttps; + + private static Uri? baseUri; + + #endregion + + #region Properties + + #region Internal Properties + + internal static bool AllowResXResources { get => GetFromSettings(); set => SetInSettings(value); } + internal static bool UseOSLanguage { get => GetFromSettings(); set => SetInSettings(value); } + internal static CultureInfo DisplayLanguage { get => GetFromSettings() ?? Res.DefaultLanguage; set => SetInSettings(value); } + internal static Uri BaseUri => baseUri ??= new Uri(ResourceRepositoryLocation); + + #endregion + + #region Private Properties + + private static string ResourceRepositoryLocation => GetFromAppConfig() + ?? (allowHttps ? defaultResourceRepositoryLocation : fallbackResourceRepositoryLocation); + + #endregion + + #endregion + + #region Constructors + + static Configuration() + { + allowHttps = !(OSUtils.IsMono && OSUtils.IsWindows); +#if NET35 + allowHttps &= OSUtils.IsWindows8OrLater; +#endif + + // To be able to resolve UserSettingsGroup of with other framework version + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; +#if NET35 + // To prevent serializing CultureInfo by DisplayName instead of Name + typeof(CultureInfo).RegisterTypeConverter(); +#endif + +#if NETFRAMEWORK + try + { + // To be able to use HTTP requests with TLS 1.2 security protocol (may not work on Windows XP) + ServicePointManager.SecurityProtocol |= +#if NET35 || NET40 + (SecurityProtocolType)3072; +#else + SecurityProtocolType.Tls12; +#endif + } + catch (NotSupportedException) + { + allowHttps = false; + } +#endif + } + + #endregion + + #region Methods + + #region Internal Methods + + internal static void SaveSettings() => Settings.Default.Save(); + + #endregion + + #region Private Methods + + private static T? GetFromSettings([CallerMemberName]string propertyName = null!) + { + try + { + return (T)Settings.Default[propertyName]; + } + catch (Exception e) when (!e.IsCritical()) + { + return default; + } + } + + private static void SetInSettings(object value, [CallerMemberName]string propertyName = null!) + { + try + { + Settings.Default[propertyName] = value; + } + catch (Exception e) when (!e.IsCritical()) + { + } + } + + private static string? GetFromAppConfig([CallerMemberName]string propertyName = null!) + { + try + { + return ConfigurationManager.AppSettings[propertyName]; + } + catch (Exception e) when (!e.IsCritical()) + { + return null; + } + } + + #endregion + + #region Event handlers + + private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args) + { +#if NETFRAMEWORK + if (args.Name.StartsWith("System, Version=", StringComparison.Ordinal)) + return typeof(UserSettingsGroup).Assembly; +#elif NETCOREAPP + if (args.Name.StartsWith("System.Configuration.ConfigurationManager, Version=", StringComparison.Ordinal)) + return typeof(UserSettingsGroup).Assembly; +#endif + return null; + } + + #endregion + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/_Classes/DrawingProgressManager.cs b/KGySoft.Drawing.ImagingTools/_Classes/DrawingProgressManager.cs index eb6b0b7..907525a 100644 --- a/KGySoft.Drawing.ImagingTools/_Classes/DrawingProgressManager.cs +++ b/KGySoft.Drawing.ImagingTools/_Classes/DrawingProgressManager.cs @@ -35,10 +35,7 @@ internal class DrawingProgressManager : IDrawingProgress #region Constructors - internal DrawingProgressManager(Action reportCallback) - { - this.reportCallback = reportCallback; - } + internal DrawingProgressManager(Action reportCallback) => this.reportCallback = reportCallback; #endregion diff --git a/KGySoft.Drawing.ImagingTools/_Classes/InstallationManager.cs b/KGySoft.Drawing.ImagingTools/_Classes/InstallationManager.cs index 43436c5..30a3d8b 100644 --- a/KGySoft.Drawing.ImagingTools/_Classes/InstallationManager.cs +++ b/KGySoft.Drawing.ImagingTools/_Classes/InstallationManager.cs @@ -18,11 +18,16 @@ using System; using System.IO; +#if !NET35 using System.Linq; +#endif +using System.Security; using KGySoft.CoreLibraries; using KGySoft.Drawing.ImagingTools.Model; +#if NET45 using KGySoft.Drawing.ImagingTools.WinApi; +#endif #endregion @@ -50,13 +55,11 @@ public static class InstallationManager debuggerVisualizerFileName }; - private static InstallationInfo availableVersion; + private static InstallationInfo? availableVersion; #endregion - #region Methods - - #region Public Methods + #region Properties /// /// Gets the available debugger visualizer version that can be installed with the class. @@ -64,6 +67,17 @@ public static class InstallationManager /// public static InstallationInfo AvailableVersion => availableVersion ??= new InstallationInfo(Files.GetExecutingPath()); + /// + /// Gets version of the currently used ImagingTools application. + /// + public static Version ImagingToolsVersion { get; } = typeof(InstallationManager).Assembly.GetName().Version!; + + #endregion + + #region Methods + + #region Public Methods + /// /// Gets the installation information of the debugger visualizer for the specified . /// @@ -77,7 +91,8 @@ public static class InstallationManager /// The directory where the debugger visualizers have to be installed. /// If the installation fails, then this parameter returns the error message; otherwise, this parameter returns . /// If the installation succeeds with warnings, then this parameter returns the warning message; otherwise, this parameter returns . - public static void Install(string directory, out string error, out string warning) + [SecuritySafeCritical] + public static void Install(string directory, out string? error, out string? warning) { if (directory == null) throw new ArgumentNullException(nameof(directory), PublicResources.ArgumentNull); @@ -114,7 +129,12 @@ public static void Install(string directory, out string error, out string warnin Uninstall(directory, out error); if (error != null) + { + // VS issue workaround, see the comment in Uninstall + if (error == Res.ErrorMessageInstallationCannotBeRemoved) + error = Res.ErrorMessageInstallationCannotBeOverwritten; return; + } foreach (string file in files) { @@ -187,13 +207,14 @@ public static void Install(string directory, out string error, out string warnin ///
/// The directory where the debugger visualizers have to be removed from. /// If the removal fails, then this parameter returns the error message; otherwise, this parameter returns . - public static void Uninstall(string directory, out string error) + public static void Uninstall(string directory, out string? error) { error = null; if (!Directory.Exists(directory)) return; - if (directory == Files.GetExecutingPath()) + string executingPath = Files.GetExecutingPath(); + if (directory == executingPath) { error = Res.ErrorMessageInstallationCannotBeRemoved; return; @@ -211,7 +232,13 @@ public static void Uninstall(string directory, out string error) } catch (Exception e) when (!e.IsCritical()) { - error = Res.ErrorMessageCouldNotDeleteFile(file, e.Message); + // VS issue workaround: if the package is installed and we try to remove the executed version during debugging, then the executing path will be + // the package installation path in uppercase, instead of the target directory (the path is the directory if the package is not installed though). + // However, the same path is in lowercase if we start the removing from the VS Tools menu instead of debugging... + if (executingPath == executingPath.ToUpperInvariant() && e is UnauthorizedAccessException) + error = Res.ErrorMessageInstallationCannotBeRemoved; + else + error = Res.ErrorMessageCouldNotDeleteFile(file, e.Message); return; } } diff --git a/KGySoft.Drawing.ImagingTools/_Classes/MemoryHelper.cs b/KGySoft.Drawing.ImagingTools/_Classes/MemoryHelper.cs index f83223d..f127d39 100644 --- a/KGySoft.Drawing.ImagingTools/_Classes/MemoryHelper.cs +++ b/KGySoft.Drawing.ImagingTools/_Classes/MemoryHelper.cs @@ -17,8 +17,8 @@ #region Usings using System; - #if NETFRAMEWORK +using System.Security; using KGySoft.Drawing.ImagingTools.WinApi; #endif @@ -48,6 +48,7 @@ internal static class MemoryHelper private static long MaxMemoryForGC #if NETFRAMEWORK { + [SecuritySafeCritical] get { if (maxMemoryForGC == null) diff --git a/KGySoft.Drawing.ImagingTools/_Classes/OSUtils.cs b/KGySoft.Drawing.ImagingTools/_Classes/OSUtils.cs index 8989301..fdc8c29 100644 --- a/KGySoft.Drawing.ImagingTools/_Classes/OSUtils.cs +++ b/KGySoft.Drawing.ImagingTools/_Classes/OSUtils.cs @@ -31,12 +31,16 @@ internal static class OSUtils private static bool? isVistaOrLater; private static bool? isWin8OrLater; private static bool? isWindows; + private static bool? isLinux; + private static bool? isMono; #endregion #region Properties internal static bool IsWindows => isWindows ??= Environment.OSVersion.Platform.In(PlatformID.Win32NT, PlatformID.Win32Windows); + internal static bool IsLinux => isLinux ??= Environment.OSVersion.Platform.In(PlatformID.Unix, (PlatformID)128); + internal static bool IsMono => isMono ??= Type.GetType("Mono.Runtime") != null; internal static bool IsVistaOrLater { diff --git a/KGySoft.Drawing.ImagingTools/_Classes/ResHelper.cs b/KGySoft.Drawing.ImagingTools/_Classes/ResHelper.cs new file mode 100644 index 0000000..ee67c70 --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/_Classes/ResHelper.cs @@ -0,0 +1,150 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: ResHelper.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Reflection; + +using KGySoft.Collections; +using KGySoft.CoreLibraries; +using KGySoft.Drawing.ImagingTools.Model; +using KGySoft.Reflection; +using KGySoft.Resources; + +#endregion + +namespace KGySoft.Drawing.ImagingTools +{ + internal static class ResHelper + { + #region Constants + + private const string coreLibrariesBaseName = "KGySoft.CoreLibraries.Messages"; + private const string drawingLibrariesBaseName = "KGySoft.Drawing.Messages"; + private const string imagingToolsBaseName = "KGySoft.Drawing.ImagingTools.Messages"; + + #endregion + + #region Fields + + private static StringKeyedDictionary? culturesCache; + private static DynamicResourceManager[]? knownResourceManagers; + + #endregion + + #region Properties + + private static StringKeyedDictionary CulturesCache + => culturesCache ??= CultureInfo.GetCultures(CultureTypes.AllCultures).ToStringKeyedDictionary(ci => ci.Name); + + private static DynamicResourceManager[] KnownResourceManagers + => knownResourceManagers ??= new[] + { + (DynamicResourceManager)Reflector.GetField(Reflector.ResolveType(typeof(LanguageSettings).Assembly, "KGySoft.Res")!, "resourceManager")!, + (DynamicResourceManager)Reflector.GetField(Reflector.ResolveType(typeof(DrawingModule).Assembly, "KGySoft.Res")!, "resourceManager")!, + (DynamicResourceManager)Reflector.GetField(typeof(Res), "resourceManager")! + }; + + #endregion + + #region Methods + + internal static HashSet GetAvailableLanguages() + { + string dir = Res.ResourcesDir; + var result = new HashSet(); + try + { + if (!Directory.Exists(dir)) + return result; + + int startIndex = dir.Length + imagingToolsBaseName.Length + 2; + string[] files = Directory.GetFiles(dir, $"{imagingToolsBaseName}.*.resx", SearchOption.TopDirectoryOnly); + foreach (string file in files) + { + StringSegment resName = file.AsSegment(startIndex, file.Length - startIndex - 5); + if (CulturesCache.TryGetValue(resName, out CultureInfo? ci) && !ci.Equals(CultureInfo.InvariantCulture)) + result.Add(ci); + } + + // checking the invariant resource as it should act as default language + if (!result.Contains(Res.DefaultLanguage) && File.Exists(Path.Combine(dir, $"{imagingToolsBaseName}.resx"))) + result.Add(Res.DefaultLanguage); + return result; + } + catch (Exception e) when (!e.IsCritical()) + { + result.Clear(); + return result; + } + } + + internal static bool TryGetCulture(string name, [MaybeNullWhen(false)] out CultureInfo culture) => CulturesCache.TryGetValue(name, out culture); + + internal static string GetBaseName(LocalizableLibraries library) => library switch + { + LocalizableLibraries.CoreLibraries => coreLibrariesBaseName, + LocalizableLibraries.DrawingLibraries => drawingLibrariesBaseName, + LocalizableLibraries.ImagingTools => imagingToolsBaseName, + _ => throw new ArgumentOutOfRangeException(nameof(library), PublicResources.EnumOutOfRange(library)) + }; + + internal static Assembly GetAssembly(LocalizableLibraries library) => library switch + { + LocalizableLibraries.CoreLibraries => typeof(LanguageSettings).Assembly, + LocalizableLibraries.DrawingLibraries => typeof(DrawingModule).Assembly, + LocalizableLibraries.ImagingTools => typeof(Res).Assembly, + _ => throw new ArgumentOutOfRangeException(nameof(library), PublicResources.EnumOutOfRange(library)) + }; + + /// + /// Generates possible missing resources for the current language in memory so next Save will persist them. + /// TODO: Remove when LanguageSettings.EnsureResourcesGenerated will be available. + /// + internal static void EnsureResourcesGenerated() + { + foreach (DynamicResourceManager resourceManager in KnownResourceManagers) + resourceManager.GetExpandoResourceSet(Res.DisplayLanguage, ResourceSetRetrieval.CreateIfNotExists, true); + } + + /// + /// Saves the pending resources of centralized DRMs. + /// TODO: Remove when LanguageSettings.SavePendingResources will be available. + /// + internal static void SavePendingResources() + { + foreach (DynamicResourceManager resourceManager in KnownResourceManagers) + resourceManager.SaveAllResources(); + } + + /// + /// Releases all resource sets without saving of centralized DRMs. + /// TODO: Remove when LanguageSettings.ReleaseAllResources will be available. + /// + internal static void ReleaseAllResources() + { + foreach (DynamicResourceManager resourceManager in KnownResourceManagers) + resourceManager.ReleaseAllResources(); + } + + #endregion + } +} diff --git a/KGySoft.Drawing.ImagingTools/_Extensions/CommandBindingsCollectionExtensions.cs b/KGySoft.Drawing.ImagingTools/_Extensions/CommandBindingsCollectionExtensions.cs index 25b7f23..1999e48 100644 --- a/KGySoft.Drawing.ImagingTools/_Extensions/CommandBindingsCollectionExtensions.cs +++ b/KGySoft.Drawing.ImagingTools/_Extensions/CommandBindingsCollectionExtensions.cs @@ -41,7 +41,7 @@ internal static class CommandBindingsCollectionExtensions #region Fields - private static ICommand propertyChangedCommand = new SourceAwareCommand(OnPropertyChangedCommand); + private static readonly ICommand propertyChangedCommand = new SourceAwareCommand(OnPropertyChangedCommand); #endregion @@ -50,7 +50,7 @@ internal static class CommandBindingsCollectionExtensions #region Internal Methods internal static void AddTwoWayPropertyBinding(this CommandBindingsCollection collection, object source, string sourcePropertyName, object target, - string targetPropertyName = null, Func format = null, Func parse = null) + string? targetPropertyName = null, Func? format = null, Func? parse = null) { collection.AddPropertyBinding(source, sourcePropertyName, targetPropertyName ?? sourcePropertyName, format, target); collection.AddPropertyBinding(target, targetPropertyName ?? sourcePropertyName, sourcePropertyName, parse, source); @@ -63,7 +63,7 @@ internal static ICommandBinding AddPropertyChangedHandler(this CommandBindingsCo if (propertyNames == null) throw new ArgumentNullException(nameof(propertyNames)); - var state = new Dictionary + var state = new Dictionary { [stateHandler] = handler, [statePropertyNames] = propertyNames @@ -81,7 +81,7 @@ private static void OnPropertyChangedCommand(ICommandSource(statePropertyNames))) return; - state.GetValueOrDefault(stateHandler)?.Invoke(); + state.GetValueOrDefault(stateHandler)?.Invoke(); } #endregion diff --git a/KGySoft.Drawing.ImagingTools/_Extensions/Int32Extensions.cs b/KGySoft.Drawing.ImagingTools/_Extensions/Int32Extensions.cs new file mode 100644 index 0000000..ddf8beb --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/_Extensions/Int32Extensions.cs @@ -0,0 +1,34 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: Int32Extensions.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; + +#endregion + +namespace KGySoft.Drawing.ImagingTools +{ + internal static class Int32Extensions + { + #region Methods + + internal static int Scale(this int size, float scale) => + (int)MathF.Round(size * scale); + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/_Extensions/PixelFormatExtensions.cs b/KGySoft.Drawing.ImagingTools/_Extensions/PixelFormatExtensions.cs index 9e0b7ff..425eb6e 100644 --- a/KGySoft.Drawing.ImagingTools/_Extensions/PixelFormatExtensions.cs +++ b/KGySoft.Drawing.ImagingTools/_Extensions/PixelFormatExtensions.cs @@ -27,9 +27,12 @@ internal static class PixelFormatExtensions #region Methods internal static bool CanBeDithered(this PixelFormat dstFormat) => dstFormat.ToBitsPerPixel() <= 16 && dstFormat != PixelFormat.Format16bppGrayScale; + + // ReSharper disable BitwiseOperatorOnEnumWithoutFlags internal static bool HasAlpha(this PixelFormat pixelFormat) => (pixelFormat & PixelFormat.Alpha) == PixelFormat.Alpha; internal static bool IsIndexed(this PixelFormat pixelFormat) => (pixelFormat & PixelFormat.Indexed) == PixelFormat.Indexed; internal static bool IsWide(this PixelFormat pixelFormat) => (pixelFormat & PixelFormat.Extended) == PixelFormat.Extended; + // ReSharper restore BitwiseOperatorOnEnumWithoutFlags #endregion } diff --git a/KGySoft.Drawing.ImagingTools/_Extensions/StringExtensions.cs b/KGySoft.Drawing.ImagingTools/_Extensions/StringExtensions.cs new file mode 100644 index 0000000..9abc90f --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/_Extensions/StringExtensions.cs @@ -0,0 +1,52 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: StringExtensions.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System.Globalization; +using System.Text; + +#endregion + +namespace KGySoft.Drawing.ImagingTools +{ + internal static class StringExtensions + { + #region Methods + + /// + /// Removes accents from strings for better chances to match a filter pattern when searching. + /// + internal static string StripAccents(this string s) + { + string decomposed = s.Normalize(NormalizationForm.FormKD); + int len = decomposed.Length; + var stripped = new StringBuilder(len); + for (int i = 0; i < len; i++) + { + UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(decomposed[i]); + if (category != UnicodeCategory.NonSpacingMark) + stripped.Append(decomposed[i]); + } + + // Note: the string is returned in a decomposed form, which is OK for searching but not for displaying it. + // If it had to be displayed, then a recombining normalization would also be necessary. + return stripped.ToString(); + } + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.ImagingTools/_Extensions/VersionExtensions.cs b/KGySoft.Drawing.ImagingTools/_Extensions/VersionExtensions.cs new file mode 100644 index 0000000..e55912a --- /dev/null +++ b/KGySoft.Drawing.ImagingTools/_Extensions/VersionExtensions.cs @@ -0,0 +1,43 @@ +#region Copyright + +/////////////////////////////////////////////////////////////////////////////// +// File: VersionExtensions.cs +/////////////////////////////////////////////////////////////////////////////// +// Copyright (C) KGy SOFT, 2005-2021 - All Rights Reserved +// +// You should have received a copy of the LICENSE file at the top-level +// directory of this distribution. If not, then this file is considered as +// an illegal copy. +// +// Unauthorized copying of this file, via any medium is strictly prohibited. +/////////////////////////////////////////////////////////////////////////////// + +#endregion + +#region Usings + +using System; + +#endregion + +namespace KGySoft.Drawing.ImagingTools +{ + internal static class VersionExtensions + { + #region Methods + + internal static bool NormalizedEquals(this Version? version, Version? other) + { + if (version is null && other is null) + return true; + if (version is null || other is null) + return false; + return version.Major == other.Major + && version.Minor == other.Minor + && (version.Build == other.Build || version.Build <= 0 && other.Build <= 0) + && (version.Revision == other.Revision || version.Revision <= 0 && other.Revision <= 0); + } + + #endregion + } +} \ No newline at end of file diff --git a/KGySoft.Drawing.Tools.sln.DotSettings b/KGySoft.Drawing.Tools.sln.DotSettings index 9152776..41c7464 100644 --- a/KGySoft.Drawing.Tools.sln.DotSettings +++ b/KGySoft.Drawing.Tools.sln.DotSettings @@ -1,5 +1,10 @@  + HINT + SUGGESTION + ERROR DO_NOT_SHOW + HINT + HINT True True NEVER @@ -9,10 +14,22 @@ UseVarWhenEvident UseVarWhenEvident UseVarWhenEvident + GC + NC + OS + UI + VM + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> True True True - True \ No newline at end of file + True + True + True + True + True + True + True \ No newline at end of file diff --git a/README.md b/README.md index 79774a2..e1f349a 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ -[![KGy SOFT .net](http://docs.kgysoft.net/drawing/icons/logo.png)](https://kgysoft.net) +[![KGy SOFT .net](https://user-images.githubusercontent.com/27336165/124292367-c93f3d00-db55-11eb-8003-6d943ee7d7fa.png)](https://kgysoft.net) # KGy SOFT Drawing Tools -KGy SOFT Drawing Tools repository contains [Debugger Visualizers](#debugger-visualizers) for several `System.Drawing` types such as `Bitmap`, `Metafile`, `Icon`, `BitmapData`, `Graphics`, etc. (see also below). The visualizers use [KGy SOFT Imaging Tools](#kgy-soft-imaging-tools) to display these types visually, which can be executed as a standalone application as well. Along with the [Debugger Visualizers Test Tool](#debugger-visualizers-test-tool) it can be considered also as a demonstration of the features of the [KGy SOFT Drawing Libraries](https://kgysoft.net/drawing). +The KGy SOFT Drawing Tools repository contains a couple of applications and some [Debugger Visualizers](#debugger-visualizers) for several `System.Drawing` types such as `Bitmap`, `Metafile`, `Icon`, `BitmapData`, `Graphics`, etc. (see also below). The visualizers use [KGy SOFT Imaging Tools](#kgy-soft-imaging-tools) to display these types visually, which can be executed as a standalone application as well. Along with the [Debugger Visualizers Test Tool](#debugger-visualizers-test-tool) it can be considered also as a demonstration of the features of the [KGy SOFT Drawing Libraries](https://kgysoft.net/drawing). [![Website](https://img.shields.io/website/https/kgysoft.net/corelibraries.svg)](https://kgysoft.net/drawing) [![Drawing Libraries Repo](https://img.shields.io/github/repo-size/koszeggy/KGySoft.Drawing.svg?label=Drawing%20Libraries)](https://github.com/koszeggy/KGySoft.Drawing) ## Table of Contents 1. [KGy SOFT Imaging Tools](#kgy-soft-imaging-tools) + - [Compatibility](#compatibility) + - [Localization](#localization) 2. [Debugger Visualizers](#debugger-visualizers) - [Installing Debugger Visualizers](#installing-debugger-visualizers) - - [Troubleshooting](#troubleshooting) + - [Troubleshooting](#troubleshooting) 3. [Download](#download) 4. [Release Notes](#release-notes) 5. [Debugger Visualizers Test Tool](#debugger-visualizers-test-tool) @@ -19,21 +21,63 @@ KGy SOFT Drawing Tools repository contains [Debugger Visualizers](#debugger-visu ## KGy SOFT Imaging Tools -![KGySoft.Drawing.ImagingTools](https://kgysoft.net/images/ImagingTools.png) +

+ KGySoft Imaging Tools on Windows 10 +
KGy SOFT Imaging Tools on Windows 10 +

The Imaging Tools application makes possible to load images and icons from file, manipulate and save them into various formats. Several modifications are possible such as rotating, resizing, changing pixel format with quantizing and dithering, adjusting brightness, contrast and gamma, or even editing the palette entries of indexed bitmaps. -![Changing Pixel Format with Quantizing and Dithering](https://kgysoft.net/images/Quantizing.png) +

+ Changing Pixel Format with Quantizing and Dithering +
Changing Pixel Format with Quantizing and Dithering +

+ +> _Tip:_ As a developer, you can access all of these image manipulaion functions by using [KGy SOFT Drawing Libraries](https://github.com/koszeggy/KGySoft.Drawing). It supports not just `System.Drawing` types but also completely managed and technology agnostic bitmap data manipulation as well. + +### Compatibility + +KGy SOFT Imaging Tools supports a wide range of platforms. Windows is supported starting with Windows XP but by using [Mono](https://www.mono-project.com/download/stable/) you can execute it also on Linux. See the [downloads](#download) for details. + +

+ KGySoft Imaging Tools on Ubuntu Linux, using dark theme +
KGySoft Imaging Tools on Ubuntu Linux, using dark theme +

+ +### Localization + +KGy SOFT Imaging Tools supports localization from .resx files. New language resources can be generated for any languages, and you can edit the texts within the application. The changes can be applied on-the-fly, without exiting the application. If you switch to a right-to-left language, then the layout is also immediately applied (at least on Windows). + +> _Tip:_ As a developer, you can use [KGy SOFT Core Libraries](https://github.com/koszeggy/KGySoft.CoreLibraries#dynamic-resource-management) if you want something similar in your application. + +The edited resources are saved in .resx files in the `Resources` subfolder of the application. Resources can be downloaded from within the application as well. + +

+ Editing resources in KGy SOFT Imaging Tools +
Editing resources in KGy SOFT Imaging Tools +

+ +> _Note:_ If you create a localization for your language feel free to [submit a new issue](https://github.com/koszeggy/KGySoft.Drawing.Tools/issues/new?assignees=&labels=&template=submit-resources.md&title=%5BRes%5D) and I will make it available for everyone. Don't forget to mention your name in the translated About menu. + +#### Help, my reasources are gone! + +If you use Imaging Tools as debugger visualizers, then it can be executed from various locations. See the bottom of the [Troubleshooting](#troubleshooting) section below. ## Debugger Visualizers Imaging Tools is packed with several debugger visualizers for Visual Studio (compatible with all versions starting with Visual Studio 2008, and supports even .NET Core 2.1 and newer platform targets). When a type is debugged in Visual Studio and there is a debugger visualizer installed for that type, then a magnifier icon appears that you can click to open the visualizer. -![Debugger Visualizer Usage](https://kgysoft.net/images/DebuggerVisualizerUsage.png) +

+ Debugger Visualizer Usage +
Debugger Visualizer Usage +

Either click the magnifier icon or choose a debugger visualizer from the drop down list (if more visualizers are applicable). -![Debugging Graphics](https://kgysoft.net/images/DebugGraphics.png) +

+ Debugging a Graphics instance +
Debugging a Graphics instance +

The `KGySoft.Drawing.DebuggerVisualizers` assembly provides debugger visualizers for the following types: - `System.Drawing.Image`: If executed for a non read-only variable or member of type `Image`, then the actual value can be replaced by any `Bitmap` or `Metafile`. @@ -45,7 +89,10 @@ The `KGySoft.Drawing.DebuggerVisualizers` assembly provides debugger visualizers - `System.Drawing.Imaging.ColorPalette`: In a non read-only context the colors can be edited. - `System.Drawing.Color`: In a non read-only context the color can be replaced. -![Debugging Palette](https://kgysoft.net/images/DebugPalette.png) +

+ Debugging a Palette instance +
Debugging a Palette instance +

### Installing Debugger Visualizers @@ -58,24 +105,33 @@ You can perform the install also from Visual Studio by the _Tools/Extensions and #### Manual Install 1. [Download](#download) the binaries and extract the .7z archive to any folder. -2. Open the folder with the extracted content. You will find three folders there: - - `net35` contains the .NET 3.5 version. Compatible with all Visual Studio versions starting with Visual Studio 2008 (tested with versions 2008-2019). Cannot be used to debug .NET Core applications. - - `net40` contains the .NET 4.0 version. You cannot use this one for Visual Studio 2008. Cannot be used to debug .NET Core applications. - - `net45` contains the .NET 4.5 version. You cannot use this one for Windows XP and Visual Studio 2008/2010. With some limitations supports also .NET Core projects (in case of issues see the [Troubleshooting](#Troubleshooting) section). - - `netcoreapp3.0` contains the .NET Core 3.0 binaries of the Imaging Tools application. Debugger visualizers are not included because it would not be recognized by Visual Studio anyway. +2. Open the folder with the extracted content. You will find four folders there: + - `net35` contains the .NET Framework 3.5 version. Compatible with all Visual Studio versions starting with Visual Studio 2008 (tested with versions 2008-2019). Cannot be used to debug .NET Core applications. + - `net40` contains the .NET Framework 4.0 version. You cannot use this one for Visual Studio 2008. Cannot be used to debug .NET Core applications. + - `net45` contains the .NET Framework 4.5 version. You cannot use this one for Windows XP and Visual Studio 2008/2010. With some limitations supports also .NET Core/.NET projects (in case of issues see the [Troubleshooting](#Troubleshooting) section). + - `net5.0-windows` contains the .NET 5.0 binaries of the Imaging Tools application. Debugger visualizers are not included because it would not be recognized by Visual Studio anyway. 3. Execute `KGySoft.Drawing.ImagingTools.exe` from one of the folders listed above. Click the _Manage Debugger Visualizer Installations..._ button (the gear icon) on the toolbar. -![Select Install](https://kgysoft.net/images/InstallSelectMenu.png) +

+ Installing Debugger Visualizers from Imaging Tools +
Installing Debugger Visualizers from Imaging Tools +

> _Note:_ Starting with version 2.1.0 the debugger visualizers can be used also for .NET Core projects from Visual Studio 2019, even though no .NET Core binaries are used. 4. In the drop down list you will see the identified Visual Studio versions in your Documents folder. You can select either one of them or the _<Custom Path...>_ menu item to install the visualizer debuggers into a custom folder. -![Select Visual Studio version](https://kgysoft.net/images/InstallSelectVSVersion.png) +

+ Selecting Visual Studio Version +
Selecting Visual Studio Version +

5. Click on the _Install_ button. On success the status will display the installed version. -![Installation Complete](https://kgysoft.net/images/InstallComplete.png) +

+ Installation Complete +
Installation Complete +

### Troubleshooting @@ -102,15 +158,27 @@ If Visual Studio cannot load the visualizer or you have other debugger visualize | Unable to load the custom visualizer (The UI-side visualizer type must derive from 'DialogDebuggerVisualizer').
![Unable to load the custom visualizer (The UI-side visualizer type must derive from 'DialogDebuggerVisualizer').](https://kgysoft.net/images/DebuggerVisualizerTrShUnableToLoadMustDeriveFrom.png) | The `Microsoft.VisualStudio.DebuggerVisualizers.dll` has been copied to the debugger visualizers installation folder. Recent Visual Studio versions can handle if a debugger visualizer references an unmatching version of this assembly but only if this assembly is not deployed along with the visualizers. | | Could not load file or assembly 'KGySoft.Drawing.DebuggerVisualizers.dll'.
![Could not load file or assembly 'KGySoft.Drawing.DebuggerVisualizers.dll'.](https://kgysoft.net/images/DebuggerVisualizerTrShCouldNotLoadVisualizer.png) or one of its dependencies. | Visual Studio 2008 supports the .NET 3.5 version only. A similar error may occur even if some files are missing. Just [install](#installing-debugger-visualizers) a correct version again. | | Value does not fall within the expected range.
![Value does not fall within the expected range.](https://kgysoft.net/images/DebuggerVisualizerTrShValueUnexpectedRange.png) | Windows XP does not support the .NET 4.5 version. | +| The app looks blurry. | If you changed the DPI settins, you need to restart the application. Per-monitor DPI awareness is not supported. | +| The visual elements are scaled incorrectly.
![Incorrectly scaled image](https://user-images.githubusercontent.com/27336165/124148578-0e993700-da90-11eb-9c67-4e06e522795b.png) | May happen if you use Imaging Tools from debugger visualizers, and you have just changed the DPI settings without signing out/in. However, signing in and out is not required if you execute the app directly. | +| I edited the language resource files but I cannot find them (or they appear to be gone) | The _Visual Studio/Tools/KGy SOFT Drawing Debugger Visualizers_ and clicking the magnifier icon executes the Imaging Tools from different locations. If you edit the language resources at one place they will not be automatically applied at the other place. Therefore, the saved resources might be at different possible locations:
• If you execute a manually deployed version the resources will be in a `Resources` subfolder in the folder you executed the Imaging Tools from.
• During debugging the tool is executed from the debugger visualizers folder: `Documents\Visual Studio \Visualizers`
• If you launch the tool from the Visual Studio Tools menu, then it is located under `ProgramData\Microsoft\VisualStudio\Packages\...` | ## Download -You can download the sources and the binaries as .zip archives [here](https://github.com/koszeggy/KGySoft.Drawing.Tools/releases). +You can download the sources and the binaries as .7z/.zip archives at the [releases](https://github.com/koszeggy/KGySoft.Drawing.Tools/releases) page. + +To support the widest possible range of platforms the binaries archive contains multiple builds in different folders. +* `net35`: This contains the .NET Framework 3.5 build and though it works on every platforms Imaging Tools supports, it is not really recommended to use as a standalone application. If you use Imaging Tools as [debugger visualizers](#installing-debugger-visualizers), then it is the only version you can use for Visual Studio 2008. For newer Visual Studio versions use it only if you want to debug a .NET Framework 2.0-3.5 application. +* `net40`: This is the .NET Framework 4.0 build. As a standalone application, it's basically recommended only for Windows XP. +* `net45`: This is the .NET Framework 4.5 build, which is the recommended version to use as a debugger visualizer for .NET Framework 4.x and .NET Core projects (including .NET 5 and newer platforms). As a standalone application, this is also the recommended version for Linux (though 3.5/4.0 also support it). +* `net5.0-windows`: This folder contains the .NET 5.0 build. As a standalone application this is the recommended version for Windows 7 and above. On the other hand, this one cannot be used as a debugger visualizer (even for .NET Core projects) and does not support Linux either. ## Debugger Visualizers Test Tool -A simple test application is also available in the download binaries. Though it was created mainly for testing purposes it also demonstrates the debugger visualizer and some `KGySoft.Drawing` features. +A simple test application is also available in the download binaries. Though it was created mainly for testing purposes it also demonstrates how to use the public API of the Imaging Tools application and the DebuggerVisualizers from a consumer library or application. -![Debugger Visualizer Test App](https://kgysoft.net/images/DebuggerVisualizerTest.png) +

+ Debugger Visualizer Test Tool +
Debugger Visualizer Test Tool +

> _Note:_ The Debugger Visualizers Test Tool directly references a specific version of the `Microsoft.VisualStudio.DebuggerVisualizers` assembly, therefore Visual Studio will not able to display visualizers when debugging this project unless you use the very same version (Visual Studio 2013). diff --git a/changelog.txt b/changelog.txt index a2c1092..903907d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -5,6 +5,70 @@ + New feature =============================================================================== +~~~~~~~~~ +* v2.4.0: +~~~~~~~~~ + +* KGySoft.Drawing.ImagingTools.exe +================================== ++ Targeting also .NET 5.0 ++ Now panning a zoomed image is possible also by clicking and dragging with the mouse (besides usual scrolling). ++ Zooming is now possible also with keyboard shortcuts, the Auto Zoom button has now a drop-down part for the + additional options. ++ Supporting localization from .resx files with editing, downloading and applying changes on-the-fly. + Note: Right-to-left languages are also supported though with some limitations, especially under Mono. + On Windows some validation tool tips are not aligned right but otherwise the layout is arranged correctly. +* Image visualizer form: + + OK/Cancel buttons in debug mode when image is editable + - Fixing several tool strip appearance issues, including high DPI and high contrast modes +* Palette visualizer form: + + OK/Cancel buttons when palette is editable + - Fixing the alpha pattern on Mono ++ Color visualizer form: OK/Cancel buttons then the color is editable +* Manual zooming is enabled even if Auto Zoom is on; this turns off Auto Zoom automatically. +* ImageViewer control: + * Turning on/off smoothing of zoomed images affects also shrunk images (previously affected enlarged images only). + * Improving performance of some image formats. +- Fixing scaling of menu item images under Linux/Mono when using high DPI +- Color Count: Result was not always shown (and progress bar was not removed) if the operation ended very quickly. +- Resize: + - Preventing that invalid sizes replace current text to 0 + - Changing error provider associated controls so the layout is not messed up on Linux +- Manage Installations: + - Remove was always enabled even it there was no installed version +- Fixing possible errors when closing forms while an async operation is still in progress. + +* API changes: + * Members are annotated for using C# 8.0 nullable references + + InstallationManager class: + + New ImagingToolsVersion property + + ViewModelFactory class: + + New CreateLanguageSettings method + + New CreateEditResources method + + New CreateDownloadResources method + + IViewModel interface + + New IsModified property + * IViewModel interface + * The IsModified property is now inherited from IViewModel + + IView interface: + + New ShowDialog(IView) overload + + ViewFactory class: + + New ShowDialog(IViewModel, IView) overload + + New LocalizableLibraries enum + + New LocalizationInfo class + +- KGySoft.Drawing.DebuggerVisualizers.dll +========================================= +- Fixing a possible IndexOutOfRangeException when debugging an animated GIF. +- Fixing possible incorrect initially displayed size info when debugging a multi-resolution icon or bitmap. +* Improved performance and reduced memory consumption when serializing and deserializing debugged objects. + +* KGySoft.Drawing.DebuggerVisualizers.Package.dll +================================================= +* ImagingTools and Manage Installations executed from the Visual Studio Tools menu are executed on a new thread + so the possible lagging of Visual Studio does not affect the performance anymore. + + ~~~~~~~~~ + v2.3.0: ~~~~~~~~~