openmohaa/code/uilib/uinotepad.cpp
smallmodel d64e88451f
Fix compiler warnings
Note that in Dismiss(), checking for `this` is not necessary (original code was generated by the compiler)
2024-08-24 15:08:42 +02:00

689 lines
19 KiB
C++

/*
===========================================================================
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<uipopup_describe *> *inner = m_popups.ObjectAt(i);
for (int j = inner->NumObjects(); j > 0; j--) {
uipopup_describe *uipd = inner->ObjectAt(j);
inner->RemoveObjectAt(j);
// NOTE: `delete uipd` is intentionally missing here!
// Since uipds created for this class have data fields
// that contain pointers only to static Event objects,
// the fields don't need to be cleaned up as they weren't
// dynamically allocated in the first place.
// See UINotepad::Create for reference.
// However, FIXME: the uipds themselves should still
// get cleaned up, but that doesn't happen yet.
}
}
}
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<uipopup_describe *> *pops = new Container<uipopup_describe *>();
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(NULL, 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<uipopup_describe *>();
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(NULL, 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);
}
str filecontents;
m_edit->getData(filecontents);
m_edit->setChanged(false);
// FIXME: m_filename could be blank
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;
}