Maverick - color your chart tabs (Indicator)

NinjaTrader 8 has no way to color chart tabs. Maverick adds it: apply one instance to each chart in a multi-tab window and pick a color per tab, so you can identify charts at a glance — ES yellow, NQ cyan, etc.

The color holds whether the tab is selected or not, survives renames and workspace save/restore, and leaves the tab’s X button intact. Zero runtime cost — it does all its work once at chart load, nothing while you trade.

Properties: Tab Color, Override Text Color, Text Color, Tab Font Size (for long tab names).

#region Using declarations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Xml.Serialization;
using NinjaTrader.Cbi;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Gui.SuperDom;
using NinjaTrader.Gui.Tools;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.Core.FloatingPoint;
using NinjaTrader.NinjaScript.DrawingTools;
#endregion

namespace NinjaTrader.NinjaScript.Indicators
{
    /// <summary>
    /// Colors this chart's tab header. Add one instance per tab and pick a color per tab.
    ///
    /// Why header-content wrap instead of TabItem.Background / Style / Template:
    ///  - Style setters lose to the skin's IsSelected triggers (color reverts when the tab is selected).
    ///  - Replacing the ControlTemplate removes the tab's X close button.
    ///  - Wrapping the Header CONTENT in a colored Border is untouched by the chrome triggers,
    ///    so the color holds in all states (selected / unselected / hover) and the X survives.
    /// NT's original header object is preserved inside the wrapper, so @INSTRUMENT-style
    /// tab-name updates continue to work. A HeaderProperty change hook re-wraps if NT
    /// replaces the header (rename, workspace restore ordering).
    /// </summary>
    public class Maverick : Indicator
    {
        private const string WrapperName = "MaverickWrapper";

        private Chart      chartWindow;
        private TabItem    tabItem;
        private object     originalHeader;
        private bool       isWrapping;   // reentrancy guard: our own Header writes must not re-trigger the hook
        private bool       subscribed;
        private System.Windows.Threading.Dispatcher uiDispatcher;   // captured at apply time; Terminated can fire with ChartControl null

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Description                 = @"Colors this chart's tab header. One instance per tab; pick a color per tab.";
                Name                        = "Maverick";
                Calculate                   = Calculate.OnBarClose;
                IsOverlay                   = true;
                IsChartOnly                 = true;
                DisplayInDataBox            = false;
                PaintPriceMarkers           = false;

