From acace2e4e967dc5a043d116bf6dc02743054d02a Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:26:13 +0100 Subject: [PATCH 1/3] Fix: AirspacePanel screenshot with display scaling - embed AirspaceFixer in NETworkManager.Controls The screenshot shown in place of embedded windows (Remote Desktop, PowerShell, PuTTY, TigerVNC, Web Console) while a dialog is open was taken with the wrong size (AirspaceFixer 1.0.6 gets a scaling factor of 1.0 for a PerMonitorV2 application) and leaked a Graphics object / device context. 1.1.0 is unlisted on NuGet and scales the (already physical) position again. AirspacePanel is now part of NETworkManager.Controls, based on AirspaceFixer and its fork AirspaceFixerDotNet (both MIT, notice kept in the source, style and license file): position in physical pixels, size scaled with the DPI of the monitor (TransformToDevice). In addition the content is always shown again when FixAirspace is reset, nothing is swapped if the template is missing or the content has no size, and the screenshot is frozen. The template is an implicit style in Resources/Styles/AirspacePanelStyles.xaml. The NuGet package is removed. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 3 +- .../NETworkManager.Controls/AirspacePanel.cs | 153 ++++++++++++++++++ .../LibraryManager.cs | 4 + .../Licenses/AirspaceFixerDotNet.txt | 22 +++ .../NETworkManager.Documentation.csproj | 4 + Source/NETworkManager/App.xaml | 1 + Source/NETworkManager/MainWindow.xaml | 8 +- Source/NETworkManager/NETworkManager.csproj | 1 - .../Resources/Styles/AirspacePanelStyles.xaml | 24 +++ Website/docs/changelog/next-release.md | 1 + 10 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 Source/NETworkManager.Controls/AirspacePanel.cs create mode 100644 Source/NETworkManager.Documentation/Licenses/AirspaceFixerDotNet.txt create mode 100644 Source/NETworkManager/Resources/Styles/AirspacePanelStyles.xaml diff --git a/AGENTS.md b/AGENTS.md index 8cd0de9c93..342e79f0e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,7 +92,8 @@ REPOSITORY_ROOT/ - **DnsClient** - DNS lookups - **IPNetwork2** - Network and subnet calculations - **PSDiscoveryProtocol** - LLDP/CDP network discovery (PowerShell module embedded) -- **AirspaceFixer** - WPF/WinForms interop fixes +- **AirspaceFixer** - WPF/WinForms interop fixes (code embedded as `AirspacePanel` in `NETworkManager.Controls`, + based on AirspaceFixer and its fork AirspaceFixerDotNet) - **log4net** - Logging framework - **LiveChartsCore.SkiaSharpView.WPF** - Charts and graphs - **Microsoft.Web.WebView2** - WebView2 control for web content diff --git a/Source/NETworkManager.Controls/AirspacePanel.cs b/Source/NETworkManager.Controls/AirspacePanel.cs new file mode 100644 index 0000000000..968cc60e8d --- /dev/null +++ b/Source/NETworkManager.Controls/AirspacePanel.cs @@ -0,0 +1,153 @@ +// Based on AirspaceFixer (https://github.com/chris84948/AirspaceFixer) and its fork AirspaceFixerDotNet +// (https://github.com/ellipticbit/AirspaceFixerDotNet), adapted for NETworkManager. +// +// MIT License +// +// Copyright (c) 2017 chris84948 +// Copyright (c) 2024 EllipticBit, LLC +// +// 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. + +using System; +using System.Drawing.Imaging; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media.Imaging; + +namespace NETworkManager.Controls; + +/// +/// Hosts content with embedded windows (e.g. Remote Desktop, PuTTY, WebView2), which are always drawn on top of +/// WPF elements (airspace issue). While is , the content is +/// replaced by a screenshot of it, so that WPF elements like dialogs can be shown above it. +/// The template is defined in Resources/Styles/AirspacePanelStyles.xaml of the application. +/// +[TemplatePart(Name = PartAirspaceContent, Type = typeof(ContentControl))] +[TemplatePart(Name = PartAirspaceScreenshot, Type = typeof(Image))] +public class AirspacePanel : ContentControl +{ + private const string PartAirspaceContent = "PART_AirspaceContent"; + private const string PartAirspaceScreenshot = "PART_AirspaceScreenshot"; + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty FixAirspaceProperty = DependencyProperty.Register(nameof(FixAirspace), + typeof(bool), typeof(AirspacePanel), new FrameworkPropertyMetadata(false, OnFixAirspaceChanged)); + + private ContentControl _airspaceContent; + private Image _airspaceScreenshot; + + /// + /// Replace the content with a screenshot of it (e.g. while a dialog is shown above the content). + /// + public bool FixAirspace + { + get => (bool)GetValue(FixAirspaceProperty); + set => SetValue(FixAirspaceProperty, value); + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _airspaceContent = GetTemplateChild(PartAirspaceContent) as ContentControl; + _airspaceScreenshot = GetTemplateChild(PartAirspaceScreenshot) as Image; + } + + private static void OnFixAirspaceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AirspacePanel panel) + panel.UpdateAirspace((bool)e.NewValue); + } + + private void UpdateAirspace(bool fix) + { + // Template not applied (yet) + if (_airspaceContent == null || _airspaceScreenshot == null) + return; + + if (fix) + { + // Content not visible or not shown in a window - nothing to capture, keep the content + var screenshot = CreateScreenshotFromContent(); + + if (screenshot == null) + return; + + _airspaceScreenshot.Source = screenshot; + _airspaceContent.Visibility = Visibility.Hidden; + _airspaceScreenshot.Visibility = Visibility.Visible; + } + else + { + // Always show the content again, even if the size changed in the meantime + _airspaceContent.Visibility = Visibility.Visible; + _airspaceScreenshot.Visibility = Visibility.Hidden; + _airspaceScreenshot.Source = null; + } + } + + private BitmapSource CreateScreenshotFromContent() + { + var presentationSource = PresentationSource.FromVisual(_airspaceContent); + + if (presentationSource?.CompositionTarget == null) + return null; + + // The application is per monitor DPI aware (PerMonitorV2, see app.manifest): PointToScreen returns physical + // pixels, but the size is in device independent pixels and has to be scaled with the DPI of the monitor. + var transformToDevice = presentationSource.CompositionTarget.TransformToDevice; + + var upperLeftPoint = _airspaceContent.PointToScreen(new Point(0, 0)); + var width = (int)Math.Round(_airspaceContent.ActualWidth * transformToDevice.M11); + var height = (int)Math.Round(_airspaceContent.ActualHeight * transformToDevice.M22); + + if (width <= 0 || height <= 0) + return null; + + using var bitmap = new System.Drawing.Bitmap(width, height); + + using (var graphics = System.Drawing.Graphics.FromImage(bitmap)) + { + graphics.CopyFromScreen((int)Math.Round(upperLeftPoint.X), (int)Math.Round(upperLeftPoint.Y), 0, 0, + new System.Drawing.Size(width, height)); + } + + return GetBitmapSource(bitmap); + } + + private static BitmapSource GetBitmapSource(System.Drawing.Bitmap bitmap) + { + using var memoryStream = new MemoryStream(); + + bitmap.Save(memoryStream, ImageFormat.Png); + memoryStream.Position = 0; + + var bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + bitmapImage.StreamSource = memoryStream; + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.EndInit(); + bitmapImage.Freeze(); + + return bitmapImage; + } +} diff --git a/Source/NETworkManager.Documentation/LibraryManager.cs b/Source/NETworkManager.Documentation/LibraryManager.cs index 5844e094ac..91e034774a 100644 --- a/Source/NETworkManager.Documentation/LibraryManager.cs +++ b/Source/NETworkManager.Documentation/LibraryManager.cs @@ -28,6 +28,10 @@ public static class LibraryManager Strings.Library_AirspaceFixer_Description, Strings.License_MITLicense, "https://github.com/chris84948/AirspaceFixer/blob/master/LICENSE"), + new("AirspaceFixerDotNet", "https://github.com/ellipticbit/AirspaceFixerDotNet", + Strings.Library_AirspaceFixer_Description, + Strings.License_MITLicense, + "https://github.com/ellipticbit/AirspaceFixerDotNet/blob/master/LICENSE"), new("ControlzEx", "https://github.com/ControlzEx/ControlzEx", Strings.Library_ControlzEx_Description, Strings.License_MITLicense, diff --git a/Source/NETworkManager.Documentation/Licenses/AirspaceFixerDotNet.txt b/Source/NETworkManager.Documentation/Licenses/AirspaceFixerDotNet.txt new file mode 100644 index 0000000000..105eb6eb9b --- /dev/null +++ b/Source/NETworkManager.Documentation/Licenses/AirspaceFixerDotNet.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2017 chris84948 +Copyright (c) 2024 EllipticBit, LLC + +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. \ No newline at end of file diff --git a/Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj b/Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj index ce00c84acb..4b1ceece5c 100644 --- a/Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj +++ b/Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj @@ -17,6 +17,7 @@ + @@ -48,6 +49,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/Source/NETworkManager/App.xaml b/Source/NETworkManager/App.xaml index 36c4e5454a..730d0e4469 100644 --- a/Source/NETworkManager/App.xaml +++ b/Source/NETworkManager/App.xaml @@ -24,6 +24,7 @@ + diff --git a/Source/NETworkManager/MainWindow.xaml b/Source/NETworkManager/MainWindow.xaml index 55261c4355..c844512895 100644 --- a/Source/NETworkManager/MainWindow.xaml +++ b/Source/NETworkManager/MainWindow.xaml @@ -10,7 +10,7 @@ xmlns:shell="clr-namespace:ControlzEx.Windows.Shell;assembly=ControlzEx" xmlns:controls="clr-namespace:NETworkManager.Controls" xmlns:interactivity="http://schemas.microsoft.com/xaml/behaviors" - xmlns:asf="clr-namespace:AirspaceFixer;assembly=AirspaceFixer" + xmlns:networkManagerControls="clr-namespace:NETworkManager.Controls;assembly=NETworkManager.Controls" xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF" xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization" xmlns:settings="clr-namespace:NETworkManager.Settings;assembly=NETworkManager.Settings" @@ -973,10 +973,10 @@ - + - + diff --git a/Source/NETworkManager/NETworkManager.csproj b/Source/NETworkManager/NETworkManager.csproj index b711ec7b83..f7162f8073 100644 --- a/Source/NETworkManager/NETworkManager.csproj +++ b/Source/NETworkManager/NETworkManager.csproj @@ -61,7 +61,6 @@ - diff --git a/Source/NETworkManager/Resources/Styles/AirspacePanelStyles.xaml b/Source/NETworkManager/Resources/Styles/AirspacePanelStyles.xaml new file mode 100644 index 0000000000..9276f83667 --- /dev/null +++ b/Source/NETworkManager/Resources/Styles/AirspacePanelStyles.xaml @@ -0,0 +1,24 @@ + + + + diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 60f2403fc7..1143661ff2 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -189,6 +189,7 @@ Release date: **xx.xx.2026** - Fixed the application list not scrolling to the selected application if it was opened from somewhere else (e.g. from the **Dashboard**, another application or the run command). [#3644](https://github.com/BornToBeRoot/NETworkManager/pull/3644) - Fixed `Enter` in the application search (title bar) being able to trigger the default button of the shown tool (e.g. start a scan in the **IP Scanner**). It only filters the applications now. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - Fixed columns that couldn't be made wider than the window (by dragging or double-clicking the gripper) in **About** and the DNS server profiles in **Settings > Network**. The grid scrolls horizontally now, the last (or main) column still fills the remaining width. [#3557](https://github.com/BornToBeRoot/NETworkManager/issues/3557) +- Fixed the screenshot that is shown in place of embedded windows (**Remote Desktop**, **PowerShell**, **PuTTY**, **TigerVNC**, **Web Console**) while a dialog is open being cropped / stretched with a display scaling above 100% or on a monitor with a different scaling. The `AirspacePanel` of [AirspaceFixer](https://github.com/chris84948/AirspaceFixer) is now part of NETworkManager, based on the fork [AirspaceFixerDotNet](https://github.com/ellipticbit/AirspaceFixerDotNet) with per monitor DPI support, which also fixes a GDI handle leak. **Firewall** From 60b368122df59402c2a7e536e1a722902b9986db Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:35:24 +0100 Subject: [PATCH 2/3] Fix: AirspacePanel: Keep the screenshot size if the window is resized while a dialog is open The screenshot was stretched / scaled to the panel (Image Stretch=Uniform). It is now shown in the size of the content when it was taken, top left aligned and clipped, like a frozen frame. A new screenshot can't be taken while the dialog is open (content hidden, dialog on top). Co-Authored-By: Claude Opus 5.5 --- Source/NETworkManager.Controls/AirspacePanel.cs | 6 ++++++ .../Resources/Styles/AirspacePanelStyles.xaml | 7 ++++++- Website/docs/changelog/next-release.md | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Source/NETworkManager.Controls/AirspacePanel.cs b/Source/NETworkManager.Controls/AirspacePanel.cs index 968cc60e8d..e03927cc18 100644 --- a/Source/NETworkManager.Controls/AirspacePanel.cs +++ b/Source/NETworkManager.Controls/AirspacePanel.cs @@ -92,6 +92,10 @@ private void UpdateAirspace(bool fix) if (screenshot == null) return; + // Show the screenshot in the size of the content when it was taken (top left aligned in the template), so + // it is not stretched / scaled if the window is resized while it is shown + _airspaceScreenshot.Width = _airspaceContent.ActualWidth; + _airspaceScreenshot.Height = _airspaceContent.ActualHeight; _airspaceScreenshot.Source = screenshot; _airspaceContent.Visibility = Visibility.Hidden; _airspaceScreenshot.Visibility = Visibility.Visible; @@ -102,6 +106,8 @@ private void UpdateAirspace(bool fix) _airspaceContent.Visibility = Visibility.Visible; _airspaceScreenshot.Visibility = Visibility.Hidden; _airspaceScreenshot.Source = null; + _airspaceScreenshot.ClearValue(WidthProperty); + _airspaceScreenshot.ClearValue(HeightProperty); } } diff --git a/Source/NETworkManager/Resources/Styles/AirspacePanelStyles.xaml b/Source/NETworkManager/Resources/Styles/AirspacePanelStyles.xaml index 9276f83667..7ff45d06fd 100644 --- a/Source/NETworkManager/Resources/Styles/AirspacePanelStyles.xaml +++ b/Source/NETworkManager/Resources/Styles/AirspacePanelStyles.xaml @@ -10,9 +10,14 @@ + + VerticalAlignment="{TemplateBinding VerticalAlignment}" + ClipToBounds="True"> + diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 1143661ff2..d80340505b 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -189,7 +189,7 @@ Release date: **xx.xx.2026** - Fixed the application list not scrolling to the selected application if it was opened from somewhere else (e.g. from the **Dashboard**, another application or the run command). [#3644](https://github.com/BornToBeRoot/NETworkManager/pull/3644) - Fixed `Enter` in the application search (title bar) being able to trigger the default button of the shown tool (e.g. start a scan in the **IP Scanner**). It only filters the applications now. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - Fixed columns that couldn't be made wider than the window (by dragging or double-clicking the gripper) in **About** and the DNS server profiles in **Settings > Network**. The grid scrolls horizontally now, the last (or main) column still fills the remaining width. [#3557](https://github.com/BornToBeRoot/NETworkManager/issues/3557) -- Fixed the screenshot that is shown in place of embedded windows (**Remote Desktop**, **PowerShell**, **PuTTY**, **TigerVNC**, **Web Console**) while a dialog is open being cropped / stretched with a display scaling above 100% or on a monitor with a different scaling. The `AirspacePanel` of [AirspaceFixer](https://github.com/chris84948/AirspaceFixer) is now part of NETworkManager, based on the fork [AirspaceFixerDotNet](https://github.com/ellipticbit/AirspaceFixerDotNet) with per monitor DPI support, which also fixes a GDI handle leak. +- Fixed the screenshot that is shown in place of embedded windows (**Remote Desktop**, **PowerShell**, **PuTTY**, **TigerVNC**, **Web Console**) while a dialog is open being cropped / stretched with a display scaling above 100% or on a monitor with a different scaling, and being stretched / scaled if the window is resized while the dialog is open (it keeps its size now). The `AirspacePanel` of [AirspaceFixer](https://github.com/chris84948/AirspaceFixer) is now part of NETworkManager, based on the fork [AirspaceFixerDotNet](https://github.com/ellipticbit/AirspaceFixerDotNet) with per monitor DPI support, which also fixes a GDI handle leak. **Firewall** From 473ff60ffcbe5092112a3b7a9283026e4c23f2b6 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:36:31 +0100 Subject: [PATCH 3/3] Docs: Changelog: Link the AirspacePanel PR #3681 Co-Authored-By: Claude Opus 5.5 --- Website/docs/changelog/next-release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index d80340505b..82a2f50fff 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -189,7 +189,7 @@ Release date: **xx.xx.2026** - Fixed the application list not scrolling to the selected application if it was opened from somewhere else (e.g. from the **Dashboard**, another application or the run command). [#3644](https://github.com/BornToBeRoot/NETworkManager/pull/3644) - Fixed `Enter` in the application search (title bar) being able to trigger the default button of the shown tool (e.g. start a scan in the **IP Scanner**). It only filters the applications now. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - Fixed columns that couldn't be made wider than the window (by dragging or double-clicking the gripper) in **About** and the DNS server profiles in **Settings > Network**. The grid scrolls horizontally now, the last (or main) column still fills the remaining width. [#3557](https://github.com/BornToBeRoot/NETworkManager/issues/3557) -- Fixed the screenshot that is shown in place of embedded windows (**Remote Desktop**, **PowerShell**, **PuTTY**, **TigerVNC**, **Web Console**) while a dialog is open being cropped / stretched with a display scaling above 100% or on a monitor with a different scaling, and being stretched / scaled if the window is resized while the dialog is open (it keeps its size now). The `AirspacePanel` of [AirspaceFixer](https://github.com/chris84948/AirspaceFixer) is now part of NETworkManager, based on the fork [AirspaceFixerDotNet](https://github.com/ellipticbit/AirspaceFixerDotNet) with per monitor DPI support, which also fixes a GDI handle leak. +- Fixed the screenshot that is shown in place of embedded windows (**Remote Desktop**, **PowerShell**, **PuTTY**, **TigerVNC**, **Web Console**) while a dialog is open being cropped / stretched with a display scaling above 100% or on a monitor with a different scaling, and being stretched / scaled if the window is resized while the dialog is open (it keeps its size now). The `AirspacePanel` of [AirspaceFixer](https://github.com/chris84948/AirspaceFixer) is now part of NETworkManager, based on the fork [AirspaceFixerDotNet](https://github.com/ellipticbit/AirspaceFixerDotNet) with per monitor DPI support, which also fixes a GDI handle leak. [#3681](https://github.com/BornToBeRoot/NETworkManager/pull/3681) **Firewall**