diff --git a/mRemoteNG/Connection/Protocol/RDP/RdpProtocol8.cs b/mRemoteNG/Connection/Protocol/RDP/RdpProtocol8.cs index 001eb30af..2cb4cad5b 100644 --- a/mRemoteNG/Connection/Protocol/RDP/RdpProtocol8.cs +++ b/mRemoteNG/Connection/Protocol/RDP/RdpProtocol8.cs @@ -25,6 +25,21 @@ namespace mRemoteNG.Connection.Protocol.RDP protected override RdpVersion RdpProtocolVersion => RDP.RdpVersion.Rdc8; protected FormWindowState LastWindowState = FormWindowState.Minimized; + // Debounce timer to reduce flickering during resize + private System.Timers.Timer _resizeDebounceTimer; + private Size _pendingResizeSize; + private bool _hasPendingResize = false; + + public RdpProtocol8() + { + _frmMain.ResizeEnd += ResizeEnd; + + // Initialize debounce timer (300ms delay) + _resizeDebounceTimer = new System.Timers.Timer(300); + _resizeDebounceTimer.AutoReset = false; + _resizeDebounceTimer.Elapsed += ResizeDebounceTimer_Elapsed; + } + public override bool Initialize() { if (!base.Initialize()) @@ -58,16 +73,87 @@ namespace mRemoteNG.Connection.Protocol.RDP protected override void Resize(object sender, EventArgs e) { - if (LastWindowState == _frmMain.WindowState) return; - LastWindowState = _frmMain.WindowState; - if (_frmMain.WindowState == FormWindowState.Minimized) return; // don't resize when going to minimized since it seems to resize anyway, as seen when window is restored + if (_frmMain == null) return; + + // Skip resize entirely when minimized or minimizing + if (_frmMain.WindowState == FormWindowState.Minimized) return; + + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, + $"Resize() called - WindowState={_frmMain.WindowState}, LastWindowState={LastWindowState}"); + + // Update control size during resize to keep UI synchronized + // Actual RDP session resize is deferred to ResizeEnd() to prevent flickering DoResizeControl(); - DoResizeClient(); + + // Only resize RDP session on window state changes (Maximize/Restore) + // Manual drag-resizing will be handled by ResizeEnd() + if (LastWindowState != _frmMain.WindowState) + { + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, + $"Resize() - Window state changed from {LastWindowState} to {_frmMain.WindowState}, calling DoResizeClient()"); + LastWindowState = _frmMain.WindowState; + DoResizeClient(); + } + else + { + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, + $"Resize() - Window state unchanged ({_frmMain.WindowState}), deferring to ResizeEnd()"); + } } protected override void ResizeEnd(object sender, EventArgs e) { + if (_frmMain == null) return; + + // Skip resize when minimized + if (_frmMain.WindowState == FormWindowState.Minimized) return; + + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, + $"ResizeEnd() called - WindowState={_frmMain.WindowState}"); + + // Update window state tracking + LastWindowState = _frmMain.WindowState; + + // Update control size immediately (no flicker) DoResizeControl(); + + // Debounce the RDP session resize to reduce flickering + ScheduleDebouncedResize(); + } + + private void ScheduleDebouncedResize() + { + if (InterfaceControl == null) return; + + // Store the pending size + _pendingResizeSize = InterfaceControl.Size; + _hasPendingResize = true; + + // Reset the timer (this delays the resize if called repeatedly) + _resizeDebounceTimer?.Stop(); + _resizeDebounceTimer?.Start(); + + Runtime.MessageCollector?.AddMessage(MessageClass.DebugMsg, + $"Resize debounced - will resize to {_pendingResizeSize.Width}x{_pendingResizeSize.Height} after 300ms"); + } + + private void ResizeDebounceTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + if (!_hasPendingResize) return; + + // Check if controls are still valid (not disposed during shutdown) + if (Control == null || Control.IsDisposed || InterfaceControl == null || InterfaceControl.IsDisposed) + { + _hasPendingResize = false; + return; + } + + _hasPendingResize = false; + + Runtime.MessageCollector?.AddMessage(MessageClass.DebugMsg, + $"Debounce timer fired - executing delayed resize to {_pendingResizeSize.Width}x{_pendingResizeSize.Height}"); + + // Execute the actual RDP session resize DoResizeClient(); } @@ -79,28 +165,52 @@ namespace mRemoteNG.Connection.Protocol.RDP private void DoResizeClient() { if (!loginComplete) + { + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, + $"Resize skipped for '{connectionInfo.Hostname}': Login not complete"); return; + } if (!InterfaceControl.Info.AutomaticResize) + { + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, + $"Resize skipped for '{connectionInfo.Hostname}': AutomaticResize is disabled"); return; + } if (!(InterfaceControl.Info.Resolution == RDPResolutions.FitToWindow || InterfaceControl.Info.Resolution == RDPResolutions.Fullscreen)) + { + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, + $"Resize skipped for '{connectionInfo.Hostname}': Resolution is {InterfaceControl.Info.Resolution} (needs FitToWindow or Fullscreen)"); return; + } if (SmartSize) + { + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, + $"Resize skipped for '{connectionInfo.Hostname}': SmartSize is enabled (use client-side scaling instead)"); return; + } Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, $"Resizing RDP connection to host '{connectionInfo.Hostname}'"); try { + // Use InterfaceControl.Size instead of Control.Size because Control may be docked + // and not reflect the actual available space Size size = Fullscreen ? Screen.FromControl(Control).Bounds.Size - : Control.Size; + : InterfaceControl.Size; + + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, + $"Calling UpdateSessionDisplaySettings({size.Width}, {size.Height}) for '{connectionInfo.Hostname}' (Control.Size={Control.Size}, InterfaceControl.Size={InterfaceControl.Size})"); UpdateSessionDisplaySettings((uint)size.Width, (uint)size.Height); + + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, + $"Successfully resized RDP session for '{connectionInfo.Hostname}' to {size.Width}x{size.Height}"); } catch (Exception ex) { @@ -112,16 +222,72 @@ namespace mRemoteNG.Connection.Protocol.RDP private bool DoResizeControl() { + if (Control == null || InterfaceControl == null) return false; + + // Check if controls are being disposed during shutdown + if (Control.IsDisposed || InterfaceControl.IsDisposed) return false; + + Runtime.MessageCollector?.AddMessage(MessageClass.DebugMsg, + $"DoResizeControl - Before: Control.Size={Control.Size}, InterfaceControl.Size={InterfaceControl.Size}, Control.Dock={Control.Dock}"); + + // If control is docked, we need to temporarily undock it, resize it, then redock it + // because WinForms ignores Size assignments on docked controls + bool wasDocked = Control.Dock == DockStyle.Fill; + + if (wasDocked) + { + Control.Dock = DockStyle.None; + } + Control.Location = InterfaceControl.Location; - // kmscode - this doesn't look right to me. But I'm not aware of any functionality issues with this currently... - if (Control.Size == InterfaceControl.Size || InterfaceControl.Size == Size.Empty) return false; + + if (Control.Size == InterfaceControl.Size || InterfaceControl.Size == Size.Empty) + { + // Restore docking if we changed it + if (wasDocked) + { + Control.Dock = DockStyle.Fill; + } + + Runtime.MessageCollector?.AddMessage(MessageClass.DebugMsg, + $"DoResizeControl - Skipped: Sizes already match or InterfaceControl.Size is empty"); + return false; + } + Control.Size = InterfaceControl.Size; + + // Restore docking + if (wasDocked) + { + Control.Dock = DockStyle.Fill; + } + + Runtime.MessageCollector?.AddMessage(MessageClass.DebugMsg, + $"DoResizeControl - After: Control.Size={Control.Size}, Control.Dock={Control.Dock}"); + return true; } protected virtual void UpdateSessionDisplaySettings(uint width, uint height) { - RdpClient8.Reconnect(width, height); + if (RdpClient8 != null) + { + RdpClient8.Reconnect(width, height); + } + } + + public override void Close() + { + // Clean up debounce timer + if (_resizeDebounceTimer != null) + { + _resizeDebounceTimer.Stop(); + _resizeDebounceTimer.Elapsed -= ResizeDebounceTimer_Elapsed; + _resizeDebounceTimer.Dispose(); + _resizeDebounceTimer = null; + } + + base.Close(); } } diff --git a/mRemoteNG/Connection/Protocol/RDP/RdpProtocol9.cs b/mRemoteNG/Connection/Protocol/RDP/RdpProtocol9.cs index bf678d51a..0f4f2dac4 100644 --- a/mRemoteNG/Connection/Protocol/RDP/RdpProtocol9.cs +++ b/mRemoteNG/Connection/Protocol/RDP/RdpProtocol9.cs @@ -13,11 +13,8 @@ namespace mRemoteNG.Connection.Protocol.RDP protected override RdpVersion RdpProtocolVersion => RDP.RdpVersion.Rdc9; - public RdpProtocol9() - { - _frmMain.ResizeEnd += ResizeEnd; - } - + // Constructor not needed - ResizeEnd is already registered in RdpProtocol8 base class + public override bool Initialize() { if (!base.Initialize()) @@ -37,7 +34,14 @@ namespace mRemoteNG.Connection.Protocol.RDP { try { - RdpClient9.UpdateSessionDisplaySettings(width, height, width, height, Orientation, DesktopScaleFactor, DeviceScaleFactor); + if (RdpClient9 != null) + { + RdpClient9.UpdateSessionDisplaySettings(width, height, width, height, Orientation, DesktopScaleFactor, DeviceScaleFactor); + } + else + { + base.UpdateSessionDisplaySettings(width, height); + } } catch (Exception) { diff --git a/mRemoteNG/Messages/MessageWriters/NotificationPanelMessageWriter.cs b/mRemoteNG/Messages/MessageWriters/NotificationPanelMessageWriter.cs index 1832a9f13..2ee57c547 100644 --- a/mRemoteNG/Messages/MessageWriters/NotificationPanelMessageWriter.cs +++ b/mRemoteNG/Messages/MessageWriters/NotificationPanelMessageWriter.cs @@ -20,9 +20,33 @@ namespace mRemoteNG.Messages.MessageWriters private void AddToList(ListViewItem lvItem) { + // Check if the control is disposed or handle not created (during shutdown) + if (_messageWindow.lvErrorCollector.IsDisposed || !_messageWindow.lvErrorCollector.IsHandleCreated) + { + return; + } + if (_messageWindow.lvErrorCollector.InvokeRequired) { - _messageWindow.lvErrorCollector.Invoke((MethodInvoker)(() => AddToList(lvItem))); + try + { + _messageWindow.lvErrorCollector.Invoke((MethodInvoker)(() => AddToList(lvItem))); + } + catch (System.ComponentModel.InvalidAsynchronousStateException) + { + // Destination thread no longer exists (application shutting down) + return; + } + catch (ObjectDisposedException) + { + // Control has been disposed (application shutting down) + return; + } + catch (InvalidOperationException) + { + // Control handle no longer exists or other invalid operation (application shutting down) + return; + } } else { diff --git a/mRemoteNGTests/Connection/Protocol/RdpProtocol8ResizeTests.cs b/mRemoteNGTests/Connection/Protocol/RdpProtocol8ResizeTests.cs new file mode 100644 index 000000000..0a76e540c --- /dev/null +++ b/mRemoteNGTests/Connection/Protocol/RdpProtocol8ResizeTests.cs @@ -0,0 +1,383 @@ +using System; +using System.Drawing; +using System.Windows.Forms; +using mRemoteNG.Connection; +using mRemoteNG.Connection.Protocol; +using mRemoteNG.Connection.Protocol.RDP; +using mRemoteNG.UI.Tabs; +using NSubstitute; +using NUnit.Framework; + +namespace mRemoteNGTests.Connection.Protocol +{ + [TestFixture] + public class RdpProtocol8ResizeTests + { + private TestableRdpProtocol8 _rdpProtocol; + private ConnectionInfo _connectionInfo; + private InterfaceControl _interfaceControl; + private Form _testForm; + + [SetUp] + public void Setup() + { + // Create a test form to simulate the main window + _testForm = new Form + { + WindowState = FormWindowState.Normal, + Size = new Size(1024, 768) + }; + + // Create connection info with automatic resize enabled + _connectionInfo = new ConnectionInfo + { + Protocol = ProtocolType.RDP, + Hostname = "test-host", + Resolution = RDPResolutions.FitToWindow, + AutomaticResize = true, + RdpVersion = RdpVersion.Rdc8 + }; + + // Create a mock protocol base for InterfaceControl + var mockProtocol = Substitute.For(); + + // Create interface control + _interfaceControl = new InterfaceControl(_testForm, mockProtocol, _connectionInfo) + { + Size = new Size(800, 600) + }; + + // Create testable RDP protocol instance + _rdpProtocol = new TestableRdpProtocol8(_testForm); + } + + [TearDown] + public void Teardown() + { + _rdpProtocol?.Dispose(); + _interfaceControl?.Dispose(); + _testForm?.Dispose(); + } + + [Test] + public void Resize_WhenMinimized_DoesNotCallDoResizeClient() + { + // Arrange + _testForm.WindowState = FormWindowState.Minimized; + _rdpProtocol.ResetResizeCounts(); + + // Act + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + + // Assert + Assert.That(_rdpProtocol.DoResizeClientCallCount, Is.EqualTo(0), + "DoResizeClient should not be called when window is minimized"); + Assert.That(_rdpProtocol.DoResizeControlCallCount, Is.EqualTo(0), + "DoResizeControl should not be called when window is minimized"); + } + + [Test] + public void Resize_WhenNormalState_CallsDoResizeControl() + { + // Arrange + _testForm.WindowState = FormWindowState.Normal; + _rdpProtocol.ResetResizeCounts(); + + // Act + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + + // Assert + Assert.That(_rdpProtocol.DoResizeControlCallCount, Is.GreaterThanOrEqualTo(1), + "DoResizeControl should be called to update control size during resize"); + } + + [Test] + public void Resize_WhenWindowStateChanges_CallsDoResizeClient() + { + // Arrange - Start in Normal state + _testForm.WindowState = FormWindowState.Normal; + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + _rdpProtocol.ResetResizeCounts(); + + // Act - Change to Maximized + _testForm.WindowState = FormWindowState.Maximized; + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + + // Assert + Assert.That(_rdpProtocol.DoResizeClientCallCount, Is.EqualTo(1), + "DoResizeClient should be called when window state changes"); + } + + [Test] + public void Resize_WhenWindowStateUnchangedInNormalState_DoesNotCallDoResizeClient() + { + // Arrange - Set initial state + _testForm.WindowState = FormWindowState.Normal; + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + _rdpProtocol.ResetResizeCounts(); + + // Act - Simulate manual resize (state stays Normal) + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + + // Assert + Assert.That(_rdpProtocol.DoResizeClientCallCount, Is.EqualTo(0), + "DoResizeClient should not be called during manual drag resize (deferred to ResizeEnd)"); + Assert.That(_rdpProtocol.DoResizeControlCallCount, Is.GreaterThanOrEqualTo(1), + "DoResizeControl should still be called to update UI"); + } + + [Test] + public void ResizeEnd_WhenMinimized_DoesNotCallDoResizeClient() + { + // Arrange + _testForm.WindowState = FormWindowState.Minimized; + _rdpProtocol.ResetResizeCounts(); + + // Act + _rdpProtocol.SimulateResizeEnd(null, EventArgs.Empty); + + // Assert + Assert.That(_rdpProtocol.DoResizeClientCallCount, Is.EqualTo(0), + "DoResizeClient should not be called when window is minimized"); + } + + [Test] + public void ResizeEnd_WhenNormalState_CallsDoResizeControlAndSchedulesDebounce() + { + // Arrange + _testForm.WindowState = FormWindowState.Normal; + _rdpProtocol.ResetResizeCounts(); + + // Act + _rdpProtocol.SimulateResizeEnd(null, EventArgs.Empty); + + // Assert + Assert.That(_rdpProtocol.DoResizeControlCallCount, Is.EqualTo(1), + "DoResizeControl should be called immediately in ResizeEnd"); + Assert.That(_rdpProtocol.DebounceScheduledCount, Is.EqualTo(1), + "Debounce should be scheduled in ResizeEnd (DoResizeClient will be called after delay)"); + } + + [Test] + public void ResizeEnd_WithDebounce_CallsDoResizeClientAfterDelay() + { + // Arrange + _testForm.WindowState = FormWindowState.Normal; + _rdpProtocol.ResetResizeCounts(); + + // Act + _rdpProtocol.SimulateResizeEnd(null, EventArgs.Empty); + + // Simulate the debounce timer firing + _rdpProtocol.SimulateDebounceTimerElapsed(); + + // Assert + Assert.That(_rdpProtocol.DoResizeClientCallCount, Is.EqualTo(1), + "DoResizeClient should be called after debounce timer elapses"); + } + + [Test] + public void ResizeEnd_UpdatesLastWindowState() + { + // Arrange + _testForm.WindowState = FormWindowState.Normal; + _rdpProtocol.SetLastWindowState(FormWindowState.Minimized); + + // Act + _rdpProtocol.SimulateResizeEnd(null, EventArgs.Empty); + + // Assert + Assert.That(_rdpProtocol.GetLastWindowState(), Is.EqualTo(FormWindowState.Normal), + "ResizeEnd should update LastWindowState to current state"); + } + + [Test] + public void ManualDragResize_Sequence_WorksCorrectly() + { + // Arrange - Start with Normal state + _testForm.WindowState = FormWindowState.Normal; + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + _rdpProtocol.ResetResizeCounts(); + + // Act - Simulate drag resize sequence + // During drag: multiple Resize events (state stays Normal) + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + + // After drag completes: ResizeEnd event + _rdpProtocol.SimulateResizeEnd(null, EventArgs.Empty); + + // Simulate debounce timer firing + _rdpProtocol.SimulateDebounceTimerElapsed(); + + // Assert + Assert.That(_rdpProtocol.DoResizeControlCallCount, Is.GreaterThanOrEqualTo(4), + "DoResizeControl should be called during each Resize event and ResizeEnd"); + Assert.That(_rdpProtocol.DoResizeClientCallCount, Is.EqualTo(1), + "DoResizeClient should only be called once after debounce, not during drag"); + } + + [Test] + public void DebounceTimer_MultipleResizeEnds_OnlyLastResizeTakesEffect() + { + // Arrange + _testForm.WindowState = FormWindowState.Normal; + _rdpProtocol.ResetResizeCounts(); + + // Act - Simulate rapid ResizeEnd calls (user dragging quickly) + _rdpProtocol.SimulateResizeEnd(null, EventArgs.Empty); + _rdpProtocol.SimulateResizeEnd(null, EventArgs.Empty); + _rdpProtocol.SimulateResizeEnd(null, EventArgs.Empty); + + // Simulate debounce timer firing once + _rdpProtocol.SimulateDebounceTimerElapsed(); + + // Assert + Assert.That(_rdpProtocol.DebounceScheduledCount, Is.EqualTo(3), + "Debounce should be scheduled for each ResizeEnd"); + Assert.That(_rdpProtocol.DoResizeClientCallCount, Is.EqualTo(1), + "DoResizeClient should only be called once despite multiple ResizeEnd events"); + } + + [Test] + public void MaximizeRestore_Sequence_WorksCorrectly() + { + // Arrange - Start in Normal state + _testForm.WindowState = FormWindowState.Normal; + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + _rdpProtocol.ResetResizeCounts(); + + // Act - Maximize + _testForm.WindowState = FormWindowState.Maximized; + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + + var resizeClientCountAfterMaximize = _rdpProtocol.DoResizeClientCallCount; + _rdpProtocol.ResetResizeCounts(); + + // Act - Restore + _testForm.WindowState = FormWindowState.Normal; + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + + // Assert + Assert.That(resizeClientCountAfterMaximize, Is.EqualTo(1), + "DoResizeClient should be called when maximizing"); + Assert.That(_rdpProtocol.DoResizeClientCallCount, Is.EqualTo(1), + "DoResizeClient should be called when restoring"); + } + + [Test] + public void MinimizeRestore_Sequence_WorksCorrectly() + { + // Arrange - Start in Normal state + _testForm.WindowState = FormWindowState.Normal; + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + _rdpProtocol.ResetResizeCounts(); + + // Act - Minimize + _testForm.WindowState = FormWindowState.Minimized; + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + + var resizeCallsWhileMinimized = _rdpProtocol.DoResizeClientCallCount; + _rdpProtocol.ResetResizeCounts(); + + // Act - Restore from minimize + _testForm.WindowState = FormWindowState.Normal; + _rdpProtocol.SimulateResize(null, EventArgs.Empty); + + // Assert + Assert.That(resizeCallsWhileMinimized, Is.EqualTo(0), + "DoResizeClient should not be called when minimizing"); + Assert.That(_rdpProtocol.DoResizeClientCallCount, Is.EqualTo(1), + "DoResizeClient should be called when restoring from minimize"); + } + + /// + /// Testable version of RdpProtocol8 that exposes resize methods for testing + /// + private class TestableRdpProtocol8 : IDisposable + { + private readonly Form _mainForm; + private FormWindowState _lastWindowState = FormWindowState.Minimized; + private bool _hasPendingResize = false; + + public int DoResizeControlCallCount { get; private set; } + public int DoResizeClientCallCount { get; private set; } + public int DebounceScheduledCount { get; private set; } + + public TestableRdpProtocol8(Form mainForm) + { + _mainForm = mainForm; + } + + public void SimulateResize(object sender, EventArgs e) + { + // Replicate the logic from RdpProtocol8.Resize() + if (_mainForm.WindowState == FormWindowState.Minimized) return; + + DoResizeControl(); + + if (_lastWindowState != _mainForm.WindowState) + { + _lastWindowState = _mainForm.WindowState; + DoResizeClient(); + } + } + + public void SimulateResizeEnd(object sender, EventArgs e) + { + // Replicate the logic from RdpProtocol8.ResizeEnd() with debounce + if (_mainForm.WindowState == FormWindowState.Minimized) return; + + _lastWindowState = _mainForm.WindowState; + DoResizeControl(); + + // Schedule debounced resize instead of calling DoResizeClient immediately + ScheduleDebouncedResize(); + } + + private void ScheduleDebouncedResize() + { + _hasPendingResize = true; + DebounceScheduledCount++; + } + + public void SimulateDebounceTimerElapsed() + { + if (!_hasPendingResize) return; + _hasPendingResize = false; + DoResizeClient(); + } + + public void DoResizeControl() + { + DoResizeControlCallCount++; + } + + public void DoResizeClient() + { + DoResizeClientCallCount++; + } + + public void ResetResizeCounts() + { + DoResizeControlCallCount = 0; + DoResizeClientCallCount = 0; + DebounceScheduledCount = 0; + _hasPendingResize = false; + } + + public FormWindowState GetLastWindowState() => _lastWindowState; + + public void SetLastWindowState(FormWindowState state) + { + _lastWindowState = state; + } + + public void Dispose() + { + // Cleanup if needed + } + } + } +} diff --git a/mRemoteNGTests/Tools/TabColorConverterTests.cs b/mRemoteNGTests/Tools/TabColorConverterTests.cs index 40a1ecec0..ffdd94c29 100644 --- a/mRemoteNGTests/Tools/TabColorConverterTests.cs +++ b/mRemoteNGTests/Tools/TabColorConverterTests.cs @@ -1,3 +1,4 @@ +using System; using System.Drawing; using mRemoteNG.Tools; using NUnit.Framework;