                TabBrush                    = Brushes.Gold;
                ApplyTextBrush              = false;
                TextBrush                   = Brushes.Black;
                TabFontSize                 = 0;
            }
            else if (State == State.Historical)
            {
                // UI work must run on the chart's dispatcher (official WPF-modifications sample pattern)
                if (ChartControl == null)
                    return;
                ChartControl.Dispatcher.InvokeAsync((Action)(() => { FindTabAndApply(); }));
            }
            else if (State == State.Terminated)
            {
                if (ChartControl != null)
                {
                    // Chart survives (F5 reload, indicator removed) — restore the original header
                    ChartControl.Dispatcher.InvokeAsync((Action)(() => { RemoveAndRestore(); }));
                }
                else if (subscribed)
                {
                    // Tab/window closing — the TabItem dies with it, nothing visual to restore,
                    // but the DPD value-changed table strongly roots the TabItem AND this instance
                    // until unsubscribed. Release it, on the window dispatcher if still alive.
                    if (uiDispatcher != null && !uiDispatcher.HasShutdownStarted)
                        uiDispatcher.InvokeAsync((Action)(() => { ReleaseHeaderHook(); tabItem = null; }));
                    else
                    {
                        ReleaseHeaderHook();
                        tabItem = null;
                    }
                }
            }
        }

        #region Tab location / wrap / restore
        private void FindTabAndApply()
        {
            chartWindow = Window.GetWindow(ChartControl.Parent) as Chart;
            if (chartWindow == null)
                return;

            // Match this indicator's ChartControl to its owning tab (official sample pattern)
            foreach (TabItem tab in chartWindow.MainTabControl.Items)
                if ((tab.Content as ChartTab) != null && (tab.Content as ChartTab).ChartControl == ChartControl)
                {
                    tabItem = tab;
                    break;
                }

            if (tabItem == null)
                return;

            uiDispatcher = tabItem.Dispatcher;

            ApplyWrap();

            // Re-wrap if NT replaces the header out from under us (tab rename, restore ordering)
            DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(TabItem.HeaderProperty, typeof(TabItem));
            if (dpd != null)
            {
                dpd.AddValueChanged(tabItem, OnHeaderChanged);
                subscribed = true;
            }
        }

        private void OnHeaderChanged(object sender, EventArgs e)
        {
            if (isWrapping)
                return;
            ApplyWrap();
        }

        private void ApplyWrap()
        {
            if (tabItem == null)
                return;

            isWrapping = true;
            try
            {
                // Already ours (e.g. a second instance, or hook re-entry after restore) — recolor only
                Border existing = tabItem.Header as Border;
                if (existing != null && existing.Name == WrapperName)
                {
                    existing.Background = TabBrush;
                    if (ApplyTextBrush)
                        TextElement.SetForeground(existing, TextBrush);
                    if (TabFontSize > 0)
                        TextElement.SetFontSize(existing, TabFontSize);
                    return;
                }

                originalHeader = tabItem.Header;

                ContentPresenter presenter = new ContentPresenter();
                Border wrap = new Border
                {
                    Name         = WrapperName,
                    Background   = TabBrush,
                    CornerRadius = new CornerRadius(2),
                    Padding      = new Thickness(2, 0, 2, 0),   // minimal: horizontal padding is stolen from the width-constrained tab text
                    Child        = presenter
                };
                if (ApplyTextBrush)
                    TextElement.SetForeground(wrap, TextBrush);
                if (TabFontSize > 0)
                    TextElement.SetFontSize(wrap, TabFontSize);

                // Order matters: detach the original from the TabItem's logical tree first,
                // then attach it inside the wrapper — never two parents at once.
                tabItem.Header    = wrap;
                presenter.Content = originalHeader;
            }
            finally { isWrapping = false; }
        }

        private void RemoveAndRestore()
        {
            if (tabItem == null)
                return;

            ReleaseHeaderHook();

            Border existing = tabItem.Header as Border;
            if (existing != null && existing.Name == WrapperName)
            {
                isWrapping = true;
                try
                {
                    // Restore from the wrapper's live content — the true original even if
                    // another instance recolored this wrapper after we created it.
                    ContentPresenter presenter = existing.Child as ContentPresenter;
                    object orig = presenter != null ? presenter.Content : originalHeader;
                    if (presenter != null)
                        presenter.Content = null;    // detach before reattaching as Header
                    tabItem.Header = orig;
                }
                finally { isWrapping = false; }
            }

            tabItem        = null;
            originalHeader = null;
            chartWindow    = null;
            uiDispatcher   = null;
        }

        private void ReleaseHeaderHook()
        {
            if (!subscribed || tabItem == null)
            {
                subscribed = false;
                return;
            }
            try
            {
                DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(TabItem.HeaderProperty, typeof(TabItem));
                if (dpd != null)
                    dpd.RemoveValueChanged(tabItem, OnHeaderChanged);
            }
            catch { }   // best-effort on the shutdown fallback path; on-dispatcher this never throws
            subscribed = false;
        }
        #endregion

        #region Properties
        private Brush tabBrush;
        [XmlIgnore]
        [Display(Name = "Tab Color", Description = "Background color applied to this chart's tab header", Order = 1, GroupName = "Parameters")]
        public Brush TabBrush
        {
            get { return tabBrush; }
            set { tabBrush = value; }
        }
        [Browsable(false)]
        public string TabBrushSerialize
        {
            get { return Serialize.BrushToString(tabBrush); }
            set { tabBrush = Serialize.StringToBrush(value); }
        }

        [NinjaScriptProperty]
        [Display(Name = "Override Text Color", Description = "Also force the tab text color (enable if the tab text is unreadable on the chosen fill)", Order = 2, GroupName = "Parameters")]
        public bool ApplyTextBrush
        { get; set; }

        private Brush textBrush;
        [XmlIgnore]
        [Display(Name = "Text Color", Description = "Tab text color when Override Text Color is enabled", Order = 3, GroupName = "Parameters")]
        public Brush TextBrush
        {
            get { return textBrush; }
            set { textBrush = value; }
        }
        [Browsable(false)]
        public string TextBrushSerialize
        {
            get { return Serialize.BrushToString(textBrush); }
            set { textBrush = Serialize.StringToBrush(value); }
        }

        [NinjaScriptProperty]
        [Range(0, 72)]
        [Display(Name = "Tab Font Size", Description = "Tab text size in points; 0 = platform default. Reduce if a long tab name gets trimmed.", Order = 4, GroupName = "Parameters")]
        public double TabFontSize
        { get; set; }
        #endregion
    }
}
3 Likes

Clever! This gives me an idea to change color of chart trader background to distinguish between minis and micros.

1 Like

Cool, that’d be nice.

I updated the code so that you can now color the exterior, the benefit of that is that you can have a full square flush coloration and you can see the difference in the example of the first picture and the second

#region Using declarations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Xml.Serialization;
using NinjaTrader.Cbi;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Gui.SuperDom;
using NinjaTrader.Gui.Tools;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.Core.FloatingPoint;
using NinjaTrader.NinjaScript.DrawingTools;
#endregion

namespace NinjaTrader.NinjaScript.Indicators
{
    /// <summary>
    /// Colors this chart's tab header. Add one instance per tab and pick a color per tab.
    ///
    /// Why header-content wrap instead of TabItem.Background / Style / Template:
    ///  - Style setters lose to the skin's IsSelected triggers (color reverts when the tab is selected).
    ///  - Replacing the ControlTemplate removes the tab's X close button.
    ///  - Wrapping the Header CONTENT in a colored Border is untouched by the chrome triggers,
    ///    so the color holds in all states (selected / unselected / hover) and the X survives.
    /// NT's original header object is preserved inside the wrapper, so @INSTRUMENT-style
    /// tab-name updates continue to work. A HeaderProperty change hook re-wraps if NT
    /// replaces the header (rename, workspace restore ordering).
    /// </summary>
    public class Maverick : Indicator
    {
        private const string WrapperName = "MaverickWrapper";

