preview
MQL5 Trading Tools (Part 39): Adding a Pinned-Tools Ribbon for Quick Access to Favorite Tools

MQL5 Trading Tools (Part 39): Adding a Pinned-Tools Ribbon for Quick Access to Favorite Tools

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

Introduction

In the previous article (Part 38), we completed a deep, tabbed settings window that gives a full editor for any selected object. That rich, organized sidebar is the right home for discovery and detailed control, but it imposes avoidable friction for repeated actions: in a typical session you reach for the same 3–7 tools (trendlines, Fibonacci, etc.) dozens of times, yet each draw requires three steps — open the sidebar, open a category, find the row. Simply "speeding up" the sidebar is insufficient because the sidebar's purpose is discovery and organization; a different, flat quick-access layer is required for frequent, one-click workflows without corrupting the catalog.

To address this, we add a pinned-tools ribbon — a second floating bar that holds only favorites, one click away, and remains hidden when empty. The implementation introduces three integrated components: an ordered pin set in the engine (append/remove/contains + toggle), a shared anti-aliased pushpin glyph used by the sidebar tile, flyout rows, and ribbon, and a draggable, resizable, horizontally scrollable ribbon with offscreen clipping and a floating scrollbar thumb. The solution is judged ready when it supports pin/unpin from the flyout, shows the Pinned category only when nonempty, preserves pin order, activates tools on click, and offers smooth drag/resize/scroll behavior.

This article is written for the intermediate-to-advanced MetaQuotes Language 5 (MQL5) developer comfortable with the inheritance chain and the floating-surface machinery we built up across the previous parts. The subtopics we will cover are:

  1. From a Deep Sidebar to One-Click Favorites
  2. Building the Pin Model and the Pushpin Affordance
  3. Implementing the Pinned-Tools Ribbon
  4. Visualization
  5. Conclusion

The sections ahead cover the motivation, the pin model and glyph, the flyout affordance, and the ribbon implementation and visualization.


From a Deep Sidebar to One-Click Favorites

The sidebar and the pinned ribbon answer two different questions. The sidebar answers "what tools exist and how are they organized," so it sorts forty-plus tools into categories behind flyouts — ideal for discovery, but a three-step trip every time we draw. The pinned ribbon answers "what do I reach for constantly," so it is flat, small, and always one click away. Rather than replace the sidebar, we layer the ribbon beside it as a fast lane for the few tools we live in, while the sidebar stays the organized home for everything. This is how the native MetaTrader 5 terminal objects structure handles its pinned/favorite/starred objects and the ordered flyouts, which we want to use as a guide. See below.

MT5 PINNED CATEGORIES

In our case, we build this on the same foundation that the property ribbon has already established. The pinned ribbon is a floating bar with its own canvas, shadow, and drag behavior. It sits in the inheritance chain above the settings window, inherits the shared machinery, and receives events through the same shell routing as other floating surfaces. The set of pinned tools lives in the engine as an ordered list, so the order we pin in is the order the icons appear, and a pushpin control on each flyout row toggles a tool in and out of that list. A pinned-tools tile in the sidebar shows or hides the ribbon, and it stays hidden until the list has at least one entry.

A few genuinely new mechanics sit on top of that. We need an ordered pin set (append/remove/contains) and an anti-aliased pushpin glyph shared by the sidebar tile, flyout rows, and the ribbon. We also need the ribbon to behave well when favorites pile up: a user-resizable width, horizontal scrolling with a thumb when the icons overflow, and an offscreen scratch canvas to clip the scrolled icons cleanly against the bar's rounded edges. See our visualized objectives below.

PINNED RIBBON ARCHITECTURE

With that relationship clear, we can move on to building the pin model and the pushpin affordance.


Building the Pin Model and the Pushpin Affordance

The Pin Storage and the Pushpin Glyph

We implement the pin model in the Tools file, and the first piece is the storage plus the pushpin glyph that every pin affordance draws — the sidebar tile, the flyout row, and the ribbon all render the same pin through one routine.

//--- WE IMPLEMENT THE PIN MODEL IN THE TOOLS FILE
//--- ToolsPalette_Tools.mqh

//+------------------------------------------------------------------+
//| CToolRegistry owns category + tool definitions and last-used map |
//+------------------------------------------------------------------+
class CToolRegistry : public CAnnotationTools
  {
protected:
   //--- Category catalog indexed by ENUM_CATEGORY (the full set of registered categories)
   CategoryDefinition m_categories[CAT_COUNT];
   //--- Per-category memory of the last tool the user selected (drives sidebar tile icon)
   TOOL_TYPE     m_lastUsedToolPerCategory[CAT_COUNT];
   //--- Ordered list of pinned tool types (array order = pin order, drives the pinned ribbon)
   TOOL_TYPE     m_pinnedTools[];

protected:
   //--- EXISTING METHODS
   
   //--- Draw the tilted-pushpin glyph anti-aliased (outline when unpinned, solid when pinned)
   void          DrawPinGlyphOnCanvas(CCanvas &canvas, int cx, int cy, int size,
                                        uint argb, bool filled);
   //--- Even-odd point-in-polygon test (drives the pin glyph's anti-aliased coverage fill)
   bool          PinPointInPoly(const double &px[], const double &py[], int n, double x, double y);
   //--- Source-over composite of argb at fractional coverage onto a canvas pixel
   void          PinBlendAA(CCanvas &canvas, int ix, int iy, uint argb, double cov);
   //--- Pinned-tools list management (ordered append/remove + queries)
   int           PinnedCount();
   TOOL_TYPE     GetPinnedTool(int idx);
   bool          IsToolPinned(TOOL_TYPE toolType);
   void          PinTool(TOOL_TYPE toolType);
   void          UnpinTool(TOOL_TYPE toolType);
   bool          ToggleToolPin(TOOL_TYPE toolType);
   //--- True when a category tile should currently be shown (CAT_PINNED hides while empty)
   bool          IsCategoryVisible(ENUM_CATEGORY cat);
   //--- Populate all categories and their tool lists (calls AddTool for every registered tool)
   void          InitAllCategoriesAndTools();
  };


//+------------------------------------------------------------------+
//| Even-odd point-in-polygon test (ray-cast parity)                 |
//+------------------------------------------------------------------+
bool CToolRegistry::PinPointInPoly(const double &px[], const double &py[], int n, double x, double y)
  {
   //--- Standard ray-cast: count edge crossings to the right of the point
   bool inside = false;
   int  j = n - 1;
   for(int i = 0; i < n; i++)
     {
      //--- Edge (j -> i) straddles the horizontal ray and crosses to the right of x
      if(((py[i] > y) != (py[j] > y)) &&
         (x < (px[j] - px[i]) * (y - py[i]) / (py[j] - py[i]) + px[i]))
         inside = !inside;
      j = i;
     }
   return inside;
  }

//+------------------------------------------------------------------+
//| Source-over composite of argb at fractional coverage onto canvas |
//+------------------------------------------------------------------+
void CToolRegistry::PinBlendAA(CCanvas &canvas, int ix, int iy, uint argb, double cov)
  {
   //--- Clamp coverage and fold it into the source alpha
   if(cov <= 0.0) return;
   if(cov > 1.0) cov = 1.0;
   double sA = (double)((argb >> 24) & 0xFF) / 255.0 * cov;
   if(sA <= 0.0) return;
   //--- Read the destination pixel and compute the composited output alpha
   uint   ex = canvas.PixelGet(ix, iy);
   double dA = (double)((ex >> 24) & 0xFF) / 255.0;
   double oA = sA + dA * (1.0 - sA);
   if(oA <= 0.0) return;
   //--- Source-over blend per channel (un-premultiplied result)
   double sR = (double)((argb >> 16) & 0xFF) / 255.0;
   double sG = (double)((argb >>  8) & 0xFF) / 255.0;
   double sB = (double)( argb        & 0xFF) / 255.0;
   double dR = (double)((ex >> 16) & 0xFF) / 255.0;
   double dG = (double)((ex >>  8) & 0xFF) / 255.0;
   double dB = (double)( ex        & 0xFF) / 255.0;
   uchar  oR = (uchar)((sR * sA + dR * dA * (1.0 - sA)) / oA * 255.0 + 0.5);
   uchar  oG = (uchar)((sG * sA + dG * dA * (1.0 - sA)) / oA * 255.0 + 0.5);
   uchar  oB = (uchar)((sB * sA + dB * dA * (1.0 - sA)) / oA * 255.0 + 0.5);
   uchar  oAb = (uchar)(oA * 255.0 + 0.5);
   canvas.PixelSet(ix, iy, ((uint)oAb << 24) | ((uint)oR << 16) | ((uint)oG << 8) | (uint)oB);
  }

