Article-22597-MQL5-Economic.../News Interact.mqh

837 lines
No EOL
32 KiB
MQL5

//+------------------------------------------------------------------+
//| News Interact.mqh |
//| Copyright 2026, Allan Munene Mutiiria. |
//| https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
//--- Include guard
#ifndef NEWS_INTERACT_MQH
#define NEWS_INTERACT_MQH
//--- Include core data definitions
#include "News Core.mqh"
//--- Include trade logic handlers
#include "News Logic.mqh"
//--- Include canvas rendering routines
#include "News Render.mqh"
//+------------------------------------------------------------------+
//| Interaction State |
//+------------------------------------------------------------------+
bool g_news_scrollDragging = false; // Scrollbar thumb drag in progress flag
//+------------------------------------------------------------------+
//| Convert chart coordinates to canvas-local coordinates |
//+------------------------------------------------------------------+
void News_ChartToCanvas(int chartX, int chartY, int &localX, int &localY)
{
//--- Subtract dashboard origin to get canvas-local coordinates
localX = chartX - g_news_dashboardX;
localY = chartY - g_news_dashboardY;
}
//+------------------------------------------------------------------+
//| Hit-test canvas-local coordinates and return hover code |
//+------------------------------------------------------------------+
int News_HitTest(int lx, int ly)
{
//--- Test right-edge resize hot zone along the full edge minus corner radii
{
const int cornerR = 8;
const int edgeMargin = 4;
const int hotTop = cornerR + edgeMargin;
const int hotBot = NEWS_DASHBOARD_H - cornerR - edgeMargin;
if(lx >= g_news_dashW - NEWS_RESIZE_HOT_W && lx <= g_news_dashW
&& ly >= hotTop && ly <= hotBot)
return NEWS_HOV_RESIZE_R;
}
//--- Test bottom-edge resize hot zone along the full edge minus corner radii
{
const int cornerR = 8;
const int edgeMargin = 4;
const int hotLeft = cornerR + edgeMargin;
const int hotRight = NEWS_DASHBOARD_W - cornerR - edgeMargin;
if(ly >= g_news_dashH - NEWS_RESIZE_HOT_H && ly <= g_news_dashH
&& lx >= hotLeft && lx <= hotRight)
return NEWS_HOV_RESIZE_B;
}
//--- Test theme toggle button
if(News_PointInRect(lx, ly, g_news_themeL, g_news_themeT,
g_news_themeR - g_news_themeL, g_news_themeB - g_news_themeT))
return NEWS_HOV_THEME;
//--- Test close button
if(News_PointInRect(lx, ly, g_news_closeL, g_news_closeT,
g_news_closeR - g_news_closeL, g_news_closeB - g_news_closeT))
return NEWS_HOV_CLOSE;
//--- Test currency filter master toggle
if(News_PointInRect(lx, ly, g_news_currTglL, g_news_currTglT,
g_news_currTglR - g_news_currTglL, g_news_currTglB - g_news_currTglT))
return NEWS_HOV_FILTER_CURR;
//--- Test impact filter master toggle
if(News_PointInRect(lx, ly, g_news_impTglL, g_news_impTglT,
g_news_impTglR - g_news_impTglL, g_news_impTglB - g_news_impTglT))
return NEWS_HOV_FILTER_IMP;
//--- Test time filter master toggle
if(News_PointInRect(lx, ly, g_news_timeTglL, g_news_timeTglT,
g_news_timeTglR - g_news_timeTglL, g_news_timeTglB - g_news_timeTglT))
return NEWS_HOV_FILTER_TIME;
//--- Test each currency chip
for(int i = 0; i < NEWS_CURR_COUNT; i++)
{
if(News_PointInRect(lx, ly, g_news_currL[i], g_news_currT[i],
g_news_currR[i] - g_news_currL[i], g_news_currB[i] - g_news_currT[i]))
return NEWS_HOV_CURR_BASE + i;
}
//--- Test each impact pill
for(int i = 0; i < NEWS_IMPACT_COUNT; i++)
{
if(News_PointInRect(lx, ly, g_news_impL[i], g_news_impT[i],
g_news_impR[i] - g_news_impL[i], g_news_impB[i] - g_news_impT[i]))
return NEWS_HOV_IMP_BASE + i;
}
//--- Test each visible event row
for(int r = 0; r < g_news_visibleRowCount; r++)
{
if(News_PointInRect(lx, ly, g_news_rowL[r], g_news_rowT[r],
g_news_rowR[r] - g_news_rowL[r], g_news_rowB[r] - g_news_rowT[r]))
return NEWS_HOV_ROW_BASE + r;
}
//--- Treat the header strip as the drag region when no button matched
if(ly >= 0 && ly < NEWS_HEADER_H) return NEWS_HOV_DRAG;
return NEWS_HOV_NONE;
}
//+------------------------------------------------------------------+
//| Test if point falls inside the events table viewport area |
//+------------------------------------------------------------------+
bool News_PointInTableArea(int lx, int ly)
{
//--- Compute table vertical bounds and test point
const int rowsTop = News_TableRowsTop();
const int rowsBot = News_TableRowsBottom();
return (lx >= NEWS_SIDE_PAD && lx < NEWS_DASHBOARD_W - NEWS_SIDE_PAD
&& ly >= rowsTop && ly < rowsBot);
}
//+------------------------------------------------------------------+
//| Handle action triggered by hover code |
//+------------------------------------------------------------------+
void News_HandleAction(int hov, bool doubleClick = false)
{
//--- Close dashboard on close button click
if(hov == NEWS_HOV_CLOSE)
{
g_news_dashboardVisible = false;
News_DestroyCanvas();
return;
}
//--- Toggle dark/light theme on theme button click
if(hov == NEWS_HOV_THEME)
{
News_ApplyTheme(!g_news_darkTheme);
News_RenderAll();
ChartRedraw();
return;
}
//--- Toggle currency filter master switch
if(hov == NEWS_HOV_FILTER_CURR)
{
g_news_filterCurrencyOn = !g_news_filterCurrencyOn;
g_news_filtersChanged = true;
News_RefreshEvents();
News_RenderAll();
ChartRedraw();
return;
}
//--- Toggle impact filter master switch
if(hov == NEWS_HOV_FILTER_IMP)
{
g_news_filterImpactOn = !g_news_filterImpactOn;
g_news_filtersChanged = true;
News_RefreshEvents();
News_RenderAll();
ChartRedraw();
return;
}
//--- Toggle time range filter master switch
if(hov == NEWS_HOV_FILTER_TIME)
{
g_news_filterTimeOn = !g_news_filterTimeOn;
g_news_filtersChanged = true;
News_RefreshEvents();
News_RenderAll();
ChartRedraw();
return;
}
//--- Toggle individual currency chip selection
if(hov >= NEWS_HOV_CURR_BASE && hov < NEWS_HOV_CURR_BASE + NEWS_CURR_COUNT)
{
const int idx = hov - NEWS_HOV_CURR_BASE;
g_news_currSelected[idx] = !g_news_currSelected[idx];
g_news_filtersChanged = true;
News_RefreshEvents();
News_RenderAll();
ChartRedraw();
return;
}
//--- Toggle individual impact pill selection
if(hov >= NEWS_HOV_IMP_BASE && hov < NEWS_HOV_IMP_BASE + NEWS_IMPACT_COUNT)
{
const int idx = hov - NEWS_HOV_IMP_BASE;
g_news_impactSelected[idx] = !g_news_impactSelected[idx];
g_news_filtersChanged = true;
News_RefreshEvents();
News_RenderAll();
ChartRedraw();
return;
}
//--- Dispatch row click by row kind
if(hov >= NEWS_HOV_ROW_BASE && hov < NEWS_HOV_ROW_BASE + NEWS_MAX_VISIBLE_ROWS)
{
const int rIdx = hov - NEWS_HOV_ROW_BASE;
if(rIdx < g_news_visibleRowCount)
{
const int planIdx = g_news_rowEventIdx[rIdx];
if(planIdx >= 0 && planIdx < ArraySize(g_news_rowPlan))
{
const int kind = g_news_rowPlan[planIdx].kind;
//--- Toggle day collapse on double-click of a day separator row
if(kind == NEWS_ROW_KIND_DAY)
{
if(doubleClick)
{
const string dateKey = g_news_rowPlan[planIdx].dateKey;
News_ToggleDayCollapsed(dateKey);
News_BuildRowPlan();
News_ScrollClamp(g_news_tableScroll);
}
}
//--- Show event details in toast on single event row click
else if(kind == NEWS_ROW_KIND_EVENT)
{
const int evIdx = g_news_rowPlan[planIdx].eventIdx;
if(evIdx >= 0 && evIdx < ArraySize(g_news_displayableEvents))
{
const NewsEvent ev = g_news_displayableEvents[evIdx];
string msg = ev.eventDate + " " + ev.eventTime + " "
+ ev.currency + " " + ev.event;
News_ShowToast(msg, false);
}
}
}
}
News_RenderAll();
ChartRedraw();
return;
}
}
//+------------------------------------------------------------------+
//| Create main and separator canvas bitmap labels |
//+------------------------------------------------------------------+
bool News_CreateCanvas()
{
//--- Return early if canvas already exists
if(g_news_canvasExists) return true;
//--- Create main canvas at maximum dimensions for crop-based resizing
if(!g_news_canv.CreateBitmapLabel(NEWS_CANVAS_NAME,
g_news_dashboardX, g_news_dashboardY,
NEWS_DASHBOARD_W_MAX, NEWS_DASHBOARD_H_MAX,
COLOR_FORMAT_ARGB_NORMALIZE))
{
Print("News_CreateCanvas: failed - ", GetLastError());
return false;
}
//--- Configure main canvas object properties
ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_BACK, false);
ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_HIDDEN, true);
ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_ZORDER, 1000);
//--- Suppress default tooltip; interact layer sets it dynamically for revised-value hover
ObjectSetString(0, NEWS_CANVAS_NAME, OBJPROP_TOOLTIP, "\n");
//--- Crop visible area to current dashboard size
ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_XSIZE, g_news_dashW);
ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_YSIZE, g_news_dashH);
g_news_canvasExists = true;
//--- Create separators overlay canvas at same maximum dimensions
if(g_news_canvSep.CreateBitmapLabel(NEWS_CANVAS_NAME_SEP,
g_news_dashboardX, g_news_dashboardY,
NEWS_DASHBOARD_W_MAX, NEWS_DASHBOARD_H_MAX,
COLOR_FORMAT_ARGB_NORMALIZE))
{
//--- Configure separator canvas object properties
ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_BACK, false);
ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_HIDDEN, true);
ObjectSetString(0, NEWS_CANVAS_NAME_SEP, OBJPROP_TOOLTIP, "\n");
ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_ZORDER, 500);
//--- Crop separator canvas to current dashboard size
ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_XSIZE, g_news_dashW);
ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_YSIZE, g_news_dashH);
g_news_canvSepExists = true;
}
return true;
}
//+------------------------------------------------------------------+
//| Destroy main canvas and separators overlay |
//+------------------------------------------------------------------+
void News_DestroyCanvas()
{
//--- Destroy separator canvas first if it exists
if(g_news_canvSepExists)
{
g_news_canvSep.Destroy();
ObjectDelete(0, NEWS_CANVAS_NAME_SEP);
g_news_canvSepExists = false;
}
//--- Abort if main canvas was never created
if(!g_news_canvasExists) return;
//--- Destroy main canvas and remove chart object
g_news_canv.Destroy();
ObjectDelete(0, NEWS_CANVAS_NAME);
g_news_canvasExists = false;
ChartRedraw();
}
//+------------------------------------------------------------------+
//| Apply visible crop to both canvases without reallocating bitmaps |
//+------------------------------------------------------------------+
void News_ResizeCanvases(int newW, int newH)
{
//--- Update visible crop on main canvas
if(g_news_canvasExists)
{
ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_XSIZE, newW);
ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_YSIZE, newH);
}
//--- Update visible crop on separator canvas
if(g_news_canvSepExists)
{
ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_XSIZE, newW);
ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_YSIZE, newH);
}
}
//+------------------------------------------------------------------+
//| Show dashboard - create canvas, load data, and render |
//+------------------------------------------------------------------+
void News_ShowDashboard()
{
//--- Abort if canvas creation fails
if(!News_CreateCanvas()) return;
//--- Mark dashboard visible and load events
g_news_dashboardVisible = true;
News_RefreshEvents();
//--- Enable mouse move and wheel chart events
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);
//--- Render and push to chart
News_RenderAll();
ChartRedraw();
}
//+------------------------------------------------------------------+
//| Hide dashboard and destroy canvas |
//+------------------------------------------------------------------+
void News_HideDashboard()
{
//--- Destroy all canvas objects and clear visibility flag
News_DestroyCanvas();
g_news_dashboardVisible = false;
}
//+------------------------------------------------------------------+
//| Initialize program |
//+------------------------------------------------------------------+
bool News_Init()
{
//--- Apply default light theme
News_ApplyTheme(false);
//--- Initialize currency/impact filters and scroll state
News_InitDefaultFilters();
News_ScrollInit(g_news_tableScroll);
//--- Set magic number on the trade helper object
g_news_trade.SetExpertMagicNumber(20260507);
//--- Load events from embedded CSV in tester, or from live API in live mode
if(MQLInfoInteger(MQL_TESTER))
{
if(!News_LoadEventsFromCsv(EconomicCalendarData))
{
Print("News_Init: failed to load CSV");
return false;
}
}
else
{
//--- Compute live date window from current server time
const datetime startDt = TimeTradeServer() - PeriodSeconds(start_time);
const datetime endDt = TimeTradeServer() + PeriodSeconds(end_time);
News_LoadEventsFromLive(startDt, endDt);
}
//--- Show dashboard after successful initialization
News_ShowDashboard();
return true;
}
//+------------------------------------------------------------------+
//| Deinitialize program |
//+------------------------------------------------------------------+
void News_Deinit()
{
//--- Hide and destroy the dashboard canvas
News_HideDashboard();
//--- Destroy persistent table scratch canvas
if(g_news_tableTmpReady)
{
g_news_tableTmp.Destroy();
g_news_tableTmpReady = false;
}
//--- Destroy primitives high-resolution fill canvas
if(g_news_prim.m_hrFillReady)
{
g_news_prim.m_hrFill.Destroy();
g_news_prim.m_hrFillReady = false;
}
//--- Destroy primitives high-resolution border canvas
if(g_news_prim.m_hrBorderReady)
{
g_news_prim.m_hrBorder.Destroy();
g_news_prim.m_hrBorderReady = false;
}
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void News_OnTick()
{
//--- Skip processing when dashboard is hidden
if(!g_news_dashboardVisible) return;
//--- Periodically refresh live event data
static datetime lastRefresh = 0;
const datetime now = TimeCurrent();
if(!MQLInfoInteger(MQL_TESTER))
{
//--- Refresh every 30 seconds in live mode
if(now - lastRefresh >= 30)
{
//--- Snapshot displayable state before refresh to detect changes
const int preCount = ArraySize(g_news_displayableEvents);
const datetime preStamp = g_news_lastEventStamp;
//--- Compute live date window and reload events
const datetime startDt = TimeTradeServer() - PeriodSeconds(start_time);
const datetime endDt = TimeTradeServer() + PeriodSeconds(end_time);
News_LoadEventsFromLive(startDt, endDt);
News_RefreshEvents();
//--- Redraw only if event list actually changed
const int postCount = ArraySize(g_news_displayableEvents);
const datetime postStamp = g_news_lastEventStamp;
if(postCount != preCount || postStamp != preStamp)
{
News_RenderAll();
ChartRedraw();
}
lastRefresh = now;
}
}
else
{
//--- Refresh every second in tester mode to keep countdown current
if(now - lastRefresh >= 1)
{
News_RefreshEvents();
News_RenderAll();
ChartRedraw();
lastRefresh = now;
}
}
//--- Check whether a news trade should be placed
News_CheckForNewsTrade();
}
//+------------------------------------------------------------------+
//| Expert timer function |
//+------------------------------------------------------------------+
void News_OnTimer()
{
//--- Skip processing when dashboard is hidden
if(!g_news_dashboardVisible) return;
//--- Manage toast lifecycle; force render while toast is alive
bool needFullRender = false;
bool toastAlive = false;
if(StringLen(g_news_toastText) > 0)
{
//--- Clear expired toast and flag a final render
if(GetTickCount64() > g_news_toastExpiryMs)
{
g_news_toastText = "";
g_news_toastExpiryMs = 0;
needFullRender = true;
}
else
{
//--- Toast is still live; animate progress bar this tick
toastAlive = true;
}
}
//--- Determine if a full render is due this tick
static ulong s_lastFullMs = 0;
const ulong nowMs = GetTickCount64();
//--- Render when toast is alive, just expired, or 5-second interval elapsed
if(toastAlive || needFullRender || (nowMs - s_lastFullMs) >= 5000)
{
News_RenderAll();
ChartRedraw();
s_lastFullMs = nowMs;
return;
}
//--- Fast path: update only changed Remain cells to reduce CPU cost
if(News_TickRemainCells()) ChartRedraw();
}
//+------------------------------------------------------------------+
//| Expert chart event function |
//+------------------------------------------------------------------+
void News_OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
//--- Skip all events when dashboard is not visible
if(!g_news_dashboardVisible) return;
//--- Handle mouse move events (left-button state arrives in sparam)
if(id == CHARTEVENT_MOUSE_MOVE)
{
const int mx = (int)lparam;
const int my = (int)dparam;
const int mstate = (int)StringToInteger(sparam);
int lx, ly;
News_ChartToCanvas(mx, my, lx, ly);
//--- Update cached canvas-local mouse position
g_news_mouseLx = lx;
g_news_mouseLy = ly;
//--- Process dashboard drag movement
if(g_news_dragging)
{
//--- Release drag when left button is no longer held
if((mstate & 1) == 0)
{
g_news_dragging = false;
}
else
{
//--- Clamp new dashboard position to chart bounds
const int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
const int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
int newX = MathMax(0, MathMin(chartW - NEWS_DASHBOARD_W, mx - g_news_dragOffsetX));
int newY = MathMax(0, MathMin(chartH - NEWS_DASHBOARD_H, my - g_news_dragOffsetY));
g_news_dashboardX = newX;
g_news_dashboardY = newY;
//--- Move main canvas object to new position
ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_XDISTANCE, newX);
ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_YDISTANCE, newY);
//--- Move separator canvas in sync with main canvas
if(g_news_canvSepExists)
{
ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_XDISTANCE, newX);
ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_YDISTANCE, newY);
}
ChartRedraw();
return;
}
}
//--- Process right-edge horizontal resize drag
if(g_news_resizing)
{
//--- Release resize when left button is no longer held
if((mstate & 1) == 0)
{
g_news_resizing = false;
}
else
{
//--- Clamp new width to min/max bounds and chart width limit
const int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
const int upperByChart = chartW - g_news_dashboardX;
const int upperBound = MathMin(NEWS_DASHBOARD_W_MAX, upperByChart);
const int delta = mx - g_news_resizeStartMouseX;
int newW = g_news_resizeStartW + delta;
if(newW < NEWS_DASHBOARD_W_MIN) newW = NEWS_DASHBOARD_W_MIN;
if(newW > upperBound) newW = upperBound;
//--- Skip render if width did not change
if(newW != g_news_dashW)
{
const int oldW = g_news_dashW;
g_news_dashW = newW;
//--- Expand: resize crop first then render to avoid blank edges
if(newW > oldW)
{
News_ResizeCanvases(newW, NEWS_DASHBOARD_H);
News_RenderAll();
}
//--- Contract: render first then crop to avoid stale content
else
{
News_RenderAll();
News_ResizeCanvases(newW, NEWS_DASHBOARD_H);
}
ChartRedraw();
}
return;
}
}
//--- Process bottom-edge vertical resize drag
if(g_news_resizingV)
{
//--- Release resize when left button is no longer held
if((mstate & 1) == 0)
{
g_news_resizingV = false;
}
else
{
//--- Clamp new height to min/max bounds and chart height limit
const int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
const int upperByChart = chartH - g_news_dashboardY;
const int upperBound = MathMin(NEWS_DASHBOARD_H_MAX, upperByChart);
const int delta = my - g_news_resizeStartMouseY;
int newH = g_news_resizeStartH + delta;
if(newH < NEWS_DASHBOARD_H_MIN) newH = NEWS_DASHBOARD_H_MIN;
if(newH > upperBound) newH = upperBound;
//--- Skip render if height did not change
if(newH != g_news_dashH)
{
const int oldH = g_news_dashH;
g_news_dashH = newH;
//--- Expand: resize crop first then render
if(newH > oldH)
{
News_ResizeCanvases(NEWS_DASHBOARD_W, newH);
News_RenderAll();
}
//--- Contract: render first then crop
else
{
News_RenderAll();
News_ResizeCanvases(NEWS_DASHBOARD_W, newH);
}
ChartRedraw();
}
return;
}
}
//--- Process scrollbar thumb drag movement
if(g_news_scrollDragging)
{
News_ScrollUpdateDrag(g_news_tableScroll, ly);
//--- End drag when left button is released
if((mstate & 1) == 0)
{
News_ScrollEndDrag(g_news_tableScroll);
g_news_scrollDragging = false;
}
News_RenderAll();
ChartRedraw();
return;
}
//--- Compute new hover code and flag redraw when it changes
const int newHov = News_HitTest(lx, ly);
bool needRedraw = (newHov != g_news_hover);
g_news_hover = newHov;
//--- Quantize cursor position to 4px steps to throttle handle redraws on resize edges
const bool onResizeEdge = (newHov == NEWS_HOV_RESIZE_R || newHov == NEWS_HOV_RESIZE_B);
const int qx = (lx / 4) * 4;
const int qy = (ly / 4) * 4;
if(onResizeEdge && (qx != g_news_cursorX || qy != g_news_cursorY)) needRedraw = true;
g_news_cursorX = qx;
g_news_cursorY = qy;
//--- Hit-test each row's revised-value triangle (10x10 hot zone around center)
int newRevHov = -1;
for(int rr = 0; rr < g_news_visibleRowCount; rr++)
{
const int triCx = g_news_revTriCx[rr];
if(triCx < 0) continue;
const int triCy = g_news_revTriCy[rr];
if(lx >= triCx - 5 && lx <= triCx + 5 && ly >= triCy - 5 && ly <= triCy + 5)
{
newRevHov = rr;
break;
}
}
//--- Update revised-value tooltip when the hovered row changes
if(newRevHov != g_news_revisedHoverRow)
{
g_news_revisedHoverRow = newRevHov;
string tip = "\n";
if(newRevHov >= 0)
{
//--- Translate visible row index to plan index then to event
const int planIdx = g_news_rowEventIdx[newRevHov];
if(planIdx >= 0 && planIdx < ArraySize(g_news_rowPlan))
{
const NewsRowEntry entry = g_news_rowPlan[planIdx];
if(entry.kind == NEWS_ROW_KIND_EVENT
&& entry.eventIdx >= 0
&& entry.eventIdx < ArraySize(g_news_displayableEvents))
{
const NewsEvent ev = g_news_displayableEvents[entry.eventIdx];
//--- Build "Revised from X" tooltip string
if(ev.hasRevised)
{
const string fromStr = News_FormatValue(ev.hasPrevious, ev.previous,
ev.unit, ev.multiplier, ev.digits);
tip = "Revised from " + fromStr;
}
}
}
}
//--- Push tooltip string to chart object
ObjectSetString(0, NEWS_CANVAS_NAME, OBJPROP_TOOLTIP, tip);
}
//--- Update scrollbar hover flags; thumb highlight requires precise hit
const bool scrollVis = News_ScrollVisible(g_news_tableScroll);
bool sbThumb = false, sbArea = false;
if(scrollVis)
{
sbThumb = News_ScrollHitThumb(g_news_tableScroll, lx, ly);
sbArea = News_PointInTableArea(lx, ly);
}
if(sbThumb != g_news_tableScroll.hoveredThumb) needRedraw = true;
if(sbArea != g_news_tableScroll.hoveredArea) needRedraw = true;
g_news_tableScroll.hoveredThumb = sbThumb;
g_news_tableScroll.hoveredArea = sbArea;
//--- Hover flag tracks only the thumb itself, not the broader track area
const bool prevHover = g_news_tableScroll.hover;
g_news_tableScroll.hover = sbThumb;
if(prevHover != g_news_tableScroll.hover) needRedraw = true;
//--- Disable chart mouse scroll when cursor is over the dashboard
const bool overDash = (lx >= 0 && lx < NEWS_DASHBOARD_W && ly >= 0 && ly < NEWS_DASHBOARD_H);
ChartSetInteger(0, CHART_MOUSE_SCROLL, !overDash);
//--- Detect fresh press, release, and double-click
static int prevMouseState = 0;
static ulong lastClickMs = 0;
static int lastClickX = -9999;
static int lastClickY = -9999;
const bool freshPress = (prevMouseState == 0 && mstate == 1);
const bool freshRelease = (prevMouseState == 1 && mstate == 0);
prevMouseState = mstate;
//--- Identify double-click by proximity and timing against previous press
bool wasDoubleClick = false;
if(freshPress)
{
const ulong nowClick = GetTickCount64();
const int ddx = lx - lastClickX;
const int ddy = ly - lastClickY;
if(nowClick - lastClickMs < 500 && (ddx * ddx + ddy * ddy) < 25)
wasDoubleClick = true;
lastClickMs = nowClick;
lastClickX = lx;
lastClickY = ly;
}
//--- Dispatch press to the appropriate interaction handler
if(freshPress)
{
//--- Begin scrollbar thumb drag
if(scrollVis && News_ScrollHitThumb(g_news_tableScroll, lx, ly))
{
News_ScrollBeginDrag(g_news_tableScroll, ly);
g_news_scrollDragging = true;
needRedraw = true;
}
//--- Begin header drag-move
else if(newHov == NEWS_HOV_DRAG)
{
g_news_dragging = true;
g_news_dragOffsetX = lx;
g_news_dragOffsetY = ly;
}
//--- Begin right-edge resize drag
else if(newHov == NEWS_HOV_RESIZE_R)
{
g_news_resizing = true;
g_news_resizeStartMouseX = mx;
g_news_resizeStartW = g_news_dashW;
}
//--- Begin bottom-edge resize drag
else if(newHov == NEWS_HOV_RESIZE_B)
{
g_news_resizingV = true;
g_news_resizeStartMouseY = my;
g_news_resizeStartH = g_news_dashH;
}
//--- Dispatch generic button or row action
else if(newHov != NEWS_HOV_NONE)
{
News_HandleAction(newHov, wasDoubleClick);
return;
}
}
//--- Clear all drag states on left-button release
if(freshRelease)
{
g_news_dragging = false;
g_news_resizing = false;
g_news_resizingV = false;
if(g_news_scrollDragging)
{
News_ScrollEndDrag(g_news_tableScroll);
g_news_scrollDragging = false;
needRedraw = true;
}
}
//--- Redraw if any hover or state change occurred
if(needRedraw)
{
News_RenderAll();
ChartRedraw();
}
return;
}
//--- Handle mouse wheel: scroll events table when cursor is over table area
if(id == CHARTEVENT_MOUSE_WHEEL)
{
const int mx = (int)(short)lparam;
const int my = (int)(short)(lparam >> 16);
const int delta = (int)dparam;
int lx, ly;
News_ChartToCanvas(mx, my, lx, ly);
if(News_PointInTableArea(lx, ly) && News_ScrollVisible(g_news_tableScroll))
{
//--- Suppress chart scroll and apply to dashboard table
ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
News_ScrollByWheel(g_news_tableScroll, delta, NEWS_ROW_H);
News_RenderAll();
ChartRedraw();
return;
}
//--- Pass wheel through to chart when cursor is not over table
ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
return;
}
//--- Handle chart resize: re-render to adapt to new chart dimensions
if(id == CHARTEVENT_CHART_CHANGE)
{
News_RenderAll();
ChartRedraw();
return;
}
}
#endif // NEWS_INTERACT_MQH