        private Chart      chartWindow;
        private TabItem    tabItem;
        private object     originalHeader;
        private bool       isWrapping;   // reentrancy guard: our own Header writes must not re-trigger the hook
        private bool       subscribed;
        private System.Windows.Threading.Dispatcher uiDispatcher;   // captured at apply time; Terminated can fire with ChartControl null
        private readonly List<FrameworkElement> paintedChrome = new List<FrameworkElement>();   // template chrome elements we painted, for exact restore

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Description                 = @"Colors this chart's tab header. One instance per tab; pick a color per tab.";
                Name                        = "Maverick";
                Calculate                   = Calculate.OnBarClose;
                IsOverlay                   = true;
                IsChartOnly                 = true;
                DisplayInDataBox            = false;
                PaintPriceMarkers           = false;

                TabBrush                    = Brushes.Gold;
                ApplyTextBrush              = false;
                TextBrush                   = Brushes.Black;
                TabFontSize                 = 0;
                ApplyChromeColor            = false;
                ChromeColor                 = Brushes.DimGray;
            }
            else if (State == State.Historical)
            {
                // UI work must run on the chart's dispatcher (official WPF-modifications sample pattern)
                if (ChartControl == null)
                    return;
                ChartControl.Dispatcher.InvokeAsync((Action)(() => { FindTabAndApply(); }));
            }
            else if (State == State.Terminated)
            {
                if (ChartControl != null)
                {
                    // Chart survives (F5 reload, indicator removed) — restore the original header
                    ChartControl.Dispatcher.InvokeAsync((Action)(() => { RemoveAndRestore(); }));
                }
                else if (subscribed)
                {
                    // Tab/window closing — the TabItem dies with it, nothing visual to restore,
                    // but the DPD value-changed table strongly roots the TabItem AND this instance
                    // until unsubscribed. Release it, on the window dispatcher if still alive.
                    if (uiDispatcher != null && !uiDispatcher.HasShutdownStarted)
                        uiDispatcher.InvokeAsync((Action)(() => { ReleaseHeaderHook(); tabItem = null; }));
                    else
                    {
                        ReleaseHeaderHook();
                        tabItem = null;
                    }
                }
            }
        }

        #region Tab location / wrap / restore
        private void FindTabAndApply()
        {
            chartWindow = Window.GetWindow(ChartControl.Parent) as Chart;
            if (chartWindow == null)
                return;

            // Match this indicator's ChartControl to its owning tab (official sample pattern)
            foreach (TabItem tab in chartWindow.MainTabControl.Items)
                if ((tab.Content as ChartTab) != null && (tab.Content as ChartTab).ChartControl == ChartControl)
                {
                    tabItem = tab;
                    break;
                }

            if (tabItem == null)
                return;

            uiDispatcher = tabItem.Dispatcher;

            ApplyWrap();

            // Re-wrap if NT replaces the header out from under us (tab rename, restore ordering)
            DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(TabItem.HeaderProperty, typeof(TabItem));
            if (dpd != null)
            {
                dpd.AddValueChanged(tabItem, OnHeaderChanged);
                subscribed = true;
            }
        }

        private void OnHeaderChanged(object sender, EventArgs e)
        {
            if (isWrapping)
                return;
            ApplyWrap();
        }

        private void OnWrapperLoaded(object sender, RoutedEventArgs e)
        {
            if (!ApplyChromeColor || ChromeColor == null || tabItem == null)
                return;

            // The tab chrome (the black around the pill) is painted by NT's internal template,
            // which consumes frozen skin brushes (e.g. BluePrint.xaml's "TabBackground") as
            // template-applied values. Local values outrank template-applied values AND the
            // template's selection triggers — so painting the chrome elements directly holds
            // in all tab states. Those elements are exactly our wrapper's visual ancestors.
            DependencyObject node = VisualTreeHelper.GetParent((DependencyObject)sender);
            while (node != null && !ReferenceEquals(node, tabItem))
            {
                Border b = node as Border;
                Panel  p = node as Panel;
                if (b != null)
                    b.Background = ChromeColor;
                else if (p != null)
                    p.Background = ChromeColor;

                FrameworkElement fe = node as FrameworkElement;
                if ((b != null || p != null) && fe != null && !paintedChrome.Contains(fe))
                    paintedChrome.Add(fe);

                node = VisualTreeHelper.GetParent(node);
            }

            // Re-assert the TabItem-level value: Loaded fires after template application,
            // so this lands after anything NT assigned during tab construction.
            tabItem.Background = ChromeColor;
        }

        private void ApplyWrap()
        {
            if (tabItem == null)
                return;

            // Chrome, TabItem level: covers the case where the template binds its chrome
            // to TabItem.Background. The template's own elements are painted in
            // OnWrapperLoaded once the visual tree exists. 
            if (ApplyChromeColor && ChromeColor != null)
                tabItem.Background = ChromeColor;

            isWrapping = true;
            try
            {
                // Already ours (e.g. a second instance, or hook re-entry after restore) — recolor only
                Border existing = tabItem.Header as Border;
                if (existing != null && existing.Name == WrapperName)
                {
                    existing.Background = TabBrush;
                    if (ApplyTextBrush)
                        TextElement.SetForeground(existing, TextBrush);
                    if (TabFontSize > 0)
                        TextElement.SetFontSize(existing, TabFontSize);
                    return;
                }

                originalHeader = tabItem.Header;

                ContentPresenter presenter = new ContentPresenter();
                Border wrap = new Border
                {
                    Name         = WrapperName,
                    Background   = TabBrush,
                    CornerRadius = new CornerRadius(2),
                    Padding      = new Thickness(2, 0, 2, 0),   // minimal: horizontal padding is stolen from the width-constrained tab text
                    Child        = presenter
                };
                if (ApplyTextBrush)
                    TextElement.SetForeground(wrap, TextBrush);
                if (TabFontSize > 0)
                    TextElement.SetFontSize(wrap, TabFontSize);

                // The chrome elements are only walkable once the wrapper is in the
                // visual tree, so chrome painting runs on Loaded.
                wrap.Loaded += OnWrapperLoaded;

                // Order matters: detach the original from the TabItem's logical tree first,
                // then attach it inside the wrapper — never two parents at once.
                tabItem.Header    = wrap;
                presenter.Content = originalHeader;
            }
            finally { isWrapping = false; }
        }

        private void RemoveAndRestore()
        {
            if (tabItem == null)
                return;

            ReleaseHeaderHook();

            Border existing = tabItem.Header as Border;
            if (existing != null && existing.Name == WrapperName)
            {
                isWrapping = true;
                try
                {
                    // Restore from the wrapper's live content — the true original even if
                    // another instance recolored this wrapper after we created it.
                    ContentPresenter presenter = existing.Child as ContentPresenter;
                    object orig = presenter != null ? presenter.Content : originalHeader;
                    if (presenter != null)
                        presenter.Content = null;    // detach before reattaching as Header
                    tabItem.Header = orig;
                }
                finally { isWrapping = false; }
            }

            // Return chrome control to NT's skin (no-op if ApplyChromeColor was never enabled)
            tabItem.ClearValue(System.Windows.Controls.Control.BackgroundProperty);
            foreach (FrameworkElement fe in paintedChrome)
            {
                Border b = fe as Border;
                if (b != null)
                {
                    b.ClearValue(Border.BackgroundProperty);
                    continue;
                }
                Panel p = fe as Panel;
                if (p != null)
                    p.ClearValue(System.Windows.Controls.Panel.BackgroundProperty);
            }
            paintedChrome.Clear();

            tabItem        = null;
            originalHeader = null;
            chartWindow    = null;
            uiDispatcher   = null;
        }

        private void ReleaseHeaderHook()
        {
            if (!subscribed || tabItem == null)
            {
                subscribed = false;
                return;
            }
            try
            {
                DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(TabItem.HeaderProperty, typeof(TabItem));
                if (dpd != null)
                    dpd.RemoveValueChanged(tabItem, OnHeaderChanged);
            }
            catch { }   // best-effort on the shutdown fallback path; on-dispatcher this never throws
            subscribed = false;
        }
        #endregion

        #region Properties
        private Brush tabBrush;
        [XmlIgnore]
        [Display(Name = "Tab Color", Description = "Background color applied to this chart's tab header", Order = 1, GroupName = "Parameters")]
        public Brush TabBrush
        {
            get { return tabBrush; }
            set { tabBrush = value; }
        }
        [Browsable(false)]
        public string TabBrushSerialize
        {
            get { return Serialize.BrushToString(tabBrush); }
            set { tabBrush = Serialize.StringToBrush(value); }
        }

        [NinjaScriptProperty]
        [Display(Name = "Override Text Color", Description = "Also force the tab text color (enable if the tab text is unreadable on the chosen fill)", Order = 2, GroupName = "Parameters")]
        public bool ApplyTextBrush
        { get; set; }

        private Brush textBrush;
        [XmlIgnore]
        [Display(Name = "Text Color", Description = "Tab text color when Override Text Color is enabled", Order = 3, GroupName = "Parameters")]
        public Brush TextBrush
        {
            get { return textBrush; }
            set { textBrush = value; }
        }
        [Browsable(false)]
        public string TextBrushSerialize
        {
            get { return Serialize.BrushToString(textBrush); }
            set { textBrush = Serialize.StringToBrush(value); }
        }

        [NinjaScriptProperty]
        [Range(0, 72)]
        [Display(Name = "Tab Font Size", Description = "Tab text size in points; 0 = platform default. Reduce if a long tab name gets trimmed.", Order = 4, GroupName = "Parameters")]
        public double TabFontSize
        { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Color Tab Chrome", Description = "Also paint the tab's surrounding chrome (the dark tab body around the label), not just the label area", Order = 5, GroupName = "Parameters")]
        public bool ApplyChromeColor
        { get; set; }

        private Brush chromeColor;
        [XmlIgnore]
        [Display(Name = "Chrome Color", Description = "Color for the tab chrome when Color Tab Chrome is enabled", Order = 6, GroupName = "Parameters")]
        public Brush ChromeColor
        {
            get { return chromeColor; }
            set { chromeColor = value; }
        }
        [Browsable(false)]
        public string ChromeColorSerialize
        {
            get { return Serialize.BrushToString(chromeColor); }
            set { chromeColor = Serialize.StringToBrush(value); }
        }
        #endregion
    }
}