//+------------------------------------------------------------------+
//| Draw the tilted-pushpin glyph (anti-aliased coverage fill)       |
//+------------------------------------------------------------------+
void CToolRegistry::DrawPinGlyphOnCanvas(CCanvas &canvas, int cx, int cy, int size,
                                          uint argb, bool filled)
  {
   //--- Pushpin silhouette in local units, traced clockwise: cap bar, barrel, flared skirt, needle to the sharp tip (vertex 6)
   double lx[13] = { -11, -11,  -7,  -7, -11, -3.5,  0, 3.5,  11,   7,   7,  11,  11 };
   double ly[13] = {   0,   5,   5,  16,  21,   21, 35,  21,  21,  16,   5,   5,   0 };
   const int n = 13;
   //--- Local centroid (rotation pivot + the focus the outline erodes toward)
   const double pivotX = 0.0, pivotY = 13.15;
   //--- Uniform scale so the silhouette fits the requested icon size
   const double s = (double)size / 42.0;
   //--- Tilt clockwise so the cap sits upper-right and the tip points lower-left (matches the reference pin)
   const double ang = 40.0 * 3.14159265358979 / 180.0;
   const double co = MathCos(ang), si = MathSin(ang);
   //--- Outer silhouette transformed into canvas pixels
   double ox[13], oy[13];
   for(int i = 0; i < n; i++)
     {
      const double dx = (lx[i] - pivotX) * s;
      const double dy = (ly[i] - pivotY) * s;
      ox[i] = (double)cx + dx * co - dy * si;
      oy[i] = (double)cy + dx * si + dy * co;
     }
   //--- Outline stroke width in pixels (the inner silhouette is the outer eroded by this)
   const double strokePx = MathMax(1.6, size * 0.10);
   //--- Inner silhouette: erode each vertex toward the centroid in LOCAL units, then apply the SAME scale/tilt as the outer
   const double insL = strokePx / s;
   double qx[13], qy[13];
   for(int i = 0; i < n; i++)
     {
      const double vx = lx[i] - pivotX, vy = ly[i] - pivotY;
      const double len = MathSqrt(vx * vx + vy * vy);
      const double f = (len > 1e-6) ? MathMax(0.0, len - insL) / len : 0.0;
      const double dx = vx * f * s;
      const double dy = vy * f * s;
      qx[i] = (double)cx + dx * co - dy * si;
      qy[i] = (double)cy + dx * si + dy * co;
     }
   //--- Pixel bounding box of the outer silhouette (1 px guard for the AA fringe)
   double minX = ox[0], maxX = ox[0], minY = oy[0], maxY = oy[0];
   for(int i = 1; i < n; i++)
     {
      if(ox[i] < minX) minX = ox[i];
      if(ox[i] > maxX) maxX = ox[i];
      if(oy[i] < minY) minY = oy[i];
      if(oy[i] > maxY) maxY = oy[i];
     }
   const int bbL = (int)MathFloor(minX) - 1, bbR = (int)MathCeil(maxX) + 1;
   const int bbT = (int)MathFloor(minY) - 1, bbB = (int)MathCeil(maxY) + 1;
   //--- 4x4 supersampling configuration for the coverage estimate
   const int    sub  = 4;
   const double step = 1.0 / sub;
   const double subN = (double)(sub * sub);
   //--- Walk the bounding box: solid = inside-outer coverage, outline = (inside-outer minus inside-inner) ring coverage
   for(int py = bbT; py <= bbB; py++)
     {
      for(int px = bbL; px <= bbR; px++)
        {
         int inO = 0, inI = 0;
         for(int a = 0; a < sub; a++)
            for(int b = 0; b < sub; b++)
              {
               const double sxp = (double)px + (a + 0.5) * step;
               const double syp = (double)py + (b + 0.5) * step;
               if(PinPointInPoly(ox, oy, n, sxp, syp))
                 {
                  inO++;
                  if(!filled && PinPointInPoly(qx, qy, n, sxp, syp)) inI++;
                 }
              }
         if(inO == 0) continue;
         //--- Coverage fraction for this pixel, then composite the glyph color
         const double cov = filled ? (double)inO / subN : (double)(inO - inI) / subN;
         PinBlendAA(canvas, px, py, argb, cov);
        }
     }
  }

First, we add the storage as an ordered array of tool types on "CToolRegistry", where the array order is the pin order that later drives the ribbon's icon sequence. Alongside it, we declare the glyph trio and the management API — "PinnedCount", "GetPinnedTool", "IsToolPinned", "PinTool", "UnpinTool", and "ToggleToolPin" — whose bodies we cover next; here we focus on the three routines that actually paint the pushpin.

We start at the bottom with "PinPointInPoly", a standard even-odd ray-cast test. We walk each polygon edge and count how many cross the horizontal ray to the right of the query point, and an odd count means the point sits inside. This is what lets us fill an arbitrary silhouette by coverage rather than relying on a built-in shape primitive.

We then composite with "PinBlendAA", a source-over blend that takes a color and a fractional coverage. We fold the coverage into the source alpha, read the destination pixel, and blend the two per channel into an un-premultiplied result, so a partially covered edge pixel mixes smoothly with whatever is already on the canvas instead of producing a hard jagged edge.

Finally, "DrawPinGlyphOnCanvas" renders the pin. It traces a 13-vertex silhouette, scales it to the requested size, and rotates it 40 degrees so the cap sits upper-right and the tip points lower-left. For the outline case, we build a second, inner silhouette by eroding each vertex toward the centroid by the stroke width, and then we walk the bounding box with four-by-four supersampling: a solid pin takes the coverage inside the outer silhouette, while an outline pin takes the ring coverage of the outer minus the inner. The "filled" flag is what switches the same routine between a pinned tool (solid) and an unpinned one (outline). You can use any geometry math you are comfortable with. If you choose the pin glyph as we did, here is its architecture.

PIN GLYPH ARCHITECTURE

With the glyph in hand, we move on to the pin management API that decides when a tool is pinned.

The Pin Management API

Here, we back the pin set with a small ordered-list API that the flyout affordance and the ribbon both call to read and mutate which tools are pinned.

//+------------------------------------------------------------------+
//| Number of currently pinned tools                                 |
//+------------------------------------------------------------------+
int CToolRegistry::PinnedCount()
  {
   return ArraySize(m_pinnedTools);
  }

//+------------------------------------------------------------------+
//| Pinned tool at the given index (TOOL_NONE when out of range)     |
//+------------------------------------------------------------------+
TOOL_TYPE CToolRegistry::GetPinnedTool(int idx)
  {
   //--- Defensive bounds check
   if(idx < 0 || idx >= ArraySize(m_pinnedTools)) return TOOL_NONE;
   return m_pinnedTools[idx];
  }

//+------------------------------------------------------------------+
//| True when the given tool is already pinned                       |
//+------------------------------------------------------------------+
bool CToolRegistry::IsToolPinned(TOOL_TYPE toolType)
  {
   //--- Linear scan over the ordered pinned list
   const int n = ArraySize(m_pinnedTools);
   for(int i = 0; i < n; i++)
      if(m_pinnedTools[i] == toolType) return true;
   return false;
  }

