diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1de0838a0..e63bba606 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/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionNodeSerializer28.cs b/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionNodeSerializer28.cs
index 06bb61246..a12d8ed78 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 ab6c15a05..7f053ea1d 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/Config/Serializers/MiscSerializers/PortScanDeserializer.cs b/mRemoteNG/Config/Serializers/MiscSerializers/PortScanDeserializer.cs
index f80081b8e..4fc09ae24 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/Connection/AbstractConnectionRecord.cs b/mRemoteNG/Connection/AbstractConnectionRecord.cs
index 9a9fe488b..c354a553d 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;
@@ -23,6 +24,7 @@ namespace mRemoteNG.Connection
private string _icon;
private string _panel;
private string _color;
+ private string _tabColor;
private string _hostname;
private ExternalAddressProvider _externalAddressProvider;
@@ -163,6 +165,13 @@ namespace mRemoteNG.Connection
{
get => GetPropertyValue("Color", _color);
set => SetField(ref _color, value, "Color");
+ LocalizedAttributes.LocalizedDisplayName(nameof(Language.TabColor)),
+ LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionTabColor)),
+ TypeConverter(typeof(MiscTools.TabColorConverter))]
+ public virtual string TabColor
+ {
+ get => GetPropertyValue("TabColor", _tabColor);
+ set => SetField(ref _tabColor, value, "TabColor");
}
#endregion
@@ -920,7 +929,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
{
@@ -932,7 +941,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
{
@@ -944,7 +953,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
{
@@ -956,7 +965,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
{
@@ -967,7 +976,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
{
@@ -978,7 +987,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
{
@@ -989,7 +998,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
{
@@ -1001,7 +1010,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
{
@@ -1013,7 +1022,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
{
@@ -1025,7 +1034,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);
@@ -1036,7 +1045,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 56a2e8838..5e35c1229 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:
@@ -290,6 +293,7 @@ namespace mRemoteNG.Connection
Icon = Settings.Default.ConDefaultIcon;
Panel = Language.General;
Color = string.Empty;
+ TabColor = string.Empty;
}
private void SetConnectionDefaults()
diff --git a/mRemoteNG/Connection/ConnectionInfoInheritance.cs b/mRemoteNG/Connection/ConnectionInfoInheritance.cs
index fcf0a2d73..1e0a28485 100644
--- a/mRemoteNG/Connection/ConnectionInfoInheritance.cs
+++ b/mRemoteNG/Connection/ConnectionInfoInheritance.cs
@@ -55,6 +55,10 @@ namespace mRemoteNG.Connection
LocalizedAttributes.LocalizedDescriptionInherit(nameof(Language.PropertyDescriptionColor)),
TypeConverter(typeof(MiscTools.YesNoTypeConverter))]
public bool Color { get; set; }
+ LocalizedAttributes.LocalizedDisplayNameInherit(nameof(Language.TabColor)),
+ LocalizedAttributes.LocalizedDescriptionInherit(nameof(Language.PropertyDescriptionTabColor)),
+ TypeConverter(typeof(MiscTools.YesNoTypeConverter))]
+ public bool TabColor { get; set; }
#endregion
diff --git a/mRemoteNG/Connection/Protocol/ARD/ProtocolARD.cs b/mRemoteNG/Connection/Protocol/ARD/ProtocolARD.cs
new file mode 100644
index 000000000..d10a7ff9c
--- /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 820724f89..b6d7fbbad 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 f4e1bd47e..c795cd8e6 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 0a793b73c..fe2cd4579 100644
--- a/mRemoteNG/Language/Language.resx
+++ b/mRemoteNG/Language/Language.resx
@@ -159,6 +159,9 @@
Automatic update settings
+
+ ARD (Apple Remote Desktop)
+
Aspect
@@ -1022,6 +1025,8 @@ If you run into such an error, please create a new connection file!
Sets the color for the connection or folder in the connections tree. Connections inherit this color from their parent folder.
+
+ Sets the color of the connection tab. Leave empty for default theme color.
Enter your password.
@@ -1188,6 +1193,9 @@ If you run into such an error, please create a new connection file!
Panel
+
+ Tab Color
+
Password
diff --git a/mRemoteNG/Tools/MiscTools.cs b/mRemoteNG/Tools/MiscTools.cs
index d35af82a8..c1579691e 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/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs b/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs
index fb53dd2cb..241c19d8e 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/mRemoteNG/UI/Tabs/DockPaneStripNG.cs b/mRemoteNG/UI/Tabs/DockPaneStripNG.cs
index a149ebbfc..0360d64e6 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
diff --git a/mRemoteNG/UI/Window/ConnectionWindow.cs b/mRemoteNG/UI/Window/ConnectionWindow.cs
index 4c1c16917..506e88e37 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()));
}
diff --git a/mRemoteNGDocumentation/user_interface/connections.rst b/mRemoteNGDocumentation/user_interface/connections.rst
index 84aa31e41..763730ea2 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
------
diff --git a/mRemoteNGTests/Connection/ConnectionInfoTests.cs b/mRemoteNGTests/Connection/ConnectionInfoTests.cs
index 681f772d6..6f8eeffe4 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;
diff --git a/mRemoteNGTests/Tools/TabColorConverterTests.cs b/mRemoteNGTests/Tools/TabColorConverterTests.cs
new file mode 100644
index 000000000..36c089d5e
--- /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);
+ }
+ }
+}