diff --git a/.github/actions/spell-check/allow/names.txt b/.github/actions/spell-check/allow/names.txt index f5d259c0f2..f570a231af 100644 --- a/.github/actions/spell-check/allow/names.txt +++ b/.github/actions/spell-check/allow/names.txt @@ -91,6 +91,7 @@ Hemmerlein hlaueriksson Horvalds Howett +hotkidfamily htcfreek Huynh Ionut @@ -98,6 +99,7 @@ jamrobot Jaswal Jaylyn jefflord +Jeremic Jordi jyuwono kai @@ -222,6 +224,7 @@ openai Quickime regedit roslyn +Skia Spotify Vanara wangyi diff --git a/.github/actions/spell-check/excludes.txt b/.github/actions/spell-check/excludes.txt index f7fb1ec196..f5710e72ce 100644 --- a/.github/actions/spell-check/excludes.txt +++ b/.github/actions/spell-check/excludes.txt @@ -126,3 +126,4 @@ ^src/common/sysinternals/Eula/ ^tools/Verification scripts/Check preview handler registration\.ps1$ ignore$ +^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$ diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 1dcfdef0e1..68555a02f9 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1513,6 +1513,7 @@ SICHINT SIDs siex sigdn +Signedness SIGNINGSCENARIO signtool SINGLEKEY diff --git a/.pipelines/applyXamlStyling.ps1 b/.pipelines/applyXamlStyling.ps1 index 7cb7b4a4b0..1facedc569 100644 --- a/.pipelines/applyXamlStyling.ps1 +++ b/.pipelines/applyXamlStyling.ps1 @@ -41,6 +41,9 @@ Write-Output "" Write-Output "Restoring dotnet tools..." dotnet tool restore --disable-parallel --no-cache +# Use Regex syntax +$PathExcludes = "(\\obj\\)|(\\bin\\)|(\\x64\\)|(\\Generated Files\\PowerRenameXAML\\)|(\\RegistryPreviewUILib\\Controls\\HexBox\\)" + if (-not $Passive) { # Look for unstaged changed files by default @@ -87,7 +90,7 @@ if (-not $Passive) } Write-Output "Running Git Diff: $gitDiffCommand" - $files = Invoke-Expression $gitDiffCommand | Select-String -Pattern "\.xaml$" + $files = Invoke-Expression $gitDiffCommand | Select-String -Pattern "\.xaml$" | Where-Object { $_ -notmatch $PathExcludes } if (-not $Passive -and -not $Main -and -not $Unstaged -and -not $Staged -and -not $LastCommit) { @@ -107,7 +110,7 @@ if (-not $Passive) else { Write-Output "Checking all files (passively)" - $files = Get-ChildItem -Path "$PSScriptRoot\..\src\*.xaml" -Recurse | Select-Object -ExpandProperty FullName | Where-Object { $_ -notmatch "(\\obj\\)|(\\bin\\)|(\\x64\\)|(\\Generated Files\\PowerRenameXAML\\)" } + $files = Get-ChildItem -Path "$PSScriptRoot\..\src\*.xaml" -Recurse | Select-Object -ExpandProperty FullName | Where-Object { $_ -notmatch $PathExcludes } if ($files.count -gt 0) { diff --git a/Directory.Packages.props b/Directory.Packages.props index 8116fe99b6..06f8e8ddef 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -69,6 +69,8 @@ + + diff --git a/NOTICE.md b/NOTICE.md index 2b94d67a4b..f36311d4ea 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1427,6 +1427,37 @@ EXHIBIT A -Mozilla Public License. ## Utility: Registry Preview +### HexBox.WinUI + +We use HexBox.WinUI to show a preview of binary values. + +**Source**: https://github.com/hotkidfamily/HexBox.WinUI + +``` +MIT License + +Copyright (c) 2019 Filip Jeremic +Copyright (c) 2024~2025 hotkidfamily@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + ### Monaco Editor **Source**: https://github.com/Microsoft/monaco-editor @@ -1457,6 +1488,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` + ## NuGet Packages used by PowerToys - AdaptiveCards.ObjectModel.WinUI3 2.0.0-beta @@ -1517,6 +1549,7 @@ SOFTWARE. - ReverseMarkdown 4.1.0 - ScipBe.Common.Office.OneNote 3.0.1 - SharpCompress 0.37.2 +- SkiaSharp.Views.WinUI 2.88.9 - StreamJsonRpc 2.21.69 - StyleCop.Analyzers 1.2.0-beta.556 - System.CodeDom 9.0.6 diff --git a/src/codeAnalysis/GlobalSuppressions.cs b/src/codeAnalysis/GlobalSuppressions.cs index d5ff98e548..ae544b0c76 100644 --- a/src/codeAnalysis/GlobalSuppressions.cs +++ b/src/codeAnalysis/GlobalSuppressions.cs @@ -76,3 +76,47 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0049:Using [INotifyPropertyChanged] is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "Peek.FilePreviewer")] [assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "MVVMTK0049:Using [INotifyPropertyChanged] is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "type", Target = "~T:Peek.UI.Views.TitleBar")] [assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0049:Using [INotifyPropertyChanged] is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib")] + +// HexBox control in RegistryPreviewUILib (We decided to copy the original code and not fix all theses problems for easier updating.) +[assembly: SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("Naming", "CA1720:Identifiers should not contain type names", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("Performance", "CA1805:Do not initialize unnecessarily", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1623:Property summary documentation should match accessors", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1642:Constructor summary documentation should begin with standard text", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1648: has been used on an element that doesn't inherit from a base class or implement an interface.", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1500:Braces for multi-line statements should not share line", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1502:Element should not be on a single line", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1505:Opening braces should not be followed by blank line", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1507:Code should not contain multiple blank lines in a row", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1508:Closing braces should not be preceded by blank line", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1509:Opening braces should not be preceded by blank line", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1513:Closing brace should be followed by blank line", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1514:Element documentation header should be preceded by blank line", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1516:Elements should be separated by blank line", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1119:Statement should not use unnecessary parenthesis", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1407:Arithmetic expressions should declare precedence", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1413:Use trailing comma in multi-line initializers", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312:Variable names should begin with lower-case letter", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1108:Block statements should not contain embedded comments", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:Split parameters should start on line after declaration", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1117:Parameters should be on same line or separate lines", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1129:Do not use default value type constructor", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1000:Keywords should be spaced correctly", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1003:Symbols should be spaced correctly", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1005:Single line comments should begin with single space", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1024:Colons Should Be Spaced Correctly", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1028:Code should not contain trailing whitespace", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] +[assembly: SuppressMessage("Usage", "CsWinRT1028:Class is not marked partial", Justification = "", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib.HexBox")] diff --git a/src/modules/registrypreview/RegistryPreview/RegistryPreview.csproj b/src/modules/registrypreview/RegistryPreview/RegistryPreview.csproj index 36bd1f4f4d..8ca723808f 100644 --- a/src/modules/registrypreview/RegistryPreview/RegistryPreview.csproj +++ b/src/modules/registrypreview/RegistryPreview/RegistryPreview.csproj @@ -31,7 +31,7 @@ - + diff --git a/src/modules/registrypreview/RegistryPreview/RegistryPreviewXAML/App.xaml b/src/modules/registrypreview/RegistryPreview/RegistryPreviewXAML/App.xaml index 7c2836890c..400d0c71fd 100644 --- a/src/modules/registrypreview/RegistryPreview/RegistryPreviewXAML/App.xaml +++ b/src/modules/registrypreview/RegistryPreview/RegistryPreviewXAML/App.xaml @@ -10,7 +10,31 @@ - + + + + + + + + + 1 + 1,1,1,2 + + + + + + + + + + + + 1 + 2 + + diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/AddressFormat.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/AddressFormat.cs new file mode 100644 index 0000000000..8219ed8b51 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/AddressFormat.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// + +namespace RegistryPreviewUILib.HexBox +{ + /// + /// Enumerates the address column formatting options. + /// + public enum AddressFormat + { + /// + /// 16 bit HEX address "0000". + /// + Address16, + + /// + /// 24 bit HEX address "00:0000". + /// + Address24, + + /// + /// 32 bit HEX address "0000:0000". + /// + Address32, + + /// + /// 48 bit HEX address "0000:00000000". + /// + Address48, + + /// + /// 64 bit HEX address "00000000:00000000". + /// + Address64, + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/CanvasCommands.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/CanvasCommands.cs new file mode 100644 index 0000000000..dca1f95725 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/CanvasCommands.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// +using System; +using System.Windows.Input; + +namespace RegistryPreviewUILib.HexBox +{ + public class RelayCommand : ICommand + { + private readonly Action _execute; + private readonly Func _canExecute; + + public RelayCommand(Action execute, Func canExecute = null) + { + _execute = execute; + _canExecute = canExecute; + } + + public event EventHandler CanExecuteChanged; + + public bool CanExecute(object parameter) + { + return _canExecute == null || _canExecute(parameter); + } + + public void Execute(object parameter) + { + _execute(parameter); + } + + public void RaiseCanExecuteChanged() + { + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/DataFormat.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/DataFormat.cs new file mode 100644 index 0000000000..31929f8b88 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/DataFormat.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// + +namespace RegistryPreviewUILib.HexBox +{ + /// + /// Enumerates the format to display integral data in. + /// + public enum DataFormat + { + /// + /// Display the data in decimal format. + /// + Decimal, + + /// + /// Display the data in hexadecimal format. + /// + Hexadecimal, + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/DataSignedness.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/DataSignedness.cs new file mode 100644 index 0000000000..4f5d95bc93 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/DataSignedness.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// + +namespace RegistryPreviewUILib.HexBox +{ + /// + /// Enumerates the signedness of the data to display. + /// + public enum DataSignedness + { + /// + /// Display the data as signed values. + /// + Signed, + + /// + /// Display the data as unsigned values. + /// + Unsigned, + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/DataType.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/DataType.cs new file mode 100644 index 0000000000..c9619bfc6b --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/DataType.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// + +namespace RegistryPreviewUILib.HexBox +{ + /// + /// Enumerates how the data (bytes read from the buffer) is to be interpreted when displayed. + /// + public enum DataType + { + /// + /// Display the data as integral (integer) values. + /// + Int_1 = 1, + /// + /// Display the data as integral (integer) values. + /// + Int_2 = 2, + /// + /// Display the data as integral (integer) values. + /// + Int_4 = 4, + /// + /// Display the data as integral (integer) values. + /// + Int_8 = 8, + /// + /// Display the data as floating point values. + /// + Float_32 = 32, + /// + /// Display the data as floating point values. + /// + Float_64 = 64, + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/HexBox.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/HexBox.cs new file mode 100644 index 0000000000..46df5a60ba --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/HexBox.cs @@ -0,0 +1,2913 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// +#pragma warning disable SA1210 // Using directives should be ordered alphabetically by namespace +#pragma warning disable SA1208 // System using directives should be placed before other using directives +using RegistryPreviewUILib.HexBox.Library.EndianConvert; +using Microsoft.UI; +using Microsoft.UI.Input; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using SkiaSharp; +using SkiaSharp.Views.Windows; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Windows.Input; +using Windows.ApplicationModel.DataTransfer; +using Windows.Foundation; +using Windows.System; +using Windows.UI.Core; +#pragma warning restore SA1208 // System using directives should be placed before other using directives +#pragma warning restore SA1210 // Using directives should be ordered alphabetically by namespace + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace RegistryPreviewUILib.HexBox +{ + [TemplatePart(Name = "ElementCanvas", Type = typeof(SKXamlCanvas))] + [TemplatePart(Name = "ElementScrollBar", Type = typeof(ScrollBar))] + public sealed class HexBox : Control, INotifyPropertyChanged + { + /// + /// Defines the address at which the data in the begins. + /// + public static readonly DependencyProperty AddressProperty = + DependencyProperty.Register(nameof(Address), typeof(ulong), typeof(HexBox), + new PropertyMetadata(0UL, OnAddressChanged)); + + /// + /// Defines the brush used to display the addresses in the address section of the control. + /// + public static readonly DependencyProperty AddressBrushProperty = + DependencyProperty.Register(nameof(AddressBrush), typeof(SolidColorBrush), typeof(HexBox), + new PropertyMetadata(new SolidColorBrush(Colors.CornflowerBlue), OnPropertyChangedInvalidateVisual)); + + /// + /// Defines the width of the addresses displayed in the address section of the control. + /// + public static readonly DependencyProperty AddressFormatProperty = + DependencyProperty.Register(nameof(AddressFormat), typeof(AddressFormat), typeof(HexBox), + new PropertyMetadata(AddressFormat.Address32, OnPropertyChangedInvalidateVisual)); + + /// + /// Defines the brush used for alternating for text in alternating (odd numbered) columns in the data section of the control. + /// + public static readonly DependencyProperty AlternatingDataColumnTextBrushProperty = + DependencyProperty.Register(nameof(AlternatingDataColumnTextBrush), typeof(SolidColorBrush), typeof(HexBox), + new PropertyMetadata(new SolidColorBrush(Colors.Gray), OnPropertyChangedInvalidateVisual)); + + /// + /// Defines the number of columns to display. + /// + public static readonly DependencyProperty ColumnsProperty = + DependencyProperty.Register(nameof(Columns), typeof(int), typeof(HexBox), + new PropertyMetadata(16, OnPropertyChangedInvalidateVisual)); + + /// + /// Defines the endianness used to interpret the data. + /// + public static readonly DependencyProperty EndiannessProperty = + DependencyProperty.Register(nameof(Endianness), typeof(Endianness), typeof(HexBox), + new PropertyMetadata(Endianness.BigEndian, OnPropertyChangedInvalidateVisual)); + + /// + /// Defines the format of the data to display. + /// + public static readonly DependencyProperty DataFormatProperty = + DependencyProperty.Register(nameof(DataFormat), typeof(DataFormat), typeof(HexBox), + new PropertyMetadata(DataFormat.Hexadecimal, OnPropertyChangedInvalidateVisual)); + + /// + /// Defines the signedness of the data to display. + /// + public static readonly DependencyProperty DataSignednessProperty = + DependencyProperty.Register(nameof(DataSignedness), typeof(DataSignedness), typeof(HexBox), + new PropertyMetadata(DataSignedness.Unsigned, OnPropertyChangedInvalidateVisual)); + + /// + /// Defines the data source which is used to read the data to display within this control. + /// + public static readonly DependencyProperty DataSourceProperty = + DependencyProperty.Register(nameof(DataSource), typeof(BinaryReader), typeof(HexBox), + new PropertyMetadata(null, OnDataSourceChanged)); + + /// + /// Defines the offset from the of the first visible data element being displayed. + /// + public static readonly DependencyProperty OffsetProperty = + DependencyProperty.Register(nameof(Offset), typeof(long), typeof(HexBox), + new PropertyMetadata(0L, OnPropertyChangedInvalidateVisual)); + + /// + /// Defines the maximum number of columns, based on the size of the control, which can be displayed. + /// + public static readonly DependencyProperty MaxVisibleColumnsProperty = + DependencyProperty.Register(nameof(MaxVisibleColumns), typeof(int), typeof(HexBox), + new PropertyMetadata(int.MaxValue, OnPropertyChangedInvalidateVisual)); + + + /// + /// Defines the maximum number of rows, based on the size of the control, which can be displayed. + /// + public static readonly DependencyProperty MaxVisibleRowsProperty = + DependencyProperty.Register(nameof(MaxVisibleRows), typeof(int), typeof(HexBox), + new PropertyMetadata(int.MaxValue, OnPropertyChangedInvalidateVisual)); + + /// + /// Defines the brush used for selection fill. + /// + public static readonly DependencyProperty SelectionBrushProperty = + DependencyProperty.Register(nameof(SelectionBrush), typeof(SolidColorBrush), typeof(HexBox), + new PropertyMetadata(new SolidColorBrush(Colors.LightPink), OnPropertyChangedInvalidateVisual)); + + /// + /// Defines the brush used for selected text. + /// + public static readonly DependencyProperty SelectionTextBrushProperty = + DependencyProperty.Register(nameof(SelectionTextBrush), typeof(SolidColorBrush), typeof(HexBox), + new PropertyMetadata(new SolidColorBrush(Colors.Black), OnPropertyChangedInvalidateVisual)); + + /// + /// Defines the offset from of where the user selection has ended. + /// + public static readonly DependencyProperty SelectionEndProperty = + DependencyProperty.Register(nameof(SelectionEnd), typeof(long), typeof(HexBox), + new PropertyMetadata(0L, OnSelectionEndChanged)); + + /// + /// Defines the offset from of where the user selection has started. + /// + public static readonly DependencyProperty SelectionStartProperty = + DependencyProperty.Register(nameof(SelectionStart), typeof(long), typeof(HexBox), + new PropertyMetadata(0L, OnSelectionStartChanged)); + + /// + /// Determines whether the user can change the layout and data format. + /// + public static readonly DependencyProperty EnforcePropertiesProperty = + DependencyProperty.Register(nameof(EnforceProperties), typeof(bool), typeof(HexBox), + new PropertyMetadata(false, OnPropertyChangedInvalidateVisual)); + + /// + /// Determines whether to show the address section of the control. + /// + public static readonly DependencyProperty ShowAddressProperty = + DependencyProperty.Register(nameof(ShowAddress), typeof(bool), typeof(HexBox), + new PropertyMetadata(true, OnPropertyChangedInvalidateVisual)); + + /// + /// Determines whether to show the data section of the control. + /// + public static readonly DependencyProperty ShowDataProperty = + DependencyProperty.Register(nameof(ShowData), typeof(bool), typeof(HexBox), + new PropertyMetadata(true, OnPropertyChangedInvalidateVisual)); + + /// + /// Determines whether to show the text section of the control. + /// + public static readonly DependencyProperty ShowTextProperty = + DependencyProperty.Register(nameof(ShowText), typeof(bool), typeof(HexBox), + new PropertyMetadata(true, OnPropertyChangedInvalidateVisual)); + + /// + /// Defines the brush used for the fill of the vertical separator line between the areas. + /// + public static readonly DependencyProperty VerticalSeparatorLineBrushProperty = + DependencyProperty.Register(nameof(VerticalSeparatorLineBrush), typeof(SolidColorBrush), typeof(HexBox), + new PropertyMetadata(new SolidColorBrush(Colors.Black), OnPropertyChangedInvalidateVisual)); + + + /// + /// Defines the format of the text to display in the text section. + /// + public static readonly DependencyProperty TextFormatProperty = + DependencyProperty.Register(nameof(TextFormat), typeof(TextFormat), typeof(HexBox), + new PropertyMetadata(TextFormat.Ascii, OnPropertyChangedInvalidateVisual)); + + /// + /// Gets the command. + /// + public ICommand SelectAllCommand + { + get { return (ICommand)GetValue(SelectAllCommandProperty); } + set { SetValue(SelectAllCommandProperty, value); } + } + + // Using a DependencyProperty as the backing store for CopyCommand. This enables animation, styling, binding, etc... + public static readonly DependencyProperty SelectAllCommandProperty = + DependencyProperty.Register("SelectAllCommand", typeof(ICommand), typeof(HexBox), new PropertyMetadata(null)); + + /// + /// Gets the command. + /// + public ICommand CopyCommand + { + get { return (ICommand)GetValue(CopyCommandProperty); } + set { SetValue(CopyCommandProperty, value); } + } + + // Using a DependencyProperty as the backing store for CopyCommand. This enables animation, styling, binding, etc... + public static readonly DependencyProperty CopyCommandProperty = + DependencyProperty.Register("CopyCommand", typeof(ICommand), typeof(HexBox), new PropertyMetadata(null)); + + /// + /// Gets the for text command. + /// + public ICommand CopyTextCommand + { + get { return (ICommand)GetValue(CopyTextCommandProperty); } + set { SetValue(CopyTextCommandProperty, value); } + } + + // Using a DependencyProperty as the backing store for CopyTextCommand. This enables animation, styling, binding, etc... + public static readonly DependencyProperty CopyTextCommandProperty = + DependencyProperty.Register("CopyTextCommand", typeof(ICommand), typeof(HexBox), new PropertyMetadata(null)); + + + private const int _MaxColumns = 128; + private const int _MaxRows = 128; + + private const int _CharsBetweenSections = 2; + private const int _CharsBetweenDataColumns = 1; + private const int _ScrollWheelScrollRows = 3; + + private Rect _AddressRect; + private Rect _DataRect; + private Rect _TextRect; + + private SKPaint _TextPaint; + private SKPaint _LinePaint; + private SKRect _TextMeasure; + private SKTypeface _TextTypeFace; + + private SKXamlCanvas _Canvas; + private string _CanvasName = "ElementCanvas"; + + private SelectionArea _HighlightBegin = SelectionArea.None; + private SelectionArea _HighlightState = SelectionArea.None; + + private double _LastVerticalScrollValue = 0; + + private ScrollBar _ScrollBar; + private string _ScrollBarName = "ElementScrollBar"; + + private SelectionAdjustment _pointerMoveSelectionAdjustment = SelectionAdjustment.None; + + /// + public event PropertyChangedEventHandler PropertyChanged; + + private enum SelectionArea + { + None, + Address, + Data, + Text, + } + + private enum SelectionAdjustment + { + None, + Up, + Down + } + + /// + /// Gets or sets the address at which the data in the begins. + /// + public ulong Address + { + get => (ulong)GetValue(AddressProperty); + + set => SetValue(AddressProperty, value); + } + + /// + /// Gets or sets the brush used to display the addresses in the address section of the control. + /// + public SolidColorBrush AddressBrush + { + get => (SolidColorBrush)GetValue(AddressBrushProperty); + + set => SetValue(AddressBrushProperty, value); + } + + /// + /// Gets or sets the brush used for alternating for text in alternating (odd numbered) columns in the data section of the control. + /// + public SolidColorBrush AlternatingDataColumnTextBrush + { + get => (SolidColorBrush)GetValue(AlternatingDataColumnTextBrushProperty); + + set => SetValue(AlternatingDataColumnTextBrushProperty, value); + } + + /// + /// Gets or sets the number of columns to display. + /// + public int Columns + { + get => (int)GetValue(ColumnsProperty); + + set => SetValue(ColumnsProperty, CoerceColumns(this, value)); + } + + /// + /// Gets or sets the endianness used to interpret the data. + /// + public Endianness Endianness + { + get => (Endianness)GetValue(EndiannessProperty); + + set => SetValue(EndiannessProperty, value); + } + + /// + /// Gets or sets the format of the data to display. + /// + public DataFormat DataFormat + { + get => (DataFormat)GetValue(DataFormatProperty); + + set => SetValue(DataFormatProperty, value); + } + + /// + /// Gets or sets the signedness of the data to display. + /// + public DataSignedness DataSignedness + { + get => (DataSignedness)GetValue(DataSignednessProperty); + + set => SetValue(DataSignednessProperty, value); + } + + /// + /// Gets or sets the data source which is used to read the data to display within this control. + /// + public BinaryReader DataSource + { + get => (BinaryReader)GetValue(DataSourceProperty); + + set => SetValue(DataSourceProperty, value); + } + + /// + /// Gets or sets the data type which is used to display within this control. + /// + public DataType DataType + { + get { return (DataType)GetValue(DataTypeProperty); } + set => SetValue(DataTypeProperty, value); + } + + // Using a DependencyProperty as the backing store for DataType. This enables animation, styling, binding, etc... + public static readonly DependencyProperty DataTypeProperty = + DependencyProperty.Register("DataType", typeof(DataType), typeof(HexBox), new PropertyMetadata(DataType.Int_1, OnDataTypeChanged)); + + /// + /// Gets or sets the width of the data to display. + /// + private int DataWidth = 1; + + /// + /// Gets a value indicating whether the user has made any selection within the control. + /// + public bool IsSelectionActive => SelectionLength != 0; + + /// + /// Gets the maximum number of columns, based on the size of the control, which can be displayed. + /// + public int MaxVisibleColumns + { + get => (int)GetValue(MaxVisibleColumnsProperty); + + private set => SetValue(MaxVisibleColumnsProperty, CoerceMaxVisibleColumns(this, value)); + } + + /// + /// Gets the maximum number of rows, based on the size of the control, which can be displayed. + /// + public int MaxVisibleRows + { + get => (int)GetValue(MaxVisibleRowsProperty); + + private set => SetValue(MaxVisibleRowsProperty, CoerceMaxVisibleRows(this, value)); + } + + /// + /// Gets or sets the offset from the of the first visible data element being displayed. + /// + public long Offset + { + get => (long)GetValue(OffsetProperty); + + set => SetValue(OffsetProperty, CoerceOffset(this, value)); + } + + /// + /// Gets lowest order address currently being selected. + /// + public ulong SelectedAddress => Address + (ulong)SelectedOffset; + + /// + /// Gets the offset from of the . + /// + public long SelectedOffset => Math.Min(SelectionStart, SelectionEnd); + + /// + /// Gets or sets the brush used for selection fill. + /// + public SolidColorBrush SelectionBrush + { + get => (SolidColorBrush)GetValue(SelectionBrushProperty); + + set => SetValue(SelectionBrushProperty, value); + } + + /// + /// Gets the offset from of where the user selection has ended. + /// + public long SelectionEnd + { + get => (long)GetValue(SelectionEndProperty); + + private set => SetValue(SelectionEndProperty, CoerceSelectionEnd(this, value)); + } + + /// + /// Gets the number of bytes selected. + /// + public long SelectionLength + { + get + { + if (SelectionStart <= SelectionEnd) + { + return SelectionEnd - SelectionStart; + } + else + { + return SelectionStart - SelectionEnd + _BytesPerColumn; + } + } + } + + /// + /// Gets the offset from of where the user selection has started. + /// + public long SelectionStart + { + get => (long)GetValue(SelectionStartProperty); + + private set + { + SetValue(SelectionStartProperty, CoerceSelectionStart(this, value)); + + // Reset SelectionStart adjustment state + _pointerMoveSelectionAdjustment = SelectionAdjustment.None; + } + } + + /// + /// Gets or sets the brush used for selected text. + /// + public SolidColorBrush SelectionTextBrush + { + get => (SolidColorBrush)GetValue(SelectionTextBrushProperty); + + set => SetValue(SelectionTextBrushProperty, value); + } + + /// + /// Gets or sets a value indicating whether the user can change the layout and data format or not. + /// + public bool EnforceProperties + { + get => (bool)GetValue(EnforcePropertiesProperty); + set => SetValue(EnforcePropertiesProperty, value); + } + + /// + /// Gets or sets a value indicating whether to show the address section of the control. + /// + public bool ShowAddress + { + get => (bool)GetValue(ShowAddressProperty); + + set => SetValue(ShowAddressProperty, value); + } + + /// + /// Gets or sets a value indicating whether to show the data section of the control. + /// + public bool ShowData + { + get => (bool)GetValue(ShowDataProperty); + + set => SetValue(ShowDataProperty, value); + } + + /// + /// Gets or sets a value indicating whether to show the text section of the control. + /// + public bool ShowText + { + get => (bool)GetValue(ShowTextProperty); + + set => SetValue(ShowTextProperty, value); + } + + /// + /// Gets or sets the brush used to display the vertical separator line between the control areas. + /// + public SolidColorBrush VerticalSeparatorLineBrush + { + get => (SolidColorBrush)GetValue(VerticalSeparatorLineBrushProperty); + + set => SetValue(VerticalSeparatorLineBrushProperty, value); + } + + /// + /// Gets or sets the width of the addresses displayed in the address section of the control. + /// + public AddressFormat AddressFormat + { + get => (AddressFormat)GetValue(AddressFormatProperty); + + set => SetValue(AddressFormatProperty, value); + } + + /// + /// Gets or sets the format of the text to display in the text section. + /// + public TextFormat TextFormat + { + get => (TextFormat)GetValue(TextFormatProperty); + + set => SetValue(TextFormatProperty, value); + } + + private double _SelectionBoxDataXPadding => _TextMeasure.Width / 4; + + private double _SelectionBoxDataYPadding => 0; + + private double _SelectionBoxTextXPadding => 0; + + private double _SelectionBoxTextYPadding => 0; + + private int _BytesPerColumn => DataWidth; + + private int _BytesPerRow => DataWidth * Columns; + + public class HighlightedRegion + { + public long Start; + public long Length; + public long End { get { return Start + Length; } } + public Brush Color; + + public HighlightedRegion() + { + + } + + public HighlightedRegion(int Start, int Length, Brush Color) + { + this.Start = Start; + this.Length = Length; + this.Color = Color; + } + + public bool IsByteSelected(long BytePos) + { + return BytePos >= Start && BytePos <= End; + } + } + + public List HighlightedRegions + { + get { return (List)GetValue(HighlightedRegionsProperty); } + set { SetValue(HighlightedRegionsProperty, value); } + } + + // Using a DependencyProperty as the backing store for HighlightedRegions. This enables animation, styling, binding, etc... + public static readonly DependencyProperty HighlightedRegionsProperty = + DependencyProperty.Register("HighlightedRegions", typeof(List), typeof(HexBox), new PropertyMetadata(new List(), OnPropertyChangedInvalidateVisual)); + + + /// + /// Clears the current selection + /// + public void ClearSelection() + { + SelectionStart = SelectionEnd = 0; + } + + + /// + /// Select all data. + /// + public void SelectAll() + { + SelectionStart = 0; + SelectionEnd = DataSource.BaseStream.Length; + } + + + /// + /// Copies the current selection of the control to the . + /// + /// Copy the text and not the data. + public void Copy(bool copyText) + { + if (IsSelectionActive) + { + StringBuilder builder = new(); + + long savedDataSourcePositionBeforeReadingData = DataSource.BaseStream.Position; + + // Adjust wrong SelectionEnd after selecting down or left to right + long selectionEnd = SelectionStart < SelectionEnd ? SelectionEnd - _BytesPerColumn : SelectionEnd; + + DataSource.BaseStream.Position = Math.Min(SelectionStart, selectionEnd); + + while (DataSource.BaseStream.Position <= Math.Max(SelectionStart, selectionEnd)) + { + if (copyText) + { + var formattedData = ReadFormattedText(); + builder.Append(formattedData); + } + else + { + var formattedData = ReadFormattedData(); + builder.Append(formattedData); + } + } + + DataSource.BaseStream.Position = savedDataSourcePositionBeforeReadingData; + + var dataPackage = new DataPackage(); + dataPackage.SetText(builder.ToString()); + Clipboard.SetContent(dataPackage); + } + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _Canvas = GetTemplateChild(_CanvasName) as SKXamlCanvas; + + if (_Canvas != null) + { + CopyCommand = new RelayCommand(CopyExecuted, CopyCanExecute); + CopyTextCommand = new RelayCommand(CopyTextExecuted, CopyCanExecute); + SelectAllCommand = new RelayCommand(SelectAllExecuted, SelectAllCanExecute); + _Canvas.PaintSurface += Canvas_PaintSurface; + } + else + { + throw new InvalidOperationException($"Could not find {_CanvasName} template child."); + } + + if (_ScrollBar != null) + { + _ScrollBar.Scroll -= OnVerticalScrollBarScroll; + } + + _ScrollBar = GetTemplateChild(_ScrollBarName) as ScrollBar; + + if (_ScrollBar != null) + { + _ScrollBar.Scroll += OnVerticalScrollBarScroll; + _ScrollBar.ValueChanged += OnVerticalScrollBarValueChanged; + + _ScrollBar.Minimum = 0; + _ScrollBar.SmallChange = 1; + _ScrollBar.LargeChange = MaxVisibleRows; + _TextTypeFace = SKTypeface.FromFamilyName(_ScrollBar.FontFamily.Source, SKFontStyle.Normal); + } + else + { + throw new InvalidOperationException($"Could not find {_ScrollBarName} template child."); + } + } + + private void DrawSelectionGeometry(SKCanvas Canvas, + Brush brush, + SKPaint pen, + Point point0, + Point point1, + SelectionArea relativeTo) + { + if ((long)point0.Y > (long)point1.Y) + { + throw new ArgumentException($"{point0.ToString()} > {point1.ToString()}", nameof(point0)); + } + + Point lhsVerticalLinePoint0; + Point rhsVerticalLinePoint0; + + double selectionBoxXPadding; + double selectionBoxYPadding; + + switch (relativeTo) + { + case SelectionArea.Data: + { + lhsVerticalLinePoint0 = new Point(_AddressRect.Left, _AddressRect.Top); + rhsVerticalLinePoint0 = new Point(_DataRect.Left, _DataRect.Top); + + selectionBoxXPadding = _SelectionBoxDataXPadding; + selectionBoxYPadding = _SelectionBoxDataYPadding; + } + + break; + + case SelectionArea.Text: + { + lhsVerticalLinePoint0 = new Point(_DataRect.Left, _DataRect.Top); + rhsVerticalLinePoint0 = new Point(_TextRect.Left, _TextRect.Top); + + selectionBoxXPadding = _SelectionBoxTextXPadding; + selectionBoxYPadding = _SelectionBoxTextYPadding; + } + + break; + + default: + { + throw new ArgumentException($"Invalid relative area {relativeTo}", nameof(relativeTo)); + } + } + + point0.X -= selectionBoxXPadding; + point1.X += selectionBoxXPadding; + point0.Y -= selectionBoxYPadding; + point1.Y += selectionBoxYPadding; + + var ps_CharsBetweenSections = _CharsBetweenSections * _TextMeasure.Width; + + SKPath path = new(); + SKPoint[] points; + + if ((long)point0.X < (long)point1.X) + { + if ((long)point0.Y < (long)point1.Y) + { + // +---------------------------+ + // | | + // | 0-------------2 + // | | | + // 6-------------7 1-------3 + // | | | + // 5-------------------4 | + // | | + // | | + // | | + // +---------------------------+ + Point point2 = new(rhsVerticalLinePoint0.X - ps_CharsBetweenSections + selectionBoxXPadding, point0.Y); + Point point3 = new(rhsVerticalLinePoint0.X - ps_CharsBetweenSections + selectionBoxXPadding, point1.Y); + Point point4 = new(point1.X, point1.Y + _TextMeasure.Height); + Point point5 = new(lhsVerticalLinePoint0.X + ps_CharsBetweenSections - selectionBoxXPadding, point1.Y + _TextMeasure.Height); + Point point6 = new(lhsVerticalLinePoint0.X + ps_CharsBetweenSections - selectionBoxXPadding, point0.Y + _TextMeasure.Height); + Point point7 = new(point0.X, point0.Y + _TextMeasure.Height); + + points = [point0.ToSKPoint(), point2.ToSKPoint(), point3.ToSKPoint(), point1.ToSKPoint(), point4.ToSKPoint(), point5.ToSKPoint(), point6.ToSKPoint(), point7.ToSKPoint()]; + } + else + { + // +---------------------------+ + // | | + // | 0-------------1 | + // | | | | + // | 3-------------2 | + // | | + // | | + // | | + // | | + // | | + // +---------------------------+ + Point point2 = new(point1.X, point1.Y + _TextMeasure.Height); + Point point3 = new(point0.X, point0.Y + _TextMeasure.Height); + + points = [point0.ToSKPoint(), point1.ToSKPoint(), point2.ToSKPoint(), point3.ToSKPoint()]; + } + } + else + { + if ((long)(point0.Y + _TextMeasure.Height) == (long)point1.Y) + { + // +---------------------------+ + // | | + // | 0-------------2 + // | | | + // 7--------1 4-------------3 + // | | | + // 6--------5 | + // | | + // | | + // | | + // +---------------------------+ + { + Point point2 = new(rhsVerticalLinePoint0.X - ps_CharsBetweenSections + selectionBoxXPadding, point0.Y); + Point point3 = new(rhsVerticalLinePoint0.X - ps_CharsBetweenSections + selectionBoxXPadding, point1.Y); + Point point4 = new(point0.X, point1.Y); + + points = [point0.ToSKPoint(), point2.ToSKPoint(), point3.ToSKPoint(), point4.ToSKPoint()]; + } + + path.AddPoly(points); + + { + Point point5 = new(point1.X, point1.Y + _TextMeasure.Height); + Point point6 = new(lhsVerticalLinePoint0.X + ps_CharsBetweenSections - selectionBoxXPadding, point1.Y + _TextMeasure.Height); + Point point7 = new(lhsVerticalLinePoint0.X + ps_CharsBetweenSections - selectionBoxXPadding, point1.Y); + points = [point1.ToSKPoint(), point5.ToSKPoint(), point6.ToSKPoint(), point7.ToSKPoint()]; + } + } + else + { + // +---------------------------+ + // | | + // | 0-------------2 + // | | | + // 6-------------7 | + // | | + // | 1------------------3 + // | | | + // 5--------4 | + // | | + // +---------------------------+ + Point point2 = new(rhsVerticalLinePoint0.X - ps_CharsBetweenSections + selectionBoxXPadding, point0.Y); + Point point3 = new(rhsVerticalLinePoint0.X - ps_CharsBetweenSections + selectionBoxXPadding, point1.Y); + Point point4 = new(point1.X, point1.Y + _TextMeasure.Height); + Point point5 = new(lhsVerticalLinePoint0.X + ps_CharsBetweenSections - selectionBoxXPadding, point1.Y + _TextMeasure.Height); + Point point6 = new(lhsVerticalLinePoint0.X + ps_CharsBetweenSections - selectionBoxXPadding, point0.Y + _TextMeasure.Height); + Point point7 = new(point0.X, point0.Y + _TextMeasure.Height); + + points = [point0.ToSKPoint(), point2.ToSKPoint(), point3.ToSKPoint(), point1.ToSKPoint(), point4.ToSKPoint(), point5.ToSKPoint(), point6.ToSKPoint(), point7.ToSKPoint()]; + } + } + + path.AddPoly(points); + if (brush is SolidColorBrush s) + pen.Color = s.Color.ToSKColor(); + Canvas.DrawPath(path, pen); + } + + private void DrawTextAccuracy(SKCanvas Canvas, SKPaint paint, SKPoint pt, string text) + { + //Canvas.DrawText(text, pt, paint); + int index = 0; + foreach (var c in text) + { + Canvas.DrawText(c.ToString(), pt.X + index * _TextMeasure.Width, pt.Y, paint); + index++; + } + } + + private void Canvas_PaintSurface(object sender, SKPaintSurfaceEventArgs e) + { + var view = sender as SKXamlCanvas; + var canvas = e.Surface.Canvas; + + if (_LinePaint == null) + { + _LinePaint = new() + { + IsStroke = true, + IsAntialias = true, + StrokeWidth = 1, + TextSize = (float)FontSize, + Typeface = _TextTypeFace, + TextAlign = SKTextAlign.Left, + }; + } + _LinePaint.Color = VerticalSeparatorLineBrush.Color.ToSKColor(); + + if (_TextPaint == null) + { + _TextPaint = new() + { + TextSize = (float)FontSize, + Typeface = _TextTypeFace, + TextScaleX = 1f, + IsAntialias = true, + TextAlign = SKTextAlign.Left, + HintingLevel = SKPaintHinting.Normal, + }; + } + + UpdateState(); + + if (DataSource != null) + { + canvas.Clear(); + long savedDataSourcePosition = DataSource.BaseStream.Position; + + DataSource.BaseStream.Position = Offset; + + if (ShowAddress) + { + var p0 = new Point(_AddressRect.Left, _AddressRect.Top).ToSKPoint(); + var p1 = new Point(_AddressRect.Right, _AddressRect.Bottom).ToSKPoint(); + + canvas.DrawLine(p0, p1, _LinePaint); + } + + if (ShowData) + { + var p0 = new Point(_DataRect.Left, _DataRect.Top).ToSKPoint(); + var p1 = new Point(_DataRect.Right, _DataRect.Bottom).ToSKPoint(); + + canvas.DrawLine(p0, p1, _LinePaint); + + if (HighlightedRegions.Count != 0 && MaxVisibleRows > 0 && Columns > 0) + { + var viewLimited = Offset + _BytesPerRow * MaxVisibleRows; + + foreach (var hlSection in HighlightedRegions) + { + if (hlSection.End <= Offset || (hlSection.Start >= viewLimited) || hlSection.Start >= hlSection.End) continue; + + var max_visible = Math.Min(hlSection.End, viewLimited); + + Point hlsP0 = ConvertOffsetToPosition(hlSection.Start, SelectionArea.Data); + Point hlsP1 = ConvertOffsetToPosition(max_visible, SelectionArea.Data); + + if (max_visible % _BytesPerRow == 0) + { + hlsP1.X = p1.X - _CharsBetweenSections * _TextMeasure.Width; + hlsP1.Y = Math.Max(hlsP0.Y, hlsP1.Y - _TextMeasure.Height); + } + else + { + hlsP1.X -= _TextMeasure.Width; + } + + DrawSelectionGeometry(canvas, hlSection.Color, _TextPaint, hlsP0, hlsP1, SelectionArea.Data); + } + } + } + + if (ShowText) + { + var p0 = new Point(_TextRect.Left, _TextRect.Top); + var p1 = new Point(_TextRect.Right, _TextRect.Bottom); + + canvas.DrawLine(p0.ToSKPoint(), p1.ToSKPoint(), _LinePaint); + + if (HighlightedRegions.Count != 0 && MaxVisibleRows > 0 && Columns > 0) + { + var viewLimited = Offset + MaxVisibleColumns * MaxVisibleRows; + + foreach (var hlSection in HighlightedRegions) + { + if (hlSection.End <= Offset || (hlSection.Start >= viewLimited) || hlSection.Start >= hlSection.End) continue; + + var max_visible = Math.Min(hlSection.End, viewLimited); + + Point hlsP0 = ConvertOffsetToPosition(hlSection.Start, SelectionArea.Text); + Point hlsP1 = ConvertOffsetToPosition(max_visible, SelectionArea.Text); + + if (max_visible % _BytesPerRow == 0) + { + hlsP1.X = p1.X - _CharsBetweenSections * _TextMeasure.Width; + hlsP1.Y = Math.Max(hlsP0.Y, hlsP1.Y - _TextMeasure.Height); + } + + DrawSelectionGeometry(canvas, hlSection.Color, _TextPaint, hlsP0, hlsP1, SelectionArea.Text); + } + } + } + + if (ShowData) + { + if (SelectionLength != 0 && MaxVisibleRows > 0 && Columns > 0) + { + Point sp0 = ConvertOffsetToPosition(SelectedOffset, SelectionArea.Data); + Point sp1 = ConvertOffsetToPosition(SelectedOffset + SelectionLength, SelectionArea.Data); + + if ((SelectedOffset + SelectionLength) % _BytesPerRow == 0) + { + sp1.X = _DataRect.Left - _CharsBetweenSections * _TextMeasure.Width; + sp1.Y = Math.Max(sp0.Y, sp1.Y - _TextMeasure.Height); + } + else + { + sp1.X -= _TextMeasure.Width; + } + + DrawSelectionGeometry(canvas, SelectionBrush, _TextPaint, sp0, sp1, SelectionArea.Data); + } + } + + if (ShowText) + { + if (SelectionLength != 0 && MaxVisibleRows > 0 && Columns > 0) + { + Point sp0 = ConvertOffsetToPosition(SelectedOffset, SelectionArea.Text); + Point sp1 = ConvertOffsetToPosition(SelectedOffset + SelectionLength, SelectionArea.Text); + + if ((SelectedOffset + SelectionLength) % _BytesPerRow == 0) + { + sp1.X = _TextRect.Left - _CharsBetweenSections * _TextMeasure.Width; + sp1.Y -= _TextMeasure.Height; + } + + DrawSelectionGeometry(canvas, SelectionBrush, _TextPaint, sp0, sp1, SelectionArea.Text); + } + } + + SKPoint origin = default; + origin.Y = _TextMeasure.Height * 3 / 4; /* left bottom to right top */ + + for (var row = 0; row < MaxVisibleRows; ++row) + { + if (ShowAddress) + { + if (DataSource.BaseStream.Position + _BytesPerColumn <= DataSource.BaseStream.Length) + { + var textToFormat = GetFormattedAddressText(Address + (ulong)DataSource.BaseStream.Position); + + if (AddressBrush is SolidColorBrush s) + { + _TextPaint.Color = s.Color.ToSKColor(); + } + canvas.DrawText(textToFormat, origin.X, origin.Y, _TextPaint); + + origin.X += (float)((CalculateAddressColumnCharWidth() + _CharsBetweenSections) * _TextMeasure.Width); + } + } + + long savedDataSourcePositionBeforeReadingData = DataSource.BaseStream.Position; + + if (ShowData) + { + origin.X += (float)(_CharsBetweenSections * _TextMeasure.Width); + + var cachedDataColumnCharWidth = CalculateDataColumnCharWidth(); + + // Needed to track text in alternating columns so we can use a different brush when drawing + var evenColumnBuilder = new StringBuilder(Columns * DataWidth); + var oddColumnBuilder = new StringBuilder(Columns * DataWidth); + + var column = 0; + + // Draw text up until selection start point + while (column < Columns) + { + if (DataSource.BaseStream.Position + _BytesPerColumn <= DataSource.BaseStream.Length) + { + if (DataSource.BaseStream.Position >= SelectedOffset) + { + break; + } + + var textToFormat = ReadFormattedData(); + + if (column % 2 == 0) + { + evenColumnBuilder.Append(textToFormat); + evenColumnBuilder.Append(' ', _CharsBetweenDataColumns); + + oddColumnBuilder.Append(' ', textToFormat.Length + _CharsBetweenDataColumns); + } + else + { + oddColumnBuilder.Append(textToFormat); + oddColumnBuilder.Append(' ', _CharsBetweenDataColumns); + + evenColumnBuilder.Append(' ', textToFormat.Length + _CharsBetweenDataColumns); + } + } + else + { + evenColumnBuilder.Append(' ', cachedDataColumnCharWidth + _CharsBetweenDataColumns); + oddColumnBuilder.Append(' ', cachedDataColumnCharWidth + _CharsBetweenDataColumns); + } + + ++column; + } + + { + if (Foreground is SolidColorBrush s) + { + _TextPaint.Color = s.Color.ToSKColor(); + } + DrawTextAccuracy(canvas, _TextPaint, origin, evenColumnBuilder.ToString()); + } + + { + if (AlternatingDataColumnTextBrush is SolidColorBrush s) + { + _TextPaint.Color = s.Color.ToSKColor(); + } + DrawTextAccuracy(canvas, _TextPaint, origin, oddColumnBuilder.ToString()); + } + origin.X += evenColumnBuilder.Length * _TextMeasure.Width; + + if (column < Columns) + { + // We'll reuse this builder for drawing selection text + evenColumnBuilder.Clear(); + + // Draw text starting from selection start point + while (column < Columns) + { + if (DataSource.BaseStream.Position + _BytesPerColumn <= DataSource.BaseStream.Length) + { + if (DataSource.BaseStream.Position >= SelectedOffset + SelectionLength) + { + break; + } + + var textToFormat = ReadFormattedData(); + + evenColumnBuilder.Append(textToFormat); + evenColumnBuilder.Append(' ', _CharsBetweenDataColumns); + } + else + { + evenColumnBuilder.Append(' ', cachedDataColumnCharWidth + _CharsBetweenDataColumns); + } + + ++column; + } + + { + if (SelectionTextBrush is SolidColorBrush s) + { + _TextPaint.Color = s.Color.ToSKColor(); + } + DrawTextAccuracy(canvas, _TextPaint, origin, evenColumnBuilder.ToString()); + } + + origin.X += evenColumnBuilder.Length * _TextMeasure.Width; + + if (column < Columns) + { + evenColumnBuilder.Clear(); + oddColumnBuilder.Clear(); + + // Draw text after end of selection + while (column < Columns) + { + if (DataSource.BaseStream.Position + _BytesPerColumn <= DataSource.BaseStream.Length) + { + var textToFormat = ReadFormattedData(); + if (column % 2 == 0) + { + evenColumnBuilder.Append(textToFormat); + evenColumnBuilder.Append(' ', _CharsBetweenDataColumns); + + oddColumnBuilder.Append(' ', textToFormat.Length + _CharsBetweenDataColumns); + } + else + { + oddColumnBuilder.Append(textToFormat); + oddColumnBuilder.Append(' ', _CharsBetweenDataColumns); + + evenColumnBuilder.Append(' ', textToFormat.Length + _CharsBetweenDataColumns); + } + } + else + { + evenColumnBuilder.Append(' ', cachedDataColumnCharWidth + _CharsBetweenDataColumns); + oddColumnBuilder.Append(' ', cachedDataColumnCharWidth + _CharsBetweenDataColumns); + } + + ++column; + } + + { + if (Foreground is SolidColorBrush s) + { + _TextPaint.Color = s.Color.ToSKColor(); + } + DrawTextAccuracy(canvas, _TextPaint, origin, evenColumnBuilder.ToString()); + } + + { + if (AlternatingDataColumnTextBrush is SolidColorBrush s) + { + _TextPaint.Color = s.Color.ToSKColor(); + } + DrawTextAccuracy(canvas, _TextPaint, origin, oddColumnBuilder.ToString()); + } + + origin.X += oddColumnBuilder.Length * _TextMeasure.Width; + } + } + + // Compensate for the extra space added at the end of the builder + origin.X += (float)((_CharsBetweenSections - _CharsBetweenDataColumns) * _TextMeasure.Width); + } + + if (ShowText) + { + origin.X += (float)(_CharsBetweenSections * _TextMeasure.Width); + + if (ShowData) + { + // Reset the stream to read one byte at a time + DataSource.BaseStream.Position = savedDataSourcePositionBeforeReadingData; + } + + var builder = new StringBuilder(Columns * DataWidth); + + var column = 0; + + // Draw text up until selection start point + while (column < Columns) + { + if (DataSource.BaseStream.Position + _BytesPerColumn <= DataSource.BaseStream.Length) + { + if (DataSource.BaseStream.Position >= SelectedOffset) + { + break; + } + + var textToFormat = ReadFormattedText(); + builder.Append(textToFormat); + } + + ++column; + } + + { + if (Foreground is SolidColorBrush s) + { + _TextPaint.Color = s.Color.ToSKColor(); + } + DrawTextAccuracy(canvas, _TextPaint, origin, builder.ToString()); + } + + if (column < Columns) + { + origin.X += builder.Length * _TextMeasure.Width; + + builder.Clear(); + + // Draw text starting from selection start point + while (column < Columns) + { + if (DataSource.BaseStream.Position + _BytesPerColumn <= DataSource.BaseStream.Length) + { + if (DataSource.BaseStream.Position >= SelectedOffset + SelectionLength) + { + break; + } + + var textToFormat = ReadFormattedText(); + builder.Append(textToFormat); + } + + ++column; + } + + { + if (SelectionTextBrush is SolidColorBrush s) + { + _TextPaint.Color = s.Color.ToSKColor(); + } + + DrawTextAccuracy(canvas, _TextPaint, origin, builder.ToString()); + } + + if (column < Columns) + { + origin.X += builder.Length * _TextMeasure.Width; + + builder.Clear(); + + // Draw text after end of selection + while (column < Columns) + { + if (DataSource.BaseStream.Position + _BytesPerColumn <= DataSource.BaseStream.Length) + { + var textToFormat = ReadFormattedText(); + builder.Append(textToFormat); + } + + ++column; + } + + { + if (Foreground is SolidColorBrush s) + { + _TextPaint.Color = s.Color.ToSKColor(); + } + + DrawTextAccuracy(canvas, _TextPaint, origin, builder.ToString()); + } + } + } + } + + origin.X = 0; + origin.Y += _TextMeasure.Height; + } + + DataSource.BaseStream.Position = savedDataSourcePosition; + } + } + + /// + /// Scrolls the contents of the control to the specified offset. + /// + /// + /// + /// The offset to scroll to. + /// + public void ScrollToOffset(long offset) + { + long maxBytesDisplayed = _BytesPerRow * MaxVisibleRows; + long lastByteOffset = (DataSource?.BaseStream?.Length ?? 1) - 1; + + // Adjust requested offset if not existing + if (offset < 0) + { + offset = 0; + } + else if (offset > lastByteOffset) + { + offset = lastByteOffset; + } + + if (Offset > offset) + { + // We need to scroll up + Offset -= ((Offset - offset - 1) / _BytesPerRow + 1) * _BytesPerRow; + } + + if (Offset + maxBytesDisplayed <= offset) + { + // We need to scroll down + Offset += ((offset - (Offset + maxBytesDisplayed)) / _BytesPerRow + 1) * _BytesPerRow; + } + } + + // Using .HasFlag(x) to correctly detect state of modifier keys (CTRL, SHIFT, ...) + private static bool IsKeyDown(VirtualKey key) => InputKeyboardSource.GetKeyStateForCurrentThread(key).HasFlag(CoreVirtualKeyStates.Down); + + /// + protected override void OnKeyDown(KeyRoutedEventArgs e) + { + base.OnKeyDown(e); + + // Context Menu + switch (e.Key) + { + case VirtualKey.Application: + { + ShowContextMenu(); + e.Handled = true; + return; + } + + case VirtualKey.F10: + { + if (IsKeyDown(VirtualKey.LeftShift) || IsKeyDown(VirtualKey.RightShift)) + { + ShowContextMenu(); + } + + e.Handled = true; + return; + } + } + + // Other keys + if (Columns > 0 && MaxVisibleRows > 0) + { + switch (e.Key) + { + case VirtualKey.A: + { + if (IsKeyDown(VirtualKey.LeftControl) || IsKeyDown(VirtualKey.RightControl)) + { + if (SelectAllCanExecute(null)) + { + SelectionStart = 0; + SelectionEnd = DataSource.BaseStream.Length; + } + } + + e.Handled = true; + break; + } + + case VirtualKey.C: + { + if (IsKeyDown(VirtualKey.LeftControl) || IsKeyDown(VirtualKey.RightControl)) + { + if (CopyCanExecute(null)) + { + if (IsKeyDown(VirtualKey.LeftShift) || IsKeyDown(VirtualKey.RightShift)) + { + // Copy text + Copy(true); + } + else + { + // Copy data + Copy(false); + } + } + } + + e.Handled = true; + break; + } + + case VirtualKey.Down: + { + if (IsKeyDown(VirtualKey.LeftShift) || IsKeyDown(VirtualKey.RightShift)) + { + SelectionEnd += _BytesPerRow; + } + else + { + SelectionStart += _BytesPerRow; + SelectionEnd = SelectionStart + _BytesPerColumn; + } + + ScrollToOffset(SelectionEnd - _BytesPerColumn); + + e.Handled = true; + + break; + } + + case VirtualKey.End: + { + if (IsKeyDown(VirtualKey.LeftControl) || IsKeyDown(VirtualKey.RightControl)) + { + SelectionEnd = DataSource.BaseStream.Length; + + if (!IsKeyDown(VirtualKey.LeftShift) && !IsKeyDown(VirtualKey.RightShift)) + { + SelectionStart = SelectionEnd - _BytesPerColumn; + } + + ScrollToOffset(SelectionEnd - _BytesPerColumn); + } + else + { + SelectionEnd += (Offset - SelectionEnd).Mod(_BytesPerRow); + + if (!IsKeyDown(VirtualKey.LeftShift) && !IsKeyDown(VirtualKey.RightShift)) + { + SelectionStart = SelectionEnd - _BytesPerColumn; + } + + ScrollToOffset(SelectionEnd - _BytesPerColumn); + } + + e.Handled = true; + + break; + } + + case VirtualKey.Home: + { + if (IsKeyDown(VirtualKey.LeftControl) || IsKeyDown(VirtualKey.RightControl)) + { + SelectionEnd = 0; + + if (!IsKeyDown(VirtualKey.LeftShift) && !IsKeyDown(VirtualKey.RightShift)) + { + SelectionStart = SelectionEnd; + SelectionEnd = SelectionStart + _BytesPerColumn; + } + + ScrollToOffset(SelectionEnd - _BytesPerColumn); + } + else + { + // TODO: Because of the way we represent selection there is no way to distinguish at the + // moment whether the selection ends at the start of the current line or the end of the + // previous line. As such, when the Shift+End hotkey is used twice consecutively a whole + // new line above the current selection will be selected. This is undesirable behavior + // that deviates from the canonical semantics of Shift+End. + SelectionEnd -= (SelectionEnd - 1 - Offset).Mod(_BytesPerRow) + 1; + + if (!IsKeyDown(VirtualKey.LeftShift) && !IsKeyDown(VirtualKey.RightShift)) + { + SelectionStart = SelectionEnd; + SelectionEnd = SelectionStart + _BytesPerColumn; + } + + ScrollToOffset(SelectionEnd - _BytesPerColumn); + } + + e.Handled = true; + + break; + } + + case VirtualKey.Left: + { + if (IsKeyDown(VirtualKey.LeftShift) || IsKeyDown(VirtualKey.RightShift)) + { + SelectionEnd -= _BytesPerColumn; + } + else + { + SelectionStart -= _BytesPerColumn; + SelectionEnd = SelectionStart + _BytesPerColumn; + } + + ScrollToOffset(SelectionEnd - _BytesPerColumn); + + e.Handled = true; + + break; + } + + case VirtualKey.PageDown: + { + bool isOffsetVisibleBeforeSelectionChange = IsOffsetVisible(SelectionEnd); + + SelectionEnd += _BytesPerRow * MaxVisibleRows; + + if (!IsKeyDown(VirtualKey.LeftShift) && !IsKeyDown(VirtualKey.RightShift)) + { + SelectionStart = SelectionEnd - _BytesPerColumn; + } + + _ScrollBar.Value += MaxVisibleRows; + + OnVerticalScrollBarScroll(_ScrollBar, ScrollEventType.SmallIncrement, _ScrollBar.Value); + + e.Handled = true; + break; + } + + case VirtualKey.PageUp: + { + bool isOffsetVisibleBeforeSelectionChange = IsOffsetVisible(SelectionEnd); + + SelectionEnd -= _BytesPerRow * MaxVisibleRows; + + if (!IsKeyDown(VirtualKey.LeftShift) && !IsKeyDown(VirtualKey.RightShift)) + { + SelectionStart = SelectionEnd - _BytesPerColumn; + SelectionEnd = SelectionStart + _BytesPerColumn; + } + + _ScrollBar.Value -= MaxVisibleRows; + + OnVerticalScrollBarScroll(_ScrollBar, ScrollEventType.SmallIncrement, _ScrollBar.Value); + + e.Handled = true; + break; + } + + case VirtualKey.Right: + { + if (IsKeyDown(VirtualKey.LeftShift) || IsKeyDown(VirtualKey.RightShift)) + { + SelectionEnd += _BytesPerColumn; + } + else + { + SelectionStart += _BytesPerColumn; + SelectionEnd = SelectionStart + _BytesPerColumn; + } + + ScrollToOffset(SelectionEnd - _BytesPerColumn); + + e.Handled = true; + break; + } + + case VirtualKey.Up: + { + if (IsKeyDown(VirtualKey.LeftShift) || IsKeyDown(VirtualKey.RightShift)) + { + SelectionEnd -= _BytesPerRow; + } + else + { + SelectionStart -= _BytesPerRow; + SelectionEnd = SelectionStart + _BytesPerColumn; + } + + ScrollToOffset(SelectionEnd - _BytesPerColumn); + + e.Handled = true; + break; + } + } + } + } + + protected override void OnDoubleTapped(DoubleTappedRoutedEventArgs e) + { + Focus(FocusState.Programmatic); + e.Handled = true; + + if (e.PointerDeviceType == PointerDeviceType.Mouse) + { + OnMouseDoubleClick(e.GetPosition(_Canvas)); + } + } + + protected override void OnPointerPressed(PointerRoutedEventArgs e) + { + Focus(FocusState.Programmatic); + e.Handled = true; + + var pps = e.GetCurrentPoint(this).Properties; + if (pps != null) + { + if (pps.PointerUpdateKind == PointerUpdateKind.LeftButtonPressed) + { + OnMouseLeftButtonDown(e); + } + } + } + + protected override void OnPointerReleased(PointerRoutedEventArgs e) + { + base.OnPointerReleased(e); + var pps = e.GetCurrentPoint(this).Properties; + if (pps != null) + { + if (pps.PointerUpdateKind == PointerUpdateKind.LeftButtonReleased) + { + OnMouseLeftButtonUp(e); + } + } + } + + protected override void OnPointerCanceled(PointerRoutedEventArgs e) + { + base.OnPointerCanceled(e); + var pps = e.GetCurrentPoint(this).Properties; + if (pps != null) + { + if (pps.PointerUpdateKind == PointerUpdateKind.LeftButtonReleased) + { + OnMouseLeftButtonUp(e); + } + } + } + + protected override void OnPointerCaptureLost(PointerRoutedEventArgs e) + { + base.OnPointerCaptureLost(e); + } + + protected override void OnPointerExited(PointerRoutedEventArgs e) + { + base.OnPointerExited(e); + } + + protected override void OnPointerEntered(PointerRoutedEventArgs e) + { + base.OnPointerEntered(e); + } + + /// + protected override void OnPointerMoved(PointerRoutedEventArgs e) + { + base.OnPointerMoved(e); + + if (_HighlightState != SelectionArea.None) + { + var position = e.GetCurrentPoint(_Canvas).Position; + var currentMouseOverOffset = ConvertPositionToOffset(position); + + switch (_HighlightState) + { + case SelectionArea.Address: + { + if (currentMouseOverOffset >= SelectionStart) + { + SelectionEnd = currentMouseOverOffset + _BytesPerRow; + } + else + { + SelectionEnd = currentMouseOverOffset; + } + + // Adjust start point + if (SelectionStart > SelectionEnd && _pointerMoveSelectionAdjustment != SelectionAdjustment.Up) + { + // If moving up and SelectionStart was previously adjusted down or not adjusted, then set SelectionStart to end of row. + SelectionStart = SelectionStart + (_BytesPerRow - _BytesPerColumn); + _pointerMoveSelectionAdjustment = SelectionAdjustment.Up; + } + else if (SelectionStart < SelectionEnd && _pointerMoveSelectionAdjustment == SelectionAdjustment.Up) + { + // If moving down and SelectionStart was previously adjusted up, then set SelectionStart to start of row. + SelectionStart = SelectionStart - (_BytesPerRow - _BytesPerColumn); + _pointerMoveSelectionAdjustment = SelectionAdjustment.Down; + } + break; + } + case SelectionArea.Data: + case SelectionArea.Text: + { + if (currentMouseOverOffset >= SelectionStart) + { + SelectionEnd = currentMouseOverOffset + _BytesPerColumn; + } + else + { + SelectionEnd = currentMouseOverOffset; + } + break; + } + } + + // Move next row into view if selection goes out of view + if (position.Y > _AddressRect.Y + _AddressRect.Height) + { + ScrollToOffset(currentMouseOverOffset + _BytesPerRow); + } + else if (position.Y < _AddressRect.Y) + { + ScrollToOffset(currentMouseOverOffset - _BytesPerRow); + } + } + } + + /// + protected override void OnPointerWheelChanged(PointerRoutedEventArgs e) + { + base.OnPointerWheelChanged(e); + var Delta = e.GetCurrentPoint(this).Properties.MouseWheelDelta; + + var value = _ScrollBar.Value; + if (Delta < 0) + { + _ScrollBar.Value += _ScrollWheelScrollRows; + + OnVerticalScrollBarScroll(_ScrollBar, ScrollEventType.SmallIncrement, _ScrollBar.Value); + } + else + { + _ScrollBar.Value -= _ScrollWheelScrollRows; + + OnVerticalScrollBarScroll(_ScrollBar, ScrollEventType.SmallDecrement, _ScrollBar.Value); + } + } + + /// + private void OnMouseDoubleClick(Point position) + { + Point addressVerticalLinePoint0 = CalculateAddressVerticalLinePoint0(); + + if (position.X < addressVerticalLinePoint0.X) + { + _HighlightBegin = SelectionArea.Address; + _HighlightState = SelectionArea.Address; + + SelectionStart = ConvertPositionToOffset(position); + SelectionEnd = SelectionStart + _BytesPerRow; + } + } + + /// + private void OnMouseLeftButtonDown(PointerRoutedEventArgs e) + { + if (_HighlightState == SelectionArea.None && CapturePointer(e.Pointer)) + { + Point position = e.GetCurrentPoint(_Canvas).Position; + + Point addressVerticalLinePoint0 = CalculateAddressVerticalLinePoint0(); + Point dataVerticalLinePoint0 = CalculateDataVerticalLinePoint0(); + Point textVerticalLinePoint0 = CalculateTextVerticalLinePoint0(); + + if (position.X < addressVerticalLinePoint0.X) + { + _HighlightBegin = SelectionArea.Address; + _HighlightState = SelectionArea.Address; + } + else if (position.X < dataVerticalLinePoint0.X) + { + _HighlightBegin = SelectionArea.Data; + _HighlightState = SelectionArea.Data; + } + else if (position.X < textVerticalLinePoint0.X) + { + _HighlightBegin = SelectionArea.Text; + _HighlightState = SelectionArea.Text; + } + + if (_HighlightState != SelectionArea.None) + { + SelectionStart = ConvertPositionToOffset(position); + + SelectionEnd = SelectionStart + _BytesPerColumn; + } + } + } + + /// + private void OnMouseLeftButtonUp(PointerRoutedEventArgs e) + { + _HighlightState = SelectionArea.None; + + ReleasePointerCapture(e.Pointer); + } + + private static void OnPropertyChangedInvalidateVisual(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var HexBox = (HexBox)d; + + HexBox.Reflush(); + } + + private static void OnSelectionEndChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var HexBox = (HexBox)d; + + HexBox.Reflush(); + + HexBox.OnPropertyChanged(nameof(SelectionEnd)); + HexBox.OnPropertyChanged(nameof(SelectionLength)); + HexBox.OnPropertyChanged(nameof(SelectedOffset)); + HexBox.OnPropertyChanged(nameof(SelectedAddress)); + HexBox.OnPropertyChanged(nameof(IsSelectionActive)); + } + + private static void OnSelectionStartChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var HexBox = (HexBox)d; + + HexBox.Reflush(); + + HexBox.OnPropertyChanged(nameof(SelectionStart)); + HexBox.OnPropertyChanged(nameof(SelectionLength)); + HexBox.OnPropertyChanged(nameof(SelectedOffset)); + HexBox.OnPropertyChanged(nameof(SelectedAddress)); + HexBox.OnPropertyChanged(nameof(IsSelectionActive)); + } + + private static object CoerceColumns(DependencyObject d, object value) + { + var HexBox = (HexBox)d; + + if (HexBox.MaxVisibleColumns == 0) + { + return (int)value; + } + else + { + return Math.Min((int)value, HexBox.MaxVisibleColumns); + } + } + + private static object CoerceMaxVisibleColumns(DependencyObject d, object value) + { + return Math.Min((int)value, _MaxColumns); + } + + private static object CoerceMaxVisibleRows(DependencyObject d, object value) + { + return Math.Min((int)value, _MaxRows); + } + + private static object CoerceSelectionStart(DependencyObject d, object value) + { + var HexBox = (HexBox)d; + + if (HexBox.DataSource != null) + { + long selectionStart = (long)value; + + // Selection offset cannot start in the middle of the data width + selectionStart -= selectionStart % HexBox._BytesPerColumn; + + // Selection start cannot be at the end of the stream so adjust by data width number of bytes + value = selectionStart.Clamp(0, HexBox.DataSource.BaseStream.Length / HexBox._BytesPerColumn * HexBox._BytesPerColumn - HexBox._BytesPerColumn); + } + else + { + value = 0L; + } + + return value; + } + + private static object CoerceSelectionEnd(DependencyObject d, object value) + { + var HexBox = (HexBox)d; + + if (HexBox.DataSource != null) + { + long selectionEnd = (long)value; + + // Selection offset cannot start in the middle of the data width + selectionEnd -= selectionEnd % HexBox._BytesPerColumn; + + // Unlike selection start the selection end can be at the end of the stream + value = selectionEnd.Clamp(0, HexBox.DataSource.BaseStream.Length / HexBox._BytesPerColumn * HexBox._BytesPerColumn); + } + else + { + value = 0L; + } + + return value; + } + + private static object CoerceOffset(DependencyObject d, object value) + { + var HexBox = (HexBox)d; + + if (HexBox.DataSource != null) + { + long offset = (long)value; + + value = offset.Clamp(0, HexBox.DataSource.BaseStream.Length); + } + else + { + value = 0L; + } + + return value; + } + + private static void OnAddressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var HexBox = (HexBox)d; + + HexBox.SelectionStart = 0; + HexBox.SelectionEnd = 0; + + HexBox.Reflush(); + + HexBox.OnPropertyChanged(nameof(Address)); + HexBox.OnPropertyChanged(nameof(SelectedAddress)); + } + + private static void OnDataTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var HexBox = (HexBox)d; + + switch (HexBox.DataType) + { + case DataType.Int_1: + HexBox.DataWidth = 1; + break; + case DataType.Int_2: + HexBox.DataWidth = 2; + break; + case DataType.Int_4: + HexBox.DataWidth = 4; + break; + case DataType.Int_8: + HexBox.DataWidth = 8; + break; + case DataType.Float_32: + HexBox.DataWidth = 4; + break; + case DataType.Float_64: + HexBox.DataWidth = 8; + break; + } + + HexBox.Reflush(); + } + + private static void OnDataSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var HexBox = (HexBox)d; + + HexBox.Offset = 0; + HexBox.SelectionStart = 0; + HexBox.SelectionEnd = 0; + + HexBox.Reflush(); + } + + private void Reflush() + { + if (_Canvas != null) + { + _Canvas.Invalidate(); + } + } + + private void OnPropertyChanged([CallerMemberName] string name = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + + private string ReadFormattedText() + { + StringBuilder builder = new(DataWidth); + + switch (TextFormat) + { + case TextFormat.Ascii: + { + for (var k = 0; k < DataWidth; ++k) + { + byte value = DataSource.ReadByte(); + + if (value > 31 && value < 127) + { + builder.Append(Convert.ToChar(value)); + } + else + { + builder.Append('.'); + } + } + + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(TextFormat)} value."); + } + } + + return builder.ToString(); + } + + private string ReadFormattedData() + { + string result; + + if (DataType < DataType.Float_32) + { + switch (DataFormat) + { + case DataFormat.Decimal: + { + if (DataSignedness == DataSignedness.Signed) + { + switch (DataType) + { + case DataType.Int_1: + { + result = $"{DataSource.ReadSByte():+#;-#;0}".PadLeft(4); + break; + } + + case DataType.Int_2: + { + result = $"{EndianBitConverter.Convert(DataSource.ReadInt16(), Endianness):+#;-#;0}".PadLeft(6); + break; + } + + case DataType.Int_4: + { + result = $"{EndianBitConverter.Convert(DataSource.ReadInt32(), Endianness):+#;-#;0}".PadLeft(11); + break; + } + + case DataType.Int_8: + { + result = $"{EndianBitConverter.Convert(DataSource.ReadInt64(), Endianness):+#;-#;0}".PadLeft(21); + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(DataWidth)} value."); + } + } + } + else if (DataSignedness == DataSignedness.Unsigned) + { + switch (DataType) + { + case DataType.Int_1: + { + result = $"{DataSource.ReadByte()}".PadLeft(3); + break; + } + + case DataType.Int_2: + { + result = $"{EndianBitConverter.Convert(DataSource.ReadUInt16(), Endianness)}".PadLeft(5); + break; + } + + case DataType.Int_4: + { + result = $"{EndianBitConverter.Convert(DataSource.ReadUInt32(), Endianness)}".PadLeft(10); + break; + } + + case DataType.Int_8: + { + result = $"{EndianBitConverter.Convert(DataSource.ReadUInt64(), Endianness)}".PadLeft(20); + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(DataWidth)} value."); + } + } + } + else + { + throw new InvalidOperationException($"Invalid {nameof(DataType)} value."); + } + } + break; + + case DataFormat.Hexadecimal: + { + switch (DataType) + { + case DataType.Int_1: + { + result = $"{DataSource.ReadByte(),0:X2}"; + break; + } + + case DataType.Int_2: + { + result = $"{EndianBitConverter.Convert(DataSource.ReadUInt16(), Endianness),0:X4}"; + break; + } + + case DataType.Int_4: + { + result = $"{EndianBitConverter.Convert(DataSource.ReadUInt32(), Endianness),0:X8}"; + break; + } + + case DataType.Int_8: + { + result = $"{EndianBitConverter.Convert(DataSource.ReadUInt64(), Endianness),0:X16}"; + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(DataWidth)} value."); + } + } + + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(DataFormat)} value."); + } + } + } + else + { + switch (DataType) + { + case DataType.Float_32: + { + var bytes = BitConverter.GetBytes(EndianBitConverter.Convert(DataSource.ReadUInt32(), Endianness)); + var value = BitConverter.ToSingle(bytes, 0); + result = $"{value:E08}".PadLeft(16); + break; + } + + case DataType.Float_64: + { + var bytes = BitConverter.GetBytes(EndianBitConverter.Convert(DataSource.ReadUInt64(), Endianness)); + var value = BitConverter.ToSingle(bytes, 0); + result = $"{value:E16}".PadLeft(24); + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(DataWidth)} value."); + } + } + } + + return result; + } + + private void SelectAllExecuted(object sender) + { + SelectAll(); + } + + private void CopyExecuted(object sender) + { + Copy(false); + } + + private void CopyTextExecuted(object sender) + { + Copy(true); + } + + private bool SelectAllCanExecute(object sender) + { + return DataSource != null && (ShowData || ShowText); + } + + private bool CopyCanExecute(object sender) + { + return IsSelectionActive && (ShowData || ShowText); + } + + private void OnVerticalScrollBarValueChanged(object sender, RangeBaseValueChangedEventArgs e) + { + _LastVerticalScrollValue = e.OldValue; + } + + private void OnVerticalScrollBarScroll(object sender, ScrollEventArgs e) + { + long newOffset = (long)e.NewValue * _BytesPerRow; + + Offset = newOffset; + } + + private void OnVerticalScrollBarScroll(object sender, ScrollEventType type, double NewValue) + { + long newOffset = (long)NewValue * _BytesPerRow; + + Offset = newOffset; + } + + private string GetFormattedAddressText(ulong address) + { + string formattedAddressText; + + switch (AddressFormat) + { + case AddressFormat.Address16: + { + formattedAddressText = $"{address & 0xFFFF,0:X4}"; + break; + } + + case AddressFormat.Address24: + { + formattedAddressText = $"{address >> 16 & 0xFF,0:X2}:{address & 0xFFFF,0:X4}"; + break; + } + + case AddressFormat.Address32: + { + formattedAddressText = $"{address >> 16 & 0xFFFF,0:X4}:{address & 0xFFFF,0:X4}"; + break; + } + + case AddressFormat.Address48: + { + formattedAddressText = $"{address >> 32 & 0xFF,0:X4}:{address & 0xFFFFFFFF,0:X8}"; + break; + } + + case AddressFormat.Address64: + { + formattedAddressText = $"{address >> 32,0:X8}:{address & 0xFFFFFFFF,0:X8}"; + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(AddressFormat)} value."); + } + } + + return formattedAddressText; + } + + private int CalculateAddressColumnCharWidth() + { + int addressColumnCharWidth; + + switch (AddressFormat) + { + case AddressFormat.Address16: + { + addressColumnCharWidth = 4; + break; + } + + case AddressFormat.Address24: + { + addressColumnCharWidth = 7; + break; + } + + case AddressFormat.Address32: + { + addressColumnCharWidth = 9; + break; + } + + case AddressFormat.Address48: + { + addressColumnCharWidth = 13; + break; + } + + case AddressFormat.Address64: + { + addressColumnCharWidth = 17; + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(AddressFormat)} value."); + } + } + + return addressColumnCharWidth; + } + + private int CalculateDataColumnCharWidth() + { + int dataColumnCharWidth; + + if (DataType < DataType.Float_32) + { + switch (DataFormat) + { + case DataFormat.Decimal: + { + switch (DataSignedness) + { + case DataSignedness.Signed: + { + switch (DataType) + { + case DataType.Int_1: + { + dataColumnCharWidth = 4; + break; + } + + case DataType.Int_2: + { + dataColumnCharWidth = 6; + break; + } + + case DataType.Int_4: + { + dataColumnCharWidth = 11; + break; + } + + case DataType.Int_8: + { + dataColumnCharWidth = 21; + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(DataWidth)} value."); + } + } + } + + break; + + case DataSignedness.Unsigned: + { + switch (DataType) + { + case DataType.Int_1: + { + dataColumnCharWidth = 3; + break; + } + + case DataType.Int_2: + { + dataColumnCharWidth = 5; + break; + } + + case DataType.Int_4: + { + dataColumnCharWidth = 10; + break; + } + + case DataType.Int_8: + { + dataColumnCharWidth = 20; + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(DataWidth)} value."); + } + } + } + + break; + + default: + { + throw new InvalidOperationException($"Invalid {nameof(DataType)} value."); + } + } + } + + break; + + case DataFormat.Hexadecimal: + { + switch (DataWidth) + { + case 1: + case 2: + case 4: + case 8: + { + dataColumnCharWidth = 2 * DataWidth; + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(DataWidth)} value."); + } + } + + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(DataFormat)} value."); + } + } + } + else + { + switch (DataType) + { + case DataType.Float_32: + { + dataColumnCharWidth = 16; + break; + } + + case DataType.Float_64: + { + dataColumnCharWidth = 24; + break; + } + + default: + { + throw new InvalidOperationException($"Invalid {nameof(DataWidth)} value."); + } + } + } + return dataColumnCharWidth; + } + + private Point CalculateAddressVerticalLinePoint0() + { + Point point1 = default; + + if (ShowAddress) + { + point1.X = (CalculateAddressColumnCharWidth() + _CharsBetweenSections) * _TextMeasure.Width; + } + + return point1; + } + + private Point CalculateAddressVerticalLinePoint1() + { + Point point2 = default; + + if (ShowAddress) + { + point2.X = (CalculateAddressColumnCharWidth() + _CharsBetweenSections) * _TextMeasure.Width; + } + + point2.Y = Math.Min(_TextMeasure.Height * (MaxVisibleRows + 1), _Canvas.ActualHeight); + + return point2; + } + + private Point CalculateDataVerticalLinePoint0() + { + Point point1 = CalculateAddressVerticalLinePoint0(); + + if (ShowData) + { + point1.X += (_CharsBetweenSections + (CalculateDataColumnCharWidth() + _CharsBetweenDataColumns) * Columns - _CharsBetweenDataColumns + _CharsBetweenSections) * _TextMeasure.Width; + } + + return point1; + } + + private Point CalculateDataVerticalLinePoint1() + { + Point point2 = CalculateAddressVerticalLinePoint1(); + + if (ShowData) + { + point2.X += (_CharsBetweenSections + (CalculateDataColumnCharWidth() + _CharsBetweenDataColumns) * Columns - _CharsBetweenDataColumns + _CharsBetweenSections) * _TextMeasure.Width; + } + + return point2; + } + + private int CalculateTextColumnCharWidth() + { + return _BytesPerColumn; + } + + private Point CalculateTextVerticalLinePoint0() + { + Point point1 = CalculateDataVerticalLinePoint0(); + + if (ShowText) + { + point1.X += (_CharsBetweenSections + CalculateTextColumnCharWidth() * Columns + _CharsBetweenSections) * _TextMeasure.Width; + } + + return point1; + } + + private Point CalculateTextVerticalLinePoint1() + { + Point point2 = CalculateDataVerticalLinePoint1(); + + if (ShowText) + { + point2.X += (_CharsBetweenSections + CalculateTextColumnCharWidth() * Columns + _CharsBetweenSections) * _TextMeasure.Width; + } + + return point2; + } + + private void UpdateState() + { + UpdateMaxVisibleRowsAndColumns(); + UpdateScrollBar(); + UpdateColumnsLayout(); + } + + private void UpdateColumnsLayout() + { + var p0 = CalculateAddressVerticalLinePoint0(); + var p1 = CalculateAddressVerticalLinePoint1(); + _AddressRect = new(p0, p1); + + p0 = CalculateDataVerticalLinePoint0(); + p1 = CalculateDataVerticalLinePoint1(); + _DataRect = new(p0, p1); + + p0 = CalculateTextVerticalLinePoint0(); + p1 = CalculateTextVerticalLinePoint1(); + _TextRect = new(p0, p1); + } + + private void UpdateMaxVisibleRowsAndColumns() + { + int maxVisibleRows = 0; + int maxVisibleColumns = 0; + + if ((ShowAddress || ShowData || ShowText) && _Canvas != null) + { + { + SKRect cellSize = new(); + string bigChars = "0123456789abcdef ABCDEF"; + for (int i = 0; i < bigChars.Length; i++) + { + var s = bigChars.Substring(i, 1); + if (_TextPaint.ContainsGlyphs(s)) // if the font does not contain the glyph, then skip it + { + var rect = new SKRect(); + _TextPaint.MeasureText(s, ref rect); + cellSize.Union(rect); + } + } + _TextMeasure = cellSize; + } + + _TextMeasure.Bottom = _TextMeasure.Height; /* 2 * line font height */ + + maxVisibleRows = Math.Max(0, (int)(_Canvas.ActualHeight / _TextMeasure.Height)); + + if (ShowData || ShowText) + { + int charsPerRow = (int)(_Canvas.ActualWidth / _TextMeasure.Width); + + if (ShowAddress) + { + charsPerRow -= CalculateAddressColumnCharWidth() + 2 * _CharsBetweenSections; + } + + if (ShowData && ShowText) + { + charsPerRow -= 3 * _CharsBetweenSections; + } + + int charsPerColumn = 0; + + if (ShowData) + { + charsPerColumn += CalculateDataColumnCharWidth() + _CharsBetweenDataColumns; + } + + if (ShowText) + { + charsPerColumn += CalculateTextColumnCharWidth(); + } + + if (charsPerColumn != 0) + { + maxVisibleColumns = Math.Max(0, charsPerRow / charsPerColumn); + } + } + else + { + maxVisibleColumns = 0; + } + } + + MaxVisibleRows = maxVisibleRows; + MaxVisibleColumns = maxVisibleColumns; + + // Maximum visible rows has now changed and so we must update the maximum amount we should scroll by + _ScrollBar.LargeChange = maxVisibleRows; + } + + private void UpdateScrollBar() + { + if ((ShowAddress || ShowData || ShowText) && DataSource != null && Columns > 0 && MaxVisibleRows > 0) + { + long q = DataSource.BaseStream.Length / _BytesPerRow; + long r = DataSource.BaseStream.Length % _BytesPerRow; + + // Each scroll value represents a single drawn row + _ScrollBar.Maximum = q + (r > 0 ? 1 : 0) - MaxVisibleRows; + + // Adjust the scroll value based on the current offset + _ScrollBar.Value = Offset / _BytesPerRow; + + // Adjust again to compensate for residual bytes if the number of bytes between the start of the stream + // and the current offset is less than the number of bytes we can display per row + if (_ScrollBar.Value == 0 && Offset > 0) + { + ++_ScrollBar.Value; + } + } + else + { + _ScrollBar.Maximum = 0; + } + } + + private long ConvertPositionToOffset(Point position) + { + long offset = Offset; + + switch (_HighlightBegin) + { + case SelectionArea.Address: + { + // Clamp the Y coordinate to within the address region + position.Y = position.Y.Clamp(_AddressRect.Top, _AddressRect.Bottom); + + // Convert the Y coordinate to the row number + position.Y /= _TextMeasure.Height; + + if (position.Y >= MaxVisibleRows) + { + // Due to floating point rounding we may end up with exactly the maximum number of rows, so adjust to compensate + --position.Y; + } + + offset += _BytesPerRow * (long)position.Y; + } + + break; + + case SelectionArea.Data: + { + var pix_CharsBetweenSections = _CharsBetweenSections * _TextMeasure.Width; + + // Clamp the X coordinate to within the data region + position.X = position.X.Clamp(_AddressRect.Left + pix_CharsBetweenSections, _DataRect.Left - pix_CharsBetweenSections); + + // Normalize with respect to the data region + position.X -= _AddressRect.Left + pix_CharsBetweenSections; + + // Convert the X coordinate to the column number + position.X /= (CalculateDataColumnCharWidth() + _CharsBetweenDataColumns) * _TextMeasure.Width; + + if (position.X >= Columns) + { + // Due to floating point rounding we may end up with exactly the maximum number of columns, so adjust to compensate + --position.X; + } + + // Clamp the Y coordinate to within the data region + position.Y = position.Y.Clamp(_DataRect.Top, _DataRect.Bottom); + + // Convert the Y coordinate to the row number + position.Y /= _TextMeasure.Height; + + if (position.Y >= MaxVisibleRows) + { + // Due to floating point rounding we may end up with exactly the maximum number of rows, so adjust to compensate + --position.Y; + } + + offset += ((long)position.Y * Columns + (long)position.X) * _BytesPerColumn; + } + + break; + + case SelectionArea.Text: + { + var pix_CharsBetweenSections = _CharsBetweenSections * _TextMeasure.Width; + + // Clamp the X coordinate to within the text region + position.X = position.X.Clamp(_DataRect.Left + pix_CharsBetweenSections, _TextRect.Left - pix_CharsBetweenSections); + + // Normalize with respect to the text region + position.X -= _DataRect.Left + pix_CharsBetweenSections; + + // Convert the X coordinate to the column number + position.X /= CalculateTextColumnCharWidth() * _TextMeasure.Width; + + if (position.X >= Columns) + { + // Due to floating point rounding we may end up with exactly the maximum number of columns, so + // adjust to compensate + --position.X; + } + + // Clamp the Y coordinate to within the text region + position.Y = position.Y.Clamp(_TextRect.Top, _TextRect.Bottom); + + // Convert the Y coordinate to the row number + position.Y /= _TextMeasure.Height; + + if (position.Y >= MaxVisibleRows) + { + // Due to floating point rounding we may end up with exactly the maximum number of rows, so adjust to compensate + --position.Y; + } + + offset += ((long)position.Y * Columns + (long)position.X) * _BytesPerColumn; + } + + break; + + default: + { + throw new InvalidOperationException($"Invalid highlight state ${_HighlightState}"); + } + } + + return offset; + } + + private Point ConvertOffsetToPosition(long offset, SelectionArea relativeTo) + { + Point position = default; + + switch (relativeTo) + { + case SelectionArea.Data: + { + position.X = _AddressRect.Left + _CharsBetweenSections * _TextMeasure.Width; + position.Y = _AddressRect.Top; + + // Normalize requested offset to a zero based column + long normalizedColumn = (offset - Offset) / _BytesPerColumn; + + position.X += (normalizedColumn % Columns + Columns) % Columns * (CalculateDataColumnCharWidth() + _CharsBetweenDataColumns) * _TextMeasure.Width; + + if (normalizedColumn < 0) + { + // Negative normalized offset means the Y position is above the current offset. Because division + // rounds toward zero we need to compensate here. + position.Y += ((normalizedColumn + 1) / Columns - 1) * _TextMeasure.Height; + } + else + { + position.Y += normalizedColumn / Columns * _TextMeasure.Height; + } + } + + break; + + case SelectionArea.Text: + { + position.X = _DataRect.Left + _CharsBetweenSections * _TextMeasure.Width; + position.Y = _DataRect.Top; + + // Normalize requested offset to a zero based column + long normalizedColumn = (offset - Offset) / _BytesPerColumn; + + position.X += (normalizedColumn % Columns + Columns) % Columns * CalculateTextColumnCharWidth() * _TextMeasure.Width; + + if (normalizedColumn < 0) + { + // Negative normalized offset means the Y position is above the current offset. Because division + // rounds toward zero we need to compensate here. + position.Y += ((normalizedColumn + 1) / Columns - 1) * _TextMeasure.Height; + } + else + { + position.Y += normalizedColumn / Columns * _TextMeasure.Height; + } + } + + break; + + default: + { + throw new ArgumentException($"Invalid relative area {relativeTo}", nameof(relativeTo)); + } + } + + return position; + } + + private bool IsOffsetVisible(long offset) + { + long maxBytesDisplayed = _BytesPerRow * MaxVisibleRows; + + return Offset <= offset && Offset + maxBytesDisplayed >= offset; + } + + /// + /// Show the context menu programatical. + /// Invoked if Application key or SCHIFT+F10 is pressed. + /// + private void ShowContextMenu() + { + // Get offset for context menu + var lastVisibleOffset = Offset + (_BytesPerRow * MaxVisibleRows) - 1; + var offset = Math.Max(Math.Max(SelectionStart, SelectionEnd), Offset); + var palcementOffset = Math.Min(offset, lastVisibleOffset); + + // Show menu + if (ShowData) + { + _Canvas.ContextFlyout.ShowAt(_Canvas, new FlyoutShowOptions + { + Position = ConvertOffsetToPosition(palcementOffset, SelectionArea.Data), + }); + } + else if (ShowText) + { + _Canvas.ContextFlyout.ShowAt(_Canvas, new FlyoutShowOptions + { + Position = ConvertOffsetToPosition(palcementOffset, SelectionArea.Text), + }); + } + else + { + _Canvas.ContextFlyout.ShowAt(_Canvas, new FlyoutShowOptions + { + Position = new Point(0, 0), + }); + } + } + + /// + /// Initializes static members of the class. + /// + public HexBox() + { + DefaultStyleKey = typeof(HexBox); + } + } + +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Library/EndianConvert/EndianBinaryReader.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Library/EndianConvert/EndianBinaryReader.cs new file mode 100644 index 0000000000..1ba0658bd7 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Library/EndianConvert/EndianBinaryReader.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// + +namespace RegistryPreviewUILib.HexBox.Library.EndianConvert +{ + using System; + using System.IO; + using System.Text; + + /// + /// Reads primitive data types as binary values in a specific encoding and endianness. + /// + public class EndianBinaryReader : BinaryReader + { + /// + /// Initializes a new instance of the class based on the specified stream, endianness, and using UTF-8 + /// encoding. + /// + /// + /// + /// The input stream. + /// + /// + /// + /// The endianness of the data in the input stream. + /// + /// + /// + /// The stream does not support reading, is null, or is already closed. + /// + public EndianBinaryReader(Stream input, Endianness endianness) + : this(input, endianness, Encoding.UTF8) + { + // Void + } + + /// + /// Initializes a new instance of the class based on the specified stream, endianness, and character + /// encoding. + /// + /// + /// + /// The input stream. + /// + /// + /// + /// The endianness of the data in the input stream. + /// + /// + /// + /// The character encoding to use. + /// + /// + /// + /// The stream does not support reading, is null, or is already closed. + /// + public EndianBinaryReader(Stream input, Endianness endianness, Encoding encoding) + : this(input, endianness, encoding, false) + { + // Void + } + + /// + /// Initializes a new instance of the class based on the specified stream, endianness, and character + /// encoding, and optionally leaves the stream open. + /// + /// + /// + /// The input stream. + /// + /// + /// + /// The endianness of the data in the input stream. + /// + /// + /// + /// The character encoding to use. + /// + /// + /// + /// true to leave the stream open after the object is disposed; false otherwise. + /// + /// + /// + /// The stream does not support reading, is null, or is already closed. + /// + public EndianBinaryReader(Stream input, Endianness endianness, Encoding encoding, bool leaveOpen) + : base(input, encoding, leaveOpen) + { + Endianness = endianness; + } + + /// + /// Gets the endianness of the data in the input stream. + /// + public Endianness Endianness + { + get; + } + + /// + /// Reads a decimal value from the current stream and advances the current position of the stream by sixteen bytes. + /// + /// + /// + /// A decimal value read from the current stream. + /// + public override decimal ReadDecimal() + { + throw new NotImplementedException(); + } + + /// + /// Reads an 8-byte floating point value from the current stream and advances the current position of the stream by eight bytes. + /// + /// + /// + /// An 8-byte floating point value read from the current stream. + /// + public override double ReadDouble() + { + throw new NotImplementedException(); + } + + /// + /// Reads a 2-byte signed integer from the current stream and advances the current position of the stream by two bytes. + /// + /// + /// + /// A 2-byte signed integer read from the current stream. + /// + public override short ReadInt16() + { + return EndianBitConverter.Convert(base.ReadInt16(), Endianness); + } + + /// + /// Reads a 4-byte signed integer from the current stream and advances the current position of the stream by four bytes. + /// + /// + /// + /// A 4-byte signed integer read from the current stream. + /// + public override int ReadInt32() + { + return EndianBitConverter.Convert(base.ReadInt32(), Endianness); + } + + /// + /// Reads an 8-byte signed integer from the current stream and advances the current position of the stream by eight bytes. + /// + /// + /// + /// An 8-byte signed integer read from the current stream. + /// + public override long ReadInt64() + { + return EndianBitConverter.Convert(base.ReadInt64(), Endianness); + } + + /// + /// Reads a 4-byte floating point value from the current stream and advances the current position of the stream by four bytes. + /// + /// + /// + /// A 4-byte floating point value read from the current stream. + /// + public override float ReadSingle() + { + throw new NotImplementedException(); + } + + /// + /// Reads a string from the current stream. The string is prefixed with the length, encoded as an integer seven bits at a time. + /// + /// + /// + /// The string being read. + /// + public override string ReadString() + { + throw new NotImplementedException(); + } + + /// + /// Reads a 2-byte unsigned integer from the current stream using little-endian encoding and advances the position of the stream by + /// two bytes. + /// + /// + /// + /// A 2-byte unsigned integer read from this stream. + /// + public override ushort ReadUInt16() + { + return EndianBitConverter.Convert(base.ReadUInt16(), Endianness); + } + + /// + /// Reads a 4-byte unsigned integer from the current stream using little-endian encoding and advances the position of the stream by + /// four bytes. + /// + /// + /// + /// A 4-byte unsigned integer read from this stream. + /// + public override uint ReadUInt32() + { + return EndianBitConverter.Convert(base.ReadUInt32(), Endianness); + } + + /// + /// Reads an 8-byte unsigned integer from the current stream using little-endian encoding and advances the position of the stream by + /// eight bytes. + /// + /// + /// + /// An 8-byte unsigned integer read from this stream. + /// + public override ulong ReadUInt64() + { + return EndianBitConverter.Convert(base.ReadUInt64(), Endianness); + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Library/EndianConvert/EndianBitConverter.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Library/EndianConvert/EndianBitConverter.cs new file mode 100644 index 0000000000..dde0287c2d --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Library/EndianConvert/EndianBitConverter.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// + +namespace RegistryPreviewUILib.HexBox.Library.EndianConvert +{ + using System; + + /// + /// Converts integral values to the native endianness of this computer architecture. + /// + public static class EndianBitConverter + { + /// + /// Gets the native endianness of this computer architecture. + /// + public static readonly Endianness NativeEndianness = BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian; + + /// + /// Converts a value from the specified endianness to the native endianness. + /// + /// + /// + /// The value to convert. + /// + /// + /// + /// The endianness of . + /// + /// + /// + /// The value converted from the specified endianness to the native endianness (). + /// + public static ushort Convert(ushort value, Endianness endianness) + { + if (endianness == NativeEndianness) + { + return value; + } + else + { + unchecked + { + return (ushort)((value & 0x00FFU) << 8 | + (value & 0xFF00U) >> 8); + } + } + } + + /// + /// Converts a value from the specified endianness to the native endianness. + /// + /// + /// + /// The value to convert. + /// + /// + /// + /// The endianness of . + /// + /// + /// + /// The value converted from the specified endianness to the native endianness (). + /// + public static uint Convert(uint value, Endianness endianness) + { + if (endianness == NativeEndianness) + { + return value; + } + else + { + unchecked + { + return (value & 0x000000FFU) << 24 | + (value & 0xFF000000U) >> 24 | + (value & 0x0000FF00U) << 8 | + (value & 0x00FF0000U) >> 8; + } + } + } + + /// + /// Converts a value from the specified endianness to the native endianness. + /// + /// + /// + /// The value to convert. + /// + /// + /// + /// The endianness of . + /// + /// + /// + /// The value converted from the specified endianness to the native endianness (). + /// + public static ulong Convert(ulong value, Endianness endianness) + { + if (endianness == NativeEndianness) + { + return value; + } + else + { + unchecked + { + return (value & 0x00000000000000FFUL) << 56 | + (value & 0xFF00000000000000UL) >> 56 | + (value & 0x000000000000FF00UL) << 40 | + (value & 0x00FF000000000000UL) >> 40 | + (value & 0x0000000000FF0000UL) << 24 | + (value & 0x0000FF0000000000UL) >> 24 | + (value & 0x00000000FF000000UL) << 8 | + (value & 0x000000FF00000000UL) >> 8; + } + } + } + + /// + /// Converts a value from the specified endianness to the native endianness. + /// + /// + /// + /// The value to convert. + /// + /// + /// + /// The endianness of . + /// + /// + /// + /// The value converted from the specified endianness to the native endianness (). + /// + public static short Convert(short value, Endianness endianness) + { + return (short)Convert((ushort)value, endianness); + } + + /// + /// Converts a value from the specified endianness to the native endianness. + /// + /// + /// + /// The value to convert. + /// + /// + /// + /// The endianness of . + /// + /// + /// + /// The value converted from the specified endianness to the native endianness (). + /// + public static int Convert(int value, Endianness endianness) + { + return (int)Convert((uint)value, endianness); + } + + /// + /// Converts a value from the specified endianness to the native endianness. + /// + /// + /// + /// The value to convert. + /// + /// + /// + /// The endianness of . + /// + /// + /// + /// The value converted from the specified endianness to the native endianness (). + /// + public static long Convert(long value, Endianness endianness) + { + return (long)Convert((ulong)value, endianness); + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Library/EndianConvert/Endianness.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Library/EndianConvert/Endianness.cs new file mode 100644 index 0000000000..7293e8b9d1 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Library/EndianConvert/Endianness.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// + +namespace RegistryPreviewUILib.HexBox.Library.EndianConvert +{ + /// + /// Represents the endianness of a value in a computer architecture. + /// + public enum Endianness + { + /// + /// Most significant byte first. + /// + BigEndian, + + /// + /// Least significant byte first. + /// + LittleEndian, + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Library/EndianConvert/FileFormatException.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Library/EndianConvert/FileFormatException.cs new file mode 100644 index 0000000000..81b96c3858 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Library/EndianConvert/FileFormatException.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// + +namespace RegistryPreviewUILib.HexBox.Library.EndianConvert +{ + using System; + + /// + /// The exception that is thrown when an input file or a data stream is malformed. + /// + [Serializable] + public sealed class FileFormatException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public FileFormatException() + { + // Void + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// + /// + /// The message that describes the error. + /// + public FileFormatException(string message) + : base(message) + { + // Void + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner + /// exception that is the cause of this exception. + /// + /// + /// + /// The message that describes the error. + /// + /// + /// + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + /// + public FileFormatException(string message, Exception innerException) + : base(message, innerException) + { + // Void + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/TextFormat.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/TextFormat.cs new file mode 100644 index 0000000000..ca5cc99683 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/TextFormat.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// + +namespace RegistryPreviewUILib.HexBox +{ + /// + /// Enumerates the text section encodings/formats that the control is able to display. + /// + public enum TextFormat + { + /// + /// Display data in ASCII (ISO-8859-1) encoding. + /// + Ascii, + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Themes/Generic.xaml b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Themes/Generic.xaml new file mode 100644 index 0000000000..8b2208128d --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Themes/Generic.xaml @@ -0,0 +1,170 @@ + + + + diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/TypeConverters.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/TypeConverters.cs new file mode 100644 index 0000000000..bed7841cab --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/TypeConverters.cs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// +#pragma warning disable SA1210 // Using directives should be ordered alphabetically by namespace +#pragma warning disable SA1208 // System using directives should be placed before other using directives +using RegistryPreviewUILib.HexBox.Library.EndianConvert; +using Microsoft.UI.Xaml.Data; +using System; +#pragma warning restore SA1208 // System using directives should be placed before other using directives +#pragma warning restore SA1210 // Using directives should be ordered alphabetically by namespace + +namespace RegistryPreviewUILib.HexBox +{ + public partial class HexboxDataTypeConverter : IValueConverter + { + /// + /// Convert a DataType value to its negation. + /// + /// The value to negate. + /// The type of the target property, as a type reference. + /// Optional parameter. Not used. + /// The language of the conversion. Not used + /// The value to be passed to the target dependency property. + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is DataType b) + { + if (parameter is string c) + { + return c == b.ToString(); + } + } + throw new NotImplementedException(); + } + + /// + /// Convert back a DataType value to its negation. + /// + /// The value to negate. + /// The type of the target property, as a type reference. + /// Optional parameter. Not used. + /// The language of the conversion. Not used + /// The value to be passed to the target dependency property. + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is bool b && parameter is string c) + { + if(c == "Int_1") + { + return DataType.Int_1; + } + else if (c == "Int_2") + { + return DataType.Int_2; + } + else if (c == "Int_4") + { + return DataType.Int_4; + } + else if (c == "Int_8") + { + return DataType.Int_8; + } + else if (c == "Float_32") + { + return DataType.Float_32; + } + else /*if (c == "Float_64")*/ + { + return DataType.Float_64; + } + } + throw new NotImplementedException(); + } + } + + public class HexboxDataSignednessConverter : IValueConverter + { + /// + /// Convert a DataSignedness value to its negation. + /// + /// The value to negate. + /// The type of the target property, as a type reference. + /// Optional parameter. Not used. + /// The language of the conversion. Not used + /// The value to be passed to the target dependency property. + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is DataSignedness b) + { + if (parameter is string c) + { + var end = c == "Signed" ? DataSignedness.Signed : DataSignedness.Unsigned; + return (b == end); + } + } + throw new NotImplementedException(); + } + + /// + /// Convert back a DataSignedness value to its negation. + /// + /// The value to negate. + /// The type of the target property, as a type reference. + /// Optional parameter. Not used. + /// The language of the conversion. Not used + /// The value to be passed to the target dependency property. + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is bool b && parameter is string c) + { + var end = c == "Signed" ? DataSignedness.Signed : DataSignedness.Unsigned; + if (b) + { + return end; + } + else + { + return c == "Signed" ? DataSignedness.Unsigned : DataSignedness.Signed; + } + } + throw new NotImplementedException(); + } + } + + public class HexboxDataFormatBoolConverter : IValueConverter + { + /// + /// Convert a DataFormat value to its negation. + /// + /// The value to negate. + /// The type of the target property, as a type reference. + /// Optional parameter. Not used. + /// The language of the conversion. Not used + /// The value to be passed to the target dependency property. + public object Convert(object value, Type targetType, object parameter, string language) + { + if(value is DataFormat f) + { + return f != DataFormat.Hexadecimal; + } + throw new NotImplementedException(); + } + + /// + /// Convert back a DataFormat value to its negation. + /// + /// The value to negate. + /// The type of the target property, as a type reference. + /// Optional parameter. Not used. + /// The language of the conversion. Not used + /// The value to be passed to the target dependency property. + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } + } + + + public class HexboxDataFormatConverter : IValueConverter + { + /// + /// Convert a DataFormat value to its negation. + /// + /// The value to negate. + /// The type of the target property, as a type reference. + /// Optional parameter. Not used. + /// The language of the conversion. Not used + /// The value to be passed to the target dependency property. + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is DataFormat b) + { + if (parameter is string c) + { + var end = c == "Decimal" ? DataFormat.Decimal: DataFormat.Hexadecimal; + return (b == end); + } + } + throw new NotImplementedException(); + } + + /// + /// Convert back a DataFormat value to its negation. + /// + /// The value to negate. + /// The type of the target property, as a type reference. + /// Optional parameter. Not used. + /// The language of the conversion. Not used + /// The value to be passed to the target dependency property. + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is bool b && parameter is string c) + { + var end = c == "Decimal" ? DataFormat.Decimal : DataFormat.Hexadecimal; + if (b) + { + return end; + } + else + { + return end == DataFormat.Decimal ? DataFormat.Hexadecimal : DataFormat.Decimal; + } + } + throw new NotImplementedException(); + } + } + + + public class BigEndianConverter : IValueConverter + { + /// + /// Convert a Endian value to its negation. + /// + /// The value to negate. + /// The type of the target property, as a type reference. + /// Optional parameter. Not used. + /// The language of the conversion. Not used + /// The value to be passed to the target dependency property. + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is Endianness b) + { + if (parameter is string c) + { + var end = c == "BigEndian" ? Endianness.BigEndian : Endianness.LittleEndian; + return (b == end); + } + } + throw new NotImplementedException(); + } + + /// + /// Convert back a Endian value to its negation. + /// + /// The value to negate. + /// The type of the target property, as a type reference. + /// Optional parameter. Not used. + /// The language of the conversion. Not used + /// The value to be passed to the target dependency property. + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is bool b && parameter is string c) + { + var end = c == "BigEndian" ? Endianness.BigEndian : Endianness.LittleEndian; + if (b) + { + return end; + } + else + { + return end == Endianness.BigEndian ? Endianness.LittleEndian : Endianness.BigEndian; + } + } + throw new NotImplementedException(); + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Utilities.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Utilities.cs new file mode 100644 index 0000000000..d2e4795038 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/Utilities.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// 2020-... created by Filip Jeremic (fjeremic) as "HexView.Wpf". +// 2024-... republished by @hotkidfamily as "HexBox.WinUI". +// 2025 Included in PowerToys. (Branch master; commit 72dcf64dc858c693a7a16887004c8ddbab61fce7.) +// + +namespace RegistryPreviewUILib.HexBox +{ + using System; + + /// + /// A utility class with miscellaneous methods. + /// + internal static class Utilities + { + /// + /// Clamps the to the range [, ]. + /// + /// + /// + /// The type of the value to clamp. + /// + /// + /// + /// The value to clamp. + /// + /// + /// + /// The upper bound on the clamped value. + /// + /// + /// + /// The lower bound on the clmaped value. + /// + /// + /// + /// The nearest value of in the range [, + /// ]. + /// + public static T Clamp(this T value, T min, T max) + where T : IComparable + { + return value.CompareTo(min) < 0 ? min : value.CompareTo(max) > 0 ? max : value; + } + + /// + /// Calculates the arithmetic modulus of modulo . + /// + /// + /// + /// The type of the values. + /// + /// + /// + /// The value to compute the modulus of. + /// + /// + /// + /// The modulus. + /// + /// + /// + /// The non-negative value r such that for some integral value q: + /// = q*m + r. + /// + public static T Mod(this T n, T m) + where T : IComparable + { + dynamic dn = n; + dynamic dm = m; + + dynamic dr = dn % dm; + + return dr.CompareTo(0) < 0 ? dr + dm : dr; + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/MonacoEditorControl.xaml b/src/modules/registrypreview/RegistryPreviewUILib/Controls/MonacoEditor/MonacoEditorControl.xaml similarity index 100% rename from src/modules/registrypreview/RegistryPreviewUILib/MonacoEditorControl.xaml rename to src/modules/registrypreview/RegistryPreviewUILib/Controls/MonacoEditor/MonacoEditorControl.xaml diff --git a/src/modules/registrypreview/RegistryPreviewUILib/MonacoEditorControl.xaml.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/MonacoEditor/MonacoEditorControl.xaml.cs similarity index 100% rename from src/modules/registrypreview/RegistryPreviewUILib/MonacoEditorControl.xaml.cs rename to src/modules/registrypreview/RegistryPreviewUILib/Controls/MonacoEditor/MonacoEditorControl.xaml.cs diff --git a/src/modules/registrypreview/RegistryPreviewUILib/MonacoHelper.cs b/src/modules/registrypreview/RegistryPreviewUILib/Controls/MonacoEditor/MonacoHelper.cs similarity index 100% rename from src/modules/registrypreview/RegistryPreviewUILib/MonacoHelper.cs rename to src/modules/registrypreview/RegistryPreviewUILib/Controls/MonacoEditor/MonacoHelper.cs diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.DataPreview.cs b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.DataPreview.cs new file mode 100644 index 0000000000..5cdffda4dd --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.DataPreview.cs @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.Windows.ApplicationModel.Resources; +using Windows.Foundation.Metadata; +using HB = RegistryPreviewUILib.HexBox; + +namespace RegistryPreviewUILib +{ + public sealed partial class RegistryPreviewMainPage : Page + { + private static bool _isDataPreviewHexBoxLoaded; + + internal async Task ShowExtendedDataPreview(string name, string type, string value) + { + // Create dialog + _isDataPreviewHexBoxLoaded = false; + var panel = new StackPanel() + { + Spacing = 16, + Padding = new Thickness(0), + }; + ContentDialog contentDialog = new ContentDialog() + { + Title = resourceLoader.GetString("DataPreviewTitle") + " - " + name, + Content = panel, + CloseButtonText = resourceLoader.GetString("DataPreviewClose"), + DefaultButton = ContentDialogButton.Primary, + Padding = new Thickness(0), + }; + contentDialog.Opened += ExtendedDataPreview_Opened; + + // Add content based on value type + switch (type) + { + case "REG_DWORD": + case "REG_QWORD": + AddHexView(ref panel, ref resourceLoader, value); + break; + case "REG_NONE": + case "REG_BINARY": + // Convert value to BinaryReader + byte[] byteArray = Convert.FromHexString(value.Replace(" ", string.Empty)); + MemoryStream memoryStream = new MemoryStream(byteArray); + BinaryReader binaryData = new BinaryReader(memoryStream); + binaryData.ReadBytes(byteArray.Length); + + // Convert value to text + // For more printable asci characters the following code lines are required: + // var cpW1252 = CodePagesEncodingProvider.Instance.GetEncoding(1252); + // || b == 128 || (b >= 130 && b <= 140) || b == 142 || (b >= 145 & b <= 156) || b >= 158 + // cpW1252.GetString([b]); + string binaryDataText = string.Empty; + foreach (byte b in byteArray) + { + // ASCII codes: + // 9, 10, 13: Space, Line Feed, Carriage Return + // 32-126: Printable characters + // 128, 130-140, 142, 145-156, 158-255: Extended printable characters + if (b == 9 || b == 10 || b == 13 || (b >= 32 && b <= 126)) + { + binaryDataText += Convert.ToChar(b); + } + } + + // Add controls + AddBinaryView(ref panel, ref resourceLoader, ref binaryData, binaryDataText); + break; + case "REG_MULTI_SZ": + var multiLineBox = new TextBox() + { + IsReadOnly = true, + AcceptsReturn = true, + TextWrapping = TextWrapping.NoWrap, + MaxHeight = 200, + FontSize = 14, + RequestedTheme = panel.ActualTheme, + Text = value, + }; + ScrollViewer.SetVerticalScrollBarVisibility(multiLineBox, ScrollBarVisibility.Auto); + ScrollViewer.SetHorizontalScrollBarVisibility(multiLineBox, ScrollBarVisibility.Auto); + AutomationProperties.SetName(multiLineBox, resourceLoader.GetString("DataPreview_AutomationPropertiesName_MultilineTextValue")); + panel.Children.Add(multiLineBox); + break; + case "REG_EXPAND_SZ": + AddExpandStringView(ref panel, ref resourceLoader, value); + break; + default: // REG_SZ + var stringBox = new TextBox() + { + IsReadOnly = true, + FontSize = 14, + RequestedTheme = panel.ActualTheme, + Text = value, + }; + AutomationProperties.SetName(stringBox, resourceLoader.GetString("DataPreview_AutomationPropertiesName_TextValue")); + panel.Children.Add(stringBox); + break; + } + + // Use this code to associate the dialog to the appropriate AppWindow by setting + // the dialog's XamlRoot to the same XamlRoot as an element that is already present in the AppWindow. + if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) + { + contentDialog.XamlRoot = this.Content.XamlRoot; + } + + // Show dialog and wait. + ChangeCursor(gridPreview, false); + _ = await contentDialog.ShowAsync(); + } + + private static void AddHexView(ref StackPanel panel, ref ResourceLoader resourceLoader, string value) + { + var hexBox = new TextBox() + { + Header = resourceLoader.GetString("DataPreviewHex"), + IsReadOnly = true, + FontSize = 14, + RequestedTheme = panel.ActualTheme, + Text = value.Split(" ")[0], + }; + var decimalBox = new TextBox() + { + Header = resourceLoader.GetString("DataPreviewDec"), + IsReadOnly = true, + FontSize = 14, + RequestedTheme = panel.ActualTheme, + Text = value.Split(" ")[1].TrimStart('(').TrimEnd(')'), + }; + panel.Children.Add(hexBox); + panel.Children.Add(decimalBox); + } + + private static void AddBinaryView(ref StackPanel panel, ref ResourceLoader resourceLoader, ref BinaryReader data, string dataText) + { + // Create SelectorBar + var navBar = new SelectorBar() + { + RequestedTheme = panel.ActualTheme, + }; + navBar.SelectionChanged += BinaryPreview_SelectorChanged; + navBar.Items.Add(new SelectorBarItem() + { + Text = resourceLoader.GetString("DataPreviewDataView"), + Tag = "DataView", + FontSize = 14, + RequestedTheme = panel.ActualTheme, + IsSelected = true, + }); + navBar.Items.Add(new SelectorBarItem() + { + Text = resourceLoader.GetString("DataPreviewVisibleText"), + Tag = "TextView", + FontSize = 14, + RequestedTheme = panel.ActualTheme, + IsSelected = false, + IsEnabled = !string.IsNullOrWhiteSpace(dataText), + }); + + // Create HexBox + var binaryPreviewBox = new HB.HexBox() + { + Height = 300, + Width = 495, + ShowAddress = true, + ShowData = true, + ShowText = true, + Columns = 8, + FontSize = 13, + RequestedTheme = panel.ActualTheme, + AddressBrush = (SolidColorBrush)Application.Current.Resources["AccentTextFillColorPrimaryBrush"], + AlternatingDataColumnTextBrush = (SolidColorBrush)Application.Current.Resources["TextFillColorSecondaryBrush"], + SelectionTextBrush = (SolidColorBrush)Application.Current.Resources["HexBox_SelectionTextBrush"], + SelectionBrush = (SolidColorBrush)Application.Current.Resources["HexBox_SelectionBackgroundBrush"], + VerticalSeparatorLineBrush = (SolidColorBrush)Application.Current.Resources["HexBox_VerticalLineBrush"], + BorderBrush = (LinearGradientBrush)Application.Current.Resources["HexBox_ControlBorderBrush"], + BorderThickness = (Thickness)Application.Current.Resources["HexBox_ControlBorderThickness"], + CornerRadius = (CornerRadius)Application.Current.Resources["ControlCornerRadius"], + DataFormat = HB.DataFormat.Hexadecimal, + DataSignedness = HB.DataSignedness.Unsigned, + DataType = HB.DataType.Int_1, + EnforceProperties = true, + Visibility = Visibility.Collapsed, + DataSource = data, + }; + AutomationProperties.SetName(binaryPreviewBox, resourceLoader.GetString("DataPreview_AutomationPropertiesName_BinaryDataPreview")); + binaryPreviewBox.Loaded += BinaryPreview_HexBoxLoaded; + binaryPreviewBox.GotFocus += BinaryPreview_HexBoxFocused; + binaryPreviewBox.LostFocus += BinaryPreview_HexBoxFocusLost; + + // Create TextBox + var visibleText = new TextBox() + { + IsReadOnly = true, + AcceptsReturn = true, + TextWrapping = TextWrapping.Wrap, + Height = 300, + Width = 495, + FontSize = 13, + Text = dataText, + RequestedTheme = panel.ActualTheme, + Visibility = Visibility.Collapsed, + }; + AutomationProperties.SetName(visibleText, resourceLoader.GetString("DataPreview_AutomationPropertiesName_VisibleTextPreview")); + + // Add controls: 0 = SelectorBar, 1 = ProgressRing, 2 = HexBox, 3 = TextBox + panel.Children.Add(navBar); + panel.Children.Add(new ProgressRing()); + panel.Children.Add(binaryPreviewBox); + panel.Children.Add(visibleText); + } + + private static void AddExpandStringView(ref StackPanel panel, ref ResourceLoader resourceLoader, string value) + { + var stringBoxRaw = new TextBox() + { + Header = resourceLoader.GetString("DataPreviewRawValue"), + IsReadOnly = true, + FontSize = 14, + RequestedTheme = panel.ActualTheme, + Text = value, + }; + var stringBoxExp = new TextBox() + { + Header = resourceLoader.GetString("DataPreviewExpandedValue"), + IsReadOnly = true, + FontSize = 14, + RequestedTheme = panel.ActualTheme, + Text = Environment.ExpandEnvironmentVariables(value), + }; + panel.Children.Add(stringBoxRaw); + panel.Children.Add(stringBoxExp); + } + + private static void BinaryPreview_SelectorChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args) + { + // Child controls: 0 = SelectorBar, 1 = ProgressRing, 2 = HexBox, 3 = TextBox + var stackPanel = sender.Parent as StackPanel; + var progressRing = (ProgressRing)stackPanel.Children[1]; + var hexBox = (HB.HexBox)stackPanel.Children[2]; + var textBox = (TextBox)stackPanel.Children[3]; + + if (sender.SelectedItem.Tag.ToString() == "DataView") + { + textBox.Visibility = Visibility.Collapsed; + if (_isDataPreviewHexBoxLoaded) + { + progressRing.Visibility = Visibility.Collapsed; + hexBox.Visibility = Visibility.Visible; + + // Clear selection aligned to TextBox + hexBox.ClearSelection(); + hexBox.Focus(FocusState.Programmatic); + } + else + { + hexBox.Visibility = Visibility.Collapsed; + progressRing.Visibility = Visibility.Visible; + progressRing.Focus(FocusState.Programmatic); + } + } + else + { + progressRing.Visibility = Visibility.Collapsed; + + hexBox.Visibility = Visibility.Collapsed; + textBox.Visibility = Visibility.Visible; + + // Workaround for wrong text selection (color) after switching back to "Visible text" + textBox.Focus(FocusState.Programmatic); + textBox.Select(0, 0); + } + } + + private static void BinaryPreview_HexBoxLoaded(object sender, RoutedEventArgs e) + { + _isDataPreviewHexBoxLoaded = true; + + // Child controls: 0 = SelectorBar, 1 = ProgressRing, 2 = HexBox, 3 = TextBox + var hexBox = (HB.HexBox)sender; + var stackPanel = hexBox.Parent as StackPanel; + var selectorBar = stackPanel.Children[0] as SelectorBar; + var progressRing = stackPanel.Children[1] as ProgressRing; + + if (selectorBar.SelectedItem.Tag.ToString() == "DataView") + { + progressRing.Visibility = Visibility.Collapsed; + hexBox.Visibility = Visibility.Visible; + } + } + + /// + /// Event handler to set correct control border if focused. + /// + private static void BinaryPreview_HexBoxFocused(object sender, RoutedEventArgs e) + { + var hexBox = (HB.HexBox)sender; + + hexBox.BorderThickness = (Thickness)Application.Current.Resources["HexBox_ControlBorderFocusedThickness"]; + hexBox.BorderBrush = (LinearGradientBrush)Application.Current.Resources["HexBox_ControlBorderFocusedBrush"]; + } + + /// + /// Event handler to set correct control border if not focused. + /// + private static void BinaryPreview_HexBoxFocusLost(object sender, RoutedEventArgs e) + { + var hexBox = (HB.HexBox)sender; + + // Workaround: Verify that the newly focused control isn't the context menu of the HexBox control + if (FocusManager.GetFocusedElement(hexBox.XamlRoot).GetType() != typeof(MenuFlyoutPresenter)) + { + hexBox.BorderThickness = (Thickness)Application.Current.Resources["HexBox_ControlBorderThickness"]; + hexBox.BorderBrush = (LinearGradientBrush)Application.Current.Resources["HexBox_ControlBorderBrush"]; + } + } + + /// + /// Make sure that for REG_Binary preview the HexBox control is focused after opening. + /// + private static void ExtendedDataPreview_Opened(ContentDialog sender, ContentDialogOpenedEventArgs e) + { + // If <_isDataPreviewHexBoxLoaded == true> then we have the right content on the dialog. + if (_isDataPreviewHexBoxLoaded) + { + // Child controls: 0 = SelectorBar, 1 = ProgressRing, 2 = HexBox, 3 = TextBox + (sender.Content as StackPanel).Children[2].Focus(FocusState.Programmatic); + } + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs index 20009533c1..c8f72837f1 100644 --- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs +++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; - using CommunityToolkit.WinUI.UI.Controls; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; @@ -415,5 +414,48 @@ namespace RegistryPreviewUILib saveButton.IsEnabled = true; }); } + + // Commands to show data preview + public void ButtonExtendedPreview_Click(object sender, RoutedEventArgs e) + { + var data = ((Button)sender).DataContext as RegistryValue; + InvokeExtendedDataPreview(data); + } + + public void MenuExtendedPreview_Click(object sender, RoutedEventArgs e) + { + var data = ((MenuFlyoutItem)sender).DataContext as RegistryValue; + InvokeExtendedDataPreview(data); + } + + private async void InvokeExtendedDataPreview(RegistryValue valueData) + { + // Only one content dialog can be open at the same time and multiple instances of data preview can crash the app. + if (_dialogSemaphore.CurrentCount == 0) + { + return; + } + + try + { + // Lock ui and request dialog lock + _dialogSemaphore.Wait(); + ChangeCursor(gridPreview, true); + + await ShowExtendedDataPreview(valueData.Name, valueData.Type, valueData.Value); + } + catch + { +#if DEBUG + throw; +#endif + } + finally + { + // Unblock ui and release dialog lock + ChangeCursor(gridPreview, false); + _dialogSemaphore.Release(); + } + } } } diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs index 3ec275abf8..57d27fdc12 100644 --- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs +++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs @@ -503,6 +503,7 @@ namespace RegistryPreviewUILib case "REG_NONE": if (value.Length <= 0) { + registryValue.IsEmptyBinary = true; value = resourceLoader.GetString("ZeroLength"); } else diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.xaml b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.xaml index 18225902e3..50a89fc223 100644 --- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.xaml +++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.xaml @@ -2,6 +2,7 @@ x:Class="RegistryPreviewUILib.RegistryPreviewMainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:converter="using:CommunityToolkit.WinUI.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:RegistryPreviewUILib" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" @@ -10,6 +11,12 @@ xmlns:ui="using:CommunityToolkit.WinUI" mc:Ignorable="d"> + + + + + + - + @@ -225,10 +232,18 @@ Spacing="8"> - - + + + - - + + + @@ -274,20 +297,36 @@ + Orientation="Horizontal" + Spacing="6"> - - + + + +