//+------------------------------------------------------------------+
//| Append a tool to the pinned list (no-op if invalid or duplicate) |
//+------------------------------------------------------------------+
void CToolRegistry::PinTool(TOOL_TYPE toolType)
  {
   //--- Reject the empty tool and anything already pinned (keeps pin order stable)
   if(toolType == TOOL_NONE) return;
   if(IsToolPinned(toolType)) return;
   //--- Grow the ordered list by one and store the new tool at the end
   const int sz = ArraySize(m_pinnedTools);
   ArrayResize(m_pinnedTools, sz + 1);
   m_pinnedTools[sz] = toolType;
  }

//+------------------------------------------------------------------+
//| Remove a tool from the pinned list, compacting the order         |
//+------------------------------------------------------------------+
void CToolRegistry::UnpinTool(TOOL_TYPE toolType)
  {
   const int n = ArraySize(m_pinnedTools);
   //--- Find the tool's slot in the ordered list
   int found = -1;
   for(int i = 0; i < n; i++)
      if(m_pinnedTools[i] == toolType) { found = i; break; }
   if(found < 0) return;
   //--- Shift every later entry down one slot to close the gap
   for(int i = found; i < n - 1; i++)
      m_pinnedTools[i] = m_pinnedTools[i + 1];
   //--- Drop the now-duplicated final slot
   ArrayResize(m_pinnedTools, n - 1);
  }

//+------------------------------------------------------------------+
//| Toggle a tool's pinned state; returns the resulting state        |
//+------------------------------------------------------------------+
bool CToolRegistry::ToggleToolPin(TOOL_TYPE toolType)
  {
   //--- Already pinned -> unpin and report the new "off" state
   if(IsToolPinned(toolType)) { UnpinTool(toolType); return false; }
   //--- Otherwise pin and report the new "on" state
   PinTool(toolType);
   return true;
  }

//+------------------------------------------------------------------+
//| True when a category tile should currently be drawn              |
//+------------------------------------------------------------------+
bool CToolRegistry::IsCategoryVisible(ENUM_CATEGORY cat)
  {
   //--- Out-of-range categories are never visible
   if((int)cat < 0 || (int)cat >= CAT_COUNT) return false;
   //--- The pinned tile only appears once at least one tool is pinned
   if(cat == CAT_PINNED) return (ArraySize(m_pinnedTools) > 0);
   //--- Every other category tile is always visible
   return true;
  }

We keep the three queries trivial — "PinnedCount" returns the array size, "GetPinnedTool" returns the tool at an index with a bounds check that falls back to the empty tool, and "IsToolPinned" linear-scans the list for a match. The list is short by nature, so a scan is more than fast enough, and we avoid the overhead of a separate lookup structure.

We mutate the list with "PinTool" and "UnpinTool". In "PinTool", we reject the empty tool and anything already pinned, then grow the array by one and store the new tool at the end, so pinning always appends and the existing pin order stays stable. In "UnpinTool", we find the tool's slot, shift every later entry down one to close the gap, and shrink the array, which preserves the relative order of everything that remains.

We wrap those in "ToggleToolPin", which unpins when the tool is already pinned and pins it otherwise, returning the resulting on or off state so the caller can update its own visuals in one call. We then add "IsCategoryVisible", which reports whether a category tile should draw — every normal category is always visible, but the pinned tile only appears once the list holds at least one tool, which is what keeps it hidden on an empty toolkit. With the model complete, we move on to the pushpin affordance in the sidebar flyout.

The Flyout Pin Affordance

We move to the sidebar file and add the pin affordance to the flyout — a hover field that tracks which row's pin is lit, a hit-tester that maps the cursor to a row's pin icon, and the pushpin that marks the Pinned category tile.

//--- WE IMPLEMENT THE PIN MODEL IN THE TOOLS FILE
//--- ToolsPalette_Sidebar.mqh

//+------------------------------------------------------------------+
//| CFlyoutPanel owns the tool-list flyout layout and rendering      |
//+------------------------------------------------------------------+
class CFlyoutPanel : public CSidebarLayout
  {
protected:
   //--- EXISTING MEMBERS
   
   //--- Row index whose pin icon is hovered (-1 = none)
   int           m_hoveredFlyoutPin;
   
   //--- EXISTING MEMBERS
protected:
   //--- EXISTING METHODS

   //--- Hit-test cursor against a row's pin icon + return the matched row index (-1 = none)
   int           HitTestFlyoutPin(int lx, int ly);
   
   //--- EXISTING MEMBERS
  };


//+------------------------------------------------------------------+
//| Hit-test a row's pin icon; returns the row index or -1           |
//+------------------------------------------------------------------+
int CFlyoutPanel::HitTestFlyoutPin(int lx, int ly)
  {
   //--- Pins exist only on regular tool rows (action categories have none)
   if(m_flyoutActiveCat == CAT_NONE || IsActionCategory(m_flyoutActiveCat)) return -1;
   //--- Resolve the row under the cursor using the same geometry as HitTestFlyoutItem
   int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools);
   int titleH = 26, dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0;
   int visibleTools = MathMin(nTools, m_flyoutMaxVisibleItems);
   int itemClipTop = titleH + m_flyoutPadding;
   int itemClipBot = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight;
   if(ly < itemClipTop || ly >= itemClipBot) return -1;
   int idx = (ly - itemClipTop + m_flyoutScrollPixels) / m_flyoutItemHeight;
   if(idx < 0 || idx >= nTools) return -1;
   //--- The pin occupies its cubicle at the row's trailing edge (kept in sync with the draw geometry: 3px inset, 16px pin)
   int pinPad = 3;
   int rowBgR = dispBx + m_flyoutWidth - m_flyoutPadding;
   int zoneR  = rowBgR;
   int zoneL  = rowBgR - pinPad - (16 + 2 * pinPad);
   if(lx < zoneL || lx > zoneR) return -1;
   return idx;
  }

//+------------------------------------------------------------------+
//| Render category icons + sidebar control labels on display canvas |
//+------------------------------------------------------------------+
void CSidebarRenderer::DrawSidebarIconLabels(TOOL_TYPE activeTool)
  {
   for(int c = 0; c < CAT_COUNT; c++)
     {
      //--- EXISTING LOGIC

      //--- Action categories take a custom canvas icon (trash bin for Delete, pushpin for Pinned)
      if(IsActionCategory((ENUM_CATEGORY)c))
        {
         //--- Pinned is non-destructive (white on the active-blue bg / on hover, normal pin color otherwise); Delete stays danger-red
         color actionIconColor;
         if((ENUM_CATEGORY)c == CAT_PINNED)
            actionIconColor = (IsPinnedTileActive() || m_hoveredCategory == (ENUM_CATEGORY)c)
                               ? m_themeColors.buttonIconActiveColor   // white on the active blue / on hover
                               : m_themeColors.buttonIconColor;        // normal pin color
         else
            actionIconColor = (isActive || m_hoveredCategory == (ENUM_CATEGORY)c)
                               ? C'255,80,80'      // brighter red on hover/active
                               : C'220,53,69';     // standard "danger" red
         drewCanvasIcon = DrawActionCategoryIconOnCanvas(tmpIcons,
                                                          (ENUM_CATEGORY)c,
                                                          tileCx, tileCy,
                                                          FlyoutIconSize,
                                                          actionIconColor);
        }
        
      //--- EXISTING LOGIC
      
     }
  }

Here, we add "m_hoveredFlyoutPin" to hold the row index whose pin is under the cursor, which drives both the hover brightening and the unpin slash we overlay when a pinned tool's pin is hovered. We then resolve clicks with "HitTestFlyoutPin". Since only regular tool rows carry a pin, the action categories have none — we bail immediately on an action or empty category. We locate the row under the cursor with the same geometry the item hit-test uses, accounting for the scroll offset and the visible-item clip, then confirm the cursor falls inside the pin's cubicle at the row's trailing edge, a fixed zone sized to the inset and pin width that we keep in sync with the draw side. A hit returns the row index; anything else returns the no-hit sentinel.

