From 4128f3404abc37a9d891daf1a2af9e9c6a0974c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:31:35 +0000 Subject: [PATCH 01/11] Initial plan From 4897771fbfb974a515310512e15ab6a97872ce88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:35:06 +0000 Subject: [PATCH 02/11] Initial plan From e67754ee9ffc413de054fb52efdd537fdd4e3768 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:35:18 +0000 Subject: [PATCH 03/11] Fix unhandled exception when closing panel with connections Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com> --- mRemoteNG/UI/Window/ConnectionWindow.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/mRemoteNG/UI/Window/ConnectionWindow.cs b/mRemoteNG/UI/Window/ConnectionWindow.cs index 4c1c1691..506e88e3 100644 --- a/mRemoteNG/UI/Window/ConnectionWindow.cs +++ b/mRemoteNG/UI/Window/ConnectionWindow.cs @@ -776,6 +776,7 @@ namespace mRemoteNG.UI.Window ProtocolBase protocolBase = sender as ProtocolBase; if (!(protocolBase?.InterfaceControl.Parent is ConnectionTab tabPage)) return; if (tabPage.Disposing || tabPage.IsDisposed) return; + if (IsDisposed || Disposing) return; tabPage.protocolClose = true; Invoke(new Action(() => tabPage.Close())); } From 0f819ade56928a96bac2988a26b2cf569532b3f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:39:14 +0000 Subject: [PATCH 04/11] Initial plan From f8b7d37af1ff16d4b451ae00e49447d87b164323 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:42:31 +0000 Subject: [PATCH 05/11] Add ARD (Apple Remote Desktop) protocol support Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com> --- .../Connection/Protocol/ARD/ProtocolARD.cs | 19 +++++++++++++++++++ .../Connection/Protocol/ProtocolFactory.cs | 3 +++ mRemoteNG/Connection/Protocol/ProtocolType.cs | 3 +++ mRemoteNG/Language/Language.resx | 3 +++ 4 files changed, 28 insertions(+) create mode 100644 mRemoteNG/Connection/Protocol/ARD/ProtocolARD.cs diff --git a/mRemoteNG/Connection/Protocol/ARD/ProtocolARD.cs b/mRemoteNG/Connection/Protocol/ARD/ProtocolARD.cs new file mode 100644 index 00000000..d10a7ff9 --- /dev/null +++ b/mRemoteNG/Connection/Protocol/ARD/ProtocolARD.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.Versioning; +using mRemoteNG.Connection.Protocol.VNC; + +namespace mRemoteNG.Connection.Protocol.ARD +{ + [SupportedOSPlatform("windows")] + public class ProtocolARD : ProtocolVNC + { + public ProtocolARD() + { + } + + public new enum Defaults + { + Port = 5900 + } + } +} diff --git a/mRemoteNG/Connection/Protocol/ProtocolFactory.cs b/mRemoteNG/Connection/Protocol/ProtocolFactory.cs index 820724f8..b6d7fbba 100644 --- a/mRemoteNG/Connection/Protocol/ProtocolFactory.cs +++ b/mRemoteNG/Connection/Protocol/ProtocolFactory.cs @@ -5,6 +5,7 @@ using mRemoteNG.Connection.Protocol.Rlogin; using mRemoteNG.Connection.Protocol.SSH; using mRemoteNG.Connection.Protocol.Telnet; using mRemoteNG.Connection.Protocol.VNC; +using mRemoteNG.Connection.Protocol.ARD; using System; using mRemoteNG.Connection.Protocol.PowerShell; using mRemoteNG.Resources.Language; @@ -28,6 +29,8 @@ namespace mRemoteNG.Connection.Protocol return rdp; case ProtocolType.VNC: return new ProtocolVNC(); + case ProtocolType.ARD: + return new ProtocolARD(); case ProtocolType.SSH1: return new ProtocolSSH1(); case ProtocolType.SSH2: diff --git a/mRemoteNG/Connection/Protocol/ProtocolType.cs b/mRemoteNG/Connection/Protocol/ProtocolType.cs index f4e1bd47..c795cd8e 100644 --- a/mRemoteNG/Connection/Protocol/ProtocolType.cs +++ b/mRemoteNG/Connection/Protocol/ProtocolType.cs @@ -35,6 +35,9 @@ namespace mRemoteNG.Connection.Protocol [LocalizedAttributes.LocalizedDescription(nameof(Language.PowerShell))] PowerShell = 10, + [LocalizedAttributes.LocalizedDescription(nameof(Language.Ard))] + ARD = 11, + [LocalizedAttributes.LocalizedDescription(nameof(Language.ExternalTool))] IntApp = 20 } diff --git a/mRemoteNG/Language/Language.resx b/mRemoteNG/Language/Language.resx index 311c87c4..8382b882 100644 --- a/mRemoteNG/Language/Language.resx +++ b/mRemoteNG/Language/Language.resx @@ -159,6 +159,9 @@ Automatic update settings + + ARD (Apple Remote Desktop) + Aspect From b19319926850ffd8fd20bf4cedb28c09886ac52e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:45:38 +0000 Subject: [PATCH 06/11] Add ARD protocol to VNC property attributes and default port handling Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com> --- .../Connection/AbstractConnectionRecord.cs | 22 +++++++++---------- mRemoteNG/Connection/ConnectionInfo.cs | 3 +++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/mRemoteNG/Connection/AbstractConnectionRecord.cs b/mRemoteNG/Connection/AbstractConnectionRecord.cs index 74ff6e15..df8bbef5 100644 --- a/mRemoteNG/Connection/AbstractConnectionRecord.cs +++ b/mRemoteNG/Connection/AbstractConnectionRecord.cs @@ -908,7 +908,7 @@ namespace mRemoteNG.Connection LocalizedAttributes.LocalizedDisplayName(nameof(Language.Compression)), LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionCompression)), TypeConverter(typeof(MiscTools.EnumTypeConverter)), - AttributeUsedInProtocol(ProtocolType.VNC), + AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD), Browsable(false)] public ProtocolVNC.Compression VNCCompression { @@ -920,7 +920,7 @@ namespace mRemoteNG.Connection LocalizedAttributes.LocalizedDisplayName(nameof(Language.Encoding)), LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionEncoding)), TypeConverter(typeof(MiscTools.EnumTypeConverter)), - AttributeUsedInProtocol(ProtocolType.VNC), + AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD), Browsable(false)] public ProtocolVNC.Encoding VNCEncoding { @@ -932,7 +932,7 @@ namespace mRemoteNG.Connection LocalizedAttributes.LocalizedDisplayName(nameof(Language.AuthenticationMode)), LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionAuthenticationMode)), TypeConverter(typeof(MiscTools.EnumTypeConverter)), - AttributeUsedInProtocol(ProtocolType.VNC), + AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD), Browsable(false)] public ProtocolVNC.AuthMode VNCAuthMode { @@ -944,7 +944,7 @@ namespace mRemoteNG.Connection LocalizedAttributes.LocalizedDisplayName(nameof(Language.ProxyType)), LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionVNCProxyType)), TypeConverter(typeof(MiscTools.EnumTypeConverter)), - AttributeUsedInProtocol(ProtocolType.VNC), + AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD), Browsable(false)] public ProtocolVNC.ProxyType VNCProxyType { @@ -955,7 +955,7 @@ namespace mRemoteNG.Connection [LocalizedAttributes.LocalizedCategory(nameof(Language.Proxy), 7), LocalizedAttributes.LocalizedDisplayName(nameof(Language.ProxyAddress)), LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionVNCProxyAddress)), - AttributeUsedInProtocol(ProtocolType.VNC), + AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD), Browsable(false)] public string VNCProxyIP { @@ -966,7 +966,7 @@ namespace mRemoteNG.Connection [LocalizedAttributes.LocalizedCategory(nameof(Language.Proxy), 7), LocalizedAttributes.LocalizedDisplayName(nameof(Language.ProxyPort)), LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionVNCProxyPort)), - AttributeUsedInProtocol(ProtocolType.VNC), + AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD), Browsable(false)] public int VNCProxyPort { @@ -977,7 +977,7 @@ namespace mRemoteNG.Connection [LocalizedAttributes.LocalizedCategory(nameof(Language.Proxy), 7), LocalizedAttributes.LocalizedDisplayName(nameof(Language.ProxyUsername)), LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionVNCProxyUsername)), - AttributeUsedInProtocol(ProtocolType.VNC), + AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD), Browsable(false)] public string VNCProxyUsername { @@ -989,7 +989,7 @@ namespace mRemoteNG.Connection LocalizedAttributes.LocalizedDisplayName(nameof(Language.ProxyPassword)), LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionVNCProxyPassword)), PasswordPropertyText(true), - AttributeUsedInProtocol(ProtocolType.VNC), + AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD), Browsable(false)] public string VNCProxyPassword { @@ -1001,7 +1001,7 @@ namespace mRemoteNG.Connection LocalizedAttributes.LocalizedDisplayName(nameof(Language.Colors)), LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionColors)), TypeConverter(typeof(MiscTools.EnumTypeConverter)), - AttributeUsedInProtocol(ProtocolType.VNC), + AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD), Browsable(false)] public ProtocolVNC.Colors VNCColors { @@ -1013,7 +1013,7 @@ namespace mRemoteNG.Connection LocalizedAttributes.LocalizedDisplayName(nameof(Language.SmartSizeMode)), LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionSmartSizeMode)), TypeConverter(typeof(MiscTools.EnumTypeConverter)), - AttributeUsedInProtocol(ProtocolType.VNC)] + AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD)] public ProtocolVNC.SmartSizeMode VNCSmartSizeMode { get => GetPropertyValue("VNCSmartSizeMode", _vncSmartSizeMode); @@ -1024,7 +1024,7 @@ namespace mRemoteNG.Connection LocalizedAttributes.LocalizedDisplayName(nameof(Language.ViewOnly)), LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionViewOnly)), TypeConverter(typeof(MiscTools.YesNoTypeConverter)), - AttributeUsedInProtocol(ProtocolType.VNC)] + AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD)] public bool VNCViewOnly { get => GetPropertyValue("VNCViewOnly", _vncViewOnly); diff --git a/mRemoteNG/Connection/ConnectionInfo.cs b/mRemoteNG/Connection/ConnectionInfo.cs index f6538436..3bb9a848 100644 --- a/mRemoteNG/Connection/ConnectionInfo.cs +++ b/mRemoteNG/Connection/ConnectionInfo.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using mRemoteNG.App; using mRemoteNG.Connection.Protocol; +using mRemoteNG.Connection.Protocol.ARD; using mRemoteNG.Connection.Protocol.Http; using mRemoteNG.Connection.Protocol.PowerShell; using mRemoteNG.Connection.Protocol.RAW; @@ -254,6 +255,8 @@ namespace mRemoteNG.Connection return (int)RdpProtocol.Defaults.Port; case ProtocolType.VNC: return (int)ProtocolVNC.Defaults.Port; + case ProtocolType.ARD: + return (int)ProtocolARD.Defaults.Port; case ProtocolType.SSH1: return (int)ProtocolSSH1.Defaults.Port; case ProtocolType.SSH2: From ac4469bb4a36484a221b7c9cd20540207d0fe774 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:48:57 +0000 Subject: [PATCH 07/11] Add ARD protocol support to UI, port scanning, and tests Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com> --- .../Serializers/MiscSerializers/PortScanDeserializer.cs | 4 ++++ .../ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs | 1 + mRemoteNGTests/Connection/ConnectionInfoTests.cs | 1 + 3 files changed, 6 insertions(+) diff --git a/mRemoteNG/Config/Serializers/MiscSerializers/PortScanDeserializer.cs b/mRemoteNG/Config/Serializers/MiscSerializers/PortScanDeserializer.cs index f80081b8..4fc09ae2 100644 --- a/mRemoteNG/Config/Serializers/MiscSerializers/PortScanDeserializer.cs +++ b/mRemoteNG/Config/Serializers/MiscSerializers/PortScanDeserializer.cs @@ -61,6 +61,10 @@ namespace mRemoteNG.Config.Serializers.MiscSerializers if (host.Vnc) finalProtocol = ProtocolType.VNC; break; + case ProtocolType.ARD: + if (host.Vnc) + finalProtocol = ProtocolType.ARD; + break; default: protocolValid = false; break; diff --git a/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs b/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs index fb53dd2c..241c19d8 100644 --- a/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs +++ b/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs @@ -168,6 +168,7 @@ namespace mRemoteNG.UI.Controls.ConnectionInfoPropertyGrid strHide.AddRange(SpecialRdpExclusions()); break; case ProtocolType.VNC: + case ProtocolType.ARD: strHide.AddRange(SpecialVncExclusions()); break; } diff --git a/mRemoteNGTests/Connection/ConnectionInfoTests.cs b/mRemoteNGTests/Connection/ConnectionInfoTests.cs index 681f772d..6f8eeffe 100644 --- a/mRemoteNGTests/Connection/ConnectionInfoTests.cs +++ b/mRemoteNGTests/Connection/ConnectionInfoTests.cs @@ -115,6 +115,7 @@ namespace mRemoteNGTests.Connection [TestCase(ProtocolType.SSH2, ExpectedResult = 22)] [TestCase(ProtocolType.Telnet, ExpectedResult = 23)] [TestCase(ProtocolType.VNC, ExpectedResult = 5900)] + [TestCase(ProtocolType.ARD, ExpectedResult = 5900)] public int GetDefaultPortReturnsCorrectPortForProtocol(ProtocolType protocolType) { _connectionInfo.Protocol = protocolType; From 3c6a4856476f0f1790c81cf624592299740b7f52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:49:32 +0000 Subject: [PATCH 08/11] Add TabColor property to connection info and implement tab coloring Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com> --- .../Xml/XmlConnectionNodeSerializer28.cs | 3 ++ .../Xml/XmlConnectionsDeserializer.cs | 2 ++ .../Connection/AbstractConnectionRecord.cs | 12 +++++++ mRemoteNG/Connection/ConnectionInfo.cs | 1 + .../Connection/ConnectionInfoInheritance.cs | 6 ++++ mRemoteNG/Language/Language.resx | 6 ++++ mRemoteNG/UI/Tabs/DockPaneStripNG.cs | 32 +++++++++++++++++-- 7 files changed, 60 insertions(+), 2 deletions(-) diff --git a/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionNodeSerializer28.cs b/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionNodeSerializer28.cs index 06bb6124..a12d8ed7 100644 --- a/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionNodeSerializer28.cs +++ b/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionNodeSerializer28.cs @@ -42,6 +42,7 @@ namespace mRemoteNG.Config.Serializers.ConnectionSerializers.Xml element.Add(new XAttribute("Descr", connectionInfo.Description)); element.Add(new XAttribute("Icon", connectionInfo.Icon)); element.Add(new XAttribute("Panel", connectionInfo.Panel)); + element.Add(new XAttribute("TabColor", connectionInfo.TabColor)); element.Add(new XAttribute("Id", connectionInfo.ConstantID)); if (!Runtime.UseCredentialManager) @@ -187,6 +188,8 @@ namespace mRemoteNG.Config.Serializers.ConnectionSerializers.Xml element.Add(new XAttribute("InheritIcon", inheritance.Icon.ToString().ToLowerInvariant())); if (inheritance.Panel) element.Add(new XAttribute("InheritPanel", inheritance.Panel.ToString().ToLowerInvariant())); + if (inheritance.TabColor) + element.Add(new XAttribute("InheritTabColor", inheritance.TabColor.ToString().ToLowerInvariant())); if (inheritance.Password) element.Add(new XAttribute("InheritPassword", inheritance.Password.ToString().ToLowerInvariant())); if (inheritance.Port) diff --git a/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionsDeserializer.cs b/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionsDeserializer.cs index ab6c15a0..7f053ea1 100644 --- a/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionsDeserializer.cs +++ b/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionsDeserializer.cs @@ -328,6 +328,7 @@ namespace mRemoteNG.Config.Serializers.ConnectionSerializers.Xml connectionInfo.Inheritance.DisplayWallpaper = xmlnode.GetAttributeAsBool("InheritDisplayWallpaper"); connectionInfo.Inheritance.Icon = xmlnode.GetAttributeAsBool("InheritIcon"); connectionInfo.Inheritance.Panel = xmlnode.GetAttributeAsBool("InheritPanel"); + connectionInfo.Inheritance.TabColor = xmlnode.GetAttributeAsBool("InheritTabColor"); connectionInfo.Inheritance.Port = xmlnode.GetAttributeAsBool("InheritPort"); connectionInfo.Inheritance.Protocol = xmlnode.GetAttributeAsBool("InheritProtocol"); connectionInfo.Inheritance.PuttySession = xmlnode.GetAttributeAsBool("InheritPuttySession"); @@ -350,6 +351,7 @@ namespace mRemoteNG.Config.Serializers.ConnectionSerializers.Xml connectionInfo.Icon = xmlnode.GetAttributeAsString("Icon"); connectionInfo.Panel = xmlnode.GetAttributeAsString("Panel"); + connectionInfo.TabColor = xmlnode.GetAttributeAsString("TabColor"); } else { diff --git a/mRemoteNG/Connection/AbstractConnectionRecord.cs b/mRemoteNG/Connection/AbstractConnectionRecord.cs index 74ff6e15..c16b0ec6 100644 --- a/mRemoteNG/Connection/AbstractConnectionRecord.cs +++ b/mRemoteNG/Connection/AbstractConnectionRecord.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.ComponentModel; +using System.Drawing; using mRemoteNG.Connection.Protocol; using mRemoteNG.Connection.Protocol.Http; using mRemoteNG.Connection.Protocol.RDP; @@ -22,6 +23,7 @@ namespace mRemoteNG.Connection private string _description; private string _icon; private string _panel; + private string _tabColor; private string _hostname; private ExternalAddressProvider _externalAddressProvider; @@ -153,6 +155,16 @@ namespace mRemoteNG.Connection set => SetField(ref _panel, value, "Panel"); } + [LocalizedAttributes.LocalizedCategory(nameof(Language.Display)), + LocalizedAttributes.LocalizedDisplayName(nameof(Language.TabColor)), + LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionTabColor)), + TypeConverter(typeof(ColorConverter))] + public virtual string TabColor + { + get => GetPropertyValue("TabColor", _tabColor); + set => SetField(ref _tabColor, value, "TabColor"); + } + #endregion #region Connection diff --git a/mRemoteNG/Connection/ConnectionInfo.cs b/mRemoteNG/Connection/ConnectionInfo.cs index f6538436..0e02383a 100644 --- a/mRemoteNG/Connection/ConnectionInfo.cs +++ b/mRemoteNG/Connection/ConnectionInfo.cs @@ -289,6 +289,7 @@ namespace mRemoteNG.Connection Description = Settings.Default.ConDefaultDescription; Icon = Settings.Default.ConDefaultIcon; Panel = Language.General; + TabColor = ""; } private void SetConnectionDefaults() diff --git a/mRemoteNG/Connection/ConnectionInfoInheritance.cs b/mRemoteNG/Connection/ConnectionInfoInheritance.cs index 1c0e2281..ec2ca3ea 100644 --- a/mRemoteNG/Connection/ConnectionInfoInheritance.cs +++ b/mRemoteNG/Connection/ConnectionInfoInheritance.cs @@ -50,6 +50,12 @@ namespace mRemoteNG.Connection TypeConverter(typeof(MiscTools.YesNoTypeConverter))] public bool Panel { get; set; } + [LocalizedAttributes.LocalizedCategory(nameof(Language.Display), 2), + LocalizedAttributes.LocalizedDisplayNameInherit(nameof(Language.TabColor)), + LocalizedAttributes.LocalizedDescriptionInherit(nameof(Language.PropertyDescriptionTabColor)), + TypeConverter(typeof(MiscTools.YesNoTypeConverter))] + public bool TabColor { get; set; } + #endregion #region Connection diff --git a/mRemoteNG/Language/Language.resx b/mRemoteNG/Language/Language.resx index 311c87c4..f4297a93 100644 --- a/mRemoteNG/Language/Language.resx +++ b/mRemoteNG/Language/Language.resx @@ -1020,6 +1020,9 @@ If you run into such an error, please create a new connection file! Sets the panel in which the connection will open. + + Sets the color of the connection tab. Leave empty for default theme color. + Enter your password. @@ -1182,6 +1185,9 @@ If you run into such an error, please create a new connection file! Panel + + Tab Color + Password diff --git a/mRemoteNG/UI/Tabs/DockPaneStripNG.cs b/mRemoteNG/UI/Tabs/DockPaneStripNG.cs index a149ebbf..0360d64e 100644 --- a/mRemoteNG/UI/Tabs/DockPaneStripNG.cs +++ b/mRemoteNG/UI/Tabs/DockPaneStripNG.cs @@ -991,8 +991,11 @@ namespace mRemoteNG.UI.Tabs rectText = DrawHelper.RtlTransform(this, rectText); rectIcon = DrawHelper.RtlTransform(this, rectIcon); - Color activeColor = DockPane.DockPanel.Theme.ColorPalette.TabSelectedActive.Background; - Color lostFocusColor = DockPane.DockPanel.Theme.ColorPalette.TabSelectedInactive.Background; + // Get custom tab color if available + Color? customTabColor = GetCustomTabColor(tab.Content); + + Color activeColor = customTabColor ?? DockPane.DockPanel.Theme.ColorPalette.TabSelectedActive.Background; + Color lostFocusColor = customTabColor ?? DockPane.DockPanel.Theme.ColorPalette.TabSelectedInactive.Background; Color inactiveColor = DockPane.DockPanel.Theme.ColorPalette.MainWindowActive.Background; Color mouseHoverColor = DockPane.DockPanel.Theme.ColorPalette.TabUnselectedHovered.Background; @@ -1056,6 +1059,31 @@ namespace mRemoteNG.UI.Tabs g.DrawIcon(tab.Content.DockHandler.Icon, rectIcon); } + private Color? GetCustomTabColor(IDockContent content) + { + try + { + if (content is ConnectionTab connectionTab) + { + InterfaceControl interfaceControl = InterfaceControl.FindInterfaceControl(connectionTab); + if (interfaceControl?.Info != null) + { + string tabColorStr = interfaceControl.Info.TabColor; + if (!string.IsNullOrEmpty(tabColorStr)) + { + ColorConverter converter = new ColorConverter(); + return (Color)converter.ConvertFromString(tabColorStr); + } + } + } + } + catch + { + // If there's any error parsing the color, just return null to use default + } + return null; + } + private bool m_isMouseDown; protected bool IsMouseDown From 5830f39d50568073b954ed381b5a8ce9e39264b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:51:26 +0000 Subject: [PATCH 09/11] Add documentation for Tab Color feature Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com> --- CHANGELOG.md | 1 + .../user_interface/connections.rst | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de0838a..e63bba60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - #2734: fix native build for Windows-x64 ### Added +- Add configurable connection tab colors to distinguish between different environments (e.g., Dev, Test, Live) - #2728 Add support for building mRemoteNG on Windows ARM64 - #2723: Read keyboardhook, gatewayaccesstoken and gatewaycredentialssource from RDP File - #2690: தமிழ் (ta) Translation update diff --git a/mRemoteNGDocumentation/user_interface/connections.rst b/mRemoteNGDocumentation/user_interface/connections.rst index 84aa31e4..763730ea 100644 --- a/mRemoteNGDocumentation/user_interface/connections.rst +++ b/mRemoteNGDocumentation/user_interface/connections.rst @@ -87,6 +87,26 @@ Icon The icon indicates the visual identifier for the connection. Clicking the icon will let you set a different icon for the connection. +Tab Color +--------- + +.. note:: + + The Tab Color property is available in the Display category of the connection properties. + +You can set a custom color for connection tabs to help distinguish between different environments (e.g., Development, Testing, Production). +This can be especially useful when working with critical systems like Live servers, where you want a clear visual reminder. + +To set a tab color: + +1. Select your connection in the Connections panel +2. In the Config panel, expand the **Display** category +3. Find the **Tab Color** property +4. Enter a color name (e.g., "Red", "Green", "Blue") or a hex color code (e.g., "#FF0000", "#00FF00") +5. Leave empty to use the default theme color + +The tab color will be applied when you open the connection. You can use inheritance to set the same color for multiple connections in a folder. + Status ------ From 5d623d80eb7eae3b28d7a2a9b68898281913f199 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 20:10:54 +0000 Subject: [PATCH 10/11] Initial plan From 347546ee0e580e843f4db520f72101900f1bb4d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 20:15:32 +0000 Subject: [PATCH 11/11] Add TabColorConverter to fix Color to String conversion issue Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com> --- .../Connection/AbstractConnectionRecord.cs | 2 +- mRemoteNG/Tools/MiscTools.cs | 126 +++++++++++++++ .../Tools/TabColorConverterTests.cs | 148 ++++++++++++++++++ 3 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 mRemoteNGTests/Tools/TabColorConverterTests.cs diff --git a/mRemoteNG/Connection/AbstractConnectionRecord.cs b/mRemoteNG/Connection/AbstractConnectionRecord.cs index c16b0ec6..90e1a3db 100644 --- a/mRemoteNG/Connection/AbstractConnectionRecord.cs +++ b/mRemoteNG/Connection/AbstractConnectionRecord.cs @@ -158,7 +158,7 @@ namespace mRemoteNG.Connection [LocalizedAttributes.LocalizedCategory(nameof(Language.Display)), LocalizedAttributes.LocalizedDisplayName(nameof(Language.TabColor)), LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionTabColor)), - TypeConverter(typeof(ColorConverter))] + TypeConverter(typeof(MiscTools.TabColorConverter))] public virtual string TabColor { get => GetPropertyValue("TabColor", _tabColor); diff --git a/mRemoteNG/Tools/MiscTools.cs b/mRemoteNG/Tools/MiscTools.cs index d35af82a..c1579691 100644 --- a/mRemoteNG/Tools/MiscTools.cs +++ b/mRemoteNG/Tools/MiscTools.cs @@ -268,5 +268,131 @@ namespace mRemoteNG.Tools return svc; } } + + public class TabColorConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(string) || sourceType == typeof(Color) || base.CanConvertFrom(context, sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + { + return destinationType == typeof(string) || destinationType == typeof(Color) || base.CanConvertTo(context, destinationType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value) + { + if (value == null || (value is string str && string.IsNullOrWhiteSpace(str))) + { + return string.Empty; + } + + if (value is string stringValue) + { + return stringValue; + } + + if (value is Color colorValue) + { + // Convert Color to string representation + // Use named color if it's a known color, otherwise use hex format + if (colorValue.IsNamedColor) + { + return colorValue.Name; + } + else + { + // Return hex format without alpha if fully opaque, otherwise include alpha + if (colorValue.A == 255) + { + return $"#{colorValue.R:X2}{colorValue.G:X2}{colorValue.B:X2}"; + } + else + { + return $"#{colorValue.A:X2}{colorValue.R:X2}{colorValue.G:X2}{colorValue.B:X2}"; + } + } + } + + return base.ConvertFrom(context, culture, value); + } + + public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (destinationType == typeof(string)) + { + if (value == null || (value is string str && string.IsNullOrWhiteSpace(str))) + { + return string.Empty; + } + return value.ToString() ?? string.Empty; + } + + if (destinationType == typeof(Color)) + { + if (value == null || (value is string str && string.IsNullOrWhiteSpace(str))) + { + return Color.Empty; + } + + if (value is string stringValue) + { + try + { + ColorConverter converter = new ColorConverter(); + return converter.ConvertFromString(stringValue) ?? Color.Empty; + } + catch + { + return Color.Empty; + } + } + } + + return base.ConvertTo(context, culture, value, destinationType) ?? throw new InvalidOperationException("Base conversion returned null."); + } + + public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) + { + return true; + } + + public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext? context) + { + // Provide a list of common colors for the dropdown + Color[] colors = + [ + Color.Red, + Color.Orange, + Color.Yellow, + Color.Green, + Color.Blue, + Color.Purple, + Color.Pink, + Color.Brown, + Color.Black, + Color.White, + Color.Gray, + Color.LightGray, + Color.DarkGray, + Color.Cyan, + Color.Magenta, + Color.Lime, + Color.Navy, + Color.Teal, + Color.Maroon, + Color.Olive + ]; + + return new StandardValuesCollection(colors); + } + + public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) + { + // Return false to allow custom values (hex codes or other color names) + return false; + } + } } } \ No newline at end of file diff --git a/mRemoteNGTests/Tools/TabColorConverterTests.cs b/mRemoteNGTests/Tools/TabColorConverterTests.cs new file mode 100644 index 00000000..36c089d5 --- /dev/null +++ b/mRemoteNGTests/Tools/TabColorConverterTests.cs @@ -0,0 +1,148 @@ +using System.Drawing; +using mRemoteNG.Tools; +using NUnit.Framework; + +namespace mRemoteNGTests.Tools +{ + public class TabColorConverterTests + { + private MiscTools.TabColorConverter _converter; + + [SetUp] + public void Setup() + { + _converter = new MiscTools.TabColorConverter(); + } + + [TestCase(typeof(string), true)] + [TestCase(typeof(Color), true)] + public void CanConvertFrom(Type typeToConvertFrom, bool expectedOutcome) + { + var actualOutcome = _converter.CanConvertFrom(typeToConvertFrom); + Assert.That(actualOutcome, Is.EqualTo(expectedOutcome)); + } + + [TestCase(typeof(string), true)] + [TestCase(typeof(Color), true)] + public void CanConvertTo(Type typeToConvertTo, bool expectedOutcome) + { + var actualOutcome = _converter.CanConvertTo(typeToConvertTo); + Assert.That(actualOutcome, Is.EqualTo(expectedOutcome)); + } + + [Test] + public void ConvertFromColorToStringNamedColor() + { + var color = Color.Red; + var result = _converter.ConvertFrom(color); + Assert.That(result, Is.EqualTo("Red")); + } + + [Test] + public void ConvertFromColorToStringCustomColor() + { + var color = Color.FromArgb(255, 128, 64, 32); + var result = _converter.ConvertFrom(color); + Assert.That(result, Is.EqualTo("#80401F")); + } + + [Test] + public void ConvertFromColorToStringCustomColorWithAlpha() + { + var color = Color.FromArgb(128, 255, 0, 0); + var result = _converter.ConvertFrom(color); + Assert.That(result, Is.EqualTo("#80FF0000")); + } + + [Test] + public void ConvertFromStringReturnsString() + { + var colorString = "Blue"; + var result = _converter.ConvertFrom(colorString); + Assert.That(result, Is.EqualTo("Blue")); + } + + [Test] + public void ConvertFromHexStringReturnsString() + { + var colorString = "#FF0000"; + var result = _converter.ConvertFrom(colorString); + Assert.That(result, Is.EqualTo("#FF0000")); + } + + [Test] + public void ConvertFromNullReturnsEmptyString() + { + var result = _converter.ConvertFrom(null); + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void ConvertFromEmptyStringReturnsEmptyString() + { + var result = _converter.ConvertFrom(""); + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void ConvertToStringFromString() + { + var colorString = "Green"; + var result = _converter.ConvertTo(colorString, typeof(string)); + Assert.That(result, Is.EqualTo("Green")); + } + + [Test] + public void ConvertToColorFromNamedString() + { + var colorString = "Red"; + var result = _converter.ConvertTo(colorString, typeof(Color)); + Assert.That(result, Is.EqualTo(Color.Red)); + } + + [Test] + public void ConvertToColorFromHexString() + { + var colorString = "#FF0000"; + var result = _converter.ConvertTo(colorString, typeof(Color)); + var expectedColor = Color.FromArgb(255, 255, 0, 0); + Assert.That(result, Is.EqualTo(expectedColor)); + } + + [Test] + public void ConvertToColorFromEmptyStringReturnsEmpty() + { + var result = _converter.ConvertTo("", typeof(Color)); + Assert.That(result, Is.EqualTo(Color.Empty)); + } + + [Test] + public void ConvertToColorFromNullReturnsEmpty() + { + var result = _converter.ConvertTo(null, typeof(Color)); + Assert.That(result, Is.EqualTo(Color.Empty)); + } + + [Test] + public void GetStandardValuesSupportedReturnsTrue() + { + var result = _converter.GetStandardValuesSupported(null); + Assert.That(result, Is.True); + } + + [Test] + public void GetStandardValuesReturnsColorList() + { + var result = _converter.GetStandardValues(null); + Assert.That(result, Is.Not.Null); + Assert.That(result.Count, Is.GreaterThan(0)); + } + + [Test] + public void GetStandardValuesExclusiveReturnsFalse() + { + var result = _converter.GetStandardValuesExclusive(null); + Assert.That(result, Is.False); + } + } +}