preview
MQL5 Trading Tools (Part 37): Adding a Per-Object Property-Editing Ribbon to the Canvas Drawing Layer

MQL5 Trading Tools (Part 37): Adding a Per-Object Property-Editing Ribbon to the Canvas Drawing Layer

MetaTrader 5Trading systems |
118 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introduction

In the previous article (Part 36), we added shape and annotation tools with an in-place label editor, so we can now draw filled shapes, drop annotation markers, and type text directly onto chart objects. But once an object is placed, its appearance is locked. The colors, opacities, line widths, dash styles, fonts, and per-level visibility are all baked in at creation time from the tool defaults or the per-tool memory — there is no way to select a rectangle and change its fill color, dim a Fibonacci level, restyle a channel's center line, or thicken a trendline after the fact. The drawing layer can create objects, but it cannot edit them.

This part fixes that with a floating property-editing ribbon. We add four new files that form a descriptor-driven property stack:

  • A descriptor system that declares what each tool exposes
  • An engine-side get/set API with snapshot-based live preview
  • Widget renderers for each control
  • A ribbon panel that appears next to the current selection and binds widgets to the engine

Selecting any object surfaces its editable properties; changing a value updates the object on the chart in real time, and cancelling rolls it back. This article is written for the intermediate-to-advanced MetaQuotes Language 5 (MQL5) developer who is comfortable with the inheritance chain we built up in the previous parts. The subtopics we will cover are:

  1. From Fixed Styles to User-Editable Properties
  2. Building the Property Descriptor System
  3. Rendering the Property-Editing Widgets
  4. Assembling the Property Ribbon
  5. Visualization
  6. Conclusion

By the end, every object on the canvas drawing layer will be fully editable through a compact, draggable ribbon, with each tool surfacing only the properties that apply to it and a live preview that updates the chart as the user drags a slider or picks a color.


From Fixed Styles to User-Editable Properties

Every drawn object already carries its full visual state as fields on the object structure — line width, line style, fill color and opacity, text color, font size, the per-level ratio and visibility arrays, the channel band sigmas, the pitchfork tine colors, and so on. Until now, those fields were written once at placement, either from the tool defaults or from the per-tool memory, and never touched again. Exposing them for editing is not a rendering problem, since the draw routines already read these fields on every redraw; it is a user-interface problem of getting a value from a widget into the right field on the right object.

The naive approach would use a bespoke editing panel, hand-built for each of the forty-odd tools. That path leads to enormous duplication and a maintenance nightmare. Instead, we take a descriptor-driven design. Each tool registers a list of property descriptors that declare what it exposes: a fill-color property, a line-width property, and a level-list property. Each descriptor names a type, a label, a value range, and a string identifier. The ribbon reads that list and renders the appropriate widget for each entry. One generic pipeline then drives every tool. Adding a property to a tool becomes a one-line registration rather than a new panel.

This system has four parts. The descriptor layer defines which properties a tool exposes. The widget layer defines how each property is rendered and edited. The engine get/set layer maps a descriptor ID to an object field. Finally, snapshot-and-restore enables live preview: slider changes apply immediately, and canceling restores the snapshot taken when the ribbon opens. With that conceptual map in place, we can move on to the implementation, but first, have a look at the visualized conceptual map below.

PROPERTIES RIBBON CONCEPTUAL FRAMEWORK


Building the Property Descriptor System

The Property Descriptor Model

We begin the property system by declaring the two foundations everything else rests on — the "PROP_TYPE" enumeration that names every kind of editable property, and the "SToolProperty" structure that describes a single property. Together, they let us treat "what a tool exposes" as plain data rather than hand-built code.

//+------------------------------------------------------------------+
//|                                      ToolsPalette_Properties.mqh |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

//--- Guard against multiple inclusion of this header
#ifndef TOOLS_PALETTE_PROPERTIES_MQH
#define TOOLS_PALETTE_PROPERTIES_MQH

//--- Pull in the Tools header so TOOL_TYPE and DrawnObject are visible
#include "ToolsPalette_Tools.mqh"

//+------------------------------------------------------------------+
//| Widget-type enum - drives which UI widget renders each property  |
//+------------------------------------------------------------------+
enum PROP_TYPE
  {
   PROP_COLOR,        // color swatch + popup grid + opacity slider
   PROP_LINE_WIDTH,   // dropdown of 1px/2px/3px/4px line previews
   PROP_LINE_STYLE,   // dropdown of continuous/dashed/dotted
   PROP_FONT_SIZE,    // dropdown of common font sizes
   PROP_TEXT,         // single-line text input
   PROP_MULTITEXT,    // multi-line text input (text-tab content)
   PROP_BOOL,         // checkbox
   PROP_OPACITY,      // slider 0..100 percent (used inside color picker)
   PROP_DROPDOWN_INT, // generic int dropdown with string options
   PROP_NUMERIC,      // number input with optional spinners
   PROP_PRICE,        // price input (for coordinate-tab rows)
   PROP_TIME,         // time/bar input (for coordinate-tab rows)
   PROP_FLOAT,        // bounded double with min/max/step/decimals
   PROP_COMPACT_ROW,  // composite row: checkbox + value + color + width + style mini-cubes on one line
   PROP_LEVEL_LIST,   // meta descriptor that expands at runtime into N compact rows + "Add level" row
   PROP_ACTION        // sentinel for action-button ribbon icons (Settings, Remove)
  };

//--- Settings-window tab group names
#define PROP_GROUP_STYLE        "Style"
#define PROP_GROUP_TEXT         "Text"
#define PROP_GROUP_COORDS       "Coordinates"
#define PROP_GROUP_VISIBILITY   "Visibility"

//+------------------------------------------------------------------+
//| SToolProperty - one descriptor entry per editable tool property  |
//+------------------------------------------------------------------+
struct SToolProperty
  {
   //--- Identity + display label + widget type + tab grouping
   string id;
   string label;
   PROP_TYPE type;
   string group;
   bool   showInRibbon;
   bool   showInSettings;
   string tooltip;

   //--- Default value (one slot per supported value type)
   color  defaultColor;
   int    defaultInt;
   double defaultDouble;
   string defaultString;
   bool   defaultBool;

   //--- Dropdown options (used by PROP_DROPDOWN_INT and similar)
   string options[];
   int    optionInts[];

   //--- Numeric bounds + step + decimal-places (used by PROP_FLOAT and PROP_LEVEL_LIST)
   double minValue;
   double maxValue;
   double stepValue;
   int    decimals;

   //--- Sub-property IDs for PROP_COMPACT_ROW (the row's checkbox/value/color/width/style cubes)
   string subVisibleId;
   string subValueId;
   string subColorId;
   string subWidthId;
   string subStyleId;

   //--- Runtime-set fields used by PROP_LEVEL_LIST pseudo-row synthesis
   int    levelIdx;
   bool   isAddLevelRow;
   string levelListPrefix;
  };