We then color the Pinned tile's icon in "DrawSidebarIconLabels", where the action categories take a custom canvas glyph rather than a tool icon. We treat the pinned tile as non-destructive, so its pushpin draws white when the tile is active or hovered and in the normal pin color otherwise, while the Delete tile stays danger-red since it clears objects. Both route through the shared action-icon drawer, which dispatches the pinned case to the same pushpin glyph we built in the Tools file.

The per-row pushpin is drawn in the flyout item render, reusing the glyph with a darkened rounded cubicle behind it and a brightening when its row is the hovered pin, keyed off "m_hoveredFlyoutPin". What the pin will do is communicated through a tooltip rather than a painted mark — the shell's mouse-move routing sets the flyout object's tooltip to "Add to Pinned Tools" or "Remove from Pinned Tools" depending on whether the hovered row's tool is already pinned. We removed the dot convention in the flyout items to use that space for the pin cubicle since it was redundant; we had it in the sidebar as well. Also, we updated the flyout category widths so they are dynamic to the maximum item width instead of rendering the entire width as a constant and leaving unused space. See the illustrations below.

Before the update.

BEFORE THE PIN UPDATE

After the update.

AFTER THE PIN UPDATE

With the affordance in place — clicking a row's pin toggles it, and the Pinned tile appears once the first tool is pinned — we move on to the pinned-tools ribbon itself.


Implementing the Pinned-Tools Ribbon

Declaring the Pinned-Tools Ribbon

We declare the pinned ribbon as "CPinnedRibbon", extending "CSettingsWindow" so it sits at the top of the floating-surface chain and inherits the canvas, theme, and event machinery everything below it provides.

//+------------------------------------------------------------------+
//|                                    ToolsPalette_RibbonPinned.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_PINNED_MQH
#define TOOLS_PALETTE_RIBBON_PINNED_MQH

//--- Pull in the settings window (and the whole chain below it) so we can extend it
#include "ToolsPalette_Settings.mqh"

//+------------------------------------------------------------------+
//| CPinnedRibbon - second ribbon holding pinned tool icons          |
//+------------------------------------------------------------------+
class CPinnedRibbon : public CSettingsWindow
  {
protected:
   //--- Backing chart-object name + display canvas
   string          m_namePinned;
   CCanvas         m_canvasPinned;
   //--- Persistent offscreen buffer for clipping scrolled icons (created once, never per-frame)
   CCanvas         m_canvasPinnedScratch;

   //--- Visibility + logical top-left position (excludes the shadow halo)
   bool            m_isPinnedVisible;
   int             m_pinnedX;
   int             m_pinnedY;
   //--- Cached position so reopening restores the user's last placement
   int             m_lastPinnedX;
   int             m_lastPinnedY;
   bool            m_lastPinnedValid;

   //--- Current content dimensions (recomputed whenever the pin set / window changes)
   int             m_pinnedWidth;
   int             m_pinnedHeight;
   //--- Shadow halo padding + body corner radius (matches the properties ribbon)
   int             m_pinnedShadowPad;
   int             m_pinnedCornerRadius;

   //--- Layout constants: grip strip width, icon size/gap, row padding
   int             m_pinnedGripWidth;
   int             m_pinnedIconSize;
   int             m_pinnedIconGap;
   int             m_pinnedIconRowPadX;
   //--- Icon band height (the icons + floating scrollbar live within this band)
   int             m_pinnedIconBandH;
   //--- Trailing-edge resize handle width
   int             m_pinnedResizeHandleW;

   //--- Visible window WIDTH in pixels (user-resizable). Large default = auto (grow to fit all icons)
   int             m_pinnedUserWidthPx;
   //--- Cached layout results from the last RecomputePinnedLayout pass
   int             m_pinnedEffViewportW;
   bool            m_pinnedNeedsScroll;
   //--- True when the trailing resize grip is active (from 2 items upward)
   bool            m_pinnedShowResize;
   //--- Pixel scroll offset of the content within the visible window (smooth, like the sidebar)
   int             m_pinnedScrollPixels;

   //--- Hover state: which pinned tool index is under the cursor (-1 = none)
   int             m_hoveredPinnedIdx;
   bool            m_hoveredPinnedScrollbar;
   //--- True while the cursor is over the scrollbar thumb specifically (drives its color)
   bool            m_hoveredPinnedThumb;
   //--- True while the cursor is over the trailing resize grip (drives its color)
   bool            m_hoveredPinnedResize;

   //--- Drag state (whole-ribbon move via the grip)
   bool            m_isPinnedDragging;
   int             m_pinnedDragOffsetX;
   int             m_pinnedDragOffsetY;
   //--- Resize state (trailing grip widens/narrows the window in pixels, smooth)
   bool            m_isPinnedResizing;
   int             m_pinnedResizeGrabX;
   int             m_pinnedResizeStartPx;
   //--- Scrollbar-thumb drag state (pixel-based, mirrors the sidebar thumb drag)
   bool            m_isPinnedThumbDragging;
   int             m_pinnedThumbDragStartX;
   int             m_pinnedThumbDragStartPixels;

protected:
   //--- Recompute width/height + clamp the visible window and scroll offset
   void            RecomputePinnedLayout();
   //--- Default spawn position (stacked just under the properties ribbon)
   void            CalcPinnedDefaultPosition(int &outX, int &outY);
   //--- Clamp the ribbon so its body always stays inside the chart
   void            ClampPinnedToChart();
   //--- Push the logical position to the chart object (offset by the shadow halo)
   void            ApplyPinnedPosition();
   //--- Body-local X where the icon row begins (just past the grip + left pad)
   int             PinnedIconRowLeft()  const { return m_pinnedGripWidth + m_pinnedIconRowPadX; }
   //--- Body-local Y of the icon row top (icons vertically centered in the band)
   int             PinnedIconRowTop()   const { return (m_pinnedIconBandH - m_pinnedIconSize) / 2; }
   //--- Effective visible-window width in pixels (cached each layout pass)
   int             PinnedViewportW()    const { return m_pinnedEffViewportW; }
   //--- Full content width (all pinned icons laid end to end)
   int             PinnedContentW();
   //--- Largest viewport width that still keeps the whole ribbon inside the chart
   int             PinnedMaxFitPx();
   //--- Grow the offscreen bitmap to the chart-max (only when needed; never during a drag)
   void            EnsurePinnedBitmap();
   //--- Crop the visible window (OBJPROP_XSIZE/YSIZE) to the current ribbon size
   void            CropPinnedCanvas();
   //--- Maximum horizontal scroll offset in pixels
   int             PinnedMaxScrollPx();
   //--- Trailing resize-handle bounds in ribbon-local coordinates
   void            GetPinnedResizeHandleRect(int &outL, int &outT, int &outR, int &outB);
   //--- Horizontal scrollbar thumb bounds in ribbon-local coordinates (false when no scroll)
   bool            GetPinnedThumbRect(int &outL, int &outT, int &outR, int &outB);
   //--- Hit-tests against the grip strip / a visible icon (returns the pinned-tool index)
   bool            HitTestPinnedGrip(int lx, int ly);
   int             HitTestPinnedIcon(int lx, int ly);
   //--- Draw a tool's glyph (custom canvas icon, else its Wingdings fallback)
   void            DrawPinnedToolGlyph(CCanvas &canvas, TOOL_TYPE toolType,
                                         int cx, int cy, int size, color glyphColor);

public:
   //--- Canvas lifecycle
   bool            CreatePinnedCanvas();
   void            DestroyPinnedCanvas();
   //--- Show / hide / toggle the ribbon
   void            ShowPinnedRibbon();
   void            HidePinnedRibbon();
   void            TogglePinnedRibbon();
   bool            IsPinnedRibbonVisible() const { return m_isPinnedVisible; }
   //--- Sidebar Pinned tile renders active (blue) while the pinned ribbon is shown (overrides CSidebarRenderer)
   virtual bool    IsPinnedTileActive() override { return m_isPinnedVisible; }
   //--- True while any pinned-ribbon drag is in progress (move / resize / scroll-thumb)
   bool            IsDraggingPinned() const
                     { return m_isPinnedDragging || m_isPinnedResizing || m_isPinnedThumbDragging; }
   //--- Rebuild + repaint after the pinned set changes while visible
   void            RefreshPinnedRibbon();
   //--- Full repaint of the ribbon body + icons + scrollbar
   void            RedrawPinnedRibbon();

   //--- Hit-test screen coords against the ribbon body (emits ribbon-local coords)
   bool            HitTestOverPinned(int mouseX, int mouseY, int &outLx, int &outLy);
   //--- Mouse routing (called by the Shell dispatcher)
   bool            PinnedMouseDown(int mouseX, int mouseY);
   bool            PinnedMouseMove(int mouseX, int mouseY, uint mouseButtons);
   bool            PinnedMouseUp();
   //--- Wheel-over-ribbon horizontal scroll
   void            ScrollPinnedByWheel(int wheelDelta);

   //--- Hook the Shell overrides to activate the clicked tool (default no-op here)
   virtual void    OnPinnedToolActivated(TOOL_TYPE toolType) { }
   //--- Hook the Shell overrides to report the active tool so its icon renders in the active color
   virtual TOOL_TYPE PinnedActiveTool() { return TOOL_NONE; }
  };

