Initial commit

This commit is contained in:
2026-02-02 04:50:13 +01:00
commit 5b11698731
22592 changed files with 7677434 additions and 0 deletions

674
gui/widgets/editable.cpp Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

278
gui/widgets/grid.h Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

208
gui/widgets/list.h Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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

View 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
View 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
View 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