mirror of
https://github.com/jmarshall23/DoomRTX.git
synced 2026-08-12 16:21:04 +02:00
4934 lines
133 KiB
C++
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;
|
|
}
|