Skip to content

Commit 9b82ee0

Browse files
BornToBeRootclaude
andauthored
Fix: Include AirspacePanel as control with custom fixes (#3681)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * Docs: Changelog: Link the AirspacePanel PR #3681 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent bfa3a49 commit 9b82ee0

10 files changed

Lines changed: 226 additions & 6 deletions

File tree

‎AGENTS.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ REPOSITORY_ROOT/
9292
- **DnsClient** - DNS lookups
9393
- **IPNetwork2** - Network and subnet calculations
9494
- **PSDiscoveryProtocol** - LLDP/CDP network discovery (PowerShell module embedded)
95-
- **AirspaceFixer** - WPF/WinForms interop fixes
95+
- **AirspaceFixer** - WPF/WinForms interop fixes (code embedded as `AirspacePanel` in `NETworkManager.Controls`,
96+
based on AirspaceFixer and its fork AirspaceFixerDotNet)
9697
- **log4net** - Logging framework
9798
- **LiveChartsCore.SkiaSharpView.WPF** - Charts and graphs
9899
- **Microsoft.Web.WebView2** - WebView2 control for web content
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Based on AirspaceFixer (https://github.com/chris84948/AirspaceFixer) and its fork AirspaceFixerDotNet
2+
// (https://github.com/ellipticbit/AirspaceFixerDotNet), adapted for NETworkManager.
3+
//
4+
// MIT License
5+
//
6+
// Copyright (c) 2017 chris84948
7+
// Copyright (c) 2024 EllipticBit, LLC
8+
//
9+
// Permission is hereby granted, free of charge, to any person obtaining a copy
10+
// of this software and associated documentation files (the "Software"), to deal
11+
// in the Software without restriction, including without limitation the rights
12+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
// copies of the Software, and to permit persons to whom the Software is
14+
// furnished to do so, subject to the following conditions:
15+
//
16+
// The above copyright notice and this permission notice shall be included in all
17+
// copies or substantial portions of the Software.
18+
//
19+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25+
// SOFTWARE.
26+
27+
using System;
28+
using System.Drawing.Imaging;
29+
using System.IO;
30+
using System.Windows;
31+
using System.Windows.Controls;
32+
using System.Windows.Media.Imaging;
33+
34+
namespace NETworkManager.Controls;
35+
36+
/// <summary>
37+
/// Hosts content with embedded windows (e.g. Remote Desktop, PuTTY, WebView2), which are always drawn on top of
38+
/// WPF elements (airspace issue). While <see cref="FixAirspace" /> is <see langword="true" />, the content is
39+
/// replaced by a screenshot of it, so that WPF elements like dialogs can be shown above it.
40+
/// The template is defined in <c>Resources/Styles/AirspacePanelStyles.xaml</c> of the application.
41+
/// </summary>
42+
[TemplatePart(Name = PartAirspaceContent, Type = typeof(ContentControl))]
43+
[TemplatePart(Name = PartAirspaceScreenshot, Type = typeof(Image))]
44+
public class AirspacePanel : ContentControl
45+
{
46+
private const string PartAirspaceContent = "PART_AirspaceContent";
47+
private const string PartAirspaceScreenshot = "PART_AirspaceScreenshot";
48+
49+
/// <summary>
50+
/// Identifies the <see cref="FixAirspace" /> dependency property.
51+
/// </summary>
52+
public static readonly DependencyProperty FixAirspaceProperty = DependencyProperty.Register(nameof(FixAirspace),
53+
typeof(bool), typeof(AirspacePanel), new FrameworkPropertyMetadata(false, OnFixAirspaceChanged));
54+
55+
private ContentControl _airspaceContent;
56+
private Image _airspaceScreenshot;
57+
58+
/// <summary>
59+
/// Replace the content with a screenshot of it (e.g. while a dialog is shown above the content).
60+
/// </summary>
61+
public bool FixAirspace
62+
{
63+
get => (bool)GetValue(FixAirspaceProperty);
64+
set => SetValue(FixAirspaceProperty, value);
65+
}
66+
67+
public override void OnApplyTemplate()
68+
{
69+
base.OnApplyTemplate();
70+
71+
_airspaceContent = GetTemplateChild(PartAirspaceContent) as ContentControl;
72+
_airspaceScreenshot = GetTemplateChild(PartAirspaceScreenshot) as Image;
73+
}
74+
75+
private static void OnFixAirspaceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
76+
{
77+
if (d is AirspacePanel panel)
78+
panel.UpdateAirspace((bool)e.NewValue);
79+
}
80+
81+
private void UpdateAirspace(bool fix)
82+
{
83+
// Template not applied (yet)
84+
if (_airspaceContent == null || _airspaceScreenshot == null)
85+
return;
86+
87+
if (fix)
88+
{
89+
// Content not visible or not shown in a window - nothing to capture, keep the content
90+
var screenshot = CreateScreenshotFromContent();
91+
92+
if (screenshot == null)
93+
return;
94+
95+
// Show the screenshot in the size of the content when it was taken (top left aligned in the template), so
96+
// it is not stretched / scaled if the window is resized while it is shown
97+
_airspaceScreenshot.Width = _airspaceContent.ActualWidth;
98+
_airspaceScreenshot.Height = _airspaceContent.ActualHeight;
99+
_airspaceScreenshot.Source = screenshot;
100+
_airspaceContent.Visibility = Visibility.Hidden;
101+
_airspaceScreenshot.Visibility = Visibility.Visible;
102+
}
103+
else
104+
{
105+
// Always show the content again, even if the size changed in the meantime
106+
_airspaceContent.Visibility = Visibility.Visible;
107+
_airspaceScreenshot.Visibility = Visibility.Hidden;
108+
_airspaceScreenshot.Source = null;
109+
_airspaceScreenshot.ClearValue(WidthProperty);
110+
_airspaceScreenshot.ClearValue(HeightProperty);
111+
}
112+
}
113+
114+
private BitmapSource CreateScreenshotFromContent()
115+
{
116+
var presentationSource = PresentationSource.FromVisual(_airspaceContent);
117+
118+
if (presentationSource?.CompositionTarget == null)
119+
return null;
120+
121+
// The application is per monitor DPI aware (PerMonitorV2, see app.manifest): PointToScreen returns physical
122+
// pixels, but the size is in device independent pixels and has to be scaled with the DPI of the monitor.
123+
var transformToDevice = presentationSource.CompositionTarget.TransformToDevice;
124+
125+
var upperLeftPoint = _airspaceContent.PointToScreen(new Point(0, 0));
126+
var width = (int)Math.Round(_airspaceContent.ActualWidth * transformToDevice.M11);
127+
var height = (int)Math.Round(_airspaceContent.ActualHeight * transformToDevice.M22);
128+
129+
if (width <= 0 || height <= 0)
130+
return null;
131+
132+
using var bitmap = new System.Drawing.Bitmap(width, height);
133+
134+
using (var graphics = System.Drawing.Graphics.FromImage(bitmap))
135+
{
136+
graphics.CopyFromScreen((int)Math.Round(upperLeftPoint.X), (int)Math.Round(upperLeftPoint.Y), 0, 0,
137+
new System.Drawing.Size(width, height));
138+
}
139+
140+
return GetBitmapSource(bitmap);
141+
}
142+
143+
private static BitmapSource GetBitmapSource(System.Drawing.Bitmap bitmap)
144+
{
145+
using var memoryStream = new MemoryStream();
146+
147+
bitmap.Save(memoryStream, ImageFormat.Png);
148+
memoryStream.Position = 0;
149+
150+
var bitmapImage = new BitmapImage();
151+
bitmapImage.BeginInit();
152+
bitmapImage.StreamSource = memoryStream;
153+
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
154+
bitmapImage.EndInit();
155+
bitmapImage.Freeze();
156+
157+
return bitmapImage;
158+
}
159+
}

‎Source/NETworkManager.Documentation/LibraryManager.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public static class LibraryManager
2828
Strings.Library_AirspaceFixer_Description,
2929
Strings.License_MITLicense,
3030
"https://github.com/chris84948/AirspaceFixer/blob/master/LICENSE"),
31+
new("AirspaceFixerDotNet", "https://github.com/ellipticbit/AirspaceFixerDotNet",
32+
Strings.Library_AirspaceFixer_Description,
33+
Strings.License_MITLicense,
34+
"https://github.com/ellipticbit/AirspaceFixerDotNet/blob/master/LICENSE"),
3135
new("ControlzEx", "https://github.com/ControlzEx/ControlzEx",
3236
Strings.Library_ControlzEx_Description,
3337
Strings.License_MITLicense,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2017 chris84948
4+
Copyright (c) 2024 EllipticBit, LLC
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.

‎Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
</PropertyGroup>
1818
<ItemGroup>
1919
<None Remove="Licenses\AirspaceFixer.txt" />
20+
<None Remove="Licenses\AirspaceFixerDotNet.txt" />
2021
<None Remove="Licenses\ControlzEx.txt" />
2122
<None Remove="Licenses\DnsClient.NET.txt" />
2223
<None Remove="Licenses\Dragablz.txt" />
@@ -48,6 +49,9 @@
4849
<Content Include="Licenses\AirspaceFixer.txt">
4950
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
5051
</Content>
52+
<Content Include="Licenses\AirspaceFixerDotNet.txt">
53+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
54+
</Content>
5155
<Content Include="Licenses\ControlzEx.txt">
5256
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
5357
</Content>

‎Source/NETworkManager/App.xaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<!-- Control templates (import before styles) -->
2525
<ResourceDictionary Source="/Resources/Templates/ValidationErrorTemplates.xaml" />
2626
<!-- Styles -->
27+
<ResourceDictionary Source="/Resources/Styles/AirspacePanelStyles.xaml" />
2728
<ResourceDictionary Source="/Resources/Styles/ButtonStyles.xaml" />
2829
<ResourceDictionary Source="/Resources/Styles/CheckBoxStyles.xaml" />
2930
<ResourceDictionary Source="/Resources/Styles/ChildWindowStyles.xaml" />

‎Source/NETworkManager/MainWindow.xaml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
xmlns:shell="clr-namespace:ControlzEx.Windows.Shell;assembly=ControlzEx"
1111
xmlns:controls="clr-namespace:NETworkManager.Controls"
1212
xmlns:interactivity="http://schemas.microsoft.com/xaml/behaviors"
13-
xmlns:asf="clr-namespace:AirspaceFixer;assembly=AirspaceFixer"
13+
xmlns:networkManagerControls="clr-namespace:NETworkManager.Controls;assembly=NETworkManager.Controls"
1414
xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF"
1515
xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization"
1616
xmlns:settings="clr-namespace:NETworkManager.Settings;assembly=NETworkManager.Settings"
@@ -973,10 +973,10 @@
973973
</interactivity:Interaction.Triggers>
974974
</Grid>
975975
</Border>
976-
<asf:AirspacePanel Grid.Column="1" Grid.Row="0"
977-
FixAirspace="{Binding Source={x:Static settings:ConfigurationManager.Current}, Path=FixAirspace}">
976+
<networkManagerControls:AirspacePanel Grid.Column="1" Grid.Row="0"
977+
FixAirspace="{Binding Source={x:Static settings:ConfigurationManager.Current}, Path=FixAirspace}">
978978
<ContentControl x:Name="ContentControlApplication" />
979-
</asf:AirspacePanel>
979+
</networkManagerControls:AirspacePanel>
980980
</Grid>
981981
<Grid>
982982
<Grid.Style>

‎Source/NETworkManager/NETworkManager.csproj‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@
6161
</Content>
6262
</ItemGroup>
6363
<ItemGroup>
64-
<PackageReference Include="AirspaceFixer" Version="1.0.6" />
6564
<PackageReference Include="DnsClient" Version="1.8.0" />
6665
<PackageReference Include="gong-wpf-dragdrop" Version="4.0.0" />
6766
<PackageReference Include="IPNetwork2" Version="5.0.1" />
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:controls="clr-namespace:NETworkManager.Controls;assembly=NETworkManager.Controls">
4+
<!--
5+
Based on AirspaceFixer (https://github.com/chris84948/AirspaceFixer) and its fork AirspaceFixerDotNet
6+
(https://github.com/ellipticbit/AirspaceFixerDotNet), MIT License.
7+
Copyright (c) 2017 chris84948, Copyright (c) 2024 EllipticBit, LLC - see NETworkManager.Controls/AirspacePanel.cs
8+
-->
9+
<Style TargetType="{x:Type controls:AirspacePanel}">
10+
<Setter Property="Template">
11+
<Setter.Value>
12+
<ControlTemplate TargetType="{x:Type controls:AirspacePanel}">
13+
<!-- ClipToBounds: The screenshot keeps its size if the window is made smaller while it is shown -->
14+
<Grid HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
15+
VerticalAlignment="{TemplateBinding VerticalAlignment}"
16+
ClipToBounds="True">
17+
<!-- Size is set in code (size of the content when the screenshot was taken), not stretched -->
18+
<Image x:Name="PART_AirspaceScreenshot"
19+
HorizontalAlignment="Left" VerticalAlignment="Top"
20+
Stretch="Fill"
21+
Visibility="Hidden" />
22+
<ContentControl x:Name="PART_AirspaceContent"
23+
Content="{TemplateBinding Content}" />
24+
</Grid>
25+
</ControlTemplate>
26+
</Setter.Value>
27+
</Setter>
28+
</Style>
29+
</ResourceDictionary>

‎Website/docs/changelog/next-release.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ Release date: **xx.xx.2026**
189189
- 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)
190190
- 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)
191191
- 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)
192+
- 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)
192193

193194
**Firewall**
194195

0 commit comments

Comments
 (0)