//+------------------------------------------------------------------+ //| AI Canvas 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 AI_CANVAS_INTERACT_MQH #define AI_CANVAS_INTERACT_MQH //--- Include required modules #include "AI Canvas State.mqh" #include "AI Canvas Render.mqh" #include "AI Canvas Editor.mqh" #include "AI Logic.mqh" //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ bool g_ai_chatScrollDragging = false; // Chat-pane scrollbar drag flag bool g_ai_editorScrollDragging = false; // Editor scrollbar drag flag bool g_ai_searchScrollDragging = false; // Search popup scrollbar drag flag //--- Forward declarations void Ai_HideDashboard(); void Ai_RecomputeLayout(); //+------------------------------------------------------------------+ //| Keyboard Override State | //+------------------------------------------------------------------+ bool g_ai_kbOverrideActive = false; // Override active flag bool g_ai_savedKbControl = true; // Saved keyboard control state bool g_ai_savedQuickNav = true; // Saved quick navigation state //+------------------------------------------------------------------+ //| Begin keyboard override to capture chart input | //+------------------------------------------------------------------+ void Ai_BeginKeyboardOverride() { //--- Bail if already active if(g_ai_kbOverrideActive) return; //--- Save current chart settings g_ai_savedKbControl = (bool)ChartGetInteger(0, CHART_KEYBOARD_CONTROL); g_ai_savedQuickNav = (bool)ChartGetInteger(0, CHART_QUICK_NAVIGATION); //--- Disable chart keyboard handling ChartSetInteger(0, CHART_KEYBOARD_CONTROL, false); ChartSetInteger(0, CHART_QUICK_NAVIGATION, false); g_ai_kbOverrideActive = true; } //+------------------------------------------------------------------+ //| End keyboard override and restore chart input | //+------------------------------------------------------------------+ void Ai_EndKeyboardOverride() { //--- Bail if not active if(!g_ai_kbOverrideActive) return; //--- Restore saved settings ChartSetInteger(0, CHART_KEYBOARD_CONTROL, g_ai_savedKbControl); ChartSetInteger(0, CHART_QUICK_NAVIGATION, g_ai_savedQuickNav); g_ai_kbOverrideActive = false; } //+------------------------------------------------------------------+ //| Convert chart coordinates to canvas-local coordinates | //+------------------------------------------------------------------+ void Ai_ChartToCanvas(int chartX, int chartY, int &localX, int &localY) { //--- Subtract dashboard origin localX = chartX - AI_DASHBOARD_X; localY = chartY - AI_DASHBOARD_Y; } //+------------------------------------------------------------------+ //| Decide between fast prompt overlay and full render after edit | //+------------------------------------------------------------------+ void Ai_FastRenderAfterEditorChange() { //--- Check if send button visual needs refresh const bool nowDisabled = Ai_PromptIsEmpty(); const bool sendStateChanged = (nowDisabled != g_ai_lastRenderedSendDisabled); //--- Check if any overlay is currently showing const bool hasOverlay = g_ai_showSearch || g_ai_showFooterDropdown || g_ai_showSmallHistory || (StringLen(g_ai_toastText) > 0); //--- Choose render path based on what changed if(sendStateChanged || hasOverlay || !g_ai_canvPromptExists) { //--- Slow path: full dashboard render Ai_RenderAll(); } else { //--- Fast path: prompt overlay only Ai_RenderPromptPaneOverlay(); } } //+------------------------------------------------------------------+ //| Hit-test canvas coordinates and return hover code | //+------------------------------------------------------------------+ int Ai_HitTest(int lx, int ly) { //--- Test footer dropdown items first if(g_ai_showFooterDropdown) { for(int i = 0; i < ArraySize(g_ai_footerDdItemL); i++) { if(AiPointInRect(lx, ly, g_ai_footerDdItemL[i], g_ai_footerDdItemT[i], g_ai_footerDdItemR[i] - g_ai_footerDdItemL[i], g_ai_footerDdItemB[i] - g_ai_footerDdItemT[i])) { return AI_HOV_FOOTER_DD_ITEM_BASE + i; } } //--- Inside dropdown body but not on item - block fall-through if(AiPointInRect(lx, ly, g_ai_footerDdL, g_ai_footerDdT, g_ai_footerDdR - g_ai_footerDdL, g_ai_footerDdB - g_ai_footerDdT)) return AI_HOV_NONE; } //--- Test search/history popup rows if(g_ai_showSearch || g_ai_showSmallHistory) { const int nRows = ArraySize(g_ai_popupRowL); for(int r = 0; r < nRows; r++) { if(AiPointInRect(lx, ly, g_ai_popupRowL[r], g_ai_popupRowT[r], g_ai_popupRowR[r] - g_ai_popupRowL[r], g_ai_popupRowB[r] - g_ai_popupRowT[r])) { //--- Right edge of row is the delete zone const int delZoneL = g_ai_popupRowR[r] - 24; if(lx >= delZoneL) { g_ai_popupHovRow = r; g_ai_popupHovDel = true; return AI_HOV_SMALL_DEL_BASE + r; } g_ai_popupHovRow = r; g_ai_popupHovDel = false; return AI_HOV_SMALL_CHAT_BASE + r; } } //--- Inside popup body but not on row - block fall-through if(AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT, g_ai_popupR - g_ai_popupL, g_ai_popupB - g_ai_popupT)) { g_ai_popupHovRow = -1; g_ai_popupHovDel = false; return AI_HOV_NONE; } } //--- Reset popup hover state when no popup hit g_ai_popupHovRow = -1; g_ai_popupHovDel = false; //--- Test theme button int tL, tT, tR, tB; Ai_GetThemeBtnRect(tL, tT, tR, tB); if(AiPointInRect(lx, ly, tL, tT, tR - tL, tB - tT)) return AI_HOV_THEME; //--- Test close button int cL, cT, cR, cB; Ai_GetCloseBtnRect(cL, cT, cR, cB); if(AiPointInRect(lx, ly, cL, cT, cR - cL, cB - cT)) return AI_HOV_CLOSE; //--- Test sidebar buttons int bL, bT, bR, bB; for(int i = 0; i < 4; i++) { Ai_GetSidebarBtnRect(i, bL, bT, bR, bB); if(AiPointInRect(lx, ly, bL, bT, bR - bL, bB - bT)) { switch(i) { case 0: return AI_HOV_SEARCH; case 1: return AI_HOV_NEW_CHAT; case 2: return AI_HOV_CLEAR; case 3: return AI_HOV_HISTORY; } } } //--- Test sidebar chat rows when expanded if(g_ai_sidebarExpanded) { int n = MathMin(7, ArraySize(g_ai_chats)); for(int r = 0; r < n; r++) { int rL, rT, rR, rB; Ai_GetSidebarChatRowRect(r, rL, rT, rR, rB); if(AiPointInRect(lx, ly, rL, rT, rR - rL, rB - rT)) { //--- Right edge is delete zone if(lx >= rR - 22) return AI_HOV_SIDE_DEL_BASE + r; return AI_HOV_SIDE_CHAT_BASE + r; } } } //--- Test sidebar toggle button int tgL, tgT, tgR, tgB; Ai_GetToggleBtnRect(tgL, tgT, tgR, tgB); if(AiPointInRect(lx, ly, tgL, tgT, tgR - tgL, tgB - tgT)) return AI_HOV_TOGGLE; //--- Test signal button (split into action half and chevron half) int sgL, sgT, sgR, sgB; Ai_GetSignalBtnRect(sgL, sgT, sgR, sgB); if(AiPointInRect(lx, ly, sgL, sgT, sgR - sgL, sgB - sgT)) { const int sepX = Ai_SignalSeparatorX(); if(lx >= sepX) return AI_HOV_SIGNAL_DD; return AI_HOV_SIGNAL; } //--- Test send button int snL, snT, snR, snB; Ai_GetSendBtnRect(snL, snT, snR, snB); if(AiPointInRect(lx, ly, snL, snT, snR - snL, snB - snT)) return AI_HOV_SEND; //--- Test regenerate button if(g_ai_regenR > g_ai_regenL && AiPointInRect(lx, ly, g_ai_regenL, g_ai_regenT, g_ai_regenR - g_ai_regenL, g_ai_regenB - g_ai_regenT)) return AI_HOV_REGEN; //--- Test export button if(g_ai_exportR > g_ai_exportL && AiPointInRect(lx, ly, g_ai_exportL, g_ai_exportT, g_ai_exportR - g_ai_exportL, g_ai_exportB - g_ai_exportT)) return AI_HOV_EXPORT; //--- Test scroll-to-bottom FAB if(g_ai_scrollFabVisible && AiPointInRect(lx, ly, g_ai_scrollFabL, g_ai_scrollFabT, g_ai_scrollFabR - g_ai_scrollFabL, g_ai_scrollFabB - g_ai_scrollFabT)) { return AI_HOV_SCROLL_FAB; } //--- Test per-user-message edit pencils const int userEditN = ArraySize(g_ai_userEditRectL); for(int ueI = 0; ueI < userEditN; ueI++) { if(g_ai_userEditRectR[ueI] > g_ai_userEditRectL[ueI] && AiPointInRect(lx, ly, g_ai_userEditRectL[ueI], g_ai_userEditRectT[ueI], g_ai_userEditRectR[ueI] - g_ai_userEditRectL[ueI], g_ai_userEditRectB[ueI] - g_ai_userEditRectT[ueI])) { return AI_HOV_USER_EDIT_BASE + ueI; } } //--- Test header drag region if(ly >= 0 && ly < AI_HEADER_H) return AI_HOV_DRAG; //--- No hit return AI_HOV_NONE; } //+------------------------------------------------------------------+ //| Test if point is inside prompt pane | //+------------------------------------------------------------------+ bool Ai_PointInPromptPane(int lx, int ly) { //--- Get prompt pane rect and test int pL, pT, pR, pB; Ai_GetPromptPaneRect(pL, pT, pR, pB); return AiPointInRect(lx, ly, pL, pT, pR - pL, pB - pT); } //+------------------------------------------------------------------+ //| Test if point is inside chat pane | //+------------------------------------------------------------------+ bool Ai_PointInChatPane(int lx, int ly) { //--- Get chat pane rect and test int cL, cT, cR, cB; Ai_GetChatPaneRect(cL, cT, cR, cB); return AiPointInRect(lx, ly, cL, cT, cR - cL, cB - cT); } //+------------------------------------------------------------------+ //| Dispatch footer dropdown action by id | //+------------------------------------------------------------------+ void Ai_DispatchFooterAction(int actionId) { //--- Route to handler based on action id switch(actionId) { case 0: AiGetAndAppendChartData(); break; // Get Chart Data case 1: AiTwinBars(); break; // Twin Bars case 2: AiGetTradeSignal(false); break; // Quick Scalp case 3: AiDailySignal(); break; // Daily Signal case 4: AiTrendRead(); break; // Trend Read case 5: AiKeyLevel(); break; // Key Level case 6: AiClearSignalDrawings(); break; // Clear Drawings default: //--- Unknown action - log and ignore Print("Ai_DispatchFooterAction: unknown actionId=", actionId); break; } } //+------------------------------------------------------------------+ //| Handle action triggered by hover code | //+------------------------------------------------------------------+ void Ai_HandleAction(int hov) { //--- Close dashboard if(hov == AI_HOV_CLOSE) { Ai_HideDashboard(); return; } //--- Toggle theme if(hov == AI_HOV_THEME) { Ai_ApplyTheme(!g_ai_darkTheme); Ai_RenderAll(); ChartRedraw(); return; } //--- Toggle sidebar expansion if(hov == AI_HOV_TOGGLE) { g_ai_sidebarExpanded = !g_ai_sidebarExpanded; Ai_RecomputeLayout(); Ai_RenderAll(); ChartRedraw(); return; } //--- Create new chat if(hov == AI_HOV_NEW_CHAT) { g_ai_showSearch = false; g_ai_showSmallHistory = false; AiCreateNewChat(); return; } //--- Clear current chat if(hov == AI_HOV_CLEAR) { //--- Skip if already empty if(StringLen(g_ai_conversationHistory) == 0 && StringLen(g_ai_editor.GetText()) == 0) { Ai_RenderAll(); ChartRedraw(); return; } //--- Snapshot title for toast before clearing string titleSnap = g_ai_currentTitle; if(StringLen(titleSnap) > 30) titleSnap = StringSubstr(titleSnap, 0, 27) + "..."; //--- Clear history and prompt g_ai_conversationHistory = ""; g_ai_currentPrompt = ""; g_ai_editor.SetText(""); //--- Persist and notify const bool savedOk = AiUpdateCurrentHistory(); if(savedOk) Ai_ShowToast("Successfully cleared chat '" + titleSnap + "'", false); else Ai_ShowToast("Failed to clear chat", true); Ai_RenderAll(); ChartRedraw(); return; } //--- Toggle history popup if(hov == AI_HOV_HISTORY) { g_ai_showSmallHistory = !g_ai_showSmallHistory; g_ai_showSearch = false; Ai_RenderAll(); ChartRedraw(); return; } //--- Toggle search popup if(hov == AI_HOV_SEARCH) { g_ai_showSearch = !g_ai_showSearch; g_ai_showSmallHistory = false; if(g_ai_showSearch) { //--- Focus search editor and reset state if(g_ai_editor.focused) g_ai_editor.focused = false; g_ai_searchEditor.focused = true; g_ai_searchEditor.SetText(""); g_ai_searchQuery = ""; g_ai_searchScroll.scrollPx = 0; Ai_BeginKeyboardOverride(); } else { //--- Unfocus search editor and end override if no other focus g_ai_searchEditor.focused = false; if(!g_ai_editor.focused) Ai_EndKeyboardOverride(); } Ai_RenderAll(); ChartRedraw(); return; } //--- Signal button action half - fire selected dropdown action if(hov == AI_HOV_SIGNAL) { g_ai_showFooterDropdown = false; g_ai_showSearch = false; g_ai_showSmallHistory = false; const int ddi = MathMax(0, MathMin(g_ai_footerDropdownSelectedIdx, AI_FOOTER_DD_COUNT - 1)); Ai_DispatchFooterAction(AI_FOOTER_DD_ACTION_IDS[ddi]); Ai_RenderAll(); ChartRedraw(); return; } //--- Signal button chevron half - toggle dropdown if(hov == AI_HOV_SIGNAL_DD) { g_ai_showFooterDropdown = !g_ai_showFooterDropdown; g_ai_showSearch = false; g_ai_showSmallHistory = false; Ai_RenderAll(); ChartRedraw(); return; } //--- Footer dropdown item selection if(hov >= AI_HOV_FOOTER_DD_ITEM_BASE && hov < AI_HOV_FOOTER_DD_ITEM_BASE + 100) { int item = hov - AI_HOV_FOOTER_DD_ITEM_BASE; g_ai_footerDropdownSelectedIdx = item; g_ai_showFooterDropdown = false; if(item >= 0 && item < AI_FOOTER_DD_COUNT) { Ai_DispatchFooterAction(AI_FOOTER_DD_ACTION_IDS[item]); } Ai_RenderAll(); ChartRedraw(); return; } //--- Send button - submit prompt if(hov == AI_HOV_SEND) { //--- Suppress empty submissions if(Ai_PromptIsEmpty()) return; string txt = g_ai_editor.GetText(); g_ai_editor.SetText(""); g_ai_currentPrompt = ""; AiSubmitMessage(txt); return; } //--- Regenerate button - resubmit last prompt if(hov == AI_HOV_REGEN) { //--- Capture last prompt then strip the turn pair and resubmit string lastPrompt = AiGetLastUserPrompt(); if(StringLen(lastPrompt) > 0) { AiRemoveLastConversationTurn(); AiSubmitMessage(lastPrompt); } return; } //--- Export button - write chat to file if(hov == AI_HOV_EXPORT) { string fname = "ChatGPT_Export_Chat" + IntegerToString(g_ai_currentChatId) + ".txt"; int h = FileOpen(fname, FILE_WRITE | FILE_TXT | FILE_ANSI); if(h != INVALID_HANDLE) { FileWriteString(h, "Title: " + g_ai_currentTitle + "\r\n\r\n"); FileWriteString(h, g_ai_conversationHistory); FileClose(h); Print("Exported chat to ", fname); Ai_ShowToast("Chat exported to " + fname, false); } else { const int err = GetLastError(); Print("Export failed: ", err); Ai_ShowToast("Export failed (error " + IntegerToString(err) + ")", true); } Ai_RenderAll(); ChartRedraw(); return; } //--- Scroll-to-bottom FAB - jump chat scroll to max if(hov == AI_HOV_SCROLL_FAB) { g_ai_chatScroll.scrollPx = AiScrollMax(g_ai_chatScroll); AiScrollClamp(g_ai_chatScroll); Ai_RenderAll(); ChartRedraw(); return; } //--- User-message edit pencil - load prompt into editor if(hov >= AI_HOV_USER_EDIT_BASE && hov < AI_HOV_USER_EDIT_BASE + 100) { const int peIdx = hov - AI_HOV_USER_EDIT_BASE; if(peIdx >= 0 && peIdx < ArraySize(g_ai_userEditPrompt)) { //--- Validate click landed in narrow pencil rect not just bubble const bool inClickRect = (peIdx < ArraySize(g_ai_userEditClickL)) && (g_ai_mouseLx >= g_ai_userEditClickL[peIdx]) && (g_ai_mouseLx < g_ai_userEditClickR[peIdx]) && (g_ai_mouseLy >= g_ai_userEditClickT[peIdx]) && (g_ai_mouseLy < g_ai_userEditClickB[peIdx]); if(!inClickRect) return; //--- Load prompt into editor const string prompt = g_ai_userEditPrompt[peIdx]; g_ai_editor.SetText(prompt); //--- Focus editor and start keyboard override if(g_ai_searchEditor.focused) g_ai_searchEditor.focused = false; if(!g_ai_editor.focused) { g_ai_editor.focused = true; Ai_BeginKeyboardOverride(); } //--- Sync currentPrompt for chat-store consistency g_ai_currentPrompt = prompt; Ai_RenderAll(); ChartRedraw(); } return; } //--- Sidebar chat row click - switch active chat if(hov >= AI_HOV_SIDE_CHAT_BASE && hov < AI_HOV_SIDE_DEL_BASE) { int row = hov - AI_HOV_SIDE_CHAT_BASE; int total = ArraySize(g_ai_chats); int chatIdx = total - 1 - row; if(chatIdx >= 0 && chatIdx < total && g_ai_chats[chatIdx].id != g_ai_currentChatId) { AiUpdateCurrentHistory(); g_ai_currentChatId = g_ai_chats[chatIdx].id; g_ai_currentTitle = g_ai_chats[chatIdx].title; g_ai_conversationHistory = g_ai_chats[chatIdx].history; Ai_RenderAll(); ChartRedraw(); } return; } //--- Sidebar delete button - remove chat if(hov >= AI_HOV_SIDE_DEL_BASE && hov < AI_HOV_SIDE_DEL_BASE + 100) { int row = hov - AI_HOV_SIDE_DEL_BASE; int total = ArraySize(g_ai_chats); int chatIdx = total - 1 - row; if(chatIdx >= 0 && chatIdx < total) { //--- Snapshot title before delete for toast string titleSnap = g_ai_chats[chatIdx].title; if(StringLen(titleSnap) > 30) titleSnap = StringSubstr(titleSnap, 0, 27) + "..."; const bool deletedOk = AiDeleteChat(g_ai_chats[chatIdx].id); //--- Notify user via toast if(deletedOk) Ai_ShowToast("Successfully deleted chat '" + titleSnap + "'", false); else Ai_ShowToast("Failed to delete chat", true); Ai_RenderAll(); ChartRedraw(); } return; } //--- Popup chat row click - switch active chat if(hov >= AI_HOV_SMALL_CHAT_BASE && hov < AI_HOV_SMALL_DEL_BASE) { int row = hov - AI_HOV_SMALL_CHAT_BASE; if(row >= 0 && row < ArraySize(g_ai_popupRowChatIdx)) { int chatIdx = g_ai_popupRowChatIdx[row]; if(chatIdx >= 0 && chatIdx < ArraySize(g_ai_chats) && g_ai_chats[chatIdx].id != g_ai_currentChatId) { AiUpdateCurrentHistory(); g_ai_currentChatId = g_ai_chats[chatIdx].id; g_ai_currentTitle = g_ai_chats[chatIdx].title; g_ai_conversationHistory = g_ai_chats[chatIdx].history; } } g_ai_showSearch = false; g_ai_showSmallHistory = false; Ai_RenderAll(); ChartRedraw(); return; } //--- Popup delete button - remove chat from popup if(hov >= AI_HOV_SMALL_DEL_BASE && hov < AI_HOV_SMALL_DEL_BASE + 100) { int row = hov - AI_HOV_SMALL_DEL_BASE; if(row >= 0 && row < ArraySize(g_ai_popupRowChatIdx)) { int chatIdx = g_ai_popupRowChatIdx[row]; if(chatIdx >= 0 && chatIdx < ArraySize(g_ai_chats)) { //--- Snapshot title before delete for toast string titleSnap = g_ai_chats[chatIdx].title; if(StringLen(titleSnap) > 30) titleSnap = StringSubstr(titleSnap, 0, 27) + "..."; const bool deletedOk = AiDeleteChat(g_ai_chats[chatIdx].id); if(deletedOk) Ai_ShowToast("Successfully deleted chat '" + titleSnap + "'", false); else Ai_ShowToast("Failed to delete chat", true); } } Ai_RenderAll(); ChartRedraw(); return; } } //+------------------------------------------------------------------+ //| Main chart event handler | //+------------------------------------------------------------------+ void Ai_OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Bail when dashboard is hidden if(!g_ai_dashboardVisible) return; //--- Handle keyboard input events if(id == CHARTEVENT_KEYDOWN) { //--- Read modifier states int vk = (int)lparam; const int shiftRaw = (int)TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT); const bool shiftDown = (shiftRaw < 0) || ((shiftRaw & 0x8000) != 0); const int ctrlRaw = (int)TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL); const bool ctrlDown = (ctrlRaw < 0) || ((ctrlRaw & 0x8000) != 0); //--- Search editor focus path if(g_ai_searchEditor.focused) { //--- Escape closes search if(vk == 27) { g_ai_searchEditor.focused = false; g_ai_showSearch = false; Ai_EndKeyboardOverride(); Ai_RenderAll(); ChartRedraw(); return; } //--- Enter ignored in search if(vk == 13) { return; } //--- Shift-modified navigation extends selection if(shiftDown) { if(vk == 37) { g_ai_searchEditor.ShiftExtendLeft(); Ai_RenderAll(); ChartRedraw(); return; } if(vk == 39) { g_ai_searchEditor.ShiftExtendRight(); Ai_RenderAll(); ChartRedraw(); return; } if(vk == 36) { g_ai_searchEditor.ShiftExtendHome(); Ai_RenderAll(); ChartRedraw(); return; } if(vk == 35) { g_ai_searchEditor.ShiftExtendEnd(); Ai_RenderAll(); ChartRedraw(); return; } } //--- Plain navigation if(vk == 37) { g_ai_searchEditor.MoveCaretLeft(); Ai_RenderAll(); ChartRedraw(); return; } if(vk == 39) { g_ai_searchEditor.MoveCaretRight(); Ai_RenderAll(); ChartRedraw(); return; } if(vk == 36) { g_ai_searchEditor.MoveCaretHome(); Ai_RenderAll(); ChartRedraw(); return; } if(vk == 35) { g_ai_searchEditor.MoveCaretEnd(); Ai_RenderAll(); ChartRedraw(); return; } //--- Backspace and delete - update query and reset scroll if(vk == 8) { g_ai_searchEditor.Backspace(); g_ai_searchQuery = g_ai_searchEditor.GetText(); g_ai_searchScroll.scrollPx = 0; Ai_RenderAll(); ChartRedraw(); return; } if(vk == 46) { g_ai_searchEditor.DeleteChar(); g_ai_searchQuery = g_ai_searchEditor.GetText(); g_ai_searchScroll.scrollPx = 0; Ai_RenderAll(); ChartRedraw(); return; } //--- Filter out non-typing keys bool isModifier2 = (vk == 16 || vk == 17 || vk == 18 || vk == 20 || vk == 144 || vk == 145 || vk == 91 || vk == 92 || vk == 93); bool isNavigation2 = (vk >= 33 && vk <= 40) || (vk == 45); bool isFunctionKey2 = (vk >= 112 && vk <= 123); bool isTab2 = (vk == 9); if(isModifier2 || isNavigation2 || isFunctionKey2 || isTab2) return; //--- Filter to printable virtual keys bool isPrintableVk2 = (vk == 32) || (vk >= 48 && vk <= 57) || (vk >= 65 && vk <= 90) || (vk >= 96 && vk <= 111) || (vk >= 186 && vk <= 223); if(!isPrintableVk2) return; //--- Translate and insert character short uch2 = TranslateKey(vk); if(uch2 > 0) { ushort code2 = (ushort)uch2; string ch2 = ShortToString(code2); g_ai_searchEditor.InsertChar(ch2); g_ai_searchQuery = g_ai_searchEditor.GetText(); g_ai_searchScroll.scrollPx = 0; Ai_RenderAll(); ChartRedraw(); } return; } //--- Main editor focus path if(g_ai_editor.focused) { //--- Enter handling - submit on Shift+Enter, newline otherwise if(vk == 13) { if(shiftDown) { //--- Suppress empty submissions if(Ai_PromptIsEmpty()) return; string txt = g_ai_editor.GetText(); g_ai_editor.SetText(""); g_ai_currentPrompt = ""; AiSubmitMessage(txt); } else { g_ai_editor.InsertNewline(); g_ai_currentPrompt = g_ai_editor.GetText(); Ai_RenderAll(); ChartRedraw(); } return; } //--- Escape blurs editor if(vk == 27) { g_ai_editor.focused = false; Ai_EndKeyboardOverride(); Ai_RenderAll(); ChartRedraw(); return; } //--- Shift-modified navigation extends selection if(shiftDown) { if(vk == 37) { g_ai_editor.ShiftExtendLeft(); Ai_RenderAll(); ChartRedraw(); return; } if(vk == 39) { g_ai_editor.ShiftExtendRight(); Ai_RenderAll(); ChartRedraw(); return; } if(vk == 38) { g_ai_editor.ShiftExtendUp(); Ai_RenderAll(); ChartRedraw(); return; } if(vk == 40) { g_ai_editor.ShiftExtendDown(); Ai_RenderAll(); ChartRedraw(); return; } if(vk == 36) { g_ai_editor.ShiftExtendHome(); Ai_RenderAll(); ChartRedraw(); return; } if(vk == 35) { g_ai_editor.ShiftExtendEnd(); Ai_RenderAll(); ChartRedraw(); return; } } //--- Plain navigation - use fast render path if(vk == 37) { g_ai_editor.MoveCaretLeft(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; } if(vk == 39) { g_ai_editor.MoveCaretRight(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; } if(vk == 38) { g_ai_editor.MoveCaretUp(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; } if(vk == 40) { g_ai_editor.MoveCaretDown(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; } if(vk == 36) { g_ai_editor.MoveCaretHome(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; } if(vk == 35) { g_ai_editor.MoveCaretEnd(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; } //--- Backspace and delete with fast render if(vk == 8) { g_ai_editor.Backspace(); g_ai_currentPrompt = g_ai_editor.GetText(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; } if(vk == 46) { g_ai_editor.DeleteChar(); g_ai_currentPrompt = g_ai_editor.GetText(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; } //--- Filter out non-typing keys bool isModifier = (vk == 16 || vk == 17 || vk == 18 || vk == 20 || vk == 144 || vk == 145 || vk == 91 || vk == 92 || vk == 93); bool isNavigation = (vk >= 33 && vk <= 40) || (vk == 45); bool isFunctionKey = (vk >= 112 && vk <= 123); bool isTab = (vk == 9); if(isModifier || isNavigation || isFunctionKey || isTab) return; //--- Filter to printable virtual keys bool isPrintableVk = (vk == 32) || (vk >= 48 && vk <= 57) || (vk >= 65 && vk <= 90) || (vk >= 96 && vk <= 111) || (vk >= 186 && vk <= 223); if(!isPrintableVk) return; //--- Translate and insert character with fast render short uch = TranslateKey(vk); if(uch > 0) { ushort code = (ushort)uch; string ch = ShortToString(code); g_ai_editor.InsertChar(ch); g_ai_currentPrompt = g_ai_editor.GetText(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); } return; } //--- Escape with no editor focused closes dashboard if(vk == 27) { Ai_HideDashboard(); return; } return; } //--- Handle mouse move events if(id == CHARTEVENT_MOUSE_MOVE) { //--- Read mouse coords and button state int mx = (int)lparam; int my = (int)dparam; int mstate = (int)sparam; int lx, ly; Ai_ChartToCanvas(mx, my, lx, ly); //--- Cache canvas-local mouse coords for render-time hover gates g_ai_mouseLx = lx; g_ai_mouseLy = ly; //--- Handle dashboard drag in progress if(g_ai_dragging) { //--- End drag on button release if((mstate & 1) == 0) { g_ai_dragging = false; } else { //--- Compute new dashboard position with bounds clamp int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); int newX = MathMax(0, MathMin(chartW - Ai_DashboardW(), mx - g_ai_dragOffsetX)); int newY = MathMax(0, MathMin(chartH - Ai_DashboardH(), my - g_ai_dragOffsetY)); //--- Apply new position AI_DASHBOARD_X = newX; AI_DASHBOARD_Y = newY; ObjectSetInteger(0, AI_CANVAS_NAME_MAIN, OBJPROP_XDISTANCE, newX); ObjectSetInteger(0, AI_CANVAS_NAME_MAIN, OBJPROP_YDISTANCE, newY); //--- Move prompt overlay in lockstep if(g_ai_canvPromptExists) { int dpL, dpT, dpR, dpB; Ai_GetPromptPaneRect(dpL, dpT, dpR, dpB); ObjectSetInteger(0, AI_CANVAS_NAME_PROMPT, OBJPROP_XDISTANCE, newX + dpL); ObjectSetInteger(0, AI_CANVAS_NAME_PROMPT, OBJPROP_YDISTANCE, newY + dpT); } ChartRedraw(); return; } } //--- Handle chat scrollbar drag if(g_ai_chatScrollDragging) { AiScrollUpdateDrag(g_ai_chatScroll, ly); if((mstate & 1) == 0) { AiScrollEndDrag(g_ai_chatScroll); g_ai_chatScrollDragging = false; } Ai_RenderAll(); ChartRedraw(); return; } //--- Handle editor scrollbar drag if(g_ai_editorScrollDragging) { AiScrollUpdateDrag(g_ai_editor.scroll, ly); if((mstate & 1) == 0) { AiScrollEndDrag(g_ai_editor.scroll); g_ai_editorScrollDragging = false; } Ai_RenderAll(); ChartRedraw(); return; } //--- Handle search scrollbar drag if(g_ai_searchScrollDragging) { AiScrollUpdateDrag(g_ai_searchScroll, ly); if((mstate & 1) == 0) { AiScrollEndDrag(g_ai_searchScroll); g_ai_searchScrollDragging = false; } Ai_RenderAll(); ChartRedraw(); return; } //--- Compute current hover code int newHov = Ai_HitTest(lx, ly); //--- Auto-show small history popup on hover when sidebar collapsed if(!g_ai_sidebarExpanded) { if(newHov == AI_HOV_HISTORY && !g_ai_showSmallHistory) { g_ai_showSmallHistory = true; g_ai_showSearch = false; } else if(g_ai_showSmallHistory) { //--- Hide popup when leaving anchor and popup body int hbL, hbT, hbR, hbB; Ai_GetSidebarBtnRect(3, hbL, hbT, hbR, hbB); bool inAnchor = AiPointInRect(lx, ly, hbL, hbT, hbR - hbL, hbB - hbT); bool inPopup = AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT, g_ai_popupR - g_ai_popupL, g_ai_popupB - g_ai_popupT); if(!inAnchor && !inPopup) g_ai_showSmallHistory = false; } } //--- Update chat scrollbar hover state bool chatHovArea = false, chatHovThumb = false; if(AiScrollVisible(g_ai_chatScroll)) { chatHovArea = Ai_PointInChatPane(lx, ly); chatHovThumb = AiScrollHitTestThumb(g_ai_chatScroll, lx, ly); } //--- Update editor scrollbar hover state bool editorHovArea = false, editorHovThumb = false; if(AiScrollVisible(g_ai_editor.scroll)) { editorHovArea = Ai_PointInPromptPane(lx, ly); editorHovThumb = AiScrollHitTestThumb(g_ai_editor.scroll, lx, ly); } //--- Update search scrollbar hover state bool searchHovArea = false, searchHovThumb = false; if(g_ai_showSearch && AiScrollVisible(g_ai_searchScroll)) { searchHovArea = AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT, g_ai_popupR - g_ai_popupL, g_ai_popupB - g_ai_popupT); searchHovThumb = AiScrollHitTestThumb(g_ai_searchScroll, lx, ly); } //--- Track if cursor is over a pencil icon's narrow click rect bool overPencilIcon = false; const int peClickN = ArraySize(g_ai_userEditClickL); for(int piI = 0; piI < peClickN; piI++) { if(lx >= g_ai_userEditClickL[piI] && lx < g_ai_userEditClickR[piI] && ly >= g_ai_userEditClickT[piI] && ly < g_ai_userEditClickB[piI]) { overPencilIcon = true; break; } } //--- Compute whether a redraw is needed for hover state changes bool needRedraw = (newHov != g_ai_hover) || (chatHovArea != g_ai_chatScroll.hoveredArea) || (chatHovThumb != g_ai_chatScroll.hoveredThumb) || (editorHovArea != g_ai_editor.scroll.hoveredArea) || (editorHovThumb != g_ai_editor.scroll.hoveredThumb) || (searchHovArea != g_ai_searchScroll.hoveredArea) || (searchHovThumb != g_ai_searchScroll.hoveredThumb) || (overPencilIcon != g_ai_overPencilIcon); //--- Update tooltip when hover code changes if(newHov != g_ai_hover) { string tip = ""; if(newHov == AI_HOV_CLOSE) tip = "Close"; else if(newHov == AI_HOV_THEME) tip = "Toggle theme"; else if(newHov == AI_HOV_TOGGLE) tip = g_ai_sidebarExpanded ? "Collapse sidebar" : "Expand sidebar"; else if(newHov == AI_HOV_NEW_CHAT) tip = "New chat"; else if(newHov == AI_HOV_CLEAR) tip = "Clear current chat"; else if(newHov == AI_HOV_HISTORY) tip = "Recent chats"; else if(newHov == AI_HOV_SEARCH) tip = "Search chats"; else if(newHov == AI_HOV_SIGNAL) tip = "Run selected action"; else if(newHov == AI_HOV_SIGNAL_DD) tip = "Choose action"; else if(newHov == AI_HOV_SEND) tip = Ai_PromptIsEmpty() ? "Type a message to send" : "Send (Shift+Enter)"; else if(newHov == AI_HOV_REGEN) tip = "Regenerate response"; else if(newHov == AI_HOV_EXPORT) tip = "Export chat to file"; else if(newHov == AI_HOV_SCROLL_FAB) tip = "Scroll to latest"; else if(newHov >= AI_HOV_USER_EDIT_BASE && newHov < AI_HOV_USER_EDIT_BASE + 100) tip = "Edit this prompt"; ObjectSetString(0, AI_CANVAS_NAME_MAIN, OBJPROP_TOOLTIP, (StringLen(tip) > 0) ? tip : "\n"); } //--- Commit hover state to globals g_ai_hover = newHov; g_ai_chatScroll.hoveredArea = chatHovArea; g_ai_chatScroll.hoveredThumb = chatHovThumb; g_ai_chatScroll.hover = chatHovArea || chatHovThumb; g_ai_editor.scroll.hoveredArea = editorHovArea; g_ai_editor.scroll.hoveredThumb = editorHovThumb; g_ai_editor.scroll.hover = editorHovArea || editorHovThumb; g_ai_searchScroll.hoveredArea = searchHovArea; g_ai_searchScroll.hoveredThumb = searchHovThumb; g_ai_searchScroll.hover = searchHovArea || searchHovThumb; g_ai_overPencilIcon = overPencilIcon; //--- Toggle chart mouse scroll based on cursor inside dashboard const bool overDashboard = (lx >= 0 && lx < Ai_DashboardW() && ly >= 0 && ly < Ai_DashboardH()); if(!g_ai_chatScrollDragging && !g_ai_editorScrollDragging && !g_ai_searchScrollDragging) ChartSetInteger(0, CHART_MOUSE_SCROLL, !overDashboard); //--- Detect fresh press/release transitions static int prevMouseState = 0; bool freshPress = (prevMouseState == 0 && mstate == 1); bool freshRelease = (prevMouseState == 1 && mstate == 0); prevMouseState = mstate; //--- Handle fresh mouse button press if(freshPress) { //--- Footer dropdown handling if(g_ai_showFooterDropdown) { bool inDdBody = AiPointInRect(lx, ly, g_ai_footerDdL, g_ai_footerDdT, g_ai_footerDdR - g_ai_footerDdL, g_ai_footerDdB - g_ai_footerDdT); bool onAnchor = (newHov == AI_HOV_SIGNAL || newHov == AI_HOV_SIGNAL_DD); bool onItem = (newHov >= AI_HOV_FOOTER_DD_ITEM_BASE && newHov < AI_HOV_FOOTER_DD_ITEM_BASE + 100); //--- Click on item dispatches action if(onItem) { Ai_HandleAction(newHov); return; } //--- Click outside body and anchor closes dropdown if(!inDdBody && !onAnchor) { g_ai_showFooterDropdown = false; Ai_RenderAll(); ChartRedraw(); return; } //--- Click inside body but not on item - swallow if(inDdBody) { return; } } //--- Search/history popup handling if(g_ai_showSearch || g_ai_showSmallHistory) { bool inPopupBody = AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT, g_ai_popupR - g_ai_popupL, g_ai_popupB - g_ai_popupT); bool onAnchorS = (newHov == AI_HOV_SEARCH); bool onAnchorH = (newHov == AI_HOV_HISTORY); bool onPopupRow = (newHov >= AI_HOV_SMALL_CHAT_BASE && newHov < AI_HOV_SMALL_DEL_BASE + 100); //--- Search popup scrollbar thumb takes priority if(g_ai_showSearch && AiScrollVisible(g_ai_searchScroll) && AiScrollHitTestThumb(g_ai_searchScroll, lx, ly)) { AiScrollBeginDrag(g_ai_searchScroll, ly); g_ai_searchScrollDragging = true; Ai_RenderAll(); ChartRedraw(); return; } //--- Click on popup row dispatches action if(onPopupRow) { Ai_HandleAction(newHov); return; } //--- Click on search input box focuses search editor if(g_ai_showSearch && g_ai_popupSearchR > g_ai_popupSearchL && AiPointInRect(lx, ly, g_ai_popupSearchL, g_ai_popupSearchT, g_ai_popupSearchR - g_ai_popupSearchL, g_ai_popupSearchB - g_ai_popupSearchT)) { if(g_ai_editor.focused) g_ai_editor.focused = false; bool wasSF = g_ai_searchEditor.focused; g_ai_searchEditor.focused = true; g_ai_searchEditor.SetCaretFromMouse( lx - g_ai_popupSearchL - 4 - g_ai_searchEditor.padX, ly - g_ai_popupSearchT - 4 - g_ai_searchEditor.padY); if(!wasSF) Ai_BeginKeyboardOverride(); Ai_RenderAll(); ChartRedraw(); return; } //--- Click outside body and anchors closes popup if(!inPopupBody && !onAnchorS && !onAnchorH) { g_ai_showSearch = false; g_ai_showSmallHistory = false; if(g_ai_searchEditor.focused) { g_ai_searchEditor.focused = false; if(!g_ai_editor.focused) Ai_EndKeyboardOverride(); } Ai_RenderAll(); ChartRedraw(); return; } //--- Click inside body but unhandled - swallow if(inPopupBody) { return; } } //--- Test scrollbar thumbs bool editorThumbDown = AiScrollVisible(g_ai_editor.scroll) && AiScrollHitTestThumb(g_ai_editor.scroll, lx, ly); bool chatThumbDown = AiScrollVisible(g_ai_chatScroll) && AiScrollHitTestThumb(g_ai_chatScroll, lx, ly); //--- Begin editor scrollbar drag if(editorThumbDown) { AiScrollBeginDrag(g_ai_editor.scroll, ly); g_ai_editorScrollDragging = true; needRedraw = true; } //--- Begin chat scrollbar drag else if(chatThumbDown) { AiScrollBeginDrag(g_ai_chatScroll, ly); g_ai_chatScrollDragging = true; needRedraw = true; } //--- Begin dashboard drag from header else if(newHov == AI_HOV_DRAG) { g_ai_dragging = true; g_ai_dragOffsetX = lx; g_ai_dragOffsetY = ly; if(g_ai_editor.focused) { g_ai_editor.focused = false; Ai_EndKeyboardOverride(); needRedraw = true; } } //--- Generic click handling else { //--- Focus editor on prompt pane click bool wasFocused = g_ai_editor.focused; if(Ai_PointInPromptPane(lx, ly)) { if(g_ai_searchEditor.focused) g_ai_searchEditor.focused = false; g_ai_editor.focused = true; int pL, pT, pR, pB; Ai_GetPromptPaneRect(pL, pT, pR, pB); g_ai_editor.SetCaretFromMouse(lx - pL - 4 - g_ai_editor.padX, ly - pT - 4 - g_ai_editor.padY); if(!wasFocused) Ai_BeginKeyboardOverride(); needRedraw = true; } else { //--- Click outside prompt blurs editor if(wasFocused) { g_ai_editor.focused = false; if(!g_ai_searchEditor.focused) Ai_EndKeyboardOverride(); needRedraw = true; } } //--- Dispatch action for non-prompt hits if(newHov != AI_HOV_NONE && newHov != AI_HOV_DRAG && !Ai_PointInPromptPane(lx, ly)) { Ai_HandleAction(newHov); return; } } } //--- Handle fresh mouse button release if(freshRelease) { g_ai_dragging = false; if(g_ai_chatScrollDragging) { AiScrollEndDrag(g_ai_chatScroll); g_ai_chatScrollDragging = false; needRedraw = true; } if(g_ai_editorScrollDragging) { AiScrollEndDrag(g_ai_editor.scroll); g_ai_editorScrollDragging = false; needRedraw = true; } } //--- Trigger redraw if any state changed if(needRedraw) { Ai_RenderAll(); ChartRedraw(); } return; } //--- Handle mouse wheel events if(id == CHARTEVENT_MOUSE_WHEEL) { //--- Decode wheel coords and delta const int mx = (int)(short)lparam; const int my = (int)(short)(lparam >> 16); const int delta = (int)dparam; int lx, ly; Ai_ChartToCanvas(mx, my, lx, ly); //--- Search popup takes priority when open and hovered if(g_ai_showSearch && AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT, g_ai_popupR - g_ai_popupL, g_ai_popupB - g_ai_popupT) && AiScrollVisible(g_ai_searchScroll)) { ChartSetInteger(0, CHART_MOUSE_SCROLL, false); AiScrollByWheel(g_ai_searchScroll, delta, 30); Ai_RenderAll(); ChartRedraw(); return; } //--- Editor scroll when over prompt pane if(Ai_PointInPromptPane(lx, ly) && AiScrollVisible(g_ai_editor.scroll)) { ChartSetInteger(0, CHART_MOUSE_SCROLL, false); AiScrollByWheel(g_ai_editor.scroll, delta, 30); Ai_RenderAll(); ChartRedraw(); return; } //--- Chat scroll when over chat pane if(Ai_PointInChatPane(lx, ly) && AiScrollVisible(g_ai_chatScroll)) { ChartSetInteger(0, CHART_MOUSE_SCROLL, false); AiScrollByWheel(g_ai_chatScroll, delta, 30); Ai_RenderAll(); ChartRedraw(); return; } //--- Restore chart mouse scroll outside dashboard ChartSetInteger(0, CHART_MOUSE_SCROLL, true); return; } //--- Re-render on chart resize/scroll if(id == CHARTEVENT_CHART_CHANGE) { Ai_RenderAll(); ChartRedraw(); return; } } //+------------------------------------------------------------------+ //| Timer event handler for blink and toast updates | //+------------------------------------------------------------------+ void Ai_OnTimer() { //--- Bail when dashboard is hidden if(!g_ai_dashboardVisible) return; //--- Track what changed for minimum redraw decision bool editorBlinkChanged = false; bool searchBlinkChanged = false; bool toastChanged = false; //--- Update editor blink state if(g_ai_editor.focused) { bool prev = g_ai_editor.blinkOn; g_ai_editor.UpdateBlink(); if(g_ai_editor.blinkOn != prev) editorBlinkChanged = true; } //--- Update search editor blink state if(g_ai_searchEditor.focused) { bool prev2 = g_ai_searchEditor.blinkOn; g_ai_searchEditor.UpdateBlink(); if(g_ai_searchEditor.blinkOn != prev2) searchBlinkChanged = true; } //--- Toast auto-dismiss after expiry bool toastAlive = false; if(StringLen(g_ai_toastText) > 0) { if(GetTickCount64() > g_ai_toastExpiryMs) { g_ai_toastText = ""; g_ai_toastExpiryMs = 0; toastChanged = true; } else { //--- Toast still alive - flag for progress bar redraw toastAlive = true; } } //--- Choose minimum redraw path based on what changed if(toastChanged || toastAlive || searchBlinkChanged) { Ai_RenderAll(); ChartRedraw(); } else if(editorBlinkChanged) { //--- Fall back to full render when overlays are present const bool hasOverlay = g_ai_showSearch || g_ai_showFooterDropdown || g_ai_showSmallHistory || (StringLen(g_ai_toastText) > 0); if(hasOverlay) { Ai_RenderAll(); ChartRedraw(); } else if(g_ai_canvPromptExists) { //--- Fast path: render only prompt overlay Ai_RenderPromptPaneOverlay(); ChartRedraw(); } else { //--- Overlay not available - fall back to legacy path Ai_RenderPromptPane(); g_ai_canvMain.Update(); ChartRedraw(); } } } #endif // AI_CANVAS_INTERACT_MQH