Initial commit
This commit is contained in:
674
gui/widgets/editable.cpp
Normal file
674
gui/widgets/editable.cpp
Normal file
@@ -0,0 +1,674 @@
|
||||
/* 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::drawWidget() {
|
||||
if (_caretVisible) {
|
||||
drawCaret(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
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) const {
|
||||
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:
|
||||
moveCaretToEnd(state.hasFlags(Common::KBD_SHIFT));
|
||||
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:
|
||||
moveCaretToStart(state.hasFlags(Common::KBD_SHIFT));
|
||||
forcecaret = true;
|
||||
dirty = true;
|
||||
break;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
void EditableWidget::moveCaretToStart(bool shiftPressed) {
|
||||
if (shiftPressed) {
|
||||
if (_selCaretPos < 0)
|
||||
_selCaretPos = _caretPos;
|
||||
|
||||
setSelectionOffset(0 - _selCaretPos);
|
||||
} else {
|
||||
clearSelection();
|
||||
}
|
||||
// Move caret to start
|
||||
setCaretPos(caretVisualPos(0));
|
||||
}
|
||||
|
||||
void EditableWidget::moveCaretToEnd(bool shiftPressed) {
|
||||
if (_shiftPressed) {
|
||||
if (_selCaretPos < 0)
|
||||
_selCaretPos = _caretPos;
|
||||
|
||||
setSelectionOffset(_editString.size() - _selCaretPos);
|
||||
} else {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
// Move caret to end
|
||||
setCaretPos(caretVisualPos(_editString.size()));
|
||||
}
|
||||
|
||||
void EditableWidget::handleOtherEvent(const Common::Event &evt) {
|
||||
bool dirty = false;
|
||||
bool forcecaret = false;
|
||||
|
||||
if (!isEnabled())
|
||||
return;
|
||||
|
||||
// First remove caret
|
||||
if (_caretVisible)
|
||||
drawCaret(true);
|
||||
|
||||
switch (evt.type) {
|
||||
case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
|
||||
switch (evt.customType) {
|
||||
case kActionHome:
|
||||
moveCaretToStart(false);
|
||||
forcecaret = true;
|
||||
dirty = true;
|
||||
break;
|
||||
case kActionShiftHome:
|
||||
moveCaretToStart(true);
|
||||
forcecaret = true;
|
||||
dirty = true;
|
||||
break;
|
||||
case kActionEnd:
|
||||
moveCaretToEnd(false);
|
||||
forcecaret = true;
|
||||
dirty = true;
|
||||
break;
|
||||
case kActionShiftEnd:
|
||||
moveCaretToEnd(true);
|
||||
forcecaret = true;
|
||||
dirty = true;
|
||||
break;
|
||||
case kActionCut:
|
||||
if (!getEditString().empty() && _selOffset != 0) {
|
||||
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);
|
||||
|
||||
_editString.erase(selBegin, selEnd - selBegin);
|
||||
setCaretPos(caretVisualPos(selBegin));
|
||||
_selCaretPos = -1;
|
||||
_selOffset = 0;
|
||||
dirty = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case kActionCopy:
|
||||
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);
|
||||
}
|
||||
break;
|
||||
|
||||
case kActionPaste:
|
||||
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;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
markAsDirty();
|
||||
|
||||
if (forcecaret)
|
||||
makeCaretVisible();
|
||||
}
|
||||
|
||||
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, bool useRelativeCoordinates) {
|
||||
// Only draw if item is visible
|
||||
if (!isVisible() || !_boss->isVisible())
|
||||
return;
|
||||
|
||||
Common::Rect editRect = getEditRect();
|
||||
|
||||
int xOff;
|
||||
int yOff;
|
||||
|
||||
if (useRelativeCoordinates) {
|
||||
xOff = getRelX();
|
||||
yOff = getRelY();
|
||||
} else {
|
||||
xOff = getAbsX();
|
||||
yOff = getAbsY();
|
||||
}
|
||||
|
||||
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 - xOff;
|
||||
else
|
||||
x += xOff;
|
||||
y += yOff;
|
||||
|
||||
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
|
||||
126
gui/widgets/editable.h
Normal file
126
gui/widgets/editable.h
Normal file
@@ -0,0 +1,126 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GUI_WIDGETS_EDITABLE_H
|
||||
#define GUI_WIDGETS_EDITABLE_H
|
||||
|
||||
#include "common/keyboard.h"
|
||||
#include "common/str.h"
|
||||
#include "gui/widget.h"
|
||||
#include "gui/ThemeEngine.h"
|
||||
#include "gui/object.h"
|
||||
|
||||
namespace Common {
|
||||
struct Rect;
|
||||
}
|
||||
|
||||
namespace GUI {
|
||||
|
||||
/**
|
||||
* Base class for widgets which need to edit text, like ListWidget and
|
||||
* EditTextWidget.
|
||||
*/
|
||||
class EditableWidget : public Widget, public CommandSender {
|
||||
protected:
|
||||
Common::U32String _editString;
|
||||
|
||||
uint32 _cmd;
|
||||
|
||||
bool _caretVisible;
|
||||
uint32 _caretTime;
|
||||
int _caretPos;
|
||||
|
||||
bool _caretInverse;
|
||||
|
||||
int _editScrollOffset;
|
||||
|
||||
int _selCaretPos;
|
||||
int _selOffset;
|
||||
bool _shiftPressed;
|
||||
bool _isDragging;
|
||||
bool _disableSelection;
|
||||
|
||||
Graphics::TextAlign _align;
|
||||
Graphics::TextAlign _drawAlign;
|
||||
|
||||
ThemeEngine::FontStyle _font;
|
||||
|
||||
ThemeEngine::TextInversionState _inversion;
|
||||
|
||||
public:
|
||||
EditableWidget(GuiObject *boss, int x, int y, int w, int h, bool scale, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0);
|
||||
EditableWidget(GuiObject *boss, int x, int y, int w, int h, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0);
|
||||
EditableWidget(GuiObject *boss, const Common::String &name, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0);
|
||||
~EditableWidget() override;
|
||||
|
||||
void init();
|
||||
|
||||
virtual void setEditString(const Common::U32String &str);
|
||||
virtual const Common::U32String &getEditString() const { return _editString; }
|
||||
|
||||
void handleTickle() override;
|
||||
void handleMouseDown(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseUp(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseMoved(int x, int y, int button) override;
|
||||
bool handleKeyDown(Common::KeyState state) override;
|
||||
bool handleKeyUp(Common::KeyState state) override;
|
||||
void handleOtherEvent(const Common::Event& evt) override;
|
||||
void reflowLayout() override;
|
||||
|
||||
void moveCaretToStart(bool shiftPressed);
|
||||
void moveCaretToEnd(bool shiftPressed);
|
||||
bool setCaretPos(int newPos);
|
||||
void setSelectionOffset(int newOffset);
|
||||
|
||||
protected:
|
||||
void drawWidget() override;
|
||||
|
||||
virtual void startEditMode() = 0;
|
||||
virtual void endEditMode() = 0;
|
||||
virtual void abortEditMode() = 0;
|
||||
/**
|
||||
* The area where text input is being made. This should exactly match the
|
||||
* rect with which the actual edit string is drawn otherwise nasty
|
||||
* graphics glitches when redrawing the caret can occur.
|
||||
*/
|
||||
virtual Common::Rect getEditRect() const = 0;
|
||||
virtual int getCaretOffset() const;
|
||||
virtual int getSelectionCarretOffset() const;
|
||||
void drawCaret(bool erase, bool useRelativeCoordinates = false);
|
||||
bool adjustOffset();
|
||||
void makeCaretVisible();
|
||||
|
||||
void defaultKeyDownHandler(Common::KeyState &state, bool &dirty, bool &forcecaret, bool &handled);
|
||||
|
||||
void setFontStyle(ThemeEngine::FontStyle font) { _font = font; }
|
||||
|
||||
virtual bool isCharAllowed(Common::u32char_type_t c) const;
|
||||
bool tryInsertChar(Common::u32char_type_t c, int pos);
|
||||
|
||||
int caretVisualPos(int logicalPos) const;
|
||||
int caretLogicalPos() const;
|
||||
|
||||
void clearSelection();
|
||||
};
|
||||
|
||||
} // End of namespace GUI
|
||||
|
||||
#endif
|
||||
175
gui/widgets/edittext.cpp
Normal file
175
gui/widgets/edittext.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
/* 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/system.h"
|
||||
#include "common/unicode-bidi.h"
|
||||
#include "gui/widgets/edittext.h"
|
||||
#include "gui/gui-manager.h"
|
||||
|
||||
#include "gui/ThemeEval.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
EditTextWidget::EditTextWidget(GuiObject *boss, int x, int y, int w, int h, bool scale, const Common::U32String &text, const Common::U32String &tooltip, uint32 cmd, uint32 finishCmd, ThemeEngine::FontStyle font)
|
||||
: EditableWidget(boss, x, y - 1, w, h + 2, scale, tooltip, cmd) {
|
||||
setFlags(WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS | WIDGET_WANT_TICKLE);
|
||||
_type = kEditTextWidget;
|
||||
_finishCmd = finishCmd;
|
||||
|
||||
_leftPadding = _rightPadding = 0;
|
||||
|
||||
setEditString(text);
|
||||
setFontStyle(font);
|
||||
}
|
||||
|
||||
EditTextWidget::EditTextWidget(GuiObject *boss, int x, int y, int w, int h, const Common::U32String &text, const Common::U32String &tooltip, uint32 cmd, uint32 finishCmd, ThemeEngine::FontStyle font)
|
||||
: EditTextWidget(boss, x, y, w, h, false, text, tooltip, cmd, finishCmd, font) {
|
||||
}
|
||||
|
||||
EditTextWidget::EditTextWidget(GuiObject *boss, const Common::String &name, const Common::U32String &text, const Common::U32String &tooltip, uint32 cmd, uint32 finishCmd, ThemeEngine::FontStyle font)
|
||||
: EditableWidget(boss, name, tooltip, cmd) {
|
||||
setFlags(WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS | WIDGET_WANT_TICKLE);
|
||||
_type = kEditTextWidget;
|
||||
_finishCmd = finishCmd;
|
||||
|
||||
_leftPadding = _rightPadding = 0;
|
||||
_shiftPressed = _isDragging = false;
|
||||
|
||||
setEditString(text);
|
||||
setFontStyle(font);
|
||||
}
|
||||
|
||||
void EditTextWidget::setEditString(const Common::U32String &str) {
|
||||
EditableWidget::setEditString(str);
|
||||
_backupString = str;
|
||||
}
|
||||
|
||||
void EditTextWidget::reflowLayout() {
|
||||
_leftPadding = g_gui.xmlEval()->getVar("Globals.EditTextWidget.Padding.Left", 0);
|
||||
_rightPadding = g_gui.xmlEval()->getVar("Globals.EditTextWidget.Padding.Right", 0);
|
||||
|
||||
EditableWidget::reflowLayout();
|
||||
}
|
||||
|
||||
void EditTextWidget::drawWidget() {
|
||||
g_gui.theme()->drawWidgetBackground(Common::Rect(_x, _y, _x + _w, _y + _h),
|
||||
ThemeEngine::kWidgetBackgroundEditText);
|
||||
|
||||
// Draw the text
|
||||
adjustOffset();
|
||||
Common::Rect drawRect = getEditRect();
|
||||
drawRect.translate(_x, _y);
|
||||
setTextDrawableArea(drawRect);
|
||||
|
||||
int x = -_editScrollOffset;
|
||||
int y = drawRect.top;
|
||||
|
||||
int selBegin = _selCaretPos;
|
||||
int selEnd = _selOffset + _selCaretPos;
|
||||
if (selBegin > selEnd)
|
||||
SWAP(selBegin, selEnd);
|
||||
selBegin = MAX(selBegin, 0);
|
||||
selEnd = MAX(selEnd, 0);
|
||||
|
||||
if (!g_gui.useRTL()) {
|
||||
Common::UnicodeBiDiText utxt(_editString);
|
||||
Common::U32String left = Common::U32String(utxt.visual.c_str(), utxt.visual.c_str() + selBegin);
|
||||
Common::U32String selected = Common::U32String(utxt.visual.c_str() + selBegin, selEnd - selBegin);
|
||||
Common::U32String right = Common::U32String(utxt.visual.c_str() + selEnd);
|
||||
Common::U32StringArray parts {left, selected, right};
|
||||
int scrollOffset = _editScrollOffset;
|
||||
for (uint i = 0; i < parts.size(); i++) {
|
||||
if (!parts[i].size())
|
||||
continue;
|
||||
Common::U32String part = parts[i];
|
||||
int partW = g_gui.getStringWidth(part, _font);
|
||||
int clipL = drawRect.left + (scrollOffset < 0 ? -scrollOffset : 0);
|
||||
int clipR = MIN(clipL + partW, (int)drawRect.right);
|
||||
if (x + partW > 0 && x < _w && clipL < drawRect.right) {
|
||||
int sO = scrollOffset < 0 ? 0 : -scrollOffset;
|
||||
_inversion = i == 1 ? ThemeEngine::kTextInversionFocus : ThemeEngine::kTextInversionNone;
|
||||
g_gui.theme()->drawText(Common::Rect(clipL, y, clipR, y + drawRect.height()), part, _state,
|
||||
_drawAlign, _inversion, sO, false, _font, ThemeEngine::kFontColorNormal,
|
||||
true, _textDrawableArea);
|
||||
}
|
||||
x += partW;
|
||||
scrollOffset -= partW;
|
||||
}
|
||||
} else {
|
||||
// The above method does not render RTL languages correctly, so fallback to default method
|
||||
// There are only two possible cases, either the whole string has been selected
|
||||
// or nothing has been selected.
|
||||
_inversion = _selOffset ? ThemeEngine::kTextInversionFocus : ThemeEngine::kTextInversionNone;
|
||||
g_gui.theme()->drawText(drawRect, _editString, _state, _drawAlign, _inversion,
|
||||
-_editScrollOffset, false, _font, ThemeEngine::kFontColorNormal, true,
|
||||
_textDrawableArea);
|
||||
}
|
||||
|
||||
EditableWidget::drawWidget();
|
||||
}
|
||||
|
||||
Common::Rect EditTextWidget::getEditRect() const {
|
||||
// Calculate (right - left) difference for editRect's X-axis coordinates:
|
||||
// (_w - 1 - _rightPadding) - (2 + _leftPadding)
|
||||
int editWidth = _w - _rightPadding - _leftPadding - 3;
|
||||
int editHeight = _h - 2;
|
||||
// Ensure r will always be a valid rect
|
||||
if (editWidth < 0) {
|
||||
editWidth = 0;
|
||||
}
|
||||
if (editHeight < 0) {
|
||||
editHeight = 0;
|
||||
}
|
||||
Common::Rect r(2 + _leftPadding, 1, 2 + _leftPadding + editWidth, 1 + editHeight);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
void EditTextWidget::receivedFocusWidget() {
|
||||
g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, true);
|
||||
}
|
||||
|
||||
void EditTextWidget::lostFocusWidget() {
|
||||
// If we lose focus, 'commit' the user changes and clear selection
|
||||
_backupString = _editString;
|
||||
drawCaret(true);
|
||||
clearSelection();
|
||||
|
||||
g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false);
|
||||
}
|
||||
|
||||
void EditTextWidget::startEditMode() {
|
||||
}
|
||||
|
||||
void EditTextWidget::endEditMode() {
|
||||
releaseFocus();
|
||||
|
||||
sendCommand(_finishCmd, 0);
|
||||
}
|
||||
|
||||
void EditTextWidget::abortEditMode() {
|
||||
setEditString(_backupString);
|
||||
sendCommand(_cmd, 0);
|
||||
|
||||
releaseFocus();
|
||||
}
|
||||
|
||||
} // End of namespace GUI
|
||||
66
gui/widgets/edittext.h
Normal file
66
gui/widgets/edittext.h
Normal file
@@ -0,0 +1,66 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GUI_WIDGETS_EDITTEXT_H
|
||||
#define GUI_WIDGETS_EDITTEXT_H
|
||||
|
||||
#include "gui/widgets/editable.h"
|
||||
#include "common/str.h"
|
||||
#include "gui/dialog.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
/* EditTextWidget */
|
||||
class EditTextWidget : public EditableWidget {
|
||||
protected:
|
||||
Common::U32String _backupString;
|
||||
|
||||
int _leftPadding;
|
||||
int _rightPadding;
|
||||
|
||||
public:
|
||||
EditTextWidget(GuiObject *boss, int x, int y, int w, int h, bool scale, const Common::U32String &text, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0, uint32 finishCmd = 0, ThemeEngine::FontStyle font = ThemeEngine::kFontStyleNormal);
|
||||
EditTextWidget(GuiObject *boss, int x, int y, int w, int h, const Common::U32String &text, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0, uint32 finishCmd = 0, ThemeEngine::FontStyle font = ThemeEngine::kFontStyleNormal);
|
||||
EditTextWidget(GuiObject *boss, const Common::String &name, const Common::U32String &text, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0, uint32 finishCmd = 0, ThemeEngine::FontStyle font = ThemeEngine::kFontStyleNormal);
|
||||
|
||||
void setEditString(const Common::U32String &str) override;
|
||||
|
||||
bool wantsFocus() override { return true; }
|
||||
|
||||
void reflowLayout() override;
|
||||
|
||||
protected:
|
||||
void drawWidget() override;
|
||||
void receivedFocusWidget() override;
|
||||
void lostFocusWidget() override;
|
||||
|
||||
void startEditMode() override;
|
||||
void endEditMode() override;
|
||||
void abortEditMode() override;
|
||||
|
||||
Common::Rect getEditRect() const override;
|
||||
|
||||
uint32 _finishCmd;
|
||||
};
|
||||
|
||||
} // End of namespace GUI
|
||||
|
||||
#endif
|
||||
1278
gui/widgets/grid.cpp
Normal file
1278
gui/widgets/grid.cpp
Normal file
File diff suppressed because it is too large
Load Diff
278
gui/widgets/grid.h
Normal file
278
gui/widgets/grid.h
Normal file
@@ -0,0 +1,278 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GUI_WIDGETS_GRID_H
|
||||
#define GUI_WIDGETS_GRID_H
|
||||
|
||||
#include "gui/dialog.h"
|
||||
#include "gui/widgets/scrollbar.h"
|
||||
#include "common/str.h"
|
||||
|
||||
#include "image/bmp.h"
|
||||
#include "image/png.h"
|
||||
#include "graphics/svg.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class ScrollBarWidget;
|
||||
class GridItemWidget;
|
||||
class GridWidget;
|
||||
|
||||
enum {
|
||||
kPlayButtonCmd = 'PLAY',
|
||||
kEditButtonCmd = 'EDIT',
|
||||
kLoadButtonCmd = 'LOAD',
|
||||
kOpenTrayCmd = 'OPTR',
|
||||
kItemClicked = 'LBX1',
|
||||
kItemDoubleClickedCmd = 'LBX2',
|
||||
kItemSizeCmd = 'SIZE'
|
||||
};
|
||||
|
||||
/* GridItemInfo */
|
||||
struct GridItemInfo {
|
||||
bool isHeader, validEntry;
|
||||
int entryID;
|
||||
Common::String engineid;
|
||||
Common::String gameid;
|
||||
Common::String title;
|
||||
Common::String description;
|
||||
Common::String extra;
|
||||
Common::String thumbPath;
|
||||
// Generic attribute value, may be any piece of metadata
|
||||
Common::String attribute;
|
||||
Common::Language language;
|
||||
Common::Platform platform;
|
||||
|
||||
int32 x, y, w, h;
|
||||
|
||||
GridItemInfo(int id, const Common::String &eid, const Common::String &gid, const Common::String &t,
|
||||
const Common::String &d, const Common::String &e, Common::Language l, Common::Platform p, bool v)
|
||||
: entryID(id), gameid(gid), engineid(eid), title(t), description(d), extra(e), language(l), platform(p), validEntry(v), isHeader(false) {
|
||||
thumbPath = Common::String::format("icons/%s-%s.png", engineid.c_str(), gameid.c_str());
|
||||
}
|
||||
|
||||
GridItemInfo(const Common::String &groupHeader, int groupID) : title(groupHeader), description(groupHeader),
|
||||
isHeader(true), validEntry(true), entryID(groupID), language(Common::UNK_LANG), platform(Common::kPlatformUnknown) {
|
||||
thumbPath = Common::String("");
|
||||
}
|
||||
};
|
||||
|
||||
/* GridItemTray */
|
||||
class GridItemTray: public Dialog, public CommandSender {
|
||||
int _entryID;
|
||||
GridWidget *_grid;
|
||||
GuiObject *_boss;
|
||||
PicButtonWidget *_playButton;
|
||||
PicButtonWidget *_loadButton;
|
||||
PicButtonWidget *_editButton;
|
||||
public:
|
||||
GridItemTray(GuiObject *boss, int x, int y, int w, int h, int entryID, GridWidget *grid);
|
||||
|
||||
void reflowLayout() override;
|
||||
|
||||
void handleCommand(CommandSender *sender, uint32 cmd, uint32 data) override;
|
||||
void handleMouseDown(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseUp(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseWheel(int x, int y, int direction) override;
|
||||
void handleMouseMoved(int x, int y, int button) override;
|
||||
};
|
||||
|
||||
|
||||
/* GridWidget */
|
||||
class GridWidget : public ContainerWidget, public CommandSender {
|
||||
protected:
|
||||
Common::HashMap<int, const Graphics::ManagedSurface *> _platformIcons;
|
||||
Common::HashMap<int, const Graphics::ManagedSurface *> _languageIcons;
|
||||
Common::HashMap<int, const Graphics::ManagedSurface *> _extraIcons;
|
||||
Common::HashMap<int, Graphics::AlphaType> _platformIconsAlpha;
|
||||
Common::HashMap<int, Graphics::AlphaType> _languageIconsAlpha;
|
||||
Common::HashMap<int, Graphics::AlphaType> _extraIconsAlpha;
|
||||
Graphics::ManagedSurface *_disabledIconOverlay;
|
||||
// Images are mapped by filename -> surface.
|
||||
Common::HashMap<Common::String, const Graphics::ManagedSurface *> _loadedSurfaces;
|
||||
|
||||
Common::Array<GridItemInfo> _dataEntryList;
|
||||
Common::Array<GridItemInfo> _headerEntryList;
|
||||
Common::Array<GridItemInfo *> _sortedEntryList;
|
||||
Common::Array<GridItemInfo *> _visibleEntryList;
|
||||
|
||||
Common::String _groupingAttribute;
|
||||
Common::HashMap<Common::U32String, int> _groupValueIndex;
|
||||
Common::Array<bool> _groupExpanded;
|
||||
Common::U32String _groupHeaderPrefix;
|
||||
Common::U32String _groupHeaderSuffix;
|
||||
Common::Array<Common::U32String> _groupHeaders;
|
||||
Common::StringMap _metadataNames;
|
||||
Common::HashMap<int, Common::Array<int> > _itemsInGroup;
|
||||
|
||||
Common::Array<GridItemWidget *> _gridItems;
|
||||
|
||||
ScrollBarWidget *_scrollBar;
|
||||
|
||||
int _scrollBarWidth;
|
||||
int _scrollWindowHeight;
|
||||
int _scrollWindowWidth;
|
||||
int _scrollSpeed;
|
||||
int _scrollPos;
|
||||
int _innerHeight;
|
||||
int _innerWidth;
|
||||
int _thumbnailHeight;
|
||||
int _thumbnailWidth;
|
||||
int _flagIconHeight;
|
||||
int _flagIconWidth;
|
||||
int _platformIconHeight;
|
||||
int _platformIconWidth;
|
||||
int _extraIconHeight;
|
||||
int _extraIconWidth;
|
||||
int _minGridXSpacing;
|
||||
int _minGridYSpacing;
|
||||
int _rows;
|
||||
int _itemsPerRow;
|
||||
int _firstVisibleItem;
|
||||
int _lastVisibleItem;
|
||||
bool _isGridInvalid;
|
||||
|
||||
int _scrollWindowPaddingX;
|
||||
int _scrollWindowPaddingY;
|
||||
int _gridHeaderHeight;
|
||||
int _gridHeaderWidth;
|
||||
int _trayHeight;
|
||||
|
||||
bool _multiSelectEnabled; /// Flag for multi-selection
|
||||
|
||||
public:
|
||||
int _gridItemHeight;
|
||||
int _gridItemWidth;
|
||||
int _gridXSpacing;
|
||||
int _gridYSpacing;
|
||||
int _thumbnailMargin;
|
||||
|
||||
bool _isTitlesVisible;
|
||||
|
||||
GridItemInfo *_selectedEntry;
|
||||
|
||||
Common::U32String _filter;
|
||||
|
||||
GridWidget(GuiObject *boss, const Common::String &name);
|
||||
~GridWidget();
|
||||
|
||||
template<typename T>
|
||||
void unloadSurfaces(Common::HashMap<T, const Graphics::ManagedSurface *> &surfaces);
|
||||
|
||||
const Graphics::ManagedSurface *filenameToSurface(const Common::String &name);
|
||||
const Graphics::ManagedSurface *languageToSurface(Common::Language languageCode, Graphics::AlphaType &alphaType);
|
||||
const Graphics::ManagedSurface *platformToSurface(Common::Platform platformCode, Graphics::AlphaType &alphaType);
|
||||
const Graphics::ManagedSurface *demoToSurface(const Common::String &extraString, Graphics::AlphaType &alphaType);
|
||||
const Graphics::ManagedSurface *disabledThumbnail();
|
||||
|
||||
/// Update _visibleEntries from _allEntries and returns true if reload is required.
|
||||
bool calcVisibleEntries();
|
||||
void setEntryList(Common::Array<GridItemInfo> *list);
|
||||
void setAttributeValues(const Common::Array<Common::U32String> &attrs);
|
||||
void setMetadataNames(const Common::StringMap &metadata);
|
||||
void setTitlesVisible(bool vis);
|
||||
void markGridAsInvalid() { _isGridInvalid = true; }
|
||||
void setGroupHeaderFormat(const Common::U32String &prefix, const Common::U32String &suffix);
|
||||
|
||||
void groupEntries();
|
||||
void sortGroups();
|
||||
bool groupExpanded(int groupID) { return _groupExpanded[groupID]; }
|
||||
void toggleGroup(int groupID);
|
||||
void loadClosedGroups(const Common::U32String &groupName);
|
||||
void saveClosedGroups(const Common::U32String &groupName);
|
||||
|
||||
void reloadThumbnails();
|
||||
void loadFlagIcons();
|
||||
void loadPlatformIcons();
|
||||
void loadExtraIcons();
|
||||
|
||||
void destroyItems();
|
||||
void calcInnerHeight();
|
||||
void calcEntrySizes();
|
||||
void updateGrid();
|
||||
void move(int x, int y);
|
||||
void scrollToEntry(int id, bool forceToTop);
|
||||
void assignEntriesToItems();
|
||||
|
||||
int getItemPos(int item);
|
||||
int getNewSel(int index);
|
||||
int getVisualPos(int entryID) const;
|
||||
void selectVisualRange(int startPos, int endPos);
|
||||
int getScrollPos() const { return _scrollPos; }
|
||||
int getSelected() const { return ((_selectedEntry == nullptr) ? -1 : _selectedEntry->entryID); }
|
||||
int getThumbnailHeight() const { return _thumbnailHeight; }
|
||||
int getThumbnailWidth() const { return _thumbnailWidth; }
|
||||
|
||||
void handleMouseWheel(int x, int y, int direction) override;
|
||||
void handleCommand(CommandSender *sender, uint32 cmd, uint32 data) override;
|
||||
void reflowLayout() override;
|
||||
|
||||
bool wantsFocus() override { return true; }
|
||||
|
||||
bool handleKeyDown(Common::KeyState state) override;
|
||||
bool handleKeyUp(Common::KeyState state) override;
|
||||
void openTrayAtSelected();
|
||||
void scrollBarRecalc();
|
||||
|
||||
void setSelected(int id);
|
||||
void setFilter(const Common::U32String &filter);
|
||||
|
||||
// Multi-selection methods
|
||||
void setMultiSelectEnabled(bool enabled) { _multiSelectEnabled = enabled; }
|
||||
bool isMultiSelectEnabled() const { return _multiSelectEnabled; }
|
||||
Common::Array<bool> _selectedItems; /// Multiple selected items (bool array)
|
||||
int _lastSelectedEntryID = -1; /// Used for Shift+Click range selection
|
||||
bool isItemSelected(int entryID) const;
|
||||
void markSelectedItem(int entryID, bool state);
|
||||
void clearSelection();
|
||||
const Common::Array<bool> &getSelectedItems() const { return _selectedItems; }
|
||||
};
|
||||
|
||||
/* GridItemWidget */
|
||||
class GridItemWidget : public ContainerWidget, public CommandSender {
|
||||
protected:
|
||||
Graphics::ManagedSurface _thumbGfx;
|
||||
Graphics::AlphaType _thumbAlpha;
|
||||
|
||||
GridItemInfo *_activeEntry;
|
||||
GridWidget *_grid;
|
||||
bool _isHighlighted;
|
||||
|
||||
public:
|
||||
GridItemWidget(GridWidget *boss);
|
||||
|
||||
void move(int x, int y);
|
||||
void update();
|
||||
void updateThumb();
|
||||
void setActiveEntry(GridItemInfo &entry);
|
||||
|
||||
void drawWidget() override;
|
||||
|
||||
void handleMouseWheel(int x, int y, int direction) override;
|
||||
void handleMouseEntered(int button) override;
|
||||
void handleMouseLeft(int button) override;
|
||||
void handleMouseDown(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseMoved(int x, int y, int button) override;
|
||||
};
|
||||
|
||||
} // End of namespace GUI
|
||||
|
||||
#endif
|
||||
551
gui/widgets/groupedlist.cpp
Normal file
551
gui/widgets/groupedlist.cpp
Normal file
@@ -0,0 +1,551 @@
|
||||
/* 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/system.h"
|
||||
#include "common/frac.h"
|
||||
#include "common/tokenizer.h"
|
||||
#include "common/translation.h"
|
||||
#include "common/config-manager.h"
|
||||
|
||||
#include "gui/widgets/groupedlist.h"
|
||||
#include "gui/widgets/scrollbar.h"
|
||||
#include "gui/dialog.h"
|
||||
#include "gui/gui-manager.h"
|
||||
|
||||
#include "gui/ThemeEval.h"
|
||||
|
||||
#define kGroupTag -2
|
||||
#define isGroupHeader(x) (_groupsVisible && ((x) <= kGroupTag))
|
||||
#define indexToGroupID(x) (kGroupTag - x)
|
||||
|
||||
namespace GUI {
|
||||
|
||||
GroupedListWidget::GroupedListWidget(Dialog *boss, const Common::String &name, const Common::U32String &tooltip, uint32 cmd)
|
||||
: ListWidget(boss, name, tooltip, cmd) {
|
||||
_groupsVisible = true;
|
||||
}
|
||||
|
||||
void GroupedListWidget::setList(const Common::U32StringArray &list) {
|
||||
ListWidget::setList(list);
|
||||
|
||||
_attributeValues.clear(); // Regenerate attributes for the new list
|
||||
groupByAttribute();
|
||||
scrollBarRecalc();
|
||||
}
|
||||
|
||||
void GroupedListWidget::setAttributeValues(const Common::U32StringArray &attrValues) {
|
||||
_attributeValues = attrValues;
|
||||
// Make sure we always have the attribute values for all the entries of the _dataList.
|
||||
// This is not foolproof, but can prevent accidentally passing attributes for the wrong
|
||||
// list of entries, such as after adding / removing games.
|
||||
if (!attrValues.empty())
|
||||
assert(_attributeValues.size() == _dataList.size());
|
||||
}
|
||||
|
||||
void GroupedListWidget::setMetadataNames(const Common::StringMap &metadata) {
|
||||
_metadataNames = metadata;
|
||||
}
|
||||
|
||||
void GroupedListWidget::setGroupHeaderFormat(const Common::U32String &prefix, const Common::U32String &suffix) {
|
||||
_groupHeaderPrefix = prefix;
|
||||
_groupHeaderSuffix = suffix;
|
||||
}
|
||||
|
||||
void GroupedListWidget::groupByAttribute() {
|
||||
_groupExpanded.clear();
|
||||
_groupHeaders.clear();
|
||||
_groupValueIndex.clear();
|
||||
_itemsInGroup.clear();
|
||||
|
||||
if (_attributeValues.empty()) {
|
||||
_groupExpanded.push_back(true);
|
||||
// I18N: Group for All items
|
||||
_groupHeaders.push_back(_("All"));
|
||||
_groupValueIndex.setVal(Common::String("All"), 0);
|
||||
for (uint i = 0; i < _dataList.size(); ++i) {
|
||||
_itemsInGroup[0].push_back(i);
|
||||
}
|
||||
sortGroups();
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint i = 0; i < _dataList.size(); ++i) {
|
||||
Common::U32StringArray::iterator attrVal = _attributeValues.begin() + i;
|
||||
if (!_groupValueIndex.contains(*attrVal)) {
|
||||
int newGroupID = _groupValueIndex.size();
|
||||
_groupValueIndex.setVal(*attrVal, newGroupID);
|
||||
_groupHeaders.push_back(*attrVal);
|
||||
_groupExpanded.push_back(true);
|
||||
}
|
||||
int groupID = _groupValueIndex.getVal(*attrVal);
|
||||
|
||||
_itemsInGroup[groupID].push_back(i);
|
||||
}
|
||||
|
||||
sortGroups();
|
||||
}
|
||||
|
||||
void GroupedListWidget::sortGroups() {
|
||||
uint oldListSize = _list.size();
|
||||
_list.clear();
|
||||
_listIndex.clear();
|
||||
|
||||
Common::sort(_groupHeaders.begin(), _groupHeaders.end(),
|
||||
[](const Common::String &first, const Common::String &second) {
|
||||
return first.empty() ? 0 : second.empty() ? 1 : first < second;
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
for (uint i = 0; i != _groupHeaders.size(); ++i) {
|
||||
Common::U32String header = _groupHeaders[i];
|
||||
Common::U32String displayedHeader;
|
||||
if (_metadataNames.contains(header)) {
|
||||
displayedHeader = _metadataNames[header];
|
||||
} else {
|
||||
displayedHeader = header;
|
||||
}
|
||||
uint groupID = _groupValueIndex[header];
|
||||
|
||||
if (_groupsVisible) {
|
||||
_listIndex.push_back(kGroupTag - groupID);
|
||||
|
||||
displayedHeader.toUppercase();
|
||||
|
||||
_list.push_back(_groupHeaderPrefix + displayedHeader + _groupHeaderSuffix);
|
||||
}
|
||||
|
||||
if (_groupExpanded[groupID]) {
|
||||
for (int *k = _itemsInGroup[groupID].begin(); k != _itemsInGroup[groupID].end(); ++k) {
|
||||
_list.push_back(_dataList[*k].orig);
|
||||
_listIndex.push_back(*k);
|
||||
}
|
||||
}
|
||||
}
|
||||
checkBounds();
|
||||
scrollBarRecalc();
|
||||
// FIXME: Temporary solution to clear/display the background ofthe scrollbar when list
|
||||
// grows too small or large during group toggle. We shouldn't have to redraw the top dialog,
|
||||
// but not doing so the background of scrollbar isn't cleared.
|
||||
if ((((uint)_scrollBar->_entriesPerPage < oldListSize) && ((uint)_scrollBar->_entriesPerPage > _list.size())) ||
|
||||
(((uint)_scrollBar->_entriesPerPage > oldListSize) && ((uint)_scrollBar->_entriesPerPage < _list.size()))) {
|
||||
g_gui.scheduleTopDialogRedraw();
|
||||
} else {
|
||||
markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void GroupedListWidget::loadClosedGroups(const Common::U32String &groupName) {
|
||||
// Recalls what groups were closed from the config
|
||||
if (ConfMan.hasKey("group_" + groupName, ConfMan.kApplicationDomain)) {
|
||||
const Common::String &val = ConfMan.get("group_" + groupName, ConfMan.kApplicationDomain);
|
||||
Common::StringTokenizer hiddenGroups(val);
|
||||
|
||||
for (Common::String tok = hiddenGroups.nextToken(); tok.size(); tok = hiddenGroups.nextToken()) {
|
||||
// See if the hidden group is in our group headers still, if so, hide it
|
||||
for (Common::U32StringArray::size_type i = 0; i < _groupHeaders.size(); ++i) {
|
||||
if (_groupHeaders[i] == tok || (tok == "unnamed" && _groupHeaders[i].size() == 0)) {
|
||||
_groupExpanded[i] = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sortGroups();
|
||||
}
|
||||
}
|
||||
|
||||
void GroupedListWidget::saveClosedGroups(const Common::U32String &groupName) {
|
||||
// Save the hidden groups to the config
|
||||
Common::String hiddenGroups;
|
||||
for (Common::U32StringArray::size_type i = 0; i < _groupHeaders.size(); ++i) {
|
||||
if (!_groupExpanded[i]) {
|
||||
if (_groupHeaders[i].size()) {
|
||||
hiddenGroups += _groupHeaders[i];
|
||||
} else {
|
||||
hiddenGroups += "unnamed";
|
||||
}
|
||||
hiddenGroups += ' ';
|
||||
}
|
||||
}
|
||||
ConfMan.set("group_" + groupName, hiddenGroups, ConfMan.kApplicationDomain);
|
||||
ConfMan.flushToDisk();
|
||||
}
|
||||
|
||||
int GroupedListWidget::findDataIndex(int data_index) const {
|
||||
// The given index is an index in the _dataList.
|
||||
// We want the index in the current _listIndex (which may be filtered and sorted) for this data.
|
||||
// Sanity check to avoid iterating on the _listIndex if we know the given index is invalid.
|
||||
if (data_index < -1 || data_index >= (int)_dataList.size())
|
||||
return -1;
|
||||
for (uint i = 0; i < _listIndex.size(); ++i) {
|
||||
if (_listIndex[i] == data_index)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void GroupedListWidget::setSelected(int item) {
|
||||
if (item < -1 || item >= (int)_dataList.size())
|
||||
return;
|
||||
|
||||
// We only have to do something if the widget is enabled and the selection actually changes
|
||||
if (isEnabled() && (_selectedItem == -1 || _selectedItem >= (int)_list.size() || _listIndex[_selectedItem] != item)) {
|
||||
if (_editMode)
|
||||
abortEditMode();
|
||||
|
||||
_selectedItem = findDataIndex(item);
|
||||
|
||||
// Clear previous selections and mark only this item
|
||||
if (_multiSelectEnabled) {
|
||||
clearSelection();
|
||||
markSelectedItem(_selectedItem, true);
|
||||
}
|
||||
|
||||
// Notify clients that the selection changed.
|
||||
sendCommand(kListSelectionChangedCmd, _selectedItem);
|
||||
|
||||
if (_selectedItem != -1 && !isItemVisible(_selectedItem)) {
|
||||
// scroll selected item to center if possible
|
||||
_currentPos = _selectedItem - _entriesPerPage / 2;
|
||||
scrollToCurrent();
|
||||
}
|
||||
markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void GroupedListWidget::handleMouseDown(int x, int y, int button, int clickCount) {
|
||||
if (!isEnabled())
|
||||
return;
|
||||
|
||||
// First check whether the selection changed
|
||||
int newSelectedItem = findItem(x, y);
|
||||
if (newSelectedItem == -1)
|
||||
return;
|
||||
|
||||
if (isGroupHeader(_listIndex[newSelectedItem])) {
|
||||
int groupID = indexToGroupID(_listIndex[newSelectedItem]);
|
||||
int oldSelection = getSelected();
|
||||
_selectedItem = -1;
|
||||
toggleGroup(groupID);
|
||||
if (oldSelection != -1) {
|
||||
_selectedItem = findDataIndex(oldSelection);
|
||||
sendCommand(kListSelectionChangedCmd, _selectedItem);
|
||||
}
|
||||
markAsDirty();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Determine where inside the string the user clicked and place the
|
||||
// caret accordingly.
|
||||
// See _editScrollOffset and EditTextWidget::handleMouseDown.
|
||||
if (_editMode)
|
||||
abortEditMode();
|
||||
|
||||
int dataIndex = _listIndex[newSelectedItem];
|
||||
if (dataIndex < 0)
|
||||
return;
|
||||
|
||||
// Get modifier keys
|
||||
int modifiers = g_system->getEventManager()->getModifierState();
|
||||
bool ctrlClick = (modifiers & Common::KBD_CTRL) != 0;
|
||||
bool shiftClick = (modifiers & Common::KBD_SHIFT) != 0;
|
||||
|
||||
// Only handle multi-select if it's enabled
|
||||
if (_multiSelectEnabled && (shiftClick || ctrlClick)) {
|
||||
if (shiftClick && _lastSelectionStartItem != -1) {
|
||||
// Shift+Click: Select range in terms of underlying data indices
|
||||
int startListIndex = _lastSelectionStartItem;
|
||||
int endListIndex = newSelectedItem;
|
||||
selectItemRange(startListIndex, endListIndex);
|
||||
_selectedItem = newSelectedItem;
|
||||
_lastSelectionStartItem = newSelectedItem;
|
||||
sendCommand(kListSelectionChangedCmd, _selectedItem);
|
||||
} else if (ctrlClick) {
|
||||
// Ctrl+Click: toggle selection for the underlying data index
|
||||
if (isItemSelected(newSelectedItem)) {
|
||||
markSelectedItem(newSelectedItem, false);
|
||||
} else {
|
||||
markSelectedItem(newSelectedItem, true);
|
||||
_selectedItem = newSelectedItem;
|
||||
_lastSelectionStartItem = newSelectedItem;
|
||||
}
|
||||
sendCommand(kListSelectionChangedCmd, _selectedItem);
|
||||
}
|
||||
} else {
|
||||
// Regular click: clear selection and select only this underlying item
|
||||
clearSelection();
|
||||
_selectedItem = newSelectedItem;
|
||||
markSelectedItem(newSelectedItem, true);
|
||||
_lastSelectionStartItem = newSelectedItem;
|
||||
sendCommand(kListSelectionChangedCmd, _selectedItem);
|
||||
}
|
||||
|
||||
// Notify clients if an item was clicked
|
||||
if (newSelectedItem >= 0)
|
||||
sendCommand(kListItemSingleClickedCmd, _selectedItem);
|
||||
|
||||
markAsDirty();
|
||||
}
|
||||
|
||||
void GroupedListWidget::handleMouseUp(int x, int y, int button, int clickCount) {
|
||||
// If this was a double click and the mouse is still over
|
||||
// the selected item, send the double click command
|
||||
if (clickCount == 2 && (_selectedItem == findItem(x, y))) {
|
||||
int selectID = getSelected();
|
||||
if (selectID >= 0) {
|
||||
sendCommand(kListItemDoubleClickedCmd, _selectedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupedListWidget::handleMouseWheel(int x, int y, int direction) {
|
||||
_scrollBar->handleMouseWheel(x, y, direction);
|
||||
}
|
||||
|
||||
void GroupedListWidget::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
|
||||
switch (cmd) {
|
||||
case kSetPositionCmd:
|
||||
if (_currentPos != (int)data) {
|
||||
_currentPos = data;
|
||||
checkBounds();
|
||||
markAsDirty();
|
||||
|
||||
// Scrollbar actions cause list focus (which triggers a redraw)
|
||||
// NOTE: ListWidget's boss is always GUI::Dialog
|
||||
((GUI::Dialog *)_boss)->setFocusWidget(this);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int GroupedListWidget::getItemPos(int item) {
|
||||
int pos = 0;
|
||||
|
||||
for (uint i = 0; i < _listIndex.size(); i++) {
|
||||
if (_listIndex[i] == item) {
|
||||
return pos;
|
||||
} else if (_listIndex[i] >= 0) { // skip headers
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int GroupedListWidget::getNewSel(int index) {
|
||||
// If the list is empty, return -1
|
||||
if (_listIndex.size() == 1){
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Find the index-th item in the list
|
||||
for (uint i = 0; i < _listIndex.size(); i++) {
|
||||
if (index == 0 && _listIndex[i] >= 0) {
|
||||
return _listIndex[i];
|
||||
} else if (_listIndex[i] >= 0) {
|
||||
index--;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are at the end of the list, return the last item.
|
||||
if (index == 0) {
|
||||
return _listIndex[_listIndex.size() - 1];
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void GroupedListWidget::toggleGroup(int groupID) {
|
||||
_groupExpanded[groupID] = !_groupExpanded[groupID];
|
||||
sortGroups();
|
||||
}
|
||||
|
||||
void GroupedListWidget::drawWidget() {
|
||||
int i, pos, len = _list.size();
|
||||
Common::U32String buffer;
|
||||
|
||||
// Draw a thin frame around the list.
|
||||
g_gui.theme()->drawWidgetBackground(Common::Rect(_x, _y, _x + _w, _y + _h),
|
||||
ThemeEngine::kWidgetBackgroundBorder);
|
||||
|
||||
// Draw the list items
|
||||
const int lineHeight = kLineHeight + _itemSpacing;
|
||||
const int indentSpacing = g_gui.getFontHeight();
|
||||
for (i = 0, pos = _currentPos; i < _entriesPerPage && pos < len; i++, pos++) {
|
||||
const int y = _y + _topPadding + lineHeight * i;
|
||||
ThemeEngine::TextInversionState inverted = ThemeEngine::kTextInversionNone;
|
||||
#if 0
|
||||
ThemeEngine::FontStyle bold = ThemeEngine::kFontStyleBold;
|
||||
#endif
|
||||
|
||||
// For grouped lists, only real items (non-headers) may be highlighted.
|
||||
int mapped = _listIndex[pos];
|
||||
bool isRealItem = (mapped >= 0);
|
||||
if (isRealItem) {
|
||||
if (isItemSelected(pos))
|
||||
inverted = _inversion;
|
||||
}
|
||||
|
||||
ThemeEngine::WidgetStateInfo itemState = getItemState(pos);
|
||||
|
||||
Common::Rect r(getEditRect());
|
||||
int pad = _leftPadding;
|
||||
int rtlPad = (_x + r.left + _leftPadding) - (_x + _hlLeftPadding);
|
||||
|
||||
// Group header / grouped list indentation logic
|
||||
if (isGroupHeader(mapped)) {
|
||||
int groupID = indexToGroupID(mapped);
|
||||
#if 0
|
||||
bold = ThemeEngine::kFontStyleBold;
|
||||
#endif
|
||||
r.left += indentSpacing + _leftPadding;
|
||||
g_gui.theme()->drawFoldIndicator(Common::Rect(_x + _hlLeftPadding + _leftPadding, y, _x + indentSpacing + _leftPadding, y + lineHeight), _groupExpanded[groupID]);
|
||||
pad = 0;
|
||||
} else if (_groupsVisible) {
|
||||
r.left += indentSpacing + _leftPadding;
|
||||
r.right -= _rightPadding;
|
||||
pad = 0;
|
||||
}
|
||||
|
||||
// If in numbering mode & not in RTL based GUI, we first print a number prefix
|
||||
if (_numberingMode != kListNumberingOff && g_gui.useRTL() == false) {
|
||||
buffer = Common::String::format("%2d. ", (pos + _numberingMode));
|
||||
g_gui.theme()->drawText(Common::Rect(_x + _hlLeftPadding, y, _x + r.left + _leftPadding, y + lineHeight),
|
||||
buffer, itemState, _drawAlign, inverted, _leftPadding, true);
|
||||
pad = 0;
|
||||
}
|
||||
|
||||
Common::Rect r1(_x + r.left, y, _x + r.right, y + lineHeight);
|
||||
|
||||
if (g_gui.useRTL()) {
|
||||
if (_scrollBar->isVisible()) {
|
||||
r1.translate(_scrollBarWidth, 0);
|
||||
}
|
||||
|
||||
if (_numberingMode != kListNumberingOff) {
|
||||
r1.translate(-rtlPad, 0);
|
||||
}
|
||||
}
|
||||
|
||||
ThemeEngine::FontColor color = ThemeEngine::kFontColorFormatting;
|
||||
|
||||
if (_selectedItem == pos && _editMode) {
|
||||
buffer = _editString;
|
||||
color = _editColor;
|
||||
adjustOffset();
|
||||
} else {
|
||||
buffer = _list[pos];
|
||||
}
|
||||
|
||||
drawFormattedText(r1, buffer, itemState, _drawAlign, inverted, pad, true, color);
|
||||
|
||||
if (_numberingMode != kListNumberingOff && g_gui.useRTL()) {
|
||||
buffer = Common::String::format(" .%2d", (pos + _numberingMode));
|
||||
|
||||
Common::Rect r2 = r1;
|
||||
|
||||
r2.left = r1.right;
|
||||
r2.right = r1.right + rtlPad;
|
||||
|
||||
g_gui.theme()->drawText(r2, buffer, itemState, _drawAlign, inverted, _leftPadding, true);
|
||||
}
|
||||
}
|
||||
if (_editMode)
|
||||
EditableWidget::drawWidget();
|
||||
}
|
||||
|
||||
void GroupedListWidget::setFilter(const Common::U32String &filter, bool redraw) {
|
||||
// FIXME: This method does not deal correctly with edit mode!
|
||||
// Until we fix that, let's make sure it isn't called while editing takes place
|
||||
assert(!_editMode);
|
||||
|
||||
Common::U32String filt = filter;
|
||||
filt.toLowercase();
|
||||
|
||||
if (_filter == filt) // Filter was not changed
|
||||
return;
|
||||
|
||||
int selectedItem = getSelected();
|
||||
|
||||
_filter = filt;
|
||||
|
||||
if (_filter.empty()) {
|
||||
// No filter -> display everything
|
||||
sortGroups();
|
||||
} else {
|
||||
// Restrict the list to everything which contains all words in _filter
|
||||
// as substrings, ignoring case.
|
||||
|
||||
Common::U32StringTokenizer tok(_filter);
|
||||
Common::U32String tmp;
|
||||
int n = 0;
|
||||
|
||||
_list.clear();
|
||||
_listIndex.clear();
|
||||
|
||||
for (auto i = _dataList.begin(); i != _dataList.end(); ++i, ++n) {
|
||||
tmp = i->clean;
|
||||
tmp.toLowercase();
|
||||
bool matches = true;
|
||||
tok.reset();
|
||||
while (!tok.empty()) {
|
||||
if (!_filterMatcher(_filterMatcherArg, n, tmp, tok.nextToken())) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
_list.push_back(i->orig);
|
||||
_listIndex.push_back(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_currentPos = 0;
|
||||
_selectedItem = -1;
|
||||
// Try to preserve the previous selection
|
||||
if (selectedItem != -1)
|
||||
setSelected(selectedItem);
|
||||
|
||||
if (redraw) {
|
||||
scrollBarRecalc();
|
||||
// Redraw the whole dialog. This is annoying, as this might be rather
|
||||
// expensive when really only the list widget and its scroll bar area
|
||||
// to be redrawn. However, since the scrollbar might change its
|
||||
// visibility status, and the list its width, we cannot just redraw
|
||||
// the two.
|
||||
// TODO: A more efficient (and elegant?) way to handle this would be to
|
||||
// introduce a kind of "BoxWidget" or "GroupWidget" which defines a
|
||||
// rectangular region and subwidgets can be placed within it.
|
||||
// Such a widget could also (optionally) draw a border (or even different
|
||||
// kinds of borders) around the objects it groups; and also a 'title'
|
||||
// (I am borrowing these "ideas" from the NSBox class in Cocoa :).
|
||||
g_gui.scheduleTopDialogRedraw();
|
||||
}
|
||||
}
|
||||
ThemeEngine::WidgetStateInfo GroupedListWidget::getItemState(int item) const {
|
||||
return _state;
|
||||
}
|
||||
|
||||
} // End of namespace GUI
|
||||
85
gui/widgets/groupedlist.h
Normal file
85
gui/widgets/groupedlist.h
Normal file
@@ -0,0 +1,85 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GUI_WIDGETS_GROUPEDLIST_H
|
||||
#define GUI_WIDGETS_GROUPEDLIST_H
|
||||
|
||||
#include "gui/widgets/list.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class ListWidget;
|
||||
|
||||
/* GroupedListWidget */
|
||||
class GroupedListWidget : public ListWidget {
|
||||
protected:
|
||||
Common::String _groupingAttribute;
|
||||
Common::HashMap<Common::U32String, int> _groupValueIndex;
|
||||
Common::Array<bool> _groupExpanded;
|
||||
Common::U32String _groupHeaderPrefix;
|
||||
Common::U32String _groupHeaderSuffix;
|
||||
Common::U32StringArray _groupHeaders;
|
||||
Common::U32StringArray _attributeValues;
|
||||
Common::StringMap _metadataNames;
|
||||
Common::HashMap<int, Common::Array<int> > _itemsInGroup;
|
||||
bool _groupsVisible;
|
||||
|
||||
public:
|
||||
GroupedListWidget(Dialog *boss, const Common::String &name, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0);
|
||||
GroupedListWidget(Dialog *boss, int x, int y, int w, int h, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0);
|
||||
|
||||
void setList(const Common::U32StringArray &list);
|
||||
void setAttributeValues(const Common::U32StringArray &attrValues);
|
||||
void setMetadataNames(const Common::StringMap &metadata);
|
||||
|
||||
void setGroupHeaderFormat(const Common::U32String &prefix, const Common::U32String &suffix);
|
||||
void groupByAttribute();
|
||||
void loadClosedGroups(const Common::U32String &groupName);
|
||||
void saveClosedGroups(const Common::U32String &groupName);
|
||||
|
||||
void setSelected(int item);
|
||||
int getSelected() const { return (_selectedItem == -1) ? _selectedItem : _listIndex[_selectedItem]; }
|
||||
|
||||
void setFilter(const Common::U32String &filter, bool redraw = true);
|
||||
|
||||
void handleMouseDown(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseUp(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseWheel(int x, int y, int direction) override;
|
||||
void handleCommand(CommandSender *sender, uint32 cmd, uint32 data) override;
|
||||
|
||||
void setGroupsVisibility(bool val) { _groupsVisible = val; }
|
||||
|
||||
int getItemPos(int item);
|
||||
int getNewSel(int index);
|
||||
|
||||
void startEditMode() override { error("Edit mode is not supported for Grouped Lists"); }
|
||||
|
||||
protected:
|
||||
void sortGroups();
|
||||
void toggleGroup(int groupID);
|
||||
void drawWidget() override;
|
||||
ThemeEngine::WidgetStateInfo getItemState(int item) const override;
|
||||
int findDataIndex(int) const;
|
||||
};
|
||||
|
||||
} // End of namespace GUI
|
||||
|
||||
#endif
|
||||
1117
gui/widgets/list.cpp
Normal file
1117
gui/widgets/list.cpp
Normal file
File diff suppressed because it is too large
Load Diff
208
gui/widgets/list.h
Normal file
208
gui/widgets/list.h
Normal file
@@ -0,0 +1,208 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GUI_WIDGETS_LIST_H
|
||||
#define GUI_WIDGETS_LIST_H
|
||||
|
||||
#include "gui/widgets/editable.h"
|
||||
#include "common/str.h"
|
||||
|
||||
#include "gui/ThemeEngine.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class ScrollBarWidget;
|
||||
|
||||
enum NumberingMode {
|
||||
kListNumberingOff = -1,
|
||||
kListNumberingZero = 0,
|
||||
kListNumberingOne = 1
|
||||
};
|
||||
|
||||
/// Some special commands
|
||||
enum {
|
||||
kListItemSingleClickedCmd = 'LIsc', ///< single click on item (sent on mouse down) - 'data' will be item index
|
||||
kListItemDoubleClickedCmd = 'LIdc', ///< double click on item (sent on mouse up) - 'data' will be item index
|
||||
kListItemActivatedCmd = 'LIac', ///< item activated by return/enter - 'data' will be item index
|
||||
kListItemRemovalRequestCmd = 'LIrm', ///< request to remove the item with the delete/backspace keys - 'data' will be item index
|
||||
kListItemEditModeStartedCmd = 'LIes', ///< edit mode started - 'data' will be item index
|
||||
kListSelectionChangedCmd = 'Lsch' ///< selection changed - 'data' will be item index
|
||||
};
|
||||
|
||||
/* ListWidget */
|
||||
class ListWidget : public EditableWidget {
|
||||
public:
|
||||
typedef bool (*FilterMatcher)(void *arg, int idx, const Common::U32String &item, const Common::U32String &token);
|
||||
|
||||
struct ListData {
|
||||
Common::U32String orig;
|
||||
Common::U32String clean;
|
||||
|
||||
ListData(const Common::U32String &o, const Common::U32String &c) { orig = o; clean = c; }
|
||||
};
|
||||
|
||||
typedef Common::Array<ListData> ListDataArray;
|
||||
|
||||
protected:
|
||||
Common::U32StringArray _list;
|
||||
Common::U32StringArray _cleanedList;
|
||||
ListDataArray _dataList;
|
||||
Common::Array<int> _listIndex;
|
||||
bool _editable;
|
||||
bool _editMode;
|
||||
NumberingMode _numberingMode;
|
||||
int _currentPos;
|
||||
int _entriesPerPage;
|
||||
int _selectedItem;
|
||||
Common::Array<bool> _selectedItems; /// Multiple selected items (bool array)
|
||||
bool _multiSelectEnabled; /// Flag for multi-selection
|
||||
ScrollBarWidget *_scrollBar;
|
||||
int _currentKeyDown;
|
||||
|
||||
Common::String _quickSelectStr;
|
||||
uint32 _quickSelectTime;
|
||||
|
||||
int _hlLeftPadding;
|
||||
int _hlRightPadding;
|
||||
int _leftPadding;
|
||||
int _rightPadding;
|
||||
int _topPadding;
|
||||
int _bottomPadding;
|
||||
int _itemSpacing;
|
||||
int _scrollBarWidth;
|
||||
|
||||
Common::U32String _filter;
|
||||
bool _quickSelect;
|
||||
bool _dictionarySelect;
|
||||
|
||||
uint32 _cmd;
|
||||
|
||||
ThemeEngine::FontColor _editColor;
|
||||
|
||||
int _lastRead;
|
||||
|
||||
FilterMatcher _filterMatcher;
|
||||
void *_filterMatcherArg;
|
||||
public:
|
||||
ListWidget(Dialog *boss, const Common::String &name, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0);
|
||||
ListWidget(Dialog *boss, int x, int y, int w, int h, bool scale, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0);
|
||||
ListWidget(Dialog *boss, int x, int y, int w, int h, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0);
|
||||
|
||||
bool containsWidget(Widget *) const override;
|
||||
Widget *findWidget(int x, int y) override;
|
||||
|
||||
void setList(const Common::U32StringArray &list);
|
||||
const Common::U32StringArray &getList() const { return _cleanedList; }
|
||||
|
||||
void append(const Common::String &s);
|
||||
|
||||
void setSelected(int item);
|
||||
int getSelected() const { return (_filter.empty() || _selectedItem == -1) ? _selectedItem : _listIndex[_selectedItem]; }
|
||||
|
||||
const Common::U32String getSelectedString() const { return stripGUIformatting(_list[_selectedItem]); }
|
||||
|
||||
/// Get visual position (index in filtered list) from real data index
|
||||
int getVisualPos(int dataIndex) const;
|
||||
|
||||
/// Multi-selection support
|
||||
const Common::Array<bool> &getSelectedItems() const { return _selectedItems; }
|
||||
bool isItemSelected(int item) const;
|
||||
void markSelectedItem(int item, bool state);
|
||||
void clearSelection();
|
||||
void selectItemRange(int from, int to);
|
||||
int _lastSelectionStartItem; /// Used for Shift+Click range selection
|
||||
void setNumberingMode(NumberingMode numberingMode) { _numberingMode = numberingMode; }
|
||||
|
||||
void scrollTo(int item);
|
||||
void scrollToEnd();
|
||||
int getCurrentScrollPos() const { return _currentPos; }
|
||||
bool isItemVisible(int item) const { return _currentPos <= item && item < _currentPos + _entriesPerPage; }
|
||||
|
||||
void enableQuickSelect(bool enable) { _quickSelect = enable; }
|
||||
Common::String getQuickSelectString() const { return _quickSelectStr; }
|
||||
|
||||
void enableDictionarySelect(bool enable) { _dictionarySelect = enable; }
|
||||
|
||||
bool isEditable() const { return _editable; }
|
||||
void setEditable(bool editable) { _editable = editable; }
|
||||
void setEditColor(ThemeEngine::FontColor color) { _editColor = color; }
|
||||
void setFilterMatcher(FilterMatcher matcher, void *arg) { _filterMatcher = matcher; _filterMatcherArg = arg; }
|
||||
|
||||
// Multi-selection methods
|
||||
void setMultiSelectEnabled(bool enabled) { _multiSelectEnabled = enabled; }
|
||||
bool isMultiSelectEnabled() const { return _multiSelectEnabled; }
|
||||
|
||||
// Made startEditMode/endEditMode for SaveLoadChooser
|
||||
void startEditMode() override;
|
||||
void endEditMode() override;
|
||||
|
||||
void setFilter(const Common::U32String &filter, bool redraw = true);
|
||||
|
||||
void handleTickle() override;
|
||||
void handleMouseDown(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseUp(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseWheel(int x, int y, int direction) override;
|
||||
void handleMouseMoved(int x, int y, int button) override;
|
||||
void handleMouseLeft(int button) override;
|
||||
bool handleKeyDown(Common::KeyState state) override;
|
||||
bool handleKeyUp(Common::KeyState state) override;
|
||||
void handleCommand(CommandSender *sender, uint32 cmd, uint32 data) override;
|
||||
|
||||
void reflowLayout() override;
|
||||
|
||||
bool wantsFocus() override { return true; }
|
||||
|
||||
static Common::U32String getThemeColor(byte r, byte g, byte b);
|
||||
static Common::U32String getThemeColor(ThemeEngine::FontColor color);
|
||||
static ThemeEngine::FontColor getThemeColor(const Common::U32String &color);
|
||||
static Common::U32String stripGUIformatting(const Common::U32String &str);
|
||||
static Common::U32String escapeString(const Common::U32String &str);
|
||||
|
||||
protected:
|
||||
void drawWidget() override;
|
||||
|
||||
/// Finds the item at position (x,y). Returns -1 if there is no item there.
|
||||
int findItem(int x, int y) const;
|
||||
void scrollBarRecalc();
|
||||
|
||||
void abortEditMode() override;
|
||||
|
||||
Common::Rect getEditRect() const override;
|
||||
int getCaretOffset() const override;
|
||||
|
||||
void copyListData(const Common::U32StringArray &list);
|
||||
|
||||
void receivedFocusWidget() override;
|
||||
void lostFocusWidget() override;
|
||||
void checkBounds();
|
||||
void scrollToCurrent();
|
||||
|
||||
virtual ThemeEngine::WidgetStateInfo getItemState(int item) const { return _state; }
|
||||
|
||||
void drawFormattedText(const Common::Rect &r, const Common::U32String &str, ThemeEngine::WidgetStateInfo state = ThemeEngine::kStateEnabled,
|
||||
Graphics::TextAlign align = Graphics::kTextAlignCenter,
|
||||
ThemeEngine::TextInversionState inverted = ThemeEngine::kTextInversionNone, int deltax = 0, bool useEllipsis = true,
|
||||
ThemeEngine::FontColor color = ThemeEngine::kFontColorFormatting);
|
||||
};
|
||||
|
||||
} // End of namespace GUI
|
||||
|
||||
#endif
|
||||
556
gui/widgets/popup.cpp
Normal file
556
gui/widgets/popup.cpp
Normal file
@@ -0,0 +1,556 @@
|
||||
/* 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/system.h"
|
||||
#include "gui/gui-manager.h"
|
||||
#include "gui/widgets/popup.h"
|
||||
|
||||
#include "gui/ThemeEval.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
//
|
||||
// PopUpDialog
|
||||
//
|
||||
|
||||
PopUpDialog::PopUpDialog(Widget *boss, const Common::String &name, int clickX, int clickY):
|
||||
Dialog(name),
|
||||
_boss(boss),
|
||||
// Remember original mouse position
|
||||
_clickX(clickX),
|
||||
_clickY(clickY),
|
||||
_selection(-1),
|
||||
_initialSelection(-1),
|
||||
_openTime(0),
|
||||
_twoColumns(false),
|
||||
_entriesPerColumn(1),
|
||||
_leftPadding(0),
|
||||
_rightPadding(0),
|
||||
_lineHeight(kLineHeight),
|
||||
_lastRead(-1) {
|
||||
_backgroundType = ThemeEngine::kDialogBackgroundNone;
|
||||
_w = _boss->getWidth();
|
||||
}
|
||||
|
||||
void PopUpDialog::open() {
|
||||
// Time the popup was opened
|
||||
_openTime = g_system->getMillis();
|
||||
|
||||
_initialSelection = _selection;
|
||||
|
||||
// Calculate real popup dimensions
|
||||
_h = _entries.size() * _lineHeight + 2;
|
||||
_w = 0;
|
||||
|
||||
for (uint i = 0; i < _entries.size(); i++) {
|
||||
int width = g_gui.getStringWidth(_entries[i]);
|
||||
|
||||
if (width > _w)
|
||||
_w = width;
|
||||
}
|
||||
|
||||
|
||||
_entriesPerColumn = 1;
|
||||
|
||||
// Perform clipping / switch to scrolling mode if we don't fit on the screen
|
||||
// FIXME - OSystem should send out notification messages when the screen
|
||||
// resolution changes... we could generalize CommandReceiver and CommandSender.
|
||||
|
||||
Common::Rect safeArea = g_system->getSafeOverlayArea();
|
||||
|
||||
// HACK: For now, we do not do scrolling. Instead, we draw the dialog
|
||||
// in two columns if it's too tall.
|
||||
|
||||
if (_h >= safeArea.height()) {
|
||||
_twoColumns = true;
|
||||
_entriesPerColumn = _entries.size() / 2;
|
||||
|
||||
if (_entries.size() & 1)
|
||||
_entriesPerColumn++;
|
||||
|
||||
_h = _entriesPerColumn * _lineHeight + 2;
|
||||
_w = 2 * _w + 10;
|
||||
|
||||
if (!(_w & 1))
|
||||
_w++;
|
||||
|
||||
if (_selection >= _entriesPerColumn) {
|
||||
_x -= _w / 2;
|
||||
_y = _boss->getAbsY() - (_selection - _entriesPerColumn) * _lineHeight;
|
||||
}
|
||||
|
||||
} else {
|
||||
_twoColumns = false;
|
||||
|
||||
_w = MAX<uint16>(_boss->getWidth(), _w + 20);
|
||||
}
|
||||
|
||||
if (_w >= safeArea.width())
|
||||
_w = safeArea.width() - 1;
|
||||
if (_x < safeArea.left)
|
||||
_x = safeArea.left;
|
||||
if (_x + _w >= safeArea.right)
|
||||
_x = safeArea.right - 1 - _w;
|
||||
|
||||
if (_h >= safeArea.height())
|
||||
_h = safeArea.height() - 1;
|
||||
if (_y < safeArea.top)
|
||||
_y = safeArea.top;
|
||||
else if (_y + _h >= safeArea.bottom)
|
||||
_y = safeArea.bottom - 1 - _h;
|
||||
|
||||
// TODO - implement scrolling if we had to move the menu, or if there are too many entries
|
||||
|
||||
_lastRead = -1;
|
||||
|
||||
Dialog::open();
|
||||
}
|
||||
|
||||
void PopUpDialog::reflowLayout() {
|
||||
}
|
||||
|
||||
void PopUpDialog::drawDialog(DrawLayer layerToDraw) {
|
||||
Dialog::drawDialog(layerToDraw);
|
||||
|
||||
int16 x = _x;
|
||||
if (g_gui.useRTL()) {
|
||||
x = g_system->getOverlayWidth() - _x - _w;
|
||||
}
|
||||
|
||||
// Draw the menu border
|
||||
g_gui.theme()->drawWidgetBackground(Common::Rect(x, _y, x + _w, _y + _h), ThemeEngine::kWidgetBackgroundPlain);
|
||||
|
||||
/*if (_twoColumns)
|
||||
g_gui.vLine(_x + _w / 2, _y, _y + _h - 2, g_gui._color);*/
|
||||
|
||||
// Draw the entries
|
||||
int count = _entries.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
drawMenuEntry(i, i == _selection);
|
||||
}
|
||||
|
||||
// The last entry may be empty. Fill it with black.
|
||||
/*if (_twoColumns && (count & 1)) {
|
||||
g_gui.fillRect(_x + 1 + _w / 2, _y + 1 + kLineHeight * (_entriesPerColumn - 1), _w / 2 - 1, kLineHeight, g_gui._bgcolor);
|
||||
}*/
|
||||
}
|
||||
|
||||
void PopUpDialog::handleMouseUp(int x, int y, int button, int clickCount) {
|
||||
int absX = x + getAbsX();
|
||||
int absY = y + getAbsY();
|
||||
|
||||
// Mouse was released. If it wasn't moved much since the original mouse down,
|
||||
// let the popup stay open. If it did move, assume the user made his selection.
|
||||
int dist = (_clickX - absX) * (_clickX - absX) + (_clickY - absY) * (_clickY - absY);
|
||||
if (dist > 3 * 3 || g_system->getMillis() - _openTime > 300) {
|
||||
int item = findItem(x, y);
|
||||
|
||||
// treat separator item as if no item was clicked
|
||||
if (item >= 0 && _entries[item].size() == 0) {
|
||||
item = -1;
|
||||
}
|
||||
|
||||
setResult(item);
|
||||
close();
|
||||
}
|
||||
_clickX = -1;
|
||||
_clickY = -1;
|
||||
_openTime = (uint32)-1;
|
||||
}
|
||||
|
||||
void PopUpDialog::handleMouseWheel(int x, int y, int direction) {
|
||||
if (direction < 0)
|
||||
moveUp();
|
||||
else if (direction > 0)
|
||||
moveDown();
|
||||
}
|
||||
|
||||
void PopUpDialog::handleMouseMoved(int x, int y, int button) {
|
||||
// Compute over which item the mouse is...
|
||||
int item = findItem(x, y);
|
||||
|
||||
// treat separator item as if no item was moused over
|
||||
if (item >= 0 && _entries[item].size() == 0)
|
||||
item = -1;
|
||||
|
||||
if (item == -1 && !isMouseDown()) {
|
||||
setSelection(_initialSelection);
|
||||
return;
|
||||
}
|
||||
|
||||
// ...and update the selection accordingly
|
||||
setSelection(item);
|
||||
if (_lastRead != item && _entries.size() > 0 && item != -1) {
|
||||
read(_entries[item]);
|
||||
_lastRead = item;
|
||||
}
|
||||
}
|
||||
|
||||
void PopUpDialog::handleMouseLeft(int button) {
|
||||
_lastRead = -1;
|
||||
}
|
||||
|
||||
void PopUpDialog::read(const Common::U32String &str) {
|
||||
if (ConfMan.hasKey("tts_enabled", "scummvm") &&
|
||||
ConfMan.getBool("tts_enabled", "scummvm")) {
|
||||
Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
|
||||
if (ttsMan != nullptr)
|
||||
ttsMan->say(str);
|
||||
}
|
||||
}
|
||||
|
||||
void PopUpDialog::handleKeyDown(Common::KeyState state) {
|
||||
if (state.keycode == Common::KEYCODE_ESCAPE) {
|
||||
// Don't change the previous selection
|
||||
setResult(-1);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMouseDown())
|
||||
return;
|
||||
|
||||
switch (state.keycode) {
|
||||
|
||||
case Common::KEYCODE_RETURN:
|
||||
case Common::KEYCODE_KP_ENTER:
|
||||
setResult(_selection);
|
||||
close();
|
||||
break;
|
||||
|
||||
// Keypad & special keys
|
||||
// - if num lock is set, we ignore the keypress
|
||||
// - if num lock is not set, we fall down to the special key case
|
||||
|
||||
case Common::KEYCODE_KP1:
|
||||
if (state.flags & Common::KBD_NUM)
|
||||
break;
|
||||
// fall through
|
||||
case Common::KEYCODE_END:
|
||||
setSelection(_entries.size()-1);
|
||||
break;
|
||||
|
||||
case Common::KEYCODE_KP2:
|
||||
if (state.flags & Common::KBD_NUM)
|
||||
break;
|
||||
// fall through
|
||||
case Common::KEYCODE_DOWN:
|
||||
moveDown();
|
||||
break;
|
||||
|
||||
case Common::KEYCODE_KP7:
|
||||
if (state.flags & Common::KBD_NUM)
|
||||
break;
|
||||
// fall through
|
||||
case Common::KEYCODE_HOME:
|
||||
setSelection(0);
|
||||
break;
|
||||
|
||||
case Common::KEYCODE_KP8:
|
||||
if (state.flags & Common::KBD_NUM)
|
||||
break;
|
||||
// fall through
|
||||
case Common::KEYCODE_UP:
|
||||
moveUp();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PopUpDialog::setPosition(int x, int y) {
|
||||
_x = x;
|
||||
_y = y;
|
||||
}
|
||||
|
||||
void PopUpDialog::setPadding(int left, int right) {
|
||||
_leftPadding = left;
|
||||
_rightPadding = right;
|
||||
}
|
||||
|
||||
void PopUpDialog::setLineHeight(int lineHeight) {
|
||||
_lineHeight = lineHeight;
|
||||
}
|
||||
|
||||
void PopUpDialog::setWidth(uint16 width) {
|
||||
_w = width;
|
||||
}
|
||||
|
||||
void PopUpDialog::appendEntry(const Common::U32String &entry) {
|
||||
_entries.push_back(entry);
|
||||
}
|
||||
|
||||
void PopUpDialog::clearEntries() {
|
||||
_entries.clear();
|
||||
}
|
||||
|
||||
int PopUpDialog::findItem(int x, int y) const {
|
||||
if (x >= 0 && x < _w && y >= 0 && y < _h) {
|
||||
if (_twoColumns) {
|
||||
uint entry = (y - 2) / _lineHeight;
|
||||
if (x > _w / 2) {
|
||||
entry += _entriesPerColumn;
|
||||
|
||||
if (entry >= _entries.size())
|
||||
return -1;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
return (y - 2) / _lineHeight;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void PopUpDialog::setSelection(int item) {
|
||||
if (item != _selection) {
|
||||
// Undraw old selection
|
||||
if (_selection >= 0)
|
||||
drawMenuEntry(_selection, false);
|
||||
|
||||
// Change selection
|
||||
_selection = item;
|
||||
|
||||
// Draw new selection
|
||||
if (item >= 0)
|
||||
drawMenuEntry(item, true);
|
||||
}
|
||||
}
|
||||
|
||||
bool PopUpDialog::isMouseDown() {
|
||||
// TODO/FIXME - need a way to determine whether any mouse buttons are pressed or not.
|
||||
// Sure, we could just count mouse button up/down events, but that is cumbersome and
|
||||
// error prone. Would be much nicer to add an API to OSystem for this...
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void PopUpDialog::moveUp() {
|
||||
if (_selection < 0) {
|
||||
setSelection(_entries.size() - 1);
|
||||
} else if (_selection > 0) {
|
||||
int item = _selection;
|
||||
do {
|
||||
item--;
|
||||
} while (item >= 0 && _entries[item].size() == 0);
|
||||
if (item >= 0)
|
||||
setSelection(item);
|
||||
}
|
||||
}
|
||||
|
||||
void PopUpDialog::moveDown() {
|
||||
int lastItem = _entries.size() - 1;
|
||||
|
||||
if (_selection < 0) {
|
||||
setSelection(0);
|
||||
} else if (_selection < lastItem) {
|
||||
int item = _selection;
|
||||
do {
|
||||
item++;
|
||||
} while (item <= lastItem && _entries[item].size() == 0);
|
||||
if (item <= lastItem)
|
||||
setSelection(item);
|
||||
}
|
||||
}
|
||||
|
||||
void PopUpDialog::drawMenuEntry(int entry, bool hilite) {
|
||||
// Draw one entry of the popup menu, including selection
|
||||
assert(entry >= 0);
|
||||
int x, y, w;
|
||||
|
||||
if (_twoColumns) {
|
||||
int n = _entries.size() / 2;
|
||||
|
||||
if (_entries.size() & 1)
|
||||
n++;
|
||||
|
||||
if (entry >= n) {
|
||||
x = _x + 1 + _w / 2;
|
||||
y = _y + 1 + _lineHeight * (entry - n);
|
||||
} else {
|
||||
x = _x + 1;
|
||||
y = _y + 1 + _lineHeight * entry;
|
||||
}
|
||||
|
||||
w = _w / 2 - 1;
|
||||
} else {
|
||||
x = _x + 1;
|
||||
y = _y + 1 + _lineHeight * entry;
|
||||
w = _w - 2;
|
||||
}
|
||||
|
||||
Common::U32String &name(_entries[entry]);
|
||||
|
||||
Common::Rect r1(x, y, x + w, y + _lineHeight);
|
||||
Common::Rect r2(x + 1, y + 2, x + w, y + 2 + _lineHeight);
|
||||
Graphics::TextAlign alignment = Graphics::kTextAlignLeft;
|
||||
int pad = _leftPadding;
|
||||
|
||||
if (g_gui.useRTL()) {
|
||||
const int16 screenW = g_system->getOverlayWidth();
|
||||
r1.left = screenW - r1.left - w;
|
||||
r1.right = r1.left + w;
|
||||
r2.left = screenW - r2.left - w;
|
||||
r2.right = r2.left + w;
|
||||
|
||||
alignment = Graphics::kTextAlignRight;
|
||||
pad = _rightPadding;
|
||||
}
|
||||
|
||||
if (name.size() == 0) {
|
||||
// Draw a separator
|
||||
g_gui.theme()->drawLineSeparator(r1);
|
||||
} else {
|
||||
g_gui.theme()->drawText(
|
||||
r2,
|
||||
name, hilite ? ThemeEngine::kStateHighlight : ThemeEngine::kStateEnabled,
|
||||
alignment, ThemeEngine::kTextInversionNone, pad, false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
|
||||
//
|
||||
// PopUpWidget
|
||||
//
|
||||
|
||||
PopUpWidget::PopUpWidget(GuiObject *boss, const Common::String &name, const Common::U32String &tooltip, uint32 cmd)
|
||||
: Widget(boss, name, tooltip), CommandSender(boss) {
|
||||
setFlags(WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS | WIDGET_IGNORE_DRAG);
|
||||
_type = kPopUpWidget;
|
||||
_cmd = cmd;
|
||||
|
||||
_selectedItem = -1;
|
||||
_leftPadding = _rightPadding = 0;
|
||||
}
|
||||
|
||||
PopUpWidget::PopUpWidget(GuiObject *boss, int x, int y, int w, int h, const Common::U32String &tooltip, uint32 cmd)
|
||||
: Widget(boss, x, y, w, h, tooltip), CommandSender(boss) {
|
||||
setFlags(WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS | WIDGET_IGNORE_DRAG);
|
||||
_type = kPopUpWidget;
|
||||
_cmd = cmd;
|
||||
|
||||
_selectedItem = -1;
|
||||
|
||||
_leftPadding = _rightPadding = 0;
|
||||
}
|
||||
|
||||
void PopUpWidget::handleMouseDown(int x, int y, int button, int clickCount) {
|
||||
if (isEnabled()) {
|
||||
PopUpDialog popupDialog(this, "", x + getAbsX(), y + getAbsY());
|
||||
popupDialog.setPosition(getAbsX(), getAbsY() - _selectedItem * kLineHeight);
|
||||
popupDialog.setPadding(_leftPadding, _rightPadding);
|
||||
popupDialog.setWidth(getWidth() - kLineHeight + 2);
|
||||
|
||||
|
||||
for (uint i = 0; i < _entries.size(); i++) {
|
||||
popupDialog.appendEntry(_entries[i].name);
|
||||
}
|
||||
popupDialog.setSelection(_selectedItem);
|
||||
|
||||
int newSel = popupDialog.runModal();
|
||||
if (newSel != -1 && _selectedItem != newSel) {
|
||||
_selectedItem = newSel;
|
||||
sendCommand(_cmd, _entries[_selectedItem].tag);
|
||||
markAsDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PopUpWidget::handleMouseWheel(int x, int y, int direction) {
|
||||
if (isEnabled()) {
|
||||
int newSelection = _selectedItem + direction;
|
||||
|
||||
// Skip separator entries
|
||||
while ((newSelection >= 0) && (newSelection < (int)_entries.size()) &&
|
||||
_entries[newSelection].name.empty()) {
|
||||
newSelection += direction;
|
||||
}
|
||||
|
||||
// Just update the selected item when we're in range
|
||||
if ((newSelection >= 0) && (newSelection < (int)_entries.size()) &&
|
||||
(newSelection != _selectedItem)) {
|
||||
_selectedItem = newSelection;
|
||||
sendCommand(_cmd, _entries[_selectedItem].tag);
|
||||
markAsDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PopUpWidget::reflowLayout() {
|
||||
_leftPadding = g_gui.xmlEval()->getVar("Globals.PopUpWidget.Padding.Left", 0);
|
||||
_rightPadding = g_gui.xmlEval()->getVar("Globals.PopUpWidget.Padding.Right", 0);
|
||||
|
||||
Widget::reflowLayout();
|
||||
}
|
||||
|
||||
void PopUpWidget::appendEntry(const Common::U32String &entry, uint32 tag) {
|
||||
Entry e;
|
||||
e.name = entry;
|
||||
e.tag = tag;
|
||||
_entries.push_back(e);
|
||||
}
|
||||
|
||||
void PopUpWidget::appendEntry(const Common::String &entry, uint32 tag) {
|
||||
appendEntry(Common::U32String(entry), tag);
|
||||
}
|
||||
|
||||
void PopUpWidget::clearEntries() {
|
||||
_entries.clear();
|
||||
_selectedItem = -1;
|
||||
}
|
||||
|
||||
void PopUpWidget::setSelected(int item) {
|
||||
if (item != _selectedItem) {
|
||||
if (item >= 0 && item < (int)_entries.size()) {
|
||||
_selectedItem = item;
|
||||
} else {
|
||||
_selectedItem = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PopUpWidget::setSelectedTag(uint32 tag) {
|
||||
uint item;
|
||||
for (item = 0; item < _entries.size(); ++item) {
|
||||
if (_entries[item].tag == tag) {
|
||||
setSelected(item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PopUpWidget::drawWidget() {
|
||||
Common::U32String sel;
|
||||
if (_selectedItem >= 0)
|
||||
sel = _entries[_selectedItem].name;
|
||||
|
||||
int pad = _leftPadding;
|
||||
|
||||
if (g_gui.useRTL() && _useRTL)
|
||||
pad = _rightPadding;
|
||||
|
||||
g_gui.theme()->drawPopUpWidget(Common::Rect(_x, _y, _x + _w, _y + _h), sel, pad, _state, (g_gui.useRTL() && _useRTL));
|
||||
}
|
||||
|
||||
} // End of namespace GUI
|
||||
142
gui/widgets/popup.h
Normal file
142
gui/widgets/popup.h
Normal file
@@ -0,0 +1,142 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GUI_WIDGETS_POPUP_H
|
||||
#define GUI_WIDGETS_POPUP_H
|
||||
|
||||
#include "gui/dialog.h"
|
||||
#include "gui/widget.h"
|
||||
#include "common/str.h"
|
||||
#include "common/array.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
/**
|
||||
* Popup or dropdown widget which, when clicked, "pop up" a list of items and
|
||||
* lets the user pick on of them.
|
||||
*
|
||||
* Implementation wise, when the user selects an item, then the specified command
|
||||
* is broadcast, with data being equal to the tag value of the selected entry.
|
||||
*/
|
||||
class PopUpWidget : public Widget, public CommandSender {
|
||||
struct Entry {
|
||||
Common::U32String name;
|
||||
uint32 tag;
|
||||
};
|
||||
typedef Common::Array<Entry> EntryList;
|
||||
protected:
|
||||
EntryList _entries;
|
||||
int _selectedItem;
|
||||
|
||||
int _leftPadding;
|
||||
int _rightPadding;
|
||||
uint32 _cmd;
|
||||
|
||||
public:
|
||||
PopUpWidget(GuiObject *boss, const Common::String &name, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0);
|
||||
PopUpWidget(GuiObject *boss, int x, int y, int w, int h, const Common::U32String &tooltip = Common::U32String(), uint32 cmd = 0);
|
||||
|
||||
void handleMouseDown(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseWheel(int x, int y, int direction) override;
|
||||
|
||||
void appendEntry(const Common::U32String &entry, uint32 tag = (uint32)-1);
|
||||
void appendEntry(const Common::String &entry, uint32 tag = (uint32)-1);
|
||||
void clearEntries();
|
||||
int numEntries() { return _entries.size(); }
|
||||
|
||||
/** Select the entry at the given index. */
|
||||
void setSelected(int item);
|
||||
|
||||
/** Select the first entry matching the given tag. */
|
||||
void setSelectedTag(uint32 tag);
|
||||
|
||||
int getSelected() const { return _selectedItem; }
|
||||
uint32 getSelectedTag() const { return (_selectedItem >= 0) ? _entries[_selectedItem].tag : (uint32)-1; }
|
||||
// const String& getSelectedString() const { return (_selectedItem >= 0) ? _entries[_selectedItem].name : String::emptyString; }
|
||||
|
||||
void handleMouseEntered(int button) override { if (_selectedItem != -1) read(_entries[_selectedItem].name); setFlags(WIDGET_HILITED); markAsDirty(); }
|
||||
void handleMouseLeft(int button) override { clearFlags(WIDGET_HILITED); markAsDirty(); }
|
||||
|
||||
void reflowLayout() override;
|
||||
protected:
|
||||
void drawWidget() override;
|
||||
};
|
||||
|
||||
/**
|
||||
* A small dialog showing a list of items and allowing the user to chose one of them
|
||||
*
|
||||
* Used by PopUpWidget and DropdownButtonWidget.
|
||||
*/
|
||||
class PopUpDialog : public Dialog {
|
||||
protected:
|
||||
Widget *_boss;
|
||||
int _clickX, _clickY;
|
||||
int _selection;
|
||||
int _initialSelection;
|
||||
uint32 _openTime;
|
||||
bool _twoColumns;
|
||||
int _entriesPerColumn;
|
||||
|
||||
int _leftPadding;
|
||||
int _rightPadding;
|
||||
int _lineHeight;
|
||||
|
||||
int _lastRead;
|
||||
|
||||
typedef Common::Array<Common::U32String> EntryList;
|
||||
EntryList _entries;
|
||||
|
||||
public:
|
||||
PopUpDialog(Widget *boss, const Common::String &name, int clickX, int clickY);
|
||||
|
||||
void open() override;
|
||||
void reflowLayout() override;
|
||||
void drawDialog(DrawLayer layerToDraw) override;
|
||||
|
||||
void handleMouseUp(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseWheel(int x, int y, int direction) override; // Scroll through entries with scroll wheel
|
||||
void handleMouseMoved(int x, int y, int button) override; // Redraw selections depending on mouse position
|
||||
void handleMouseLeft(int button) override;
|
||||
void handleKeyDown(Common::KeyState state) override; // Scroll through entries with arrow keys etc.
|
||||
|
||||
void setPosition(int x, int y);
|
||||
void setPadding(int left, int right);
|
||||
void setLineHeight(int lineHeight);
|
||||
void setWidth(uint16 width);
|
||||
|
||||
void appendEntry(const Common::U32String &entry);
|
||||
void clearEntries();
|
||||
void setSelection(int item);
|
||||
|
||||
protected:
|
||||
void drawMenuEntry(int entry, bool hilite);
|
||||
|
||||
int findItem(int x, int y) const;
|
||||
bool isMouseDown();
|
||||
|
||||
void moveUp();
|
||||
void moveDown();
|
||||
void read(const Common::U32String &str);
|
||||
};
|
||||
|
||||
} // End of namespace GUI
|
||||
|
||||
#endif
|
||||
272
gui/widgets/richtext.cpp
Normal file
272
gui/widgets/richtext.cpp
Normal file
@@ -0,0 +1,272 @@
|
||||
/* 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/system.h"
|
||||
#include "common/unicode-bidi.h"
|
||||
|
||||
#include "graphics/macgui/mactext.h"
|
||||
|
||||
#include "gui/gui-manager.h"
|
||||
|
||||
#include "gui/ThemeEngine.h"
|
||||
#include "gui/ThemeEval.h"
|
||||
|
||||
#include "gui/widgets/richtext.h"
|
||||
#include "gui/widgets/scrollbar.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
const Graphics::TTFMap ttfFamily[] = {
|
||||
{"NotoSans-Regular.ttf", Graphics::kMacFontRegular},
|
||||
{"NotoSans-Bold.ttf", Graphics::kMacFontBold},
|
||||
{"NotoSerif-Italic.ttf", Graphics::kMacFontItalic},
|
||||
{"NotoSerif-Bold-Italic.ttf", Graphics::kMacFontBold | Graphics::kMacFontItalic},
|
||||
{nullptr, 0}
|
||||
};
|
||||
|
||||
RichTextWidget::RichTextWidget(GuiObject *boss, int x, int y, int w, int h, bool scale, const Common::U32String &text, const Common::U32String &tooltip)
|
||||
: Widget(boss, x, y, w, h, scale, tooltip), CommandSender(nullptr) {
|
||||
|
||||
_text = text;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
RichTextWidget::RichTextWidget(GuiObject *boss, int x, int y, int w, int h, const Common::U32String &text, const Common::U32String &tooltip)
|
||||
: RichTextWidget(boss, x, y, w, h, false, text, tooltip) {
|
||||
}
|
||||
|
||||
RichTextWidget::RichTextWidget(GuiObject *boss, const Common::String &name, const Common::U32String &text, const Common::U32String &tooltip)
|
||||
: Widget(boss, name, tooltip), CommandSender(nullptr) {
|
||||
|
||||
_text = text;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
void RichTextWidget::init() {
|
||||
setFlags(WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_TRACK_MOUSE | WIDGET_DYN_TOOLTIP);
|
||||
|
||||
_type = kRichTextWidget;
|
||||
|
||||
_verticalScroll = new ScrollBarWidget(this, _w - 16, 0, 16, _h);
|
||||
_verticalScroll->setTarget(this);
|
||||
_scrolledX = 0;
|
||||
_scrolledY = 0;
|
||||
|
||||
_innerMargin = g_gui.xmlEval()->getVar("Globals.RichTextWidget.InnerMargin", 0);
|
||||
_scrollbarWidth = g_gui.xmlEval()->getVar("Globals.Scrollbar.Width", 0);
|
||||
|
||||
_textWidth = MAX(1, _w - _scrollbarWidth - 2 * _innerMargin);
|
||||
_textHeight = MAX(1, _h - 2 * _innerMargin);
|
||||
|
||||
_limitH = 140;
|
||||
}
|
||||
|
||||
|
||||
RichTextWidget::~RichTextWidget() {
|
||||
delete _txtWnd;
|
||||
|
||||
if (_surface)
|
||||
_surface->free();
|
||||
|
||||
delete _surface;
|
||||
}
|
||||
|
||||
void RichTextWidget::handleMouseWheel(int x, int y, int direction) {
|
||||
_verticalScroll->handleMouseWheel(x, y, direction);
|
||||
}
|
||||
|
||||
void RichTextWidget::handleMouseDown(int x, int y, int button, int clickCount) {
|
||||
_mouseDownY = _mouseDownStartY = y;
|
||||
}
|
||||
|
||||
void RichTextWidget::handleMouseUp(int x, int y, int button, int clickCount) {
|
||||
// Allow some tiny finger slipping
|
||||
if (ABS(_mouseDownY - _mouseDownStartY) > 5) {
|
||||
_mouseDownY = _mouseDownStartY = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_mouseDownY = _mouseDownStartY = 0;
|
||||
|
||||
Common::String link = _txtWnd->getMouseLink(x - _innerMargin + _scrolledX, y - _innerMargin + _scrolledY).encode();
|
||||
|
||||
if (link.hasPrefixIgnoreCase("http"))
|
||||
g_system->openUrl(link);
|
||||
}
|
||||
|
||||
void RichTextWidget::handleMouseMoved(int x, int y, int button) {
|
||||
if (_mouseDownStartY == 0 || _mouseDownY == y)
|
||||
return;
|
||||
|
||||
int h = _txtWnd->getTextHeight();
|
||||
int prevScrolledY = _scrolledY;
|
||||
|
||||
_scrolledY = CLIP(_scrolledY - (y - _mouseDownY), 0, h);
|
||||
|
||||
_mouseDownY = y;
|
||||
|
||||
if (_scrolledY == prevScrolledY)
|
||||
return;
|
||||
|
||||
recalc();
|
||||
_verticalScroll->recalc();
|
||||
markAsDirty();
|
||||
}
|
||||
|
||||
void RichTextWidget::handleTooltipUpdate(int x, int y) {
|
||||
_tooltip = _txtWnd->getMouseLink(x - _innerMargin + _scrolledX, y - _innerMargin + _scrolledY);
|
||||
}
|
||||
|
||||
void RichTextWidget::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
|
||||
Widget::handleCommand(sender, cmd, data);
|
||||
switch (cmd) {
|
||||
case kSetPositionCmd:
|
||||
_scrolledY = _verticalScroll->_currentPos;
|
||||
reflowLayout();
|
||||
g_gui.scheduleTopDialogRedraw();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RichTextWidget::recalc() {
|
||||
_innerMargin = g_gui.xmlEval()->getVar("Globals.RichTextWidget.InnerMargin", 0);
|
||||
_scrollbarWidth = g_gui.xmlEval()->getVar("Globals.Scrollbar.Width", 0);
|
||||
_textWidth = MAX(1, _w - _scrollbarWidth - 2 * _innerMargin);
|
||||
_textHeight = MAX(1, _h - 2 * _innerMargin);
|
||||
_limitH = _textHeight;
|
||||
|
||||
// Workaround: Currently Graphics::MacText::setMaxWidth does not work well.
|
||||
// There is a known limitation that the size is skipped when the text contains table,
|
||||
// and there is also an issue with the font.
|
||||
// So for now we recreate the widget.
|
||||
// if (_txtWnd) {
|
||||
// _txtWnd->setMaxWidth(_textWidth);
|
||||
// if (_surface->w != _textWidth || _surface->h != _textHeight)
|
||||
// _surface->create(_textWidth, _textHeight, g_gui.getWM()->_pixelformat);
|
||||
// } else {
|
||||
// createWidget();
|
||||
// }
|
||||
if (!_surface || _surface->w != _textWidth) {
|
||||
delete _txtWnd;
|
||||
createWidget();
|
||||
} else if (_surface->h != _textHeight)
|
||||
_surface->create(_textWidth, _textHeight, g_gui.getWM()->_pixelformat);
|
||||
|
||||
int h = _txtWnd->getTextHeight();
|
||||
|
||||
if (h <= _limitH) _scrolledY = 0;
|
||||
if (_scrolledY > h - _limitH) _scrolledY = MAX(0, h - _limitH);
|
||||
|
||||
_verticalScroll->_numEntries = h;
|
||||
_verticalScroll->_currentPos = _scrolledY;
|
||||
_verticalScroll->_entriesPerPage = _limitH;
|
||||
_verticalScroll->_singleStep = _h / 4;
|
||||
_verticalScroll->setPos(_w - _scrollbarWidth, 0);
|
||||
_verticalScroll->setSize(_scrollbarWidth, _h - 1);
|
||||
}
|
||||
|
||||
void RichTextWidget::createWidget() {
|
||||
Graphics::MacWindowManager *wm = g_gui.getWM();
|
||||
|
||||
uint32 bg = wm->_pixelformat.ARGBToColor(0, 0xff, 0xff, 0xff); // transparent
|
||||
TextColorData *normal = g_gui.theme()->getTextColorData(kTextColorNormal);
|
||||
uint32 fg = wm->_pixelformat.RGBToColor(normal->r, normal->g, normal->b);
|
||||
|
||||
const int fontHeight = g_gui.xmlEval()->getVar("Globals.Font.Height", 25);
|
||||
|
||||
int newId;
|
||||
if (ConfMan.hasKey("gui_language") && !ConfMan.get("gui_language").empty())
|
||||
// MacFONTs do not contain diacritic marks or non-English characters, so we have to use TTF instead
|
||||
newId = wm->_fontMan->registerTTFFont(ttfFamily);
|
||||
else
|
||||
newId = Graphics::kMacFontNewYork;
|
||||
Graphics::MacFont macFont(newId, fontHeight, Graphics::kMacFontRegular);
|
||||
|
||||
_txtWnd = new Graphics::MacText(Common::U32String(), wm, &macFont, fg, bg, _textWidth, Graphics::kTextAlignLeft);
|
||||
|
||||
if (!_imageArchive.empty())
|
||||
_txtWnd->setImageArchive(_imageArchive);
|
||||
|
||||
_txtWnd->setMarkdownText(_text);
|
||||
|
||||
if (_surface)
|
||||
_surface->create(_textWidth, _textHeight, g_gui.getWM()->_pixelformat);
|
||||
else
|
||||
_surface = new Graphics::ManagedSurface(_textWidth, _textHeight, wm->_pixelformat);
|
||||
}
|
||||
|
||||
void RichTextWidget::reflowLayout() {
|
||||
Widget::reflowLayout();
|
||||
|
||||
recalc();
|
||||
|
||||
_verticalScroll->setVisible(_verticalScroll->_numEntries > _limitH); //show when there is something to scroll
|
||||
_verticalScroll->recalc();
|
||||
}
|
||||
|
||||
void RichTextWidget::drawWidget() {
|
||||
if (!_txtWnd)
|
||||
recalc();
|
||||
|
||||
g_gui.theme()->drawWidgetBackground(Common::Rect(_x, _y, _x + _w, _y + _h), ThemeEngine::kWidgetBackgroundPlain);
|
||||
|
||||
_surface->clear(g_gui.getWM()->_pixelformat.ARGBToColor(0, 0xff, 0xff, 0xff)); // transparent
|
||||
|
||||
_txtWnd->draw(_surface, 0, _scrolledY, _textWidth, _textHeight, 0, 0);
|
||||
|
||||
g_gui.theme()->drawManagedSurface(Common::Point(_x + _innerMargin, _y + _innerMargin), *_surface, Graphics::ALPHA_FULL);
|
||||
}
|
||||
|
||||
void RichTextWidget::draw() {
|
||||
Widget::draw();
|
||||
|
||||
if (_verticalScroll->isVisible()) {
|
||||
_verticalScroll->draw();
|
||||
}
|
||||
}
|
||||
|
||||
void RichTextWidget::markAsDirty() {
|
||||
Widget::markAsDirty();
|
||||
|
||||
if (_verticalScroll->isVisible()) {
|
||||
_verticalScroll->markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
bool RichTextWidget::containsWidget(Widget *w) const {
|
||||
if (w == _verticalScroll || _verticalScroll->containsWidget(w))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
Widget *RichTextWidget::findWidget(int x, int y) {
|
||||
if (_verticalScroll->isVisible() && x >= _w - _scrollbarWidth)
|
||||
return _verticalScroll;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
} // End of namespace GUI
|
||||
89
gui/widgets/richtext.h
Normal file
89
gui/widgets/richtext.h
Normal file
@@ -0,0 +1,89 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GUI_WIDGETS_RICHTEXT_H
|
||||
#define GUI_WIDGETS_RICHTEXT_H
|
||||
|
||||
#include "common/str.h"
|
||||
#include "gui/widget.h"
|
||||
|
||||
namespace Graphics {
|
||||
class MacText;
|
||||
class ManagedSurface;
|
||||
}
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class ScrollBarWidget;
|
||||
|
||||
/* RichTextWidget */
|
||||
class RichTextWidget : public Widget, public CommandSender {
|
||||
protected:
|
||||
Graphics::MacText *_txtWnd = nullptr;
|
||||
Graphics::ManagedSurface *_surface = nullptr;
|
||||
Common::U32String _text;
|
||||
|
||||
ScrollBarWidget *_verticalScroll;
|
||||
int16 _scrolledX, _scrolledY;
|
||||
int _mouseDownY = 0;
|
||||
int _mouseDownStartY = 0;
|
||||
int _innerMargin;
|
||||
int _scrollbarWidth;
|
||||
uint16 _limitH;
|
||||
int _textWidth;
|
||||
int _textHeight;
|
||||
|
||||
Common::Path _imageArchive;
|
||||
|
||||
public:
|
||||
RichTextWidget(GuiObject *boss, int x, int y, int w, int h, bool scale, const Common::U32String &text, const Common::U32String &tooltip = Common::U32String());
|
||||
RichTextWidget(GuiObject *boss, int x, int y, int w, int h, const Common::U32String &text, const Common::U32String &tooltip = Common::U32String());
|
||||
RichTextWidget(GuiObject *boss, const Common::String &name, const Common::U32String &text, const Common::U32String &tooltip = Common::U32String());
|
||||
|
||||
~RichTextWidget();
|
||||
|
||||
void reflowLayout() override;
|
||||
void draw() override;
|
||||
|
||||
void handleCommand(CommandSender *sender, uint32 cmd, uint32 data) override;
|
||||
void handleMouseWheel(int x, int y, int direction) override;
|
||||
void handleMouseDown(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseUp(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseMoved(int x, int y, int button) override;
|
||||
void handleTooltipUpdate(int x, int y) override;
|
||||
|
||||
void markAsDirty() override;
|
||||
|
||||
bool containsWidget(Widget *) const override;
|
||||
|
||||
void setImageArchive(const Common::Path &fname) { _imageArchive = fname; }
|
||||
|
||||
protected:
|
||||
void init();
|
||||
void recalc();
|
||||
void drawWidget() override;
|
||||
void createWidget();
|
||||
Widget *findWidget(int x, int y) override;
|
||||
};
|
||||
|
||||
} // End of namespace GUI
|
||||
|
||||
#endif
|
||||
209
gui/widgets/scrollbar.cpp
Normal file
209
gui/widgets/scrollbar.cpp
Normal file
@@ -0,0 +1,209 @@
|
||||
/* 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/timer.h"
|
||||
#include "gui/widgets/scrollbar.h"
|
||||
#include "gui/gui-manager.h"
|
||||
#include "gui/ThemeEngine.h"
|
||||
#include "gui/widgets/scrollcontainer.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
#define UP_DOWN_BOX_HEIGHT (_w+1)
|
||||
|
||||
ScrollBarWidget::ScrollBarWidget(GuiObject *boss, int x, int y, int w, int h)
|
||||
: Widget (boss, x, y, w, h), CommandSender(boss) {
|
||||
setFlags(WIDGET_ENABLED | WIDGET_TRACK_MOUSE | WIDGET_CLEARBG | WIDGET_WANT_TICKLE);
|
||||
_type = kScrollBarWidget;
|
||||
|
||||
_part = kNoPart;
|
||||
_sliderHeight = 0;
|
||||
_sliderPos = 0;
|
||||
|
||||
_draggingPart = kNoPart;
|
||||
_sliderDeltaMouseDownPos = 0;
|
||||
|
||||
_numEntries = 0;
|
||||
_entriesPerPage = 0;
|
||||
_currentPos = 0;
|
||||
_singleStep = 1;
|
||||
|
||||
_repeatTimer = 0;
|
||||
}
|
||||
|
||||
void ScrollBarWidget::handleMouseDown(int x, int y, int button, int clickCount) {
|
||||
int old_pos = _currentPos;
|
||||
|
||||
// Do nothing if there are less items than fit on one page
|
||||
if (_numEntries <= _entriesPerPage)
|
||||
return;
|
||||
|
||||
if (y <= UP_DOWN_BOX_HEIGHT) {
|
||||
// Up arrow
|
||||
_currentPos -= _singleStep;
|
||||
_repeatTimer = g_system->getMillis() + kRepeatInitialDelay;
|
||||
_draggingPart = kUpArrowPart;
|
||||
} else if (y >= _h - UP_DOWN_BOX_HEIGHT) {
|
||||
// Down arrow
|
||||
_currentPos += _singleStep;
|
||||
_repeatTimer = g_system->getMillis() + kRepeatInitialDelay;
|
||||
_draggingPart = kDownArrowPart;
|
||||
} else if (y < _sliderPos) {
|
||||
_currentPos -= _entriesPerPage - 1;
|
||||
} else if (y >= _sliderPos + _sliderHeight) {
|
||||
_currentPos += _entriesPerPage - 1;
|
||||
} else {
|
||||
_draggingPart = kSliderPart;
|
||||
_sliderDeltaMouseDownPos = y - _sliderPos;
|
||||
}
|
||||
|
||||
// Make sure that _currentPos is still inside the bounds
|
||||
checkBounds(old_pos);
|
||||
}
|
||||
|
||||
void ScrollBarWidget::handleMouseUp(int x, int y, int button, int clickCount) {
|
||||
_draggingPart = kNoPart;
|
||||
_repeatTimer = 0;
|
||||
}
|
||||
|
||||
void ScrollBarWidget::handleMouseWheel(int x, int y, int direction) {
|
||||
int old_pos = _currentPos;
|
||||
|
||||
if (_numEntries < _entriesPerPage)
|
||||
return;
|
||||
|
||||
if (direction < 0) {
|
||||
_currentPos -= _singleStep;
|
||||
} else {
|
||||
_currentPos += _singleStep;
|
||||
}
|
||||
|
||||
// Make sure that _currentPos is still inside the bounds
|
||||
checkBounds(old_pos);
|
||||
}
|
||||
|
||||
void ScrollBarWidget::handleMouseMoved(int x, int y, int button) {
|
||||
// Do nothing if there are less items than fit on one page
|
||||
if (_numEntries <= _entriesPerPage)
|
||||
return;
|
||||
|
||||
if (_draggingPart == kSliderPart) {
|
||||
int old_pos = _currentPos;
|
||||
_sliderPos = y - _sliderDeltaMouseDownPos;
|
||||
|
||||
if (_sliderPos < UP_DOWN_BOX_HEIGHT)
|
||||
_sliderPos = UP_DOWN_BOX_HEIGHT;
|
||||
|
||||
if (_sliderPos > _h - UP_DOWN_BOX_HEIGHT - _sliderHeight)
|
||||
_sliderPos = _h - UP_DOWN_BOX_HEIGHT - _sliderHeight;
|
||||
|
||||
_currentPos =
|
||||
(_sliderPos - UP_DOWN_BOX_HEIGHT) * (_numEntries - _entriesPerPage) / (_h - 2 * UP_DOWN_BOX_HEIGHT - _sliderHeight);
|
||||
checkBounds(old_pos);
|
||||
} else {
|
||||
int old_part = _part;
|
||||
|
||||
if (y <= UP_DOWN_BOX_HEIGHT) // Up arrow
|
||||
_part = kUpArrowPart;
|
||||
else if (y >= _h - UP_DOWN_BOX_HEIGHT) // Down arrow
|
||||
_part = kDownArrowPart;
|
||||
else if (y < _sliderPos)
|
||||
_part = kPageUpPart;
|
||||
else if (y >= _sliderPos + _sliderHeight)
|
||||
_part = kPageDownPart;
|
||||
else
|
||||
_part = kSliderPart;
|
||||
|
||||
if (old_part != _part)
|
||||
markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBarWidget::handleTickle() {
|
||||
if (_repeatTimer) {
|
||||
const uint32 curTime = g_system->getMillis();
|
||||
if (curTime >= _repeatTimer) {
|
||||
const int old_pos = _currentPos;
|
||||
|
||||
if (_part == kUpArrowPart)
|
||||
_currentPos -= 3 * _singleStep;
|
||||
else if (_part == kDownArrowPart)
|
||||
_currentPos += 3 * _singleStep;
|
||||
|
||||
checkBounds(old_pos);
|
||||
|
||||
_repeatTimer = curTime + kRepeatDelay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBarWidget::checkBounds(int old_pos) {
|
||||
if (_numEntries <= _entriesPerPage || _currentPos < 0)
|
||||
_currentPos = 0;
|
||||
else if (_currentPos > _numEntries - _entriesPerPage)
|
||||
_currentPos = _numEntries - _entriesPerPage;
|
||||
|
||||
if (old_pos != _currentPos) {
|
||||
recalc();
|
||||
markAsDirty();
|
||||
sendCommand(kSetPositionCmd, _currentPos);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBarWidget::recalc() {
|
||||
if (_numEntries > _entriesPerPage) {
|
||||
_sliderHeight = (_h - 2 * UP_DOWN_BOX_HEIGHT) * _entriesPerPage / _numEntries;
|
||||
if (_sliderHeight < UP_DOWN_BOX_HEIGHT)
|
||||
_sliderHeight = UP_DOWN_BOX_HEIGHT;
|
||||
|
||||
_sliderPos =
|
||||
UP_DOWN_BOX_HEIGHT + (_h - 2 * UP_DOWN_BOX_HEIGHT - _sliderHeight) * _currentPos / (_numEntries - _entriesPerPage);
|
||||
if (_sliderPos < 0)
|
||||
_sliderPos = 0;
|
||||
setVisible(true);
|
||||
} else {
|
||||
_sliderHeight = _h - 2 * UP_DOWN_BOX_HEIGHT;
|
||||
_sliderPos = UP_DOWN_BOX_HEIGHT;
|
||||
setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBarWidget::drawWidget() {
|
||||
if (_draggingPart != kNoPart)
|
||||
_part = _draggingPart;
|
||||
|
||||
ThemeEngine::ScrollbarState state = ThemeEngine::kScrollbarStateNo;
|
||||
if (_numEntries <= _entriesPerPage) {
|
||||
state = ThemeEngine::kScrollbarStateSinglePage;
|
||||
} else if (_part == kUpArrowPart) {
|
||||
state = ThemeEngine::kScrollbarStateUp;
|
||||
} else if (_part == kDownArrowPart) {
|
||||
state = ThemeEngine::kScrollbarStateDown;
|
||||
} else if (_part == kSliderPart) {
|
||||
state = ThemeEngine::kScrollbarStateSlider;
|
||||
}
|
||||
|
||||
g_gui.theme()->drawScrollbar(Common::Rect(_x, _y, _x + _w, _y + _h), _sliderPos, _sliderHeight, state);
|
||||
}
|
||||
|
||||
} // End of namespace GUI
|
||||
89
gui/widgets/scrollbar.h
Normal file
89
gui/widgets/scrollbar.h
Normal file
@@ -0,0 +1,89 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GUI_WIDGETS_SCROLLBAR_H
|
||||
#define GUI_WIDGETS_SCROLLBAR_H
|
||||
|
||||
#include "gui/widget.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
enum {
|
||||
kSetPositionCmd = 'SETP'
|
||||
};
|
||||
|
||||
|
||||
class ScrollBarWidget : public Widget, public CommandSender {
|
||||
protected:
|
||||
typedef enum {
|
||||
kNoPart,
|
||||
kUpArrowPart,
|
||||
kDownArrowPart,
|
||||
kSliderPart,
|
||||
kPageUpPart,
|
||||
kPageDownPart
|
||||
} Part;
|
||||
|
||||
Part _part;
|
||||
int _sliderHeight;
|
||||
int _sliderPos;
|
||||
|
||||
Part _draggingPart;
|
||||
int _sliderDeltaMouseDownPos;
|
||||
|
||||
enum {
|
||||
kRepeatInitialDelay = 500,
|
||||
kRepeatDelay = 100
|
||||
};
|
||||
uint32 _repeatTimer;
|
||||
|
||||
public:
|
||||
int _numEntries;
|
||||
int _entriesPerPage;
|
||||
int _currentPos;
|
||||
int _singleStep;
|
||||
|
||||
public:
|
||||
ScrollBarWidget(GuiObject *boss, int x, int y, int w, int h);
|
||||
|
||||
void handleMouseDown(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseUp(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseWheel(int x, int y, int direction) override;
|
||||
void handleMouseMoved(int x, int y, int button) override;
|
||||
void handleMouseEntered(int button) override { setFlags(WIDGET_HILITED); }
|
||||
void handleMouseLeft(int button) override { clearFlags(WIDGET_HILITED); _part = kNoPart; markAsDirty(); }
|
||||
void handleTickle() override;
|
||||
bool wantsFocus() override { return true; }
|
||||
|
||||
// FIXME - this should be private, but then we also have to add accessors
|
||||
// for _numEntries, _entriesPerPage and _currentPos. This again leads to the question:
|
||||
// should these accessors force a redraw?
|
||||
void recalc();
|
||||
|
||||
void checkBounds(int old_pos);
|
||||
|
||||
protected:
|
||||
void drawWidget() override;
|
||||
};
|
||||
|
||||
} // End of namespace GUI
|
||||
|
||||
#endif
|
||||
184
gui/widgets/scrollcontainer.cpp
Normal file
184
gui/widgets/scrollcontainer.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
/* 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/util.h"
|
||||
#include "gui/widgets/scrollcontainer.h"
|
||||
#include "gui/gui-manager.h"
|
||||
|
||||
#include "gui/ThemeEval.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
ScrollContainerWidget::ScrollContainerWidget(GuiObject *boss, int x, int y, int w, int h, uint32 reflowCmd)
|
||||
: Widget(boss, x, y, w, h), CommandSender(nullptr), _reflowCmd(reflowCmd) {
|
||||
init();
|
||||
}
|
||||
|
||||
ScrollContainerWidget::ScrollContainerWidget(GuiObject *boss, const Common::String &name, const Common::String &dialogName, uint32 reflowCmd)
|
||||
: Widget(boss, name), CommandSender(nullptr), _reflowCmd(reflowCmd), _dialogName(dialogName) {
|
||||
init();
|
||||
}
|
||||
|
||||
void ScrollContainerWidget::init() {
|
||||
setFlags(WIDGET_ENABLED);
|
||||
_type = kScrollContainerWidget;
|
||||
_backgroundType = ThemeEngine::kWidgetBackgroundPlain;
|
||||
_verticalScroll = new ScrollBarWidget(this, _w, 0, 16, _h);
|
||||
_verticalScroll->setTarget(this);
|
||||
_scrolledX = 0;
|
||||
_scrolledY = 0;
|
||||
_limitH = 140;
|
||||
recalc();
|
||||
}
|
||||
|
||||
void ScrollContainerWidget::handleMouseWheel(int x, int y, int direction) {
|
||||
_verticalScroll->handleMouseWheel(x, y, direction);
|
||||
}
|
||||
|
||||
void ScrollContainerWidget::recalc() {
|
||||
_scrollbarWidth = g_gui.xmlEval()->getVar("Globals.Scrollbar.Width", 0);
|
||||
_limitH = _h;
|
||||
|
||||
//calculate virtual height
|
||||
const int spacing = g_gui.xmlEval()->getVar("Globals.Font.Height", 16); //on the bottom
|
||||
int min = spacing, max = 0;
|
||||
Widget *ptr = _firstWidget;
|
||||
while (ptr) {
|
||||
if (ptr != _verticalScroll && ptr->isVisible()) {
|
||||
int y = ptr->getAbsY() - getChildY();
|
||||
min = MIN(min, y - spacing);
|
||||
max = MAX(max, y + ptr->getHeight() + spacing);
|
||||
}
|
||||
ptr = ptr->next();
|
||||
}
|
||||
int h = max - min;
|
||||
|
||||
if (h <= _limitH) _scrolledY = 0;
|
||||
if (_scrolledY > h - _limitH) _scrolledY = 0;
|
||||
|
||||
_verticalScroll->_numEntries = h;
|
||||
_verticalScroll->_currentPos = _scrolledY;
|
||||
_verticalScroll->_entriesPerPage = _limitH;
|
||||
_verticalScroll->_singleStep = kLineHeight;
|
||||
_verticalScroll->setPos(_w, _scrolledY);
|
||||
_verticalScroll->setSize(_scrollbarWidth, _limitH-1);
|
||||
}
|
||||
|
||||
|
||||
ScrollContainerWidget::~ScrollContainerWidget() {}
|
||||
|
||||
int16 ScrollContainerWidget::getChildX() const {
|
||||
return getAbsX() - _scrolledX;
|
||||
}
|
||||
|
||||
int16 ScrollContainerWidget::getChildY() const {
|
||||
return getAbsY() - _scrolledY;
|
||||
}
|
||||
|
||||
uint16 ScrollContainerWidget::getWidth() const {
|
||||
// NOTE: if you change that, make sure to do the same
|
||||
// changes in the ThemeLayoutScrollContainerWidget (gui/ThemeLayout.cpp)
|
||||
// NOTE: this width is used for clipping, so it *includes*
|
||||
// scrollbars, because it starts from getAbsX(), not getChildX()
|
||||
return _w + _scrollbarWidth;
|
||||
}
|
||||
|
||||
uint16 ScrollContainerWidget::getHeight() const {
|
||||
return _limitH;
|
||||
}
|
||||
|
||||
void ScrollContainerWidget::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
|
||||
Widget::handleCommand(sender, cmd, data);
|
||||
switch (cmd) {
|
||||
case kSetPositionCmd:
|
||||
_scrolledY = _verticalScroll->_currentPos;
|
||||
reflowLayout();
|
||||
g_gui.scheduleTopDialogRedraw();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollContainerWidget::reflowLayout() {
|
||||
Widget::reflowLayout();
|
||||
|
||||
if (!_dialogName.empty()) {
|
||||
g_gui.xmlEval()->reflowDialogLayout(_dialogName, _firstWidget);
|
||||
}
|
||||
|
||||
//reflow layout of inner widgets
|
||||
Widget *ptr = _firstWidget;
|
||||
while (ptr) {
|
||||
ptr->reflowLayout();
|
||||
ptr = ptr->next();
|
||||
}
|
||||
|
||||
//hide and move widgets, if needed
|
||||
sendCommand(_reflowCmd, 0);
|
||||
|
||||
//recalculate height
|
||||
recalc();
|
||||
|
||||
_verticalScroll->setVisible(_verticalScroll->_numEntries > _limitH); //show when there is something to scroll
|
||||
_verticalScroll->recalc();
|
||||
}
|
||||
|
||||
void ScrollContainerWidget::drawWidget() {
|
||||
g_gui.theme()->drawWidgetBackground(Common::Rect(_x, _y, _x + _w, _y + getHeight()), _backgroundType);
|
||||
}
|
||||
|
||||
void ScrollContainerWidget::draw() {
|
||||
Widget::draw();
|
||||
|
||||
if (_verticalScroll->isVisible()) {
|
||||
_verticalScroll->draw();
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollContainerWidget::markAsDirty() {
|
||||
Widget::markAsDirty();
|
||||
|
||||
if (_verticalScroll->isVisible()) {
|
||||
_verticalScroll->markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
bool ScrollContainerWidget::containsWidget(Widget *w) const {
|
||||
if (w == _verticalScroll || _verticalScroll->containsWidget(w))
|
||||
return true;
|
||||
return containsWidgetInChain(_firstWidget, w);
|
||||
}
|
||||
|
||||
Widget *ScrollContainerWidget::findWidget(int x, int y) {
|
||||
if (_verticalScroll->isVisible() && x >= _w)
|
||||
return _verticalScroll;
|
||||
Widget *w = Widget::findWidgetInChain(_firstWidget, x + _scrolledX, y + _scrolledY);
|
||||
if (w)
|
||||
return w;
|
||||
return this;
|
||||
}
|
||||
|
||||
void ScrollContainerWidget::setBackgroundType(ThemeEngine::WidgetBackground backgroundType) {
|
||||
_backgroundType = backgroundType;
|
||||
}
|
||||
|
||||
} // End of namespace GUI
|
||||
75
gui/widgets/scrollcontainer.h
Normal file
75
gui/widgets/scrollcontainer.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GUI_WIDGETS_SCROLLCONTAINER_H
|
||||
#define GUI_WIDGETS_SCROLLCONTAINER_H
|
||||
|
||||
#include "gui/widget.h"
|
||||
#include "common/str.h"
|
||||
#include "gui/widgets/scrollbar.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class ScrollContainerWidget: public Widget, public CommandSender {
|
||||
ScrollBarWidget *_verticalScroll;
|
||||
int16 _scrolledX, _scrolledY;
|
||||
int _scrollbarWidth;
|
||||
uint16 _limitH;
|
||||
uint32 _reflowCmd;
|
||||
ThemeEngine::WidgetBackground _backgroundType;
|
||||
Common::String _dialogName;
|
||||
|
||||
void recalc();
|
||||
|
||||
public:
|
||||
ScrollContainerWidget(GuiObject *boss, int x, int y, int w, int h, uint32 reflowCmd = 0);
|
||||
ScrollContainerWidget(GuiObject *boss, const Common::String &name, const Common::String &dialogName, uint32 reflowCmd = 0);
|
||||
~ScrollContainerWidget() override;
|
||||
|
||||
void init();
|
||||
void handleCommand(CommandSender *sender, uint32 cmd, uint32 data) override;
|
||||
void reflowLayout() override;
|
||||
|
||||
bool containsWidget(Widget *) const override;
|
||||
|
||||
void setBackgroundType(ThemeEngine::WidgetBackground backgroundType);
|
||||
|
||||
void handleMouseWheel(int x, int y, int direction) override;
|
||||
|
||||
// We overload getChildY to make sure child widgets are positioned correctly.
|
||||
// Essentially this compensates for the space taken up by the tab title header.
|
||||
int16 getChildX() const override;
|
||||
int16 getChildY() const override;
|
||||
uint16 getWidth() const override;
|
||||
uint16 getHeight() const override;
|
||||
|
||||
void draw() override;
|
||||
void markAsDirty() override;
|
||||
|
||||
protected:
|
||||
void drawWidget() override;
|
||||
|
||||
Widget *findWidget(int x, int y) override;
|
||||
};
|
||||
|
||||
} // End of namespace GUI
|
||||
|
||||
#endif
|
||||
493
gui/widgets/tab.cpp
Normal file
493
gui/widgets/tab.cpp
Normal file
@@ -0,0 +1,493 @@
|
||||
/* 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/util.h"
|
||||
#include "gui/widgets/tab.h"
|
||||
#include "gui/gui-manager.h"
|
||||
#include "gui/widgets/scrollcontainer.h"
|
||||
|
||||
#include "gui/ThemeEval.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
enum {
|
||||
kCmdLeft = 'LEFT',
|
||||
kCmdRight = 'RGHT'
|
||||
};
|
||||
|
||||
void TabWidget::recalc() {
|
||||
_minTabWidth = g_gui.xmlEval()->getVar("Globals.TabWidget.Tab.Width");
|
||||
_tabHeight = g_gui.xmlEval()->getVar("Globals.TabWidget.Tab.Height");
|
||||
_titleVPad = g_gui.xmlEval()->getVar("Globals.TabWidget.Tab.Padding.Top");
|
||||
|
||||
_bodyTP = g_gui.xmlEval()->getVar("Globals.TabWidget.Body.Padding.Top");
|
||||
_bodyBP = g_gui.xmlEval()->getVar("Globals.TabWidget.Body.Padding.Bottom");
|
||||
_bodyLP = g_gui.xmlEval()->getVar("Globals.TabWidget.Body.Padding.Left");
|
||||
_bodyRP = g_gui.xmlEval()->getVar("Globals.TabWidget.Body.Padding.Right");
|
||||
|
||||
_butRP = g_gui.xmlEval()->getVar("Globals.TabWidget.NavButton.Padding.Right", 0);
|
||||
_butTP = g_gui.xmlEval()->getVar("Globals.TabWidget.NavButton.Padding.Top", 0);
|
||||
_butW = g_gui.xmlEval()->getVar("Globals.TabWidget.NavButton.Width", 10);
|
||||
_butH = g_gui.xmlEval()->getVar("Globals.TabWidget.NavButton.Height", 10);
|
||||
|
||||
_titleSpacing = g_gui.xmlEval()->getVar("Globals.TabWidget.TitleSpacing");
|
||||
}
|
||||
|
||||
TabWidget::TabWidget(GuiObject *boss, int x, int y, int w, int h, ThemeEngine::TextAlignVertical alignV)
|
||||
: Widget(boss, x, y, w, h), _bodyBackgroundType(ThemeEngine::kDialogBackgroundDefault), _alignV(alignV) {
|
||||
init();
|
||||
}
|
||||
|
||||
TabWidget::TabWidget(GuiObject *boss, const Common::String &name, ThemeEngine::TextAlignVertical alignV)
|
||||
: Widget(boss, name), _bodyBackgroundType(ThemeEngine::kDialogBackgroundDefault), _alignV(alignV) {
|
||||
init();
|
||||
}
|
||||
|
||||
void TabWidget::init() {
|
||||
setFlags(WIDGET_ENABLED | WIDGET_TRACK_MOUSE);
|
||||
_type = kTabWidget;
|
||||
_activeTab = -1;
|
||||
_firstVisibleTab = 0;
|
||||
_lastVisibleTab = 0;
|
||||
_navButtonsVisible = false;
|
||||
|
||||
recalc();
|
||||
|
||||
int x = _w - _butRP - _butW * 2 - 2;
|
||||
int y = _butTP - _tabHeight;
|
||||
|
||||
if (_alignV == ThemeEngine::kTextAlignVBottom)
|
||||
y = _h - _tabHeight + _butTP;
|
||||
|
||||
Common::String leftArrow = g_gui.useRTL() ? ">" : "<";
|
||||
Common::String rightArrow = g_gui.useRTL() ? "<" : ">";
|
||||
|
||||
_navLeft = new ButtonWidget(this, x, y, _butW, _butH, Common::U32String(leftArrow), Common::U32String(), kCmdLeft);
|
||||
_navRight = new ButtonWidget(this, x + _butW + 2, y, _butW, _butH, Common::U32String(rightArrow), Common::U32String(), kCmdRight);
|
||||
|
||||
_navLeft->setEnabled(false);
|
||||
_navRight->setEnabled(true);
|
||||
|
||||
_lastRead = -1;
|
||||
}
|
||||
|
||||
TabWidget::~TabWidget() {
|
||||
// If widgets were added or removed in the current tab, without tabs
|
||||
// having been switched using setActiveTab() afterward, then the
|
||||
// firstWidget in the _tabs list for the active tab may not be up to
|
||||
// date. So update it now.
|
||||
|
||||
if (_activeTab != -1)
|
||||
_tabs[_activeTab].firstWidget = _firstWidget;
|
||||
_firstWidget = nullptr;
|
||||
for (uint i = 0; i < _tabs.size(); ++i) {
|
||||
delete _tabs[i].firstWidget;
|
||||
_tabs[i].firstWidget = nullptr;
|
||||
}
|
||||
_tabs.clear();
|
||||
delete _navRight;
|
||||
}
|
||||
|
||||
int16 TabWidget::getChildY() const {
|
||||
// NOTE: if you change that, make sure to do the same
|
||||
// changes in the ThemeLayoutTabWidget (gui/ThemeLayout.cpp)
|
||||
return getAbsY() + _tabHeight;
|
||||
}
|
||||
|
||||
uint16 TabWidget::getHeight() const {
|
||||
// NOTE: if you change that, make sure to do the same
|
||||
// changes in the ThemeLayoutTabWidget (gui/ThemeLayout.cpp)
|
||||
// NOTE: this height is used for clipping, so it *includes*
|
||||
// tabs, because it starts from getAbsY(), not getChildY()
|
||||
return _h + _tabHeight;
|
||||
}
|
||||
|
||||
int TabWidget::addTab(const Common::U32String &title, const Common::String &dialogName) {
|
||||
// Add a new tab page
|
||||
Tab newTab;
|
||||
newTab.title = title;
|
||||
newTab.dialogName = dialogName;
|
||||
newTab.firstWidget = nullptr;
|
||||
|
||||
// Determine the new tab width
|
||||
int newWidth = g_gui.getStringWidth(title) + _titleSpacing;
|
||||
if (newWidth < _minTabWidth)
|
||||
newWidth = _minTabWidth;
|
||||
newTab._tabWidth = newWidth;
|
||||
|
||||
_tabs.push_back(newTab);
|
||||
|
||||
int numTabs = _tabs.size();
|
||||
|
||||
// Activate the new tab, also writes back our _firstWidget
|
||||
setActiveTab(numTabs - 1);
|
||||
|
||||
return _activeTab;
|
||||
}
|
||||
|
||||
void TabWidget::removeTab(int tabID) {
|
||||
assert(0 <= tabID && tabID < (int)_tabs.size());
|
||||
|
||||
// Deactivate the tab if it's currently the active one
|
||||
if (tabID == _activeTab) {
|
||||
_tabs[tabID].firstWidget = _firstWidget;
|
||||
releaseFocus();
|
||||
_firstWidget = nullptr;
|
||||
}
|
||||
|
||||
// Dispose the widgets in that tab and then the tab itself
|
||||
delete _tabs[tabID].firstWidget;
|
||||
_tabs.remove_at(tabID);
|
||||
|
||||
// Adjust _firstVisibleTab if necessary
|
||||
if (_firstVisibleTab >= (int)_tabs.size()) {
|
||||
_firstVisibleTab = MAX(0, (int)_tabs.size() - 1);
|
||||
}
|
||||
|
||||
// The active tab was removed, so select a new active one (if any remains)
|
||||
if (tabID == _activeTab) {
|
||||
_activeTab = -1;
|
||||
if (tabID >= (int)_tabs.size())
|
||||
tabID = _tabs.size() - 1;
|
||||
if (tabID >= 0)
|
||||
setActiveTab(tabID);
|
||||
}
|
||||
|
||||
// Finally trigger a redraw
|
||||
g_gui.scheduleTopDialogRedraw();
|
||||
}
|
||||
|
||||
int TabWidget::getTabCount() {
|
||||
return (int)_tabs.size();
|
||||
}
|
||||
|
||||
void TabWidget::setActiveTab(int tabID) {
|
||||
assert(0 <= tabID && tabID < (int)_tabs.size());
|
||||
if (_activeTab != tabID) {
|
||||
// Exchange the widget lists, and switch to the new tab
|
||||
if (_activeTab != -1) {
|
||||
_tabs[_activeTab].firstWidget = _firstWidget;
|
||||
releaseFocus();
|
||||
}
|
||||
_activeTab = tabID;
|
||||
_firstWidget = _tabs[tabID].firstWidget;
|
||||
|
||||
// Also ensure the tab is visible in the tab bar
|
||||
if (_firstVisibleTab > tabID)
|
||||
setFirstVisible(tabID, true);
|
||||
while (_lastVisibleTab < tabID)
|
||||
setFirstVisible(_firstVisibleTab + 1, false);
|
||||
|
||||
g_gui.scheduleTopDialogRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void TabWidget::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
|
||||
Widget::handleCommand(sender, cmd, data);
|
||||
|
||||
switch (cmd) {
|
||||
case kCmdLeft:
|
||||
if (!_navRight->isEnabled()) {
|
||||
_navRight->setEnabled(true);
|
||||
}
|
||||
|
||||
if (_firstVisibleTab > 0) {
|
||||
setFirstVisible(_firstVisibleTab - 1);
|
||||
}
|
||||
if (_firstVisibleTab == 0) {
|
||||
_navLeft->setEnabled(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case kCmdRight:
|
||||
if (!_navLeft->isEnabled()) {
|
||||
_navLeft->setEnabled(true);
|
||||
}
|
||||
|
||||
if (_lastVisibleTab + 1 < (int)_tabs.size()) {
|
||||
setFirstVisible(_firstVisibleTab + 1, false);
|
||||
}
|
||||
if (_lastVisibleTab + 1 == (int)_tabs.size()) {
|
||||
_navRight->setEnabled(false);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TabWidget::handleMouseDown(int x, int y, int button, int clickCount) {
|
||||
if (x < 0)
|
||||
return;
|
||||
|
||||
// Determine which tab was clicked
|
||||
int tabID;
|
||||
for (tabID = _firstVisibleTab; tabID <= _lastVisibleTab; ++tabID) {
|
||||
x -= _tabs[tabID]._tabWidth;
|
||||
if (x < 0)
|
||||
break;
|
||||
}
|
||||
|
||||
// If a tab was clicked, switch to that pane
|
||||
if (tabID <= _lastVisibleTab)
|
||||
setActiveTab(tabID);
|
||||
}
|
||||
|
||||
void TabWidget::handleMouseMoved(int x, int y, int button) {
|
||||
if (_alignV == ThemeEngine::kTextAlignVBottom) {
|
||||
if (y < _w - _tabHeight || y > _w)
|
||||
return;
|
||||
} else {
|
||||
if (y < 0 || y >= _tabHeight)
|
||||
return;
|
||||
}
|
||||
|
||||
if (x < 0)
|
||||
return;
|
||||
|
||||
// Determine which tab the mouse is on
|
||||
int tabID;
|
||||
for (tabID = _firstVisibleTab; tabID <= _lastVisibleTab; ++tabID) {
|
||||
x -= _tabs[tabID]._tabWidth;
|
||||
if (x < 0)
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabID <= _lastVisibleTab) {
|
||||
if (tabID != _lastRead) {
|
||||
read(_tabs[tabID].title);
|
||||
_lastRead = tabID;
|
||||
}
|
||||
}
|
||||
else
|
||||
_lastRead = -1;
|
||||
}
|
||||
|
||||
bool TabWidget::handleKeyDown(Common::KeyState state) {
|
||||
if (state.hasFlags(Common::KBD_SHIFT) && state.keycode == Common::KEYCODE_TAB)
|
||||
adjustTabs(kTabBackwards);
|
||||
else if (state.keycode == Common::KEYCODE_TAB)
|
||||
adjustTabs(kTabForwards);
|
||||
|
||||
return Widget::handleKeyDown(state);
|
||||
}
|
||||
|
||||
void TabWidget::handleMouseWheel(int x, int y, int direction) {
|
||||
if (direction == 1) {
|
||||
adjustTabs(kTabForwards);
|
||||
} else {
|
||||
adjustTabs(kTabBackwards);
|
||||
}
|
||||
}
|
||||
|
||||
void TabWidget::adjustTabs(int value) {
|
||||
// Determine which tab is next
|
||||
int tabID = _activeTab + value;
|
||||
int lastVis = _lastVisibleTab;
|
||||
|
||||
if (tabID >= (int)_tabs.size())
|
||||
tabID = 0;
|
||||
else if (tabID < 0)
|
||||
tabID = ((int)_tabs.size() - 1);
|
||||
|
||||
setActiveTab(tabID);
|
||||
|
||||
if (_navButtonsVisible) {
|
||||
if (lastVis != _lastVisibleTab) {
|
||||
_navLeft->setEnabled(true);
|
||||
_navRight->setEnabled(true);
|
||||
}
|
||||
|
||||
if (_firstVisibleTab == 0)
|
||||
_navLeft->setEnabled(false);
|
||||
else if (_lastVisibleTab == (int)_tabs.size() - 1)
|
||||
_navRight->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
int TabWidget::getFirstVisible() const {
|
||||
return _firstVisibleTab;
|
||||
}
|
||||
|
||||
void TabWidget::setFirstVisible(int tabID, bool adjustIfRoom) {
|
||||
assert(0 <= tabID && tabID < (int)_tabs.size());
|
||||
_firstVisibleTab = tabID;
|
||||
|
||||
computeLastVisibleTab(adjustIfRoom);
|
||||
|
||||
g_gui.scheduleTopDialogRedraw(); // TODO: Necessary?
|
||||
}
|
||||
|
||||
void TabWidget::reflowLayout() {
|
||||
Widget::reflowLayout();
|
||||
|
||||
// NOTE: if you change that, make sure to do the same
|
||||
// changes in the ThemeLayoutTabWidget (gui/ThemeLayout.cpp)
|
||||
recalc();
|
||||
|
||||
// If widgets were added or removed in the current tab, without tabs
|
||||
// having been switched using setActiveTab() afterward, then the
|
||||
// firstWidget in the _tabs list for the active tab may not be up to
|
||||
// date. So update it now.
|
||||
if (_activeTab != -1)
|
||||
_tabs[_activeTab].firstWidget = _firstWidget;
|
||||
|
||||
for (uint i = 0; i < _tabs.size(); ++i) {
|
||||
g_gui.xmlEval()->reflowDialogLayout(_tabs[i].dialogName, _tabs[i].firstWidget);
|
||||
|
||||
Widget *w = _tabs[i].firstWidget;
|
||||
while (w) {
|
||||
w->reflowLayout();
|
||||
w = w->next();
|
||||
}
|
||||
}
|
||||
|
||||
for (uint i = 0; i < _tabs.size(); ++i) {
|
||||
// Determine the new tab width
|
||||
int newWidth = g_gui.getStringWidth(_tabs[i].title) + _titleSpacing;
|
||||
if (newWidth < _minTabWidth)
|
||||
newWidth = _minTabWidth;
|
||||
_tabs[i]._tabWidth = newWidth;
|
||||
}
|
||||
|
||||
// See how many tabs fit on screen.
|
||||
// We do this in a loop, because it will change if we need to
|
||||
// add left/right scroll buttons, if we scroll left to use free
|
||||
// space on the right, or a combination of those.
|
||||
_navButtonsVisible = _firstVisibleTab > 0;
|
||||
do {
|
||||
computeLastVisibleTab(true);
|
||||
|
||||
if (_firstVisibleTab > 0 || _lastVisibleTab + 1 < (int)_tabs.size()) {
|
||||
if (!_navButtonsVisible)
|
||||
_navButtonsVisible = true;
|
||||
else
|
||||
break;
|
||||
} else {
|
||||
if (_navButtonsVisible)
|
||||
_navButtonsVisible = false;
|
||||
else
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
|
||||
int x = _w - _butRP - _butW * 2 - 2;
|
||||
int y = _butTP - _tabHeight;
|
||||
|
||||
if (_alignV == ThemeEngine::kTextAlignVBottom)
|
||||
y = _h - _tabHeight + _butTP;
|
||||
|
||||
_navLeft->resize(x, y, _butW, _butH, false);
|
||||
_navRight->resize(x + _butW + 2, y, _butW, _butH, false);
|
||||
}
|
||||
|
||||
void TabWidget::drawWidget() {
|
||||
Common::Array<Common::U32String> tabs;
|
||||
Common::Array<int> widths;
|
||||
for (int i = _firstVisibleTab; i <= _lastVisibleTab; ++i) {
|
||||
tabs.push_back(_tabs[i].title);
|
||||
widths.push_back(_tabs[i]._tabWidth);
|
||||
}
|
||||
|
||||
g_gui.theme()->drawDialogBackground(
|
||||
Common::Rect(_x + _bodyLP, _y + _bodyTP, _x + _w - _bodyRP, _y + _h - _bodyBP + _tabHeight),
|
||||
_bodyBackgroundType);
|
||||
|
||||
g_gui.theme()->drawTab(Common::Rect(_x, _y, _x + _w, _y + _h), _tabHeight, widths, tabs,
|
||||
_activeTab - _firstVisibleTab, (g_gui.useRTL() && _useRTL), _alignV);
|
||||
}
|
||||
|
||||
void TabWidget::draw() {
|
||||
Widget::draw();
|
||||
|
||||
if (_navButtonsVisible) {
|
||||
_navLeft->draw();
|
||||
_navRight->draw();
|
||||
}
|
||||
}
|
||||
|
||||
void TabWidget::markAsDirty() {
|
||||
Widget::markAsDirty();
|
||||
|
||||
if (_navButtonsVisible) {
|
||||
_navLeft->markAsDirty();
|
||||
_navRight->markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
bool TabWidget::containsWidget(Widget *w) const {
|
||||
if (w == _navLeft || w == _navRight || _navLeft->containsWidget(w) || _navRight->containsWidget(w))
|
||||
return true;
|
||||
return containsWidgetInChain(_firstWidget, w);
|
||||
}
|
||||
|
||||
|
||||
Widget *TabWidget::findWidget(int x, int y) {
|
||||
if ((_alignV == ThemeEngine::kTextAlignVBottom && y < _h - _tabHeight) ||
|
||||
(_alignV == ThemeEngine::kTextAlignVTop && y >= _tabHeight)) {
|
||||
// Iterate over all child widgets and find the one which was clicked
|
||||
return Widget::findWidgetInChain(_firstWidget, x, y - _tabHeight);
|
||||
}
|
||||
|
||||
if (_navButtonsVisible) {
|
||||
if ((_alignV == ThemeEngine::kTextAlignVTop && y >= _butTP && y < _butTP + _butH) ||
|
||||
(_alignV == ThemeEngine::kTextAlignVBottom && y >= _h - _butH - _butTP + _tabHeight && y < _h + _tabHeight)) {
|
||||
if (x >= _w - _butRP - _butW * 2 - 2 && x < _w - _butRP - _butW - 2)
|
||||
return _navLeft;
|
||||
|
||||
if (x >= _w - _butRP - _butW && x < _w - _butRP)
|
||||
return _navRight;
|
||||
}
|
||||
}
|
||||
|
||||
// Click was in the tab area
|
||||
return this;
|
||||
}
|
||||
|
||||
void TabWidget::computeLastVisibleTab(bool adjustFirstIfRoom) {
|
||||
int availableWidth = _w;
|
||||
if (_navButtonsVisible)
|
||||
availableWidth -= 2 + _butW * 2 + _butRP;
|
||||
|
||||
_lastVisibleTab = _tabs.size() - 1;
|
||||
for (int i = _firstVisibleTab; i < (int)_tabs.size(); ++i) {
|
||||
if (_tabs[i]._tabWidth > availableWidth) {
|
||||
if (i > _firstVisibleTab)
|
||||
_lastVisibleTab = i - 1;
|
||||
else
|
||||
_lastVisibleTab = _firstVisibleTab; // Always show 1
|
||||
break;
|
||||
}
|
||||
availableWidth -= _tabs[i]._tabWidth;
|
||||
}
|
||||
|
||||
if (adjustFirstIfRoom) {
|
||||
// If possible, scroll to fit if there's unused space to the right
|
||||
while (_firstVisibleTab > 0 && _tabs[_firstVisibleTab-1]._tabWidth <= availableWidth) {
|
||||
availableWidth -= _tabs[_firstVisibleTab-1]._tabWidth;
|
||||
_firstVisibleTab--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // End of namespace GUI
|
||||
144
gui/widgets/tab.h
Normal file
144
gui/widgets/tab.h
Normal file
@@ -0,0 +1,144 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GUI_WIDGETS_TAB_H
|
||||
#define GUI_WIDGETS_TAB_H
|
||||
|
||||
#include "gui/widget.h"
|
||||
#include "common/str.h"
|
||||
#include "common/array.h"
|
||||
|
||||
namespace GUI {
|
||||
|
||||
enum {
|
||||
kTabForwards = 1,
|
||||
kTabBackwards = -1
|
||||
};
|
||||
|
||||
class TabWidget : public Widget {
|
||||
struct Tab {
|
||||
Common::U32String title;
|
||||
Common::String dialogName;
|
||||
Widget *firstWidget;
|
||||
int _tabWidth;
|
||||
};
|
||||
typedef Common::Array<Tab> TabList;
|
||||
|
||||
protected:
|
||||
int _activeTab;
|
||||
int _firstVisibleTab;
|
||||
int _lastVisibleTab;
|
||||
TabList _tabs;
|
||||
int _tabHeight;
|
||||
int _minTabWidth;
|
||||
int _titleSpacing;
|
||||
|
||||
ThemeEngine::TextAlignVertical _alignV;
|
||||
|
||||
int _bodyRP, _bodyTP, _bodyLP, _bodyBP;
|
||||
ThemeEngine::DialogBackground _bodyBackgroundType;
|
||||
|
||||
int _titleVPad;
|
||||
|
||||
int _butRP, _butTP, _butW, _butH;
|
||||
|
||||
ButtonWidget *_navLeft, *_navRight;
|
||||
bool _navButtonsVisible;
|
||||
int _lastRead;
|
||||
|
||||
void recalc();
|
||||
|
||||
public:
|
||||
TabWidget(GuiObject *boss, int x, int y, int w, int h, ThemeEngine::TextAlignVertical alignV = ThemeEngine::kTextAlignVTop);
|
||||
TabWidget(GuiObject *boss, const Common::String &name, ThemeEngine::TextAlignVertical alignV = ThemeEngine::kTextAlignVTop);
|
||||
~TabWidget() override;
|
||||
|
||||
void init();
|
||||
|
||||
/**
|
||||
* Add a new tab with the given title. Returns a unique ID which can be used
|
||||
* to identify the tab (to remove it / activate it etc.).
|
||||
*/
|
||||
int addTab(const Common::U32String &title, const Common::String &dialogName);
|
||||
|
||||
/**
|
||||
* Remove the tab with the given tab ID. Disposes all child widgets of that tab.
|
||||
* TODO: This code is *unfinished*. In particular, it changes the
|
||||
* tabIDs, so that they are not unique anymore! This is bad.
|
||||
* If we need to, we could fix this by changing the tab IDs from being an index
|
||||
* into the _tabs array to a real "unique" ID, which gets stored in the Tab struct.
|
||||
* It won't be difficult to implement this, but since currently no code seems to make
|
||||
* use of the feature...
|
||||
*/
|
||||
void removeTab(int tabID);
|
||||
|
||||
int getActiveTab() {
|
||||
return _activeTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active tab by specifying a valid tab ID.
|
||||
* setActiveTab changes the value of _firstWidget. This means new
|
||||
* Widgets are always added to the active tab.
|
||||
*/
|
||||
void setActiveTab(int tabID);
|
||||
|
||||
int getTabCount();
|
||||
|
||||
void setTabTitle(int tabID, const Common::U32String &title) {
|
||||
assert(0 <= tabID && tabID < (int)_tabs.size());
|
||||
_tabs[tabID].title = title;
|
||||
}
|
||||
|
||||
void handleMouseDown(int x, int y, int button, int clickCount) override;
|
||||
void handleMouseMoved(int x, int y, int button) override;
|
||||
void handleMouseLeft(int button) override { _lastRead = -1; };
|
||||
bool handleKeyDown(Common::KeyState state) override;
|
||||
void handleMouseWheel(int x, int y, int direction) override;
|
||||
void handleCommand(CommandSender *sender, uint32 cmd, uint32 data) override;
|
||||
virtual int getFirstVisible() const;
|
||||
virtual void setFirstVisible(int tabID, bool adjustIfRoom = false);
|
||||
|
||||
bool containsWidget(Widget *) const override;
|
||||
|
||||
void reflowLayout() override;
|
||||
|
||||
void draw() override;
|
||||
void markAsDirty() override;
|
||||
|
||||
protected:
|
||||
// We overload getChildY to make sure child widgets are positioned correctly.
|
||||
// Essentially this compensates for the space taken up by the tab title header.
|
||||
int16 getChildY() const override;
|
||||
uint16 getHeight() const override;
|
||||
|
||||
void drawWidget() override;
|
||||
|
||||
Widget *findWidget(int x, int y) override;
|
||||
|
||||
virtual void adjustTabs(int value);
|
||||
|
||||
virtual void computeLastVisibleTab(bool adjustFirstIfRoom);
|
||||
};
|
||||
|
||||
} // End of namespace GUI
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user