/* =========================================================================== Copyright (C) 2015-2024 the OpenMoHAA team This file is part of OpenMoHAA source code. OpenMoHAA source code is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. OpenMoHAA source code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenMoHAA source code; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ #include "ui_local.h" Event EV_Notepad_SaveAs ( "_notepad_saveas", EV_DEFAULT, NULL, NULL, "Event sent when user selected Save As..." ); Event EV_Notepad_Save ( "_notepad_save", EV_DEFAULT, NULL, NULL, "Event sent when user selected Save" ); Event EV_Notepad_Open ( "_notepad_open", EV_DEFAULT, NULL, NULL, "Event sent when user selected Open" ); Event EV_Notepad_OpenFile ( "openfile", EV_DEFAULT, "s", "nameOfFile", "called to open a file in the notepad" ); Event EV_Notepad_Find ( "_notepad_find", EV_DEFAULT, NULL, NULL, "Event sent when user selected Find" ); Event EV_Notepad_Goto ( "_notepad_goto", EV_DEFAULT, NULL, NULL, "Event sent when user selected Go to line" ); Event EV_Notepad_Copy ( "_notepad_copy", EV_DEFAULT, NULL, NULL, "Event sent when user selected Copy" ); Event EV_Notepad_Cut ( "_notepad_cut", EV_DEFAULT, NULL, NULL, "Event sent when user selected Cut" ); Event EV_Notepad_Paste ( "_notepad_paste", EV_DEFAULT, NULL, NULL, "Event sent when user selected Paste" ); Event W_Notepad_ChildSizeChanged ( "_notepad_childsizechanged", EV_DEFAULT, NULL, NULL, "Signal that the child area of the floating window has changed" ); CLASS_DECLARATION(UIMultiLineEdit, UINotepadEdit, NULL) { {&W_LeftMouseDown, &UINotepadEdit::MousePressed}, {NULL, NULL } }; CLASS_DECLARATION(UIFloatingWindow, UINotepad, NULL) { {&W_Notepad_ChildSizeChanged, &UINotepad::ChildSizeChanged}, {&EV_Notepad_SaveAs, &UINotepad::SaveAs }, {&EV_Notepad_Save, &UINotepad::Save }, {&EV_Notepad_Open, &UINotepad::Open }, {&EV_Notepad_OpenFile, &UINotepad::OpenFile }, {&UIFloatingWindow::W_ClosePressed, &UINotepad::ClosePressed }, {&EV_Notepad_Find, &UINotepad::OnFind }, {&EV_Notepad_Goto, &UINotepad::OnGoto }, {&EV_Notepad_Copy, &UINotepad::OnCopy }, {&EV_Notepad_Paste, &UINotepad::OnPaste }, {&EV_Notepad_Cut, &UINotepad::OnCut }, {NULL, NULL } }; #define CTRL_EVENT_COUNT 10 static ctrlevent_s controlEvents[CTRL_EVENT_COUNT] = { {'o', &EV_Notepad_Open }, {'s', &EV_Notepad_Save }, {'a', &EV_Notepad_SaveAs }, {'w', &UIFloatingWindow::W_ClosePressed}, {'f', &EV_Notepad_Find }, {'g', &EV_Notepad_Goto }, {'c', &EV_Notepad_Copy }, {'x', &EV_Notepad_Cut }, {'v', &EV_Notepad_Paste }, {0, NULL }, }; bool UI_LoadNotepadFile(const char *filename) { UINotepad *uinp = new UINotepad(); UIRect2D rect = UIRect2D(100.0f, 100.0f, 450.0f, 300.0f); if (uinp->Create(NULL, rect, filename)) { uWinMan.ActivateControl(uinp); return true; } if (uinp) { delete uinp; } return false; } UINotepadEdit::UINotepadEdit() { m_notepad = NULL; } void UINotepadEdit::CharEvent(int ch) { if (!m_notepad->ProcessCharEvent(ch)) { UIMultiLineEdit::CharEvent(ch); } } void UINotepadEdit::setNotepad(UINotepad *notepad) { m_notepad = notepad; } bool UINotepadEdit::GotoLine(int line) { if (line < 1 || line - 1 >= m_lines.getCount()) { return false; } m_selection.begin.line = m_selection.end.line = line - 1; m_selection.begin.column = 0; m_selection.end.column = 999999; BoundSelectionPoint(m_selection.end); EnsureSelectionPointVisible(m_selection.end); return true; } bool UINotepadEdit::FindText(const char *text, int offsetFromSel) { if (!text) { return false; } str lowersearch = text; lowersearch.tolower(); selectionpoint_s *topsel, *botsel; SortSelection(&topsel, &botsel); int foundat = -1; bool searchfromtop = false; // This loop runs at most twice: // - once for searchfromtop == false, // - optionally for searchfromtop == true, // but only if text couldn't be found between the cursor and the end of the file while (true) { int i = 0; m_lines.IterateFromHead(); for (; m_lines.IsCurrentValid(); i++, m_lines.IterateNext()) { if (i < topsel->line && !searchfromtop) { // skip lines before the cursor if not searching from the top continue; } if (i > topsel->line && searchfromtop) { // no need to continue if we've looped back to the cursor return false; } str findin = m_lines.getCurrent(); findin.tolower(); const char *where = findin.c_str(); // Added in OPM to avoid code repetition bool searchLineFromCursor = i == topsel->line && !searchfromtop; if (searchLineFromCursor) { // Start searching on the current line, BUT ONLY from the cursor position. // Offset it with the selection offset: // the purpose of this is to prevent matching // on a string that was just found and is still selected, // by offsetting it if (findin.length() <= topsel->column + offsetFromSel) { // cursor is at the end of the line, nothing left to search in this one continue; } where = &where[topsel->column + offsetFromSel]; } const char *found = strstr(where, lowersearch); if (found) { // use the beginning of the line to calculate where the matched text is foundat = found - where; if (searchLineFromCursor) { // take cursor pos. into account if a match is found on the same line as the cursor foundat += topsel->column + offsetFromSel; } break; } } if (foundat == -1) { // couldn't find text in current search iteration if (!searchfromtop) { // try searching from the beginning of the document searchfromtop = true; continue; } // text not found return false; } else { // jump to found text and select it m_selection.begin.column = foundat; m_selection.end.column = foundat + strlen(text); m_selection.begin.line = m_selection.end.line = i; EnsureSelectionPointVisible(m_selection.end); return true; } } } void UINotepadEdit::MousePressed(Event *ev) { if (m_notepad->m_state != STATE_TIMED_MESSAGE) { m_notepad->m_state = STATE_NONE; } UIMultiLineEdit::MouseDown(ev); } UINotepad::UINotepad() { m_state = STATE_NONE; m_edit = NULL; m_menu = NULL; m_status = NULL; } UINotepad::~UINotepad() { for (int i = m_popups.NumObjects(); i > 0; i--) { Container *inner = m_popups.ObjectAt(i); for (int j = inner->NumObjects(); j > 0; j--) { uipopup_describe *uipd = inner->ObjectAt(j); inner->RemoveObjectAt(j); delete uipd; } } } void UINotepad::TimeMessage(const char *message, int howlong) { m_state = STATE_TIMED_MESSAGE; m_timedmessage.die = uid.time + howlong; m_timedmessage.text = message; } bool UINotepad::OpenFile(const char *filename) { char *data = NULL; if (!filename || FS_ReadFile(filename, (void **)&data) == -1) { return false; } setFileName(filename); if (m_edit) { m_edit->Empty(); m_edit->setData(data); } FS_FreeFile(data); return true; } bool UINotepad::Create(UIWidget *parent, UIRect2D& rect, const char *filename) { UIFloatingWindow::Create(parent, rect, "", UColor(0.15f, 0.196f, 0.278f), UHudColor); m_edit = new UINotepadEdit(); m_edit->setNotepad(this); m_edit->InitFrame(getChildSpace(), 0.0f, 0.0f, 20.0f, 20.0f, 0, "verdana-12"); m_edit->setBackgroundColor(UWhite, true); m_edit->setForegroundColor(UBlack); m_edit->setFont("verdana-14"); m_status = new UIStatusBar(WND_ALIGN_BOTTOM, 20.0f); m_status->InitFrame(getChildSpace(), 0.0f, 0.0f, 20.0f, 20.0f, 0, "verdana-12"); m_status->EnableSizeBox(this); m_menu = new UIPulldownMenu(); m_menu->CreateAligned(getChildSpace(), this); // Fixed in OPM: menu text was hard to read when notepad is active //m_menu->setForegroundColor(UBlack); m_menu->setForegroundColor(UHudColor); Container *pops = new Container(); uipopup_describe *uipd = new uipopup_describe("Open (Ctrl-O)", UIP_EVENT, &EV_Notepad_Open, NULL); pops->AddObject(uipd); uipd = new uipopup_describe("Save (Ctrl-S)", UIP_EVENT, &EV_Notepad_Save, NULL); pops->AddObject(uipd); uipd = new uipopup_describe("Save As...(Ctrl-A)", UIP_EVENT, &EV_Notepad_SaveAs, NULL); pops->AddObject(uipd); uipd = new uipopup_describe(str(), UIP_SEPARATOR, NULL, NULL); pops->AddObject(uipd); uipd = new uipopup_describe("Close (Ctrl-W)", UIP_EVENT, &UIFloatingWindow::W_ClosePressed, NULL); pops->AddObject(uipd); m_popups.AddObject(pops); for (int i = 1; i <= pops->NumObjects(); i++) { m_menu->AddUIPopupDescribe("File", pops->ObjectAt(i)); } pops = new Container(); uipd = new uipopup_describe("Copy (Ctrl-C)", UIP_EVENT, &EV_Notepad_Copy, NULL); pops->AddObject(uipd); uipd = new uipopup_describe("Cut (Ctrl-X)", UIP_EVENT, &EV_Notepad_Cut, NULL); pops->AddObject(uipd); uipd = new uipopup_describe("Paste (Ctrl-V)", UIP_EVENT, &EV_Notepad_Paste, NULL); pops->AddObject(uipd); uipd = new uipopup_describe(str(), UIP_SEPARATOR, NULL, NULL); pops->AddObject(uipd); uipd = new uipopup_describe("Find (Ctrl-F)", UIP_EVENT, &EV_Notepad_Find, NULL); pops->AddObject(uipd); uipd = new uipopup_describe("Go to line... (Ctrl-G)", UIP_EVENT, &EV_Notepad_Goto, NULL); pops->AddObject(uipd); m_popups.AddObject(pops); for (int i = 1; i <= pops->NumObjects(); i++) { m_menu->AddUIPopupDescribe("Edit", pops->ObjectAt(i)); } setFileName(""); OpenFile(filename); getChildSpace()->Connect(this, W_SizeChanged, W_Notepad_ChildSizeChanged); getChildSpace()->SendSignal(W_SizeChanged); getChildSpace()->AllowActivate(false); return true; } void UINotepad::ChildSizeChanged(Event *ev) { m_menu->Realign(); UISize2D menuSize = m_menu->getSize(); UISize2D childSpaceSize = getChildSpace()->getSize(); UIRect2D frame(0.0f, menuSize.height, childSpaceSize.width, childSpaceSize.height - 20.0f - menuSize.height); m_edit->setFrame(frame); } void UINotepad::SaveAs(Event *ev) { m_state = STATE_SAVE_AS; m_textinput.text = ""; } void UINotepad::Save(Event *ev) { if (!m_filename.length()) { SaveAs(NULL); // Fixed in OPM: // devs probably forgot to return here - saving was attempted, // even though there was no filename to save the file with return; } str filecontents; m_edit->getData(filecontents); m_edit->setChanged(false); FS_WriteTextFile(m_filename, filecontents, filecontents.length()); str result = "Saved " + m_filename; TimeMessage(result, 3000); } void UINotepad::Open(Event *ev) { PickFile(m_filename, this, EV_Notepad_OpenFile); } void UINotepad::OpenFile(Event *ev) { OpenFile(ev->GetString(1)); } void UINotepad::ClosePressed(Event *ev) { uWinMan.ActivateControl(this); if (m_edit->IsChanged()) { m_state = STATE_CONFIRMCLOSE; m_textinput.text = "y"; } else { PostEvent(EV_Remove, 0.0f); // Fixed in OPM: // in the original game, the control is not deactivated after removal, // so the already heap-freed UINotepad instance still "reacts" to // KeyEvents and CharEvents, creating a use-after-free scenario // that causes crashes when debug heap is enabled uWinMan.DeactivateCurrentControl(); } } void UINotepad::OnFind(Event *ev) { m_state = STATE_FIND_TEXT; m_textinput.text = m_lastfind; } void UINotepad::OnGoto(Event *ev) { m_state = STATE_GOTO_LINE; m_textinput.text = ""; } void UINotepad::OnCopy(Event *ev) { m_edit->CopySelection(); } void UINotepad::OnPaste(Event *ev) { m_edit->PasteSelection(); } void UINotepad::OnCut(Event *ev) { m_edit->CopySelection(); m_edit->DeleteSelection(); } bool UINotepad::ProcessControlEvents(int ch) { if (ch < CTRL('a') || ch > CTRL('z')) { // not a control character return false; } // Fixed in OPM: // Original code didn't bounds-check controlEvents for (int i = 0; i < CTRL_EVENT_COUNT; i++) { ctrlevent_s *event = &controlEvents[i]; if (!event->ev) { // no event assigned return false; } if (ch == CTRL(event->ch)) { // found it PostEvent( new Event(*event->ev), 0.0f ); // create a new Event as it will be deleted by ProcessEvent in L_ProcessPendingEvents return true; } } return false; } bool UINotepad::ProcessCharEvent(int ch) { if (ProcessControlEvents(ch)) { return true; } if (m_state == STATE_NONE || m_state == STATE_TIMED_MESSAGE) { return false; } str text; switch (ch) { case '\b': // Backspace m_textinput.text.CapLength(m_textinput.text.length() - 1); break; case K_ENTER: switch (m_state) { case STATE_GOTO_LINE: { if (!m_textinput.text.length()) { m_state = STATE_NONE; return true; } int line = atoi(m_textinput.text); if (m_edit->GotoLine(line)) { m_state = STATE_NONE; return true; } text = "Line '" + m_textinput.text + "' doesn't exist."; TimeMessage(text, 3000); break; } case STATE_FIND_TEXT: if (!m_textinput.text.length()) { m_state = STATE_NONE; return true; } if (!m_edit->FindText(m_textinput.text, 1)) { text = "Text: '" + m_textinput.text + "' could not be found."; TimeMessage(text, 3000); } m_lastfind = m_textinput.text; return true; case STATE_SAVE_AS: m_state = STATE_NONE; if (!m_textinput.text.length()) { return true; } setFileName(m_textinput.text); Save(NULL); return true; case STATE_CONFIRMCLOSE: { m_state = STATE_NONE; if (!m_textinput.text.length()) { return true; } char choice = tolower(m_textinput.text[0]); if (choice != 'n' && choice != 'y') { return true; } if (choice == 'y') { Save(NULL); } // close window PostEvent(new Event(EV_Remove), 0.0f); // Fixed in OPM: // in the original game, the control is not deactivated after removal, // so the already heap-freed UINotepad instance still "reacts" to // KeyEvents and CharEvents, creating a use-after-free scenario, // causing crashes when debug heap is enabled uWinMan.DeactivateCurrentControl(); return true; } default: m_state = STATE_NONE; return true; } return true; case K_ESCAPE: case K_TAB: m_state = STATE_NONE; return true; } if (!isprint(ch)) { return true; } if (m_state == STATE_CONFIRMCLOSE) { m_textinput.text = ""; } m_textinput.text += (char)ch; return true; } void UINotepad::Draw(void) { UColor out = URed; out.ScaleColor(0.5f); out.ScaleAlpha(0.5f); m_status->setBackgroundColor(out, true); m_status->setForegroundColor(UHudColor); str text; switch (m_state) { case STATE_GOTO_LINE: text = "Goto line (tab cancels): " + m_textinput.text; break; case STATE_FIND_TEXT: text = "Find text (tab cancels): " + m_textinput.text; break; case STATE_SAVE_AS: text = "Save As (tab cancels): " + m_textinput.text; break; case STATE_CONFIRMCLOSE: text = "Save " + m_filename + "? (yes/no/cancel) " + m_textinput.text; break; case STATE_TIMED_MESSAGE: if (uid.time > m_timedmessage.die) { m_state = STATE_NONE; default: m_status->setBackgroundColor(getBackgroundColor(), true); m_status->setForegroundColor(getForegroundColor()); // Fixed in OPM m_menu->setForegroundColor(getForegroundColor()); // Fixed in OPM UIPoint2D selpoint = m_edit->getEndSelPoint(); text = va("Line %d, column %d", (int)selpoint.y + 1, (int)selpoint.x + 1); } else { m_status->setBackgroundColor(getBackgroundColor(), true); m_status->setForegroundColor(URed); text = m_timedmessage.text; } break; } m_status->setTitle(text); UIFloatingWindow::Draw(); } void UINotepad::setFileName(const char *filename) { str text = str(filename) + " - Notepad"; setTitle(text); m_filename = filename; }