Files
scummvm/graphics/macgui/macmenu.cpp
T
Torbjörn Andersson d4c9c7208f GRAPHICS: MACGUI: Avoid recursion in Mac menu event processing
When moving the mouse through the menu bar, over a part that's not
occupied by menu items, the Mac menu class will start calling
processEvent() recursively. This is bad for two reasons: During the
recursion (which can easily reach a depth of dozens or even hundreds of
calls) there is no delay, so it will use 100% CPU. And once the
recursion unwinds, all the delays will come at once.

This moves the call to eventLoop() to after the first event has been
fully processed. Hopefully that will have approximately the same desired
effect, without any of the bad side effects.
2023-12-16 21:14:59 +01:00

1582 lines
42 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 "common/system.h"
#include "common/stack.h"
#include "common/keyboard.h"
#include "common/macresman.h"
#include "common/formats/winexe_pe.h"
#include "common/unicode-bidi.h"
#include "graphics/primitives.h"
#include "graphics/font.h"
#include "graphics/fonts/ttf.h"
#include "graphics/macgui/macfontmanager.h"
#include "graphics/macgui/macwindowmanager.h"
#include "graphics/macgui/macwindow.h"
#include "graphics/macgui/macmenu.h"
namespace Graphics {
enum {
kMenuHeight = 20,
kMenuLeftMargin = 7,
kMenuSpacing = 13,
kMenuPadding = 16,
kMenuDropdownPadding = 14,
kMenuDropdownItemHeight = 16,
kMenuItemHeight = 20,
kMenuWin95LeftDropdownPadding = 34,
kMenuWin95RightDropdownPadding = 57,
kMenuWin95DropdownItemHeight = 20
};
enum {
kMenuHighLevel = -1
};
enum {
kGrayed = 1,
kInactive = 2,
kPopUp = 16,
kMenuBarBreak = 32,
kMenuBreak = 64,
kEndMenu = 128
};
struct MacMenuSubMenu;
MacMenuSubMenu::~MacMenuSubMenu() {
for (uint i = 0; i < items.size(); i++)
delete items[i];
}
void MacMenuSubMenu::enableAllItems() {
for (uint i = 0; i < items.size(); ++i) {
items[i]->enabled = true;
if (items[i]->submenu) {
items[i]->submenu->enableAllItems();
}
}
}
MacMenu::MacMenu(int id, const Common::Rect &bounds, MacWindowManager *wm)
: BaseMacWindow(id, false, wm) {
_loadedFont = NULL;
_font = getMenuFont();
_align = kTextAlignLeft;
_type = MacWindowConstants::kWindowMenu;
_screen.create(bounds.width(), bounds.height(), PixelFormat::createFormatCLUT8());
_bbox.left = 0;
_bbox.top = 0;
_bbox.right = _screen.w;
_bbox.bottom = kMenuHeight;
_dimensionsDirty = true;
if (_wm->_mode & kWMModeWin95 && !(_wm->_mode & kWMModeForceMacFontsInWin95)) {
_menuDropdownItemHeight = kMenuWin95DropdownItemHeight;
_menuLeftDropdownPadding = kMenuWin95LeftDropdownPadding;
_menuRightDropdownPadding = kMenuWin95RightDropdownPadding;
} else {
_menuDropdownItemHeight = kMenuDropdownItemHeight;
_menuLeftDropdownPadding = kMenuDropdownPadding;
_menuRightDropdownPadding = kMenuDropdownPadding;
}
if (_wm->_mode & kWMModeAutohideMenu)
_isVisible = false;
else
_isVisible = true;
_activeItem = -1;
_activeSubItem = -1;
_lastActiveItem = -1;
_lastActiveSubItem = -1;
_ccallback = NULL;
_unicodeccallback = NULL;
_cdata = NULL;
_tempSurface.create(_screen.w, _font->getFontHeight(), PixelFormat::createFormatCLUT8());
}
MacMenu::~MacMenu() {
for (uint i = 0; i < _items.size(); i++)
delete _items[i];
delete _loadedFont;
}
Common::StringArray *MacMenu::readMenuFromResource(Common::SeekableReadStream *res) {
res->skip(10);
int enableFlags = res->readUint32BE();
Common::String menuName = res->readPascalString();
Common::String menuItem = res->readPascalString();
int menuItemNumber = 1;
Common::String menu;
byte itemData[4];
while (!menuItem.empty()) {
if (!menu.empty()) {
menu += ';';
}
if ((enableFlags & (1 << menuItemNumber)) == 0) {
menu += '(';
}
menu += menuItem;
res->read(itemData, 4);
static const char styles[] = {'B', 'I', 'U', 'O', 'S', 'C', 'E', 0};
for (int i = 0; styles[i] != 0; i++) {
if ((itemData[3] & (1 << i)) != 0) {
menu += '<';
menu += styles[i];
}
}
if (itemData[1] != 0) {
menu += '/';
menu += (char)itemData[1];
}
menuItem = res->readPascalString();
menuItemNumber++;
}
Common::StringArray *result = new Common::StringArray;
result->push_back(menuName);
result->push_back(menu);
debug(4, "menuName: %s", menuName.c_str());
debug(4, "menu: %s", menu.c_str());
return result;
}
static Common::U32String readUnicodeString(Common::SeekableReadStream *stream) {
Common::Array<uint32> strData;
uint16 wchar;
while ((wchar = stream->readUint16LE()) != '\0') {
strData.push_back(wchar);
}
return strData.empty() ? Common::U32String() : Common::U32String(strData.data(), strData.size());
}
void MacMenu::setAlignment(Graphics::TextAlign align) {
_align = align;
}
MacMenu *MacMenu::createMenuFromPEexe(Common::PEResources *exe, MacWindowManager *wm) {
Common::SeekableReadStream *menuData = exe->getResource(Common::kWinMenu, 128);
if (!menuData)
return nullptr;
menuData->readUint16LE(); // wVersion
menuData->readUint16LE(); // cbHeaderSize
MacMenu *menu = wm->addMenu();
Common::Stack<MacMenuSubMenu *> menus;
Common::Stack<bool> popups;
int depth = 0;
bool lastPopUp = false;
while (depth >= 0) {
uint16 flags = menuData->readUint16LE();
if (flags & kPopUp) {
if (depth == 0) {
menu->addMenuItem(nullptr, readUnicodeString(menuData));
} else {
Common::U32String name = readUnicodeString(menuData);
menu->addMenuItem(menus.top(), name, -1, 0, 0, !(flags & kGrayed));
}
MacMenuSubMenu *submenu = menu->addSubMenu(menus.size() ? menus.top() : nullptr);
menus.push(submenu);
popups.push(lastPopUp);
lastPopUp = (flags & kEndMenu) != 0;
depth++;
} else {
int action = menuData->readUint16LE();
Common::U32String name = readUnicodeString(menuData);
if (depth > 0) {
menu->addMenuItem(menus.top(), name, action, 0, 0, !(flags & kGrayed));
}
if (flags & kEndMenu) {
menus.pop();
depth--;
if (lastPopUp) {
depth--;
if (menus.size())
menus.pop();
}
lastPopUp = popups.pop();
}
}
}
menu->processTabs();
delete menuData;
if (gDebugLevel > 5)
menu->printMenu();
if (wm->_mode & Graphics::kWMModeWin95)
menu->enableAllMenus();
return menu;
}
void MacMenu::printMenu(int level, MacMenuSubMenu *submenu) {
if (!level) {
for (uint i = 0; i < _items.size(); i++) {
debug("0. %s --> %d", _items[i]->unicode ? convertFromU32String(_items[i]->unicodeText).c_str() : _items[i]->text.c_str(), _items[i]->action);
if (_items[i]->submenu != nullptr)
printMenu(level + 1, _items[i]->submenu);
}
} else {
for (uint i = 0; i < submenu->items.size(); i++) {
debugN("%d. ", level);
for (int j = 0; j < level; j++)
debugN(" ");
debug("%s --> %d", submenu->items[i]->unicode ? convertFromU32String(submenu->items[i]->unicodeText).c_str() : submenu->items[i]->text.c_str(), submenu->items[i]->action);
if (submenu->items[i]->submenu != nullptr)
printMenu(level + 1, submenu->items[i]->submenu);
}
}
}
int MacMenu::numberOfMenus() {
return _items.size();
}
MacMenuItem *MacMenu::getMenuItem(const Common::String &menuId) {
MacMenuItem *menu = nullptr;
for (uint i = 0; i < _items.size(); i++) {
// TODO: support unicode text menu
// didn't support unicode item finding yet
if (!_items[i]->unicode) {
if (_items[i]->text.equalsIgnoreCase(menuId)) {
menu = _items[i];
break;
}
}
}
return menu;
}
MacMenuItem *MacMenu::getMenuItem(int menuId) {
MacMenuItem *menu = nullptr;
if ((uint)menuId < _items.size())
menu = _items[menuId];
return menu;
}
MacMenuItem *MacMenu::getSubMenuItem(MacMenuItem *menu, const Common::String &itemId) {
if (!menu) {
warning("MacMenu::getSubMenuItem: can not find given menu");
return nullptr;
}
if (!menu->submenu) {
warning("MacMenu::getSubMenuItem: menu %s doesn't have submenu", menu->text.c_str());
return nullptr;
}
for (uint i = 0; i < menu->submenu->items.size(); i++) {
if (!menu->submenu->items[i]->unicode) {
if (menu->submenu->items[i]->text.equalsIgnoreCase(itemId))
return menu->submenu->items[i];
}
}
warning("MacMenu::getSubMenuItem: menu %s doesn't have item with id %s", menu->text.c_str(), itemId.c_str());
return nullptr;
}
MacMenuItem *MacMenu::getSubMenuItem(MacMenuItem *menu, int itemId) {
if (!menu) {
warning("MacMenu::getSubMenuItem: menuId out of bounds");
return nullptr;
}
if (!menu->submenu) {
warning("MacMenu::getSubMenuItem: menu %s doesn't have submenu", menu->text.c_str());
return nullptr;
}
if ((uint)itemId < menu->submenu->items.size())
return menu->submenu->items[itemId];
warning("MacMenu::getSubMenuItem: itemId %d out of bounds in menu %s", itemId, menu->text.c_str());
return nullptr;
}
int MacMenu::numberOfMenuItems(MacMenuItem *menu) {
if (!menu) {
warning("MacMenu::numberOfMenuItems: can not find menu");
return 0;
}
if (!menu->submenu) {
warning("MacMenu::numberOfMenuItems: menu with id %s has no submenu", menu->text.c_str());
return 0;
}
return menu->submenu->items.size();
}
void MacMenu::addStaticMenus(const MacMenuData *data) {
MacMenuItem *about = new MacMenuItem(_wm->_fontMan->hasBuiltInFonts() ? "\xa9" : "\xf0"); // (c) Symbol as the most resembling apple
_items.push_back(about);
for (int i = 0; data[i].menunum; i++) {
const MacMenuData *m = &data[i];
MacMenuSubMenu *submenu = getSubmenu(nullptr, m->menunum);
if (m->menunum == kMenuHighLevel) {
addMenuItem(nullptr, m->title);
continue;
}
if (submenu == nullptr)
submenu = addSubMenu(nullptr, m->menunum);
addMenuItem(submenu, m->title, m->action, 0, m->shortcut, m->enabled);
}
calcDimensions();
}
MacMenuSubMenu *MacMenu::addSubMenu(MacMenuSubMenu *submenu, int index) {
_dimensionsDirty = true;
if (submenu == nullptr) {
if (index == -1)
index = _items.size() - 1;
if (_items[index]->submenu != nullptr)
warning("Overwritting submenu #%d", index);
return (_items[index]->submenu = new MacMenuSubMenu());
} else {
if (index == -1)
index = submenu->items.size() - 1;
if (submenu->items[index]->submenu != nullptr)
warning("Overwritting submenu #%d", index);
return (submenu->items[index]->submenu = new MacMenuSubMenu());
}
}
MacMenuSubMenu *MacMenu::getSubmenu(MacMenuSubMenu *submenu, int index) {
if (submenu == nullptr) {
if (index < 0 || (uint)index >= _items.size())
return nullptr;
return _items[index]->submenu;
} else {
if (index < 0 || (uint)index >= submenu->items.size())
return nullptr;
return submenu->items[index]->submenu;
}
}
int MacMenu::addMenuItem(MacMenuSubMenu *submenu, const Common::String &text, int action, int style, char shortcut, bool enabled, bool checked) {
_dimensionsDirty = true;
if (submenu == nullptr) {
MacMenuItem *i = new MacMenuItem(text);
_items.push_back(i);
return _items.size() - 1;
}
_dimensionsDirty = true;
submenu->items.push_back(new MacMenuItem(text, action, style, shortcut, -1, enabled, checked));
return submenu->items.size() - 1;
}
int MacMenu::addMenuItem(MacMenuSubMenu *submenu, const Common::U32String &text, int action, int style, char shortcut, bool enabled, bool checked) {
_dimensionsDirty = true;
Common::U32String amp("&");
Common::U32String res;
int shortcutPos = -1;
for (uint i = 0; i < text.size(); i++)
if (text[i] == amp[0]) {
if (i < text.size() - 1) {
if ((text[i + 1] & 0xff) != '&') {
shortcut = text[i + 1] & 0xff;
shortcutPos = i;
} else {
res += text[i];
}
}
} else {
res += text[i];
}
if (submenu == nullptr) {
_items.push_back(new MacMenuItem(res, -1, 0, shortcut, shortcutPos));
return _items.size() - 1;
}
submenu->items.push_back(new MacMenuItem(res, action, style, shortcut, shortcutPos, enabled, checked));
return submenu->items.size() - 1;
}
void MacMenu::insertMenuItem(MacMenuSubMenu *submenu, const Common::String &text, uint pos, int action, int style, char shortcut, bool enabled, bool checked) {
_dimensionsDirty = true;
if (submenu == nullptr) {
if (pos >= _items.size()) {
_dimensionsDirty = false;
} else {
MacMenuItem *i = new MacMenuItem(text);
_items.insert(_items.begin() + pos, i);
}
return;
}
if (pos >= submenu->items.size()) {
_dimensionsDirty = false;
return;
}
_dimensionsDirty = true;
submenu->items.insert(submenu->items.begin() + pos, new MacMenuItem(text, action, style, shortcut, -1, enabled, checked));
}
void MacMenu::insertMenuItem(MacMenuSubMenu *submenu, const Common::U32String &text, uint pos, int action, int style, char shortcut, bool enabled, bool checked) {
_dimensionsDirty = true;
Common::U32String amp("&");
Common::U32String res;
int shortcutPos = -1;
for (uint i = 0; i < text.size(); i++)
if (text[i] == amp[0]) {
if (i < text.size() - 1) {
if ((text[i + 1] & 0xff) != '&') {
shortcut = text[i + 1] & 0xff;
shortcutPos = i;
} else {
res += text[i];
}
}
} else {
res += text[i];
}
if (submenu == nullptr) {
if (pos >= _items.size()) {
_dimensionsDirty = false;
} else {
_items.insert(_items.begin() + pos, new MacMenuItem(res, -1, 0, shortcut, shortcutPos));
}
return;
}
if (pos >= submenu->items.size()) {
_dimensionsDirty = false;
return;
}
submenu->items.insert(submenu->items.begin() + pos, new MacMenuItem(res, action, style, shortcut, shortcutPos, enabled, checked));
}
void MacMenu::removeMenuItem(MacMenuSubMenu *submenu, uint pos) {
_dimensionsDirty = true;
if (submenu == nullptr) {
if (pos >= _items.size()) {
_dimensionsDirty = false;
} else {
delete _items.remove_at(pos);
}
return;
}
if (pos >= submenu->items.size()) {
_dimensionsDirty = false;
return;
}
delete submenu->items.remove_at(pos);
}
void MacMenu::calcDimensions() {
// Calculate menu dimensions
int y = 1;
int x = 18;
for (uint i = 0; i < _items.size(); i++) {
int w = _items[i]->unicode ? _font->getStringWidth(_items[i]->unicodeText) : _font->getStringWidth(_items[i]->text);
if (_items[i]->bbox.bottom == 0) {
_items[i]->bbox.left = x - kMenuLeftMargin;
_items[i]->bbox.top = y;
_items[i]->bbox.right = x + w + kMenuSpacing - kMenuLeftMargin;
_items[i]->bbox.bottom = y + _font->getFontHeight() + (_wm->_fontMan->hasBuiltInFonts() ? 3 : 2);
}
calcSubMenuBounds(_items[i]->submenu, _items[i]->bbox.left - 1, _items[i]->bbox.bottom + 1);
if (_align == kTextAlignRight) {
int right = _items[i]->bbox.right;
int width = right - _items[i]->bbox.left;
_items[i]->bbox.left = _bbox.right + _bbox.left - right;
_items[i]->bbox.right = _items[i]->bbox.left + width;
}
x += w + kMenuSpacing;
}
_dimensionsDirty = false;
}
void MacMenu::loadMenuResource(Common::MacResManager *resFork, uint16 id) {
Common::SeekableReadStream *res = resFork->getResource(MKTAG('M', 'E', 'N', 'U'), id);
assert(res);
uint16 menuID = res->readUint16BE();
/* uint16 width = */ res->readUint16BE();
/* uint16 height = */ res->readUint16BE();
/* uint16 resourceID = */ res->readUint16BE();
/* uint16 placeholder = */ res->readUint16BE();
uint32 initialState = res->readUint32BE();
Common::String menuTitle = res->readPascalString();
if (!menuTitle.empty()) {
addMenuItem(nullptr, menuTitle);
MacMenuSubMenu *submenu = addSubMenu(nullptr);
initialState >>= 1;
// Read submenu items
int action = menuID << 16;
while (true) {
Common::String subMenuTitle = res->readPascalString();
if (subMenuTitle.empty())
break;
/* uint8 icon = */ res->readByte();
uint8 key = res->readByte();
/* uint8 mark = */ res->readByte();
uint8 style = res->readByte();
addMenuItem(submenu, subMenuTitle, action++, style, key, initialState & 1);
initialState >>= 1;
}
}
delete res;
}
void MacMenu::loadMenuBarResource(Common::MacResManager *resFork, uint16 id) {
Common::SeekableReadStream *res = resFork->getResource(MKTAG('M', 'B', 'A', 'R'), id);
assert(res);
uint16 count = res->readUint16BE();
for (int i = 0; i < count; i++) {
loadMenuResource(resFork, res->readUint16BE());
}
}
void MacMenu::setCheckMark(MacMenuItem *menuItem, bool checkMark) {
if (menuItem) {
menuItem->checked = checkMark;
_contentIsDirty = true;
}
}
void MacMenu::setEnabled(MacMenuItem *menuItem, bool enabled) {
if (menuItem) {
menuItem->enabled = enabled;
_contentIsDirty = true;
}
}
void MacMenu::setName(MacMenuItem *menuItem, const Common::String &name) {
if (menuItem) {
menuItem->text = name;
_contentIsDirty = true;
}
}
void MacMenu::setAction(MacMenuItem *menuItem, int actionId) {
if (menuItem) {
menuItem->action = actionId;
}
}
bool MacMenu::getCheckMark(MacMenuItem *menuItem) {
return menuItem ? menuItem->checked : false;
}
bool MacMenu::getEnabled(MacMenuItem *menuItem) {
return menuItem ? menuItem->enabled : false;
}
Common::String MacMenu::getName(MacMenuItem *menuItem) {
return menuItem ? menuItem->text : Common::String();
}
int MacMenu::getAction(MacMenuItem *menuItem) {
return menuItem ? menuItem->action : 0;
}
void MacMenu::clearSubMenu(int id) {
MacMenuItem *menu = _items[id];
if (menu->submenu == nullptr)
return;
for (uint j = 0; j < menu->submenu->items.size(); j++)
delete menu->submenu->items[j];
menu->submenu->items.clear();
}
void MacMenu::createSubMenuFromString(int id, const char *str, int commandId) {
clearSubMenu(id);
Common::String string(str);
Common::String item;
MacMenuSubMenu *submenu = getSubmenu(nullptr, id);
if (submenu == nullptr)
submenu = addSubMenu(nullptr, id);
for (uint i = 0; i < string.size(); i++) {
while (i < string.size() && (string[i] != ';' && string[i] != '\r')) // Read token, consume \r for popup menu (MacPopUp)
item += string[i++];
if (item.lastChar() == ']') { // we have command id
item.deleteLastChar();
const char *p = strrchr(item.c_str(), '[');
p++;
if (p == NULL) {
error("MacMenu::createSubMenuFromString(): Malformed menu item: '%s', bad format for actionId", item.c_str());
}
commandId = atoi(p);
item = Common::String(item.c_str(), p - 1);
}
if (item == "(-") {
addMenuItem(submenu, NULL, 0);
} else {
bool enabled = true;
bool checked = false;
int style = 0;
char shortcut = 0;
const char *shortPtr = strrchr(item.c_str(), '/');
if (shortPtr != NULL) {
if (strlen(shortPtr) >= 2) {
shortcut = shortPtr[1];
item.deleteChar(shortPtr - item.c_str());
item.deleteChar(shortPtr - item.c_str());
} else {
// if we only have one / without shortcut key, we skip it.
item.deleteChar(shortPtr - item.c_str());
// error("MacMenu::createSubMenuFromString(): Unexpected shortcut: '%s', item '%s' in menu '%s'", shortPtr, item.c_str(), string.c_str());
}
}
while (item.size() >= 2 && item[item.size() - 2] == '<') {
char c = item.lastChar();
if (c == 'B') {
style |= kMacFontBold;
} else if (c == 'I') {
style |= kMacFontItalic;
} else if (c == 'U') {
style |= kMacFontUnderline;
} else if (c == 'O') {
style |= kMacFontOutline;
} else if (c == 'S') {
style |= kMacFontShadow;
} else if (c == 'C') {
style |= kMacFontCondense;
} else if (c == 'E') {
style |= kMacFontExtend;
}
item.deleteLastChar();
item.deleteLastChar();
}
Common::String tmpitem(item);
tmpitem.trim();
// is that any places locate a parenthese will disable the item, or only the first char and last char counts.
if (tmpitem.size() > 0 && (tmpitem[0] == '(' || tmpitem.lastChar() == '(')) {
enabled = false;
for (uint j = 0; j < item.size(); j++)
if (item[j] == '(') {
item.deleteChar(j);
break;
}
} else if (tmpitem.size() >= 2 && tmpitem[0] == '!' && (uint8)tmpitem[1] == 195) {
// this is the !√ situation, we need to set item checked, 195 represent √ in director
checked = true;
item = item.substr(2, Common::String::npos);
}
addMenuItem(submenu, item, commandId, style, shortcut, enabled, checked);
}
item.clear();
}
}
const Font *MacMenu::getMenuFont(int slant) {
#ifdef USE_FREETYPE2
if (_wm->_mode & kWMModeWin95 && !(_wm->_mode & kWMModeForceMacFontsInWin95)) {
if (!_loadedFont) {
_loadedFont = Graphics::loadTTFFontFromArchive("ms_sans_serif.ttf", 16);
if (_loadedFont)
return _loadedFont;
} else {
return _loadedFont;
}
// If font was not loaded, fallback
}
#endif
return _wm->_fontMan->getFont(Graphics::MacFont(kMacFontChicago, 12, slant));
}
const Common::String MacMenu::getAcceleratorString(MacMenuItem *item, const char *prefix) {
if (item->shortcut == 0)
return Common::String();
return Common::String::format("%s%c%c", prefix, (_wm->_fontMan->hasBuiltInFonts() ? '^' : '\x11'), item->shortcut);
}
void MacMenu::processTabs() {
for (uint i = 0; i < _items.size(); i++)
processSubmenuTabs(_items[i]->submenu);
}
void MacMenu::processSubmenuTabs(MacMenuSubMenu *submenu) {
if (submenu == nullptr)
return;
for (uint i = 0; i < submenu->items.size(); i++) {
MacMenuSubMenu *menu = submenu->items[i]->submenu;
if (menu != nullptr)
processSubmenuTabs(menu);
}
int maxWidth = 0;
bool haveTabs = false;
Common::U32String tabSymbol("\t");
// First, we replace \t with two spaces, and thus, obtain
// the widest string
for (uint i = 0; i < submenu->items.size(); i++) {
MacMenuItem *item = submenu->items[i];
if (item->unicodeText.empty())
continue;
Common::U32String res(item->unicodeText);
uint32 pos = item->unicodeText.find(tabSymbol);
if (pos != Common::U32String::npos) {
// Sanity check
if (pos == 0 || pos >= item->unicodeText.size())
error("Malformed menu: tab position");
if (item->unicodeText.find(tabSymbol, pos + 1) != Common::U32String::npos)
error("Malformed menu: extra tab");
haveTabs = true;
Common::U32String start = item->unicodeText.substr(0, pos);
Common::U32String end = item->unicodeText.substr(pos + 1);
res = start;
res += Common::U32String(" ");
res += end;
}
int width = _font->getStringWidth(res);
if (width > maxWidth) {
maxWidth = width;
}
}
if (!haveTabs)
return;
// Now expand each tab to the relevant width
// And yes, right edge is going to be uneven
for (uint i = 0; i < submenu->items.size(); i++) {
MacMenuItem *item = submenu->items[i];
if (item->unicodeText.empty())
continue;
uint32 pos = item->unicodeText.find(tabSymbol);
if (pos == Common::U32String::npos)
continue;
Common::U32String start = item->unicodeText.substr(0, pos);
Common::U32String end = item->unicodeText.substr(pos + 1);
Common::U32String res;
Common::U32String spaces(" ");
int width;
do {
res = start;
res += spaces;
res += end;
width = _font->getStringWidth(res);
spaces += spaces[0];
} while (width < maxWidth);
item->unicodeText = res;
}
}
int MacMenu::calcSubMenuWidth(MacMenuSubMenu *submenu) {
int maxWidth = 0;
if (submenu == nullptr)
return maxWidth;
for (uint i = 0; i < submenu->items.size(); i++) {
MacMenuItem *item = submenu->items[i];
if (!item->text.empty()) {
Common::String text(item->text);
Common::String acceleratorText(getAcceleratorString(item, " "));
const Font *font = getMenuFont(item->style);
int width = font->getStringWidth(text);
if (!acceleratorText.empty()) {
width += _font->getStringWidth(acceleratorText);
}
if (item->submenu != nullptr) { // If we're drawing triangle
width += _font->getStringWidth(" ");
}
if (width > maxWidth) {
maxWidth = width;
}
} else if (!item->unicodeText.empty()) {
Common::U32String text(item->unicodeText);
if (item->submenu != nullptr) // If we're drawing triangle
text += Common::U32String(" ");
int width = _font->getStringWidth(text);
if (width > maxWidth) {
maxWidth = width;
}
}
}
return maxWidth > 200 ? 200 : maxWidth;
}
void MacMenu::calcSubMenuBounds(MacMenuSubMenu *submenu, int x, int y) {
if (submenu == nullptr)
return;
// TODO: cache maxWidth
int maxWidth = calcSubMenuWidth(submenu);
int x1 = x;
int y1 = y;
int x2 = x1 + maxWidth + _menuLeftDropdownPadding + _menuRightDropdownPadding - 4;
int y2 = y1 + submenu->items.size() * _menuDropdownItemHeight + 2;
submenu->bbox.left = x1;
submenu->bbox.top = y1;
submenu->bbox.right = x2;
submenu->bbox.bottom = y2;
if (_align == kTextAlignRight) {
submenu->bbox.left = _bbox.right + _bbox.left - x2;
submenu->bbox.right = submenu->bbox.left + x2 - x1;
}
for (uint i = 0; i < submenu->items.size(); i++) {
MacMenuSubMenu *menu = submenu->items[i]->submenu;
if (menu != nullptr)
calcSubMenuBounds(menu, x2 - 4, y1 + i * _menuDropdownItemHeight + 1);
}
}
static void drawPixelPlain(int x, int y, int color, void *data) {
ManagedSurface *surface = (ManagedSurface *)data;
if (x >= 0 && x < surface->w && y >= 0 && y < surface->h)
*((byte *)surface->getBasePtr(x, y)) = (byte)color;
}
static void drawFilledRoundRect(ManagedSurface *surface, Common::Rect &rect, int arc, int color) {
drawRoundRect(rect, arc, color, true, drawPixelPlain, surface);
}
static void underlineAccelerator(ManagedSurface *dst, const Font *font, const Common::UnicodeBiDiText &txt, int x, int y, int shortcutPos, uint32 color) {
if (shortcutPos == -1)
return;
int visualPos = txt.getVisualPosition(shortcutPos);
Common::U32String s(txt.visual);
// Erase characters only if it is not end of the string
if ((uint)(visualPos + 1) < s.size())
s.erase(visualPos + 1);
int pos2 = font->getStringWidth(s);
s.deleteLastChar();
int pos1 = font->getStringWidth(s);
dst->hLine(x + pos1, y + font->getFontHeight(), x + pos2 - 1, color);
}
bool MacMenu::draw(ManagedSurface *g, bool forceRedraw) {
Common::Rect r(_bbox);
if (!_isVisible)
return false;
if (_dimensionsDirty)
calcDimensions();
if (!_contentIsDirty && !forceRedraw)
return false;
_contentIsDirty = false;
_screen.clear(_wm->_colorBlack);
bool shouldUseDesktopArc = !(_wm->_mode & kWMModeWin95) || (_wm->_mode & kWMModeForceMacBorder);
drawFilledRoundRect(&_screen, r, shouldUseDesktopArc ? kDesktopArc : 0, _wm->_colorWhite);
r.top = 7;
_screen.fillRect(r, _wm->_colorWhite);
r.top = kMenuHeight - 1;
r.bottom++;
_screen.fillRect(r, _wm->_colorGreen);
r.bottom--;
_screen.fillRect(r, _wm->_colorBlack);
for (uint i = 0; i < _items.size(); i++) {
int color = _wm->_colorBlack;
MacMenuItem *it = _items[i];
if ((uint)_activeItem == i) {
Common::Rect hbox = it->bbox;
hbox.left -= 1;
hbox.right += 3;
hbox.bottom += 1;
if (_align == kTextAlignRight) {
hbox.left -= 2;
hbox.right -= 2;
}
_screen.fillRect(hbox, _wm->_colorBlack);
color = _wm->_colorWhite;
}
int y = it->bbox.top + (_wm->_fontMan->hasBuiltInFonts() ? 2 : 1);
int x = _align == kTextAlignRight ? -kMenuLeftMargin : kMenuLeftMargin;
x += it->bbox.left;
if (it->unicode) {
int accOff = _align == kTextAlignRight ? it->bbox.width() - _font->getStringWidth(it->unicodeText) : 0;
Common::UnicodeBiDiText utxt(it->unicodeText);
_font->drawString(&_screen, utxt.visual, x, y, it->bbox.width(), color, _align, 0, true);
underlineAccelerator(&_screen, _font, utxt, x + accOff, y, it->shortcutPos, color);
} else {
const Font *font = getMenuFont(it->style);
font->drawString(&_screen, it->text, x, y, it->bbox.width(), color, Graphics::kTextAlignLeft, 0, true);
}
}
if ((_wm->_mode & kWMModalMenuMode) || !_wm->_screen)
g_system->copyRectToScreen(_screen.getBasePtr(_bbox.left, _bbox.top), _screen.pitch, _bbox.left, _bbox.top, _bbox.width(), _bbox.height());
for (uint i = 0; i < _menustack.size(); i++) {
renderSubmenu(_menustack[i], (i == _menustack.size() - 1));
}
if (g)
g->transBlitFrom(_screen, _wm->_colorGreen);
if (!(_wm->_mode & kWMModalMenuMode) && g)
g_system->copyRectToScreen(g->getPixels(), g->pitch, 0, 0, g->w, g->h);
return true;
}
void MacMenu::renderSubmenu(MacMenuSubMenu *menu, bool recursive) {
Common::Rect *r = &menu->bbox;
if (r->width() == 0 || r->height() == 0)
return;
_screen.fillRect(*r, _wm->_colorWhite);
_screen.frameRect(*r, _wm->_colorBlack);
_screen.vLine(r->right, r->top + 3, r->bottom + 1, _wm->_colorBlack);
_screen.vLine(r->right + 1, r->top + 3, r->bottom + 1, _wm->_colorBlack);
_screen.hLine(r->left + 3, r->bottom, r->right + 1, _wm->_colorBlack);
_screen.hLine(r->left + 3, r->bottom + 1, r->right + 1, _wm->_colorBlack);
int y = r->top + 1;
int x = _align == kTextAlignRight ? -_menuRightDropdownPadding: _menuLeftDropdownPadding;
x += r->left;
for (uint i = 0; i < menu->items.size(); i++) {
Common::String text(menu->items[i]->text);
Common::String acceleratorText(getAcceleratorString(menu->items[i], ""));
Common::U32String unicodeText(menu->items[i]->unicodeText);
int accOff = _align == kTextAlignRight ? r->width() - _font->getStringWidth(unicodeText) : 0;
int shortcutPos = menu->items[i]->shortcutPos;
int accelX = r->right - 25;
int arrowX = r->right - 14;
if (_align == kTextAlignRight) {
accelX = r->left + 25;
arrowX = r->left + 14;
}
int color = _wm->_colorBlack;
if (i == (uint)menu->highlight && (!text.empty() || !unicodeText.empty()) && menu->items[i]->enabled) {
color = _wm->_colorWhite;
Common::Rect trect(r->left, y - (_wm->_fontMan->hasBuiltInFonts() ? 1 : 0), r->right, y + _font->getFontHeight());
_screen.fillRect(trect, _wm->_colorBlack);
}
if (!text.empty() || !unicodeText.empty()) {
ManagedSurface *s = &_screen;
int tx = x, ty = y;
if (!menu->items[i]->enabled) {
s = &_tempSurface;
tx = 0;
ty = 0;
accelX -= x;
_tempSurface.clear(_wm->_colorGreen);
}
if (menu->items[i]->unicode) {
Common::UnicodeBiDiText utxt(unicodeText);
_font->drawString(s, utxt.visual, tx, ty, r->width(), color, _align, 0, true);
underlineAccelerator(s, _font, utxt, tx + accOff, ty, shortcutPos, color);
} else {
const Font *font = getMenuFont(menu->items[i]->style);
font->drawString(s, text, tx, ty, r->width(), color, Graphics::kTextAlignLeft, 0, true);
}
if (menu->items[i]->checked) {
const Font *font = getMenuFont(menu->items[i]->style);
int checkSymbol = _wm->_fontMan->hasBuiltInFonts() ? 0xD7 : 18;
int padding = _align == kTextAlignRight ? -_menuRightDropdownPadding: _menuLeftDropdownPadding;
int offset = padding - font->getCharWidth(checkSymbol);
// calculating the padding and offset, we draw the √ at the center
font->drawChar(s, checkSymbol, tx - padding + offset, ty, color);
}
if (!acceleratorText.empty() && shortcutPos == -1)
_font->drawString(s, acceleratorText, accelX, ty, r->width(), color);
if (menu->items[i]->submenu != nullptr)
drawSubMenuArrow(s, arrowX, ty, color);
if (!menu->items[i]->enabled) {
// I am lazy to extend drawString() with plotProc as a parameter, so
// fake it here
for (int ii = 0; ii < _tempSurface.h; ii++) {
const byte *src = (const byte *)_tempSurface.getBasePtr(0, ii);
byte *dst = (byte *)_screen.getBasePtr(x, y + ii);
byte pat = _wm->getBuiltinPatterns()[kPatternCheckers2 - 1][ii % 8];
for (int j = 0; j < r->width(); j++) {
if (*src != _wm->_colorGreen && (pat & (1 << (7 - (x + j) % 8))))
*dst = *src;
src++;
dst++;
}
}
}
} else { // Delimiter
bool flip = r->left & 2;
byte *ptr = (byte *)_screen.getBasePtr(r->left + 1, y + _menuDropdownItemHeight / 2);
for (int xx = r->left + 1; xx <= r->right - 1; xx++, ptr++) {
*ptr = flip ? _wm->_colorBlack : _wm->_colorWhite;
flip = !flip;
}
}
y += _menuDropdownItemHeight;
}
if (recursive && menu->highlight != -1 && menu->items[menu->highlight]->submenu != nullptr)
renderSubmenu(menu->items[menu->highlight]->submenu, false);
if (_wm->_mode & kWMModalMenuMode) {
// TODO: Instead of cropping, reposition the submenu
int w = r->width() + 2;
if (r->left + w >= _screen.w)
w = _screen.w - 1 - r->left;
int h = r->height() + 2;
if (r->top + h >= _screen.h)
h = _screen.h - 1 - r->top;
Graphics::ManagedSurface g = *_wm->_screenCopy;
g.transBlitFrom(_screen, _wm->_colorGreen);
g_system->copyRectToScreen(g.getBasePtr(r->left, r->top), g.pitch, r->left, r->top, w, h);
}
}
void MacMenu::drawSubMenuArrow(ManagedSurface *dst, int x, int y, int color) {
int height = 11;
for (int col = 0; col < 6; col++) {
if (_align != kTextAlignRight) {
dst->vLine(x + col, y + col + 1, y + col + height + 1, color);
} else {
dst->vLine(x - col, y + col + 1, y + col + height + 1, color);
}
height -= 2;
}
}
bool MacMenu::processEvent(Common::Event &event) {
if (!_isVisible && event.type != Common::EVENT_KEYDOWN)
return false;
switch (event.type) {
case Common::EVENT_QUIT:
closeMenu();
return false;
case Common::EVENT_KEYDOWN:
return keyEvent(event);
case Common::EVENT_LBUTTONDOWN:
return mouseClick(event.mouse.x, event.mouse.y);
case Common::EVENT_LBUTTONUP:
return mouseRelease(event.mouse.x, event.mouse.y);
case Common::EVENT_MOUSEMOVE:
return mouseMove(event.mouse.x, event.mouse.y);
default:
return false;
}
}
bool MacMenu::keyEvent(Common::Event &event) {
if (event.type != Common::EVENT_KEYDOWN)
return false;
if (event.kbd.flags & (Common::KBD_ALT | Common::KBD_CTRL | Common::KBD_META)) {
if (event.kbd.ascii >= 0x20 && event.kbd.ascii <= 0x7f) {
return processMenuShortCut(event.kbd.ascii);
}
}
return false;
}
bool MacMenu::checkIntersects(Common::Rect &rect) {
if (_bbox.intersects(rect))
return true;
for (uint i = 0; i < _menustack.size(); i++)
if (_menustack[i]->bbox.intersects(rect))
return true;
return false;
}
bool MacMenu::mouseClick(int x, int y) {
if (_bbox.contains(x, y)) {
for (uint i = 0; i < _items.size(); i++) {
if (_items[i]->bbox.contains(x, y)) {
if ((uint)_activeItem == i)
return true; // We catch the event but do not do anything
if (_activeItem != -1) { // Restore background
if (_items[_activeItem]->submenu != nullptr) {
if (_wm->_mode & kWMModalMenuMode)
g_system->copyRectToScreen(_wm->_screenCopy->getPixels(), _wm->_screenCopy->pitch, 0, 0, _wm->_screenCopy->w, _wm->_screenCopy->h);
_menustack.pop_back(); // Drop previous submenu
_contentIsDirty = true;
_wm->setFullRefresh(true);
}
}
_activeItem = i;
_activeSubItem = -1;
if (_items[_activeItem]->submenu != nullptr) {
_menustack.push_back(_items[_activeItem]->submenu);
_items[_activeItem]->submenu->highlight = -1;
_contentIsDirty = true;
}
}
}
if (!_active)
_wm->activateMenu();
setActive(true);
return true;
}
if (!_active)
return false;
if (_menustack.size() > 0 && _menustack.back()->bbox.contains(x, y)) {
MacMenuSubMenu *menu = _menustack.back();
int numSubItem = menu->ytoItem(y, _menuDropdownItemHeight);
if (numSubItem != _activeSubItem) {
if (_wm->_mode & kWMModalMenuMode) {
if (_activeSubItem == -1 || menu->items[_activeSubItem]->submenu != nullptr)
g_system->copyRectToScreen(_wm->_screenCopy->getPixels(), _wm->_screenCopy->pitch, 0, 0, _wm->_screenCopy->w, _wm->_screenCopy->h);
}
_activeSubItem = numSubItem;
menu->highlight = _activeSubItem;
_contentIsDirty = true;
_wm->setFullRefresh(true);
}
return true;
}
if (_activeSubItem != -1 && _menustack.back()->items[_activeSubItem]->submenu != nullptr) {
if (_menustack.back()->items[_activeSubItem]->submenu->bbox.contains(x, y)) {
_menustack.push_back(_menustack.back()->items[_activeSubItem]->submenu);
_activeSubItem = 0;
_contentIsDirty = true;
_wm->setFullRefresh(true);
_menustack.back()->highlight = 0;
return true;
}
}
if (_menustack.size() > 1) {
if (_menustack[_menustack.size() - 2]->bbox.contains(x, y)) {
_menustack.back()->highlight = -1; // Erase it for the closed popup
if (_wm->_mode & kWMModalMenuMode) {
int x1 = _menustack.back()->bbox.left;
int y1 = _menustack.back()->bbox.top;
uint w = _menustack.back()->bbox.width() + 2;
uint h = _menustack.back()->bbox.height() + 2;
if (x1 + (int)w > _wm->_screenCopy->w)
w = _wm->_screenCopy->w - 1 - x1;
if (y1 + (int)h > _wm->_screenCopy->h)
h = _wm->_screenCopy->h - 1 - y1;
g_system->copyRectToScreen(_wm->_screenCopy->getBasePtr(x1, y1), _wm->_screenCopy->pitch, x1, y1, w, h);
}
_menustack.pop_back();
MacMenuSubMenu *menu = _menustack.back();
_activeSubItem = menu->highlight = menu->ytoItem(y, _menuDropdownItemHeight);
_contentIsDirty = true;
_wm->setFullRefresh(true);
return true;
}
}
if (_activeItem != -1) {
_activeSubItem = -1;
return true;
}
return false;
}
bool MacMenu::contains(int x, int y) {
if (_bbox.contains(x, y))
return true;
for (uint i = 0; i < _menustack.size(); i++) {
if (_menustack[i]->bbox.contains(x, y))
return true;
}
if (_activeSubItem != -1 && _menustack.back()->items[_activeSubItem]->submenu != nullptr)
if (_menustack.back()->items[_activeSubItem]->submenu->bbox.contains(x, y))
return true;
return false;
}
bool MacMenu::mouseMove(int x, int y) {
if (_active) {
if (mouseClick(x, y))
return true;
} else if ((_wm->_mode & kWMModeAutohideMenu) && !_bbox.contains(x, y)) {
_isVisible = false;
if (_wm->_mode & kWMModalMenuMode) {
_wm->disableScreenCopy();
}
}
return false;
}
bool MacMenu::checkCallback(bool unicode) {
if (unicode) {
if (_unicodeccallback == nullptr) {
warning("MacMenu: no Unicode callback set");
return false;
}
} else {
if (_ccallback == nullptr) {
warning("MacMenu: no callback set");
return false;
}
}
return true;
}
void MacMenu::closeMenu() {
setActive(false);
if (_wm->_mode & kWMModeAutohideMenu)
_isVisible = false;
if (_wm->_mode & kWMModalMenuMode) {
_wm->disableScreenCopy();
}
for (uint i = 0; i < _menustack.size(); i++) {
_menustack[i]->highlight = -1;
}
_activeItem = -1;
_activeSubItem = -1;
_menustack.clear();
_wm->setFullRefresh(true);
}
bool MacMenu::mouseRelease(int x, int y) {
if (!_active)
return false;
bool haveCallBack = false;
if (_activeItem != -1 && _activeSubItem != -1 && _menustack.back()->items[_activeSubItem]->enabled) {
// no action if item has submenu
if (_menustack.back()->items[_activeSubItem]->submenu) {
return false;
}
if (_menustack.back()->items[_activeSubItem]->unicode) {
if (checkCallback(true)) {
(*_unicodeccallback)(_menustack.back()->items[_activeSubItem]->action,
_menustack.back()->items[_activeSubItem]->unicodeText, _cdata);
haveCallBack = true;
}
} else {
if (checkCallback()) {
(*_ccallback)(_menustack.back()->items[_activeSubItem]->action,
_menustack.back()->items[_activeSubItem]->text, _cdata);
haveCallBack = true;
}
}
}
// Set last active items and subitems before leaving!
_lastActiveItem = _activeItem;
_lastActiveSubItem = _activeSubItem;
// if the mode is not win95, or the click position is outside of the menu, then we close it
if (!(_wm->_mode & kWMModeWin95) || !contains(x, y) || haveCallBack)
closeMenu();
return true;
}
bool MacMenu::processMenuShortCut(uint16 ascii) {
ascii = tolower(ascii);
for (uint i = 0; i < _items.size(); i++) {
if (_items[i]->submenu != nullptr) {
for (uint j = 0; j < _items[i]->submenu->items.size(); j++) {
if (_items[i]->submenu->items[j]->enabled && tolower(_items[i]->submenu->items[j]->shortcut) == ascii) {
if (_items[i]->submenu->items[j]->unicode) {
if (checkCallback(true))
(*_unicodeccallback)(_items[i]->submenu->items[j]->action, _items[i]->submenu->items[j]->unicodeText, _cdata);
} else {
if (checkCallback())
(*_ccallback)(_items[i]->submenu->items[j]->action, _items[i]->submenu->items[j]->text, _cdata);
}
return true;
}
}
}
}
return false;
}
void MacMenu::enableCommand(int menunum, int action, bool state) {
if (!_items[menunum]->submenu)
return;
for (uint i = 0; i < _items[menunum]->submenu->items.size(); i++)
if (_items[menunum]->submenu->items[i]->action == action)
_items[menunum]->submenu->items[i]->enabled = state;
_contentIsDirty = true;
}
void MacMenu::enableCommand(const char *menuitem, const char *menuaction, bool state) {
uint menunum = 0;
while (menunum < _items.size()) {
assert(!_items[menunum]->unicode);
if (_items[menunum]->text.equalsIgnoreCase(menuitem))
break;
else
menunum++;
}
if (menunum == _items.size())
return;
if (_items[menunum]->submenu != nullptr)
for (uint i = 0; i < _items[menunum]->submenu->items.size(); i++) {
assert(!_items[menunum]->submenu->items[i]->unicode);
if (_items[menunum]->submenu->items[i]->text.equalsIgnoreCase(menuaction))
_items[menunum]->submenu->items[i]->enabled = state;
}
_contentIsDirty = true;
}
void MacMenu::enableCommand(const Common::U32String &menuitem, const Common::U32String &menuaction, bool state) {
uint menunum = 0;
while (menunum < _items.size()) {
assert(_items[menunum]->unicode);
if (_items[menunum]->unicodeText.equals(menuitem))
break;
else
menunum++;
}
if (menunum == _items.size())
return;
if (_items[menunum]->submenu != nullptr)
for (uint i = 0; i < _items[menunum]->submenu->items.size(); i++) {
assert(_items[menunum]->submenu->items[i]->unicode);
if (_items[menunum]->submenu->items[i]->unicodeText.equals(menuaction))
_items[menunum]->submenu->items[i]->enabled = state;
}
_contentIsDirty = true;
}
void MacMenu::enableAllMenus() {
for (uint i = 0; i < _items.size(); i++)
if (_items[i]->submenu != nullptr)
_items[i]->submenu->enableAllItems();
_contentIsDirty = true;
}
void MacMenu::disableAllMenus() {
for (uint i = 1; i < _items.size(); i++) // Leave About menu on
if (_items[i]->submenu != nullptr)
for (uint j = 0; j < _items[i]->submenu->items.size(); j++)
_items[i]->submenu->items[j]->enabled = false;
_contentIsDirty = true;
}
void MacMenu::eventLoop() {
_contentIsDirty = true;
while (_active) {
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
processEvent(event);
draw(_wm->_screen);
}
if (_active) {
g_system->updateScreen();
g_system->delayMillis(10);
}
}
}
} // End of namespace Graphics