scummvm/gui/widgets/editable.cpp
PushmePullyu 4867e14d60 GUI: EditableWidget: Clear selection when handling backspace/delete
Clear the selection after deleting a character.

Not doing so can result in out-of-bounds reads in
EditTextWidget::drawWidget(), where _selCaretPos and _selOffset are
used as offsets, and to a failed assertion when calling
EditableWidget::defaultKeyDownHandler():

gui/widgets/editable.cpp:566:
bool GUI::EditableWidget::setCaretPos(int):
Assertion `newPos >= 0 && newPos <= (int)_editString.size()' failed.

Fixes #14584
2023-08-22 13:23:19 +03:00

619 lines
17 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/rect.h"
#include "common/system.h"
#include "common/unicode-bidi.h"
#include "gui/widgets/editable.h"
#include "gui/gui-manager.h"
#include "graphics/font.h"
namespace GUI {
EditableWidget::EditableWidget(GuiObject *boss, int x, int y, int w, int h, bool scale, const Common::U32String &tooltip, uint32 cmd)
: Widget(boss, x, y, w, h, scale, tooltip), CommandSender(boss), _cmd(cmd) {
setFlags(WIDGET_TRACK_MOUSE);
init();
}
EditableWidget::EditableWidget(GuiObject *boss, int x, int y, int w, int h, const Common::U32String &tooltip, uint32 cmd)
: EditableWidget(boss, x, y, w, h, false, tooltip, cmd) {
}
EditableWidget::EditableWidget(GuiObject *boss, const Common::String &name, const Common::U32String &tooltip, uint32 cmd)
: Widget(boss, name, tooltip), CommandSender(boss), _cmd(cmd) {
setFlags(WIDGET_TRACK_MOUSE);
init();
}
void EditableWidget::init() {
_caretVisible = false;
_caretTime = 0;
_caretPos = 0;
_caretInverse = false;
_editScrollOffset = 0;
_selCaretPos = -1;
_selOffset = 0;
_shiftPressed = _isDragging = false;
_disableSelection = g_gui.useRTL();
_align = g_gui.useRTL() ? Graphics::kTextAlignRight : Graphics::kTextAlignLeft;
_drawAlign = _align;
_font = ThemeEngine::kFontStyleBold;
_inversion = ThemeEngine::kTextInversionNone;
}
EditableWidget::~EditableWidget() {
}
void EditableWidget::reflowLayout() {
Widget::reflowLayout();
_editScrollOffset = g_gui.getStringWidth(_editString, _font) - getEditRect().width();
if (_editScrollOffset < 0) {
_editScrollOffset = 0;
_drawAlign = _align;
} else {
_drawAlign = Graphics::kTextAlignLeft;
}
}
void EditableWidget::setEditString(const Common::U32String &str) {
// TODO: We probably should filter the input string here,
// e.g. using tryInsertChar.
_editString = str;
clearSelection();
setCaretPos(caretVisualPos(str.size()));
markAsDirty();
}
bool EditableWidget::isCharAllowed(Common::u32char_type_t c) const {
return (c >= 32 && c <= 127) || c >= 160;
}
bool EditableWidget::tryInsertChar(Common::u32char_type_t c, int pos) {
if (!isCharAllowed(c))
return false;
_editString.insertChar(c, pos);
return true;
}
int EditableWidget::caretVisualPos(int logicalPos) {
return Common::convertBiDiU32String(_editString + " ").getVisualPosition(logicalPos);
}
int EditableWidget::caretLogicalPos() const {
return Common::convertBiDiU32String(_editString + " ").getLogicalPosition(_caretPos);
}
void EditableWidget::handleTickle() {
uint32 time = g_system->getMillis();
if (_caretTime < time && isEnabled()) {
_caretTime = time + kCaretBlinkTime;
drawCaret(_caretVisible);
}
}
void EditableWidget::handleMouseDown(int x, int y, int button, int clickCount) {
if (!isEnabled())
return;
_isDragging = true;
// Select all text incase of double press
if (clickCount > 1) {
_selCaretPos = 0;
setCaretPos(caretVisualPos(_editString.size()));
setSelectionOffset(_editString.size() - _selCaretPos);
markAsDirty();
return;
}
// Clear any selection
if (_selOffset != 0 && !_shiftPressed)
clearSelection();
else if (_shiftPressed && _selCaretPos < 0 && !_disableSelection)
_selCaretPos = _caretPos;
if (g_gui.useRTL()) {
x = _w - x;
}
x += _editScrollOffset;
int width = 0;
if (_drawAlign == Graphics::kTextAlignRight)
width = _editScrollOffset + getEditRect().width() - g_gui.getStringWidth(_editString, _font);
uint i, last = 0;
for (i = 0; i < _editString.size(); ++i) {
const uint cur = _editString[i];
width += g_gui.getCharWidth(cur, _font) + g_gui.getKerningOffset(last, cur, _font);
if (width >= x && width > _editScrollOffset)
break;
last = cur;
}
setCaretPos(i);
if (_selCaretPos >= 0 && !_disableSelection)
setSelectionOffset(i - _selCaretPos);
markAsDirty();
}
void EditableWidget::handleMouseUp(int x, int y, int button, int clickCount) {
if(isEnabled())
_isDragging = false;
}
void EditableWidget::handleMouseMoved(int x, int y, int button) {
if (_isDragging && isEnabled() && !_disableSelection) {
if (_selCaretPos < 0)
_selCaretPos = _caretPos;
if (g_gui.useRTL()) {
x = _w - x;
}
if (x < 0 && _editScrollOffset > 0) {
_editScrollOffset += x;
if(_editScrollOffset < 0)
_editScrollOffset = 0;
}
x += _editScrollOffset;
int width = 0;
if (_drawAlign == Graphics::kTextAlignRight)
width = _editScrollOffset + getEditRect().width() - g_gui.getStringWidth(_editString, _font);
uint i, last = 0;
for (i = 0; i < _editString.size(); ++i) {
const uint cur = _editString[i];
width += g_gui.getCharWidth(cur, _font) + g_gui.getKerningOffset(last, cur, _font);
if (width >= x && width > _editScrollOffset)
break;
last = cur;
}
setCaretPos(i);
if(_selCaretPos >= 0)
setSelectionOffset(i - _selCaretPos);
markAsDirty();
}
}
bool EditableWidget::handleKeyUp(Common::KeyState state) {
_shiftPressed = state.hasFlags(Common::KBD_SHIFT);
return false;
}
bool EditableWidget::handleKeyDown(Common::KeyState state) {
bool handled = true;
bool dirty = false;
bool forcecaret = false;
int deleteIndex;
if (!isEnabled())
return false;
// First remove caret
if (_caretVisible)
drawCaret(true);
_shiftPressed = state.hasFlags(Common::KBD_SHIFT);
// Remap numeric keypad if NUM lock is *not* active.
// This code relies on the fact that the various KEYCODE_KP* values are
// consecutive.
if (0 == (state.flags & Common::KBD_NUM)
&& Common::KEYCODE_KP0 <= state.keycode
&& state.keycode <= Common::KEYCODE_KP_PERIOD) {
const Common::KeyCode remap[11] = {
Common::KEYCODE_INSERT, // KEYCODE_KP0
Common::KEYCODE_END, // KEYCODE_KP1
Common::KEYCODE_DOWN, // KEYCODE_KP2
Common::KEYCODE_PAGEDOWN, // KEYCODE_KP3
Common::KEYCODE_LEFT, // KEYCODE_KP4
Common::KEYCODE_INVALID, // KEYCODE_KP5
Common::KEYCODE_RIGHT, // KEYCODE_KP6
Common::KEYCODE_HOME, // KEYCODE_KP7
Common::KEYCODE_UP, // KEYCODE_KP8
Common::KEYCODE_PAGEUP, // KEYCODE_KP9
Common::KEYCODE_DELETE, // KEYCODE_KP_PERIOD
};
state.keycode = remap[state.keycode - Common::KEYCODE_KP0];
}
switch (state.keycode) {
case Common::KEYCODE_RETURN:
case Common::KEYCODE_KP_ENTER:
// confirm edit and exit editmode
endEditMode();
dirty = true;
break;
case Common::KEYCODE_ESCAPE:
abortEditMode();
dirty = true;
break;
case Common::KEYCODE_BACKSPACE:
deleteIndex = caretLogicalPos();
if (deleteIndex > 0 && _selOffset == 0) {
deleteIndex--;
_editString.deleteChar(deleteIndex);
setCaretPos(caretVisualPos(deleteIndex));
_selCaretPos = -1;
dirty = true;
sendCommand(_cmd, 0);
} else if (deleteIndex >= 0 && _selOffset != 0) {
int selBegin = _selCaretPos;
int selEnd = _selCaretPos + _selOffset;
if (selBegin > selEnd)
SWAP(selBegin, selEnd);
_editString.erase(selBegin, selEnd - selBegin);
setCaretPos(caretVisualPos(selBegin));
_selCaretPos = -1;
_selOffset = 0;
dirty = true;
sendCommand(_cmd, 0);
}
forcecaret = true;
break;
case Common::KEYCODE_DELETE:
deleteIndex = caretLogicalPos();
if (deleteIndex < (int)_editString.size()) {
_editString.deleteChar(deleteIndex);
setCaretPos(caretVisualPos(deleteIndex));
_selCaretPos = -1;
_selOffset = 0;
dirty = true;
sendCommand(_cmd, 0);
}
forcecaret = true;
break;
case Common::KEYCODE_DOWN:
case Common::KEYCODE_END:
// Move caret to end
setCaretPos(caretVisualPos(_editString.size()));
if (state.hasFlags(Common::KBD_SHIFT))
setSelectionOffset(_editString.size() - _selCaretPos);
else
clearSelection();
forcecaret = true;
dirty = true;
break;
case Common::KEYCODE_LEFT:
if (state.hasFlags(Common::KBD_SHIFT)) {
if (_disableSelection)
break;
if (_selCaretPos < 0)
_selCaretPos = _caretPos;
if (_caretPos > 0)
_selOffset--;
} else {
clearSelection();
}
// Move caret one left (if possible)
if (_caretPos > 0) {
dirty = setCaretPos(_caretPos - 1);
}
forcecaret = true;
dirty = true;
break;
case Common::KEYCODE_RIGHT:
if (state.hasFlags(Common::KBD_SHIFT)) {
if (_disableSelection)
break;
if (_selCaretPos < 0)
_selCaretPos = _caretPos;
if (_selOffset + _selCaretPos < (int)_editString.size())
_selOffset++;
} else {
clearSelection();
}
// Move caret one right (if possible)
if (_caretPos < (int)_editString.size()) {
dirty = setCaretPos(_caretPos + 1);
}
forcecaret = true;
dirty = true;
break;
case Common::KEYCODE_UP:
case Common::KEYCODE_HOME:
// Move caret to start
setCaretPos(caretVisualPos(0));
if (state.hasFlags(Common::KBD_SHIFT))
setSelectionOffset(0 - _selCaretPos);
else
clearSelection();
forcecaret = true;
dirty = true;
break;
case Common::KEYCODE_v:
if (state.flags & Common::KBD_CTRL) {
if (g_system->hasTextInClipboard()) {
Common::U32String text = g_system->getTextFromClipboard();
if (_selOffset != 0) {
int selBegin = _selCaretPos;
int selEnd = _selCaretPos + _selOffset;
if (selBegin > selEnd)
SWAP(selBegin, selEnd);
_editString.replace(selBegin, selEnd - selBegin, text);
setCaretPos(caretVisualPos(selBegin));
const int logicalPosition = caretLogicalPos();
setCaretPos(caretVisualPos(logicalPosition + text.size()));
clearSelection();
} else {
for (uint32 i = 0; i < text.size(); ++i) {
const int logicalPosition = caretLogicalPos();
if (tryInsertChar(text[i], logicalPosition))
setCaretPos(caretVisualPos(logicalPosition + 1));
}
}
dirty = true;
}
} else {
defaultKeyDownHandler(state, dirty, forcecaret, handled);
}
break;
case Common::KEYCODE_c:
if (state.flags & Common::KBD_CTRL) {
if (!getEditString().empty()) {
int selBegin = _selCaretPos;
int selEnd = _selCaretPos + _selOffset;
if (selBegin > selEnd)
SWAP(selBegin, selEnd);
const Common::U32String selected(getEditString().begin() + selBegin, getEditString().begin() + selEnd);
g_system->setTextInClipboard(selected);
}
} else {
defaultKeyDownHandler(state, dirty, forcecaret, handled);
}
break;
#ifdef MACOSX
// Let ctrl-a / ctrl-e move the caret to the start / end of the line.
//
// These shortcuts go back a long time for command line programs. As
// for edit fields in GUIs, they are supported natively on macOS,
// which is why I enabled these shortcuts there.
// On other systems (Windows, Gnome), Ctrl-A by default means
// "select all", which is why I didn't enable the shortcuts there
// for now, to avoid potential confusion.
//
// But since we don't support text selection, and since at least Gnome
// can be configured to also support ctrl-a and ctrl-e, we may want
// to extend this code to other targets, maybe even all. I'll leave
// this to other porters to decide, though.
case Common::KEYCODE_a:
case Common::KEYCODE_e:
if (state.flags & Common::KBD_CTRL) {
if (state.keycode == Common::KEYCODE_a) {
// Move caret to start
dirty = setCaretPos(0);
forcecaret = true;
} else if (state.keycode == Common::KEYCODE_e) {
// Move caret to end
dirty = setCaretPos(_editString.size());
forcecaret = true;
}
clearSelection();
break;
} else {
defaultKeyDownHandler(state, dirty, forcecaret, handled);
}
break;
#endif
default:
defaultKeyDownHandler(state, dirty, forcecaret, handled);
}
if (dirty)
markAsDirty();
if (forcecaret)
makeCaretVisible();
return handled;
}
void EditableWidget::defaultKeyDownHandler(Common::KeyState &state, bool &dirty, bool &forcecaret, bool &handled) {
if (isCharAllowed(state.ascii)) {
// Incase of a selection, replace the selection with the character
if (_selCaretPos >= 0) {
int selBegin = _selCaretPos;
int selEnd = _selCaretPos + _selOffset;
if (selBegin > selEnd)
SWAP(selBegin, selEnd);
_editString.replace(selBegin, selEnd - selBegin, Common::U32String(state.ascii));
if(_editString.size() > 0)
selBegin++;
setCaretPos(caretVisualPos(selBegin));
_selCaretPos = -1;
_selOffset = 0;
} else {
// Insert char normally at caretPos
const int logicalPosition = caretLogicalPos();
_editString.insertChar(state.ascii, logicalPosition);
setCaretPos(caretVisualPos(logicalPosition + 1));
}
dirty = true;
forcecaret = true;
sendCommand(_cmd, 0);
} else {
handled = false;
}
}
int EditableWidget::getCaretOffset() const {
Common::UnicodeBiDiText utxt(_editString);
Common::U32String substr(utxt.visual.begin(), utxt.visual.begin() + _caretPos);
return g_gui.getStringWidth(substr, _font) - _editScrollOffset;
}
int EditableWidget::getSelectionCarretOffset() const {
Common::UnicodeBiDiText utxt(_editString);
Common::U32String substr(utxt.visual.begin(), utxt.visual.begin() + _selCaretPos);
return g_gui.getStringWidth(substr, _font) - _editScrollOffset;
}
void EditableWidget::drawCaret(bool erase) {
// Only draw if item is visible
if (!isVisible() || !_boss->isVisible())
return;
Common::Rect editRect = getEditRect();
int x = editRect.left;
int y = editRect.top;
if (_align == Graphics::kTextAlignRight) {
int strVisibleWidth = g_gui.getStringWidth(_editString, _font) - _editScrollOffset;
if (strVisibleWidth > editRect.width()) {
_drawAlign = Graphics::kTextAlignLeft;
strVisibleWidth = editRect.width();
} else {
_drawAlign = _align;
}
x = editRect.right - strVisibleWidth;
}
const int caretOffset = getCaretOffset();
x += caretOffset;
if (y < 0 || y + editRect.height() > _h)
return;
if (g_gui.useRTL())
x += g_system->getOverlayWidth() - _w - getAbsX() + g_gui.getOverlayOffset();
else
x += getAbsX();
y += getAbsY();
g_gui.theme()->drawCaret(Common::Rect(x, y, x + 1, y + editRect.height()), erase);
if (erase) {
Common::U32String character;
int width;
if ((uint)_caretPos < _editString.size()) {
Common::UnicodeBiDiText utxt(_editString);
const Common::u32char_type_t chr = utxt.visual[_caretPos];
width = g_gui.getCharWidth(chr, _font);
character = Common::U32String(chr);
const uint32 last = (_caretPos > 0) ? utxt.visual[_caretPos - 1] : 0;
x += g_gui.getKerningOffset(last, chr, _font);
} else {
// We draw a fake space here to assure that removing the caret
// does not result in color glitches in case the edit rect is
// drawn with an inversion.
width = g_gui.getCharWidth(' ', _font);
character = " ";
}
// TODO: Right now we manually prevent text from being drawn outside
// the edit area here. We might want to consider to use
// setTextDrawableArea for this. However, it seems that only
// EditTextWidget uses that but not ListWidget. Thus, one should check
// whether we can unify the drawing in the text area first to avoid
// possible glitches due to different methods used.
ThemeEngine::TextInversionState inversion = _inversion;
if (!_disableSelection)
inversion = (_selOffset < 0) ? ThemeEngine::kTextInversionFocus : ThemeEngine::kTextInversionNone;
width = MIN(editRect.width() - caretOffset, width);
if (width > 0) {
g_gui.theme()->drawText(Common::Rect(x, y, x + width, y + editRect.height()), character,
_state, _drawAlign, inversion, 0, false, _font,
ThemeEngine::kFontColorNormal, true, _textDrawableArea);
}
}
_caretVisible = !erase;
}
bool EditableWidget::setCaretPos(int newPos) {
assert(newPos >= 0 && newPos <= (int)_editString.size());
_caretPos = newPos;
return adjustOffset();
}
bool EditableWidget::adjustOffset() {
// check if the caret is still within the textbox; if it isn't,
// adjust _editScrollOffset
int caretpos = getCaretOffset();
const int editWidth = getEditRect().width();
if (caretpos < 0) {
// scroll left
_editScrollOffset += caretpos;
return true;
} else if (caretpos >= editWidth) {
// scroll right
_editScrollOffset -= (editWidth - caretpos);
return true;
} else if (_editScrollOffset > 0) {
const int strWidth = g_gui.getStringWidth(_editString, _font);
if (strWidth - _editScrollOffset < editWidth) {
// scroll right
_editScrollOffset = (strWidth - editWidth);
if (_editScrollOffset < 0)
_editScrollOffset = 0;
}
}
return false;
}
void EditableWidget::makeCaretVisible() {
_caretTime = g_system->getMillis() + kCaretBlinkTime;
_caretVisible = true;
drawCaret(false);
}
void EditableWidget::clearSelection() {
_selCaretPos = -1;
_selOffset = 0;
markAsDirty();
}
void EditableWidget::setSelectionOffset(int newOffset) {
_selOffset = newOffset;
}
} // End of namespace GUI