openmohaa/code/uilib/uifield.cpp
pryon 110ffc3a01
Implement UIField (#90)
* Implement UIField

* Formatting and naming fixes

- Ran clang-format on previously modified files
- Replaced ifdef guards with `#pragma once`
- Changed wording in comments above OPM-only changes for easier searchability
2024-01-04 23:51:16 +01:00

326 lines
9.9 KiB
C++

/*
===========================================================================
Copyright (C) 2015-2023 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"
CLASS_DECLARATION(UIWidget, UIField, NULL) {
{&W_LeftMouseDown, &UIField::Pressed},
{NULL, NULL }
};
UIField::UIField()
{
AllowActivate(qtrue);
m_iPreStep = 0;
}
void UIField::Draw(void)
{
UpdateData();
// the width of the widget that can be filled with valid text
float fVirtualWidth = m_frame.size.width / m_vVirtualScale[0] - m_font->getWidth(" ..", -1);
float indentLength = m_indent;
float cursorSize = m_font->getCharWidth('_');
// The total pixel length of the entire string in the editfield buffer
float totalDrawLength = m_font->getWidth(m_edit.m_buffer, -1) + indentLength;
// The index of the character that has to be shown first
// when viewing the end of the string to properly fill out the field
int iMaxPreStep;
float fEndLen = totalDrawLength + cursorSize;
int buflen = strlen(m_edit.m_buffer);
// Calculate the max prestep
for (iMaxPreStep = 0; fEndLen > fVirtualWidth && iMaxPreStep < buflen; iMaxPreStep++) {
float fCharWidth = m_font->getCharWidth(m_edit.m_buffer[iMaxPreStep]);
fEndLen -= fCharWidth;
} // fEndLen is discarded
// Index of the last currently visible character in the textfield
int iLastChar;
fEndLen = indentLength + cursorSize;
for (iLastChar = m_iPreStep; fEndLen < fVirtualWidth; iLastChar++) {
if (char c = m_edit.m_buffer[iLastChar]) {
float fCharWidth = m_font->getCharWidth(c);
fEndLen += fCharWidth;
} else {
// important to quit early!
// otherwise iLastChar would get incremented one extra time
break;
}
}
if (IsActive()) {
if (m_edit.m_cursor >= m_iPreStep) {
// Fixed in OPM
// m_iPreStep < iLastChar check added to avoid passing negative size to Q_strncpyz and crashing the game
while (iLastChar <= m_edit.m_cursor && m_iPreStep < iMaxPreStep && m_iPreStep < iLastChar
) { // is text too long to fit?
fEndLen -= m_font->getCharWidth(m_edit.m_buffer[m_iPreStep]);
m_iPreStep++;
// FIXME: repetitive logic
if (char c = m_edit.m_buffer[iLastChar]) {
float fCharWidth = m_font->getCharWidth(c);
fEndLen += fCharWidth;
} else {
break;
}
while (fEndLen < fVirtualWidth) {
iLastChar++;
if (char c = m_edit.m_buffer[iLastChar]) {
float fCharWidth = m_font->getCharWidth(c);
fEndLen += fCharWidth;
} else {
break;
}
}
}
} else {
m_iPreStep = m_edit.m_cursor;
}
m_iPreStep = Q_min(m_iPreStep, iMaxPreStep);
}
// Put the portion of the text intended for display into the temptext buffer
char temptext[EDITFIELD_BUFSIZE];
Q_strncpyz(temptext, &m_edit.m_buffer[m_iPreStep], iLastChar - m_iPreStep + 1);
// Calculate cursor position
float cursorPos = indentLength - 1.0f;
// Added in OPM
// new i < EDITFIELD_BUFSIZE check for extra safety
for (int i = m_iPreStep; i < m_edit.m_cursor && i < EDITFIELD_BUFSIZE; i++) {
float fCharWidth = m_font->getCharWidth(m_edit.m_buffer[i]);
cursorPos += fCharWidth;
}
cursorPos = Q_min(cursorPos, fVirtualWidth);
// Calculate text height
float y = m_frame.size.height / m_vVirtualScale[1] - m_font->getHeight(qfalse);
if (m_bottomindent * 2 <= y) // upper and lower margin
{
y -= m_bottomindent;
} else {
y /= 2;
}
// Show text
m_font->setColor(m_foreground_color);
m_font->Print(m_indent, y, temptext, -1, m_bVirtual);
// Display blinking cursor if field is in focus
if (IsActive()) {
const char *cursorChar = (uid.time / 250) & 1 ? "|" : "_";
m_font->Print(cursorPos, y, cursorChar, -1, m_bVirtual);
}
Q_strncpyz(temptext, "..", EDITFIELD_BUFSIZE);
if (m_iPreStep) {
// Display leading dots if not starting from the beginning
m_font->Print(2.0f, y, temptext, -1, m_bVirtual);
}
// Added in OPM
// new iLastChar < EDITFIELD_BUFSIZE check for extra safety
if (iLastChar < EDITFIELD_BUFSIZE && m_edit.m_buffer[iLastChar]) {
// Display trailing dots if not ending at the end
fVirtualWidth = m_frame.size.width / m_vVirtualScale[0] - m_font->getWidth(temptext, -1) - 2.0f;
m_font->Print(fVirtualWidth, y, temptext, -1, m_bVirtual);
}
}
qboolean UIField::KeyEvent(int key, unsigned int time)
{
if (key == 'l' && uii.Sys_IsKeyDown(K_CTRL)) {
return qtrue;
}
if (key == K_ENTER || key == K_KP_ENTER) {
if (m_cvarname.length()) {
uii.Cvar_Set(m_cvarname, m_edit.m_buffer);
}
if (m_commandhandler) {
m_commandhandler(str(m_edit.m_buffer) + '\n', NULL);
}
if (m_command.length()) {
str result = m_command + ' ' + m_edit.m_buffer + '\n';
Cbuf_AddText(result);
}
return qtrue;
}
if ((key == K_INS || key == K_KP_INS) && uii.Sys_IsKeyDown(K_SHIFT)) {
return qtrue;
}
int len = strlen(m_edit.m_buffer);
switch (key) {
case K_DEL: // DELETE key, backspace is handled in CharEvent
if (m_edit.m_cursor < len) {
memmove(&m_edit.m_buffer[m_edit.m_cursor], &m_edit.m_buffer[m_edit.m_cursor + 1], len - m_edit.m_cursor);
if (m_cvarname.length()) {
uii.Cvar_Set(m_cvarname, m_edit.m_buffer);
}
}
return qtrue;
case K_RIGHTARROW:
if (m_edit.m_cursor < len) {
m_edit.m_cursor++;
}
return qtrue;
case K_LEFTARROW:
if (m_edit.m_cursor > 0) {
m_edit.m_cursor--;
}
return qtrue;
}
// HOME or Ctrl + A
if (key == K_HOME || (tolower(key) == 'a' && uii.Sys_IsKeyDown(K_CTRL))) {
m_edit.m_cursor = 0;
return qtrue;
}
// END or Ctrl + E
if (key == K_END || (tolower(key) == 'e' && uii.Sys_IsKeyDown(K_CTRL))) {
m_edit.m_cursor = len;
return qtrue;
}
return key == K_INS;
}
void UIField::CharEvent(int ch)
{
uii.Snd_PlaySound("sound/menu/typekey.wav");
if (ch == 3 || ch == 22) // Ctrl + C (^C) or Ctrl + V (^V)
{
return;
}
int len = strlen(m_edit.m_buffer);
switch (ch) {
case '\b': // backspace or Ctrl + H (^H) - DEL is handled in KeyEvent
if (m_edit.m_cursor <= 0) {
// already at beginning of field
return;
}
// Move all text after the cursor one position back
memmove(&m_edit.m_buffer[m_edit.m_cursor - 1], &m_edit.m_buffer[m_edit.m_cursor], len + 1 - m_edit.m_cursor);
// move back cursor by one
m_edit.m_cursor--;
if (m_cvarname.length()) {
uii.Cvar_Set(m_cvarname, m_edit.m_buffer);
}
return;
case 1: // Home (^A)
m_edit.m_cursor = 0;
return;
case 5: // End (^E)
m_edit.m_cursor = len;
return;
}
// original check only does ch < 32,
// this one excludes DEL too but it shouldn't be a problem,
// DEL is handled in KeyEvent
if (iscntrl(ch)) {
return;
}
len++; // new printable character was typed
// Fixed in OPM
// originally this was len == EDITFIELD_BUFSIZE, just being extra safe
if (len >= EDITFIELD_BUFSIZE) // is it out of bounds?
{
return;
}
// make room for the character in the buffer
memmove(&m_edit.m_buffer[m_edit.m_cursor + 1], &m_edit.m_buffer[m_edit.m_cursor], len - m_edit.m_cursor);
// insert the new character at the cursor's location
m_edit.m_buffer[m_edit.m_cursor] = ch;
// advance the cursor
m_edit.m_cursor++;
// if the character was inserted at the end of the string,
// make sure it remains NULL-terminated
if (m_edit.m_cursor == len) {
m_edit.m_buffer[m_edit.m_cursor] = NULL;
}
if (m_cvarname.length()) {
uii.Cvar_Set(m_cvarname, m_edit.m_buffer);
}
}
void UIField::UpdateData(void)
{
if (m_cvarname.length()) {
str::snprintf(m_edit.m_buffer, EDITFIELD_BUFSIZE, "%s", CvarGetForUI(m_cvarname, ""));
m_edit.m_cursor = Q_min(m_edit.m_cursor, strlen(m_edit.m_buffer));
}
}
void UIField::Pressed(Event *ev)
{
float xpos = ev->GetFloat(1);
float point = (xpos - m_frame.pos.x) / m_vVirtualScale[0];
point = Q_max(point, 0.0f); // added in 2.15
int iStep = m_iPreStep;
float fStringWidth = m_indent;
while (char c = m_edit.m_buffer[iStep]) {
iStep++;
fStringWidth += m_font->getCharWidth(c);
if (point <= fStringWidth) {
if (iStep > 0) {
iStep--; // put the cursor BEFORE the clicked character
}
break;
}
}
m_edit.m_cursor = iStep;
}