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