//+------------------------------------------------------------------+ //| Tools Palette Part 3.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) //+------------------------------------------------------------------+ //| 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); }; //+------------------------------------------------------------------+ //| 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++) { //--- Compute sub-pixel offsets from pixel centre 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; //--- Determine edge vertical extents 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; //--- Compute intersection parameter along the edge double t = (cy - vy[i]) / (vy[ni] - vy[i]); if (t < 0.0 || t > 1.0) continue; //--- Record intersection X coordinate 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++) { //--- Compute scanline centre Y and prepare intersection buffer 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; //--- Determine edge vertical extents 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; //--- Compute intersection parameter along the edge double t = (cy - vy[i]) / (vy[ni] - vy[i]); if (t < 0.0 || t > 1.0) continue; //--- Record intersection X coordinate 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++) { //--- Compute distance from arc centre 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++; } //--- Skip pixel if no sub-samples qualify 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); } //+------------------------------------------------------------------+ //| 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 string m_nameSidebar; // Object name of the sidebar bitmap label string m_nameFlyout; // Object name of the flyout bitmap label protected: //--- Create all canvas objects at the given 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); }; //+------------------------------------------------------------------+ //| Create all canvas objects at the given dimensions | //+------------------------------------------------------------------+ bool CCanvasLayer::CreateAllCanvases(int w, int h) { //--- 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; } return true; } //+------------------------------------------------------------------+ //| Destroy all canvas objects and remove chart objects | //+------------------------------------------------------------------+ void CCanvasLayer::DestroyAllCanvases() { //--- Destroy display canvas and remove its chart object m_canvasSidebar.Destroy(); ObjectDelete(0, m_nameSidebar); //--- Destroy the high-resolution sidebar working canvas m_canvasSidebarHighRes.Destroy(); //--- Destroy flyout canvas and remove its chart object m_canvasFlyout.Destroy(); ObjectDelete(0, m_nameFlyout); //--- Destroy the high-resolution flyout working canvas m_canvasFlyoutHighRes.Destroy(); } //+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+ //| 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; //--- Clear snapped height override when transitioning from float 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) { int trackH = visibleTools * m_flyoutItemHeight; m_flyoutScrollThumbHeight = MathMax(20, (int)(trackH * (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); //--- 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)); //--- 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 — Draw and composite all sidebar visual elements | //+------------------------------------------------------------------+ class CSidebarRenderer : public CFlyoutPanel { protected: ENUM_CATEGORY m_hoveredCategory; // Currently hovered category button, or CAT_NONE bool m_isCloseButtonHovered; // Flag indicating the close button is hovered bool m_isThemeButtonHovered; // Flag indicating the theme toggle button is hovered bool m_isGripAreaHovered; // Flag indicating the drag-grip area is hovered protected: //--- Draw and composite the full sidebar onto its canvas void DrawSidebar(TOOL_TYPE activeTool); //--- Draw the header grip strip at high resolution void DrawHeaderStripHR(int canvasW, int canvasH); //--- Draw a single category button with active, hover, and dot states at high resolution void DrawCategoryButtonHR(CCanvas &target, int xHR, int yHR, int sizeHR, bool isActive, bool isHovered, bool hasDot); //--- Draw icon glyphs and separator lines onto the display canvas void DrawSidebarIconLabels(TOOL_TYPE activeTool); //--- Draw the sidebar scroll thumb pill overlay onto the display canvas void DrawSidebarScrollPillOverlay(); }; //+------------------------------------------------------------------+ //| Draw and composite the full sidebar onto its canvas | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawSidebar(TOOL_TYPE activeTool) { //--- Compute scroll thumb height if sidebar is scrollable if (CalcSidebarMaxScrollPixels() > 0) { int trackH = CalcSidebarViewportPixels(); m_sidebarScrollThumbHeight = MathMax(20, (int)(trackH * (double)trackH / CalcSidebarTotalScrollPixels())); } //--- Compute high-resolution canvas dimensions int ws = m_sidebarWidth * m_supersampleFactor, hs = m_sidebarHeight * m_supersampleFactor; //--- Resize high-res canvas if 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); //--- Compute background alpha from opacity input uchar bgA = (uchar)(255 * BackgroundOpacity); //--- Determine which corners are rounded based on snap state bool rTL = (m_snapState != SNAP_LEFT), rBL = rTL, rTR = (m_snapState != SNAP_RIGHT), rBR = rTR; //--- Fill sidebar background with selective rounded corners FillSelectiveRoundRectHR(m_canvasSidebarHighRes, 0, 0, ws, hs, m_panelCornerRadius * m_supersampleFactor, ColorToARGB(m_themeColors.sidebarBackground, bgA), rTL, rTR, rBL, rBR); //--- Draw left snap flush edge using foreground chart colour if (m_snapState == SNAP_LEFT) m_canvasSidebarHighRes.FillRectangle(0, 0, m_supersampleFactor - 1, hs - 1, ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255)); //--- Draw right snap flush edge using foreground chart colour 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 grip strip onto the high-res canvas DrawHeaderStripHR(ws, hs); //--- Determine the category of the currently active tool ENUM_CATEGORY activeCat = GetCategoryForActiveTool(activeTool); //--- Use clipped temp canvas when sidebar is scrollable to avoid overdraw outside clip region if (m_sidebarMaxVisibleCats < CAT_COUNT) { //--- Create a temporary high-res canvas for clipped button drawing CCanvas tmpHR; tmpHR.Create("SB_TmpHR", ws, hs, COLOR_FORMAT_ARGB_NORMALIZE); tmpHR.Erase(0x00000000); //--- Draw each visible category button onto the temporary 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); } //--- Blend only the clipped region from the temp canvas onto the main high-res canvas 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); } //--- Destroy the temporary canvas tmpHR.Destroy(); } else { //--- Draw all category buttons directly onto the high-res canvas when no scrolling is needed 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 at high resolution if enabled 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 high-res canvas into the display-resolution canvas DownsampleCanvas(m_canvasSidebar, m_canvasSidebarHighRes, m_supersampleFactor); //--- Overlay icon glyphs and separator lines onto the display canvas DrawSidebarIconLabels(activeTool); //--- Overlay the scroll thumb pill if hover or drag is active DrawSidebarScrollPillOverlay(); //--- Flush the display canvas to the chart m_canvasSidebar.Update(); } //+------------------------------------------------------------------+ //| Draw the header grip strip at high resolution | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawHeaderStripHR(int canvasW, int canvasH) { //--- Compute scaled header height and border inset int headerH = m_headerGripHeight * m_supersampleFactor, brd = BorderWidth * m_supersampleFactor; //--- Compute horizontal insets based on snap state int inL = (m_snapState == SNAP_LEFT) ? 0 : brd, inR = (m_snapState == SNAP_RIGHT) ? 0 : brd; //--- Compute header fill rectangle position and size int hx = inL, hy = brd, hw = canvasW - inL - inR, hh = headerH - brd; //--- Compute inner corner radius inset from border int innerR = MathMax(0, m_panelCornerRadius * m_supersampleFactor - brd); //--- Determine top corner rounding based on snap state bool rTL = (m_snapState != SNAP_LEFT), rTR = (m_snapState != SNAP_RIGHT); //--- Select header fill colour based on active theme color hdrFill = m_isDarkTheme ? C'25,29,40' : C'245,247,252'; //--- Fill the upper portion of the header strip with rounded top corners FillSelectiveRoundRectHR(m_canvasSidebarHighRes, hx, hy, hw, hh, innerR, ColorToARGB(hdrFill, 255), rTL, rTR, false, false); //--- Fill the lower half of the header to square off the bottom m_canvasSidebarHighRes.FillRectangle(hx, hy + hh / 2, hx + hw - 1, headerH - 1, ColorToARGB(hdrFill, 255)); //--- Draw close button hover highlight 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 dot row position and height at high resolution int row2Y = m_categoryButtonSize * m_supersampleFactor, row2H = 20 * m_supersampleFactor; //--- Draw grip area hover highlight if hovered if (m_isGripAreaHovered) m_canvasSidebarHighRes.FillRectangle(inL, row2Y, canvasW - inR - 1, row2Y + row2H - 1, ColorToARGB(C'25,130,80', 255)); //--- Compute theme toggle row position and height at high resolution int row3Y = (m_categoryButtonSize + 20) * m_supersampleFactor, row3H = (m_headerGripHeight - m_categoryButtonSize - 20) * m_supersampleFactor; //--- Draw theme button hover highlight 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); //--- Compute dot spacing and radius at high resolution int gapX = 6 * m_supersampleFactor, dotR = 2 * m_supersampleFactor; //--- Draw three horizontally centred grip dots for (int col = 0; col < 3; col++) m_canvasSidebarHighRes.FillCircle(canvasW / 2 + (col - 1) * gapX, row2Y + row2H / 2, dotR, dotColor); } //+------------------------------------------------------------------+ //| Draw a category button with active, hover, and dot states at HR | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawCategoryButtonHR(CCanvas &target, int xHR, int yHR, int sizeHR, bool isActive, bool isHovered, bool hasDot) { //--- Compute corner radius at high resolution int cornerHR = 6 * m_supersampleFactor; //--- Draw active state: filled background and accent bar indicator if (isActive) { //--- Fill button background with active colour FillRoundRectHR(target, xHR, yHR, sizeHR, sizeHR, cornerHR, ColorToARGB(m_themeColors.buttonActiveBackground, 255)); //--- Compute accent bar dimensions and position int barW = 3 * m_supersampleFactor, barH = sizeHR / 2; //--- Place accent bar on the outer edge away from the snap direction int barX = (m_snapState == SNAP_RIGHT) ? xHR + sizeHR + m_supersampleFactor : xHR - barW - m_supersampleFactor; //--- Draw the accent bar as a small rounded rect FillRoundRectHR(target, barX, yHR + sizeHR / 4, barW, barH, m_supersampleFactor, ColorToARGB(m_themeColors.accentBarColor, 255)); } //--- Draw hover state: filled background without accent bar else if (isHovered) FillRoundRectHR(target, xHR, yHR, sizeHR, sizeHR, cornerHR, ColorToARGB(m_themeColors.buttonHoverBackground, 255)); //--- Draw the multi-tool indicator dot in the bottom-right corner if applicable 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)); } //+------------------------------------------------------------------+ //| Draw icon glyphs and separator lines onto the display canvas | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawSidebarIconLabels(TOOL_TYPE activeTool) { //--- Determine the active category for icon colour selection ENUM_CATEGORY activeCat = GetCategoryForActiveTool(activeTool); int clipTop = CalcClipTop(), clipBot = CalcClipBottom(); //--- Create a temporary display-resolution canvas for icon blitting CCanvas tmpIcons; tmpIcons.Create("SB_TmpIcons", m_sidebarWidth, m_sidebarHeight, COLOR_FORMAT_ARGB_NORMALIZE); tmpIcons.Erase(0x00000000); //--- Compute the blit region: full height when no scroll, clip region otherwise int blitY0 = (m_sidebarMaxVisibleCats < CAT_COUNT) ? clipTop : 0; int blitY1 = (m_sidebarMaxVisibleCats < CAT_COUNT) ? clipBot : m_sidebarHeight; //--- Copy the existing sidebar pixels into the temp canvas within 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 each visible category icon onto the temp canvas 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); //--- Select icon colour based on active state color iconColor = isActive ? m_themeColors.buttonIconActiveColor : m_themeColors.buttonIconColor; //--- Set font and measure icon glyph dimensions for centring 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 centred within the button area tmpIcons.TextOut(btnX + (m_categoryButtonSize - iw) / 2, btnY + (m_categoryButtonSize - ih) / 2, sym, ColorToARGB(iconColor, 255)); } //--- Copy the updated icon pixels back to the sidebar 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)); //--- Destroy the temporary icon canvas tmpIcons.Destroy(); //--- Compute separator line left and right extents based on snap and border int brd = BorderWidth; int sepL = (m_snapState == SNAP_LEFT) ? 0 : brd, sepR = m_sidebarWidth - 1 - ((m_snapState == SNAP_RIGHT) ? 0 : brd); //--- Pack separator line colours for primary and secondary lines uint sepCol = ColorToARGB(m_themeColors.separatorColor, 255); uint sepCol2 = ColorToARGB(m_isDarkTheme ? C'45,52,66' : C'195,202,215', 255); //--- Draw primary separator below the header strip m_canvasSidebar.Line(sepL, m_headerGripHeight - 1, sepR, m_headerGripHeight - 1, sepCol); //--- Draw first secondary separator below the header row m_canvasSidebar.Line(sepL, m_categoryButtonSize, sepR, m_categoryButtonSize, sepCol2); //--- Draw second secondary separator below the theme toggle row m_canvasSidebar.Line(sepL, m_categoryButtonSize + 20, sepR, m_categoryButtonSize + 20, sepCol2); //--- Select close icon colour based on hover state color closeIconColor = m_isCloseButtonHovered ? clrWhite : m_themeColors.buttonIconColor; //--- Set font and measure close icon dimensions for centring m_canvasSidebar.FontSet("Webdings", CategoryIconSize); string closeSym = CharToString((uchar)114); // Webdings char 114 = close/X glyph int clW = m_canvasSidebar.TextWidth(closeSym), clH = m_canvasSidebar.TextHeight(closeSym); //--- Draw the close icon centred in the top header button slot m_canvasSidebar.TextOut((m_sidebarWidth - clW) / 2, (m_categoryButtonSize - clH) / 2, closeSym, ColorToARGB(closeIconColor, 255)); //--- Compute theme toggle row extents int row3Y = m_categoryButtonSize + 20, row3H = m_headerGripHeight - m_categoryButtonSize - 20; //--- Select theme icon colour based on hover state color themeIconColor = m_isThemeButtonHovered ? clrWhite : m_themeColors.buttonIconColor; //--- Set font and measure theme icon dimensions for centring m_canvasSidebar.FontSet("Wingdings", CategoryIconSize); string themeSym = CharToString((uchar)91); // Wingdings char 91 = sun/moon glyph int thW = m_canvasSidebar.TextWidth(themeSym), thH = m_canvasSidebar.TextHeight(themeSym); //--- Draw the theme toggle icon centred in its row slot m_canvasSidebar.TextOut((m_sidebarWidth - thW) / 2, row3Y + (row3H - thH) / 2, themeSym, ColorToARGB(themeIconColor, 255)); //--- Draw the bottom resize grip highlight strip if hovered or being dragged if (m_isBottomResizeHovered || m_isResizingBottomEdge) { int stripH = 3, gripY = m_sidebarHeight - stripH - 1; //--- Select bar colour: accent when dragging, hover colour otherwise color barC = m_isResizingBottomEdge ? m_themeColors.accentBarColor : m_themeColors.scrollArrowHoverColor; m_canvasSidebar.FillRectangle(8, gripY, m_sidebarWidth - 9, gripY + stripH - 1, ColorToARGB(barC, 210)); } } //+------------------------------------------------------------------+ //| Draw the sidebar scroll thumb pill overlay onto display canvas | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawSidebarScrollPillOverlay() { //--- Skip drawing if scrolling is not needed or neither hovered nor dragging if (CalcSidebarMaxScrollPixels() <= 0 || (!m_isHoveredSidebarScrollArea && !m_isSidebarThumbDragging)) return; //--- Compute scroll track geometry int trackY = CalcClipTop(), trackH = CalcSidebarViewportPixels(); m_sidebarScrollThumbHeight = MathMax(20, (int)(trackH * (double)trackH / CalcSidebarTotalScrollPixels())); int maxPx = CalcSidebarMaxScrollPixels(); //--- Compute the thumb Y position from the current scroll fraction double pos = (maxPx > 0) ? (double)m_sidebarScrollPixels / maxPx : 0.0; int thumbY = trackY + (int)(pos * (trackH - m_sidebarScrollThumbHeight)); //--- Compute scroll pill X position based on snap state 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 it 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; } } //--- 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_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 — Route and handle all chart interaction events | //+------------------------------------------------------------------+ class CChartEventHandler : public CSidebarRenderer { protected: int m_previousMouseButtonState; // Mouse button state recorded on the previous move event protected: //--- Dispatch an incoming chart event to the appropriate handler void RouteChartEvent(const int id, const long &lp, const double &dp, const string &sp, TOOL_TYPE &activeTool); //--- Handle CHARTEVENT_CHART_CHANGE to reflow layout on resize void OnChartChangeEvent(TOOL_TYPE activeTool); //--- Handle CHARTEVENT_MOUSE_WHEEL to scroll sidebar or flyout void OnMouseWheelEvent(int mouseX, int mouseY, int wheelDelta, TOOL_TYPE activeTool); //--- Handle CHARTEVENT_MOUSE_MOVE to process all mouse interactions void OnMouseMoveEvent(int mouseX, int mouseY, int mouseButtons, TOOL_TYPE &activeTool); //--- Move the panel to follow the mouse during a drag operation void HandlePanelDragMove(int mouseX, int mouseY, TOOL_TYPE activeTool); //--- Finalise a panel drag by snapping to the nearest edge void HandlePanelDragRelease(TOOL_TYPE activeTool); //--- Resize the panel bottom edge as the mouse moves void HandleBottomResizeDrag(int mouseX, int mouseY, TOOL_TYPE activeTool); //--- Scroll the sidebar by moving the scroll thumb void HandleSidebarThumbDrag(int mouseX, int mouseY, TOOL_TYPE activeTool); //--- Finalise a sidebar scroll thumb drag void HandleSidebarThumbRelease(TOOL_TYPE activeTool); //--- Scroll the flyout list by moving the flyout scroll thumb void HandleFlyoutThumbDrag(int mouseX, int mouseY); //--- Finalise a flyout scroll thumb drag void HandleFlyoutThumbRelease(); //--- Recompute all hover state flags and trigger redraws as needed void UpdateAllHoverStates(int mouseX, int mouseY, bool overSidebar, bool overFlyout, int lx, int ly, int flx, int fly, TOOL_TYPE activeTool); //--- Handle a mouse button-down event within 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 an incoming chart event to the appropriate handler | //+------------------------------------------------------------------+ void CChartEventHandler::RouteChartEvent(const int id, const long &lp, const double &dp, const string &sp, TOOL_TYPE &activeTool) { //--- Forward chart change events to the resize/reflow handler if (id == CHARTEVENT_CHART_CHANGE) { OnChartChangeEvent(activeTool); return; } //--- Forward mouse wheel events to the scroll handler if (id == CHARTEVENT_MOUSE_WHEEL) { OnMouseWheelEvent((int)(short)lp, (int)(short)(lp >> 16), (int)dp, activeTool); return; } //--- Forward mouse move events to the full mouse interaction handler if (id == CHARTEVENT_MOUSE_MOVE) OnMouseMoveEvent((int)lp, (int)dp, (int)sp, activeTool); } //+------------------------------------------------------------------+ //| Handle chart change event to reflow layout on resize | //+------------------------------------------------------------------+ void CChartEventHandler::OnChartChangeEvent(TOOL_TYPE activeTool) { //--- Reset all drag and thumb states on chart geometry change m_previousMouseButtonState = 0; m_isPanelDragging = false; m_isResizingBottomEdge = false; m_isSidebarThumbDragging = false; m_isFlyoutThumbDragging = false; //--- Restore chart mouse scroll in case it was locked during a drag ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Reposition and reflow snapped panels only if (m_snapState != SNAP_FLOAT) { //--- Recalculate snapped panel X position from chart width 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 snapped height override within the new chart height if (m_snappedSidebarHeight > 0) m_snappedSidebarHeight = MathMin(m_snappedSidebarHeight, (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - m_panelY - 8); //--- Recompute layout and resize canvases CalcSidebarHeight(); ResizeSidebarCanvases(m_sidebarWidth, m_sidebarHeight); DrawSidebar(activeTool); //--- Reposition the flyout if it is currently visible if (m_isFlyoutVisible) ShowFlyout(m_flyoutActiveCat, activeTool); } ChartRedraw(); } //+------------------------------------------------------------------+ //| Handle mouse wheel event to scroll sidebar or flyout list | //+------------------------------------------------------------------+ 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 when the wheel is over the sidebar and it is scrollable if (overSidebar && m_sidebarMaxVisibleCats < CAT_COUNT) { //--- Lock chart scroll to prevent chart panning while scrolling the sidebar ChartSetInteger(0, CHART_MOUSE_SCROLL, false); int step = MathMax(1, MouseScrollSpeed); //--- Scroll down on negative delta, up on positive m_sidebarScrollPixels = MathMax(0, MathMin(m_sidebarScrollPixels + ((wheelDelta < 0) ? step : -step), CalcSidebarMaxScrollPixels())); HideFlyout(); DrawSidebar(activeTool); ChartRedraw(); return; } //--- Scroll the flyout list when the wheel is over the flyout and it is scrollable if (overFlyout && m_isFlyoutVisible && m_flyoutActiveCat != CAT_NONE) { int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); if (nTools > m_flyoutMaxVisibleItems) { //--- Lock chart scroll to prevent chart panning while scrolling the flyout ChartSetInteger(0, CHART_MOUSE_SCROLL, false); int maxPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight; //--- Scroll flyout list by the configured step amount 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 wheel is not over any panel ChartSetInteger(0, CHART_MOUSE_SCROLL, true); } //+------------------------------------------------------------------+ //| Move the panel to follow the mouse during a drag operation | //+------------------------------------------------------------------+ void CChartEventHandler::HandlePanelDragMove(int mouseX, int mouseY, TOOL_TYPE activeTool) { //--- Clamp panel position within chart bounds int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); m_panelX = MathMax(0, MathMin(chartW - m_sidebarWidth, mouseX - m_dragOffsetX)); m_panelY = MathMax(0, MathMin(chartH - m_sidebarHeight, mouseY - m_dragOffsetY)); //--- Update the chart object position to follow the mouse ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY); //--- Reposition the flyout to follow the sidebar if visible if (m_isFlyoutVisible) ShowFlyout(m_flyoutActiveCat, activeTool); DrawSidebar(activeTool); ChartRedraw(); } //+------------------------------------------------------------------+ //| Finalise panel drag by snapping to the nearest edge | //+------------------------------------------------------------------+ void CChartEventHandler::HandlePanelDragRelease(TOOL_TYPE activeTool) { //--- Clear drag state flag m_isPanelDragging = false; //--- Attempt to snap the panel to a chart edge TrySnapToEdge(); //--- Recompute layout after potential snap state change CalcSidebarHeight(); ResizeSidebarCanvases(m_sidebarWidth, m_sidebarHeight); DrawSidebar(activeTool); ChartRedraw(); } //+------------------------------------------------------------------+ //| Resize the panel bottom edge as the mouse moves | //+------------------------------------------------------------------+ void CChartEventHandler::HandleBottomResizeDrag(int mouseX, int mouseY, TOOL_TYPE activeTool) { //--- Compute vertical mouse delta from drag start int dy = mouseY - m_bottomResizeDragStartY; int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Compute natural height for all buttons and height clamp limits 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 panel height clamped within valid bounds int newH = MathMax(minH, MathMin(maxH, m_bottomResizeStartHeight + dy)); //--- Apply the new height if it has changed if (newH != m_sidebarHeight) { //--- Store as snapped height override for non-floating panels if (m_snapState != SNAP_FLOAT) m_snappedSidebarHeight = newH; else m_sidebarHeight = newH; CalcSidebarHeight(); ResizeSidebarCanvases(m_sidebarWidth, m_sidebarHeight); DrawSidebar(activeTool); ChartRedraw(); } } //+------------------------------------------------------------------+ //| Scroll the sidebar by moving the scroll thumb | //+------------------------------------------------------------------+ void CChartEventHandler::HandleSidebarThumbDrag(int mouseX, int mouseY, TOOL_TYPE activeTool) { //--- Compute the available travel distance for the thumb int trackH = CalcSidebarViewportPixels(), travel = trackH - m_sidebarScrollThumbHeight; if (travel > 0) { //--- Map mouse delta to scroll offset delta int dy = mouseY - m_sidebarThumbDragStartY; int maxPx = CalcSidebarMaxScrollPixels(); int newPx = MathMax(0, MathMin(maxPx, m_sidebarThumbDragStartPixels + (int)MathRound((double)dy / travel * maxPx))); //--- Apply the new scroll offset if it has changed if (newPx != m_sidebarScrollPixels) { m_sidebarScrollPixels = newPx; HideFlyout(); DrawSidebar(activeTool); ChartRedraw(); } } } //+------------------------------------------------------------------+ //| Finalise a sidebar scroll thumb drag | //+------------------------------------------------------------------+ void CChartEventHandler::HandleSidebarThumbRelease(TOOL_TYPE activeTool) { //--- Clear the sidebar thumb dragging flag and redraw m_isSidebarThumbDragging = false; DrawSidebar(activeTool); ChartRedraw(); } //+------------------------------------------------------------------+ //| Scroll the flyout list by moving the flyout scroll thumb | //+------------------------------------------------------------------+ void CChartEventHandler::HandleFlyoutThumbDrag(int mouseX, int mouseY) { //--- Exit if no flyout category is active if (m_flyoutActiveCat == CAT_NONE) return; int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); if (nTools <= m_flyoutMaxVisibleItems) return; //--- Compute the available travel distance for the flyout thumb int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight, travel = trackH - m_flyoutScrollThumbHeight; if (travel > 0) { //--- Map mouse delta to flyout scroll offset delta int dy = mouseY - m_flyoutThumbDragStartY; int maxPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight; int newPx = MathMax(0, MathMin(maxPx, m_flyoutThumbDragStartPixels + (int)MathRound((double)dy / travel * maxPx))); //--- Apply the new flyout scroll offset if it has changed if (newPx != m_flyoutScrollPixels) { m_flyoutScrollPixels = newPx; DrawFlyoutForCategory(m_flyoutActiveCat, TOOL_NONE); ChartRedraw(); } } } //+------------------------------------------------------------------+ //| Finalise a flyout scroll thumb drag | //+------------------------------------------------------------------+ void CChartEventHandler::HandleFlyoutThumbRelease() { //--- Clear the flyout thumb dragging flag and redraw m_isFlyoutThumbDragging = false; DrawFlyoutForCategory(m_flyoutActiveCat, TOOL_NONE); ChartRedraw(); } //+------------------------------------------------------------------+ //| Recompute all hover states and trigger redraws as needed | //+------------------------------------------------------------------+ void CChartEventHandler::UpdateAllHoverStates(int mouseX, int mouseY, bool overSidebar, bool overFlyout, int lx, int ly, int flx, int fly, TOOL_TYPE activeTool) { //--- Snapshot all hover flags before updating 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 states when the mouse 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 scroll thumb hover state if the sidebar is scrollable if (CalcSidebarMaxScrollPixels() > 0) { int trackY = CalcClipTop(), trackH = CalcSidebarViewportPixels(); m_isHoveredSidebarScrollArea = (ly >= trackY && ly <= trackY + trackH); if (m_isHoveredSidebarScrollArea) { //--- Check if the mouse is over the narrow scroll pill column int tw = m_sidebarScrollThinWidth, thinX = (m_snapState == SNAP_RIGHT) ? 2 : m_sidebarWidth - tw - 2; if (lx >= thinX - 4 && lx <= thinX + tw + 4) { //--- Compute thumb Y position from scroll fraction and check hit 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); } } } } //--- Clear hovered category if not over the flyout either else if (!overFlyout) m_hoveredCategory = CAT_NONE; //--- Recompute flyout hover states when the mouse is over the flyout if (overFlyout) { m_hoveredFlyoutItem = HitTestFlyoutItem(flx, fly); if (m_hoveredFlyoutItem < 0) m_hoveredFlyoutItem = -1; //--- Mark flyout scroll area as hovered if it is scrollable 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, dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0; //--- Compute the scroll pill X column based on pointer direction int thinX = m_flyoutPointerOnLeft ? (dispBx + m_flyoutWidth - tw - 2) : (dispBx + 2); if (flx >= thinX - 6 && flx <= thinX + tw + 6 && fly >= itemsTop && fly <= itemsTop + trackH) { //--- Compute flyout thumb Y position from scroll fraction and check hit 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); } } } //--- Clear flyout item hover when not over either panel else if (!overSidebar) { m_hoveredFlyoutItem = -1; m_isHoveredFlyoutScrollArea = false; } //--- Show the flyout when hovering a new category button if (overSidebar && m_hoveredCategory != CAT_NONE && !m_isCloseButtonHovered && !m_isThemeButtonHovered && !m_isGripAreaHovered) { if (m_hoveredCategory != m_flyoutActiveCat) ShowFlyout(m_hoveredCategory, activeTool); } //--- Hide the flyout when the mouse leaves both panels else if (!overFlyout && m_isFlyoutVisible) { //--- Allow brief transition across the gap between sidebar and 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(); } } //--- Trigger a redraw only when any hover state has 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 mouse button-down event within 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 sidebar scroll thumb and track clicks 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)); //--- Begin thumb drag if the click is on the thumb if (ly >= sliderY && ly <= sliderY + m_sidebarScrollThumbHeight) { 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 step when clicking the track int step = m_categoryButtonSize + m_categoryButtonPadding; m_sidebarScrollPixels = MathMax(0, MathMin(maxPx, m_sidebarScrollPixels + ((ly < sliderY) ? -step : step))); HideFlyout(); DrawSidebar(activeTool); ChartRedraw(); } return; } } //--- Handle flyout scroll thumb and track clicks 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, 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)); //--- Begin flyout thumb drag if the click is on the thumb if (fly >= sliderY && fly <= sliderY + m_flyoutScrollThumbHeight) { 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 when clicking the track 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 CHARTEVENT_MOUSE_MOVE to process all mouse interactions | //+------------------------------------------------------------------+ 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); //--- Handle active drag and thumb operations first, before hover or click logic 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; } //--- Recompute all hover states for the current mouse position UpdateAllHoverStates(mouseX, mouseY, overSidebar, overFlyout, lx, ly, flx, fly, activeTool); //--- Manage chart scroll lock: lock when over any panel, unlock otherwise bool overAny = overSidebar || overFlyout; if (!m_isSidebarThumbDragging && !m_isPanelDragging && !m_isResizingBottomEdge && !m_isFlyoutThumbDragging) ChartSetInteger(0, CHART_MOUSE_SCROLL, !overAny); //--- Detect a fresh left-button press and route to the click-down handler if (mouseButtons == 1 && m_previousMouseButtonState == 0) HandleMouseClickDown(mouseX, mouseY, overSidebar, overFlyout, lx, ly, flx, fly, activeTool); //--- Record button state for next event comparison m_previousMouseButtonState = mouseButtons; } //+------------------------------------------------------------------+ //| CLASS 9 — Place chart drawing objects from tool interactions | //+------------------------------------------------------------------+ class CDrawingEngine : public CChartEventHandler { protected: int m_drawnObjectCounter; // Running counter used to generate unique object names int m_toolDrawingClickCount;// Number of chart clicks recorded for the current tool placement datetime m_drawPoint1Time; // Chart time of the first placement click datetime m_drawPoint2Time; // Chart time of the second placement click double m_drawPoint1Price; // Chart price of the first placement click double m_drawPoint2Price; // Chart price of the second placement click protected: //--- Generate a unique name for the next drawing object string MakeUniqueObjectName(); //--- Process a chart click and dispatch to the appropriate object creator void HandleDrawingClick(int mouseX, int mouseY, TOOL_TYPE &activeTool, string &instruction); //--- Create a chart object that requires a single placement click void CreateSingleClickObject(int sub, datetime t, double p, TOOL_TYPE toolType); //--- Create a chart object that requires two placement clicks void CreateTwoClickObject(int sub, TOOL_TYPE toolType); //--- Create a chart object that requires three placement clicks void CreateThreeClickObject(int sub, datetime t3, double p3, TOOL_TYPE toolType); }; //+------------------------------------------------------------------+ //| Generate a unique name for the next drawing object | //+------------------------------------------------------------------+ string CDrawingEngine::MakeUniqueObjectName() { //--- Increment the counter and combine it with the current time for uniqueness m_drawnObjectCounter++; return "ToolsPalette_Drawing_" + IntegerToString(m_drawnObjectCounter) + "_" + IntegerToString((int)TimeCurrent()); } //+------------------------------------------------------------------+ //| Process a chart click and dispatch to the appropriate creator | //+------------------------------------------------------------------+ void CDrawingEngine::HandleDrawingClick(int mouseX, int mouseY, TOOL_TYPE &activeTool, string &instruction) { //--- Convert screen coordinates to chart time and price datetime barTime; double barPrice; int sub; if (!ChartXYToTimePrice(m_chartId, mouseX, mouseY, sub, barTime, barPrice)) return; //--- Exit if the active tool requires no clicks int clicksNeeded = GetRequiredClickCount(activeTool); if (clicksNeeded <= 0) return; //--- Increment the click count for the ongoing placement sequence m_toolDrawingClickCount++; if (m_toolDrawingClickCount == 1) { //--- Record the first anchor point m_drawPoint1Time = barTime; m_drawPoint1Price = barPrice; //--- Create object immediately for single-click tools if (clicksNeeded == 1) { 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 the second anchor point m_drawPoint2Time = barTime; m_drawPoint2Price = barPrice; //--- Create object immediately for two-click tools if (clicksNeeded == 2) { CreateTwoClickObject(sub, activeTool); m_toolDrawingClickCount = 0; activeTool = TOOL_NONE; instruction = ""; } else instruction = "Click third point for " + GetToolLabel(activeTool) + "."; } else if (m_toolDrawingClickCount == 3) { //--- Create object for three-click tools using all three recorded anchor points CreateThreeClickObject(sub, barTime, barPrice, activeTool); m_toolDrawingClickCount = 0; activeTool = TOOL_NONE; instruction = ""; } } //+------------------------------------------------------------------+ //| Create a chart object that requires a single placement click | //+------------------------------------------------------------------+ void CDrawingEngine::CreateSingleClickObject(int sub, datetime t, double p, TOOL_TYPE toolType) { string name = MakeUniqueObjectName(); bool ok = false; switch (toolType) { //--- Create a horizontal line at the clicked price 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; //--- Create a vertical line at the clicked 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; //--- Create a text label at the clicked position 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; //--- Create an arrow-up annotation at the clicked position 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; //--- Create an arrow-down annotation at the clicked position 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; //--- Create a thumbs-up annotation at the clicked position 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; //--- Create a thumbs-down annotation at the clicked position 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; //--- Create a left price label at the clicked position 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; //--- Create a stop sign annotation at the clicked position 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; //--- Create a check mark annotation at the clicked position 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; //--- Create a Fibonacci time zones object at the clicked position 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; } //--- Mark created objects as selectable and trigger a chart redraw 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 placement clicks | //+------------------------------------------------------------------+ void CDrawingEngine::CreateTwoClickObject(int sub, TOOL_TYPE toolType) { string name = MakeUniqueObjectName(); bool ok = false; color objColor = clrDodgerBlue; //--- Retrieve the two recorded anchor points datetime t1 = m_drawPoint1Time, t2 = m_drawPoint2Time; double p1 = m_drawPoint1Price, p2 = m_drawPoint2Price; switch (toolType) { //--- Create a standard trend line between the two anchor points case TOOL_TRENDLINE: ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2); break; //--- Create a ray line starting at the first anchor and extending 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; //--- Create an extended line running through both anchors in both directions 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; //--- Create a measure/info line and annotate it with pip distance 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; //--- Create a filled rectangle between the two anchor points 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; //--- Create a triangle with a 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; //--- Create an ellipse using anchor points and a mid-edge third point 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; //--- Create a Fibonacci retracement between the two anchor points case TOOL_FIBO_RETRACEMENT: ok = ObjectCreate(m_chartId, name, OBJ_FIBO, sub, t1, p1, t2, p2); if (ok) objColor = clrGold; break; //--- Create a Fibonacci expansion between the two anchor points case TOOL_FIBO_EXPANSION: ok = ObjectCreate(m_chartId, name, OBJ_EXPANSION, sub, t1, p1, t2, p2); if (ok) objColor = clrGold; break; //--- Create a Fibonacci fan between the two anchor points case TOOL_FIBO_FAN: ok = ObjectCreate(m_chartId, name, OBJ_FIBOFAN, sub, t1, p1, t2, p2); if (ok) objColor = clrGold; break; //--- Create Fibonacci arcs between the two anchor points case TOOL_FIBO_ARCS: ok = ObjectCreate(m_chartId, name, OBJ_FIBOARC, sub, t1, p1, t2, p2); if (ok) objColor = clrGold; break; //--- Create a Gann line between the two anchor points case TOOL_GANN_LINE: ok = ObjectCreate(m_chartId, name, OBJ_GANNLINE, sub, t1, p1, t2, p2); if (ok) objColor = clrOrangeRed; break; //--- Create a Gann fan from the first anchor point case TOOL_GANN_FAN: ok = ObjectCreate(m_chartId, name, OBJ_GANNFAN, sub, t1, p1, t2, p2); if (ok) objColor = clrOrangeRed; break; //--- Create a Gann grid between the two anchor points case TOOL_GANN_GRID: ok = ObjectCreate(m_chartId, name, OBJ_GANNGRID, sub, t1, p1, t2, p2); if (ok) objColor = clrOrangeRed; break; //--- Create a regression channel between the two anchor points case TOOL_REGRESSION_CHANNEL: ok = ObjectCreate(m_chartId, name, OBJ_REGRESSION, sub, t1, p1, t2, p2); if (ok) objColor = clrCornflowerBlue; break; //--- Create a standard deviation channel between the two anchor points case TOOL_STDDEV_CHANNEL: ok = ObjectCreate(m_chartId, name, OBJ_STDDEVCHANNEL, sub, t1, p1, t2, p2); if (ok) objColor = clrCornflowerBlue; break; default: break; } //--- Apply shared properties and trigger a chart redraw 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 placement clicks | //+------------------------------------------------------------------+ void CDrawingEngine::CreateThreeClickObject(int sub, datetime t3, double p3, TOOL_TYPE toolType) { string name = MakeUniqueObjectName(); bool ok = false; color objColor = clrDodgerBlue; //--- Retrieve the first two recorded anchor points datetime t1 = m_drawPoint1Time, t2 = m_drawPoint2Time; double p1 = m_drawPoint1Price, p2 = m_drawPoint2Price; switch (toolType) { //--- Create a parallel channel using all 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; //--- Create a Fibonacci channel using all 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; //--- Create a pitchfork variant using all three anchor points 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 shared properties and trigger a chart redraw 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 10 — Top-level sidebar shell exposing the public interface | //+------------------------------------------------------------------+ class CToolsSidebar : public CDrawingEngine { private: TOOL_TYPE m_currentActiveTool; // Currently active drawing tool, or TOOL_NONE string m_currentInstruction; // Instruction text shown during multi-click tool placement public: CToolsSidebar() { InitDefaults(); } // Construct and apply default state ~CToolsSidebar() { Destroy(); } // Destruct and clean up all resources //--- Initialise the sidebar and build all canvas objects bool Init(long chartId); //--- Destroy all canvas objects and release resources void Destroy(); //--- Handle all incoming chart events for the sidebar void OnEvent(const int id, const long &lp, const double &dp, const string &sp); private: //--- Set all member variables to their compile-time default values void InitDefaults(); //--- Toggle the given tool on or off as the active drawing tool void ToggleTool(TOOL_TYPE toolType); //--- Deactivate the current tool and reset placement state void DeactivateCurrentTool(); //--- Remove all drawing objects placed by this indicator instance void CleanupAllDrawnObjects(); }; //+------------------------------------------------------------------+ //| Set all member variables to their compile-time default values | //+------------------------------------------------------------------+ void CToolsSidebar::InitDefaults() { //--- Reset chart reference to default m_chartId = 0; //--- Set the sidebar bitmap label object name m_nameSidebar = "ToolsPalette_Sidebar"; //--- Set the flyout bitmap label object name m_nameFlyout = "ToolsPalette_Flyout"; //--- Set supersampling factor for high-res rendering m_supersampleFactor = 4; //--- Set default button size and spacing m_categoryButtonSize = 36; m_categoryButtonPadding = 6; //--- Set panel corner rounding radius m_panelCornerRadius = 10; //--- Set header and grip strip height m_headerGripHeight = 92; //--- Set sidebar panel width m_sidebarWidth = 48; //--- Reset computed height and visible category count m_sidebarHeight = 0; m_sidebarMaxVisibleCats = 0; //--- Reset scroll state m_sidebarScrollPixels = 0; m_sidebarScrollThumbHeight = 30; m_sidebarScrollThinWidth = 3; m_isSidebarThumbDragging = false; m_sidebarThumbDragStartY = 0; m_sidebarThumbDragStartPixels = 0; m_isHoveredSidebarScrollArea = false; m_isHoveredSidebarThumb = false; //--- Reset panel position to origin m_panelX = 0; m_panelY = CanvasY; //--- Default snap state to left edge m_snapState = SNAP_LEFT; //--- Reset panel drag state m_isPanelDragging = false; m_dragOffsetX = 0; m_dragOffsetY = 0; //--- Reset snapped height override and resize state m_snappedSidebarHeight = 0; m_isResizingBottomEdge = false; m_bottomResizeDragStartY = 0; m_bottomResizeStartHeight = 0; m_isBottomResizeHovered = false; //--- Reset all header button hover flags m_hoveredCategory = CAT_NONE; m_isCloseButtonHovered = false; m_isThemeButtonHovered = false; m_isGripAreaHovered = false; //--- Set flyout layout defaults m_flyoutWidth = 195; m_flyoutItemHeight = 32; m_flyoutPadding = 7; m_flyoutPointerWidth = 10; m_flyoutPointerHeight = 8; m_flyoutPointerLocalY = 40; m_flyoutPointerOnLeft = true; //--- Reset flyout visibility and category state m_isFlyoutVisible = false; m_flyoutActiveCat = CAT_NONE; m_hoveredFlyoutItem = -1; //--- Reset flyout scroll state m_flyoutScrollPixels = 0; m_flyoutMaxVisibleItems = 5; m_flyoutScrollThumbHeight = 30; m_isFlyoutThumbDragging = false; m_flyoutThumbDragStartY = 0; m_flyoutThumbDragStartPixels = 0; m_isHoveredFlyoutScrollArea = false; m_isHoveredFlyoutThumb = false; //--- Apply the starting theme from user input m_isDarkTheme = StartDark; //--- Reset mouse button tracking m_previousMouseButtonState = 0; //--- Reset active tool and instruction text m_currentActiveTool = TOOL_NONE; m_currentInstruction = ""; //--- Reset drawing object placement state m_drawnObjectCounter = 0; m_toolDrawingClickCount = 0; m_drawPoint1Time = 0; m_drawPoint2Time = 0; m_drawPoint1Price = 0.0; m_drawPoint2Price = 0.0; } //+------------------------------------------------------------------+ //| Initialise the sidebar and build all canvas objects | //+------------------------------------------------------------------+ bool CToolsSidebar::Init(long chartId) { //--- Reset all members to defaults before initialising InitDefaults(); //--- Store the target chart identifier m_chartId = chartId; //--- Set initial panel X position based on default snap state m_panelX = (m_snapState == SNAP_RIGHT) ? (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - m_sidebarWidth : 0; //--- Populate all category and tool definitions InitAllCategoriesAndTools(); //--- Apply the active theme colour set ApplyTheme(); //--- Compute and set the sidebar panel height CalcSidebarHeight(); //--- Create all canvas layers; abort on failure if (!CreateAllCanvases(m_sidebarWidth, m_sidebarHeight)) return false; //--- Position 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); //--- Set the flyout Z-order above the sidebar ObjectSetInteger(0, m_nameFlyout, OBJPROP_ZORDER, 200); //--- Hide the flyout and draw the initial sidebar frame HideFlyout(); DrawSidebar(m_currentActiveTool); //--- Reapply position in case the draw call shifted the object ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY); //--- Enable mouse move and wheel events ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true); ChartSetInteger(0, CHART_MOUSE_SCROLL, true); return true; } //+------------------------------------------------------------------+ //| Destroy all canvas objects and release resources | //+------------------------------------------------------------------+ void CToolsSidebar::Destroy() { //--- Deactivate the active tool before destroying m_currentActiveTool = TOOL_NONE; //--- Delegate canvas cleanup to the layer base class DestroyAllCanvases(); //--- Remove all chart drawing objects placed by this instance CleanupAllDrawnObjects(); //--- Restore chart mouse scroll to the default state ChartSetInteger(0, CHART_MOUSE_SCROLL, true); } //+------------------------------------------------------------------+ //| Toggle the given tool on or off as the active drawing tool | //+------------------------------------------------------------------+ void CToolsSidebar::ToggleTool(TOOL_TYPE toolType) { //--- Deactivate if the pointer tool is selected or the tool is already active if (toolType == TOOL_POINTER || m_currentActiveTool == toolType) { m_currentActiveTool = TOOL_NONE; m_toolDrawingClickCount = 0; m_currentInstruction = ""; } else { //--- Activate the new tool and reset its click counter and instruction m_currentActiveTool = toolType; m_toolDrawingClickCount = 0; m_currentInstruction = "Click on chart to place " + GetToolLabel(toolType) + "."; } } //+------------------------------------------------------------------+ //| Deactivate the current tool and reset placement state | //+------------------------------------------------------------------+ void CToolsSidebar::DeactivateCurrentTool() { //--- Clear tool state and instruction text m_currentActiveTool = TOOL_NONE; m_toolDrawingClickCount = 0; m_currentInstruction = ""; //--- Redraw the sidebar to reflect the deactivated state DrawSidebar(m_currentActiveTool); ChartRedraw(); } //+------------------------------------------------------------------+ //| Remove all drawing objects placed by this indicator instance | //+------------------------------------------------------------------+ void CToolsSidebar::CleanupAllDrawnObjects() { //--- Iterate backwards through all chart objects to safely delete matches int total = ObjectsTotal(m_chartId); for (int i = total - 1; i >= 0; i--) { string n = ObjectName(m_chartId, i); //--- Delete objects whose names match the drawing prefix if (StringFind(n, "ToolsPalette_Drawing_") == 0) ObjectDelete(m_chartId, n); } } //+------------------------------------------------------------------+ //| Handle all incoming chart events for the sidebar | //+------------------------------------------------------------------+ void CToolsSidebar::OnEvent(const int id, const long &lp, const double &dp, const string &sp) { //--- Deactivate the active tool when Escape is pressed 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 on fresh left-click over the theme button if (mouseButtons == 1 && m_previousMouseButtonState == 0 && overSidebar && m_isThemeButtonHovered) { ToggleTheme(); DrawSidebar(m_currentActiveTool); if (m_isFlyoutVisible) DrawFlyoutForCategory(m_flyoutActiveCat, m_currentActiveTool); ChartRedraw(); m_previousMouseButtonState = mouseButtons; return; } //--- Handle tool selection from flyout on fresh left-click over a flyout item 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) { //--- Toggle the selected tool and redraw ToggleTool(m_categories[(int)m_flyoutActiveCat].tools[m_hoveredFlyoutItem].toolType); HideFlyout(); DrawSidebar(m_currentActiveTool); ChartRedraw(); } m_previousMouseButtonState = mouseButtons; return; } //--- Handle direct tool selection for single-tool categories on fresh left-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 chart placement clicks when a drawing tool is active if (mouseButtons == 1 && m_previousMouseButtonState == 0 && m_currentActiveTool != TOOL_NONE && m_currentActiveTool != TOOL_POINTER && !overSidebar && !overFlyout) { HandleDrawingClick(mouseX, mouseY, m_currentActiveTool, m_currentInstruction); DrawSidebar(m_currentActiveTool); m_previousMouseButtonState = mouseButtons; return; } //--- Forward remaining mouse move logic to the chart event handler RouteChartEvent(id, lp, dp, sp, m_currentActiveTool); //--- Update the sidebar tooltip based on current hover state 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; } //--- Forward all other chart events to the routing handler RouteChartEvent(id, lp, dp, sp, m_currentActiveTool); } //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ CToolsSidebar g_sidebar; // Global sidebar instance //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //--- Initialise the sidebar and fail if canvas creation fails if (!g_sidebar.Init(ChartID())) return INIT_FAILED; //--- Force a chart redraw to display the sidebar immediately ChartRedraw(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- //--- Destroy the sidebar and remove all canvas objects g_sidebar.Destroy(); //--- Force a chart redraw to clear the sidebar from view ChartRedraw(); } //+------------------------------------------------------------------+ //| Expert chart event function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lp, const double &dp, const string &sp) { //--- //--- Forward the chart event to the sidebar event handler g_sidebar.OnEvent(id, lp, dp, sp); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+