We guard the header with the "TOOLS_PALETTE_PROPERTIES_MQH" include macro and pull in the Tools header so "TOOL_TYPE" and the "DrawnObject" structures are visible. The "PROP_TYPE" enumeration then names each widget kind the ribbon and settings window know how to render — a color picker ("PROP_COLOR"), the line width and style dropdowns ("PROP_LINE_WIDTH" and "PROP_LINE_STYLE"), the font-size dropdown, single- and multi-line text inputs, a checkbox ("PROP_BOOL"), an opacity slider, a generic integer dropdown, numeric and price and time inputs, and a bounded float. Three entries are special: "PROP_COMPACT_ROW" is a composite that packs a checkbox, value stepper, and color/width/style mini-cubes onto a single line; "PROP_LEVEL_LIST" is a meta-descriptor that expands at runtime into one compact row per Fibonacci or Gann level plus an "Add level" row; and "PROP_ACTION" is a sentinel for the ribbon's action buttons like Settings and Remove.

We also define four tab-group name macros ("PROP_GROUP_STYLE", "PROP_GROUP_TEXT", "PROP_GROUP_COORDS", "PROP_GROUP_VISIBILITY") that sort properties into tabs in the settings window. We define this early because we will need it when we integrate the settings window, which will complete the structure. We intend to use the default MQL5 web tools window for this settings palette, though it has no major difference from the native one. See an example below for clarity.

MQL5 WEB AND NATIVE TERMINAL TRENDLINE SETTINGS COMPARISON

Next, the "SToolProperty" struct is one descriptor entry per editable property. The identity block carries the string id (the key the engine gets/sets API uses to find the underlying field), the display label, the "PROP_TYPE", the tab group, two flags for whether the property shows in the ribbon and in the settings window, and a tooltip. Because a property can hold any of several value kinds, we give it one default slot per type — color, int, double, string, and bool — and the registration code fills whichever slot matches. The dropdown arrays hold the option labels and their backing integer values for "PROP_DROPDOWN_INT", and the numeric block holds the min, max, step, and decimal-place count for "PROP_FLOAT" and the level list.

The last two blocks support the composite types. The sub-property ID fields wire each mini-cube of a compact row to its own underlying property — the row's checkbox, value, color, width, and style each resolve to a separate engine property by ID. And the runtime fields at the bottom — "levelIdx", "isAddLevelRow", and "levelListPrefix" — are left empty in the static descriptors and filled in only when a "PROP_LEVEL_LIST" is expanded into its per-level rows at render time, which is how one level-list descriptor becomes N concrete rows. With the descriptor model declared, we can move on to the registration helpers that build these entries.

The Registration Helpers

We add three small helpers that the per-tool registration functions lean on. Two of them append descriptor entries to a property list, and the third resolves the opacity property that pairs with a given color property.

//+------------------------------------------------------------------+
//| Map a color property ID to its sibling opacity property ID       |
//+------------------------------------------------------------------+
string ColorToOpacityProp(string colorPropId)
  {
   //--- Hand-rolled mapping for the 5 known top-level color properties
   if(colorPropId == "textColor")    return "textOpacity";
   if(colorPropId == "fillColor")    return "fillOpacity";
   if(colorPropId == "fillColor2")   return "fillOpacity2";
   if(colorPropId == "midColor")     return "midOpacity";
   if(colorPropId == "centerColor")  return "centerOpacity";
   //--- Per-level color IDs end with ":color"; rewrite the suffix to ":opacity"
   const int trailLen = StringLen(":color");
   const int propLen  = StringLen(colorPropId);
   if(propLen > trailLen
      && StringSubstr(colorPropId, propLen - trailLen, trailLen) == ":color")
     {
      return StringSubstr(colorPropId, 0, propLen - trailLen) + ":opacity";
     }
   //--- Default fallback: every unknown color maps to the main line opacity
   return "lineOpacity";
  }

//+------------------------------------------------------------------+
//| Append a compact-row descriptor (checkbox + value + color cubes) |
//+------------------------------------------------------------------+
void AddCompactRowDescriptor(SToolProperty &props[],
                              string id, string label,
                              string subVisibleId, string subValueId,
                              string subColorId, string subWidthId,
                              string subStyleId,
                              string group = "Style",
                              double minValue = 0.0, double maxValue = 100.0,
                              double stepValue = 0.1, int decimals = 2)
  {
   //--- Grow the array by one + populate the new entry
   const int n = ArraySize(props);
   ArrayResize(props, n + 1);
   props[n].id              = id;
   props[n].label           = label;
   props[n].type            = PROP_COMPACT_ROW;
   props[n].group           = group;
   //--- Compact rows live only in the settings window, never in the ribbon
   props[n].showInRibbon    = false;
   props[n].showInSettings  = true;
   props[n].tooltip         = "";
   //--- Default-value slots (compact rows themselves don't carry a value, but populate for safety)
   props[n].defaultColor    = clrBlack;
   props[n].defaultInt      = 0;
   props[n].defaultDouble   = 0.0;
   props[n].defaultString   = "";
   props[n].defaultBool     = false;
   //--- Numeric stepper bounds for the row's value cube
   props[n].minValue        = minValue;
   props[n].maxValue        = maxValue;
   props[n].stepValue       = stepValue;
   props[n].decimals        = decimals;
   //--- Sub-property IDs that wire each mini-cube to its underlying engine property
   props[n].subVisibleId    = subVisibleId;
   props[n].subValueId      = subValueId;
   props[n].subColorId      = subColorId;
   props[n].subWidthId      = subWidthId;
   props[n].subStyleId      = subStyleId;
   //--- Not a level pseudo-row (those get their levelIdx set at runtime)
   props[n].levelIdx        = -1;
   props[n].isAddLevelRow   = false;
   props[n].levelListPrefix = "";
  }

//+------------------------------------------------------------------+
//| Append a single SToolProperty entry to a property list           |
//+------------------------------------------------------------------+
void AddPropertyDescriptor(SToolProperty &props[],
                            string id,
                            string label,
                            PROP_TYPE type,
                            string group,
                            bool showInRibbon,
                            bool showInSettings,
                            string tooltip = "")
  {
   //--- Grow the array by one + populate the new entry's identity fields
   int n = ArraySize(props);
   ArrayResize(props, n + 1);
   props[n].id              = id;
   props[n].label           = label;
   props[n].type            = type;
   props[n].group           = group;
   props[n].showInRibbon    = showInRibbon;
   props[n].showInSettings  = showInSettings;
   props[n].tooltip         = tooltip;
   //--- Not a level pseudo-row (caller can override later for the level-list case)
   props[n].levelIdx        = -1;
   props[n].isAddLevelRow   = false;
   props[n].levelListPrefix = "";
  }

Here, the "ColorToOpacityProp" function exists because every color in the system has a matching opacity that the color picker edits alongside it — picking a fill color and dragging its opacity slider both write back through the property API, so the picker needs to know which opacity ID goes with the color ID it was opened for. For the five top-level colors (text, fill, second fill, mid-line, center), the mapping is hand-rolled. For per-level colors, whose IDs follow a "prefix:color" convention, we rewrite the trailing ":color" to ":opacity" so a Fibonacci level's color ID resolves to that same level's opacity ID. Anything unrecognized falls back to the main line opacity.

"AddCompactRowDescriptor" appends a "PROP_COMPACT_ROW" entry — the one-line composite that packs a checkbox, value stepper, and color, width, and style mini-cubes. The function grows the descriptor array by one and populates the new entry, marking it settings-only (compact rows never appear in the ribbon), setting the numeric stepper bounds for the value cube, and wiring each of the five sub-property IDs so every mini-cube knows which underlying engine property it edits. The level-list runtime fields are left empty here since a static compact row is not itself a per-level pseudo-row.