Hello Maverick. Very grateful for this tab colouring indicator.

Don’t know if its just me but once loaded onto a chart I can’t change/edit the text I originally had for the tab. Using NT version 8.1.7.

For example lets say I labelled a tab ES. I then load the tab coloring indicator but then I can’t change the tab label text (ES) or even delete the text. If I try to rename via right click chart tab rename and change the tab name it changes in the properties box but not on the chart tab.

This happens even if there’s only one tab or more than one tab on the chart. I can’t edit any of the tab names once the indicator is loaded or if I delete the indicator.

Again my thanks

Paste what you just said into Claude and it will fix it.

@Maverick Thanks for your reply. Unfortunately I’ve not been able to do what you suggested.

If any one can post a fix that would be most welcome.

many thanks

#region Using declarations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Xml.Serialization;
using NinjaTrader.Cbi;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Gui.SuperDom;
using NinjaTrader.Gui.Tools;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.Core.FloatingPoint;
using NinjaTrader.NinjaScript.DrawingTools;
#endregion

namespace NinjaTrader.NinjaScript.Indicators
{
    /// <summary>
    /// Colors this chart's tab. One instance per tab; pick a color per tab.
    ///
    /// CONTRACT — TabItem.Header is never written (sole bounded exception:
    /// HealLegacyWrapper). A local Header write permanently destroys NT's name-update
    /// binding (WPF: local value replaces a binding; ClearValue does NOT restore it) —
    /// that was the original rename-stops-working bug. Coloring instead paints the
    /// template's own Border/Panel elements: local values outrank template-applied
    /// values AND IsSelected triggers, so color holds in all states and the X survives.
    ///
    /// TEXT is written as CURRENT values only (SetCurrentValue): skins choreograph the
    /// label's selected/unselected layers via triggers, and a local text value defeats
    /// the hide-trigger — the doubled/ghosted tab-text bug. Current values yield to
    /// triggers; OnSelectionChanged re-asserts after each flip; invisible ghost layers
    /// are never touched. Restore never calls ClearValue on this path — intact sources
    /// (bindings, DynamicResources) are re-evaluated via InvalidateProperty.
    ///
    /// OWNERSHIP — a static per-TabItem registry captures each property's original
    /// state once and stacks owners; last owner out restores, earlier removals repaint
    /// the survivor. Statics don't cross NinjaScript generations: on F5 the old
    /// generation's queued restore precedes the new apply (same dispatcher, FIFO).
    /// Residual cross-generation race worst case: a captured color, never a destroyed
    /// binding.
    /// </summary>
    public class Maverick : Indicator
    {
        private const string WrapperName = "MaverickWrapper";   // legacy wrapper from pre-fix builds

