/* =========================================================================== IceTech GPL Source Code Copyright (C) 2026 Justin Marshall This file is part of the IceTech GPL Source Code (?IceTech Source Code?). IceTech Source Code is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. IceTech Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with IceTech Source Code. If not, see . In addition, the IceTech Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the IceTech Source Code. If not, please request a copy in writing from id Software at the address below. If you have questions concerning this license or the applicable additional terms, you may contact in writing Justin Marshall, justinmarshall20@gmail.com =========================================================================== */ #include "precompiled.h" #pragma hdrstop #include #include #include "../../sys/win32/rc/Common_resource.h" #include "../../sys/win32/rc/ScriptEditor_resource.h" #include "../radiant/qe3.h" #include "../radiant/Radiant.h" #include "../comafx/DialogGoToLine.h" #include "DialogScriptEditor.h" #include "../radiant/XYWnd.h" #ifdef ID_DEBUG_MEMORY #undef new #undef DEBUG_NEW #define DEBUG_NEW new #endif #ifndef IDC_SCRIPTEDITOR_BUTTON_FIND #define IDC_SCRIPTEDITOR_BUTTON_FIND 0x7C10 #endif #ifndef IDC_SCRIPTEDITOR_BUTTON_REPLACE #define IDC_SCRIPTEDITOR_BUTTON_REPLACE 0x7C11 #endif #ifndef IDC_SCRIPTEDITOR_BUTTON_GOTOLINE #define IDC_SCRIPTEDITOR_BUTTON_GOTOLINE 0x7C12 #endif #ifndef IDC_SCRIPTEDITOR_BUTTON_INTELLISENSE #define IDC_SCRIPTEDITOR_BUTTON_INTELLISENSE 0x7C13 #endif #ifndef IDC_SCRIPTEDITOR_INTELLISENSE_LIST #define IDC_SCRIPTEDITOR_INTELLISENSE_LIST 0x7C14 #endif #ifndef IDC_SCRIPTEDITOR_TITLE #define IDC_SCRIPTEDITOR_TITLE 0x7C15 #endif #ifndef IDC_SCRIPTEDITOR_PATH #define IDC_SCRIPTEDITOR_PATH 0x7C16 #endif #ifndef IDC_SCRIPTEDITOR_STATUS #define IDC_SCRIPTEDITOR_STATUS 0x7C17 #endif #ifndef IDC_SCRIPTEDITOR_LANGUAGE #define IDC_SCRIPTEDITOR_LANGUAGE 0x7C18 #endif #ifndef IDC_SCRIPTEDITOR_BUTTON_NEW #define IDC_SCRIPTEDITOR_BUTTON_NEW 0x7C08 #endif #ifndef IDC_SCRIPTEDITOR_BUTTON_OPEN #define IDC_SCRIPTEDITOR_BUTTON_OPEN 0x7C19 #endif #ifndef IDC_SCRIPTEDITOR_BUTTON_SAVEAS #define IDC_SCRIPTEDITOR_BUTTON_SAVEAS 0x7C1A #endif #ifndef IDC_SCRIPTEDITOR_SIGNATURE_HELP #define IDC_SCRIPTEDITOR_SIGNATURE_HELP 0x7C1B #endif #ifndef IDC_SCRIPTBROWSER_FILTER #define IDC_SCRIPTBROWSER_FILTER 0x7C50 #endif #ifndef IDC_SCRIPTBROWSER_LIST #define IDC_SCRIPTBROWSER_LIST 0x7C51 #endif #ifndef IDC_SCRIPTBROWSER_LOCAL #define IDC_SCRIPTBROWSER_LOCAL 0x7C52 #endif #ifndef IDC_SCRIPTBROWSER_PANEL #define IDC_SCRIPTBROWSER_PANEL 0x7C53 #endif #ifndef ID_FILE_NEW #define ID_FILE_NEW 0xE100 #endif #ifndef ID_FILE_SAVE #define ID_FILE_SAVE 0xE103 #endif #ifndef ID_FILE_SAVE_AS #define ID_FILE_SAVE_AS 0xE104 #endif #ifndef ID_SCRIPTEDITOR_INDEX_TIMER #define ID_SCRIPTEDITOR_INDEX_TIMER 0x7C80 #endif #ifndef IDC_SCRIPTSAVE_PATH #define IDC_SCRIPTSAVE_PATH 0x7C70 #endif #ifndef IDC_SCRIPTSAVE_LOCAL #define IDC_SCRIPTSAVE_LOCAL 0x7C71 #endif #ifndef IDC_SCRIPTSAVE_PANEL #define IDC_SCRIPTSAVE_PANEL 0x7C72 #endif #ifndef WM_SCRIPTEDITOR_DEFERRED_INTELLISENSE #define WM_SCRIPTEDITOR_DEFERRED_INTELLISENSE (WM_USER + 0x531) #endif #ifndef WM_SCRIPTEDITOR_INTELLISENSE_KEY #define WM_SCRIPTEDITOR_INTELLISENSE_KEY (WM_USER + 0x532) #endif #ifndef EN_SELCHANGE #define EN_SELCHANGE 0x0702 #endif static const COLORREF SE_DARK_BG = RGB(14, 16, 20); static const COLORREF SE_DARK_PANEL = RGB(22, 25, 31); static const COLORREF SE_DARK_PANEL_2 = RGB(28, 32, 40); static const COLORREF SE_DARK_EDIT = RGB(11, 13, 18); static const COLORREF SE_DARK_EDIT_LINE = RGB(15, 18, 24); static const COLORREF SE_DARK_GUTTER = RGB(18, 21, 27); static const COLORREF SE_DARK_BORDER = RGB(58, 66, 78); static const COLORREF SE_DARK_TEXT = RGB(226, 232, 240); static const COLORREF SE_DARK_MUTED = RGB(148, 163, 184); static const COLORREF SE_DARK_DISABLED = RGB(95, 105, 122); static const COLORREF SE_DARK_ACCENT = RGB(87, 166, 255); static const COLORREF SE_DARK_WARNING = RGB(251, 191, 36); static const COLORREF SE_DARK_SELECTION = RGB(37, 83, 128); static const COLORREF SE_DARK_SELECTION_TEXT = RGB(245, 249, 255); static const COLORREF SE_DARK_COMMENT = RGB(128, 190, 106); static const COLORREF SE_DARK_STRING = RGB(206, 145, 120); static const COLORREF SE_DARK_ALT_STRING = RGB(214, 157, 133); static const COLORREF SE_DARK_NUMBER = RGB(181, 206, 168); static const COLORREF SE_DARK_KEYWORD = RGB(86, 156, 214); static const COLORREF SE_DARK_TYPE = RGB(78, 201, 176); static const COLORREF SE_DARK_FUNCTION = RGB(220, 220, 170); static const COLORREF SE_DARK_OPERATOR = RGB(212, 212, 212); static const COLORREF SE_DARK_LINE_NUMBER = RGB(93, 101, 116); static const COLORREF SE_DARK_CARET = RGB(245, 249, 255); static const int SE_TAB_SIZE = 4; static const int SE_MIN_EDITOR_WIDTH = 360; static const int SE_MIN_EDITOR_HEIGHT = 220; static const UINT_PTR SE_INTELLISENSE_TIMER_ID = 0x7CF0; #ifndef CLEARTYPE_NATURAL_QUALITY #define CLEARTYPE_NATURAL_QUALITY 6 #endif static bool ScriptEditorCreateFontPixels(CFont& font, const char* faceName, int pointSize, bool mono) { if (font.GetSafeHandle()) { font.DeleteObject(); } HDC hDC = ::GetDC(NULL); int dpiY = hDC ? ::GetDeviceCaps(hDC, LOGPIXELSY) : 96; if (hDC) { ::ReleaseDC(NULL, hDC); } LOGFONTA lf; memset(&lf, 0, sizeof(lf)); lf.lfHeight = -MulDiv(pointSize, dpiY, 72); lf.lfWeight = FW_NORMAL; lf.lfCharSet = ANSI_CHARSET; lf.lfOutPrecision = OUT_TT_PRECIS; lf.lfClipPrecision = CLIP_DEFAULT_PRECIS; lf.lfQuality = CLEARTYPE_NATURAL_QUALITY; lf.lfPitchAndFamily = mono ? (FIXED_PITCH | FF_MODERN) : (VARIABLE_PITCH | FF_SWISS); const char* fallbackFace = mono ? "Consolas" : "Segoe UI"; idStr::Copynz(lf.lfFaceName, faceName && faceName[0] ? faceName : fallbackFace, sizeof(lf.lfFaceName)); return font.CreateFontIndirectA(&lf) != FALSE; } static UINT FindDialogMessage = ::RegisterWindowMessage(FINDMSGSTRING); typedef struct scriptEventInfo_s { idStr name; idStr parms; idStr help; } scriptEventInfo_t; static idList scriptEvents; typedef struct scriptCompletionInfo_s { idStr name; idStr insertText; idStr kind; idStr source; idStr help; int weight; } scriptCompletionInfo_t; static idList scriptCompletions; static idList scriptVirtualFiles; static bool scriptCoreBuilt = false; static bool scriptDatabaseBuilt = false; static bool scriptVirtualFileListBuilt = false; static DialogScriptEditor* g_StandaloneScriptEditor = NULL; DialogScriptEditor* DialogScriptEditor::primaryEditor = NULL; static const char* scriptKeywords[] = { "accum", "ai", "boolean", "break", "case", "catch", "const", "continue", "default", "do", "else", "entity", "float", "for", "function", "if", "integer", "namespace", "object", "return", "scriptEvent", "string", "sys", "thread", "true", "false", "vector", "void", "while", NULL }; static const char* guiKeywords[] = { "windowDef", "renderDef", "choiceDef", "editDef", "listDef", "bindDef", "onAction", "onInit", "onTime", "onNamedEvent", "rect", "visible", "text", "forecolor", "backcolor", "matcolor", "background", "font", "textscale", "float", "vec4", NULL }; static void ScriptEditorActivateDockParent(DialogScriptEditor* editor) { if (!editor || !editor->GetSafeHwnd()) { return; } for (CWnd* parent = editor->GetParent(); parent && parent->GetSafeHwnd(); parent = parent->GetParent()) { if (parent->IsKindOf(RUNTIME_CLASS(CXYDockWnd))) { static_cast(parent)->SelectScriptEditorTab(); return; } } } static void ScriptEditorFillRect(HDC hDC, const RECT& rc, COLORREF color) { HBRUSH brush = ::CreateSolidBrush(color); ::FillRect(hDC, &rc, brush); ::DeleteObject(brush); } static void ScriptEditorFrameRect(HDC hDC, const RECT& rc, COLORREF color) { HBRUSH brush = ::CreateSolidBrush(color); ::FrameRect(hDC, &rc, brush); ::DeleteObject(brush); } static void ScriptEditorApplyNativeDarkTheme(HWND hWnd) { if (!hWnd) { return; } typedef HRESULT(WINAPI* SetWindowThemeProc)(HWND, LPCWSTR, LPCWSTR); typedef BOOL(WINAPI* AllowDarkModeForWindowProc)(HWND, BOOL); static HMODULE hUxTheme = ::LoadLibraryA("uxtheme.dll"); static SetWindowThemeProc pSetWindowTheme = hUxTheme ? (SetWindowThemeProc)::GetProcAddress(hUxTheme, "SetWindowTheme") : NULL; static AllowDarkModeForWindowProc pAllowDarkModeForWindow = hUxTheme ? (AllowDarkModeForWindowProc)::GetProcAddress(hUxTheme, MAKEINTRESOURCEA(133)) : NULL; if (pAllowDarkModeForWindow) { pAllowDarkModeForWindow(hWnd, TRUE); } if (pSetWindowTheme) { pSetWindowTheme(hWnd, L"DarkMode_Explorer", NULL); } ::SendMessage(hWnd, WM_THEMECHANGED, 0, 0); } static CString ScriptEditorNormalizeText(const char* text) { CString out = text ? text : ""; out.Replace("\r\n", "\n"); out.Replace("\r", "\n"); out.Replace("\v", "\n"); return out; } static CString ScriptEditorToCRLF(const CString& text) { CString out = text; out.Replace("\r\n", "\n"); out.Replace("\r", "\n"); out.Replace("\n", "\r\n"); return out; } static bool ScriptEditorIsIdentifierChar(int ch) { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '#'; } static bool ScriptEditorIsIdentifierStart(int ch) { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' || ch == '#'; } static bool ScriptEditorIsSpaceOrTab(int ch) { return ch == ' ' || ch == '\t'; } static bool ScriptEditorIsTypeKeyword(const CString& word) { return word == "void" || word == "float" || word == "integer" || word == "boolean" || word == "string" || word == "vector" || word == "entity" || word == "object" || word == "windowDef" || word == "vec4"; } static int ScriptEditorUpperChar(int ch) { if (ch >= 'a' && ch <= 'z') { return ch - 'a' + 'A'; } return ch; } static int ScriptEditorCompareNoCase(const CString& a, const CString& b) { CString aa = a; CString bb = b; aa.MakeLower(); bb.MakeLower(); return aa.Compare(bb); } static bool ScriptEditorIsKeywordName(const char* name) { if (!name || !name[0]) { return false; } for (int i = 0; scriptKeywords[i]; i++) { if (idStr::Icmp(scriptKeywords[i], name) == 0) { return true; } } for (int i = 0; guiKeywords[i]; i++) { if (idStr::Icmp(guiKeywords[i], name) == 0) { return true; } } return false; } static CString ScriptEditorNormalizeVirtualPath(const char* path) { CString out = path ? path : ""; out.TrimLeft(); out.TrimRight(); out.Replace("\\", "/"); while (out.Find("//") >= 0) { out.Replace("//", "/"); } return out; } static bool ScriptEditorHasExtension(const CString& path, const char* extension) { CString ext = extension ? extension : ""; CString p = path; ext.MakeLower(); p.MakeLower(); if (ext.GetLength() <= 0) { return true; } if (ext[0] != '.') { ext = "." + ext; } return p.Right(ext.GetLength()) == ext; } static void ScriptEditorAddVirtualFile(const char* basePath, const char* fileName) { CString file = ScriptEditorNormalizeVirtualPath(fileName); CString base = ScriptEditorNormalizeVirtualPath(basePath); while (file.GetLength() > 0 && (file[0] == '/' || file[0] == '\\')) { file.Delete(0, 1); } while (base.GetLength() > 0 && (base[base.GetLength() - 1] == '/' || base[base.GetLength() - 1] == '\\')) { base.Delete(base.GetLength() - 1, 1); } if (file.IsEmpty()) { return; } if (!ScriptEditorHasExtension(file, ".script") && !ScriptEditorHasExtension(file, ".gui")) { return; } if (!base.IsEmpty()) { CString fileLower = file; CString baseLower = base; fileLower.MakeLower(); baseLower.MakeLower(); const CString baseWithSlash = baseLower + "/"; if (fileLower != baseLower && fileLower.Left(baseWithSlash.GetLength()) != baseWithSlash) { file = base + "/" + file; file = ScriptEditorNormalizeVirtualPath(file); } } while (file.GetLength() > 0 && file[0] == '/') { file.Delete(0, 1); } for (int i = 0; i < scriptVirtualFiles.Num(); i++) { if (scriptVirtualFiles[i].Icmp(file) == 0) { return; } } scriptVirtualFiles.Append(idStr(file)); } static void ScriptEditorListVirtualFilesFromTree(const char* basePath, const char* extension) { if (!fileSystem || !basePath || !extension) { return; } idFileList* treeList = fileSystem->ListFilesTree(basePath, extension, true); if (treeList) { for (int i = 0; i < treeList->GetNumFiles(); i++) { ScriptEditorAddVirtualFile(basePath, treeList->GetFile(i)); } fileSystem->FreeFileList(treeList); } // Some idFileSystem implementations only expose pk4/pk5-backed files through // ListFiles(..., fullRelativePath = true), so collect both ways. idFileList* flatList = fileSystem->ListFiles(basePath, extension, true, true); if (flatList) { for (int i = 0; i < flatList->GetNumFiles(); i++) { ScriptEditorAddVirtualFile(basePath, flatList->GetFile(i)); } fileSystem->FreeFileList(flatList); } } static void ScriptEditorAddCompletion(const char* name, const char* insertText, const char* kind, const char* source, const char* help, int weight) { if (!name || !name[0]) { return; } if (idStr::Length(name) > 128) { return; } if (!ScriptEditorIsIdentifierStart(name[0])) { return; } for (const char* p = name; *p; p++) { if (!ScriptEditorIsIdentifierChar(*p) && *p != ':') { return; } } const char* completionKind = kind && kind[0] ? kind : "symbol"; for (int i = 0; i < scriptCompletions.Num(); i++) { if (scriptCompletions[i].name.Icmp(name) == 0 && scriptCompletions[i].kind.Icmp(completionKind) == 0) { if (weight > scriptCompletions[i].weight) { scriptCompletions[i].insertText = insertText && insertText[0] ? insertText : name; scriptCompletions[i].source = source && source[0] ? source : ""; scriptCompletions[i].help = help && help[0] ? help : ""; scriptCompletions[i].weight = weight; } return; } } scriptCompletionInfo_t completion; completion.name = name; completion.insertText = insertText && insertText[0] ? insertText : name; completion.kind = completionKind; completion.source = source && source[0] ? source : ""; completion.help = help && help[0] ? help : ""; completion.weight = weight; scriptCompletions.Append(completion); } static void ScriptEditorRemoveCompletionsFromSource(const char* sourceName) { if (!sourceName || !sourceName[0]) { return; } for (int i = scriptCompletions.Num() - 1; i >= 0; i--) { if (scriptCompletions[i].source.Icmp(sourceName) == 0) { scriptCompletions.RemoveIndex(i); } } } static int ScriptEditorFindNextNonSpace(const CString& text, int index) { while (index < text.GetLength() && (text[index] == ' ' || text[index] == '\t' || text[index] == '\r' || text[index] == '\n')) { index++; } return index; } static CString ScriptEditorReadIdentifierAt(const CString& text, int index, int* endIndex = NULL) { CString word; if (index < 0 || index >= text.GetLength() || !ScriptEditorIsIdentifierStart(text[index])) { if (endIndex) { *endIndex = index; } return word; } int i = index; while (i < text.GetLength() && ScriptEditorIsIdentifierChar(text[i])) { word += text[i]; i++; } if (endIndex) { *endIndex = i; } return word; } static CString ScriptEditorPreviousIdentifier(const CString& text, int index) { int i = index - 1; while (i >= 0 && (text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n')) { i--; } if (i < 0 || !ScriptEditorIsIdentifierChar(text[i])) { return ""; } int end = i + 1; while (i >= 0 && ScriptEditorIsIdentifierChar(text[i])) { i--; } return text.Mid(i + 1, end - (i + 1)); } static void ScriptEditorScanTextForCompletions(const char* sourceName, const char* rawText) { CString text = ScriptEditorNormalizeText(rawText); CString previousWord; bool inString = false; bool inLiteral = false; bool inLineComment = false; bool inBlockComment = false; for (int i = 0; i < text.GetLength(); ) { char ch = text[i]; char next = (i + 1 < text.GetLength()) ? text[i + 1] : '\0'; if (inLineComment) { if (ch == '\n') { inLineComment = false; } i++; continue; } if (inBlockComment) { if (ch == '*' && next == '/') { inBlockComment = false; i += 2; } else { i++; } continue; } if (inString) { if (ch == '"' && (i == 0 || text[i - 1] != '\\')) { inString = false; } i++; continue; } if (inLiteral) { if (ch == '\'' && (i == 0 || text[i - 1] != '\\')) { inLiteral = false; } i++; continue; } if (ch == '/' && next == '/') { inLineComment = true; i += 2; continue; } if (ch == '/' && next == '*') { inBlockComment = true; i += 2; continue; } if (ch == '"') { inString = true; i++; continue; } if (ch == '\'') { inLiteral = true; i++; continue; } if (ScriptEditorIsIdentifierStart(ch)) { int wordEnd = i; CString word = ScriptEditorReadIdentifierAt(text, i, &wordEnd); int after = ScriptEditorFindNextNonSpace(text, wordEnd); char afterChar = after < text.GetLength() ? text[after] : '\0'; CString prev = previousWord; CString prevLower = prev; prevLower.MakeLower(); if (!ScriptEditorIsKeywordName(word) && word.GetLength() >= 2) { if (afterChar == '(') { CString signature = word; int sigEnd = after; int depth = 0; for (int s = after; s < text.GetLength() && s - after < 240; s++) { char sigCh = text[s]; if (sigCh == '(') { depth++; } else if (sigCh == ')') { depth--; if (depth <= 0) { sigEnd = s; break; } } else if (sigCh == '\n' || sigCh == ';' || sigCh == '{') { sigEnd = s - 1; break; } sigEnd = s; } if (sigEnd >= after) { signature += text.Mid(after, sigEnd - after + 1); } signature.Replace("\n", " "); signature.Replace("\t", " "); ScriptEditorAddCompletion(word, word, "function", sourceName, signature, 85); } else if (prevLower == "object") { ScriptEditorAddCompletion(word, word, "object", sourceName, sourceName, 80); } else if (prevLower == "namespace") { ScriptEditorAddCompletion(word, word, "namespace", sourceName, sourceName, 70); } else if (ScriptEditorIsTypeKeyword(prev)) { ScriptEditorAddCompletion(word, word, "variable", sourceName, sourceName, 55); } else if (word.GetLength() >= 3) { ScriptEditorAddCompletion(word, word, "symbol", sourceName, sourceName, 20); } } previousWord = word; i = wordEnd; continue; } i++; } } static void ScriptEditorSubclassButton(CWnd& wnd); static bool ScriptEditorIsModalInputMessage(UINT message) { return (message >= WM_KEYFIRST && message <= WM_KEYLAST) || (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST) || message == WM_NCLBUTTONDOWN || message == WM_NCRBUTTONDOWN || message == WM_NCMBUTTONDOWN || message == WM_NCLBUTTONDBLCLK || message == WM_NCRBUTTONDBLCLK || message == WM_NCMBUTTONDBLCLK; } static bool ScriptEditorMessageTargetsWindow(HWND root, HWND target) { return root && target && (target == root || ::IsChild(root, target)); } static void ScriptEditorCenterChildRectOnOwner(CRect& rect, CWnd* owner) { if (!owner || !owner->GetSafeHwnd()) { return; } CRect client; owner->GetClientRect(client); int wantedW = rect.Width(); int wantedH = rect.Height(); const int margin = 18; if (wantedW > client.Width() - margin * 2) { wantedW = max(260, client.Width() - margin * 2); } if (wantedH > client.Height() - margin * 2) { wantedH = max(160, client.Height() - margin * 2); } int x = client.left + (client.Width() - wantedW) / 2; int y = client.top + (client.Height() - wantedH) / 2; if (x < client.left + margin) { x = client.left + margin; } if (y < client.top + margin) { y = client.top + margin; } rect.SetRect(x, y, x + wantedW, y + wantedH); } //============================================================================= // Script file browser for files exposed by the Doom filesystem, including pk5s. // This is a lightweight modal CWnd rather than a resource-template dialog so the // editor can be dropped into existing tools projects without adding new .rc data. //============================================================================= class CScriptFileBrowserWnd : public CWnd { DECLARE_DYNAMIC(CScriptFileBrowserWnd) public: CScriptFileBrowserWnd(); virtual ~CScriptFileBrowserWnd(); INT_PTR DoModal(CWnd* owner, CString& selectedPath); protected: afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg BOOL OnEraseBkgnd(CDC* pDC); afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor); afx_msg void OnPaint(); afx_msg void OnFilterChanged(); afx_msg void OnOpen(); afx_msg void OnCancel(); afx_msg void OnLocalFile(); afx_msg void OnListDoubleClick(); afx_msg void OnClose(); DECLARE_MESSAGE_MAP() private: CStatic m_title; CStatic m_filterLabel; CEdit m_filter; CListBox m_list; CButton m_open; CButton m_cancel; CButton m_local; CFont m_font; CBrush m_backBrush; CBrush m_panelBrush; CBrush m_editBrush; CString m_selectedPath; INT_PTR m_result; bool m_done; private: void LayoutChildren(void); void RebuildList(void); void Finish(INT_PTR result); }; IMPLEMENT_DYNAMIC(CScriptFileBrowserWnd, CWnd) BEGIN_MESSAGE_MAP(CScriptFileBrowserWnd, CWnd) ON_WM_CREATE() ON_WM_SIZE() ON_WM_ERASEBKGND() ON_WM_CTLCOLOR() ON_WM_PAINT() ON_WM_CLOSE() ON_EN_CHANGE(IDC_SCRIPTBROWSER_FILTER, OnFilterChanged) ON_LBN_DBLCLK(IDC_SCRIPTBROWSER_LIST, OnListDoubleClick) ON_BN_CLICKED(IDOK, OnOpen) ON_BN_CLICKED(IDCANCEL, OnCancel) ON_BN_CLICKED(IDC_SCRIPTBROWSER_LOCAL, OnLocalFile) END_MESSAGE_MAP() CScriptFileBrowserWnd::CScriptFileBrowserWnd() : m_result(0) , m_done(false) { m_backBrush.CreateSolidBrush(SE_DARK_BG); m_panelBrush.CreateSolidBrush(SE_DARK_PANEL); m_editBrush.CreateSolidBrush(SE_DARK_EDIT); } CScriptFileBrowserWnd::~CScriptFileBrowserWnd() { } INT_PTR CScriptFileBrowserWnd::DoModal(CWnd* owner, CString& selectedPath) { selectedPath.Empty(); m_selectedPath.Empty(); m_result = 0; m_done = false; if (owner == NULL || !owner->GetSafeHwnd()) { common->Printf("Script Editor: cannot open VFS browser without a valid editor parent.\n"); return IDCANCEL; } CString className = AfxRegisterWndClass( CS_DBLCLKS, ::LoadCursor(NULL, IDC_ARROW), (HBRUSH)m_backBrush.GetSafeHandle(), NULL ); CRect rect(0, 0, 760, 520); ScriptEditorCenterChildRectOnOwner(rect, owner); if (className.IsEmpty()) { common->Printf("Script Editor: failed to register VFS browser window class.\n"); return IDCANCEL; } // This used to be a top-level WS_POPUP window with its owner disabled. // In the Radiant tab host that could leave a separate owner/tool window in // front and make the browser itself impossible to click. Keep it as an // in-tab child overlay instead: no owner disabling, no SetForegroundWindow, // no taskbar/tool-window focus handoff. if (!CreateEx( WS_EX_CLIENTEDGE | WS_EX_CONTROLPARENT, className, "Open Script From PK5 / VFS", WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, rect, owner, IDC_SCRIPTBROWSER_PANEL )) { common->Printf("Script Editor: failed to create in-tab VFS browser window, GetLastError=%lu.\n", ::GetLastError()); return IDCANCEL; } HWND oldFocus = ::GetFocus(); ShowWindow(SW_SHOW); SetWindowPos(&wndTop, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); UpdateWindow(); if (m_filter.GetSafeHwnd()) { m_filter.SetFocus(); } MSG msg; while (!m_done && GetSafeHwnd() && ::GetMessage(&msg, NULL, 0, 0)) { if (ScriptEditorIsModalInputMessage(msg.message) && !ScriptEditorMessageTargetsWindow(GetSafeHwnd(), msg.hwnd)) { if (m_filter.GetSafeHwnd()) { m_filter.SetFocus(); } continue; } if (!IsDialogMessage(&msg)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } } if (oldFocus && ::IsWindow(oldFocus)) { ::SetFocus(oldFocus); } if (GetSafeHwnd()) { DestroyWindow(); } selectedPath = m_selectedPath; return m_result ? m_result : IDCANCEL; } int CScriptFileBrowserWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CWnd::OnCreate(lpCreateStruct) == -1) { return -1; } m_font.CreatePointFont(85, "MS Shell Dlg"); m_title.Create("Open a script from the loaded pk5/dev filesystem", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); m_filterLabel.Create("Filter:", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); m_filter.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL, CRect(0, 0, 0, 0), this, IDC_SCRIPTBROWSER_FILTER); m_list.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | LBS_SORT, CRect(0, 0, 0, 0), this, IDC_SCRIPTBROWSER_LIST); m_open.Create("Open", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, CRect(0, 0, 0, 0), this, IDOK); m_cancel.Create("Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDCANCEL); m_local.Create("Local...", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDC_SCRIPTBROWSER_LOCAL); CWnd* windows[] = { &m_title, &m_filterLabel, &m_filter, &m_list, &m_open, &m_cancel, &m_local }; for (int i = 0; i < sizeof(windows) / sizeof(windows[0]); i++) { if (windows[i] && windows[i]->GetSafeHwnd()) { windows[i]->SetFont(&m_font); } } ScriptEditorSubclassButton(m_open); ScriptEditorSubclassButton(m_cancel); ScriptEditorSubclassButton(m_local); ScriptEditorApplyNativeDarkTheme(m_filter.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(m_list.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(m_open.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(m_cancel.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(m_local.GetSafeHwnd()); RebuildList(); LayoutChildren(); return 0; } void CScriptFileBrowserWnd::LayoutChildren(void) { if (!GetSafeHwnd()) { return; } CRect client; GetClientRect(client); const int margin = 12; const int buttonW = 86; const int buttonH = 25; const int gap = 8; if (m_title.GetSafeHwnd()) { m_title.MoveWindow(margin, 8, client.Width() - margin * 2, 22, TRUE); } if (m_filterLabel.GetSafeHwnd()) { m_filterLabel.MoveWindow(margin, 38, 46, 22, TRUE); } if (m_filter.GetSafeHwnd()) { m_filter.MoveWindow(margin + 52, 38, client.Width() - margin * 2 - 52, 22, TRUE); } if (m_list.GetSafeHwnd()) { m_list.MoveWindow(margin, 68, client.Width() - margin * 2, client.Height() - 68 - 44, TRUE); } int y = client.bottom - 34; int x = client.right - margin - buttonW; if (m_cancel.GetSafeHwnd()) { m_cancel.MoveWindow(x, y, buttonW, buttonH, TRUE); } x -= buttonW + gap; if (m_open.GetSafeHwnd()) { m_open.MoveWindow(x, y, buttonW, buttonH, TRUE); } if (m_local.GetSafeHwnd()) { m_local.MoveWindow(margin, y, buttonW, buttonH, TRUE); } } void CScriptFileBrowserWnd::RebuildList(void) { if (!m_list.GetSafeHwnd()) { return; } CString filter; if (m_filter.GetSafeHwnd()) { m_filter.GetWindowText(filter); } filter.MakeLower(); m_list.ResetContent(); for (int i = 0; i < scriptVirtualFiles.Num(); i++) { CString path = scriptVirtualFiles[i].c_str(); CString pathLower = path; pathLower.MakeLower(); if (filter.IsEmpty() || pathLower.Find(filter) >= 0) { m_list.AddString(path); } } if (m_list.GetCount() > 0) { m_list.SetCurSel(0); } } void CScriptFileBrowserWnd::Finish(INT_PTR result) { m_result = result; m_done = true; } void CScriptFileBrowserWnd::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy); LayoutChildren(); } BOOL CScriptFileBrowserWnd::OnEraseBkgnd(CDC* pDC) { CRect client; GetClientRect(client); pDC->FillSolidRect(client, SE_DARK_BG); return TRUE; } HBRUSH CScriptFileBrowserWnd::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { if (pDC) { pDC->SetBkMode(TRANSPARENT); pDC->SetTextColor(SE_DARK_TEXT); } if (nCtlColor == CTLCOLOR_EDIT || nCtlColor == CTLCOLOR_LISTBOX) { if (pDC) { pDC->SetBkColor(SE_DARK_EDIT); pDC->SetTextColor(SE_DARK_TEXT); } return (HBRUSH)m_editBrush.GetSafeHandle(); } if (nCtlColor == CTLCOLOR_STATIC) { return (HBRUSH)m_backBrush.GetSafeHandle(); } return (HBRUSH)m_backBrush.GetSafeHandle(); } void CScriptFileBrowserWnd::OnPaint() { CPaintDC dc(this); CRect client; GetClientRect(client); dc.FillSolidRect(client, SE_DARK_BG); CRect header(client.left, client.top, client.right, 34); dc.FillSolidRect(header, SE_DARK_PANEL); dc.FillSolidRect(client.left, header.bottom - 1, client.Width(), 1, SE_DARK_BORDER); } void CScriptFileBrowserWnd::OnFilterChanged() { RebuildList(); } void CScriptFileBrowserWnd::OnOpen() { CString typedPath; if (m_filter.GetSafeHwnd()) { m_filter.GetWindowText(typedPath); typedPath.TrimLeft(); typedPath.TrimRight(); typedPath.Replace("\\", "/"); } const bool typedLooksLikePath = !typedPath.IsEmpty() && (typedPath.Find('/') >= 0 || typedPath.Find(':') >= 0 || ScriptEditorHasExtension(typedPath, ".script") || ScriptEditorHasExtension(typedPath, ".gui") || ScriptEditorHasExtension(typedPath, ".txt")); if (typedLooksLikePath) { if (!ScriptEditorHasExtension(typedPath, ".script") && !ScriptEditorHasExtension(typedPath, ".gui") && !ScriptEditorHasExtension(typedPath, ".txt")) { typedPath += ".script"; } m_selectedPath = typedPath; Finish(IDOK); return; } int sel = m_list.GetCurSel(); if (sel != LB_ERR) { m_list.GetText(sel, m_selectedPath); Finish(IDOK); return; } MessageBox("Select a script file, or type a virtual path such as script/foo.script.", "Open Script", MB_OK | MB_ICONINFORMATION); } void CScriptFileBrowserWnd::OnCancel() { Finish(IDCANCEL); } void CScriptFileBrowserWnd::OnLocalFile() { CFileDialog dlg( TRUE, NULL, NULL, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, "Script Files (*.script)|*.script|GUI Files (*.gui)|*.gui|Text Files (*.txt)|*.txt|All Files (*.*)|*.*||", this ); if (dlg.DoModal() == IDOK) { m_selectedPath = dlg.GetPathName(); Finish(IDOK); } } void CScriptFileBrowserWnd::OnListDoubleClick() { OnOpen(); } void CScriptFileBrowserWnd::OnClose() { Finish(IDCANCEL); } //============================================================================= // Save path prompt. Lets users save a new buffer or a pk5-backed override to // a virtual dev path such as script/my_new_file.script, with a Local... fallback. //============================================================================= class CScriptSavePathWnd : public CWnd { DECLARE_DYNAMIC(CScriptSavePathWnd) public: CScriptSavePathWnd(); virtual ~CScriptSavePathWnd(); INT_PTR DoModal(CWnd* owner, CString& selectedPath, const char* suggestedPath); protected: afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg BOOL OnEraseBkgnd(CDC* pDC); afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor); afx_msg void OnPaint(); afx_msg void OnSave(); afx_msg void OnCancel(); afx_msg void OnLocalFile(); afx_msg void OnClose(); DECLARE_MESSAGE_MAP() private: CStatic m_title; CStatic m_help; CEdit m_path; CButton m_save; CButton m_cancel; CButton m_local; CFont m_font; CBrush m_backBrush; CBrush m_panelBrush; CBrush m_editBrush; CString m_selectedPath; CString m_suggestedPath; INT_PTR m_result; bool m_done; private: void LayoutChildren(void); void Finish(INT_PTR result); }; IMPLEMENT_DYNAMIC(CScriptSavePathWnd, CWnd) BEGIN_MESSAGE_MAP(CScriptSavePathWnd, CWnd) ON_WM_CREATE() ON_WM_SIZE() ON_WM_ERASEBKGND() ON_WM_CTLCOLOR() ON_WM_PAINT() ON_WM_CLOSE() ON_BN_CLICKED(IDOK, OnSave) ON_BN_CLICKED(IDCANCEL, OnCancel) ON_BN_CLICKED(IDC_SCRIPTSAVE_LOCAL, OnLocalFile) END_MESSAGE_MAP() CScriptSavePathWnd::CScriptSavePathWnd() : m_result(0) , m_done(false) { m_backBrush.CreateSolidBrush(SE_DARK_BG); m_panelBrush.CreateSolidBrush(SE_DARK_PANEL); m_editBrush.CreateSolidBrush(SE_DARK_EDIT); } CScriptSavePathWnd::~CScriptSavePathWnd() { } INT_PTR CScriptSavePathWnd::DoModal(CWnd* owner, CString& selectedPath, const char* suggestedPath) { selectedPath.Empty(); m_selectedPath.Empty(); m_suggestedPath = suggestedPath && suggestedPath[0] ? suggestedPath : "script/new_script.script"; m_result = 0; m_done = false; if (owner == NULL || !owner->GetSafeHwnd()) { common->Printf("Script Editor: cannot open Save As prompt without a valid editor parent.\n"); return IDCANCEL; } CString className = AfxRegisterWndClass( CS_DBLCLKS, ::LoadCursor(NULL, IDC_ARROW), (HBRUSH)m_backBrush.GetSafeHandle(), NULL ); CRect rect(0, 0, 620, 190); ScriptEditorCenterChildRectOnOwner(rect, owner); if (className.IsEmpty()) { common->Printf("Script Editor: failed to register Save As window class.\n"); return IDCANCEL; } // Keep Save As inside the script-editor tab for the same reason as the VFS // browser: no external tool window, no disabled owner frame, no focus trap. if (!CreateEx( WS_EX_CLIENTEDGE | WS_EX_CONTROLPARENT, className, "Save Script As", WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, rect, owner, IDC_SCRIPTSAVE_PANEL )) { common->Printf("Script Editor: failed to create in-tab Save As window, GetLastError=%lu.\n", ::GetLastError()); return IDCANCEL; } HWND oldFocus = ::GetFocus(); ShowWindow(SW_SHOW); SetWindowPos(&wndTop, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); UpdateWindow(); if (m_path.GetSafeHwnd()) { m_path.SetSel(0, -1); m_path.SetFocus(); } MSG msg; while (!m_done && GetSafeHwnd() && ::GetMessage(&msg, NULL, 0, 0)) { if (ScriptEditorIsModalInputMessage(msg.message) && !ScriptEditorMessageTargetsWindow(GetSafeHwnd(), msg.hwnd)) { if (m_path.GetSafeHwnd()) { m_path.SetFocus(); } continue; } if (!IsDialogMessage(&msg)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } } if (oldFocus && ::IsWindow(oldFocus)) { ::SetFocus(oldFocus); } if (GetSafeHwnd()) { DestroyWindow(); } selectedPath = m_selectedPath; return m_result ? m_result : IDCANCEL; } int CScriptSavePathWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CWnd::OnCreate(lpCreateStruct) == -1) { return -1; } m_font.CreatePointFont(85, "MS Shell Dlg"); m_title.Create("Save script to dev path", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); m_help.Create("Use a virtual path like script/my_file.script to create or override a pk5 script in fs_devpath.", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); m_path.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL, CRect(0, 0, 0, 0), this, IDC_SCRIPTSAVE_PATH); m_save.Create("Save", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, CRect(0, 0, 0, 0), this, IDOK); m_cancel.Create("Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDCANCEL); m_local.Create("Local...", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDC_SCRIPTSAVE_LOCAL); CWnd* windows[] = { &m_title, &m_help, &m_path, &m_save, &m_cancel, &m_local }; for (int i = 0; i < sizeof(windows) / sizeof(windows[0]); i++) { if (windows[i] && windows[i]->GetSafeHwnd()) { windows[i]->SetFont(&m_font); } } m_path.SetWindowText(m_suggestedPath); ScriptEditorSubclassButton(m_save); ScriptEditorSubclassButton(m_cancel); ScriptEditorSubclassButton(m_local); ScriptEditorApplyNativeDarkTheme(m_path.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(m_save.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(m_cancel.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(m_local.GetSafeHwnd()); LayoutChildren(); return 0; } void CScriptSavePathWnd::LayoutChildren(void) { if (!GetSafeHwnd()) { return; } CRect client; GetClientRect(client); const int margin = 12; const int buttonW = 86; const int buttonH = 25; const int gap = 8; if (m_title.GetSafeHwnd()) { m_title.MoveWindow(margin, 8, client.Width() - margin * 2, 22, TRUE); } if (m_help.GetSafeHwnd()) { m_help.MoveWindow(margin, 38, client.Width() - margin * 2, 24, TRUE); } if (m_path.GetSafeHwnd()) { m_path.MoveWindow(margin, 72, client.Width() - margin * 2, 24, TRUE); } int y = client.bottom - 40; int x = client.right - margin - buttonW; if (m_cancel.GetSafeHwnd()) { m_cancel.MoveWindow(x, y, buttonW, buttonH, TRUE); } x -= buttonW + gap; if (m_save.GetSafeHwnd()) { m_save.MoveWindow(x, y, buttonW, buttonH, TRUE); } if (m_local.GetSafeHwnd()) { m_local.MoveWindow(margin, y, buttonW, buttonH, TRUE); } } void CScriptSavePathWnd::Finish(INT_PTR result) { m_result = result; m_done = true; } void CScriptSavePathWnd::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy); LayoutChildren(); } BOOL CScriptSavePathWnd::OnEraseBkgnd(CDC* pDC) { CRect client; GetClientRect(client); pDC->FillSolidRect(client, SE_DARK_BG); return TRUE; } HBRUSH CScriptSavePathWnd::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { if (pDC) { pDC->SetBkMode(TRANSPARENT); pDC->SetTextColor(SE_DARK_TEXT); } if (nCtlColor == CTLCOLOR_EDIT) { if (pDC) { pDC->SetBkColor(SE_DARK_EDIT); pDC->SetTextColor(SE_DARK_TEXT); } return (HBRUSH)m_editBrush.GetSafeHandle(); } if (nCtlColor == CTLCOLOR_STATIC) { return (HBRUSH)m_backBrush.GetSafeHandle(); } return (HBRUSH)m_backBrush.GetSafeHandle(); } void CScriptSavePathWnd::OnPaint() { CPaintDC dc(this); CRect client; GetClientRect(client); dc.FillSolidRect(client, SE_DARK_BG); CRect header(client.left, client.top, client.right, 34); dc.FillSolidRect(header, SE_DARK_PANEL); dc.FillSolidRect(client.left, header.bottom - 1, client.Width(), 1, SE_DARK_BORDER); } void CScriptSavePathWnd::OnSave() { m_path.GetWindowText(m_selectedPath); m_selectedPath.TrimLeft(); m_selectedPath.TrimRight(); m_selectedPath.Replace("\\", "/"); if (m_selectedPath.IsEmpty()) { MessageBox("Enter a virtual path such as script/my_script.script.", "Save Script As", MB_OK | MB_ICONINFORMATION); return; } if (!ScriptEditorHasExtension(m_selectedPath, ".script") && !ScriptEditorHasExtension(m_selectedPath, ".gui") && !ScriptEditorHasExtension(m_selectedPath, ".txt")) { m_selectedPath += ".script"; } Finish(IDOK); } void CScriptSavePathWnd::OnCancel() { Finish(IDCANCEL); } void CScriptSavePathWnd::OnLocalFile() { CFileDialog dlg( FALSE, "script", m_suggestedPath, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, "Script Files (*.script)|*.script|GUI Files (*.gui)|*.gui|Text Files (*.txt)|*.txt|All Files (*.*)|*.*||", this ); if (dlg.DoModal() == IDOK) { m_selectedPath = dlg.GetPathName(); Finish(IDOK); } } void CScriptSavePathWnd::OnClose() { Finish(IDCANCEL); } //============================================================================= // Custom editor control. This is deliberately implemented here instead of using // the old syntax-rich edit control so the script editor owns all theme, syntax, selection, // caret, scrolling, and IntelliSense behavior. //============================================================================= class CScriptEditorCtrl : public CWnd { DECLARE_DYNAMIC(CScriptEditorCtrl) public: CScriptEditorCtrl(); virtual ~CScriptEditorCtrl(); BOOL Create(CWnd* parent, UINT id); void LimitText(int maxChars); void RefreshMetrics(void); void SetText(const char* text); void GetText(idStr& text) const; int GetTextLength(void) const; int GetLineCount(void) const; void GetCursorPos(int& line, int& column, int& character) const; void GoToLine(int line); void GetSel(long& start, long& end) const; void SetSel(long start, long end); CString GetSelText(void) const; void ReplaceSel(const char* text, BOOL canUndo); bool FindNext(const char* find, bool matchCase, bool matchWholeWords, bool searchForward); int ReplaceAll(const char* find, const char* replace, bool matchCase, bool matchWholeWords); void ClearKeywords(void); void AddKeyword(const char* keyword); bool LoadKeyWordsFromFile(const char* fileName); void ClearFunctionWords(void); void AddFunctionWord(const char* word); void SetCaseSensitive(bool caseSensitive); CPoint GetCaretPoint(void) const; CString GetCurrentWord(long* wordStart = NULL, long* wordEnd = NULL) const; protected: afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnPaint(); afx_msg BOOL OnEraseBkgnd(CDC* pDC); afx_msg UINT OnGetDlgCode(); afx_msg void OnSetFocus(CWnd* oldWnd); afx_msg void OnKillFocus(CWnd* newWnd); afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags); DECLARE_MESSAGE_MAP() private: CString m_text; CStringArray m_keywords; CStringArray m_functionWords; CArray m_lineStarts; CString m_undoText; int m_undoCaret; int m_undoAnchor; bool m_hasUndo; int m_textLimit; int m_caret; int m_selAnchor; int m_firstLine; int m_xOffset; int m_charWidth; int m_lineHeight; int m_ascent; int m_gutterWidth; int m_maxColumns; int m_preferredColumn; bool m_dragging; bool m_hasFocus; bool m_caseSensitive; bool m_inSetText; private: void RebuildLineCache(void); void UpdateScrollBars(void); void UpdateCaret(void); void EnsureCaretVisible(void); void NotifyChange(void); void NotifySelectionChange(void); void SaveUndo(void); void RestoreUndo(void); void QueueIntelliSenseUpdate(bool hide); void ReplaceRange(int start, int end, const CString& replacement, bool canUndo, bool notify); void DeleteSelectionOrRange(int start, int end); void IndentSelection(bool unindent); int ClampIndex(int index) const; int GetSelectionStart(void) const; int GetSelectionEnd(void) const; bool HasSelection(void) const; int GetLineFromChar(int index) const; int GetLineStart(int line) const; int GetLineEnd(int line) const; CString GetLineText(int line) const; int VisualColumnForLineOffset(int line, int offset) const; int CharOffsetFromVisualColumn(int line, int column) const; int CharFromPoint(CPoint point) const; int VisualColumnFromIndex(int index) const; int IndexFromLineAndVisualColumn(int line, int column) const; void MoveCaretTo(int index, bool keepSelection, bool verticalMove); void MoveCaretVertical(int deltaLines, bool keepSelection); void CopySelectionToClipboard(bool cut); void PasteFromClipboard(void); bool IsKeyword(const CString& word) const; bool IsFunctionWord(const CString& word) const; bool IsWholeWordMatch(const CString& source, int index, int length) const; int FindText(const CString& source, const CString& needle, int start, bool matchCase, bool wholeWord, bool forward) const; bool IsBlockCommentAtLine(int line) const; void ColorizeLine(const CString& lineText, bool& inBlockComment, COLORREF* colors) const; void DrawEditorLine(CDC& dc, int line, int y, bool& inBlockComment, int selStart, int selEnd); void DrawTextRun(CDC& dc, const CString& lineText, int lineStart, int tokenStart, int tokenEnd, int& visualColumn, int y, COLORREF color, int selStart, int selEnd); }; IMPLEMENT_DYNAMIC(CScriptEditorCtrl, CWnd) BEGIN_MESSAGE_MAP(CScriptEditorCtrl, CWnd) ON_WM_CREATE() ON_WM_PAINT() ON_WM_ERASEBKGND() ON_WM_GETDLGCODE() ON_WM_SETFOCUS() ON_WM_KILLFOCUS() ON_WM_SIZE() ON_WM_VSCROLL() ON_WM_HSCROLL() ON_WM_MOUSEWHEEL() ON_WM_LBUTTONDOWN() ON_WM_LBUTTONUP() ON_WM_MOUSEMOVE() ON_WM_KEYDOWN() ON_WM_CHAR() END_MESSAGE_MAP() CScriptEditorCtrl::CScriptEditorCtrl() { m_undoCaret = 0; m_undoAnchor = 0; m_hasUndo = false; m_textLimit = 1024 * 1024; m_caret = 0; m_selAnchor = 0; m_firstLine = 0; m_xOffset = 0; m_charWidth = 8; m_lineHeight = 16; m_ascent = 12; m_gutterWidth = 48; m_maxColumns = 0; m_preferredColumn = -1; m_dragging = false; m_hasFocus = false; m_caseSensitive = false; m_inSetText = false; RebuildLineCache(); } CScriptEditorCtrl::~CScriptEditorCtrl() { } BOOL CScriptEditorCtrl::Create(CWnd* parent, UINT id) { CString className = AfxRegisterWndClass( CS_DBLCLKS, ::LoadCursor(NULL, IDC_IBEAM), (HBRUSH)::GetStockObject(NULL_BRUSH), NULL ); return CWnd::CreateEx( WS_EX_CLIENTEDGE, className, "IceTechScriptEditorTextView", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL | WS_HSCROLL | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 0, 0), parent, id ); } int CScriptEditorCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CWnd::OnCreate(lpCreateStruct) == -1) { return -1; } RefreshMetrics(); UpdateScrollBars(); return 0; } void CScriptEditorCtrl::LimitText(int maxChars) { m_textLimit = maxChars; } void CScriptEditorCtrl::RefreshMetrics(void) { CClientDC dc(this); CFont* font = GetFont(); CFont* oldFont = font ? dc.SelectObject(font) : NULL; TEXTMETRIC tm; memset(&tm, 0, sizeof(tm)); dc.GetTextMetrics(&tm); // Measure an average monospace run instead of only "M". Some DPI/font // substitutions make "M" wider than the actual cell width, which makes the // custom editor look oddly spaced and harder to read. CSize tenChars = dc.GetTextExtent("0000000000", 10); m_charWidth = max(1, tenChars.cx / 10); // Give ClearType a little more vertical breathing room on the dark editor. m_lineHeight = max(1, tm.tmHeight + tm.tmExternalLeading + 5); m_ascent = tm.tmAscent; if (oldFont) { dc.SelectObject(oldFont); } UpdateScrollBars(); UpdateCaret(); Invalidate(FALSE); } void CScriptEditorCtrl::SetText(const char* text) { m_inSetText = true; m_text = ScriptEditorNormalizeText(text); if (m_text.GetLength() > m_textLimit) { m_text = m_text.Left(m_textLimit); } m_caret = 0; m_selAnchor = 0; m_firstLine = 0; m_xOffset = 0; m_preferredColumn = -1; m_hasUndo = false; RebuildLineCache(); UpdateScrollBars(); UpdateCaret(); Invalidate(FALSE); m_inSetText = false; NotifySelectionChange(); } void CScriptEditorCtrl::GetText(idStr& text) const { text = (LPCTSTR)m_text; } int CScriptEditorCtrl::GetTextLength(void) const { return m_text.GetLength(); } int CScriptEditorCtrl::GetLineCount(void) const { return max(1, (int)m_lineStarts.GetSize()); } void CScriptEditorCtrl::GetCursorPos(int& line, int& column, int& character) const { line = GetLineFromChar(m_caret) + 1; column = VisualColumnFromIndex(m_caret) + 1; character = m_caret + 1; } void CScriptEditorCtrl::GoToLine(int line) { line = max(0, min(line, GetLineCount() - 1)); MoveCaretTo(GetLineStart(line), false, false); } void CScriptEditorCtrl::GetSel(long& start, long& end) const { start = GetSelectionStart(); end = GetSelectionEnd(); } void CScriptEditorCtrl::SetSel(long start, long end) { start = ClampIndex((int)start); end = ClampIndex((int)end); m_selAnchor = (int)start; m_caret = (int)end; m_preferredColumn = -1; EnsureCaretVisible(); UpdateScrollBars(); UpdateCaret(); Invalidate(FALSE); NotifySelectionChange(); } CString CScriptEditorCtrl::GetSelText(void) const { if (!HasSelection()) { return ""; } return m_text.Mid(GetSelectionStart(), GetSelectionEnd() - GetSelectionStart()); } void CScriptEditorCtrl::ReplaceSel(const char* text, BOOL canUndo) { CString replacement = ScriptEditorNormalizeText(text); ReplaceRange(GetSelectionStart(), GetSelectionEnd(), replacement, canUndo ? true : false, true); } void CScriptEditorCtrl::ClearKeywords(void) { m_keywords.RemoveAll(); Invalidate(FALSE); } void CScriptEditorCtrl::AddKeyword(const char* keyword) { if (!keyword || !keyword[0]) { return; } CString word = keyword; for (int i = 0; i < m_keywords.GetSize(); i++) { if (ScriptEditorCompareNoCase(m_keywords[i], word) == 0) { return; } } m_keywords.Add(word); } bool CScriptEditorCtrl::LoadKeyWordsFromFile(const char* fileName) { idParser src; idToken token, name, description; if (!src.LoadFile(fileName)) { return false; } while (src.ReadToken(&token)) { if (token.Icmp("keywords") != 0) { src.SkipBracedSection(); continue; } src.ExpectTokenString("{"); while (src.ReadToken(&token)) { if (token == "}") { break; } if (token != "{") { continue; } // editors/*.def entries are { "keyword", ( r, g, b ), "description" }. // This custom editor owns the actual dark palette, but still imports the names. src.ExpectTokenType(TT_STRING, 0, &name); AddKeyword(name.c_str()); src.ExpectTokenString(","); src.ExpectTokenString("("); src.ExpectTokenType(TT_NUMBER, TT_INTEGER, &token); src.ExpectTokenString(","); src.ExpectTokenType(TT_NUMBER, TT_INTEGER, &token); src.ExpectTokenString(","); src.ExpectTokenType(TT_NUMBER, TT_INTEGER, &token); src.ExpectTokenString(")"); src.ExpectTokenString(","); src.ExpectTokenType(TT_STRING, 0, &description); src.ExpectTokenString("}"); } } Invalidate(FALSE); return true; } void CScriptEditorCtrl::ClearFunctionWords(void) { m_functionWords.RemoveAll(); Invalidate(FALSE); } void CScriptEditorCtrl::AddFunctionWord(const char* word) { if (!word || !word[0]) { return; } CString functionWord = word; for (int i = 0; i < m_functionWords.GetSize(); i++) { if (ScriptEditorCompareNoCase(m_functionWords[i], functionWord) == 0) { return; } } m_functionWords.Add(functionWord); } void CScriptEditorCtrl::SetCaseSensitive(bool caseSensitive) { m_caseSensitive = caseSensitive; Invalidate(FALSE); } int CScriptEditorCtrl::ClampIndex(int index) const { if (index < 0) { return 0; } if (index > m_text.GetLength()) { return m_text.GetLength(); } return index; } int CScriptEditorCtrl::GetSelectionStart(void) const { return min(m_caret, m_selAnchor); } int CScriptEditorCtrl::GetSelectionEnd(void) const { return max(m_caret, m_selAnchor); } bool CScriptEditorCtrl::HasSelection(void) const { return m_caret != m_selAnchor; } void CScriptEditorCtrl::RebuildLineCache(void) { m_lineStarts.RemoveAll(); m_lineStarts.Add(0); m_maxColumns = 0; int column = 0; for (int i = 0; i < m_text.GetLength(); i++) { char ch = m_text[i]; if (ch == '\n') { if (column > m_maxColumns) { m_maxColumns = column; } column = 0; if (i + 1 <= m_text.GetLength()) { m_lineStarts.Add(i + 1); } } else if (ch == '\t') { column += SE_TAB_SIZE - (column % SE_TAB_SIZE); } else { column++; } } if (column > m_maxColumns) { m_maxColumns = column; } int digits = 1; int lines = GetLineCount(); while (lines >= 10) { lines /= 10; digits++; } m_gutterWidth = max(48, digits * m_charWidth + 24); } int CScriptEditorCtrl::GetLineFromChar(int index) const { index = ClampIndex(index); int low = 0; int high = (int)m_lineStarts.GetSize() - 1; while (low <= high) { int mid = (low + high) >> 1; int start = m_lineStarts[mid]; int next = (mid + 1 < m_lineStarts.GetSize()) ? m_lineStarts[mid + 1] : m_text.GetLength() + 1; if (index < start) { high = mid - 1; } else if (index >= next) { low = mid + 1; } else { return mid; } } return max(0, (int)m_lineStarts.GetSize() - 1); } int CScriptEditorCtrl::GetLineStart(int line) const { line = max(0, min(line, GetLineCount() - 1)); return m_lineStarts[line]; } int CScriptEditorCtrl::GetLineEnd(int line) const { line = max(0, min(line, GetLineCount() - 1)); int end = (line + 1 < GetLineCount()) ? m_lineStarts[line + 1] - 1 : m_text.GetLength(); if (end > 0 && end <= m_text.GetLength() && m_text[end - 1] == '\r') { end--; } return max(GetLineStart(line), end); } CString CScriptEditorCtrl::GetLineText(int line) const { int start = GetLineStart(line); int end = GetLineEnd(line); return m_text.Mid(start, end - start); } int CScriptEditorCtrl::VisualColumnForLineOffset(int line, int offset) const { CString lineText = GetLineText(line); offset = max(0, min(offset, lineText.GetLength())); int column = 0; for (int i = 0; i < offset; i++) { if (lineText[i] == '\t') { column += SE_TAB_SIZE - (column % SE_TAB_SIZE); } else { column++; } } return column; } int CScriptEditorCtrl::CharOffsetFromVisualColumn(int line, int column) const { CString lineText = GetLineText(line); column = max(0, column); int visual = 0; for (int i = 0; i < lineText.GetLength(); i++) { int next = visual; if (lineText[i] == '\t') { next += SE_TAB_SIZE - (next % SE_TAB_SIZE); } else { next++; } if (column < next) { return i; } visual = next; } return lineText.GetLength(); } int CScriptEditorCtrl::VisualColumnFromIndex(int index) const { int line = GetLineFromChar(index); return VisualColumnForLineOffset(line, index - GetLineStart(line)); } int CScriptEditorCtrl::IndexFromLineAndVisualColumn(int line, int column) const { line = max(0, min(line, GetLineCount() - 1)); return GetLineStart(line) + CharOffsetFromVisualColumn(line, column); } CPoint CScriptEditorCtrl::GetCaretPoint(void) const { int line = GetLineFromChar(m_caret); int column = VisualColumnFromIndex(m_caret); return CPoint(m_gutterWidth + column * m_charWidth - m_xOffset, (line - m_firstLine) * m_lineHeight); } int CScriptEditorCtrl::CharFromPoint(CPoint point) const { CRect client; GetClientRect(client); int line = m_firstLine + max(0, point.y) / max(1, m_lineHeight); line = max(0, min(line, GetLineCount() - 1)); int x = point.x - m_gutterWidth + m_xOffset; int column = max(0, (x + m_charWidth / 2) / max(1, m_charWidth)); return IndexFromLineAndVisualColumn(line, column); } CString CScriptEditorCtrl::GetCurrentWord(long* wordStart, long* wordEnd) const { int start = ClampIndex(m_caret); int end = start; while (start > 0 && ScriptEditorIsIdentifierChar(m_text[start - 1])) { start--; } while (end < m_text.GetLength() && ScriptEditorIsIdentifierChar(m_text[end])) { end++; } if (wordStart) { *wordStart = start; } if (wordEnd) { *wordEnd = end; } return m_text.Mid(start, m_caret - start); } void CScriptEditorCtrl::UpdateScrollBars(void) { if (!GetSafeHwnd()) { return; } CRect client; GetClientRect(client); int visibleLines = max(1, client.Height() / max(1, m_lineHeight)); SCROLLINFO si; memset(&si, 0, sizeof(si)); si.cbSize = sizeof(si); si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS; si.nMin = 0; si.nMax = max(0, GetLineCount() - 1); si.nPage = visibleLines; m_firstLine = max(0, min(m_firstLine, max(0, GetLineCount() - visibleLines))); si.nPos = m_firstLine; SetScrollInfo(SB_VERT, &si, TRUE); int contentWidth = max(0, m_maxColumns * m_charWidth); int viewWidth = max(1, client.Width() - m_gutterWidth); memset(&si, 0, sizeof(si)); si.cbSize = sizeof(si); si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS; si.nMin = 0; si.nMax = max(0, contentWidth); si.nPage = viewWidth; m_xOffset = max(0, min(m_xOffset, max(0, contentWidth - viewWidth))); si.nPos = m_xOffset; SetScrollInfo(SB_HORZ, &si, TRUE); } void CScriptEditorCtrl::UpdateCaret(void) { if (!GetSafeHwnd() || !m_hasFocus) { return; } CPoint pt = GetCaretPoint(); ::SetCaretPos(pt.x, pt.y + 1); } void CScriptEditorCtrl::EnsureCaretVisible(void) { CRect client; GetClientRect(client); int visibleLines = max(1, client.Height() / max(1, m_lineHeight)); int line = GetLineFromChar(m_caret); if (line < m_firstLine) { m_firstLine = line; } else if (line >= m_firstLine + visibleLines) { m_firstLine = line - visibleLines + 1; } int columnX = VisualColumnFromIndex(m_caret) * m_charWidth; int viewWidth = max(1, client.Width() - m_gutterWidth); if (columnX < m_xOffset) { m_xOffset = columnX; } else if (columnX + m_charWidth > m_xOffset + viewWidth) { m_xOffset = columnX + m_charWidth - viewWidth; } m_firstLine = max(0, m_firstLine); m_xOffset = max(0, m_xOffset); } void CScriptEditorCtrl::NotifyChange(void) { if (m_inSetText || !GetParent() || !GetParent()->GetSafeHwnd()) { return; } NMHDR hdr; hdr.hwndFrom = GetSafeHwnd(); hdr.idFrom = GetDlgCtrlID(); hdr.code = EN_CHANGE; GetParent()->SendMessage(WM_NOTIFY, (EN_CHANGE << 16) | GetDlgCtrlID(), (LPARAM)&hdr); } void CScriptEditorCtrl::NotifySelectionChange(void) { if (!GetParent() || !GetParent()->GetSafeHwnd()) { return; } NMHDR hdr; hdr.hwndFrom = GetSafeHwnd(); hdr.idFrom = GetDlgCtrlID(); hdr.code = EN_SELCHANGE; GetParent()->SendMessage(WM_NOTIFY, (EN_SELCHANGE << 16) | GetDlgCtrlID(), (LPARAM)&hdr); } void CScriptEditorCtrl::SaveUndo(void) { m_undoText = m_text; m_undoCaret = m_caret; m_undoAnchor = m_selAnchor; m_hasUndo = true; } void CScriptEditorCtrl::RestoreUndo(void) { if (!m_hasUndo) { return; } CString oldText = m_text; int oldCaret = m_caret; int oldAnchor = m_selAnchor; m_text = m_undoText; m_caret = ClampIndex(m_undoCaret); m_selAnchor = ClampIndex(m_undoAnchor); m_undoText = oldText; m_undoCaret = oldCaret; m_undoAnchor = oldAnchor; RebuildLineCache(); EnsureCaretVisible(); UpdateScrollBars(); UpdateCaret(); Invalidate(FALSE); NotifyChange(); NotifySelectionChange(); QueueIntelliSenseUpdate(false); } void CScriptEditorCtrl::QueueIntelliSenseUpdate(bool hide) { CWnd* parent = GetParent(); if (parent && parent->GetSafeHwnd()) { parent->PostMessage(WM_SCRIPTEDITOR_DEFERRED_INTELLISENSE, hide ? 2 : 1, 0); } } void CScriptEditorCtrl::ReplaceRange(int start, int end, const CString& replacement, bool canUndo, bool notify) { start = ClampIndex(start); end = ClampIndex(end); if (end < start) { int temp = start; start = end; end = temp; } CString insert = replacement; insert = ScriptEditorNormalizeText(insert); int newLength = m_text.GetLength() - (end - start) + insert.GetLength(); if (newLength > m_textLimit) { int allowed = m_textLimit - (m_text.GetLength() - (end - start)); if (allowed < 0) { allowed = 0; } insert = insert.Left(allowed); } if (canUndo) { SaveUndo(); } m_text = m_text.Left(start) + insert + m_text.Mid(end); m_caret = start + insert.GetLength(); m_selAnchor = m_caret; m_preferredColumn = -1; RebuildLineCache(); EnsureCaretVisible(); UpdateScrollBars(); UpdateCaret(); Invalidate(FALSE); if (notify) { NotifyChange(); NotifySelectionChange(); } } void CScriptEditorCtrl::DeleteSelectionOrRange(int start, int end) { ReplaceRange(start, end, "", true, true); } void CScriptEditorCtrl::IndentSelection(bool unindent) { int selStart = GetSelectionStart(); int selEnd = GetSelectionEnd(); int startLine = GetLineFromChar(selStart); int endLine = GetLineFromChar(selEnd); if (selEnd > selStart && selEnd == GetLineStart(endLine) && endLine > startLine) { endLine--; } SaveUndo(); int delta = 0; for (int line = startLine; line <= endLine; line++) { int pos = GetLineStart(line) + delta; if (unindent) { if (pos < m_text.GetLength() && m_text[pos] == '\t') { m_text = m_text.Left(pos) + m_text.Mid(pos + 1); delta -= 1; } else { int remove = 0; while (remove < SE_TAB_SIZE && pos + remove < m_text.GetLength() && m_text[pos + remove] == ' ') { remove++; } if (remove > 0) { m_text = m_text.Left(pos) + m_text.Mid(pos + remove); delta -= remove; } } } else { m_text = m_text.Left(pos) + "\t" + m_text.Mid(pos); delta += 1; } } RebuildLineCache(); int newStart = GetLineStart(startLine); int newEnd = GetLineEnd(endLine); m_selAnchor = newStart; m_caret = newEnd; EnsureCaretVisible(); UpdateScrollBars(); UpdateCaret(); Invalidate(FALSE); NotifyChange(); NotifySelectionChange(); } void CScriptEditorCtrl::MoveCaretTo(int index, bool keepSelection, bool verticalMove) { m_caret = ClampIndex(index); if (!keepSelection) { m_selAnchor = m_caret; } if (!verticalMove) { m_preferredColumn = -1; } EnsureCaretVisible(); UpdateScrollBars(); UpdateCaret(); Invalidate(FALSE); NotifySelectionChange(); } void CScriptEditorCtrl::MoveCaretVertical(int deltaLines, bool keepSelection) { int line = GetLineFromChar(m_caret); if (m_preferredColumn < 0) { m_preferredColumn = VisualColumnFromIndex(m_caret); } line = max(0, min(line + deltaLines, GetLineCount() - 1)); MoveCaretTo(IndexFromLineAndVisualColumn(line, m_preferredColumn), keepSelection, true); } void CScriptEditorCtrl::CopySelectionToClipboard(bool cut) { if (!HasSelection()) { return; } CString selection = ScriptEditorToCRLF(GetSelText()); if (OpenClipboard()) { EmptyClipboard(); HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, selection.GetLength() + 1); if (hMem) { char* dst = (char*)GlobalLock(hMem); if (dst) { memcpy(dst, (LPCTSTR)selection, selection.GetLength()); dst[selection.GetLength()] = '\0'; GlobalUnlock(hMem); SetClipboardData(CF_TEXT, hMem); } else { GlobalFree(hMem); } } CloseClipboard(); } if (cut) { DeleteSelectionOrRange(GetSelectionStart(), GetSelectionEnd()); } } void CScriptEditorCtrl::PasteFromClipboard(void) { if (!OpenClipboard()) { return; } HGLOBAL hMem = GetClipboardData(CF_TEXT); if (hMem) { char* src = (char*)GlobalLock(hMem); if (src) { ReplaceRange(GetSelectionStart(), GetSelectionEnd(), ScriptEditorNormalizeText(src), true, true); GlobalUnlock(hMem); } } CloseClipboard(); } bool CScriptEditorCtrl::IsKeyword(const CString& word) const { for (int i = 0; i < m_keywords.GetSize(); i++) { if (m_caseSensitive) { if (m_keywords[i] == word) { return true; } } else if (ScriptEditorCompareNoCase(m_keywords[i], word) == 0) { return true; } } return false; } bool CScriptEditorCtrl::IsFunctionWord(const CString& word) const { for (int i = 0; i < m_functionWords.GetSize(); i++) { if (m_caseSensitive) { if (m_functionWords[i] == word) { return true; } } else if (ScriptEditorCompareNoCase(m_functionWords[i], word) == 0) { return true; } } return false; } bool CScriptEditorCtrl::IsWholeWordMatch(const CString& source, int index, int length) const { if (index > 0 && ScriptEditorIsIdentifierChar(source[index - 1])) { return false; } if (index + length < source.GetLength() && ScriptEditorIsIdentifierChar(source[index + length])) { return false; } return true; } int CScriptEditorCtrl::FindText(const CString& source, const CString& needle, int start, bool matchCase, bool wholeWord, bool forward) const { if (needle.GetLength() == 0 || source.GetLength() == 0) { return -1; } CString hay = source; CString find = needle; if (!matchCase) { hay.MakeLower(); find.MakeLower(); } start = max(0, min(start, source.GetLength())); if (forward) { for (int i = start; i <= hay.GetLength() - find.GetLength(); i++) { if (hay.Mid(i, find.GetLength()) == find && (!wholeWord || IsWholeWordMatch(source, i, find.GetLength()))) { return i; } } for (int i = 0; i < start && i <= hay.GetLength() - find.GetLength(); i++) { if (hay.Mid(i, find.GetLength()) == find && (!wholeWord || IsWholeWordMatch(source, i, find.GetLength()))) { return i; } } } else { int begin = min(start, hay.GetLength() - find.GetLength()); for (int i = begin; i >= 0; i--) { if (hay.Mid(i, find.GetLength()) == find && (!wholeWord || IsWholeWordMatch(source, i, find.GetLength()))) { return i; } } for (int i = hay.GetLength() - find.GetLength(); i > start; i--) { if (hay.Mid(i, find.GetLength()) == find && (!wholeWord || IsWholeWordMatch(source, i, find.GetLength()))) { return i; } } } return -1; } bool CScriptEditorCtrl::FindNext(const char* find, bool matchCase, bool matchWholeWords, bool searchForward) { CString needle = find ? find : ""; if (needle.IsEmpty()) { return false; } int start = searchForward ? GetSelectionEnd() : GetSelectionStart() - 1; int index = FindText(m_text, needle, start, matchCase, matchWholeWords, searchForward); if (index < 0) { return false; } SetSel(index, index + needle.GetLength()); return true; } int CScriptEditorCtrl::ReplaceAll(const char* find, const char* replace, bool matchCase, bool matchWholeWords) { CString needle = find ? find : ""; CString replacement = ScriptEditorNormalizeText(replace ? replace : ""); if (needle.IsEmpty()) { return 0; } SaveUndo(); int count = 0; int index = FindText(m_text, needle, 0, matchCase, matchWholeWords, true); while (index >= 0) { m_text = m_text.Left(index) + replacement + m_text.Mid(index + needle.GetLength()); count++; int nextStart = index + replacement.GetLength(); int nextIndex = FindText(m_text, needle, nextStart, matchCase, matchWholeWords, true); if (nextIndex < 0 || nextIndex <= index) { break; } index = nextIndex; } if (count > 0) { m_caret = 0; m_selAnchor = 0; RebuildLineCache(); EnsureCaretVisible(); UpdateScrollBars(); UpdateCaret(); Invalidate(FALSE); NotifyChange(); NotifySelectionChange(); } return count; } bool CScriptEditorCtrl::IsBlockCommentAtLine(int line) const { bool inComment = false; for (int l = 0; l < line; l++) { CString text = GetLineText(l); for (int i = 0; i < text.GetLength(); i++) { if (!inComment && i + 1 < text.GetLength() && text[i] == '/' && text[i + 1] == '*') { inComment = true; i++; } else if (inComment && i + 1 < text.GetLength() && text[i] == '*' && text[i + 1] == '/') { inComment = false; i++; } } } return inComment; } void CScriptEditorCtrl::ColorizeLine(const CString& lineText, bool& inBlockComment, COLORREF* colors) const { int len = lineText.GetLength(); for (int i = 0; i < len; i++) { colors[i] = SE_DARK_TEXT; } for (int i = 0; i < len; ) { if (inBlockComment) { colors[i] = SE_DARK_COMMENT; if (i + 1 < len && lineText[i] == '*' && lineText[i + 1] == '/') { colors[i + 1] = SE_DARK_COMMENT; inBlockComment = false; i += 2; } else { i++; } continue; } char ch = lineText[i]; if (ch == '/' && i + 1 < len && lineText[i + 1] == '/') { for (int j = i; j < len; j++) { colors[j] = SE_DARK_COMMENT; } break; } if (ch == '/' && i + 1 < len && lineText[i + 1] == '*') { colors[i] = SE_DARK_COMMENT; colors[i + 1] = SE_DARK_COMMENT; inBlockComment = true; i += 2; continue; } if (ch == '"' || ch == '\'') { char quote = ch; COLORREF stringColor = (quote == '"') ? SE_DARK_STRING : SE_DARK_ALT_STRING; colors[i++] = stringColor; while (i < len) { colors[i] = stringColor; if (lineText[i] == quote && (i == 0 || lineText[i - 1] != '\\')) { i++; break; } i++; } continue; } if (ch >= '0' && ch <= '9') { int start = i; while (i < len && ((lineText[i] >= '0' && lineText[i] <= '9') || lineText[i] == '.' || lineText[i] == 'x' || lineText[i] == 'X' || (lineText[i] >= 'a' && lineText[i] <= 'f') || (lineText[i] >= 'A' && lineText[i] <= 'F'))) { i++; } for (int j = start; j < i; j++) { colors[j] = SE_DARK_NUMBER; } continue; } if (ScriptEditorIsIdentifierStart(ch)) { int start = i; while (i < len && ScriptEditorIsIdentifierChar(lineText[i])) { i++; } CString word = lineText.Mid(start, i - start); COLORREF color = SE_DARK_TEXT; if (IsFunctionWord(word)) { color = SE_DARK_FUNCTION; } else if (ScriptEditorIsTypeKeyword(word)) { color = SE_DARK_TYPE; } else if (IsKeyword(word)) { color = SE_DARK_KEYWORD; } for (int j = start; j < i; j++) { colors[j] = color; } continue; } if (strchr("{}[]().,;:+-*/%=!<>|&", ch)) { colors[i] = SE_DARK_OPERATOR; } i++; } } void CScriptEditorCtrl::DrawTextRun(CDC& dc, const CString& lineText, int lineStart, int tokenStart, int tokenEnd, int& visualColumn, int y, COLORREF color, int selStart, int selEnd) { for (int i = tokenStart; i < tokenEnd; ) { const int charIndex = lineStart + i; const bool selected = (charIndex >= selStart && charIndex < selEnd); // Tabs still need fixed-cell expansion. if (lineText[i] == '\t') { const int widthColumns = SE_TAB_SIZE - (visualColumn % SE_TAB_SIZE); CString drawText; for (int s = 0; s < widthColumns; s++) { drawText += ' '; } CRect cell( m_gutterWidth + visualColumn * m_charWidth - m_xOffset, y, m_gutterWidth + (visualColumn + widthColumns) * m_charWidth - m_xOffset, y + m_lineHeight ); if (selected) { dc.FillSolidRect(cell, SE_DARK_SELECTION); dc.SetTextColor(SE_DARK_SELECTION_TEXT); } else { dc.SetTextColor(color); } dc.TextOut(cell.left, y + 2, drawText); visualColumn += widthColumns; i++; continue; } // Draw contiguous non-tab text as a run instead of one character at a time. // This preserves ClearType/subpixel rendering much better and removes the // "weird spacing" look in the editor. int j = i + 1; while (j < tokenEnd && lineText[j] != '\t') { const int nextCharIndex = lineStart + j; const bool nextSelected = (nextCharIndex >= selStart && nextCharIndex < selEnd); if (nextSelected != selected) { break; } j++; } const int columns = j - i; CString drawText = lineText.Mid(i, columns); CRect cell( m_gutterWidth + visualColumn * m_charWidth - m_xOffset, y, m_gutterWidth + (visualColumn + columns) * m_charWidth - m_xOffset, y + m_lineHeight ); if (selected) { dc.FillSolidRect(cell, SE_DARK_SELECTION); dc.SetTextColor(SE_DARK_SELECTION_TEXT); } else { dc.SetTextColor(color); } dc.TextOut(cell.left, y + 2, drawText); visualColumn += columns; i = j; } } void CScriptEditorCtrl::DrawEditorLine(CDC& dc, int line, int y, bool& inBlockComment, int selStart, int selEnd) { CRect client; GetClientRect(client); CString lineText = GetLineText(line); int lineStart = GetLineStart(line); if (line == GetLineFromChar(m_caret)) { CRect currentLine(client.left, y, client.right, y + m_lineHeight); dc.FillSolidRect(currentLine, SE_DARK_EDIT_LINE); } CRect gutter(client.left, y, client.left + m_gutterWidth - 1, y + m_lineHeight); dc.FillSolidRect(gutter, SE_DARK_GUTTER); dc.SetTextColor(SE_DARK_LINE_NUMBER); dc.SetBkMode(TRANSPARENT); CString lineNumber; lineNumber.Format("%d", line + 1); dc.DrawText(lineNumber, gutter, DT_RIGHT | DT_VCENTER | DT_SINGLELINE); if (lineText.GetLength() <= 0) { return; } COLORREF* colors = new COLORREF[lineText.GetLength()]; ColorizeLine(lineText, inBlockComment, colors); int visualColumn = 0; int runStart = 0; COLORREF runColor = colors[0]; for (int i = 1; i <= lineText.GetLength(); i++) { if (i == lineText.GetLength() || colors[i] != runColor) { DrawTextRun(dc, lineText, lineStart, runStart, i, visualColumn, y, runColor, selStart, selEnd); if (i < lineText.GetLength()) { runStart = i; runColor = colors[i]; } } } delete[] colors; } void CScriptEditorCtrl::OnPaint() { CPaintDC paintDC(this); CRect client; GetClientRect(client); if (client.Width() <= 0 || client.Height() <= 0) { return; } CDC memDC; CBitmap backBuffer; CBitmap* oldBitmap = NULL; CDC* drawDC = &paintDC; if (memDC.CreateCompatibleDC(&paintDC) && backBuffer.CreateCompatibleBitmap(&paintDC, client.Width(), client.Height())) { oldBitmap = memDC.SelectObject(&backBuffer); drawDC = &memDC; } CDC& dc = *drawDC; dc.FillSolidRect(client, SE_DARK_EDIT); CFont* font = GetFont(); CFont* oldFont = font ? dc.SelectObject(font) : NULL; dc.SetBkMode(TRANSPARENT); CRect gutter(client.left, client.top, client.left + m_gutterWidth, client.bottom); dc.FillSolidRect(gutter, SE_DARK_GUTTER); dc.FillSolidRect(m_gutterWidth - 1, client.top, 1, client.Height(), SE_DARK_BORDER); int selStart = GetSelectionStart(); int selEnd = GetSelectionEnd(); bool inBlockComment = IsBlockCommentAtLine(m_firstLine); int visibleLines = client.Height() / max(1, m_lineHeight) + 1; for (int i = 0; i < visibleLines; i++) { int line = m_firstLine + i; if (line >= GetLineCount()) { break; } DrawEditorLine(dc, line, i * m_lineHeight, inBlockComment, selStart, selEnd); } if (oldFont) { dc.SelectObject(oldFont); } if (drawDC != &paintDC) { paintDC.BitBlt(0, 0, client.Width(), client.Height(), &memDC, 0, 0, SRCCOPY); if (oldBitmap) { memDC.SelectObject(oldBitmap); } } } BOOL CScriptEditorCtrl::OnEraseBkgnd(CDC* pDC) { return TRUE; } UINT CScriptEditorCtrl::OnGetDlgCode() { return DLGC_WANTALLKEYS | DLGC_WANTARROWS | DLGC_WANTCHARS | DLGC_WANTMESSAGE | DLGC_WANTTAB; } void CScriptEditorCtrl::OnSetFocus(CWnd* oldWnd) { CWnd::OnSetFocus(oldWnd); m_hasFocus = true; ::CreateCaret(GetSafeHwnd(), NULL, 2, max(1, m_lineHeight - 2)); UpdateCaret(); ::ShowCaret(GetSafeHwnd()); } void CScriptEditorCtrl::OnKillFocus(CWnd* newWnd) { ::HideCaret(GetSafeHwnd()); ::DestroyCaret(); m_hasFocus = false; CWnd::OnKillFocus(newWnd); } void CScriptEditorCtrl::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy); EnsureCaretVisible(); UpdateScrollBars(); UpdateCaret(); Invalidate(FALSE); } void CScriptEditorCtrl::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { SCROLLINFO si; memset(&si, 0, sizeof(si)); si.cbSize = sizeof(si); si.fMask = SIF_ALL; GetScrollInfo(SB_VERT, &si); int pos = si.nPos; switch (nSBCode) { case SB_LINEUP: pos--; break; case SB_LINEDOWN: pos++; break; case SB_PAGEUP: pos -= (int)si.nPage; break; case SB_PAGEDOWN: pos += (int)si.nPage; break; case SB_THUMBTRACK: case SB_THUMBPOSITION: pos = si.nTrackPos; break; } m_firstLine = max(si.nMin, min(pos, si.nMax - (int)si.nPage + 1)); UpdateScrollBars(); UpdateCaret(); Invalidate(FALSE); } void CScriptEditorCtrl::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { SCROLLINFO si; memset(&si, 0, sizeof(si)); si.cbSize = sizeof(si); si.fMask = SIF_ALL; GetScrollInfo(SB_HORZ, &si); int pos = si.nPos; switch (nSBCode) { case SB_LINELEFT: pos -= m_charWidth; break; case SB_LINERIGHT: pos += m_charWidth; break; case SB_PAGELEFT: pos -= (int)si.nPage; break; case SB_PAGERIGHT: pos += (int)si.nPage; break; case SB_THUMBTRACK: case SB_THUMBPOSITION: pos = si.nTrackPos; break; } m_xOffset = max(si.nMin, min(pos, si.nMax - (int)si.nPage + 1)); UpdateScrollBars(); UpdateCaret(); Invalidate(FALSE); } BOOL CScriptEditorCtrl::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) { int lines = -3 * ((int)zDelta) / WHEEL_DELTA; m_firstLine = max(0, min(m_firstLine + lines, GetLineCount() - 1)); UpdateScrollBars(); UpdateCaret(); Invalidate(FALSE); return TRUE; } void CScriptEditorCtrl::OnLButtonDown(UINT nFlags, CPoint point) { SetFocus(); m_dragging = true; SetCapture(); int index = CharFromPoint(point); MoveCaretTo(index, (nFlags & MK_SHIFT) != 0, false); QueueIntelliSenseUpdate(true); } void CScriptEditorCtrl::OnLButtonUp(UINT nFlags, CPoint point) { if (m_dragging) { m_dragging = false; if (GetCapture() == this) { ReleaseCapture(); } } CWnd::OnLButtonUp(nFlags, point); } void CScriptEditorCtrl::OnMouseMove(UINT nFlags, CPoint point) { if (m_dragging) { MoveCaretTo(CharFromPoint(point), true, false); return; } CWnd::OnMouseMove(nFlags, point); } void CScriptEditorCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { CWnd* parent = GetParent(); if (parent && parent->GetSafeHwnd() && parent->SendMessage(WM_SCRIPTEDITOR_INTELLISENSE_KEY, nChar, nFlags) != 0) { return; } bool ctrl = (::GetKeyState(VK_CONTROL) & 0x8000) != 0; bool shift = (::GetKeyState(VK_SHIFT) & 0x8000) != 0; if (ctrl) { switch (nChar) { case 'A': SetSel(0, m_text.GetLength()); return; case 'C': CopySelectionToClipboard(false); return; case 'X': CopySelectionToClipboard(true); QueueIntelliSenseUpdate(false); return; case 'V': PasteFromClipboard(); QueueIntelliSenseUpdate(false); return; case 'Z': RestoreUndo(); return; case VK_SPACE: if (GetParent() && GetParent()->GetSafeHwnd()) { GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(IDC_SCRIPTEDITOR_BUTTON_INTELLISENSE, BN_CLICKED), (LPARAM)GetSafeHwnd()); } return; case VK_HOME: MoveCaretTo(0, shift, false); return; case VK_END: MoveCaretTo(m_text.GetLength(), shift, false); return; } } switch (nChar) { case VK_LEFT: if (HasSelection() && !shift) { MoveCaretTo(GetSelectionStart(), false, false); } else { MoveCaretTo(m_caret - 1, shift, false); } return; case VK_RIGHT: if (HasSelection() && !shift) { MoveCaretTo(GetSelectionEnd(), false, false); } else { MoveCaretTo(m_caret + 1, shift, false); } return; case VK_UP: MoveCaretVertical(-1, shift); return; case VK_DOWN: MoveCaretVertical(1, shift); return; case VK_PRIOR: MoveCaretVertical(-max(1, 10), shift); return; case VK_NEXT: MoveCaretVertical(max(1, 10), shift); return; case VK_HOME: MoveCaretTo(GetLineStart(GetLineFromChar(m_caret)), shift, false); return; case VK_END: MoveCaretTo(GetLineEnd(GetLineFromChar(m_caret)), shift, false); return; case VK_BACK: if (HasSelection()) { DeleteSelectionOrRange(GetSelectionStart(), GetSelectionEnd()); } else if (m_caret > 0) { DeleteSelectionOrRange(m_caret - 1, m_caret); } QueueIntelliSenseUpdate(false); return; case VK_DELETE: if (HasSelection()) { DeleteSelectionOrRange(GetSelectionStart(), GetSelectionEnd()); } else if (m_caret < m_text.GetLength()) { DeleteSelectionOrRange(m_caret, m_caret + 1); } QueueIntelliSenseUpdate(false); return; case VK_TAB: if (HasSelection()) { IndentSelection(shift); } else if (!shift) { ReplaceRange(GetSelectionStart(), GetSelectionEnd(), "\t", true, true); } QueueIntelliSenseUpdate(true); return; case VK_RETURN: { int line = GetLineFromChar(m_caret); CString current = GetLineText(line); int offset = m_caret - GetLineStart(line); CString indent = "\n"; for (int i = 0; i < current.GetLength() && ScriptEditorIsSpaceOrTab(current[i]); i++) { indent += current[i]; } int prev = offset - 1; while (prev >= 0 && ScriptEditorIsSpaceOrTab(current[prev])) { prev--; } if (prev >= 0 && current[prev] == '{') { indent += '\t'; } ReplaceRange(GetSelectionStart(), GetSelectionEnd(), indent, true, true); QueueIntelliSenseUpdate(true); return; } } CWnd::OnKeyDown(nChar, nRepCnt, nFlags); } void CScriptEditorCtrl::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) { if (nChar == VK_TAB || nChar == VK_RETURN || nChar == VK_BACK || nChar == 0) { return; } if (nChar < 32) { return; } CString insert; insert.Format("%c", (char)nChar); ReplaceRange(GetSelectionStart(), GetSelectionEnd(), insert, true, true); CWnd* parent = GetParent(); if (parent && parent->GetSafeHwnd()) { if (nChar == '(' || nChar == ',') { parent->PostMessage(WM_SCRIPTEDITOR_DEFERRED_INTELLISENSE, 3, 0); return; } if (nChar == ')' || nChar == ';' || nChar == '{' || nChar == '}') { parent->PostMessage(WM_SCRIPTEDITOR_DEFERRED_INTELLISENSE, 4, 0); QueueIntelliSenseUpdate(true); return; } } if (nChar == '(' || nChar == ',') { QueueIntelliSenseUpdate(false); return; } if (nChar == ')' || nChar == ';') { QueueIntelliSenseUpdate(true); return; } const bool canPredict = ScriptEditorIsIdentifierChar(nChar) || nChar == '.' || nChar == ':'; QueueIntelliSenseUpdate(!canPredict); } //============================================================================= // Dark owner-drawn helper controls //============================================================================= static const char* SE_DARK_BUTTON_OLDPROC = "IceTech.ScriptEditor.DarkButtonOldProc"; static const char* SE_DARK_STATIC_OLDPROC = "IceTech.ScriptEditor.DarkStaticOldProc"; static const char* SE_DARK_LIST_OLDPROC = "IceTech.ScriptEditor.DarkListOldProc"; #ifndef BS_TYPEMASK #define BS_TYPEMASK 0x0000000F #endif static void ScriptEditorDrawButton(HWND hWnd, HDC hDC) { RECT rc; ::GetClientRect(hWnd, &rc); const UINT style = (UINT)::GetWindowLong(hWnd, GWL_STYLE); const UINT type = style & BS_TYPEMASK; const bool enabled = ::IsWindowEnabled(hWnd) ? true : false; const bool pushed = (::SendMessage(hWnd, BM_GETSTATE, 0, 0) & BST_PUSHED) != 0; const bool defaultButton = (type == BS_DEFPUSHBUTTON); char text[128]; text[0] = '\0'; ::GetWindowTextA(hWnd, text, sizeof(text)); COLORREF face = pushed ? RGB(18, 22, 28) : RGB(31, 36, 45); if (defaultButton && enabled) { face = pushed ? RGB(42, 82, 126) : RGB(33, 68, 107); } ScriptEditorFillRect(hDC, rc, face); ScriptEditorFrameRect(hDC, rc, enabled ? (defaultButton ? SE_DARK_ACCENT : SE_DARK_BORDER) : RGB(42, 48, 56)); HFONT font = (HFONT)::SendMessage(hWnd, WM_GETFONT, 0, 0); HFONT oldFont = font ? (HFONT)::SelectObject(hDC, font) : NULL; ::SetBkMode(hDC, TRANSPARENT); ::SetTextColor(hDC, enabled ? SE_DARK_TEXT : SE_DARK_DISABLED); RECT textRect = rc; if (pushed) { ::OffsetRect(&textRect, 1, 1); } ::DrawTextA(hDC, text, -1, &textRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); if (oldFont) { ::SelectObject(hDC, oldFont); } } static LRESULT CALLBACK ScriptEditorDarkButtonProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { WNDPROC oldProc = (WNDPROC)::GetPropA(hWnd, SE_DARK_BUTTON_OLDPROC); if (!oldProc) { return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } switch (uMsg) { case WM_ERASEBKGND: return 1; case WM_PAINT: { PAINTSTRUCT ps; HDC hDC = ::BeginPaint(hWnd, &ps); ScriptEditorDrawButton(hWnd, hDC); ::EndPaint(hWnd, &ps); return 0; } case WM_PRINTCLIENT: ScriptEditorDrawButton(hWnd, (HDC)wParam); return 0; case WM_MOUSEMOVE: case WM_LBUTTONDOWN: case WM_LBUTTONUP: case WM_ENABLE: case WM_SETTEXT: case BM_SETSTATE: { LRESULT result = ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); ::InvalidateRect(hWnd, NULL, FALSE); return result; } case WM_NCDESTROY: { ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldProc); ::RemovePropA(hWnd, SE_DARK_BUTTON_OLDPROC); return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); } } return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); } static void ScriptEditorSubclassButton(CWnd& wnd) { HWND hWnd = wnd.GetSafeHwnd(); if (!hWnd || ::GetPropA(hWnd, SE_DARK_BUTTON_OLDPROC)) { return; } WNDPROC oldProc = (WNDPROC)::GetWindowLongPtr(hWnd, GWLP_WNDPROC); ::SetPropA(hWnd, SE_DARK_BUTTON_OLDPROC, (HANDLE)oldProc); ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)ScriptEditorDarkButtonProc); ::InvalidateRect(hWnd, NULL, TRUE); } static void ScriptEditorDrawStatic(HWND hWnd, HDC hDC) { RECT rc; ::GetClientRect(hWnd, &rc); ScriptEditorFillRect(hDC, rc, SE_DARK_PANEL); char text[512]; text[0] = '\0'; ::GetWindowTextA(hWnd, text, sizeof(text)); HFONT font = (HFONT)::SendMessage(hWnd, WM_GETFONT, 0, 0); HFONT oldFont = font ? (HFONT)::SelectObject(hDC, font) : NULL; ::SetBkMode(hDC, TRANSPARENT); COLORREF textColor = SE_DARK_TEXT; const int id = ::GetDlgCtrlID(hWnd); if (id == IDC_SCRIPTEDITOR_PATH || id == IDC_SCRIPTEDITOR_STATUS || id == IDC_SCRIPTEDITOR_LANGUAGE) { textColor = SE_DARK_MUTED; } ::SetTextColor(hDC, textColor); RECT textRect = rc; textRect.left += 4; textRect.right -= 4; UINT flags = DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS; if (id == IDC_SCRIPTEDITOR_LANGUAGE) { flags = DT_RIGHT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS; } ::DrawTextA(hDC, text, -1, &textRect, flags); if (oldFont) { ::SelectObject(hDC, oldFont); } } static LRESULT CALLBACK ScriptEditorDarkStaticProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { WNDPROC oldProc = (WNDPROC)::GetPropA(hWnd, SE_DARK_STATIC_OLDPROC); if (!oldProc) { return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } if (uMsg == WM_PAINT) { PAINTSTRUCT ps; HDC hDC = ::BeginPaint(hWnd, &ps); ScriptEditorDrawStatic(hWnd, hDC); ::EndPaint(hWnd, &ps); return 0; } if (uMsg == WM_ERASEBKGND) { return 1; } if (uMsg == WM_SETTEXT) { LRESULT result = ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); ::InvalidateRect(hWnd, NULL, FALSE); return result; } if (uMsg == WM_NCDESTROY) { ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldProc); ::RemovePropA(hWnd, SE_DARK_STATIC_OLDPROC); return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); } return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); } static void ScriptEditorSubclassStatic(CWnd& wnd) { HWND hWnd = wnd.GetSafeHwnd(); if (!hWnd || ::GetPropA(hWnd, SE_DARK_STATIC_OLDPROC)) { return; } WNDPROC oldProc = (WNDPROC)::GetWindowLongPtr(hWnd, GWLP_WNDPROC); ::SetPropA(hWnd, SE_DARK_STATIC_OLDPROC, (HANDLE)oldProc); ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)ScriptEditorDarkStaticProc); } static LRESULT CALLBACK ScriptEditorDarkListProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { WNDPROC oldProc = (WNDPROC)::GetPropA(hWnd, SE_DARK_LIST_OLDPROC); if (!oldProc) { return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } if (uMsg == WM_NCDESTROY) { ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldProc); ::RemovePropA(hWnd, SE_DARK_LIST_OLDPROC); return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); } if (uMsg == WM_KEYDOWN && (wParam == VK_RETURN || wParam == VK_TAB)) { ::SendMessage(::GetParent(hWnd), WM_COMMAND, MAKEWPARAM(IDC_SCRIPTEDITOR_INTELLISENSE_LIST, LBN_DBLCLK), (LPARAM)hWnd); return 0; } return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); } static void ScriptEditorSubclassList(CWnd& wnd) { HWND hWnd = wnd.GetSafeHwnd(); if (!hWnd || ::GetPropA(hWnd, SE_DARK_LIST_OLDPROC)) { return; } WNDPROC oldProc = (WNDPROC)::GetWindowLongPtr(hWnd, GWLP_WNDPROC); ::SetPropA(hWnd, SE_DARK_LIST_OLDPROC, (HANDLE)oldProc); ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)ScriptEditorDarkListProc); } /* ================ Script editor event helpers ================ */ static bool GetFunctionParms(const char* funcName, CString& parmString) { for (int i = 0; i < scriptEvents.Num(); i++) { if (scriptEvents[i].name.Cmp(funcName) == 0) { parmString = scriptEvents[i].parms; return true; } } return false; } static bool GetToolTip(const char* name, CString& string) { for (int i = 0; i < scriptEvents.Num(); i++) { if (scriptEvents[i].name.Cmp(name) == 0) { string = scriptEvents[i].help + scriptEvents[i].parms; return true; } } return false; } toolTip_t DialogScriptEditor::toolTips[] = { { IDC_SCRIPTEDITOR_BUTTON_NEW, "Create a new script buffer" }, { IDC_SCRIPTEDITOR_BUTTON_OPEN, "Open a script from the loaded pk5/dev filesystem" }, { IDOK, "Save script" }, { IDC_SCRIPTEDITOR_BUTTON_SAVEAS, "Save script to a new file" }, { IDCANCEL, "Revert to last saved version" }, { IDC_SCRIPTEDITOR_BUTTON_FIND, "Find" }, { IDC_SCRIPTEDITOR_BUTTON_REPLACE, "Replace" }, { IDC_SCRIPTEDITOR_BUTTON_GOTOLINE, "Go to line" }, { 0, NULL } }; IMPLEMENT_DYNAMIC(DialogScriptEditor, CWnd) BEGIN_MESSAGE_MAP(DialogScriptEditor, CWnd) ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnToolTipNotify) ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnToolTipNotify) ON_WM_CREATE() ON_WM_DESTROY() ON_WM_ACTIVATE() ON_WM_MOVE() ON_WM_SIZE() ON_WM_SIZING() ON_WM_SETFOCUS() ON_WM_TIMER() ON_WM_ERASEBKGND() ON_WM_CTLCOLOR() ON_WM_PAINT() ON_WM_DRAWITEM() ON_WM_MEASUREITEM() ON_COMMAND(ID_FILE_NEW, OnFileNew) ON_COMMAND(ID_FILE_OPEN, OnFileOpen) ON_COMMAND(ID_FILE_SAVE_AS, OnFileSaveAs) ON_COMMAND(ID_EDIT_FIND, OnEditFind) ON_COMMAND(ID_EDIT_REPLACE, OnEditReplace) ON_COMMAND(ID_SCRIPTEDITOR_FIND_NEXT, OnEditFindNext) ON_COMMAND(ID_SCRIPTEDITOR_GOTOLINE, OnEditGoToLine) ON_COMMAND(IDC_SCRIPTEDITOR_BUTTON_NEW, OnFileNew) ON_COMMAND(IDC_SCRIPTEDITOR_BUTTON_OPEN, OnFileOpen) ON_COMMAND(IDC_SCRIPTEDITOR_BUTTON_SAVEAS, OnFileSaveAs) ON_COMMAND(IDC_SCRIPTEDITOR_BUTTON_FIND, OnEditFind) ON_COMMAND(IDC_SCRIPTEDITOR_BUTTON_REPLACE, OnEditReplace) ON_COMMAND(IDC_SCRIPTEDITOR_BUTTON_GOTOLINE, OnEditGoToLine) ON_COMMAND(IDC_SCRIPTEDITOR_BUTTON_INTELLISENSE, OnEditShowIntelliSense) ON_REGISTERED_MESSAGE(FindDialogMessage, OnFindDialogMessage) ON_MESSAGE(WM_SCRIPTEDITOR_DEFERRED_INTELLISENSE, OnDeferredIntelliSense) ON_MESSAGE(WM_SCRIPTEDITOR_INTELLISENSE_KEY, OnIntelliSenseKey) ON_NOTIFY(EN_CHANGE, IDC_SCRIPTEDITOR_EDIT_TEXT, OnEnChangeEdit) ON_NOTIFY(EN_SELCHANGE, IDC_SCRIPTEDITOR_EDIT_TEXT, OnEditorSelectionChanged) ON_LBN_DBLCLK(IDC_SCRIPTEDITOR_INTELLISENSE_LIST, OnIntelliSenseDblClick) ON_LBN_SELCHANGE(IDC_SCRIPTEDITOR_INTELLISENSE_LIST, OnIntelliSenseSelChange) ON_BN_CLICKED(IDOK, OnBnClickedOk) ON_BN_CLICKED(IDCANCEL, OnBnClickedCancel) END_MESSAGE_MAP() DialogScriptEditor::DialogScriptEditor(CWnd* pParent /*=NULL*/) : CWnd() , scriptEdit(NULL) , findDlg(NULL) , matchCase(false) , matchWholeWords(false) , searchForward(true) , firstLine(1) , isDirty(false) , internalChange(false) , isActive(FALSE) , m_intelliSenseReady(false) , m_intelliSenseCoreReady(false) , m_virtualFileListReady(false) , m_backgroundScanActive(false) , m_backgroundScanNextFile(0) , m_intelliSenseTimer(0) , m_indexedScriptFiles(0) , m_indexedSymbols(0) { m_hAccel = NULL; initialRect.SetRect(0, 0, 960, 640); m_backBrush.CreateSolidBrush(SE_DARK_BG); m_panelBrush.CreateSolidBrush(SE_DARK_PANEL); m_editBrush.CreateSolidBrush(SE_DARK_EDIT); m_hotBrush.CreateSolidBrush(SE_DARK_PANEL_2); } DialogScriptEditor::~DialogScriptEditor() { if (primaryEditor == this) { primaryEditor = NULL; } delete scriptEdit; scriptEdit = NULL; } BOOL DialogScriptEditor::Create(CWnd* pParent, UINT nID) { CRect rect(0, 0, 0, 0); return Create(rect, pParent, nID); } BOOL DialogScriptEditor::Create(const RECT& rect, CWnd* pParent, UINT nID) { CString className = AfxRegisterWndClass( CS_DBLCLKS, ::LoadCursor(NULL, IDC_ARROW), (HBRUSH)m_backBrush.GetSafeHandle(), NULL ); DWORD style = WS_CLIPCHILDREN | WS_CLIPSIBLINGS; DWORD exStyle = 0; if (pParent) { style |= WS_CHILD; } else { style |= WS_OVERLAPPEDWINDOW | WS_VISIBLE; exStyle |= WS_EX_APPWINDOW; } return CWnd::CreateEx( exStyle, className, "Script Editor", style, rect, pParent, nID ); } DialogScriptEditor* DialogScriptEditor::GetPrimaryEditor(void) { return primaryEditor; } DialogScriptEditor* ScriptEditorGetPrimaryEditor(void) { return DialogScriptEditor::GetPrimaryEditor(); } int DialogScriptEditor::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CWnd::OnCreate(lpCreateStruct) == -1) { return -1; } primaryEditor = this; com_editors |= EDITOR_SCRIPT; InitEditorChrome(); ApplyDarkTheme(); m_hAccel = ::LoadAccelerators(AfxGetResourceHandle(), MAKEINTRESOURCE(IDR_ACCELERATOR_SCRIPTEDITOR)); EnableToolTips(TRUE); EnsureIntelliSenseCore(); // Do not enumerate or parse every pk5 script synchronously while the dock tab // is being created. A short timer starts the incremental background indexer // after the window is visible and responsive. if (m_intelliSenseTimer == 0) { m_intelliSenseTimer = SetTimer(SE_INTELLISENSE_TIMER_ID, 250, NULL); } UpdateLanguageLabel("TEXT"); UpdateTitle(); UpdateStatusBar(); LayoutChildren(); SetDirty(false); return 0; } void DialogScriptEditor::InitEditorChrome(void) { ScriptEditorCreateFontPixels(m_uiFont, "Segoe UI", 9, false); ScriptEditorCreateFontPixels(m_editorFont, "Consolas", 11, true); m_titleLabel.Create("SCRIPT EDITOR", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this, IDC_SCRIPTEDITOR_TITLE); m_pathLabel.Create("No file loaded", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this, IDC_SCRIPTEDITOR_PATH); m_languageLabel.Create("", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this, IDC_SCRIPTEDITOR_LANGUAGE); m_statusLine.Create("", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this, IDC_SCRIPTEDITOR_STATUS); m_signatureHelp.Create("", WS_CHILD | SS_CENTERIMAGE | SS_LEFT, CRect(0, 0, 0, 0), this, IDC_SCRIPTEDITOR_SIGNATURE_HELP); m_signatureHelp.ShowWindow(SW_HIDE); newButton.Create("New", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDC_SCRIPTEDITOR_BUTTON_NEW); openButton.Create("Open Script...", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDC_SCRIPTEDITOR_BUTTON_OPEN); okButton.Create("Save", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, CRect(0, 0, 0, 0), this, IDOK); saveAsButton.Create("Save As...", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDC_SCRIPTEDITOR_BUTTON_SAVEAS); cancelButton.Create("Revert", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDCANCEL); findButton.Create("Find", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDC_SCRIPTEDITOR_BUTTON_FIND); replaceButton.Create("Replace", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDC_SCRIPTEDITOR_BUTTON_REPLACE); goToButton.Create("Go To", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDC_SCRIPTEDITOR_BUTTON_GOTOLINE); scriptEdit = new CScriptEditorCtrl(); scriptEdit->Create(this, IDC_SCRIPTEDITOR_EDIT_TEXT); scriptEdit->LimitText(1024 * 1024); scriptEdit->SetFont(&m_editorFont); scriptEdit->RefreshMetrics(); m_intellisenseList.Create( WS_CHILD | WS_BORDER | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | LBS_OWNERDRAWFIXED | LBS_HASSTRINGS, CRect(0, 0, 0, 0), this, IDC_SCRIPTEDITOR_INTELLISENSE_LIST ); m_intellisenseList.ShowWindow(SW_HIDE); CWnd* fontWindows[] = { &m_titleLabel, &m_pathLabel, &m_languageLabel, &m_statusLine, &m_signatureHelp, &newButton, &openButton, &okButton, &saveAsButton, &cancelButton, &findButton, &replaceButton, &goToButton, &m_intellisenseList }; for (int i = 0; i < sizeof(fontWindows) / sizeof(fontWindows[0]); i++) { if (fontWindows[i] && fontWindows[i]->GetSafeHwnd()) { fontWindows[i]->SetFont(&m_uiFont); } } } void DialogScriptEditor::ApplyDarkTheme(void) { ScriptEditorSubclassStatic(m_titleLabel); ScriptEditorSubclassStatic(m_pathLabel); ScriptEditorSubclassStatic(m_languageLabel); ScriptEditorSubclassStatic(m_statusLine); ScriptEditorSubclassStatic(m_signatureHelp); ScriptEditorSubclassButton(newButton); ScriptEditorSubclassButton(openButton); ScriptEditorSubclassButton(okButton); ScriptEditorSubclassButton(saveAsButton); ScriptEditorSubclassButton(cancelButton); ScriptEditorSubclassButton(findButton); ScriptEditorSubclassButton(replaceButton); ScriptEditorSubclassButton(goToButton); ScriptEditorSubclassList(m_intellisenseList); ScriptEditorApplyNativeDarkTheme(newButton.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(openButton.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(okButton.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(saveAsButton.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(cancelButton.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(findButton.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(replaceButton.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(goToButton.GetSafeHwnd()); ScriptEditorApplyNativeDarkTheme(m_intellisenseList.GetSafeHwnd()); } BOOL DialogScriptEditor::PreTranslateMessage(MSG* pMsg) { if (HandleIntelliSenseKey(pMsg)) { return TRUE; } if (pMsg && WM_KEYFIRST <= pMsg->message && pMsg->message <= WM_KEYLAST) { if (m_hAccel && GetSafeHwnd() && ::TranslateAccelerator(m_hWnd, m_hAccel, pMsg)) { return TRUE; } } return CWnd::PreTranslateMessage(pMsg); } BOOL DialogScriptEditor::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) { if (CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) { return TRUE; } return FALSE; } void DialogScriptEditor::InitScriptEvents(void) { int index; idParser src; idToken token; idStr whiteSpace; scriptEventInfo_t info; if (!src.LoadFile("script/doom_events.script")) { return; } scriptEvents.Clear(); while (src.ReadToken(&token)) { if (token == "scriptEvent") { src.GetLastWhiteSpace(whiteSpace); index = whiteSpace.Find("//"); if (index != -1) { info.help = whiteSpace.Right(whiteSpace.Length() - index); info.help.Replace("\r", ""); info.help.Replace("\n", "\r\n"); } else { info.help = ""; } src.ExpectTokenType(TT_NAME, 0, &token); info.parms = token; src.ExpectTokenType(TT_NAME, 0, &token); info.name = token; src.ExpectTokenString("("); info.parms += " " + info.name + "("; while (src.ReadToken(&token) && token != ";") { info.parms.Append(" " + token); } scriptEvents.Append(info); } } } void DialogScriptEditor::EnsureIntelliSenseCore(void) { if (scriptCoreBuilt) { m_intelliSenseCoreReady = true; m_intelliSenseReady = true; m_indexedSymbols = scriptCompletions.Num(); return; } // Fast path only: keywords and the engine event definitions. This deliberately // does not scan every script file so the editor tab can open immediately. InitScriptEvents(); for (int k = 0; scriptKeywords[k]; k++) { ScriptEditorAddCompletion(scriptKeywords[k], scriptKeywords[k], "keyword", "built-in", "script keyword", 100); } for (int k = 0; guiKeywords[k]; k++) { ScriptEditorAddCompletion(guiKeywords[k], guiKeywords[k], "gui", "built-in", "gui keyword", 90); } for (int i = 0; i < scriptEvents.Num(); i++) { ScriptEditorAddCompletion(scriptEvents[i].name.c_str(), scriptEvents[i].name.c_str(), "event", "script/doom_events.script", scriptEvents[i].parms.c_str(), 140); } scriptCoreBuilt = true; m_intelliSenseCoreReady = true; m_intelliSenseReady = true; m_indexedSymbols = scriptCompletions.Num(); } void DialogScriptEditor::RebuildVirtualScriptFileList(void) { scriptVirtualFiles.Clear(); if (fileSystem) { // Try both common extension spellings. Different idTech file-system forks // disagree about whether ListFilesTree wants ".script" or "script". ScriptEditorListVirtualFilesFromTree("script", ".script"); ScriptEditorListVirtualFilesFromTree("script", "script"); ScriptEditorListVirtualFilesFromTree("scripts", ".script"); ScriptEditorListVirtualFilesFromTree("scripts", "script"); ScriptEditorListVirtualFilesFromTree("guis", ".gui"); ScriptEditorListVirtualFilesFromTree("guis", "gui"); ScriptEditorListVirtualFilesFromTree("gui", ".gui"); ScriptEditorListVirtualFilesFromTree("gui", "gui"); // Last-resort root scan. This catches pk5s that do not expose the expected // directory listing under script/ but do expose extension queries globally. if (scriptVirtualFiles.Num() <= 1) { ScriptEditorListVirtualFilesFromTree("", ".script"); ScriptEditorListVirtualFilesFromTree("", "script"); ScriptEditorListVirtualFilesFromTree("", ".gui"); ScriptEditorListVirtualFilesFromTree("", "gui"); } } // Keep the core event file available even if a particular filesystem backend // does not include it in a directory listing. ScriptEditorAddVirtualFile("", "script/doom_events.script"); scriptVirtualFileListBuilt = true; m_virtualFileListReady = true; } void DialogScriptEditor::EnsureVirtualScriptFileList(bool forceRebuild) { if (forceRebuild || !scriptVirtualFileListBuilt) { RebuildVirtualScriptFileList(); } else { m_virtualFileListReady = true; } } void DialogScriptEditor::StartIntelliSenseBackgroundScan(bool forceRebuild) { if (forceRebuild) { if (m_intelliSenseTimer) { KillTimer(m_intelliSenseTimer); m_intelliSenseTimer = 0; } scriptCompletions.Clear(); scriptEvents.Clear(); scriptCoreBuilt = false; scriptDatabaseBuilt = false; scriptVirtualFileListBuilt = false; m_intelliSenseCoreReady = false; m_virtualFileListReady = false; m_backgroundScanNextFile = 0; m_indexedScriptFiles = 0; m_indexedSymbols = 0; } EnsureIntelliSenseCore(); if (scriptDatabaseBuilt && !forceRebuild) { m_backgroundScanActive = false; return; } // Directory enumeration is cheap compared to parsing every file and is needed // for the Open Script browser. The heavier parsing below happens by timer. EnsureVirtualScriptFileList(forceRebuild); if (scriptVirtualFiles.Num() <= 0) { m_backgroundScanActive = false; return; } m_backgroundScanActive = true; if (m_intelliSenseTimer == 0 && GetSafeHwnd()) { m_intelliSenseTimer = SetTimer(SE_INTELLISENSE_TIMER_ID, 40, NULL); } } void DialogScriptEditor::IndexNextScriptFiles(int maxFiles) { if (!m_backgroundScanActive) { return; } EnsureIntelliSenseCore(); EnsureVirtualScriptFileList(false); int indexedThisTick = 0; while (m_backgroundScanNextFile < scriptVirtualFiles.Num() && indexedThisTick < maxFiles) { const char* path = scriptVirtualFiles[m_backgroundScanNextFile].c_str(); m_backgroundScanNextFile++; // doom_events.script is already parsed by InitScriptEvents(), but scanning it // again is harmless. Skipping it avoids duplicated generic function symbols. if (idStr::Icmp(path, "script/doom_events.script") == 0) { continue; } idStr text; if (LoadTextFromFile(path, text)) { ScriptEditorRemoveCompletionsFromSource(path); ScriptEditorScanTextForCompletions(path, text.c_str()); m_indexedScriptFiles++; } indexedThisTick++; } m_indexedSymbols = scriptCompletions.Num(); if (m_backgroundScanNextFile >= scriptVirtualFiles.Num()) { m_backgroundScanActive = false; scriptDatabaseBuilt = true; if (m_intelliSenseTimer) { KillTimer(m_intelliSenseTimer); m_intelliSenseTimer = 0; } if (m_statusLine.GetSafeHwnd()) { CString status; status.Format("IntelliSense ready: indexed %d script/gui files and %d symbols.", m_indexedScriptFiles, m_indexedSymbols); m_statusLine.SetWindowText(status); } } else if (m_statusLine.GetSafeHwnd()) { CString status; status.Format("IntelliSense indexing %d/%d files...", m_backgroundScanNextFile, scriptVirtualFiles.Num()); m_statusLine.SetWindowText(status); } } void DialogScriptEditor::UpdateLiveCompletions(void) { ScriptEditorRemoveCompletionsFromSource(""); if (!scriptEdit) { return; } idStr text; scriptEdit->GetText(text); if (text.Length() > 0) { ScriptEditorScanTextForCompletions("", text.c_str()); } m_indexedSymbols = scriptCompletions.Num(); } void DialogScriptEditor::BuildIntelliSenseDatabase(bool forceRebuild) { StartIntelliSenseBackgroundScan(forceRebuild); } void DialogScriptEditor::ConfigureEditorForFile(const char* newFileName) { idStr extension; idStr(newFileName ? newFileName : "").ExtractFileExtension(extension); if (!scriptEdit) { return; } EnsureIntelliSenseCore(); scriptEdit->SetCaseSensitive(false); scriptEdit->ClearKeywords(); scriptEdit->ClearFunctionWords(); if (extension.Icmp("script") == 0) { scriptEdit->SetCaseSensitive(true); for (int k = 0; scriptKeywords[k]; k++) { scriptEdit->AddKeyword(scriptKeywords[k]); } scriptEdit->LoadKeyWordsFromFile("editors/script.def"); for (int i = 0; i < scriptCompletions.Num(); i++) { if (scriptCompletions[i].kind.Icmp("function") == 0 || scriptCompletions[i].kind.Icmp("event") == 0) { scriptEdit->AddFunctionWord(scriptCompletions[i].name.c_str()); } } UpdateLanguageLabel("SCRIPT"); } else if (extension.Icmp("gui") == 0) { for (int k = 0; guiKeywords[k]; k++) { scriptEdit->AddKeyword(guiKeywords[k]); } scriptEdit->LoadKeyWordsFromFile("editors/gui.def"); UpdateLanguageLabel("GUI"); } else { for (int k = 0; scriptKeywords[k]; k++) { scriptEdit->AddKeyword(scriptKeywords[k]); } UpdateLanguageLabel(extension.Length() ? extension.c_str() : "TEXT"); } } bool DialogScriptEditor::IsNativeFilePath(const char* path) const { if (!path || !path[0]) { return false; } if (path[0] && path[1] == ':') { return true; } if (path[0] == '\\' || path[0] == '/') { return true; } return false; } bool DialogScriptEditor::LoadTextFromFile(const char* path, idStr& text) const { text = ""; if (!path || !path[0]) { return false; } void* buffer = NULL; if (fileSystem && fileSystem->ReadFile(path, &buffer) != -1) { text = (char*)buffer; fileSystem->FreeFile(buffer); return true; } FILE* fp = fopen(path, "rb"); if (!fp) { return false; } fseek(fp, 0, SEEK_END); long len = ftell(fp); fseek(fp, 0, SEEK_SET); if (len < 0) { fclose(fp); return false; } char* raw = new char[len + 1]; long readLen = (long)fread(raw, 1, len, fp); raw[readLen] = '\0'; fclose(fp); text = raw; delete[] raw; return true; } bool DialogScriptEditor::WriteTextToFile(const char* path, const char* text, int textLength) const { if (!path || !path[0]) { return false; } if (IsNativeFilePath(path)) { FILE* fp = fopen(path, "wb"); if (!fp) { return false; } int written = (int)fwrite(text, 1, textLength, fp); fclose(fp); return written == textLength; } return fileSystem && fileSystem->WriteFile(path, text, textLength, "fs_devpath") != -1; } bool DialogScriptEditor::OpenFileFromDialog(void) { EnsureVirtualScriptFileList(true); StartIntelliSenseBackgroundScan(false); CString selectedPath; CScriptFileBrowserWnd browser; if (browser.DoModal(this, selectedPath) != IDOK || selectedPath.IsEmpty()) { FocusEditor(); return false; } OpenFile(selectedPath); return true; } bool DialogScriptEditor::PromptSavePath(CString& selectedPath) { CString suggested; if (fileName.Length() > 0) { suggested = fileName.c_str(); } else { suggested = "script/new_script.script"; } CScriptSavePathWnd prompt; return prompt.DoModal(this, selectedPath, suggested) == IDOK && !selectedPath.IsEmpty(); } bool DialogScriptEditor::SaveFileAs(void) { return SaveFileAsDialog(); } bool DialogScriptEditor::SaveFileAsDialog(void) { CString selectedPath; if (!PromptSavePath(selectedPath)) { FocusEditor(); return false; } idStr oldFileName = fileName; fileName = (LPCTSTR)selectedPath; ConfigureEditorForFile(fileName.c_str()); if (!SaveFile()) { fileName = oldFileName; UpdateTitle(); FocusEditor(); return false; } ScriptEditorAddVirtualFile("", fileName.c_str()); scriptVirtualFileListBuilt = true; m_virtualFileListReady = true; UpdateTitle(); FocusEditor(); return true; } void DialogScriptEditor::NewFile(void) { if (!ConfirmDiscardChanges()) { FocusEditor(); return; } fileName = ""; loadedText = ""; ConfigureEditorForFile("script/new_script.script"); SetEditorTextNormalized(""); SetDirty(false); UpdateTitle(); UpdateStatusBar(); FocusEditor(); } void DialogScriptEditor::OpenFile(const char* newFileName) { if (!GetSafeHwnd() || !newFileName || !newFileName[0] || !scriptEdit) { return; } if (!ConfirmDiscardChanges()) { return; } idStr scriptText; if (!LoadTextFromFile(newFileName, scriptText)) { MessageBox(va("Couldn't open: %s", newFileName), "Script Editor", MB_OK | MB_ICONERROR); return; } fileName = newFileName; ConfigureEditorForFile(fileName.c_str()); SetEditorTextNormalized(scriptText.c_str()); loadedText = scriptText; loadedText.Replace("\r\n", "\n"); loadedText.Replace("\r", "\n"); loadedText.Replace("\v", "\n"); SetDirty(false); UpdateTitle(); UpdateStatusBar(); FocusEditor(); } void DialogScriptEditor::SetEditorTextNormalized(const char* text) { internalChange = true; if (scriptEdit) { scriptEdit->SetText(text ? text : ""); } internalChange = false; } bool DialogScriptEditor::GetEditorTextNormalized(idStr& text) const { if (!scriptEdit || !scriptEdit->GetSafeHwnd()) { text = ""; return false; } idStr raw; scriptEdit->GetText(raw); CString crlf = ScriptEditorToCRLF(CString(raw.c_str())); text = (LPCTSTR)crlf; return true; } bool DialogScriptEditor::SaveFile(void) { if (fileName.Length() == 0) { return SaveFileAsDialog(); } idStr scriptText; GetEditorTextNormalized(scriptText); common->Printf("Writing '%s'...\n", fileName.c_str()); if (!WriteTextToFile(fileName.c_str(), scriptText.c_str(), scriptText.Length())) { MessageBox(va("Couldn't save: %s", fileName.c_str()), va("Error saving: %s", fileName.c_str()), MB_OK | MB_ICONERROR); return false; } loadedText = scriptText; loadedText.Replace("\r\n", "\n"); loadedText.Replace("\r", "\n"); SetDirty(false); UpdateStatusBar(); return true; } bool DialogScriptEditor::ReloadFile(void) { if (fileName.Length() == 0) { return false; } idStr oldName = fileName; fileName = ""; OpenFile(oldName.c_str()); return true; } bool DialogScriptEditor::HasOpenFile(void) const { return fileName.Length() > 0; } bool DialogScriptEditor::IsDirty(void) const { return isDirty; } const char* DialogScriptEditor::GetFileName(void) const { return fileName.c_str(); } bool DialogScriptEditor::ConfirmDiscardChanges(void) { if (!isDirty) { return true; } int result = MessageBox("The current script has unsaved changes. Save them before continuing?", "Script Editor", MB_YESNOCANCEL | MB_ICONQUESTION); if (result == IDCANCEL) { return false; } if (result == IDYES) { return SaveFile(); } return true; } void DialogScriptEditor::SetDirty(bool dirty) { isDirty = dirty; if (okButton.GetSafeHwnd()) { okButton.EnableWindow(dirty || fileName.Length() == 0); } if (saveAsButton.GetSafeHwnd()) { saveAsButton.EnableWindow(TRUE); } if (cancelButton.GetSafeHwnd()) { cancelButton.EnableWindow(fileName.Length() > 0); } UpdateTitle(); } void DialogScriptEditor::UpdateTitle(void) { CString title; if (fileName.Length() > 0) { title.Format("SCRIPT EDITOR%s", isDirty ? " *" : ""); m_pathLabel.SetWindowText(fileName.c_str()); } else { title.Format("SCRIPT EDITOR%s", isDirty ? " *" : ""); m_pathLabel.SetWindowText(isDirty ? "New script (not saved yet)" : "No file loaded"); } m_titleLabel.SetWindowText(title); SetWindowText(fileName.Length() > 0 ? va("Script Editor (%s)%s", fileName.c_str(), isDirty ? " *" : "") : "Script Editor"); } void DialogScriptEditor::UpdateLanguageLabel(const char* extension) { CString text; if (m_intelliSenseReady) { text.Format("%s | IntelliSense: %d symbols", extension && extension[0] ? extension : "TEXT", m_indexedSymbols); } else { text.Format("%s | IntelliSense indexing...", extension && extension[0] ? extension : "TEXT"); } m_languageLabel.SetWindowText(text); } void DialogScriptEditor::UpdateStatusBar(void) { if (!scriptEdit || !scriptEdit->GetSafeHwnd() || !m_statusLine.GetSafeHwnd()) { return; } int line = 0; int column = 0; int character = 0; scriptEdit->GetCursorPos(line, column, character); CString status; status.Format("Line: %d Column: %d Character: %d%s", line, column, character, isDirty ? " Modified" : ""); m_statusLine.SetWindowText(status); } void DialogScriptEditor::LayoutChildren(void) { if (!GetSafeHwnd()) { return; } CRect client; GetClientRect(client); const int margin = 8; const int topH = 58; const int bottomH = 24; const int buttonH = 24; const int buttonGap = 6; int x = client.left + margin; int y = client.top + 6; if (m_titleLabel.GetSafeHwnd()) { m_titleLabel.MoveWindow(x, y, 160, 22, TRUE); } int rightX = client.right - margin; CWnd* buttons[] = { &newButton, &openButton, &okButton, &saveAsButton, &cancelButton, &findButton, &replaceButton, &goToButton }; int widths[] = { 58, 112, 66, 86, 72, 64, 74, 68 }; for (int i = sizeof(buttons) / sizeof(buttons[0]) - 1; i >= 0; i--) { if (buttons[i]->GetSafeHwnd()) { rightX -= widths[i]; buttons[i]->MoveWindow(rightX, y, widths[i], buttonH, TRUE); rightX -= buttonGap; } } CRect pathRect(x, y + 26, client.right - margin - 250, y + 48); if (pathRect.right < pathRect.left + 80) { pathRect.right = pathRect.left + 80; } if (m_pathLabel.GetSafeHwnd()) { m_pathLabel.MoveWindow(pathRect, TRUE); } if (m_languageLabel.GetSafeHwnd()) { m_languageLabel.MoveWindow(client.right - margin - 240, y + 26, 240, 22, TRUE); } CRect editRect( client.left + margin, client.top + topH, client.right - margin, client.bottom - bottomH - margin ); if (editRect.Width() < SE_MIN_EDITOR_WIDTH) { editRect.right = editRect.left + SE_MIN_EDITOR_WIDTH; } if (editRect.Height() < SE_MIN_EDITOR_HEIGHT) { editRect.bottom = editRect.top + SE_MIN_EDITOR_HEIGHT; } if (scriptEdit && scriptEdit->GetSafeHwnd()) { scriptEdit->MoveWindow(editRect, TRUE); } if (m_statusLine.GetSafeHwnd()) { m_statusLine.MoveWindow(client.left + margin, client.bottom - bottomH, client.Width() - margin * 2, bottomH - 2, TRUE); } if (m_intellisenseList.GetSafeHwnd() && m_intellisenseList.IsWindowVisible()) { PositionIntelliSenseWindow(); } if (m_signatureHelp.GetSafeHwnd() && m_signatureHelp.IsWindowVisible()) { PositionParameterHint(); } } void DialogScriptEditor::SetActive(BOOL bActive) { isActive = bActive; if (bActive) { FocusEditor(); } } void DialogScriptEditor::FocusEditor(void) { if (scriptEdit && scriptEdit->GetSafeHwnd()) { scriptEdit->SetFocus(); } } void DialogScriptEditor::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy); LayoutChildren(); } void DialogScriptEditor::OnSizing(UINT nSide, LPRECT lpRect) { CWnd::OnSizing(nSide, lpRect); if (!lpRect) { return; } if (lpRect->right - lpRect->left < initialRect.Width()) { lpRect->right = lpRect->left + initialRect.Width(); } if (lpRect->bottom - lpRect->top < initialRect.Height()) { lpRect->bottom = lpRect->top + initialRect.Height(); } } void DialogScriptEditor::OnMove(int x, int y) { CWnd::OnMove(x, y); } void DialogScriptEditor::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized) { CWnd::OnActivate(nState, pWndOther, bMinimized); } void DialogScriptEditor::OnSetFocus(CWnd* pOldWnd) { CWnd::OnSetFocus(pOldWnd); FocusEditor(); } void DialogScriptEditor::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == SE_INTELLISENSE_TIMER_ID) { if (!m_backgroundScanActive) { if (scriptDatabaseBuilt) { KillTimer(m_intelliSenseTimer); m_intelliSenseTimer = 0; return; } StartIntelliSenseBackgroundScan(false); } if (m_backgroundScanActive) { IndexNextScriptFiles(2); } else if (m_intelliSenseTimer) { KillTimer(m_intelliSenseTimer); m_intelliSenseTimer = 0; } return; } CWnd::OnTimer(nIDEvent); } void DialogScriptEditor::OnDestroy() { if (m_intelliSenseTimer) { KillTimer(m_intelliSenseTimer); m_intelliSenseTimer = 0; } if (findDlg) { if (findDlg->GetSafeHwnd()) { findDlg->DestroyWindow(); } delete findDlg; findDlg = NULL; } if (primaryEditor == this) { primaryEditor = NULL; } CWnd::OnDestroy(); } BOOL DialogScriptEditor::OnEraseBkgnd(CDC* pDC) { CRect client; GetClientRect(client); pDC->FillSolidRect(client, SE_DARK_BG); return TRUE; } void DialogScriptEditor::OnPaint() { CPaintDC dc(this); CRect client; GetClientRect(client); dc.FillSolidRect(client, SE_DARK_BG); CRect header(client.left, client.top, client.right, client.top + 56); dc.FillSolidRect(header, SE_DARK_PANEL); dc.FillSolidRect(client.left, header.bottom - 1, client.Width(), 1, SE_DARK_BORDER); CRect editFrame; if (scriptEdit && scriptEdit->GetSafeHwnd()) { scriptEdit->GetWindowRect(editFrame); ScreenToClient(editFrame); editFrame.InflateRect(1, 1); dc.Draw3dRect(editFrame, SE_DARK_BORDER, RGB(4, 6, 10)); } } HBRUSH DialogScriptEditor::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { if (pDC) { pDC->SetBkMode(TRANSPARENT); pDC->SetTextColor(SE_DARK_TEXT); } if (nCtlColor == CTLCOLOR_LISTBOX) { if (pDC) { pDC->SetBkColor(SE_DARK_EDIT); pDC->SetTextColor(SE_DARK_TEXT); } return (HBRUSH)m_editBrush.GetSafeHandle(); } if (nCtlColor == CTLCOLOR_STATIC) { return (HBRUSH)m_panelBrush.GetSafeHandle(); } return (HBRUSH)m_backBrush.GetSafeHandle(); } void DialogScriptEditor::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT dis) { if (nIDCtl == IDC_SCRIPTEDITOR_INTELLISENSE_LIST && dis) { HDC hDC = dis->hDC; RECT rc = dis->rcItem; bool selected = (dis->itemState & ODS_SELECTED) != 0; ScriptEditorFillRect(hDC, rc, selected ? SE_DARK_SELECTION : SE_DARK_PANEL_2); if (dis->itemID != (UINT)-1) { CString text; m_intellisenseList.GetText(dis->itemID, text); HFONT font = (HFONT)m_uiFont.GetSafeHandle(); HFONT oldFont = font ? (HFONT)::SelectObject(hDC, font) : NULL; ::SetBkMode(hDC, TRANSPARENT); ::SetTextColor(hDC, selected ? SE_DARK_SELECTION_TEXT : SE_DARK_TEXT); rc.left += 8; rc.right -= 4; ::DrawTextA(hDC, text, -1, &rc, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); if (oldFont) { ::SelectObject(hDC, oldFont); } } return; } CWnd::OnDrawItem(nIDCtl, dis); } void DialogScriptEditor::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT mis) { if (nIDCtl == IDC_SCRIPTEDITOR_INTELLISENSE_LIST && mis) { mis->itemHeight = 20; return; } CWnd::OnMeasureItem(nIDCtl, mis); } BOOL DialogScriptEditor::OnToolTipNotify(UINT id, NMHDR* pNMHDR, LRESULT* pResult) { return DefaultOnToolTipNotify(toolTips, id, pNMHDR, pResult); } void DialogScriptEditor::OnFileNew() { NewFile(); } void DialogScriptEditor::OnFileOpen() { OpenFileFromDialog(); } void DialogScriptEditor::OnFileSaveAs() { SaveFileAsDialog(); } void DialogScriptEditor::OnEditGoToLine() { if (!scriptEdit || !scriptEdit->GetSafeHwnd()) { return; } DialogGoToLine goToLineDlg; goToLineDlg.SetRange(1, scriptEdit->GetLineCount()); if (goToLineDlg.DoModal() != IDOK) { return; } scriptEdit->GoToLine(goToLineDlg.GetLine() - 1); FocusEditor(); UpdateStatusBar(); } void DialogScriptEditor::OnEditFind() { CString selText = scriptEdit ? scriptEdit->GetSelText() : ""; if (selText.GetLength()) { findStr = selText; } if (!findDlg) { findDlg = new CFindReplaceDialog(); findDlg->Create(TRUE, findStr, "", FR_DOWN, this); } } void DialogScriptEditor::OnEditFindNext() { if (findStr.GetLength() == 0) { OnEditFind(); return; } if (scriptEdit && scriptEdit->FindNext(findStr, matchCase, matchWholeWords, searchForward)) { FocusEditor(); UpdateStatusBar(); } else { AfxMessageBox("The specified text was not found.", MB_OK | MB_ICONINFORMATION, 0); } } void DialogScriptEditor::OnEditReplace() { CString selText = scriptEdit ? scriptEdit->GetSelText() : ""; if (selText.GetLength()) { findStr = selText; } if (!findDlg) { findDlg = new CFindReplaceDialog(); findDlg->Create(FALSE, findStr, "", FR_DOWN, this); } } void DialogScriptEditor::OnEditShowIntelliSense() { ShowIntelliSense(true); } LRESULT DialogScriptEditor::OnFindDialogMessage(WPARAM wParam, LPARAM lParam) { if (findDlg == NULL) { return 0; } if (findDlg->IsTerminating()) { findDlg = NULL; return 0; } if (findDlg->FindNext()) { findStr = findDlg->GetFindString(); matchCase = findDlg->MatchCase() != FALSE; matchWholeWords = findDlg->MatchWholeWord() != FALSE; searchForward = findDlg->SearchDown() != FALSE; OnEditFindNext(); } if (findDlg->ReplaceCurrent()) { long selStart, selEnd; replaceStr = findDlg->GetReplaceString(); scriptEdit->GetSel(selStart, selEnd); if (selEnd > selStart) { scriptEdit->ReplaceSel(replaceStr, TRUE); SetDirty(true); } } if (findDlg->ReplaceAll()) { replaceStr = findDlg->GetReplaceString(); findStr = findDlg->GetFindString(); matchCase = findDlg->MatchCase() != FALSE; matchWholeWords = findDlg->MatchWholeWord() != FALSE; int numReplaces = scriptEdit ? scriptEdit->ReplaceAll(findStr, replaceStr, matchCase, matchWholeWords) : 0; if (numReplaces == 0) { AfxMessageBox("The specified text was not found.", MB_OK | MB_ICONINFORMATION, 0); } else { SetDirty(true); AfxMessageBox(va("Replaced %d occurrences.", numReplaces), MB_OK | MB_ICONINFORMATION, 0); } } return 0; } void DialogScriptEditor::OnEnChangeEdit(NMHDR* pNMHDR, LRESULT* pResult) { if (!internalChange) { SetDirty(true); UpdateStatusBar(); } if (pResult) { *pResult = 0; } } void DialogScriptEditor::OnEditorSelectionChanged(NMHDR* pNMHDR, LRESULT* pResult) { if (m_signatureHelp.GetSafeHwnd() && m_signatureHelp.IsWindowVisible() && !IsFunctionArgumentContext()) { HideFunctionParameterHint(); } UpdateStatusBar(); if (pResult) { *pResult = 0; } } LRESULT DialogScriptEditor::OnDeferredIntelliSense(WPARAM wParam, LPARAM lParam) { if (wParam == 2) { HideIntelliSense(); HideFunctionParameterHint(); return 0; } if (wParam == 3) { HideIntelliSense(); ShowFunctionParameterHint(); return 0; } if (wParam == 4) { HideFunctionParameterHint(); return 0; } ShowIntelliSense(false); if (IsFunctionArgumentContext()) { ShowFunctionParameterHint(); } else { HideFunctionParameterHint(); } return 0; } LRESULT DialogScriptEditor::OnIntelliSenseKey(WPARAM wParam, LPARAM lParam) { MSG msg; memset(&msg, 0, sizeof(msg)); msg.hwnd = scriptEdit ? scriptEdit->GetSafeHwnd() : GetSafeHwnd(); msg.message = WM_KEYDOWN; msg.wParam = wParam; msg.lParam = lParam; return HandleIntelliSenseKey(&msg) ? 1 : 0; } bool DialogScriptEditor::HandleIntelliSenseKey(MSG* msg) { if (!msg || msg->message != WM_KEYDOWN) { return false; } if (msg->wParam == VK_SPACE && (::GetKeyState(VK_CONTROL) & 0x8000)) { ShowIntelliSense(true); return true; } if (!m_intellisenseList.GetSafeHwnd() || !m_intellisenseList.IsWindowVisible()) { return false; } int count = m_intellisenseList.GetCount(); int sel = m_intellisenseList.GetCurSel(); if (sel == LB_ERR) { sel = 0; } switch (msg->wParam) { case VK_ESCAPE: HideIntelliSense(); HideFunctionParameterHint(); return true; case VK_RETURN: case VK_TAB: CompleteIntelliSense(); return true; case VK_UP: if (count > 0) { sel = max(0, sel - 1); m_intellisenseList.SetCurSel(sel); OnIntelliSenseSelChange(); } return true; case VK_DOWN: if (count > 0) { sel = min(count - 1, sel + 1); m_intellisenseList.SetCurSel(sel); OnIntelliSenseSelChange(); } return true; case VK_LEFT: case VK_RIGHT: case VK_HOME: case VK_END: HideIntelliSense(); return false; } return false; } void DialogScriptEditor::ShowIntelliSense(bool forced) { if (!m_intellisenseList.GetSafeHwnd() || !scriptEdit) { return; } EnsureIntelliSenseCore(); StartIntelliSenseBackgroundScan(false); UpdateLiveCompletions(); long wordStart = 0; long wordEnd = 0; CString prefix = GetCurrentWord(&wordStart, &wordEnd); if (!forced && prefix.GetLength() < 2 && !IsMemberCompletionContext(wordStart)) { HideIntelliSense(); return; } PopulateIntelliSense(prefix, forced); if (m_intellisenseList.GetCount() <= 0) { HideIntelliSense(); if (forced) { m_statusLine.SetWindowText("No IntelliSense matches."); } return; } m_intellisenseList.SetCurSel(0); PositionIntelliSenseWindow(); m_intellisenseList.ShowWindow(SW_SHOWNA); m_intellisenseList.Invalidate(FALSE); FocusEditor(); OnIntelliSenseSelChange(); } void DialogScriptEditor::HideIntelliSense(void) { if (m_intellisenseList.GetSafeHwnd() && m_intellisenseList.IsWindowVisible()) { m_intellisenseList.ShowWindow(SW_HIDE); UpdateStatusBar(); } } void DialogScriptEditor::PopulateIntelliSense(const CString& prefix, bool forced) { m_intellisenseList.ResetContent(); CString needle = prefix; needle.MakeLower(); const int maxItems = forced ? 256 : 96; int added = 0; long memberWordStart = 0; long memberWordEnd = 0; GetCurrentWord(&memberWordStart, &memberWordEnd); const bool memberContext = IsMemberCompletionContext(memberWordStart); // First pass: strong prefix matches, with high-weight entries naturally coming // first because the database is built as keywords/events/functions/symbols. for (int pass = 0; pass < 2 && added < maxItems; pass++) { for (int i = 0; i < scriptCompletions.Num() && added < maxItems; i++) { if (memberContext && scriptCompletions[i].kind.Icmp("event") != 0) { continue; } CString name = scriptCompletions[i].name.c_str(); CString lower = name; lower.MakeLower(); bool match = false; if (forced && needle.GetLength() == 0) { match = true; } else if (pass == 0) { match = StartsWithNoCase(name, prefix); } else if (forced && needle.GetLength() >= 2) { match = lower.Find(needle) >= 0; } if (!match) { continue; } CString display; display.Format("%-32s %s", scriptCompletions[i].name.c_str(), scriptCompletions[i].kind.c_str()); if (m_intellisenseList.FindStringExact(-1, display) != LB_ERR) { continue; } int item = m_intellisenseList.AddString(display); if (item != LB_ERR) { m_intellisenseList.SetItemData(item, (DWORD_PTR)i); added++; } } } } bool DialogScriptEditor::IsMemberCompletionContext(long wordStart) const { if (!scriptEdit) { return false; } idStr text; scriptEdit->GetText(text); int i = (int)wordStart - 1; while (i >= 0 && (text[i] == ' ' || text[i] == '\t')) { i--; } if (i >= 0 && text[i] == '.') { return true; } if (i >= 1 && text[i] == ':' && text[i - 1] == ':') { return true; } return false; } bool DialogScriptEditor::IsFunctionArgumentContext(void) const { CString functionName; CString signature; int argumentIndex = 0; return GetFunctionSignatureBeforeCaret(functionName, signature, argumentIndex); } bool DialogScriptEditor::GetFunctionSignatureBeforeCaret(CString& functionName, CString& signature, int& argumentIndex) const { functionName.Empty(); signature.Empty(); argumentIndex = 0; if (!scriptEdit) { return false; } long selStart = 0; long selEnd = 0; scriptEdit->GetSel(selStart, selEnd); if (selStart != selEnd) { return false; } idStr raw; scriptEdit->GetText(raw); CString text = raw.c_str(); int caret = max(0, min((int)selEnd, text.GetLength())); int depth = 0; int openParen = -1; for (int i = caret - 1; i >= 0; i--) { char ch = text[i]; if (ch == ')') { depth++; } else if (ch == '(') { if (depth == 0) { openParen = i; break; } depth--; } else if (depth == 0 && (ch == ';' || ch == '{' || ch == '}')) { return false; } } if (openParen < 0) { return false; } functionName = ScriptEditorPreviousIdentifier(text, openParen); if (functionName.IsEmpty() || ScriptEditorIsKeywordName(functionName)) { return false; } int nested = 0; for (int i = openParen + 1; i < caret; i++) { char ch = text[i]; if (ch == '(') { nested++; } else if (ch == ')' && nested > 0) { nested--; } else if (ch == ',' && nested == 0) { argumentIndex++; } } int best = -1; for (int i = 0; i < scriptCompletions.Num(); i++) { if (scriptCompletions[i].name.Icmp(functionName) != 0) { continue; } if (scriptCompletions[i].kind.Icmp("event") == 0 || scriptCompletions[i].kind.Icmp("function") == 0) { best = i; if (scriptCompletions[i].kind.Icmp("event") == 0) { break; } } } if (best >= 0) { signature = scriptCompletions[best].help.c_str(); if (signature.IsEmpty()) { signature.Format("%s( ... )", functionName); } } else { signature.Format("%s( ... )", functionName); } signature.Replace("\r\n", " "); signature.Replace("\r", " "); signature.Replace("\n", " "); signature.Replace("\t", " "); while (signature.Find(" ") >= 0) { signature.Replace(" ", " "); } CString argText; argText.Format(" arg %d", argumentIndex + 1); signature += argText; return true; } void DialogScriptEditor::ShowFunctionParameterHint(void) { EnsureIntelliSenseCore(); UpdateLiveCompletions(); CString functionName; CString signature; int argumentIndex = 0; if (!GetFunctionSignatureBeforeCaret(functionName, signature, argumentIndex)) { HideFunctionParameterHint(); return; } if (!m_signatureHelp.GetSafeHwnd()) { return; } m_signatureHelp.SetWindowText(signature); PositionParameterHint(); m_signatureHelp.ShowWindow(SW_SHOWNA); m_signatureHelp.SetWindowPos(&wndTop, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); if (m_statusLine.GetSafeHwnd()) { m_statusLine.SetWindowText(signature); } } void DialogScriptEditor::HideFunctionParameterHint(void) { if (m_signatureHelp.GetSafeHwnd() && m_signatureHelp.IsWindowVisible()) { m_signatureHelp.ShowWindow(SW_HIDE); UpdateStatusBar(); } } void DialogScriptEditor::PositionParameterHint(void) { if (!m_signatureHelp.GetSafeHwnd() || !scriptEdit || !scriptEdit->GetSafeHwnd()) { return; } CPoint caret = scriptEdit->GetCaretPoint(); scriptEdit->ClientToScreen(&caret); ScreenToClient(&caret); CRect client; GetClientRect(client); const int width = 560; const int height = 28; CRect rc(caret.x, caret.y + 22, caret.x + width, caret.y + 22 + height); if (m_intellisenseList.GetSafeHwnd() && m_intellisenseList.IsWindowVisible()) { rc.OffsetRect(0, 206); } if (rc.right > client.right - 8) { rc.OffsetRect((client.right - 8) - rc.right, 0); } if (rc.bottom > client.bottom - 32) { rc.OffsetRect(0, -height - 46); } if (rc.left < client.left + 8) { rc.OffsetRect(client.left + 8 - rc.left, 0); } if (rc.top < client.top + 8) { rc.OffsetRect(0, client.top + 8 - rc.top); } m_signatureHelp.MoveWindow(rc, TRUE); m_signatureHelp.SetWindowPos(&wndTop, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); } void DialogScriptEditor::PositionIntelliSenseWindow(void) { if (!m_intellisenseList.GetSafeHwnd() || !scriptEdit || !scriptEdit->GetSafeHwnd()) { return; } CPoint caret = scriptEdit->GetCaretPoint(); scriptEdit->ClientToScreen(&caret); ScreenToClient(&caret); CRect client; GetClientRect(client); const int width = 320; const int height = 200; CRect rc(caret.x, caret.y + 20, caret.x + width, caret.y + 20 + height); if (rc.right > client.right - 8) { rc.OffsetRect((client.right - 8) - rc.right, 0); } if (rc.bottom > client.bottom - 32) { rc.OffsetRect(0, -height - 28); } if (rc.left < client.left + 8) { rc.OffsetRect(client.left + 8 - rc.left, 0); } if (rc.top < client.top + 8) { rc.OffsetRect(0, client.top + 8 - rc.top); } m_intellisenseList.MoveWindow(rc, TRUE); m_intellisenseList.SetWindowPos(&wndTop, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); } void DialogScriptEditor::CompleteIntelliSense(void) { int sel = m_intellisenseList.GetCurSel(); if (sel == LB_ERR) { return; } CString completion; DWORD_PTR data = m_intellisenseList.GetItemData(sel); if (data != (DWORD_PTR)LB_ERR && data < (DWORD_PTR)scriptCompletions.Num()) { completion = scriptCompletions[(int)data].insertText.c_str(); } else { m_intellisenseList.GetText(sel, completion); int space = completion.Find(' '); if (space > 0) { completion = completion.Left(space); } } InsertCompletion(completion); HideIntelliSense(); FocusEditor(); } void DialogScriptEditor::OnIntelliSenseDblClick() { CompleteIntelliSense(); } void DialogScriptEditor::OnIntelliSenseSelChange() { int sel = m_intellisenseList.GetCurSel(); if (sel == LB_ERR) { return; } DWORD_PTR data = m_intellisenseList.GetItemData(sel); if (data != (DWORD_PTR)LB_ERR && data < (DWORD_PTR)scriptCompletions.Num()) { const scriptCompletionInfo_t& completion = scriptCompletions[(int)data]; CString status; if (completion.help.Length() > 0) { status.Format("%s [%s] %s", completion.name.c_str(), completion.kind.c_str(), completion.help.c_str()); } else if (completion.source.Length() > 0) { status.Format("%s [%s] %s", completion.name.c_str(), completion.kind.c_str(), completion.source.c_str()); } else { status.Format("%s [%s]", completion.name.c_str(), completion.kind.c_str()); } m_statusLine.SetWindowText(status); return; } CString name; m_intellisenseList.GetText(sel, name); m_statusLine.SetWindowText(name); } CString DialogScriptEditor::GetCurrentWord(long* wordStart, long* wordEnd) const { if (!scriptEdit) { if (wordStart) { *wordStart = 0; } if (wordEnd) { *wordEnd = 0; } return ""; } return scriptEdit->GetCurrentWord(wordStart, wordEnd); } bool DialogScriptEditor::IsIdentifierChar(int ch) const { return ScriptEditorIsIdentifierChar(ch); } bool DialogScriptEditor::StartsWithNoCase(const CString& text, const CString& prefix) const { if (prefix.GetLength() == 0) { return true; } if (text.GetLength() < prefix.GetLength()) { return false; } CString left = text.Left(prefix.GetLength()); CString a = left; CString b = prefix; a.MakeLower(); b.MakeLower(); return a == b; } void DialogScriptEditor::InsertCompletion(const CString& completion) { if (!scriptEdit) { return; } long wordStart = 0; long wordEnd = 0; GetCurrentWord(&wordStart, &wordEnd); long selStart = 0; long selEnd = 0; scriptEdit->GetSel(selStart, selEnd); if (selStart != selEnd) { wordStart = min(selStart, selEnd); wordEnd = max(selStart, selEnd); } scriptEdit->SetSel(wordStart, wordEnd); scriptEdit->ReplaceSel(completion, TRUE); SetDirty(true); UpdateStatusBar(); } void DialogScriptEditor::OnBnClickedOk() { SaveFile(); FocusEditor(); } void DialogScriptEditor::OnBnClickedCancel() { if (fileName.Length() == 0) { return; } if (isDirty) { if (MessageBox("Discard changes and reload the last saved version?", "Revert", MB_YESNO | MB_ICONQUESTION) != IDYES) { FocusEditor(); return; } } ReloadFile(); FocusEditor(); } void ScriptEditorInit(const idDict* spawnArgs) { if (renderSystem->IsFullScreen()) { common->Printf("Cannot run the script editor in fullscreen mode.\n" "Set r_fullscreen to 0 and vid_restart.\n"); return; } InitAfx(); DialogScriptEditor* editor = DialogScriptEditor::GetPrimaryEditor(); if (editor && editor->GetSafeHwnd()) { idKeyInput::ClearStates(); ScriptEditorActivateDockParent(editor); editor->ShowWindow(SW_SHOW); editor->SetActive(TRUE); editor->FocusEditor(); return; } if (g_StandaloneScriptEditor == NULL) { g_StandaloneScriptEditor = new DialogScriptEditor(); } if (g_StandaloneScriptEditor->GetSafeHwnd() == NULL) { CRect rct(80, 80, 1180, 820); g_StandaloneScriptEditor->Create(rct, NULL, IDD_DIALOG_SCRIPTEDITOR); } idKeyInput::ClearStates(); g_StandaloneScriptEditor->ShowWindow(SW_SHOW); g_StandaloneScriptEditor->SetActive(TRUE); g_StandaloneScriptEditor->FocusEditor(); if (spawnArgs) { const char* scriptFile = spawnArgs->GetString("file"); if (scriptFile && scriptFile[0]) { g_StandaloneScriptEditor->OpenFile(scriptFile); } } } void ScriptEditorRun(void) { #if _MSC_VER >= 1300 MSG* msg = AfxGetCurrentMessage(); #else MSG* msg = &m_msgCur; #endif while (::PeekMessage(msg, NULL, NULL, NULL, PM_NOREMOVE)) { if (!AfxGetApp()->PumpMessage()) { } } } void ScriptEditorShutdown(void) { if (g_StandaloneScriptEditor) { delete g_StandaloneScriptEditor; g_StandaloneScriptEditor = NULL; } scriptEvents.Clear(); scriptCompletions.Clear(); scriptVirtualFiles.Clear(); scriptCoreBuilt = false; scriptDatabaseBuilt = false; scriptVirtualFileListBuilt = false; }