/* 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 . * */ #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(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 &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 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