837 lines
32 KiB
MQL5
837 lines
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
|