PopUpWidget.cpp
/* ScummVM - Scumm Interpreter
* Copyright (C) 2002-2006 The ScummVM project
*
* 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.
*
* $Header$
*/
#include "common/stdafx.h"
#include "common/system.h"
#include "gui/dialog.h"
#include "gui/newgui.h"
#include "gui/PopUpWidget.h"
#include "base/engine.h"
namespace GUI {
//
// PopUpDialog
//
class PopUpDialog : public Dialog {
protected:
PopUpWidget *_popUpBoss;
int _clickX, _clickY;
byte *_buffer;
int _selection;
uint32 _openTime;
bool _twoColumns;
int _entriesPerColumn;
public:
PopUpDialog(PopUpWidget *boss, int clickX, int clickY, WidgetSize ws = kDefaultWidgetSize);
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(uint16 ascii, int keycode, int modifiers); // 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, WidgetSize ws)
: Dialog(0, 0, 16, 16),
_popUpBoss(boss) {
// Copy the selection index
_selection = _popUpBoss->_selectedItem;
// Calculate real popup dimensions
_x = _popUpBoss->getAbsX() + _popUpBoss->_labelWidth;
_y = _popUpBoss->getAbsY() - _popUpBoss->_selectedItem * kLineHeight;
_h = _popUpBoss->_entries.size() * kLineHeight + 2;
_w = _popUpBoss->_w - kLineHeight + 2 - _popUpBoss->_labelWidth;
// 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;
// Time the popup was opened
_openTime = getMillis();
}
void PopUpDialog::drawDialog() {
// Draw the menu border
g_gui.hLine(_x, _y, _x+_w - 1, g_gui._color);
g_gui.hLine(_x, _y + _h - 1, _x + _w - 1, g_gui._shadowcolor);
g_gui.vLine(_x, _y, _y+_h - 1, g_gui._color);
g_gui.vLine(_x + _w - 1, _y, _y + _h - 1, g_gui._shadowcolor);
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);
}
g_gui.addDirtyRect(_x, _y, _w, _h);
}
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 || 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())
return;
// ...and update the selection accordingly
setSelection(item);
}
void PopUpDialog::handleKeyDown(uint16 ascii, int keycode, int modifiers) {
if (keycode == 27) { // escape
close();
return;
}
if (isMouseDown())
return;
switch (keycode) {
case '\n': // enter/return
case '\r':
setResult(_selection);
close();
break;
case 256+17: // up arrow
moveUp();
break;
case 256+18: // down arrow
moveDown();
break;
case 256+22: // home
setSelection(0);
break;
case 256+23: // end
setSelection(_popUpBoss->_entries.size()-1);
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;
g_gui.fillRect(x, y, w, kLineHeight, hilite ? g_gui._textcolorhi : g_gui._bgcolor);
if (name.size() == 0) {
// Draw a separator
g_gui.hLine(x - 1, y + kLineHeight / 2, x + w, g_gui._shadowcolor);
g_gui.hLine(x, y + 1 + kLineHeight / 2, x + w, g_gui._color);
} else {
g_gui.drawString(name, x + 1, y + 2, w - 2, hilite ? g_gui._bgcolor : g_gui._textcolor);
}
g_gui.addDirtyRect(x, y, w, kLineHeight);
}
#pragma mark -
//
// PopUpWidget
//
PopUpWidget::PopUpWidget(GuiObject *boss, int x, int y, int w, int h, const String &label, uint labelWidth, WidgetSize ws)
: Widget(boss, x, y - 1, w, h + 2), CommandSender(boss), _ws(ws), _label(label), _labelWidth(labelWidth) {
_flags = WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS;
_type = kPopUpWidget;
_selectedItem = -1;
if (!_label.isEmpty() && _labelWidth == 0)
_labelWidth = g_gui.getStringWidth(_label);
}
void PopUpWidget::handleMouseDown(int x, int y, int button, int clickCount) {
if (isEnabled()) {
PopUpDialog popupDialog(this, x + getAbsX(), y + getAbsY(), _ws);
int newSel = popupDialog.runModal();
if (newSel != -1 && _selectedItem != newSel) {
_selectedItem = newSel;
sendCommand(kPopUpItemSelectedCmd, _entries[_selectedItem].tag);
}
}
}
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) {
// FIXME
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(bool hilite) {
NewGui *gui = &g_gui;
int x = _x + _labelWidth;
int w = _w - _labelWidth;
// Draw the label, if any
if (_labelWidth > 0)
gui->drawString(_label, _x, _y + 3, _labelWidth, isEnabled() ? gui->_textcolor : gui->_color, kTextAlignRight);
// Draw a thin frame around us.
gui->hLine(x, _y, x + w - 1, gui->_color);
gui->hLine(x, _y +_h-1, x + w - 1, gui->_shadowcolor);
gui->vLine(x, _y, _y+_h-1, gui->_color);
gui->vLine(x + w - 1, _y, _y +_h - 1, gui->_shadowcolor);
// Draw a set of arrows at the right end to signal this is a dropdown/popup
Common::Point p0, p1;
p0 = Common::Point(x + w + 1 - _h / 2, _y + 4);
p1 = Common::Point(x + w + 1 - _h / 2, _y + _h - 4);
Graphics::Surface &surf = g_gui.getScreen();
OverlayColor color = !isEnabled() ? gui->_color : hilite ? gui->_textcolorhi : gui->_textcolor;
// Evil HACK to draw filled triangles
// FIXME: The "big" version is pretty ugly.
for (; p1.y - p0.y > 1; p0.y++, p0.x--, p1.y--, p1.x++) {
surf.drawLine(p0.x, p0.y, p1.x, p0.y, color);
surf.drawLine(p0.x, p1.y, p1.x, p1.y, color);
}
// Draw the selected entry, if any
if (_selectedItem >= 0) {
TextAlignment align = (g_gui.getStringWidth(_entries[_selectedItem].name) > w-6) ? kTextAlignRight : kTextAlignLeft;
gui->drawString(_entries[_selectedItem].name, x+2, _y+3, w-6, !isEnabled() ? gui->_color : gui->_textcolor, align);
}
}
} // End of namespace GUI