        private static readonly object RegistryLock = new object();
        private static readonly Dictionary<TabItem, TabState> Registry = new Dictionary<TabItem, TabState>();

        /// <summary>Per-TabItem originals + owner stack. Saved touched only on that tab's
        /// dispatcher; dictionary lock-guarded (NT chart windows can live on different UI threads).</summary>
        private sealed class TabState
        {
            public readonly List<SavedValue> Saved  = new List<SavedValue>();
            public readonly List<Maverick>   Owners = new List<Maverick>();
        }

        private sealed class SavedValue
        {
            public DependencyObject                Target;
            public DependencyProperty              Property;
            public System.Windows.Data.BindingBase Binding;      // non-null if a binding held this property
            public object                          LocalValue;   // literal local value, if any
            public bool                            HasLocal;
            public bool                            CurrentOnly;  // written via SetCurrentValue — no local value created
        }

        private Chart          chartWindow;
        private TabItem        tabItem;
        private TabState       tabState;
        private bool           subscribed;
        private bool           loadedHooked;
        private bool           shutdownHooked;
        private bool           selectionHooked;
        private volatile bool  terminated;      // set FIRST in Terminated; gates every queued callback
        private System.Windows.Threading.Dispatcher uiDispatcher;

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Description       = @"Colors this chart's tab. One instance per tab; pick a color per tab.";
                Name              = "Maverick";
                Calculate         = Calculate.OnBarClose;
                IsOverlay         = true;
                IsChartOnly       = true;
                DisplayInDataBox  = false;
                PaintPriceMarkers = false;

