/* =========================================================================== 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" #include "../qcommon/localization.h" Event EV_Time_Dragged ( "_umledit_drag_timer", EV_DEFAULT, NULL, NULL, "Used to time mouse drag events" ); Event EV_UIMultiLineEdit_UpdateCvarEvent ( "updatecvar", EV_DEFAULT, NULL, NULL, "Causes text box to update itself using the assigned cvar" ); Event EV_UIMultiLineEdit_SetEdit ( "edit", EV_DEFAULT, "b", "bool", "Set whether or not the text is editable" ); CLASS_DECLARATION(UIWidget, UIMultiLineEdit, NULL) { {&W_LeftMouseDown, &UIMultiLineEdit::MouseDown }, {&W_LeftMouseUp, &UIMultiLineEdit::MouseUp }, {&W_LeftMouseDragged, &UIMultiLineEdit::MouseDragged }, {&EV_Time_Dragged, &UIMultiLineEdit::DragTimer }, {&W_SizeChanged, &UIMultiLineEdit::SizeChanged }, {&EV_UIMultiLineEdit_UpdateCvarEvent, &UIMultiLineEdit::UpdateCvarEvent}, {&EV_UIMultiLineEdit_SetEdit, &UIMultiLineEdit::SetEdit }, {NULL, NULL } }; UIMultiLineEdit::UIMultiLineEdit() { m_vertscroll = NULL; m_mouseState = M_NONE; m_shiftForcedDown = 0; m_edit = true; Empty(); } void UIMultiLineEdit::Empty(void) { m_lines.RemoveAllItems(); m_lines.AddTail(str()); m_selection.begin = {}; m_selection.end = {}; if (m_vertscroll) { m_vertscroll->setNumItems(1); m_vertscroll->setTopItem(0); } m_changed = false; } void UIMultiLineEdit::SetEdit(Event *ev) { m_edit = ev->GetBoolean(1); } void UIMultiLineEdit::setData(const char *data) { str toadd; char s[2] {}; const char *p; m_lines.RemoveAllItems(); for (p = data; *p; p++) { if (*p == '\n') { m_lines.AddTail(toadd); toadd = ""; } else if (*p == '\r') { continue; } else { s[0] = *p; toadd += s; } } m_lines.AddTail(toadd); m_selection.begin = {}; m_selection.end = {}; if (m_vertscroll) { m_vertscroll->setNumItems(m_lines.getCount()); m_vertscroll->setTopItem(0); } m_changed = false; } void UIMultiLineEdit::getData(str& data) { int i; data = ""; for (i = 0, m_lines.IterateFromHead(); m_lines.IsCurrentValid(); i++, m_lines.IterateNext()) { str text = m_lines.getCurrent(); if (i == 0 && !text.length() && m_lines.getCount() <= 1) { // file is empty return; } data += text; if (i != m_lines.getCount() - 1) { data += "\n"; } } } void UIMultiLineEdit::FrameInitialized(void) { delete m_vertscroll; m_vertscroll = new UIVertScroll(); m_vertscroll->setPageHeight(m_frame.size.height / m_font->getHeight(getHighResScale())); m_vertscroll->setTopItem(0); m_vertscroll->setNumItems(m_lines.getCount()); m_vertscroll->InitFrameAlignRight(this, 0, 0); Connect(this, W_SizeChanged, W_SizeChanged); AllowActivate(true); } void UIMultiLineEdit::SizeChanged(Event *ev) { m_vertscroll->setPageHeight(m_frame.size.height / m_font->getHeight(getVirtualScale())); m_vertscroll->InitFrameAlignRight(this, 0, 0); } void UIMultiLineEdit::SortSelection(selectionpoint_t **topsel, selectionpoint_t **botsel) { if (m_selection.begin.line < m_selection.end.line) { *topsel = &m_selection.begin; *botsel = &m_selection.end; } else if (m_selection.begin.line <= m_selection.end.line && m_selection.begin.column <= m_selection.end.column) { *topsel = &m_selection.begin; *botsel = &m_selection.end; } else { *topsel = &m_selection.end; *botsel = &m_selection.begin; } } void UIMultiLineEdit::UpdateCvarEvent(Event *ev) { if (m_cvarname.length()) { const char *s = uii.Cvar_GetString(m_cvarname, ""); if (s) { setData(s); } } } void UIMultiLineEdit::Draw(void) { float aty; int i; UColor selectionColor(UWhite); UColor selectionBG(0, 0, 0.5f, 1); selectionpoint_t *topsel = NULL; selectionpoint_t *botsel = NULL; SortSelection(&topsel, &botsel); m_lines.IterateFromHead(); for (i = 0; i < m_vertscroll->getTopItem(); i++) { m_lines.IterateNext(); } for (i = m_vertscroll->getTopItem(), aty = 0; m_lines.IsCurrentValid() && aty < m_frame.size.height; m_lines.IterateNext(), i++) { str& cur = m_lines.getCurrent(); int caret = 0; if (i < topsel->line || i > botsel->line) { // Print regular line without any selection or cursor present m_font->setColor(m_foreground_color); m_font->Print(0, aty / getVirtualScale()[1], cur, -1, getVirtualScale()); } else { // Current line contains cursor and/or selected text float linewidth = m_font->getWidth(cur, -1); if (i > topsel->line && i < botsel->line) { // all text in current line is selected, it's part of a larger selection, // print entire line with the selection highlight box around it DrawBox(0.0f, aty, linewidth * getVirtualScale()[0], m_font->getHeight(getVirtualScale()), selectionBG, 1.f); m_font->setColor(selectionColor); // Fixed in OPM: // don't spam LOCALIZATION ERROR messages to console // for clicking lines in the opened text document //m_font->Print(0, aty, Sys_LV_CL_ConvertString(cur), -1, m_bVirtual); m_font->Print(0, aty / getVirtualScale()[1], cur, -1, getVirtualScale()); } else if (i != topsel->line) { // part of this line is selected, and selection continues/began above if (i == botsel->line) { // sanity check, should always be true int toplen = m_font->getWidth(cur, botsel->column); // X coord of highlighting end if (toplen) { // selection contains text from the beginning of the line, // print it with the selection highlight box around it m_font->setColor(selectionColor); DrawBox(0, aty, toplen * getVirtualScale()[0], m_font->getHeight(getVirtualScale()), selectionBG, 1.f); m_font->Print(0, aty / getVirtualScale()[1], cur, botsel->column, getVirtualScale()); } if (toplen < linewidth) { // is there still text on this line after the selection? m_font->setColor(m_foreground_color); m_font->Print(toplen, aty / getVirtualScale()[1], &cur[botsel->column], -1, getVirtualScale()); } if (botsel == &m_selection.end) { // Place cursor at the end of the selection if it was started from above caret = toplen; } } } else if (i != botsel->line) { // part of this line is selected, and selection continues/began below int toplen = m_font->getWidth(cur, topsel->column); // X coord of highlighting start if (topsel->column) { // is there any text on this line before the selection? m_font->setColor(m_foreground_color); m_font->Print(0, aty / getVirtualScale()[1], cur, topsel->column, getVirtualScale()); } if (toplen < linewidth) { // is there any selected text before the end of the line? // print the selected text with the selection highlight box around it m_font->setColor(selectionColor); DrawBox(toplen * getVirtualScale()[0], aty, (linewidth - toplen) * getVirtualScale()[0], m_font->getHeight(getVirtualScale()), selectionBG, 1.f); m_font->Print(toplen, aty / getVirtualScale()[1], &cur[topsel->column], -1, getVirtualScale()); } if (topsel == &m_selection.end) { // Place cursor before the highlight box if selection was made from the bottom up caret = toplen; } } else { // current line contains the cursor if (topsel->column == botsel->column) { // no selection or highlighted text caret = m_font->getWidth(cur, topsel->column); m_font->setColor(m_foreground_color); // Fixed in OPM: // don't spam LOCALIZATION ERROR messages to console // for clicking lines in the opened text document //m_font->Print(0, aty, Sys_LV_CL_ConvertString(cur), -1, m_bVirtual); m_font->Print(0, aty / getVirtualScale()[1], cur, -1, getVirtualScale()); } else { // selection starts and ends on this line int toplen = m_font->getWidth(cur, topsel->column); // X coord of highlighting start int botlen = toplen + m_font->getWidth(&cur[topsel->column], botsel->column - topsel->column); // X coord of selection end if (toplen) { // is there any text on this line before the selection? m_font->setColor(m_foreground_color); m_font->Print(0, aty / getVirtualScale()[1], cur, topsel->column, getVirtualScale()); } if (botlen != toplen) { // is the selection wider than 0 pixels? (sanity check, always true) // print the selected text with the selection highlight box around it DrawBox(toplen * getVirtualScale()[0], aty, (botlen - toplen) * getVirtualScale()[0], m_font->getHeight(getVirtualScale()), selectionBG, 1.f); m_font->setColor(selectionColor); m_font->Print(toplen, aty / getVirtualScale()[1], &cur[topsel->column], botsel->column - topsel->column, getVirtualScale()); } if (cur.length() != botsel->column) { // is there still text on this line after the selection? // print the leftover text m_font->setColor(m_foreground_color); // Fixed in OPM: // highlighting text made the rest of the text after the selection on the line disappear. // Cause: the last two arguments were incorrectly passed in originally, // always specifying maxlen as m_bVirtual (which is usually zero). // m_font->Print(botlen, aty, &cur[botsel->column], m_bVirtual, false); m_font->Print(botlen, aty / getVirtualScale()[1], &cur[botsel->column], -1, getVirtualScale()); } // Place the cursor at the end of the selection... caret = botlen; if (topsel == &m_selection.end) { // ... except if selection was made from the bottom up caret = toplen; } } m_font->getWidth(cur, botsel->column); m_font->getWidth(cur, topsel->column); } } if (m_selection.end.line == i && (uid.time % 750) >= 375 && IsActive()) { // draw cursor caret DrawBox(caret * getVirtualScale()[0], aty, 2.f, m_font->getHeight(getVirtualScale()), UBlack, 1.f); } aty += m_font->getHeight(getVirtualScale()); } } // num is the ZERO-BASED index of the sought line! str& UIMultiLineEdit::LineFromLineNumber(int num, bool resetpos) { static str emptyLine; void *pos; int i; pos = m_lines.getPosition(); for (i = 0, m_lines.IterateFromHead(); i < num && m_lines.IsCurrentValid(); i++) { m_lines.IterateNext(); } if (m_lines.IsCurrentValid()) { str& ret = m_lines.getCurrent(); if (resetpos) { m_lines.setPosition(pos); } return ret; } if (resetpos) { m_lines.setPosition(pos); } return emptyLine; } void UIMultiLineEdit::PointToSelectionPoint(const UIPoint2D& p, selectionpoint_t& sel) { int clickedLine; int i; float totalWidth = 0; float lastWidth = 0; clickedLine = m_vertscroll->getTopItem() + p.y / m_font->getHeight(getVirtualScale()); clickedLine = Q_min(clickedLine, m_lines.getCount() - 1); if (clickedLine < 0) { sel.line = 0; sel.column = 0; return; } const char *line = LineFromLineNumber(clickedLine, true).c_str(); for (i = 0; line[i] && totalWidth < p.x; i++) { lastWidth = m_font->getCharWidth(line[i]) * getVirtualScale()[0]; totalWidth += lastWidth; } if (line[i] && i) { lastWidth *= 0.5f; if (totalWidth - lastWidth >= p.x) { i--; } } sel.line = clickedLine; sel.column = i; } void UIMultiLineEdit::MouseDown(Event *ev) { PointToSelectionPoint(MouseEventToClientPoint(ev), m_selection.begin); m_selection.end = m_selection.begin; EnsureSelectionPointVisible(m_selection.end); m_mouseState = M_DRAGGING; uWinMan.setFirstResponder(this); } void UIMultiLineEdit::MouseUp(Event *ev) { CancelEventsOfType(EV_Time_Dragged); if (uWinMan.getFirstResponder() == this) { uWinMan.setFirstResponder(NULL); } if (m_mouseState == M_DRAGGING) { EnsureSelectionPointVisible(m_selection.end); } m_mouseState = M_NONE; } void UIMultiLineEdit::MouseDragged(Event *ev) { m_dragState.lastPos = MouseEventToClientPoint(ev); PointToSelectionPoint(m_dragState.lastPos, m_selection.end); if (!EventPending(EV_Time_Dragged)) { PostEvent(new Event(EV_Time_Dragged), 0); } } void UIMultiLineEdit::DragTimer(Event *ev) { int oldtop; PointToSelectionPoint(m_dragState.lastPos, m_selection.end); oldtop = m_vertscroll->getTopItem(); EnsureSelectionPointVisible(m_selection.end); if (m_vertscroll->getTopItem() != oldtop) { PostEvent(new Event(EV_Time_Dragged), 0.25f); } } void UIMultiLineEdit::EnsureSelectionPointVisible(selectionpoint_t& point) { int newtop; if (point.line > m_vertscroll->getTopItem() && point.line < m_vertscroll->getPageHeight() + m_vertscroll->getTopItem()) { return; } if (m_vertscroll->getTopItem() > point.line) { newtop = point.line; } else { newtop = point.line - m_vertscroll->getPageHeight() + 1; } if (newtop < 0) { newtop = 0; } m_vertscroll->setTopItem(newtop); } void UIMultiLineEdit::BoundSelectionPoint(selectionpoint_t& point) { // since LineFromLineNumber expects a zero-based line index, // clamp it to one less than the number of lines if the selection point // is right at the end of the text document point.line = Q_clamp_int(point.line, 0, m_lines.getCount() - 1); str& line = LineFromLineNumber(point.line, true); point.column = Q_clamp_int(point.column, 0, line.length()); } qboolean UIMultiLineEdit::KeyEvent(int key, unsigned int time) { bool caret_moved = false; qboolean key_rec = false; switch (key) { case K_UPARROW: m_selection.end.line--; caret_moved = true; key_rec = true; break; case K_DOWNARROW: m_selection.end.line++; caret_moved = true; key_rec = true; break; case K_LEFTARROW: m_selection.end.column--; if (m_selection.end.column < 0) { m_selection.end.column = 999999; m_selection.end.line--; } if (m_selection.end.line < 0) { m_selection.end.column = 0; } caret_moved = true; key_rec = true; break; case K_RIGHTARROW: { int oldcol = m_selection.end.column; m_selection.end.column++; BoundSelectionPoint(m_selection.end); if (m_selection.end.column == oldcol) { m_selection.end.column = 0; m_selection.end.line++; if (m_selection.end.line >= m_lines.getCount()) { m_selection.end.line--; m_selection.end.column = 999999; } } caret_moved = true; key_rec = true; break; } case K_INS: if (!IsSelectionEmpty()) { if (uii.Sys_IsKeyDown(K_CTRL)) { CopySelection(); } else if (uii.Sys_IsKeyDown(K_SHIFT)) { PasteSelection(); } } break; case K_DEL: // Fixed in OPM: // DEL key needed to be pressed twice to delete a single character, // as the first hit would only create an "internal" selection, // but wouldn't actually delete the character after the cursor. // The DeleteSelection() method should ALWAYS be called, // not only when there's an active selection. if (IsSelectionEmpty()) { m_shiftForcedDown = true; KeyEvent(K_RIGHTARROW, 0); m_shiftForcedDown = false; } else if (uii.Sys_IsKeyDown(K_SHIFT)) { CopySelection(); } DeleteSelection(); break; case K_PGDN: if (m_selection.end.line == (m_vertscroll->getTopItem() + m_vertscroll->getPageHeight() - 1)) { m_selection.end.line = m_vertscroll->getPageHeight() + m_selection.end.line; } else { m_selection.end.line = m_vertscroll->getTopItem() + m_vertscroll->getPageHeight() - 1; } caret_moved = true; key_rec = true; break; case K_PGUP: if (m_selection.end.line == m_vertscroll->getTopItem()) { m_selection.end.line -= m_vertscroll->getPageHeight(); } else { m_selection.end.line = m_vertscroll->getTopItem(); } caret_moved = true; key_rec = true; break; case K_HOME: if (uii.Sys_IsKeyDown(K_CTRL)) { m_selection.end.column = 0; m_selection.end.line = 0; } else { m_selection.end.column = 0; } caret_moved = true; key_rec = true; break; case K_END: if (uii.Sys_IsKeyDown(K_CTRL)) { m_selection.end.column = 999999; m_selection.end.line = m_lines.getCount(); } else { m_selection.end.column = 999999; } caret_moved = true; key_rec = true; break; case K_MWHEELDOWN: m_vertscroll->AttemptScrollTo(m_vertscroll->getTopItem() + 2); key_rec = true; break; case K_MWHEELUP: m_vertscroll->AttemptScrollTo(m_vertscroll->getTopItem() - 2); key_rec = true; break; } if (caret_moved) { BoundSelectionPoint(m_selection.end); if (!uii.Sys_IsKeyDown(K_SHIFT) && !m_shiftForcedDown) { m_selection.begin.line = m_selection.end.line; m_selection.begin.column = m_selection.end.column; } EnsureSelectionPointVisible(m_selection.end); } return key_rec; } void UIMultiLineEdit::DeleteSelection(void) { selectionpoint_t *topsel; selectionpoint_t *botsel; int i; if (IsSelectionEmpty()) { return; } m_changed = true; SortSelection(&topsel, &botsel); if (topsel->line == botsel->line) { str& line = LineFromLineNumber(topsel->line, true); intptr_t newlen = line.length() - (botsel->column - topsel->column); for (i = topsel->column; i < newlen; i++) { line[i] = line[i + botsel->column - topsel->column]; } line.CapLength(newlen); *botsel = *topsel; EnsureSelectionPointVisible(*topsel); return; } else if (botsel->line - topsel->line > 1) { for (i = 0, m_lines.IterateFromHead(); m_lines.IsCurrentValid() && i < botsel->line; i++, m_lines.IterateNext()) { if (i > topsel->line) { m_lines.RemoveCurrentSetPrev(); i--; botsel->line--; } } } for (i = 0, m_lines.IterateFromHead(); m_lines.IsCurrentValid() && i < topsel->line; i++, m_lines.IterateNext()) {} // delete topmost line of the selection, but only up to topsel->column str& topline = m_lines.getCurrent(); if (topline.length() > topsel->column) { topline.CapLength(topsel->column); } m_lines.IterateNext(); str line = m_lines.getCurrent(); // merge remainder of topmost line with the remainder after the end of selection if (line.length() > botsel->column) { topline += &line[botsel->column]; } m_lines.RemoveCurrentSetPrev(); *botsel = *topsel; m_vertscroll->setNumItems(m_lines.getCount()); EnsureSelectionPointVisible(*topsel); } void UIMultiLineEdit::CharEvent(int ch) { intptr_t i; if (!m_edit) { return; } if (ch >= ' ' || ch == '\t') { DeleteSelection(); m_changed = true; str& line = LineFromLineNumber(m_selection.begin.line, true); line += " "; for (i = line.length() - 1; i > m_selection.begin.column; i--) { line[i] = line[i - 1]; } line[m_selection.begin.column] = ch; m_selection.begin.column++; m_selection.end.column++; } else if (ch == '\b') { if (IsSelectionEmpty()) { m_shiftForcedDown = true; KeyEvent(K_LEFTARROW, 0); m_shiftForcedDown = false; } DeleteSelection(); } else if (ch == '\r') { DeleteSelection(); m_changed = true; str& line = LineFromLineNumber(m_selection.begin.line, false); str otherline = ""; if (line.length() > m_selection.begin.column) { otherline = &line[m_selection.begin.column]; line.CapLength(m_selection.begin.column); } if (m_lines.IsCurrentValid()) { m_lines.InsertAfterCurrent(otherline); } else { m_lines.AddTail(otherline); } m_selection.begin.column = 0; m_selection.begin.line++; m_selection.end.column = m_selection.begin.column; m_selection.end.line = m_selection.begin.line; m_vertscroll->setNumItems(m_lines.getCount()); EnsureSelectionPointVisible(m_selection.end); } } bool UIMultiLineEdit::IsSelectionEmpty(void) { return m_selection.begin.column == m_selection.end.column && m_selection.begin.line == m_selection.end.line; } void UIMultiLineEdit::CopySelection(void) { selectionpoint_t *topsel; selectionpoint_t *botsel; str line; str clipText; if (IsSelectionEmpty()) { return; } SortSelection(&topsel, &botsel); line = LineFromLineNumber(topsel->line, true); if (line.length() > topsel->column) { clipText += &line[topsel->column]; } if (topsel->line == botsel->line) { clipText.CapLength(botsel->column - topsel->column); } else { for (int i = topsel->line + 1; i < botsel->line; ++i) { clipText += "\n" + LineFromLineNumber(i, 1); } line = LineFromLineNumber(botsel->line, true); if (line.length() > botsel->column) { line.CapLength(botsel->column); } clipText += "\n" + line; } uii.Sys_SetClipboard(clipText); } void UIMultiLineEdit::PasteSelection(void) { str sel; int i; // variable added in OPM as str cannot handle NULL assignment // we can get NULL here if clipboard is empty/couldn't be retrieved const char *clipboardData = uii.Sys_GetClipboard(); if (clipboardData == NULL) { return; } sel = clipboardData; DeleteSelection(); for (i = 0; i < sel.length(); i++) { if (sel[i] == '\n') { CharEvent('\r'); } else if (sel[i] == '\r') { // Changed in OPM: // NOP, drop CR characters. // On Linux/Mac they aren't present anyway, // on Windows we already have LF chars next to them. // The filtering for CR on the Windows side was originally done // in Sys_GetWholeClipboard with a "manual" selective strcpy, // but here we iterate over all characters of the clipboard anyways, // so this feels like a better place to do the filtering. } else { CharEvent(sel[i]); // FIXME: this is VERY slow and jams up the EventQueue! } } } UIPoint2D UIMultiLineEdit::getEndSelPoint(void) { return UIPoint2D(m_selection.end.column, m_selection.end.line); } void UIMultiLineEdit::setChanged(bool b) { m_changed = b; } bool UIMultiLineEdit::IsChanged(void) { return m_changed; } void UIMultiLineEdit::Scroll(Event *ev) {}