"AddPropertyDescriptor" is the general-purpose appender for every non-composite property. It grows the array, fills the identity block from its arguments — id, label, type, group, the two visibility flags, and the tooltip — and clears the level-list runtime fields, which a caller can override afterward for the level-list case. Both appenders follow the same grow-by-one-then-populate pattern, so the registration functions in the next steps read as a flat sequence of "AddPropertyDescriptor" calls, one per property a tool exposes. Next, we will define the actual logic to register these tools.

Registering a Tool's Property Set

We now look at a representative registration function, "RegisterLineToolWithLabelProperties", which declares the full property set for the labeled line tools — trendline, ray, extended line, horizontal and vertical lines, info line, and trend angle. Every tool family has one of these functions, and they all read as a flat sequence of "AddPropertyDescriptor" calls.

//+------------------------------------------------------------------+
//| Register standard property set for line tools WITH text label    |
//+------------------------------------------------------------------+
void RegisterLineToolWithLabelProperties(SToolProperty &props[])
  {
   //--- Line color (ribbon + settings)
   AddPropertyDescriptor(props, "lineColor", "Line color",
                          PROP_COLOR, PROP_GROUP_STYLE,
                          true, true, "Line tool colors");
   //--- Text color (ribbon + settings, lives in the Text tab)
   AddPropertyDescriptor(props, "textColor", "Text color",
                          PROP_COLOR, PROP_GROUP_TEXT,
                          true, true, "Line tool text colors");
   //--- Line width + style (default 2 px, solid)
   AddPropertyDescriptor(props, "lineWidth", "Width",
                          PROP_LINE_WIDTH, PROP_GROUP_STYLE,
                          true, true, "Line tool widths");
   props[ArraySize(props) - 1].defaultInt = 2;
   AddPropertyDescriptor(props, "lineStyle", "Style",
                          PROP_LINE_STYLE, PROP_GROUP_STYLE,
                          true, true, "Style");
   props[ArraySize(props) - 1].defaultInt = 0;
   //--- Line opacity (settings only)
   AddPropertyDescriptor(props, "lineOpacity", "Line opacity",
                          PROP_OPACITY, PROP_GROUP_STYLE,
                          false, true, "");
   props[ArraySize(props) - 1].defaultInt = 100;
   //--- Label text + per-text opacity + font size + bold + alignment
   AddPropertyDescriptor(props, "text", "Text",
                          PROP_TEXT, PROP_GROUP_TEXT,
                          false, true, "");
   AddPropertyDescriptor(props, "textOpacity", "Text opacity",
                          PROP_OPACITY, PROP_GROUP_TEXT,
                          false, true, "");
   props[ArraySize(props) - 1].defaultInt = 100;
   AddPropertyDescriptor(props, "fontSize", "Font size",
                          PROP_FONT_SIZE, PROP_GROUP_TEXT,
                          false, true, "");
   props[ArraySize(props) - 1].defaultInt = 11;
   AddPropertyDescriptor(props, "bold", "Bold",
                          PROP_BOOL, PROP_GROUP_TEXT,
                          false, true, "");
   props[ArraySize(props) - 1].defaultBool = false;
   //--- Vertical alignment (default Top = 0)
   AddPropertyDescriptor(props, "vAlign", "Vertical alignment",
                          PROP_DROPDOWN_INT, PROP_GROUP_TEXT,
                          false, true, "");
   props[ArraySize(props) - 1].defaultInt = 0;
   //--- Horizontal alignment (default Center = 1)
   AddPropertyDescriptor(props, "hAlign", "Horizontal alignment",
                          PROP_DROPDOWN_INT, PROP_GROUP_TEXT,
                          false, true, "");
   props[ArraySize(props) - 1].defaultInt = 1;
  }

The function appends one descriptor per editable property that the labeled line tools expose. The line color and text color come first, both flagged to appear in the ribbon and the settings window, with the text color sorted into the Text tab. The line width and style follow, also ribbon-visible, and immediately after each call, we reach back into the just-appended entry via "props[ArraySize(props) - 1]" to set its default integer — width defaults to 2 pixels and style to 0 (solid). This back-reference is the idiom for setting a default, since "AddPropertyDescriptor" only fills the identity block and leaves the value slots untouched.

The remaining properties are settings-only — they have their ribbon flag set to false, so they appear in the settings window but not on the compact ribbon. The line opacity defaults to 100, then the label text, its own per-text opacity (also 100), the font size (defaulting to 11), a bold checkbox, and the vertical and horizontal alignment dropdowns round out the Text tab. The alignment dropdowns are "PROP_DROPDOWN_INT" entries whose option labels and values get attached later by the widget layer, with vertical alignment defaulting to Top (0) and horizontal to Center (1).

The order of these calls matters because it is the order in which the settings window renders the rows. Every other registration function in the file follows this same shape — closed shapes, channels, pitchforks, Gann tools, and the annotations each append the descriptors that apply to them and set their defaults the same way, so we will not walk through the rest individually. The one that introduces genuinely new mechanics is the Fibonacci registration, which we cover next.

Registering the Fibonacci Level List

We now reach the level-list mechanism, which is what lets the Fibonacci and Gann tools expose an editable list of ratio levels rather than a fixed set of properties. We define a reusable helper, "AddLevelListDescriptor", and then look at how the Fibonacci retracement registers its own level list.

//+------------------------------------------------------------------+
//| Append a PROP_LEVEL_LIST descriptor (helper for other Fib/Gann)  |
//+------------------------------------------------------------------+
void AddLevelListDescriptor(SToolProperty &props[],
                             string id, string label,
                             double minR, double maxR, double stepR, int dec)
  {
   //--- Grow + populate the new descriptor
   const int n = ArraySize(props);
   ArrayResize(props, n + 1);
   props[n].id              = id;
   props[n].label           = label;
   props[n].type            = PROP_LEVEL_LIST;
   props[n].group           = PROP_GROUP_STYLE;
   //--- Levels live only in the settings window
   props[n].showInRibbon    = false;
   props[n].showInSettings  = true;
   props[n].tooltip         = "";
   //--- Default-value slots
   props[n].defaultColor    = clrBlack;
   props[n].defaultInt      = 0;
   props[n].defaultDouble   = 0.0;
   props[n].defaultString   = "";
   props[n].defaultBool     = false;
   //--- Caller-supplied per-level ratio bounds + step + decimal places
   props[n].minValue        = minR;
   props[n].maxValue        = maxR;
   props[n].stepValue       = stepR;
   props[n].decimals        = dec;
   //--- No per-row sub-IDs (rows are synthesized at runtime from the level data)
   props[n].subVisibleId    = "";
   props[n].subValueId      = "";
   props[n].subColorId      = "";
   props[n].subWidthId      = "";
   props[n].subStyleId      = "";
   props[n].levelIdx        = -1;
   props[n].isAddLevelRow   = false;
   props[n].levelListPrefix = "";
  }

