scummvm/gui/ThemeEngine.cpp
Vladimir Serbinenko ef3eda01e0 GUI: Fix black rectangle around cursor in modern remastered them without RGB
Modern remastered uses cursor with alpha. It's not correctly converted and
so we get black rectangle around it when using --disable-16bit
2023-02-02 19:36:12 +03:00

2143 lines
63 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/file.h"
#include "common/fs.h"
#include "common/compression/unzip.h"
#include "common/tokenizer.h"
#include "common/translation.h"
#include "common/unicode-bidi.h"
#include "graphics/blit.h"
#include "graphics/cursorman.h"
#include "graphics/fontman.h"
#include "graphics/surface.h"
#include "graphics/svg.h"
#include "graphics/VectorRenderer.h"
#include "graphics/fonts/bdf.h"
#include "graphics/fonts/ttf.h"
#include "image/bmp.h"
#include "image/png.h"
#include "gui/widget.h"
#include "gui/ThemeEngine.h"
#include "gui/ThemeEval.h"
#include "gui/ThemeParser.h"
namespace GUI {
const char *const ThemeEngine::kImageLogo = "logo.bmp";
const char *const ThemeEngine::kImageLogoSmall = "logo_small.bmp";
const char *const ThemeEngine::kImageSearch = "search.bmp";
const char *const ThemeEngine::kImageGroup = "groupbtn.bmp";
const char *const ThemeEngine::kImageEraser = "eraser.bmp";
const char *const ThemeEngine::kImageDelButton = "delbtn.bmp";
const char *const ThemeEngine::kImageList = "list.bmp";
const char *const ThemeEngine::kImageGrid = "grid.bmp";
const char *const ThemeEngine::kImageStopButton = "stopbtn.bmp";
const char *const ThemeEngine::kImageEditButton = "editbtn.bmp";
const char *const ThemeEngine::kImageSwitchModeButton = "switchbtn.bmp";
const char *const ThemeEngine::kImageFastReplayButton = "fastreplay.bmp";
const char *const ThemeEngine::kImageStopSmallButton = "stopbtn_small.bmp";
const char *const ThemeEngine::kImageEditSmallButton = "editbtn_small.bmp";
const char *const ThemeEngine::kImageSwitchModeSmallButton = "switchbtn_small.bmp";
const char *const ThemeEngine::kImageFastReplaySmallButton = "fastreplay_small.bmp";
struct TextDrawData {
const Graphics::Font *_fontPtr;
};
struct TextColorData {
int r, g, b;
};
struct WidgetDrawData {
/** List of all the steps needed to draw this widget */
Common::List<Graphics::DrawStep> _steps;
TextData _textDataId;
TextColor _textColorId;
Graphics::TextAlign _textAlignH;
GUI::ThemeEngine::TextAlignVertical _textAlignV;
/** Extra space that the widget occupies when it's drawn.
E.g. when taking into account rounded corners, drop shadows, etc
Used when restoring the widget background */
uint16 _backgroundOffset;
uint16 _shadowOffset;
DrawLayer _layer;
/**
* Calculates the background threshold offset of a given DrawData item.
* After fully loading all DrawSteps of a DrawData item, this function must be
* called in order to calculate if such draw steps would be drawn outside of
* the actual widget drawing zone (e.g. shadows). If this is the case, a constant
* value will be added when restoring the background of the widget.
*/
void calcBackgroundOffset();
};
/**********************************************************
* Data definitions for theme engine elements
*********************************************************/
struct DrawDataInfo {
DrawData id; ///< The actual ID of the DrawData item.
const char *name; ///< The name of the DrawData item as it appears in the Theme Description files
DrawLayer layer; ///< Sets whether this item is part of the foreground or background layer of its dialog
DrawData parent; ///< Parent DrawData item, for items that overlay. E.g. kDDButtonIdle -> kDDButtonHover
};
/**
* Default values for each DrawData item.
*/
static const DrawDataInfo kDrawDataDefaults[] = {
{kDDMainDialogBackground, "mainmenu_bg", kDrawLayerBackground, kDDNone},
{kDDSpecialColorBackground, "special_bg", kDrawLayerBackground, kDDNone},
{kDDPlainColorBackground, "plain_bg", kDrawLayerBackground, kDDNone},
{kDDTooltipBackground, "tooltip_bg", kDrawLayerBackground, kDDNone},
{kDDDefaultBackground, "default_bg", kDrawLayerBackground, kDDNone},
{kDDTextSelectionBackground, "text_selection", kDrawLayerForeground, kDDNone},
{kDDTextSelectionFocusBackground, "text_selection_focus", kDrawLayerForeground, kDDNone},
{kDDThumbnailBackground, "thumb_bg", kDrawLayerForeground, kDDNone},
{kDDGridItemIdle, "griditem_bg", kDrawLayerForeground, kDDNone},
{kDDGridItemHover, "griditem_hover_bg", kDrawLayerForeground, kDDNone},
{kDDWidgetBackgroundDefault, "widget_default", kDrawLayerBackground, kDDNone},
{kDDWidgetBackgroundSmall, "widget_small", kDrawLayerBackground, kDDNone},
{kDDWidgetBackgroundEditText, "widget_textedit", kDrawLayerBackground, kDDNone},
{kDDWidgetBackgroundSlider, "widget_slider", kDrawLayerBackground, kDDNone},
{kDDButtonIdle, "button_idle", kDrawLayerBackground, kDDNone},
{kDDButtonHover, "button_hover", kDrawLayerForeground, kDDButtonIdle},
{kDDButtonDisabled, "button_disabled", kDrawLayerBackground, kDDNone},
{kDDButtonPressed, "button_pressed", kDrawLayerForeground, kDDButtonIdle},
{kDDDropDownButtonIdle, "dropdown_button_idle", kDrawLayerBackground, kDDNone},
{kDDDropDownButtonHoverLeft, "dropdown_button_hover_left", kDrawLayerForeground, kDDDropDownButtonIdle},
{kDDDropDownButtonHoverRight, "dropdown_button_hover_right", kDrawLayerForeground, kDDDropDownButtonIdle},
{kDDDropDownButtonDisabled, "dropdown_button_disabled", kDrawLayerForeground, kDDNone},
{kDDDropDownButtonPressedLeft, "dropdown_button_pressed_left", kDrawLayerForeground, kDDDropDownButtonIdle},
{kDDDropDownButtonPressedRight, "dropdown_button_pressed_right", kDrawLayerForeground, kDDDropDownButtonIdle},
{kDDDropDownButtonIdleRTL, "dropdown_button_idle_rtl", kDrawLayerBackground, kDDNone},
{kDDDropDownButtonHoverLeftRTL, "dropdown_button_hover_left_rtl", kDrawLayerForeground, kDDDropDownButtonIdleRTL},
{kDDDropDownButtonHoverRightRTL, "dropdown_button_hover_right_rtl", kDrawLayerForeground, kDDDropDownButtonIdleRTL},
{kDDDropDownButtonDisabledRTL, "dropdown_button_disabled_rtl", kDrawLayerForeground, kDDNone},
{kDDDropDownButtonPressedLeftRTL, "dropdown_button_pressed_left_rtl", kDrawLayerForeground, kDDDropDownButtonIdleRTL},
{kDDDropDownButtonPressedRightRTL, "dropdown_button_pressed_right_rtl", kDrawLayerForeground, kDDDropDownButtonIdleRTL},
{kDDSliderFull, "slider_full", kDrawLayerForeground, kDDNone},
{kDDSliderHover, "slider_hover", kDrawLayerForeground, kDDNone},
{kDDSliderDisabled, "slider_disabled", kDrawLayerForeground, kDDNone},
{kDDCheckboxDefault, "checkbox_default", kDrawLayerBackground, kDDNone},
{kDDCheckboxDisabled, "checkbox_disabled", kDrawLayerBackground, kDDNone},
{kDDCheckboxSelected, "checkbox_selected", kDrawLayerForeground, kDDCheckboxDefault},
{kDDCheckboxDisabledSelected, "checkbox_disabled_selected", kDrawLayerForeground, kDDCheckboxDisabled},
{kDDRadiobuttonDefault, "radiobutton_default", kDrawLayerBackground, kDDNone},
{kDDRadiobuttonDisabled, "radiobutton_disabled", kDrawLayerBackground, kDDNone},
{kDDRadiobuttonSelected, "radiobutton_selected", kDrawLayerForeground, kDDRadiobuttonDefault},
{kDDTabActive, "tab_active", kDrawLayerForeground, kDDTabInactive},
{kDDTabInactive, "tab_inactive", kDrawLayerBackground, kDDNone},
{kDDTabBackground, "tab_background", kDrawLayerBackground, kDDNone},
{kDDScrollbarBase, "scrollbar_base", kDrawLayerBackground, kDDNone},
{kDDScrollbarButtonIdle, "scrollbar_button_idle", kDrawLayerBackground, kDDNone},
{kDDScrollbarButtonHover, "scrollbar_button_hover", kDrawLayerForeground, kDDScrollbarButtonIdle},
{kDDScrollbarHandleIdle, "scrollbar_handle_idle", kDrawLayerForeground, kDDNone},
{kDDScrollbarHandleHover, "scrollbar_handle_hover", kDrawLayerForeground, kDDScrollbarBase},
{kDDPopUpIdle, "popup_idle", kDrawLayerBackground, kDDNone},
{kDDPopUpHover, "popup_hover", kDrawLayerForeground, kDDPopUpIdle},
{kDDPopUpDisabled, "popup_disabled", kDrawLayerBackground, kDDNone},
{kDDPopUpIdleRTL, "popup_idle_rtl", kDrawLayerBackground, kDDNone},
{kDDPopUpHoverRTL, "popup_hover_rtl", kDrawLayerForeground, kDDPopUpIdleRTL},
{kDDPopUpDisabledRTL, "popup_disabled_rtl", kDrawLayerBackground, kDDNone},
{kDDCaret, "caret", kDrawLayerForeground, kDDNone},
{kDDSeparator, "separator", kDrawLayerBackground, kDDNone},
};
/**********************************************************
* ThemeEngine class
*********************************************************/
ThemeEngine::ThemeEngine(Common::String id, GraphicsMode mode) :
_system(nullptr), _vectorRenderer(nullptr),
_layerToDraw(kDrawLayerBackground), _bytesPerPixel(0), _graphicsMode(kGfxDisabled),
_font(nullptr), _initOk(false), _themeOk(false), _enabled(false), _themeFiles(),
_cursor(nullptr), _scaleFactor(1.0f) {
_baseWidth = 640; // Default sane values
_baseHeight = 480;
_system = g_system;
_parser = new ThemeParser(this);
_themeEval = new GUI::ThemeEval();
_themeEval->setScaleFactor(_scaleFactor);
_useCursor = false;
for (int i = 0; i < kDrawDataMAX; ++i) {
_widgets[i] = nullptr;
}
for (int i = 0; i < kTextDataMAX; ++i) {
_texts[i] = nullptr;
}
for (int i = 0; i < kTextColorMAX; ++i) {
_textColors[i] = nullptr;
}
// We currently allow two different ways of theme selection in our config file:
// 1) Via full path
// 2) Via a basename, which will need to be translated into a full path
// This function assures we have a correct path to pass to the ThemeEngine
// constructor.
_themeFile = getThemeFile(id);
// We will use getThemeId to retrive the theme id from the given filename
// here, since the user could have passed a fixed filename as 'id'.
_themeId = getThemeId(_themeFile);
_graphicsMode = mode;
_themeArchive = nullptr;
_initOk = false;
_cursorHotspotX = _cursorHotspotY = 0;
_cursorWidth = _cursorHeight = 0;
_cursorTransparent = 255;
#ifndef USE_RGB_COLOR
_cursorFormat = Graphics::PixelFormat::createFormatCLUT8();
_cursorPalSize = 0;
#endif
// We prefer files in archive bundles over the common search paths.
_themeFiles.add("default", &SearchMan, 0, false);
}
ThemeEngine::~ThemeEngine() {
delete _vectorRenderer;
_vectorRenderer = nullptr;
_screen.free();
_backBuffer.free();
unloadTheme();
unloadExtraFont();
// Release all graphics surfaces
for (ImagesMap::iterator i = _bitmaps.begin(); i != _bitmaps.end(); ++i) {
Graphics::ManagedSurface *surf = i->_value;
if (surf) {
surf->free();
delete surf;
}
}
_bitmaps.clear();
delete _parser;
delete _themeEval;
delete[] _cursor;
}
/**********************************************************
* Rendering mode management
*********************************************************/
const ThemeEngine::Renderer ThemeEngine::_rendererModes[] = {
{ _s("Disabled GFX"), _sc("Disabled GFX", "lowres"), "none", kGfxDisabled },
{ _s("Standard renderer"), _s("Standard"), "normal", kGfxStandard },
#ifndef DISABLE_FANCY_THEMES
{ _s("Antialiased renderer"), _s("Antialiased"), "antialias", kGfxAntialias }
#endif
};
const uint ThemeEngine::_rendererModesSize = ARRAYSIZE(ThemeEngine::_rendererModes);
const ThemeEngine::GraphicsMode ThemeEngine::_defaultRendererMode =
#ifndef DISABLE_FANCY_THEMES
ThemeEngine::kGfxAntialias;
#else
ThemeEngine::kGfxStandard;
#endif
ThemeEngine::GraphicsMode ThemeEngine::findMode(const Common::String &cfg) {
for (uint i = 0; i < _rendererModesSize; ++i) {
if (cfg.equalsIgnoreCase(_rendererModes[i].cfg))
return _rendererModes[i].mode;
}
return kGfxDisabled;
}
const char *ThemeEngine::findModeConfigName(GraphicsMode mode) {
for (uint i = 0; i < _rendererModesSize; ++i) {
if (mode == _rendererModes[i].mode)
return _rendererModes[i].cfg;
}
return findModeConfigName(kGfxDisabled);
}
void ThemeEngine::setBaseResolution(int w, int h, float s) {
_baseWidth = w;
_baseHeight = h;
_scaleFactor = s;
_parser->setBaseResolution(w, h, s);
_themeEval->setScaleFactor(s);
}
/**********************************************************
* Theme setup/initialization
*********************************************************/
bool ThemeEngine::init() {
// reset everything and reload the graphics
_initOk = false;
_overlayFormat = _system->getOverlayFormat();
setGraphicsMode(_graphicsMode);
if (_screen.getPixels() && _backBuffer.getPixels()) {
_initOk = true;
}
// TODO: Instead of hard coding the font here, it should be possible
// to specify the fonts to be used for each resolution in the theme XML.
if (_screen.w >= 400 && _screen.h >= 300) {
_font = FontMan.getFontByUsage(Graphics::FontManager::kBigGUIFont);
} else {
_font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
}
// Try to create a Common::Archive with the files of the theme.
if (!_themeArchive && !_themeFile.empty()) {
Common::FSNode node(_themeFile);
if (node.isDirectory()) {
_themeArchive = new Common::FSDirectory(node);
} else if (_themeFile.matchString("*.zip", true)) {
// TODO: Also use "node" directly?
// Look for the zip file via SearchMan
Common::ArchiveMemberPtr member = SearchMan.getMember(_themeFile);
if (member) {
_themeArchive = Common::makeZipArchive(member->createReadStream());
if (!_themeArchive) {
warning("Failed to open Zip archive '%s'.", member->getName().c_str());
}
} else {
_themeArchive = Common::makeZipArchive(node);
if (!_themeArchive) {
warning("Failed to open Zip archive '%s'.", node.getPath().c_str());
}
}
}
if (_themeArchive)
_themeFiles.add("theme_archive", _themeArchive, 1, true);
}
// Load the theme
// We pass the theme file here by default, so the user will
// have a descriptive error message. The only exception will
// be the builtin theme which has no filename.
loadTheme(_themeFile.empty() ? _themeId : _themeFile);
return ready();
}
void ThemeEngine::clearAll() {
if (_initOk) {
_system->clearOverlay();
_system->grabOverlay(*_backBuffer.surfacePtr());
}
}
void ThemeEngine::refresh() {
// Flush all bitmaps if the overlay pixel format changed.
if (_overlayFormat != _system->getOverlayFormat()) {
for (ImagesMap::iterator i = _bitmaps.begin(); i != _bitmaps.end(); ++i) {
Graphics::ManagedSurface *surf = i->_value;
if (surf) {
surf->free();
delete surf;
}
}
_bitmaps.clear();
}
init();
if (_enabled) {
_system->showOverlay();
if (_useCursor) {
#ifndef USE_RGB_COLOR
CursorMan.replaceCursorPalette(_cursorPal, 0, _cursorPalSize);
#endif
CursorMan.replaceCursor(_cursor, _cursorWidth, _cursorHeight, _cursorHotspotX, _cursorHotspotY, _cursorTransparent, true, &_cursorFormat);
}
}
}
void ThemeEngine::enable() {
if (_enabled)
return;
showCursor();
_system->showOverlay();
clearAll();
_enabled = true;
}
void ThemeEngine::disable() {
if (!_enabled)
return;
_system->hideOverlay();
hideCursor();
_enabled = false;
}
void ThemeEngine::setGraphicsMode(GraphicsMode mode) {
switch (mode) {
case kGfxStandard:
#ifndef DISABLE_FANCY_THEMES
case kGfxAntialias:
#endif
if (g_system->getOverlayFormat().bytesPerPixel == 4) {
_bytesPerPixel = sizeof(uint32);
break;
} else if (g_system->getOverlayFormat().bytesPerPixel == 2) {
_bytesPerPixel = sizeof(uint16);
break;
} else if (g_system->getOverlayFormat().bytesPerPixel == 1) {
_bytesPerPixel = sizeof(uint8);
break;
}
// fall through
default:
error("Invalid graphics mode");
}
uint32 width = _system->getOverlayWidth();
uint32 height = _system->getOverlayHeight();
_backBuffer.free();
_backBuffer.create(width, height, _overlayFormat);
_screen.free();
_screen.create(width, height, _overlayFormat);
delete _vectorRenderer;
_vectorRenderer = Graphics::createRenderer(mode);
_vectorRenderer->setSurface(&_screen);
// Since we reinitialized our screen surfaces we know nothing has been
// drawn so far. Sometimes we still end up with dirty screen bits in the
// list. Clearing it avoids invalid overlay writes when the backend
// resizes the overlay.
_dirtyScreen.clear();
}
void WidgetDrawData::calcBackgroundOffset() {
uint maxShadow = 0, maxBevel = 0;
for (Common::List<Graphics::DrawStep>::const_iterator step = _steps.begin();
step != _steps.end(); ++step) {
if ((step->autoWidth || step->autoHeight) && step->shadow > maxShadow)
maxShadow = step->shadow;
if (step->drawingCall == &Graphics::VectorRenderer::drawCallback_BEVELSQ && step->bevel > maxBevel)
maxBevel = step->bevel;
}
_backgroundOffset = maxBevel;
_shadowOffset = maxShadow;
}
void ThemeEngine::restoreBackground(Common::Rect r) {
if (_vectorRenderer->getActiveSurface() == &_backBuffer) {
// Only restore the background when drawing to the screen surface
return;
}
r.clip(_screen.w, _screen.h);
_vectorRenderer->blitSurface(&_backBuffer, r);
addDirtyRect(r);
}
/**********************************************************
* Theme elements management
*********************************************************/
void ThemeEngine::addDrawStep(const Common::String &drawDataId, const Graphics::DrawStep &step) {
DrawData id = parseDrawDataId(drawDataId);
assert(id != kDDNone && _widgets[id] != nullptr);
_widgets[id]->_steps.push_back(step);
}
bool ThemeEngine::addTextData(const Common::String &drawDataId, TextData textId, TextColor colorId, Graphics::TextAlign alignH, TextAlignVertical alignV) {
DrawData id = parseDrawDataId(drawDataId);
if (id == -1 || textId == -1 || colorId == kTextColorMAX || !_widgets[id])
return false;
_widgets[id]->_textDataId = textId;
_widgets[id]->_textColorId = colorId;
_widgets[id]->_textAlignH = alignH;
_widgets[id]->_textAlignV = alignV;
return true;
}
bool ThemeEngine::addFont(TextData textId, const Common::String &language, const Common::String &file, const Common::String &scalableFile, const int pointsize) {
if (textId == -1)
return false;
if (!language.empty()) {
#ifdef USE_TRANSLATION
Common::String cl = TransMan.getCurrentLanguage();
#else
Common::String cl("en");
#endif
if (!cl.matchString(language, true))
return true; // Skip
if (_texts[textId] != nullptr) // We already loaded something
return true;
}
if (_texts[textId] != nullptr)
delete _texts[textId];
_texts[textId] = new TextDrawData;
if (file == "default") {
_texts[textId]->_fontPtr = _font;
} else {
_texts[textId]->_fontPtr = loadFont(file, scalableFile, pointsize, textId == kTextDataDefault);
if (!_texts[textId]->_fontPtr) {
warning("Couldn't load font '%s'/'%s'", file.c_str(), scalableFile.empty()? "(none)" : scalableFile.c_str());
#ifdef USE_TRANSLATION
TransMan.setLanguage("en");
Common::TextToSpeechManager *ttsMan;
if ((ttsMan = g_system->getTextToSpeechManager()) != nullptr)
ttsMan->setLanguage("en");
#endif // USE_TRANSLATION
// No font, cleanup TextDrawData
delete _texts[textId];
_texts[textId] = nullptr;
return false; // fall-back attempt failed
}
}
return true;
}
// This should work independently of the translation manager, since it is is just about displaying
// correct ingame messages in Chinese, Korean and Japanese games, even if the launcher is set to
// English and/or the translations are disabled.
Common::Array<Common::Language> getLangIdentifiers(const Common::String &language) {
struct IdStr2Lang {
const char strId[6];
Common::Language lang;
};
// I have added only the languages that currently make sense (the only other extra font that we
// currently have is for Hindi, but that language is only supported in the launcher, there are
// no games in Hindi).
IdStr2Lang matchPairs[] = {
// { "hi", Common::UNK_LANG },
{ "ja", Common::JA_JPN },
{ "ko", Common::KO_KOR },
{ "zh", Common::ZH_ANY },
{ "zh", Common::ZH_CHN },
{ "zh", Common::ZH_TWN }
};
Common::Array<Common::Language> result;
for (int i = 0; i < ARRAYSIZE(matchPairs); ++i) {
if (language.contains(matchPairs[i].strId))
result.push_back(matchPairs[i].lang);
}
return result;
}
void ThemeEngine::storeFontNames(TextData textId, const Common::String &language, const Common::String &file, const Common::String &scalableFile, const int pointsize) {
if (language.empty())
return;
Common::Array<Common::Language> langs = getLangIdentifiers(language);
if (langs.empty())
return;
Common::Array<LangExtraFont>::iterator entry = Common::find(_langExtraFonts.begin(), _langExtraFonts.end(), langs[0]);
if (entry == _langExtraFonts.end())
_langExtraFonts.push_back(LangExtraFont(textId, langs, file, scalableFile, pointsize));
else
entry->storeFileNames(textId, file, scalableFile, pointsize);
}
bool ThemeEngine::loadExtraFont(FontStyle style, Common::Language lang) {
if (style >= kFontStyleMax)
return false;
Common::Array<LangExtraFont>::iterator entry = Common::find(_langExtraFonts.begin(), _langExtraFonts.end(), lang);
if (entry == _langExtraFonts.end())
return false;
TextData td = fontStyleToData(style);
return addFont(kTextDataExtraLang, Common::String(), entry->file(td), entry->sclFile(td), entry->fntSize(td));
}
bool ThemeEngine::addTextColor(TextColor colorId, int r, int g, int b) {
if (colorId >= kTextColorMAX)
return false;
if (_textColors[colorId] != nullptr)
delete _textColors[colorId];
_textColors[colorId] = new TextColorData;
_textColors[colorId]->r = r;
_textColors[colorId]->g = g;
_textColors[colorId]->b = b;
return true;
}
bool ThemeEngine::addBitmap(const Common::String &filename, const Common::String &scalablefile, int width, int height) {
// Nothing has to be done if the bitmap already has been loaded.
Graphics::ManagedSurface *surf = _bitmaps[filename];
if (surf) {
surf->free();
delete surf;
surf = nullptr;
_bitmaps.erase(filename);
}
if (!scalablefile.empty()) {
Graphics::SVGBitmap *image = nullptr;
Common::ArchiveMemberList members;
_themeFiles.listMatchingMembers(members, scalablefile);
for (Common::ArchiveMemberList::const_iterator i = members.begin(), end = members.end(); i != end; ++i) {
Common::SeekableReadStream *stream = (*i)->createReadStream();
if (stream) {
image = new Graphics::SVGBitmap(stream);
delete stream;
break;
}
}
if (image) {
_bitmaps[filename] = new Graphics::ManagedSurface(width * _scaleFactor, height * _scaleFactor, *image->getPixelFormat());
image->render(*_bitmaps[filename], width * _scaleFactor, height * _scaleFactor);
delete image;
} else {
return false;
}
return true;
}
const Graphics::Surface *srcSurface = nullptr;
if (filename.hasSuffix(".png")) {
// Maybe it is PNG?
#ifdef USE_PNG
Image::PNGDecoder decoder;
Common::ArchiveMemberList members;
_themeFiles.listMatchingMembers(members, filename);
for (Common::ArchiveMemberList::const_iterator i = members.begin(), end = members.end(); i != end; ++i) {
Common::SeekableReadStream *stream = (*i)->createReadStream();
if (stream) {
if (!decoder.loadStream(*stream))
error("Error decoding PNG");
srcSurface = decoder.getSurface();
delete stream;
if (srcSurface)
break;
}
}
if (srcSurface && srcSurface->format.bytesPerPixel != 1)
surf = new Graphics::ManagedSurface(srcSurface->convertTo(_overlayFormat));
#else
error("No PNG support compiled in");
#endif
} else {
// If not, try to load the bitmap via the BitmapDecoder class.
Image::BitmapDecoder bitmapDecoder;
Common::ArchiveMemberList members;
_themeFiles.listMatchingMembers(members, filename);
for (Common::ArchiveMemberList::const_iterator i = members.begin(), end = members.end(); i != end; ++i) {
Common::SeekableReadStream *stream = (*i)->createReadStream();
if (stream) {
bitmapDecoder.loadStream(*stream);
srcSurface = bitmapDecoder.getSurface();
delete stream;
if (srcSurface)
break;
}
}
if (srcSurface && srcSurface->format.bytesPerPixel != 1)
surf = new Graphics::ManagedSurface(srcSurface->convertTo(_overlayFormat));
}
if (_scaleFactor != 1.0 && surf) {
Graphics::Surface *tmp2 = surf->rawSurface().scale(surf->w * _scaleFactor, surf->h * _scaleFactor, false);
surf->free();
delete surf;
surf = new Graphics::ManagedSurface(tmp2);
}
// Store the surface into our hashmap (attention, may store NULL entries!)
_bitmaps[filename] = surf;
return surf != nullptr;
}
bool ThemeEngine::addDrawData(const Common::String &data, bool cached) {
DrawData id = parseDrawDataId(data);
if (id == -1)
return false;
if (_widgets[id] != nullptr)
delete _widgets[id];
_widgets[id] = new WidgetDrawData;
_widgets[id]->_layer = kDrawDataDefaults[id].layer;
_widgets[id]->_textDataId = kTextDataNone;
return true;
}
/**********************************************************
* Theme XML loading
*********************************************************/
void ThemeEngine::loadTheme(const Common::String &themeId) {
unloadTheme();
debug(6, "Loading theme %s", themeId.c_str());
if (themeId == "builtin") {
_themeOk = loadDefaultXML();
} else {
// Load the archive containing image and XML data
_themeOk = loadThemeXML(themeId);
}
if (!_themeOk) {
warning("Failed to load theme '%s'", themeId.c_str());
return;
}
for (int i = 0; i < kDrawDataMAX; ++i) {
if (_widgets[i] == nullptr) {
warning("Missing data asset: '%s' in theme '%s", kDrawDataDefaults[i].name, themeId.c_str());
} else {
_widgets[i]->calcBackgroundOffset();
}
}
}
void ThemeEngine::unloadTheme() {
if (!_themeOk)
return;
for (int i = 0; i < kDrawDataMAX; ++i) {
delete _widgets[i];
_widgets[i] = nullptr;
}
for (int i = 0; i < kTextDataMAX; ++i) {
// Don't unload the language specific extra font here or it will be lost after a refresh() call.
if (i == kTextDataExtraLang)
continue;
delete _texts[i];
_texts[i] = nullptr;
}
for (int i = 0; i < kTextColorMAX; ++i) {
delete _textColors[i];
_textColors[i] = nullptr;
}
_themeEval->reset();
_themeOk = false;
}
void ThemeEngine::unloadExtraFont() {
delete _texts[kTextDataExtraLang];
_texts[kTextDataExtraLang] = nullptr;
}
bool ThemeEngine::loadDefaultXML() {
// The default XML theme is included on runtime from a pregenerated
// file inside the themes directory.
// Use the Python script "makedeftheme.py" to convert a normal XML theme
// into the "default.inc" file, which is ready to be included in the code.
#ifndef DISABLE_GUI_BUILTIN_THEME
#include "themes/default.inc"
int xmllen = 0;
for (int i = 0; i < ARRAYSIZE(defaultXML); i++)
xmllen += strlen(defaultXML[i]);
byte *tmpXML = (byte *)malloc(xmllen + 1);
tmpXML[0] = '\0';
for (int i = 0; i < ARRAYSIZE(defaultXML); i++)
strncat((char *)tmpXML, defaultXML[i], xmllen);
if (!_parser->loadBuffer(tmpXML, xmllen)) {
free(tmpXML);
return false;
}
_themeName = "ScummVM Classic Theme (Builtin Version)";
_themeId = "builtin";
_themeFile.clear();
bool result = _parser->parse();
_parser->close();
free(tmpXML);
return result;
#else
warning("The built-in theme is not enabled in the current build. Please load an external theme");
return false;
#endif
}
bool ThemeEngine::loadThemeXML(const Common::String &themeId) {
assert(_parser);
assert(_themeArchive);
_themeName.clear();
//
// Now that we have a Common::Archive, verify that it contains a valid THEMERC File
//
Common::File themercFile;
themercFile.open("THEMERC", *_themeArchive);
if (!themercFile.isOpen()) {
warning("Theme '%s' contains no 'THEMERC' file.", themeId.c_str());
return false;
}
Common::String stxHeader = themercFile.readLine();
if (!themeConfigParseHeader(stxHeader, _themeName) || _themeName.empty()) {
warning("Corrupted 'THEMERC' file in theme '%s'", themeId.c_str());
return false;
}
Common::ArchiveMemberList members;
if (0 == _themeArchive->listMatchingMembers(members, "*.stx")) {
warning("Found no STX files for theme '%s'.", themeId.c_str());
return false;
}
//
// Loop over all STX files, load and parse them
//
for (Common::ArchiveMemberList::iterator i = members.begin(); i != members.end(); ++i) {
assert((*i)->getName().hasSuffix(".stx"));
if (_parser->loadStream((*i)->createReadStream()) == false) {
warning("Failed to load STX file '%s'", (*i)->getName().c_str());
_parser->close();
return false;
}
if (_parser->parse() == false) {
warning("Failed to parse STX file '%s'", (*i)->getName().c_str());
_parser->close();
return false;
}
_parser->close();
}
assert(!_themeName.empty());
return true;
}
/**********************************************************
* Draw Date descriptors drawing functions
*********************************************************/
void ThemeEngine::drawDD(DrawData type, const Common::Rect &r, uint32 dynamic, bool forceRestore) {
WidgetDrawData *drawData = _widgets[type];
if (!drawData)
return;
if (kDrawDataDefaults[type].parent != kDDNone && kDrawDataDefaults[type].parent != type)
drawDD(kDrawDataDefaults[type].parent, r, dynamic);
Common::Rect area = r;
area.clip(_screen.w, _screen.h);
Common::Rect extendedRect = area;
extendedRect.grow(kDirtyRectangleThreshold + drawData->_backgroundOffset);
if (drawData->_shadowOffset > drawData->_backgroundOffset) {
extendedRect.right += drawData->_shadowOffset - drawData->_backgroundOffset;
extendedRect.bottom += drawData->_shadowOffset - drawData->_backgroundOffset;
}
if (!_clip.isEmpty()) {
extendedRect.clip(_clip);
}
// Cull the elements not in the clip rect
if (extendedRect.isEmpty()) {
return;
}
if (forceRestore || drawData->_layer == kDrawLayerBackground)
restoreBackground(extendedRect);
if (drawData->_layer == _layerToDraw) {
Common::List<Graphics::DrawStep>::const_iterator step;
for (step = drawData->_steps.begin(); step != drawData->_steps.end(); ++step) {
_vectorRenderer->drawStep(area, _clip, *step, dynamic);
}
addDirtyRect(extendedRect);
}
}
void ThemeEngine::drawDDText(TextData type, TextColor color, const Common::Rect &r, const Common::U32String &text,
bool restoreBg, bool ellipsis, Graphics::TextAlign alignH, TextAlignVertical alignV,
int deltax, const Common::Rect &drawableTextArea) {
if (type == kTextDataNone || !_texts[type] || _layerToDraw == kDrawLayerBackground)
return;
Common::Rect area = r;
area.clip(_screen.w, _screen.h);
Common::Rect dirty = drawableTextArea;
if (dirty.isEmpty()) dirty = area;
else dirty.clip(area);
if (!_clip.isEmpty()) {
dirty.clip(_clip);
}
// HACK: One small pixel should be invisible enough
if (dirty.isEmpty()) dirty = Common::Rect(0, 0, 1, 1);
if (restoreBg)
restoreBackground(dirty);
_vectorRenderer->setFgColor(_textColors[color]->r, _textColors[color]->g, _textColors[color]->b);
#ifdef USE_FRIBIDI
_vectorRenderer->drawString(_texts[type]->_fontPtr, Common::convertBiDiU32String(text), area, alignH, alignV, deltax, ellipsis, dirty);
#else
_vectorRenderer->drawString(_texts[type]->_fontPtr, text, area, alignH, alignV, deltax, ellipsis, dirty);
#endif
addDirtyRect(dirty);
}
/**********************************************************
* Widget drawing functions
*********************************************************/
void ThemeEngine::drawButton(const Common::Rect &r, const Common::U32String &str, WidgetStateInfo state, uint16 hints) {
if (!ready())
return;
DrawData dd = kDDButtonIdle;
if (state == kStateEnabled)
dd = kDDButtonIdle;
else if (state == kStateHighlight)
dd = kDDButtonHover;
else if (state == kStateDisabled)
dd = kDDButtonDisabled;
else if (state == kStatePressed)
dd = kDDButtonPressed;
drawDD(dd, r, 0, hints & WIDGET_CLEARBG);
drawDDText(getTextData(dd), getTextColor(dd), r, str, false, true, convertTextAlignH(_widgets[dd]->_textAlignH, false),
_widgets[dd]->_textAlignV);
}
void ThemeEngine::drawDropDownButton(const Common::Rect &r, uint32 dropdownWidth, const Common::U32String &str,
ThemeEngine::WidgetStateInfo buttonState, bool inButton, bool inDropdown, bool rtl) {
if (!ready())
return;
DrawData dd;
if (buttonState == kStateHighlight && inButton)
dd = rtl ? kDDDropDownButtonHoverLeftRTL : kDDDropDownButtonHoverLeft;
else if (buttonState == kStateHighlight && inDropdown)
dd = rtl ? kDDDropDownButtonHoverRightRTL : kDDDropDownButtonHoverRight;
else if (buttonState == kStateDisabled)
dd = rtl ? kDDDropDownButtonDisabledRTL : kDDDropDownButtonDisabled;
else if (buttonState == kStatePressed && inButton)
dd = rtl ? kDDDropDownButtonPressedLeftRTL : kDDDropDownButtonPressedLeft;
else if (buttonState == kStatePressed && inDropdown)
dd = rtl ? kDDDropDownButtonPressedRightRTL : kDDDropDownButtonPressedRight;
else
dd = rtl ? kDDDropDownButtonIdleRTL : kDDDropDownButtonIdle;
drawDD(dd, r);
// Center the text in the button without using the area of the drop down button
Common::Rect textRect = r;
textRect.left = r.left + dropdownWidth;
textRect.right = r.right - dropdownWidth;
// Don't draw text if we don't have enough room for it
if (!textRect.isValidRect()) {
return;
}
drawDDText(getTextData(dd), getTextColor(dd), textRect, str, false, true, convertTextAlignH(_widgets[dd]->_textAlignH, rtl),
_widgets[dd]->_textAlignV);
}
void ThemeEngine::drawLineSeparator(const Common::Rect &r) {
if (!ready())
return;
drawDD(kDDSeparator, r);
}
void ThemeEngine::drawCheckbox(const Common::Rect &r, int spacing, const Common::U32String &str, bool checked,
WidgetStateInfo state, bool overrideText, bool rtl) {
if (!ready())
return;
Common::Rect r2 = r;
DrawData dd = kDDCheckboxDefault;
if (checked)
dd = kDDCheckboxSelected;
if (state == kStateDisabled)
dd = checked ? kDDCheckboxDisabledSelected : kDDCheckboxDisabled;
const int checkBoxSize = MIN((int)r.height(), getFontHeight());
r2.left = rtl ? r.right - checkBoxSize : r2.left;
r2.bottom = r2.top + checkBoxSize;
r2.right = r2.left + checkBoxSize;
drawDD(dd, r2);
if (rtl) {
r2.right -= checkBoxSize + spacing;
r2.left = r.left;
} else {
r2.left += checkBoxSize + spacing;
r2.right = r.right;
}
if (r2.right > r2.left) {
TextColor color = overrideText ? GUI::TextColor::kTextColorOverride : getTextColor(dd);
drawDDText(getTextData(dd), color, r2, str, true, false, convertTextAlignH(_widgets[dd]->_textAlignH, rtl),
_widgets[dd]->_textAlignV);
}
}
void ThemeEngine::drawRadiobutton(const Common::Rect &r, int spacing, const Common::U32String &str, bool checked, WidgetStateInfo state, bool rtl) {
if (!ready())
return;
Common::Rect r2 = r;
DrawData dd = kDDRadiobuttonDefault;
if (checked)
dd = kDDRadiobuttonSelected;
if (state == kStateDisabled)
dd = kDDRadiobuttonDisabled;
const int checkBoxSize = MIN((int)r.height(), getFontHeight());
r2.left = rtl ? r2.right - checkBoxSize : r2.left;
r2.bottom = r2.top + checkBoxSize;
r2.right = r2.left + checkBoxSize;
drawDD(dd, r2);
if (rtl) {
r2.right -= checkBoxSize + spacing;
r2.left = r.left;
} else {
r2.left += checkBoxSize + spacing;
r2.right = MAX(r2.left, r.right);
}
drawDDText(getTextData(dd), getTextColor(dd), r2, str, true, false, convertTextAlignH(_widgets[dd]->_textAlignH, rtl),
_widgets[dd]->_textAlignV);
}
void ThemeEngine::drawSlider(const Common::Rect &r, int width, WidgetStateInfo state, bool rtl) {
if (!ready())
return;
DrawData dd = kDDSliderFull;
if (state == kStateHighlight)
dd = kDDSliderHover;
else if (state == kStateDisabled)
dd = kDDSliderDisabled;
Common::Rect r2 = r;
r2.setWidth(MIN((int16)width, r.width()));
// r2.top++; r2.bottom--; r2.left++; r2.right--;
if (rtl) {
r2.left = r.right - r2.width();
r2.right = r.right;
}
drawWidgetBackground(r, kWidgetBackgroundSlider);
drawDD(dd, r2);
}
void ThemeEngine::drawScrollbar(const Common::Rect &r, int sliderY, int sliderHeight, ScrollbarState scrollState) {
if (!ready())
return;
drawDD(kDDScrollbarBase, r);
Common::Rect r2 = r;
const int buttonExtra = r.width() + 1; // scrollbar.cpp's UP_DOWN_BOX_HEIGHT
r2.bottom = r2.top + buttonExtra;
drawDD(scrollState == kScrollbarStateUp ? kDDScrollbarButtonHover : kDDScrollbarButtonIdle, r2,
Graphics::VectorRenderer::kTriangleUp);
r2.translate(0, r.height() - r2.height());
drawDD(scrollState == kScrollbarStateDown ? kDDScrollbarButtonHover : kDDScrollbarButtonIdle, r2,
Graphics::VectorRenderer::kTriangleDown);
r2 = r;
r2.left += 1;
r2.right -= 1;
r2.top += sliderY;
r2.bottom = r2.top + sliderHeight;
drawDD(scrollState == kScrollbarStateSlider ? kDDScrollbarHandleHover : kDDScrollbarHandleIdle, r2);
}
void ThemeEngine::drawDialogBackground(const Common::Rect &r, DialogBackground bgtype) {
if (!ready())
return;
switch (bgtype) {
case kDialogBackgroundMain:
drawDD(kDDMainDialogBackground, r);
break;
case kDialogBackgroundSpecial:
drawDD(kDDSpecialColorBackground, r);
break;
case kDialogBackgroundPlain:
drawDD(kDDPlainColorBackground, r);
break;
case kDialogBackgroundTooltip:
drawDD(kDDTooltipBackground, r);
break;
case kDialogBackgroundDefault:
drawDD(kDDDefaultBackground, r);
break;
default:
// fallthrough intended
case kDialogBackgroundNone:
// no op
break;
}
}
void ThemeEngine::drawCaret(const Common::Rect &r, bool erase) {
if (!ready())
return;
if (erase) {
restoreBackground(r);
} else
drawDD(kDDCaret, r);
}
void ThemeEngine::drawPopUpWidget(const Common::Rect &r, const Common::U32String &sel, int deltax, WidgetStateInfo state, bool rtl) {
if (!ready())
return;
DrawData dd = rtl ? kDDPopUpIdleRTL : kDDPopUpIdle;
if (state == kStateEnabled)
dd = rtl ? kDDPopUpIdleRTL : kDDPopUpIdle;
else if (state == kStateHighlight)
dd = rtl ? kDDPopUpHoverRTL : kDDPopUpHover;
else if (state == kStateDisabled)
dd = rtl ? kDDPopUpDisabledRTL : kDDPopUpDisabled;
drawDD(dd, r);
if (!sel.empty() && r.width() >= 13 && r.height() >= 1) {
Common::Rect text(r.left + 3, r.top + 1, r.right - 10, r.bottom);
drawDDText(getTextData(dd), getTextColor(dd), text, sel, true, false, convertTextAlignH(_widgets[dd]->_textAlignH, rtl),
_widgets[dd]->_textAlignV, deltax);
}
}
void ThemeEngine::drawSurface(const Common::Point &p, const Graphics::ManagedSurface &surface, bool themeTrans) {
if (!ready())
return;
if (_layerToDraw == kDrawLayerBackground)
return;
_vectorRenderer->setClippingRect(_clip);
_vectorRenderer->blitKeyBitmap(&surface, p, themeTrans);
Common::Rect dirtyRect = Common::Rect(p.x, p.y, p.x + surface.w, p.y + surface.h);
dirtyRect.clip(_clip);
addDirtyRect(dirtyRect);
}
void ThemeEngine::drawWidgetBackground(const Common::Rect &r, WidgetBackground background) {
if (!ready())
return;
switch (background) {
case kWidgetBackgroundNo:
break;
case kWidgetBackgroundBorderSmall:
drawDD(kDDWidgetBackgroundSmall, r);
break;
case kWidgetBackgroundEditText:
drawDD(kDDWidgetBackgroundEditText, r);
break;
case kWidgetBackgroundSlider:
drawDD(kDDWidgetBackgroundSlider, r);
break;
case kThumbnailBackground:
drawDD(kDDThumbnailBackground, r);
break;
case kGridItemBackground:
drawDD(kDDGridItemIdle, r);
break;
case kGridItemHighlight:
drawDD(kDDGridItemHover, r);
break;
default:
drawDD(kDDWidgetBackgroundDefault, r);
break;
}
}
void ThemeEngine::drawTab(const Common::Rect &r, int tabHeight, const Common::Array<int> &tabWidths,
const Common::Array<Common::U32String> &tabs, int active, bool rtl) {
if (!ready())
return;
assert(tabs.size() == tabWidths.size());
drawDD(kDDTabBackground, Common::Rect(r.left, r.top, r.right, r.top + tabHeight));
const int numTabs = (int)tabs.size();
int width = 0;
if (rtl) {
for (int i = 0; i < numTabs; i++) {
width += tabWidths[i];
}
width = r.width() - width;
}
int activePos = -1;
for (int i = 0; i < numTabs; i++) {
int current = rtl ? (numTabs - i - 1) : i;
if (r.left + width > r.right || r.left + width + tabWidths[current] > r.right) {
width += tabWidths[current];
continue;
}
if (current == active) {
activePos = width;
width += tabWidths[current];
continue;
}
Common::Rect tabRect(r.left + width, r.top, r.left + width + tabWidths[current], r.top + tabHeight);
drawDD(kDDTabInactive, tabRect);
drawDDText(getTextData(kDDTabInactive), getTextColor(kDDTabInactive), tabRect, tabs[current], false, false,
convertTextAlignH(_widgets[kDDTabInactive]->_textAlignH, rtl), _widgets[kDDTabInactive]->_textAlignV);
width += tabWidths[current];
}
if (activePos >= 0) {
Common::Rect tabRect(r.left + activePos, r.top, r.left + activePos + tabWidths[active], r.top + tabHeight);
const uint16 tabLeft = activePos;
const uint16 tabRight = MAX(r.right - tabRect.right, 0);
drawDD(kDDTabActive, tabRect, (tabLeft << 16) | (tabRight & 0xFFFF));
drawDDText(getTextData(kDDTabActive), getTextColor(kDDTabActive), tabRect, tabs[active], false, false,
convertTextAlignH(_widgets[kDDTabActive]->_textAlignH, rtl), _widgets[kDDTabActive]->_textAlignV);
}
}
void ThemeEngine::drawText(const Common::Rect &r, const Common::U32String &str, WidgetStateInfo state,
Graphics::TextAlign align, TextInversionState inverted, int deltax, bool useEllipsis,
FontStyle font, FontColor color, bool restore, const Common::Rect &drawableTextArea) {
if (!ready())
return;
TextColor colorId = kTextColorMAX;
switch (color) {
case kFontColorNormal:
if (inverted) {
colorId = kTextColorNormalInverted;
break;
}
switch (state) {
case kStateDisabled:
colorId = kTextColorNormalDisabled;
break;
case kStateHighlight:
colorId = kTextColorNormalHover;
break;
default:
// fallthrough intended
case kStateEnabled:
case kStatePressed:
colorId = kTextColorNormal;
break;
}
break;
case kFontColorAlternate:
if (inverted) {
colorId = kTextColorAlternativeInverted;
break;
}
switch (state) {
case kStateDisabled:
colorId = kTextColorAlternativeDisabled;
break;
case kStateHighlight:
colorId = kTextColorAlternativeHover;
break;
default:
// fallthrough intended
case kStateEnabled:
case kStatePressed:
colorId = kTextColorAlternative;
break;
}
break;
case kFontColorOverride:
if (inverted) {
colorId = kTextColorOverrideInverted;
break;
}
switch (state) {
case kStateDisabled:
colorId = kTextColorAlternativeDisabled;
break;
case kStateHighlight:
colorId = kTextColorOverrideHover;
break;
default:
// fallthrough intended
case kStateEnabled:
case kStatePressed:
colorId = kTextColorOverride;
break;
}
break;
default:
return;
}
TextData textId = fontStyleToData(font);
switch (inverted) {
case kTextInversion:
drawDD(kDDTextSelectionBackground, r);
restore = false;
break;
case kTextInversionFocus:
drawDD(kDDTextSelectionFocusBackground, r);
restore = false;
break;
default:
break;
}
drawDDText(textId, colorId, r, str, restore, useEllipsis, align, kTextAlignVCenter, deltax, drawableTextArea);
}
void ThemeEngine::drawChar(const Common::Rect &r, byte ch, const Graphics::Font *font, FontColor color) {
if (!ready())
return;
Common::Rect charArea = r;
charArea.clip(_screen.w, _screen.h);
uint32 rgbColor = _overlayFormat.RGBToColor(_textColors[color]->r, _textColors[color]->g, _textColors[color]->b);
// TODO: Handle clipping when drawing chars
restoreBackground(charArea);
font->drawChar(&_screen, ch, charArea.left, charArea.top, rgbColor);
addDirtyRect(charArea);
}
void ThemeEngine::drawFoldIndicator(const Common::Rect &r, bool expanded) {
Graphics::VectorRenderer::TriangleOrientation orient;
if (_layerToDraw == kDrawLayerBackground)
return;
if (expanded)
orient = Graphics::VectorRenderer::kTriangleDown;
else
orient = Graphics::VectorRenderer::kTriangleRight;
_vectorRenderer->setFillMode(Graphics::VectorRenderer::kFillForeground);
_vectorRenderer->setFgColor(_textColors[kTextColorNormal]->r, _textColors[kTextColorNormal]->g, _textColors[kTextColorNormal]->b);
_vectorRenderer->drawTriangle(r.left + r.width() / 4, r.top + r.height() / 4, r.width() / 2, r.height() / 2, orient);
addDirtyRect(r);
}
void ThemeEngine::debugWidgetPosition(const char *name, const Common::Rect &r) {
_font->drawString(&_screen, name, r.left, r.top, r.width(), 0xFFFF, Graphics::kTextAlignRight, 0, true);
_screen.hLine(r.left, r.top, r.right, 0xFFFF);
_screen.hLine(r.left, r.bottom, r.right, 0xFFFF);
_screen.vLine(r.left, r.top, r.bottom, 0xFFFF);
_screen.vLine(r.right, r.top, r.bottom, 0xFFFF);
}
/**********************************************************
* Screen/overlay management
*********************************************************/
void ThemeEngine::copyBackBufferToScreen() {
memcpy(_screen.getPixels(), _backBuffer.getPixels(), (size_t)(_screen.pitch * _screen.h));
}
void ThemeEngine::updateScreen() {
#ifdef LAYOUT_DEBUG_DIALOG
_vectorRenderer->fillSurface();
_themeEval->debugDraw(&_screen, _font);
_vectorRenderer->copyWholeFrame(_system);
#else
updateDirtyScreen();
#endif
}
void ThemeEngine::addDirtyRect(Common::Rect r) {
// Clip the rect to screen coords
r.clip(_screen.w, _screen.h);
// If it is empty after clipping, we are done
if (r.isEmpty())
return;
// Check if the new rectangle is contained within another in the list
Common::List<Common::Rect>::iterator it;
for (it = _dirtyScreen.begin(); it != _dirtyScreen.end();) {
// If we find a rectangle which fully contains the new one,
// we can abort the search.
if (it->contains(r))
return;
// Conversely, if we find rectangles which are contained in
// the new one, we can remove them
if (r.contains(*it))
it = _dirtyScreen.erase(it);
else
++it;
}
// If we got here, we can safely add r to the list of dirty rects.
_dirtyScreen.push_back(r);
}
void ThemeEngine::updateDirtyScreen() {
if (_dirtyScreen.empty())
return;
Common::List<Common::Rect>::iterator i;
for (i = _dirtyScreen.begin(); i != _dirtyScreen.end(); ++i) {
_vectorRenderer->copyFrame(_system, *i);
}
_dirtyScreen.clear();
}
void ThemeEngine::applyScreenShading(ShadingStyle style) {
if (style != kShadingNone) {
_vectorRenderer->applyScreenShading(style);
addDirtyRect(Common::Rect(0, 0, _screen.w, _screen.h));
}
}
bool ThemeEngine::createCursor(const Common::String &filename, int hotspotX, int hotspotY) {
// Try to locate the specified file among all loaded bitmaps
const Graphics::ManagedSurface *cursor = _bitmaps[filename];
if (!cursor)
return false;
// Set up the cursor parameters
_cursorHotspotX = hotspotX;
_cursorHotspotY = hotspotY;
_cursorWidth = cursor->w;
_cursorHeight = cursor->h;
#ifdef USE_RGB_COLOR
_cursorFormat = cursor->format;
_cursorTransparent = _cursorFormat.RGBToColor(0xFF, 0, 0xFF);
// Allocate a new buffer for the cursor
delete[] _cursor;
_cursor = new byte[_cursorWidth * _cursorHeight * _cursorFormat.bytesPerPixel];
assert(_cursor);
Graphics::copyBlit(_cursor, (const byte *)cursor->getPixels(),
_cursorWidth * _cursorFormat.bytesPerPixel, cursor->pitch,
_cursorWidth, _cursorHeight, _cursorFormat.bytesPerPixel);
_useCursor = true;
#else
if (!_system->hasFeature(OSystem::kFeatureCursorPalette))
return true;
// Allocate a new buffer for the cursor
delete[] _cursor;
_cursor = new byte[_cursorWidth * _cursorHeight];
assert(_cursor);
memset(_cursor, 0xFF, sizeof(byte) * _cursorWidth * _cursorHeight);
// the transparent color is 0xFF00FF
const uint32 colTransparent = cursor->format.RGBToColor(0xFF, 0, 0xFF);
const uint32 alphaMask = cursor->format.ARGBToColor(0x80, 0, 0, 0);
// Now, scan the bitmap. We have to convert it from 16 bit color mode
// to 8 bit mode, and have to create a suitable palette on the fly.
uint colorsFound = 0;
Common::HashMap<int, int> colorToIndex;
const byte *src = (const byte *)cursor->getPixels();
for (uint y = 0; y < _cursorHeight; ++y) {
for (uint x = 0; x < _cursorWidth; ++x) {
uint32 color = colTransparent;
byte r, g, b;
if (cursor->format.bytesPerPixel == 2) {
color = READ_UINT16(src);
} else if (cursor->format.bytesPerPixel == 4) {
color = READ_UINT32(src);
}
src += cursor->format.bytesPerPixel;
// Skip transparency
if (color == colTransparent
// Replace with transparent is alpha is present and < 50%
|| (alphaMask != 0 && (color & alphaMask) == 0))
continue;
cursor->format.colorToRGB(color, r, g, b);
const int col = (r << 16) | (g << 8) | b;
// If there is no entry yet for this color in the palette: Add one
if (!colorToIndex.contains(col)) {
if (colorsFound >= MAX_CURS_COLORS) {
warning("Cursor contains too many colors (%d, but only %d are allowed)", colorsFound, MAX_CURS_COLORS);
return false;
}
const int index = colorsFound++;
colorToIndex[col] = index;
_cursorPal[index * 3 + 0] = r;
_cursorPal[index * 3 + 1] = g;
_cursorPal[index * 3 + 2] = b;
}
// Copy pixel from the 16 bit source surface to the 8bit target surface
const int index = colorToIndex[col];
_cursor[y * _cursorWidth + x] = index;
}
}
_useCursor = true;
_cursorPalSize = colorsFound;
#endif
return true;
}
/**********************************************************
* Legacy GUI::Theme support functions
*********************************************************/
const Graphics::Font *ThemeEngine::getFont(FontStyle font) const {
return _texts[fontStyleToData(font)]->_fontPtr;
}
int ThemeEngine::getFontHeight(FontStyle font) const {
return ready() ? _texts[fontStyleToData(font)]->_fontPtr->getFontHeight() : 0;
}
int ThemeEngine::getStringWidth(const Common::U32String &str, FontStyle font) const {
return ready() ? _texts[fontStyleToData(font)]->_fontPtr->getStringWidth(str) : 0;
}
int ThemeEngine::getCharWidth(uint32 c, FontStyle font) const {
return ready() ? _texts[fontStyleToData(font)]->_fontPtr->getCharWidth(c) : 0;
}
int ThemeEngine::getKerningOffset(uint32 left, uint32 right, FontStyle font) const {
return ready() ? _texts[fontStyleToData(font)]->_fontPtr->getKerningOffset(left, right) : 0;
}
TextData ThemeEngine::getTextData(DrawData ddId) const {
return _widgets[ddId] ? (TextData)_widgets[ddId]->_textDataId : kTextDataNone;
}
TextColor ThemeEngine::getTextColor(DrawData ddId) const {
return _widgets[ddId] ? _widgets[ddId]->_textColorId : kTextColorMAX;
}
DrawData ThemeEngine::parseDrawDataId(const Common::String &name) const {
for (int i = 0; i < kDrawDataMAX; ++i)
if (name.compareToIgnoreCase(kDrawDataDefaults[i].name) == 0)
return kDrawDataDefaults[i].id;
return kDDNone;
}
/**********************************************************
* External data loading
*********************************************************/
const Graphics::Font *ThemeEngine::loadScalableFont(const Common::String &filename, const int pointsize, Common::String &name) {
#ifdef USE_FREETYPE2
name = Common::String::format("%s@%d", filename.c_str(), pointsize);
// Try already loaded fonts.
const Graphics::Font *font = FontMan.getFontByName(name);
if (font)
return font;
Common::ArchiveMemberList members;
_themeFiles.listMatchingMembers(members, filename);
for (Common::ArchiveMemberList::const_iterator i = members.begin(), end = members.end(); i != end; ++i) {
Common::SeekableReadStream *stream = (*i)->createReadStream();
if (stream) {
font = Graphics::loadTTFFont(*stream, pointsize, Graphics::kTTFSizeModeCharacter, 0, Graphics::kTTFRenderModeLight);
delete stream;
if (font)
return font;
}
}
// Try loading the font from the common fonts archive.
font = Graphics::loadTTFFontFromArchive(filename, pointsize, Graphics::kTTFSizeModeCharacter, 0, Graphics::kTTFRenderModeLight);
if (font)
return font;
#endif
return nullptr;
}
const Graphics::Font *ThemeEngine::loadFont(const Common::String &filename, Common::String &name) {
name = filename;
// Try already loaded fonts.
const Graphics::Font *font = FontMan.getFontByName(name);
if (font)
return font;
Common::ArchiveMemberList members;
const Common::String cacheFilename(genCacheFilename(filename));
_themeFiles.listMatchingMembers(members, cacheFilename);
_themeFiles.listMatchingMembers(members, filename);
for (Common::ArchiveMemberList::const_iterator i = members.begin(), end = members.end(); i != end; ++i) {
Common::SeekableReadStream *stream = (*i)->createReadStream();
if (stream) {
if ((*i)->getName().equalsIgnoreCase(cacheFilename)) {
font = Graphics::BdfFont::loadFromCache(*stream);
} else {
font = Graphics::BdfFont::loadFont(*stream);
if (font && !cacheFilename.empty()) {
if (!Graphics::BdfFont::cacheFontData(*(const Graphics::BdfFont *)font, cacheFilename))
warning("Couldn't create cache file for font '%s'", filename.c_str());
}
}
delete stream;
if (font)
return font;
}
}
return nullptr;
}
const Graphics::Font *ThemeEngine::loadFont(const Common::String &filename, const Common::String &scalableFilename, const int pointsize, const bool makeLocalizedFont) {
Common::String fontName;
const Graphics::Font *font = nullptr;
// Prefer scalable fonts over non-scalable fonts
if (!scalableFilename.empty())
font = loadScalableFont(scalableFilename, pointsize, fontName);
// We can use non-scalable fonts, but only for English
bool allowNonScalable = true;
#ifdef USE_TRANSLATION
allowNonScalable = TransMan.currentIsBuiltinLanguage();
#endif
if (!font && allowNonScalable)
font = loadFont(filename, fontName);
// If the font is successfully loaded store it in the font manager.
if (font) {
FontMan.assignFontToName(fontName, font);
// If this font should be the new default localized font, we set it up
// for that.
if (makeLocalizedFont)
FontMan.setLocalizedFont(fontName);
}
return font;
}
Common::String ThemeEngine::genCacheFilename(const Common::String &filename) const {
Common::String cacheName(filename);
for (int i = cacheName.size() - 1; i >= 0; --i) {
if (cacheName[i] == '.') {
while ((uint)i < cacheName.size() - 1) {
cacheName.deleteLastChar();
}
cacheName += "fcc";
return cacheName;
}
}
return Common::String();
}
/**********************************************************
* Static Theme XML functions
*********************************************************/
bool ThemeEngine::themeConfigParseHeader(Common::String header, Common::String &themeName) {
// Check that header is not corrupted
if ((byte)header[0] > 127) {
warning("Corrupted theme header found");
return false;
}
header.trim();
if (header.empty())
return false;
if (header[0] != '[' || header.lastChar() != ']')
return false;
header.deleteChar(0);
header.deleteLastChar();
Common::StringTokenizer tok(header, ":");
if (tok.nextToken() != SCUMMVM_THEME_VERSION_STR)
return false;
themeName = tok.nextToken();
Common::String author = tok.nextToken();
return tok.empty();
}
bool ThemeEngine::themeConfigUsable(const Common::ArchiveMember &member, Common::String &themeName) {
Common::File stream;
bool foundHeader = false;
if (member.getName().matchString("*.zip", true)) {
Common::Archive *zipArchive = Common::makeZipArchive(member.createReadStream());
if (zipArchive && zipArchive->hasFile("THEMERC")) {
stream.open("THEMERC", *zipArchive);
}
delete zipArchive;
}
if (stream.isOpen()) {
Common::String stxHeader = stream.readLine();
foundHeader = themeConfigParseHeader(stxHeader, themeName);
}
return foundHeader;
}
bool ThemeEngine::themeConfigUsable(const Common::FSNode &node, Common::String &themeName) {
Common::File stream;
bool foundHeader = false;
if (node.getName().matchString("*.zip", true) && !node.isDirectory()) {
Common::Archive *zipArchive = Common::makeZipArchive(node);
if (zipArchive && zipArchive->hasFile("THEMERC")) {
// Open THEMERC from the ZIP file.
stream.open("THEMERC", *zipArchive);
}
// Delete the ZIP archive again. Note: This only works because
// stream.open() only uses ZipArchive::createReadStreamForMember,
// and that in turn happens to read all the data for a given
// archive member into a memory block. So there will be no dangling
// reference to zipArchive anywhere. This could change if we
// ever modify ZipArchive::createReadStreamForMember.
delete zipArchive;
} else if (node.isDirectory()) {
Common::FSNode headerfile = node.getChild("THEMERC");
if (!headerfile.exists() || !headerfile.isReadable() || headerfile.isDirectory())
return false;
if (!stream.open(headerfile))
return false;
}
if (stream.isOpen()) {
Common::String stxHeader = stream.readLine();
foundHeader = themeConfigParseHeader(stxHeader, themeName);
}
return foundHeader;
}
namespace {
struct TDComparator {
const Common::String _id;
TDComparator(const Common::String &id) : _id(id) {}
bool operator()(const ThemeEngine::ThemeDescriptor &r) {
return _id == r.id;
}
};
} // end of anonymous namespace
void ThemeEngine::listUsableThemes(Common::List<ThemeDescriptor> &list) {
#ifndef DISABLE_GUI_BUILTIN_THEME
ThemeDescriptor th;
th.name = "ScummVM Classic Theme (Builtin Version)";
th.id = "builtin";
th.filename.clear();
list.push_back(th);
#endif
if (ConfMan.hasKey("themepath"))
listUsableThemes(Common::FSNode(ConfMan.get("themepath")), list);
listUsableThemes(SearchMan, list);
// Now we need to strip all duplicates
// TODO: It might not be the best idea to strip duplicates. The user might
// have different versions of a specific theme in his paths, thus this code
// might show him the wrong version. The problem is we have no ways of checking
// a theme version currently. Also since we want to avoid saving the full path
// in the config file we can not do any better currently.
Common::List<ThemeDescriptor> output;
for (Common::List<ThemeDescriptor>::const_iterator i = list.begin(); i != list.end(); ++i) {
if (Common::find_if(output.begin(), output.end(), TDComparator(i->id)) == output.end())
output.push_back(*i);
}
list = output;
output.clear();
}
void ThemeEngine::listUsableThemes(Common::Archive &archive, Common::List<ThemeDescriptor> &list) {
ThemeDescriptor td;
Common::ArchiveMemberList fileList;
archive.listMatchingMembers(fileList, "*.zip");
for (Common::ArchiveMemberList::iterator i = fileList.begin();
i != fileList.end(); ++i) {
td.name.clear();
if (themeConfigUsable(**i, td.name)) {
td.filename = (*i)->getName();
td.id = (*i)->getName();
// If the name of the node object also contains
// the ".zip" suffix, we will strip it.
if (td.id.matchString("*.zip", true)) {
for (int j = 0; j < 4; ++j)
td.id.deleteLastChar();
}
list.push_back(td);
}
}
fileList.clear();
}
void ThemeEngine::listUsableThemes(const Common::FSNode &node, Common::List<ThemeDescriptor> &list, int depth) {
if (!node.exists() || !node.isReadable() || !node.isDirectory())
return;
ThemeDescriptor td;
// Check whether we point to a valid theme directory.
if (themeConfigUsable(node, td.name)) {
td.filename = node.getPath();
td.id = node.getName();
list.push_back(td);
// A theme directory should never contain any other themes
// thus we just return to the caller here.
return;
}
Common::FSList fileList;
// Check all files. We need this to find all themes inside ZIP archives.
if (!node.getChildren(fileList, Common::FSNode::kListFilesOnly))
return;
for (Common::FSList::iterator i = fileList.begin(); i != fileList.end(); ++i) {
// We will only process zip files for now
if (!i->getPath().matchString("*.zip", true))
continue;
td.name.clear();
if (themeConfigUsable(*i, td.name)) {
td.filename = i->getPath();
td.id = i->getName();
// If the name of the node object also contains
// the ".zip" suffix, we will strip it.
if (td.id.matchString("*.zip", true)) {
for (int j = 0; j < 4; ++j)
td.id.deleteLastChar();
}
list.push_back(td);
}
}
fileList.clear();
// Check if we exceeded the given recursion depth
if (depth - 1 == -1)
return;
// As next step we will search all subdirectories
if (!node.getChildren(fileList, Common::FSNode::kListDirectoriesOnly))
return;
for (Common::FSList::iterator i = fileList.begin(); i != fileList.end(); ++i)
listUsableThemes(*i, list, depth == -1 ? - 1 : depth - 1);
}
Common::String ThemeEngine::getThemeFile(const Common::String &id) {
// FIXME: Actually "default" rather sounds like it should use
// our default theme which would mean "scummremastered" instead
// of the builtin one.
if (id.equalsIgnoreCase("default"))
return Common::String();
// For our builtin theme we don't have to do anything for now too
if (id.equalsIgnoreCase("builtin"))
return Common::String();
Common::FSNode node(id);
// If the given id is a full path we'll just use it
if (node.exists() && (node.isDirectory() || node.getName().matchString("*.zip", true)))
return id;
// FIXME:
// A very ugly hack to map a id to a filename, this will generate
// a complete theme list, thus it is slower than it could be.
// But it is the easiest solution for now.
Common::List<ThemeDescriptor> list;
listUsableThemes(list);
for (Common::List<ThemeDescriptor>::const_iterator i = list.begin(); i != list.end(); ++i) {
if (id.equalsIgnoreCase(i->id))
return i->filename;
}
warning("Could not find theme '%s' falling back to builtin", id.c_str());
// If no matching id has been found we will
// just fall back to the builtin theme
return Common::String();
}
Common::String ThemeEngine::getThemeId(const Common::String &filename) {
// If no filename has been given we will initialize the builtin theme
if (filename.empty())
return "builtin";
Common::FSNode node(filename);
if (node.exists()) {
if (node.getName().matchString("*.zip", true)) {
Common::String id = node.getName();
for (int i = 0; i < 4; ++i)
id.deleteLastChar();
return id;
} else {
return node.getName();
}
}
// FIXME:
// A very ugly hack to map a id to a filename, this will generate
// a complete theme list, thus it is slower than it could be.
// But it is the easiest solution for now.
Common::List<ThemeDescriptor> list;
listUsableThemes(list);
for (Common::List<ThemeDescriptor>::const_iterator i = list.begin(); i != list.end(); ++i) {
if (filename.equalsIgnoreCase(i->filename))
return i->id;
}
return "builtin";
}
void ThemeEngine::showCursor() {
if (_useCursor) {
#ifndef USE_RGB_COLOR
CursorMan.pushCursorPalette(_cursorPal, 0, _cursorPalSize);
#endif
CursorMan.pushCursor(_cursor, _cursorWidth, _cursorHeight, _cursorHotspotX, _cursorHotspotY, _cursorTransparent, true, &_cursorFormat);
CursorMan.showMouse(true);
}
}
void ThemeEngine::hideCursor() {
if (_useCursor) {
#ifndef USE_RGB_COLOR
CursorMan.popCursorPalette();
#endif
CursorMan.popCursor();
}
}
void ThemeEngine::drawToBackbuffer() {
_vectorRenderer->setSurface(&_backBuffer);
}
void ThemeEngine::drawToScreen() {
_vectorRenderer->setSurface(&_screen);
}
Common::Rect ThemeEngine::swapClipRect(const Common::Rect &newRect) {
Common::Rect oldRect = _clip;
_clip = newRect;
return oldRect;
}
const Common::Rect ThemeEngine::getClipRect() {
return _clip;
}
void ThemeEngine::disableClipRect() {
_clip = Common::Rect(_screen.w, _screen.h);
}
} // End of namespace GUI.