//+------------------------------------------------------------------+ //| Tools Palette Part 4.mq5 | //| 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 //--- Include canvas drawing library #include //+------------------------------------------------------------------+ //| Icon definitions — category and tool buttons | //+------------------------------------------------------------------+ struct SIconDefinition { string fontName; uchar charCode; }; // Store icon font and character code //--- Define icon for each tool category using font/char pairs SIconDefinition ICON_CATEGORY_CURSORS = { "Wingdings", (uchar)'v' }; // Cursors category icon SIconDefinition ICON_CATEGORY_LINES = { "Wingdings 3", (uchar)'&' }; // Lines category icon SIconDefinition ICON_CATEGORY_CHANNELS = { "Wingdings 3", (uchar)'2' }; // Channels category icon SIconDefinition ICON_CATEGORY_PITCHFORK = { "Wingdings 3", (uchar)'H' }; // Pitchfork category icon SIconDefinition ICON_CATEGORY_GANN = { "Wingdings", (uchar)'T' }; // Gann category icon SIconDefinition ICON_CATEGORY_FIBONACCI = { "Wingdings", (uchar)'z' }; // Fibonacci category icon SIconDefinition ICON_CATEGORY_SHAPES = { "Wingdings", (uchar)'o' }; // Shapes category icon SIconDefinition ICON_CATEGORY_ANNOTATIONS = { "Webdings", (uchar)'>' }; // Annotations category icon //--- Define icon for each individual drawing tool using font/char pairs SIconDefinition ICON_TOOL_POINTER = { "Wingdings 3", (uchar)'-' }; // Pointer tool icon SIconDefinition ICON_TOOL_CROSSHAIR = { "Wingdings", (uchar)'W' }; // Crosshair tool icon SIconDefinition ICON_TOOL_TRENDLINE = { "Wingdings 3", (uchar)'&' }; // Trend line tool icon SIconDefinition ICON_TOOL_HLINE = { "Wingdings 3", (uchar)'"' }; // Horizontal line tool icon SIconDefinition ICON_TOOL_VLINE = { "Wingdings 3", (uchar)'#' }; // Vertical line tool icon SIconDefinition ICON_TOOL_RAY = { "Wingdings 3", (uchar)'&' }; // Ray line tool icon SIconDefinition ICON_TOOL_EXTENDED_LINE = { "Wingdings 3", (uchar)'1' }; // Extended line tool icon SIconDefinition ICON_TOOL_INFO_LINE = { "Wingdings 3", (uchar)'2' }; // Info/measure line tool icon SIconDefinition ICON_TOOL_PARALLEL_CH = { "Wingdings 3", (uchar)'H' }; // Parallel channel tool icon SIconDefinition ICON_TOOL_REGRESSION_CH = { "Wingdings 3", (uchar)'I' }; // Regression channel tool icon SIconDefinition ICON_TOOL_STDDEV_CH = { "Wingdings 3", (uchar)'J' }; // Standard deviation channel tool icon SIconDefinition ICON_TOOL_PITCHFORK = { "Wingdings 3", (uchar)'H' }; // Andrew's pitchfork tool icon SIconDefinition ICON_TOOL_SCHIFF = { "Wingdings 3", (uchar)'I' }; // Schiff pitchfork tool icon SIconDefinition ICON_TOOL_MOD_SCHIFF = { "Wingdings 3", (uchar)'K' }; // Modified Schiff pitchfork tool icon SIconDefinition ICON_TOOL_GANN_LINE = { "Wingdings 3", (uchar)'&' }; // Gann line tool icon SIconDefinition ICON_TOOL_GANN_FAN = { "Wingdings 3", (uchar)'0' }; // Gann fan tool icon SIconDefinition ICON_TOOL_GANN_GRID = { "Wingdings", (uchar)'i' }; // Gann grid tool icon SIconDefinition ICON_TOOL_FIBO_RET = { "Wingdings", (uchar)'[' }; // Fibonacci retracement tool icon SIconDefinition ICON_TOOL_FIBO_EXP = { "Wingdings 3", (uchar)'&' }; // Fibonacci expansion tool icon SIconDefinition ICON_TOOL_FIBO_CH = { "Wingdings 3", (uchar)'H' }; // Fibonacci channel tool icon SIconDefinition ICON_TOOL_FIBO_TZ = { "Wingdings 3", (uchar)'#' }; // Fibonacci time zones tool icon SIconDefinition ICON_TOOL_FIBO_FAN = { "Wingdings 3", (uchar)'J' }; // Fibonacci fan tool icon SIconDefinition ICON_TOOL_FIBO_ARCS = { "Wingdings", (uchar)'l' }; // Fibonacci arcs tool icon SIconDefinition ICON_TOOL_RECTANGLE = { "Wingdings", (uchar)'o' }; // Rectangle tool icon SIconDefinition ICON_TOOL_TRIANGLE = { "Wingdings 3", (uchar)'p' }; // Triangle tool icon SIconDefinition ICON_TOOL_ELLIPSE = { "Wingdings", (uchar)'l' }; // Ellipse tool icon SIconDefinition ICON_TOOL_TEXT = { "Webdings", (uchar)'>' }; // Text label tool icon SIconDefinition ICON_TOOL_ARROW_UP = { "Wingdings", (uchar)225 }; // Arrow up tool icon SIconDefinition ICON_TOOL_ARROW_DOWN = { "Wingdings", (uchar)226 }; // Arrow down tool icon SIconDefinition ICON_TOOL_THUMB_UP = { "Wingdings", (uchar)'C' }; // Thumbs up tool icon SIconDefinition ICON_TOOL_THUMB_DOWN = { "Wingdings", (uchar)'D' }; // Thumbs down tool icon SIconDefinition ICON_TOOL_PRICE_LABEL = { "Wingdings", (uchar)234 }; // Left price label tool icon SIconDefinition ICON_TOOL_STOP_SIGN = { "Wingdings", (uchar)251 }; // Stop sign tool icon SIconDefinition ICON_TOOL_CHECK_MARK = { "Wingdings", (uchar)252 }; // Check mark tool icon //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ enum ENUM_SNAP_STATE { SNAP_LEFT, // Snap sidebar to left edge SNAP_RIGHT, // Snap sidebar to right edge SNAP_FLOAT // Allow sidebar to float freely }; enum TOOL_TYPE { TOOL_NONE = 0, // No tool active TOOL_POINTER, // Default pointer cursor TOOL_CROSSHAIR, // Crosshair / measure cursor TOOL_TRENDLINE, // Trend line drawing tool TOOL_HLINE, // Horizontal line drawing tool TOOL_VLINE, // Vertical line drawing tool TOOL_RAY, // Ray line drawing tool TOOL_EXTENDED_LINE, // Extended (infinite) line drawing tool TOOL_INFO_LINE, // Info / measure line drawing tool TOOL_PARALLEL_CHANNEL, // Parallel channel drawing tool TOOL_REGRESSION_CHANNEL,// Regression channel drawing tool TOOL_STDDEV_CHANNEL, // Standard deviation channel drawing tool TOOL_PITCHFORK, // Andrew's pitchfork drawing tool TOOL_SCHIFF_PITCHFORK, // Schiff pitchfork drawing tool TOOL_MOD_SCHIFF, // Modified Schiff pitchfork drawing tool TOOL_GANN_LINE, // Gann line drawing tool TOOL_GANN_FAN, // Gann fan drawing tool TOOL_GANN_GRID, // Gann grid drawing tool TOOL_FIBO_RETRACEMENT, // Fibonacci retracement drawing tool TOOL_FIBO_EXPANSION, // Fibonacci expansion drawing tool TOOL_FIBO_CHANNEL, // Fibonacci channel drawing tool TOOL_FIBO_TIMEZONES, // Fibonacci time zones drawing tool TOOL_FIBO_FAN, // Fibonacci fan drawing tool TOOL_FIBO_ARCS, // Fibonacci arcs drawing tool TOOL_RECTANGLE, // Rectangle shape drawing tool TOOL_TRIANGLE, // Triangle shape drawing tool TOOL_ELLIPSE, // Ellipse shape drawing tool TOOL_TEXT, // Text label annotation tool TOOL_ARROW_UP, // Arrow up annotation tool TOOL_ARROW_DOWN, // Arrow down annotation tool TOOL_THUMB_UP, // Thumbs up annotation tool TOOL_THUMB_DOWN, // Thumbs down annotation tool TOOL_PRICE_LABEL, // Left price label annotation tool TOOL_STOP_SIGN, // Stop sign annotation tool TOOL_CHECK_MARK // Check mark annotation tool }; enum ENUM_CATEGORY { CAT_NONE = -1, // No category selected CAT_CURSORS = 0, // Cursors category index CAT_LINES, // Lines category index CAT_CHANNELS, // Channels category index CAT_PITCHFORK, // Pitchfork category index CAT_GANN, // Gann category index CAT_FIBONACCI, // Fibonacci category index CAT_SHAPES, // Shapes category index CAT_ANNOTATIONS, // Annotations category index CAT_COUNT // Total number of categories }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input int CanvasY = 50; // Canvas Y Position input double BackgroundOpacity = 0.92; // Background Opacity (0.0 - 1.0) input bool StartDark = true; // Start In Dark Theme input int BorderWidth = 1; // Border Width (px) input int CategoryIconSize = 26; // Category Icon Size (pt) input int FlyoutIconSize = 22; // Flyout Icon Size (pt) input int FlyoutLabelSize = 15; // Flyout Label Font Size (pt) input int FlyoutTitleSize = 14; // Flyout Title Font Size (pt) input int MouseScrollSpeed = 8; // Mouse Scroll Step (px) input int SnapThreshold = 40; // Edge Snap Threshold (px) input int ReticleOffset = 30; // Crosshair Reticle Offset (px) input int ReticleTickLen = 14; // Crosshair Reticle Tick Length (px) input int ReticleThickness = 2; // Crosshair Reticle Tick Thickness (px) input int MagDiameter = 180; // Magnifier Diameter (px) input double MagZoom = 3.0; // Magnifier Zoom Factor input int MagOffset = 45; // Magnifier Offset From Cursor (px) input int AxisLabelFontSize = 9; // Axis Label Font Size (pt) input string AxisLabelFont = "Arial"; // Axis Label Font //+------------------------------------------------------------------+ //| Tool definition structure | //+------------------------------------------------------------------+ struct ToolDefinition { TOOL_TYPE toolType; // Unique tool type identifier string toolLabel; // Display label shown in the flyout panel string iconFontName;// Font name used to render the tool icon uchar iconCharCode;// Character code of the tool icon glyph string tooltipText; // Tooltip string shown on hover }; //+------------------------------------------------------------------+ //| Category definition structure | //+------------------------------------------------------------------+ struct CategoryDefinition { string categoryLabel; // Display label for the category string iconFontName; // Font name used to render the category icon uchar iconCharCode; // Character code of the category icon glyph ToolDefinition tools[]; // Dynamic array of tools belonging to this category }; //+------------------------------------------------------------------+ //| Theme color set structure | //+------------------------------------------------------------------+ struct ThemeColorSet { color sidebarBackground; // Background fill color of the sidebar panel color sidebarBorder; // Outline border color of the sidebar panel color buttonHoverBackground; // Background fill when a category button is hovered color buttonActiveBackground; // Background fill when a category button is active color buttonIconColor; // Default color used to render category icons color buttonIconActiveColor; // Icon color when the button is in active state color flyoutBackground; // Background fill color of the flyout panel color flyoutBorder; // Outline border color of the flyout panel color flyoutItemHoverBackground; // Background fill of a hovered flyout item row color flyoutTextColor; // Default text color of flyout item labels color flyoutTextActiveColor; // Text color of the active flyout item label color flyoutTitleColor; // Color of the flyout panel title text color gripDotsColor; // Color of the drag-grip dot indicators color closeButtonHoverColor; // Background fill of the close button on hover color themeButtonHoverColor; // Background fill of the theme button on hover color separatorColor; // Color of the horizontal separator lines color accentBarColor; // Color of the active tool accent bar indicator color scrollArrowColor; // Default color of the scroll thumb pill color scrollArrowHoverColor; // Color of the scroll thumb pill on hover }; //+------------------------------------------------------------------+ //| CLASS 1 — Blend and draw low-level canvas primitives | //+------------------------------------------------------------------+ class CCanvasPrimitives { protected: //--- Blend a single pixel onto the canvas using alpha compositing void BlendPixelSet(CCanvas &canvas, int x, int y, uint sourceARGB); //--- Downsample a high-res canvas into a lower-res destination void DownsampleCanvas(CCanvas &dst, CCanvas &src, int factor); //--- Fill a single corner quadrant of a rounded rectangle at high res void FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY); //--- Fill a full rounded rectangle at high resolution void FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb); //--- Fill a rounded rectangle with per-corner rounding control at high res void FillSelectiveRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, bool rTL, bool rTR, bool rBL, bool rBR); //--- Fill a triangle using scanline rasterization at high resolution void FillTriangleHR(CCanvas &canvas, int x0, int y0, int x1, int y1, int x2, int y2, uint argb); //--- Fill a quadrilateral shape using scanline rasterization void FillQuadrilateralBorder(CCanvas &canvas, double &vx[], double &vy[], uint argb); //--- Draw a single thick border edge between two points void DrawBorderEdge(CCanvas &canvas, double x0, double y0, double x1, double y1, int thickness, uint argb); //--- Check whether an angle falls within a given arc range bool IsAngleBetween(double angle, double startAngle, double endAngle); //--- Draw a corner arc segment with specified thickness and angle range void DrawCornerArc(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb, double startAngle, double endAngle); //--- Draw a rounded rectangle border with per-corner rounding control at high res void DrawSelectiveRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, int thickness, bool rTL, bool rTR, bool rBL, bool rBR); //--- Draw a line using Bresenham's algorithm with alpha blending void DrawBresenhamLine(CCanvas &canvas, int x0, int y0, int x1, int y1, uint argb); }; //+------------------------------------------------------------------+ //| Blend source pixel onto canvas using alpha compositing | //+------------------------------------------------------------------+ void CCanvasPrimitives::BlendPixelSet(CCanvas &canvas, int x, int y, uint src) { //--- Skip pixels outside canvas bounds if (x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return; //--- Read existing destination pixel uint dst = canvas.PixelGet(x, y); //--- Unpack source ARGB channels to normalised floats double sA = ((src >> 24) & 0xFF) / 255.0, sR = ((src >> 16) & 0xFF) / 255.0; double sG = ((src >> 8) & 0xFF) / 255.0, sB = ( src & 0xFF) / 255.0; //--- Unpack destination ARGB channels to normalised floats double dA = ((dst >> 24) & 0xFF) / 255.0, dR = ((dst >> 16) & 0xFF) / 255.0; double dG = ((dst >> 8) & 0xFF) / 255.0, dB = ( dst & 0xFF) / 255.0; //--- Compute output alpha using standard over-compositing formula double oA = sA + dA * (1.0 - sA); //--- Write fully transparent pixel and exit if output alpha is zero if (oA == 0.0) { canvas.PixelSet(x, y, 0); return; } //--- Write blended ARGB pixel to canvas canvas.PixelSet(x, y, ((uint)(uchar)(oA * 255 + 0.5) << 24) | ((uint)(uchar)((sR * sA + dR * dA * (1.0 - sA)) / oA * 255 + 0.5) << 16) | ((uint)(uchar)((sG * sA + dG * dA * (1.0 - sA)) / oA * 255 + 0.5) << 8) | (uint)(uchar)((sB * sA + dB * dA * (1.0 - sA)) / oA * 255 + 0.5)); } //+------------------------------------------------------------------+ //| Downsample high-res canvas into destination by averaging pixels | //+------------------------------------------------------------------+ void CCanvasPrimitives::DownsampleCanvas(CCanvas &dst, CCanvas &src, int factor) { //--- Cache destination dimensions and squared sample count int dW = dst.Width(), dH = dst.Height(), ss2 = factor * factor; //--- Iterate over every destination pixel for (int py = 0; py < dH; py++) for (int px = 0; px < dW; px++) { //--- Accumulate channel sums across the source sample block double sA = 0, sR = 0, sG = 0, sB = 0, wc = 0; for (int dy = 0; dy < factor; dy++) for (int dx = 0; dx < factor; dx++) { //--- Compute source sample coordinates int sx = px * factor + dx, sy = py * factor + dy; //--- Skip samples outside source bounds if (sx >= src.Width() || sy >= src.Height()) continue; //--- Read source pixel and extract alpha uint p = src.PixelGet(sx, sy); uchar a = (uchar)((p >> 24) & 0xFF); //--- Accumulate alpha unconditionally sA += a; //--- Accumulate colour channels only for non-transparent samples if (a > 0) { sR += (p >> 16) & 0xFF; sG += (p >> 8) & 0xFF; sB += p & 0xFF; wc += 1.0; } } //--- Compute averaged output alpha uchar fa = (uchar)(sA / ss2); //--- Write transparent pixel and skip if result is fully transparent if (fa == 0 || wc == 0) { dst.PixelSet(px, py, 0); continue; } //--- Write averaged ARGB pixel to destination canvas dst.PixelSet(px, py, ((uint)fa << 24) | ((uint)(uchar)(sR / wc) << 16) | ((uint)(uchar)(sG / wc) << 8) | (uint)(uchar)(sB / wc)); } } //+------------------------------------------------------------------+ //| Fill one corner quadrant of a rounded rect at high resolution | //+------------------------------------------------------------------+ void CCanvasPrimitives::FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY) { //--- Cache radius as double and extract alpha and RGB components double rd = (double)radius; uchar bA = (uchar)((argb >> 24) & 0xFF); uint rgb = argb & 0x00FFFFFF; //--- Set sub-pixel sample count and derived values int sub = 4; double step = 1.0 / sub; int subSq = sub * sub; //--- Iterate over pixel neighbourhood around corner centre for (int dy = -(radius + 1); dy <= (radius + 1); dy++) for (int dx = -(radius + 1); dx <= (radius + 1); dx++) { //--- Check pixel belongs to the target quadrant bool inQ = ((signX > 0) ? (dx >= 0) : (dx <= 0)) && ((signY > 0) ? (dy >= 0) : (dy <= 0)); if (!inQ) continue; //--- Compute distance from corner centre double dist = MathSqrt((double)(dx * dx + dy * dy)); //--- Skip pixels too far outside radius if (dist > rd + 1.0) continue; //--- Fill pixels fully inside radius without anti-aliasing if (dist <= rd - 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; } //--- Count sub-pixel samples falling inside the circle int inside = 0; for (int sy = 0; sy < sub; sy++) for (int sx = 0; sx < sub; sx++) { double sdx = (double)dx - 0.5 + (sx + 0.5) * step; double sdy = (double)dy - 0.5 + (sy + 0.5) * step; if (sdx * sdx + sdy * sdy <= rd * rd) inside++; } //--- Skip pixel if no sub-samples are inside if (inside == 0) continue; //--- Blend anti-aliased pixel using coverage fraction BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb); } } //+------------------------------------------------------------------+ //| Fill a fully rounded rectangle at high resolution | //+------------------------------------------------------------------+ void CCanvasPrimitives::FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb) { //--- Clamp radius to half the smallest dimension radius = MathMin(radius, MathMin(w / 2, h / 2)); //--- Fall back to plain rectangle when radius is zero or negative if (radius <= 0) { canvas.FillRectangle(x, y, x + w - 1, y + h - 1, argb); return; } //--- Fill horizontal centre strip canvas.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb); //--- Fill left vertical strip canvas.FillRectangle(x, y + radius, x + radius - 1, y + h - radius - 1, argb); //--- Fill right vertical strip canvas.FillRectangle(x + w - radius, y + radius, x + w - 1, y + h - radius - 1, argb); //--- Fill top-left corner quadrant FillCornerQuadrantHR(canvas, x + radius, y + radius, radius, argb, -1, -1); //--- Fill top-right corner quadrant FillCornerQuadrantHR(canvas, x + w - radius, y + radius, radius, argb, 1, -1); //--- Fill bottom-left corner quadrant FillCornerQuadrantHR(canvas, x + radius, y + h - radius, radius, argb, -1, 1); //--- Fill bottom-right corner quadrant FillCornerQuadrantHR(canvas, x + w - radius, y + h - radius, radius, argb, 1, 1); } //+------------------------------------------------------------------+ //| Fill rounded rectangle with per-corner rounding at high res | //+------------------------------------------------------------------+ void CCanvasPrimitives::FillSelectiveRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, bool rTL, bool rTR, bool rBL, bool rBR) { //--- Clamp radius to half the smallest dimension radius = MathMin(radius, MathMin(w / 2, h / 2)); //--- Fall back to plain rectangle when radius is zero or negative if (radius <= 0) { canvas.FillRectangle(x, y, x + w - 1, y + h - 1, argb); return; } //--- Fill horizontal centre strip spanning full width canvas.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb); //--- Fill left strip, respecting top-left and bottom-left rounding canvas.FillRectangle(x, y + (rTL ? radius : 0), x + radius - 1, y + h - 1 - (rBL ? radius : 0), argb); //--- Fill right strip, respecting top-right and bottom-right rounding canvas.FillRectangle(x + w - radius, y + (rTR ? radius : 0), x + w - 1, y + h - 1 - (rBR ? radius : 0), argb); //--- Fill or square top-left corner based on rounding flag if (rTL) FillCornerQuadrantHR(canvas, x + radius, y + radius, radius, argb, -1, -1); else canvas.FillRectangle(x, y, x + radius - 1, y + radius - 1, argb); //--- Fill or square top-right corner based on rounding flag if (rTR) FillCornerQuadrantHR(canvas, x + w - radius, y + radius, radius, argb, 1, -1); else canvas.FillRectangle(x + w - radius, y, x + w - 1, y + radius - 1, argb); //--- Fill or square bottom-left corner based on rounding flag if (rBL) FillCornerQuadrantHR(canvas, x + radius, y + h - radius, radius, argb, -1, 1); else canvas.FillRectangle(x, y + h - radius, x + radius - 1, y + h - 1, argb); //--- Fill or square bottom-right corner based on rounding flag if (rBR) FillCornerQuadrantHR(canvas, x + w - radius, y + h - radius, radius, argb, 1, 1); else canvas.FillRectangle(x + w - radius, y + h - radius, x + w - 1, y + h - 1, argb); } //+------------------------------------------------------------------+ //| Fill a triangle using scanline rasterization at high resolution | //+------------------------------------------------------------------+ void CCanvasPrimitives::FillTriangleHR(CCanvas &canvas, int x0, int y0, int x1, int y1, int x2, int y2, uint argb) { //--- Store triangle vertices as floating-point arrays for scanline processing double vx[3] = { (double)x0, (double)x1, (double)x2 }; double vy[3] = { (double)y0, (double)y1, (double)y2 }; //--- Find vertical bounding extent of the triangle double minY = vy[0], maxY = vy[0]; for (int i = 1; i < 3; i++) { if (vy[i] < minY) minY = vy[i]; if (vy[i] > maxY) maxY = vy[i]; } //--- Iterate over each horizontal scanline within the bounding box for (int scanY = (int)MathCeil(minY); scanY <= (int)MathFloor(maxY); scanY++) { //--- Compute scanline centre Y and prepare intersection buffer double cy = (double)scanY + 0.5; double xi[6]; int nc = 0; //--- Compute X intersections with each edge of the triangle for (int i = 0; i < 3; i++) { int ni = (i + 1) % 3; double eMin = (vy[i] < vy[ni]) ? vy[i] : vy[ni], eMax = (vy[i] > vy[ni]) ? vy[i] : vy[ni]; //--- Skip edges that do not cross the current scanline if (cy < eMin || cy > eMax || MathAbs(vy[ni] - vy[i]) < 1e-12) continue; double t = (cy - vy[i]) / (vy[ni] - vy[i]); if (t < 0.0 || t > 1.0) continue; xi[nc++] = vx[i] + t * (vx[ni] - vx[i]); } //--- Sort intersections left to right for (int a = 0; a < nc - 1; a++) for (int b = a + 1; b < nc; b++) if (xi[a] > xi[b]) { double tmp = xi[a]; xi[a] = xi[b]; xi[b] = tmp; } //--- Fill pixels between paired intersection spans for (int p = 0; p + 1 < nc; p += 2) for (int fx = (int)MathCeil(xi[p]); fx <= (int)MathFloor(xi[p + 1]); fx++) canvas.PixelSet(fx, scanY, argb); } } //+------------------------------------------------------------------+ //| Fill a quadrilateral using scanline rasterization | //+------------------------------------------------------------------+ void CCanvasPrimitives::FillQuadrilateralBorder(CCanvas &canvas, double &vx[], double &vy[], uint argb) { //--- Find vertical bounding extent of the quad double minY = vy[0], maxY = vy[0]; for (int i = 1; i < 4; i++) { if (vy[i] < minY) minY = vy[i]; if (vy[i] > maxY) maxY = vy[i]; } //--- Iterate over each horizontal scanline within the bounding box for (int scanY = (int)MathCeil(minY); scanY <= (int)MathCeil(maxY) - 1; scanY++) { double cy = (double)scanY + 0.5; double xi[8]; int nc = 0; //--- Compute X intersections with each edge of the quad for (int i = 0; i < 4; i++) { int ni = (i + 1) % 4; double eMin = (vy[i] < vy[ni]) ? vy[i] : vy[ni], eMax = (vy[i] > vy[ni]) ? vy[i] : vy[ni]; if (cy < eMin || cy > eMax || MathAbs(vy[ni] - vy[i]) < 1e-12) continue; double t = (cy - vy[i]) / (vy[ni] - vy[i]); if (t < 0.0 || t > 1.0) continue; xi[nc++] = vx[i] + t * (vx[ni] - vx[i]); } //--- Sort intersections left to right for (int a = 0; a < nc - 1; a++) for (int b = a + 1; b < nc; b++) if (xi[a] > xi[b]) { double tmp = xi[a]; xi[a] = xi[b]; xi[b] = tmp; } //--- Fill pixels between paired intersection spans for (int p = 0; p + 1 < nc; p += 2) for (int fx = (int)MathCeil(xi[p]); fx <= (int)MathCeil(xi[p + 1]) - 1; fx++) canvas.PixelSet(fx, scanY, argb); } } //+------------------------------------------------------------------+ //| Draw a thick border edge between two endpoints | //+------------------------------------------------------------------+ void CCanvasPrimitives::DrawBorderEdge(CCanvas &canvas, double x0, double y0, double x1, double y1, int thickness, uint argb) { //--- Compute edge direction vector and length double dx = x1 - x0, dy = y1 - y0, len = MathSqrt(dx * dx + dy * dy); //--- Skip degenerate edges with near-zero length if (len < 1e-6) return; //--- Compute perpendicular and unit direction vectors double px = -dy / len, py = dx / len, ex = dx / len, ey = dy / len; //--- Compute half-thickness and end cap extension double ht = thickness / 2.0, ext = 0.23 * thickness; //--- Extend start and end points slightly for mitre cap effect double sx = x0 - ex * ext, sy = y0 - ey * ext, ex2 = x1 + ex * ext, ey2 = y1 + ey * ext; //--- Build quad vertices offset perpendicular to edge direction double tvx[4] = { sx - px*ht, sx + px*ht, ex2 + px*ht, ex2 - px*ht }; double tvy[4] = { sy - py*ht, sy + py*ht, ey2 + py*ht, ey2 - py*ht }; //--- Fill the resulting quad as the border edge shape FillQuadrilateralBorder(canvas, tvx, tvy, argb); } //+------------------------------------------------------------------+ //| Check whether an angle falls within a start-to-end arc range | //+------------------------------------------------------------------+ bool CCanvasPrimitives::IsAngleBetween(double angle, double start, double end) { //--- Normalise all angles to the [0, 2π) range double tp = 2.0 * M_PI; angle = MathMod(angle + tp, tp); start = MathMod(start + tp, tp); end = MathMod(end + tp, tp); //--- Return true if angle lies within the arc from start to end return MathMod(angle - start + tp, tp) <= MathMod(end - start + tp, tp); } //+------------------------------------------------------------------+ //| Draw an anti-aliased corner arc with specified thickness | //+------------------------------------------------------------------+ void CCanvasPrimitives::DrawCornerArc(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb, double startAngle, double endAngle) { //--- Compute outer and inner radii for the arc ring double oR = (double)radius, iR = MathMax(0.0, (double)radius - thickness); //--- Extract alpha and RGB components from packed colour uchar bA = (uchar)((argb >> 24) & 0xFF); uint rgb = argb & 0x00FFFFFF; //--- Set sub-pixel sample count and pixel scan radius int sub = 4; double step = 1.0 / sub; int subSq = sub * sub, pr = (int)(oR + 2.0); //--- Iterate over pixels in the bounding box of the arc for (int dy = -pr; dy <= pr; dy++) for (int dx = -pr; dx <= pr; dx++) { double dist = MathSqrt((double)(dx * dx + dy * dy)); //--- Skip pixels clearly outside the ring or wrong angle if (dist > oR + 1.0 || dist < iR - 1.0) continue; if (!IsAngleBetween(MathArctan2((double)dy, (double)dx), startAngle, endAngle)) continue; //--- Fill pixels fully inside the ring without anti-aliasing if (dist <= oR - 1.0 && dist >= iR + 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; } //--- Count sub-pixel samples inside the arc ring and angle range int inside = 0; for (int sy = 0; sy < sub; sy++) for (int sx = 0; sx < sub; sx++) { double sdx = (double)dx - 0.5 + (sx + 0.5) * step, sdy = (double)dy - 0.5 + (sy + 0.5) * step; double sd = MathSqrt(sdx * sdx + sdy * sdy); if (sd >= iR && sd <= oR && IsAngleBetween(MathArctan2(sdy, sdx), startAngle, endAngle)) inside++; } if (inside == 0) continue; //--- Write fully opaque pixel if all sub-samples qualify if (inside >= subSq) canvas.PixelSet(cx + dx, cy + dy, argb); //--- Blend anti-aliased pixel using coverage fraction else BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb); } } //+------------------------------------------------------------------+ //| Draw rounded rect border with per-corner rounding at high res | //+------------------------------------------------------------------+ void CCanvasPrimitives::DrawSelectiveRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, int thickness, bool rTL, bool rTR, bool rBL, bool rBR) { //--- Skip drawing when border width is disabled if (BorderWidth <= 0) return; //--- Clamp radius to half the smallest dimension radius = MathMin(radius, MathMin(w / 2, h / 2)); //--- Compute per-corner radii based on rounding flags and half-thickness offset int tlR = rTL ? radius : 0, trR = rTR ? radius : 0, blR = rBL ? radius : 0, brR = rBR ? radius : 0, h2 = thickness / 2; //--- Draw top edge DrawBorderEdge(canvas, x + tlR, y + h2, x + w - trR, y + h2, thickness, argb); //--- Draw right edge if any right corner is rounded if (rTR || rBR) DrawBorderEdge(canvas, x + w - h2, y + trR, x + w - h2, y + h - brR, thickness, argb); //--- Draw bottom edge DrawBorderEdge(canvas, x + w - brR, y + h - h2, x + blR, y + h - h2, thickness, argb); //--- Draw left edge if any left corner is rounded if (rTL || rBL) DrawBorderEdge(canvas, x + h2, y + h - blR, x + h2, y + tlR, thickness, argb); //--- Draw top-left corner arc if rounded if (rTL) DrawCornerArc(canvas, x + radius, y + radius, radius, thickness, argb, M_PI, M_PI * 1.5); //--- Draw top-right corner arc if rounded if (rTR) DrawCornerArc(canvas, x + w - radius, y + radius, radius, thickness, argb, M_PI * 1.5, M_PI * 2.0); //--- Draw bottom-left corner arc if rounded if (rBL) DrawCornerArc(canvas, x + radius, y + h - radius, radius, thickness, argb, M_PI * 0.5, M_PI); //--- Draw bottom-right corner arc if rounded if (rBR) DrawCornerArc(canvas, x + w - radius, y + h - radius, radius, thickness, argb, 0.0, M_PI * 0.5); } //+------------------------------------------------------------------+ //| Draw a line between two points using Bresenham's algorithm | //+------------------------------------------------------------------+ void CCanvasPrimitives::DrawBresenhamLine(CCanvas &canvas, int x0, int y0, int x1, int y1, uint argb) { //--- Compute absolute deltas and step directions int dx = MathAbs(x1 - x0), dy = MathAbs(y1 - y0); int sx = (x0 < x1) ? 1 : -1, sy = (y0 < y1) ? 1 : -1, err = dx - dy; int w = canvas.Width(), h = canvas.Height(); //--- Iterate pixel by pixel until the endpoint is reached while (true) { //--- Blend the current pixel onto the canvas if within bounds if (x0 >= 0 && x0 < w && y0 >= 0 && y0 < h) BlendPixelSet(canvas, x0, y0, argb); //--- Stop when the endpoint is reached if (x0 == x1 && y0 == y1) break; int e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } //+------------------------------------------------------------------+ //| CLASS 2 — Manage and apply light and dark theme colour sets | //+------------------------------------------------------------------+ class CThemeManager : public CCanvasPrimitives { protected: bool m_isDarkTheme; // Active theme flag: true = dark, false = light ThemeColorSet m_themeColors; // Active colour set for the current theme protected: //--- Apply colour values matching the current theme state void ApplyTheme(); //--- Toggle between dark and light theme and reapply colours void ToggleTheme(); }; //+------------------------------------------------------------------+ //| Apply colour values matching the current theme state | //+------------------------------------------------------------------+ void CThemeManager::ApplyTheme() { //--- Apply dark theme colour assignments if (m_isDarkTheme) { m_themeColors.sidebarBackground = C'30,34,45'; // Dark navy background m_themeColors.sidebarBorder = C'200,210,225'; // Light blue-grey border m_themeColors.buttonHoverBackground = C'30,100,200'; // Blue hover background m_themeColors.buttonActiveBackground = C'41,98,255'; // Bright blue active background m_themeColors.buttonIconColor = C'220,225,235'; // Near-white icon colour m_themeColors.buttonIconActiveColor = clrWhite; // Pure white active icon m_themeColors.flyoutBackground = C'36,41,54'; // Dark flyout background m_themeColors.flyoutBorder = C'200,210,225'; // Light flyout border m_themeColors.flyoutItemHoverBackground = C'30,100,200'; // Blue flyout row hover m_themeColors.flyoutTextColor = C'200,210,225'; // Light flyout text m_themeColors.flyoutTextActiveColor = clrWhite; // White active flyout text m_themeColors.flyoutTitleColor = C'90,105,130'; // Muted blue-grey title m_themeColors.gripDotsColor = C'90,100,120'; // Muted slate grip dots m_themeColors.closeButtonHoverColor = C'235,55,55'; // Red close button hover m_themeColors.themeButtonHoverColor = C'255,200,50'; // Yellow theme button hover m_themeColors.separatorColor = C'44,50,64'; // Dark separator line m_themeColors.accentBarColor = C'41,98,255'; // Bright blue accent bar m_themeColors.scrollArrowColor = C'120,130,150'; // Muted scroll thumb m_themeColors.scrollArrowHoverColor = clrWhite; // White scroll thumb hover } else { //--- Apply light theme colour assignments m_themeColors.sidebarBackground = clrWhite; // White background m_themeColors.sidebarBorder = C'30,35,45'; // Dark border m_themeColors.buttonHoverBackground = C'30,100,200'; // Blue hover background m_themeColors.buttonActiveBackground = C'41,98,255'; // Bright blue active background m_themeColors.buttonIconColor = C'40,45,58'; // Dark icon colour m_themeColors.buttonIconActiveColor = clrWhite; // White active icon m_themeColors.flyoutBackground = clrWhite; // White flyout background m_themeColors.flyoutBorder = C'30,35,45'; // Dark flyout border m_themeColors.flyoutItemHoverBackground = C'30,100,200'; // Blue flyout row hover m_themeColors.flyoutTextColor = C'40,45,58'; // Dark flyout text m_themeColors.flyoutTextActiveColor = clrWhite; // White active flyout text m_themeColors.flyoutTitleColor = C'130,140,160'; // Muted grey title m_themeColors.gripDotsColor = C'160,170,185'; // Light grey grip dots m_themeColors.closeButtonHoverColor = C'210,35,35'; // Red close button hover m_themeColors.themeButtonHoverColor = C'150,100,0'; // Amber theme button hover m_themeColors.separatorColor = C'210,215,225'; // Light separator line m_themeColors.accentBarColor = C'41,98,255'; // Bright blue accent bar m_themeColors.scrollArrowColor = C'120,130,145'; // Muted scroll thumb m_themeColors.scrollArrowHoverColor = C'40,45,58'; // Dark scroll thumb hover } } //+------------------------------------------------------------------+ //| Toggle between dark and light theme and reapply colours | //+------------------------------------------------------------------+ void CThemeManager::ToggleTheme() { //--- Flip the active theme flag and reapply colour assignments m_isDarkTheme = !m_isDarkTheme; ApplyTheme(); } //+------------------------------------------------------------------+ //| CLASS 3 — Register all tool and category definitions | //+------------------------------------------------------------------+ class CToolRegistry : public CThemeManager { protected: CategoryDefinition m_categories[CAT_COUNT]; // Array of all category definitions protected: //--- Populate all categories and their associated tool lists void InitAllCategoriesAndTools(); //--- Append a single tool entry to the given category tool array void AddTool(ToolDefinition &arr[], TOOL_TYPE type, string label, string font, uchar code, string tooltip); //--- Return the category that owns the given active tool type ENUM_CATEGORY GetCategoryForActiveTool(TOOL_TYPE activeTool); //--- Return the number of chart clicks required to place the given tool int GetRequiredClickCount(TOOL_TYPE toolType); //--- Return the display label string for the given tool type string GetToolLabel(TOOL_TYPE toolType); }; //+------------------------------------------------------------------+ //| Append a single tool entry to a category tool array | //+------------------------------------------------------------------+ void CToolRegistry::AddTool(ToolDefinition &arr[], TOOL_TYPE type, string label, string font, uchar code, string tooltip) { //--- Expand the array by one slot to accommodate the new tool int sz = ArraySize(arr); ArrayResize(arr, sz + 1); //--- Populate all fields of the new tool definition arr[sz].toolType = type; arr[sz].toolLabel = label; arr[sz].iconFontName = font; arr[sz].iconCharCode = code; arr[sz].tooltipText = tooltip; } //+------------------------------------------------------------------+ //| Populate all categories and their associated tool lists | //+------------------------------------------------------------------+ void CToolRegistry::InitAllCategoriesAndTools() { //--- Assign Cursors category definition and reset its tool array m_categories[CAT_CURSORS].categoryLabel = "Cursors"; m_categories[CAT_CURSORS].iconFontName = ICON_CATEGORY_CURSORS.fontName; m_categories[CAT_CURSORS].iconCharCode = ICON_CATEGORY_CURSORS.charCode; ArrayResize(m_categories[CAT_CURSORS].tools, 0); //--- Add pointer and crosshair tools to Cursors AddTool(m_categories[CAT_CURSORS].tools, TOOL_POINTER, "Pointer", ICON_TOOL_POINTER.fontName, ICON_TOOL_POINTER.charCode, "Default Pointer"); AddTool(m_categories[CAT_CURSORS].tools, TOOL_CROSSHAIR, "Crosshair", ICON_TOOL_CROSSHAIR.fontName, ICON_TOOL_CROSSHAIR.charCode, "Crosshair / Measure"); //--- Assign Lines category definition and reset its tool array m_categories[CAT_LINES].categoryLabel = "Lines"; m_categories[CAT_LINES].iconFontName = ICON_CATEGORY_LINES.fontName; m_categories[CAT_LINES].iconCharCode = ICON_CATEGORY_LINES.charCode; ArrayResize(m_categories[CAT_LINES].tools, 0); //--- Add all line drawing tools to Lines AddTool(m_categories[CAT_LINES].tools, TOOL_TRENDLINE, "Trend Line", ICON_TOOL_TRENDLINE.fontName, ICON_TOOL_TRENDLINE.charCode, "Trend Line"); AddTool(m_categories[CAT_LINES].tools, TOOL_HLINE, "Horizontal", ICON_TOOL_HLINE.fontName, ICON_TOOL_HLINE.charCode, "Horizontal Line"); AddTool(m_categories[CAT_LINES].tools, TOOL_VLINE, "Vertical", ICON_TOOL_VLINE.fontName, ICON_TOOL_VLINE.charCode, "Vertical Line"); AddTool(m_categories[CAT_LINES].tools, TOOL_RAY, "Ray", ICON_TOOL_RAY.fontName, ICON_TOOL_RAY.charCode, "Ray Line"); AddTool(m_categories[CAT_LINES].tools, TOOL_EXTENDED_LINE, "Extended", ICON_TOOL_EXTENDED_LINE.fontName, ICON_TOOL_EXTENDED_LINE.charCode, "Extended Line"); AddTool(m_categories[CAT_LINES].tools, TOOL_INFO_LINE, "Info Line", ICON_TOOL_INFO_LINE.fontName, ICON_TOOL_INFO_LINE.charCode, "Info / Measure Line"); //--- Assign Channels category definition and reset its tool array m_categories[CAT_CHANNELS].categoryLabel = "Channels"; m_categories[CAT_CHANNELS].iconFontName = ICON_CATEGORY_CHANNELS.fontName; m_categories[CAT_CHANNELS].iconCharCode = ICON_CATEGORY_CHANNELS.charCode; ArrayResize(m_categories[CAT_CHANNELS].tools, 0); //--- Add all channel drawing tools to Channels AddTool(m_categories[CAT_CHANNELS].tools, TOOL_PARALLEL_CHANNEL, "Parallel Channel", ICON_TOOL_PARALLEL_CH.fontName, ICON_TOOL_PARALLEL_CH.charCode, "Parallel Channel"); AddTool(m_categories[CAT_CHANNELS].tools, TOOL_REGRESSION_CHANNEL, "Regression", ICON_TOOL_REGRESSION_CH.fontName, ICON_TOOL_REGRESSION_CH.charCode, "Regression Channel"); AddTool(m_categories[CAT_CHANNELS].tools, TOOL_STDDEV_CHANNEL, "Std Deviation", ICON_TOOL_STDDEV_CH.fontName, ICON_TOOL_STDDEV_CH.charCode, "Standard Deviation Channel"); //--- Assign Pitchfork category definition and reset its tool array m_categories[CAT_PITCHFORK].categoryLabel = "Pitchfork"; m_categories[CAT_PITCHFORK].iconFontName = ICON_CATEGORY_PITCHFORK.fontName; m_categories[CAT_PITCHFORK].iconCharCode = ICON_CATEGORY_PITCHFORK.charCode; ArrayResize(m_categories[CAT_PITCHFORK].tools, 0); //--- Add all pitchfork drawing tools to Pitchfork AddTool(m_categories[CAT_PITCHFORK].tools, TOOL_PITCHFORK, "Andrew's Fork", ICON_TOOL_PITCHFORK.fontName, ICON_TOOL_PITCHFORK.charCode, "Andrew's Pitchfork"); AddTool(m_categories[CAT_PITCHFORK].tools, TOOL_SCHIFF_PITCHFORK, "Schiff Fork", ICON_TOOL_SCHIFF.fontName, ICON_TOOL_SCHIFF.charCode, "Schiff Pitchfork"); AddTool(m_categories[CAT_PITCHFORK].tools, TOOL_MOD_SCHIFF, "Mod. Schiff", ICON_TOOL_MOD_SCHIFF.fontName, ICON_TOOL_MOD_SCHIFF.charCode, "Modified Schiff Pitchfork"); //--- Assign Gann category definition and reset its tool array m_categories[CAT_GANN].categoryLabel = "Gann"; m_categories[CAT_GANN].iconFontName = ICON_CATEGORY_GANN.fontName; m_categories[CAT_GANN].iconCharCode = ICON_CATEGORY_GANN.charCode; ArrayResize(m_categories[CAT_GANN].tools, 0); //--- Add all Gann drawing tools to Gann AddTool(m_categories[CAT_GANN].tools, TOOL_GANN_LINE, "Gann Line", ICON_TOOL_GANN_LINE.fontName, ICON_TOOL_GANN_LINE.charCode, "Gann Line"); AddTool(m_categories[CAT_GANN].tools, TOOL_GANN_FAN, "Gann Fan", ICON_TOOL_GANN_FAN.fontName, ICON_TOOL_GANN_FAN.charCode, "Gann Fan"); AddTool(m_categories[CAT_GANN].tools, TOOL_GANN_GRID, "Gann Grid", ICON_TOOL_GANN_GRID.fontName, ICON_TOOL_GANN_GRID.charCode, "Gann Grid"); //--- Assign Fibonacci category definition and reset its tool array m_categories[CAT_FIBONACCI].categoryLabel = "Fibonacci"; m_categories[CAT_FIBONACCI].iconFontName = ICON_CATEGORY_FIBONACCI.fontName; m_categories[CAT_FIBONACCI].iconCharCode = ICON_CATEGORY_FIBONACCI.charCode; ArrayResize(m_categories[CAT_FIBONACCI].tools, 0); //--- Add all Fibonacci drawing tools to Fibonacci AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_RETRACEMENT, "Retracement", ICON_TOOL_FIBO_RET.fontName, ICON_TOOL_FIBO_RET.charCode, "Fibonacci Retracement"); AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_EXPANSION, "Expansion", ICON_TOOL_FIBO_EXP.fontName, ICON_TOOL_FIBO_EXP.charCode, "Fibonacci Expansion"); AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_CHANNEL, "Fib Channel", ICON_TOOL_FIBO_CH.fontName, ICON_TOOL_FIBO_CH.charCode, "Fibonacci Channel"); AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_TIMEZONES, "Time Zones", ICON_TOOL_FIBO_TZ.fontName, ICON_TOOL_FIBO_TZ.charCode, "Fibonacci Time Zones"); AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_FAN, "Fib Fan", ICON_TOOL_FIBO_FAN.fontName, ICON_TOOL_FIBO_FAN.charCode, "Fibonacci Fan"); AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_ARCS, "Fib Arcs", ICON_TOOL_FIBO_ARCS.fontName, ICON_TOOL_FIBO_ARCS.charCode, "Fibonacci Arcs"); //--- Assign Shapes category definition and reset its tool array m_categories[CAT_SHAPES].categoryLabel = "Shapes"; m_categories[CAT_SHAPES].iconFontName = ICON_CATEGORY_SHAPES.fontName; m_categories[CAT_SHAPES].iconCharCode = ICON_CATEGORY_SHAPES.charCode; ArrayResize(m_categories[CAT_SHAPES].tools, 0); //--- Add all shape drawing tools to Shapes AddTool(m_categories[CAT_SHAPES].tools, TOOL_RECTANGLE, "Rectangle", ICON_TOOL_RECTANGLE.fontName, ICON_TOOL_RECTANGLE.charCode, "Rectangle"); AddTool(m_categories[CAT_SHAPES].tools, TOOL_TRIANGLE, "Triangle", ICON_TOOL_TRIANGLE.fontName, ICON_TOOL_TRIANGLE.charCode, "Triangle"); AddTool(m_categories[CAT_SHAPES].tools, TOOL_ELLIPSE, "Ellipse", ICON_TOOL_ELLIPSE.fontName, ICON_TOOL_ELLIPSE.charCode, "Ellipse"); //--- Assign Annotations category definition and reset its tool array m_categories[CAT_ANNOTATIONS].categoryLabel = "Annotate"; m_categories[CAT_ANNOTATIONS].iconFontName = ICON_CATEGORY_ANNOTATIONS.fontName; m_categories[CAT_ANNOTATIONS].iconCharCode = ICON_CATEGORY_ANNOTATIONS.charCode; ArrayResize(m_categories[CAT_ANNOTATIONS].tools, 0); //--- Add all annotation tools to Annotations AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_TEXT, "Text", ICON_TOOL_TEXT.fontName, ICON_TOOL_TEXT.charCode, "Text Label"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_ARROW_UP, "Arrow Up", ICON_TOOL_ARROW_UP.fontName, ICON_TOOL_ARROW_UP.charCode, "Arrow Up"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_ARROW_DOWN, "Arrow Down", ICON_TOOL_ARROW_DOWN.fontName, ICON_TOOL_ARROW_DOWN.charCode, "Arrow Down"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_THUMB_UP, "Thumb Up", ICON_TOOL_THUMB_UP.fontName, ICON_TOOL_THUMB_UP.charCode, "Thumbs Up"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_THUMB_DOWN, "Thumb Down", ICON_TOOL_THUMB_DOWN.fontName, ICON_TOOL_THUMB_DOWN.charCode, "Thumbs Down"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_PRICE_LABEL, "Price Label", ICON_TOOL_PRICE_LABEL.fontName, ICON_TOOL_PRICE_LABEL.charCode, "Left Price Label"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_STOP_SIGN, "Stop Sign", ICON_TOOL_STOP_SIGN.fontName, ICON_TOOL_STOP_SIGN.charCode, "Stop Sign"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_CHECK_MARK, "Check Mark", ICON_TOOL_CHECK_MARK.fontName, ICON_TOOL_CHECK_MARK.charCode, "Check Mark"); } //+------------------------------------------------------------------+ //| Return the category that owns the given active tool type | //+------------------------------------------------------------------+ ENUM_CATEGORY CToolRegistry::GetCategoryForActiveTool(TOOL_TYPE activeTool) { //--- Return no category for inactive or pointer tool states if (activeTool == TOOL_NONE || activeTool == TOOL_POINTER) return CAT_NONE; //--- Search all categories and their tool lists for a match for (int c = 0; c < CAT_COUNT; c++) for (int t = 0; t < ArraySize(m_categories[c].tools); t++) if (m_categories[c].tools[t].toolType == activeTool) return (ENUM_CATEGORY)c; return CAT_NONE; } //+------------------------------------------------------------------+ //| Return click count required to place the given tool | //+------------------------------------------------------------------+ int CToolRegistry::GetRequiredClickCount(TOOL_TYPE toolType) { switch (toolType) { //--- Cursor tools require no chart clicks case TOOL_POINTER: case TOOL_CROSSHAIR: return 0; //--- Single-click tools are placed with one chart interaction case TOOL_HLINE: case TOOL_VLINE: case TOOL_TEXT: case TOOL_ARROW_UP: case TOOL_ARROW_DOWN: case TOOL_THUMB_UP: case TOOL_THUMB_DOWN: case TOOL_PRICE_LABEL: case TOOL_STOP_SIGN: case TOOL_CHECK_MARK: case TOOL_FIBO_TIMEZONES: return 1; //--- Two-click tools require a start and end point case TOOL_TRENDLINE: case TOOL_RAY: case TOOL_EXTENDED_LINE: case TOOL_INFO_LINE: case TOOL_RECTANGLE: case TOOL_TRIANGLE: case TOOL_ELLIPSE: case TOOL_FIBO_RETRACEMENT: case TOOL_FIBO_EXPANSION: case TOOL_FIBO_FAN: case TOOL_FIBO_ARCS: case TOOL_GANN_LINE: case TOOL_GANN_FAN: case TOOL_GANN_GRID: case TOOL_REGRESSION_CHANNEL: case TOOL_STDDEV_CHANNEL: return 2; //--- Three-click tools require three anchor points case TOOL_PARALLEL_CHANNEL: case TOOL_FIBO_CHANNEL: case TOOL_PITCHFORK: case TOOL_SCHIFF_PITCHFORK: case TOOL_MOD_SCHIFF: return 3; //--- Default to single click for unrecognised tool types default: return 1; } } //+------------------------------------------------------------------+ //| Return the display label string for the given tool type | //+------------------------------------------------------------------+ string CToolRegistry::GetToolLabel(TOOL_TYPE toolType) { //--- Search all categories and tool lists for a label match for (int c = 0; c < CAT_COUNT; c++) for (int t = 0; t < ArraySize(m_categories[c].tools); t++) if (m_categories[c].tools[t].toolType == toolType) return m_categories[c].tools[t].toolLabel; return "None"; } //+------------------------------------------------------------------+ //| CLASS 4 — Create, destroy, and resize all canvas layers | //+------------------------------------------------------------------+ class CCanvasLayer : public CToolRegistry { protected: int m_supersampleFactor; // Supersampling multiplier for high-res rendering long m_chartId; // Chart identifier this layer belongs to CCanvas m_canvasSidebar; // Final display-resolution sidebar canvas CCanvas m_canvasSidebarHighRes; // High-resolution sidebar canvas for supersampling CCanvas m_canvasFlyout; // Final display-resolution flyout canvas CCanvas m_canvasFlyoutHighRes; // High-resolution flyout canvas for supersampling CCanvas m_canvasReticle; // Crosshair reticle tick-mark canvas CCanvas m_canvasMagnifier; // Circular magnifier lens canvas CCanvas m_canvasCrossVertical; // Crosshair vertical line canvas (1 × chartH) CCanvas m_canvasCrossHorizontal; // Crosshair horizontal line canvas (chartW × 1) CCanvas m_canvasCrossPriceLabel; // Crosshair price axis label canvas CCanvas m_canvasCrossTimeLabel; // Crosshair time axis label canvas CCanvas m_canvasMeasureVertical; // Measure mode vertical anchor line canvas CCanvas m_canvasMeasureHorizontal; // Measure mode horizontal anchor line canvas CCanvas m_canvasMeasurePriceLabel; // Measure mode price axis label canvas CCanvas m_canvasMeasureTimeLabel; // Measure mode time axis label canvas CCanvas m_canvasMeasureDiagonalLine; // Measure mode diagonal line canvas (chartW × chartH) string m_nameSidebar; // Object name of the sidebar bitmap label string m_nameFlyout; // Object name of the flyout bitmap label string m_nameReticle; // Object name of the reticle bitmap label string m_nameMagnifier; // Object name of the magnifier bitmap label string m_nameCrossVertical; // Object name of the crosshair vertical bitmap label string m_nameCrossHorizontal; // Object name of the crosshair horizontal bitmap label string m_nameCrossPriceLabel; // Object name of the crosshair price label bitmap string m_nameCrossTimeLabel; // Object name of the crosshair time label bitmap string m_nameMeasureVertical; // Object name of the measure vertical bitmap label string m_nameMeasureHorizontal; // Object name of the measure horizontal bitmap label string m_nameMeasurePriceLabel; // Object name of the measure price label bitmap string m_nameMeasureTimeLabel; // Object name of the measure time label bitmap string m_nameMeasureDiagonalLine; // Object name of the measure diagonal line bitmap protected: //--- Create all canvas objects at the given sidebar dimensions bool CreateAllCanvases(int w, int h); //--- Destroy all canvas objects and remove chart objects void DestroyAllCanvases(); //--- Resize both sidebar canvases to the given dimensions void ResizeSidebarCanvases(int w, int h); //--- Fill the crosshair vertical line canvas with the foreground colour void DrawCrossVerticalLinePixels(int chartH); //--- Fill the crosshair horizontal line canvas with the foreground colour void DrawCrossHorizontalLinePixels(int chartW); //--- Fill the measure vertical line canvas with the foreground colour at reduced opacity void DrawMeasureVerticalLinePixels(int chartH); //--- Fill the measure horizontal line canvas with the foreground colour at reduced opacity void DrawMeasureHorizontalLinePixels(int chartW); }; //+------------------------------------------------------------------+ //| Create all canvas objects at the given sidebar dimensions | //+------------------------------------------------------------------+ bool CCanvasLayer::CreateAllCanvases(int w, int h) { //--- Read current chart dimensions for full-width/height canvas sizing int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Create the display-resolution sidebar bitmap label canvas if (!m_canvasSidebar.CreateBitmapLabel(0, 0, m_nameSidebar, 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create sidebar canvas"); return false; } //--- Create the high-resolution sidebar canvas for supersampled drawing if (!m_canvasSidebarHighRes.Create("ToolsPalette_SidebarHR", w * m_supersampleFactor, h * m_supersampleFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create sidebar HR canvas"); return false; } //--- Create the display-resolution flyout bitmap label canvas if (!m_canvasFlyout.CreateBitmapLabel(0, 0, m_nameFlyout, 0, 0, 200, 200, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create flyout canvas"); return false; } //--- Create the high-resolution flyout canvas for supersampled drawing if (!m_canvasFlyoutHighRes.Create("ToolsPalette_FlyoutHR", 200 * m_supersampleFactor, 200 * m_supersampleFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create flyout HR canvas"); return false; } //--- Create the reticle canvas sized to fit the tick-mark geometry int reticleSize = 2 * (ReticleOffset + ReticleTickLen / 2) + 6; if (!m_canvasReticle.CreateBitmapLabel(0, 0, m_nameReticle, 0, 0, reticleSize, reticleSize, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create reticle canvas"); return false; } //--- Hide the reticle until the crosshair tool is active ObjectSetInteger(0, m_nameReticle, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_nameReticle, OBJPROP_ZORDER, 90); //--- Create the magnifier lens canvas if (!m_canvasMagnifier.CreateBitmapLabel(0, 0, m_nameMagnifier, 0, 0, MagDiameter, MagDiameter, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create magnifier canvas"); return false; } //--- Hide the magnifier until the crosshair tool is active ObjectSetInteger(0, m_nameMagnifier, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_nameMagnifier, OBJPROP_ZORDER, 95); //--- Create the crosshair vertical line canvas (1 pixel wide, full chart height) if (!m_canvasCrossVertical.CreateBitmapLabel(0, 0, m_nameCrossVertical, 0, 0, 1, chartH, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create cross vertical canvas"); return false; } ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_ZORDER, 80); //--- Pre-fill the vertical line pixels with the chart foreground colour DrawCrossVerticalLinePixels(chartH); //--- Create the crosshair horizontal line canvas (full chart width, 1 pixel tall) if (!m_canvasCrossHorizontal.CreateBitmapLabel(0, 0, m_nameCrossHorizontal, 0, 0, chartW, 1, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create cross horizontal canvas"); return false; } ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_ZORDER, 80); //--- Pre-fill the horizontal line pixels with the chart foreground colour DrawCrossHorizontalLinePixels(chartW); //--- Create the crosshair price axis label canvas if (!m_canvasCrossPriceLabel.CreateBitmapLabel(0, 0, m_nameCrossPriceLabel, 0, 0, 80, 18, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create cross price label canvas"); return false; } ObjectSetInteger(0, m_nameCrossPriceLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_nameCrossPriceLabel, OBJPROP_ZORDER, 85); //--- Create the crosshair time axis label canvas if (!m_canvasCrossTimeLabel.CreateBitmapLabel(0, 0, m_nameCrossTimeLabel, 0, 0, 140, 18, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create cross time label canvas"); return false; } ObjectSetInteger(0, m_nameCrossTimeLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_nameCrossTimeLabel, OBJPROP_ZORDER, 85); //--- Create the measure mode vertical anchor line canvas if (!m_canvasMeasureVertical.CreateBitmapLabel(0, 0, m_nameMeasureVertical, 0, 0, 1, chartH, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create measure vertical canvas"); return false; } ObjectSetInteger(0, m_nameMeasureVertical, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_nameMeasureVertical, OBJPROP_ZORDER, 79); //--- Pre-fill the measure vertical pixels at reduced opacity DrawMeasureVerticalLinePixels(chartH); //--- Create the measure mode horizontal anchor line canvas if (!m_canvasMeasureHorizontal.CreateBitmapLabel(0, 0, m_nameMeasureHorizontal, 0, 0, chartW, 1, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create measure horizontal canvas"); return false; } ObjectSetInteger(0, m_nameMeasureHorizontal, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_nameMeasureHorizontal, OBJPROP_ZORDER, 79); //--- Pre-fill the measure horizontal pixels at reduced opacity DrawMeasureHorizontalLinePixels(chartW); //--- Create the measure mode price axis label canvas if (!m_canvasMeasurePriceLabel.CreateBitmapLabel(0, 0, m_nameMeasurePriceLabel, 0, 0, 80, 18, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create measure price label canvas"); return false; } ObjectSetInteger(0, m_nameMeasurePriceLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_nameMeasurePriceLabel, OBJPROP_ZORDER, 84); //--- Create the measure mode time axis label canvas if (!m_canvasMeasureTimeLabel.CreateBitmapLabel(0, 0, m_nameMeasureTimeLabel, 0, 0, 140, 18, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create measure time label canvas"); return false; } ObjectSetInteger(0, m_nameMeasureTimeLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_nameMeasureTimeLabel, OBJPROP_ZORDER, 84); //--- Create the measure mode diagonal line canvas (full chart size) if (!m_canvasMeasureDiagonalLine.CreateBitmapLabel(0, 0, m_nameMeasureDiagonalLine, 0, 0, chartW, chartH, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create measure diagonal canvas"); return false; } ObjectSetInteger(0, m_nameMeasureDiagonalLine, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_nameMeasureDiagonalLine, OBJPROP_ZORDER, 78); //--- Clear the diagonal canvas to fully transparent and flush m_canvasMeasureDiagonalLine.Erase(0x00000000); m_canvasMeasureDiagonalLine.Update(); return true; } //+------------------------------------------------------------------+ //| Destroy all canvas objects and remove chart objects | //+------------------------------------------------------------------+ void CCanvasLayer::DestroyAllCanvases() { //--- ALL canvas objects created with CreateBitmapLabel need explicit ObjectDelete //--- after Destroy() — without it they stay as ghost objects on the chart and //--- CreateBitmapLabel fails silently on the next Init() call (parameter change restart) m_canvasSidebar.Destroy(); ObjectDelete(0, m_nameSidebar); m_canvasSidebarHighRes.Destroy(); m_canvasFlyout.Destroy(); ObjectDelete(0, m_nameFlyout); m_canvasFlyoutHighRes.Destroy(); m_canvasReticle.Destroy(); ObjectDelete(0, m_nameReticle); m_canvasMagnifier.Destroy(); ObjectDelete(0, m_nameMagnifier); m_canvasCrossVertical.Destroy(); ObjectDelete(0, m_nameCrossVertical); m_canvasCrossHorizontal.Destroy(); ObjectDelete(0, m_nameCrossHorizontal); m_canvasCrossPriceLabel.Destroy(); ObjectDelete(0, m_nameCrossPriceLabel); m_canvasCrossTimeLabel.Destroy(); ObjectDelete(0, m_nameCrossTimeLabel); m_canvasMeasureVertical.Destroy(); ObjectDelete(0, m_nameMeasureVertical); m_canvasMeasureHorizontal.Destroy(); ObjectDelete(0, m_nameMeasureHorizontal); m_canvasMeasurePriceLabel.Destroy(); ObjectDelete(0, m_nameMeasurePriceLabel); m_canvasMeasureTimeLabel.Destroy(); ObjectDelete(0, m_nameMeasureTimeLabel); m_canvasMeasureDiagonalLine.Destroy(); ObjectDelete(0, m_nameMeasureDiagonalLine); } //+------------------------------------------------------------------+ //| Resize both sidebar canvases to the given dimensions | //+------------------------------------------------------------------+ void CCanvasLayer::ResizeSidebarCanvases(int w, int h) { //--- Resize the display-resolution sidebar canvas m_canvasSidebar.Resize(w, h); //--- Update the chart object dimensions to match ObjectSetInteger(0, m_nameSidebar, OBJPROP_XSIZE, w); ObjectSetInteger(0, m_nameSidebar, OBJPROP_YSIZE, h); //--- Resize the high-resolution canvas scaled by the supersample factor m_canvasSidebarHighRes.Resize(w * m_supersampleFactor, h * m_supersampleFactor); } //+------------------------------------------------------------------+ //| Fill the crosshair vertical line canvas with foreground colour | //+------------------------------------------------------------------+ void CCanvasLayer::DrawCrossVerticalLinePixels(int chartH) { //--- Clear the canvas to fully transparent m_canvasCrossVertical.Erase(0x00000000); //--- Pack the chart foreground colour at full opacity uint col = ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255); //--- Set every pixel in the single-column canvas to the foreground colour for (int y = 0; y < chartH; y++) m_canvasCrossVertical.PixelSet(0, y, col); m_canvasCrossVertical.Update(); } //+------------------------------------------------------------------+ //| Fill the crosshair horizontal line canvas with foreground colour | //+------------------------------------------------------------------+ void CCanvasLayer::DrawCrossHorizontalLinePixels(int chartW) { //--- Clear the canvas to fully transparent m_canvasCrossHorizontal.Erase(0x00000000); //--- Pack the chart foreground colour at full opacity uint col = ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255); //--- Set every pixel in the single-row canvas to the foreground colour for (int x = 0; x < chartW; x++) m_canvasCrossHorizontal.PixelSet(x, 0, col); m_canvasCrossHorizontal.Update(); } //+------------------------------------------------------------------+ //| Fill measure vertical line canvas at reduced opacity | //+------------------------------------------------------------------+ void CCanvasLayer::DrawMeasureVerticalLinePixels(int chartH) { //--- Clear the canvas to fully transparent m_canvasMeasureVertical.Erase(0x00000000); //--- Pack the chart foreground colour at 200/255 opacity for visual distinction uint col = ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 200); //--- Set every pixel in the single-column canvas for (int y = 0; y < chartH; y++) m_canvasMeasureVertical.PixelSet(0, y, col); m_canvasMeasureVertical.Update(); } //+------------------------------------------------------------------+ //| Fill measure horizontal line canvas at reduced opacity | //+------------------------------------------------------------------+ void CCanvasLayer::DrawMeasureHorizontalLinePixels(int chartW) { //--- Clear the canvas to fully transparent m_canvasMeasureHorizontal.Erase(0x00000000); //--- Pack the chart foreground colour at 200/255 opacity for visual distinction uint col = ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 200); //--- Set every pixel in the single-row canvas for (int x = 0; x < chartW; x++) m_canvasMeasureHorizontal.PixelSet(x, 0, col); m_canvasMeasureHorizontal.Update(); } //+------------------------------------------------------------------+ //| CLASS 5 — Compute and maintain sidebar layout and geometry | //+------------------------------------------------------------------+ class CSidebarLayout : public CCanvasLayer { protected: int m_panelX; // Horizontal position of the sidebar panel int m_panelY; // Vertical position of the sidebar panel int m_sidebarWidth; // Width of the sidebar panel in pixels int m_sidebarHeight; // Height of the sidebar panel in pixels int m_categoryButtonSize; // Size of each category button in pixels int m_categoryButtonPadding; // Vertical gap between category buttons int m_panelCornerRadius; // Corner rounding radius of the panel int m_headerGripHeight; // Height of the top header and grip area ENUM_SNAP_STATE m_snapState; // Current snap alignment state int m_sidebarMaxVisibleCats; // Maximum number of visible category buttons int m_sidebarScrollPixels; // Current vertical scroll offset in pixels int m_sidebarScrollThumbHeight; // Height of the sidebar scroll thumb pill int m_sidebarScrollThinWidth; // Width of the sidebar scroll thumb pill bool m_isSidebarThumbDragging; // Flag indicating scroll thumb drag in progress int m_sidebarThumbDragStartY; // Mouse Y when sidebar thumb drag started int m_sidebarThumbDragStartPixels;// Scroll offset when sidebar thumb drag started bool m_isHoveredSidebarScrollArea;// Flag indicating mouse is over sidebar scroll area bool m_isHoveredSidebarThumb; // Flag indicating mouse is over sidebar scroll thumb bool m_isPanelDragging; // Flag indicating panel drag in progress int m_dragOffsetX; // Mouse X offset from panel origin when drag started int m_dragOffsetY; // Mouse Y offset from panel origin when drag started bool m_isResizingBottomEdge; // Flag indicating bottom resize drag in progress int m_bottomResizeDragStartY; // Mouse Y when bottom resize drag started int m_bottomResizeStartHeight; // Panel height when bottom resize drag started int m_snappedSidebarHeight; // User-set height override while panel is snapped bool m_isBottomResizeHovered; // Flag indicating mouse is over the bottom resize grip protected: //--- Compute and set the sidebar panel height based on available chart space void CalcSidebarHeight(); //--- Compute the Y pixel position of a category button by index int CalcCategoryButtonY(int idx); //--- Compute the top clipping boundary for the category button area int CalcClipTop(); //--- Compute the bottom clipping boundary for the category button area int CalcClipBottom(); //--- Compute total pixel height of all category buttons stacked int CalcSidebarTotalScrollPixels(); //--- Compute the visible viewport pixel height for category buttons int CalcSidebarViewportPixels(); //--- Compute the maximum allowable scroll offset in pixels int CalcSidebarMaxScrollPixels(); //--- Check whether the category button at the given index is within the visible clip area bool IsCategoryButtonVisible(int idx); //--- Attempt to snap the panel to a chart edge based on current position void TrySnapToEdge(); //--- Test whether the given screen coordinates hit the sidebar panel bool HitTestOverSidebar(int mouseX, int mouseY, int &lx, int &ly); //--- Return the category under the given local coordinates, or CAT_NONE ENUM_CATEGORY HitTestCategoryButton(int lx, int ly); //--- Test whether the given local coordinates hit the grip drag area bool HitTestOverGripArea(int lx, int ly); //--- Test whether the given local coordinates hit the close button area bool HitTestOverCloseButton(int lx, int ly); //--- Test whether the given local coordinates hit the theme toggle button area bool HitTestOverThemeButton(int lx, int ly); //--- Test whether the given local coordinates hit the bottom resize grip bool HitTestOverBottomResizeGrip(int lx, int ly); }; //+------------------------------------------------------------------+ //| Compute and set sidebar height based on available chart space | //+------------------------------------------------------------------+ void CSidebarLayout::CalcSidebarHeight() { //--- Get current chart height in pixels int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Define vertical padding constants int topPad = 8, botPad = 10; //--- Set button gap spacing m_categoryButtonPadding = 6; //--- Handle snapped panel height computation if (m_snapState != SNAP_FLOAT) { //--- Pin panel to fixed Y offset below chart top m_panelY = 30; ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY); //--- Compute maximum available height below panel top offset int availH = chartH - m_panelY - 8; //--- Compute ideal natural height to fit all category buttons int naturalH = m_headerGripHeight + topPad + CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad; //--- Compute minimum height to show at least three category buttons int minH = m_headerGripHeight + topPad + 3 * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad; //--- Apply user-set snapped height override if present, otherwise use natural size if (m_snappedSidebarHeight > 0) m_sidebarHeight = MathMax(minH, MathMin(MathMin(naturalH, availH), m_snappedSidebarHeight)); else m_sidebarHeight = MathMax(minH, MathMin(naturalH, availH)); } else { //--- Compute natural and maximum height bounds for a floating panel int naturalH = m_headerGripHeight + topPad + CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad; int maxH = chartH - m_panelY - 20; int minH = m_headerGripHeight + topPad + 3 * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad; //--- Clamp the floating panel height within valid bounds if (m_sidebarHeight < minH || m_sidebarHeight > MathMin(naturalH, maxH)) m_sidebarHeight = MathMin(naturalH, maxH); } //--- Compute usable height for the button area int btnAreaH = m_sidebarHeight - m_headerGripHeight - topPad - botPad; //--- Compute total height needed for all buttons at natural spacing int fullBtnH = CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding; //--- All buttons fit: show all and clear scroll offset if (fullBtnH <= btnAreaH) { m_sidebarMaxVisibleCats = CAT_COUNT; m_sidebarScrollPixels = 0; } else { //--- Compute how many buttons fit and clamp scroll offset within valid range m_sidebarMaxVisibleCats = MathMax(3, MathMin(CAT_COUNT, btnAreaH / (m_categoryButtonSize + m_categoryButtonPadding))); m_sidebarScrollPixels = MathMax(0, MathMin(m_sidebarScrollPixels, CalcSidebarMaxScrollPixels())); } } //+------------------------------------------------------------------+ //| Compute Y pixel position of a category button by index | //+------------------------------------------------------------------+ int CSidebarLayout::CalcCategoryButtonY(int idx) { //--- Return scroll-adjusted Y offset below the header grip area return m_headerGripHeight + 8 + idx * (m_categoryButtonSize + m_categoryButtonPadding) - m_sidebarScrollPixels; } //+------------------------------------------------------------------+ //| Compute top clip boundary for the category button area | //+------------------------------------------------------------------+ int CSidebarLayout::CalcClipTop() { //--- Return Y position just below the header grip bottom edge return m_headerGripHeight + 8; } //+------------------------------------------------------------------+ //| Compute bottom clip boundary for the category button area | //+------------------------------------------------------------------+ int CSidebarLayout::CalcClipBottom() { //--- Return Y position leaving bottom padding inside the panel return m_sidebarHeight - 10; } //+------------------------------------------------------------------+ //| Compute total pixel height of all category buttons stacked | //+------------------------------------------------------------------+ int CSidebarLayout::CalcSidebarTotalScrollPixels() { //--- Return the combined height of all buttons including inter-button gaps return CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding; } //+------------------------------------------------------------------+ //| Compute the visible viewport pixel height for category buttons | //+------------------------------------------------------------------+ int CSidebarLayout::CalcSidebarViewportPixels() { //--- Return the pixel height of the visible button clip region return CalcClipBottom() - CalcClipTop(); } //+------------------------------------------------------------------+ //| Compute the maximum allowable scroll offset in pixels | //+------------------------------------------------------------------+ int CSidebarLayout::CalcSidebarMaxScrollPixels() { //--- Return zero if all buttons fit; otherwise return the overflow amount return MathMax(0, CalcSidebarTotalScrollPixels() - CalcSidebarViewportPixels()); } //+------------------------------------------------------------------+ //| Check whether a category button is within the visible clip area | //+------------------------------------------------------------------+ bool CSidebarLayout::IsCategoryButtonVisible(int idx) { //--- All buttons are visible when scroll is not needed if (m_sidebarMaxVisibleCats >= CAT_COUNT) return true; //--- Compute the button's scroll-adjusted Y position int y = CalcCategoryButtonY(idx); //--- Return true if the button overlaps the clip region return (y + m_categoryButtonSize > CalcClipTop() && y < CalcClipBottom()); } //+------------------------------------------------------------------+ //| Attempt to snap the panel to a chart edge based on position | //+------------------------------------------------------------------+ void CSidebarLayout::TrySnapToEdge() { //--- Get current chart width for right-edge detection int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); ENUM_SNAP_STATE prev = m_snapState; //--- Snap to left edge if panel is within the snap threshold if (m_panelX <= SnapThreshold) { m_snapState = SNAP_LEFT; m_panelX = 0; if (prev == SNAP_FLOAT) m_snappedSidebarHeight = 0; } //--- Snap to right edge if panel right boundary is within the snap threshold else if (m_panelX + m_sidebarWidth >= chartW - SnapThreshold) { m_snapState = SNAP_RIGHT; m_panelX = chartW - m_sidebarWidth; if (prev == SNAP_FLOAT) m_snappedSidebarHeight = 0; } else { //--- Set floating state and clear snapped height when leaving a snapped edge m_snapState = SNAP_FLOAT; if (prev != SNAP_FLOAT) { m_snappedSidebarHeight = 0; m_categoryButtonPadding = 6; } } //--- Update the chart object X position to reflect the snapped or free position ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); } //+------------------------------------------------------------------+ //| Test whether screen coordinates hit the sidebar panel | //+------------------------------------------------------------------+ bool CSidebarLayout::HitTestOverSidebar(int mouseX, int mouseY, int &lx, int &ly) { //--- Compute local coordinates relative to the panel origin lx = mouseX - m_panelX; ly = mouseY - m_panelY; //--- Return true if local coordinates fall within the panel bounds return (lx >= 0 && lx < m_sidebarWidth && ly >= 0 && ly < m_sidebarHeight); } //+------------------------------------------------------------------+ //| Return the category under local coordinates, or CAT_NONE | //+------------------------------------------------------------------+ ENUM_CATEGORY CSidebarLayout::HitTestCategoryButton(int lx, int ly) { //--- Reject coordinates outside the category button clip region if (ly < CalcClipTop() || ly >= CalcClipBottom()) return CAT_NONE; //--- Compute horizontal start of the centred button column int btnX = (m_sidebarWidth - m_categoryButtonSize) / 2; //--- Test each visible category button for a hit for (int c = 0; c < CAT_COUNT; c++) { if (!IsCategoryButtonVisible(c)) continue; int btnY = CalcCategoryButtonY(c); //--- Return category if local coordinates fall within the button bounds if (lx >= btnX && lx <= btnX + m_categoryButtonSize && ly >= btnY && ly <= btnY + m_categoryButtonSize && ly < m_sidebarHeight - 8) return (ENUM_CATEGORY)c; } return CAT_NONE; } //+------------------------------------------------------------------+ //| Test whether local coordinates hit the grip drag area | //+------------------------------------------------------------------+ bool CSidebarLayout::HitTestOverGripArea(int lx, int ly) { //--- Return true if coordinates fall within the horizontal grip strip return (lx >= 0 && lx < m_sidebarWidth && ly >= m_categoryButtonSize && ly < m_categoryButtonSize + 20); } //+------------------------------------------------------------------+ //| Test whether local coordinates hit the close button area | //+------------------------------------------------------------------+ bool CSidebarLayout::HitTestOverCloseButton(int lx, int ly) { //--- Return true if coordinates fall within the top close button slot return (lx >= 0 && lx < m_sidebarWidth && ly >= 0 && ly < m_categoryButtonSize); } //+------------------------------------------------------------------+ //| Test whether local coordinates hit the theme toggle button area | //+------------------------------------------------------------------+ bool CSidebarLayout::HitTestOverThemeButton(int lx, int ly) { //--- Return true if coordinates fall within the theme toggle row return (lx >= 0 && lx < m_sidebarWidth && ly >= m_categoryButtonSize + 20 && ly < m_headerGripHeight); } //+------------------------------------------------------------------+ //| Test whether local coordinates hit the bottom resize grip | //+------------------------------------------------------------------+ bool CSidebarLayout::HitTestOverBottomResizeGrip(int lx, int ly) { //--- Return true if coordinates fall within the bottom resize handle strip return (lx >= 0 && lx < m_sidebarWidth && ly >= m_sidebarHeight - 8 && ly < m_sidebarHeight); } //+------------------------------------------------------------------+ //| CLASS 6 — Manage the flyout tool selection panel | //+------------------------------------------------------------------+ class CFlyoutPanel : public CSidebarLayout { protected: int m_flyoutWidth; // Width of the flyout body (excluding pointer triangle) int m_flyoutItemHeight; // Height of each tool item row in the flyout int m_flyoutPadding; // Horizontal and vertical padding inside the flyout int m_flyoutPointerWidth; // Half-height of the pointer triangle int m_flyoutPointerHeight; // Depth (horizontal extent) of the pointer triangle int m_flyoutPointerLocalY; // Local Y centre of the pointer tip within the flyout bool m_flyoutPointerOnLeft; // Flag indicating the pointer faces left toward the sidebar bool m_isFlyoutVisible; // Flag indicating the flyout is currently visible ENUM_CATEGORY m_flyoutActiveCat; // Category whose tools are currently shown in the flyout int m_hoveredFlyoutItem; // Index of the hovered flyout item row, or -1 int m_flyoutScrollPixels; // Current vertical scroll offset of the flyout list int m_flyoutMaxVisibleItems; // Maximum number of visible tool rows in the flyout int m_flyoutScrollThumbHeight; // Height of the flyout scroll thumb pill bool m_isFlyoutThumbDragging; // Flag indicating flyout scroll thumb drag in progress int m_flyoutThumbDragStartY; // Mouse Y when flyout thumb drag started int m_flyoutThumbDragStartPixels;// Scroll offset when flyout thumb drag started bool m_isHoveredFlyoutScrollArea; // Flag indicating mouse is over the flyout scroll area bool m_isHoveredFlyoutThumb; // Flag indicating mouse is over the flyout scroll thumb protected: //--- Show the flyout panel for the given category, highlighting the active tool void ShowFlyout(ENUM_CATEGORY cat, TOOL_TYPE activeTool); //--- Hide the flyout panel and reset its state void HideFlyout(); //--- Draw and composite the full flyout panel for the given category void DrawFlyoutForCategory(ENUM_CATEGORY cat, TOOL_TYPE activeTool); //--- Draw the flyout scroll thumb pill overlay onto the display canvas void DrawFlyoutScrollPillOverlay(ENUM_CATEGORY cat); //--- Draw the flyout body border at high resolution void DrawFlyoutBodyBorderHR(int x, int y, int w, int h, int r, int thickness, uint borderColor); //--- Test whether screen coordinates hit the visible flyout panel bool HitTestOverFlyout(int mouseX, int mouseY, int &lx, int &ly); //--- Return the flyout item index under the given local coordinates, or -1 int HitTestFlyoutItem(int lx, int ly); }; //+------------------------------------------------------------------+ //| Hide the flyout panel and reset its state | //+------------------------------------------------------------------+ void CFlyoutPanel::HideFlyout() { //--- Reset hover item and scroll offset m_hoveredFlyoutItem = -1; m_flyoutScrollPixels = 0; //--- Clear scroll hover flags m_isHoveredFlyoutScrollArea = false; m_isHoveredFlyoutThumb = false; //--- Hide the flyout chart object from all timeframes ObjectSetInteger(0, m_nameFlyout, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); //--- Mark the flyout as hidden and clear the active category m_isFlyoutVisible = false; m_flyoutActiveCat = CAT_NONE; } //+------------------------------------------------------------------+ //| Test whether screen coordinates hit the visible flyout panel | //+------------------------------------------------------------------+ bool CFlyoutPanel::HitTestOverFlyout(int mouseX, int mouseY, int &lx, int &ly) { //--- Skip test if flyout is not visible if (!m_isFlyoutVisible) return false; //--- Read the flyout chart object position and size int fx = (int)ObjectGetInteger(0, m_nameFlyout, OBJPROP_XDISTANCE); int fy = (int)ObjectGetInteger(0, m_nameFlyout, OBJPROP_YDISTANCE); int fw = (int)ObjectGetInteger(0, m_nameFlyout, OBJPROP_XSIZE); int fh = (int)ObjectGetInteger(0, m_nameFlyout, OBJPROP_YSIZE); //--- Compute local coordinates relative to the flyout origin lx = mouseX - fx; ly = mouseY - fy; //--- Return true if the mouse is within the flyout bounds return (mouseX >= fx && mouseX < fx + fw && mouseY >= fy && mouseY < fy + fh); } //+------------------------------------------------------------------+ //| Return flyout item index under local coordinates, or -1 | //+------------------------------------------------------------------+ int CFlyoutPanel::HitTestFlyoutItem(int lx, int ly) { //--- Return no hit if no category is active if (m_flyoutActiveCat == CAT_NONE) return -1; int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); //--- Compute title row height and body left offset for pointer direction int titleH = 26, dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0; int visibleTools = MathMin(nTools, m_flyoutMaxVisibleItems); //--- Exclude clicks on the scroll thumb column when scrolling is active if (nTools > m_flyoutMaxVisibleItems) { int tw = m_sidebarScrollThinWidth; if (!m_flyoutPointerOnLeft) { if (lx <= dispBx + tw + 8) return -1; } else { if (lx >= dispBx + m_flyoutWidth - tw - 8) return -1; } } //--- Compute the vertical clip region for item rows int itemClipTop = titleH + m_flyoutPadding, itemClipBot = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight; //--- Return no hit if Y is outside the item clip region if (ly < itemClipTop || ly >= itemClipBot) return -1; //--- Compute item index from Y position accounting for scroll offset int idx = (ly - itemClipTop + m_flyoutScrollPixels) / m_flyoutItemHeight; if (idx < 0 || idx >= nTools) return -1; return idx; } //+------------------------------------------------------------------+ //| Show the flyout for the given category with active tool state | //+------------------------------------------------------------------+ void CFlyoutPanel::ShowFlyout(ENUM_CATEGORY cat, TOOL_TYPE activeTool) { //--- Hide flyout and exit if the category has no tools int nTools = ArraySize(m_categories[(int)cat].tools); if (nTools == 0) { HideFlyout(); return; } //--- Reset the flyout scroll offset on each new show m_flyoutScrollPixels = 0; //--- Compute flyout panel height based on visible tool count int titleH = 26, visibleTools = MathMin(nTools, m_flyoutMaxVisibleItems); int flyH = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight + m_flyoutPadding; int totalW = m_flyoutWidth + m_flyoutPointerHeight; //--- Read chart dimensions for bounds checking int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Determine pointer direction and flyout X position based on snap state bool ptrLeft; int flyX; if (m_snapState == SNAP_LEFT) { ptrLeft = true; flyX = m_panelX + m_sidebarWidth; } else if (m_snapState == SNAP_RIGHT) { ptrLeft = false; flyX = m_panelX - totalW; } else { //--- For floating panels, prefer opening to the right with fallback to left int rightX = m_panelX + m_sidebarWidth; if (rightX + totalW <= chartW - 4) { ptrLeft = true; flyX = rightX; } else { ptrLeft = false; flyX = m_panelX - totalW; if (flyX < 0) { ptrLeft = true; flyX = rightX; } } } m_flyoutPointerOnLeft = ptrLeft; //--- Compute the flyout Y position aligned to the hovered category button centre int btnCentreY = m_panelY + CalcCategoryButtonY((int)cat) + m_categoryButtonSize / 2; int flyY = btnCentreY - (titleH + m_flyoutPadding + 6); //--- Clamp flyout Y within chart bounds if (flyY + flyH > chartH - 8) flyY = chartH - flyH - 8; if (flyY < 4) flyY = 4; //--- Clamp the pointer local Y to stay within the flyout rounded corners m_flyoutPointerLocalY = MathMax(m_panelCornerRadius + m_flyoutPointerWidth + 2, MathMin(flyH - m_panelCornerRadius - m_flyoutPointerWidth - 2, btnCentreY - flyY)); //--- Position the flyout chart object and mark it visible ObjectSetInteger(0, m_nameFlyout, OBJPROP_XDISTANCE, flyX); ObjectSetInteger(0, m_nameFlyout, OBJPROP_YDISTANCE, flyY); m_isFlyoutVisible = true; m_flyoutActiveCat = cat; //--- Draw the flyout contents and make the object visible on all timeframes DrawFlyoutForCategory(cat, activeTool); ObjectSetInteger(0, m_nameFlyout, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); } //+------------------------------------------------------------------+ //| Draw the flyout body border at high resolution | //+------------------------------------------------------------------+ void CFlyoutPanel::DrawFlyoutBodyBorderHR(int x, int y, int w, int h, int r, int thickness, uint borderColor) { //--- Skip drawing when border width is disabled if (BorderWidth <= 0) return; //--- Clamp corner radius and compute half-thickness offset r = MathMin(r, MathMin(w / 2, h / 2)); int h2 = thickness / 2; //--- Draw all four border edges of the flyout body DrawBorderEdge(m_canvasFlyoutHighRes, x + r, y + h2, x + w - r, y + h2, thickness, borderColor); DrawBorderEdge(m_canvasFlyoutHighRes, x + w - h2, y + r, x + w - h2, y + h - r, thickness, borderColor); DrawBorderEdge(m_canvasFlyoutHighRes, x + w - r, y + h - h2, x + r, y + h - h2, thickness, borderColor); DrawBorderEdge(m_canvasFlyoutHighRes, x + h2, y + h - r, x + h2, y + r, thickness, borderColor); //--- Draw all four corner arcs of the flyout body DrawCornerArc(m_canvasFlyoutHighRes, x + r, y + r, r, thickness, borderColor, M_PI, M_PI * 1.5); DrawCornerArc(m_canvasFlyoutHighRes, x + w - r, y + r, r, thickness, borderColor, M_PI * 1.5, M_PI * 2.0); DrawCornerArc(m_canvasFlyoutHighRes, x + r, y + h - r, r, thickness, borderColor, M_PI * 0.5, M_PI); DrawCornerArc(m_canvasFlyoutHighRes, x + w - r, y + h - r, r, thickness, borderColor, 0.0, M_PI * 0.5); } //+------------------------------------------------------------------+ //| Draw and composite the full flyout panel for the given category | //+------------------------------------------------------------------+ void CFlyoutPanel::DrawFlyoutForCategory(ENUM_CATEGORY cat, TOOL_TYPE activeTool) { //--- Exit early if the category has no tools int nTools = ArraySize(m_categories[(int)cat].tools); if (nTools == 0) return; //--- Compute layout dimensions int titleH = 26, visibleTools = MathMin(nTools, m_flyoutMaxVisibleItems); bool needsScroll = (nTools > m_flyoutMaxVisibleItems); int flyH = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight + m_flyoutPadding; int totalW = m_flyoutWidth + m_flyoutPointerHeight; //--- Compute high-res canvas dimensions int ws = totalW * m_supersampleFactor, hs = flyH * m_supersampleFactor; bool ptrLeft = m_flyoutPointerOnLeft; //--- Compute scroll thumb height if scrolling is needed if (needsScroll) m_flyoutScrollThumbHeight = MathMax(20, (int)(MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight * (double)m_flyoutMaxVisibleItems / nTools)); //--- Resize display and high-res canvases if flyout dimensions have changed if (m_canvasFlyout.Width() != totalW || m_canvasFlyout.Height() != flyH) m_canvasFlyout.Resize(totalW, flyH); if (m_canvasFlyoutHighRes.Width() != ws || m_canvasFlyoutHighRes.Height() != hs) m_canvasFlyoutHighRes.Resize(ws, hs); //--- Update the flyout chart object size to match ObjectSetInteger(0, m_nameFlyout, OBJPROP_XSIZE, totalW); ObjectSetInteger(0, m_nameFlyout, OBJPROP_YSIZE, flyH); //--- Clear the high-res canvas to fully transparent m_canvasFlyoutHighRes.Erase(0x00000000); //--- Compute horizontal body offset and width at high resolution int bx = ptrLeft ? m_flyoutPointerHeight * m_supersampleFactor : 0; int bw = m_flyoutWidth * m_supersampleFactor; int br = m_panelCornerRadius * m_supersampleFactor; //--- Compute pointer tip Y and half-height at high resolution int ptrCY = MathMax(br + m_flyoutPointerWidth * m_supersampleFactor + m_supersampleFactor, MathMin(hs - br - m_flyoutPointerWidth * m_supersampleFactor - m_supersampleFactor, m_flyoutPointerLocalY * m_supersampleFactor)); int ptrHHS = m_flyoutPointerWidth * m_supersampleFactor; //--- Compute pointer tip and base X based on pointer direction int tipX = ptrLeft ? 0 : ws - 1, baseX = ptrLeft ? bx : bx + bw - 1; //--- Pack background and border colours uchar flyBgA = (uchar)(255 * BackgroundOpacity); uint fillARGB = ColorToARGB(m_themeColors.flyoutBackground, flyBgA); uint borderARGB = ColorToARGB(m_themeColors.flyoutBorder, 255); int brdT = BorderWidth * m_supersampleFactor; //--- Fill flyout body background with rounded corners FillRoundRectHR(m_canvasFlyoutHighRes, bx, 0, bw, hs, br, fillARGB); //--- Fill the pointer triangle background FillTriangleHR(m_canvasFlyoutHighRes, tipX, ptrCY, baseX, ptrCY - ptrHHS, baseX, ptrCY + ptrHHS, fillARGB); //--- Draw flyout body border and pointer edges if border is enabled if (BorderWidth > 0) { if (ptrLeft) { //--- Draw body border on all four sides DrawFlyoutBodyBorderHR(bx, 0, bw, hs, br, brdT, borderARGB); //--- Erase the body-left gap where the pointer connects m_canvasFlyoutHighRes.FillRectangle(bx, ptrCY - ptrHHS, bx + brdT + m_supersampleFactor, ptrCY + ptrHHS, fillARGB); //--- Draw pointer triangle border edges DrawBorderEdge(m_canvasFlyoutHighRes, (double)bx, (double)(ptrCY - ptrHHS), (double)tipX, (double)ptrCY, brdT, borderARGB); DrawBorderEdge(m_canvasFlyoutHighRes, (double)tipX, (double)ptrCY, (double)bx, (double)(ptrCY + ptrHHS), brdT, borderARGB); } else { //--- Compute the body right boundary for the right-facing pointer int bodyRight = bx + bw; DrawFlyoutBodyBorderHR(bx, 0, bw, hs, br, brdT, borderARGB); //--- Erase the body-right gap where the pointer connects m_canvasFlyoutHighRes.FillRectangle(bodyRight - brdT - m_supersampleFactor, ptrCY - ptrHHS, bodyRight, ptrCY + ptrHHS, fillARGB); //--- Draw pointer triangle border edges for right-facing pointer DrawBorderEdge(m_canvasFlyoutHighRes, (double)bodyRight, (double)(ptrCY - ptrHHS), (double)tipX, (double)ptrCY, brdT, borderARGB); DrawBorderEdge(m_canvasFlyoutHighRes, (double)tipX, (double)ptrCY, (double)bodyRight, (double)(ptrCY + ptrHHS), brdT, borderARGB); } } //--- Fill the flyout title strip background with rounded top corners color titleFill = m_isDarkTheme ? C'25,29,40' : C'245,247,252'; int tbrd = MathMax(brdT, m_supersampleFactor), innerTR = MathMax(0, br - tbrd); FillSelectiveRoundRectHR(m_canvasFlyoutHighRes, bx + tbrd, tbrd, bw - 2 * tbrd, titleH * m_supersampleFactor - tbrd, innerTR, ColorToARGB(titleFill, 255), true, true, false, false); //--- Square off the lower half of the title strip m_canvasFlyoutHighRes.FillRectangle(bx + tbrd, (titleH / 2) * m_supersampleFactor, bx + bw - tbrd - 1, titleH * m_supersampleFactor - 1, ColorToARGB(titleFill, 255)); //--- Compute item clip boundaries int itemClipTop = titleH + m_flyoutPadding, itemClipBot = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight; //--- Draw item highlight backgrounds — use tmpHighRes when scrolling so highlights //--- never bleed above itemClipTop into the title strip after DownsampleCanvas if (needsScroll) { //--- Draw all scrolled item backgrounds onto a temporary HR canvas CCanvas tmpHighRes; tmpHighRes.Create("FlyoutTmpHR", ws, hs, COLOR_FORMAT_ARGB_NORMALIZE); tmpHighRes.Erase(0x00000000); for (int t = 0; t < nTools; t++) { //--- Compute scroll-adjusted item Y at high resolution int itemY = (titleH + m_flyoutPadding + t * m_flyoutItemHeight - m_flyoutScrollPixels) * m_supersampleFactor; //--- Skip items fully above or below the clip region if (itemY + (m_flyoutItemHeight - 2) * m_supersampleFactor <= itemClipTop * m_supersampleFactor) continue; if (itemY >= itemClipBot * m_supersampleFactor) continue; bool isActive = (activeTool == m_categories[(int)cat].tools[t].toolType); bool isHovered = (m_hoveredFlyoutItem == t && m_flyoutActiveCat == cat); int itemH = (m_flyoutItemHeight - 2) * m_supersampleFactor, padS = m_flyoutPadding * m_supersampleFactor; //--- Fill active item row background onto temp canvas if (isActive) FillRoundRectHR(tmpHighRes, bx + padS, itemY, bw - 2 * padS, itemH, 5 * m_supersampleFactor, ColorToARGB(m_themeColors.buttonActiveBackground, 255)); //--- Fill hovered item row background onto temp canvas else if (isHovered) FillRoundRectHR(tmpHighRes, bx + padS, itemY, bw - 2 * padS, itemH, 5 * m_supersampleFactor, ColorToARGB(m_themeColors.flyoutItemHoverBackground, 255)); //--- Draw active indicator dot onto temp canvas if (isActive) tmpHighRes.FillCircle(bx + bw - m_flyoutPadding * m_supersampleFactor - 5 * m_supersampleFactor, itemY + itemH / 2, 3 * m_supersampleFactor, ColorToARGB(m_themeColors.flyoutTextActiveColor, 255)); } //--- Blit only the clip region from temp onto the main HR canvas for (int y = itemClipTop * m_supersampleFactor; y < itemClipBot * m_supersampleFactor && y < hs; y++) for (int x = 0; x < ws; x++) { uint px = tmpHighRes.PixelGet(x, y); if (((px >> 24) & 0xFF) > 0) BlendPixelSet(m_canvasFlyoutHighRes, x, y, px); } tmpHighRes.Destroy(); } else { //--- No scrolling — all items fit, draw directly onto the HR canvas for (int t = 0; t < visibleTools; t++) { bool isActive = (activeTool == m_categories[(int)cat].tools[t].toolType); bool isHovered = (m_hoveredFlyoutItem == t && m_flyoutActiveCat == cat); //--- Compute item Y at high resolution (no scroll offset needed) int itemY = (titleH + m_flyoutPadding + t * m_flyoutItemHeight) * m_supersampleFactor; int itemH = (m_flyoutItemHeight - 2) * m_supersampleFactor; int padS = m_flyoutPadding * m_supersampleFactor; //--- Fill active item row background if (isActive) FillRoundRectHR(m_canvasFlyoutHighRes, bx + padS, itemY, bw - 2 * padS, itemH, 5 * m_supersampleFactor, ColorToARGB(m_themeColors.buttonActiveBackground, 255)); //--- Fill hovered item row background else if (isHovered) FillRoundRectHR(m_canvasFlyoutHighRes, bx + padS, itemY, bw - 2 * padS, itemH, 5 * m_supersampleFactor, ColorToARGB(m_themeColors.flyoutItemHoverBackground, 255)); //--- Draw active indicator dot if (isActive) m_canvasFlyoutHighRes.FillCircle(bx + bw - m_flyoutPadding * m_supersampleFactor - 5 * m_supersampleFactor, itemY + itemH / 2, 3 * m_supersampleFactor, ColorToARGB(m_themeColors.flyoutTextActiveColor, 255)); } } //--- Downsample high-res canvas into the display-resolution flyout canvas DownsampleCanvas(m_canvasFlyout, m_canvasFlyoutHighRes, m_supersampleFactor); //--- Compute display-resolution body left offset int dispBx = ptrLeft ? m_flyoutPointerHeight : 0; //--- Draw horizontal separator below the title strip m_canvasFlyout.Line(dispBx + BorderWidth, titleH, dispBx + m_flyoutWidth - BorderWidth - 1, titleH, ColorToARGB(m_themeColors.flyoutBorder, 255)); //--- Draw the uppercased category title text string titleStr = m_categories[(int)cat].categoryLabel; StringToUpper(titleStr); m_canvasFlyout.FontSet("Arial Bold", FlyoutTitleSize); m_canvasFlyout.TextOut(dispBx + m_flyoutPadding + 4, 6, titleStr, ColorToARGB(m_themeColors.flyoutTitleColor, 255)); //--- Draw the tool count badge if the category has more than one tool if (nTools > 1) { string countStr = IntegerToString(nTools); m_canvasFlyout.FontSet("Arial", 15); int cw = m_canvasFlyout.TextWidth(countStr); //--- Right-align the count badge within the title strip m_canvasFlyout.TextOut(dispBx + m_flyoutWidth - m_flyoutPadding - cw - 4, 8, countStr, ColorToARGB(m_themeColors.flyoutTitleColor, 200)); } //--- Create a temporary canvas for the icon and label text pass //--- Drawing directly onto m_canvasFlyout has no Y clipping; TextOut at scrolled //--- positions bleeds into the title strip. tmpText is seeded only for the clip //--- region and blitted back, so glyphs outside [itemClipTop, itemClipBot) are discarded CCanvas tmpText; tmpText.Create("FlyoutTmpText", m_canvasFlyout.Width(), m_canvasFlyout.Height(), COLOR_FORMAT_ARGB_NORMALIZE); tmpText.Erase(0x00000000); //--- Seed the clip region with existing flyout pixels as the drawing background for (int y = itemClipTop; y < itemClipBot && y < m_canvasFlyout.Height(); y++) for (int x = 0; x < m_canvasFlyout.Width(); x++) tmpText.PixelSet(x, y, m_canvasFlyout.PixelGet(x, y)); //--- Draw icon glyphs and label text for each tool row onto the temp canvas for (int t = 0; t < nTools; t++) { //--- Compute scroll-adjusted display-resolution item Y int itemY = titleH + m_flyoutPadding + t * m_flyoutItemHeight - m_flyoutScrollPixels; int itemH = m_flyoutItemHeight - 2; //--- Skip rows fully outside the clip region if (itemY + itemH <= itemClipTop || itemY >= itemClipBot) continue; bool isActive = (activeTool == m_categories[(int)cat].tools[t].toolType); bool isHovered = (m_hoveredFlyoutItem == t && m_flyoutActiveCat == cat); //--- Select icon and text colours based on state color iconColor = isActive ? m_themeColors.flyoutTextActiveColor : (isHovered ? clrWhite : m_themeColors.buttonIconColor); color textColor = isActive ? m_themeColors.flyoutTextActiveColor : (isHovered ? clrWhite : m_themeColors.flyoutTextColor); //--- Set icon font and draw tool icon glyph onto the temp canvas tmpText.FontSet(m_categories[(int)cat].tools[t].iconFontName, FlyoutIconSize); string sym = CharToString(m_categories[(int)cat].tools[t].iconCharCode); int ih = tmpText.TextHeight(sym); tmpText.TextOut(dispBx + m_flyoutPadding + 8, itemY + (itemH - ih) / 2, sym, ColorToARGB(iconColor, 255)); //--- Set label font and draw tool label text onto the temp canvas tmpText.FontSet("Arial", FlyoutLabelSize); int lh = tmpText.TextHeight(m_categories[(int)cat].tools[t].toolLabel); tmpText.TextOut(dispBx + m_flyoutPadding + 34, itemY + (itemH - lh) / 2, m_categories[(int)cat].tools[t].toolLabel, ColorToARGB(textColor, 255)); } //--- Blit only the clip region back onto the display canvas, discarding any out-of-bounds draws for (int y = itemClipTop; y < itemClipBot && y < m_canvasFlyout.Height(); y++) for (int x = 0; x < m_canvasFlyout.Width(); x++) m_canvasFlyout.PixelSet(x, y, tmpText.PixelGet(x, y)); //--- Destroy the temporary canvas tmpText.Destroy(); //--- Overlay the scroll thumb pill if hover or drag is active DrawFlyoutScrollPillOverlay(cat); //--- Flush the display canvas to the chart m_canvasFlyout.Update(); } //+------------------------------------------------------------------+ //| Draw the flyout scroll thumb pill overlay onto display canvas | //+------------------------------------------------------------------+ void CFlyoutPanel::DrawFlyoutScrollPillOverlay(ENUM_CATEGORY cat) { //--- Skip drawing if neither hovered nor dragging if (!m_isHoveredFlyoutScrollArea && !m_isFlyoutThumbDragging) return; if (cat == CAT_NONE) return; int nTools = ArraySize(m_categories[(int)cat].tools); //--- Skip if all items are visible and no scroll is needed if (nTools <= m_flyoutMaxVisibleItems) return; //--- Compute scroll track geometry int titleH = 26, itemsTop = titleH + m_flyoutPadding; int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight; m_flyoutScrollThumbHeight = MathMax(20, (int)(trackH * (double)m_flyoutMaxVisibleItems / nTools)); //--- Compute the thumb Y position from the current scroll fraction int maxScrollPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight; double scrollPos = (maxScrollPx > 0) ? (double)m_flyoutScrollPixels / maxScrollPx : 0.0; int thumbY = itemsTop + (int)(scrollPos * (trackH - m_flyoutScrollThumbHeight)); //--- Compute scroll pill X position based on pointer direction int tw = m_sidebarScrollThinWidth, dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0; int thinX = m_flyoutPointerOnLeft ? (dispBx + m_flyoutWidth - tw - 2) : (dispBx + 2); //--- Select pill colour and opacity based on interaction state color pillColor; uchar pillAlpha; if (m_isFlyoutThumbDragging) { pillColor = m_themeColors.accentBarColor; pillAlpha = 255; } else if (m_isHoveredFlyoutThumb) { pillColor = m_themeColors.scrollArrowHoverColor; pillAlpha = 255; } else { pillColor = m_themeColors.scrollArrowColor; pillAlpha = 180; } uint thumbARGB = ColorToARGB(pillColor, pillAlpha); //--- Create a temporary high-res canvas for the pill shape int pws = tw * m_supersampleFactor, phs = m_flyoutScrollThumbHeight * m_supersampleFactor; CCanvas pillHR; pillHR.Create("FlyoutPillHR_tmp", pws, phs, COLOR_FORMAT_ARGB_NORMALIZE); pillHR.Erase(0x00000000); //--- Fill the pill with a fully rounded rect at high resolution FillRoundRectHR(pillHR, 0, 0, pws, phs, MathMax(1, pws / 2), thumbARGB); //--- Downsample the pill and blend it onto the flyout display canvas for (int py = 0; py < m_flyoutScrollThumbHeight; py++) for (int px = 0; px < tw; px++) { //--- Accumulate channel sums across the high-res sample block double sumA = 0, sumR = 0, sumG = 0, sumB = 0, wc = 0; for (int dy = 0; dy < m_supersampleFactor; dy++) for (int dx = 0; dx < m_supersampleFactor; dx++) { int sx = px * m_supersampleFactor + dx, sy = py * m_supersampleFactor + dy; if (sx >= pws || sy >= phs) continue; uint p = pillHR.PixelGet(sx, sy); uchar a = (uchar)((p >> 24) & 0xFF); sumA += a; if (a > 0) { sumR += (p >> 16) & 0xFF; sumG += (p >> 8) & 0xFF; sumB += p & 0xFF; wc += 1.0; } } //--- Compute averaged output alpha and blend onto the display canvas int ss2 = m_supersampleFactor * m_supersampleFactor; uchar fa = (uchar)(sumA / ss2); if (fa > 0 && wc > 0) BlendPixelSet(m_canvasFlyout, thinX + px, thumbY + py, ((uint)fa << 24) | ((uint)(uchar)(sumR / wc) << 16) | ((uint)(uchar)(sumG / wc) << 8) | (uint)(uchar)(sumB / wc)); } //--- Destroy the temporary high-res pill canvas pillHR.Destroy(); } //+------------------------------------------------------------------+ //| CLASS 7 — Render sidebar buttons, icons, and scroll overlay | //+------------------------------------------------------------------+ class CSidebarRenderer : public CFlyoutPanel { protected: ENUM_CATEGORY m_hoveredCategory; // Category button currently under the mouse cursor bool m_isCloseButtonHovered; // Flag indicating mouse is over the close button bool m_isThemeButtonHovered; // Flag indicating mouse is over the theme toggle button bool m_isGripAreaHovered; // Flag indicating mouse is over the drag grip strip protected: //--- Render the full sidebar panel for the given active tool void DrawSidebar(TOOL_TYPE activeTool); //--- Render the header strip background and control rows at high resolution void DrawHeaderStripHR(int canvasW, int canvasH); //--- Render a single category button background and accent bar at high resolution void DrawCategoryButtonHR(CCanvas &target, int xHR, int yHR, int sizeHR, bool isActive, bool isHovered, bool hasDot); //--- Render all category icon glyphs and sidebar control labels onto the display canvas void DrawSidebarIconLabels(TOOL_TYPE activeTool); //--- Render the sidebar scroll thumb pill overlay onto the display canvas void DrawSidebarScrollPillOverlay(); }; //+------------------------------------------------------------------+ //| Render the full sidebar panel for the given active tool | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawSidebar(TOOL_TYPE activeTool) { //--- Compute the scroll thumb height when the sidebar is scrollable if (CalcSidebarMaxScrollPixels() > 0) { int trackH = CalcSidebarViewportPixels(); m_sidebarScrollThumbHeight = MathMax(20, (int)(trackH * (double)trackH / CalcSidebarTotalScrollPixels())); } //--- Compute high-res canvas dimensions int ws = m_sidebarWidth * m_supersampleFactor, hs = m_sidebarHeight * m_supersampleFactor; //--- Resize the high-res canvas if sidebar dimensions have changed if (m_canvasSidebarHighRes.Width() != ws || m_canvasSidebarHighRes.Height() != hs) m_canvasSidebarHighRes.Resize(ws, hs); //--- Clear the high-res canvas to fully transparent m_canvasSidebarHighRes.Erase(0x00000000); //--- Pack background alpha from the opacity input uchar bgA = (uchar)(255 * BackgroundOpacity); //--- Determine which corners are rounded based on the snap state bool rTL = (m_snapState != SNAP_LEFT), rBL = rTL; bool rTR = (m_snapState != SNAP_RIGHT), rBR = rTR; //--- Fill the sidebar background with selective corner rounding FillSelectiveRoundRectHR(m_canvasSidebarHighRes, 0, 0, ws, hs, m_panelCornerRadius * m_supersampleFactor, ColorToARGB(m_themeColors.sidebarBackground, bgA), rTL, rTR, rBL, rBR); //--- Draw the snapped edge flush border line on the snap side if (m_snapState == SNAP_LEFT) m_canvasSidebarHighRes.FillRectangle(0, 0, m_supersampleFactor - 1, hs - 1, ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255)); else if (m_snapState == SNAP_RIGHT) m_canvasSidebarHighRes.FillRectangle(ws - m_supersampleFactor, 0, ws - 1, hs - 1, ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255)); //--- Draw the header strip background and control rows DrawHeaderStripHR(ws, hs); //--- Determine which category is active for the current tool ENUM_CATEGORY activeCat = GetCategoryForActiveTool(activeTool); //--- When scrolling is active, draw buttons onto a temp canvas and blit only the clip region if (m_sidebarMaxVisibleCats < CAT_COUNT) { CCanvas tmpHR; tmpHR.Create("SB_TmpHR", ws, hs, COLOR_FORMAT_ARGB_NORMALIZE); tmpHR.Erase(0x00000000); //--- Draw each visible category button onto the temp canvas for (int c = 0; c < CAT_COUNT; c++) { if (!IsCategoryButtonVisible(c)) continue; DrawCategoryButtonHR(tmpHR, (m_sidebarWidth - m_categoryButtonSize) / 2 * m_supersampleFactor, CalcCategoryButtonY(c) * m_supersampleFactor, m_categoryButtonSize * m_supersampleFactor, activeCat == (ENUM_CATEGORY)c, m_hoveredCategory == (ENUM_CATEGORY)c, ArraySize(m_categories[c].tools) > 1); } //--- Blit only the clip region to prevent buttons bleeding into the header int clipTop = CalcClipTop() * m_supersampleFactor, clipBot = CalcClipBottom() * m_supersampleFactor; for (int y = clipTop; y < clipBot && y < hs; y++) for (int x = 0; x < ws; x++) { uint px = tmpHR.PixelGet(x, y); if (((px >> 24) & 0xFF) > 0) BlendPixelSet(m_canvasSidebarHighRes, x, y, px); } tmpHR.Destroy(); } else { //--- All buttons fit: draw directly onto the high-res canvas for (int c = 0; c < CAT_COUNT; c++) DrawCategoryButtonHR(m_canvasSidebarHighRes, (m_sidebarWidth - m_categoryButtonSize) / 2 * m_supersampleFactor, CalcCategoryButtonY(c) * m_supersampleFactor, m_categoryButtonSize * m_supersampleFactor, activeCat == (ENUM_CATEGORY)c, m_hoveredCategory == (ENUM_CATEGORY)c, ArraySize(m_categories[c].tools) > 1); } //--- Draw the panel border with selective corner rounding if (BorderWidth > 0) DrawSelectiveRoundRectBorderHR(m_canvasSidebarHighRes, 0, 0, ws, hs, m_panelCornerRadius * m_supersampleFactor, ColorToARGB(m_themeColors.sidebarBorder, 255), BorderWidth * m_supersampleFactor, rTL, rTR, rBL, rBR); //--- Downsample the high-res canvas into the display-resolution sidebar canvas DownsampleCanvas(m_canvasSidebar, m_canvasSidebarHighRes, m_supersampleFactor); //--- Draw icon glyphs and control labels onto the display canvas DrawSidebarIconLabels(activeTool); //--- Draw the scroll thumb pill overlay if scroll is active DrawSidebarScrollPillOverlay(); //--- Flush the display canvas to the chart m_canvasSidebar.Update(); } //+------------------------------------------------------------------+ //| Render the header strip background and control rows at high res | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawHeaderStripHR(int canvasW, int canvasH) { //--- Compute header and border dimensions at high resolution int headerH = m_headerGripHeight * m_supersampleFactor; int brd = BorderWidth * m_supersampleFactor; //--- Compute inner horizontal insets based on snap state int inL = (m_snapState == SNAP_LEFT) ? 0 : brd; int inR = (m_snapState == SNAP_RIGHT) ? 0 : brd; //--- Compute header fill area dimensions int hx = inL, hy = brd, hw = canvasW - inL - inR, hh = headerH - brd; int innerR = MathMax(0, m_panelCornerRadius * m_supersampleFactor - brd); bool rTL = (m_snapState != SNAP_LEFT), rTR = (m_snapState != SNAP_RIGHT); //--- Fill header strip background with rounded top corners color hdrFill = m_isDarkTheme ? C'25,29,40' : C'245,247,252'; FillSelectiveRoundRectHR(m_canvasSidebarHighRes, hx, hy, hw, hh, innerR, ColorToARGB(hdrFill, 255), rTL, rTR, false, false); //--- Square off the lower half of the header strip m_canvasSidebarHighRes.FillRectangle(hx, hy + hh / 2, hx + hw - 1, headerH - 1, ColorToARGB(hdrFill, 255)); //--- Highlight the close button row if hovered if (m_isCloseButtonHovered) FillSelectiveRoundRectHR(m_canvasSidebarHighRes, inL, hy, canvasW - inL - inR, m_categoryButtonSize * m_supersampleFactor, innerR, ColorToARGB(m_themeColors.closeButtonHoverColor, 255), rTL, rTR, false, false); //--- Compute grip strip row geometry int row2Y = m_categoryButtonSize * m_supersampleFactor, row2H = 20 * m_supersampleFactor; //--- Highlight the grip row if hovered if (m_isGripAreaHovered) m_canvasSidebarHighRes.FillRectangle(inL, row2Y, canvasW - inR - 1, row2Y + row2H - 1, ColorToARGB(C'25,130,80', 255)); //--- Compute theme button row geometry int row3Y = (m_categoryButtonSize + 20) * m_supersampleFactor; int row3H = (m_headerGripHeight - m_categoryButtonSize - 20) * m_supersampleFactor; //--- Highlight the theme toggle row if hovered if (m_isThemeButtonHovered) m_canvasSidebarHighRes.FillRectangle(inL, row3Y, canvasW - inR - 1, row3Y + row3H - 1, ColorToARGB(C'110,60,200', 255)); //--- Select grip dot colour based on hover state uint dotColor = m_isGripAreaHovered ? ColorToARGB(clrWhite, 255) : ColorToARGB(m_themeColors.buttonIconColor, 255); //--- Draw three evenly-spaced grip dots centred in the grip strip int gapX = 6 * m_supersampleFactor, dotR = 2 * m_supersampleFactor; for (int col = 0; col < 3; col++) m_canvasSidebarHighRes.FillCircle(canvasW / 2 + (col - 1) * gapX, row2Y + row2H / 2, dotR, dotColor); } //+------------------------------------------------------------------+ //| Render a single category button background at high resolution | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawCategoryButtonHR(CCanvas &target, int xHR, int yHR, int sizeHR, bool isActive, bool isHovered, bool hasDot) { //--- Compute the corner radius for category buttons at high resolution int cornerHR = 6 * m_supersampleFactor; if (isActive) { //--- Fill the active button background FillRoundRectHR(target, xHR, yHR, sizeHR, sizeHR, cornerHR, ColorToARGB(m_themeColors.buttonActiveBackground, 255)); //--- Compute the accent bar dimensions and X position int barW = 3 * m_supersampleFactor, barH = sizeHR / 2; int barX = (m_snapState == SNAP_RIGHT) ? xHR + sizeHR + m_supersampleFactor : xHR - barW - m_supersampleFactor; //--- Draw the accent bar adjacent to the active button FillRoundRectHR(target, barX, yHR + sizeHR / 4, barW, barH, m_supersampleFactor, ColorToARGB(m_themeColors.accentBarColor, 255)); } else if (isHovered) { //--- Fill the hovered button background FillRoundRectHR(target, xHR, yHR, sizeHR, sizeHR, cornerHR, ColorToARGB(m_themeColors.buttonHoverBackground, 255)); } //--- Draw the multi-tool indicator dot in the bottom-right corner if (hasDot) target.FillCircle( xHR + sizeHR - 6 * m_supersampleFactor, yHR + sizeHR - 6 * m_supersampleFactor, 2 * m_supersampleFactor, ColorToARGB(isActive ? m_themeColors.buttonIconActiveColor : m_themeColors.gripDotsColor, 180)); } //+------------------------------------------------------------------+ //| Render category icons and sidebar control labels onto display | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawSidebarIconLabels(TOOL_TYPE activeTool) { //--- Determine which category owns the current active tool ENUM_CATEGORY activeCat = GetCategoryForActiveTool(activeTool); int clipTop = CalcClipTop(), clipBot = CalcClipBottom(); //--- Create a temp canvas seeded with the current sidebar pixels to draw icons onto CCanvas tmpIcons; tmpIcons.Create("SB_TmpIcons", m_sidebarWidth, m_sidebarHeight, COLOR_FORMAT_ARGB_NORMALIZE); tmpIcons.Erase(0x00000000); //--- Determine the vertical range to seed and blit based on scroll state int blitY0 = (m_sidebarMaxVisibleCats < CAT_COUNT) ? clipTop : 0; int blitY1 = (m_sidebarMaxVisibleCats < CAT_COUNT) ? clipBot : m_sidebarHeight; //--- Seed the temp canvas with existing sidebar pixels in the blit region for (int y = blitY0; y < blitY1 && y < m_sidebarHeight; y++) for (int x = 0; x < m_sidebarWidth; x++) tmpIcons.PixelSet(x, y, m_canvasSidebar.PixelGet(x, y)); //--- Draw the category icon glyph for each visible button for (int c = 0; c < CAT_COUNT; c++) { if (!IsCategoryButtonVisible(c)) continue; int btnY = CalcCategoryButtonY(c), btnX = (m_sidebarWidth - m_categoryButtonSize) / 2; bool isActive = (activeCat == (ENUM_CATEGORY)c); color iconColor = isActive ? m_themeColors.buttonIconActiveColor : m_themeColors.buttonIconColor; //--- Set the category icon font and measure the glyph dimensions tmpIcons.FontSet(m_categories[c].iconFontName, CategoryIconSize); string sym = CharToString(m_categories[c].iconCharCode); int iw = tmpIcons.TextWidth(sym), ih = tmpIcons.TextHeight(sym); //--- Draw the icon glyph centred within the button bounds tmpIcons.TextOut(btnX + (m_categoryButtonSize - iw) / 2, btnY + (m_categoryButtonSize - ih) / 2, sym, ColorToARGB(iconColor, 255)); } //--- Blit the icon region back onto the display canvas for (int y = blitY0; y < blitY1 && y < m_sidebarHeight; y++) for (int x = 0; x < m_sidebarWidth; x++) m_canvasSidebar.PixelSet(x, y, tmpIcons.PixelGet(x, y)); tmpIcons.Destroy(); //--- Compute separator line horizontal extents respecting snap-side insets int brd = BorderWidth; int sepL = (m_snapState == SNAP_LEFT) ? 0 : brd; int sepR = m_sidebarWidth - 1 - ((m_snapState == SNAP_RIGHT) ? 0 : brd); //--- Pack separator colours uint sepCol = ColorToARGB(m_themeColors.separatorColor, 255); uint sepCol2 = ColorToARGB(m_isDarkTheme ? C'45,52,66' : C'195,202,215', 255); //--- Draw header bottom separator m_canvasSidebar.Line(sepL, m_headerGripHeight - 1, sepR, m_headerGripHeight - 1, sepCol); //--- Draw grip strip top and bottom separators m_canvasSidebar.Line(sepL, m_categoryButtonSize, sepR, m_categoryButtonSize, sepCol2); m_canvasSidebar.Line(sepL, m_categoryButtonSize + 20, sepR, m_categoryButtonSize + 20, sepCol2); //--- Draw close button icon centred in the close button row color closeIconColor = m_isCloseButtonHovered ? clrWhite : m_themeColors.buttonIconColor; m_canvasSidebar.FontSet("Webdings", CategoryIconSize); string closeSym = CharToString((uchar)114); int clW = m_canvasSidebar.TextWidth(closeSym), clH = m_canvasSidebar.TextHeight(closeSym); m_canvasSidebar.TextOut((m_sidebarWidth - clW) / 2, (m_categoryButtonSize - clH) / 2, closeSym, ColorToARGB(closeIconColor, 255)); //--- Compute theme button row geometry and draw theme icon int row3Y = m_categoryButtonSize + 20, row3H = m_headerGripHeight - m_categoryButtonSize - 20; color themeIconColor = m_isThemeButtonHovered ? clrWhite : m_themeColors.buttonIconColor; m_canvasSidebar.FontSet("Wingdings", CategoryIconSize); string themeSym = CharToString((uchar)91); int thW = m_canvasSidebar.TextWidth(themeSym), thH = m_canvasSidebar.TextHeight(themeSym); m_canvasSidebar.TextOut((m_sidebarWidth - thW) / 2, row3Y + (row3H - thH) / 2, themeSym, ColorToARGB(themeIconColor, 255)); //--- Draw the bottom resize grip indicator bar when hovered or dragging if (m_isBottomResizeHovered || m_isResizingBottomEdge) { int stripH = 3, gripY = m_sidebarHeight - stripH - 1; color barC = m_isResizingBottomEdge ? m_themeColors.accentBarColor : m_themeColors.scrollArrowHoverColor; m_canvasSidebar.FillRectangle(8, gripY, m_sidebarWidth - 9, gripY + stripH - 1, ColorToARGB(barC, 210)); } } //+------------------------------------------------------------------+ //| Render the sidebar scroll thumb pill overlay onto display canvas | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawSidebarScrollPillOverlay() { //--- Skip if no scroll is needed or neither hover nor drag is active if (CalcSidebarMaxScrollPixels() <= 0 || (!m_isHoveredSidebarScrollArea && !m_isSidebarThumbDragging)) return; //--- Compute the scroll track geometry int trackY = CalcClipTop(), trackH = CalcSidebarViewportPixels(); m_sidebarScrollThumbHeight = MathMax(20, (int)(trackH * (double)trackH / CalcSidebarTotalScrollPixels())); int maxPx = CalcSidebarMaxScrollPixels(); //--- Compute thumb Y from the current scroll fraction double pos = (maxPx > 0) ? (double)m_sidebarScrollPixels / maxPx : 0.0; int thumbY = trackY + (int)(pos * (trackH - m_sidebarScrollThumbHeight)); //--- Compute thumb X position on the non-snap side of the sidebar int tw = m_sidebarScrollThinWidth; int thinX = (m_snapState == SNAP_RIGHT) ? 2 : m_sidebarWidth - tw - 2; //--- Select pill colour and opacity based on interaction state color pillColor; uchar pillAlpha; if (m_isSidebarThumbDragging) { pillColor = m_themeColors.accentBarColor; pillAlpha = 255; } else if (m_isHoveredSidebarThumb) { pillColor = m_themeColors.scrollArrowHoverColor; pillAlpha = 255; } else { pillColor = m_themeColors.scrollArrowColor; pillAlpha = 180; } uint thumbARGB = ColorToARGB(pillColor, pillAlpha); //--- Create a temporary high-res canvas for the pill shape int pws = tw * m_supersampleFactor, phs = m_sidebarScrollThumbHeight * m_supersampleFactor; CCanvas pillHR; pillHR.Create("SB_PillHR_tmp", pws, phs, COLOR_FORMAT_ARGB_NORMALIZE); pillHR.Erase(0x00000000); //--- Fill the pill with a fully rounded rect at high resolution FillRoundRectHR(pillHR, 0, 0, pws, phs, MathMax(1, pws / 2), thumbARGB); //--- Downsample the pill and blend onto the sidebar display canvas for (int py = 0; py < m_sidebarScrollThumbHeight; py++) for (int px = 0; px < tw; px++) { //--- Accumulate channel sums across the high-res sample block double sumA = 0, sumR = 0, sumG = 0, sumB = 0, wc = 0; for (int dy = 0; dy < m_supersampleFactor; dy++) for (int dx = 0; dx < m_supersampleFactor; dx++) { int sx = px * m_supersampleFactor + dx, sy = py * m_supersampleFactor + dy; if (sx >= pws || sy >= phs) continue; uint p = pillHR.PixelGet(sx, sy); uchar a = (uchar)((p >> 24) & 0xFF); sumA += a; if (a > 0) { sumR += (p >> 16) & 0xFF; sumG += (p >> 8) & 0xFF; sumB += p & 0xFF; wc += 1.0; } } int ss2 = m_supersampleFactor * m_supersampleFactor; uchar fa = (uchar)(sumA / ss2); if (fa > 0 && wc > 0) BlendPixelSet(m_canvasSidebar, thinX + px, thumbY + py, ((uint)fa << 24) | ((uint)(uchar)(sumR / wc) << 16) | ((uint)(uchar)(sumG / wc) << 8) | (uint)(uchar)(sumB / wc)); } //--- Destroy the temporary high-res pill canvas pillHR.Destroy(); } //+------------------------------------------------------------------+ //| CLASS 8 — Manage crosshair, reticle, magnifier, and measure mode | //+------------------------------------------------------------------+ class CCrosshairManager : public CSidebarRenderer { protected: int m_reticleCanvasSize; // Pixel size of the square reticle canvas bool m_isReticleVisible; // Flag indicating the reticle is currently shown bool m_isMagnifierVisible; // Flag indicating the magnifier lens is shown bool m_isCrossVertVisible; // Flag indicating the vertical crosshair line is shown bool m_isCrossHorizVisible; // Flag indicating the horizontal crosshair line is shown bool m_isCrossPriceLabelVisible; // Flag indicating the crosshair price label is shown bool m_isCrossTimeLabelVisible; // Flag indicating the crosshair time label is shown bool m_isMeasureVertVisible; // Flag indicating the measure vertical anchor line is shown bool m_isMeasureHorizVisible; // Flag indicating the measure horizontal anchor line is shown bool m_isMeasurePriceLabelVisible; // Flag indicating the measure price label is shown bool m_isMeasureTimeLabelVisible; // Flag indicating the measure time label is shown bool m_isMeasureDiagonalVisible; // Flag indicating the measure diagonal line is shown bool m_isMeasuringActive; // Flag indicating measure mode is locked to an anchor point datetime m_measureAnchorTime; // Chart time of the measure mode anchor point double m_measureAnchorPrice; // Price level of the measure mode anchor point int m_measureAnchorPixelX; // Screen X of the measure mode anchor point int m_measureAnchorPixelY; // Screen Y of the measure mode anchor point ulong m_lastClickTimeMicros; // Microsecond timestamp of the last mouse click int m_lastMagMouseX; // Last mouse X used to draw the magnifier lens int m_lastMagMouseY; // Last mouse Y used to draw the magnifier lens protected: //--- Draw the reticle tick-mark crosses onto the reticle canvas void DrawReticleTickMarks(); //--- Make the reticle canvas visible on the chart void ShowReticle(); //--- Hide the reticle canvas from the chart void HideReticle(); //--- Move the reticle canvas to follow the mouse cursor void UpdateReticlePosition(int mouseX, int mouseY); //--- Make the crosshair vertical line canvas visible void ShowCrossVertical(); //--- Hide the crosshair vertical line canvas void HideCrossVertical(); //--- Move the crosshair vertical line to the given screen X void UpdateCrossVerticalPosition(int mouseX); //--- Make the crosshair horizontal line canvas visible void ShowCrossHorizontal(); //--- Hide the crosshair horizontal line canvas void HideCrossHorizontal(); //--- Move the crosshair horizontal line to the given screen Y void UpdateCrossHorizontalPosition(int mouseY); //--- Make the crosshair price axis label visible void ShowCrossPriceLabel(); //--- Hide the crosshair price axis label void HideCrossPriceLabel(); //--- Make the crosshair time axis label visible void ShowCrossTimeLabel(); //--- Hide the crosshair time axis label void HideCrossTimeLabel(); //--- Draw and position one axis label canvas next to the crosshair void DrawAndPositionAxisLabel(CCanvas &labelCanvas, string objectName, string labelText, bool isPriceAxis, int crosshairPixelPos, int chartWidth, int chartHeight); //--- Update both crosshair axis labels to reflect the current mouse position void UpdateCrosshairAxisLabels(int mouseX, int mouseY, datetime barTime, double barPrice); //--- Show all measure mode line and label canvases void ShowMeasureLines(); //--- Hide all measure mode line and label canvases void HideMeasureLines(); //--- Move the measure vertical anchor line to the given screen X void UpdateMeasureVerticalPosition(int pixelX); //--- Move the measure horizontal anchor line to the given screen Y void UpdateMeasureHorizontalPosition(int pixelY); //--- Update the measure anchor axis labels at the anchor chart coordinate void UpdateMeasureAnchorLabels(); //--- Make the magnifier lens canvas visible void ShowMagnifier(); //--- Hide the magnifier lens canvas void HideMagnifier(); //--- Move the magnifier and redraw its lens content if the mouse has moved void UpdateMagnifierPosition(int mouseX, int mouseY, datetime barTime, double barPrice); //--- Render the zoomed candle chart content inside the circular magnifier lens void DrawMagnifierLensContent(int mouseX, int mouseY, datetime centerTime, double centerPrice); //--- Redraw the measure diagonal line from the anchor to the current mouse position void UpdateMeasureDiagonalLine(int currentMouseX, int currentMouseY); //--- Update the floating measure info label near the cursor with bar/pip statistics void UpdateMeasurementInfoLabel(int mouseX, int mouseY, datetime barTime, double barPrice); //--- Hide all crosshair element canvases in one call void HideAllCrosshairElements(); //--- Show all crosshair element canvases in one call void ShowAllCrosshairElements(); //--- Handle a potential double-click to toggle measure mode anchor void HandleCrosshairDoubleClick(int mouseX, int mouseY, datetime barTime, double barPrice); //--- Delete all measure mode chart objects and hide canvases void DeleteAllMeasureObjects(); }; //+------------------------------------------------------------------+ //| Draw the reticle tick-mark crosses onto the reticle canvas | //+------------------------------------------------------------------+ void CCrosshairManager::DrawReticleTickMarks() { //--- Clear the reticle canvas to fully transparent m_canvasReticle.Erase(0x00000000); //--- Compute the centre of the square reticle canvas int cx = m_reticleCanvasSize / 2, cy = m_reticleCanvasSize / 2; //--- Cache tick geometry from inputs int off = ReticleOffset, tl = ReticleTickLen / 2, th = ReticleThickness; //--- Pack the chart foreground colour at slightly reduced opacity uint col = ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 230); //--- Draw left tick marks above and below the horizontal axis m_canvasReticle.FillRectangle(cx - off - tl, cy - th - 1, cx - off + tl, cy - 2, col); m_canvasReticle.FillRectangle(cx - off - tl, cy + 2, cx - off + tl, cy + th + 1, col); //--- Draw right tick marks above and below the horizontal axis m_canvasReticle.FillRectangle(cx + off - tl, cy - th - 1, cx + off + tl, cy - 2, col); m_canvasReticle.FillRectangle(cx + off - tl, cy + 2, cx + off + tl, cy + th + 1, col); //--- Draw top tick marks left and right of the vertical axis m_canvasReticle.FillRectangle(cx - th - 1, cy - off - tl, cx - 2, cy - off + tl, col); m_canvasReticle.FillRectangle(cx + 2, cy - off - tl, cx + th + 1, cy - off + tl, col); //--- Draw bottom tick marks left and right of the vertical axis m_canvasReticle.FillRectangle(cx - th - 1, cy + off - tl, cx - 2, cy + off + tl, col); m_canvasReticle.FillRectangle(cx + 2, cy + off - tl, cx + th + 1, cy + off + tl, col); m_canvasReticle.Update(); } //+------------------------------------------------------------------+ //| Make the reticle canvas visible on the chart | //+------------------------------------------------------------------+ void CCrosshairManager::ShowReticle() { //--- Skip if already visible if (m_isReticleVisible) return; //--- Draw fresh tick marks before making visible DrawReticleTickMarks(); //--- Make the reticle chart object visible on all timeframes ObjectSetInteger(0, m_nameReticle, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isReticleVisible = true; } //+------------------------------------------------------------------+ //| Hide the reticle canvas from the chart | //+------------------------------------------------------------------+ void CCrosshairManager::HideReticle() { //--- Skip if already hidden if (!m_isReticleVisible) return; ObjectSetInteger(0, m_nameReticle, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isReticleVisible = false; } //+------------------------------------------------------------------+ //| Move the reticle canvas to follow the mouse cursor | //+------------------------------------------------------------------+ void CCrosshairManager::UpdateReticlePosition(int mouseX, int mouseY) { if (!m_isReticleVisible) return; //--- Centre the reticle canvas on the mouse cursor int half = m_reticleCanvasSize / 2; ObjectSetInteger(0, m_nameReticle, OBJPROP_XDISTANCE, mouseX - half); ObjectSetInteger(0, m_nameReticle, OBJPROP_YDISTANCE, mouseY - half); } //+------------------------------------------------------------------+ //| Make the crosshair vertical line canvas visible | //+------------------------------------------------------------------+ void CCrosshairManager::ShowCrossVertical() { if (m_isCrossVertVisible) return; ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isCrossVertVisible = true; } //+------------------------------------------------------------------+ //| Hide the crosshair vertical line canvas | //+------------------------------------------------------------------+ void CCrosshairManager::HideCrossVertical() { if (!m_isCrossVertVisible) return; ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isCrossVertVisible = false; } //+------------------------------------------------------------------+ //| Move the crosshair vertical line to the given screen X | //+------------------------------------------------------------------+ void CCrosshairManager::UpdateCrossVerticalPosition(int mouseX) { if (!m_isCrossVertVisible) return; //--- Position the 1-pixel-wide canvas at the cursor X ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_XDISTANCE, mouseX); ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_YDISTANCE, 0); } //+------------------------------------------------------------------+ //| Make the crosshair horizontal line canvas visible | //+------------------------------------------------------------------+ void CCrosshairManager::ShowCrossHorizontal() { if (m_isCrossHorizVisible) return; ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isCrossHorizVisible = true; } //+------------------------------------------------------------------+ //| Hide the crosshair horizontal line canvas | //+------------------------------------------------------------------+ void CCrosshairManager::HideCrossHorizontal() { if (!m_isCrossHorizVisible) return; ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isCrossHorizVisible = false; } //+------------------------------------------------------------------+ //| Move the crosshair horizontal line to the given screen Y | //+------------------------------------------------------------------+ void CCrosshairManager::UpdateCrossHorizontalPosition(int mouseY) { if (!m_isCrossHorizVisible) return; //--- Position the 1-pixel-tall canvas at the cursor Y ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_XDISTANCE, 0); ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_YDISTANCE, mouseY); } //+------------------------------------------------------------------+ //| Make the crosshair price axis label visible | //+------------------------------------------------------------------+ void CCrosshairManager::ShowCrossPriceLabel() { if (m_isCrossPriceLabelVisible) return; ObjectSetInteger(0, m_nameCrossPriceLabel, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isCrossPriceLabelVisible = true; } //+------------------------------------------------------------------+ //| Hide the crosshair price axis label | //+------------------------------------------------------------------+ void CCrosshairManager::HideCrossPriceLabel() { if (!m_isCrossPriceLabelVisible) return; ObjectSetInteger(0, m_nameCrossPriceLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isCrossPriceLabelVisible = false; } //+------------------------------------------------------------------+ //| Make the crosshair time axis label visible | //+------------------------------------------------------------------+ void CCrosshairManager::ShowCrossTimeLabel() { if (m_isCrossTimeLabelVisible) return; ObjectSetInteger(0, m_nameCrossTimeLabel, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isCrossTimeLabelVisible = true; } //+------------------------------------------------------------------+ //| Hide the crosshair time axis label | //+------------------------------------------------------------------+ void CCrosshairManager::HideCrossTimeLabel() { if (!m_isCrossTimeLabelVisible) return; ObjectSetInteger(0, m_nameCrossTimeLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isCrossTimeLabelVisible = false; } //+------------------------------------------------------------------+ //| Draw and position one axis label canvas next to the crosshair | //+------------------------------------------------------------------+ void CCrosshairManager::DrawAndPositionAxisLabel(CCanvas &labelCanvas, string objectName, string labelText, bool isPriceAxis, int crosshairPixelPos, int chartWidth, int chartHeight) { color fgColor = (color)ChartGetInteger(0, CHART_COLOR_FOREGROUND); color bgColor = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND); uint fg = ColorToARGB(fgColor, 255), bg = ColorToARGB(bgColor, 255); //--- Measure text using standalone API TextSetFont(AxisLabelFont, -AxisLabelFontSize * 10); uint tw = 0, th = 0; TextGetSize(labelText, tw, th); int lw = (int)tw + 8, lh = (int)th + 4; if (labelCanvas.Width() != lw || labelCanvas.Height() != lh) labelCanvas.Resize(lw, lh); ObjectSetInteger(0, objectName, OBJPROP_XSIZE, lw); ObjectSetInteger(0, objectName, OBJPROP_YSIZE, lh); //--- Render text into raw buffer using XRGB — no alpha, pure colors uint textBuf[]; int totalPx = lw * lh; ArrayResize(textBuf, totalPx); ArrayFill(textBuf, 0, totalPx, bg & 0x00FFFFFF); TextOut(labelText, 4, 2, TA_LEFT | TA_TOP, textBuf, lw, lh, fg & 0x00FFFFFF, COLOR_FORMAT_XRGB_NOALPHA); //--- Copy clean pixels onto canvas with full alpha for (int py = 0; py < lh; py++) for (int px = 0; px < lw; px++) labelCanvas.PixelSet(px, py, textBuf[py * lw + px] | 0xFF000000); //--- Draw border on top labelCanvas.Rectangle(0, 0, lw - 1, lh - 1, fg); labelCanvas.Update(); //--- Position the label at the appropriate axis edge if (isPriceAxis) { ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, chartWidth - lw + 1); ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, crosshairPixelPos - lh / 2); } else { ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, crosshairPixelPos - lw / 2); ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, chartHeight - lh); } } //+------------------------------------------------------------------+ //| Update both crosshair axis labels for the current mouse position | //+------------------------------------------------------------------+ void CCrosshairManager::UpdateCrosshairAxisLabels(int mouseX, int mouseY, datetime barTime, double barPrice) { //--- Read chart dimensions and symbol digit count int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); //--- Draw and position the price label on the right axis at cursor Y DrawAndPositionAxisLabel(m_canvasCrossPriceLabel, m_nameCrossPriceLabel, DoubleToString(barPrice, digits), true, mouseY, chartW, chartH); //--- Draw and position the time label on the bottom axis at cursor X DrawAndPositionAxisLabel(m_canvasCrossTimeLabel, m_nameCrossTimeLabel, TimeToString(barTime, TIME_DATE | TIME_MINUTES), false, mouseX, chartW, chartH); } //+------------------------------------------------------------------+ //| Show all measure mode line and label canvases | //+------------------------------------------------------------------+ void CCrosshairManager::ShowMeasureLines() { //--- Show each measure canvas if not already visible if (!m_isMeasureVertVisible) { ObjectSetInteger(0, m_nameMeasureVertical, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isMeasureVertVisible = true; } if (!m_isMeasureHorizVisible) { ObjectSetInteger(0, m_nameMeasureHorizontal, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isMeasureHorizVisible = true; } if (!m_isMeasureDiagonalVisible) { ObjectSetInteger(0, m_nameMeasureDiagonalLine, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isMeasureDiagonalVisible = true; } if (!m_isMeasurePriceLabelVisible) { ObjectSetInteger(0, m_nameMeasurePriceLabel, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isMeasurePriceLabelVisible = true; } if (!m_isMeasureTimeLabelVisible) { ObjectSetInteger(0, m_nameMeasureTimeLabel, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isMeasureTimeLabelVisible = true; } } //+------------------------------------------------------------------+ //| Hide all measure mode line and label canvases | //+------------------------------------------------------------------+ void CCrosshairManager::HideMeasureLines() { //--- Hide each measure canvas if currently visible if (m_isMeasureVertVisible) { ObjectSetInteger(0, m_nameMeasureVertical, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isMeasureVertVisible = false; } if (m_isMeasureHorizVisible) { ObjectSetInteger(0, m_nameMeasureHorizontal, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isMeasureHorizVisible = false; } if (m_isMeasureDiagonalVisible) { //--- Clear the diagonal canvas before hiding to avoid stale pixels ObjectSetInteger(0, m_nameMeasureDiagonalLine, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_canvasMeasureDiagonalLine.Erase(0x00000000); m_canvasMeasureDiagonalLine.Update(); m_isMeasureDiagonalVisible = false; } if (m_isMeasurePriceLabelVisible) { ObjectSetInteger(0, m_nameMeasurePriceLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isMeasurePriceLabelVisible = false; } if (m_isMeasureTimeLabelVisible) { ObjectSetInteger(0, m_nameMeasureTimeLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isMeasureTimeLabelVisible = false; } } //+------------------------------------------------------------------+ //| Move the measure vertical anchor line to the given screen X | //+------------------------------------------------------------------+ void CCrosshairManager::UpdateMeasureVerticalPosition(int pixelX) { if (!m_isMeasureVertVisible) return; ObjectSetInteger(0, m_nameMeasureVertical, OBJPROP_XDISTANCE, pixelX); ObjectSetInteger(0, m_nameMeasureVertical, OBJPROP_YDISTANCE, 0); } //+------------------------------------------------------------------+ //| Move the measure horizontal anchor line to the given screen Y | //+------------------------------------------------------------------+ void CCrosshairManager::UpdateMeasureHorizontalPosition(int pixelY) { if (!m_isMeasureHorizVisible) return; ObjectSetInteger(0, m_nameMeasureHorizontal, OBJPROP_XDISTANCE, 0); ObjectSetInteger(0, m_nameMeasureHorizontal, OBJPROP_YDISTANCE, pixelY); } //+------------------------------------------------------------------+ //| Update the measure anchor axis labels at the anchor coordinate | //+------------------------------------------------------------------+ void CCrosshairManager::UpdateMeasureAnchorLabels() { //--- Read chart dimensions and symbol digit count int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); //--- Convert the anchor chart coordinate to screen pixel position int fx = 0, fy = 0; if (!ChartTimePriceToXY(m_chartId, 0, m_measureAnchorTime, m_measureAnchorPrice, fx, fy)) return; //--- Draw and position the anchor price label on the right axis DrawAndPositionAxisLabel(m_canvasMeasurePriceLabel, m_nameMeasurePriceLabel, DoubleToString(m_measureAnchorPrice, digits), true, fy, chartW, chartH); //--- Draw and position the anchor time label on the bottom axis DrawAndPositionAxisLabel(m_canvasMeasureTimeLabel, m_nameMeasureTimeLabel, TimeToString(m_measureAnchorTime, TIME_DATE | TIME_MINUTES), false, fx, chartW, chartH); } //+------------------------------------------------------------------+ //| Make the magnifier lens canvas visible | //+------------------------------------------------------------------+ void CCrosshairManager::ShowMagnifier() { if (m_isMagnifierVisible) return; ObjectSetInteger(0, m_nameMagnifier, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isMagnifierVisible = true; } //+------------------------------------------------------------------+ //| Hide the magnifier lens canvas | //+------------------------------------------------------------------+ void CCrosshairManager::HideMagnifier() { if (!m_isMagnifierVisible) return; ObjectSetInteger(0, m_nameMagnifier, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isMagnifierVisible = false; } //+------------------------------------------------------------------+ //| Move the magnifier and redraw lens content if cursor has moved | //+------------------------------------------------------------------+ void CCrosshairManager::UpdateMagnifierPosition(int mouseX, int mouseY, datetime barTime, double barPrice) { if (!m_isMagnifierVisible) return; int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); int diam = MagDiameter; //--- Prefer placing the magnifier to the upper-right of the cursor int magX = (mouseX + MagOffset + diam < chartW) ? mouseX + MagOffset : mouseX - MagOffset - diam; int magY = (mouseY - MagOffset - diam > 0) ? mouseY - MagOffset - diam : mouseY + MagOffset; //--- Clamp the magnifier position within chart bounds magX = MathMax(2, MathMin(chartW - diam - 2, magX)); magY = MathMax(2, MathMin(chartH - diam - 2, magY)); ObjectSetInteger(0, m_nameMagnifier, OBJPROP_XDISTANCE, magX); ObjectSetInteger(0, m_nameMagnifier, OBJPROP_YDISTANCE, magY); //--- Skip redrawing lens content if cursor has not moved if (mouseX == m_lastMagMouseX && mouseY == m_lastMagMouseY) return; m_lastMagMouseX = mouseX; m_lastMagMouseY = mouseY; //--- Redraw lens content for the new cursor position DrawMagnifierLensContent(mouseX, mouseY, barTime, barPrice); } //+------------------------------------------------------------------+ //| Render zoomed candle chart content inside the magnifier lens | //+------------------------------------------------------------------+ void CCrosshairManager::DrawMagnifierLensContent(int mouseX, int mouseY, datetime centerTime, double centerPrice) { int diam = MagDiameter, radius = diam / 2; double zoom = MagZoom; //--- Resize magnifier canvas if diameter has changed if (m_canvasMagnifier.Width() != diam || m_canvasMagnifier.Height() != diam) m_canvasMagnifier.Resize(diam, diam); m_canvasMagnifier.Erase(0x00000000); //--- Read chart colour settings for consistent lens rendering color bgColor = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND); color fgColor = (color)ChartGetInteger(0, CHART_COLOR_FOREGROUND); color bullBody = (color)ChartGetInteger(0, CHART_COLOR_CANDLE_BULL); color bearBody = (color)ChartGetInteger(0, CHART_COLOR_CANDLE_BEAR); color bullBord = (color)ChartGetInteger(0, CHART_COLOR_CHART_UP); color bearBord = (color)ChartGetInteger(0, CHART_COLOR_CHART_DOWN); color askColor = (color)ChartGetInteger(0, CHART_COLOR_ASK); color bidColor = (color)ChartGetInteger(0, CHART_COLOR_BID); bool showAsk = (ChartGetInteger(0, CHART_SHOW_ASK_LINE) != 0); bool showBid = (ChartGetInteger(0, CHART_SHOW_BID_LINE) != 0); //--- Read chart price range and bar width for coordinate mapping int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); double chartMax = ChartGetDouble(0, CHART_PRICE_MAX, 0); double chartMin = ChartGetDouble(0, CHART_PRICE_MIN, 0); double chartRange = MathMax(chartMax - chartMin, _Point * 100); int barWidth = (int)MathPow(2.0, (int)ChartGetInteger(0, CHART_SCALE)); double pricePerPixel = chartRange / chartH; double radiusSq = (double)(radius - 3) * (radius - 3); //--- Fill the circular lens background uint bgARGB = ColorToARGB(bgColor, 255); double bgRSq = (double)(radius - 2) * (radius - 2); for (int py = 0; py < diam; py++) for (int px = 0; px < diam; px++) { double ddx = px - radius, ddy = py - radius; if (ddx * ddx + ddy * ddy <= bgRSq) m_canvasMagnifier.PixelSet(px, py, bgARGB); } //--- Compute bar range to fetch based on zoom and visible bar width int chartVisibleBars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS); int halfRange = MathMin((int)((radius * 2.0 / zoom) / MathMax(1, barWidth)) / 2 + 2, chartVisibleBars / 2 + 2); int cursorBar = iBarShift(_Symbol, _Period, centerTime, false); if (cursorBar < 0) cursorBar = 0; //--- Fetch OHLC rates for the visible bar range MqlRates rates[]; ArraySetAsSeries(rates, false); int startBar = MathMax(0, cursorBar - halfRange); int copied = CopyRates(_Symbol, _Period, startBar, cursorBar + halfRange + 1 - startBar, rates); //--- Compute zoomed wick and border thickness int wickThickness = MathMax(1, (int)MathRound(zoom * 0.55)); int borderThickness = MathMax(1, (int)MathRound(zoom * 0.45)); //--- Draw each candle inside the lens clipped to the circle if (copied > 0) { for (int i = 0; i < copied; i++) { //--- Convert bar time/price to screen pixel position int barPxX = 0, barPxY = 0; if (!ChartTimePriceToXY(m_chartId, 0, rates[i].time, rates[i].close, barPxX, barPxY)) continue; //--- Map screen pixel to lens X using zoom factor int lensX = radius + (int)((barPxX - mouseX) * zoom); //--- Compute zoomed bar body width int zbw = MathMax(3, (int)(barWidth * zoom * 0.65)); if (zbw % 2 == 0) zbw++; int bh = zbw / 2; if (lensX + bh < 0 || lensX - bh >= diam) continue; //--- Determine candle direction and pack colours bool isBull = (rates[i].close >= rates[i].open); uint wickARGB = ColorToARGB(isBull ? bullBord : bearBord, 255); uint bodyARGB = ColorToARGB(isBull ? bullBody : bearBody, 255); uint borderARGB = ColorToARGB(isBull ? bullBord : bearBord, 255); //--- Convert high/low/open/close to lens Y coordinates int lensHi = radius - (int)((rates[i].high - centerPrice) / pricePerPixel * zoom); int lensLo = radius - (int)((rates[i].low - centerPrice) / pricePerPixel * zoom); double bTop = isBull ? rates[i].close : rates[i].open; double bBot = isBull ? rates[i].open : rates[i].close; int lensBT = radius - (int)((bTop - centerPrice) / pricePerPixel * zoom); int lensBB = radius - (int)((bBot - centerPrice) / pricePerPixel * zoom); //--- Ensure minimum body height of 1 pixel if (lensBB - lensBT < 1) lensBB = lensBT + 1; //--- Draw the wick clipped to the lens circle int wickHalf = wickThickness / 2; for (int wy = MathMax(0, lensHi); wy <= MathMin(diam - 1, lensLo); wy++) for (int wx = lensX - wickHalf; wx <= lensX + wickHalf; wx++) { if (wx < 0 || wx >= diam) continue; double ddx = wx - radius, ddy = wy - radius; if (ddx * ddx + ddy * ddy < radiusSq) m_canvasMagnifier.PixelSet(wx, wy, wickARGB); } //--- Draw the candle body fill clipped to the lens circle for (int by = MathMax(0, lensBT); by <= MathMin(diam - 1, lensBB); by++) for (int bx = lensX - bh; bx <= lensX + bh; bx++) { if (bx < 0 || bx >= diam) continue; double ddx = bx - radius, ddy = by - radius; if (ddx * ddx + ddy * ddy < radiusSq) m_canvasMagnifier.PixelSet(bx, by, bodyARGB); } //--- Draw the candle body border clipped to the lens circle for (int bt = 0; bt < borderThickness; bt++) { int topRow = MathMax(0, lensBT + bt), botRow = MathMin(diam - 1, lensBB - bt); for (int bx = lensX - bh; bx <= lensX + bh; bx++) { if (bx < 0 || bx >= diam) continue; double ddx = bx - radius; double ddyT = topRow - radius, ddyB = botRow - radius; if (ddx * ddx + ddyT * ddyT < radiusSq) m_canvasMagnifier.PixelSet(bx, topRow, borderARGB); if (ddx * ddx + ddyB * ddyB < radiusSq) m_canvasMagnifier.PixelSet(bx, botRow, borderARGB); } int leftCol = lensX - bh + bt, rightCol = lensX + bh - bt; for (int by = MathMax(0, lensBT); by <= MathMin(diam - 1, lensBB); by++) { double ddy = by - radius; double ddxL = leftCol - radius, ddxR = rightCol - radius; if (leftCol >= 0 && leftCol < diam && ddxL * ddxL + ddy * ddy < radiusSq) m_canvasMagnifier.PixelSet(leftCol, by, borderARGB); if (rightCol >= 0 && rightCol < diam && ddxR * ddxR + ddy * ddy < radiusSq) m_canvasMagnifier.PixelSet(rightCol, by, borderARGB); } } } } //--- Draw Bid line inside lens if enabled if (showBid) { double bidPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); int bidY = radius - (int)((bidPrice - centerPrice) / pricePerPixel * zoom); uint bidARGB = ColorToARGB(bidColor, 200); for (int gx = 0; gx < diam; gx++) { double ddx = gx - radius, ddy = bidY - radius; if (ddx * ddx + ddy * ddy < radiusSq) BlendPixelSet(m_canvasMagnifier, gx, bidY, bidARGB); } } //--- Draw Ask line inside lens if enabled if (showAsk) { double askPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK); int askY = radius - (int)((askPrice - centerPrice) / pricePerPixel * zoom); uint askARGB = ColorToARGB(askColor, 200); for (int gx = 0; gx < diam; gx++) { double ddx = gx - radius, ddy = askY - radius; if (ddx * ddx + ddy * ddy < radiusSq) BlendPixelSet(m_canvasMagnifier, gx, askY, askARGB); } } //--- Draw the anti-aliased circular lens ring border uint ringARGB = ColorToARGB(m_isDarkTheme ? C'140,150,170' : C'80,90,110', 255); double outerR = radius - 1.0, innerR = outerR - 2.5; for (int py = 0; py < diam; py++) for (int px = 0; px < diam; px++) { double ddx = px - radius + 0.5, ddy = py - radius + 0.5; double dist = MathSqrt(ddx * ddx + ddy * ddy); if (dist < innerR - 1.0 || dist > outerR + 1.0) continue; //--- Compute anti-aliased ring edge coverage double alpha = MathMin(MathMin(1.0, dist - (innerR - 1.0)), MathMin(1.0, outerR + 1.0 - dist)); if (alpha <= 0.0) continue; BlendPixelSet(m_canvasMagnifier, px, py, ((uint)(uchar)(alpha * 255.0) << 24) | (ringARGB & 0x00FFFFFF)); } //--- Draw a faint dashed crosshair at lens centre uint crossARGB = ColorToARGB(fgColor, 60); for (int px = 0; px < diam; px++) { if (px % 4 == 0) continue; double ddx = (double)(px - radius); if (ddx * ddx >= radiusSq) continue; BlendPixelSet(m_canvasMagnifier, px, radius, crossARGB); } for (int py = 0; py < diam; py++) { if (py % 4 == 0) continue; double ddy = (double)(py - radius); if (ddy * ddy >= radiusSq) continue; BlendPixelSet(m_canvasMagnifier, radius, py, crossARGB); } //--- Draw the current price label inside the lens near the bottom int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); m_canvasMagnifier.FontSet("Arial Bold", 11); string priceStr = DoubleToString(centerPrice, digits); int tw = m_canvasMagnifier.TextWidth(priceStr), th = m_canvasMagnifier.TextHeight(priceStr); int tx = radius - tw / 2, ty = diam - th - 16; double tdy = ty - radius; //--- Only draw the label if it fits within the lens circle if (tdy * tdy + 4 < radiusSq) { m_canvasMagnifier.FillRectangle(tx - 4, ty - 1, tx + tw + 4, ty + th + 1, (ringARGB & 0x00FFFFFF) | 0xFF000000); m_canvasMagnifier.TextOut(tx, ty, priceStr, ColorToARGB(clrWhite, 255)); } m_canvasMagnifier.Update(); } //+------------------------------------------------------------------+ //| Redraw the measure diagonal line from anchor to current cursor | //+------------------------------------------------------------------+ void CCrosshairManager::UpdateMeasureDiagonalLine(int currentMouseX, int currentMouseY) { if (!m_isMeasureDiagonalVisible) return; //--- Clear the full-screen diagonal canvas before redrawing m_canvasMeasureDiagonalLine.Erase(0x00000000); //--- Draw an anti-aliased Bresenham line from anchor to current cursor DrawBresenhamLine(m_canvasMeasureDiagonalLine, m_measureAnchorPixelX, m_measureAnchorPixelY, currentMouseX, currentMouseY, ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 220)); m_canvasMeasureDiagonalLine.Update(); } //+------------------------------------------------------------------+ //| Update the floating measure info label near the cursor | //+------------------------------------------------------------------+ void CCrosshairManager::UpdateMeasurementInfoLabel(int mouseX, int mouseY, datetime barTime, double barPrice) { string labelName = "ToolsPalette_MeasureInfoLabel"; //--- Compute bar count between anchor and cursor using period seconds long periodSec = PeriodSeconds(_Period); int barCount = (int)MathAbs(m_measureAnchorTime / periodSec - barTime / periodSec); //--- Compute pip distance using correct pip size for the symbol double pointSize = SymbolInfoDouble(_Symbol, SYMBOL_POINT); long digits = SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); double pipSize = (digits == 3 || digits == 5) ? pointSize * 10.0 : pointSize; double pips = MathAbs(barPrice - m_measureAnchorPrice) / pipSize; //--- Build the label text string with bar count, pip distance, and raw price difference string labelText = StringFormat("%d bars, %.1f pips, Diff: %s", barCount, pips, DoubleToString(MathAbs(barPrice - m_measureAnchorPrice), (int)digits)); //--- Create the OBJ_LABEL chart object if it does not yet exist if (ObjectFind(m_chartId, labelName) < 0) { ObjectCreate(m_chartId, labelName, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(m_chartId, labelName, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(m_chartId, labelName, OBJPROP_FONTSIZE, 9); ObjectSetString(m_chartId, labelName, OBJPROP_FONT, "Arial"); ObjectSetInteger(m_chartId, labelName, OBJPROP_COLOR, (color)ChartGetInteger(0, CHART_COLOR_FOREGROUND)); } //--- Update the label position to follow the cursor with a small offset ObjectSetInteger(m_chartId, labelName, OBJPROP_XDISTANCE, mouseX + 20); ObjectSetInteger(m_chartId, labelName, OBJPROP_YDISTANCE, mouseY + 3); ObjectSetString(m_chartId, labelName, OBJPROP_TEXT, labelText); } //+------------------------------------------------------------------+ //| Hide all crosshair element canvases in one call | //+------------------------------------------------------------------+ void CCrosshairManager::HideAllCrosshairElements() { HideReticle(); HideMagnifier(); HideCrossVertical(); HideCrossHorizontal(); HideCrossPriceLabel(); HideCrossTimeLabel(); } //+------------------------------------------------------------------+ //| Show all crosshair element canvases in one call | //+------------------------------------------------------------------+ void CCrosshairManager::ShowAllCrosshairElements() { ShowReticle(); ShowMagnifier(); ShowCrossVertical(); ShowCrossHorizontal(); ShowCrossPriceLabel(); ShowCrossTimeLabel(); } //+------------------------------------------------------------------+ //| Delete all measure mode chart objects and hide canvases | //+------------------------------------------------------------------+ void CCrosshairManager::DeleteAllMeasureObjects() { //--- Hide all measure canvases HideMeasureLines(); //--- Remove the floating info label chart object ObjectDelete(m_chartId, "ToolsPalette_MeasureInfoLabel"); } //+------------------------------------------------------------------+ //| Handle a potential double-click to toggle measure mode anchor | //+------------------------------------------------------------------+ void CCrosshairManager::HandleCrosshairDoubleClick(int mouseX, int mouseY, datetime barTime, double barPrice) { ulong nowMicros = GetMicrosecondCount(); //--- Detect double-click by checking time since last click if (nowMicros - m_lastClickTimeMicros < 500000) { if (!m_isMeasuringActive) { //--- Lock the measure anchor to the current chart coordinate m_measureAnchorTime = barTime; m_measureAnchorPrice = barPrice; m_measureAnchorPixelX = mouseX; m_measureAnchorPixelY = mouseY; m_isMeasuringActive = true; //--- Disable chart scroll while measuring ChartSetInteger(0, CHART_MOUSE_SCROLL, false); } else { //--- Release the measure anchor and clean up all measure objects m_isMeasuringActive = false; DeleteAllMeasureObjects(); ChartSetInteger(0, CHART_MOUSE_SCROLL, true); } //--- Reset click timer to prevent triple-click retriggering m_lastClickTimeMicros = 0; } else { //--- Record this click as the first of a potential double-click pair m_lastClickTimeMicros = nowMicros; } } //+------------------------------------------------------------------+ //| CLASS 9 — Route chart events to the appropriate handlers | //+------------------------------------------------------------------+ class CChartEventHandler : public CCrosshairManager { protected: int m_previousMouseButtonState; // Mouse button state recorded during the last MOUSE_MOVE event protected: //--- Dispatch a chart event to the correct handler method void RouteChartEvent(const int id, const long &lp, const double &dp, const string &sp, TOOL_TYPE &activeTool); //--- Handle CHARTEVENT_CHART_CHANGE: reposition and resize all canvases void OnChartChangeEvent(TOOL_TYPE activeTool); //--- Handle CHARTEVENT_MOUSE_WHEEL: scroll sidebar or flyout under the cursor void OnMouseWheelEvent(int mouseX, int mouseY, int wheelDelta, TOOL_TYPE activeTool); //--- Handle CHARTEVENT_MOUSE_MOVE: dispatch all sub-handlers for mouse interaction void OnMouseMoveEvent(int mouseX, int mouseY, int mouseButtons, TOOL_TYPE &activeTool); //--- Move the sidebar panel to follow the mouse during a drag void HandlePanelDragMove(int mouseX, int mouseY, TOOL_TYPE activeTool); //--- Finalise a panel drag, snap the panel, and redraw void HandlePanelDragRelease(TOOL_TYPE activeTool); //--- Resize the sidebar height during a bottom-edge drag void HandleBottomResizeDrag(int mouseX, int mouseY, TOOL_TYPE activeTool); //--- Scroll the sidebar list by dragging the sidebar scroll thumb void HandleSidebarThumbDrag(int mouseX, int mouseY, TOOL_TYPE activeTool); //--- Finalise a sidebar thumb drag and redraw void HandleSidebarThumbRelease(TOOL_TYPE activeTool); //--- Scroll the flyout list by dragging the flyout scroll thumb void HandleFlyoutThumbDrag(int mouseX, int mouseY); //--- Finalise a flyout thumb drag and redraw void HandleFlyoutThumbRelease(); //--- Recompute all hover flags and redraw if any state changed void UpdateAllHoverStates(int mouseX, int mouseY, bool overSidebar, bool overFlyout, int lx, int ly, int flx, int fly, TOOL_TYPE activeTool); //--- Handle a fresh left-button press on the sidebar or flyout void HandleMouseClickDown(int mouseX, int mouseY, bool overSidebar, bool overFlyout, int lx, int ly, int flx, int fly, TOOL_TYPE &activeTool); }; //+------------------------------------------------------------------+ //| Dispatch a chart event to the correct handler method | //+------------------------------------------------------------------+ void CChartEventHandler::RouteChartEvent(const int id, const long &lp, const double &dp, const string &sp, TOOL_TYPE &activeTool) { //--- Dispatch chart change events (window resize, scroll, zoom) if (id == CHARTEVENT_CHART_CHANGE) { OnChartChangeEvent(activeTool); return; } //--- Dispatch mouse wheel scroll events if (id == CHARTEVENT_MOUSE_WHEEL) { OnMouseWheelEvent((int)(short)lp, (int)(short)(lp >> 16), (int)dp, activeTool); return; } //--- Dispatch mouse move events (includes button state in sp) if (id == CHARTEVENT_MOUSE_MOVE) OnMouseMoveEvent((int)lp, (int)dp, (int)sp, activeTool); } //+------------------------------------------------------------------+ //| Handle chart change: reposition and resize all canvases | //+------------------------------------------------------------------+ void CChartEventHandler::OnChartChangeEvent(TOOL_TYPE activeTool) { //--- Reset all drag and interaction state on chart resize/redraw m_previousMouseButtonState = 0; m_isPanelDragging = false; m_isResizingBottomEdge = false; m_isSidebarThumbDragging = false; m_isFlyoutThumbDragging = false; m_lastMagMouseX = -9999; m_lastMagMouseY = -9999; ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Reposition and redraw snapped panels to keep them flush with the chart edge if (m_snapState != SNAP_FLOAT) { int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); m_panelX = (m_snapState == SNAP_RIGHT) ? chartW - m_sidebarWidth : 0; ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); //--- Clamp the snapped height override within the new chart bounds if (m_snappedSidebarHeight > 0) m_snappedSidebarHeight = MathMin(m_snappedSidebarHeight, (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - m_panelY - 8); CalcSidebarHeight(); ResizeSidebarCanvases(m_sidebarWidth, m_sidebarHeight); DrawSidebar(activeTool); if (m_isFlyoutVisible) ShowFlyout(m_flyoutActiveCat, activeTool); } //--- Read the updated chart dimensions int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Resize and redraw the full-height crosshair vertical line canvas m_canvasCrossVertical.Resize(1, chartH); DrawCrossVerticalLinePixels(chartH); ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_YSIZE, chartH); //--- Resize and redraw the full-width crosshair horizontal line canvas m_canvasCrossHorizontal.Resize(chartW, 1); DrawCrossHorizontalLinePixels(chartW); ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_XSIZE, chartW); //--- Resize and redraw the full-height measure vertical line canvas m_canvasMeasureVertical.Resize(1, chartH); DrawMeasureVerticalLinePixels(chartH); ObjectSetInteger(0, m_nameMeasureVertical, OBJPROP_YSIZE, chartH); //--- Resize and redraw the full-width measure horizontal line canvas m_canvasMeasureHorizontal.Resize(chartW, 1); DrawMeasureHorizontalLinePixels(chartW); ObjectSetInteger(0, m_nameMeasureHorizontal, OBJPROP_XSIZE, chartW); //--- Resize and clear the full-screen measure diagonal line canvas m_canvasMeasureDiagonalLine.Resize(chartW, chartH); m_canvasMeasureDiagonalLine.Erase(0x00000000); m_canvasMeasureDiagonalLine.Update(); ObjectSetInteger(0, m_nameMeasureDiagonalLine, OBJPROP_XSIZE, chartW); ObjectSetInteger(0, m_nameMeasureDiagonalLine, OBJPROP_YSIZE, chartH); //--- Update measure anchor screen positions if measuring is active if (m_isMeasuringActive) { int fx = 0, fy = 0; if (ChartTimePriceToXY(m_chartId, 0, m_measureAnchorTime, m_measureAnchorPrice, fx, fy)) { m_measureAnchorPixelX = fx; m_measureAnchorPixelY = fy; UpdateMeasureVerticalPosition(fx); UpdateMeasureHorizontalPosition(fy); UpdateMeasureAnchorLabels(); } } ChartRedraw(); } //+------------------------------------------------------------------+ //| Handle mouse wheel: scroll sidebar or flyout under the cursor | //+------------------------------------------------------------------+ void CChartEventHandler::OnMouseWheelEvent(int mouseX, int mouseY, int wheelDelta, TOOL_TYPE activeTool) { int lx, ly, flx, fly; bool overSidebar = HitTestOverSidebar(mouseX, mouseY, lx, ly); bool overFlyout = HitTestOverFlyout(mouseX, mouseY, flx, fly); //--- Scroll the sidebar if the wheel is over the sidebar and scrolling is needed if (overSidebar && m_sidebarMaxVisibleCats < CAT_COUNT) { ChartSetInteger(0, CHART_MOUSE_SCROLL, false); m_sidebarScrollPixels = MathMax(0, MathMin( m_sidebarScrollPixels + ((wheelDelta < 0) ? MathMax(1, MouseScrollSpeed) : -MathMax(1, MouseScrollSpeed)), CalcSidebarMaxScrollPixels())); HideFlyout(); DrawSidebar(activeTool); ChartRedraw(); return; } //--- Scroll the flyout if the wheel is over the flyout and scrolling is needed if (overFlyout && m_isFlyoutVisible && m_flyoutActiveCat != CAT_NONE) { int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); if (nTools > m_flyoutMaxVisibleItems) { ChartSetInteger(0, CHART_MOUSE_SCROLL, false); int maxPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight; m_flyoutScrollPixels = MathMax(0, MathMin( m_flyoutScrollPixels + ((wheelDelta < 0) ? MathMax(1, MouseScrollSpeed) : -MathMax(1, MouseScrollSpeed)), maxPx)); DrawFlyoutForCategory(m_flyoutActiveCat, activeTool); ChartRedraw(); } return; } //--- Restore chart scroll when the cursor is not over any panel ChartSetInteger(0, CHART_MOUSE_SCROLL, true); } //+------------------------------------------------------------------+ //| Move the sidebar panel during a drag interaction | //+------------------------------------------------------------------+ void CChartEventHandler::HandlePanelDragMove(int mouseX, int mouseY, TOOL_TYPE activeTool) { int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Clamp the panel position within chart bounds m_panelX = MathMax(0, MathMin(chartW - m_sidebarWidth, mouseX - m_dragOffsetX)); m_panelY = MathMax(0, MathMin(chartH - m_sidebarHeight, mouseY - m_dragOffsetY)); ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY); //--- Keep the flyout aligned if it is open if (m_isFlyoutVisible) ShowFlyout(m_flyoutActiveCat, activeTool); DrawSidebar(activeTool); ChartRedraw(); } //+------------------------------------------------------------------+ //| Finalise a panel drag, snap the panel, and redraw | //+------------------------------------------------------------------+ void CChartEventHandler::HandlePanelDragRelease(TOOL_TYPE activeTool) { m_isPanelDragging = false; TrySnapToEdge(); CalcSidebarHeight(); ResizeSidebarCanvases(m_sidebarWidth, m_sidebarHeight); DrawSidebar(activeTool); ChartRedraw(); } //+------------------------------------------------------------------+ //| Resize the sidebar height during a bottom-edge drag | //+------------------------------------------------------------------+ void CChartEventHandler::HandleBottomResizeDrag(int mouseX, int mouseY, TOOL_TYPE activeTool) { int dy = mouseY - m_bottomResizeDragStartY; int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Compute allowed height range for the resize operation int naturalH = m_headerGripHeight + 8 + CAT_COUNT * (m_categoryButtonSize + 6) - 6 + 10; int minH = m_headerGripHeight + 8 + 10 + 3 * (m_categoryButtonSize + 6) - 6; int maxH = (m_snapState != SNAP_FLOAT) ? MathMin(naturalH, chartH - m_panelY - 8) : chartH - m_panelY - 8; //--- Compute the new height clamped within bounds int newH = MathMax(minH, MathMin(maxH, m_bottomResizeStartHeight + dy)); if (newH != m_sidebarHeight) { //--- Apply the new height as a snapped override or a direct float height if (m_snapState != SNAP_FLOAT) m_snappedSidebarHeight = newH; else m_sidebarHeight = newH; CalcSidebarHeight(); ResizeSidebarCanvases(m_sidebarWidth, m_sidebarHeight); DrawSidebar(activeTool); ChartRedraw(); } } //+------------------------------------------------------------------+ //| Scroll the sidebar list by dragging the sidebar scroll thumb | //+------------------------------------------------------------------+ void CChartEventHandler::HandleSidebarThumbDrag(int mouseX, int mouseY, TOOL_TYPE activeTool) { int trackH = CalcSidebarViewportPixels(), travel = trackH - m_sidebarScrollThumbHeight; if (travel > 0) { int dy = mouseY - m_sidebarThumbDragStartY; int maxPx = CalcSidebarMaxScrollPixels(); //--- Map thumb drag displacement to scroll pixel offset int newPx = MathMax(0, MathMin(maxPx, m_sidebarThumbDragStartPixels + (int)MathRound((double)dy / travel * maxPx))); if (newPx != m_sidebarScrollPixels) { m_sidebarScrollPixels = newPx; HideFlyout(); DrawSidebar(activeTool); ChartRedraw(); } } } //+------------------------------------------------------------------+ //| Finalise a sidebar thumb drag and redraw | //+------------------------------------------------------------------+ void CChartEventHandler::HandleSidebarThumbRelease(TOOL_TYPE activeTool) { m_isSidebarThumbDragging = false; DrawSidebar(activeTool); ChartRedraw(); } //+------------------------------------------------------------------+ //| Scroll the flyout list by dragging the flyout scroll thumb | //+------------------------------------------------------------------+ void CChartEventHandler::HandleFlyoutThumbDrag(int mouseX, int mouseY) { if (m_flyoutActiveCat == CAT_NONE) return; int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); if (nTools <= m_flyoutMaxVisibleItems) return; int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight; int travel = trackH - m_flyoutScrollThumbHeight; if (travel > 0) { int dy = mouseY - m_flyoutThumbDragStartY; int maxPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight; //--- Map thumb drag displacement to flyout scroll pixel offset int newPx = MathMax(0, MathMin(maxPx, m_flyoutThumbDragStartPixels + (int)MathRound((double)dy / travel * maxPx))); if (newPx != m_flyoutScrollPixels) { m_flyoutScrollPixels = newPx; DrawFlyoutForCategory(m_flyoutActiveCat, TOOL_NONE); ChartRedraw(); } } } //+------------------------------------------------------------------+ //| Finalise a flyout thumb drag and redraw | //+------------------------------------------------------------------+ void CChartEventHandler::HandleFlyoutThumbRelease() { m_isFlyoutThumbDragging = false; DrawFlyoutForCategory(m_flyoutActiveCat, TOOL_NONE); ChartRedraw(); } //+------------------------------------------------------------------+ //| Recompute all hover flags and redraw if any state changed | //+------------------------------------------------------------------+ void CChartEventHandler::UpdateAllHoverStates(int mouseX, int mouseY, bool overSidebar, bool overFlyout, int lx, int ly, int flx, int fly, TOOL_TYPE activeTool) { //--- Snapshot previous hover states for change detection ENUM_CATEGORY prevHovCat = m_hoveredCategory; int prevHovItem = m_hoveredFlyoutItem; bool prevClose = m_isCloseButtonHovered, prevTheme = m_isThemeButtonHovered; bool prevGrip = m_isGripAreaHovered, prevSBA = m_isHoveredSidebarScrollArea; bool prevFSA = m_isHoveredFlyoutScrollArea, prevBR = m_isBottomResizeHovered; bool prevSbTh = m_isHoveredSidebarThumb, prevFlyTh = m_isHoveredFlyoutThumb; //--- Clear all hover flags before recomputing m_isCloseButtonHovered = m_isThemeButtonHovered = m_isGripAreaHovered = false; m_isBottomResizeHovered = m_isHoveredSidebarScrollArea = m_isHoveredSidebarThumb = false; m_isHoveredFlyoutScrollArea = m_isHoveredFlyoutThumb = false; //--- Recompute sidebar hover flags when cursor is over the sidebar if (overSidebar) { m_hoveredCategory = HitTestCategoryButton(lx, ly); m_isCloseButtonHovered = HitTestOverCloseButton(lx, ly); m_isThemeButtonHovered = HitTestOverThemeButton(lx, ly); m_isGripAreaHovered = HitTestOverGripArea(lx, ly); m_isBottomResizeHovered = HitTestOverBottomResizeGrip(lx, ly); //--- Recompute sidebar scroll thumb hover if scroll is active if (CalcSidebarMaxScrollPixels() > 0) { int trackY = CalcClipTop(), trackH = CalcSidebarViewportPixels(); m_isHoveredSidebarScrollArea = (ly >= trackY && ly <= trackY + trackH); if (m_isHoveredSidebarScrollArea) { int tw = m_sidebarScrollThinWidth; int thinX = (m_snapState == SNAP_RIGHT) ? 2 : m_sidebarWidth - tw - 2; if (lx >= thinX - 4 && lx <= thinX + tw + 4) { int maxPx = CalcSidebarMaxScrollPixels(); int sliderY = trackY + (int)((maxPx > 0 ? (double)m_sidebarScrollPixels / maxPx : 0.0) * (trackH - m_sidebarScrollThumbHeight)); m_isHoveredSidebarThumb = (ly >= sliderY && ly <= sliderY + m_sidebarScrollThumbHeight); } } } } else if (!overFlyout) m_hoveredCategory = CAT_NONE; //--- Recompute flyout hover flags when cursor is over the flyout if (overFlyout) { m_hoveredFlyoutItem = HitTestFlyoutItem(flx, fly); if (m_hoveredFlyoutItem < 0) m_hoveredFlyoutItem = -1; //--- Check if scroll area hover applies m_isHoveredFlyoutScrollArea = m_isFlyoutVisible && m_flyoutActiveCat != CAT_NONE && (ArraySize(m_categories[(int)m_flyoutActiveCat].tools) > m_flyoutMaxVisibleItems); if (m_isHoveredFlyoutScrollArea) { int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); int titleH = 26, itemsTop = titleH + m_flyoutPadding; int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight; int tw = m_sidebarScrollThinWidth; int dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0; int thinX = m_flyoutPointerOnLeft ? (dispBx + m_flyoutWidth - tw - 2) : (dispBx + 2); if (flx >= thinX - 6 && flx <= thinX + tw + 6 && fly >= itemsTop && fly <= itemsTop + trackH) { int maxPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight; int sliderY = itemsTop + (int)((maxPx > 0 ? (double)m_flyoutScrollPixels / maxPx : 0.0) * (trackH - m_flyoutScrollThumbHeight)); m_isHoveredFlyoutThumb = (fly >= sliderY && fly <= sliderY + m_flyoutScrollThumbHeight); } } } else if (!overSidebar) { m_hoveredFlyoutItem = -1; m_isHoveredFlyoutScrollArea = false; } //--- Open the flyout for a hovered category button that is not a header control if (overSidebar && m_hoveredCategory != CAT_NONE && !m_isCloseButtonHovered && !m_isThemeButtonHovered && !m_isGripAreaHovered) { if (m_hoveredCategory != m_flyoutActiveCat) ShowFlyout(m_hoveredCategory, activeTool); } else if (!overFlyout && m_isFlyoutVisible) { //--- Keep the flyout open when transitioning between the panel and the flyout bool transitEdge = false; if (overSidebar) { int margin = m_sidebarWidth / 4; transitEdge = (m_snapState == SNAP_LEFT) ? (lx >= m_sidebarWidth - margin) : (m_snapState == SNAP_RIGHT) ? (lx <= margin) : (m_flyoutPointerOnLeft ? (lx >= m_sidebarWidth - margin) : (lx <= margin)); } if (!transitEdge) { HideFlyout(); ChartRedraw(); } } //--- Redraw if any hover state changed bool changed = (prevHovCat != m_hoveredCategory || prevHovItem != m_hoveredFlyoutItem || prevClose != m_isCloseButtonHovered || prevTheme != m_isThemeButtonHovered || prevGrip != m_isGripAreaHovered || prevSBA != m_isHoveredSidebarScrollArea || prevFSA != m_isHoveredFlyoutScrollArea || prevBR != m_isBottomResizeHovered || prevSbTh != m_isHoveredSidebarThumb || prevFlyTh != m_isHoveredFlyoutThumb); if (changed) { DrawSidebar(activeTool); if (m_isFlyoutVisible) DrawFlyoutForCategory(m_flyoutActiveCat, activeTool); ChartRedraw(); } } //+------------------------------------------------------------------+ //| Handle a fresh left-button press on the sidebar or flyout | //+------------------------------------------------------------------+ void CChartEventHandler::HandleMouseClickDown(int mouseX, int mouseY, bool overSidebar, bool overFlyout, int lx, int ly, int flx, int fly, TOOL_TYPE &activeTool) { //--- Handle clicks on the sidebar scroll track or thumb if (overSidebar && CalcSidebarMaxScrollPixels() > 0) { int trackY = CalcClipTop(), trackH = CalcSidebarViewportPixels(), tw = m_sidebarScrollThinWidth; int thinX = (m_snapState == SNAP_RIGHT) ? 2 : m_sidebarWidth - tw - 2; if (lx >= thinX - 4 && lx <= thinX + tw + 4 && ly >= trackY && ly <= trackY + trackH) { int maxPx = CalcSidebarMaxScrollPixels(); int sliderY = trackY + (int)((maxPx > 0 ? (double)m_sidebarScrollPixels / maxPx : 0.0) * (trackH - m_sidebarScrollThumbHeight)); if (ly >= sliderY && ly <= sliderY + m_sidebarScrollThumbHeight) { //--- Begin thumb drag m_isSidebarThumbDragging = true; m_sidebarThumbDragStartY = mouseY; m_sidebarThumbDragStartPixels = m_sidebarScrollPixels; ChartSetInteger(0, CHART_MOUSE_SCROLL, false); HideFlyout(); DrawSidebar(activeTool); ChartRedraw(); } else { //--- Page-scroll the sidebar by one button size on track click int step = m_categoryButtonSize + m_categoryButtonPadding; m_sidebarScrollPixels = MathMax(0, MathMin(maxPx, m_sidebarScrollPixels + ((ly < sliderY) ? -step : step))); HideFlyout(); DrawSidebar(activeTool); ChartRedraw(); } return; } } //--- Handle clicks on the flyout scroll track or thumb if (overFlyout && m_flyoutActiveCat != CAT_NONE) { int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); if (nTools > m_flyoutMaxVisibleItems) { int titleH = 26, itemsTop = titleH + m_flyoutPadding; int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight; int tw = m_sidebarScrollThinWidth; int dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0; int thinX = m_flyoutPointerOnLeft ? (dispBx + m_flyoutWidth - tw - 2) : (dispBx + 2); if (flx >= thinX - 6 && flx <= thinX + tw + 6 && fly >= itemsTop && fly <= itemsTop + trackH) { int maxPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight; int sliderY = itemsTop + (int)((maxPx > 0 ? (double)m_flyoutScrollPixels / maxPx : 0.0) * (trackH - m_flyoutScrollThumbHeight)); if (fly >= sliderY && fly <= sliderY + m_flyoutScrollThumbHeight) { //--- Begin flyout thumb drag m_isFlyoutThumbDragging = true; m_flyoutThumbDragStartY = mouseY; m_flyoutThumbDragStartPixels = m_flyoutScrollPixels; ChartSetInteger(0, CHART_MOUSE_SCROLL, false); DrawFlyoutForCategory(m_flyoutActiveCat, activeTool); ChartRedraw(); } else { //--- Page-scroll the flyout by one item height on track click m_flyoutScrollPixels = MathMax(0, MathMin(maxPx, m_flyoutScrollPixels + ((fly < sliderY) ? -m_flyoutItemHeight : m_flyoutItemHeight))); DrawFlyoutForCategory(m_flyoutActiveCat, activeTool); ChartRedraw(); } return; } } } //--- Begin panel drag when clicking the grip area if (overSidebar && HitTestOverGripArea(lx, ly) && !m_isCloseButtonHovered && !m_isThemeButtonHovered) { m_isPanelDragging = true; m_dragOffsetX = lx; m_dragOffsetY = ly; ChartSetInteger(0, CHART_MOUSE_SCROLL, false); HideFlyout(); return; } //--- Begin bottom resize drag when clicking the resize grip if (overSidebar && HitTestOverBottomResizeGrip(lx, ly)) { m_isResizingBottomEdge = true; m_bottomResizeDragStartY = mouseY; m_bottomResizeStartHeight = m_sidebarHeight; ChartSetInteger(0, CHART_MOUSE_SCROLL, false); HideFlyout(); return; } //--- Remove the indicator when the close button is clicked if (overSidebar && m_isCloseButtonHovered) { ExpertRemove(); return; } } //+------------------------------------------------------------------+ //| Handle mouse move: dispatch all sub-handlers for mouse events | //+------------------------------------------------------------------+ void CChartEventHandler::OnMouseMoveEvent(int mouseX, int mouseY, int mouseButtons, TOOL_TYPE &activeTool) { int lx, ly, flx, fly; bool overSidebar = HitTestOverSidebar(mouseX, mouseY, lx, ly); bool overFlyout = !overSidebar && HitTestOverFlyout(mouseX, mouseY, flx, fly); //--- Delegate to active drag and resize handlers first if (m_isPanelDragging && mouseButtons == 1) { HandlePanelDragMove(mouseX, mouseY, activeTool); m_previousMouseButtonState = mouseButtons; return; } if (m_isPanelDragging && mouseButtons == 0) { HandlePanelDragRelease(activeTool); m_previousMouseButtonState = mouseButtons; return; } if (m_isResizingBottomEdge && mouseButtons == 1) { HandleBottomResizeDrag(mouseX, mouseY, activeTool); m_previousMouseButtonState = mouseButtons; return; } if (m_isResizingBottomEdge && mouseButtons == 0) { m_isResizingBottomEdge = false; m_previousMouseButtonState = mouseButtons; return; } if (m_isSidebarThumbDragging && mouseButtons == 1) { HandleSidebarThumbDrag(mouseX, mouseY, activeTool); m_previousMouseButtonState = mouseButtons; return; } if (m_isSidebarThumbDragging && mouseButtons == 0) { HandleSidebarThumbRelease(activeTool); m_previousMouseButtonState = mouseButtons; return; } if (m_isFlyoutThumbDragging && mouseButtons == 1) { HandleFlyoutThumbDrag(mouseX, mouseY); m_previousMouseButtonState = mouseButtons; return; } if (m_isFlyoutThumbDragging && mouseButtons == 0) { HandleFlyoutThumbRelease(); m_previousMouseButtonState = mouseButtons; return; } //--- Update hover state for all sidebar and flyout elements UpdateAllHoverStates(mouseX, mouseY, overSidebar, overFlyout, lx, ly, flx, fly, activeTool); //--- Restore or suppress chart scroll based on cursor position bool overAny = overSidebar || overFlyout; if (!m_isSidebarThumbDragging && !m_isPanelDragging && !m_isResizingBottomEdge && !m_isFlyoutThumbDragging) ChartSetInteger(0, CHART_MOUSE_SCROLL, !overAny); //--- Handle a new left-button press if (mouseButtons == 1 && m_previousMouseButtonState == 0) HandleMouseClickDown(mouseX, mouseY, overSidebar, overFlyout, lx, ly, flx, fly, activeTool); m_previousMouseButtonState = mouseButtons; } //+------------------------------------------------------------------+ //| CLASS 10 — Handle chart drawing tool click interactions | //+------------------------------------------------------------------+ class CDrawingEngine : public CChartEventHandler { protected: int m_drawnObjectCounter; // Counter used to generate unique object name suffixes int m_toolDrawingClickCount;// Number of clicks recorded for the current drawing operation datetime m_drawPoint1Time; // Chart time of the first click for multi-click tools datetime m_drawPoint2Time; // Chart time of the second click for three-click tools double m_drawPoint1Price; // Price level of the first click for multi-click tools double m_drawPoint2Price; // Price level of the second click for three-click tools protected: //--- Generate a unique chart object name for a new drawn object string MakeUniqueObjectName(); //--- Process a chart click for the active drawing tool void HandleDrawingClick(int mouseX, int mouseY, TOOL_TYPE &activeTool, string &instruction); //--- Create a chart object that requires only one click to place void CreateSingleClickObject(int sub, datetime t, double p, TOOL_TYPE toolType); //--- Create a chart object that requires two clicks to place void CreateTwoClickObject(int sub, TOOL_TYPE toolType); //--- Create a chart object that requires three clicks to place void CreateThreeClickObject(int sub, datetime t3, double p3, TOOL_TYPE toolType); }; //+------------------------------------------------------------------+ //| Generate a unique chart object name for a new drawn object | //+------------------------------------------------------------------+ string CDrawingEngine::MakeUniqueObjectName() { m_drawnObjectCounter++; //--- Combine counter and current time for guaranteed uniqueness return "ToolsPalette_Drawing_" + IntegerToString(m_drawnObjectCounter) + "_" + IntegerToString((int)TimeCurrent()); } //+------------------------------------------------------------------+ //| Process a chart click for the active drawing tool | //+------------------------------------------------------------------+ void CDrawingEngine::HandleDrawingClick(int mouseX, int mouseY, TOOL_TYPE &activeTool, string &instruction) { datetime barTime; double barPrice; int sub; //--- Convert screen coordinates to chart time and price if (!ChartXYToTimePrice(m_chartId, mouseX, mouseY, sub, barTime, barPrice)) return; int clicksNeeded = GetRequiredClickCount(activeTool); if (clicksNeeded <= 0) return; m_toolDrawingClickCount++; if (m_toolDrawingClickCount == 1) { //--- Record first anchor point m_drawPoint1Time = barTime; m_drawPoint1Price = barPrice; if (clicksNeeded == 1) { //--- Place single-click object immediately CreateSingleClickObject(sub, barTime, barPrice, activeTool); m_toolDrawingClickCount = 0; activeTool = TOOL_NONE; instruction = ""; } else instruction = "Click second point for " + GetToolLabel(activeTool) + "."; } else if (m_toolDrawingClickCount == 2) { //--- Record second anchor point m_drawPoint2Time = barTime; m_drawPoint2Price = barPrice; if (clicksNeeded == 2) { //--- Place two-click object CreateTwoClickObject(sub, activeTool); m_toolDrawingClickCount = 0; activeTool = TOOL_NONE; instruction = ""; } else instruction = "Click third point for " + GetToolLabel(activeTool) + "."; } else if (m_toolDrawingClickCount == 3) { //--- Place three-click object with all three anchor points CreateThreeClickObject(sub, barTime, barPrice, activeTool); m_toolDrawingClickCount = 0; activeTool = TOOL_NONE; instruction = ""; } } //+------------------------------------------------------------------+ //| Create a chart object that requires only one click to place | //+------------------------------------------------------------------+ void CDrawingEngine::CreateSingleClickObject(int sub, datetime t, double p, TOOL_TYPE toolType) { string name = MakeUniqueObjectName(); bool ok = false; switch (toolType) { //--- Horizontal line at the clicked price level case TOOL_HLINE: ok = ObjectCreate(m_chartId, name, OBJ_HLINE, 0, 0, p); if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrDodgerBlue); ObjectSetInteger(m_chartId, name, OBJPROP_STYLE, STYLE_DASH); } break; //--- Vertical line at the clicked bar time case TOOL_VLINE: ok = ObjectCreate(m_chartId, name, OBJ_VLINE, 0, t, 0); if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrDodgerBlue); ObjectSetInteger(m_chartId, name, OBJPROP_STYLE, STYLE_DASH); } break; //--- Text label at the clicked coordinate case TOOL_TEXT: ok = ObjectCreate(m_chartId, name, OBJ_TEXT, sub, t, p); if (ok) { ObjectSetString(m_chartId, name, OBJPROP_TEXT, "Text"); ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrWhite); ObjectSetInteger(m_chartId, name, OBJPROP_FONTSIZE, 10); } break; //--- Arrow up annotation at the clicked coordinate case TOOL_ARROW_UP: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_UP, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrLime); break; //--- Arrow down annotation at the clicked coordinate case TOOL_ARROW_DOWN: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_DOWN, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrRed); break; //--- Thumbs up annotation at the clicked coordinate case TOOL_THUMB_UP: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_THUMB_UP, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrLime); break; //--- Thumbs down annotation at the clicked coordinate case TOOL_THUMB_DOWN: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_THUMB_DOWN, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrRed); break; //--- Left price label annotation at the clicked coordinate case TOOL_PRICE_LABEL: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_LEFT_PRICE, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrDodgerBlue); break; //--- Stop sign annotation at the clicked coordinate case TOOL_STOP_SIGN: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_STOP, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrRed); break; //--- Check mark annotation at the clicked coordinate case TOOL_CHECK_MARK: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_CHECK, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrLime); break; //--- Fibonacci time zones object placed at the clicked bar case TOOL_FIBO_TIMEZONES: ok = ObjectCreate(m_chartId, name, OBJ_FIBOTIMES, sub, t, p, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrGold); break; default: break; } //--- Select the newly created object so the user can adjust it if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_SELECTABLE, true); ObjectSetInteger(m_chartId, name, OBJPROP_SELECTED, true); ChartRedraw(m_chartId); } } //+------------------------------------------------------------------+ //| Create a chart object that requires two clicks to place | //+------------------------------------------------------------------+ void CDrawingEngine::CreateTwoClickObject(int sub, TOOL_TYPE toolType) { string name = MakeUniqueObjectName(); bool ok = false; color objColor = clrDodgerBlue; //--- Retrieve stored first and second anchor points datetime t1 = m_drawPoint1Time, t2 = m_drawPoint2Time; double p1 = m_drawPoint1Price, p2 = m_drawPoint2Price; switch (toolType) { //--- Standard trend line between two points case TOOL_TRENDLINE: ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2); break; //--- Ray line extending to the right case TOOL_RAY: ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_RAY_RIGHT, true); break; //--- Extended (infinite in both directions) line case TOOL_EXTENDED_LINE: ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2); if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_RAY_LEFT, true); ObjectSetInteger(m_chartId, name, OBJPROP_RAY_RIGHT, true); } break; //--- Info/measure line with pip distance label case TOOL_INFO_LINE: ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2); if (ok) { ObjectSetString(m_chartId, name, OBJPROP_TEXT, StringFormat("%.0f pips", MathAbs(p2 - p1) / SymbolInfoDouble(_Symbol, SYMBOL_POINT) / 10.0)); objColor = clrMediumSlateBlue; } break; //--- Filled rectangle between two diagonal corners case TOOL_RECTANGLE: ok = ObjectCreate(m_chartId, name, OBJ_RECTANGLE, sub, t1, p1, t2, p2); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_FILL, true); break; //--- Triangle with auto-computed third vertex case TOOL_TRIANGLE: ok = ObjectCreate(m_chartId, name, OBJ_TRIANGLE, sub, t1, p1, t2, p2, t1 + (t2 - t1) / 2, p1 - MathAbs(p2 - p1)); if (ok) objColor = clrMediumSlateBlue; break; //--- Ellipse fitted to two diagonal corners case TOOL_ELLIPSE: ok = ObjectCreate(m_chartId, name, OBJ_ELLIPSE, sub, t1, p1, t2, p2, t1, p1 + (p2 - p1) / 2); if (ok) objColor = clrMediumOrchid; break; //--- Fibonacci retracement levels case TOOL_FIBO_RETRACEMENT: ok = ObjectCreate(m_chartId, name, OBJ_FIBO, sub, t1, p1, t2, p2); if (ok) objColor = clrGold; break; //--- Fibonacci expansion levels case TOOL_FIBO_EXPANSION: ok = ObjectCreate(m_chartId, name, OBJ_EXPANSION, sub, t1, p1, t2, p2); if (ok) objColor = clrGold; break; //--- Fibonacci fan lines case TOOL_FIBO_FAN: ok = ObjectCreate(m_chartId, name, OBJ_FIBOFAN, sub, t1, p1, t2, p2); if (ok) objColor = clrGold; break; //--- Fibonacci arcs case TOOL_FIBO_ARCS: ok = ObjectCreate(m_chartId, name, OBJ_FIBOARC, sub, t1, p1, t2, p2); if (ok) objColor = clrGold; break; //--- Gann line case TOOL_GANN_LINE: ok = ObjectCreate(m_chartId, name, OBJ_GANNLINE, sub, t1, p1, t2, p2); if (ok) objColor = clrOrangeRed; break; //--- Gann fan lines from anchor case TOOL_GANN_FAN: ok = ObjectCreate(m_chartId, name, OBJ_GANNFAN, sub, t1, p1, t2, p2); if (ok) objColor = clrOrangeRed; break; //--- Gann grid case TOOL_GANN_GRID: ok = ObjectCreate(m_chartId, name, OBJ_GANNGRID, sub, t1, p1, t2, p2); if (ok) objColor = clrOrangeRed; break; //--- Linear regression channel case TOOL_REGRESSION_CHANNEL: ok = ObjectCreate(m_chartId, name, OBJ_REGRESSION, sub, t1, p1, t2, p2); if (ok) objColor = clrCornflowerBlue; break; //--- Standard deviation channel case TOOL_STDDEV_CHANNEL: ok = ObjectCreate(m_chartId, name, OBJ_STDDEVCHANNEL, sub, t1, p1, t2, p2); if (ok) objColor = clrCornflowerBlue; break; default: break; } //--- Apply common properties and select the new object if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, objColor); ObjectSetInteger(m_chartId, name, OBJPROP_WIDTH, 1); ObjectSetInteger(m_chartId, name, OBJPROP_SELECTABLE, true); ObjectSetInteger(m_chartId, name, OBJPROP_SELECTED, true); ChartRedraw(m_chartId); } } //+------------------------------------------------------------------+ //| Create a chart object that requires three clicks to place | //+------------------------------------------------------------------+ void CDrawingEngine::CreateThreeClickObject(int sub, datetime t3, double p3, TOOL_TYPE toolType) { string name = MakeUniqueObjectName(); bool ok = false; color objColor = clrDodgerBlue; //--- Retrieve stored first and second anchor points datetime t1 = m_drawPoint1Time, t2 = m_drawPoint2Time; double p1 = m_drawPoint1Price, p2 = m_drawPoint2Price; switch (toolType) { //--- Parallel channel defined by three anchor points case TOOL_PARALLEL_CHANNEL: ok = ObjectCreate(m_chartId, name, OBJ_CHANNEL, sub, t1, p1, t2, p2, t3, p3); if (ok) objColor = clrCornflowerBlue; break; //--- Fibonacci channel defined by three anchor points case TOOL_FIBO_CHANNEL: ok = ObjectCreate(m_chartId, name, OBJ_FIBOCHANNEL, sub, t1, p1, t2, p2, t3, p3); if (ok) objColor = clrGold; break; //--- Andrew's, Schiff, and Modified Schiff pitchforks share the same object type case TOOL_PITCHFORK: case TOOL_SCHIFF_PITCHFORK: case TOOL_MOD_SCHIFF: ok = ObjectCreate(m_chartId, name, OBJ_PITCHFORK, sub, t1, p1, t2, p2, t3, p3); if (ok) objColor = clrMediumSeaGreen; break; default: break; } //--- Apply common properties and select the new object if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, objColor); ObjectSetInteger(m_chartId, name, OBJPROP_WIDTH, 1); ObjectSetInteger(m_chartId, name, OBJPROP_SELECTABLE, true); ObjectSetInteger(m_chartId, name, OBJPROP_SELECTED, true); ChartRedraw(m_chartId); } } //+------------------------------------------------------------------+ //| CLASS 11 — Top-level indicator class (entry point) | //+------------------------------------------------------------------+ class CToolsSidebar : public CDrawingEngine { private: TOOL_TYPE m_currentActiveTool; // Currently active drawing or cursor tool string m_currentInstruction; // On-screen instruction text for multi-click tools public: CToolsSidebar() { InitDefaults(); } // Initialise all members to safe defaults on construction ~CToolsSidebar() { Destroy(); } // Clean up all canvases and chart objects on destruction //--- Initialise all canvases and register chart event hooks bool Init(long chartId); //--- Destroy all canvases, chart objects, and restore chart state void Destroy(); //--- Dispatch all incoming chart events void OnEvent(const int id, const long &lp, const double &dp, const string &sp); private: //--- Set all member variables to their default starting values void InitDefaults(); //--- Toggle the given tool on or off as the active tool void ToggleTool(TOOL_TYPE toolType); //--- Deactivate the current tool and redraw void DeactivateCurrentTool(); //--- Clean up crosshair and measure mode when switching away from the crosshair tool void CleanupCrosshairOnToolSwitch(); //--- Remove all drawn chart objects created by this indicator void CleanupAllDrawnObjects(); //--- Update all crosshair and measure canvases for the current mouse position void HandleCrosshairMouseMove(int mouseX, int mouseY, bool overSidebar, bool overFlyout); }; //+------------------------------------------------------------------+ //| Set all member variables to their default starting values | //+------------------------------------------------------------------+ void CToolsSidebar::InitDefaults() { //--- Chart and canvas identity m_chartId = 0; m_nameSidebar = "ToolsPalette_Sidebar"; m_nameFlyout = "ToolsPalette_Flyout"; m_nameReticle = "ToolsPalette_Reticle"; m_nameMagnifier = "ToolsPalette_Magnifier"; m_nameCrossVertical = "ToolsPalette_CrosshairVertical"; m_nameCrossHorizontal = "ToolsPalette_CrosshairHorizontal"; m_nameCrossPriceLabel = "ToolsPalette_CrosshairPriceLabel"; m_nameCrossTimeLabel = "ToolsPalette_CrosshairTimeLabel"; m_nameMeasureVertical = "ToolsPalette_MeasureVertical"; m_nameMeasureHorizontal = "ToolsPalette_MeasureHorizontal"; m_nameMeasurePriceLabel = "ToolsPalette_MeasurePriceLabel"; m_nameMeasureTimeLabel = "ToolsPalette_MeasureTimeLabel"; m_nameMeasureDiagonalLine = "ToolsPalette_MeasureDiagonalLine"; //--- Rendering parameters m_supersampleFactor = 4; // 4× supersampling multiplier m_categoryButtonSize = 36; // Category button square size in pixels m_categoryButtonPadding = 6; // Gap between category buttons in pixels m_panelCornerRadius = 10; // Sidebar panel corner radius in pixels m_headerGripHeight = 92; // Header strip height including close/grip/theme rows m_sidebarWidth = 48; // Fixed sidebar width in pixels m_sidebarHeight = 0; // Computed on first CalcSidebarHeight call m_sidebarMaxVisibleCats = 0; m_sidebarScrollPixels = 0; m_sidebarScrollThumbHeight = 30; m_sidebarScrollThinWidth = 3; m_isSidebarThumbDragging = false; m_sidebarThumbDragStartY = 0; m_sidebarThumbDragStartPixels = 0; m_isHoveredSidebarScrollArea = false; m_isHoveredSidebarThumb = false; //--- Panel position and drag state m_panelX = 0; m_panelY = CanvasY; m_snapState = SNAP_LEFT; m_isPanelDragging = false; m_dragOffsetX = 0; m_dragOffsetY = 0; m_snappedSidebarHeight = 0; m_isResizingBottomEdge = false; m_bottomResizeDragStartY = 0; m_bottomResizeStartHeight = 0; m_isBottomResizeHovered = false; //--- Sidebar hover state m_hoveredCategory = CAT_NONE; m_isCloseButtonHovered = false; m_isThemeButtonHovered = false; m_isGripAreaHovered = false; //--- Flyout layout and state m_flyoutWidth = 195; m_flyoutItemHeight = 32; m_flyoutPadding = 7; m_flyoutPointerWidth = 10; m_flyoutPointerHeight = 8; m_flyoutPointerLocalY = 40; m_flyoutPointerOnLeft = true; m_isFlyoutVisible = false; m_flyoutActiveCat = CAT_NONE; m_hoveredFlyoutItem = -1; m_flyoutScrollPixels = 0; m_flyoutMaxVisibleItems = 5; m_flyoutScrollThumbHeight = 30; m_isFlyoutThumbDragging = false; m_flyoutThumbDragStartY = 0; m_flyoutThumbDragStartPixels = 0; m_isHoveredFlyoutScrollArea = false; m_isHoveredFlyoutThumb = false; //--- Theme state m_isDarkTheme = StartDark; //--- Crosshair and reticle state m_reticleCanvasSize = 2 * (ReticleOffset + ReticleTickLen / 2) + 6; m_isReticleVisible = false; m_isMagnifierVisible = false; m_isCrossVertVisible = false; m_isCrossHorizVisible = false; m_isCrossPriceLabelVisible = false; m_isCrossTimeLabelVisible = false; //--- Measure mode state m_isMeasureVertVisible = false; m_isMeasureHorizVisible = false; m_isMeasurePriceLabelVisible = false; m_isMeasureTimeLabelVisible = false; m_isMeasureDiagonalVisible = false; m_isMeasuringActive = false; m_measureAnchorTime = 0; m_measureAnchorPrice = 0.0; m_measureAnchorPixelX = 0; m_measureAnchorPixelY = 0; m_lastClickTimeMicros = 0; m_lastMagMouseX = -9999; m_lastMagMouseY = -9999; //--- Mouse and drawing interaction state m_previousMouseButtonState = 0; m_currentActiveTool = TOOL_NONE; m_currentInstruction = ""; m_drawnObjectCounter = 0; m_toolDrawingClickCount = 0; m_drawPoint1Time = 0; m_drawPoint2Time = 0; m_drawPoint1Price = 0.0; m_drawPoint2Price = 0.0; } //+------------------------------------------------------------------+ //| Initialise all canvases and register chart event hooks | //+------------------------------------------------------------------+ bool CToolsSidebar::Init(long chartId) { //--- Reset all members and store the chart ID InitDefaults(); m_chartId = chartId; //--- Position the panel flush with the snap edge m_panelX = (m_snapState == SNAP_RIGHT) ? (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - m_sidebarWidth : 0; //--- Register all categories and tools, apply the starting theme, and compute layout InitAllCategoriesAndTools(); ApplyTheme(); CalcSidebarHeight(); //--- Create all canvas chart objects; abort init on failure if (!CreateAllCanvases(m_sidebarWidth, m_sidebarHeight)) return false; //--- Position and configure the sidebar chart object ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY); ObjectSetInteger(0, m_nameSidebar, OBJPROP_ZORDER, 100); ObjectSetInteger(0, m_nameFlyout, OBJPROP_ZORDER, 200); //--- Perform the initial render pass HideFlyout(); DrawSidebar(m_currentActiveTool); //--- Re-confirm position after the first draw (draw may resize internally) ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY); //--- Enable mouse move and wheel events for this chart ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true); ChartSetInteger(0, CHART_MOUSE_SCROLL, true); return true; } //+------------------------------------------------------------------+ //| Destroy all canvases, chart objects, and restore chart state | //+------------------------------------------------------------------+ void CToolsSidebar::Destroy() { //--- Clean up crosshair and measure resources before destroying canvases CleanupCrosshairOnToolSwitch(); m_currentActiveTool = TOOL_NONE; //--- Destroy all canvas objects and their associated chart labels DestroyAllCanvases(); //--- Remove all drawn chart objects created during the session CleanupAllDrawnObjects(); //--- Restore chart scrolling ChartSetInteger(0, CHART_MOUSE_SCROLL, true); } //+------------------------------------------------------------------+ //| Toggle the given tool on or off as the active tool | //+------------------------------------------------------------------+ void CToolsSidebar::ToggleTool(TOOL_TYPE toolType) { //--- Clean up crosshair state before switching tools CleanupCrosshairOnToolSwitch(); if (toolType == TOOL_POINTER || m_currentActiveTool == toolType) { //--- Deactivate the current tool when toggling pointer or the same tool m_currentActiveTool = TOOL_NONE; m_toolDrawingClickCount = 0; m_currentInstruction = ""; } else { //--- Activate the new tool and set the appropriate instruction text m_currentActiveTool = toolType; m_toolDrawingClickCount = 0; if (toolType == TOOL_CROSSHAIR) { m_currentInstruction = "Move mouse for crosshair. Double-click to measure."; ShowAllCrosshairElements(); } else m_currentInstruction = "Click on chart to place " + GetToolLabel(toolType) + "."; } } //+------------------------------------------------------------------+ //| Deactivate the current tool and redraw | //+------------------------------------------------------------------+ void CToolsSidebar::DeactivateCurrentTool() { CleanupCrosshairOnToolSwitch(); m_currentActiveTool = TOOL_NONE; m_toolDrawingClickCount = 0; m_currentInstruction = ""; DrawSidebar(m_currentActiveTool); ChartRedraw(); } //+------------------------------------------------------------------+ //| Clean up crosshair and measure mode when switching tools | //+------------------------------------------------------------------+ void CToolsSidebar::CleanupCrosshairOnToolSwitch() { //--- Only clean up if the crosshair was the active tool or measure mode is locked if (m_currentActiveTool == TOOL_CROSSHAIR || m_isMeasuringActive) { HideAllCrosshairElements(); if (m_isMeasuringActive) { //--- Release the measure anchor and restore chart scrolling m_isMeasuringActive = false; DeleteAllMeasureObjects(); ChartSetInteger(0, CHART_MOUSE_SCROLL, true); } } } //+------------------------------------------------------------------+ //| Remove all drawn chart objects created by this indicator | //+------------------------------------------------------------------+ void CToolsSidebar::CleanupAllDrawnObjects() { //--- Iterate in reverse to safely delete while counting int total = ObjectsTotal(m_chartId); for (int i = total - 1; i >= 0; i--) { string n = ObjectName(m_chartId, i); //--- Delete any object whose name begins with the drawing prefix if (StringFind(n, "ToolsPalette_Drawing_") == 0) ObjectDelete(m_chartId, n); } //--- Remove the floating measure info label ObjectDelete(m_chartId, "ToolsPalette_MeasureInfoLabel"); } //+------------------------------------------------------------------+ //| Update all crosshair and measure canvases for the cursor pos | //+------------------------------------------------------------------+ void CToolsSidebar::HandleCrosshairMouseMove(int mouseX, int mouseY, bool overSidebar, bool overFlyout) { //--- Skip if the crosshair tool is not active if (m_currentActiveTool != TOOL_CROSSHAIR) return; //--- Hide all crosshair elements when the cursor is over a panel if (overSidebar || overFlyout) { HideAllCrosshairElements(); return; } //--- Show all crosshair elements when the cursor is on the chart ShowAllCrosshairElements(); datetime barTime; double barPrice; int subWindow; if (ChartXYToTimePrice(m_chartId, mouseX, mouseY, subWindow, barTime, barPrice)) { //--- Update all crosshair line and label positions UpdateCrossVerticalPosition(mouseX); UpdateCrossHorizontalPosition(mouseY); UpdateCrosshairAxisLabels(mouseX, mouseY, barTime, barPrice); UpdateReticlePosition(mouseX, mouseY); UpdateMagnifierPosition(mouseX, mouseY, barTime, barPrice); //--- Update measure mode elements if measuring is active if (m_isMeasuringActive) { int fx = 0, fy = 0; if (ChartTimePriceToXY(m_chartId, 0, m_measureAnchorTime, m_measureAnchorPrice, fx, fy)) { ShowMeasureLines(); UpdateMeasureVerticalPosition(fx); UpdateMeasureHorizontalPosition(fy); UpdateMeasureAnchorLabels(); } UpdateMeasureDiagonalLine(mouseX, mouseY); UpdateMeasurementInfoLabel(mouseX, mouseY, barTime, barPrice); } ChartRedraw(); } } //+------------------------------------------------------------------+ //| Dispatch all incoming chart events | //+------------------------------------------------------------------+ void CToolsSidebar::OnEvent(const int id, const long &lp, const double &dp, const string &sp) { //--- Deactivate the active tool on Escape key press if (id == CHARTEVENT_KEYDOWN && lp == 27) { DeactivateCurrentTool(); return; } if (id == CHARTEVENT_MOUSE_MOVE) { int mouseX = (int)lp, mouseY = (int)dp, mouseButtons = (int)sp; int lx, ly, flx, fly; bool overSidebar = HitTestOverSidebar(mouseX, mouseY, lx, ly); bool overFlyout = !overSidebar && HitTestOverFlyout(mouseX, mouseY, flx, fly); //--- Handle theme toggle button click if (mouseButtons == 1 && m_previousMouseButtonState == 0 && overSidebar && m_isThemeButtonHovered) { ToggleTheme(); DrawSidebar(m_currentActiveTool); if (m_isFlyoutVisible) DrawFlyoutForCategory(m_flyoutActiveCat, m_currentActiveTool); //--- Redraw crosshair canvases to match the new theme foreground colour if (m_isCrossVertVisible) { int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); DrawCrossVerticalLinePixels(chartH); } if (m_isCrossHorizVisible) { int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); DrawCrossHorizontalLinePixels(chartW); } if (m_isReticleVisible) DrawReticleTickMarks(); ChartRedraw(); m_previousMouseButtonState = mouseButtons; return; } //--- Handle flyout item selection click if (mouseButtons == 1 && m_previousMouseButtonState == 0 && overFlyout && m_hoveredFlyoutItem >= 0 && m_flyoutActiveCat != CAT_NONE) { int nT = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); if (m_hoveredFlyoutItem < nT) { ToggleTool(m_categories[(int)m_flyoutActiveCat].tools[m_hoveredFlyoutItem].toolType); HideFlyout(); DrawSidebar(m_currentActiveTool); ChartRedraw(); } m_previousMouseButtonState = mouseButtons; return; } //--- Handle single-tool category button click if (mouseButtons == 1 && m_previousMouseButtonState == 0 && overSidebar && m_hoveredCategory != CAT_NONE && !m_isCloseButtonHovered && !m_isThemeButtonHovered && !m_isGripAreaHovered && ArraySize(m_categories[(int)m_hoveredCategory].tools) == 1) { ToggleTool(m_categories[(int)m_hoveredCategory].tools[0].toolType); HideFlyout(); DrawSidebar(m_currentActiveTool); ChartRedraw(); m_previousMouseButtonState = mouseButtons; return; } //--- Handle crosshair double-click to toggle measure mode anchor if (mouseButtons == 1 && m_previousMouseButtonState == 0 && m_currentActiveTool == TOOL_CROSSHAIR && !overSidebar && !overFlyout) { datetime barTime; double barPrice; int subWindow; if (ChartXYToTimePrice(m_chartId, mouseX, mouseY, subWindow, barTime, barPrice)) HandleCrosshairDoubleClick(mouseX, mouseY, barTime, barPrice); m_previousMouseButtonState = mouseButtons; return; } //--- Handle drawing tool placement click on the chart if (mouseButtons == 1 && m_previousMouseButtonState == 0 && m_currentActiveTool != TOOL_NONE && m_currentActiveTool != TOOL_CROSSHAIR && m_currentActiveTool != TOOL_POINTER && !overSidebar && !overFlyout) { HandleDrawingClick(mouseX, mouseY, m_currentActiveTool, m_currentInstruction); DrawSidebar(m_currentActiveTool); m_previousMouseButtonState = mouseButtons; return; } //--- Update crosshair canvas positions for the current cursor location HandleCrosshairMouseMove(mouseX, mouseY, overSidebar, overFlyout); //--- Delegate remaining mouse move processing to the event handler chain RouteChartEvent(id, lp, dp, sp, m_currentActiveTool); //--- Update the sidebar tooltip text based on what the cursor is hovering over string tip = ""; if (overSidebar && m_hoveredCategory != CAT_NONE) tip = m_categories[(int)m_hoveredCategory].categoryLabel; if (overFlyout && m_hoveredFlyoutItem >= 0 && m_flyoutActiveCat != CAT_NONE) { int nT = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); if (m_hoveredFlyoutItem < nT) tip = m_categories[(int)m_flyoutActiveCat].tools[m_hoveredFlyoutItem].tooltipText; } ObjectSetString(0, m_nameSidebar, OBJPROP_TOOLTIP, tip); return; } //--- Delegate all non-mouse-move chart events to the routing chain RouteChartEvent(id, lp, dp, sp, m_currentActiveTool); } //+------------------------------------------------------------------+ //| Global indicator instance | //+------------------------------------------------------------------+ CToolsSidebar g_sidebar; //+------------------------------------------------------------------+ //| Expert initialisation function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialise the sidebar with the current chart ID if (!g_sidebar.Init(ChartID())) return INIT_FAILED; ChartRedraw(); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Destroy all canvases and chart objects on removal g_sidebar.Destroy(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Chart event handler — forward all events to the sidebar | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lp, const double &dp, const string &sp) { g_sidebar.OnEvent(id, lp, dp, sp); } //+------------------------------------------------------------------+ //| Tick handler — no per-tick logic required | //+------------------------------------------------------------------+ void OnTick() {} //+------------------------------------------------------------------+