//+------------------------------------------------------------------+
//| Register property set for Fibonacci retracement (PROP_LEVEL_LIST)|
//+------------------------------------------------------------------+
void RegisterFibonacciRetracementProperties(SToolProperty &props[])
  {
   //--- Default level color (ribbon-only; cascades onto per-level colors when changed)
   AddPropertyDescriptor(props, "lineColor", "Default level color",
                          PROP_COLOR, PROP_GROUP_STYLE,
                          true, false, "Default color for new levels");
   //--- Levels list descriptor - manually populated (predates AddLevelListDescriptor below)
   const int n = ArraySize(props);
   ArrayResize(props, n + 1);
   props[n].id              = "fibo";
   props[n].label           = "Levels";
   props[n].type            = PROP_LEVEL_LIST;
   props[n].group           = PROP_GROUP_STYLE;
   //--- Levels live only in the settings window, never in the ribbon
   props[n].showInRibbon    = false;
   props[n].showInSettings  = true;
   props[n].tooltip         = "";
   //--- Default-value slots
   props[n].defaultColor    = clrBlack;
   props[n].defaultInt      = 0;
   props[n].defaultDouble   = 0.0;
   props[n].defaultString   = "";
   props[n].defaultBool     = false;
   //--- Ratio bounds for each level's value cube: -10..10 with 0.01 step and 3 decimal places
   props[n].minValue        = -10.0;
   props[n].maxValue        =  10.0;
   props[n].stepValue       = 0.01;
   props[n].decimals        = 3;
   //--- The level-list itself has no per-row sub-IDs (rows are synthesized at runtime)
   props[n].subVisibleId    = "";
   props[n].subValueId      = "";
   props[n].subColorId      = "";
   props[n].subWidthId      = "";
   props[n].subStyleId      = "";
   props[n].levelIdx        = -1;
   props[n].isAddLevelRow   = false;
   props[n].levelListPrefix = "";
  }

We use "AddLevelListDescriptor" to append a single "PROP_LEVEL_LIST" entry. We grow the array by one and populate it much like the other appenders, marking it settings-only and storing the caller-supplied per-level ratio bounds — the min, max, step, and decimal-place count that each level's value cube will respect when we edit a ratio. We deliberately leave all five sub-property ID fields empty here, because unlike a static compact row, the per-level rows of a level list do not exist yet — we synthesize them at runtime from the object's actual level data, so there is nothing to wire up at registration time. We need these levels to be the same as the native terminal does. See below.

FIBONACCI LEVELS IN MT5

In "RegisterFibonacciRetracementProperties", we first add a "lineColor" descriptor labeled "Default level color". We flag it ribbon-visible but settings-hidden, and we treat it as a cascade control — when the user changes it from the ribbon, that color flows onto every per-level color. We then append the level-list descriptor itself. This one we populate by hand rather than calling the helper, since this registration predates the helper, but the result is identical: a "PROP_LEVEL_LIST" entry with id "fibo", settings-only visibility, and ratio bounds of -10 to 10 with a 0.01 step and three decimal places.

What makes this descriptor different from everything we have registered so far is that it is a meta-descriptor — it does not describe one row, it describes a family of rows. When the settings window renders it, we expand the single "fibo" entry into one compact row per level the object currently holds, plus a trailing "Add level" row, filling in the "levelIdx" and "levelListPrefix" runtime fields on each synthesized row so the widget layer knows which level it edits. We handle that expansion in the widget and ribbon layers, so for now we just register the one descriptor and move on. The Gann fan and Gann box register their level lists the same way through "AddLevelListDescriptor", and the master dispatcher routes each tool type to the registration function that applies to it. Next, in a new file, we will describe the logic to get and set the object properties in the drawing engine as follows.

Reading a Property by String ID

We now cross over to the engine side, where the descriptors actually reach the object's fields. We define "GetObjectProperty", the read half of the property API, and walk through its color overload — the int, bool, double, and string overloads and the whole setter family all follow this same id-dispatch shape, so this is the only snippet we need from this file.

//+------------------------------------------------------------------+
//|                               ToolsPalette_Engine_Properties.mqh |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

//--- Guard against multiple inclusion of this header
#ifndef TOOLS_PALETTE_ENGINE_PROPERTIES_MQH
#define TOOLS_PALETTE_ENGINE_PROPERTIES_MQH

//--- Pull in the Tools header so CDrawingEngine and DrawnObject are visible to the bodies below
#include "ToolsPalette_Tools.mqh"

//+------------------------------------------------------------------+
//| Read a color-typed property by string ID                         |
//+------------------------------------------------------------------+
bool CDrawingEngine::GetObjectProperty(int objId, string propId, color &outValue)
  {
   //--- Resolve the object's array index by ID; bail on unknown ID
   int idx = FindObjectIndexById(objId);
   if(idx < 0) return false;

   //--- Primary stroke/fill color of the object
   if(propId == "lineColor")
     {
      outValue = m_drawnObjects[idx].objColor;
      return true;
     }
   //--- Text color (for label-bearing tools)
   if(propId == "textColor")
     {
      outValue = m_drawnObjects[idx].textColor;
      return true;
     }
   //--- Fill color (for shapes with a fill)
   if(propId == "fillColor")
     {
      outValue = m_drawnObjects[idx].fillColor;
      return true;
     }
   //--- Channel midline color
   if(propId == "midColor")
     {
      outValue = m_drawnObjects[idx].midColor;
      return true;
     }
   //--- Secondary fill color (e.g., the second std-dev band fill)
   if(propId == "fillColor2")
     {
      outValue = m_drawnObjects[idx].fillColor2;
      return true;
     }
   //--- Regression centerline color
   if(propId == "centerColor")
     {
      outValue = m_drawnObjects[idx].centerColor;
      return true;
     }
   //--- Pitchfork per-part-group color getters
   if(propId == "medianColor") { outValue = m_drawnObjects[idx].medianColor; return true; }
   if(propId == "outerColor")  { outValue = m_drawnObjects[idx].outerColor;  return true; }
   if(propId == "innerColor")  { outValue = m_drawnObjects[idx].innerColor;  return true; }
   //--- Per-level color for ANY PROP_LEVEL_LIST tool (fibo/fibex/fibch/fibtz/fibfan/fibarc/gannfan/gannbox)
     {
      string pfx; int li; string fld;
      if(ParseLevelPropId(propId, pfx, li, fld) && fld == "color")
        {
         //--- Dispatch by prefix to the matching tool's level-color array
         if(pfx == "fibo")
           {
            if(li < ArraySize(m_drawnObjects[idx].fiboLevelColor))
              { outValue = m_drawnObjects[idx].fiboLevelColor[li]; return true; }
           }
         else if(pfx == "fibex")
           {
            if(li < ArraySize(m_drawnObjects[idx].fibexLevelColor))
              { outValue = m_drawnObjects[idx].fibexLevelColor[li]; return true; }
           }
         else if(pfx == "fibch")
           {
            if(li < ArraySize(m_drawnObjects[idx].fibchLevelColor))
              { outValue = m_drawnObjects[idx].fibchLevelColor[li]; return true; }
           }
         else if(pfx == "fibtz")
           {
            if(li < ArraySize(m_drawnObjects[idx].fibtzLevelColor))
              { outValue = m_drawnObjects[idx].fibtzLevelColor[li]; return true; }
           }
         else if(pfx == "fibfan")
           {
            if(li < ArraySize(m_drawnObjects[idx].fibfanLevelColor))
              { outValue = m_drawnObjects[idx].fibfanLevelColor[li]; return true; }
           }
         else if(pfx == "fibarc")
           {
            if(li < ArraySize(m_drawnObjects[idx].fibarcLevelColor))
              { outValue = m_drawnObjects[idx].fibarcLevelColor[li]; return true; }
           }
         else if(pfx == "gannfan")
           {
            if(li < ArraySize(m_drawnObjects[idx].gannfanLevelColor))
              { outValue = m_drawnObjects[idx].gannfanLevelColor[li]; return true; }
           }
         else if(pfx == "gannbox")
           {
            if(li < ArraySize(m_drawnObjects[idx].gannboxLevelColor))
              { outValue = m_drawnObjects[idx].gannboxLevelColor[li]; return true; }
           }
         //--- Unknown prefix or out-of-range level index
         return false;
        }
     }

   //--- Unrecognized property ID for the color type
   return false;
  }

