//+------------------------------------------------------------------+ //| 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