                TabBrush          = Brushes.Gold;
                ApplyTextBrush    = false;
                TextBrush         = Brushes.Black;
                TabFontSize       = 0;
                ApplyChromeColor  = false;
            }
            else if (State == State.Historical)
            {
                if (ChartControl == null)
                    return;
                ChartControl.Dispatcher.InvokeAsync((Action)(() => { FindTabAndApply(); }));
            }
            else if (State == State.Terminated)
            {
                terminated = true;

                System.Windows.Threading.Dispatcher d = ChartControl != null ? ChartControl.Dispatcher : uiDispatcher;
                if (d != null && !d.HasShutdownStarted)
                    d.InvokeAsync((Action)(() => { TearDownOnUiThread(); }));
                else
                {
                    // Dispatcher gone/shutting down: queued items would be aborted, and WPF
                    // objects must not be touched from this thread. OnDispatcherShutdown
                    // (raised ON that thread before the queue purge) carried the cleanup.
                    tabItem = null; tabState = null; chartWindow = null; uiDispatcher = null;
                }
            }
        }

        #region Locate / lifecycle
        private void FindTabAndApply()
        {
            if (terminated)
                return;

            ChartControl cc = ChartControl;
            if (cc == null || cc.Parent == null)
                return;

            chartWindow = Window.GetWindow(cc.Parent) as Chart;
            if (chartWindow == null)
                return;

            foreach (TabItem tab in chartWindow.MainTabControl.Items)
                if ((tab.Content as ChartTab) != null && (tab.Content as ChartTab).ChartControl == cc)
                {
                    tabItem = tab;
                    break;
                }

            if (tabItem == null)
                return;

            uiDispatcher = tabItem.Dispatcher;

            lock (RegistryLock)
            {
                TabState st;
                if (!Registry.TryGetValue(tabItem, out st))
                {
                    st = new TabState();
                    Registry[tabItem] = st;
                }
                st.Owners.Add(this);
                tabState = st;
            }

            HealLegacyWrapper();
            Apply();

            // Observation-only hooks (never write Header): re-apply on tree rebuild,
            // header swap (rename), and selection flips (current-value text re-assert).
            tabItem.Loaded += OnTabLoaded;
            loadedHooked = true;

            DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(TabItem.HeaderProperty, typeof(TabItem));
            if (dpd != null)
            {
                dpd.AddValueChanged(tabItem, OnHeaderChanged);
                subscribed = true;
            }

            DependencyPropertyDescriptor sel = DependencyPropertyDescriptor.FromProperty(TabItem.IsSelectedProperty, typeof(TabItem));
            if (sel != null)
            {
                sel.AddValueChanged(tabItem, OnSelectionChanged);
                selectionHooked = true;
            }

            // ShutdownStarted fires on THIS dispatcher's thread before pending items are
            // aborted — last reliable moment to release the DPD subscriptions (which
            // strongly root the TabItem and this instance) when a chart window closes.
            uiDispatcher.ShutdownStarted += OnDispatcherShutdown;
            shutdownHooked = true;

            // Terminated may have fired mid-run; we are on the UI thread — tear down, don't leak.
            if (terminated)
                TearDownOnUiThread();
        }

        /// <summary>THE one bounded Header write: lifts the real header out of a wrapper
        /// left by pre-fix builds. Guarded — a malformed wrapper is left alone rather than
        /// risking a null header. Cannot resurrect a binding the old build destroyed
        /// (that needs a workspace reload).</summary>
        private void HealLegacyWrapper()
        {
            if (tabItem == null)
                return;

            Border legacy = tabItem.Header as Border;
            if (legacy == null || legacy.Name != WrapperName)
                return;

            ContentPresenter presenter = legacy.Child as ContentPresenter;
            if (presenter == null || presenter.Content == null)
                return;

            object inner = presenter.Content;
            presenter.Content = null;
            tabItem.Header = inner;
        }

        private void OnTabLoaded(object sender, RoutedEventArgs e)        { if (!terminated) Apply(); }
        private void OnHeaderChanged(object sender, EventArgs e)          { if (!terminated) Apply(); }
        private void OnSelectionChanged(object sender, EventArgs e)       { if (!terminated) Apply(); }

        /// <summary>On the dispatcher's own thread as it begins shutdown (window closing):
        /// release hooks + registry while still legal. No visual restore — the tab dies.</summary>
        private void OnDispatcherShutdown(object sender, EventArgs e)
        {
            ReleaseHooks();
            UnregisterOwner(false);
            tabItem = null; tabState = null; chartWindow = null;
        }

        private void TearDownOnUiThread()
        {
            ReleaseHooks();
            UnregisterOwner(true);
            tabItem = null; tabState = null; chartWindow = null; uiDispatcher = null;
        }

        /// <summary>Last owner out restores platform state; a surviving owner is repainted,
        /// so removal order no longer decides the final color.</summary>
        private void UnregisterOwner(bool restoreVisuals)
        {
            if (tabState == null || tabItem == null)
                return;

            Maverick survivor = null;
            bool last = false;

            lock (RegistryLock)
            {
                tabState.Owners.Remove(this);
                if (tabState.Owners.Count == 0)
                {
                    last = true;
                    Registry.Remove(tabItem);
                }
                else
                    survivor = tabState.Owners[tabState.Owners.Count - 1];
            }

            if (last)
            {
                if (restoreVisuals)
                    RestoreTracked(tabState.Saved);
                else
                    tabState.Saved.Clear();
            }
            else if (survivor != null)
                survivor.Apply();   // same tab, same dispatcher
        }

        private void ReleaseHooks()
        {
            if (tabItem == null && uiDispatcher == null)
            {
                subscribed = false; loadedHooked = false; shutdownHooked = false; selectionHooked = false;
                return;
            }

            try
            {
                if (loadedHooked && tabItem != null)
                    tabItem.Loaded -= OnTabLoaded;

                if (subscribed && tabItem != null)
                {
                    DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(TabItem.HeaderProperty, typeof(TabItem));
                    if (dpd != null)
                        dpd.RemoveValueChanged(tabItem, OnHeaderChanged);
                }

                if (selectionHooked && tabItem != null)
                {
                    DependencyPropertyDescriptor sel = DependencyPropertyDescriptor.FromProperty(TabItem.IsSelectedProperty, typeof(TabItem));
                    if (sel != null)
                        sel.RemoveValueChanged(tabItem, OnSelectionChanged);
                }

                if (shutdownHooked && uiDispatcher != null)
                    uiDispatcher.ShutdownStarted -= OnDispatcherShutdown;
            }
            catch { }   // call sites are on the owning thread; belt-and-braces only

            subscribed = false; loadedHooked = false; shutdownHooked = false; selectionHooked = false;
        }
        #endregion

        #region Painting
        /// <summary>Idempotent; capture-once makes repeated calls safe.</summary>
        private void Apply()
        {
            if (terminated || tabItem == null || tabState == null)
                return;

            PruneDetached();

            List<DependencyObject> hosts = FindHeaderHosts();
            if (hosts.Count == 0)
                return;    // fail closed — unrecognized template; OnTabLoaded retries

            // Multiple presenters can share one header (state duplicates); chain from the
            // first is authoritative, text styling covers all.
            DependencyObject host = hosts[0];

            List<DependencyObject> chain = new List<DependencyObject>();
            DependencyObject node = VisualTreeHelper.GetParent(host);
            while (node != null && !ReferenceEquals(node, tabItem))
            {
                if (node is Border || node is Panel)
                    chain.Add(node);
                node = VisualTreeHelper.GetParent(node);
            }

            // [0] = label area, always TabBrush; outer layers get TabBrush too when
            // Color Tab Chrome is on (flush square vs pill).
            for (int i = 0; i < chain.Count; i++)
            {
                if (i > 0 && !ApplyChromeColor)
                    continue;
                if (TabBrush != null)
                    SetTracked(chain[i], BackgroundPropertyOf(chain[i]), TabBrush);
            }

            if (TabBrush != null && (chain.Count == 0 || ApplyChromeColor))
                SetTracked(tabItem, System.Windows.Controls.Control.BackgroundProperty, TabBrush);

            ApplyTextStyling(hosts);
        }

        /// <summary>All text writes are CURRENT values (see class contract); ghost layers
        /// (collapsed / ~zero opacity / transparent foreground) are template state
        /// machinery and never touched.</summary>
        private void ApplyTextStyling(List<DependencyObject> hosts)
        {
            bool wantFore = ApplyTextBrush && TextBrush != null;
            bool wantSize = TabFontSize > 0;
            if (!wantFore && !wantSize)
                return;

            if (wantFore)
            {
                SetTracked(tabItem, TextElement.ForegroundProperty, TextBrush, true);
                SetTracked(tabItem, System.Windows.Controls.Control.ForegroundProperty, TextBrush, true);
            }
            if (wantSize)
            {
                SetTracked(tabItem, TextElement.FontSizeProperty, TabFontSize, true);
                SetTracked(tabItem, System.Windows.Controls.Control.FontSizeProperty, TabFontSize, true);
            }

            List<DependencyObject> labels = new List<DependencyObject>();
            for (int h = 0; h < hosts.Count; h++)
                CollectDescendants(hosts[h], typeof(TextBlock), labels, 0);

            for (int i = 0; i < labels.Count; i++)
            {
                TextBlock tb = labels[i] as TextBlock;
                if (tb == null || IsGhostText(tb))
                    continue;
                if (wantFore)
                    SetTracked(tb, TextBlock.ForegroundProperty, TextBrush, true);
                if (wantSize)
                    SetTracked(tb, TextBlock.FontSizeProperty, TabFontSize, true);
            }
        }

        private static bool IsGhostText(TextBlock tb)
        {
            if (tb.Visibility != Visibility.Visible)
                return true;
            if (tb.Opacity <= 0.01)
                return true;
            SolidColorBrush scb = tb.Foreground as SolidColorBrush;
            return scb != null && scb.Color.A == 0;
        }
        #endregion

        #region Tracked writes — capture before overwrite, restore exactly
        /// <summary>Records prior state (once per target+property, shared across owners)
        /// then writes. currentOnly = SetCurrentValue: no local value created, triggers
        /// keep working, bindings/expressions untouched. Local path: a property carrying
        /// an unrestorable expression (e.g. DynamicResource) is never written — fail
        /// closed, a missing color beats a destroyed source.</summary>
        private void SetTracked(DependencyObject target, DependencyProperty dp, object value, bool currentOnly = false)
        {
            if (target == null || dp == null || value == null || tabState == null)
                return;

            if (!IsTracked(target, dp))
            {
                SavedValue sv = new SavedValue();
                sv.Target   = target;
                sv.Property = dp;
                sv.Binding  = System.Windows.Data.BindingOperations.GetBindingBase(target, dp);

                object lv     = target.ReadLocalValue(dp);
                bool   isExpr = lv is System.Windows.Expression;

                if (isExpr && sv.Binding == null && !currentOnly)
                    return;

                sv.HasLocal    = lv != DependencyProperty.UnsetValue && !isExpr;
                sv.LocalValue  = sv.HasLocal ? lv : null;
                sv.CurrentOnly = currentOnly;

                tabState.Saved.Add(sv);
            }

            if (currentOnly)
                target.SetCurrentValue(dp, value);
            else
                target.SetValue(dp, value);
        }

        private bool IsTracked(DependencyObject target, DependencyProperty dp)
        {
            List<SavedValue> saved = tabState.Saved;
            for (int i = 0; i < saved.Count; i++)
                if (ReferenceEquals(saved[i].Target, target) && saved[i].Property == dp)
                    return true;
            return false;
        }

        private void RestoreTracked(List<SavedValue> saved)
        {
            for (int i = saved.Count - 1; i >= 0; i--)
            {
                SavedValue sv = saved[i];
                if (sv.Target == null || sv.Property == null)
                    continue;

                try
                {
                    if (sv.CurrentOnly)
                    {
                        // ClearValue is BANNED here: on a property whose local slot holds a
                        // binding or dynamic-resource expression it destroys that source.
                        // Literal local → exact re-assert; every other source is intact →
                        // re-evaluate it over our lingering current value.
                        if (sv.HasLocal)
                            sv.Target.SetValue(sv.Property, sv.LocalValue);
                        else
                            sv.Target.InvalidateProperty(sv.Property);
                    }
                    else if (sv.Binding != null)
                        System.Windows.Data.BindingOperations.SetBinding(sv.Target, sv.Property, sv.Binding);
                    else if (sv.HasLocal)
                        sv.Target.SetValue(sv.Property, sv.LocalValue);
                    else
                        sv.Target.ClearValue(sv.Property);
                }
                catch { }
            }
            saved.Clear();
        }

        /// <summary>Drops entries whose target left the tab's visual tree (NT rebuilt the
        /// header): no longer displayed, nothing to restore, holding them roots dead UI.</summary>
        private void PruneDetached()
        {
            if (tabState == null || tabItem == null)
                return;

            List<SavedValue> saved = tabState.Saved;
            for (int i = saved.Count - 1; i >= 0; i--)
            {
                DependencyObject t = saved[i].Target;
                if (t == null) { saved.RemoveAt(i); continue; }
                if (ReferenceEquals(t, tabItem))
                    continue;

                bool attached = false;
                DependencyObject node = t;
                for (int hop = 0; node != null && hop < 64; hop++)
                {
                    if (ReferenceEquals(node, tabItem)) { attached = true; break; }
                    node = VisualTreeHelper.GetParent(node);
                }
                if (!attached)
                    saved.RemoveAt(i);
            }
        }
        #endregion

        #region Visual tree
        /// <summary>All header hosts: identity matches first (a string Header can be the
        /// Content of several state-duplicate presenters), then TemplatedParent matches.
        /// NO blind fallbacks — guessing risks painting the close button. Empty = decline.</summary>
        private List<DependencyObject> FindHeaderHosts()
        {
            List<DependencyObject> hosts = new List<DependencyObject>();
            if (tabItem == null)
                return hosts;

            List<DependencyObject> presenters = new List<DependencyObject>();
            CollectDescendants(tabItem, typeof(ContentPresenter), presenters, 0);

            object hdr = tabItem.Header;

            if (hdr != null)
                for (int i = 0; i < presenters.Count; i++)
                {
                    ContentPresenter cp = presenters[i] as ContentPresenter;
                    if (cp != null && ReferenceEquals(cp.Content, hdr))
                        hosts.Add(cp);
                }

            for (int i = 0; i < presenters.Count; i++)
            {
                ContentPresenter cp = presenters[i] as ContentPresenter;
                if (cp != null && ReferenceEquals(cp.TemplatedParent, tabItem) && !hosts.Contains(cp))
                    hosts.Add(cp);
            }

            return hosts;
        }

        private static void CollectDescendants(DependencyObject root, Type type, List<DependencyObject> results, int depth)
        {
            if (root == null || depth > 24)
                return;

            int count = VisualTreeHelper.GetChildrenCount(root);
            for (int i = 0; i < count; i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(root, i);
                if (child == null)
                    continue;
                if (type.IsInstanceOfType(child))
                    results.Add(child);
                CollectDescendants(child, type, results, depth + 1);
            }
        }

        private static DependencyProperty BackgroundPropertyOf(DependencyObject o)
        {
            if (o is Border)
                return Border.BackgroundProperty;
            if (o is Panel)
                return System.Windows.Controls.Panel.BackgroundProperty;
            return System.Windows.Controls.Control.BackgroundProperty;
        }
        #endregion

        #region Properties
        private Brush tabBrush;
        [XmlIgnore]
        [Display(Name = "Tab Color", Description = "Color applied to this chart's tab", Order = 1, GroupName = "Parameters")]
        public Brush TabBrush
        {
            get { return tabBrush; }
            set { tabBrush = value; }
        }
        [Browsable(false)]
        public string TabBrushSerialize
        {
            get { return Serialize.BrushToString(tabBrush); }
            set { tabBrush = Serialize.StringToBrush(value); }
        }

        [NinjaScriptProperty]
        [Display(Name = "Override Text Color", Description = "Also force the tab text color (enable if the tab text is unreadable on the chosen fill)", Order = 2, GroupName = "Parameters")]
        public bool ApplyTextBrush
        { get; set; }

        private Brush textBrush;
        [XmlIgnore]
        [Display(Name = "Text Color", Description = "Tab text color when Override Text Color is enabled", Order = 3, GroupName = "Parameters")]
        public Brush TextBrush
        {
            get { return textBrush; }
            set { textBrush = value; }
        }
        [Browsable(false)]
        public string TextBrushSerialize
        {
            get { return Serialize.BrushToString(textBrush); }
            set { textBrush = Serialize.StringToBrush(value); }
        }

        [NinjaScriptProperty]
        [Range(0, 72)]
        [Display(Name = "Tab Font Size", Description = "Tab text size in points; 0 = platform default. Reduce if a long tab name gets trimmed.", Order = 4, GroupName = "Parameters")]
        public double TabFontSize
        { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Color Tab Chrome", Description = "Extend Tab Color to the tab's surrounding body (flush square look) instead of just the label area", Order = 5, GroupName = "Parameters")]
        public bool ApplyChromeColor
        { get; set; }
        #endregion
    }
}

Heres the fix. You should be good to go now.

@Maverick :+1: :+1: Many thanks, very kind of you…

I have been trying to change the tab colors dynamically based on a condition. Do you know if this can be done.