We start by resolving the object's array index from its id via "FindObjectIndexById", bailing out if the id is unknown. From there, we match the incoming "propId" string against each color the object can expose and write the matching field into the output parameter. We handle the top-level colors first — "lineColor" reads the object's primary stroke color, "textColor" the label color, "fillColor" and "fillColor2" the two fill colors, "midColor" the channel mid-line, and "centerColor" the regression center line. We then handle the three pitchfork tine colors ("medianColor", "outerColor", "innerColor") on single lines since they are simple field reads.

The interesting branch is the per-level color path, which we reach for any tool that carries a level list. We call "ParseLevelPropId" to split a level color id into its prefix, level index, and field name, and when the field is "color," we dispatch on the prefix to the matching tool's level-color array — "fibo" reads "fiboLevelColor", "gannfan" reads "gannfanLevelColor", and so on across all eight level-list tools. We bounds-check the level index against the array size before reading, so a stale ID pointing past the end of a shrunk level list fails cleanly rather than reading out of range. Anything we do not recognize returns false, which tells the caller the property does not apply to this object.

The rest of the objects repeat this pattern. We provide a "GetObjectProperty" overload per value type and a matching "SetObjectProperty" per type, where the setters take the same ID and an extra "preview" flag and write the field instead of reading it. Since every one of these follows the id-dispatch we just walked through, we move on to the widgets that drive these calls.


Rendering the Property-Editing Widgets

The Transparency-Checker Helpers

We open the widget file with two small painting helpers that lay down the checkerboard pattern behind any color we are editing. We need this because a swatch showing a semi-transparent color has to sit on a checker backdrop for the transparency to read visually — a flat backdrop would make a 40%-opacity red look like a solid pale pink instead of a see-through one. Here is what a transparency checker looks like in context for reference.

TRANSPARENCY CHECKER SAMPLE

Any colors can be used for the checker pattern.

//+------------------------------------------------------------------+
//|                                 ToolsPalette_PropertyWidgets.mqh |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

//--- Guard against multiple inclusion of this header
#ifndef TOOLS_PALETTE_PROPERTY_WIDGETS_MQH
#define TOOLS_PALETTE_PROPERTY_WIDGETS_MQH

//--- Pull in the Sidebar header (transitively pulls Primitives + Tools)
#include "ToolsPalette_Sidebar.mqh"

//+------------------------------------------------------------------+
//| Paint a transparency-checker pattern clipped to a pill shape     |
//+------------------------------------------------------------------+
void WidgetCheckerFillPill(CCanvas &canvas,
                            int l, int t, int r, int b,
                            int checkerSize)
  {
   //--- Bail on degenerate rectangles
   const int h = b - t;
   if(h <= 0 || r <= l) return;
   //--- Pill = rect with semicircular ends - radius is half the height
   const int radius = h / 2;
   //--- Two checker colors (light + lighter gray)
   const uint checkerA = ColorToARGB(C'200,200,200', 255);
   const uint checkerB = ColorToARGB(C'232,232,232', 255);
   const int yMid = (t + b) / 2;
   //--- Walk every row, compute the per-row x-inset that keeps us inside the pill
   for(int y = t; y < b; y++)
     {
      //--- Distance from the pill's vertical centerline - rows beyond `radius` are outside
      const int dyFromMid = MathAbs(y - yMid);
      if(dyFromMid >= radius) continue;
      //--- Half-width of the pill at this y (from circle equation x = sqrt(r^2 - dy^2))
      const double rr = (double)radius;
      const double extent = MathSqrt(rr * rr - (double)dyFromMid * (double)dyFromMid);
      const int rowInset = (int)(rr - extent);
      const int xStart = l + rowInset;
      const int xEnd   = r - rowInset;
      //--- Paint each column with the alternating checker pattern
      for(int x = xStart; x < xEnd; x++)
        {
         const bool dark = (((x - l) / checkerSize) + ((y - t) / checkerSize)) % 2 == 0;
         canvas.PixelSet(x, y, dark ? checkerA : checkerB);
        }
     }
  }

//+------------------------------------------------------------------+
//| Paint a transparency-checker pattern inside a plain rectangle    |
//+------------------------------------------------------------------+
void WidgetCheckerFillRect(CCanvas &canvas,
                            int l, int t, int r, int b,
                            int checkerSize)
  {
   //--- Bail on degenerate rectangles
   if(r <= l || b <= t) return;
   //--- Two checker colors (light + lighter gray)
   const uint checkerA = ColorToARGB(C'200,200,200', 255);
   const uint checkerB = ColorToARGB(C'232,232,232', 255);
   //--- Walk every pixel; xor the cell index to pick the alternating color
   for(int y = t; y < b; y++)
     {
      for(int x = l; x < r; x++)
        {
         const bool dark = (((x - l) / checkerSize) + ((y - t) / checkerSize)) % 2 == 0;
         canvas.PixelSet(x, y, dark ? checkerA : checkerB);
        }
     }
  }

We guard the header with the "TOOLS_PALETTE_PROPERTY_WIDGETS_MQH" macro and pull in the Sidebar header, which transitively brings along the primitives and the Tools layer that the widgets draw on top of.

We paint the pill-clipped version in "WidgetCheckerFillPill". After rejecting degenerate rectangles, we treat the shape as a rectangle with semicircular caps whose radius is half the height. We iterate over rows and compute each row's distance from the vertical centerline. Rows beyond the radius lie outside the caps and are skipped. For the remaining rows, we use the circle equation to compute the half-width and inset the row accordingly. Within each row's clipped span, we pick the checker color by adding the cell's column and row indices and testing the sum's parity, which gives the familiar alternating two-tone grid. We use the two light grays so the pattern stays subtle behind whatever color we lay over it.

We paint the simpler version in "WidgetCheckerFillRect", which fills a plain rectangle with no rounding. We walk every pixel in the rectangle and pick the checker color with the same parity test on the cell indices. We reach for this rectangular variant wherever a swatch or preview area is square-cornered, and the pill variant wherever the control has rounded ends. Next, we will do the color picker swatch and the rendering pipeline.

The Color Picker Palette

We begin the color picker — the richest widget in the ribbon — by laying down the layout constants that fix its geometry and the preset palette it offers.

