Files
scummvm-cursorfix/engines/scumm/macgui/macgui_impl.cpp
2026-02-02 04:50:13 +01:00

1339 lines
35 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/config-manager.h"
#include "common/enc-internal.h"
#include "common/macresman.h"
#include "common/str.h"
#include "graphics/cursorman.h"
#include "graphics/maccursor.h"
#include "graphics/macgamma.h"
#include "graphics/paletteman.h"
#include "graphics/fonts/macfont.h"
#include "graphics/macgui/macwindowmanager.h"
#include "image/cicn.h"
#include "image/pict.h"
#include "scumm/macgui/macgui_impl.h"
#include "scumm/scumm.h"
#include "scumm/scumm_v4.h"
namespace Scumm {
static void activateMenuCallback(void *ref) {
MacGuiImpl *gui = (MacGuiImpl *)ref;
gui->onMenuOpen();
}
// ===========================================================================
// Base class for Macintosh game user interface. Handles menus, fonts, image
// loading, etc.
// ===========================================================================
MacGuiImpl::MacGuiImpl(ScummEngine *vm, const Common::Path &resourceFile) : _vm(vm), _system(_vm->_system), _surface(_vm->_macScreen), _resourceFile(resourceFile) {
_fonts.clear();
_strsStrings.clear();
// kMacRomanConversionTable is a conversion table from Mac Roman
// 128-255 to unicode. What we need, however, is a mapping from
// unicode 160-255 to Mac Roman.
for (int i = 0; i < ARRAYSIZE(_unicodeToMacRoman); i++)
_unicodeToMacRoman[i] = 0;
for (int i = 0; i < ARRAYSIZE(Common::kMacRomanConversionTable); i++) {
int unicode = Common::kMacRomanConversionTable[i];
if (unicode >= 160 && unicode <= 255)
_unicodeToMacRoman[unicode - 160] = 128 + i;
}
}
MacGuiImpl::~MacGuiImpl() {
delete _bannerWindow;
delete _windowManager;
}
uint32 MacGuiImpl::getBlack() const {
return _windowManager->_colorBlack;
}
uint32 MacGuiImpl::getWhite() const {
return _windowManager->_colorWhite;
}
int MacGuiImpl::toMacRoman(int unicode) const {
if (unicode >= 32 && unicode <= 127)
return unicode;
if (unicode < 160 || unicode > 255)
return 0;
int macRoman = _unicodeToMacRoman[unicode - 160];
// These characters are defined in Mac Roman, but apparently not
// present in older fonts like Chicago?
if (macRoman >= 0xD9 && macRoman != 0xF0)
macRoman = 0;
return macRoman;
}
void MacGuiImpl::setPaletteDirty() {
_paletteDirty = true;
}
void MacGuiImpl::updatePalette() {
if (_paletteDirty && !_suspendPaletteUpdates) {
_paletteDirty = false;
_windowManager->passPalette(_vm->_currentPalette, getNumColors());
}
}
bool MacGuiImpl::handleEvent(Common::Event event) {
// The situation we're trying to avoid here is the user opening e.g.
// the save dialog using keyboard shortcuts while the game is paused.
if (_vm->_messageBannerActive)
return false;
return _windowManager->processEvent(event);
}
MacGuiImpl::DelayStatus MacGuiImpl::delay(uint32 ms) {
uint32 to;
to = _system->getMillis() + ms;
while (ms == 0 || _system->getMillis() < to) {
Common::Event event;
while (_system->getEventManager()->pollEvent(event)) {
switch (event.type) {
case Common::EVENT_QUIT:
return kDelayAborted;
case Common::EVENT_LBUTTONDOWN:
return kDelayInterrupted;
default:
break;
}
}
uint32 delta = to - _system->getMillis();
if (delta > 0) {
_system->delayMillis(MIN<uint32>(delta, 10));
_system->updateScreen();
}
}
return kDelayDone;
}
// --------------------------------------------------------------------------
// Menu handling
//
// In the original, the menu was activated by pressing the "Command key".
// This does not seem very friendly to touch devices, so we instead use the
// "mouse over" method in the Mac Window Manager class.
//
// Later games added clicking to open the menu, and even an option to keep the
// menu bar always visible.
//
// TODO: Ideally we should perhaps implement some of these behaviors, but I'm
// not sure which one can sensibly coexist.
// --------------------------------------------------------------------------
void MacGuiImpl::menuCallback(int id, Common::String &name, void *data) {
MacGuiImpl *gui = (MacGuiImpl *)data;
// This menu item (e.g. a menu separator) has no action, so it can be
// immediately ignored.
if (id == 0)
return;
Graphics::MacMenu *menu = gui->_windowManager->getMenu();
bool forceMenuClosed = false;
// If the menu is opened through a shortcut key, force it to activate
// to avoid screen corruption. In that case, we also force the menu to
// close afterwards, or the game will stay paused. Which is
// particularly bad during a restart.
if (!menu->_active) {
gui->_windowManager->activateMenu();
forceMenuClosed = true;
}
// This is how we keep the menu bar visible.
menu->closeMenu();
menu->setActive(true);
menu->setVisible(true);
gui->updateWindowManager();
gui->handleMenu(id, name);
if (forceMenuClosed)
menu->closeMenu();
}
bool MacGuiImpl::initialize() {
if (!readStrings()) {
// FIXME: THIS IS A TEMPORARY WORKAROUND
// The Mac GUI initialization must never fail for Loom or Last Crusade,
// because it's used for more than just the Mac menus and dialogs. So
// for those games we just silently disable the original GUI, and let
// the game start anyway.
if (_vm->_game.id == GID_LOOM || _vm->_game.id == GID_INDY3)
_vm->_useOriginalGUI = false;
else
return false;
}
uint32 menuMode = Graphics::kWMModeNoDesktop | Graphics::kWMModeAutohideMenu |
Graphics::kWMModalMenuMode | Graphics::kWMModeNoCursorOverride | Graphics::kWMModeForceMacFonts;
// Allow a more modern UX: the menu doesn't close if the mouse accidentally goes outside the menu area
if (_vm->enhancementEnabled(kEnhUIUX))
menuMode |= Graphics::kWMModeWin95 | Graphics::kWMModeForceMacFontsInWin95 | Graphics::kWMModeForceMacBorder;
_windowManager = new Graphics::MacWindowManager(menuMode);
_windowManager->setEngine(_vm);
_windowManager->setScreen(640, _vm->_useMacScreenCorrectHeight ? 480 : 400);
_windowManager->setEngineActivateMenuCallback(this, activateMenuCallback);
if (_vm->isUsingOriginalGUI()) {
_windowManager->setMenuHotzone(Common::Rect(640, 23));
_windowManager->setMenuDelay(250);
Common::MacResManager resource;
Graphics::MacMenu *menu = _windowManager->addMenu();
resource.open(_resourceFile);
// Add the Apple menu
const Graphics::MacMenuData menuSubItems[] = {
{ 0, nullptr, 0, 0, false }
};
menu->setCommandsCallback(menuCallback, this);
// Newer games define their menus through an MBAR resource
Common::SeekableReadStream *mbar = resource.getResource(MKTAG('M', 'B', 'A', 'R'), 128);
if (mbar) {
uint16 numMenus = mbar->readUint16BE();
for (uint i = 0; i < numMenus; i++) {
uint16 menuId = mbar->readUint16BE();
addMenu(menu, menuId);
}
delete mbar;
} else {
Common::String aboutMenuDef = _strsStrings[kMSIAboutGameName].c_str();
int maxMenu = -1;
switch (_vm->_game.id) {
case GID_INDY3:
case GID_LOOM:
maxMenu = 130;
break;
case GID_MONKEY:
maxMenu = 131;
break;
default:
maxMenu = 132;
}
if (_vm->_game.id == GID_LOOM) {
aboutMenuDef += ";";
if (!_vm->enhancementEnabled(kEnhUIUX))
aboutMenuDef += "(";
aboutMenuDef += "Drafts Inventory";
}
menu->addStaticMenus(menuSubItems);
menu->createSubMenuFromString(0, aboutMenuDef.c_str(), 0);
for (int i = 129; i <= maxMenu; i++) {
addMenu(menu, i);
}
resource.close();
}
// Assign sensible IDs to the menu items
int numberOfMenus = menu->numberOfMenus();
for (int i = 0; i < numberOfMenus; i++) {
Graphics::MacMenuItem *item = menu->getMenuItem(i);
int numberOfMenuItems = menu->numberOfMenuItems(item);
int id = 100 * (i + 1);
for (int j = 0; j < numberOfMenuItems; j++) {
Graphics::MacMenuItem *subItem = menu->getSubMenuItem(item, j);
Common::String str = menu->getName(subItem);
if (!str.empty()) {
menu->setAction(subItem, id++);
}
if (str.contains("^3")) {
Common::replace(str, "^3", name());
menu->setName(subItem, str);
}
}
}
}
// Register custom fonts. The font family just happens to match the
// printed name of the game.
const Common::String fontFamily = name();
const Common::Array<Graphics::MacFontFamily *> &fontFamilies = _windowManager->_fontMan->getFontFamilies();
_windowManager->_fontMan->loadFonts(_resourceFile);
for (uint i = 0; i < fontFamilies.size(); i++) {
if (fontFamilies[i]->getName() == fontFamily) {
_gameFontId = fontFamilies[i]->getFontFamilyId();
break;
}
}
return true;
}
void MacGuiImpl::addMenu(Graphics::MacMenu *menu, int menuId) {
Common::MacResManager resource;
resource.open(_resourceFile);
Common::SeekableReadStream *res = resource.getResource(MKTAG('M', 'E', 'N', 'U'), menuId);
if (!res) {
resource.close();
return;
}
Common::StringArray *menuDef = Graphics::MacMenu::readMenuFromResource(res);
Common::String name = menuDef->operator[](0);
Common::String string = menuDef->operator[](1);
int id = menu->addMenuItem(nullptr, name);
// The CD version of Fate of Atlantis has a menu item for toggling graphics
// smoothing. We retroactively add that to the remaining V5 games, but not
// to Loom and Last Crusade.
if (_vm->enhancementEnabled(kEnhUIUX)) {
if ((_vm->_game.id == GID_MONKEY || _vm->_game.id == GID_MONKEY2) && id == 3) {
string += ";(-;Smooth Graphics";
}
// Floppy version
if (_vm->_game.id == GID_INDY4 && !string.contains("Smooth Graphics") && id == 3) {
string += ";(-;Smooth Graphics";
}
}
menu->createSubMenuFromString(id, string.c_str(), 0);
delete menuDef;
delete res;
resource.close();
}
void MacGuiImpl::updateMenus() {
// We can't use the name of the menus here, because there are
// non-English versions. Let's hope the menu positions are always the
// same, at least!
Graphics::MacMenu *menu = _windowManager->getMenu();
Graphics::MacMenuItem *gameMenu = menu->getMenuItem(1);
Graphics::MacMenuItem *loadMenu = menu->getSubMenuItem(gameMenu, 0);
Graphics::MacMenuItem *saveMenu = menu->getSubMenuItem(gameMenu, 1);
loadMenu->enabled = _vm->canLoadGameStateCurrently();
saveMenu->enabled = _vm->canSaveGameStateCurrently();
}
bool MacGuiImpl::handleMenu(int id, Common::String &name) {
int saveSlotToHandle = -1;
Common::String savegameName;
switch (id) {
case 100: // About
runAboutDialog();
return true;
case 200: // Open
if (runOpenDialog(saveSlotToHandle)) {
if (saveSlotToHandle > -1) {
_vm->loadGameState(saveSlotToHandle);
if (_vm->_game.id == GID_INDY3)
((ScummEngine_v4 *)_vm)->updateIQPoints();
}
}
return true;
case 201: // Save
_vm->beginTextInput();
if (runSaveDialog(saveSlotToHandle, savegameName)) {
if (saveSlotToHandle > -1) {
_vm->saveGameState(saveSlotToHandle, savegameName);
}
}
_vm->endTextInput();
return true;
case 202: // Restart
if (runRestartDialog())
_vm->restart();
return true;
case 203: // Pause
if (!_vm->_messageBannerActive) {
_windowManager->getMenu()->closeMenu();
if (_vm->_game.version == 3)
_vm->mac_showOldStyleBannerAndPause(_vm->getGUIString(gsPause), -1);
else
_vm->showBannerAndPause(0, -1, _vm->getGUIString(gsPause));
}
return true;
// In the original, the Edit menu is active during save dialogs, though
// only Cut, Copy and Paste.
case 300: // Undo
case 301: // Cut
case 302: // Copy
case 303: // Paste
case 304: // Clear
return true;
}
return false;
}
void MacGuiImpl::updateWindowManager() {
Graphics::MacMenu *menu = _windowManager->getMenu();
if (!menu)
return;
updateMenus();
if (!_windowManager->isMenuActive() && _menuIsActive)
onMenuClose();
if (menu->isVisible())
updatePalette();
_windowManager->draw();
}
void MacGuiImpl::onMenuOpen() {
// We want the arrow cursor for menus. Note that the menu triggers even
// when the mouse is invisible, which may or may not be a bug. But the
// original did allow you to open the menu with Alt even when the
// cursor was visible, so for now it's a feature.
if (!_menuIsActive) {
_menuIsActive = true;
_cursorWasVisible = CursorMan.showMouse(true);
_windowManager->pushCursor(Graphics::MacGUIConstants::kMacCursorArrow);
}
}
void MacGuiImpl::onMenuClose() {
if (_menuIsActive) {
_menuIsActive = false;
if (_windowManager->getCursorType() == Graphics::MacGUIConstants::kMacCursorArrow)
_windowManager->popCursor();
CursorMan.showMouse(_cursorWasVisible);
}
}
// ---------------------------------------------------------------------------
// Font handling
// ---------------------------------------------------------------------------
const Graphics::Font *MacGuiImpl::getFont(FontId fontId) {
const Graphics::Font *font = _fonts.getValOrDefault((int)fontId);
if (font)
return font;
int id;
int size;
int slant;
switch (fontId) {
case kSystemFont:
id = Graphics::kMacFontSystem;
size = 12;
slant = Graphics::kMacFontRegular;
break;
default:
getFontParams(fontId, id, size, slant);
break;
}
font = _windowManager->_fontMan->getFont(Graphics::MacFont(id, size, slant));
_fonts[(int)fontId] = font;
return font;
}
bool MacGuiImpl::getFontParams(FontId fontId, int &id, int &size, int &slant) const {
switch (fontId) {
case kAboutFontHeaderOutside:
id = _gameFontId;
size = 12;
slant = Graphics::kMacFontItalic | Graphics::kMacFontBold | Graphics::kMacFontOutline;
return true;
case kAboutFontHeaderInside:
id = _gameFontId;
size = 12;
slant = Graphics::kMacFontItalic | Graphics::kMacFontBold | Graphics::kMacFontExtend;
return true;
case kAboutFontRegular:
id = Graphics::kMacFontApplication;
size = 9;
slant = Graphics::kMacFontRegular;
return true;
case kAboutFontBold:
id = _gameFontId;
size = 9;
slant = Graphics::kMacFontRegular;
return true;
case kAboutFontExtraBold:
id = _gameFontId;
size = 9;
slant = Graphics::kMacFontBold;
return true;
default:
return false;
}
}
Graphics::Surface *MacGuiImpl::createRemappedSurface(const Graphics::Surface *surface, const byte *palette, uint colorCount) {
Graphics::Surface *s = new Graphics::Surface();
s->create(surface->w, surface->h, Graphics::PixelFormat::createFormatCLUT8());
byte paletteMap[256];
memset(paletteMap, 0, sizeof(paletteMap));
const byte monoPalette[] = {
0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00
};
if (colorCount == 0) {
colorCount = 2;
palette = monoPalette;
}
for (uint i = 0; i < colorCount; i++) {
int r = palette[3 * i];
int g = palette[3 * i + 1];
int b = palette[3 * i + 2];
uint32 c;
c = _windowManager->findBestColor(r, g, b);
paletteMap[i] = c;
}
// Colors outside the palette are not remapped.
for (uint i = colorCount; i < 256; i++)
paletteMap[i] = i;
if (palette) {
for (int y = 0; y < s->h; y++) {
for (int x = 0; x < s->w; x++) {
uint color = surface->getPixel(x, y);
if (color > colorCount)
color = getBlack();
else
color = paletteMap[color];
s->setPixel(x, y, color);
}
}
} else {
s->copyFrom(*surface);
}
return s;
}
bool MacGuiImpl::setupResourceCursor(int id, int &width, int &height, int &hotspotX, int &hotspotY, int &animate) {
Common::MacResManager resource;
Graphics::MacCursor macCursor;
bool success = false;
resource.open(_resourceFile);
Common::SeekableReadStream *curs = resource.getResource(MKTAG('C', 'U', 'R', 'S'), id);
if (curs && macCursor.readFromStream(*curs)) {
width = macCursor.getWidth();
height = macCursor.getHeight();
hotspotX = macCursor.getHotspotX();
hotspotY = macCursor.getHotspotY();
animate = 0;
_windowManager->replaceCursor(Graphics::MacGUIConstants::kMacCursorCustom, &macCursor);
success = true;
}
delete curs;
resource.close();
return success;
}
// ---------------------------------------------------------------------------
// Icon loader
// ---------------------------------------------------------------------------
bool MacGuiImpl::loadIcon(int id, Graphics::Surface **icon, Graphics::Surface **mask) {
bool result = false;
Common::MacResManager resource;
resource.open(_resourceFile);
Common::SeekableReadStream *res = resource.getResource(MKTAG('c', 'i', 'c', 'n'), id);
Image::CicnDecoder iconDecoder;
*icon = nullptr;
*mask = nullptr;
if (res && iconDecoder.loadStream(*res)) {
result = true;
const Graphics::Surface *s1 = iconDecoder.getSurface();
const Graphics::Surface *s2 = iconDecoder.getMask();
const Graphics::Palette &palette = iconDecoder.getPalette();
*icon = createRemappedSurface(s1, palette.data(), palette.size());
*mask = new Graphics::Surface();
(*mask)->copyFrom(*s2);
}
delete res;
resource.close();
return result;
}
// ---------------------------------------------------------------------------
// PICT loader
// ---------------------------------------------------------------------------
Graphics::Surface *MacGuiImpl::loadPict(int id) {
Common::MacResManager resource;
resource.open(_resourceFile);
Common::SeekableReadStream *res = resource.getResource(MKTAG('P', 'I', 'C', 'T'), id);
Image::PICTDecoder pictDecoder;
Graphics::Surface *s = nullptr;
if (res && pictDecoder.loadStream(*res)) {
const Graphics::Surface *surface = pictDecoder.getSurface();
const Graphics::Palette &palette = pictDecoder.getPalette();
s = createRemappedSurface(surface, palette.data(), palette.size());
}
delete res;
resource.close();
return s;
}
// ---------------------------------------------------------------------------
// Window handling
// ---------------------------------------------------------------------------
// In the older Mac games, the GUI and game graphics are made to coexist, so
// the GUI should look good regardless of what's on screen. In the newer ones
// there is no such guarantee, so the screen is blacked out whenever the GUI
// is shown.
//
// We hard-code the upper part of the palette to have all the colors used by
// the Mac Window manager and window borders. This is so that drawing the
// window will not mess up what's already on screen. The lower part is used for
// any graphical elements (pictures and icons) in the window. Anything in
// between is set to black.
//
// The Mac window manager gets the palette before gamma correction, the backend
// gets it afterwards.
void MacGuiImpl::setMacGuiColors(Graphics::Palette &palette) {
// Colors used by the Mac Window Manager
palette.set(255, 0x00, 0x00, 0x00); // Black
palette.set(254, 0xFF, 0xFF, 0xFF); // White
palette.set(253, 0x80, 0x80, 0x80); // Gray80
palette.set(252, 0x88, 0x88, 0x88); // Gray88
palette.set(251, 0xEE, 0xEE, 0xEE); // GrayEE
palette.set(250, 0x00, 0xFF, 0x00); // Green
palette.set(249, 0x00, 0xCF, 0x00); // Green2
// Colors used by Mac dialog window borders
palette.set(248, 0xCC, 0xCC, 0xFF);
palette.set(247, 0xBB, 0xBB, 0xBB);
palette.set(246, 0x66, 0x66, 0x99);
for (int i = 0; i < 246; i++)
palette.set(i, 0x00, 0x00, 0x00);
}
MacGuiImpl::MacDialogWindow *MacGuiImpl::createWindow(Common::Rect bounds, MacDialogWindowStyle windowStyle, MacDialogMenuStyle menuStyle) {
if (!_vm->_isModernMacVersion) {
updatePalette();
_macBlack = _windowManager->_colorBlack;
_macWhite = _windowManager->_colorWhite;
}
if (bounds.left < 0 || bounds.top < 0 || bounds.right >= 640 || bounds.bottom >= 400) {
// This happens with the Last Crusade file dialogs.
bounds.moveTo((640 - bounds.width()) / 2, 27);
}
// Adjust the dialog to the actual screen size. This is slightly
// wasteful since we've already adjusted the dialog coordinates for
// 640x400 pixels, but that may not be a bad thing if we want to keep
// support for that resolution later.
bounds.translate(0, 2 * _vm->_macScreenDrawOffset);
return new MacDialogWindow(this, _system, _surface, bounds, windowStyle, menuStyle);
}
MacGuiImpl::MacDialogWindow *MacGuiImpl::createDialog(int dialogId) {
Common::Rect bounds;
// Default dialog sizes for dialogs without a DITL resource.
if (!_vm->_isModernMacVersion) {
bounds.top = 0;
bounds.left = 0;
bounds.bottom = 86;
bounds.right = 340;
bounds.translate(86, 88);
} else {
bounds.top = 0;
bounds.left = 0;
bounds.bottom = 113;
bounds.right = 267;
bounds.translate(187, 94);
}
return createDialog(dialogId, bounds);
}
MacGuiImpl::MacDialogWindow *MacGuiImpl::createDialog(int dialogId, Common::Rect bounds) {
uint32 black = getBlack();
Common::MacResManager resource;
Common::SeekableReadStream *res;
Common::String gameFileResStr = _strsStrings[kMSIGameFile].c_str();
resource.open(_resourceFile);
res = resource.getResource(MKTAG('D', 'L', 'O', 'G'), dialogId);
if (res) {
bounds.top = res->readUint16BE();
bounds.left = res->readUint16BE();
bounds.bottom = res->readUint16BE();
bounds.right = res->readUint16BE();
// Grow the window to include the outer bounds
bounds.grow(8);
// Compensate for the original not drawing the game at the very top of
// the screen.
bounds.translate(0, -40);
}
delete res;
_macWhite = _windowManager->_colorWhite;
_macBlack = _windowManager->_colorBlack;
if (_vm->_isModernMacVersion) {
res = resource.getResource(MKTAG('D', 'I', 'T', 'L'), dialogId);
if (!res)
return nullptr;
saveScreen();
// Collect the palettes from all the icons and pictures in the
// dialog, and combine them into a single palette that they
// will all be remapped to use. This is probably not what the
// original did, as the colors become slightly different. Maybe
// the original just picked one of the palettes, and then used
// the closest available colors for the rest?
//
// That might explain why some of them have seemingly larger
// palettes than necessary, but why not use the exact colors
// when we can?
Common::HashMap<uint32, byte> paletteMap;
int numWindowColors = 0;
// Additional colors for hard-coded elements
paletteMap[0xCDCDCD] = numWindowColors++;
int numItems = res->readUint16BE() + 1;
for (int i = 0; i < numItems; i++) {
res->skip(12);
int type = res->readByte();
int len = res->readByte();
Image::PICTDecoder pictDecoder;
Image::CicnDecoder iconDecoder;
Common::SeekableReadStream *imageRes = nullptr;
Image::ImageDecoder *decoder = nullptr;
switch (type & 0x7F) {
case 32:
imageRes = resource.getResource(MKTAG('c', 'i', 'c', 'n'), res->readUint16BE());
decoder = &iconDecoder;
break;
case 64:
imageRes = resource.getResource(MKTAG('P', 'I', 'C', 'T'), res->readUint16BE());
decoder = &pictDecoder;
break;
default:
res->skip(len);
break;
}
if (imageRes && decoder->loadStream(*imageRes)) {
const Graphics::Palette &palette = decoder->getPalette();
for (uint j = 0; j < palette.size(); j++) {
byte r, g, b;
palette.get(j, r, g, b);
uint32 color = (r << 16) | (g << 8) | b;
if (!paletteMap.contains(color))
paletteMap[color] = numWindowColors++;
}
}
if (len & 1)
res->skip(1);
delete imageRes;
}
delete res;
Graphics::Palette palette(256);
setMacGuiColors(palette);
for (auto &k : paletteMap) {
int r = (k._key >> 16) & 0xFF;
int g = (k._key >> 8) & 0xFF;
int b = k._key & 0xFF;
palette.set(k._value, r, g, b);
}
_windowManager->passPalette(palette.data(), 256);
if (_vm->_useGammaCorrection) {
for (int i = 0; i < 256; i++) {
byte r, g, b;
palette.get(i, r, g, b);
r = Graphics::macGammaCorrectionLookUp[r];
g = Graphics::macGammaCorrectionLookUp[g];
b = Graphics::macGammaCorrectionLookUp[b];
palette.set(i, r, g, b);
}
}
_system->getPaletteManager()->setPalette(palette);
}
res = resource.getResource(MKTAG('D', 'I', 'T', 'L'), dialogId);
if (!res)
return nullptr;
MacDialogWindow *window = createWindow(bounds);
if (res) {
int numItems = res->readUint16BE() + 1;
for (int i = 0; i < numItems; i++) {
res->skip(4); // Placeholder for handle or procedure pointer
Common::Rect r;
r.top = res->readUint16BE();
r.left = res->readUint16BE();
r.bottom = res->readUint16BE();
r.right = res->readUint16BE();
// Move to appropriate position on inner surface
r.translate(2, 2);
int type = res->readByte();
int len = res->readByte();
Common::String str;
bool enabled = ((type & 0x80) == 0);
switch (type & 0x7F) {
case 0:
// User item
// window->innerSurface()->frameRect(r, black);
res->skip(len);
break;
case 4:
// Button
res->seek(-1, SEEK_CUR);
str = res->readPascalString();
window->addButton(r, str, enabled);
break;
case 5:
// Checkbox
res->seek(-1, SEEK_CUR);
str = res->readPascalString();
window->addCheckbox(r, str, enabled);
break;
case 7:
// Control
window->addControl(r, res->readUint16BE());
break;
case 8:
// Static text
res->seek(-1, SEEK_CUR);
str = res->readPascalString();
window->addStaticText(r, str, enabled);
break;
case 16:
{
// Editable text
// Adjust for pixel accuracy...
r.left -= 1;
MacGuiImpl::MacEditText *editText = window->addEditText(r, gameFileResStr, enabled);
editText->selectAll();
window->innerSurface()->frameRect(Common::Rect(r.left - 2, r.top - 3, r.right + 3, r.bottom + 3), black);
res->skip(len);
break;
}
case 32:
// Icon
window->addIcon(r.left, r.top, res->readUint16BE(), enabled);
break;
case 64:
// Picture
window->addPicture(r, res->readUint16BE(), enabled);
break;
default:
warning("MacGuiImpl::createDialog(): Unknown item type %d", type);
res->skip(len);
break;
}
if (len & 1)
res->skip(1);
}
}
delete res;
resource.close();
return window;
}
// ---------------------------------------------------------------------------
// Standard dialogs
// ---------------------------------------------------------------------------
// A standard file picker dialog doesn't really make sense in ScummVM, so we
// make something that just looks similar to one.
bool MacGuiImpl::runOpenDialog(int &saveSlotToHandle) {
Common::Rect bounds(88, 28, 448, 210);
MacDialogWindow *window = createWindow(bounds);
MacButton *buttonOpen = window->addButton(Common::Rect(254, 137, 334, 157), "Open", true);
MacButton *buttonCancel = window->addButton(Common::Rect(254, 106, 334, 126), "Cancel", true);
window->addButton(Common::Rect(254, 62, 334, 82), "Desktop", false);
window->addButton(Common::Rect(254, 34, 334, 54), "Eject", false);
window->drawDottedHLine(253, 93, 334);
bool availSlots[100];
int slotIds[100];
Common::StringArray savegameNames;
prepareSaveLoad(savegameNames, availSlots, slotIds, ARRAYSIZE(availSlots));
drawFakePathList(window, Common::Rect(14, 8, 232, 27), name().c_str());
drawFakeDriveLabel(window, Common::Rect(232, 10, 344, 28), "ScummVM");
MacListBox *listBox = window->addListBox(Common::Rect(14, 31, 232, 161), savegameNames, true);
window->setDefaultWidget(buttonOpen);
while (!_vm->shouldQuit()) {
MacDialogEvent event;
while (window->runDialog(event)) {
switch (event.type) {
case kDialogClick:
if (event.widget == buttonOpen || event.widget == listBox) {
saveSlotToHandle =
listBox->getValue() < ARRAYSIZE(slotIds) ?
slotIds[listBox->getValue()] : -1;
delete window;
return true;
} else if (event.widget == buttonCancel) {
delete window;
return false;
}
break;
default:
break;
}
}
window->delayAndUpdate();
}
// When quitting, do not load the saved game
delete window;
return false;
}
bool MacGuiImpl::runSaveDialog(int &saveSlotToHandle, Common::String &saveName) {
uint32 black = getBlack();
Common::Rect bounds(110, 27, 470, 231);
MacDialogWindow *window = createWindow(bounds);
MacButton *buttonSave = window->addButton(Common::Rect(254, 163, 334, 183), "Save", true);
MacButton *buttonCancel = window->addButton(Common::Rect(254, 132, 334, 152), "Cancel", true);
window->addButton(Common::Rect(254, 90, 334, 110), "New", false);
window->addButton(Common::Rect(254, 62, 334, 82), "Desktop", false);
window->addButton(Common::Rect(254, 34, 334, 54), "Eject", false);
bool busySlots[100];
int slotIds[100];
Common::StringArray savegameNames;
prepareSaveLoad(savegameNames, busySlots, slotIds, ARRAYSIZE(busySlots));
Common::String saveGameFileAsResStr = _strsStrings[kMSISaveGameFileAs].c_str();
Common::String gameFileResStr = _strsStrings[kMSIGameFile].c_str();
int firstAvailableSlot = -1;
for (int i = 1; i < ARRAYSIZE(busySlots); i++) { // Skip the autosave slot
if (!busySlots[i]) {
firstAvailableSlot = i;
break;
}
}
drawFakePathList(window, Common::Rect(14, 8, 232, 27), name().c_str());
drawFakeDriveLabel(window, Common::Rect(232, 10, 344, 28), "ScummVM");
window->addListBox(Common::Rect(14, 31, 232, 129), savegameNames, true, true);
MacGuiImpl::MacEditText *editText = window->addEditText(Common::Rect(16, 159, 229, 175), gameFileResStr, true);
Graphics::Surface *s = window->innerSurface();
const Graphics::Font *font = getFont(kSystemFont);
s->frameRect(Common::Rect(14, 156, 232, 178), black);
window->drawDottedHLine(253, 121, 334);
font->drawString(s, saveGameFileAsResStr, 14, 138, 218, black, Graphics::kTextAlignLeft, 4);
window->setDefaultWidget(buttonSave);
editText->selectAll();
while (!_vm->shouldQuit()) {
MacDialogEvent event;
while (window->runDialog(event)) {
switch (event.type) {
case kDialogClick:
if (event.widget == buttonSave) {
saveName = editText->getText();
saveSlotToHandle = firstAvailableSlot;
delete window;
return true;
} else if (event.widget == buttonCancel) {
delete window;
return false;
}
break;
case kDialogKeyDown:
if (event.widget == editText) {
// Disable "Save" button when text is empty
buttonSave->setEnabled(!editText->getText().empty());
}
break;
default:
break;
}
}
window->delayAndUpdate();
}
// When quitting, do not save the game
delete window;
return false;
}
void MacGuiImpl::prepareSaveLoad(Common::StringArray &savegameNames, bool *availSlots, int *slotIds, int size) {
int saveCounter = 0;
for (int i = 0; i < size; i++) {
slotIds[i] = -1;
}
Common::String name;
_vm->listSavegames(availSlots, size);
for (int i = 0; i < size; i++) {
if (availSlots[i]) {
// Save the slot ids for slots which actually contain savegames...
slotIds[saveCounter] = i;
saveCounter++;
if (_vm->getSavegameName(i, name)) {
Common::String temp = Common::U32String(name, _vm->getDialogCodePage()).encode(Common::kMacRoman);
savegameNames.push_back(temp);
} else {
// The original printed "WARNING... old savegame", but we do support old savegames :-)
savegameNames.push_back(Common::String::format("%s", "WARNING: wrong save version"));
}
}
}
}
bool MacGuiImpl::runOkCancelDialog(Common::String text) {
// Widgets:
//
// 0 - Okay button
// 1 - Cancel button
// 2 - "^0" text
MacDialogWindow *window = createDialog(502);
MacButton *buttonOk = (MacButton *)window->getWidget(kWidgetButton, 0);
MacButton *buttonCancel = (MacButton *)window->getWidget(kWidgetButton, 1);
window->setDefaultWidget(buttonOk);
window->addSubstitution(text);
while (!_vm->shouldQuit()) {
MacDialogEvent event;
while (window->runDialog(event)) {
switch (event.type) {
case kDialogClick:
if (event.widget == buttonOk) {
delete window;
return true;
} else if (event.widget == buttonCancel) {
delete window;
return false;
}
default:
break;
}
}
window->delayAndUpdate();
}
// Quitting is the same as clicking Ok
delete window;
return true;
}
void MacGuiImpl::drawFakePathList(MacDialogWindow *window, Common::Rect r, const char *text) {
uint32 black = getBlack();
const Graphics::Font *font = getFont(kSystemFont);
// Draw the text...
int x0 = r.left + 23;
int x1 = r.right - 23;
font->drawString(window->innerSurface(), text, x0 + 1, r.top + 2, x1 - x0, black, Graphics::kTextAlignCenter, 0, true);
int width = font->getStringWidth(text);
if (width < x1 - x0) {
int middle = (x0 + x1) / 2;
x0 = middle - width / 2;
x1 = middle + width / 2;
}
Common::Rect iconRect(16, 16);
const uint16 folderIcon[16] = {
0x0000, 0x0000, 0x0000, 0x0000, 0x1E00, 0x21FC, 0x2002, 0xFFE2,
0x8012, 0x4012, 0x400A, 0x200A, 0x2006, 0x1FFE, 0x0000, 0x0000
};
const uint16 arrowDownIcon[16] = {
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x3FF8, 0x1FF0, 0x0FE0,
0x07C0, 0x0380, 0x0100, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
};
// Draw the icon...
iconRect.moveTo(x0 - 19, r.top + 1);
drawBitmap(window->innerSurface(), iconRect, folderIcon, black);
// Draw the arrow...
iconRect.moveTo(x1 + 4, r.top + 1);
drawBitmap(window->innerSurface(), iconRect, arrowDownIcon, black);
// Draw the black frame...
window->innerSurface()->frameRect(Common::Rect(x0 - 23, r.top, x1 + 23, r.bottom - 1), black);
// Draw the shadows...
window->innerSurface()->hLine(x0 - 20, r.bottom - 1, x1 + 23, black);
window->innerSurface()->vLine(x1 + 23, r.top + 3, r.bottom - 1, black);
}
void MacGuiImpl::drawFakeDriveLabel(MacDialogWindow *window, Common::Rect r, const char *text) {
uint32 black = getBlack();
const Graphics::Font *font = getFont(kSystemFont);
font->drawString(window->innerSurface(), text, r.left, r.top, r.width(), black, Graphics::kTextAlignCenter, 9, true);
const uint16 hardDriveIcon[16] = {
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x7FFE, 0x8001, 0x8001,
0xA001, 0x8001, 0x7FFE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
};
Common::Rect iconRect(16, 16);
int width = font->getStringWidth(text);
if (width < r.width())
iconRect.moveTo((r.left + r.right - width - 20) / 2, r.top);
else
iconRect.moveTo(r.left, r.top);
drawBitmap(window->innerSurface(), iconRect, hardDriveIcon, black);
}
bool MacGuiImpl::runQuitDialog() {
return runOkCancelDialog(_strsStrings[kMSIAreYouSureYouWantToQuit].c_str());
}
bool MacGuiImpl::runRestartDialog() {
return runOkCancelDialog(_strsStrings[kMSIAreYouSureYouWantToRestart].c_str());
}
void MacGuiImpl::drawBanner(char *message) {
if (_bannerWindow)
undrawBanner();
_bannerWindow = createWindow(
Common::Rect(70, 189, 570, 211),
kWindowStyleRounded, kMenuStyleNone);
const Graphics::Font *font = getFont(_vm->_game.id == GID_INDY3 ? kIndy3FontMedium : kLoomFontMedium);
Graphics::Surface *s = _bannerWindow->innerSurface();
font->drawString(s, (char *)message, 0, 0, s->w, getBlack(), Graphics::kTextAlignCenter);
_bannerWindow->show();
}
void MacGuiImpl::undrawBanner() {
if (_bannerWindow) {
delete _bannerWindow;
_bannerWindow = nullptr;
}
}
void MacGuiImpl::drawBitmap(Graphics::Surface *s, Common::Rect r, const uint16 *bitmap, uint32 color) const {
assert(r.width() <= 16);
for (int y = 0; y < r.height(); y++) {
uint16 bit = 0x8000;
for (int x = 0; x < r.width(); x++) {
if (bitmap[y] & bit)
s->setPixel(r.left + x, r.top + y, color);
bit >>= 1;
}
}
}
} // End of namespace Scumm