We give the ribbon two canvases — the visible one it paints to, and a persistent offscreen scratch canvas we use to clip the scrolled icons against the bar's rounded edges. We create that scratch once rather than per frame, since allocating a canvas on every redraw during a scroll would be wasteful. Alongside them, we hold the visibility flag, the logical top-left position that excludes the shadow halo, and a cached last-position so reopening restores where we last left the bar.

We then declare the layout state in a few groups. The dimensions and styling fields carry the current width and height, the shadow padding, and the corner radius that matches the properties ribbon. The layout constants fix the grip strip width, the icon size and gap, the row padding, the icon band height, and the trailing resize-handle width. A small set of cached layout results — the effective viewport width, the needs-scroll flag, the resize-visible flag, and the pixel scroll offset — are recomputed each layout pass so the render and hit-test paths read them rather than recompute them.

We keep three blocks of interaction state, one per gesture the ribbon supports. The hover block tracks which icon, the scrollbar, the thumb, and the resize grip are under the cursor, each driving its own color. The drag block holds the whole-ribbon move offsets, the resize block holds the grab anchor, the starting width for the trailing-grip resize, and the thumb block holds the scrollbar-thumb drag anchors. We also expose a handful of small inline helpers for the icon-row origin and the viewport width, so the geometry routines stay terse.

On the public side, we declare the canvas lifecycle, the show, hide, and toggle entry points, the refresh and full-redraw routines, the hit-test and mouse routers the shell dispatches into, and the wheel scroll handler. We override "IsPinnedTileActive" so the sidebar's Pinned tile renders in its active color while the ribbon is showing, and we leave two hooks — "OnPinnedToolActivated" and "PinnedActiveTool" — as no-ops here for the shell to override, one to activate a clicked tool and one to report the active tool so its icon renders highlighted. With the surface declared, we move on to the canvas lifecycle and layout.

Layout, the Resize Clamp, and the Handle Rectangle

We recompute the ribbon's geometry in "RecomputePinnedLayout", and the resize-aware width math is the new piece here — the rest of the layout follows the same shape as the property ribbon, and the scroll thumb mirrors the sidebar's.

//+------------------------------------------------------------------+
//| Recompute width/height + clamp the visible window and scroll     |
//+------------------------------------------------------------------+
void CPinnedRibbon::RecomputePinnedLayout()
  {
   //--- Total pinned tools available
   const int total = PinnedCount();
   //--- The trailing resize grip is available from 2 items upward
   m_pinnedShowResize = (total >= 2);
   //--- Full content width and the largest viewport the chart can hold
   const int contentW = PinnedContentW();
   const int maxFit   = PinnedMaxFitPx();
   //--- Effective viewport = user's chosen width, capped at content width (no dead margin) then at chart fit (overflow scrolls)
   int eff = m_pinnedUserWidthPx;
   if(eff > contentW) eff = contentW;     // capped at the content => no dead space on the right
   if(eff > maxFit)   eff = maxFit;       // capped at the chart width => scroll when there are too many
   if(eff < m_pinnedIconSize) eff = (contentW < m_pinnedIconSize) ? contentW : m_pinnedIconSize;
   if(eff < 0) eff = 0;
   m_pinnedEffViewportW = eff;
   //--- Scroll is needed only when the content cannot fully fit the visible window
   m_pinnedNeedsScroll = (eff < contentW);
   //--- Clamp the pixel scroll offset to its valid range
   const int maxPx = PinnedMaxScrollPx();
   if(m_pinnedScrollPixels > maxPx) m_pinnedScrollPixels = maxPx;
   if(m_pinnedScrollPixels < 0)     m_pinnedScrollPixels = 0;

   //--- Width = grip + left pad + visible-window width + right pad (+ resize grip from 2 items)
   int w = m_pinnedGripWidth + m_pinnedIconRowPadX + m_pinnedEffViewportW + m_pinnedIconRowPadX;
   if(m_pinnedShowResize) w += m_pinnedResizeHandleW;
   m_pinnedWidth = w;
   //--- Height is constant; the horizontal scrollbar FLOATS over the band's bottom edge (no reserved lane)
   m_pinnedHeight = m_pinnedIconBandH;
   //--- Ensure the bitmap is large enough (grows only when the chart widens; a no-op during a resize-drag)
   EnsurePinnedBitmap();
  }

//+------------------------------------------------------------------+
//| Grow the bitmap to fit the pinned content (chart-capped, no crop)|
//+------------------------------------------------------------------+
void CPinnedRibbon::EnsurePinnedBitmap()
  {
   //--- Size to the largest window the chart can hold (NOT capped at current content) so adding a pin never reallocates the bitmap (reallocation is what flickers); only a wider chart can grow it
   const int maxViewport = PinnedMaxFitPx();
   const int maxBody = m_pinnedGripWidth + 2 * m_pinnedIconRowPadX
                     + maxViewport + m_pinnedResizeHandleW;
   const int cw = maxBody + 2 * m_pinnedShadowPad;
   const int ch = m_pinnedIconBandH + 2 * m_pinnedShadowPad;
   //--- Grow-only: reallocate solely when the chart itself widens (the bitmap is pre-sized to chart-max); never during a resize-drag
   if(m_canvasPinned.Width() >= cw && m_canvasPinned.Height() >= ch) return;
   const int gw = MathMax(cw, m_canvasPinned.Width());
   const int gh = MathMax(ch, m_canvasPinned.Height());
   m_canvasPinned.Resize(gw, gh);
   m_canvasPinnedScratch.Resize(gw, gh);
  }

//+------------------------------------------------------------------+
//| Largest viewport width that keeps the whole ribbon in the chart  |
//+------------------------------------------------------------------+
int CPinnedRibbon::PinnedMaxFitPx()
  {
   //--- Chrome around the icon row (grip + both pads + resize grip + shadow halo + edge margin)
   const int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0);
   const int chrome = m_pinnedGripWidth + 2 * m_pinnedIconRowPadX
                    + m_pinnedResizeHandleW + 2 * m_pinnedShadowPad + 16;
   int fit = chartW - chrome;
   //--- Always allow at least one icon
   if(fit < m_pinnedIconSize) fit = m_pinnedIconSize;
   return fit;
  }

//+------------------------------------------------------------------+
//| Trailing resize-handle bounds in ribbon-local coordinates        |
//+------------------------------------------------------------------+
void CPinnedRibbon::GetPinnedResizeHandleRect(int &outL, int &outT, int &outR, int &outB)
  {
   //--- A vertical strip on the trailing edge, spanning the icon band
   outR = m_pinnedWidth;
   outL = m_pinnedWidth - m_pinnedResizeHandleW;
   outT = 0;
   outB = m_pinnedIconBandH;
  }