//--- Color picker layout constants (grid + spacing + opacity strip + label box)
#define COLORPICKER_GRID_COLS         10
#define COLORPICKER_GRID_ROWS         8
#define COLORPICKER_SWATCH_SIZE       18
#define COLORPICKER_SWATCH_GAP         5
#define COLORPICKER_SWATCH_RADIUS      4
#define COLORPICKER_PAD_X             11
#define COLORPICKER_PAD_Y_TOP         11
#define COLORPICKER_PAD_Y_BOTTOM      11
#define COLORPICKER_GRID_TO_DIVIDER   10
#define COLORPICKER_DIVIDER_TO_OPACITY 10
#define COLORPICKER_OPACITY_LABEL_H   14
#define COLORPICKER_OPACITY_LABEL_GAP  6
#define COLORPICKER_OPACITY_STRIP_H   10
#define COLORPICKER_OPACITY_BOX_W     46
#define COLORPICKER_OPACITY_BOX_H     22
#define COLORPICKER_OPACITY_BOX_GAP    8
#define COLORPICKER_RING_OFFSET        2
#define COLORPICKER_RING_THICKNESS     2
#define COLORPICKER_WHITE_BORDER_COLOR_R  220
#define COLORPICKER_WHITE_BORDER_COLOR_G  220
#define COLORPICKER_WHITE_BORDER_COLOR_B  220

//+------------------------------------------------------------------+
//| Color picker preset palette (80 swatches arranged as 10x8 grid)  |
//+------------------------------------------------------------------+
color GetColorPickerSwatch(int idx)
  {
   //--- 80-entry preset palette organized by tonal row
   static color s_palette[80] =
     {
      //--- Row 0: grayscale (white to black)
      C'255,255,255', C'232,232,232', C'209,209,209', C'186,186,186', C'163,163,163',
      C'140,140,140', C'117,117,117', C'94,94,94',    C'47,47,47',    C'0,0,0',
      //--- Row 1: lightest pastels
      C'255,205,210', C'255,224,178', C'255,249,196', C'200,230,201', C'178,235,242',
      C'179,229,252', C'197,202,233', C'209,196,233', C'225,190,231', C'248,187,208',
      //--- Row 2: soft pastels
      C'255,154,162', C'255,196,148', C'255,236,139', C'165,214,167', C'128,222,234',
      C'129,212,250', C'159,168,218', C'179,157,219', C'206,147,216', C'244,143,177',
      //--- Row 3: pastel-vivid
      C'255,138,128', C'255,167, 38', C'255,213,  0', C'129,199,132', C' 77,208,225',
      C' 79,195,247', C'121,134,203', C'149,117,205', C'186,104,200', C'240,98 ,146',
      //--- Row 4: clean mid
      C'244, 67, 54', C'255,152,  0', C'255,193,  7', C' 76,175, 80', C'  0,188,212',
      C' 33,150,243', C' 63, 81,181', C'103, 58,183', C'156, 39,176', C'233, 30, 99',
      //--- Row 5: deeper mid
      C'229, 57, 53', C'251,140,  0', C'251,177,  0', C' 56,142, 60', C'  0,151,167',
      C' 30,136,229', C' 57, 73,171', C' 94, 53,177', C'142, 36,170', C'216, 27, 96',
      //--- Row 6: deep saturated
      C'198, 40, 40', C'239,108,  0', C'245,127, 23', C' 46,125, 50', C'  0,121,107',
      C' 21,101,192', C' 48, 63,159', C' 81, 45,168', C'123, 31,162', C'194, 24, 91',
      //--- Row 7: darkest
      C'183, 28, 28', C'230, 81,  0', C'191, 54, 12', C' 27, 94, 32', C'  0, 96, 100',
      C' 13, 71,161', C' 26, 35,126', C' 49, 27,146', C' 74, 20,140', C'136, 14, 79'
     };
   //--- Out-of-range index returns the sentinel clrNONE
   if(idx < 0 || idx >= 80) return clrNONE;
   return s_palette[idx];
  }

//+------------------------------------------------------------------+
//| Reverse lookup: find the palette index for a given color value   |
//+------------------------------------------------------------------+
int FindColorPickerSwatchIndex(color c)
  {
   //--- Linear scan through the 80-entry palette; return -1 if not present
   for(int i = 0; i < 80; i++)
     {
      if(GetColorPickerSwatch(i) == c) return i;
     }
   return -1;
  }

We start with a block of "#define" constants that pin down every dimension of the picker — ten columns by eight rows of eighteen-pixel swatches with a five-pixel gap and a four-pixel corner radius, plus the padding, the divider spacing, the opacity strip height, the opacity box size, and the selection ring offset and thickness. We keep these as named constants in one place so the layout math in the render and hit-test routines stays readable, and so a single edit here shifts the whole picker consistently.

Next, we define "GetColorPickerSwatch", which returns one of eighty preset colors by index from a static array arranged as that ten-by-eight grid. We run the first row grayscale from white to black, then sweep the seven rows below from the lightest pastels down to the darkest saturated tones, keeping each column in one hue family so we can scan down for a darker shade or across for a different hue. We hand back clrNONE for an out-of-range index so callers can treat it as a missing entry.

Finally, we add "FindColorPickerSwatchIndex" to go the other way, linear-scanning the palette for a color value and returning its index, or -1 when the color is not a preset. We call this when the picker opens on an existing object — it tells us which swatch matches the object's current color, so we can ring it, and the -1 result tells us the object is holding a custom color outside the preset grid. We chose these colors for flexibility in case one wants to expand the palette in the future. Feel free to add more columns or rows. We prioritized the black and white to be at the top row since it is the one most used. When rendered, this will give us the following outcome.

COLOR PICKER GRID

To render the line-property popovers, we use the following approach.

Rendering the Line-Width and Line-Style Popovers

We render both the line-width popover and the line-style popover from a single function, "RenderLinePopoverRows", since the two dropdowns share an identical row layout and differ only in which axis changes down the rows.

//+------------------------------------------------------------------+
//| Render the rows of the line-width OR line-style popover          |
//+------------------------------------------------------------------+
void RenderLinePopoverRows(CCanvas &canvas,
                            int originX, int originY, int bodyW,
                            int activeOptionIdx,
                            int hoveredIdx,
                            int lineWidthOption,
                            int lineStyleOption,
                            color strokeColor,
                            const ThemeColorSet &theme)
  {
   //--- 4 fixed rows (LINEPOPOVER_NUM_OPTIONS) - one per width or style
   for(int i = 0; i < LINEPOPOVER_NUM_OPTIONS; i++)
     {
      int rL, rT, rR, rB;
      GetLinePopoverRowRect(i, originX, originY, bodyW, rL, rT, rR, rB);
      //--- Active-row highlight (stronger than hover)
      if(i == activeOptionIdx)
        {
         const uint actArgb = ColorToARGB(theme.flyoutTextColor, 55);
         for(int yy = rT; yy < rB; yy++)
            for(int xx = rL; xx < rR; xx++)
               WidgetBlendPixel(canvas, xx, yy, actArgb);
        }
      else if(i == hoveredIdx)
        {
         //--- Hover-row highlight (more subtle)
         const uint hovArgb = ColorToARGB(theme.flyoutTextColor, 25);
         for(int yy = rT; yy < rB; yy++)
            for(int xx = rL; xx < rR; xx++)
               WidgetBlendPixel(canvas, xx, yy, hovArgb);
        }
      //--- Width and style preview: in width-popover, w varies (i+1) and s is fixed; in style-popover, s varies (i) and w is fixed
      const int w = (lineWidthOption >= 0) ? lineWidthOption : (i + 1);
      const int s = (lineStyleOption >= 0) ? lineStyleOption : i;
      //--- Stroke preview at the left of the row
      const int sL = rL + LINEPOPOVER_PAD_X;
      const int sR = sL + LINEPOPOVER_STROKE_W;
      StrokeLinePreview(canvas, sL, rT, sR, rB, w, s,
                          ColorToARGB(strokeColor, 255));
      //--- Label text: "Xpx" for width-popover, style name for style-popover, empty for the orthogonal axis
      string label = "";
      if(lineWidthOption < 0)
         label = IntegerToString(i + 1) + "px";
      else if(lineStyleOption < 0)
         label = GetLineStyleLabel(i);
      //--- Position the label to the right of the stroke preview with gap, vertically centered
      const int textX = sR + LINEPOPOVER_STROKE_LABEL_GAP;
      const int textY = (rT + rB) / 2 - 7;
      canvas.FontSet("Arial", -100);
      canvas.TextOut(textX, textY, label,
                       ColorToARGB(theme.flyoutTextColor, 240));
     }
  }

