diff options
Diffstat (limited to 'src/resources/theme.cpp')
-rw-r--r-- | src/resources/theme.cpp | 426 |
1 files changed, 299 insertions, 127 deletions
diff --git a/src/resources/theme.cpp b/src/resources/theme.cpp index 694f2210..ea2cef45 100644 --- a/src/resources/theme.cpp +++ b/src/resources/theme.cpp @@ -25,7 +25,6 @@ #include "configuration.h" #include "log.h" -#include "textrenderer.h" #include "resources/dye.h" #include "resources/image.h" @@ -38,7 +37,6 @@ #include <guichan/widget.hpp> #include <algorithm> -#include <optional> /** * Initializes the directory in which the client looks for GUI themes, which at @@ -55,18 +53,33 @@ static void initDefaultThemePath() defaultThemePath = "graphics/gui/"; } -static std::optional<std::string> findThemePath(const std::string &theme) +static bool isThemePath(const std::string &theme) { - if (theme.empty()) - return {}; + return FS::exists(defaultThemePath + theme + "/theme.xml"); +} - std::string themePath = defaultThemePath; - themePath += theme; - if (FS::isDirectory(themePath)) - return themePath; +ThemeInfo::ThemeInfo(const std::string &path) + : path(path) +{ + auto themeFile = getFullPath() + "/theme.xml"; + if (!FS::exists(themeFile)) + return; - return {}; + auto doc = std::make_unique<XML::Document>(themeFile); + XML::Node rootNode = doc->rootNode(); + if (!rootNode || rootNode.name() != "theme") + return; + + if (rootNode.attribute("name", name) && !name.empty()) + this->doc = std::move(doc); + else + Log::error("Theme '%s' has no name!", path.c_str()); +} + +std::string ThemeInfo::getFullPath() const +{ + return defaultThemePath + path; } @@ -120,11 +133,11 @@ void Skin::draw(Graphics *graphics, const WidgetState &state) const if constexpr (std::is_same_v<T, ImageRect>) { - graphics->drawImageRect(state.x + part.offsetX, + graphics->drawImageRect(data, + state.x + part.offsetX, state.y + part.offsetY, state.width, - state.height, - data); + state.height); } else if constexpr (std::is_same_v<T, Image*>) { @@ -132,12 +145,21 @@ void Skin::draw(Graphics *graphics, const WidgetState &state) const } else if constexpr (std::is_same_v<T, ColoredRectangle>) { + const auto color = graphics->getColor(); + // TODO: Take GUI alpha into account graphics->setColor(data.color); - graphics->fillRectangle(gcn::Rectangle(state.x + part.offsetX, - state.y + part.offsetY, - state.width, - state.height)); - graphics->setColor(gcn::Color(255, 255, 255)); + + const gcn::Rectangle rect(state.x + part.offsetX, + state.y + part.offsetY, + state.width, + state.height); + + if (data.filled) + graphics->fillRectangle(rect); + else + graphics->drawRectangle(rect); + + graphics->setColor(color); } }, part.data); } @@ -195,7 +217,7 @@ void Skin::updateAlpha(float alpha) for (auto &part : state.parts) { if (auto rect = std::get_if<ImageRect>(&part.data)) - rect->setAlpha(alpha); + rect->image->setAlpha(alpha); else if (auto img = std::get_if<Image *>(&part.data)) (*img)->setAlpha(alpha); } @@ -203,41 +225,60 @@ void Skin::updateAlpha(float alpha) } -Theme::Theme(const std::string &path) - : Palette(THEME_COLORS_END) - , mThemePath(path) - , mProgressColors(THEME_PROG_END) +Theme::Theme(const ThemeInfo &themeInfo) + : mThemePath(themeInfo.getFullPath()) { listen(Event::ConfigChannel); - readTheme("theme.xml"); + readTheme(themeInfo); + + if (mPalettes.empty()) + { + Log::info("Error, theme did not define any palettes: %s", + themeInfo.getPath().c_str()); - mColors[HIGHLIGHT].ch = 'H'; - mColors[CHAT].ch = 'C'; - mColors[GM].ch = 'G'; - mColors[PLAYER].ch = 'Y'; - mColors[WHISPER].ch = 'W'; - mColors[IS].ch = 'I'; - mColors[PARTY].ch = 'P'; - mColors[GUILD].ch = 'U'; - mColors[SERVER].ch = 'S'; - mColors[LOGGER].ch = 'L'; - mColors[HYPERLINK].ch = '<'; + // Avoid crashing + mPalettes.emplace_back(THEME_COLORS_END); + } } -Theme::~Theme() = default; +Theme::~Theme() +{ + for (auto &[_, image] : mIcons) + delete image; +} std::string Theme::prepareThemePath() { initDefaultThemePath(); // Try theme from settings - auto themePath = findThemePath(config.theme); + if (isThemePath(config.theme)) + return config.theme; // Try theme from branding - if (!themePath) - themePath = findThemePath(branding.getStringValue("theme")); + if (isThemePath(branding.getStringValue("theme"))) + return branding.getStringValue("theme"); - return themePath.value_or(defaultThemePath); + return std::string(); +} + +std::vector<ThemeInfo> Theme::getAvailableThemes() +{ + std::vector<ThemeInfo> themes; + themes.emplace_back(std::string()); + + for (const auto &entry : FS::enumerateFiles(defaultThemePath)) + { + ThemeInfo theme{entry}; + if (theme.isValid()) + themes.push_back(std::move(theme)); + } + + std::sort(themes.begin(), themes.end(), [](const ThemeInfo &a, const ThemeInfo &b) { + return a.getName() < b.getName(); + }); + + return themes; } std::string Theme::resolvePath(const std::string &path) const @@ -269,14 +310,9 @@ ResourceRef<Image> Theme::getImageFromTheme(const std::string &path) return gui->getTheme()->getImage(path); } -const gcn::Color &Theme::getThemeColor(int type, int alpha) -{ - return gui->getTheme()->getColor(type, alpha); -} - -const gcn::Color &Theme::getThemeColor(char c, bool &valid) +const gcn::Color &Theme::getThemeColor(int type) { - return gui->getTheme()->getColor(c, valid); + return gui->getTheme()->getColor(type); } gcn::Color Theme::getProgressColor(int type, float progress) @@ -289,14 +325,62 @@ gcn::Color Theme::getProgressColor(int type, float progress) return gcn::Color(color[0], color[1], color[2]); } +const Palette &Theme::getPalette(size_t index) const +{ + return mPalettes.at(index < mPalettes.size() ? index : 0); +} + +const gcn::Color &Theme::getColor(int type) const +{ + return getPalette(0).getColor(type); +} + +std::optional<int> Theme::getColorIdForChar(char c) +{ + switch (c) { + case '0': return BLACK; + case '1': return RED; + case '2': return GREEN; + case '3': return BLUE; + case '4': return ORANGE; + case '5': return YELLOW; + case '6': return PINK; + case '7': return PURPLE; + case '8': return GRAY; + case '9': return BROWN; + + case 'H': return HIGHLIGHT; + case 'C': return CHAT; + case 'G': return GM; + case 'g': return GLOBAL; + case 'Y': return PLAYER; + case 'W': return WHISPER; + // case 'w': return WHISPER_TAB_OFFLINE; + case 'I': return IS; + case 'P': return PARTY; + case 'U': return GUILD; + case 'S': return SERVER; + case 'L': return LOGGER; + case '<': return HYPERLINK; + // case 's': return SELFNICK; + case 'o': return OLDCHAT; + case 'a': return AWAYCHAT; + } + + return {}; +} + void Theme::drawSkin(Graphics *graphics, SkinType type, const WidgetState &state) const { getSkin(type).draw(graphics, state); } -void Theme::drawProgressBar(Graphics *graphics, const gcn::Rectangle &area, - const gcn::Color &color, float progress, - const std::string &text) const +void Theme::drawProgressBar(Graphics *graphics, + const gcn::Rectangle &area, + const gcn::Color &color, + float progress, + const std::string &text, + ProgressPalette progressType) const { gcn::Font *oldFont = graphics->getFont(); gcn::Color oldColor = graphics->getColor(); @@ -325,17 +409,21 @@ void Theme::drawProgressBar(Graphics *graphics, const gcn::Rectangle &area, { if (auto skinState = skin.getState(widgetState.flags)) { - auto font = skinState->textFormat.bold ? boldFont : gui->getFont(); + const TextFormat *textFormat = &skinState->textFormat; + + if (progressType < THEME_PROG_END && mProgressTextFormats[progressType]) + textFormat = &(*mProgressTextFormats[progressType]); + + auto font = textFormat->bold ? boldFont : gui->getFont(); const int textX = area.x + area.width / 2; const int textY = area.y + (area.height - font->getHeight()) / 2; - TextRenderer::renderText(graphics, - text, - textX, - textY, - gcn::Graphics::CENTER, - font, - skinState->textFormat); + graphics->drawText(text, + textX, + textY, + gcn::Graphics::CENTER, + font, + *textFormat); } } @@ -350,14 +438,13 @@ const Skin &Theme::getSkin(SkinType skinType) const return it != mSkins.end() ? it->second : emptySkin; } -int Theme::getMinWidth(SkinType skinType) const +const Image *Theme::getIcon(const std::string &name) const { - return getSkin(skinType).getMinWidth(); -} + auto it = mIcons.find(name); + if (it == mIcons.end()) + return nullptr; -int Theme::getMinHeight(SkinType skinType) const -{ - return getSkin(skinType).getMinHeight(); + return it->second; } void Theme::setMinimumOpacity(float minimumOpacity) @@ -377,8 +464,8 @@ void Theme::updateAlpha() mAlpha = alpha; - for (auto &skin : mSkins) - skin.second.updateAlpha(mAlpha); + for (auto &[_, skin] : mSkins) + skin.updateAlpha(mAlpha); } void Theme::event(Event::Channel channel, const Event &event) @@ -395,20 +482,21 @@ static bool check(bool value, const char *msg, ...) { if (!value) { - va_list args; - va_start(args, msg); - logger->log(msg, args); - va_end(args); + va_list ap; + va_start(ap, msg); + Log::vinfo(msg, ap); + va_end(ap); } return !value; } -bool Theme::readTheme(const std::string &filename) +bool Theme::readTheme(const ThemeInfo &themeInfo) { - logger->log("Loading theme '%s'.", filename.c_str()); + Log::info("Loading %s theme from '%s'...", + themeInfo.getName().c_str(), + themeInfo.getPath().c_str()); - XML::Document doc(resolvePath(filename)); - XML::Node rootNode = doc.rootNode(); + XML::Node rootNode = themeInfo.getDocument().rootNode(); if (!rootNode || rootNode.name() != "theme") return false; @@ -417,13 +505,17 @@ bool Theme::readTheme(const std::string &filename) { if (childNode.name() == "skin") readSkinNode(childNode); - else if (childNode.name() == "color") - readColorNode(childNode); + else if (childNode.name() == "palette") + readPaletteNode(childNode); else if (childNode.name() == "progressbar") readProgressBarNode(childNode); + else if (childNode.name() == "icon") + readIconNode(childNode); + else + Log::info("Theme: Unknown node '%s'!", childNode.name().data()); } - logger->log("Finished loading theme."); + Log::info("Finished loading theme."); for (auto &[_, skin] : mSkins) skin.updateAlpha(mAlpha); @@ -434,8 +526,10 @@ bool Theme::readTheme(const std::string &filename) static std::optional<SkinType> readSkinType(std::string_view type) { if (type == "Window") return SkinType::Window; + if (type == "ToolWindow") return SkinType::ToolWindow; if (type == "Popup") return SkinType::Popup; if (type == "SpeechBubble") return SkinType::SpeechBubble; + if (type == "Desktop") return SkinType::Desktop; if (type == "Button") return SkinType::Button; if (type == "ButtonUp") return SkinType::ButtonUp; if (type == "ButtonDown") return SkinType::ButtonDown; @@ -459,6 +553,9 @@ static std::optional<SkinType> readSkinType(std::string_view type) if (type == "SliderHandle") return SkinType::SliderHandle; if (type == "ResizeGrip") return SkinType::ResizeGrip; if (type == "ShortcutBox") return SkinType::ShortcutBox; + if (type == "EquipmentBox") return SkinType::EquipmentBox; + if (type == "ItemSlot") return SkinType::ItemSlot; + if (type == "EmoteSlot") return SkinType::EmoteSlot; return {}; } @@ -471,18 +568,40 @@ void Theme::readSkinNode(XML::Node node) auto &skin = mSkins[*skinType]; + node.attribute("width", skin.width); + node.attribute("height", skin.height); node.attribute("frameSize", skin.frameSize); node.attribute("padding", skin.padding); node.attribute("spacing", skin.spacing); node.attribute("titleBarHeight", skin.titleBarHeight); node.attribute("titleOffsetX", skin.titleOffsetX); node.attribute("titleOffsetY", skin.titleOffsetY); + node.attribute("palette", skin.palette); + node.attribute("showButtons", skin.showButtons); for (auto childNode : node.children()) if (childNode.name() == "state") readSkinStateNode(childNode, skin); } +static void readSkinStateRectNode(XML::Node node, SkinState &state) +{ + auto &part = state.parts.emplace_back(); + auto &rect = part.data.emplace<ColoredRectangle>(); + + node.attribute("color", rect.color); + node.attribute("alpha", rect.color.a); + node.attribute("fill", rect.filled); +} + +static void readTextNode(XML::Node node, TextFormat &textFormat) +{ + node.attribute("bold", textFormat.bold); + node.attribute("color", textFormat.color); + node.attribute("outlineColor", textFormat.outlineColor); + node.attribute("shadowColor", textFormat.shadowColor); +} + void Theme::readSkinStateNode(XML::Node node, Skin &skin) const { SkinState state; @@ -511,21 +630,12 @@ void Theme::readSkinStateNode(XML::Node node, Skin &skin) const else if (childNode.name() == "rect") readSkinStateRectNode(childNode, state); else if (childNode.name() == "text") - readSkinStateTextNode(childNode, state); + readTextNode(childNode, state.textFormat); } skin.addState(std::move(state)); } -void Theme::readSkinStateTextNode(XML::Node node, SkinState &state) const -{ - auto &textFormat = state.textFormat; - node.attribute("bold", textFormat.bold); - node.attribute("color", textFormat.color); - node.attribute("outlineColor", textFormat.outlineColor); - node.attribute("shadowColor", textFormat.shadowColor); -} - template<> inline void fromString(const char *str, FillMode &value) { @@ -580,24 +690,13 @@ void Theme::readSkinStateImgNode(XML::Node node, SkinState &state) const if (left + right + top + bottom > 0) { auto &border = part.data.emplace<ImageRect>(); + border.left = left; + border.right = right; + border.top = top; + border.bottom = bottom; + border.image.reset(image->getSubImage(x, y, width, height)); node.attribute("fill", border.fillMode); - - const int gridx[4] = {x, x + left, x + width - right, x + width}; - const int gridy[4] = {y, y + top, y + height - bottom, y + height}; - unsigned a = 0; - - for (unsigned y = 0; y < 3; y++) - { - for (unsigned x = 0; x < 3; x++) - { - border.grid[a] = image->getSubImage(gridx[x], - gridy[y], - gridx[x + 1] - gridx[x], - gridy[y + 1] - gridy[y]); - a++; - } - } } else { @@ -611,8 +710,8 @@ inline void fromString(const char *str, gcn::Color &value) if (strlen(str) < 7 || str[0] != '#') { error: - logger->log("Error, invalid theme color palette: %s", str); - value = Palette::BLACK; + Log::info("Error, invalid theme color palette: %s", str); + value = gcn::Color(0, 0, 0); return; } @@ -637,31 +736,73 @@ inline void fromString(const char *str, gcn::Color &value) value = gcn::Color(v); } -void Theme::readSkinStateRectNode(XML::Node node, SkinState &state) const +void Theme::readIconNode(XML::Node node) { - auto &part = state.parts.emplace_back(); - auto &rect = part.data.emplace<ColoredRectangle>(); + std::string name; + std::string src; + node.attribute("name", name); + node.attribute("src", src); - node.attribute("color", rect.color); - node.attribute("alpha", rect.color.a); + if (check(!name.empty(), "Theme: 'icon' element has empty 'name' attribute!")) + return; + if (check(!src.empty(), "Theme: 'icon' element has empty 'src' attribute!")) + return; + + auto image = getImage(src); + if (check(image, "Theme: Failed to load image '%s'!", src.c_str())) + return; + + int x = 0; + int y = 0; + int width = image->getWidth(); + int height = image->getHeight(); + + node.attribute("x", x); + node.attribute("y", y); + node.attribute("width", width); + node.attribute("height", height); + + if (check(x >= 0 || y >= 0, "Theme: Invalid position value!")) + return; + if (check(width >= 0 || height >= 0, "Theme: Invalid size value!")) + return; + if (check(x + width <= image->getWidth() || y + height <= image->getHeight(), "Theme: Image size out of bounds!")) + return; + + mIcons[name] = image->getSubImage(x, y, width, height); } -static int readColorType(const std::string &type) +static int readColorId(const std::string &id) { static constexpr const char *colors[Theme::THEME_COLORS_END] = { "TEXT", + "BLACK", + "RED", + "GREEN", + "BLUE", + "ORANGE", + "YELLOW", + "PINK", + "PURPLE", + "GRAY", + "BROWN", + "CARET", "SHADOW", "OUTLINE", - "PARTY_CHAT_TAB", - "PARTY_SOCIAL_TAB", + "PARTY_TAB", + "WHISPER_TAB", "BACKGROUND", "HIGHLIGHT", + "HIGHLIGHT_TEXT", "TAB_FLASH", "SHOP_WARNING", "ITEM_EQUIPPED", "CHAT", + "OLDCHAT", + "AWAYCHAT", "BUBBLE_TEXT", "GM", + "GLOBAL", "PLAYER", "WHISPER", "IS", @@ -687,17 +828,17 @@ static int readColorType(const std::string &type) "SERVER_VERSION_NOT_SUPPORTED" }; - if (type.empty()) + if (id.empty()) return -1; for (int i = 0; i < Theme::THEME_COLORS_END; i++) - if (type == colors[i]) + if (id == colors[i]) return i; return -1; } -static Palette::GradientType readColorGradient(const std::string &grad) +static Palette::GradientType readGradientType(const std::string &grad) { static constexpr const char *grads[] = { "STATIC", @@ -716,22 +857,42 @@ static Palette::GradientType readColorGradient(const std::string &grad) return Palette::STATIC; } -void Theme::readColorNode(XML::Node node) +static void readColorNode(XML::Node node, Palette &palette) { - const int type = readColorType(node.getProperty("id", std::string())); - if (check(type > 0, "Theme: 'color' element has invalid or no 'type' attribute!")) + const auto idStr = node.getProperty("id", std::string()); + const int id = readColorId(idStr); + if (check(id >= 0, "Theme: 'color' element has unknown 'id' attribute: '%s'!", idStr.c_str())) return; gcn::Color color; if (check(node.attribute("color", color), "Theme: 'color' element missing 'color' attribute!")) return; - const GradientType grad = readColorGradient(node.getProperty("effect", std::string())); + std::optional<gcn::Color> outlineColor; + node.attribute("outlineColor", outlineColor); - mColors[type].set(type, color, grad, 10); + const auto grad = readGradientType(node.getProperty("effect", std::string())); + palette.setColor(id, color, outlineColor, grad, 10); } -static int readProgressType(const std::string &type) +void Theme::readPaletteNode(XML::Node node) +{ + int paletteId; + if (node.attribute("id", paletteId) && static_cast<size_t>(paletteId) != mPalettes.size()) + Log::info("Theme: Non-consecutive palette 'id' attribute with value %d!", paletteId); + + Palette &palette = mPalettes.emplace_back(THEME_COLORS_END); + + for (auto childNode : node.children()) + { + if (childNode.name() == "color") + readColorNode(childNode, palette); + else + Log::info("Theme: Unknown node '%s'!", childNode.name().data()); + } +} + +static int readProgressId(const std::string &id) { static constexpr const char *colors[Theme::THEME_PROG_END] = { "DEFAULT", @@ -744,11 +905,11 @@ static int readProgressType(const std::string &type) "JOB" }; - if (type.empty()) + if (id.empty()) return -1; for (int i = 0; i < Theme::THEME_PROG_END; i++) - if (type == colors[i]) + if (id == colors[i]) return i; return -1; @@ -756,9 +917,20 @@ static int readProgressType(const std::string &type) void Theme::readProgressBarNode(XML::Node node) { - const int type = readProgressType(node.getProperty("id", std::string())); - if (type < 0) // invalid or no type given + const auto idStr = node.getProperty("id", std::string()); + const int id = readProgressId(idStr); + if (check(id >= 0, "Theme: 'progress' element has unknown 'id' attribute: '%s'!", idStr.c_str())) return; - mProgressColors[type] = std::make_unique<DyePalette>(node.getProperty("color", std::string())); + std::string color; + if (node.attribute("color", color)) + mProgressColors[id] = std::make_unique<DyePalette>(color); + + for (auto childNode : node.children()) + { + if (childNode.name() == "text") + readTextNode(childNode, mProgressTextFormats[id].emplace()); + else + Log::info("Theme: Unknown node '%s' in progressbar!", childNode.name().data()); + } } |