//+------------------------------------------------------------------+
//| Horizontal scrollbar thumb bounds (false when no scroll needed)  |
//+------------------------------------------------------------------+
bool CPinnedRibbon::GetPinnedThumbRect(int &outL, int &outT, int &outR, int &outB)
  {
   //--- Initialize out-params and bail when nothing scrolls
   outL = outT = outR = outB = 0;
   if(!m_pinnedNeedsScroll) return false;
   //--- Track spans from the first icon to the right padding (minus the resize grip when present)
   const int trackL = PinnedIconRowLeft();
   const int trackR = m_pinnedWidth - m_pinnedIconRowPadX
                    - (m_pinnedShowResize ? m_pinnedResizeHandleW : 0);
   const int trackW = trackR - trackL;
   if(trackW <= 8) return false;
   //--- Thumb width = track * (viewport / content), proportional to the visible fraction (min 16 px)
   const int contentW = PinnedContentW();
   if(contentW <= 0) return false;
   int thumbW = (int)((double)trackW * (double)PinnedViewportW() / (double)contentW);
   if(thumbW < 16)     thumbW = 16;
   if(thumbW > trackW) thumbW = trackW;
   //--- Thumb X = track left + (pixel-scroll fraction * available travel)
   const int maxPx  = PinnedMaxScrollPx();
   const int travel = trackW - thumbW;
   const int thumbX = (maxPx > 0)
                      ? trackL + (int)((double)travel * (double)m_pinnedScrollPixels / (double)maxPx)
                      : trackL;
   //--- Thumb is a 3-px pill that floats over the band's bottom edge with a 1-px gap to the bottom
   const int pillH = 3;
   outL = thumbX;
   outR = thumbX + thumbW;
   outB = m_pinnedIconBandH - 1;
   outT = outB - pillH;
   return true;
  }

We drive everything from "RecomputePinnedLayout". We enable the trailing resize grip only from two pinned tools upward, since a single-icon bar has nothing to resize toward. We then resolve the effective viewport width by taking the user's chosen width and capping it twice — first at the full content width so a wide drag leaves no dead space past the last icon, then at the largest width the chart can hold so an over-wide drag turns into scrolling instead of spilling off-screen. From that, we set the needs-scroll flag, clamp the scroll offset into its new range, and compute the body width as the grip plus pads plus viewport, adding the resize-handle width only when the grip is shown.

We keep the bitmap ahead of the content in "EnsurePinnedBitmap", sizing it to the largest window the chart can hold rather than to the current content. This is what lets a resize drag stay smooth — because the backing canvas is already chart-max wide, widening the ribbon never reallocates the bitmap mid-drag, and reallocation is what would flicker. We grow it only when the chart itself widens. The ceiling for both the bitmap and the resize comes from "PinnedMaxFitPx", which subtracts the ribbon's chrome — grip, pads, resize handle, shadow halo, and a margin — from the chart width and floors the result at one icon, so the resize can never drag the bar past the chart edge.

We place the grab zone in "GetPinnedResizeHandleRect" as a vertical strip on the trailing edge, the handle is wide and spans the icon band, which the mouse-down test checks against to start a resize. We position the scroll thumb in "GetPinnedThumbRect" using the same proportional-thumb math as the sidebar and flyout scrollbars — thumb width as the visible fraction of the track, thumb position from the scroll fraction — so we will not walk through it in detail. With the layout and the handle rect settled, we move on to the mouse handling that drives the resize.

Driving the Resize from the Mouse Routers

We route every ribbon gesture through the three mouse handlers, and the resize is the new behavior worth walking — the grip drag, thumb drag, and icon activation mirror the property ribbon and sidebar we already covered.

//+------------------------------------------------------------------+
//| Hit-test ribbon-local coords against the leading grip strip      |
//+------------------------------------------------------------------+
bool CPinnedRibbon::HitTestPinnedGrip(int lx, int ly)
  {
   //--- Grip occupies the leftmost strip across the icon band height
   return (lx >= 0 && lx < m_pinnedGripWidth && ly >= 0 && ly < m_pinnedIconBandH);
  }

//+------------------------------------------------------------------+
//| Hit-test screen coords against the ribbon body (emits local x/y) |
//+------------------------------------------------------------------+
bool CPinnedRibbon::HitTestOverPinned(int mouseX, int mouseY, int &outLx, int &outLy)
  {
   //--- Bail when hidden
   if(!m_isPinnedVisible) return false;
   //--- Translate to ribbon-local coordinates + containment test
   outLx = mouseX - m_pinnedX;
   outLy = mouseY - m_pinnedY;
   return (outLx >= 0 && outLx < m_pinnedWidth && outLy >= 0 && outLy < m_pinnedHeight);
  }

//+------------------------------------------------------------------+
//| Route a mouse-down on the ribbon (grip / resize / scroll / icon) |
//+------------------------------------------------------------------+
bool CPinnedRibbon::PinnedMouseDown(int mouseX, int mouseY)
  {
   //--- Bail when hidden + ignore clicks outside the body
   if(!m_isPinnedVisible) return false;
   int lx, ly;
   if(!HitTestOverPinned(mouseX, mouseY, lx, ly)) return false;

   //--- Grip strip -> begin a whole-ribbon drag (lock chart scroll so it doesn't slide)
   if(HitTestPinnedGrip(lx, ly))
     {
      m_isPinnedDragging  = true;
      m_pinnedDragOffsetX = lx;
      m_pinnedDragOffsetY = ly;
      ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
      return true;
     }

   //--- Trailing resize grip -> begin a smooth pixel resize of the visible window
   if(m_pinnedShowResize)
     {
      int hL, hT, hR, hB;
      GetPinnedResizeHandleRect(hL, hT, hR, hB);
      if(lx >= hL && lx < hR && ly >= hT && ly < hB)
        {
         m_isPinnedResizing    = true;
         m_pinnedResizeGrabX   = mouseX;
         m_pinnedResizeStartPx = m_pinnedEffViewportW;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
         return true;
        }
     }

   //--- Scrollbar thumb -> begin a thumb drag (generous vertical grab zone since the pill is thin)
   if(m_pinnedNeedsScroll)
     {
      int tL, tT, tR, tB;
      if(GetPinnedThumbRect(tL, tT, tR, tB)
         && lx >= tL && lx < tR && ly >= tT - 6 && ly <= tB + 2)
        {
         m_isPinnedThumbDragging      = true;
         m_pinnedThumbDragStartX      = mouseX;
         m_pinnedThumbDragStartPixels = m_pinnedScrollPixels;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
         return true;
        }
     }

   //--- Icon click -> resolve the pinned tool and route activation through the hook
   const int pinIdx = HitTestPinnedIcon(lx, ly);
   if(pinIdx >= 0)
     {
      const TOOL_TYPE tool = GetPinnedTool(pinIdx);
      if(tool != TOOL_NONE)
         OnPinnedToolActivated(tool);
      return true;
     }

   //--- Body click outside any control - consume so it doesn't leak to the chart
   return true;
  }