We start by looping over the four fixed rows, fetching each row's rectangle from "GetLinePopoverRowRect" and painting its highlight — a stronger tint when the row is the active option matching the object's current value, and a subtler tint when the row is merely under the cursor. We then work out the width and style this row should preview: when the caller fixes "lineWidthOption", the style varies down the rows as "i" while the width stays put, which gives us the style popover, and when it fixes "lineStyleOption", the width varies as "i + 1" while the style stays put, which gives us the width popover.

Next, we draw a short stroke preview at the left of the row through "StrokeLinePreview" using that resolved width and style, so we see the exact line we are about to choose. Finally, we build the row label — "Xpx" for the width popover, or the style name from "GetLineStyleLabel" for the style popover, and an empty string on whichever axis does not vary — and we paint it just to the right of the preview, vertically centered in the row. To assemble the property ribbon, we house the logic in a new file, so maintenance can be easy.


Assembling the Property Ribbon

Declaring the Ribbon Class

We declare the ribbon itself as "CRibbon", extending "CSidebarRenderer" so it slots into the same inheritance chain as the sidebar and inherits its canvas and theme machinery.

//+------------------------------------------------------------------+
//|                                          ToolsPalette_Ribbon.mqh |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

//--- Guard against multiple inclusion of this header
#ifndef TOOLS_PALETTE_RIBBON_MQH
#define TOOLS_PALETTE_RIBBON_MQH

//--- Pull in the Sidebar (base class), property descriptor system, and widget renderers
#include "ToolsPalette_Sidebar.mqh"
#include "ToolsPalette_Properties.mqh"
#include "ToolsPalette_PropertyWidgets.mqh"

//+------------------------------------------------------------------+
//| CRibbon - quick-access property-editing ribbon for the selection |
//+------------------------------------------------------------------+
class CRibbon : public CSidebarRenderer
  {
protected:
   //--- Ribbon canvas + high-res supersample backing canvas
   string          m_nameRibbon;
   CCanvas         m_canvasRibbon;
   CCanvas         m_canvasRibbonHighRes;

   //--- Ribbon dimensions (recomputed when content changes)
   int             m_ribbonWidth;
   int             m_ribbonHeight;
   int             m_ribbonCornerRadius;
   int             m_ribbonShadowPad;

   //--- Current ribbon position + last-known position cache (for restore after hide/show)
   int             m_ribbonX;
   int             m_ribbonY;
   int             m_lastRibbonX;
   int             m_lastRibbonY;
   bool            m_lastRibbonValid;

   //--- Visibility + bound object ID (the drawn object the ribbon edits)
   bool            m_isRibbonVisible;
   int             m_ribbonOwnerObjectId;

   //--- Drag state (set while the user is moving the ribbon via the grip)
   bool            m_isRibbonDragging;
   int             m_ribbonDragOffsetX;
   int             m_ribbonDragOffsetY;

   //--- Width of the drag-grip area on the left edge
   int             m_ribbonGripWidth;

   //--- Icon row geometry (size, gap between icons, left/right padding inside the body)
   int             m_ribbonIconSize;
   int             m_ribbonIconGap;
   int             m_ribbonIconRowPadX;

   //--- Hovered icon index (-1 when nothing is hovered)
   int             m_hoveredRibbonIconIdx;

   //--- Filtered property descriptor list (subset of BuildPropertyListForTool that's ribbon-eligible)
   SToolProperty   m_ribbonProperties[];

   //--- Popover canvas + high-res backing canvas
   string          m_namePopover;
   CCanvas         m_canvasPopover;
   CCanvas         m_canvasPopoverHighRes;

   //--- Popover visibility + which property's popover is open + screen-space rect
   bool            m_isPopoverVisible;
   string          m_activePopoverPropId;
   int             m_popoverX;
   int             m_popoverY;
   int             m_popoverWidth;
   int             m_popoverHeight;
   int             m_popoverShadowPad;
   int             m_popoverCornerRadius;

   //--- True when a parent owns the property-edit snapshot lifecycle (Settings window case)
   bool            m_popoverSnapshotIsExternal;

   //--- Opacity slider drag state + current slider value
   bool            m_isOpacityDragging;
   int             m_opacityValuePct;

   //--- Hovered popover item index (swatch / row / row depending on popover type)
   int             m_hoveredPopoverSwatchIdx;

   //--- Inline opacity-box editing state (text buffer + caret position + blink phase)
   bool            m_isEditingBoxOpacity;
   string          m_boxEditBuffer;
   int             m_boxCaretPos;
   bool            m_boxCaretOn;
   ulong           m_boxBlinkTickAt;

public:
   //--- Lifecycle - create/destroy the ribbon's chart-object canvases
   bool            CreateRibbonCanvases();
   void            DestroyRibbonCanvases();

   //--- Show/hide the ribbon bound to a specific drawn-object ID
   void            ShowRibbonFor(int objId);
   void            HideRibbon();
   bool            IsRibbonVisible() const { return m_isRibbonVisible; }

   //--- Hook for subclasses that route the Settings action button into a settings window
   virtual void    OpenSettingsWindowForObject(int objId) { }

   //--- Force a full re-render of the ribbon canvas
   void            RedrawRibbon();

   //--- Mouse routing + grip hit-test (returns local coords via out-params)
   bool            HitTestOverRibbon(int mouseX, int mouseY, int &lx, int &ly);
   bool            HitTestOverRibbonGrip(int lx, int ly);
   bool            RibbonMouseMove(int mouseX, int mouseY, uint mouseButtons);
   bool            RibbonMouseDown(int mouseX, int mouseY);
   bool            RibbonMouseUp();

   //--- Popover lifecycle + show for a specific property id (optionally with flip anchor)
   bool            CreatePopoverCanvas();
   void            DestroyPopoverCanvas();
   void            ShowPopoverForProperty(string propId, int anchorX, int anchorY,
                                            int flipBaseTop = -1);
   void            HidePopover();
   bool            IsPopoverVisible() const { return m_isPopoverVisible; }
   void            RedrawPopover();
   bool            HitTestOverPopover(int mouseX, int mouseY, int &lx, int &ly);
   bool            PopoverMouseDown(int mouseX, int mouseY);
   bool            PopoverMouseMove(int mouseX, int mouseY, uint mouseButtons);
   bool            PopoverMouseUp();

   //--- Popover keyboard handler (for the editable opacity box) + caret-blink tick
   bool            PopoverKeyDown(uint keyCode);
   void            RibbonTick();

   //--- Selection-changed hook from CSidebarRenderer - show/hide ribbon based on the new selection
   virtual void    OnSelectionChanged(int objId)
     {
      //--- Positive objId = a single drawn object was selected; otherwise hide
      if(objId >= 0)
         ShowRibbonFor(objId);
      else
         HideRibbon();
     }

   //--- Hook for subclasses that need to know when the popover just closed (e.g., refocus a parent)
   virtual void    OnPopoverClosed() { /* default no-op */ }

protected:
   //--- Position calculation + clamp-to-chart-bounds + apply to the chart object
   void            CalcRibbonDefaultPosition(int &outX, int &outY);
   void            ClampRibbonToChart();
   void            ApplyRibbonPosition();
   //--- Draw the 2x4 grip-dot pattern on the left edge
   void            DrawGripDots(CCanvas &canvas, int hrScale);

   //--- Rebuild the filtered descriptor list + compute the ribbon's content-width
   void            RefreshRibbonProperties();
   int             CalcRibbonContentWidth();
   void            DrawRibbonIcons();
   int             HitTestRibbonIcon(int lx, int ly);
   void            GetRibbonIconBounds(int idx, int &outX, int &outY, int &outSize);
   int             GetRibbonIconWidthForType(PROP_TYPE t);

   //--- Popover positioning + flip-when-clipped logic
   void            ApplyPopoverPosition();
   void            ClampPopoverToChart();
   void            CalcPopoverPositionForAnchor(int anchorX, int anchorY,
                                                  int popoverW, int popoverH,
                                                  int &outX, int &outY,
                                                  int flipBaseTop = -1);

   //--- Commit the opacity-box text buffer back to the live opacity property
   void            CommitBoxEditToOpacity();
  };

