scummvm/engines/lure/menu.cpp
Lars Sundström baf59da8d0 LURE: Improve top menu handling for touch based systems
The top menu bar was problematic to control on touch based systems
since the default behaviour for taps is to send button down and up
events on taps. Make the top menu work in this case as well by
closing the menu only when the user have made a decision.
2023-08-22 23:37:11 +01:00

661 lines
20 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "lure/menu.h"
#include "lure/luredefs.h"
#include "lure/decode.h"
#include "lure/surface.h"
#include "lure/res_struct.h"
#include "lure/res.h"
#include "lure/strings.h"
#include "lure/room.h"
#include "lure/events.h"
#include "lure/lure.h"
namespace Lure {
MenuRecord::MenuRecord(const MenuRecordBounds *bounds, int numParams, ...) {
// Store list of pointers to strings
va_list params;
_numEntries = numParams;
_entries = (const char **) malloc(sizeof(const char *) * _numEntries);
va_start(params, numParams);
for (int index = 0; index < _numEntries; ++index)
_entries[index] = va_arg(params, const char *);
va_end(params);
// Store position data
_hsxstart = bounds->left; _hsxend = bounds->right;
_xstart = bounds->contentsX << 3;
_width = (bounds->contentsWidth + 3) << 3;
}
MenuRecord::~MenuRecord() {
free(_entries);
_entries = nullptr;
}
const char *MenuRecord::getEntry(uint8 index) {
if (index >= _numEntries) error("Invalid menuitem index specified: %d", index);
return _entries[index];
}
/*--------------------------------------------------------------------------*/
static Menu *int_menu = nullptr;
const MenuRecordLanguage menuList[] = {
{Common::EN_ANY, {{40, 87, 3, 7}, {127, 179, 13, 12}, {224, 281, 27, 10}}},
{Common::IT_ITA, {{40, 98, 4, 6}, {120, 195, 14, 11}, {208, 281, 24, 13}}},
{Common::FR_FRA, {{40, 90, 3, 7}, {120, 195, 13, 11}, {232, 273, 23, 13}}},
{Common::DE_DEU, {{44, 95, 1, 11}, {135, 178, 8, 23}, {232, 273, 22, 15}}},
{Common::ES_ESP, {{40, 90, 3, 8}, {120, 195, 11, 13}, {208, 281, 17, 18}}},
{Common::RU_RUS, {{40, 87, 3, 7}, {127, 179, 13, 12}, {224, 281, 27, 10}}},
{Common::UNK_LANG, {{0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}}
};
Menu::Menu() {
int_menu = this;
StringList &sl = Resources::getReference().stringList();
Common::Language language = LureEngine::getReference().getLanguage();
MemoryBlock *data = Disk::getReference().getEntry(MENU_RESOURCE_ID);
PictureDecoder decoder;
_menu = decoder.decode(data, SCREEN_SIZE);
delete data;
const MenuRecordLanguage *rec = &menuList[0];
while ((rec->language != Common::UNK_LANG) && (rec->language != language))
++rec;
if (rec->language == Common::UNK_LANG)
error("Unknown language encountered in top line handler");
_menus[0] = new MenuRecord(&rec->menus[0], 1, sl.getString(S_CREDITS));
_menus[1] = new MenuRecord(&rec->menus[1], 3,
sl.getString(S_RESTART_GAME), sl.getString(S_SAVE_GAME), sl.getString(S_RESTORE_GAME));
_menus[2] = new MenuRecord(&rec->menus[2], 3,
sl.getString(S_QUIT), sl.getString(S_SLOW_TEXT), sl.getString(S_SOUND_ON));
_selectedMenu = nullptr;
}
Menu::~Menu() {
for (int ctr=0; ctr<NUM_MENUS; ++ctr) delete _menus[ctr];
delete _menu;
}
Menu &Menu::getReference() {
return *int_menu;
}
uint8 Menu::execute() {
OSystem &system = *g_system;
LureEngine &engine = LureEngine::getReference();
Mouse &mouse = Mouse::getReference();
Events &events = Events::getReference();
Screen &screen = Screen::getReference();
mouse.setCursorNum(CURSOR_ARROW);
system.copyRectToScreen(_menu->data(), FULL_SCREEN_WIDTH, 0, 0,
FULL_SCREEN_WIDTH, MENUBAR_Y_SIZE);
_selectedMenu = nullptr;
_surfaceMenu = nullptr;
_selectedIndex = 0;
while (mouse.lButton() || mouse.rButton() || g_system->hasFeature(OSystem::kFeatureTouchscreen)) {
while (events.pollEvent()) {
if (engine.shouldQuit()) return MENUITEM_NONE;
if (mouse.y() < MENUBAR_Y_SIZE) {
MenuRecord *p = getMenuAt(mouse.x());
if (_selectedMenu != p) {
// If necessary, remove prior menu
if (_selectedMenu) {
toggleHighlight(_selectedMenu);
screen.updateArea(0, 0, FULL_SCREEN_WIDTH, _surfaceMenu->height() + 8);
delete _surfaceMenu;
_surfaceMenu = nullptr;
_selectedIndex = 0;
}
_selectedMenu = p;
// If a new menu is selected, show it
if (_selectedMenu) {
toggleHighlight(_selectedMenu);
_surfaceMenu = Surface::newDialog(
_selectedMenu->width(), _selectedMenu->numEntries(),
_selectedMenu->entries(), false, DEFAULT_TEXT_COLOR, false);
_surfaceMenu->copyToScreen(_selectedMenu->xstart(), MENUBAR_Y_SIZE);
}
system.copyRectToScreen(_menu->data(), FULL_SCREEN_WIDTH, 0, 0,
FULL_SCREEN_WIDTH, MENUBAR_Y_SIZE);
}
}
// Check for changing selected index
uint8 index = getIndexAt(mouse.x(), mouse.y());
if (index != _selectedIndex) {
if (_selectedIndex != 0) toggleHighlightItem(_selectedIndex);
_selectedIndex = index;
if (_selectedIndex != 0) toggleHighlightItem(_selectedIndex);
}
}
if (g_system->hasFeature(OSystem::kFeatureTouchscreen)) {
// Close menu only when a sub menu is shown and
// the user has either clicked on a selected index
// or no index (outside the sub menu == cancelled)
if (mouse.lButton() &&
_surfaceMenu != nullptr) {
break;
}
}
system.updateScreen();
system.delayMillis(10);
}
delete _surfaceMenu;
// Deselect the currently selected menu header
if (_selectedMenu)
toggleHighlight(_selectedMenu);
// Restore the previous screen
screen.update();
if ((_selectedMenu == nullptr) || (_selectedIndex == 0)) return MENUITEM_NONE;
else if (_selectedMenu == _menus[0])
return MENUITEM_CREDITS;
else if (_selectedMenu == _menus[1]) {
switch (_selectedIndex) {
case 1:
return MENUITEM_RESTART_GAME;
case 2:
return MENUITEM_SAVE_GAME;
case 3:
return MENUITEM_RESTORE_GAME;
default:
break;
}
} else {
switch (_selectedIndex) {
case 1:
return MENUITEM_QUIT;
case 2:
return MENUITEM_TEXT_SPEED;
case 3:
return MENUITEM_SOUND;
default:
break;
}
}
return MENUITEM_NONE;
}
MenuRecord *Menu::getMenuAt(int x) {
for (int ctr = 0; ctr < NUM_MENUS; ++ctr)
if ((x >= _menus[ctr]->hsxstart()) && (x <= _menus[ctr]->hsxend()))
return _menus[ctr];
return nullptr;
}
uint8 Menu::getIndexAt(uint16 x, uint16 y) {
if (!_selectedMenu) return 0;
int ys = MENUBAR_Y_SIZE + Surface::textY();
int ye = MENUBAR_Y_SIZE + (_surfaceMenu->height() - Surface::textY());
if ((y < ys) || (y > ye)) return 0;
uint16 yRelative = y - ys;
uint8 index = (uint8) (yRelative / 8) + 1;
if (index > _selectedMenu->numEntries()) index = _selectedMenu->numEntries();
return index;
}
#define MENUBAR_SELECTED_COLOR 0xf7
void Menu::toggleHighlight(MenuRecord *menuRec) {
const byte colorList[4] = {4, 2, 0, 0xf7};
const byte *colors = LureEngine::getReference().isEGA() ? &colorList[0] : &colorList[2];
byte *addr = _menu->data();
for (uint16 y=0; y<MENUBAR_Y_SIZE; ++y) {
for (uint16 x=menuRec->hsxstart(); x<=menuRec->hsxend(); ++x) {
if (addr[x] == colors[0]) addr[x] = colors[1];
else if (addr[x] == colors[1]) addr[x] = colors[0];
}
addr += FULL_SCREEN_WIDTH;
}
}
void Menu::toggleHighlightItem(uint8 index) {
const byte colorList[4] = {EGA_DIALOG_TEXT_COLOR, EGA_DIALOG_WHITE_COLOR,
VGA_DIALOG_TEXT_COLOR, VGA_DIALOG_WHITE_COLOR};
const byte *colors = LureEngine::getReference().isEGA() ? &colorList[0] : &colorList[2];
byte *p = _surfaceMenu->data().data() + (Surface::textY() +
((index - 1) * FONT_HEIGHT)) * _surfaceMenu->width() + Surface::textX();
int numBytes =_surfaceMenu->width() - Surface::textX() * 2;
for (int y = 0; y < FONT_HEIGHT; ++y, p += _surfaceMenu->width()) {
byte *pTemp = p;
for (int x = 0; x < numBytes; ++x, ++pTemp) {
if (*pTemp == colors[0]) *pTemp = colors[1];
else if (*pTemp == colors[1]) *pTemp = colors[0];
}
}
_surfaceMenu->copyToScreen(_selectedMenu->xstart(), MENUBAR_Y_SIZE);
}
/*--------------------------------------------------------------------------*/
uint16 PopupMenu::ShowInventory() {
Resources &rsc = Resources::getReference();
StringData &strings = StringData::getReference();
uint16 numItems = rsc.numInventoryItems();
uint16 itemCtr = 0;
char **itemNames = (char **) Memory::alloc(sizeof(char *) * numItems);
uint16 *idList = (uint16 *) Memory::alloc(sizeof(uint16) * numItems);
HotspotDataList::iterator i;
for (i = rsc.hotspotData().begin(); i != rsc.hotspotData().end(); ++i) {
HotspotData const &hotspot = **i;
if (hotspot.roomNumber == PLAYER_ID) {
idList[itemCtr] = hotspot.hotspotId;
char *hotspotName = itemNames[itemCtr++] = (char *) malloc(MAX_HOTSPOT_NAME_SIZE);
strings.getString(hotspot.nameId, hotspotName);
}
}
uint16 result = Show(numItems, const_cast<const char **>(itemNames));
if (result != 0xffff) result = idList[result];
for (itemCtr = 0; itemCtr < numItems; ++itemCtr)
free(itemNames[itemCtr]);
Memory::dealloc(itemNames);
Memory::dealloc(idList);
return result;
}
#define MAX_NUM_DISPLAY_ITEMS 20
uint16 PopupMenu::ShowItems(Action contextAction, uint16 roomNumber) {
Resources &res = Resources::getReference();
ValueTableData &fields = res.fieldList();
RoomDataList &rooms = res.roomData();
HotspotDataList &hotspots = res.hotspotData();
StringData &strings = StringData::getReference();
Room &room = Room::getReference();
Screen &screen = Screen::getReference();
Mouse &mouse = Mouse::getReference();
RoomDataList::iterator ir;
HotspotDataList::iterator ih;
uint16 entryIds[MAX_NUM_DISPLAY_ITEMS];
uint16 nameIds[MAX_NUM_DISPLAY_ITEMS];
char *entryNames[MAX_NUM_DISPLAY_ITEMS];
int numItems = 0;
int itemCtr;
uint32 contextBitflag = 1 << (contextAction - 1);
// Loop for rooms
for (ir = rooms.begin(); ir != rooms.end(); ++ir) {
RoomData const &roomData = **ir;
// Pre-condition checks for whether to skip room
if ((roomData.hdrFlags != 15) && ((roomData.hdrFlags & fields.hdrFlagMask()) == 0))
continue;
if (((roomData.flags & HOTSPOTFLAG_MENU_EXCLUSION) != 0) || ((roomData.flags & HOTSPOTFLAG_FOUND) == 0))
continue;
if ((roomData.actions & contextBitflag) == 0)
continue;
// Add room to list of entries to display
if (numItems == MAX_NUM_DISPLAY_ITEMS) error("Out of space in ask list");
entryIds[numItems] = roomData.roomNumber;
nameIds[numItems] = roomData.roomNumber;
entryNames[numItems] = (char *) Memory::alloc(MAX_HOTSPOT_NAME_SIZE);
strings.getString(roomData.roomNumber, entryNames[numItems]);
++numItems;
}
// Loop for hotspots
for (ih = hotspots.begin(); ih != hotspots.end(); ++ih) {
HotspotData const &hotspot = **ih;
if ((hotspot.headerFlags != 15) &&
((hotspot.headerFlags & fields.hdrFlagMask()) == 0))
continue;
if (((hotspot.flags & HOTSPOTFLAG_MENU_EXCLUSION) != 0) || ((hotspot.flags & HOTSPOTFLAG_FOUND) == 0))
// Skip the current hotspot
continue;
// If the hotspot is room specific, skip if the character will not be in the specified room
if (((hotspot.flags & HOTSPOTFLAG_ROOM_SPECIFIC) != 0) &&
(hotspot.roomNumber != roomNumber))
continue;
// If hotspot does not allow action, then skip it
if ((hotspot.actions & contextBitflag) == 0)
continue;
// If a special hotspot Id, then skip displaying
if ((hotspot.nameId == 0x17A) || (hotspot.nameId == 0x147))
continue;
// Check if the hotspot's name is already used in an already set item
itemCtr = 0;
while ((itemCtr < numItems) && (nameIds[itemCtr] != hotspot.nameId))
++itemCtr;
if (itemCtr != numItems)
// Item's name is already present - skip hotspot
continue;
// Add hotspot to list of entries to display
if (numItems == MAX_NUM_DISPLAY_ITEMS) error("Out of space in ask list");
entryIds[numItems] = hotspot.hotspotId;
nameIds[numItems] = hotspot.nameId;
entryNames[numItems] = (char *) Memory::alloc(MAX_HOTSPOT_NAME_SIZE);
strings.getString(hotspot.nameId, entryNames[numItems]);
++numItems;
}
if (numItems == 0) {
// No items, so add a 'nothing' to the statusLine
if (LureEngine::getReference().getLanguage() == Common::RU_RUS)
Common::strcat_s(room.statusLine(), MAX_DESC_SIZE, "(ybxtuj ytn)");
else
Common::strcat_s(room.statusLine(), MAX_DESC_SIZE, "(nothing)");
}
room.update();
screen.update();
mouse.waitForRelease();
if (numItems == 0)
// Return flag for no items to ask for
return 0xfffe;
// Display items
uint16 result = Show(numItems, const_cast<const char **>(entryNames));
if (result != 0xffff) result = entryIds[result];
// Deallocate display strings
for (itemCtr = 0; itemCtr < numItems; ++itemCtr)
Memory::dealloc(entryNames[itemCtr]);
return result;
}
static int entryCompare(const char **p1, const char **p2) {
return strcmp(*p1, *p2);
}
typedef int (*CompareMethod)(const void*, const void*);
Action PopupMenu::Show(uint32 actionMask) {
StringList &stringList = Resources::getReference().stringList();
int numEntries = 0;
uint32 v = actionMask;
int index;
int currentAction;
uint16 resultIndex;
Action resultAction;
for (index = 1; index <= EXAMINE; ++index, v >>= 1) {
if (v & 1) ++numEntries;
}
const char **strList = (const char **) Memory::alloc(sizeof(char *) * numEntries);
int strIndex = 0;
for (currentAction = 0; currentAction < (int)EXAMINE; ++currentAction) {
if ((actionMask & (1 << currentAction)) != 0) {
strList[strIndex] = stringList.getString(currentAction);
++strIndex;
}
}
// Sort the list
qsort(strList, numEntries, sizeof(const char *), (CompareMethod) entryCompare);
// Show the entries
resultIndex = Show(numEntries, strList);
resultAction = NONE;
if (resultIndex != 0xffff) {
// Scan through the list of actions to find the selected entry
for (currentAction = 0; currentAction < (int)EXAMINE; ++currentAction) {
if (strList[resultIndex] == stringList.getString(currentAction)) {
resultAction = (Action) (currentAction + 1);
break;
}
}
}
Memory::dealloc(strList);
return resultAction;
}
Action PopupMenu::Show(int numEntries, Action *actions) {
StringList &stringList = Resources::getReference().stringList();
const char **strList = (const char **) Memory::alloc(sizeof(char *) * numEntries);
Action *actionPtr = actions;
for (int index = 0; index < numEntries; ++index)
strList[index] = stringList.getString(*actionPtr++);
uint16 result = Show(numEntries, strList);
Memory::dealloc(strList);
if (result == 0xffff) return NONE;
else return actions[result];
}
uint16 PopupMenu::Show(int numEntries, const char *actions[]) {
if (numEntries == 0) return 0xffff;
LureEngine &engine = LureEngine::getReference();
Events &e = Events::getReference();
Mouse &mouse = Mouse::getReference();
OSystem &system = *g_system;
Screen &screen = Screen::getReference();
bool isEGA = LureEngine::getReference().isEGA();
byte textColor = isEGA ? EGA_DIALOG_TEXT_COLOR : VGA_DIALOG_TEXT_COLOR;
byte whiteColor = isEGA ? EGA_DIALOG_WHITE_COLOR : VGA_DIALOG_WHITE_COLOR;
const uint16 yMiddle = FULL_SCREEN_HEIGHT / 2;
uint16 numLines = 0, oldX = 0, oldY = 0;
bool clickable_menu = g_system->hasFeature(OSystem::kFeatureTouchscreen);
if (clickable_menu) {
// The whole menu is shown and the items are click-selectable
mouse.pushCursorNum(CURSOR_ARROW);
numLines = numEntries;
} else {
oldX = mouse.x();
oldY = mouse.y();
mouse.cursorOff();
mouse.setPosition(FULL_SCREEN_WIDTH / 2, yMiddle);
// Round up number of lines in dialog to next odd number
numLines = (numEntries / 2) * 2 + 1;
if (numLines > 5) numLines = 5;
}
// Figure out the character width
uint16 numCols = 0;
for (int ctr = 0; ctr < numEntries; ++ctr) {
int len = strlen(actions[ctr]);
if (len > numCols)
numCols = len;
}
// Create the dialog surface
Common::Point size;
Surface::getDialogBounds(size, numCols, numLines, false);
Surface *s = new Surface(size.x, size.y);
s->createDialog();
int selectedIndex = 0;
bool refreshFlag = true;
Common::Rect r;
if (clickable_menu) {
r.left = Surface::textX();
r.right = s->width() - Surface::textX() + 1;
r.top = Surface::textY();
r.bottom = s->height() - Surface::textY() + 1;
}
bool bailOut = false;
while (!bailOut) {
if (refreshFlag) {
// Set up the contents of the menu
s->refreshDialog();
for (int index = 0; index < numLines; ++index) {
int actionIndex = clickable_menu ? index : selectedIndex - (numLines / 2) + index;
if ((actionIndex >= 0) && (actionIndex < numEntries)) {
byte color = textColor;
if (index == (clickable_menu ? selectedIndex : numLines / 2))
color = whiteColor;
s->writeString(Surface::textX(), Surface::textY() + index * FONT_HEIGHT,
actions[actionIndex], true,
color, false);
}
}
s->copyToScreen(0, yMiddle-(s->height() / 2));
system.updateScreen();
refreshFlag = false;
}
while (e.pollEvent()) {
if (engine.shouldQuit()) {
selectedIndex = 0xffff;
bailOut = true;
break;
} else if (e.type() == Common::EVENT_WHEELUP) {
// Scroll upwards
if (selectedIndex > 0) {
--selectedIndex;
refreshFlag = true;
}
} else if (e.type() == Common::EVENT_WHEELDOWN) {
// Scroll downwards
if (selectedIndex < numEntries - 1) {
++selectedIndex;
refreshFlag = true;
}
} else if (e.type() == Common::EVENT_KEYDOWN) {
uint16 keycode = e.event().kbd.keycode;
if (((keycode == Common::KEYCODE_KP8) || (keycode == Common::KEYCODE_UP)) && (selectedIndex > 0)) {
--selectedIndex;
refreshFlag = true;
} else if (((keycode == Common::KEYCODE_KP2) || (keycode == Common::KEYCODE_DOWN)) &&
(selectedIndex < numEntries-1)) {
++selectedIndex;
refreshFlag = true;
} else if ((keycode == Common::KEYCODE_RETURN) || (keycode == Common::KEYCODE_KP_ENTER)) {
bailOut = true;
break;
} else if (keycode == Common::KEYCODE_ESCAPE) {
selectedIndex = 0xffff;
bailOut = true;
break;
}
} else if (clickable_menu && (e.type() == Common::EVENT_LBUTTONDOWN || e.type() == Common::EVENT_MOUSEMOVE)) {
int16 x = mouse.x();
int16 y = mouse.y() - yMiddle + (s->height() / 2);
refreshFlag = true;
if (r.contains(x, y)) {
selectedIndex = (y - r.top) / FONT_HEIGHT;
if (e.type() == Common::EVENT_LBUTTONDOWN) {
bailOut = true;
break;
}
}
} else if (!clickable_menu && ((e.type() == Common::EVENT_LBUTTONDOWN) ||
(e.type() == Common::EVENT_MBUTTONDOWN))) {
//mouse.waitForRelease();
bailOut = true;
break;
} else if (e.type() == Common::EVENT_RBUTTONDOWN) {
mouse.waitForRelease();
selectedIndex = 0xffff;
bailOut = true;
break;
}
}
if (!bailOut) {
if (!clickable_menu) {
// Warping the mouse to "neutral" even if the top/bottom menu
// entry has been reached has both pros and cons. It makes the
// menu behave a bit more sensibly, but it also makes it harder
// to move the mouse pointer out of the ScummVM window.
if (mouse.y() < yMiddle - POPMENU_CHANGE_SENSITIVITY) {
if (selectedIndex > 0) {
--selectedIndex;
refreshFlag = true;
}
mouse.setPosition(FULL_SCREEN_WIDTH / 2, yMiddle);
} else if (mouse.y() > yMiddle + POPMENU_CHANGE_SENSITIVITY) {
if (selectedIndex < numEntries - 1) {
++selectedIndex;
refreshFlag = true;
}
mouse.setPosition(FULL_SCREEN_WIDTH / 2, yMiddle);
}
}
system.delayMillis(20);
}
}
// bailOut
delete s;
if (clickable_menu) {
mouse.popCursor();
} else {
mouse.setPosition(oldX, oldY);
mouse.cursorOn();
}
screen.update();
return selectedIndex;
}
} // End of namespace Lure