diff --git a/neo/engine/tools/comafx/CSyntaxRichEditCtrl.cpp b/neo/engine/tools/comafx/CSyntaxRichEditCtrl.cpp index dce496ba..d0a7adcc 100644 --- a/neo/engine/tools/comafx/CSyntaxRichEditCtrl.cpp +++ b/neo/engine/tools/comafx/CSyntaxRichEditCtrl.cpp @@ -56,15 +56,46 @@ const int FUNCPARMTOOLTIP_WIDTH = 16; const int FUNCPARMTOOLTIP_HEIGHT = 20; const int FUNCPARMTOOLTIP_OFFSET = 16; -const COLORREF DEFAULT_BACK_COLOR = SRE_COLOR_WHITE; -const COLORREF INVALID_BACK_COLOR = SRE_COLOR_WHITE - 2; -const COLORREF MULTILINE_COMMENT_BACK_COLOR = SRE_COLOR_WHITE - 1; +// Dark theme colors used by the docked script editor. These are kept in the +// syntax control instead of only in DialogScriptEditor because TOM restores +// m_DefaultFont during HighlightSyntax(), so a white default font/background +// here will repaint the editor white even after EM_SETBKGNDCOLOR was applied. +const COLORREF SRE_DARK_DEFAULT_TEXT = RGB( 226, 232, 240 ); +const COLORREF SRE_DARK_DEFAULT_BACK = RGB( 11, 13, 18 ); +const COLORREF SRE_DARK_INVALID_BACK = RGB( 17, 24, 39 ); +const COLORREF SRE_DARK_MULTILINE_COMMENT_BACK = RGB( 13, 24, 18 ); +const COLORREF SRE_DARK_COMMENT = RGB( 106, 153, 85 ); +const COLORREF SRE_DARK_STRING = RGB( 214, 157, 133 ); +const COLORREF SRE_DARK_STRING_ALT = RGB( 220, 180, 130 ); +const COLORREF SRE_DARK_LITERAL = RGB( 181, 206, 168 ); +const COLORREF SRE_DARK_BRACE = RGB( 255, 203, 107 ); + +const COLORREF DEFAULT_BACK_COLOR = SRE_DARK_DEFAULT_BACK; +const COLORREF INVALID_BACK_COLOR = SRE_DARK_INVALID_BACK; +const COLORREF MULTILINE_COMMENT_BACK_COLOR = SRE_DARK_MULTILINE_COMMENT_BACK; + +static COLORREF SREMakeReadableOnDark( COLORREF color ) { + int r = GetRValue( color ); + int g = GetGValue( color ); + int b = GetBValue( color ); + int luma = ( r * 299 + g * 587 + b * 114 ) / 1000; + + // Old .def files were authored for a white editor and often use black or + // very dark colors. Blend those toward a readable foreground for the dark UI. + if ( luma < 90 ) { + r = ( r + 120 > 255 ) ? 255 : r + 120; + g = ( g + 120 > 255 ) ? 255 : g + 120; + b = ( b + 120 > 255 ) ? 255 : b + 120; + } + + return RGB( r, g, b ); +} #define IDC_LISTBOX_AUTOCOMPLETE 700 #define IDC_EDITBOX_FUNCPARMS 701 static keyWord_t defaultKeyWords[] = { - { NULL, SRE_COLOR_BLACK, "" } + { NULL, SRE_DARK_DEFAULT_TEXT, "" } }; BEGIN_MESSAGE_MAP(CSyntaxRichEditCtrl, CRichEditCtrl) @@ -93,6 +124,7 @@ CSyntaxRichEditCtrl::CSyntaxRichEditCtrl */ CSyntaxRichEditCtrl::CSyntaxRichEditCtrl( void ) { m_TextDoc = NULL; + m_DefaultFont = NULL; keyWords = defaultKeyWords; keyWordColors = NULL; keyWordLengths = NULL; @@ -125,9 +157,18 @@ CSyntaxRichEditCtrl::~CSyntaxRichEditCtrl */ CSyntaxRichEditCtrl::~CSyntaxRichEditCtrl( void ) { FreeKeyWordsFromFile(); - delete m_pchTip; - delete m_pwchTip; - m_DefaultFont->Release(); + delete [] m_pchTip; + delete [] m_pwchTip; + + if ( m_DefaultFont != NULL ) { + m_DefaultFont->Release(); + m_DefaultFont = NULL; + } + + if ( m_TextDoc != NULL ) { + m_TextDoc->Release(); + m_TextDoc = NULL; + } } /* @@ -136,6 +177,10 @@ CSyntaxRichEditCtrl::InitFont ================ */ void CSyntaxRichEditCtrl::InitFont( void ) { + if ( m_TextDoc == NULL || GetSafeHwnd() == NULL ) { + return; + } + LOGFONT lf; CFont font; PARAFORMAT pf; @@ -171,27 +216,39 @@ void CSyntaxRichEditCtrl::InitFont( void ) { defaultCharFormat.yHeight = FONT_HEIGHT * 20; defaultCharFormat.bCharSet = ANSI_CHARSET; defaultCharFormat.bPitchAndFamily = FIXED_PITCH | FF_MODERN; - defaultCharFormat.crTextColor = SRE_COLOR_BLACK; + defaultCharFormat.crTextColor = SRE_DARK_DEFAULT_TEXT; defaultCharFormat.crBackColor = DEFAULT_BACK_COLOR; defaultCharFormat.dwEffects = CFE_PROTECTED; strcpy( defaultCharFormat.szFaceName, FONT_NAME ); defaultCharFormat.cbSize = sizeof( defaultCharFormat ); SetDefaultCharFormat( defaultCharFormat ); + SendMessage( EM_SETBKGNDCOLOR, 0, DEFAULT_BACK_COLOR ); - defaultColor = SRE_COLOR_BLACK; - singleLineCommentColor = SRE_COLOR_DARK_GREEN; - multiLineCommentColor = SRE_COLOR_DARK_GREEN; - stringColor[0] = stringColor[1] = SRE_COLOR_DARK_CYAN; - literalColor = SRE_COLOR_GREY; - braceHighlightColor = SRE_COLOR_RED; + defaultColor = SRE_DARK_DEFAULT_TEXT; + singleLineCommentColor = SRE_DARK_COMMENT; + multiLineCommentColor = SRE_DARK_COMMENT; + stringColor[0] = SRE_DARK_STRING; + stringColor[1] = SRE_DARK_STRING_ALT; + literalColor = SRE_DARK_LITERAL; + braceHighlightColor = SRE_DARK_BRACE; // get the default tom::ITextFont - tom::ITextRange *irange; - tom::ITextFont *ifont; + tom::ITextRange *irange = NULL; + tom::ITextFont *ifont = NULL; - m_TextDoc->Range( 0, 0, &irange ); - irange->get_Font( &ifont ); + if ( m_DefaultFont != NULL ) { + m_DefaultFont->Release(); + m_DefaultFont = NULL; + } + + if ( m_TextDoc->Range( 0, 0, &irange ) != S_OK || irange == NULL ) { + return; + } + if ( irange->get_Font( &ifont ) != S_OK || ifont == NULL ) { + irange->Release(); + return; + } ifont->get_Duplicate( &m_DefaultFont ); @@ -235,14 +292,24 @@ CSyntaxRichEditCtrl::Init */ void CSyntaxRichEditCtrl::Init( void ) { - // get the Rich Edit ITextDocument to use the wonky TOM interface - IRichEditOle *ire = GetIRichEditOle(); - IUnknown *iu = (IUnknown *)ire; - if ( iu == NULL || iu->QueryInterface( tom::IID_ITextDocument, (void**) &m_TextDoc ) != S_OK ) { - m_TextDoc = NULL; + if ( GetSafeHwnd() == NULL ) { + return; } - InitFont(); + // get the Rich Edit ITextDocument to use the wonky TOM interface + // WM_SIZE can arrive during CRichEditCtrl::Create before Init() runs, so + // every TOM-dependent path must tolerate m_TextDoc == NULL until this succeeds. + if ( m_TextDoc == NULL ) { + IRichEditOle *ire = GetIRichEditOle(); + IUnknown *iu = (IUnknown *)ire; + if ( iu == NULL || iu->QueryInterface( tom::IID_ITextDocument, (void**) &m_TextDoc ) != S_OK ) { + m_TextDoc = NULL; + } + } + + if ( m_TextDoc != NULL && m_DefaultFont == NULL ) { + InitFont(); + } InitSyntaxHighlighting(); @@ -252,14 +319,18 @@ void CSyntaxRichEditCtrl::Init( void ) { // create auto complete list box CRect rect( 0, 0, AUTOCOMPLETE_WIDTH, AUTOCOMPLETE_HEIGHT ); - autoCompleteListBox.Create( WS_DLGFRAME | WS_VISIBLE | WS_VSCROLL | LBS_SORT | LBS_NOTIFY, rect, this, IDC_LISTBOX_AUTOCOMPLETE ); - autoCompleteListBox.SetFont( GetParent()->GetFont() ); - autoCompleteListBox.ShowWindow( FALSE ); + if ( autoCompleteListBox.GetSafeHwnd() == NULL ) { + autoCompleteListBox.Create( WS_DLGFRAME | WS_VISIBLE | WS_VSCROLL | LBS_SORT | LBS_NOTIFY, rect, this, IDC_LISTBOX_AUTOCOMPLETE ); + autoCompleteListBox.SetFont( GetParent()->GetFont() ); + autoCompleteListBox.ShowWindow( FALSE ); + } // create function parameter tool tip - funcParmToolTip.Create( WS_VISIBLE | WS_BORDER, rect, this, IDC_EDITBOX_FUNCPARMS ); - funcParmToolTip.SetFont( GetParent()->GetFont() ); - funcParmToolTip.ShowWindow( FALSE ); + if ( funcParmToolTip.GetSafeHwnd() == NULL ) { + funcParmToolTip.Create( WS_VISIBLE | WS_BORDER, rect, this, IDC_EDITBOX_FUNCPARMS ); + funcParmToolTip.SetFont( GetParent()->GetFont() ); + funcParmToolTip.ShowWindow( FALSE ); + } } /* @@ -379,7 +450,7 @@ bool CSyntaxRichEditCtrl::LoadKeyWordsFromFile( const char *fileName ) { src.ExpectTokenString( "}" ); keyword.keyWord = Mem_CopyString( name ); - keyword.color = RGB( red, green, blue ); + keyword.color = SREMakeReadableOnDark( RGB( red, green, blue ) ); keyword.description = Mem_CopyString( description ); keyWordsFromFile.Append( keyword ); @@ -391,7 +462,7 @@ bool CSyntaxRichEditCtrl::LoadKeyWordsFromFile( const char *fileName ) { } keyword.keyWord = NULL; - keyword.color = RGB( 255, 255, 255 ); + keyword.color = SRE_DARK_DEFAULT_TEXT; keyword.description = NULL; keyWordsFromFile.Append( keyword ); @@ -548,6 +619,10 @@ CSyntaxRichEditCtrl::SetDefaultFont ================ */ void CSyntaxRichEditCtrl::SetDefaultFont( int startCharIndex, int endCharIndex ) { + if ( m_TextDoc == NULL || m_DefaultFont == NULL ) { + return; + } + tom::ITextRange *range; updateSyntaxHighlighting = false; @@ -569,6 +644,10 @@ CSyntaxRichEditCtrl::SetColor ================ */ void CSyntaxRichEditCtrl::SetColor( int startCharIndex, int endCharIndex, COLORREF foreColor, COLORREF backColor, bool bold ) { + if ( m_TextDoc == NULL ) { + return; + } + tom::ITextRange *range; tom::ITextFont *font; long prop; @@ -602,6 +681,10 @@ CSyntaxRichEditCtrl::GetForeColor ================ */ COLORREF CSyntaxRichEditCtrl::GetForeColor( int charIndex ) const { + if ( m_TextDoc == NULL ) { + return defaultColor; + } + tom::ITextRange *range; tom::ITextFont *font; long foreColor; @@ -609,7 +692,7 @@ COLORREF CSyntaxRichEditCtrl::GetForeColor( int charIndex ) const { m_TextDoc->Range( charIndex, charIndex, &range ); range->get_Font( &font ); - font->get_BackColor( &foreColor ); + font->get_ForeColor( &foreColor ); font->Release(); range->Release(); @@ -623,6 +706,10 @@ CSyntaxRichEditCtrl::GetBackColor ================ */ COLORREF CSyntaxRichEditCtrl::GetBackColor( int charIndex ) const { + if ( m_TextDoc == NULL ) { + return DEFAULT_BACK_COLOR; + } + tom::ITextRange *range; tom::ITextFont *font; long backColor; @@ -646,6 +733,10 @@ CSyntaxRichEditCtrl::HighlightSyntax ================ */ void CSyntaxRichEditCtrl::HighlightSyntax( int startCharIndex, int endCharIndex ) { + if ( m_TextDoc == NULL || m_DefaultFont == NULL || !updateSyntaxHighlighting ) { + return; + } + int c, t, line, charIndex, textLength, syntaxStart, keyWordLength, keyWordIndex; const char *keyWord; CHARRANGE visRange; @@ -654,6 +745,9 @@ void CSyntaxRichEditCtrl::HighlightSyntax( int startCharIndex, int endCharIndex // get text length GetTextRange( 0, GetTextLength(), text ); textLength = text.GetLength(); + if ( textLength <= 0 ) { + return; + } // make sure the indexes are within bounds if ( startCharIndex < 0 ) { @@ -684,11 +778,11 @@ void CSyntaxRichEditCtrl::HighlightSyntax( int startCharIndex, int endCharIndex // never update beyond the visible range if ( startCharIndex < visRange.cpMin ) { - SetColor( startCharIndex, visRange.cpMin - 1, SRE_COLOR_BLACK, INVALID_BACK_COLOR, false ); + SetColor( startCharIndex, visRange.cpMin - 1, defaultColor, INVALID_BACK_COLOR, false ); startCharIndex = visRange.cpMin; } if ( visRange.cpMax < endCharIndex ) { - SetColor( visRange.cpMax, endCharIndex, SRE_COLOR_BLACK, INVALID_BACK_COLOR, false ); + SetColor( visRange.cpMax, endCharIndex, defaultColor, INVALID_BACK_COLOR, false ); endCharIndex = visRange.cpMax; if ( endCharIndex >= textLength ) { endCharIndex = textLength - 1; @@ -843,7 +937,7 @@ void CSyntaxRichEditCtrl::UpdateVisibleRange( void ) { long backColor; bool update = false; - if ( !updateSyntaxHighlighting ) { + if ( !updateSyntaxHighlighting || m_TextDoc == NULL || m_DefaultFont == NULL || GetTextLength() <= 0 ) { return; } @@ -917,13 +1011,26 @@ CSyntaxRichEditCtrl::GetText ================ */ void CSyntaxRichEditCtrl::GetText( idStr &text, int startCharIndex, int endCharIndex ) const { - tom::ITextRange *range; - BSTR bstr; + if ( m_TextDoc == NULL ) { + CString fallback; + GetTextRange( startCharIndex, endCharIndex, fallback ); + text = fallback; + return; + } + + tom::ITextRange *range = NULL; + BSTR bstr = NULL; USES_CONVERSION; - m_TextDoc->Range( startCharIndex, endCharIndex, &range ); + if ( m_TextDoc->Range( startCharIndex, endCharIndex, &range ) != S_OK || range == NULL ) { + text.Clear(); + return; + } range->get_Text( &bstr ); text = W2A( bstr ); + if ( bstr != NULL ) { + ::SysFreeString( bstr ); + } range->Release(); text.StripTrailingOnce( "\r" ); // remove last carriage return which is always added to a tom::ITextRange } @@ -945,6 +1052,10 @@ CSyntaxRichEditCtrl::FindNext ================ */ bool CSyntaxRichEditCtrl::FindNext( const char *find, bool matchCase, bool matchWholeWords, bool searchForward ) { + if ( m_TextDoc == NULL ) { + return false; + } + long selStart, selEnd, flags, search, length, start; tom::ITextRange *range; @@ -994,6 +1105,10 @@ CSyntaxRichEditCtrl::ReplaceAll ================ */ int CSyntaxRichEditCtrl::ReplaceAll( const char *find, const char *replace, bool matchCase, bool matchWholeWords ) { + if ( m_TextDoc == NULL ) { + return 0; + } + long selStart, selEnd, flags, search, length, start; int numReplaced; tom::ITextRange *range; @@ -1034,6 +1149,12 @@ CSyntaxRichEditCtrl::ReplaceText ================ */ void CSyntaxRichEditCtrl::ReplaceText( int startCharIndex, int endCharIndex, const char *replace ) { + if ( m_TextDoc == NULL ) { + SetSel( startCharIndex, endCharIndex ); + ReplaceSel( replace, TRUE ); + return; + } + tom::ITextRange *range; CComBSTR bstr( replace ); @@ -1352,6 +1473,15 @@ CSyntaxRichEditCtrl::GoToLine void CSyntaxRichEditCtrl::GoToLine( int line ) { int index = LineIndex( line ); + if ( index < 0 ) { + index = 0; + } + + if ( m_TextDoc == NULL ) { + SetSel( index, index ); + RedrawWindow(); + return; + } m_TextDoc->Freeze( NULL ); @@ -1756,6 +1886,10 @@ BOOL CSyntaxRichEditCtrl::OnMouseWheel( UINT nFlags, short zDelta, CPoint pt ) { return TRUE; } + if ( m_TextDoc == NULL ) { + return CRichEditCtrl::OnMouseWheel( nFlags, zDelta, pt ); + } + m_TextDoc->Freeze( NULL ); LineScroll( -3 * ( (int) zDelta ) / WHEEL_DELTA, 0 ); @@ -1792,6 +1926,11 @@ CSyntaxRichEditCtrl::OnSize ================ */ void CSyntaxRichEditCtrl::OnSize( UINT nType, int cx, int cy ) { + if ( m_TextDoc == NULL ) { + CRichEditCtrl::OnSize( nType, cx, cy ); + return; + } + m_TextDoc->Freeze( NULL ); CRichEditCtrl::OnSize( nType, cx, cy ); @@ -1807,6 +1946,12 @@ CSyntaxRichEditCtrl::OnVScroll ================ */ void CSyntaxRichEditCtrl::OnVScroll( UINT nSBCode, UINT nPos, CScrollBar* pScrollBar ) { + if ( m_TextDoc == NULL ) { + CRichEditCtrl::OnVScroll( nSBCode, nPos, pScrollBar ); + SetFocus(); + return; + } + m_TextDoc->Freeze( NULL ); CRichEditCtrl::OnVScroll( nSBCode, nPos, pScrollBar ); @@ -1868,16 +2013,14 @@ CSyntaxRichEditCtrl::OnChange void CSyntaxRichEditCtrl::OnChange() { long selStart, selEnd; - if ( !updateSyntaxHighlighting ) { - return; + if ( updateSyntaxHighlighting && m_TextDoc != NULL && m_DefaultFont != NULL ) { + GetSel( selStart, selEnd ); + selStart = Min( selStart, updateRange.cpMin ); + selEnd = Max( selEnd, updateRange.cpMax ); + + HighlightSyntax( selStart, selEnd ); } - GetSel( selStart, selEnd ); - selStart = Min( selStart, updateRange.cpMin ); - selEnd = Max( selEnd, updateRange.cpMax ); - - HighlightSyntax( selStart, selEnd ); - // send EN_CHANGE notification to parent window NMHDR pNMHDR; pNMHDR.hwndFrom = GetSafeHwnd(); diff --git a/neo/engine/tools/debugger/DebuggerApp.h b/neo/engine/tools/debugger/DebuggerApp.h index e81778d0..4e16a6d4 100644 --- a/neo/engine/tools/debugger/DebuggerApp.h +++ b/neo/engine/tools/debugger/DebuggerApp.h @@ -29,7 +29,6 @@ If you have questions concerning this license or the applicable additional terms #define DEBUGGERAPP_H_ #include "../../sys/win32/win_local.h" -#include "../../framework/sync/Msg.h" #ifndef REGISTRYOPTIONS_H_ #include "../common/RegistryOptions.h" diff --git a/neo/engine/tools/debugger/DebuggerServer.cpp b/neo/engine/tools/debugger/DebuggerServer.cpp index 4fe6cbfc..9e170be7 100644 --- a/neo/engine/tools/debugger/DebuggerServer.cpp +++ b/neo/engine/tools/debugger/DebuggerServer.cpp @@ -29,13 +29,13 @@ If you have questions concerning this license or the applicable additional terms #include "precompiled.h" #pragma hdrstop -#include "../../game/gamesys/Event.h" -#include "../../game/gamesys/Class.h" -#include "../../game/script/Script_Program.h" -#include "../../game/script/Script_Interpreter.h" -#include "../../game/script/Script_Thread.h" -#include "../../game/script/Script_Compiler.h" -#include "../../framework/sync/Msg.h" +//#include "../../../doom3/game/gamesys/Event.h" +//#include "../../../doom3/game/gamesys/Class.h" +//#include "../../../doom3/game/script/Script_Program.h" +//#include "../../../doom3/game/script/Script_Interpreter.h" +//#include "../../../doom3/game/script/Script_Thread.h" +//#include "../../../doom3/game/script/Script_Compiler.h" +//#include "../../framework/sync/Msg.h" #include "DebuggerApp.h" #include "DebuggerServer.h" diff --git a/neo/engine/tools/decl/DialogDeclBrowser.cpp b/neo/engine/tools/decl/DialogDeclBrowser.cpp index 34f270ed..26b3e5c5 100644 --- a/neo/engine/tools/decl/DialogDeclBrowser.cpp +++ b/neo/engine/tools/decl/DialogDeclBrowser.cpp @@ -326,10 +326,10 @@ void DialogDeclBrowser::EditSelected( void ) const { idStr typeName, declName; GetDeclName( item, typeName, declName ); DialogScriptEditor *scriptEditor; - scriptEditor = new DialogScriptEditor; - scriptEditor->Create( IDD_DIALOG_SCRIPTEDITOR, GetParent() ); - scriptEditor->OpenFile( typeName + "/" + declName + ( ( type == DECLTYPE_SCRIPT ) ? ".script" : ".gui" ) ); - scriptEditor->ShowWindow( SW_SHOW ); + //scriptEditor = new DialogScriptEditor; + //scriptEditor->Create( IDD_DIALOG_SCRIPTEDITOR, GetParent() ); + //scriptEditor->OpenFile( typeName + "/" + declName + ( ( type == DECLTYPE_SCRIPT ) ? ".script" : ".gui" ) ); + //scriptEditor->ShowWindow( SW_SHOW ); scriptEditor->SetFocus(); break; } diff --git a/neo/engine/tools/radiant/XYWnd.cpp b/neo/engine/tools/radiant/XYWnd.cpp index b3be0c58..8a5a566f 100644 --- a/neo/engine/tools/radiant/XYWnd.cpp +++ b/neo/engine/tools/radiant/XYWnd.cpp @@ -47,6 +47,7 @@ If you have questions concerning this license or the applicable additional terms #define ID_XYDOCK_XYPAGE 0x7A03 #define ID_XYDOCK_GAMEPAGE 0x7A04 #define ID_XYDOCK_MODELVIEWPAGE 0x7A05 +#define ID_XYDOCK_SCRIPTEDITORPAGE 0x7A06 #define ID_GAME_HOST 0x7A30 #define ID_GAME_FILL_FIT 0x7A40 @@ -1609,6 +1610,9 @@ int CXYDockWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) { item.pszText = "Model View"; m_wndTabs.InsertItem(XYDOCK_TAB_MODELVIEW, &item); + item.pszText = "Script Editor"; + m_wndTabs.InsertItem(XYDOCK_TAB_SCRIPTEDITOR, &item); + CString pageClass = AfxRegisterWndClass( CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, ::LoadCursor(NULL, IDC_ARROW), @@ -1638,6 +1642,11 @@ int CXYDockWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) { } m_wndModelPage.ShowWindow(SW_HIDE); + if (!m_wndScriptPage.Create(&m_wndTabs, ID_XYDOCK_SCRIPTEDITORPAGE)) { + return -1; + } + m_wndScriptPage.ShowWindow(SW_HIDE); + if (!m_wndMenuBar.Create(&m_wndXYPage, 0x7A01)) { return -1; } @@ -1777,6 +1786,7 @@ void CXYDockWnd::ShowActiveTab() { const BOOL xyActive = (m_nActiveTab == XYDOCK_TAB_XY); const BOOL gameActive = (m_nActiveTab == XYDOCK_TAB_GAME); const BOOL modelActive = (m_nActiveTab == XYDOCK_TAB_MODELVIEW); + const BOOL scriptActive = (m_nActiveTab == XYDOCK_TAB_SCRIPTEDITOR); if (m_wndXYPage.GetSafeHwnd()) { m_wndXYPage.ShowWindow(xyActive ? SW_SHOW : SW_HIDE); @@ -1803,10 +1813,19 @@ void CXYDockWnd::ShowActiveTab() { } m_wndModelPage.SetActive(modelActive); } + + if (m_wndScriptPage.GetSafeHwnd()) { + m_wndScriptPage.ShowWindow(scriptActive ? SW_SHOW : SW_HIDE); + if (scriptActive) { + m_wndScriptPage.SetWindowPos(&wndTop, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + } + m_wndScriptPage.SetActive(scriptActive); + } } void CXYDockWnd::SetActiveTab(int nTab) { - if (nTab != XYDOCK_TAB_XY && nTab != XYDOCK_TAB_GAME && nTab != XYDOCK_TAB_MODELVIEW) { + if (nTab != XYDOCK_TAB_XY && nTab != XYDOCK_TAB_GAME && nTab != XYDOCK_TAB_MODELVIEW && nTab != XYDOCK_TAB_SCRIPTEDITOR) { nTab = XYDOCK_TAB_XY; } @@ -1835,6 +1854,9 @@ void CXYDockWnd::SetActiveTab(int nTab) { if (m_wndModelPage.GetSafeHwnd()) { m_wndModelPage.SetActive(FALSE); } + if (m_wndScriptPage.GetSafeHwnd()) { + m_wndScriptPage.SetActive(FALSE); + } FocusGameWindow(); } @@ -1851,10 +1873,33 @@ void CXYDockWnd::SetActiveTab(int nTab) { if (m_wndModelPage.GetSafeHwnd()) { m_wndModelPage.SetActive(TRUE); } + if (m_wndScriptPage.GetSafeHwnd()) { + m_wndScriptPage.SetActive(FALSE); + } common->ActivateTool(true); FocusModelView(); } + else if (m_nActiveTab == XYDOCK_TAB_SCRIPTEDITOR) { + // + // Script Editor tab selected: + // - game HWND is deactivated + // - editor/tool input stays active + // - focus moves to the embedded syntax editor + // + if (m_wndGamePage.GetSafeHwnd()) { + m_wndGamePage.SetActive(FALSE); + } + if (m_wndModelPage.GetSafeHwnd()) { + m_wndModelPage.SetActive(FALSE); + } + if (m_wndScriptPage.GetSafeHwnd()) { + m_wndScriptPage.SetActive(TRUE); + } + + common->ActivateTool(true); + FocusScriptEditor(); + } else { // // XY tab selected: @@ -1868,6 +1913,9 @@ void CXYDockWnd::SetActiveTab(int nTab) { if (m_wndModelPage.GetSafeHwnd()) { m_wndModelPage.SetActive(FALSE); } + if (m_wndScriptPage.GetSafeHwnd()) { + m_wndScriptPage.SetActive(FALSE); + } common->ActivateTool(true); FocusXYWindow(); @@ -1896,6 +1944,10 @@ void CXYDockWnd::SelectModelViewTab() { SetActiveTab(XYDOCK_TAB_MODELVIEW); } +void CXYDockWnd::SelectScriptEditorTab() { + SetActiveTab(XYDOCK_TAB_SCRIPTEDITOR); +} + BOOL CXYDockWnd::IsGameTabActive() const { return (m_nActiveTab == XYDOCK_TAB_GAME) ? TRUE : FALSE; } @@ -1904,6 +1956,10 @@ BOOL CXYDockWnd::IsModelViewTabActive() const { return (m_nActiveTab == XYDOCK_TAB_MODELVIEW) ? TRUE : FALSE; } +BOOL CXYDockWnd::IsScriptEditorTabActive() const { + return (m_nActiveTab == XYDOCK_TAB_SCRIPTEDITOR) ? TRUE : FALSE; +} + void CXYDockWnd::AttachGameWindow(HWND hGameWnd) { m_wndGamePage.AttachGameWindow(hGameWnd); m_wndGamePage.SetActive(IsGameTabActive()); @@ -1929,6 +1985,19 @@ void CXYDockWnd::FocusModelView() { m_wndModelPage.FocusModelView(); } +void CXYDockWnd::FocusScriptEditor() { + m_wndScriptPage.FocusEditor(); +} + +DialogScriptEditor *CXYDockWnd::GetScriptEditor() { + return &m_wndScriptPage; +} + +void CXYDockWnd::OpenScriptFile(const char *fileName) { + SelectScriptEditorTab(); + m_wndScriptPage.OpenFile(fileName); +} + void CXYDockWnd::FocusXYWindow() { m_wndMDIContainer.FocusXYWindow(); } @@ -1976,6 +2045,11 @@ void CXYDockWnd::LayoutChildren() { m_wndModelPage.LayoutChildren(); } + if (m_wndScriptPage.GetSafeHwnd()) { + m_wndScriptPage.MoveWindow(pageRect, TRUE); + m_wndScriptPage.LayoutChildren(); + } + ShowActiveTab(); if (m_pToolBar && m_pToolBar->GetSafeHwnd() && m_wndXYPage.GetSafeHwnd()) { @@ -2039,6 +2113,15 @@ BOOL CXYDockWnd::TrackMenuMnemonic(UINT nChar) { return FALSE; } +BOOL CXYDockWnd::PreTranslateMessage(MSG *pMsg) { + if (m_nActiveTab == XYDOCK_TAB_SCRIPTEDITOR && m_wndScriptPage.GetSafeHwnd()) { + if (m_wndScriptPage.PreTranslateMessage(pMsg)) { + return TRUE; + } + } + return CWnd::PreTranslateMessage(pMsg); +} + void CXYDockWnd::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy); LayoutChildren(); @@ -2057,6 +2140,11 @@ LRESULT CXYDockWnd::OnIdleUpdateCmdUI(WPARAM wParam, LPARAM lParam) { } BOOL CXYDockWnd::OnCommand(WPARAM wParam, LPARAM lParam) { + if (m_nActiveTab == XYDOCK_TAB_SCRIPTEDITOR && m_wndScriptPage.GetSafeHwnd()) { + m_wndScriptPage.SendMessage(WM_COMMAND, wParam, lParam); + return TRUE; + } + if (m_nActiveTab != XYDOCK_TAB_XY) { return CWnd::OnCommand(wParam, lParam); } @@ -2092,6 +2180,10 @@ BOOL CXYDockWnd::OnCmdMsg(UINT nID, int nCode, void *pExtra, AFX_CMDHANDLERINFO return TRUE; } + if (m_nActiveTab == XYDOCK_TAB_SCRIPTEDITOR && m_wndScriptPage.GetSafeHwnd()) { + return m_wndScriptPage.OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); + } + if (m_nActiveTab != XYDOCK_TAB_XY) { return FALSE; } diff --git a/neo/engine/tools/radiant/XYWnd.h b/neo/engine/tools/radiant/XYWnd.h index 1e23f68e..e25c7e84 100644 --- a/neo/engine/tools/radiant/XYWnd.h +++ b/neo/engine/tools/radiant/XYWnd.h @@ -42,6 +42,7 @@ If you have questions concerning this license or the applicable additional terms #include #include "ModelViewDock.h" +#include "../script/DialogScriptEditor.h" class CXYWnd; class CZWnd; @@ -211,7 +212,7 @@ protected: // CXYDockWnd // // Owns the embedded XY editor layout and the new docked game preview: -// [ tabs: XY | Game ] +// [ tabs: XY | Game | Model View | Script Editor ] // // XY tab: // [ menu bar ] @@ -222,6 +223,11 @@ protected: // [ game command strip / fill controls ] // [ hosted game HWND ] // +// Script Editor tab: +// [ dark command strip ] +// [ syntax/intellisense editor ] +// [ status strip ] +// // The docked Z window starts at 5% of the MDI container width. The divider in // the MDI client can be dragged to resize the Z and XY top panes interactively. //============================================================================= @@ -264,23 +270,31 @@ public: HWND GetDockedGameWindow() const; BOOL IsGameTabActive() const; BOOL IsModelViewTabActive() const; + BOOL IsScriptEditorTabActive() const; void SelectXYTab(); void SelectGameTab(); void SelectModelViewTab(); + void SelectScriptEditorTab(); void FocusGameWindow(); void FocusXYWindow(); void FocusModelView(); + void FocusScriptEditor(); + DialogScriptEditor *GetScriptEditor(); + void OpenScriptFile(const char *fileName); + virtual BOOL PreTranslateMessage(MSG *pMsg); protected: enum { XYDOCK_TAB_XY = 0, XYDOCK_TAB_GAME = 1, - XYDOCK_TAB_MODELVIEW = 2 + XYDOCK_TAB_MODELVIEW = 2, + XYDOCK_TAB_SCRIPTEDITOR = 3 }; CTabCtrl m_wndTabs; CWnd m_wndXYPage; CGameDockWnd m_wndGamePage; CModelViewDockWnd m_wndModelPage; + DialogScriptEditor m_wndScriptPage; CXYMenuBar m_wndMenuBar; CXYMDIContainerWnd m_wndMDIContainer; CToolBar *m_pToolBar; diff --git a/neo/engine/tools/script/DialogScriptEditor.cpp b/neo/engine/tools/script/DialogScriptEditor.cpp index 1619bbe0..aa953a3b 100644 --- a/neo/engine/tools/script/DialogScriptEditor.cpp +++ b/neo/engine/tools/script/DialogScriptEditor.cpp @@ -2,9 +2,9 @@ =========================================================================== IceTech GPL Source Code -Copyright (C) 2026 Justin Marshall +Copyright (C) 2026 Justin Marshall -This file is part of the IceTech GPL Source Code (?IceTech Source Code?). +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 @@ -29,11 +29,17 @@ If you have questions concerning this license or the applicable additional terms #include "precompiled.h" #pragma hdrstop +#include +#include + #include "../../sys/win32/rc/Common_resource.h" #include "../../sys/win32/rc/ScriptEditor_resource.h" +#include "../radiant/qe3.h" +#include "../radiant/Radiant.h" #include "../comafx/DialogGoToLine.h" #include "DialogScriptEditor.h" +#include "../radiant/XYWnd.h" #ifdef ID_DEBUG_MEMORY #undef new @@ -41,6 +47,156 @@ If you have questions concerning this license or the applicable additional terms #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; @@ -50,154 +206,2912 @@ typedef struct scriptEventInfo_s { static idList scriptEvents; -static DialogScriptEditor *g_ScriptDialog = NULL; +typedef struct scriptCompletionInfo_s { + idStr name; + idStr insertText; + idStr kind; + idStr source; + idStr help; + int weight; +} scriptCompletionInfo_t; -// DialogScriptEditor dialog +static idList scriptCompletions; +static idList scriptVirtualFiles; +static bool scriptCoreBuilt = false; +static bool scriptDatabaseBuilt = false; +static bool scriptVirtualFileListBuilt = false; +static DialogScriptEditor* g_StandaloneScriptEditor = NULL; -static UINT FindDialogMessage = ::RegisterWindowMessage( FINDMSGSTRING ); +DialogScriptEditor* DialogScriptEditor::primaryEditor = NULL; -toolTip_t DialogScriptEditor::toolTips[] = { - { IDOK, "save" }, - { IDCANCEL, "cancel" }, - { 0, 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 +}; -IMPLEMENT_DYNAMIC(DialogScriptEditor, CDialog) - -/* -================ -DialogScriptEditor::DialogScriptEditor -================ -*/ -DialogScriptEditor::DialogScriptEditor( CWnd* pParent /*=NULL*/ ) - : CDialog(DialogScriptEditor::IDD, pParent) - , findDlg(NULL) - , matchCase(false) - , matchWholeWords(false) - , firstLine(0) -{ -} - -/* -================ -DialogScriptEditor::~DialogScriptEditor -================ -*/ -DialogScriptEditor::~DialogScriptEditor() { -} - -/* -================ -DialogScriptEditor::DoDataExchange -================ -*/ -void DialogScriptEditor::DoDataExchange(CDataExchange* pDX) { - CDialog::DoDataExchange(pDX); - //{{AFX_DATA_MAP(DialogScriptEditor) - DDX_Control(pDX, IDC_SCRIPTEDITOR_EDIT_TEXT, scriptEdit); - DDX_Control(pDX, IDOK, okButton); - DDX_Control(pDX, IDCANCEL, cancelButton); - //}}AFX_DATA_MAP -} - -/* -================ -DialogScriptEditor::PreTranslateMessage -================ -*/ -BOOL DialogScriptEditor::PreTranslateMessage( MSG* pMsg ) { - if ( WM_KEYFIRST <= pMsg->message && pMsg->message <= WM_KEYLAST ) { - if ( m_hAccel && ::TranslateAccelerator( m_hWnd, m_hAccel, pMsg ) ) { - return TRUE; - } - } - return CWnd::PreTranslateMessage(pMsg); -} - -/* -================ -DialogScriptEditor::UpdateStatusBar -================ -*/ -void DialogScriptEditor::UpdateStatusBar( void ) { - int line, column, character; - - scriptEdit.GetCursorPos( line, column, character ); - statusBar.SetWindowText( va( "Line: %d, Column: %d, Character: %d", line, column, character ) ); -} - -/* -================ -DialogScriptEditor::InitScriptEvents -================ -*/ -void DialogScriptEditor::InitScriptEvents( void ) { - int index; - idParser src; - idToken token; - idStr whiteSpace; - scriptEventInfo_t info; - - if ( !src.LoadFile( "script/doom_events.script" ) ) { +static void ScriptEditorActivateDockParent(DialogScriptEditor* editor) { + if (!editor || !editor->GetSafeHwnd()) { 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 ); + for (CWnd* parent = editor->GetParent(); parent && parent->GetSafeHwnd(); parent = parent->GetParent()) { + if (parent->IsKindOf(RUNTIME_CLASS(CXYDockWnd))) { + static_cast(parent)->SelectScriptEditorTab(); + return; } } } -/* -================ -GetScriptEvents -================ -*/ -bool GetScriptEvents( const char *objectName, CListBox &listBox ) { - for ( int i = 0; i < scriptEvents.Num(); i++ ) { - listBox.AddString( scriptEvents[i].name ); +static void ScriptEditorFillRect(HDC hDC, const RECT& rc, COLORREF color) { + HBRUSH brush = ::CreateSolidBrush(color); + ::FillRect(hDC, &rc, brush); + ::DeleteObject(brush); +} + +static void ScriptEditorFrameRect(HDC hDC, const RECT& rc, COLORREF color) { + HBRUSH brush = ::CreateSolidBrush(color); + ::FrameRect(hDC, &rc, brush); + ::DeleteObject(brush); +} + +static void ScriptEditorApplyNativeDarkTheme(HWND hWnd) { + if (!hWnd) { + return; + } + + typedef HRESULT(WINAPI* SetWindowThemeProc)(HWND, LPCWSTR, LPCWSTR); + typedef BOOL(WINAPI* AllowDarkModeForWindowProc)(HWND, BOOL); + static HMODULE hUxTheme = ::LoadLibraryA("uxtheme.dll"); + static SetWindowThemeProc pSetWindowTheme = hUxTheme ? (SetWindowThemeProc)::GetProcAddress(hUxTheme, "SetWindowTheme") : NULL; + static AllowDarkModeForWindowProc pAllowDarkModeForWindow = hUxTheme ? (AllowDarkModeForWindowProc)::GetProcAddress(hUxTheme, MAKEINTRESOURCEA(133)) : NULL; + + if (pAllowDarkModeForWindow) { + pAllowDarkModeForWindow(hWnd, TRUE); + } + if (pSetWindowTheme) { + pSetWindowTheme(hWnd, L"DarkMode_Explorer", NULL); + } + ::SendMessage(hWnd, WM_THEMECHANGED, 0, 0); +} + +static CString ScriptEditorNormalizeText(const char* text) { + CString out = text ? text : ""; + out.Replace("\r\n", "\n"); + out.Replace("\r", "\n"); + out.Replace("\v", "\n"); + return out; +} + +static CString ScriptEditorToCRLF(const CString& text) { + CString out = text; + out.Replace("\r\n", "\n"); + out.Replace("\r", "\n"); + out.Replace("\n", "\r\n"); + return out; +} + +static bool ScriptEditorIsIdentifierChar(int ch) { + return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '#'; +} + +static bool ScriptEditorIsIdentifierStart(int ch) { + return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' || ch == '#'; +} + +static bool ScriptEditorIsSpaceOrTab(int ch) { + return ch == ' ' || ch == '\t'; +} + +static bool ScriptEditorIsTypeKeyword(const CString& word) { + return word == "void" || word == "float" || word == "integer" || word == "boolean" || word == "string" || + word == "vector" || word == "entity" || word == "object" || word == "windowDef" || word == "vec4"; +} + +static int ScriptEditorUpperChar(int ch) { + if (ch >= 'a' && ch <= 'z') { + return ch - 'a' + 'A'; + } + return ch; +} + +static int ScriptEditorCompareNoCase(const CString& a, const CString& b) { + CString aa = a; + CString bb = b; + aa.MakeLower(); + bb.MakeLower(); + return aa.Compare(bb); +} + + +static bool ScriptEditorIsKeywordName(const char* name) { + if (!name || !name[0]) { + return false; + } + for (int i = 0; scriptKeywords[i]; i++) { + if (idStr::Icmp(scriptKeywords[i], name) == 0) { + return true; + } + } + for (int i = 0; guiKeywords[i]; i++) { + if (idStr::Icmp(guiKeywords[i], name) == 0) { + return true; + } + } + return false; +} + +static CString ScriptEditorNormalizeVirtualPath(const char* path) { + CString out = path ? path : ""; + out.TrimLeft(); + out.TrimRight(); + out.Replace("\\", "/"); + while (out.Find("//") >= 0) { + out.Replace("//", "/"); + } + return out; +} + +static bool ScriptEditorHasExtension(const CString& path, const char* extension) { + CString ext = extension ? extension : ""; + CString p = path; + ext.MakeLower(); + p.MakeLower(); + if (ext.GetLength() <= 0) { + return true; + } + if (ext[0] != '.') { + ext = "." + ext; + } + return p.Right(ext.GetLength()) == ext; +} + +static void ScriptEditorAddVirtualFile(const char* basePath, const char* fileName) { + CString file = ScriptEditorNormalizeVirtualPath(fileName); + CString base = ScriptEditorNormalizeVirtualPath(basePath); + + while (file.GetLength() > 0 && (file[0] == '/' || file[0] == '\\')) { + file.Delete(0, 1); + } + while (base.GetLength() > 0 && (base[base.GetLength() - 1] == '/' || base[base.GetLength() - 1] == '\\')) { + base.Delete(base.GetLength() - 1, 1); + } + + if (file.IsEmpty()) { + return; + } + if (!ScriptEditorHasExtension(file, ".script") && !ScriptEditorHasExtension(file, ".gui")) { + return; + } + if (!base.IsEmpty()) { + CString fileLower = file; + CString baseLower = base; + fileLower.MakeLower(); + baseLower.MakeLower(); + + const CString baseWithSlash = baseLower + "/"; + if (fileLower != baseLower && fileLower.Left(baseWithSlash.GetLength()) != baseWithSlash) { + file = base + "/" + file; + file = ScriptEditorNormalizeVirtualPath(file); + } + } + + while (file.GetLength() > 0 && file[0] == '/') { + file.Delete(0, 1); + } + + for (int i = 0; i < scriptVirtualFiles.Num(); i++) { + if (scriptVirtualFiles[i].Icmp(file) == 0) { + return; + } + } + scriptVirtualFiles.Append(idStr(file)); +} + +static void ScriptEditorListVirtualFilesFromTree(const char* basePath, const char* extension) { + if (!fileSystem || !basePath || !extension) { + return; + } + + idFileList* treeList = fileSystem->ListFilesTree(basePath, extension, true); + if (treeList) { + for (int i = 0; i < treeList->GetNumFiles(); i++) { + ScriptEditorAddVirtualFile(basePath, treeList->GetFile(i)); + } + fileSystem->FreeFileList(treeList); + } + + // Some idFileSystem implementations only expose pk4/pk5-backed files through + // ListFiles(..., fullRelativePath = true), so collect both ways. + idFileList* flatList = fileSystem->ListFiles(basePath, extension, true, true); + if (flatList) { + for (int i = 0; i < flatList->GetNumFiles(); i++) { + ScriptEditorAddVirtualFile(basePath, flatList->GetFile(i)); + } + fileSystem->FreeFileList(flatList); + } +} + +static void ScriptEditorAddCompletion(const char* name, const char* insertText, const char* kind, const char* source, const char* help, int weight) { + if (!name || !name[0]) { + return; + } + if (idStr::Length(name) > 128) { + return; + } + if (!ScriptEditorIsIdentifierStart(name[0])) { + return; + } + + for (const char* p = name; *p; p++) { + if (!ScriptEditorIsIdentifierChar(*p) && *p != ':') { + return; + } + } + + const char* completionKind = kind && kind[0] ? kind : "symbol"; + for (int i = 0; i < scriptCompletions.Num(); i++) { + if (scriptCompletions[i].name.Icmp(name) == 0 && scriptCompletions[i].kind.Icmp(completionKind) == 0) { + if (weight > scriptCompletions[i].weight) { + scriptCompletions[i].insertText = insertText && insertText[0] ? insertText : name; + scriptCompletions[i].source = source && source[0] ? source : ""; + scriptCompletions[i].help = help && help[0] ? help : ""; + scriptCompletions[i].weight = weight; + } + return; + } + } + + scriptCompletionInfo_t completion; + completion.name = name; + completion.insertText = insertText && insertText[0] ? insertText : name; + completion.kind = completionKind; + completion.source = source && source[0] ? source : ""; + completion.help = help && help[0] ? help : ""; + completion.weight = weight; + scriptCompletions.Append(completion); +} + +static void ScriptEditorRemoveCompletionsFromSource(const char* sourceName) { + if (!sourceName || !sourceName[0]) { + return; + } + for (int i = scriptCompletions.Num() - 1; i >= 0; i--) { + if (scriptCompletions[i].source.Icmp(sourceName) == 0) { + scriptCompletions.RemoveIndex(i); + } + } +} + +static int ScriptEditorFindNextNonSpace(const CString& text, int index) { + while (index < text.GetLength() && (text[index] == ' ' || text[index] == '\t' || text[index] == '\r' || text[index] == '\n')) { + index++; + } + return index; +} + +static CString ScriptEditorReadIdentifierAt(const CString& text, int index, int* endIndex = NULL) { + CString word; + if (index < 0 || index >= text.GetLength() || !ScriptEditorIsIdentifierStart(text[index])) { + if (endIndex) { + *endIndex = index; + } + return word; + } + int i = index; + while (i < text.GetLength() && ScriptEditorIsIdentifierChar(text[i])) { + word += text[i]; + i++; + } + if (endIndex) { + *endIndex = i; + } + return word; +} + +static CString ScriptEditorPreviousIdentifier(const CString& text, int index) { + int i = index - 1; + while (i >= 0 && (text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n')) { + i--; + } + if (i < 0 || !ScriptEditorIsIdentifierChar(text[i])) { + return ""; + } + int end = i + 1; + while (i >= 0 && ScriptEditorIsIdentifierChar(text[i])) { + i--; + } + return text.Mid(i + 1, end - (i + 1)); +} + +static void ScriptEditorScanTextForCompletions(const char* sourceName, const char* rawText) { + CString text = ScriptEditorNormalizeText(rawText); + CString previousWord; + bool inString = false; + bool inLiteral = false; + bool inLineComment = false; + bool inBlockComment = false; + + for (int i = 0; i < text.GetLength(); ) { + char ch = text[i]; + char next = (i + 1 < text.GetLength()) ? text[i + 1] : '\0'; + + if (inLineComment) { + if (ch == '\n') { + inLineComment = false; + } + i++; + continue; + } + if (inBlockComment) { + if (ch == '*' && next == '/') { + inBlockComment = false; + i += 2; + } + else { + i++; + } + continue; + } + if (inString) { + if (ch == '"' && (i == 0 || text[i - 1] != '\\')) { + inString = false; + } + i++; + continue; + } + if (inLiteral) { + if (ch == '\'' && (i == 0 || text[i - 1] != '\\')) { + inLiteral = false; + } + i++; + continue; + } + + if (ch == '/' && next == '/') { + inLineComment = true; + i += 2; + continue; + } + if (ch == '/' && next == '*') { + inBlockComment = true; + i += 2; + continue; + } + if (ch == '"') { + inString = true; + i++; + continue; + } + if (ch == '\'') { + inLiteral = true; + i++; + continue; + } + + if (ScriptEditorIsIdentifierStart(ch)) { + int wordEnd = i; + CString word = ScriptEditorReadIdentifierAt(text, i, &wordEnd); + int after = ScriptEditorFindNextNonSpace(text, wordEnd); + char afterChar = after < text.GetLength() ? text[after] : '\0'; + CString prev = previousWord; + CString prevLower = prev; + prevLower.MakeLower(); + + if (!ScriptEditorIsKeywordName(word) && word.GetLength() >= 2) { + if (afterChar == '(') { + CString signature = word; + int sigEnd = after; + int depth = 0; + for (int s = after; s < text.GetLength() && s - after < 240; s++) { + char sigCh = text[s]; + if (sigCh == '(') { + depth++; + } + else if (sigCh == ')') { + depth--; + if (depth <= 0) { + sigEnd = s; + break; + } + } + else if (sigCh == '\n' || sigCh == ';' || sigCh == '{') { + sigEnd = s - 1; + break; + } + sigEnd = s; + } + if (sigEnd >= after) { + signature += text.Mid(after, sigEnd - after + 1); + } + signature.Replace("\n", " "); + signature.Replace("\t", " "); + ScriptEditorAddCompletion(word, word, "function", sourceName, signature, 85); + } + else if (prevLower == "object") { + ScriptEditorAddCompletion(word, word, "object", sourceName, sourceName, 80); + } + else if (prevLower == "namespace") { + ScriptEditorAddCompletion(word, word, "namespace", sourceName, sourceName, 70); + } + else if (ScriptEditorIsTypeKeyword(prev)) { + ScriptEditorAddCompletion(word, word, "variable", sourceName, sourceName, 55); + } + else if (word.GetLength() >= 3) { + ScriptEditorAddCompletion(word, word, "symbol", sourceName, sourceName, 20); + } + } + + previousWord = word; + i = wordEnd; + continue; + } + + i++; + } +} + + +static void ScriptEditorSubclassButton(CWnd& wnd); + +static bool ScriptEditorIsModalInputMessage(UINT message) { + return (message >= WM_KEYFIRST && message <= WM_KEYLAST) || + (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST) || + message == WM_NCLBUTTONDOWN || message == WM_NCRBUTTONDOWN || + message == WM_NCMBUTTONDOWN || message == WM_NCLBUTTONDBLCLK || + message == WM_NCRBUTTONDBLCLK || message == WM_NCMBUTTONDBLCLK; +} + +static bool ScriptEditorMessageTargetsWindow(HWND root, HWND target) { + return root && target && (target == root || ::IsChild(root, target)); +} + +static void ScriptEditorCenterChildRectOnOwner(CRect& rect, CWnd* owner) { + if (!owner || !owner->GetSafeHwnd()) { + return; + } + + CRect client; + owner->GetClientRect(client); + + int wantedW = rect.Width(); + int wantedH = rect.Height(); + + const int margin = 18; + if (wantedW > client.Width() - margin * 2) { + wantedW = max(260, client.Width() - margin * 2); + } + if (wantedH > client.Height() - margin * 2) { + wantedH = max(160, client.Height() - margin * 2); + } + + int x = client.left + (client.Width() - wantedW) / 2; + int y = client.top + (client.Height() - wantedH) / 2; + if (x < client.left + margin) { + x = client.left + margin; + } + if (y < client.top + margin) { + y = client.top + margin; + } + rect.SetRect(x, y, x + wantedW, y + wantedH); +} + +//============================================================================= +// Script file browser for files exposed by the Doom filesystem, including pk5s. +// This is a lightweight modal CWnd rather than a resource-template dialog so the +// editor can be dropped into existing tools projects without adding new .rc data. +//============================================================================= +class CScriptFileBrowserWnd : public CWnd { + DECLARE_DYNAMIC(CScriptFileBrowserWnd) + +public: + CScriptFileBrowserWnd(); + virtual ~CScriptFileBrowserWnd(); + + INT_PTR DoModal(CWnd* owner, CString& selectedPath); + +protected: + afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg BOOL OnEraseBkgnd(CDC* pDC); + afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor); + afx_msg void OnPaint(); + afx_msg void OnFilterChanged(); + afx_msg void OnOpen(); + afx_msg void OnCancel(); + afx_msg void OnLocalFile(); + afx_msg void OnListDoubleClick(); + afx_msg void OnClose(); + + DECLARE_MESSAGE_MAP() + +private: + CStatic m_title; + CStatic m_filterLabel; + CEdit m_filter; + CListBox m_list; + CButton m_open; + CButton m_cancel; + CButton m_local; + CFont m_font; + CBrush m_backBrush; + CBrush m_panelBrush; + CBrush m_editBrush; + CString m_selectedPath; + INT_PTR m_result; + bool m_done; + +private: + void LayoutChildren(void); + void RebuildList(void); + void Finish(INT_PTR result); +}; + +IMPLEMENT_DYNAMIC(CScriptFileBrowserWnd, CWnd) + +BEGIN_MESSAGE_MAP(CScriptFileBrowserWnd, CWnd) + ON_WM_CREATE() + ON_WM_SIZE() + ON_WM_ERASEBKGND() + ON_WM_CTLCOLOR() + ON_WM_PAINT() + ON_WM_CLOSE() + ON_EN_CHANGE(IDC_SCRIPTBROWSER_FILTER, OnFilterChanged) + ON_LBN_DBLCLK(IDC_SCRIPTBROWSER_LIST, OnListDoubleClick) + ON_BN_CLICKED(IDOK, OnOpen) + ON_BN_CLICKED(IDCANCEL, OnCancel) + ON_BN_CLICKED(IDC_SCRIPTBROWSER_LOCAL, OnLocalFile) +END_MESSAGE_MAP() + +CScriptFileBrowserWnd::CScriptFileBrowserWnd() + : m_result(0) + , m_done(false) { + m_backBrush.CreateSolidBrush(SE_DARK_BG); + m_panelBrush.CreateSolidBrush(SE_DARK_PANEL); + m_editBrush.CreateSolidBrush(SE_DARK_EDIT); +} + +CScriptFileBrowserWnd::~CScriptFileBrowserWnd() { +} + +INT_PTR CScriptFileBrowserWnd::DoModal(CWnd* owner, CString& selectedPath) { + selectedPath.Empty(); + m_selectedPath.Empty(); + m_result = 0; + m_done = false; + + if (owner == NULL || !owner->GetSafeHwnd()) { + common->Printf("Script Editor: cannot open VFS browser without a valid editor parent.\n"); + return IDCANCEL; + } + + CString className = AfxRegisterWndClass( + CS_DBLCLKS, + ::LoadCursor(NULL, IDC_ARROW), + (HBRUSH)m_backBrush.GetSafeHandle(), + NULL + ); + + CRect rect(0, 0, 760, 520); + ScriptEditorCenterChildRectOnOwner(rect, owner); + + if (className.IsEmpty()) { + common->Printf("Script Editor: failed to register VFS browser window class.\n"); + return IDCANCEL; + } + + // This used to be a top-level WS_POPUP window with its owner disabled. + // In the Radiant tab host that could leave a separate owner/tool window in + // front and make the browser itself impossible to click. Keep it as an + // in-tab child overlay instead: no owner disabling, no SetForegroundWindow, + // no taskbar/tool-window focus handoff. + if (!CreateEx( + WS_EX_CLIENTEDGE | WS_EX_CONTROLPARENT, + className, + "Open Script From PK5 / VFS", + WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, + rect, + owner, + IDC_SCRIPTBROWSER_PANEL + )) { + common->Printf("Script Editor: failed to create in-tab VFS browser window, GetLastError=%lu.\n", ::GetLastError()); + return IDCANCEL; + } + + HWND oldFocus = ::GetFocus(); + ShowWindow(SW_SHOW); + SetWindowPos(&wndTop, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + UpdateWindow(); + if (m_filter.GetSafeHwnd()) { + m_filter.SetFocus(); + } + + MSG msg; + while (!m_done && GetSafeHwnd() && ::GetMessage(&msg, NULL, 0, 0)) { + if (ScriptEditorIsModalInputMessage(msg.message) && + !ScriptEditorMessageTargetsWindow(GetSafeHwnd(), msg.hwnd)) { + if (m_filter.GetSafeHwnd()) { + m_filter.SetFocus(); + } + continue; + } + if (!IsDialogMessage(&msg)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + } + + if (oldFocus && ::IsWindow(oldFocus)) { + ::SetFocus(oldFocus); + } + if (GetSafeHwnd()) { + DestroyWindow(); + } + + selectedPath = m_selectedPath; + return m_result ? m_result : IDCANCEL; +} + +int CScriptFileBrowserWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) { + if (CWnd::OnCreate(lpCreateStruct) == -1) { + return -1; + } + + m_font.CreatePointFont(85, "MS Shell Dlg"); + m_title.Create("Open a script from the loaded pk5/dev filesystem", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); + m_filterLabel.Create("Filter:", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); + m_filter.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL, CRect(0, 0, 0, 0), this, IDC_SCRIPTBROWSER_FILTER); + m_list.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | LBS_SORT, CRect(0, 0, 0, 0), this, IDC_SCRIPTBROWSER_LIST); + m_open.Create("Open", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, CRect(0, 0, 0, 0), this, IDOK); + m_cancel.Create("Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDCANCEL); + m_local.Create("Local...", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDC_SCRIPTBROWSER_LOCAL); + + CWnd* windows[] = { &m_title, &m_filterLabel, &m_filter, &m_list, &m_open, &m_cancel, &m_local }; + for (int i = 0; i < sizeof(windows) / sizeof(windows[0]); i++) { + if (windows[i] && windows[i]->GetSafeHwnd()) { + windows[i]->SetFont(&m_font); + } + } + + ScriptEditorSubclassButton(m_open); + ScriptEditorSubclassButton(m_cancel); + ScriptEditorSubclassButton(m_local); + ScriptEditorApplyNativeDarkTheme(m_filter.GetSafeHwnd()); + ScriptEditorApplyNativeDarkTheme(m_list.GetSafeHwnd()); + ScriptEditorApplyNativeDarkTheme(m_open.GetSafeHwnd()); + ScriptEditorApplyNativeDarkTheme(m_cancel.GetSafeHwnd()); + ScriptEditorApplyNativeDarkTheme(m_local.GetSafeHwnd()); + + RebuildList(); + LayoutChildren(); + return 0; +} + +void CScriptFileBrowserWnd::LayoutChildren(void) { + if (!GetSafeHwnd()) { + return; + } + CRect client; + GetClientRect(client); + const int margin = 12; + const int buttonW = 86; + const int buttonH = 25; + const int gap = 8; + + if (m_title.GetSafeHwnd()) { + m_title.MoveWindow(margin, 8, client.Width() - margin * 2, 22, TRUE); + } + if (m_filterLabel.GetSafeHwnd()) { + m_filterLabel.MoveWindow(margin, 38, 46, 22, TRUE); + } + if (m_filter.GetSafeHwnd()) { + m_filter.MoveWindow(margin + 52, 38, client.Width() - margin * 2 - 52, 22, TRUE); + } + if (m_list.GetSafeHwnd()) { + m_list.MoveWindow(margin, 68, client.Width() - margin * 2, client.Height() - 68 - 44, TRUE); + } + + int y = client.bottom - 34; + int x = client.right - margin - buttonW; + if (m_cancel.GetSafeHwnd()) { + m_cancel.MoveWindow(x, y, buttonW, buttonH, TRUE); + } + x -= buttonW + gap; + if (m_open.GetSafeHwnd()) { + m_open.MoveWindow(x, y, buttonW, buttonH, TRUE); + } + if (m_local.GetSafeHwnd()) { + m_local.MoveWindow(margin, y, buttonW, buttonH, TRUE); + } +} + +void CScriptFileBrowserWnd::RebuildList(void) { + if (!m_list.GetSafeHwnd()) { + return; + } + CString filter; + if (m_filter.GetSafeHwnd()) { + m_filter.GetWindowText(filter); + } + filter.MakeLower(); + + m_list.ResetContent(); + for (int i = 0; i < scriptVirtualFiles.Num(); i++) { + CString path = scriptVirtualFiles[i].c_str(); + CString pathLower = path; + pathLower.MakeLower(); + if (filter.IsEmpty() || pathLower.Find(filter) >= 0) { + m_list.AddString(path); + } + } + if (m_list.GetCount() > 0) { + m_list.SetCurSel(0); + } +} + +void CScriptFileBrowserWnd::Finish(INT_PTR result) { + m_result = result; + m_done = true; +} + +void CScriptFileBrowserWnd::OnSize(UINT nType, int cx, int cy) { + CWnd::OnSize(nType, cx, cy); + LayoutChildren(); +} + +BOOL CScriptFileBrowserWnd::OnEraseBkgnd(CDC* pDC) { + CRect client; + GetClientRect(client); + pDC->FillSolidRect(client, SE_DARK_BG); + return TRUE; +} + +HBRUSH CScriptFileBrowserWnd::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { + if (pDC) { + pDC->SetBkMode(TRANSPARENT); + pDC->SetTextColor(SE_DARK_TEXT); + } + if (nCtlColor == CTLCOLOR_EDIT || nCtlColor == CTLCOLOR_LISTBOX) { + if (pDC) { + pDC->SetBkColor(SE_DARK_EDIT); + pDC->SetTextColor(SE_DARK_TEXT); + } + return (HBRUSH)m_editBrush.GetSafeHandle(); + } + if (nCtlColor == CTLCOLOR_STATIC) { + return (HBRUSH)m_backBrush.GetSafeHandle(); + } + return (HBRUSH)m_backBrush.GetSafeHandle(); +} + +void CScriptFileBrowserWnd::OnPaint() { + CPaintDC dc(this); + CRect client; + GetClientRect(client); + dc.FillSolidRect(client, SE_DARK_BG); + CRect header(client.left, client.top, client.right, 34); + dc.FillSolidRect(header, SE_DARK_PANEL); + dc.FillSolidRect(client.left, header.bottom - 1, client.Width(), 1, SE_DARK_BORDER); +} + +void CScriptFileBrowserWnd::OnFilterChanged() { + RebuildList(); +} + +void CScriptFileBrowserWnd::OnOpen() { + CString typedPath; + if (m_filter.GetSafeHwnd()) { + m_filter.GetWindowText(typedPath); + typedPath.TrimLeft(); + typedPath.TrimRight(); + typedPath.Replace("\\", "/"); + } + + const bool typedLooksLikePath = + !typedPath.IsEmpty() && + (typedPath.Find('/') >= 0 || typedPath.Find(':') >= 0 || + ScriptEditorHasExtension(typedPath, ".script") || + ScriptEditorHasExtension(typedPath, ".gui") || + ScriptEditorHasExtension(typedPath, ".txt")); + + if (typedLooksLikePath) { + if (!ScriptEditorHasExtension(typedPath, ".script") && + !ScriptEditorHasExtension(typedPath, ".gui") && + !ScriptEditorHasExtension(typedPath, ".txt")) { + typedPath += ".script"; + } + m_selectedPath = typedPath; + Finish(IDOK); + return; + } + + int sel = m_list.GetCurSel(); + if (sel != LB_ERR) { + m_list.GetText(sel, m_selectedPath); + Finish(IDOK); + return; + } + + MessageBox("Select a script file, or type a virtual path such as script/foo.script.", "Open Script", MB_OK | MB_ICONINFORMATION); +} + +void CScriptFileBrowserWnd::OnCancel() { + Finish(IDCANCEL); +} + +void CScriptFileBrowserWnd::OnLocalFile() { + CFileDialog dlg( + TRUE, + NULL, + NULL, + OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, + "Script Files (*.script)|*.script|GUI Files (*.gui)|*.gui|Text Files (*.txt)|*.txt|All Files (*.*)|*.*||", + this + ); + if (dlg.DoModal() == IDOK) { + m_selectedPath = dlg.GetPathName(); + Finish(IDOK); + } +} + +void CScriptFileBrowserWnd::OnListDoubleClick() { + OnOpen(); +} + +void CScriptFileBrowserWnd::OnClose() { + Finish(IDCANCEL); +} + + +//============================================================================= +// Save path prompt. Lets users save a new buffer or a pk5-backed override to +// a virtual dev path such as script/my_new_file.script, with a Local... fallback. +//============================================================================= +class CScriptSavePathWnd : public CWnd { + DECLARE_DYNAMIC(CScriptSavePathWnd) + +public: + CScriptSavePathWnd(); + virtual ~CScriptSavePathWnd(); + + INT_PTR DoModal(CWnd* owner, CString& selectedPath, const char* suggestedPath); + +protected: + afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg BOOL OnEraseBkgnd(CDC* pDC); + afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor); + afx_msg void OnPaint(); + afx_msg void OnSave(); + afx_msg void OnCancel(); + afx_msg void OnLocalFile(); + afx_msg void OnClose(); + + DECLARE_MESSAGE_MAP() + +private: + CStatic m_title; + CStatic m_help; + CEdit m_path; + CButton m_save; + CButton m_cancel; + CButton m_local; + CFont m_font; + CBrush m_backBrush; + CBrush m_panelBrush; + CBrush m_editBrush; + CString m_selectedPath; + CString m_suggestedPath; + INT_PTR m_result; + bool m_done; + +private: + void LayoutChildren(void); + void Finish(INT_PTR result); +}; + +IMPLEMENT_DYNAMIC(CScriptSavePathWnd, CWnd) + +BEGIN_MESSAGE_MAP(CScriptSavePathWnd, CWnd) + ON_WM_CREATE() + ON_WM_SIZE() + ON_WM_ERASEBKGND() + ON_WM_CTLCOLOR() + ON_WM_PAINT() + ON_WM_CLOSE() + ON_BN_CLICKED(IDOK, OnSave) + ON_BN_CLICKED(IDCANCEL, OnCancel) + ON_BN_CLICKED(IDC_SCRIPTSAVE_LOCAL, OnLocalFile) +END_MESSAGE_MAP() + +CScriptSavePathWnd::CScriptSavePathWnd() + : m_result(0) + , m_done(false) { + m_backBrush.CreateSolidBrush(SE_DARK_BG); + m_panelBrush.CreateSolidBrush(SE_DARK_PANEL); + m_editBrush.CreateSolidBrush(SE_DARK_EDIT); +} + +CScriptSavePathWnd::~CScriptSavePathWnd() { +} + +INT_PTR CScriptSavePathWnd::DoModal(CWnd* owner, CString& selectedPath, const char* suggestedPath) { + selectedPath.Empty(); + m_selectedPath.Empty(); + m_suggestedPath = suggestedPath && suggestedPath[0] ? suggestedPath : "script/new_script.script"; + m_result = 0; + m_done = false; + + if (owner == NULL || !owner->GetSafeHwnd()) { + common->Printf("Script Editor: cannot open Save As prompt without a valid editor parent.\n"); + return IDCANCEL; + } + + CString className = AfxRegisterWndClass( + CS_DBLCLKS, + ::LoadCursor(NULL, IDC_ARROW), + (HBRUSH)m_backBrush.GetSafeHandle(), + NULL + ); + + CRect rect(0, 0, 620, 190); + ScriptEditorCenterChildRectOnOwner(rect, owner); + + if (className.IsEmpty()) { + common->Printf("Script Editor: failed to register Save As window class.\n"); + return IDCANCEL; + } + + // Keep Save As inside the script-editor tab for the same reason as the VFS + // browser: no external tool window, no disabled owner frame, no focus trap. + if (!CreateEx( + WS_EX_CLIENTEDGE | WS_EX_CONTROLPARENT, + className, + "Save Script As", + WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, + rect, + owner, + IDC_SCRIPTSAVE_PANEL + )) { + common->Printf("Script Editor: failed to create in-tab Save As window, GetLastError=%lu.\n", ::GetLastError()); + return IDCANCEL; + } + + HWND oldFocus = ::GetFocus(); + ShowWindow(SW_SHOW); + SetWindowPos(&wndTop, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + UpdateWindow(); + if (m_path.GetSafeHwnd()) { + m_path.SetSel(0, -1); + m_path.SetFocus(); + } + + MSG msg; + while (!m_done && GetSafeHwnd() && ::GetMessage(&msg, NULL, 0, 0)) { + if (ScriptEditorIsModalInputMessage(msg.message) && + !ScriptEditorMessageTargetsWindow(GetSafeHwnd(), msg.hwnd)) { + if (m_path.GetSafeHwnd()) { + m_path.SetFocus(); + } + continue; + } + if (!IsDialogMessage(&msg)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + } + + if (oldFocus && ::IsWindow(oldFocus)) { + ::SetFocus(oldFocus); + } + if (GetSafeHwnd()) { + DestroyWindow(); + } + + selectedPath = m_selectedPath; + return m_result ? m_result : IDCANCEL; +} + +int CScriptSavePathWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) { + if (CWnd::OnCreate(lpCreateStruct) == -1) { + return -1; + } + + m_font.CreatePointFont(85, "MS Shell Dlg"); + m_title.Create("Save script to dev path", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); + m_help.Create("Use a virtual path like script/my_file.script to create or override a pk5 script in fs_devpath.", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); + m_path.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL, CRect(0, 0, 0, 0), this, IDC_SCRIPTSAVE_PATH); + m_save.Create("Save", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, CRect(0, 0, 0, 0), this, IDOK); + m_cancel.Create("Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDCANCEL); + m_local.Create("Local...", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, IDC_SCRIPTSAVE_LOCAL); + + CWnd* windows[] = { &m_title, &m_help, &m_path, &m_save, &m_cancel, &m_local }; + for (int i = 0; i < sizeof(windows) / sizeof(windows[0]); i++) { + if (windows[i] && windows[i]->GetSafeHwnd()) { + windows[i]->SetFont(&m_font); + } + } + + m_path.SetWindowText(m_suggestedPath); + ScriptEditorSubclassButton(m_save); + ScriptEditorSubclassButton(m_cancel); + ScriptEditorSubclassButton(m_local); + ScriptEditorApplyNativeDarkTheme(m_path.GetSafeHwnd()); + ScriptEditorApplyNativeDarkTheme(m_save.GetSafeHwnd()); + ScriptEditorApplyNativeDarkTheme(m_cancel.GetSafeHwnd()); + ScriptEditorApplyNativeDarkTheme(m_local.GetSafeHwnd()); + + LayoutChildren(); + return 0; +} + +void CScriptSavePathWnd::LayoutChildren(void) { + if (!GetSafeHwnd()) { + return; + } + CRect client; + GetClientRect(client); + const int margin = 12; + const int buttonW = 86; + const int buttonH = 25; + const int gap = 8; + + if (m_title.GetSafeHwnd()) { + m_title.MoveWindow(margin, 8, client.Width() - margin * 2, 22, TRUE); + } + if (m_help.GetSafeHwnd()) { + m_help.MoveWindow(margin, 38, client.Width() - margin * 2, 24, TRUE); + } + if (m_path.GetSafeHwnd()) { + m_path.MoveWindow(margin, 72, client.Width() - margin * 2, 24, TRUE); + } + + int y = client.bottom - 40; + int x = client.right - margin - buttonW; + if (m_cancel.GetSafeHwnd()) { + m_cancel.MoveWindow(x, y, buttonW, buttonH, TRUE); + } + x -= buttonW + gap; + if (m_save.GetSafeHwnd()) { + m_save.MoveWindow(x, y, buttonW, buttonH, TRUE); + } + if (m_local.GetSafeHwnd()) { + m_local.MoveWindow(margin, y, buttonW, buttonH, TRUE); + } +} + +void CScriptSavePathWnd::Finish(INT_PTR result) { + m_result = result; + m_done = true; +} + +void CScriptSavePathWnd::OnSize(UINT nType, int cx, int cy) { + CWnd::OnSize(nType, cx, cy); + LayoutChildren(); +} + +BOOL CScriptSavePathWnd::OnEraseBkgnd(CDC* pDC) { + CRect client; + GetClientRect(client); + pDC->FillSolidRect(client, SE_DARK_BG); + return TRUE; +} + +HBRUSH CScriptSavePathWnd::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { + if (pDC) { + pDC->SetBkMode(TRANSPARENT); + pDC->SetTextColor(SE_DARK_TEXT); + } + if (nCtlColor == CTLCOLOR_EDIT) { + if (pDC) { + pDC->SetBkColor(SE_DARK_EDIT); + pDC->SetTextColor(SE_DARK_TEXT); + } + return (HBRUSH)m_editBrush.GetSafeHandle(); + } + if (nCtlColor == CTLCOLOR_STATIC) { + return (HBRUSH)m_backBrush.GetSafeHandle(); + } + return (HBRUSH)m_backBrush.GetSafeHandle(); +} + +void CScriptSavePathWnd::OnPaint() { + CPaintDC dc(this); + CRect client; + GetClientRect(client); + dc.FillSolidRect(client, SE_DARK_BG); + CRect header(client.left, client.top, client.right, 34); + dc.FillSolidRect(header, SE_DARK_PANEL); + dc.FillSolidRect(client.left, header.bottom - 1, client.Width(), 1, SE_DARK_BORDER); +} + +void CScriptSavePathWnd::OnSave() { + m_path.GetWindowText(m_selectedPath); + m_selectedPath.TrimLeft(); + m_selectedPath.TrimRight(); + m_selectedPath.Replace("\\", "/"); + if (m_selectedPath.IsEmpty()) { + MessageBox("Enter a virtual path such as script/my_script.script.", "Save Script As", MB_OK | MB_ICONINFORMATION); + return; + } + if (!ScriptEditorHasExtension(m_selectedPath, ".script") && !ScriptEditorHasExtension(m_selectedPath, ".gui") && !ScriptEditorHasExtension(m_selectedPath, ".txt")) { + m_selectedPath += ".script"; + } + Finish(IDOK); +} + +void CScriptSavePathWnd::OnCancel() { + Finish(IDCANCEL); +} + +void CScriptSavePathWnd::OnLocalFile() { + CFileDialog dlg( + FALSE, + "script", + m_suggestedPath, + OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, + "Script Files (*.script)|*.script|GUI Files (*.gui)|*.gui|Text Files (*.txt)|*.txt|All Files (*.*)|*.*||", + this + ); + if (dlg.DoModal() == IDOK) { + m_selectedPath = dlg.GetPathName(); + Finish(IDOK); + } +} + +void CScriptSavePathWnd::OnClose() { + Finish(IDCANCEL); +} + +//============================================================================= +// Custom editor control. This is deliberately implemented here instead of using +// the old syntax-rich edit control so the script editor owns all theme, syntax, selection, +// caret, scrolling, and IntelliSense behavior. +//============================================================================= +class CScriptEditorCtrl : public CWnd { + DECLARE_DYNAMIC(CScriptEditorCtrl) + +public: + CScriptEditorCtrl(); + virtual ~CScriptEditorCtrl(); + + BOOL Create(CWnd* parent, UINT id); + void LimitText(int maxChars); + void RefreshMetrics(void); + + void SetText(const char* text); + void GetText(idStr& text) const; + int GetTextLength(void) const; + int GetLineCount(void) const; + void GetCursorPos(int& line, int& column, int& character) const; + void GoToLine(int line); + + void GetSel(long& start, long& end) const; + void SetSel(long start, long end); + CString GetSelText(void) const; + void ReplaceSel(const char* text, BOOL canUndo); + + bool FindNext(const char* find, bool matchCase, bool matchWholeWords, bool searchForward); + int ReplaceAll(const char* find, const char* replace, bool matchCase, bool matchWholeWords); + + void ClearKeywords(void); + void AddKeyword(const char* keyword); + bool LoadKeyWordsFromFile(const char* fileName); + void ClearFunctionWords(void); + void AddFunctionWord(const char* word); + void SetCaseSensitive(bool caseSensitive); + + CPoint GetCaretPoint(void) const; + CString GetCurrentWord(long* wordStart = NULL, long* wordEnd = NULL) const; + +protected: + afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); + afx_msg void OnPaint(); + afx_msg BOOL OnEraseBkgnd(CDC* pDC); + afx_msg UINT OnGetDlgCode(); + afx_msg void OnSetFocus(CWnd* oldWnd); + afx_msg void OnKillFocus(CWnd* newWnd); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); + afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); + afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); + afx_msg void OnLButtonDown(UINT nFlags, CPoint point); + afx_msg void OnLButtonUp(UINT nFlags, CPoint point); + afx_msg void OnMouseMove(UINT nFlags, CPoint point); + afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); + afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags); + + DECLARE_MESSAGE_MAP() + +private: + CString m_text; + CStringArray m_keywords; + CStringArray m_functionWords; + CArray m_lineStarts; + CString m_undoText; + int m_undoCaret; + int m_undoAnchor; + bool m_hasUndo; + int m_textLimit; + int m_caret; + int m_selAnchor; + int m_firstLine; + int m_xOffset; + int m_charWidth; + int m_lineHeight; + int m_ascent; + int m_gutterWidth; + int m_maxColumns; + int m_preferredColumn; + bool m_dragging; + bool m_hasFocus; + bool m_caseSensitive; + bool m_inSetText; + +private: + void RebuildLineCache(void); + void UpdateScrollBars(void); + void UpdateCaret(void); + void EnsureCaretVisible(void); + void NotifyChange(void); + void NotifySelectionChange(void); + void SaveUndo(void); + void RestoreUndo(void); + void QueueIntelliSenseUpdate(bool hide); + void ReplaceRange(int start, int end, const CString& replacement, bool canUndo, bool notify); + void DeleteSelectionOrRange(int start, int end); + void IndentSelection(bool unindent); + + int ClampIndex(int index) const; + int GetSelectionStart(void) const; + int GetSelectionEnd(void) const; + bool HasSelection(void) const; + int GetLineFromChar(int index) const; + int GetLineStart(int line) const; + int GetLineEnd(int line) const; + CString GetLineText(int line) const; + int VisualColumnForLineOffset(int line, int offset) const; + int CharOffsetFromVisualColumn(int line, int column) const; + int CharFromPoint(CPoint point) const; + int VisualColumnFromIndex(int index) const; + int IndexFromLineAndVisualColumn(int line, int column) const; + void MoveCaretTo(int index, bool keepSelection, bool verticalMove); + void MoveCaretVertical(int deltaLines, bool keepSelection); + void CopySelectionToClipboard(bool cut); + void PasteFromClipboard(void); + + bool IsKeyword(const CString& word) const; + bool IsFunctionWord(const CString& word) const; + bool IsWholeWordMatch(const CString& source, int index, int length) const; + int FindText(const CString& source, const CString& needle, int start, bool matchCase, bool wholeWord, bool forward) const; + bool IsBlockCommentAtLine(int line) const; + void ColorizeLine(const CString& lineText, bool& inBlockComment, COLORREF* colors) const; + void DrawEditorLine(CDC& dc, int line, int y, bool& inBlockComment, int selStart, int selEnd); + void DrawTextRun(CDC& dc, const CString& lineText, int lineStart, int tokenStart, int tokenEnd, int& visualColumn, int y, COLORREF color, int selStart, int selEnd); +}; + +IMPLEMENT_DYNAMIC(CScriptEditorCtrl, CWnd) + +BEGIN_MESSAGE_MAP(CScriptEditorCtrl, CWnd) + ON_WM_CREATE() + ON_WM_PAINT() + ON_WM_ERASEBKGND() + ON_WM_GETDLGCODE() + ON_WM_SETFOCUS() + ON_WM_KILLFOCUS() + ON_WM_SIZE() + ON_WM_VSCROLL() + ON_WM_HSCROLL() + ON_WM_MOUSEWHEEL() + ON_WM_LBUTTONDOWN() + ON_WM_LBUTTONUP() + ON_WM_MOUSEMOVE() + ON_WM_KEYDOWN() + ON_WM_CHAR() +END_MESSAGE_MAP() + +CScriptEditorCtrl::CScriptEditorCtrl() { + m_undoCaret = 0; + m_undoAnchor = 0; + m_hasUndo = false; + m_textLimit = 1024 * 1024; + m_caret = 0; + m_selAnchor = 0; + m_firstLine = 0; + m_xOffset = 0; + m_charWidth = 8; + m_lineHeight = 16; + m_ascent = 12; + m_gutterWidth = 48; + m_maxColumns = 0; + m_preferredColumn = -1; + m_dragging = false; + m_hasFocus = false; + m_caseSensitive = false; + m_inSetText = false; + RebuildLineCache(); +} + +CScriptEditorCtrl::~CScriptEditorCtrl() { +} + +BOOL CScriptEditorCtrl::Create(CWnd* parent, UINT id) { + CString className = AfxRegisterWndClass( + CS_DBLCLKS, + ::LoadCursor(NULL, IDC_IBEAM), + (HBRUSH)::GetStockObject(NULL_BRUSH), + NULL + ); + + return CWnd::CreateEx( + WS_EX_CLIENTEDGE, + className, + "IceTechScriptEditorTextView", + WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL | WS_HSCROLL | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, + CRect(0, 0, 0, 0), + parent, + id + ); +} + +int CScriptEditorCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct) { + if (CWnd::OnCreate(lpCreateStruct) == -1) { + return -1; + } + RefreshMetrics(); + UpdateScrollBars(); + return 0; +} + +void CScriptEditorCtrl::LimitText(int maxChars) { + m_textLimit = maxChars; +} + +void CScriptEditorCtrl::RefreshMetrics(void) { + CClientDC dc(this); + CFont* font = GetFont(); + CFont* oldFont = font ? dc.SelectObject(font) : NULL; + + TEXTMETRIC tm; + memset(&tm, 0, sizeof(tm)); + dc.GetTextMetrics(&tm); + + // Measure an average monospace run instead of only "M". Some DPI/font + // substitutions make "M" wider than the actual cell width, which makes the + // custom editor look oddly spaced and harder to read. + CSize tenChars = dc.GetTextExtent("0000000000", 10); + m_charWidth = max(1, tenChars.cx / 10); + + // Give ClearType a little more vertical breathing room on the dark editor. + m_lineHeight = max(1, tm.tmHeight + tm.tmExternalLeading + 5); + m_ascent = tm.tmAscent; + + if (oldFont) { + dc.SelectObject(oldFont); + } + + UpdateScrollBars(); + UpdateCaret(); + Invalidate(FALSE); +} + +void CScriptEditorCtrl::SetText(const char* text) { + m_inSetText = true; + m_text = ScriptEditorNormalizeText(text); + if (m_text.GetLength() > m_textLimit) { + m_text = m_text.Left(m_textLimit); + } + m_caret = 0; + m_selAnchor = 0; + m_firstLine = 0; + m_xOffset = 0; + m_preferredColumn = -1; + m_hasUndo = false; + RebuildLineCache(); + UpdateScrollBars(); + UpdateCaret(); + Invalidate(FALSE); + m_inSetText = false; + NotifySelectionChange(); +} + +void CScriptEditorCtrl::GetText(idStr& text) const { + text = (LPCTSTR)m_text; +} + +int CScriptEditorCtrl::GetTextLength(void) const { + return m_text.GetLength(); +} + +int CScriptEditorCtrl::GetLineCount(void) const { + return max(1, (int)m_lineStarts.GetSize()); +} + +void CScriptEditorCtrl::GetCursorPos(int& line, int& column, int& character) const { + line = GetLineFromChar(m_caret) + 1; + column = VisualColumnFromIndex(m_caret) + 1; + character = m_caret + 1; +} + +void CScriptEditorCtrl::GoToLine(int line) { + line = max(0, min(line, GetLineCount() - 1)); + MoveCaretTo(GetLineStart(line), false, false); +} + +void CScriptEditorCtrl::GetSel(long& start, long& end) const { + start = GetSelectionStart(); + end = GetSelectionEnd(); +} + +void CScriptEditorCtrl::SetSel(long start, long end) { + start = ClampIndex((int)start); + end = ClampIndex((int)end); + m_selAnchor = (int)start; + m_caret = (int)end; + m_preferredColumn = -1; + EnsureCaretVisible(); + UpdateScrollBars(); + UpdateCaret(); + Invalidate(FALSE); + NotifySelectionChange(); +} + +CString CScriptEditorCtrl::GetSelText(void) const { + if (!HasSelection()) { + return ""; + } + return m_text.Mid(GetSelectionStart(), GetSelectionEnd() - GetSelectionStart()); +} + +void CScriptEditorCtrl::ReplaceSel(const char* text, BOOL canUndo) { + CString replacement = ScriptEditorNormalizeText(text); + ReplaceRange(GetSelectionStart(), GetSelectionEnd(), replacement, canUndo ? true : false, true); +} + +void CScriptEditorCtrl::ClearKeywords(void) { + m_keywords.RemoveAll(); + Invalidate(FALSE); +} + +void CScriptEditorCtrl::AddKeyword(const char* keyword) { + if (!keyword || !keyword[0]) { + return; + } + CString word = keyword; + for (int i = 0; i < m_keywords.GetSize(); i++) { + if (ScriptEditorCompareNoCase(m_keywords[i], word) == 0) { + return; + } + } + m_keywords.Add(word); +} + +bool CScriptEditorCtrl::LoadKeyWordsFromFile(const char* fileName) { + idParser src; + idToken token, name, description; + if (!src.LoadFile(fileName)) { + return false; + } + + while (src.ReadToken(&token)) { + if (token.Icmp("keywords") != 0) { + src.SkipBracedSection(); + continue; + } + + src.ExpectTokenString("{"); + while (src.ReadToken(&token)) { + if (token == "}") { + break; + } + if (token != "{") { + continue; + } + + // editors/*.def entries are { "keyword", ( r, g, b ), "description" }. + // This custom editor owns the actual dark palette, but still imports the names. + src.ExpectTokenType(TT_STRING, 0, &name); + AddKeyword(name.c_str()); + src.ExpectTokenString(","); + src.ExpectTokenString("("); + src.ExpectTokenType(TT_NUMBER, TT_INTEGER, &token); + src.ExpectTokenString(","); + src.ExpectTokenType(TT_NUMBER, TT_INTEGER, &token); + src.ExpectTokenString(","); + src.ExpectTokenType(TT_NUMBER, TT_INTEGER, &token); + src.ExpectTokenString(")"); + src.ExpectTokenString(","); + src.ExpectTokenType(TT_STRING, 0, &description); + src.ExpectTokenString("}"); + } + } + Invalidate(FALSE); + return true; +} + +void CScriptEditorCtrl::ClearFunctionWords(void) { + m_functionWords.RemoveAll(); + Invalidate(FALSE); +} + +void CScriptEditorCtrl::AddFunctionWord(const char* word) { + if (!word || !word[0]) { + return; + } + CString functionWord = word; + for (int i = 0; i < m_functionWords.GetSize(); i++) { + if (ScriptEditorCompareNoCase(m_functionWords[i], functionWord) == 0) { + return; + } + } + m_functionWords.Add(functionWord); +} + +void CScriptEditorCtrl::SetCaseSensitive(bool caseSensitive) { + m_caseSensitive = caseSensitive; + Invalidate(FALSE); +} + +int CScriptEditorCtrl::ClampIndex(int index) const { + if (index < 0) { + return 0; + } + if (index > m_text.GetLength()) { + return m_text.GetLength(); + } + return index; +} + +int CScriptEditorCtrl::GetSelectionStart(void) const { + return min(m_caret, m_selAnchor); +} + +int CScriptEditorCtrl::GetSelectionEnd(void) const { + return max(m_caret, m_selAnchor); +} + +bool CScriptEditorCtrl::HasSelection(void) const { + return m_caret != m_selAnchor; +} + +void CScriptEditorCtrl::RebuildLineCache(void) { + m_lineStarts.RemoveAll(); + m_lineStarts.Add(0); + + m_maxColumns = 0; + int column = 0; + for (int i = 0; i < m_text.GetLength(); i++) { + char ch = m_text[i]; + if (ch == '\n') { + if (column > m_maxColumns) { + m_maxColumns = column; + } + column = 0; + if (i + 1 <= m_text.GetLength()) { + m_lineStarts.Add(i + 1); + } + } + else if (ch == '\t') { + column += SE_TAB_SIZE - (column % SE_TAB_SIZE); + } + else { + column++; + } + } + if (column > m_maxColumns) { + m_maxColumns = column; + } + + int digits = 1; + int lines = GetLineCount(); + while (lines >= 10) { + lines /= 10; + digits++; + } + m_gutterWidth = max(48, digits * m_charWidth + 24); +} + +int CScriptEditorCtrl::GetLineFromChar(int index) const { + index = ClampIndex(index); + int low = 0; + int high = (int)m_lineStarts.GetSize() - 1; + while (low <= high) { + int mid = (low + high) >> 1; + int start = m_lineStarts[mid]; + int next = (mid + 1 < m_lineStarts.GetSize()) ? m_lineStarts[mid + 1] : m_text.GetLength() + 1; + if (index < start) { + high = mid - 1; + } + else if (index >= next) { + low = mid + 1; + } + else { + return mid; + } + } + return max(0, (int)m_lineStarts.GetSize() - 1); +} + +int CScriptEditorCtrl::GetLineStart(int line) const { + line = max(0, min(line, GetLineCount() - 1)); + return m_lineStarts[line]; +} + +int CScriptEditorCtrl::GetLineEnd(int line) const { + line = max(0, min(line, GetLineCount() - 1)); + int end = (line + 1 < GetLineCount()) ? m_lineStarts[line + 1] - 1 : m_text.GetLength(); + if (end > 0 && end <= m_text.GetLength() && m_text[end - 1] == '\r') { + end--; + } + return max(GetLineStart(line), end); +} + +CString CScriptEditorCtrl::GetLineText(int line) const { + int start = GetLineStart(line); + int end = GetLineEnd(line); + return m_text.Mid(start, end - start); +} + +int CScriptEditorCtrl::VisualColumnForLineOffset(int line, int offset) const { + CString lineText = GetLineText(line); + offset = max(0, min(offset, lineText.GetLength())); + int column = 0; + for (int i = 0; i < offset; i++) { + if (lineText[i] == '\t') { + column += SE_TAB_SIZE - (column % SE_TAB_SIZE); + } + else { + column++; + } + } + return column; +} + +int CScriptEditorCtrl::CharOffsetFromVisualColumn(int line, int column) const { + CString lineText = GetLineText(line); + column = max(0, column); + int visual = 0; + for (int i = 0; i < lineText.GetLength(); i++) { + int next = visual; + if (lineText[i] == '\t') { + next += SE_TAB_SIZE - (next % SE_TAB_SIZE); + } + else { + next++; + } + if (column < next) { + return i; + } + visual = next; + } + return lineText.GetLength(); +} + +int CScriptEditorCtrl::VisualColumnFromIndex(int index) const { + int line = GetLineFromChar(index); + return VisualColumnForLineOffset(line, index - GetLineStart(line)); +} + +int CScriptEditorCtrl::IndexFromLineAndVisualColumn(int line, int column) const { + line = max(0, min(line, GetLineCount() - 1)); + return GetLineStart(line) + CharOffsetFromVisualColumn(line, column); +} + +CPoint CScriptEditorCtrl::GetCaretPoint(void) const { + int line = GetLineFromChar(m_caret); + int column = VisualColumnFromIndex(m_caret); + return CPoint(m_gutterWidth + column * m_charWidth - m_xOffset, (line - m_firstLine) * m_lineHeight); +} + +int CScriptEditorCtrl::CharFromPoint(CPoint point) const { + CRect client; + GetClientRect(client); + int line = m_firstLine + max(0, point.y) / max(1, m_lineHeight); + line = max(0, min(line, GetLineCount() - 1)); + int x = point.x - m_gutterWidth + m_xOffset; + int column = max(0, (x + m_charWidth / 2) / max(1, m_charWidth)); + return IndexFromLineAndVisualColumn(line, column); +} + +CString CScriptEditorCtrl::GetCurrentWord(long* wordStart, long* wordEnd) const { + int start = ClampIndex(m_caret); + int end = start; + + while (start > 0 && ScriptEditorIsIdentifierChar(m_text[start - 1])) { + start--; + } + while (end < m_text.GetLength() && ScriptEditorIsIdentifierChar(m_text[end])) { + end++; + } + + if (wordStart) { + *wordStart = start; + } + if (wordEnd) { + *wordEnd = end; + } + return m_text.Mid(start, m_caret - start); +} + +void CScriptEditorCtrl::UpdateScrollBars(void) { + if (!GetSafeHwnd()) { + return; + } + + CRect client; + GetClientRect(client); + int visibleLines = max(1, client.Height() / max(1, m_lineHeight)); + + SCROLLINFO si; + memset(&si, 0, sizeof(si)); + si.cbSize = sizeof(si); + si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS; + si.nMin = 0; + si.nMax = max(0, GetLineCount() - 1); + si.nPage = visibleLines; + m_firstLine = max(0, min(m_firstLine, max(0, GetLineCount() - visibleLines))); + si.nPos = m_firstLine; + SetScrollInfo(SB_VERT, &si, TRUE); + + int contentWidth = max(0, m_maxColumns * m_charWidth); + int viewWidth = max(1, client.Width() - m_gutterWidth); + memset(&si, 0, sizeof(si)); + si.cbSize = sizeof(si); + si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS; + si.nMin = 0; + si.nMax = max(0, contentWidth); + si.nPage = viewWidth; + m_xOffset = max(0, min(m_xOffset, max(0, contentWidth - viewWidth))); + si.nPos = m_xOffset; + SetScrollInfo(SB_HORZ, &si, TRUE); +} + +void CScriptEditorCtrl::UpdateCaret(void) { + if (!GetSafeHwnd() || !m_hasFocus) { + return; + } + CPoint pt = GetCaretPoint(); + ::SetCaretPos(pt.x, pt.y + 1); +} + +void CScriptEditorCtrl::EnsureCaretVisible(void) { + CRect client; + GetClientRect(client); + int visibleLines = max(1, client.Height() / max(1, m_lineHeight)); + int line = GetLineFromChar(m_caret); + + if (line < m_firstLine) { + m_firstLine = line; + } + else if (line >= m_firstLine + visibleLines) { + m_firstLine = line - visibleLines + 1; + } + + int columnX = VisualColumnFromIndex(m_caret) * m_charWidth; + int viewWidth = max(1, client.Width() - m_gutterWidth); + if (columnX < m_xOffset) { + m_xOffset = columnX; + } + else if (columnX + m_charWidth > m_xOffset + viewWidth) { + m_xOffset = columnX + m_charWidth - viewWidth; + } + m_firstLine = max(0, m_firstLine); + m_xOffset = max(0, m_xOffset); +} + +void CScriptEditorCtrl::NotifyChange(void) { + if (m_inSetText || !GetParent() || !GetParent()->GetSafeHwnd()) { + return; + } + NMHDR hdr; + hdr.hwndFrom = GetSafeHwnd(); + hdr.idFrom = GetDlgCtrlID(); + hdr.code = EN_CHANGE; + GetParent()->SendMessage(WM_NOTIFY, (EN_CHANGE << 16) | GetDlgCtrlID(), (LPARAM)&hdr); +} + +void CScriptEditorCtrl::NotifySelectionChange(void) { + if (!GetParent() || !GetParent()->GetSafeHwnd()) { + return; + } + NMHDR hdr; + hdr.hwndFrom = GetSafeHwnd(); + hdr.idFrom = GetDlgCtrlID(); + hdr.code = EN_SELCHANGE; + GetParent()->SendMessage(WM_NOTIFY, (EN_SELCHANGE << 16) | GetDlgCtrlID(), (LPARAM)&hdr); +} + +void CScriptEditorCtrl::SaveUndo(void) { + m_undoText = m_text; + m_undoCaret = m_caret; + m_undoAnchor = m_selAnchor; + m_hasUndo = true; +} + +void CScriptEditorCtrl::RestoreUndo(void) { + if (!m_hasUndo) { + return; + } + CString oldText = m_text; + int oldCaret = m_caret; + int oldAnchor = m_selAnchor; + m_text = m_undoText; + m_caret = ClampIndex(m_undoCaret); + m_selAnchor = ClampIndex(m_undoAnchor); + m_undoText = oldText; + m_undoCaret = oldCaret; + m_undoAnchor = oldAnchor; + RebuildLineCache(); + EnsureCaretVisible(); + UpdateScrollBars(); + UpdateCaret(); + Invalidate(FALSE); + NotifyChange(); + NotifySelectionChange(); + QueueIntelliSenseUpdate(false); +} + +void CScriptEditorCtrl::QueueIntelliSenseUpdate(bool hide) { + CWnd* parent = GetParent(); + if (parent && parent->GetSafeHwnd()) { + parent->PostMessage(WM_SCRIPTEDITOR_DEFERRED_INTELLISENSE, hide ? 2 : 1, 0); + } +} + +void CScriptEditorCtrl::ReplaceRange(int start, int end, const CString& replacement, bool canUndo, bool notify) { + start = ClampIndex(start); + end = ClampIndex(end); + if (end < start) { + int temp = start; + start = end; + end = temp; + } + + CString insert = replacement; + insert = ScriptEditorNormalizeText(insert); + int newLength = m_text.GetLength() - (end - start) + insert.GetLength(); + if (newLength > m_textLimit) { + int allowed = m_textLimit - (m_text.GetLength() - (end - start)); + if (allowed < 0) { + allowed = 0; + } + insert = insert.Left(allowed); + } + + if (canUndo) { + SaveUndo(); + } + + m_text = m_text.Left(start) + insert + m_text.Mid(end); + m_caret = start + insert.GetLength(); + m_selAnchor = m_caret; + m_preferredColumn = -1; + RebuildLineCache(); + EnsureCaretVisible(); + UpdateScrollBars(); + UpdateCaret(); + Invalidate(FALSE); + if (notify) { + NotifyChange(); + NotifySelectionChange(); + } +} + +void CScriptEditorCtrl::DeleteSelectionOrRange(int start, int end) { + ReplaceRange(start, end, "", true, true); +} + +void CScriptEditorCtrl::IndentSelection(bool unindent) { + int selStart = GetSelectionStart(); + int selEnd = GetSelectionEnd(); + int startLine = GetLineFromChar(selStart); + int endLine = GetLineFromChar(selEnd); + if (selEnd > selStart && selEnd == GetLineStart(endLine) && endLine > startLine) { + endLine--; + } + + SaveUndo(); + int delta = 0; + for (int line = startLine; line <= endLine; line++) { + int pos = GetLineStart(line) + delta; + if (unindent) { + if (pos < m_text.GetLength() && m_text[pos] == '\t') { + m_text = m_text.Left(pos) + m_text.Mid(pos + 1); + delta -= 1; + } + else { + int remove = 0; + while (remove < SE_TAB_SIZE && pos + remove < m_text.GetLength() && m_text[pos + remove] == ' ') { + remove++; + } + if (remove > 0) { + m_text = m_text.Left(pos) + m_text.Mid(pos + remove); + delta -= remove; + } + } + } + else { + m_text = m_text.Left(pos) + "\t" + m_text.Mid(pos); + delta += 1; + } + } + + RebuildLineCache(); + int newStart = GetLineStart(startLine); + int newEnd = GetLineEnd(endLine); + m_selAnchor = newStart; + m_caret = newEnd; + EnsureCaretVisible(); + UpdateScrollBars(); + UpdateCaret(); + Invalidate(FALSE); + NotifyChange(); + NotifySelectionChange(); +} + +void CScriptEditorCtrl::MoveCaretTo(int index, bool keepSelection, bool verticalMove) { + m_caret = ClampIndex(index); + if (!keepSelection) { + m_selAnchor = m_caret; + } + if (!verticalMove) { + m_preferredColumn = -1; + } + EnsureCaretVisible(); + UpdateScrollBars(); + UpdateCaret(); + Invalidate(FALSE); + NotifySelectionChange(); +} + +void CScriptEditorCtrl::MoveCaretVertical(int deltaLines, bool keepSelection) { + int line = GetLineFromChar(m_caret); + if (m_preferredColumn < 0) { + m_preferredColumn = VisualColumnFromIndex(m_caret); + } + line = max(0, min(line + deltaLines, GetLineCount() - 1)); + MoveCaretTo(IndexFromLineAndVisualColumn(line, m_preferredColumn), keepSelection, true); +} + +void CScriptEditorCtrl::CopySelectionToClipboard(bool cut) { + if (!HasSelection()) { + return; + } + + CString selection = ScriptEditorToCRLF(GetSelText()); + if (OpenClipboard()) { + EmptyClipboard(); + HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, selection.GetLength() + 1); + if (hMem) { + char* dst = (char*)GlobalLock(hMem); + if (dst) { + memcpy(dst, (LPCTSTR)selection, selection.GetLength()); + dst[selection.GetLength()] = '\0'; + GlobalUnlock(hMem); + SetClipboardData(CF_TEXT, hMem); + } + else { + GlobalFree(hMem); + } + } + CloseClipboard(); + } + + if (cut) { + DeleteSelectionOrRange(GetSelectionStart(), GetSelectionEnd()); + } +} + +void CScriptEditorCtrl::PasteFromClipboard(void) { + if (!OpenClipboard()) { + return; + } + HGLOBAL hMem = GetClipboardData(CF_TEXT); + if (hMem) { + char* src = (char*)GlobalLock(hMem); + if (src) { + ReplaceRange(GetSelectionStart(), GetSelectionEnd(), ScriptEditorNormalizeText(src), true, true); + GlobalUnlock(hMem); + } + } + CloseClipboard(); +} + +bool CScriptEditorCtrl::IsKeyword(const CString& word) const { + for (int i = 0; i < m_keywords.GetSize(); i++) { + if (m_caseSensitive) { + if (m_keywords[i] == word) { + return true; + } + } + else if (ScriptEditorCompareNoCase(m_keywords[i], word) == 0) { + return true; + } + } + return false; +} + +bool CScriptEditorCtrl::IsFunctionWord(const CString& word) const { + for (int i = 0; i < m_functionWords.GetSize(); i++) { + if (m_caseSensitive) { + if (m_functionWords[i] == word) { + return true; + } + } + else if (ScriptEditorCompareNoCase(m_functionWords[i], word) == 0) { + return true; + } + } + return false; +} + +bool CScriptEditorCtrl::IsWholeWordMatch(const CString& source, int index, int length) const { + if (index > 0 && ScriptEditorIsIdentifierChar(source[index - 1])) { + return false; + } + if (index + length < source.GetLength() && ScriptEditorIsIdentifierChar(source[index + length])) { + return false; } return true; } +int CScriptEditorCtrl::FindText(const CString& source, const CString& needle, int start, bool matchCase, bool wholeWord, bool forward) const { + if (needle.GetLength() == 0 || source.GetLength() == 0) { + return -1; + } + + CString hay = source; + CString find = needle; + if (!matchCase) { + hay.MakeLower(); + find.MakeLower(); + } + + start = max(0, min(start, source.GetLength())); + if (forward) { + for (int i = start; i <= hay.GetLength() - find.GetLength(); i++) { + if (hay.Mid(i, find.GetLength()) == find && (!wholeWord || IsWholeWordMatch(source, i, find.GetLength()))) { + return i; + } + } + for (int i = 0; i < start && i <= hay.GetLength() - find.GetLength(); i++) { + if (hay.Mid(i, find.GetLength()) == find && (!wholeWord || IsWholeWordMatch(source, i, find.GetLength()))) { + return i; + } + } + } + else { + int begin = min(start, hay.GetLength() - find.GetLength()); + for (int i = begin; i >= 0; i--) { + if (hay.Mid(i, find.GetLength()) == find && (!wholeWord || IsWholeWordMatch(source, i, find.GetLength()))) { + return i; + } + } + for (int i = hay.GetLength() - find.GetLength(); i > start; i--) { + if (hay.Mid(i, find.GetLength()) == find && (!wholeWord || IsWholeWordMatch(source, i, find.GetLength()))) { + return i; + } + } + } + return -1; +} + +bool CScriptEditorCtrl::FindNext(const char* find, bool matchCase, bool matchWholeWords, bool searchForward) { + CString needle = find ? find : ""; + if (needle.IsEmpty()) { + return false; + } + int start = searchForward ? GetSelectionEnd() : GetSelectionStart() - 1; + int index = FindText(m_text, needle, start, matchCase, matchWholeWords, searchForward); + if (index < 0) { + return false; + } + SetSel(index, index + needle.GetLength()); + return true; +} + +int CScriptEditorCtrl::ReplaceAll(const char* find, const char* replace, bool matchCase, bool matchWholeWords) { + CString needle = find ? find : ""; + CString replacement = ScriptEditorNormalizeText(replace ? replace : ""); + if (needle.IsEmpty()) { + return 0; + } + + SaveUndo(); + int count = 0; + int index = FindText(m_text, needle, 0, matchCase, matchWholeWords, true); + while (index >= 0) { + m_text = m_text.Left(index) + replacement + m_text.Mid(index + needle.GetLength()); + count++; + int nextStart = index + replacement.GetLength(); + int nextIndex = FindText(m_text, needle, nextStart, matchCase, matchWholeWords, true); + if (nextIndex < 0 || nextIndex <= index) { + break; + } + index = nextIndex; + } + + if (count > 0) { + m_caret = 0; + m_selAnchor = 0; + RebuildLineCache(); + EnsureCaretVisible(); + UpdateScrollBars(); + UpdateCaret(); + Invalidate(FALSE); + NotifyChange(); + NotifySelectionChange(); + } + return count; +} + +bool CScriptEditorCtrl::IsBlockCommentAtLine(int line) const { + bool inComment = false; + for (int l = 0; l < line; l++) { + CString text = GetLineText(l); + for (int i = 0; i < text.GetLength(); i++) { + if (!inComment && i + 1 < text.GetLength() && text[i] == '/' && text[i + 1] == '*') { + inComment = true; + i++; + } + else if (inComment && i + 1 < text.GetLength() && text[i] == '*' && text[i + 1] == '/') { + inComment = false; + i++; + } + } + } + return inComment; +} + +void CScriptEditorCtrl::ColorizeLine(const CString& lineText, bool& inBlockComment, COLORREF* colors) const { + int len = lineText.GetLength(); + for (int i = 0; i < len; i++) { + colors[i] = SE_DARK_TEXT; + } + + for (int i = 0; i < len; ) { + if (inBlockComment) { + colors[i] = SE_DARK_COMMENT; + if (i + 1 < len && lineText[i] == '*' && lineText[i + 1] == '/') { + colors[i + 1] = SE_DARK_COMMENT; + inBlockComment = false; + i += 2; + } + else { + i++; + } + continue; + } + + char ch = lineText[i]; + if (ch == '/' && i + 1 < len && lineText[i + 1] == '/') { + for (int j = i; j < len; j++) { + colors[j] = SE_DARK_COMMENT; + } + break; + } + if (ch == '/' && i + 1 < len && lineText[i + 1] == '*') { + colors[i] = SE_DARK_COMMENT; + colors[i + 1] = SE_DARK_COMMENT; + inBlockComment = true; + i += 2; + continue; + } + if (ch == '"' || ch == '\'') { + char quote = ch; + COLORREF stringColor = (quote == '"') ? SE_DARK_STRING : SE_DARK_ALT_STRING; + colors[i++] = stringColor; + while (i < len) { + colors[i] = stringColor; + if (lineText[i] == quote && (i == 0 || lineText[i - 1] != '\\')) { + i++; + break; + } + i++; + } + continue; + } + if (ch >= '0' && ch <= '9') { + int start = i; + while (i < len && ((lineText[i] >= '0' && lineText[i] <= '9') || lineText[i] == '.' || lineText[i] == 'x' || lineText[i] == 'X' || (lineText[i] >= 'a' && lineText[i] <= 'f') || (lineText[i] >= 'A' && lineText[i] <= 'F'))) { + i++; + } + for (int j = start; j < i; j++) { + colors[j] = SE_DARK_NUMBER; + } + continue; + } + if (ScriptEditorIsIdentifierStart(ch)) { + int start = i; + while (i < len && ScriptEditorIsIdentifierChar(lineText[i])) { + i++; + } + CString word = lineText.Mid(start, i - start); + COLORREF color = SE_DARK_TEXT; + if (IsFunctionWord(word)) { + color = SE_DARK_FUNCTION; + } + else if (ScriptEditorIsTypeKeyword(word)) { + color = SE_DARK_TYPE; + } + else if (IsKeyword(word)) { + color = SE_DARK_KEYWORD; + } + for (int j = start; j < i; j++) { + colors[j] = color; + } + continue; + } + if (strchr("{}[]().,;:+-*/%=!<>|&", ch)) { + colors[i] = SE_DARK_OPERATOR; + } + i++; + } +} + +void CScriptEditorCtrl::DrawTextRun(CDC& dc, const CString& lineText, int lineStart, int tokenStart, int tokenEnd, int& visualColumn, int y, COLORREF color, int selStart, int selEnd) { + for (int i = tokenStart; i < tokenEnd; ) { + const int charIndex = lineStart + i; + const bool selected = (charIndex >= selStart && charIndex < selEnd); + + // Tabs still need fixed-cell expansion. + if (lineText[i] == '\t') { + const int widthColumns = SE_TAB_SIZE - (visualColumn % SE_TAB_SIZE); + + CString drawText; + for (int s = 0; s < widthColumns; s++) { + drawText += ' '; + } + + CRect cell( + m_gutterWidth + visualColumn * m_charWidth - m_xOffset, + y, + m_gutterWidth + (visualColumn + widthColumns) * m_charWidth - m_xOffset, + y + m_lineHeight + ); + + if (selected) { + dc.FillSolidRect(cell, SE_DARK_SELECTION); + dc.SetTextColor(SE_DARK_SELECTION_TEXT); + } + else { + dc.SetTextColor(color); + } + + dc.TextOut(cell.left, y + 2, drawText); + visualColumn += widthColumns; + i++; + continue; + } + + // Draw contiguous non-tab text as a run instead of one character at a time. + // This preserves ClearType/subpixel rendering much better and removes the + // "weird spacing" look in the editor. + int j = i + 1; + while (j < tokenEnd && lineText[j] != '\t') { + const int nextCharIndex = lineStart + j; + const bool nextSelected = (nextCharIndex >= selStart && nextCharIndex < selEnd); + if (nextSelected != selected) { + break; + } + j++; + } + + const int columns = j - i; + CString drawText = lineText.Mid(i, columns); + + CRect cell( + m_gutterWidth + visualColumn * m_charWidth - m_xOffset, + y, + m_gutterWidth + (visualColumn + columns) * m_charWidth - m_xOffset, + y + m_lineHeight + ); + + if (selected) { + dc.FillSolidRect(cell, SE_DARK_SELECTION); + dc.SetTextColor(SE_DARK_SELECTION_TEXT); + } + else { + dc.SetTextColor(color); + } + + dc.TextOut(cell.left, y + 2, drawText); + + visualColumn += columns; + i = j; + } +} + +void CScriptEditorCtrl::DrawEditorLine(CDC& dc, int line, int y, bool& inBlockComment, int selStart, int selEnd) { + CRect client; + GetClientRect(client); + CString lineText = GetLineText(line); + int lineStart = GetLineStart(line); + + if (line == GetLineFromChar(m_caret)) { + CRect currentLine(client.left, y, client.right, y + m_lineHeight); + dc.FillSolidRect(currentLine, SE_DARK_EDIT_LINE); + } + + CRect gutter(client.left, y, client.left + m_gutterWidth - 1, y + m_lineHeight); + dc.FillSolidRect(gutter, SE_DARK_GUTTER); + dc.SetTextColor(SE_DARK_LINE_NUMBER); + dc.SetBkMode(TRANSPARENT); + CString lineNumber; + lineNumber.Format("%d", line + 1); + dc.DrawText(lineNumber, gutter, DT_RIGHT | DT_VCENTER | DT_SINGLELINE); + + if (lineText.GetLength() <= 0) { + return; + } + + COLORREF* colors = new COLORREF[lineText.GetLength()]; + ColorizeLine(lineText, inBlockComment, colors); + + int visualColumn = 0; + int runStart = 0; + COLORREF runColor = colors[0]; + for (int i = 1; i <= lineText.GetLength(); i++) { + if (i == lineText.GetLength() || colors[i] != runColor) { + DrawTextRun(dc, lineText, lineStart, runStart, i, visualColumn, y, runColor, selStart, selEnd); + if (i < lineText.GetLength()) { + runStart = i; + runColor = colors[i]; + } + } + } + + delete[] colors; +} + +void CScriptEditorCtrl::OnPaint() { + CPaintDC paintDC(this); + CRect client; + GetClientRect(client); + + if (client.Width() <= 0 || client.Height() <= 0) { + return; + } + + CDC memDC; + CBitmap backBuffer; + CBitmap* oldBitmap = NULL; + CDC* drawDC = &paintDC; + + if (memDC.CreateCompatibleDC(&paintDC) && backBuffer.CreateCompatibleBitmap(&paintDC, client.Width(), client.Height())) { + oldBitmap = memDC.SelectObject(&backBuffer); + drawDC = &memDC; + } + + CDC& dc = *drawDC; + dc.FillSolidRect(client, SE_DARK_EDIT); + + CFont* font = GetFont(); + CFont* oldFont = font ? dc.SelectObject(font) : NULL; + dc.SetBkMode(TRANSPARENT); + + CRect gutter(client.left, client.top, client.left + m_gutterWidth, client.bottom); + dc.FillSolidRect(gutter, SE_DARK_GUTTER); + dc.FillSolidRect(m_gutterWidth - 1, client.top, 1, client.Height(), SE_DARK_BORDER); + + int selStart = GetSelectionStart(); + int selEnd = GetSelectionEnd(); + bool inBlockComment = IsBlockCommentAtLine(m_firstLine); + int visibleLines = client.Height() / max(1, m_lineHeight) + 1; + for (int i = 0; i < visibleLines; i++) { + int line = m_firstLine + i; + if (line >= GetLineCount()) { + break; + } + DrawEditorLine(dc, line, i * m_lineHeight, inBlockComment, selStart, selEnd); + } + + if (oldFont) { + dc.SelectObject(oldFont); + } + + if (drawDC != &paintDC) { + paintDC.BitBlt(0, 0, client.Width(), client.Height(), &memDC, 0, 0, SRCCOPY); + if (oldBitmap) { + memDC.SelectObject(oldBitmap); + } + } +} + +BOOL CScriptEditorCtrl::OnEraseBkgnd(CDC* pDC) { + return TRUE; +} + +UINT CScriptEditorCtrl::OnGetDlgCode() { + return DLGC_WANTALLKEYS | DLGC_WANTARROWS | DLGC_WANTCHARS | DLGC_WANTMESSAGE | DLGC_WANTTAB; +} + +void CScriptEditorCtrl::OnSetFocus(CWnd* oldWnd) { + CWnd::OnSetFocus(oldWnd); + m_hasFocus = true; + ::CreateCaret(GetSafeHwnd(), NULL, 2, max(1, m_lineHeight - 2)); + UpdateCaret(); + ::ShowCaret(GetSafeHwnd()); +} + +void CScriptEditorCtrl::OnKillFocus(CWnd* newWnd) { + ::HideCaret(GetSafeHwnd()); + ::DestroyCaret(); + m_hasFocus = false; + CWnd::OnKillFocus(newWnd); +} + +void CScriptEditorCtrl::OnSize(UINT nType, int cx, int cy) { + CWnd::OnSize(nType, cx, cy); + EnsureCaretVisible(); + UpdateScrollBars(); + UpdateCaret(); + Invalidate(FALSE); +} + +void CScriptEditorCtrl::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { + SCROLLINFO si; + memset(&si, 0, sizeof(si)); + si.cbSize = sizeof(si); + si.fMask = SIF_ALL; + GetScrollInfo(SB_VERT, &si); + + int pos = si.nPos; + switch (nSBCode) { + case SB_LINEUP: pos--; break; + case SB_LINEDOWN: pos++; break; + case SB_PAGEUP: pos -= (int)si.nPage; break; + case SB_PAGEDOWN: pos += (int)si.nPage; break; + case SB_THUMBTRACK: + case SB_THUMBPOSITION: pos = si.nTrackPos; break; + } + m_firstLine = max(si.nMin, min(pos, si.nMax - (int)si.nPage + 1)); + UpdateScrollBars(); + UpdateCaret(); + Invalidate(FALSE); +} + +void CScriptEditorCtrl::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { + SCROLLINFO si; + memset(&si, 0, sizeof(si)); + si.cbSize = sizeof(si); + si.fMask = SIF_ALL; + GetScrollInfo(SB_HORZ, &si); + + int pos = si.nPos; + switch (nSBCode) { + case SB_LINELEFT: pos -= m_charWidth; break; + case SB_LINERIGHT: pos += m_charWidth; break; + case SB_PAGELEFT: pos -= (int)si.nPage; break; + case SB_PAGERIGHT: pos += (int)si.nPage; break; + case SB_THUMBTRACK: + case SB_THUMBPOSITION: pos = si.nTrackPos; break; + } + m_xOffset = max(si.nMin, min(pos, si.nMax - (int)si.nPage + 1)); + UpdateScrollBars(); + UpdateCaret(); + Invalidate(FALSE); +} + +BOOL CScriptEditorCtrl::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) { + int lines = -3 * ((int)zDelta) / WHEEL_DELTA; + m_firstLine = max(0, min(m_firstLine + lines, GetLineCount() - 1)); + UpdateScrollBars(); + UpdateCaret(); + Invalidate(FALSE); + return TRUE; +} + +void CScriptEditorCtrl::OnLButtonDown(UINT nFlags, CPoint point) { + SetFocus(); + m_dragging = true; + SetCapture(); + int index = CharFromPoint(point); + MoveCaretTo(index, (nFlags & MK_SHIFT) != 0, false); + QueueIntelliSenseUpdate(true); +} + +void CScriptEditorCtrl::OnLButtonUp(UINT nFlags, CPoint point) { + if (m_dragging) { + m_dragging = false; + if (GetCapture() == this) { + ReleaseCapture(); + } + } + CWnd::OnLButtonUp(nFlags, point); +} + +void CScriptEditorCtrl::OnMouseMove(UINT nFlags, CPoint point) { + if (m_dragging) { + MoveCaretTo(CharFromPoint(point), true, false); + return; + } + CWnd::OnMouseMove(nFlags, point); +} + +void CScriptEditorCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { + CWnd* parent = GetParent(); + if (parent && parent->GetSafeHwnd() && parent->SendMessage(WM_SCRIPTEDITOR_INTELLISENSE_KEY, nChar, nFlags) != 0) { + return; + } + + bool ctrl = (::GetKeyState(VK_CONTROL) & 0x8000) != 0; + bool shift = (::GetKeyState(VK_SHIFT) & 0x8000) != 0; + + if (ctrl) { + switch (nChar) { + case 'A': + SetSel(0, m_text.GetLength()); + return; + case 'C': + CopySelectionToClipboard(false); + return; + case 'X': + CopySelectionToClipboard(true); + QueueIntelliSenseUpdate(false); + return; + case 'V': + PasteFromClipboard(); + QueueIntelliSenseUpdate(false); + return; + case 'Z': + RestoreUndo(); + return; + case VK_SPACE: + if (GetParent() && GetParent()->GetSafeHwnd()) { + GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(IDC_SCRIPTEDITOR_BUTTON_INTELLISENSE, BN_CLICKED), (LPARAM)GetSafeHwnd()); + } + return; + case VK_HOME: + MoveCaretTo(0, shift, false); + return; + case VK_END: + MoveCaretTo(m_text.GetLength(), shift, false); + return; + } + } + + switch (nChar) { + case VK_LEFT: + if (HasSelection() && !shift) { + MoveCaretTo(GetSelectionStart(), false, false); + } + else { + MoveCaretTo(m_caret - 1, shift, false); + } + return; + case VK_RIGHT: + if (HasSelection() && !shift) { + MoveCaretTo(GetSelectionEnd(), false, false); + } + else { + MoveCaretTo(m_caret + 1, shift, false); + } + return; + case VK_UP: + MoveCaretVertical(-1, shift); + return; + case VK_DOWN: + MoveCaretVertical(1, shift); + return; + case VK_PRIOR: + MoveCaretVertical(-max(1, 10), shift); + return; + case VK_NEXT: + MoveCaretVertical(max(1, 10), shift); + return; + case VK_HOME: + MoveCaretTo(GetLineStart(GetLineFromChar(m_caret)), shift, false); + return; + case VK_END: + MoveCaretTo(GetLineEnd(GetLineFromChar(m_caret)), shift, false); + return; + case VK_BACK: + if (HasSelection()) { + DeleteSelectionOrRange(GetSelectionStart(), GetSelectionEnd()); + } + else if (m_caret > 0) { + DeleteSelectionOrRange(m_caret - 1, m_caret); + } + QueueIntelliSenseUpdate(false); + return; + case VK_DELETE: + if (HasSelection()) { + DeleteSelectionOrRange(GetSelectionStart(), GetSelectionEnd()); + } + else if (m_caret < m_text.GetLength()) { + DeleteSelectionOrRange(m_caret, m_caret + 1); + } + QueueIntelliSenseUpdate(false); + return; + case VK_TAB: + if (HasSelection()) { + IndentSelection(shift); + } + else if (!shift) { + ReplaceRange(GetSelectionStart(), GetSelectionEnd(), "\t", true, true); + } + QueueIntelliSenseUpdate(true); + return; + case VK_RETURN: + { + int line = GetLineFromChar(m_caret); + CString current = GetLineText(line); + int offset = m_caret - GetLineStart(line); + CString indent = "\n"; + for (int i = 0; i < current.GetLength() && ScriptEditorIsSpaceOrTab(current[i]); i++) { + indent += current[i]; + } + int prev = offset - 1; + while (prev >= 0 && ScriptEditorIsSpaceOrTab(current[prev])) { + prev--; + } + if (prev >= 0 && current[prev] == '{') { + indent += '\t'; + } + ReplaceRange(GetSelectionStart(), GetSelectionEnd(), indent, true, true); + QueueIntelliSenseUpdate(true); + return; + } + } + + CWnd::OnKeyDown(nChar, nRepCnt, nFlags); +} + +void CScriptEditorCtrl::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) { + if (nChar == VK_TAB || nChar == VK_RETURN || nChar == VK_BACK || nChar == 0) { + return; + } + if (nChar < 32) { + return; + } + + CString insert; + insert.Format("%c", (char)nChar); + ReplaceRange(GetSelectionStart(), GetSelectionEnd(), insert, true, true); + + CWnd* parent = GetParent(); + if (parent && parent->GetSafeHwnd()) { + if (nChar == '(' || nChar == ',') { + parent->PostMessage(WM_SCRIPTEDITOR_DEFERRED_INTELLISENSE, 3, 0); + return; + } + if (nChar == ')' || nChar == ';' || nChar == '{' || nChar == '}') { + parent->PostMessage(WM_SCRIPTEDITOR_DEFERRED_INTELLISENSE, 4, 0); + QueueIntelliSenseUpdate(true); + return; + } + } + + if (nChar == '(' || nChar == ',') { + QueueIntelliSenseUpdate(false); + return; + } + if (nChar == ')' || nChar == ';') { + QueueIntelliSenseUpdate(true); + return; + } + + const bool canPredict = ScriptEditorIsIdentifierChar(nChar) || nChar == '.' || nChar == ':'; + QueueIntelliSenseUpdate(!canPredict); +} + +//============================================================================= +// Dark owner-drawn helper controls +//============================================================================= +static const char* SE_DARK_BUTTON_OLDPROC = "IceTech.ScriptEditor.DarkButtonOldProc"; +static const char* SE_DARK_STATIC_OLDPROC = "IceTech.ScriptEditor.DarkStaticOldProc"; +static const char* SE_DARK_LIST_OLDPROC = "IceTech.ScriptEditor.DarkListOldProc"; + +#ifndef BS_TYPEMASK +#define BS_TYPEMASK 0x0000000F +#endif + +static void ScriptEditorDrawButton(HWND hWnd, HDC hDC) { + RECT rc; + ::GetClientRect(hWnd, &rc); + + const UINT style = (UINT)::GetWindowLong(hWnd, GWL_STYLE); + const UINT type = style & BS_TYPEMASK; + const bool enabled = ::IsWindowEnabled(hWnd) ? true : false; + const bool pushed = (::SendMessage(hWnd, BM_GETSTATE, 0, 0) & BST_PUSHED) != 0; + const bool defaultButton = (type == BS_DEFPUSHBUTTON); + + char text[128]; + text[0] = '\0'; + ::GetWindowTextA(hWnd, text, sizeof(text)); + + COLORREF face = pushed ? RGB(18, 22, 28) : RGB(31, 36, 45); + if (defaultButton && enabled) { + face = pushed ? RGB(42, 82, 126) : RGB(33, 68, 107); + } + + ScriptEditorFillRect(hDC, rc, face); + ScriptEditorFrameRect(hDC, rc, enabled ? (defaultButton ? SE_DARK_ACCENT : SE_DARK_BORDER) : RGB(42, 48, 56)); + + HFONT font = (HFONT)::SendMessage(hWnd, WM_GETFONT, 0, 0); + HFONT oldFont = font ? (HFONT)::SelectObject(hDC, font) : NULL; + ::SetBkMode(hDC, TRANSPARENT); + ::SetTextColor(hDC, enabled ? SE_DARK_TEXT : SE_DARK_DISABLED); + + RECT textRect = rc; + if (pushed) { + ::OffsetRect(&textRect, 1, 1); + } + ::DrawTextA(hDC, text, -1, &textRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); + + if (oldFont) { + ::SelectObject(hDC, oldFont); + } +} + +static LRESULT CALLBACK ScriptEditorDarkButtonProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + WNDPROC oldProc = (WNDPROC)::GetPropA(hWnd, SE_DARK_BUTTON_OLDPROC); + if (!oldProc) { + return ::DefWindowProc(hWnd, uMsg, wParam, lParam); + } + + switch (uMsg) { + case WM_ERASEBKGND: + return 1; + case WM_PAINT: + { + PAINTSTRUCT ps; + HDC hDC = ::BeginPaint(hWnd, &ps); + ScriptEditorDrawButton(hWnd, hDC); + ::EndPaint(hWnd, &ps); + return 0; + } + case WM_PRINTCLIENT: + ScriptEditorDrawButton(hWnd, (HDC)wParam); + return 0; + case WM_MOUSEMOVE: + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_ENABLE: + case WM_SETTEXT: + case BM_SETSTATE: + { + LRESULT result = ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); + ::InvalidateRect(hWnd, NULL, FALSE); + return result; + } + case WM_NCDESTROY: + { + ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldProc); + ::RemovePropA(hWnd, SE_DARK_BUTTON_OLDPROC); + return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); + } + } + + return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); +} + +static void ScriptEditorSubclassButton(CWnd& wnd) { + HWND hWnd = wnd.GetSafeHwnd(); + if (!hWnd || ::GetPropA(hWnd, SE_DARK_BUTTON_OLDPROC)) { + return; + } + WNDPROC oldProc = (WNDPROC)::GetWindowLongPtr(hWnd, GWLP_WNDPROC); + ::SetPropA(hWnd, SE_DARK_BUTTON_OLDPROC, (HANDLE)oldProc); + ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)ScriptEditorDarkButtonProc); + ::InvalidateRect(hWnd, NULL, TRUE); +} + +static void ScriptEditorDrawStatic(HWND hWnd, HDC hDC) { + RECT rc; + ::GetClientRect(hWnd, &rc); + ScriptEditorFillRect(hDC, rc, SE_DARK_PANEL); + + char text[512]; + text[0] = '\0'; + ::GetWindowTextA(hWnd, text, sizeof(text)); + + HFONT font = (HFONT)::SendMessage(hWnd, WM_GETFONT, 0, 0); + HFONT oldFont = font ? (HFONT)::SelectObject(hDC, font) : NULL; + ::SetBkMode(hDC, TRANSPARENT); + + COLORREF textColor = SE_DARK_TEXT; + const int id = ::GetDlgCtrlID(hWnd); + if (id == IDC_SCRIPTEDITOR_PATH || id == IDC_SCRIPTEDITOR_STATUS || id == IDC_SCRIPTEDITOR_LANGUAGE) { + textColor = SE_DARK_MUTED; + } + ::SetTextColor(hDC, textColor); + + RECT textRect = rc; + textRect.left += 4; + textRect.right -= 4; + UINT flags = DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS; + if (id == IDC_SCRIPTEDITOR_LANGUAGE) { + flags = DT_RIGHT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS; + } + ::DrawTextA(hDC, text, -1, &textRect, flags); + + if (oldFont) { + ::SelectObject(hDC, oldFont); + } +} + +static LRESULT CALLBACK ScriptEditorDarkStaticProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + WNDPROC oldProc = (WNDPROC)::GetPropA(hWnd, SE_DARK_STATIC_OLDPROC); + if (!oldProc) { + return ::DefWindowProc(hWnd, uMsg, wParam, lParam); + } + if (uMsg == WM_PAINT) { + PAINTSTRUCT ps; + HDC hDC = ::BeginPaint(hWnd, &ps); + ScriptEditorDrawStatic(hWnd, hDC); + ::EndPaint(hWnd, &ps); + return 0; + } + if (uMsg == WM_ERASEBKGND) { + return 1; + } + if (uMsg == WM_SETTEXT) { + LRESULT result = ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); + ::InvalidateRect(hWnd, NULL, FALSE); + return result; + } + if (uMsg == WM_NCDESTROY) { + ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldProc); + ::RemovePropA(hWnd, SE_DARK_STATIC_OLDPROC); + return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); + } + return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); +} + +static void ScriptEditorSubclassStatic(CWnd& wnd) { + HWND hWnd = wnd.GetSafeHwnd(); + if (!hWnd || ::GetPropA(hWnd, SE_DARK_STATIC_OLDPROC)) { + return; + } + WNDPROC oldProc = (WNDPROC)::GetWindowLongPtr(hWnd, GWLP_WNDPROC); + ::SetPropA(hWnd, SE_DARK_STATIC_OLDPROC, (HANDLE)oldProc); + ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)ScriptEditorDarkStaticProc); +} + +static LRESULT CALLBACK ScriptEditorDarkListProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + WNDPROC oldProc = (WNDPROC)::GetPropA(hWnd, SE_DARK_LIST_OLDPROC); + if (!oldProc) { + return ::DefWindowProc(hWnd, uMsg, wParam, lParam); + } + if (uMsg == WM_NCDESTROY) { + ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldProc); + ::RemovePropA(hWnd, SE_DARK_LIST_OLDPROC); + return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); + } + if (uMsg == WM_KEYDOWN && (wParam == VK_RETURN || wParam == VK_TAB)) { + ::SendMessage(::GetParent(hWnd), WM_COMMAND, MAKEWPARAM(IDC_SCRIPTEDITOR_INTELLISENSE_LIST, LBN_DBLCLK), (LPARAM)hWnd); + return 0; + } + return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); +} + +static void ScriptEditorSubclassList(CWnd& wnd) { + HWND hWnd = wnd.GetSafeHwnd(); + if (!hWnd || ::GetPropA(hWnd, SE_DARK_LIST_OLDPROC)) { + return; + } + WNDPROC oldProc = (WNDPROC)::GetWindowLongPtr(hWnd, GWLP_WNDPROC); + ::SetPropA(hWnd, SE_DARK_LIST_OLDPROC, (HANDLE)oldProc); + ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)ScriptEditorDarkListProc); +} + /* ================ -GetFunctionParms +Script editor event helpers ================ */ -bool GetFunctionParms( const char *funcName, CString &parmString ) { - for ( int i = 0; i < scriptEvents.Num(); i++ ) { - if ( scriptEvents[i].name.Cmp( funcName ) == 0 ) { +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; } @@ -205,14 +3119,9 @@ bool GetFunctionParms( const char *funcName, CString &parmString ) { return false; } -/* -================ -GetToolTip -================ -*/ -bool GetToolTip( const char *name, CString &string ) { - for ( int i = 0; i < scriptEvents.Num(); i++ ) { - if ( scriptEvents[i].name.Cmp( name ) == 0 ) { +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; } @@ -220,537 +3129,1805 @@ bool GetToolTip( const char *name, CString &string ) { return false; } -/* -================ -DialogScriptEditor::OpenFile -================ -*/ -void DialogScriptEditor::OpenFile( const char *fileName ) { - int numLines = 0; - int numCharsPerLine = 0; - int maxCharsPerLine = 0; - idStr scriptText, extension; - CRect rect; - void *buffer; +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 } +}; - scriptEdit.Init(); - scriptEdit.AllowPathNames( false ); +IMPLEMENT_DYNAMIC(DialogScriptEditor, CWnd) - idStr( fileName ).ExtractFileExtension( extension ); - - if ( extension.Icmp( "script" ) == 0 ) { - InitScriptEvents(); - scriptEdit.SetCaseSensitive( true ); - scriptEdit.LoadKeyWordsFromFile( "editors/script.def" ); - scriptEdit.SetObjectMemberCallback( GetScriptEvents ); - scriptEdit.SetFunctionParmCallback( GetFunctionParms ); - scriptEdit.SetToolTipCallback( GetToolTip ); - } else if ( extension.Icmp( "gui" ) == 0 ) { - scriptEdit.SetStringColor( SRE_COLOR_DARK_CYAN, SRE_COLOR_LIGHT_BROWN ); - scriptEdit.LoadKeyWordsFromFile( "editors/gui.def" ); - } - - if ( fileSystem->ReadFile( fileName, &buffer ) == -1 ) { - return; - } - scriptText = (char *) buffer; - fileSystem->FreeFile( buffer ); - - this->fileName = fileName; - - // clean up new-line crapola - scriptText.Replace( "\r", "" ); - scriptText.Replace( "\n", "\r" ); - scriptText.Replace( "\v", "\r" ); - - scriptEdit.SetText( scriptText ); - - for( const char *ptr = scriptText.c_str(); *ptr; ptr++ ) { - if ( *ptr == '\r' ) { - if ( numCharsPerLine > maxCharsPerLine ) { - maxCharsPerLine = numCharsPerLine; - } - numCharsPerLine = 0; - numLines++; - } else if ( *ptr == '\t' ) { - numCharsPerLine += TAB_SIZE; - } else { - numCharsPerLine++; - } - } - - SetWindowText( va( "Script Editor (%s)", fileName ) ); - - rect.left = initialRect.left; - rect.right = rect.left + maxCharsPerLine * FONT_WIDTH + 32; - rect.top = initialRect.top; - rect.bottom = rect.top + numLines * (FONT_HEIGHT+8) + 24 + 56; - if ( rect.right < initialRect.right ) { - rect.right = initialRect.right; - } else if ( rect.right - rect.left > 1024 ) { - rect.right = rect.left + 1024; - } - if ( rect.bottom < initialRect.bottom ) { - rect.bottom = initialRect.bottom; - } else if ( rect.bottom - rect.top > 768 ) { - rect.bottom = rect.top + 768; - } - MoveWindow( rect ); - - okButton.EnableWindow( FALSE ); - - UpdateStatusBar(); - - scriptEdit.SetFocus(); -} - -/* -================ -DialogScriptEditor::OnInitDialog -================ -*/ -BOOL DialogScriptEditor::OnInitDialog() { - - com_editors |= EDITOR_SCRIPT; - - CDialog::OnInitDialog(); - - // load accelerator table - m_hAccel = ::LoadAccelerators( AfxGetResourceHandle(), MAKEINTRESOURCE( IDR_ACCELERATOR_SCRIPTEDITOR ) ); - - // create status bar - statusBar.CreateEx( SBARS_SIZEGRIP, WS_CHILD | WS_VISIBLE | CBRS_BOTTOM, initialRect, this, AFX_IDW_STATUS_BAR ); - - scriptEdit.LimitText( 1024 * 1024 ); - - GetClientRect( initialRect ); - - SetWindowText( "Script Editor" ); - - EnableToolTips( TRUE ); - - okButton.EnableWindow( FALSE ); - - UpdateStatusBar(); - - return FALSE; // return TRUE unless you set the focus to a control - // EXCEPTION: OCX Property Pages should return FALSE -} - -BEGIN_MESSAGE_MAP(DialogScriptEditor, CDialog) +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_MSGFILTER, IDC_SCRIPTEDITOR_EDIT_TEXT, OnEnInputEdit) + 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() -/* -================ -ScriptEditorInit -================ -*/ -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; - } - - if ( g_ScriptDialog == NULL ) { - InitAfx(); - g_ScriptDialog = new DialogScriptEditor(); - } - - if ( g_ScriptDialog->GetSafeHwnd() == NULL) { - g_ScriptDialog->Create( IDD_DIALOG_SCRIPTEDITOR ); -/* - // FIXME: restore position - CRect rct; - g_ScriptDialog->SetWindowPos( NULL, rct.left, rct.top, 0, 0, SWP_NOSIZE ); -*/ - } - - idKeyInput::ClearStates(); - - g_ScriptDialog->ShowWindow( SW_SHOW ); - g_ScriptDialog->SetFocus(); - - if ( spawnArgs ) { - } +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); } -/* -================ -ScriptEditorRun -================ -*/ -void ScriptEditorRun( void ) { -#if _MSC_VER >= 1300 - MSG *msg = AfxGetCurrentMessage(); // TODO Robert fix me!! -#else - MSG *msg = &m_msgCur; -#endif +DialogScriptEditor::~DialogScriptEditor() { + if (primaryEditor == this) { + primaryEditor = NULL; + } + delete scriptEdit; + scriptEdit = NULL; +} - while( ::PeekMessage(msg, NULL, NULL, NULL, PM_NOREMOVE) ) { - // pump message - if ( !AfxGetApp()->PumpMessage() ) { +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); } } } -/* -================ -ScriptEditorShutdown -================ -*/ -void ScriptEditorShutdown( void ) { - delete g_ScriptDialog; - g_ScriptDialog = NULL; +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 = ""; + } -// DialogScriptEditor message handlers + src.ExpectTokenType(TT_NAME, 0, &token); + info.parms = token; -/* -================ -DialogScriptEditor::OnActivate -================ -*/ -void DialogScriptEditor::OnActivate( UINT nState, CWnd *pWndOther, BOOL bMinimized ) { - CDialog::OnActivate( nState, pWndOther, bMinimized ); -} + src.ExpectTokenType(TT_NAME, 0, &token); + info.name = token; -/* -================ -DialogScriptEditor::OnToolTipNotify -================ -*/ -BOOL DialogScriptEditor::OnToolTipNotify( UINT id, NMHDR *pNMHDR, LRESULT *pResult ) { - return DefaultOnToolTipNotify( toolTips, id, pNMHDR, pResult ); -} + src.ExpectTokenString("("); -/* -================ -DialogScriptEditor::OnSetFocus -================ -*/ -void DialogScriptEditor::OnSetFocus( CWnd *pOldWnd ) { - CDialog::OnSetFocus( pOldWnd ); -} + info.parms += " " + info.name + "("; + while (src.ReadToken(&token) && token != ";") { + info.parms.Append(" " + token); + } -/* -================ -DialogScriptEditor::OnDestroy -================ -*/ -void DialogScriptEditor::OnDestroy() { - return CDialog::OnDestroy(); -} - -/* -================ -DialogScriptEditor::OnMove -================ -*/ -void DialogScriptEditor::OnMove( int x, int y ) { - if ( GetSafeHwnd() ) { - CRect rct; - GetWindowRect( rct ); - // FIXME: save position - } - CDialog::OnMove( x, y ); -} - -/* -================ -DialogScriptEditor::OnSize -================ -*/ -#define BORDER_SIZE 0 -#define BUTTON_SPACE 4 -#define TOOLBAR_HEIGHT 24 - -void DialogScriptEditor::OnSize( UINT nType, int cx, int cy ) { - CRect clientRect, rect; - - LockWindowUpdate(); - - CDialog::OnSize( nType, cx, cy ); - - GetClientRect( clientRect ); - - if ( scriptEdit.GetSafeHwnd() ) { - rect.left = BORDER_SIZE; - rect.top = BORDER_SIZE; - rect.right = clientRect.Width() - BORDER_SIZE; - rect.bottom = clientRect.Height() - 56; - scriptEdit.MoveWindow( rect.left, rect.top, rect.Width(), rect.Height() ); - } - - if ( okButton.GetSafeHwnd() ) { - okButton.GetClientRect( rect ); - int width = rect.Width(); - int height = rect.Height(); - rect.left = clientRect.Width() - BORDER_SIZE - BUTTON_SPACE - 2 * width; - rect.top = clientRect.Height() - TOOLBAR_HEIGHT - height; - rect.right = clientRect.Width() - BORDER_SIZE - BUTTON_SPACE - width; - rect.bottom = clientRect.Height() - TOOLBAR_HEIGHT; - okButton.MoveWindow( rect.left, rect.top, rect.Width(), rect.Height() ); - } - - if ( cancelButton.GetSafeHwnd() ) { - cancelButton.GetClientRect( rect ); - int width = rect.Width(); - int height = rect.Height(); - rect.left = clientRect.Width() - BORDER_SIZE - width; - rect.top = clientRect.Height() - TOOLBAR_HEIGHT - height; - rect.right = clientRect.Width() - BORDER_SIZE; - rect.bottom = clientRect.Height() - TOOLBAR_HEIGHT; - cancelButton.MoveWindow( rect.left, rect.top, rect.Width(), rect.Height() ); - } - - if ( statusBar.GetSafeHwnd() ) { - rect.left = clientRect.Width() - 2; - rect.top = clientRect.Height() - 2; - rect.right = clientRect.Width() - 2; - rect.bottom = clientRect.Height() - 2; - statusBar.MoveWindow( rect.left, rect.top, rect.Width(), rect.Height() ); - } - - UnlockWindowUpdate(); -} - -/* -================ -DialogScriptEditor::OnSizing -================ -*/ -void DialogScriptEditor::OnSizing( UINT nSide, LPRECT lpRect ) { - /* - 1 = left - 2 = right - 3 = top - 4 = left - top - 5 = right - top - 6 = bottom - 7 = left - bottom - 8 = right - bottom - */ - - CDialog::OnSizing( nSide, lpRect ); - - if ( ( nSide - 1 ) % 3 == 0 ) { - if ( lpRect->right - lpRect->left < initialRect.Width() ) { - lpRect->left = lpRect->right - initialRect.Width(); - } - } else if ( ( nSide - 2 ) % 3 == 0 ) { - if ( lpRect->right - lpRect->left < initialRect.Width() ) { - lpRect->right = lpRect->left + initialRect.Width(); - } - } - if ( nSide >= 3 && nSide <= 5 ) { - if ( lpRect->bottom - lpRect->top < initialRect.Height() ) { - lpRect->top = lpRect->bottom - initialRect.Height(); - } - } else if ( nSide >= 6 && nSide <= 9 ) { - if ( lpRect->bottom - lpRect->top < initialRect.Height() ) { - lpRect->bottom = lpRect->top + initialRect.Height(); + scriptEvents.Append(info); } } } -/* -================ -DialogScriptEditor::OnEditGoToLine -================ -*/ -void DialogScriptEditor::OnEditGoToLine() { - DialogGoToLine goToLineDlg; - - goToLineDlg.SetRange( firstLine, firstLine + scriptEdit.GetLineCount() - 1 ); - if ( goToLineDlg.DoModal() != IDOK ) { +void DialogScriptEditor::EnsureIntelliSenseCore(void) { + if (scriptCoreBuilt) { + m_intelliSenseCoreReady = true; + m_intelliSenseReady = true; + m_indexedSymbols = scriptCompletions.Num(); return; } - scriptEdit.GoToLine( goToLineDlg.GetLine() - firstLine ); + + // Fast path only: keywords and the engine event definitions. This deliberately + // does not scan every script file so the editor tab can open immediately. + InitScriptEvents(); + for (int k = 0; scriptKeywords[k]; k++) { + ScriptEditorAddCompletion(scriptKeywords[k], scriptKeywords[k], "keyword", "built-in", "script keyword", 100); + } + for (int k = 0; guiKeywords[k]; k++) { + ScriptEditorAddCompletion(guiKeywords[k], guiKeywords[k], "gui", "built-in", "gui keyword", 90); + } + for (int i = 0; i < scriptEvents.Num(); i++) { + ScriptEditorAddCompletion(scriptEvents[i].name.c_str(), scriptEvents[i].name.c_str(), "event", "script/doom_events.script", scriptEvents[i].parms.c_str(), 140); + } + + scriptCoreBuilt = true; + m_intelliSenseCoreReady = true; + m_intelliSenseReady = true; + m_indexedSymbols = scriptCompletions.Num(); +} + +void DialogScriptEditor::RebuildVirtualScriptFileList(void) { + scriptVirtualFiles.Clear(); + + if (fileSystem) { + // Try both common extension spellings. Different idTech file-system forks + // disagree about whether ListFilesTree wants ".script" or "script". + ScriptEditorListVirtualFilesFromTree("script", ".script"); + ScriptEditorListVirtualFilesFromTree("script", "script"); + ScriptEditorListVirtualFilesFromTree("scripts", ".script"); + ScriptEditorListVirtualFilesFromTree("scripts", "script"); + ScriptEditorListVirtualFilesFromTree("guis", ".gui"); + ScriptEditorListVirtualFilesFromTree("guis", "gui"); + ScriptEditorListVirtualFilesFromTree("gui", ".gui"); + ScriptEditorListVirtualFilesFromTree("gui", "gui"); + + // Last-resort root scan. This catches pk5s that do not expose the expected + // directory listing under script/ but do expose extension queries globally. + if (scriptVirtualFiles.Num() <= 1) { + ScriptEditorListVirtualFilesFromTree("", ".script"); + ScriptEditorListVirtualFilesFromTree("", "script"); + ScriptEditorListVirtualFilesFromTree("", ".gui"); + ScriptEditorListVirtualFilesFromTree("", "gui"); + } + } + + // Keep the core event file available even if a particular filesystem backend + // does not include it in a directory listing. + ScriptEditorAddVirtualFile("", "script/doom_events.script"); + scriptVirtualFileListBuilt = true; + m_virtualFileListReady = true; +} + +void DialogScriptEditor::EnsureVirtualScriptFileList(bool forceRebuild) { + if (forceRebuild || !scriptVirtualFileListBuilt) { + RebuildVirtualScriptFileList(); + } + else { + m_virtualFileListReady = true; + } +} + +void DialogScriptEditor::StartIntelliSenseBackgroundScan(bool forceRebuild) { + if (forceRebuild) { + if (m_intelliSenseTimer) { + KillTimer(m_intelliSenseTimer); + m_intelliSenseTimer = 0; + } + scriptCompletions.Clear(); + scriptEvents.Clear(); + scriptCoreBuilt = false; + scriptDatabaseBuilt = false; + scriptVirtualFileListBuilt = false; + m_intelliSenseCoreReady = false; + m_virtualFileListReady = false; + m_backgroundScanNextFile = 0; + m_indexedScriptFiles = 0; + m_indexedSymbols = 0; + } + + EnsureIntelliSenseCore(); + if (scriptDatabaseBuilt && !forceRebuild) { + m_backgroundScanActive = false; + return; + } + + // Directory enumeration is cheap compared to parsing every file and is needed + // for the Open Script browser. The heavier parsing below happens by timer. + EnsureVirtualScriptFileList(forceRebuild); + + if (scriptVirtualFiles.Num() <= 0) { + m_backgroundScanActive = false; + return; + } + + m_backgroundScanActive = true; + if (m_intelliSenseTimer == 0 && GetSafeHwnd()) { + m_intelliSenseTimer = SetTimer(SE_INTELLISENSE_TIMER_ID, 40, NULL); + } +} + +void DialogScriptEditor::IndexNextScriptFiles(int maxFiles) { + if (!m_backgroundScanActive) { + return; + } + + EnsureIntelliSenseCore(); + EnsureVirtualScriptFileList(false); + + int indexedThisTick = 0; + while (m_backgroundScanNextFile < scriptVirtualFiles.Num() && indexedThisTick < maxFiles) { + const char* path = scriptVirtualFiles[m_backgroundScanNextFile].c_str(); + m_backgroundScanNextFile++; + + // doom_events.script is already parsed by InitScriptEvents(), but scanning it + // again is harmless. Skipping it avoids duplicated generic function symbols. + if (idStr::Icmp(path, "script/doom_events.script") == 0) { + continue; + } + + idStr text; + if (LoadTextFromFile(path, text)) { + ScriptEditorRemoveCompletionsFromSource(path); + ScriptEditorScanTextForCompletions(path, text.c_str()); + m_indexedScriptFiles++; + } + indexedThisTick++; + } + + m_indexedSymbols = scriptCompletions.Num(); + if (m_backgroundScanNextFile >= scriptVirtualFiles.Num()) { + m_backgroundScanActive = false; + scriptDatabaseBuilt = true; + if (m_intelliSenseTimer) { + KillTimer(m_intelliSenseTimer); + m_intelliSenseTimer = 0; + } + if (m_statusLine.GetSafeHwnd()) { + CString status; + status.Format("IntelliSense ready: indexed %d script/gui files and %d symbols.", m_indexedScriptFiles, m_indexedSymbols); + m_statusLine.SetWindowText(status); + } + } + else if (m_statusLine.GetSafeHwnd()) { + CString status; + status.Format("IntelliSense indexing %d/%d files...", m_backgroundScanNextFile, scriptVirtualFiles.Num()); + m_statusLine.SetWindowText(status); + } +} + +void DialogScriptEditor::UpdateLiveCompletions(void) { + ScriptEditorRemoveCompletionsFromSource(""); + if (!scriptEdit) { + return; + } + idStr text; + scriptEdit->GetText(text); + if (text.Length() > 0) { + ScriptEditorScanTextForCompletions("", text.c_str()); + } + m_indexedSymbols = scriptCompletions.Num(); +} + +void DialogScriptEditor::BuildIntelliSenseDatabase(bool forceRebuild) { + StartIntelliSenseBackgroundScan(forceRebuild); +} + +void DialogScriptEditor::ConfigureEditorForFile(const char* newFileName) { + idStr extension; + idStr(newFileName ? newFileName : "").ExtractFileExtension(extension); + + if (!scriptEdit) { + return; + } + + EnsureIntelliSenseCore(); + + scriptEdit->SetCaseSensitive(false); + scriptEdit->ClearKeywords(); + scriptEdit->ClearFunctionWords(); + + if (extension.Icmp("script") == 0) { + scriptEdit->SetCaseSensitive(true); + for (int k = 0; scriptKeywords[k]; k++) { + scriptEdit->AddKeyword(scriptKeywords[k]); + } + scriptEdit->LoadKeyWordsFromFile("editors/script.def"); + for (int i = 0; i < scriptCompletions.Num(); i++) { + if (scriptCompletions[i].kind.Icmp("function") == 0 || scriptCompletions[i].kind.Icmp("event") == 0) { + scriptEdit->AddFunctionWord(scriptCompletions[i].name.c_str()); + } + } + UpdateLanguageLabel("SCRIPT"); + } + else if (extension.Icmp("gui") == 0) { + for (int k = 0; guiKeywords[k]; k++) { + scriptEdit->AddKeyword(guiKeywords[k]); + } + scriptEdit->LoadKeyWordsFromFile("editors/gui.def"); + UpdateLanguageLabel("GUI"); + } + else { + for (int k = 0; scriptKeywords[k]; k++) { + scriptEdit->AddKeyword(scriptKeywords[k]); + } + UpdateLanguageLabel(extension.Length() ? extension.c_str() : "TEXT"); + } +} + +bool DialogScriptEditor::IsNativeFilePath(const char* path) const { + if (!path || !path[0]) { + return false; + } + if (path[0] && path[1] == ':') { + return true; + } + if (path[0] == '\\' || path[0] == '/') { + return true; + } + return false; +} + +bool DialogScriptEditor::LoadTextFromFile(const char* path, idStr& text) const { + text = ""; + if (!path || !path[0]) { + return false; + } + + void* buffer = NULL; + if (fileSystem && fileSystem->ReadFile(path, &buffer) != -1) { + text = (char*)buffer; + fileSystem->FreeFile(buffer); + return true; + } + + FILE* fp = fopen(path, "rb"); + if (!fp) { + return false; + } + fseek(fp, 0, SEEK_END); + long len = ftell(fp); + fseek(fp, 0, SEEK_SET); + if (len < 0) { + fclose(fp); + return false; + } + char* raw = new char[len + 1]; + long readLen = (long)fread(raw, 1, len, fp); + raw[readLen] = '\0'; + fclose(fp); + text = raw; + delete[] raw; + return true; +} + +bool DialogScriptEditor::WriteTextToFile(const char* path, const char* text, int textLength) const { + if (!path || !path[0]) { + return false; + } + + if (IsNativeFilePath(path)) { + FILE* fp = fopen(path, "wb"); + if (!fp) { + return false; + } + int written = (int)fwrite(text, 1, textLength, fp); + fclose(fp); + return written == textLength; + } + + return fileSystem && fileSystem->WriteFile(path, text, textLength, "fs_devpath") != -1; +} + +bool DialogScriptEditor::OpenFileFromDialog(void) { + EnsureVirtualScriptFileList(true); + StartIntelliSenseBackgroundScan(false); + + CString selectedPath; + CScriptFileBrowserWnd browser; + if (browser.DoModal(this, selectedPath) != IDOK || selectedPath.IsEmpty()) { + FocusEditor(); + return false; + } + + OpenFile(selectedPath); + return true; +} + +bool DialogScriptEditor::PromptSavePath(CString& selectedPath) { + CString suggested; + if (fileName.Length() > 0) { + suggested = fileName.c_str(); + } + else { + suggested = "script/new_script.script"; + } + + CScriptSavePathWnd prompt; + return prompt.DoModal(this, selectedPath, suggested) == IDOK && !selectedPath.IsEmpty(); +} + + +bool DialogScriptEditor::SaveFileAs(void) { + return SaveFileAsDialog(); +} + +bool DialogScriptEditor::SaveFileAsDialog(void) { + CString selectedPath; + if (!PromptSavePath(selectedPath)) { + FocusEditor(); + return false; + } + + idStr oldFileName = fileName; + fileName = (LPCTSTR)selectedPath; + ConfigureEditorForFile(fileName.c_str()); + + if (!SaveFile()) { + fileName = oldFileName; + UpdateTitle(); + FocusEditor(); + return false; + } + + ScriptEditorAddVirtualFile("", fileName.c_str()); + scriptVirtualFileListBuilt = true; + m_virtualFileListReady = true; + UpdateTitle(); + FocusEditor(); + return true; +} + +void DialogScriptEditor::NewFile(void) { + if (!ConfirmDiscardChanges()) { + FocusEditor(); + return; + } + + fileName = ""; + loadedText = ""; + ConfigureEditorForFile("script/new_script.script"); + SetEditorTextNormalized(""); + SetDirty(false); + UpdateTitle(); + UpdateStatusBar(); + FocusEditor(); +} + +void DialogScriptEditor::OpenFile(const char* newFileName) { + if (!GetSafeHwnd() || !newFileName || !newFileName[0] || !scriptEdit) { + return; + } + + if (!ConfirmDiscardChanges()) { + return; + } + + idStr scriptText; + if (!LoadTextFromFile(newFileName, scriptText)) { + MessageBox(va("Couldn't open: %s", newFileName), "Script Editor", MB_OK | MB_ICONERROR); + return; + } + + fileName = newFileName; + ConfigureEditorForFile(fileName.c_str()); + SetEditorTextNormalized(scriptText.c_str()); + loadedText = scriptText; + loadedText.Replace("\r\n", "\n"); + loadedText.Replace("\r", "\n"); + loadedText.Replace("\v", "\n"); + SetDirty(false); + UpdateTitle(); + UpdateStatusBar(); + FocusEditor(); +} + +void DialogScriptEditor::SetEditorTextNormalized(const char* text) { + internalChange = true; + if (scriptEdit) { + scriptEdit->SetText(text ? text : ""); + } + internalChange = false; +} + +bool DialogScriptEditor::GetEditorTextNormalized(idStr& text) const { + if (!scriptEdit || !scriptEdit->GetSafeHwnd()) { + text = ""; + return false; + } + + idStr raw; + scriptEdit->GetText(raw); + CString crlf = ScriptEditorToCRLF(CString(raw.c_str())); + text = (LPCTSTR)crlf; + return true; +} + +bool DialogScriptEditor::SaveFile(void) { + if (fileName.Length() == 0) { + return SaveFileAsDialog(); + } + + idStr scriptText; + GetEditorTextNormalized(scriptText); + + common->Printf("Writing '%s'...\n", fileName.c_str()); + if (!WriteTextToFile(fileName.c_str(), scriptText.c_str(), scriptText.Length())) { + MessageBox(va("Couldn't save: %s", fileName.c_str()), va("Error saving: %s", fileName.c_str()), MB_OK | MB_ICONERROR); + return false; + } + + loadedText = scriptText; + loadedText.Replace("\r\n", "\n"); + loadedText.Replace("\r", "\n"); + SetDirty(false); + UpdateStatusBar(); + return true; +} + +bool DialogScriptEditor::ReloadFile(void) { + if (fileName.Length() == 0) { + return false; + } + idStr oldName = fileName; + fileName = ""; + OpenFile(oldName.c_str()); + return true; +} + +bool DialogScriptEditor::HasOpenFile(void) const { + return fileName.Length() > 0; +} + +bool DialogScriptEditor::IsDirty(void) const { + return isDirty; +} + +const char* DialogScriptEditor::GetFileName(void) const { + return fileName.c_str(); +} + +bool DialogScriptEditor::ConfirmDiscardChanges(void) { + if (!isDirty) { + return true; + } + + int result = MessageBox("The current script has unsaved changes. Save them before continuing?", "Script Editor", MB_YESNOCANCEL | MB_ICONQUESTION); + if (result == IDCANCEL) { + return false; + } + if (result == IDYES) { + return SaveFile(); + } + return true; +} + +void DialogScriptEditor::SetDirty(bool dirty) { + isDirty = dirty; + if (okButton.GetSafeHwnd()) { + okButton.EnableWindow(dirty || fileName.Length() == 0); + } + if (saveAsButton.GetSafeHwnd()) { + saveAsButton.EnableWindow(TRUE); + } + if (cancelButton.GetSafeHwnd()) { + cancelButton.EnableWindow(fileName.Length() > 0); + } + UpdateTitle(); +} + +void DialogScriptEditor::UpdateTitle(void) { + CString title; + if (fileName.Length() > 0) { + title.Format("SCRIPT EDITOR%s", isDirty ? " *" : ""); + m_pathLabel.SetWindowText(fileName.c_str()); + } + else { + title.Format("SCRIPT EDITOR%s", isDirty ? " *" : ""); + m_pathLabel.SetWindowText(isDirty ? "New script (not saved yet)" : "No file loaded"); + } + m_titleLabel.SetWindowText(title); + SetWindowText(fileName.Length() > 0 ? va("Script Editor (%s)%s", fileName.c_str(), isDirty ? " *" : "") : "Script Editor"); +} + +void DialogScriptEditor::UpdateLanguageLabel(const char* extension) { + CString text; + if (m_intelliSenseReady) { + text.Format("%s | IntelliSense: %d symbols", extension && extension[0] ? extension : "TEXT", m_indexedSymbols); + } + else { + text.Format("%s | IntelliSense indexing...", extension && extension[0] ? extension : "TEXT"); + } + m_languageLabel.SetWindowText(text); +} + +void DialogScriptEditor::UpdateStatusBar(void) { + if (!scriptEdit || !scriptEdit->GetSafeHwnd() || !m_statusLine.GetSafeHwnd()) { + return; + } + + int line = 0; + int column = 0; + int character = 0; + scriptEdit->GetCursorPos(line, column, character); + + CString status; + status.Format("Line: %d Column: %d Character: %d%s", line, column, character, isDirty ? " Modified" : ""); + m_statusLine.SetWindowText(status); +} + +void DialogScriptEditor::LayoutChildren(void) { + if (!GetSafeHwnd()) { + return; + } + + CRect client; + GetClientRect(client); + + const int margin = 8; + const int topH = 58; + const int bottomH = 24; + const int buttonH = 24; + const int buttonGap = 6; + int x = client.left + margin; + int y = client.top + 6; + + if (m_titleLabel.GetSafeHwnd()) { + m_titleLabel.MoveWindow(x, y, 160, 22, TRUE); + } + + int rightX = client.right - margin; + CWnd* buttons[] = { &newButton, &openButton, &okButton, &saveAsButton, &cancelButton, &findButton, &replaceButton, &goToButton }; + int widths[] = { 58, 112, 66, 86, 72, 64, 74, 68 }; + for (int i = sizeof(buttons) / sizeof(buttons[0]) - 1; i >= 0; i--) { + if (buttons[i]->GetSafeHwnd()) { + rightX -= widths[i]; + buttons[i]->MoveWindow(rightX, y, widths[i], buttonH, TRUE); + rightX -= buttonGap; + } + } + + CRect pathRect(x, y + 26, client.right - margin - 250, y + 48); + if (pathRect.right < pathRect.left + 80) { + pathRect.right = pathRect.left + 80; + } + if (m_pathLabel.GetSafeHwnd()) { + m_pathLabel.MoveWindow(pathRect, TRUE); + } + if (m_languageLabel.GetSafeHwnd()) { + m_languageLabel.MoveWindow(client.right - margin - 240, y + 26, 240, 22, TRUE); + } + + CRect editRect( + client.left + margin, + client.top + topH, + client.right - margin, + client.bottom - bottomH - margin + ); + if (editRect.Width() < SE_MIN_EDITOR_WIDTH) { + editRect.right = editRect.left + SE_MIN_EDITOR_WIDTH; + } + if (editRect.Height() < SE_MIN_EDITOR_HEIGHT) { + editRect.bottom = editRect.top + SE_MIN_EDITOR_HEIGHT; + } + if (scriptEdit && scriptEdit->GetSafeHwnd()) { + scriptEdit->MoveWindow(editRect, TRUE); + } + + if (m_statusLine.GetSafeHwnd()) { + m_statusLine.MoveWindow(client.left + margin, client.bottom - bottomH, client.Width() - margin * 2, bottomH - 2, TRUE); + } + + if (m_intellisenseList.GetSafeHwnd() && m_intellisenseList.IsWindowVisible()) { + PositionIntelliSenseWindow(); + } + if (m_signatureHelp.GetSafeHwnd() && m_signatureHelp.IsWindowVisible()) { + PositionParameterHint(); + } +} + +void DialogScriptEditor::SetActive(BOOL bActive) { + isActive = bActive; + if (bActive) { + FocusEditor(); + } +} + +void DialogScriptEditor::FocusEditor(void) { + if (scriptEdit && scriptEdit->GetSafeHwnd()) { + scriptEdit->SetFocus(); + } +} + +void DialogScriptEditor::OnSize(UINT nType, int cx, int cy) { + CWnd::OnSize(nType, cx, cy); + LayoutChildren(); +} + +void DialogScriptEditor::OnSizing(UINT nSide, LPRECT lpRect) { + CWnd::OnSizing(nSide, lpRect); + if (!lpRect) { + return; + } + if (lpRect->right - lpRect->left < initialRect.Width()) { + lpRect->right = lpRect->left + initialRect.Width(); + } + if (lpRect->bottom - lpRect->top < initialRect.Height()) { + lpRect->bottom = lpRect->top + initialRect.Height(); + } +} + +void DialogScriptEditor::OnMove(int x, int y) { + CWnd::OnMove(x, y); +} + +void DialogScriptEditor::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized) { + CWnd::OnActivate(nState, pWndOther, bMinimized); +} + +void DialogScriptEditor::OnSetFocus(CWnd* pOldWnd) { + CWnd::OnSetFocus(pOldWnd); + FocusEditor(); +} + +void DialogScriptEditor::OnTimer(UINT_PTR nIDEvent) { + if (nIDEvent == SE_INTELLISENSE_TIMER_ID) { + if (!m_backgroundScanActive) { + if (scriptDatabaseBuilt) { + KillTimer(m_intelliSenseTimer); + m_intelliSenseTimer = 0; + return; + } + StartIntelliSenseBackgroundScan(false); + } + if (m_backgroundScanActive) { + IndexNextScriptFiles(2); + } + else if (m_intelliSenseTimer) { + KillTimer(m_intelliSenseTimer); + m_intelliSenseTimer = 0; + } + return; + } + CWnd::OnTimer(nIDEvent); +} + +void DialogScriptEditor::OnDestroy() { + if (m_intelliSenseTimer) { + KillTimer(m_intelliSenseTimer); + m_intelliSenseTimer = 0; + } + if (findDlg) { + if (findDlg->GetSafeHwnd()) { + findDlg->DestroyWindow(); + } + delete findDlg; + findDlg = NULL; + } + if (primaryEditor == this) { + primaryEditor = NULL; + } + CWnd::OnDestroy(); +} + +BOOL DialogScriptEditor::OnEraseBkgnd(CDC* pDC) { + CRect client; + GetClientRect(client); + pDC->FillSolidRect(client, SE_DARK_BG); + return TRUE; +} + +void DialogScriptEditor::OnPaint() { + CPaintDC dc(this); + CRect client; + GetClientRect(client); + dc.FillSolidRect(client, SE_DARK_BG); + + CRect header(client.left, client.top, client.right, client.top + 56); + dc.FillSolidRect(header, SE_DARK_PANEL); + dc.FillSolidRect(client.left, header.bottom - 1, client.Width(), 1, SE_DARK_BORDER); + + CRect editFrame; + if (scriptEdit && scriptEdit->GetSafeHwnd()) { + scriptEdit->GetWindowRect(editFrame); + ScreenToClient(editFrame); + editFrame.InflateRect(1, 1); + dc.Draw3dRect(editFrame, SE_DARK_BORDER, RGB(4, 6, 10)); + } +} + +HBRUSH DialogScriptEditor::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { + if (pDC) { + pDC->SetBkMode(TRANSPARENT); + pDC->SetTextColor(SE_DARK_TEXT); + } + + if (nCtlColor == CTLCOLOR_LISTBOX) { + if (pDC) { + pDC->SetBkColor(SE_DARK_EDIT); + pDC->SetTextColor(SE_DARK_TEXT); + } + return (HBRUSH)m_editBrush.GetSafeHandle(); + } + + if (nCtlColor == CTLCOLOR_STATIC) { + return (HBRUSH)m_panelBrush.GetSafeHandle(); + } + + return (HBRUSH)m_backBrush.GetSafeHandle(); +} + +void DialogScriptEditor::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT dis) { + if (nIDCtl == IDC_SCRIPTEDITOR_INTELLISENSE_LIST && dis) { + HDC hDC = dis->hDC; + RECT rc = dis->rcItem; + bool selected = (dis->itemState & ODS_SELECTED) != 0; + ScriptEditorFillRect(hDC, rc, selected ? SE_DARK_SELECTION : SE_DARK_PANEL_2); + if (dis->itemID != (UINT)-1) { + CString text; + m_intellisenseList.GetText(dis->itemID, text); + HFONT font = (HFONT)m_uiFont.GetSafeHandle(); + HFONT oldFont = font ? (HFONT)::SelectObject(hDC, font) : NULL; + ::SetBkMode(hDC, TRANSPARENT); + ::SetTextColor(hDC, selected ? SE_DARK_SELECTION_TEXT : SE_DARK_TEXT); + rc.left += 8; + rc.right -= 4; + ::DrawTextA(hDC, text, -1, &rc, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); + if (oldFont) { + ::SelectObject(hDC, oldFont); + } + } + return; + } + CWnd::OnDrawItem(nIDCtl, dis); +} + +void DialogScriptEditor::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT mis) { + if (nIDCtl == IDC_SCRIPTEDITOR_INTELLISENSE_LIST && mis) { + mis->itemHeight = 20; + return; + } + CWnd::OnMeasureItem(nIDCtl, mis); +} + +BOOL DialogScriptEditor::OnToolTipNotify(UINT id, NMHDR* pNMHDR, LRESULT* pResult) { + return DefaultOnToolTipNotify(toolTips, id, pNMHDR, pResult); +} + +void DialogScriptEditor::OnFileNew() { + NewFile(); +} + +void DialogScriptEditor::OnFileOpen() { + OpenFileFromDialog(); +} + +void DialogScriptEditor::OnFileSaveAs() { + SaveFileAsDialog(); +} + +void DialogScriptEditor::OnEditGoToLine() { + if (!scriptEdit || !scriptEdit->GetSafeHwnd()) { + return; + } + + DialogGoToLine goToLineDlg; + goToLineDlg.SetRange(1, scriptEdit->GetLineCount()); + if (goToLineDlg.DoModal() != IDOK) { + return; + } + scriptEdit->GoToLine(goToLineDlg.GetLine() - 1); + FocusEditor(); + UpdateStatusBar(); } -/* -================ -DialogScriptEditor::OnEditFind -================ -*/ void DialogScriptEditor::OnEditFind() { - - CString selText = scriptEdit.GetSelText(); - if ( selText.GetLength() ) { + CString selText = scriptEdit ? scriptEdit->GetSelText() : ""; + if (selText.GetLength()) { findStr = selText; } - // create find/replace dialog - if ( !findDlg ) { - findDlg = new CFindReplaceDialog(); // Must be created on the heap - findDlg->Create( TRUE, findStr, "", FR_DOWN, this ); + if (!findDlg) { + findDlg = new CFindReplaceDialog(); + findDlg->Create(TRUE, findStr, "", FR_DOWN, this); } } -/* -================ -DialogScriptEditor::OnEditFindNext -================ -*/ void DialogScriptEditor::OnEditFindNext() { - if ( scriptEdit.FindNext( findStr, matchCase, matchWholeWords, searchForward ) ) { - scriptEdit.SetFocus(); - } else { - AfxMessageBox( "The specified text was not found.", MB_OK | MB_ICONINFORMATION, 0 ); + 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); } } -/* -================ -DialogScriptEditor::OnEditReplace -================ -*/ void DialogScriptEditor::OnEditReplace() { - - CString selText = scriptEdit.GetSelText(); - if ( selText.GetLength() ) { + CString selText = scriptEdit ? scriptEdit->GetSelText() : ""; + if (selText.GetLength()) { findStr = selText; } - // create find/replace dialog - if ( !findDlg ) { - findDlg = new CFindReplaceDialog(); // Must be created on the heap - findDlg->Create( FALSE, findStr, "", FR_DOWN, this ); + if (!findDlg) { + findDlg = new CFindReplaceDialog(); + findDlg->Create(FALSE, findStr, "", FR_DOWN, this); } } -/* -================ -DialogScriptEditor::OnFindDialogMessage -================ -*/ -LRESULT DialogScriptEditor::OnFindDialogMessage( WPARAM wParam, LPARAM lParam ) { - if ( findDlg == NULL ) { +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->IsTerminating()) { + findDlg = NULL; + return 0; + } - if( findDlg->FindNext() ) { + if (findDlg->FindNext()) { findStr = findDlg->GetFindString(); matchCase = findDlg->MatchCase() != FALSE; matchWholeWords = findDlg->MatchWholeWord() != FALSE; searchForward = findDlg->SearchDown() != FALSE; - OnEditFindNext(); - } + } - if ( findDlg->ReplaceCurrent() ) { + if (findDlg->ReplaceCurrent()) { long selStart, selEnd; - replaceStr = findDlg->GetReplaceString(); - scriptEdit.GetSel( selStart, selEnd ); - if ( selEnd > selStart ) { - scriptEdit.ReplaceSel( replaceStr, TRUE ); + scriptEdit->GetSel(selStart, selEnd); + if (selEnd > selStart) { + scriptEdit->ReplaceSel(replaceStr, TRUE); + SetDirty(true); } } - if ( findDlg->ReplaceAll() ) { + if (findDlg->ReplaceAll()) { replaceStr = findDlg->GetReplaceString(); findStr = findDlg->GetFindString(); matchCase = findDlg->MatchCase() != FALSE; matchWholeWords = findDlg->MatchWholeWord() != FALSE; - int numReplaces = scriptEdit.ReplaceAll( findStr, replaceStr, matchCase, matchWholeWords ); - if ( numReplaces == 0 ) { - AfxMessageBox( "The specified text was not found.", MB_OK | MB_ICONINFORMATION, 0 ); - } else { - AfxMessageBox( va( "Replaced %d occurances.", numReplaces ), MB_OK | MB_ICONINFORMATION, 0 ); + 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; } -/* -================ -DialogScriptEditor::OnEnChangeEdit -================ -*/ -void DialogScriptEditor::OnEnChangeEdit( NMHDR *pNMHDR, LRESULT *pResult ) { - okButton.EnableWindow( TRUE ); -} - -/* -================ -DialogScriptEditor::OnEnInputEdit -================ -*/ -void DialogScriptEditor::OnEnInputEdit( NMHDR *pNMHDR, LRESULT *pResult ) { - MSGFILTER *msgFilter = (MSGFILTER *)pNMHDR; - - if ( msgFilter->msg != 512 && msgFilter->msg != 33 ) { +void DialogScriptEditor::OnEnChangeEdit(NMHDR* pNMHDR, LRESULT* pResult) { + if (!internalChange) { + SetDirty(true); UpdateStatusBar(); } - - *pResult = 0; + if (pResult) { + *pResult = 0; + } } -/* -================ -DialogScriptEditor::OnBnClickedOk -================ -*/ -void DialogScriptEditor::OnBnClickedOk() { - idStr scriptText; +void DialogScriptEditor::OnEditorSelectionChanged(NMHDR* pNMHDR, LRESULT* pResult) { + if (m_signatureHelp.GetSafeHwnd() && m_signatureHelp.IsWindowVisible() && !IsFunctionArgumentContext()) { + HideFunctionParameterHint(); + } + UpdateStatusBar(); + if (pResult) { + *pResult = 0; + } +} - common->Printf( "Writing \'%s\'...\n", fileName.c_str() ); +LRESULT DialogScriptEditor::OnDeferredIntelliSense(WPARAM wParam, LPARAM lParam) { + if (wParam == 2) { + HideIntelliSense(); + HideFunctionParameterHint(); + return 0; + } - scriptEdit.GetText( scriptText ); + if (wParam == 3) { + HideIntelliSense(); + ShowFunctionParameterHint(); + return 0; + } - // clean up new-line crapola - scriptText.Replace( "\n", "" ); - scriptText.Replace( "\r", "\r\n" ); - scriptText.Replace( "\v", "\r\n" ); + if (wParam == 4) { + HideFunctionParameterHint(); + return 0; + } - if ( fileSystem->WriteFile( fileName, scriptText, scriptText.Length(), "fs_devpath" ) == -1 ) { - MessageBox( va( "Couldn't save: %s", fileName.c_str() ), va( "Error saving: %s", fileName.c_str() ), MB_OK | MB_ICONERROR ); + 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; } - okButton.EnableWindow( FALSE ); + 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(); } -/* -================ -DialogScriptEditor::OnBnClickedCancel -================ -*/ void DialogScriptEditor::OnBnClickedCancel() { - if ( okButton.IsWindowEnabled() ) { - if ( MessageBox( "Cancel changes?", "Cancel", MB_YESNO | MB_ICONQUESTION ) != IDYES ) { + 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; } } - OnCancel(); + 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; } diff --git a/neo/engine/tools/script/DialogScriptEditor.h b/neo/engine/tools/script/DialogScriptEditor.h index 64437389..8006c2da 100644 --- a/neo/engine/tools/script/DialogScriptEditor.h +++ b/neo/engine/tools/script/DialogScriptEditor.h @@ -31,43 +31,82 @@ If you have questions concerning this license or the applicable additional terms #pragma once -#include "../comafx/CSyntaxRichEditCtrl.h" +class CScriptEditorCtrl; - -// DialogScriptEditor dialog - -class DialogScriptEditor : public CDialog { +//============================================================================= +// DialogScriptEditor +// +// Dockable Script Editor tab page. This version intentionally does not use +// the old syntax-rich edit control. The text editor is a custom dark-themed +// CWnd owned by DialogScriptEditor, with double-buffered manual drawing, +// caret/selection handling, syntax coloring, find/replace, virtual pk5 script +// opening, automatic incremental IntelliSense, signature help, and Save As/New +// file support. +//============================================================================= +class DialogScriptEditor : public CWnd { DECLARE_DYNAMIC(DialogScriptEditor) public: - DialogScriptEditor( CWnd* pParent = NULL ); // standard constructor + DialogScriptEditor( CWnd* pParent = NULL ); virtual ~DialogScriptEditor(); + BOOL Create( CWnd *pParent, UINT nID ); + BOOL Create( const RECT &rect, CWnd *pParent, UINT nID ); + void OpenFile( const char *fileName ); + bool SaveFile( void ); + bool SaveFileAs( void ); + bool ReloadFile( void ); + void NewFile( void ); + bool HasOpenFile( void ) const; + bool IsDirty( void ) const; + const char * GetFileName( void ) const; + + void SetActive( BOOL bActive ); + void FocusEditor( void ); + void LayoutChildren( void ); + void ShowIntelliSense( bool forced = false ); + void HideIntelliSense( void ); + + static DialogScriptEditor *GetPrimaryEditor( void ); //{{AFX_VIRTUAL(DialogScriptEditor) - virtual BOOL OnInitDialog(); - virtual void DoDataExchange( CDataExchange* pDX ); // DDX/DDV support virtual BOOL PreTranslateMessage( MSG* pMsg ); + virtual BOOL OnCmdMsg( UINT nID, int nCode, void *pExtra, AFX_CMDHANDLERINFO *pHandlerInfo ); //}}AFX_VIRTUAL protected: //{{AFX_MSG(DialogScriptEditor) - afx_msg BOOL OnToolTipNotify( UINT id, NMHDR *pNMHDR, LRESULT *pResult ); - afx_msg void OnSetFocus( CWnd *pOldWnd ); + afx_msg int OnCreate( LPCREATESTRUCT lpCreateStruct ); afx_msg void OnDestroy(); afx_msg void OnActivate( UINT nState, CWnd* pWndOther, BOOL bMinimized ); afx_msg void OnMove( int x, int y ); afx_msg void OnSize( UINT nType, int cx, int cy ); afx_msg void OnSizing( UINT nSide, LPRECT lpRect ); + afx_msg void OnSetFocus( CWnd *pOldWnd ); + afx_msg void OnTimer( UINT_PTR nIDEvent ); + afx_msg BOOL OnEraseBkgnd( CDC *pDC ); + afx_msg HBRUSH OnCtlColor( CDC *pDC, CWnd *pWnd, UINT nCtlColor ); + afx_msg void OnPaint(); + afx_msg void OnDrawItem( int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct ); + afx_msg void OnMeasureItem( int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct ); + afx_msg BOOL OnToolTipNotify( UINT id, NMHDR *pNMHDR, LRESULT *pResult ); + afx_msg void OnFileNew(); + afx_msg void OnFileOpen(); + afx_msg void OnFileSaveAs(); afx_msg void OnEditGoToLine(); afx_msg void OnEditFind(); afx_msg void OnEditFindNext(); afx_msg void OnEditReplace(); + afx_msg void OnEditShowIntelliSense(); afx_msg LRESULT OnFindDialogMessage( WPARAM wParam, LPARAM lParam ); + afx_msg LRESULT OnDeferredIntelliSense( WPARAM wParam, LPARAM lParam ); + afx_msg LRESULT OnIntelliSenseKey( WPARAM wParam, LPARAM lParam ); afx_msg void OnEnChangeEdit( NMHDR *pNMHDR, LRESULT *pResult ); - afx_msg void OnEnInputEdit( NMHDR *pNMHDR, LRESULT *pResult ); + afx_msg void OnEditorSelectionChanged( NMHDR *pNMHDR, LRESULT *pResult ); + afx_msg void OnIntelliSenseDblClick(); + afx_msg void OnIntelliSenseSelChange(); afx_msg void OnBnClickedOk(); afx_msg void OnBnClickedCancel(); //}}AFX_MSG @@ -75,30 +114,96 @@ protected: DECLARE_MESSAGE_MAP() private: - //{{AFX_DATA(DialogScriptEditor) - enum { IDD = IDD_DIALOG_SCRIPTEDITOR }; - CStatusBarCtrl statusBar; - CSyntaxRichEditCtrl scriptEdit; + CStatic m_titleLabel; + CStatic m_pathLabel; + CStatic m_statusLine; + CStatic m_languageLabel; + CStatic m_signatureHelp; + CScriptEditorCtrl *scriptEdit; + CListBox m_intellisenseList; + CButton newButton; + CButton openButton; + CButton saveAsButton; CButton okButton; CButton cancelButton; - //}}AFX_DATA + CButton findButton; + CButton replaceButton; + CButton goToButton; + + CFont m_editorFont; + CFont m_uiFont; + CBrush m_backBrush; + CBrush m_panelBrush; + CBrush m_editBrush; + CBrush m_hotBrush; static toolTip_t toolTips[]; + static DialogScriptEditor *primaryEditor; HACCEL m_hAccel; CRect initialRect; - CFindReplaceDialog *findDlg; + CFindReplaceDialog *findDlg; CString findStr; CString replaceStr; bool matchCase; bool matchWholeWords; bool searchForward; idStr fileName; - int firstLine; + idStr loadedText; + int firstLine; + bool isDirty; + bool internalChange; + BOOL isActive; + bool m_intelliSenseReady; + bool m_intelliSenseCoreReady; + bool m_virtualFileListReady; + bool m_backgroundScanActive; + int m_backgroundScanNextFile; + UINT_PTR m_intelliSenseTimer; + int m_indexedScriptFiles; + int m_indexedSymbols; private: void InitScriptEvents( void ); + void InitEditorChrome( void ); + void ApplyDarkTheme( void ); + void ConfigureEditorForFile( const char *fileName ); + void EnsureIntelliSenseCore( void ); + void EnsureVirtualScriptFileList( bool forceRebuild = false ); + void StartIntelliSenseBackgroundScan( bool forceRebuild = false ); + void IndexNextScriptFiles( int maxFiles ); + void UpdateLiveCompletions( void ); + void BuildIntelliSenseDatabase( bool forceRebuild = false ); + void RebuildVirtualScriptFileList( void ); + bool OpenFileFromDialog( void ); + bool PromptSavePath( CString &selectedPath ); + bool SaveFileAsDialog( void ); + bool LoadTextFromFile( const char *path, idStr &text ) const; + bool WriteTextToFile( const char *path, const char *text, int textLength ) const; + bool IsNativeFilePath( const char *path ) const; + void SetDirty( bool dirty ); + void UpdateTitle( void ); void UpdateStatusBar( void ); + void UpdateLanguageLabel( const char *extension ); + void SetEditorTextNormalized( const char *text ); + bool GetEditorTextNormalized( idStr &text ) const; + bool ConfirmDiscardChanges( void ); + bool HandleIntelliSenseKey( MSG *msg ); + bool IsMemberCompletionContext( long wordStart ) const; + bool IsFunctionArgumentContext( void ) const; + void CompleteIntelliSense( void ); + void PopulateIntelliSense( const CString &prefix, bool forced ); + bool GetFunctionSignatureBeforeCaret( CString &functionName, CString &signature, int &argumentIndex ) const; + void ShowFunctionParameterHint( void ); + void HideFunctionParameterHint( void ); + void PositionParameterHint( void ); + CString GetCurrentWord( long *wordStart = NULL, long *wordEnd = NULL ) const; + bool IsIdentifierChar( int ch ) const; + bool StartsWithNoCase( const CString &text, const CString &prefix ) const; + void InsertCompletion( const CString &completion ); + void PositionIntelliSenseWindow( void ); }; +DialogScriptEditor *ScriptEditorGetPrimaryEditor( void ); + #endif /* !__DIALOGSCRIPTEDITOR_H__ */