First, we guard the header and pull in three includes — the Sidebar base class, the property descriptor system, and the widget renderers — so the ribbon has both the descriptors that tell it what to show and the widgets that draw each control.

Then, we give the ribbon two backing canvases, a visible one and a high-resolution supersample canvas behind it, alongside the fields that track its dimensions, its current and last-known position (so we can restore it after a hide and show), its visibility, and the ID of the drawn object it currently edits. We also hold the drag state for moving the ribbon by its grip, the icon-row geometry, the hovered icon index, and the filtered descriptor list — the ribbon-eligible subset of what "BuildPropertyListForTool" returns.

We also define a second field set for the popover: its canvases, visibility, active property ID, and screen rectangle. Additional fields track opacity-slider dragging, the hovered item, and inline opacity-box editing (buffer, caret position, blink state). This allows exact opacity input, not only slider dragging.

On the public side, we declare the canvas lifecycle, the show and hide bound to an object ID, the ribbon mouse routing, the popover lifecycle with its mouse and keyboard handlers, and the "RibbonTick" caret-blink driver. We override "OnSelectionChanged" — the hook inherited from the sidebar — to show the ribbon for a positive object ID and hide it otherwise, which is what makes the ribbon appear and disappear as the selection changes.

Finally, we keep the internals protected — the position calculation and clamp-to-chart logic, the grip-dot drawing, the descriptor-list rebuild and content-width math, the icon layout and hit-testing, the popover positioning with its flip-when-clipped logic, and the commit of the opacity-box text back to the live property. The ribbon implementation follows the same approach as the sidebar and tool flyouts, since it is mainly rendering and registry work. That marks the end of the implementation. What remains is testing the new features when wired into the shell, and that is handled in the next section.


Visualization

We compile the program, attach it to the chart, and select objects one at a time to confirm the ribbon surfaces the right controls and edits them live.

BACKTEST GIF

During testing, the ribbon popped up beside each selection and showed only the properties that applied to that tool, and clicking an icon opened the matching popover — the swatch grid for colors, the preview rows for width and style. Picking a color or dragging the opacity slider updated the object on the chart in real time, typing an exact value into the opacity box worked the same way, and dragging the ribbon by its grip moved it without disturbing the selection underneath.


Conclusion

In conclusion, we added a per-object property-editing ribbon to the canvas drawing layer in MQL5, built on four new files: a descriptor system that declares what each tool exposes, an engine-side get/set API with snapshot-based live preview, widget renderers for the color picker, dropdowns, and compact rows, and the floating ribbon itself that binds to the current selection. We wired it into the event loop through the inheritance chain. Selecting an object shows only the applicable properties. Clicking an icon opens the matching popover. Changing a value updates the object in real time, and canceling restores the snapshot taken when the ribbon opened. After reading this article, you will be able to:

  • Build a descriptor-driven property system where each tool registers what it exposes as plain data, and one generic pipeline renders and edits every tool.
  • Implement an engine-side get and set API keyed by string property IDs, with snapshot and restore that makes a dragged-slider preview live yet undo-safe.
  • Assemble a floating, draggable ribbon that binds to the current selection, lays out per-property icons, and opens color, width, and style popovers wired to the engine.

In the next part of the series, we will build the full tabbed settings window that the ribbon's Settings button opens, with Style, Text, Coordinates, and Visibility tabs, editable per-level rows, and direct coordinate and numeric input for the selected object. Stay tuned.


Attachments

S/N
NameTypeDescription
1Tools Palette Part 8.zip
Archive
A ready-to-extract archive containing all 18 project files in a single folder. Unzip it into your MetaTrader 5 terminal data folder; the files will be placed under MQL5/Experts/ with every file in its correct location, ready to compile.
Attached files |
Shape of Price: An Introduction to TDA and Takens Embedding in MQL5 Shape of Price: An Introduction to TDA and Takens Embedding in MQL5
The article presents a practical foundation for shape analysis of price series in MQL5. It implements Takens time‑delay embedding to build a phase‑space point cloud and computes the full pairwise distance matrix under selectable norms. The CTDAPointCloud and CTDADistance classes are provided with a demo script that embeds chart data and outputs results, preparing inputs for downstream topological tools.
Price Action Analysis Toolkit Development (Part 73): Building a Weekend Gap Trading Signal System in MQL5 Price Action Analysis Toolkit Development (Part 73): Building a Weekend Gap Trading Signal System in MQL5
We extend the weekend gap toolkit with an indicator that turns gap structure into tradeable signals. When price confirms back into the gap, the indicator issues buy/sell arrows, sets TP at the opposite edge, and places SL using current-week extremes. It maintains non-repainting behavior, reconstructs historical signals, updates live, and provides EA-ready buffers for entry markers and TP/SL to support automation.
Community of Scientists Optimization (CoSO): Theory Community of Scientists Optimization (CoSO): Theory
Secrets of effective optimization of trading strategies in metaheuristic approaches. Community of Scientists Optimization is a new population-based algorithm inspired by the mechanisms of the scientific community. Unlike traditional nature-inspired metaphors, CoSO models unique aspects of human scientific activity: publishing results in journals, competing for grants, and forming research teams.
From Static MA to Adaptive Filtering (Part 2): Implementing the SAMA_NLMS Indicator in MQL5 From Static MA to Adaptive Filtering (Part 2): Implementing the SAMA_NLMS Indicator in MQL5
This article implements the NLMS-based Self-Adaptive Moving Average as a working MQL5 indicator. It provides the complete source code and explains the key design choices, including inline execution, uniform weight seeding, closed‑bar updates, and stability bounds, along with installation, usage, and limitations. The result is a compiled, chart‑ready SAMA_NLMS indicator and a clear basis for subsequent EA benchmarking.