//+------------------------------------------------------------------+
//| Route mouse-move: active drag/resize/scroll, else hover state    |
//+------------------------------------------------------------------+
bool CPinnedRibbon::PinnedMouseMove(int mouseX, int mouseY, uint mouseButtons)
  {
   //--- Active whole-ribbon drag
   if(m_isPinnedDragging)
     {
      //--- Button released mid-drag -> end the drag defensively
      if((mouseButtons & 1) == 0) { PinnedMouseUp(); return false; }
      //--- Apply the grab offset, clamp, reposition, and cache the new spot
      m_pinnedX = mouseX - m_pinnedDragOffsetX;
      m_pinnedY = mouseY - m_pinnedDragOffsetY;
      ClampPinnedToChart();
      ApplyPinnedPosition();
      m_lastPinnedX     = m_pinnedX;
      m_lastPinnedY     = m_pinnedY;
      m_lastPinnedValid = true;
      ChartRedraw();
      return true;
     }

   //--- Active smooth resize of the visible window (pixel-for-pixel, like the sidebar edge resize)
   if(m_isPinnedResizing)
     {
      //--- Button released mid-resize -> end it
      if((mouseButtons & 1) == 0) { PinnedMouseUp(); return false; }
      //--- New window width follows the cursor 1:1 from the grab point
      const int delta = mouseX - m_pinnedResizeGrabX;
      int newPx = m_pinnedResizeStartPx + delta;
      //--- Clamp to [one icon, the largest width the chart can hold]
      const int maxFit = PinnedMaxFitPx();
      if(newPx < m_pinnedIconSize) newPx = m_pinnedIconSize;
      if(newPx > maxFit)           newPx = maxFit;
      //--- Rebuild only when the width actually changed
      if(newPx != m_pinnedUserWidthPx)
        {
         //--- Capture the old visible width to decide expand vs contract
         const int oldWidth = m_pinnedWidth;
         m_pinnedUserWidthPx = newPx;
         RecomputePinnedLayout();
         ClampPinnedToChart();
         ApplyPinnedPosition();
         if(m_pinnedWidth > oldWidth) CropPinnedCanvas();
         RedrawPinnedRibbon();
         ChartRedraw();
        }
      return true;
     }

   //--- Active scrollbar-thumb drag (pixel-based, smooth - mirrors the sidebar thumb drag)
   if(m_isPinnedThumbDragging)
     {
      //--- Button released mid-drag -> end it
      if((mouseButtons & 1) == 0) { PinnedMouseUp(); return false; }
      //--- Recompute the track + thumb geometry to map cursor travel to a pixel offset
      const int trackL = PinnedIconRowLeft();
      const int trackR = m_pinnedWidth - m_pinnedIconRowPadX
                       - (m_pinnedShowResize ? m_pinnedResizeHandleW : 0);
      const int trackW = trackR - trackL;
      const int contentW = PinnedContentW();
      int thumbW = (contentW > 0) ? (int)((double)trackW * (double)PinnedViewportW() / (double)contentW) : 16;
      if(thumbW < 16)     thumbW = 16;
      if(thumbW > trackW) thumbW = trackW;
      const int travel = trackW - thumbW;
      const int maxPx  = PinnedMaxScrollPx();
      //--- Translate thumb travel proportionally into scroll pixels; clamp to [0, maxPx]
      if(travel > 0)
        {
         const int dx = mouseX - m_pinnedThumbDragStartX;
         int newPx = m_pinnedThumbDragStartPixels + (int)MathRound((double)dx / travel * maxPx);
         if(newPx < 0)     newPx = 0;
         if(newPx > maxPx) newPx = maxPx;
         if(newPx != m_pinnedScrollPixels)
           { m_pinnedScrollPixels = newPx; RedrawPinnedRibbon(); ChartRedraw(); }
        }
      return true;
     }

   //--- Bail when hidden
   if(!m_isPinnedVisible) return false;
   //--- Hover pass: resolve the hovered tool index + scrollbar visibility + thumb hover
   int lx, ly;
   const bool over = HitTestOverPinned(mouseX, mouseY, lx, ly);
   const int newHover = over ? HitTestPinnedIcon(lx, ly) : -1;
   //--- Scrollbar autohides: shown whenever the cursor is over the ribbon while scrollable
   const bool newSbHover = (over && m_pinnedNeedsScroll);
   //--- Thumb-specific hover drives the pill color (generous vertical zone since the pill is thin)
   bool newThumbHover = false;
   if(over && m_pinnedNeedsScroll)
     {
      int tL, tT, tR, tB;
      if(GetPinnedThumbRect(tL, tT, tR, tB)
         && lx >= tL && lx < tR && ly >= tT - 6 && ly <= tB + 2)
         newThumbHover = true;
     }
   //--- Resize-grip hover drives the grip-line color (hover tint vs idle)
   bool newResizeHover = false;
   if(over && m_pinnedShowResize)
     {
      int hL, hT, hR, hB;
      GetPinnedResizeHandleRect(hL, hT, hR, hB);
      if(lx >= hL && lx < hR && ly >= hT && ly < hB)
         newResizeHover = true;
     }
   //--- Repaint + update the tooltip only when something changed
   if(newHover != m_hoveredPinnedIdx || newSbHover != m_hoveredPinnedScrollbar
      || newThumbHover != m_hoveredPinnedThumb || newResizeHover != m_hoveredPinnedResize)
     {
      m_hoveredPinnedIdx       = newHover;
      m_hoveredPinnedScrollbar = newSbHover;
      m_hoveredPinnedThumb     = newThumbHover;
      m_hoveredPinnedResize    = newResizeHover;
      //--- Tooltip = the hovered tool's label
      string tip = "";
      if(newHover >= 0)
        {
         const TOOL_TYPE t = GetPinnedTool(newHover);
         if(t != TOOL_NONE) tip = GetToolLabel(t);
        }
      ObjectSetString(0, m_namePinned, OBJPROP_TOOLTIP, tip);
      RedrawPinnedRibbon();
      ChartRedraw();
     }
   //--- Returning over=true tells the dispatcher to swallow the event
   return over;
  }

//+------------------------------------------------------------------+
//| End any active drag / resize / scroll-thumb interaction          |
//+------------------------------------------------------------------+
bool CPinnedRibbon::PinnedMouseUp()
  {
   //--- Track whether any interaction was active
   bool consumed = false;
   if(m_isPinnedDragging)      { m_isPinnedDragging = false;      consumed = true; }
   if(m_isPinnedResizing)      { m_isPinnedResizing = false;      consumed = true; }
   if(m_isPinnedThumbDragging) { m_isPinnedThumbDragging = false; consumed = true; }
   //--- Restore chart wheel scroll + repaint once when an interaction ended
   if(consumed)
     {
      ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
      RedrawPinnedRibbon();
      ChartRedraw();
     }
   return consumed;
  }

First, we resolve the two containment tests — "HitTestPinnedGrip" checks the leading grip strip and "HitTestOverPinned" translates screen coordinates to ribbon-local and tests the body bounds. In "PinnedMouseDown", after confirming the click is on the body, we check the targets in priority order. The resize branch is the one to note: when the grip is showing and the click lands in the handle rect, we set the resizing flag, cache the cursor's screen X as the grab point, snapshot the current effective viewport width as the starting width, and lock chart scrolling so the chart does not slide under us mid-resize. The grip drag, the scroll-thumb drag, and the icon click that routes the pinned tool through "OnPinnedToolActivated" all follow the same shape we have used before.

We handle the live resize in "PinnedMouseMove". We take the cursor delta from the grab X and add it to the starting width so the trailing edge follows the cursor one-for-one, then clamp the result to between one icon and the largest width the chart can hold from "PinnedMaxFitPx". We rebuild only when the width actually changed — we write the new user width, re-run "RecomputePinnedLayout", reclamp and reapply the position, crop the visible canvas when the ribbon grew, and repaint. The whole-ribbon drag, the thumb drag, and the hover pass are the familiar patterns; the only resize-specific bit in the hover pass is the branch that lights the resize grip when the cursor is over its handle rect.

We end every gesture in "PinnedMouseUp", which clears whichever of the move, resize, and thumb flags was set, restores chart wheel scrolling, and repaints once so the grip and thumb return to their idle colors. When wired to the shell, we get the following outcome.

WIRED PIN RIBBON

This completes the objectives for this part. What remains now is testing the program, and that is handled in the next section.


Visualization

We compile the program, attach it to the chart, pin a few tools from the sidebar flyout, and toggle the pinned ribbon to confirm that the icons, dragging, resizing, and scrolling all behave.

TOOLS PALETTE GIF TEST

During testing, clicking a row's pushpin in the flyout toggled the tool in and out of the pinned set, with the tooltip naming the action and the Pinned tile appearing once the first tool was pinned. The ribbon toggled from that tile, rendered the pinned icons in pin order, and activated the matching tool on a single click. Dragging the grip moved the whole bar, dragging the trailing handle resized its width pixel-for-pixel up to the chart edge, and once the icons overflowed the bar, a floating scrollbar appeared that scrolled them by thumb drag and by mouse wheel.


