Warrior_EA/System/StatusLabel.mqh

203 lines
9.9 KiB
MQL5

//+------------------------------------------------------------------+
//| StatusLabel.mqh |
//| AnimateDread |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "AnimateDread"
#property link "https://www.mql5.com"
//+------------------------------------------------------------------+
//| Replaces Comment() for on-chart status text. Comment() draws |
//| directly onto the chart surface with no background, so status text |
//| becomes unreadable over candles. OBJ_LABEL also does not render |
//| embedded '\n' as line breaks (it printed as one truncated line), |
//| so each line of the status text gets its own OBJ_LABEL, and each |
//| label gets its own OBJ_RECTANGLE_LABEL background sized to that |
//| line's actual rendered pixel width via TextGetSize() - not a fixed |
//| per-character guess, which was oversized on this system's DPI/font |
//| metrics and left one big box behind the whole block. Some source |
//| lines are wider than the chart pane itself, which clipped the |
//| OBJ_LABEL text at the chart edge while its background - sized off |
//| the full, unclipped string - kept extending to the true width; |
//| word-wrapping every line to the chart's own pixel width keeps what |
//| is drawn and what is measured the same string. Separately, |
//| ObjectSetString(OBJPROP_TEXT) itself silently truncates a label's |
//| text at ~63 characters regardless of chart width or the pixel wrap |
//| above - confirmed by several status lines all cutting off almost |
//| exactly at 63 chars even on a wide chart - so wrapping also |
//| enforces a hard character cap well under that limit, which is what |
//| actually keeps every line fully visible. |
//+------------------------------------------------------------------+
#define STATUS_LABEL_PREFIX "WarriorStatusLine"
#define STATUS_LABEL_FONTSIZE 9
#define STATUS_LABEL_FONT "Consolas"
#define STATUS_LABEL_PAD_X 3
#define STATUS_LABEL_PAD_Y 1
#define STATUS_LABEL_LINE_GAP 2 // extra pixels between successive lines' backgrounds
#define STATUS_LABEL_RIGHT_MARGIN 20
#define STATUS_LABEL_MIN_WRAP_WIDTH 150
#define STATUS_LABEL_MAX_CHARS 55 // safe margin under OBJPROP_TEXT's observed ~63-char truncation
#define STATUS_LABEL_MAX_LINES 64
int g_statusLabelLineCount = 0;
void EnsureStatusLine(int idx)
{
string bg = STATUS_LABEL_PREFIX + (string)idx + "BG";
string txt = STATUS_LABEL_PREFIX + (string)idx + "Text";
if(ObjectFind(0, bg) < 0)
{
ObjectCreate(0, bg, OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, bg, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, bg, OBJPROP_BGCOLOR, (color)ColorToARGB(clrBlack, 150));
ObjectSetInteger(0, bg, OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, bg, OBJPROP_COLOR, clrNONE);
ObjectSetInteger(0, bg, OBJPROP_BACK, false);
ObjectSetInteger(0, bg, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, bg, OBJPROP_SELECTED, false);
ObjectSetInteger(0, bg, OBJPROP_HIDDEN, true);
}
if(ObjectFind(0, txt) < 0)
{
ObjectCreate(0, txt, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, txt, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, txt, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
ObjectSetInteger(0, txt, OBJPROP_FONTSIZE, STATUS_LABEL_FONTSIZE);
ObjectSetString(0, txt, OBJPROP_FONT, STATUS_LABEL_FONT);
ObjectSetInteger(0, txt, OBJPROP_COLOR, clrWhite);
ObjectSetInteger(0, txt, OBJPROP_BACK, false);
ObjectSetInteger(0, txt, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, txt, OBJPROP_SELECTED, false);
ObjectSetInteger(0, txt, OBJPROP_HIDDEN, true);
}
}
//+------------------------------------------------------------------+
//| Appends one already-short-enough chunk to outLines/outCount, |
//| force-splitting it further if it still exceeds |
//| STATUS_LABEL_MAX_CHARS (a single word longer than the cap, with |
//| no spaces to break on). |
//+------------------------------------------------------------------+
void AppendStatusChunk(string chunk, string &outLines[], int &outCount)
{
int pos = 0;
int len = StringLen(chunk);
while(len - pos > STATUS_LABEL_MAX_CHARS)
{
ArrayResize(outLines, outCount + 1);
outLines[outCount++] = StringSubstr(chunk, pos, STATUS_LABEL_MAX_CHARS);
pos += STATUS_LABEL_MAX_CHARS;
}
ArrayResize(outLines, outCount + 1);
outLines[outCount++] = StringSubstr(chunk, pos);
}
//+------------------------------------------------------------------+
//| Greedily breaks one logical line into as many visual lines as |
//| needed to keep each one's measured TextGetSize() width within |
//| maxWidth AND its character count within STATUS_LABEL_MAX_CHARS - |
//| the latter is what actually matters (see this file's header |
//| comment on ObjectSetString's ~63-char truncation), the pixel width |
//| check just wraps sooner on a narrow chart. Requires TextSetFont() |
//| to already have been called by the caller so TextGetSize() |
//| measures against the same font actually rendered. |
//+------------------------------------------------------------------+
void WrapLineInto(string line, int maxWidth, string &outLines[], int &outCount)
{
uint dummyW = 0, dummyH = 0;
TextGetSize(line, dummyW, dummyH);
if(StringLen(line) == 0 || ((int)dummyW <= maxWidth && StringLen(line) <= STATUS_LABEL_MAX_CHARS))
{
ArrayResize(outLines, outCount + 1);
outLines[outCount++] = line;
return;
}
string words[];
int wordCount = StringSplit(line, ' ', words);
string current = "";
for(int w = 0; w < wordCount; w++)
{
string candidate = (StringLen(current) == 0) ? words[w] : current + " " + words[w];
uint cw = 0, ch = 0;
TextGetSize(candidate, cw, ch);
bool tooWide = ((int)cw > maxWidth || StringLen(candidate) > STATUS_LABEL_MAX_CHARS);
if(tooWide && StringLen(current) > 0)
{
AppendStatusChunk(current, outLines, outCount);
current = words[w];
}
else
current = candidate;
}
if(StringLen(current) > 0)
AppendStatusChunk(current, outLines, outCount);
}
void SetStatusLabel(string text, int x = 10, int y = 20)
{
//--- TextSetFont's size argument is tenths of a point when negative (matches OBJPROP_FONTSIZE's
//--- point-based units above) - must be set before any TextGetSize() call since it's global state
TextSetFont(STATUS_LABEL_FONT, -STATUS_LABEL_FONTSIZE * 10, 0);
long chartWidthPx = 0;
ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0, chartWidthPx);
int maxWidth = (int)chartWidthPx - x - STATUS_LABEL_RIGHT_MARGIN;
if(maxWidth < STATUS_LABEL_MIN_WRAP_WIDTH)
maxWidth = STATUS_LABEL_MIN_WRAP_WIDTH;
string rawLines[];
int rawCount = StringSplit(text, '\n', rawLines);
string lines[];
int lineCount = 0;
for(int r = 0; r < rawCount && lineCount < STATUS_LABEL_MAX_LINES; r++)
WrapLineInto(rawLines[r], maxWidth, lines, lineCount);
if(lineCount > STATUS_LABEL_MAX_LINES)
lineCount = STATUS_LABEL_MAX_LINES;
int lineY = y;
for(int i = 0; i < lineCount; i++)
{
EnsureStatusLine(i);
string bg = STATUS_LABEL_PREFIX + (string)i + "BG";
string txt = STATUS_LABEL_PREFIX + (string)i + "Text";
uint textW = 0, textH = 0;
//--- an empty line measures 0x0, which would collapse its background - fall back to a single
//--- space so blank separator lines still reserve a row of height
TextGetSize(StringLen(lines[i]) > 0 ? lines[i] : " ", textW, textH);
ObjectSetInteger(0, bg, OBJPROP_XDISTANCE, x - STATUS_LABEL_PAD_X);
ObjectSetInteger(0, bg, OBJPROP_YDISTANCE, lineY - STATUS_LABEL_PAD_Y);
ObjectSetInteger(0, bg, OBJPROP_XSIZE, (int)textW + STATUS_LABEL_PAD_X * 2);
ObjectSetInteger(0, bg, OBJPROP_YSIZE, (int)textH + STATUS_LABEL_PAD_Y * 2);
ObjectSetInteger(0, txt, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, txt, OBJPROP_YDISTANCE, lineY);
ObjectSetString(0, txt, OBJPROP_TEXT, lines[i]);
lineY += (int)textH + STATUS_LABEL_LINE_GAP;
}
//--- fewer lines than last call - remove the now-unused trailing line objects rather than
//--- leaving stale text (e.g. a previous longer status block) hanging below the new one
for(int i = lineCount; i < g_statusLabelLineCount; i++)
{
string bg = STATUS_LABEL_PREFIX + (string)i + "BG";
string txt = STATUS_LABEL_PREFIX + (string)i + "Text";
if(ObjectFind(0, bg) >= 0)
ObjectDelete(0, bg);
if(ObjectFind(0, txt) >= 0)
ObjectDelete(0, txt);
}
g_statusLabelLineCount = lineCount;
//--- ObjectSet*() alone does not trigger a repaint - without this the new text/background only
//--- appears once something else forces a redraw (dragging the chart, clicking an object, a new
//--- bar), which made updates look stalled between those events
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| Removes every status line object. Mirrors the old Comment("") |
//| clear on shutdown - ObjectsDeleteAll(0) elsewhere clears chart |
//| objects, but callers still need an explicit clear at points where |
//| only the status text (not the whole chart) should be reset. |
//+------------------------------------------------------------------+
void ClearStatusLabel(void)
{
for(int i = 0; i < g_statusLabelLineCount; i++)
{
string bg = STATUS_LABEL_PREFIX + (string)i + "BG";
string txt = STATUS_LABEL_PREFIX + (string)i + "Text";
if(ObjectFind(0, bg) >= 0)
ObjectDelete(0, bg);
if(ObjectFind(0, txt) >= 0)
ObjectDelete(0, txt);
}
g_statusLabelLineCount = 0;
ChartRedraw(0);
}