diff options
author | Max Horn | 2010-11-16 10:11:57 +0000 |
---|---|---|
committer | Max Horn | 2010-11-16 10:11:57 +0000 |
commit | 427dc1ae93fe9aafe97a2796b11db6ac940a97b1 (patch) | |
tree | ff4e8bea780e9d078c3066fee72640154ba35a74 /gui/widgets | |
parent | c734fc2b3eb327fd442e9a7a62beeec964f2830b (diff) | |
download | scummvm-rg350-427dc1ae93fe9aafe97a2796b11db6ac940a97b1.tar.gz scummvm-rg350-427dc1ae93fe9aafe97a2796b11db6ac940a97b1.tar.bz2 scummvm-rg350-427dc1ae93fe9aafe97a2796b11db6ac940a97b1.zip |
GUI: Move major widgets to new directory gui/widgets
Also renamed the source/header files, now they are more closely
aligned to how we rename most other source files
svn-id: r54264
Diffstat (limited to 'gui/widgets')
-rw-r--r-- | gui/widgets/editable.cpp | 304 | ||||
-rw-r--r-- | gui/widgets/editable.h | 95 | ||||
-rw-r--r-- | gui/widgets/edittext.cpp | 118 | ||||
-rw-r--r-- | gui/widgets/edittext.h | 69 | ||||
-rw-r--r-- | gui/widgets/list.cpp | 734 | ||||
-rw-r--r-- | gui/widgets/list.h | 157 | ||||
-rw-r--r-- | gui/widgets/popup.cpp | 463 | ||||
-rw-r--r-- | gui/widgets/popup.h | 91 | ||||
-rw-r--r-- | gui/widgets/scrollbar.cpp | 233 | ||||
-rw-r--r-- | gui/widgets/scrollbar.h | 84 | ||||
-rw-r--r-- | gui/widgets/tab.cpp | 320 | ||||
-rw-r--r-- | gui/widgets/tab.h | 118 |
12 files changed, 2786 insertions, 0 deletions
diff --git a/gui/widgets/editable.cpp b/gui/widgets/editable.cpp new file mode 100644 index 0000000000..9817f562ad --- /dev/null +++ b/gui/widgets/editable.cpp @@ -0,0 +1,304 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +#include "common/events.h" +#include "gui/widgets/editable.h" +#include "gui/GuiManager.h" + +namespace GUI { + +EditableWidget::EditableWidget(GuiObject *boss, int x, int y, int w, int h, const char *tooltip, uint32 cmd) + : Widget(boss, x, y, w, h, tooltip), CommandSender(boss), _cmd(cmd) { + init(); +} + +EditableWidget::EditableWidget(GuiObject *boss, const String &name, const char *tooltip, uint32 cmd) + : Widget(boss, name, tooltip), CommandSender(boss), _cmd(cmd) { + init(); +} + +void EditableWidget::init() { + _caretVisible = false; + _caretTime = 0; + _caretPos = 0; + + _caretInverse = false; + + _editScrollOffset = 0; + + _font = ThemeEngine::kFontStyleBold; + _inversion = ThemeEngine::kTextInversionNone; +} + +EditableWidget::~EditableWidget() { +} + +void EditableWidget::reflowLayout() { + Widget::reflowLayout(); + + _editScrollOffset = g_gui.getStringWidth(_editString, _font) - getEditRect().width(); + if (_editScrollOffset < 0) + _editScrollOffset = 0; +} + +void EditableWidget::setEditString(const String &str) { + // TODO: We probably should filter the input string here, + // e.g. using tryInsertChar. + _editString = str; + _caretPos = 0; +} + +bool EditableWidget::tryInsertChar(byte c, int pos) { + if ((c >= 32 && c <= 127) || c >= 160) { + _editString.insertChar(c, pos); + return true; + } + return false; +} + +void EditableWidget::handleTickle() { + uint32 time = g_system->getMillis(); + if (_caretTime < time) { + _caretTime = time + kCaretBlinkTime; + drawCaret(_caretVisible); + } +} + +bool EditableWidget::handleKeyDown(Common::KeyState state) { + bool handled = true; + bool dirty = false; + bool forcecaret = false; + + // First remove caret + if (_caretVisible) + drawCaret(true); + + 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: + if (_caretPos > 0) { + _caretPos--; + _editString.deleteChar(_caretPos); + dirty = true; + + sendCommand(_cmd, 0); + } + forcecaret = true; + break; + + // Keypad & special keys + // - if num lock is set, we always go to the default case + // - if num lock is not set, we either fall down to the special key case + // or ignore the key press in case of 0 (INSERT), 2 (DOWN), 3 (PGDWN) + // 5, 8 (UP) and 9 (PGUP) + + case Common::KEYCODE_KP0: + case Common::KEYCODE_KP2: + case Common::KEYCODE_KP3: + case Common::KEYCODE_KP5: + case Common::KEYCODE_KP8: + case Common::KEYCODE_KP9: + if (state.flags & Common::KBD_NUM) + defaultKeyDownHandler(state, dirty, forcecaret, handled); + break; + + case Common::KEYCODE_KP_PERIOD: + if (state.flags & Common::KBD_NUM) { + defaultKeyDownHandler(state, dirty, forcecaret, handled); + break; + } + case Common::KEYCODE_DELETE: + if (_caretPos < (int)_editString.size()) { + _editString.deleteChar(_caretPos); + dirty = true; + + sendCommand(_cmd, 0); + } + forcecaret = true; + break; + + case Common::KEYCODE_KP1: + if (state.flags & Common::KBD_NUM) { + defaultKeyDownHandler(state, dirty, forcecaret, handled); + break; + } + case Common::KEYCODE_END: + dirty = setCaretPos(_editString.size()); + forcecaret = true; + break; + + case Common::KEYCODE_KP4: + if (state.flags & Common::KBD_NUM) { + defaultKeyDownHandler(state, dirty, forcecaret, handled); + break; + } + case Common::KEYCODE_LEFT: + if (_caretPos > 0) { + dirty = setCaretPos(_caretPos - 1); + } + forcecaret = true; + dirty = true; + break; + + case Common::KEYCODE_KP6: + if (state.flags & Common::KBD_NUM) { + defaultKeyDownHandler(state, dirty, forcecaret, handled); + break; + } + case Common::KEYCODE_RIGHT: + if (_caretPos < (int)_editString.size()) { + dirty = setCaretPos(_caretPos + 1); + } + forcecaret = true; + dirty = true; + break; + + case Common::KEYCODE_KP7: + if (state.flags & Common::KBD_NUM) { + defaultKeyDownHandler(state, dirty, forcecaret, handled); + break; + } + case Common::KEYCODE_HOME: + dirty = setCaretPos(0); + forcecaret = true; + break; + + default: + defaultKeyDownHandler(state, dirty, forcecaret, handled); + } + + if (dirty) + draw(); + + if (forcecaret) + makeCaretVisible(); + + return handled; +} + +void EditableWidget::defaultKeyDownHandler(Common::KeyState &state, bool &dirty, bool &forcecaret, bool &handled) { + if (state.ascii < 256 && tryInsertChar((byte)state.ascii, _caretPos)) { + _caretPos++; + dirty = true; + forcecaret = true; + + sendCommand(_cmd, 0); + } else { + handled = false; + } +} + +int EditableWidget::getCaretOffset() const { + int caretpos = 0; + for (int i = 0; i < _caretPos; i++) + caretpos += g_gui.getCharWidth(_editString[i], _font); + + caretpos -= _editScrollOffset; + + return caretpos; +} + +void EditableWidget::drawCaret(bool erase) { + // Only draw if item is visible + if (!isVisible() || !_boss->isVisible()) + return; + + Common::Rect editRect = getEditRect(); + + int x = editRect.left; + int y = editRect.top; + + x += getCaretOffset(); + + if (y < 0 || y + editRect.height() - 2 >= _h) + return; + + x += getAbsX(); + y += getAbsY(); + + g_gui.theme()->drawCaret(Common::Rect(x, y, x + 1, y + editRect.height() - 2), erase); + + if (erase) { + if ((uint)_caretPos < _editString.size()) { + GUI::EditableWidget::String chr(_editString[_caretPos]); + int chrWidth = g_gui.getCharWidth(_editString[_caretPos], _font); + g_gui.theme()->drawText(Common::Rect(x, y, x + chrWidth, y + editRect.height() - 2), chr, _state, Graphics::kTextAlignLeft, _inversion, 0, false, _font); + } + } + + _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); +} + +} // End of namespace GUI diff --git a/gui/widgets/editable.h b/gui/widgets/editable.h new file mode 100644 index 0000000000..0cd1f2721c --- /dev/null +++ b/gui/widgets/editable.h @@ -0,0 +1,95 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +#ifndef GUI_WIDGETS_EDITABLE_H +#define GUI_WIDGETS_EDITABLE_H + +#include "common/str.h" +#include "common/rect.h" +#include "gui/widget.h" +#include "gui/GuiManager.h" + +namespace GUI { + +/** + * Base class for widgets which need to edit text, like ListWidget and + * EditTextWidget. + */ +class EditableWidget : public Widget, public CommandSender { +public: + typedef Common::String String; +protected: + String _editString; + + uint32 _cmd; + + bool _caretVisible; + uint32 _caretTime; + int _caretPos; + + bool _caretInverse; + + int _editScrollOffset; + + ThemeEngine::FontStyle _font; + + ThemeEngine::TextInversionState _inversion; + +public: + EditableWidget(GuiObject *boss, int x, int y, int w, int h, const char *tooltip = 0, uint32 cmd = 0); + EditableWidget(GuiObject *boss, const String &name, const char *tooltip = 0, uint32 cmd = 0); + virtual ~EditableWidget(); + + void init(); + + virtual void setEditString(const String &str); + virtual const String &getEditString() const { return _editString; } + + virtual void handleTickle(); + virtual bool handleKeyDown(Common::KeyState state); + + virtual void reflowLayout(); + +protected: + virtual void startEditMode() = 0; + virtual void endEditMode() = 0; + virtual void abortEditMode() = 0; + + virtual Common::Rect getEditRect() const = 0; + virtual int getCaretOffset() const; + void drawCaret(bool erase); + bool setCaretPos(int newPos); + bool adjustOffset(); + void makeCaretVisible(); + + void defaultKeyDownHandler(Common::KeyState &state, bool &dirty, bool &forcecaret, bool &handled); + + void setFontStyle(ThemeEngine::FontStyle font) { _font = font; } + + virtual bool tryInsertChar(byte c, int pos); +}; + +} // End of namespace GUI + +#endif diff --git a/gui/widgets/edittext.cpp b/gui/widgets/edittext.cpp new file mode 100644 index 0000000000..1e2ba69478 --- /dev/null +++ b/gui/widgets/edittext.cpp @@ -0,0 +1,118 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +#include "gui/widgets/edittext.h" +#include "gui/dialog.h" +#include "gui/GuiManager.h" + +#include "gui/ThemeEval.h" + +namespace GUI { + +EditTextWidget::EditTextWidget(GuiObject *boss, int x, int y, int w, int h, const String &text, const char *tooltip, uint32 cmd) + : EditableWidget(boss, x, y - 1, w, h + 2, tooltip, cmd) { + setFlags(WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS | WIDGET_WANT_TICKLE); + _type = kEditTextWidget; + + setEditString(text); +} + +EditTextWidget::EditTextWidget(GuiObject *boss, const String &name, const String &text, const char *tooltip, uint32 cmd) + : EditableWidget(boss, name, tooltip, cmd) { + setFlags(WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS | WIDGET_WANT_TICKLE); + _type = kEditTextWidget; + + setEditString(text); +} + +void EditTextWidget::setEditString(const String &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::handleMouseDown(int x, int y, int button, int clickCount) { + // First remove caret + if (_caretVisible) + drawCaret(true); + + x += _editScrollOffset; + + int width = 0; + uint i; + + for (i = 0; i < _editString.size(); ++i) { + width += g_gui.theme()->getCharWidth(_editString[i], _font); + if (width >= x) + break; + } + if (setCaretPos(i)) + draw(); +} + + +void EditTextWidget::drawWidget() { + g_gui.theme()->drawWidgetBackground(Common::Rect(_x, _y, _x+_w, _y+_h), 0, ThemeEngine::kWidgetBackgroundEditText); + + // Draw the text + adjustOffset(); + g_gui.theme()->drawText(Common::Rect(_x+2+ _leftPadding,_y+2, _x+_leftPadding+getEditRect().width()+2, _y+_h-2), _editString, _state, Graphics::kTextAlignLeft, ThemeEngine::kTextInversionNone, -_editScrollOffset, false, _font); +} + +Common::Rect EditTextWidget::getEditRect() const { + Common::Rect r(2 + _leftPadding, 2, _w - 2 - _leftPadding - _rightPadding, _h-1); + + return r; +} + +void EditTextWidget::receivedFocusWidget() { +} + +void EditTextWidget::lostFocusWidget() { + // If we loose focus, 'commit' the user changes + _backupString = _editString; + drawCaret(true); +} + +void EditTextWidget::startEditMode() { +} + +void EditTextWidget::endEditMode() { + releaseFocus(); +} + +void EditTextWidget::abortEditMode() { + setEditString(_backupString); + sendCommand(_cmd, 0); + releaseFocus(); +} + +} // End of namespace GUI diff --git a/gui/widgets/edittext.h b/gui/widgets/edittext.h new file mode 100644 index 0000000000..a2549882ca --- /dev/null +++ b/gui/widgets/edittext.h @@ -0,0 +1,69 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +#ifndef GUI_WIDGETS_EDITTEXT_H +#define GUI_WIDGETS_EDITTEXT_H + +#include "gui/widgets/editable.h" +#include "common/str.h" + +namespace GUI { + +/* EditTextWidget */ +class EditTextWidget : public EditableWidget { +protected: + typedef Common::String String; + + String _backupString; + + int _leftPadding; + int _rightPadding; + +public: + EditTextWidget(GuiObject *boss, int x, int y, int w, int h, const String &text, const char *tooltip = 0, uint32 cmd = 0); + EditTextWidget(GuiObject *boss, const String &name, const String &text, const char *tooltp = 0, uint32 cmd = 0); + + void setEditString(const String &str); + + virtual void handleMouseDown(int x, int y, int button, int clickCount); + + virtual bool wantsFocus() { return true; } + + virtual void reflowLayout(); + +protected: + void drawWidget(); + void receivedFocusWidget(); + void lostFocusWidget(); + + void startEditMode(); + void endEditMode(); + void abortEditMode(); + + Common::Rect getEditRect() const; +}; + +} // End of namespace GUI + +#endif diff --git a/gui/widgets/list.cpp b/gui/widgets/list.cpp new file mode 100644 index 0000000000..bcbe0b9cbf --- /dev/null +++ b/gui/widgets/list.cpp @@ -0,0 +1,734 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +#include "common/system.h" +#include "common/events.h" +#include "common/frac.h" +#include "common/tokenizer.h" + +#include "gui/widgets/list.h" +#include "gui/widgets/scrollbar.h" +#include "gui/dialog.h" +#include "gui/GuiManager.h" + +#include "gui/ThemeEval.h" + +namespace GUI { + +ListWidget::ListWidget(Dialog *boss, const String &name, const char *tooltip, uint32 cmd) + : EditableWidget(boss, name, tooltip), _cmd(cmd) { + + _scrollBar = NULL; + _textWidth = NULL; + + // This ensures that _entriesPerPage is properly initialised. + reflowLayout(); + + _scrollBar = new ScrollBarWidget(this, _w - _scrollBarWidth + 1, 0, _scrollBarWidth, _h); + _scrollBar->setTarget(this); + + setFlags(WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS | WIDGET_WANT_TICKLE); + _type = kListWidget; + _editMode = false; + _numberingMode = kListNumberingOne; + _currentPos = 0; + _selectedItem = -1; + _currentKeyDown = 0; + + _quickSelectTime = 0; + + // The item is selected, thus _bgcolor is used to draw the caret and _textcolorhi to erase it + _caretInverse = true; + + // FIXME: This flag should come from widget definition + _editable = true; + + _quickSelect = true; + _editColor = ThemeEngine::kFontColorNormal; +} + +ListWidget::ListWidget(Dialog *boss, int x, int y, int w, int h, const char *tooltip, uint32 cmd) + : EditableWidget(boss, x, y, w, h, tooltip), _cmd(cmd) { + + _scrollBar = NULL; + _textWidth = NULL; + + // This ensures that _entriesPerPage is properly initialised. + reflowLayout(); + + _scrollBar = new ScrollBarWidget(this, _w - _scrollBarWidth + 1, 0, _scrollBarWidth, _h); + _scrollBar->setTarget(this); + + setFlags(WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS | WIDGET_WANT_TICKLE); + _type = kListWidget; + _editMode = false; + _numberingMode = kListNumberingOne; + _currentPos = 0; + _selectedItem = -1; + _currentKeyDown = 0; + + _quickSelectTime = 0; + + // The item is selected, thus _bgcolor is used to draw the caret and _textcolorhi to erase it + _caretInverse = true; + + // FIXME: This flag should come from widget definition + _editable = true; +} + +ListWidget::~ListWidget() { + delete[] _textWidth; +} + +Widget *ListWidget::findWidget(int x, int y) { + if (x >= _w - _scrollBarWidth) + return _scrollBar; + + return this; +} + +void ListWidget::setSelected(int item) { + // HACK/FIXME: If our _listIndex has a non zero size, + // we will need to look up, whether the user selected + // item is present in that list + if (_listIndex.size()) { + int filteredItem = -1; + + for (uint i = 0; i < _listIndex.size(); ++i) { + if (_listIndex[i] == item) { + filteredItem = i; + break; + } + } + + item = filteredItem; + } + + assert(item >= -1 && item < (int)_list.size()); + + // We only have to do something if the widget is enabled and the selection actually changes + if (isEnabled() && _selectedItem != item) { + if (_editMode) + abortEditMode(); + + _selectedItem = item; + + // Notify clients that the selection changed. + sendCommand(kListSelectionChangedCmd, _selectedItem); + + _currentPos = _selectedItem - _entriesPerPage / 2; + scrollToCurrent(); + draw(); + } +} + +ThemeEngine::FontColor ListWidget::getSelectionColor() const { + if (_listColors.empty()) + return ThemeEngine::kFontColorNormal; + + if (_filter.empty()) + return _listColors[_selectedItem]; + else + return _listColors[_listIndex[_selectedItem]]; +} + +void ListWidget::setList(const StringArray &list, const ColorList *colors) { + if (_editMode && _caretVisible) + drawCaret(true); + + // Copy everything + _dataList = list; + _list = list; + _filter.clear(); + _listIndex.clear(); + _listColors.clear(); + + if (colors) { + _listColors = *colors; + assert(_listColors.size() == _dataList.size()); + } + + int size = list.size(); + if (_currentPos >= size) + _currentPos = size - 1; + if (_currentPos < 0) + _currentPos = 0; + _selectedItem = -1; + _editMode = false; + g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false); + scrollBarRecalc(); +} + +void ListWidget::append(const String &s, ThemeEngine::FontColor color) { + if (_dataList.size() == _listColors.size()) { + // If the color list has the size of the data list, we append the color. + _listColors.push_back(color); + } else if (!_listColors.size() && color != ThemeEngine::kFontColorNormal) { + // If it's the first entry to use a non default color, we will fill + // up all other entries of the color list with the default color and + // add the requested color for the new entry. + for (uint i = 0; i < _dataList.size(); ++i) + _listColors.push_back(ThemeEngine::kFontColorNormal); + _listColors.push_back(color); + } + + _dataList.push_back(s); + _list.push_back(s); + + setFilter(_filter, false); + + scrollBarRecalc(); +} + +void ListWidget::scrollTo(int item) { + int size = _list.size(); + if (item >= size) + item = size - 1; + if (item < 0) + item = 0; + + if (_currentPos != item) { + _currentPos = item; + scrollBarRecalc(); + } +} + +void ListWidget::scrollBarRecalc() { + _scrollBar->_numEntries = _list.size(); + _scrollBar->_entriesPerPage = _entriesPerPage; + _scrollBar->_currentPos = _currentPos; + _scrollBar->recalc(); +} + +void ListWidget::handleTickle() { + if (_editMode) + EditableWidget::handleTickle(); +} + +void ListWidget::handleMouseDown(int x, int y, int button, int clickCount) { + if (!isEnabled()) + return; + + // First check whether the selection changed + int newSelectedItem = findItem(x, y); + + if (_selectedItem != newSelectedItem && newSelectedItem != -1) { + if (_editMode) + abortEditMode(); + _selectedItem = newSelectedItem; + sendCommand(kListSelectionChangedCmd, _selectedItem); + } + + // TODO: Determine where inside the string the user clicked and place the + // caret accordingly. + // See _editScrollOffset and EditTextWidget::handleMouseDown. + draw(); + +} + +void ListWidget::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)) && + _selectedItem >= 0) { + sendCommand(kListItemDoubleClickedCmd, _selectedItem); + } +} + +void ListWidget::handleMouseWheel(int x, int y, int direction) { + _scrollBar->handleMouseWheel(x, y, direction); +} + + +int ListWidget::findItem(int x, int y) const { + if (y < _topPadding) return -1; + int item = (y - _topPadding) / kLineHeight + _currentPos; + if (item >= _currentPos && item < _currentPos + _entriesPerPage && + item < (int)_list.size()) + return item; + else + return -1; +} + +static int matchingCharsIgnoringCase(const char *x, const char *y, bool &stop) { + int match = 0; + while (*x && *y && tolower(*x) == tolower(*y)) { + ++x; + ++y; + ++match; + } + stop = !*y || (*x && (tolower(*x) >= tolower(*y))); + return match; +} + +bool ListWidget::handleKeyDown(Common::KeyState state) { + bool handled = true; + bool dirty = false; + int oldSelectedItem = _selectedItem; + + if (!_editMode && state.keycode <= Common::KEYCODE_z && isprint((unsigned char)state.ascii)) { + // Quick selection mode: Go to first list item starting with this key + // (or a substring accumulated from the last couple key presses). + // Only works in a useful fashion if the list entries are sorted. + uint32 time = g_system->getMillis(); + if (_quickSelectTime < time) { + _quickSelectStr = (char)state.ascii; + } else { + _quickSelectStr += (char)state.ascii; + } + _quickSelectTime = time + 300; // TODO: Turn this into a proper constant (kQuickSelectDelay ?) + + if (_quickSelect) { + // FIXME: This is bad slow code (it scans the list linearly each time a + // key is pressed); it could be much faster. Only of importance if we have + // quite big lists to deal with -- so for now we can live with this lazy + // implementation :-) + int newSelectedItem = 0; + int bestMatch = 0; + bool stop; + for (StringArray::const_iterator i = _list.begin(); i != _list.end(); ++i) { + const int match = matchingCharsIgnoringCase(i->c_str(), _quickSelectStr.c_str(), stop); + if (match > bestMatch || stop) { + _selectedItem = newSelectedItem; + bestMatch = match; + if (stop) + break; + } + newSelectedItem++; + } + + scrollToCurrent(); + } else { + sendCommand(_cmd, 0); + } + } else if (_editMode) { + // Class EditableWidget handles all text editing related key presses for us + handled = EditableWidget::handleKeyDown(state); + } else { + // not editmode + + switch (state.keycode) { + case Common::KEYCODE_RETURN: + case Common::KEYCODE_KP_ENTER: + if (_selectedItem >= 0) { + // override continuous enter keydown + if (_editable && (_currentKeyDown != Common::KEYCODE_RETURN && _currentKeyDown != Common::KEYCODE_KP_ENTER)) { + dirty = true; + startEditMode(); + } else + sendCommand(kListItemActivatedCmd, _selectedItem); + } + break; + + // Keypad & special keys + // - if num lock is set, we do not handle the keypress + // - if num lock is not set, we either fall down to the special key case + // or ignore the key press for 0, 4, 5 and 6 + + case Common::KEYCODE_KP_PERIOD: + if (state.flags & Common::KBD_NUM) { + handled = false; + break; + } + case Common::KEYCODE_BACKSPACE: + case Common::KEYCODE_DELETE: + if (_selectedItem >= 0) { + if (_editable) { + // Ignore delete and backspace when the list item is editable + } else { + sendCommand(kListItemRemovalRequestCmd, _selectedItem); + } + } + break; + + case Common::KEYCODE_KP1: + if (state.flags & Common::KBD_NUM) { + handled = false; + break; + } + case Common::KEYCODE_END: + _selectedItem = _list.size() - 1; + break; + + + case Common::KEYCODE_KP2: + if (state.flags & Common::KBD_NUM) { + handled = false; + break; + } + case Common::KEYCODE_DOWN: + if (_selectedItem < (int)_list.size() - 1) + _selectedItem++; + break; + + case Common::KEYCODE_KP3: + if (state.flags & Common::KBD_NUM) { + handled = false; + break; + } + case Common::KEYCODE_PAGEDOWN: + _selectedItem += _entriesPerPage - 1; + if (_selectedItem >= (int)_list.size() ) + _selectedItem = _list.size() - 1; + break; + + case Common::KEYCODE_KP7: + if (state.flags & Common::KBD_NUM) { + handled = false; + break; + } + case Common::KEYCODE_HOME: + _selectedItem = 0; + break; + + case Common::KEYCODE_KP8: + if (state.flags & Common::KBD_NUM) { + handled = false; + break; + } + case Common::KEYCODE_UP: + if (_selectedItem > 0) + _selectedItem--; + break; + + case Common::KEYCODE_KP9: + if (state.flags & Common::KBD_NUM) { + handled = false; + break; + } + case Common::KEYCODE_PAGEUP: + _selectedItem -= _entriesPerPage - 1; + if (_selectedItem < 0) + _selectedItem = 0; + break; + + default: + handled = false; + } + + scrollToCurrent(); + } + + if (dirty || _selectedItem != oldSelectedItem) + draw(); + + if (_selectedItem != oldSelectedItem) { + sendCommand(kListSelectionChangedCmd, _selectedItem); + // also draw scrollbar + _scrollBar->draw(); + } + + return handled; +} + +bool ListWidget::handleKeyUp(Common::KeyState state) { + if (state.keycode == _currentKeyDown) + _currentKeyDown = 0; + return true; +} + +void ListWidget::receivedFocusWidget() { + _inversion = ThemeEngine::kTextInversionFocus; + + // Redraw the widget so the selection color will change + draw(); +} + +void ListWidget::lostFocusWidget() { + _inversion = ThemeEngine::kTextInversion; + + // If we lose focus, we simply forget the user changes + _editMode = false; + g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false); + drawCaret(true); + draw(); +} + +void ListWidget::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) { + switch (cmd) { + case kSetPositionCmd: + if (_currentPos != (int)data) { + _currentPos = data; + draw(); + + // Scrollbar actions cause list focus (which triggers a redraw) + // NOTE: ListWidget's boss is always GUI::Dialog + ((GUI::Dialog *)_boss)->setFocusWidget(this); + } + break; + } +} + +void ListWidget::drawWidget() { + int i, pos, len = _list.size(); + Common::String buffer; + + // Draw a thin frame around the list. + g_gui.theme()->drawWidgetBackground(Common::Rect(_x, _y, _x + _w, _y + _h), 0, ThemeEngine::kWidgetBackgroundBorder); + const int scrollbarW = (_scrollBar && _scrollBar->isVisible()) ? _scrollBarWidth : 0; + + // Draw the list items + for (i = 0, pos = _currentPos; i < _entriesPerPage && pos < len; i++, pos++) { + const int y = _y + _topPadding + kLineHeight * i; + const int fontHeight = kLineHeight; + ThemeEngine::TextInversionState inverted = ThemeEngine::kTextInversionNone; + + // Draw the selected item inverted, on a highlighted background. + if (_selectedItem == pos) + inverted = _inversion; + + Common::Rect r(getEditRect()); + int pad = _leftPadding; + + // If in numbering mode, we first print a number prefix + if (_numberingMode != kListNumberingOff) { + char temp[10]; + sprintf(temp, "%2d. ", (pos + _numberingMode)); + buffer = temp; + g_gui.theme()->drawText(Common::Rect(_x, y, _x + r.left + _leftPadding, y + fontHeight - 2), + buffer, _state, Graphics::kTextAlignLeft, inverted, _leftPadding, true); + pad = 0; + } + + int width; + + ThemeEngine::FontColor color = ThemeEngine::kFontColorNormal; + + if (!_listColors.empty()) { + if (_filter.empty() || _selectedItem == -1) + color = _listColors[pos]; + else + color = _listColors[_listIndex[pos]]; + } + + if (_selectedItem == pos && _editMode) { + buffer = _editString; + color = _editColor; + adjustOffset(); + width = _w - r.left - _hlRightPadding - _leftPadding - scrollbarW; + g_gui.theme()->drawText(Common::Rect(_x + r.left, y, _x + r.left + width, y + fontHeight - 2), buffer, _state, + Graphics::kTextAlignLeft, inverted, pad, true, ThemeEngine::kFontStyleBold, color); + } else { + buffer = _list[pos]; + width = _w - r.left - scrollbarW; + g_gui.theme()->drawText(Common::Rect(_x + r.left, y, _x + r.left + width, y + fontHeight - 2), buffer, _state, + Graphics::kTextAlignLeft, inverted, pad, true, ThemeEngine::kFontStyleBold, color); + } + + _textWidth[i] = width; + } +} + +Common::Rect ListWidget::getEditRect() const { + Common::Rect r(_hlLeftPadding, 0, _w - _hlLeftPadding - _hlRightPadding, kLineHeight - 1); + const int offset = (_selectedItem - _currentPos) * kLineHeight + _topPadding; + r.top += offset; + r.bottom += offset; + + if (_numberingMode != kListNumberingOff) { + char temp[10]; + // FIXME: Assumes that all digits have the same width. + sprintf(temp, "%2d. ", (_list.size() - 1 + _numberingMode)); + r.left += g_gui.getStringWidth(temp) + _leftPadding; + } + + return r; +} + +void ListWidget::scrollToCurrent() { + // Only do something if the current item is not in our view port + if (_selectedItem < _currentPos) { + // it's above our view + _currentPos = _selectedItem; + } else if (_selectedItem >= _currentPos + _entriesPerPage ) { + // it's below our view + _currentPos = _selectedItem - _entriesPerPage + 1; + } + + if (_currentPos < 0 || _entriesPerPage > (int)_list.size()) + _currentPos = 0; + else if (_currentPos + _entriesPerPage > (int)_list.size()) + _currentPos = _list.size() - _entriesPerPage; + + _scrollBar->_currentPos = _currentPos; + _scrollBar->recalc(); +} + +void ListWidget::scrollToEnd() { + if (_currentPos + _entriesPerPage < (int)_list.size()) { + _currentPos = _list.size() - _entriesPerPage; + } else { + return; + } + + _scrollBar->_currentPos = _currentPos; + _scrollBar->recalc(); + _scrollBar->draw(); +} + +void ListWidget::startEditMode() { + if (_editable && !_editMode && _selectedItem >= 0) { + _editMode = true; + setEditString(_list[_selectedItem]); + if (_listColors.empty()) { + _editColor = ThemeEngine::kFontColorNormal; + } else { + if (_filter.empty()) + _editColor = _listColors[_selectedItem]; + else + _editColor = _listColors[_listIndex[_selectedItem]]; + } + draw(); + g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, true); + } +} + +void ListWidget::endEditMode() { + if (!_editMode) + return; + // send a message that editing finished with a return/enter key press + _editMode = false; + _list[_selectedItem] = _editString; + g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false); + sendCommand(kListItemActivatedCmd, _selectedItem); +} + +void ListWidget::abortEditMode() { + // undo any changes made + assert(_selectedItem >= 0); + _editMode = false; + //drawCaret(true); + //draw(); + g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false); +} + +void ListWidget::reflowLayout() { + Widget::reflowLayout(); + + _leftPadding = g_gui.xmlEval()->getVar("Globals.ListWidget.Padding.Left", 0); + _rightPadding = g_gui.xmlEval()->getVar("Globals.ListWidget.Padding.Right", 0); + _topPadding = g_gui.xmlEval()->getVar("Globals.ListWidget.Padding.Top", 0); + _bottomPadding = g_gui.xmlEval()->getVar("Globals.ListWidget.Padding.Bottom", 0); + _hlLeftPadding = g_gui.xmlEval()->getVar("Globals.ListWidget.hlLeftPadding", 0); + _hlRightPadding = g_gui.xmlEval()->getVar("Globals.ListWidget.hlRightPadding", 0); + + _scrollBarWidth = g_gui.xmlEval()->getVar("Globals.Scrollbar.Width", 0); + + // HACK: Once we take padding into account, there are times where + // integer rounding leaves a big chunk of white space in the bottom + // of the list. + // We do a rough rounding on the decimal places of Entries Per Page, + // to add another entry even if it goes a tad over the padding. + frac_t entriesPerPage = intToFrac(_h - _topPadding - _bottomPadding) / kLineHeight; + + // Our threshold before we add another entry is 0.9375 (0xF000 with FRAC_BITS being 16). + const frac_t threshold = intToFrac(15) / 16; + + if ((frac_t)(entriesPerPage & FRAC_LO_MASK) >= threshold) + entriesPerPage += FRAC_ONE; + + _entriesPerPage = fracToInt(entriesPerPage); + assert(_entriesPerPage > 0); + + delete[] _textWidth; + _textWidth = new int[_entriesPerPage]; + + for (int i = 0; i < _entriesPerPage; i++) + _textWidth[i] = 0; + + if (_scrollBar) { + _scrollBar->resize(_w - _scrollBarWidth + 1, 0, _scrollBarWidth, _h); + scrollBarRecalc(); + scrollToCurrent(); + } +} + +void ListWidget::setFilter(const String &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); + + String filt = filter; + filt.toLowercase(); + + if (_filter == filt) // Filter was not changed + return; + + _filter = filt; + + if (_filter.empty()) { + // No filter -> display everything + _list = _dataList; + _listIndex.clear(); + } else { + // Restrict the list to everything which contains all words in _filter + // as substrings, ignoring case. + + Common::StringTokenizer tok(_filter); + String tmp; + int n = 0; + + _list.clear(); + _listIndex.clear(); + + for (StringArray::iterator i = _dataList.begin(); i != _dataList.end(); ++i, ++n) { + tmp = *i; + tmp.toLowercase(); + bool matches = true; + tok.reset(); + while (!tok.empty()) { + if (!tmp.contains(tok.nextToken())) { + matches = false; + break; + } + } + + if (matches) { + _list.push_back(*i); + _listIndex.push_back(n); + } + } + } + + _currentPos = 0; + _selectedItem = -1; + + 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 :). + _boss->draw(); + } +} + +} // End of namespace GUI diff --git a/gui/widgets/list.h b/gui/widgets/list.h new file mode 100644 index 0000000000..4bc9adc5ef --- /dev/null +++ b/gui/widgets/list.h @@ -0,0 +1,157 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +#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 { + kListItemDoubleClickedCmd = 'LIdb', ///< double click on item - '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 + kListSelectionChangedCmd = 'Lsch' ///< selection changed - 'data' will be item index +}; + +/* ListWidget */ +class ListWidget : public EditableWidget { +public: + typedef Common::String String; + typedef Common::Array<Common::String> StringArray; + typedef Common::Array<ThemeEngine::FontColor> ColorList; +protected: + StringArray _list; + StringArray _dataList; + ColorList _listColors; + Common::Array<int> _listIndex; + bool _editable; + bool _editMode; + NumberingMode _numberingMode; + int _currentPos; + int _entriesPerPage; + int _selectedItem; + ScrollBarWidget *_scrollBar; + int _currentKeyDown; + + String _quickSelectStr; + uint32 _quickSelectTime; + + int _hlLeftPadding; + int _hlRightPadding; + int _leftPadding; + int _rightPadding; + int _topPadding; + int _bottomPadding; + int _scrollBarWidth; + + String _filter; + bool _quickSelect; + + uint32 _cmd; + + ThemeEngine::FontColor _editColor; + +public: + ListWidget(Dialog *boss, const String &name, const char *tooltip = 0, uint32 cmd = 0); + ListWidget(Dialog *boss, int x, int y, int w, int h, const char *tooltip = 0, uint32 cmd = 0); + virtual ~ListWidget(); + + virtual Widget *findWidget(int x, int y); + + void setList(const StringArray &list, const ColorList *colors = 0); + const StringArray &getList() const { return _dataList; } + + void append(const String &s, ThemeEngine::FontColor color = ThemeEngine::kFontColorNormal); + + void setSelected(int item); + int getSelected() const { return (_filter.empty() || _selectedItem == -1) ? _selectedItem : _listIndex[_selectedItem]; } + + const String &getSelectedString() const { return _list[_selectedItem]; } + ThemeEngine::FontColor getSelectionColor() const; + + void setNumberingMode(NumberingMode numberingMode) { _numberingMode = numberingMode; } + + void scrollTo(int item); + void scrollToEnd(); + + void enableQuickSelect(bool enable) { _quickSelect = enable; } + String getQuickSelectString() const { return _quickSelectStr; } + + bool isEditable() const { return _editable; } + void setEditable(bool editable) { _editable = editable; } + void setEditColor(ThemeEngine::FontColor color) { _editColor = color; } + + // Made startEditMode/endEditMode for SaveLoadChooser + void startEditMode(); + void endEditMode(); + + void setFilter(const String &filter, bool redraw = true); + + virtual void handleTickle(); + virtual void handleMouseDown(int x, int y, int button, int clickCount); + virtual void handleMouseUp(int x, int y, int button, int clickCount); + virtual void handleMouseWheel(int x, int y, int direction); + virtual bool handleKeyDown(Common::KeyState state); + virtual bool handleKeyUp(Common::KeyState state); + virtual void handleCommand(CommandSender *sender, uint32 cmd, uint32 data); + + virtual void reflowLayout(); + + virtual bool wantsFocus() { return true; } + +protected: + void drawWidget(); + + /// 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(); + + Common::Rect getEditRect() const; + + void receivedFocusWidget(); + void lostFocusWidget(); + void scrollToCurrent(); + + int *_textWidth; +}; + +} // End of namespace GUI + +#endif diff --git a/gui/widgets/popup.cpp b/gui/widgets/popup.cpp new file mode 100644 index 0000000000..fb3fee2926 --- /dev/null +++ b/gui/widgets/popup.cpp @@ -0,0 +1,463 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +#include "common/system.h" +#include "common/events.h" +#include "gui/dialog.h" +#include "gui/GuiManager.h" +#include "gui/widgets/popup.h" +#include "engines/engine.h" + +#include "gui/ThemeEval.h" + +namespace GUI { + +// +// PopUpDialog +// + +class PopUpDialog : public Dialog { +protected: + PopUpWidget *_popUpBoss; + int _clickX, _clickY; + byte *_buffer; + int _selection; + uint32 _openTime; + bool _twoColumns; + int _entriesPerColumn; + + int _leftPadding; + int _rightPadding; + +public: + PopUpDialog(PopUpWidget *boss, int clickX, int clickY); + + void drawDialog(); + + void handleMouseUp(int x, int y, int button, int clickCount); + void handleMouseWheel(int x, int y, int direction); // Scroll through entries with scroll wheel + void handleMouseMoved(int x, int y, int button); // Redraw selections depending on mouse position + void handleKeyDown(Common::KeyState state); // Scroll through entries with arrow keys etc. + +protected: + void drawMenuEntry(int entry, bool hilite); + + int findItem(int x, int y) const; + void setSelection(int item); + bool isMouseDown(); + + void moveUp(); + void moveDown(); +}; + +PopUpDialog::PopUpDialog(PopUpWidget *boss, int clickX, int clickY) + : Dialog(0, 0, 16, 16), + _popUpBoss(boss) { + + // Copy the selection index + _selection = _popUpBoss->_selectedItem; + + // Calculate real popup dimensions + _x = _popUpBoss->getAbsX(); + _y = _popUpBoss->getAbsY() - _popUpBoss->_selectedItem * kLineHeight; + _h = _popUpBoss->_entries.size() * kLineHeight + 2; + _w = _popUpBoss->_w - kLineHeight + 2; + + _leftPadding = _popUpBoss->_leftPadding; + _rightPadding = _popUpBoss->_rightPadding; + + // 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. + + const int screenH = g_system->getOverlayHeight(); + + // HACK: For now, we do not do scrolling. Instead, we draw the dialog + // in two columns if it's too tall. + + if (_h >= screenH) { + const int screenW = g_system->getOverlayWidth(); + + _twoColumns = true; + _entriesPerColumn = _popUpBoss->_entries.size() / 2; + + if (_popUpBoss->_entries.size() & 1) + _entriesPerColumn++; + + _h = _entriesPerColumn * kLineHeight + 2; + _w = 0; + + for (uint i = 0; i < _popUpBoss->_entries.size(); i++) { + int width = g_gui.getStringWidth(_popUpBoss->_entries[i].name); + + if (width > _w) + _w = width; + } + + _w = 2 * _w + 10; + + if (!(_w & 1)) + _w++; + + if (_popUpBoss->_selectedItem >= _entriesPerColumn) { + _x -= _w / 2; + _y = _popUpBoss->getAbsY() - (_popUpBoss->_selectedItem - _entriesPerColumn) * kLineHeight; + } + + if (_w >= screenW) + _w = screenW - 1; + if (_x < 0) + _x = 0; + if (_x + _w >= screenW) + _x = screenW - 1 - _w; + } else + _twoColumns = false; + + if (_h >= screenH) + _h = screenH - 1; + if (_y < 0) + _y = 0; + else if (_y + _h >= screenH) + _y = screenH - 1 - _h; + + // TODO - implement scrolling if we had to move the menu, or if there are too many entries + + // Remember original mouse position + _clickX = clickX - _x; + _clickY = clickY - _y; + + _openTime = 0; +} + +void PopUpDialog::drawDialog() { + // Draw the menu border + g_gui.theme()->drawWidgetBackground(Common::Rect(_x, _y, _x+_w, _y+_h), 0); + + /*if (_twoColumns) + g_gui.vLine(_x + _w / 2, _y, _y + _h - 2, g_gui._color);*/ + + // Draw the entries + int count = _popUpBoss->_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); + }*/ + + if (_openTime == 0) { + // Time the popup was opened + _openTime = g_system->getMillis(); + } +} + +void PopUpDialog::handleMouseUp(int x, int y, int button, int clickCount) { + // 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 - x) * (_clickX - x) + (_clickY - y) * (_clickY - y); + if (dist > 3 * 3 || g_system->getMillis() - _openTime > 300) { + setResult(_selection); + 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); + + if (item >= 0 && _popUpBoss->_entries[item].name.size() == 0) + item = -1; + + if (item == -1 && !isMouseDown()) { + setSelection(_popUpBoss->_selectedItem); + return; + } + + // ...and update the selection accordingly + setSelection(item); +} + +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; + case Common::KEYCODE_END: + setSelection(_popUpBoss->_entries.size()-1); + break; + + case Common::KEYCODE_KP2: + if (state.flags & Common::KBD_NUM) + break; + case Common::KEYCODE_DOWN: + moveDown(); + break; + + case Common::KEYCODE_KP7: + if (state.flags & Common::KBD_NUM) + break; + case Common::KEYCODE_HOME: + setSelection(0); + break; + + case Common::KEYCODE_KP8: + if (state.flags & Common::KBD_NUM) + break; + case Common::KEYCODE_UP: + moveUp(); + break; + + default: + break; + } +} + +int PopUpDialog::findItem(int x, int y) const { + if (x >= 0 && x < _w && y >= 0 && y < _h) { + if (_twoColumns) { + uint entry = (y - 2) / kLineHeight; + if (x > _w / 2) { + entry += _entriesPerColumn; + + if (entry >= _popUpBoss->_entries.size()) + return -1; + } + return entry; + } + return (y - 2) / kLineHeight; + } + 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(_popUpBoss->_entries.size() - 1); + } else if (_selection > 0) { + int item = _selection; + do { + item--; + } while (item >= 0 && _popUpBoss->_entries[item].name.size() == 0); + if (item >= 0) + setSelection(item); + } +} + +void PopUpDialog::moveDown() { + int lastItem = _popUpBoss->_entries.size() - 1; + + if (_selection < 0) { + setSelection(0); + } else if (_selection < lastItem) { + int item = _selection; + do { + item++; + } while (item <= lastItem && _popUpBoss->_entries[item].name.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 = _popUpBoss->_entries.size() / 2; + + if (_popUpBoss->_entries.size() & 1) + n++; + + if (entry >= n) { + x = _x + 1 + _w / 2; + y = _y + 1 + kLineHeight * (entry - n); + } else { + x = _x + 1; + y = _y + 1 + kLineHeight * entry; + } + + w = _w / 2 - 1; + } else { + x = _x + 1; + y = _y + 1 + kLineHeight * entry; + w = _w - 2; + } + + Common::String &name(_popUpBoss->_entries[entry].name); + + if (name.size() == 0) { + // Draw a separator + g_gui.theme()->drawLineSeparator(Common::Rect(x, y, x+w, y+kLineHeight)); + } else { + g_gui.theme()->drawText(Common::Rect(x+1, y+2, x+w, y+2+kLineHeight), name, hilite ? ThemeEngine::kStateHighlight : ThemeEngine::kStateEnabled, + Graphics::kTextAlignLeft, ThemeEngine::kTextInversionNone, _leftPadding); + } +} + + +#pragma mark - + +// +// PopUpWidget +// + +PopUpWidget::PopUpWidget(GuiObject *boss, const String &name, const char *tooltip) + : Widget(boss, name, tooltip), CommandSender(boss) { + setFlags(WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS | WIDGET_IGNORE_DRAG); + _type = kPopUpWidget; + + _selectedItem = -1; +} + +void PopUpWidget::handleMouseDown(int x, int y, int button, int clickCount) { + if (isEnabled()) { + PopUpDialog popupDialog(this, x + getAbsX(), y + getAbsY()); + int newSel = popupDialog.runModal(); + if (newSel != -1 && _selectedItem != newSel) { + _selectedItem = newSel; + sendCommand(kPopUpItemSelectedCmd, _entries[_selectedItem].tag); + } + } +} + +void PopUpWidget::handleMouseWheel(int x, int y, int direction) { + int newSelection = _selectedItem + direction; + + // Skip separator entries + while ((newSelection >= 0) && (newSelection < (int)_entries.size()) && + _entries[newSelection].name.equals("")) { + newSelection += direction; + } + + // Just update the selected item when we're in range + if ((newSelection >= 0) && (newSelection < (int)_entries.size()) && + (newSelection != _selectedItem)) { + _selectedItem = newSelection; + draw(); + } +} + +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 String &entry, uint32 tag) { + Entry e; + e.name = entry; + e.tag = tag; + _entries.push_back(e); +} + +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::String sel; + if (_selectedItem >= 0) + sel = _entries[_selectedItem].name; + g_gui.theme()->drawPopUpWidget(Common::Rect(_x, _y, _x + _w, _y + _h), sel, _leftPadding, _state, Graphics::kTextAlignLeft); +} + +} // End of namespace GUI diff --git a/gui/widgets/popup.h b/gui/widgets/popup.h new file mode 100644 index 0000000000..b3b3e30837 --- /dev/null +++ b/gui/widgets/popup.h @@ -0,0 +1,91 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +#ifndef GUI_WIDGETS_POPUP_H +#define GUI_WIDGETS_POPUP_H + +#include "gui/widget.h" +#include "common/str.h" +#include "common/array.h" + +namespace GUI { + +enum { + kPopUpItemSelectedCmd = 'POPs' +}; + +/** + * 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 a kPopUpItemSelectedCmd + * is broadcast, with data being equal to the tag value of the selected entry. + */ +class PopUpWidget : public Widget, public CommandSender { + friend class PopUpDialog; + typedef Common::String String; + + struct Entry { + String name; + uint32 tag; + }; + typedef Common::Array<Entry> EntryList; +protected: + EntryList _entries; + int _selectedItem; + + int _leftPadding; + int _rightPadding; + +public: + PopUpWidget(GuiObject *boss, const String &name, const char *tooltip = 0); + + void handleMouseDown(int x, int y, int button, int clickCount); + void handleMouseWheel(int x, int y, int direction); + + void appendEntry(const 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) { setFlags(WIDGET_HILITED); draw(); } + void handleMouseLeft(int button) { clearFlags(WIDGET_HILITED); draw(); } + + virtual void reflowLayout(); +protected: + void drawWidget(); +}; + +} // End of namespace GUI + +#endif diff --git a/gui/widgets/scrollbar.cpp b/gui/widgets/scrollbar.cpp new file mode 100644 index 0000000000..2671fc7d61 --- /dev/null +++ b/gui/widgets/scrollbar.cpp @@ -0,0 +1,233 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +#include "gui/widgets/scrollbar.h" +#include "gui/dialog.h" +#include "gui/GuiManager.h" + +#include "common/timer.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; +} + +static void upArrowRepeater(void *ref) { + ScrollBarWidget *sb = (ScrollBarWidget *)ref; + int old_pos = sb->_currentPos; + + sb->_currentPos -= 3; + sb->checkBounds(old_pos); + + g_system->getTimerManager()->removeTimerProc(&upArrowRepeater); + g_system->getTimerManager()->installTimerProc(&upArrowRepeater, 1000000/10, ref); +} + +static void downArrowRepeater(void *ref) { + ScrollBarWidget *sb = (ScrollBarWidget *)ref; + int old_pos = sb->_currentPos; + + sb->_currentPos += 3; + sb->checkBounds(old_pos); + + g_system->getTimerManager()->removeTimerProc(&downArrowRepeater); + g_system->getTimerManager()->installTimerProc(&downArrowRepeater, 1000000/10, ref); +} + +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--; + _draggingPart = kUpArrowPart; + g_system->getTimerManager()->installTimerProc(&upArrowRepeater, 1000000/2, this); + } else if (y >= _h - UP_DOWN_BOX_HEIGHT) { + // Down arrow + _currentPos++; + _draggingPart = kDownArrowPart; + g_system->getTimerManager()->installTimerProc(&downArrowRepeater, 1000000/2, this); + } 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; + + g_system->getTimerManager()->removeTimerProc(&upArrowRepeater); + g_system->getTimerManager()->removeTimerProc(&downArrowRepeater); +} + +void ScrollBarWidget::handleMouseWheel(int x, int y, int direction) { + int old_pos = _currentPos; + + if (_numEntries < _entriesPerPage) + return; + + if (direction < 0) { + _currentPos--; + } else { + _currentPos++; + } + + // 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) + draw(); + } +} + +void ScrollBarWidget::handleTickle() { +/* + // FIXME/TODO - this code is supposed to allow for "click-repeat" (like key repeat), + // i.e. if you click on one of the arrows and keep clicked, it will scroll + // continuously. However, just like key repeat, this requires two delays: + // First an "initial" delay that has to pass before repeating starts (otherwise + // it is near to impossible to achieve single clicks). Secondly, a repeat delay + // that determines how often per second a click is simulated. + int old_pos = _currentPos; + + if (_draggingPart == kUpArrowPart) + _currentPos--; + else if (_draggingPart == kDownArrowPart) + _currentPos++; + + // Make sure that _currentPos is still inside the bounds + checkBounds(old_pos); +*/ +} + +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(); + draw(); + 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, _state); +} + +} // End of namespace GUI diff --git a/gui/widgets/scrollbar.h b/gui/widgets/scrollbar.h new file mode 100644 index 0000000000..53a3369fb4 --- /dev/null +++ b/gui/widgets/scrollbar.h @@ -0,0 +1,84 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +#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; + +public: + int _numEntries; + int _entriesPerPage; + int _currentPos; + +public: + ScrollBarWidget(GuiObject *boss, int x, int y, int w, int h); + + void handleMouseDown(int x, int y, int button, int clickCount); + void handleMouseUp(int x, int y, int button, int clickCount); + void handleMouseWheel(int x, int y, int direction); + void handleMouseMoved(int x, int y, int button); + void handleMouseEntered(int button) { setFlags(WIDGET_HILITED); } + void handleMouseLeft(int button) { clearFlags(WIDGET_HILITED); _part = kNoPart; draw(); } + void handleTickle(); + + // 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(); +}; + +} // End of namespace GUI + +#endif diff --git a/gui/widgets/tab.cpp b/gui/widgets/tab.cpp new file mode 100644 index 0000000000..48b4dab430 --- /dev/null +++ b/gui/widgets/tab.cpp @@ -0,0 +1,320 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +#include "common/util.h" +#include "gui/widgets/tab.h" +#include "gui/dialog.h" +#include "gui/GuiManager.h" + +#include "gui/ThemeEval.h" + +namespace GUI { + +enum { + kCmdLeft = 'LEFT', + kCmdRight = 'RGHT' +}; + +TabWidget::TabWidget(GuiObject *boss, int x, int y, int w, int h) + : Widget(boss, x, y, w, h), _bodyBackgroundType(GUI::ThemeEngine::kDialogBackgroundDefault) { + init(); +} + +TabWidget::TabWidget(GuiObject *boss, const String &name) + : Widget(boss, name), _bodyBackgroundType(GUI::ThemeEngine::kDialogBackgroundDefault) { + init(); +} + +void TabWidget::init() { + setFlags(WIDGET_ENABLED); + _type = kTabWidget; + _activeTab = -1; + _firstVisibleTab = 0; + + _tabWidth = 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.NavButtonPadding.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); + + int x = _w - _butRP - _butW * 2 - 2; + int y = _butTP - _tabHeight; + _navLeft = new ButtonWidget(this, x, y, _butW, _butH, "<", 0, kCmdLeft); + _navRight = new ButtonWidget(this, x + _butW + 2, y, _butW, _butH, ">", 0, kCmdRight); +} + +TabWidget::~TabWidget() { + _firstWidget = 0; + for (uint i = 0; i < _tabs.size(); ++i) { + delete _tabs[i].firstWidget; + _tabs[i].firstWidget = 0; + } + _tabs.clear(); + delete _navRight; +} + +int16 TabWidget::getChildY() const { + return getAbsY() + _tabHeight; +} + +int TabWidget::addTab(const String &title) { + // Add a new tab page + Tab newTab; + newTab.title = title; + newTab.firstWidget = 0; + + _tabs.push_back(newTab); + + int numTabs = _tabs.size(); + + // HACK: Nintendo DS uses a custom config dialog. This dialog does not work with + // our default "Globals.TabWidget.Tab.Width" setting. + // + // TODO: Add proper handling in the theme layout for such cases. + // + // There are different solutions to this problem: + // - offer a "Tab.Width" setting per tab widget and thus let the Ninteno DS + // backend set a default value for its special dialog. + // + // - change our themes to use auto width calculaction by default + // + // - change "Globals.TabWidget.Tab.Width" to be the minimal tab width setting and + // rename it accordingly. + // Actually this solution is pretty similar to our HACK for the Nintendo DS + // backend. This hack enables auto width calculation by default with the + // "Globals.TabWidget.Tab.Width" value as minimal width for the tab buttons. + // + // - we might also consider letting every tab button having its own width. + // + // - other solutions you can think of, which are hopefully less evil ;-). + // + // Of course also the Ninteno DS' dialog should be in our layouting engine, instead + // of being hard coded like it is right now. + // + // There are checks for __DS__ all over this source file to take care of the + // aforemnetioned problem. +#ifdef __DS__ + if (true) { +#else + if (g_gui.xmlEval()->getVar("Globals.TabWidget.Tab.Width") == 0) { +#endif + if (_tabWidth == 0) + _tabWidth = 40; + // Determine the new tab width + int newWidth = g_gui.getStringWidth(title) + 2 * 3; + if (_tabWidth < newWidth) + _tabWidth = newWidth; + int maxWidth = _w / numTabs; + if (_tabWidth > maxWidth) + _tabWidth = maxWidth; + } + + // Activate the new tab + setActiveTab(numTabs - 1); + + return _activeTab; +} + +void TabWidget::removeTab(int tabID) { + assert(0 <= tabID && tabID < (int)_tabs.size()); + + // Deactive the tab if it's currently the active one + if (tabID == _activeTab) { + _tabs[tabID].firstWidget = _firstWidget; + releaseFocus(); + _firstWidget = 0; + } + + // 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 + _boss->draw(); +} + +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; + _boss->draw(); + } +} + + +void TabWidget::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) { + Widget::handleCommand(sender, cmd, data); + + switch (cmd) { + case kCmdLeft: + if (_firstVisibleTab) { + _firstVisibleTab--; + draw(); + } + break; + + case kCmdRight: + if (_firstVisibleTab + _w / _tabWidth < (int)_tabs.size()) { + _firstVisibleTab++; + draw(); + } + break; + } +} + +void TabWidget::handleMouseDown(int x, int y, int button, int clickCount) { + assert(y < _tabHeight); + + // Determine which tab was clicked + int tabID = -1; + if (x >= 0 && (x % _tabWidth) < _tabWidth) { + tabID = x / _tabWidth; + if (tabID >= (int)_tabs.size()) + tabID = -1; + } + + // If a tab was clicked, switch to that pane + if (tabID >= 0 && tabID + _firstVisibleTab < (int)_tabs.size()) { + setActiveTab(tabID + _firstVisibleTab); + } +} + +bool TabWidget::handleKeyDown(Common::KeyState state) { + // TODO: maybe there should be a way to switch between tabs + // using the keyboard? E.g. Alt-Shift-Left/Right-Arrow or something + // like that. + return Widget::handleKeyDown(state); +} + +void TabWidget::reflowLayout() { + Widget::reflowLayout(); + + for (uint i = 0; i < _tabs.size(); ++i) { + Widget *w = _tabs[i].firstWidget; + while (w) { + w->reflowLayout(); + w = w->next(); + } + } + + _tabHeight = g_gui.xmlEval()->getVar("Globals.TabWidget.Tab.Height"); + _tabWidth = g_gui.xmlEval()->getVar("Globals.TabWidget.Tab.Width"); + _titleVPad = g_gui.xmlEval()->getVar("Globals.TabWidget.Tab.Padding.Top"); + + if (_tabWidth == 0) { + _tabWidth = 40; +#ifdef __DS__ + } + if (true) { +#endif + int maxWidth = _w / _tabs.size(); + + for (uint i = 0; i < _tabs.size(); ++i) { + // Determine the new tab width + int newWidth = g_gui.getStringWidth(_tabs[i].title) + 2 * 3; + if (_tabWidth < newWidth) + _tabWidth = newWidth; + if (_tabWidth > maxWidth) + _tabWidth = maxWidth; + } + } + + _butRP = g_gui.xmlEval()->getVar("Globals.TabWidget.NavButton.PaddingRight", 0); + _butTP = g_gui.xmlEval()->getVar("Globals.TabWidget.NavButton.Padding.Top", 0); + _butW = g_gui.xmlEval()->getVar("GlobalsTabWidget.NavButton.Width", 10); + _butH = g_gui.xmlEval()->getVar("Globals.TabWidget.NavButton.Height", 10); + + int x = _w - _butRP - _butW * 2 - 2; + int y = _butTP - _tabHeight; + _navLeft->resize(x, y, _butW, _butH); + _navRight->resize(x + _butW + 2, y, _butW, _butH); +} + +void TabWidget::drawWidget() { + Common::Array<Common::String> tabs; + for (int i = _firstVisibleTab; i < (int)_tabs.size(); ++i) { + tabs.push_back(_tabs[i].title); + } + g_gui.theme()->drawDialogBackground(Common::Rect(_x + _bodyLP, _y + _bodyTP, _x+_w-_bodyRP, _y+_h-_bodyBP), _bodyBackgroundType); + + g_gui.theme()->drawTab(Common::Rect(_x, _y, _x+_w, _y+_h), _tabHeight, _tabWidth, tabs, _activeTab - _firstVisibleTab, 0, _titleVPad); +} + +void TabWidget::draw() { + Widget::draw(); + + if (_tabWidth * _tabs.size() > _w) { + _navLeft->draw(); + _navRight->draw(); + } +} + +Widget *TabWidget::findWidget(int x, int y) { + if (y < _tabHeight) { + if (_tabWidth * _tabs.size() > _w) { + if (y >= _butTP && y < _butTP + _butH) { + 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; + } else { + // Iterate over all child widgets and find the one which was clicked + return Widget::findWidgetInChain(_firstWidget, x, y - _tabHeight); + } +} + +} // End of namespace GUI diff --git a/gui/widgets/tab.h b/gui/widgets/tab.h new file mode 100644 index 0000000000..aec68cca0a --- /dev/null +++ b/gui/widgets/tab.h @@ -0,0 +1,118 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +#ifndef GUI_WIDGETS_TAB_H +#define GUI_WIDGETS_TAB_H + +#include "gui/widget.h" +#include "common/str.h" +#include "common/array.h" + +namespace GUI { + +class TabWidget : public Widget { + typedef Common::String String; + struct Tab { + String title; + Widget *firstWidget; + }; + typedef Common::Array<Tab> TabList; + +protected: + int _activeTab; + int _firstVisibleTab; + TabList _tabs; + int _tabWidth; + int _tabHeight; + + int _bodyRP, _bodyTP, _bodyLP, _bodyBP; + ThemeEngine::DialogBackground _bodyBackgroundType; + + int _titleVPad; + + int _butRP, _butTP, _butW, _butH; + + ButtonWidget *_navLeft, *_navRight; + +public: + TabWidget(GuiObject *boss, int x, int y, int w, int h); + TabWidget(GuiObject *boss, const String &name); + ~TabWidget(); + + 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 String &title); + + /** + * 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); + + void setTabTitle(int tabID, const String &title) { + assert(0 <= tabID && tabID < (int)_tabs.size()); + _tabs[tabID].title = title; + } + + virtual void handleMouseDown(int x, int y, int button, int clickCount); + virtual bool handleKeyDown(Common::KeyState state); + virtual void handleCommand(CommandSender *sender, uint32 cmd, uint32 data); + + virtual void reflowLayout(); + + virtual void draw(); + +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. + virtual int16 getChildY() const; + + virtual void drawWidget(); + + virtual Widget *findWidget(int x, int y); +}; + +} // End of namespace GUI + +#endif |