Conclusion

In conclusion, we turned the three-step draw flow into a one-click path by adding a pinned-tools ribbon that coexists with the sidebar: discovery remains in the deep catalog, while your most-used tools sit immediately at hand. The article delivers an integrated, testable feature set that matches the readiness criteria we set out at the start:

  • Engine API: an ordered pin store with "PinnedCount", "GetPinnedTool", "IsToolPinned", "PinTool", "UnpinTool", and "ToggleToolPin", preserving pin order and exposing simple queries.
  • Unified glyph: an anti-aliased pushpin renderer that draws outline or solid states, so the same visual is reused across the sidebar tile, flyout rows, and the ribbon.
  • Flyout affordance: per-row pushpins with hover feedback and a context tooltip, and a Pinned category tile that appears only when the pin list is nonempty.
  • Pinned ribbon: a floating surface built on the existing settings-window foundation that supports dragging, pixel-for-pixel trailing-edge resize clamped to the chart, horizontal scrolling with a proportional thumb, and a persistent offscreen scratch canvas to cleanly clip scrolled icons without reallocating during interaction.
  • Integration hooks: "OnPinnedToolActivated" and "PinnedActiveTool" for the shell to wire activation and to highlight the active tool.

These pieces close the gap from problem to solution: frequent tools are accessible in one click, pin order is preserved, and the ribbon scales ergonomically as favorites accumulate. After reading this article, you will be able to:

  • Build an ordered pin set with an append, remove, and query API, backed by an anti-aliased pushpin glyph that one routine renders as outline or solid across every surface that shows it.
  • Add a pin affordance to a sidebar flyout — a per-row pushpin with hover feedback and an add-or-remove tooltip — plus a category tile that stays hidden until the set has at least one entry.
  • Implement a floating, draggable, user-resizable, horizontally scrollable ribbon with offscreen icon clipping and a smooth pixel-for-pixel resize handle.

From here, you can extend the toolkit by adding more tools, persisting the pin list across sessions, or slimming the implementation for environments with tighter resource budgets. Good luck.


Attachments

S/N
Name
Type
Description
1 Tools Palette Part 10.mq5 Expert Advisor Main entry point that owns the global sidebar instance and forwards initialization, deinitialization, chart event, and timer callbacks to it.
2 ToolsPalette_Annotations.mqh Include File Annotation tools covering Text, Arrow, Arrow Marker, Arrow Up, Arrow Down, Note, Price Note, Callout, and Comment, plus the shared word-wrap and editable text rendering helpers.
3 ToolsPalette_Channels.mqh Include File Channels, pitchforks, and Gann tools library carrying every channel, pitchfork, and Gann draw routine and hit tester.
4 ToolsPalette_Crosshair.mqh Include File Crosshair manager that owns the reticle, magnifier, cross-line, and measurement canvases plus their axis labels.
5 ToolsPalette_Engine_Edit.mqh Include File Drawing engine method bodies for the in-place label editing subsystem covering edit lifecycle, caret operations, selection model, and chart keyboard override.
6 ToolsPalette_Engine_Interact.mqh Include File Drawing engine method bodies for pointer-mode interaction covering hit testing dispatch, handle reshape logic, drag move and release, and selection management.
7 ToolsPalette_Engine_Properties.mqh Include File Drawing engine method bodies for the property get and set API, snapshot and restore for live preview, and per-level add and remove.
8 ToolsPalette_Engine_Render.mqh Include File Drawing engine method bodies for the render pipeline covering full-redraw dispatch, rubber-band preview, label rendering, and permanent axis labels.
9 ToolsPalette_Fibonacci.mqh Include File Fibonacci tools library covering retracement, expansion, channel, time zone, speed resistance fan, and speed resistance arcs.
10 ToolsPalette_Lines.mqh Include File Base line tools class providing every line drawing routine plus the shared handle renderer and tool icon dispatch.
11 ToolsPalette_Primitives.mqh Include File Foundational pixel primitives covering alpha-compositing pixel set, anti-aliased thick line, supersampled fills, theme manager, and the input parameter declarations.
12 ToolsPalette_Properties.mqh Include File Property descriptor system defining the property type enum, the descriptor struct, the per-tool registration functions, and the build dispatcher.
13 ToolsPalette_PropertyWidgets.mqh Include File Property-editing widget renderers covering the color picker, the line width and style popovers, the compact rows, and the ribbon icons.
14 ToolsPalette_RibbonPinned.mqh Include File Pinned-tools ribbon: a floating, draggable, user-resizable, horizontally scrollable bar of pinned tool icons with offscreen icon clipping.
15 ToolsPalette_RibbonProperties.mqh Include File Per-object property-editing ribbon that pops up beside the selection and opens the color, width, and style popovers wired to the engine.
16 ToolsPalette_Settings.mqh Include File Tabbed settings window layout and rendering covering the tab strip, property rows, chips, scrollbars, and the Text tab.
17 ToolsPalette_Settings_Interact.mqh Include File Settings window interaction covering mouse routing, inline coordinate and float editing, and the sub-popover dispatch.
18 ToolsPalette_Shapes.mqh Include File Shape tools library covering Rectangle, Triangle, Rotated Rectangle, Rotated Ellipse, Path, Circle, Arc, and Curve.
19 ToolsPalette_Shell.mqh Include File Sidebar shell that routes chart events, dispatches keyboard input, drives the caret blink timer, and wires both ribbons into the event loop.
20 ToolsPalette_Sidebar.mqh Include File Sidebar renderer and flyout panel, now carrying the per-row pushpin affordance and the Pinned category tile that appears once a tool is pinned.
21 ToolsPalette_Tools.mqh Include File Tool registry plus the drawing engine class, now holding the ordered pinned-tools set, its management API, and the anti-aliased pushpin glyph.
22 Tools Palette Part 10.zip Archive A ready-to-extract archive containing all 21 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 |
MQL5 Bootstrap (II): Essential Validators for Robust Trading Systems MQL5 Bootstrap (II): Essential Validators for Robust Trading Systems
The article builds a reusable validation layer for Expert Advisors in MQL5. It implements lot-size rules and normalization, SL/TP and freeze-level guards, price digit normalization, margin sufficiency checks, unchanged-level filtering on modifications, account order-limit control, new-bar detection, symbol tradability checks, economic-calendar news windows, and session detectors. The result is cleaner code and fewer terminal errors in live trading.
Implementing Walk-Forward Efficiency Ratio Scoring in MQL5 to Detect Over-Optimized Strategies Implementing Walk-Forward Efficiency Ratio Scoring in MQL5 to Detect Over-Optimized Strategies
Parameter optimization inside MetaTrader 5's Strategy Tester routinely produces strategies that perform well in-sample and collapse on forward data. This article builds a native MQL5 Walk-Forward Efficiency scoring engine that quantifies how much of a strategy's in-sample Sharpe ratio transfers to each out-of-sample window. The distribution is rendered as a CCanvas histogram and validated against real EURUSD Daily backtest data.
How to Connect AI Agents to MQL5 Algo Forge via MCP How to Connect AI Agents to MQL5 Algo Forge via MCP
This article extends Part 1 by giving an AI access to the development lifecycle on MQL5 Algo Forge. We implement an MCP server over the Forgejo REST API so an agent can create repositories, commit Expert Advisors, branch from main, open pull requests, file issues, and tag releases. You will get a ready-to-run Python server, clear tools, and a safer, reversible workflow.
Building a Broker-Agnostic Symbol Resolution Layer in MQL5 Building a Broker-Agnostic Symbol Resolution Layer in MQL5
We implement a symbol resolution framework that abstracts broker naming differences in MetaTrader 5. Using a persistent mapping store, layered resolution with validation, a hash-indexed registry, and a cache, it returns selectable symbols with live market data and logs unresolved cases. Practically, you can deploy the same EA across brokers and keep symbol access consistent at low runtime cost.