/* * The ManaPlus Client * Copyright (C) 2004-2009 The Mana World Development Team * Copyright (C) 2009-2010 The Mana Developers * Copyright (C) 2011-2019 The ManaPlus Developers * Copyright (C) 2009 Aethyra Development Team * * This file is part of The ManaPlus Client. * * 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 2 of the License, or * 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 "gui/widgets/browserbox.h" #include "enums/gui/linkhighlightmode.h" #include "gui/gui.h" #include "gui/mouseoverlink.h" #include "gui/skin.h" #include "gui/fonts/font.h" #include "gui/widgets/browserbox.inc" #include "gui/widgets/linkhandler.h" #include "render/graphics.h" #include "resources/imageset.h" #include "resources/image/image.h" #include "resources/loaders/imageloader.h" #include "resources/loaders/imagesetloader.h" #include "utils/browserboxtools.h" #include "utils/checkutils.h" #include "utils/foreach.h" #include "utils/stringutils.h" #include "utils/timer.h" #include "utils/translation/podict.h" #include #include "debug.h" ImageSet *BrowserBox::mEmotes = nullptr; int BrowserBox::mInstances = 0; BrowserBox::BrowserBox(const Widget2 *const widget, const Opaque opaque, const std::string &skin) : Widget(widget), MouseListener(), WidgetListener(), mTextRows(), mTextRowLinksCount(), mLineParts(), mLinks(), mLinkHandler(nullptr), mSkin(nullptr), mHighlightMode(0), mSelectedLink(-1), mMaxRows(0), mHeight(0), mWidth(0), mYStart(0), mUpdateTime(-1), mPadding(0), mNewLinePadding(15U), mItemPadding(0), mDataWidth(0), mHighlightColor(getThemeColor(ThemeColorId::HIGHLIGHT, 255U)), mHyperLinkColor(getThemeColor(ThemeColorId::HYPERLINK, 255U)), mOpaque(opaque), mUseLinksAndUserColors(true), mUseEmotes(true), mAlwaysUpdate(true), mProcessVars(false), mEnableImages(false), mEnableKeys(false), mEnableTabs(false) { mAllowLogic = false; setFocusable(true); addMouseListener(this); addWidgetListener(this); mBackgroundColor = getThemeColor(ThemeColorId::BACKGROUND, 255U); if (theme != nullptr) { mSkin = theme->load(skin, "browserbox.xml", true, Theme::getThemePath()); } if (mInstances == 0) { mEmotes = Loader::getImageSet( "graphics/sprites/chatemotes.png", 17, 18); } mInstances ++; if (mSkin != nullptr) { mPadding = mSkin->getPadding(); mNewLinePadding = CAST_U32( mSkin->getOption("newLinePadding", 15)); mItemPadding = mSkin->getOption("itemPadding"); if (mSkin->getOption("highlightBackground") != 0) mHighlightMode |= LinkHighlightMode::BACKGROUND; if (mSkin->getOption("highlightUnderline") != 0) mHighlightMode |= LinkHighlightMode::UNDERLINE; } readColor(BLACK); readColor(RED); readColor(GREEN); readColor(BLUE); readColor(ORANGE); readColor(YELLOW); readColor(PINK); readColor(PURPLE); readColor(GRAY); readColor(BROWN); mForegroundColor = getThemeColor(ThemeColorId::BROWSERBOX, 255U); mForegroundColor2 = getThemeColor(ThemeColorId::BROWSERBOX_OUTLINE, 255U); } BrowserBox::~BrowserBox() { if (gui != nullptr) gui->removeDragged(this); if (theme != nullptr) { theme->unload(mSkin); mSkin = nullptr; } mInstances --; if (mInstances == 0) { if (mEmotes != nullptr) { mEmotes->decRef(); mEmotes = nullptr; } } } void BrowserBox::setLinkHandler(LinkHandler* linkHandler) { mLinkHandler = linkHandler; } void BrowserBox::addRow(const std::string &row, const bool atTop) { std::string tmp = row; std::string newRow; const Font *const font = getFont(); int linksCount = 0; if (getWidth() < 0) return; if (mProcessVars) { BrowserBoxTools::replaceVars(tmp); } // Use links and user defined colors if (mUseLinksAndUserColors) { BrowserLink bLink; // Check for links in format "@@link|Caption@@" const uint32_t sz = CAST_U32(mTextRows.size()); if (mEnableKeys) { BrowserBoxTools::replaceKeys(tmp); } size_t idx1 = tmp.find("@@"); while (idx1 != std::string::npos) { const size_t idx2 = tmp.find('|', idx1); const size_t idx3 = tmp.find("@@", idx2); if (idx2 == std::string::npos || idx3 == std::string::npos) break; bLink.link = tmp.substr(idx1 + 2, idx2 - (idx1 + 2)); bLink.caption = tmp.substr(idx2 + 1, idx3 - (idx2 + 1)); bLink.y1 = CAST_S32(sz) * font->getHeight(); bLink.y2 = bLink.y1 + font->getHeight(); if (bLink.caption.empty()) { bLink.caption = BrowserBoxTools::replaceLinkCommands( bLink.link); if (translator != nullptr) bLink.caption = translator->getStr(bLink.caption); } newRow.append(tmp.substr(0, idx1)); std::string tmp2 = newRow; idx1 = tmp2.find("##"); while (idx1 != std::string::npos) { tmp2.erase(idx1, 3); idx1 = tmp2.find("##"); } bLink.x1 = font->getWidth(tmp2) - 1; bLink.x2 = bLink.x1 + font->getWidth(bLink.caption) + 1; if (atTop) mLinks.insert(mLinks.begin(), bLink); else mLinks.push_back(bLink); linksCount ++; newRow.append("##<").append(bLink.caption); tmp.erase(0, idx3 + 2); if (!tmp.empty()) newRow.append("##>"); idx1 = tmp.find("@@"); } newRow.append(tmp); } // Don't use links and user defined colors else { newRow = row; } if (mEnableTabs) { BrowserBoxTools::replaceTabs(newRow); } if (atTop) { mTextRows.push_front(newRow); mTextRowLinksCount.push_front(linksCount); } else { mTextRows.push_back(newRow); mTextRowLinksCount.push_back(linksCount); } // discard older rows when a row limit has been set if (mMaxRows > 0 && !mTextRows.empty()) { while (mTextRows.size() > CAST_SIZE(mMaxRows)) { mTextRows.pop_front(); int cnt = mTextRowLinksCount.front(); mTextRowLinksCount.pop_front(); while ((cnt != 0) && !mLinks.empty()) { mLinks.erase(mLinks.begin()); cnt --; } } } const int fontHeight = font->getHeight(); unsigned int y = 0; unsigned int nextChar; const char *const hyphen = "~"; const unsigned int hyphenWidth = CAST_U32( font->getWidth(hyphen)); unsigned int x = 0; FOR_EACH (TextRowCIter, i, mTextRows) { std::string tempRow = *i; for (uint32_t j = 0, sz = CAST_U32(tempRow.size()); j < sz; j++) { const std::string character = tempRow.substr(j, 1); x += CAST_U32(font->getWidth(character)); nextChar = j + 1; // Wraping between words (at blank spaces) if (nextChar < sz && tempRow.at(nextChar) == ' ') { int nextSpacePos = CAST_U32( tempRow.find(' ', (nextChar + 1))); if (nextSpacePos <= 0) nextSpacePos = CAST_U32(sz) - 1U; const unsigned int nextWordWidth = CAST_U32(font->getWidth( tempRow.substr(nextChar, (CAST_U32(nextSpacePos) - nextChar)))); if ((x + nextWordWidth + 10) > CAST_U32(getWidth())) { x = mNewLinePadding; // Ident in new line y += 1; j ++; } } // Wrapping looong lines (brutal force) else if ((x + 2 * hyphenWidth) > CAST_U32(getWidth())) { x = mNewLinePadding; // Ident in new line y += 1; } } } setHeight(fontHeight * (CAST_S32( CAST_U32(mTextRows.size()) + y))); mUpdateTime = 0; updateHeight(); } void BrowserBox::addRow(const std::string &cmd, const char *const text) { addRow(strprintf("@@%s|%s@@", encodeLinkText(cmd).c_str(), encodeLinkText(text).c_str()), false); } void BrowserBox::addImage(const std::string &path) { if (!mEnableImages) return; mTextRows.push_back("~~~" + path); mTextRowLinksCount.push_back(0); } void BrowserBox::clearRows() { mTextRows.clear(); mTextRowLinksCount.clear(); mLinks.clear(); setWidth(0); setHeight(0); mSelectedLink = -1; mUpdateTime = 0; mDataWidth = 0; updateHeight(); } void BrowserBox::mousePressed(MouseEvent &event) { if (mLinkHandler == nullptr) return; const LinkIterator i = std::find_if(mLinks.begin(), mLinks.end(), MouseOverLink(event.getX(), event.getY())); if (i != mLinks.end()) { mLinkHandler->handleLink(i->link, &event); event.consume(); } } void BrowserBox::mouseMoved(MouseEvent &event) { const LinkIterator i = std::find_if(mLinks.begin(), mLinks.end(), MouseOverLink(event.getX(), event.getY())); mSelectedLink = (i != mLinks.end()) ? CAST_S32(i - mLinks.begin()) : -1; } void BrowserBox::mouseExited(MouseEvent &event A_UNUSED) { mSelectedLink = -1; } void BrowserBox::draw(Graphics *const graphics) { BLOCK_START("BrowserBox::draw") const ClipRect &cr = graphics->getTopClip(); mYStart = cr.y - cr.yOffset; const int yEnd = mYStart + cr.height; if (mYStart < 0) mYStart = 0; if (mDimension.width != mWidth) { mWidth = mDimension.width; mHeight = calcHeight(); setHeight(mHeight); mUpdateTime = cur_time; if (mDimension.width != mWidth) reportAlways("browserbox resize in draw") } if (mOpaque == Opaque_true) { graphics->setColor(mBackgroundColor); graphics->fillRectangle(Rect(0, 0, mDimension.width, mDimension.height)); } if (mSelectedLink >= 0 && mSelectedLink < CAST_S32(mLinks.size())) { if ((mHighlightMode & LinkHighlightMode::BACKGROUND) != 0U) { BrowserLink &link = mLinks[CAST_SIZE(mSelectedLink)]; graphics->setColor(mHighlightColor); graphics->fillRectangle(Rect( link.x1, link.y1, link.x2 - link.x1, link.y2 - link.y1)); } if ((mHighlightMode & LinkHighlightMode::UNDERLINE) != 0U) { BrowserLink &link = mLinks[CAST_SIZE(mSelectedLink)]; graphics->setColor(mHyperLinkColor); graphics->drawLine( link.x1, link.y2, link.x2, link.y2); } } Font *const font = getFont(); FOR_EACH (LinePartCIter, i, mLineParts) { const LinePart &part = *i; if (part.mY + 50 < mYStart) continue; if (part.mY > yEnd) break; if (part.mType == 0U) { if (part.mBold) { boldFont->drawString(graphics, part.mColor, part.mColor2, part.mText, part.mX, part.mY); } else { font->drawString(graphics, part.mColor, part.mColor2, part.mText, part.mX, part.mY); } } else if (part.mImage != nullptr) { graphics->drawImage(part.mImage, part.mX, part.mY); } } BLOCK_END("BrowserBox::draw") } void BrowserBox::safeDraw(Graphics *const graphics) { BrowserBox::draw(graphics); } int BrowserBox::calcHeight() { unsigned int y = CAST_U32(mPadding); int wrappedLines = 0; int moreHeight = 0; int maxWidth = mDimension.width - mPadding; int link = 0; bool bold = false; unsigned int wWidth = CAST_U32(maxWidth); if (maxWidth < 0) return 1; const Font *const font = getFont(); const int fontHeight = font->getHeight() + 2 * mItemPadding; const int fontWidthMinus = font->getWidth("-"); const char *const hyphen = "~"; const int hyphenWidth = font->getWidth(hyphen); Color selColor[2] = {mForegroundColor, mForegroundColor2}; const Color textColor[2] = {mForegroundColor, mForegroundColor2}; mLineParts.clear(); FOR_EACH (TextRowCIter, i, mTextRows) { unsigned int x = CAST_U32(mPadding); const std::string row = *(i); bool wrapped = false; int objects = 0; // Check for separator lines if (row.find("---", 0) == 0) { const int dashWidth = fontWidthMinus; for (x = CAST_U32(mPadding); x < wWidth; x ++) { mLineParts.push_back(LinePart(CAST_S32(x), CAST_S32(y) + mItemPadding, selColor[0], selColor[1], "-", false)); x += CAST_U32(CAST_S32( dashWidth) - 2); } y += CAST_U32(fontHeight); continue; } else if (mEnableImages && row.find("~~~", 0) == 0) { std::string str = row.substr(3); const size_t sz = str.size(); if (sz > 2 && str.substr(sz - 1) == "~") str = str.substr(0, sz - 1); Image *const img = Loader::getImage(str); if (img != nullptr) { img->incRef(); mLineParts.push_back(LinePart(CAST_S32(x), CAST_S32(y) + mItemPadding, selColor[0], selColor[1], img)); y += CAST_U32(img->getHeight() + 2); moreHeight += img->getHeight(); if (img->getWidth() > maxWidth) maxWidth = img->getWidth() + 2; } continue; } Color prevColor[2]; prevColor[0] = selColor[0]; prevColor[1] = selColor[1]; bold = false; const int xPadding = CAST_S32(mNewLinePadding) + mPadding; for (size_t start = 0, end = std::string::npos; start != std::string::npos; start = end, end = std::string::npos) { bool processed(false); // Wrapped line continuation shall be indented if (wrapped) { y += CAST_U32(fontHeight); x = CAST_U32(xPadding); wrapped = false; } size_t idx1 = end; size_t idx2 = end; // "Tokenize" the string at control sequences if (mUseLinksAndUserColors) idx1 = row.find("##", start + 1); if (start == 0 || mUseLinksAndUserColors) { // Check for color change in format "##x", x = [L,P,0..9] if (row.find("##", start) == start && row.size() > start + 2) { const signed char c = row.at(start + 2); bool valid(false); const Color col[2] = { getThemeCharColor(c, valid), getThemeCharColor(CAST_S8( c | 0x80), valid) }; if (c == '>') { selColor[0] = prevColor[0]; selColor[1] = prevColor[1]; } else if (c == '<') { prevColor[0] = selColor[0]; prevColor[1] = selColor[1]; selColor[0] = col[0]; selColor[1] = col[1]; } else if (c == 'B') { bold = true; } else if (c == 'b') { bold = false; } else if (valid) { selColor[0] = col[0]; selColor[1] = col[1]; } else { switch (c) { case '0': selColor[0] = mColors[0][ColorName::BLACK]; selColor[1] = mColors[1][ColorName::BLACK]; break; case '1': selColor[0] = mColors[0][ColorName::RED]; selColor[1] = mColors[1][ColorName::RED]; break; case '2': selColor[0] = mColors[0][ColorName::GREEN]; selColor[1] = mColors[1][ColorName::GREEN]; break; case '3': selColor[0] = mColors[0][ColorName::BLUE]; selColor[1] = mColors[1][ColorName::BLUE]; break; case '4': selColor[0] = mColors[0][ColorName::ORANGE]; selColor[1] = mColors[1][ColorName::ORANGE]; break; case '5': selColor[0] = mColors[0][ColorName::YELLOW]; selColor[1] = mColors[1][ColorName::YELLOW]; break; case '6': selColor[0] = mColors[0][ColorName::PINK]; selColor[1] = mColors[1][ColorName::PINK]; break; case '7': selColor[0] = mColors[0][ColorName::PURPLE]; selColor[1] = mColors[1][ColorName::PURPLE]; break; case '8': selColor[0] = mColors[0][ColorName::GRAY]; selColor[1] = mColors[1][ColorName::GRAY]; break; case '9': selColor[0] = mColors[0][ColorName::BROWN]; selColor[1] = mColors[1][ColorName::BROWN]; break; default: selColor[0] = textColor[0]; selColor[1] = textColor[1]; break; } } if (c == '<' && link < CAST_S32(mLinks.size())) { int size; if (bold) { size = boldFont->getWidth( mLinks[CAST_SIZE(link)].caption) + 1; } else { size = font->getWidth( mLinks[CAST_SIZE(link)].caption) + 1; } BrowserLink &linkRef = mLinks[CAST_SIZE( link)]; linkRef.x1 = CAST_S32(x); linkRef.y1 = CAST_S32(y); linkRef.x2 = linkRef.x1 + size; linkRef.y2 = CAST_S32(y) + fontHeight - 1; link++; } processed = true; start += 3; if (start == row.size()) break; } } if (mUseEmotes) idx2 = row.find("%%", start + 1); if (idx1 < idx2) end = idx1; else end = idx2; if (mUseEmotes) { // check for emote icons if (row.size() > start + 2 && row.substr(start, 2) == "%%") { if (objects < 5) { const int cid = row.at(start + 2) - '0'; if (cid >= 0) { if (mEmotes != nullptr) { const size_t sz = mEmotes->size(); if (CAST_SIZE(cid) < sz) { Image *const img = mEmotes->get( CAST_SIZE(cid)); if (img != nullptr) { mLineParts.push_back(LinePart( CAST_S32(x), CAST_S32(y) + mItemPadding, selColor[0], selColor[1], img)); x += 18; } } } } objects ++; processed = true; } start += 3; if (start == row.size()) { if (x > mDataWidth) mDataWidth = x; break; } } } const size_t len = (end == std::string::npos) ? end : end - start; if (start >= row.length()) break; std::string part = row.substr(start, len); int width = 0; if (bold) width = boldFont->getWidth(part); else width = font->getWidth(part); // Auto wrap mode if (wWidth > 0 && width > 0 && (x + CAST_U32(width) + 10) > wWidth) { bool forced = false; /* FIXME: This code layout makes it easy to crash remote clients by talking garbage. Forged long utf-8 characters will cause either a buffer underflow in substr or an infinite loop in the main loop. */ do { if (!forced) end = row.rfind(' ', end); // Check if we have to (stupidly) force-wrap if (end == std::string::npos || end <= start) { forced = true; end = row.size(); x += CAST_U32(hyphenWidth); continue; } // Skip to the start of the current character while ((row[end] & 192) == 128) end--; end--; // And then to the last byte of the previous one part = row.substr(start, end - start + 1); if (bold) width = boldFont->getWidth(part); else width = font->getWidth(part); } while (end > start && width > 0 && (x + CAST_U32(width) + 10) > wWidth); if (forced) { x -= CAST_U32(hyphenWidth); mLineParts.push_back(LinePart( CAST_S32(wWidth) - hyphenWidth, CAST_S32(y) + mItemPadding, selColor[0], selColor[1], hyphen, bold)); end++; // Skip to the next character } else { end += 2; // Skip to after the space } wrapped = true; wrappedLines++; } mLineParts.push_back(LinePart(CAST_S32(x), CAST_S32(y) + mItemPadding, selColor[0], selColor[1], part.c_str(), bold)); if (bold) width = boldFont->getWidth(part); else width = font->getWidth(part); if (width == 0 && !processed) break; x += CAST_U32(width); if (x > mDataWidth) mDataWidth = x; } y += CAST_U32(fontHeight); } if (CAST_S32(wWidth) != maxWidth) setWidth(maxWidth); return (CAST_S32(mTextRows.size()) + wrappedLines) * fontHeight + moreHeight + 2 * mPadding; } void BrowserBox::updateHeight() { if (mAlwaysUpdate || mUpdateTime != cur_time || mTextRows.size() < 3 || (mUpdateTime == 0)) { mWidth = mDimension.width; mHeight = calcHeight(); setHeight(mHeight); mUpdateTime = cur_time; } } void BrowserBox::updateSize(const bool always) { if (always) mUpdateTime = 0; updateHeight(); } std::string BrowserBox::getTextAtPos(const int x, const int y) const { int textX = 0; int textY = 0; getAbsolutePosition(textX, textY); if (x < textX || y < textY) return std::string(); textY = y - textY; std::string str; int lastY = 0; FOR_EACH (LinePartCIter, i, mLineParts) { const LinePart &part = *i; if (part.mY + 50 < mYStart) continue; if (part.mY > textY) break; if (part.mY > lastY) { str = part.mText; lastY = part.mY; } else { str.append(part.mText); } } return str; } void BrowserBox::setForegroundColorAll(const Color &color1, const Color &color2) { mForegroundColor = color1; mForegroundColor2 = color2; } void BrowserBox::moveSelectionUp() { if (mSelectedLink <= 0) mSelectedLink = CAST_S32(mLinks.size()) - 1; else mSelectedLink --; } void BrowserBox::moveSelectionDown() { mSelectedLink ++; if (mSelectedLink >= static_cast(mLinks.size())) mSelectedLink = 0; } void BrowserBox::selectSelection() { if ((mLinkHandler == nullptr) || mSelectedLink < 0 || mSelectedLink >= static_cast(mLinks.size())) { return; } mLinkHandler->handleLink(mLinks[CAST_SIZE(mSelectedLink)].link, nullptr); } void BrowserBox::widgetResized(const Event &event A_UNUSED) { updateHeight(); }