/* * The Mana Client * Copyright (C) 2004-2009 The Mana World Development Team * Copyright (C) 2009-2012 The Mana Developers * Copyright (C) 2009 Aethyra Development Team * * This file is part of The Mana 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 "gui/gui.h" #include "gui/truetypefont.h" #include "gui/widgets/linkhandler.h" #include "resources/itemdb.h" #include "resources/iteminfo.h" #include "resources/theme.h" #include "utils/stringutils.h" #include #include #include #include struct LayoutContext { LayoutContext(gcn::Font *font); int y = 0; gcn::Font *font; const int fontHeight; const int minusWidth; const int tildeWidth; int lineHeight; gcn::Color selColor; const gcn::Color textColor; }; LayoutContext::LayoutContext(gcn::Font *font) : font(font) , fontHeight(font->getHeight()) , minusWidth(font->getWidth("-")) , tildeWidth(font->getWidth("~")) , lineHeight(fontHeight) , selColor(Theme::getThemeColor(Theme::TEXT)) , textColor(Theme::getThemeColor(Theme::TEXT)) { if (auto *trueTypeFont = dynamic_cast(font)) lineHeight = trueTypeFont->getLineHeight(); } BrowserBox::BrowserBox(Mode mode): mMode(mode) { setFocusable(true); addMouseListener(this); } BrowserBox::~BrowserBox() = default; void BrowserBox::addRow(const std::string &row) { TextRow &newRow = mTextRows.emplace_back(); // Use links and user defined colors if (mUseLinksAndUserColors) { std::string tmp = row; // Check for links in format "@@link|Caption@@" auto idx1 = tmp.find("@@"); while (idx1 != std::string::npos) { const auto idx2 = tmp.find("|", idx1); const auto idx3 = tmp.find("@@", idx2); if (idx2 == std::string::npos || idx3 == std::string::npos) break; BrowserLink &link = newRow.links.emplace_back(); link.link = tmp.substr(idx1 + 2, idx2 - (idx1 + 2)); link.caption = tmp.substr(idx2 + 1, idx3 - (idx2 + 1)); if (link.caption.empty()) { const int id = atoi(link.link.c_str()); if (id) link.caption = itemDb->get(id).name; else link.caption = link.link; } newRow.text += tmp.substr(0, idx1); newRow.text += "##<" + link.caption; tmp.erase(0, idx3 + 2); if (!tmp.empty()) { newRow.text += "##>"; } idx1 = tmp.find("@@"); } newRow.text += tmp; } // Don't use links and user defined colors else { newRow.text = row; } // Layout the newly added row LayoutContext context(getFont()); context.y = getHeight(); layoutTextRow(newRow, context); // Auto size mode if (mMode == AUTO_SIZE && newRow.width > getWidth()) setWidth(newRow.width); // Discard older rows when a row limit has been set // (this might invalidate the newRow reference) int removedHeight = 0; while (mMaxRows > 0 && mTextRows.size() > mMaxRows) { removedHeight += mTextRows.front().height; mTextRows.pop_front(); } if (removedHeight > 0) { for (auto &row : mTextRows) { for (auto &part : row.parts) part.y -= removedHeight; for (auto &link : row.links) link.rect.y -= removedHeight; } } setHeight(context.y - removedHeight); } void BrowserBox::clearRows() { mTextRows.clear(); setSize(0, 0); mHoveredLink.reset(); maybeRelayoutText(); } void BrowserBox::mousePressed(gcn::MouseEvent &event) { if (!mLinkHandler) return; updateHoveredLink(event.getX(), event.getY()); if (mHoveredLink) { mLinkHandler->handleLink(mHoveredLink->link); gui->setCursorType(Cursor::POINTER); } } void BrowserBox::mouseMoved(gcn::MouseEvent &event) { updateHoveredLink(event.getX(), event.getY()); gui->setCursorType(mHoveredLink ? Cursor::HAND : Cursor::POINTER); event.consume(); // Suppress mouse cursor change by parent } void BrowserBox::mouseExited(gcn::MouseEvent &event) { mHoveredLink.reset(); } void BrowserBox::draw(gcn::Graphics *graphics) { const gcn::ClipRectangle &cr = graphics->getCurrentClipArea(); int yStart = cr.y - cr.yOffset; int yEnd = yStart + cr.height; if (yStart < 0) yStart = 0; if (getWidth() != mLastLayoutWidth) maybeRelayoutText(); if (mHoveredLink) { auto &link = *mHoveredLink; const gcn::Rectangle &rect = link.rect; if (mHighlightMode & BACKGROUND) { graphics->setColor(Theme::getThemeColor(Theme::HIGHLIGHT)); graphics->fillRectangle(rect); } if (mHighlightMode & UNDERLINE) { graphics->setColor(Theme::getThemeColor(Theme::HYPERLINK)); graphics->drawLine(rect.x, rect.y + rect.height, rect.x + rect.width, rect.y + rect.height); } } for (const auto &row : mTextRows) { for (const auto &part : row.parts) { if (part.y + 50 < yStart) continue; if (part.y > yEnd) return; auto font = part.font; // Handle text shadows if (mShadows) { graphics->setColor(Theme::getThemeColor(Theme::SHADOW, part.color.a / 2)); if (mOutline) font->drawString(graphics, part.text, part.x + 2, part.y + 2); else font->drawString(graphics, part.text, part.x + 1, part.y + 1); } if (mOutline) { // Text outline graphics->setColor(Theme::getThemeColor(Theme::OUTLINE, part.color.a / 4)); font->drawString(graphics, part.text, part.x + 1, part.y); font->drawString(graphics, part.text, part.x - 1, part.y); font->drawString(graphics, part.text, part.x, part.y + 1); font->drawString(graphics, part.text, part.x, part.y - 1); } // the main text graphics->setColor(part.color); font->drawString(graphics, part.text, part.x, part.y); } } } /** * Relayouts all text rows and returns the new height of the BrowserBox. */ void BrowserBox::relayoutText() { LayoutContext context(getFont()); for (auto &row : mTextRows) layoutTextRow(row, context); mLastLayoutWidth = getWidth(); mLayoutTimer.set(100); setHeight(context.y); } /** * Layers out the given \a row of text starting at the given \a context position. * @return the context position for the next row. */ void BrowserBox::layoutTextRow(TextRow &row, LayoutContext &context) { // each line starts with normal font in default color context.font = getFont(); context.selColor = context.textColor; const int startY = context.y; row.parts.clear(); unsigned linkIndex = 0; bool wrapped = false; int x = 0; // Check for separator lines if (startsWith(row.text, "---")) { for (x = 0; x < getWidth(); x += context.minusWidth - 1) { row.parts.push_back(LinePart { x, context.y, context.selColor, "-", context.font }); } context.y += row.height; row.width = getWidth(); row.height = context.y - startY; return; } gcn::Color prevColor = context.selColor; // TODO: Check if we must take texture size limits into account here // TODO: Check if some of the O(n) calls can be removed for (std::string::size_type start = 0, end = std::string::npos; start != std::string::npos; start = end, end = std::string::npos) { // Wrapped line continuation shall be indented if (wrapped) { context.y += context.lineHeight; x = mWrapIndent; wrapped = false; } if (mUseLinksAndUserColors || start == 0) { // Check for color or font change in format "##x", x = [<,>,B,p,0..9] while (row.text.size() > start + 2 && row.text.find("##", start) == start) { const char c = row.text.at(start + 2); start += 3; bool valid; const gcn::Color &col = Theme::getThemeColor(c, valid); if (c == '>') { context.selColor = prevColor; } else if (c == '<') { prevColor = context.selColor; context.selColor = col; } else if (c == 'B') { context.font = boldFont; } else if (c == 'b') { context.font = getFont(); } else if (valid) { context.selColor = col; } else switch (c) { case '1': context.selColor = RED; break; case '2': context.selColor = GREEN; break; case '3': context.selColor = BLUE; break; case '4': context.selColor = ORANGE; break; case '5': context.selColor = YELLOW; break; case '6': context.selColor = PINK; break; case '7': context.selColor = PURPLE; break; case '8': context.selColor = GRAY; break; case '9': context.selColor = BROWN; break; case '0': default: context.selColor = context.textColor; } // Update the position of the links if (c == '<' && linkIndex < row.links.size()) { auto &link = row.links[linkIndex]; link.rect.x = x; link.rect.y = context.y; link.rect.width = context.font->getWidth(link.caption) + 1; link.rect.height = context.fontHeight - 1; linkIndex++; } } } if (start >= row.text.length()) break; // "Tokenize" the string at control sequences if (mUseLinksAndUserColors) end = row.text.find("##", start + 1); std::string::size_type len = end == std::string::npos ? end : end - start; std::string part = row.text.substr(start, len); int partWidth = context.font->getWidth(part); // Auto wrap mode if (mMode == AUTO_WRAP && getWidth() > 0 && partWidth > 0 && (x + partWidth) > getWidth()) { 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.text.rfind(' ', end); // Check if we have to (stupidly) force-wrap if (end == std::string::npos || end <= start) { forced = true; end = row.text.size(); x += context.tildeWidth; // Account for the wrap-notifier continue; } // Skip to the start of the current character while ((row.text[end] & 192) == 128) end--; end--; // And then to the last byte of the previous one part = row.text.substr(start, end - start + 1); partWidth = context.font->getWidth(part); } while (end > start && partWidth > 0 && (x + partWidth) > getWidth()); if (forced) { x -= context.tildeWidth; // Remove the wrap-notifier accounting row.parts.push_back(LinePart { getWidth() - context.tildeWidth, context.y, context.selColor, "~", getFont() }); end++; // Skip to the next character } else { end += 2; // Skip to after the space } wrapped = true; } row.parts.push_back(LinePart { x, context.y, context.selColor, std::move(part), context.font }); row.width = std::max(row.width, x + partWidth); if (mMode == AUTO_WRAP && partWidth == 0) break; x += partWidth; } context.y += context.lineHeight; row.height = context.y - startY; } void BrowserBox::updateHoveredLink(int x, int y) { mHoveredLink.reset(); for (const auto &row : mTextRows) { for (const auto &link : row.links) { if (link.contains(x, y)) { mHoveredLink = link; return; } } } } void BrowserBox::maybeRelayoutText() { // Reduce relayouting frequency when there is a lot of text if (mTextRows.size() > 100) if (!mLayoutTimer.passed()) return; relayoutText(); }