1328 lines
No EOL
50 KiB
MQL5
1328 lines
No EOL
50 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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 |