Files
DoomRTX/neo/engine/tools/script/DialogScriptEditor.cpp
T
2026-05-18 12:58:13 -07:00

4934 lines
133 KiB
C++

/*
===========================================================================
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 <http://www.gnu.org/licenses/>.
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 <stdio.h>
#include <ctype.h>
#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<scriptEventInfo_t> scriptEvents;
typedef struct scriptCompletionInfo_s {
idStr name;
idStr insertText;
idStr kind;
idStr source;
idStr help;
int weight;
} scriptCompletionInfo_t;
static idList<scriptCompletionInfo_t> scriptCompletions;
static idList<idStr> 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<CXYDockWnd*>(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<int, int> 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("<current buffer>");
if (!scriptEdit) {
return;
}
idStr text;
scriptEdit->GetText(text);
if (text.Length() > 0) {
ScriptEditorScanTextForCompletions("<current buffer>", 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;
}