You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openmw-tes3mp/apps/openmw/mwgui/formatting.cpp

513 lines
19 KiB
C++

#include "formatting.hpp"
#include <MyGUI_EditText.h>
#include <MyGUI_Gui.h>
#include <MyGUI_EditBox.h>
#include <MyGUI_ImageBox.h>
#include <MyGUI_FontManager.h>
// correctBookartPath
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
#include <boost/algorithm/string/replace.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <components/interpreter/defines.hpp>
#include <components/misc/stringops.hpp>
#include "../mwscript/interpretercontext.hpp"
namespace MWGui
{
namespace Formatting
{
/* BookTextParser */
BookTextParser::BookTextParser(const std::string & text)
: mIndex(0), mText(text), mIgnoreNewlineTags(true), mIgnoreLineEndings(true), mClosingTag(false)
{
MWScript::InterpreterContext interpreterContext(NULL, MWWorld::Ptr()); // empty arguments, because there is no locals or actor
mText = Interpreter::fixDefinesBook(mText, interpreterContext);
boost::algorithm::replace_all(mText, "\r", "");
// vanilla game does not show any text after the last EOL tag.
const std::string lowerText = Misc::StringUtils::lowerCase(mText);
int brIndex = lowerText.rfind("<br>");
int pIndex = lowerText.rfind("<p>");
if (brIndex == pIndex)
mText = "";
else if (brIndex > pIndex)
mText = mText.substr(0, brIndex+4);
else
mText = mText.substr(0, pIndex+3);
registerTag("br", Event_BrTag);
registerTag("p", Event_PTag);
registerTag("img", Event_ImgTag);
registerTag("div", Event_DivTag);
registerTag("font", Event_FontTag);
}
void BookTextParser::registerTag(const std::string & tag, BookTextParser::Events type)
{
mTagTypes[tag] = type;
}
std::string BookTextParser::getReadyText() const
{
return mReadyText;
}
BookTextParser::Events BookTextParser::next()
{
while (mIndex < mText.size())
{
char ch = mText[mIndex];
if (ch == '<')
{
const size_t tagStart = mIndex + 1;
const size_t tagEnd = mText.find('>', tagStart);
if (tagEnd == std::string::npos)
throw std::runtime_error("BookTextParser Error: Tag is not terminated");
parseTag(mText.substr(tagStart, tagEnd - tagStart));
mIndex = tagEnd;
if (mTagTypes.find(mTag) != mTagTypes.end())
{
Events type = mTagTypes.at(mTag);
if (type == Event_BrTag || type == Event_PTag)
{
if (!mIgnoreNewlineTags)
{
if (type == Event_BrTag)
mBuffer.push_back('\n');
else
{
mBuffer.append("\n\n");
}
}
mIgnoreLineEndings = true;
}
else
flushBuffer();
if (type == Event_ImgTag)
{
mIgnoreNewlineTags = false;
}
++mIndex;
return type;
}
}
else
{
if (!mIgnoreLineEndings || ch != '\n')
{
mBuffer.push_back(ch);
mIgnoreLineEndings = false;
mIgnoreNewlineTags = false;
}
}
++mIndex;
}
flushBuffer();
return Event_EOF;
}
void BookTextParser::flushBuffer()
{
mReadyText = mBuffer;
mBuffer.clear();
}
const BookTextParser::Attributes & BookTextParser::getAttributes() const
{
return mAttributes;
}
bool BookTextParser::isClosingTag() const
{
return mClosingTag;
}
void BookTextParser::parseTag(std::string tag)
{
size_t tagNameEndPos = tag.find(' ');
mAttributes.clear();
mTag = tag.substr(0, tagNameEndPos);
Misc::StringUtils::lowerCaseInPlace(mTag);
if (mTag.empty())
return;
mClosingTag = (mTag[0] == '/');
if (mClosingTag)
{
mTag.erase(mTag.begin());
return;
}
if (tagNameEndPos == std::string::npos)
return;
tag.erase(0, tagNameEndPos+1);
while (!tag.empty())
{
size_t sepPos = tag.find('=');
if (sepPos == std::string::npos)
return;
std::string key = tag.substr(0, sepPos);
Misc::StringUtils::lowerCaseInPlace(key);
tag.erase(0, sepPos+1);
std::string value;
if (tag.empty())
return;
if (tag[0] == '"')
{
size_t quoteEndPos = tag.find('"', 1);
if (quoteEndPos == std::string::npos)
throw std::runtime_error("BookTextParser Error: Missing end quote in tag");
value = tag.substr(1, quoteEndPos-1);
tag.erase(0, quoteEndPos+2);
}
else
{
size_t valEndPos = tag.find(' ');
if (valEndPos == std::string::npos)
{
value = tag;
tag.erase();
}
else
{
value = tag.substr(0, valEndPos);
tag.erase(0, valEndPos+1);
}
}
mAttributes[key] = value;
}
}
/* BookFormatter */
Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget * parent, const std::string & markup, const int pageWidth, const int pageHeight)
{
Paginator pag(pageWidth, pageHeight);
while (parent->getChildCount())
{
MyGUI::Gui::getInstance().destroyWidget(parent->getChildAt(0));
}
mTextStyle = TextStyle();
mBlockStyle = BlockStyle();
MyGUI::Widget * paper = parent->createWidget<MyGUI::Widget>("Widget", MyGUI::IntCoord(0, 0, pag.getPageWidth(), pag.getPageHeight()), MyGUI::Align::Left | MyGUI::Align::Top);
paper->setNeedMouseFocus(false);
BookTextParser parser(markup);
bool brBeforeLastTag = false;
bool isPrevImg = false;
for (;;)
{
BookTextParser::Events event = parser.next();
if (event == BookTextParser::Event_BrTag || event == BookTextParser::Event_PTag)
continue;
std::string plainText = parser.getReadyText();
// for cases when linebreaks are used to cause a shift to the next page
// if the split text block ends in an empty line, proceeding text block(s) should have leading empty lines removed
if (pag.getIgnoreLeadingEmptyLines())
{
while (!plainText.empty())
{
if (plainText[0] == '\n')
plainText.erase(plainText.begin());
else
{
pag.setIgnoreLeadingEmptyLines(false);
break;
}
}
}
if (plainText.empty())
brBeforeLastTag = true;
else
{
// Each block of text (between two tags / boundary and tag) will be displayed in a separate editbox widget,
// which means an additional linebreak will be created between them.
// ^ This is not what vanilla MW assumes, so we must deal with line breaks around tags appropriately.
bool brAtStart = (plainText[0] == '\n');
bool brAtEnd = (plainText[plainText.size()-1] == '\n');
if (brAtStart && !brBeforeLastTag && !isPrevImg)
plainText.erase(plainText.begin());
if (plainText.size() && brAtEnd)
plainText.erase(plainText.end()-1);
#if (MYGUI_VERSION < MYGUI_DEFINE_VERSION(3, 2, 2))
// splitting won't be fully functional until 3.2.2 (see TextElement::pageSplit())
// hack: prevent newlines at the end of the book possibly creating unnecessary pages
if (event == BookTextParser::Event_EOF)
{
while (plainText.size() && plainText[plainText.size()-1] == '\n')
plainText.erase(plainText.end()-1);
}
#endif
if (!plainText.empty() || brBeforeLastTag || isPrevImg)
{
TextElement elem(paper, pag, mBlockStyle,
mTextStyle, plainText);
elem.paginate();
}
brBeforeLastTag = brAtEnd;
}
if (event == BookTextParser::Event_EOF)
break;
isPrevImg = (event == BookTextParser::Event_ImgTag);
switch (event)
{
case BookTextParser::Event_ImgTag:
{
const BookTextParser::Attributes & attr = parser.getAttributes();
if (attr.find("src") == attr.end() || attr.find("width") == attr.end() || attr.find("height") == attr.end())
continue;
std::string src = attr.at("src");
int width = MyGUI::utility::parseInt(attr.at("width"));
int height = MyGUI::utility::parseInt(attr.at("height"));
bool exists;
std::string correctedSrc = MWBase::Environment::get().getWindowManager()->correctBookartPath(src, width, height, &exists);
if (!exists)
{
std::cerr << "Warning: Could not find \"" << src << "\" referenced by an <img> tag." << std::endl;
break;
}
pag.setIgnoreLeadingEmptyLines(false);
ImageElement elem(paper, pag, mBlockStyle,
correctedSrc, width, height);
elem.paginate();
break;
}
case BookTextParser::Event_FontTag:
if (parser.isClosingTag())
resetFontProperties();
else
handleFont(parser.getAttributes());
break;
case BookTextParser::Event_DivTag:
handleDiv(parser.getAttributes());
break;
default:
break;
}
}
// insert last page
if (pag.getStartTop() != pag.getCurrentTop())
pag << Paginator::Page(pag.getStartTop(), pag.getStartTop() + pag.getPageHeight());
paper->setSize(paper->getWidth(), pag.getCurrentTop());
return pag.getPages();
}
Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget * parent, const std::string & markup)
{
return markupToWidget(parent, markup, parent->getWidth(), parent->getHeight());
}
void BookFormatter::resetFontProperties()
{
mTextStyle = TextStyle();
}
void BookFormatter::handleDiv(const BookTextParser::Attributes & attr)
{
if (attr.find("align") == attr.end())
return;
std::string align = attr.at("align");
if (Misc::StringUtils::ciEqual(align, "center"))
mBlockStyle.mAlign = MyGUI::Align::HCenter;
else if (Misc::StringUtils::ciEqual(align, "left"))
mBlockStyle.mAlign = MyGUI::Align::Left;
else if (Misc::StringUtils::ciEqual(align, "right"))
mBlockStyle.mAlign = MyGUI::Align::Right;
}
void BookFormatter::handleFont(const BookTextParser::Attributes & attr)
{
if (attr.find("color") != attr.end())
{
unsigned int color;
std::stringstream ss;
ss << attr.at("color");
ss >> std::hex >> color;
mTextStyle.mColour = MyGUI::Colour(
(color>>16 & 0xFF) / 255.f,
(color>>8 & 0xFF) / 255.f,
(color & 0xFF) / 255.f);
}
if (attr.find("face") != attr.end())
{
std::string face = attr.at("face");
mTextStyle.mFont = face;
}
if (attr.find("size") != attr.end())
{
/// \todo
}
}
/* GraphicElement */
GraphicElement::GraphicElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle)
: mParent(parent), mPaginator(pag), mBlockStyle(blockStyle)
{
}
void GraphicElement::paginate()
{
int newTop = mPaginator.getCurrentTop() + getHeight();
while (newTop-mPaginator.getStartTop() > mPaginator.getPageHeight())
{
int newStartTop = pageSplit();
mPaginator << Paginator::Page(mPaginator.getStartTop(), newStartTop);
mPaginator.setStartTop(newStartTop);
}
mPaginator.setCurrentTop(newTop);
}
int GraphicElement::pageSplit()
{
return mPaginator.getStartTop() + mPaginator.getPageHeight();
}
/* TextElement */
TextElement::TextElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle,
const TextStyle & textStyle, const std::string & text)
: GraphicElement(parent, pag, blockStyle),
mTextStyle(textStyle)
{
MyGUI::EditBox* box = parent->createWidget<MyGUI::EditBox>("NormalText",
MyGUI::IntCoord(0, pag.getCurrentTop(), pag.getPageWidth(), 0), MyGUI::Align::Left | MyGUI::Align::Top,
parent->getName() + MyGUI::utility::toString(parent->getChildCount()));
box->setEditStatic(true);
box->setEditMultiLine(true);
box->setEditWordWrap(true);
box->setNeedMouseFocus(false);
box->setNeedKeyFocus(false);
box->setMaxTextLength(text.size());
box->setTextAlign(mBlockStyle.mAlign);
box->setTextColour(mTextStyle.mColour);
box->setFontName(mTextStyle.mFont);
box->setCaption(MyGUI::TextIterator::toTagsString(text));
box->setSize(box->getSize().width, box->getTextSize().height);
mEditBox = box;
}
int TextElement::currentFontHeight() const
{
std::string fontName(mTextStyle.mFont == "Default" ? MyGUI::FontManager::getInstance().getDefaultFont() : mTextStyle.mFont);
MyGUI::IFont* font = MyGUI::FontManager::getInstance().getByName(fontName);
if (!font)
return 0;
return font->getDefaultHeight();
}
int TextElement::getHeight()
{
return mEditBox->getTextSize().height;
}
int TextElement::pageSplit()
{
// split lines
const int lineHeight = currentFontHeight();
unsigned int lastLine = (mPaginator.getStartTop() + mPaginator.getPageHeight() - mPaginator.getCurrentTop());
if (lineHeight > 0)
lastLine /= lineHeight;
int ret = mPaginator.getCurrentTop() + lastLine * lineHeight;
// first empty lines that would go to the next page should be ignored
// unfortunately, getLineInfo method won't be available until 3.2.2
#if (MYGUI_VERSION >= MYGUI_DEFINE_VERSION(3, 2, 2))
mPaginator.setIgnoreLeadingEmptyLines(true);
const MyGUI::VectorLineInfo & lines = mEditBox->getSubWidgetText()->castType<MyGUI::EditText>()->getLineInfo();
for (unsigned int i = lastLine; i < lines.size(); ++i)
{
if (lines[i].width == 0)
ret += lineHeight;
else
{
mPaginator.setIgnoreLeadingEmptyLines(false);
break;
}
}
#endif
return ret;
}
/* ImageElement */
ImageElement::ImageElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle,
const std::string & src, int width, int height)
: GraphicElement(parent, pag, blockStyle),
mImageHeight(height)
{
int left = 0;
if (mBlockStyle.mAlign.isHCenter())
left += (pag.getPageWidth() - width) / 2;
else if (mBlockStyle.mAlign.isLeft())
left = 0;
else if (mBlockStyle.mAlign.isRight())
left += pag.getPageWidth() - width;
mImageBox = parent->createWidget<MyGUI::ImageBox> ("ImageBox",
MyGUI::IntCoord(left, pag.getCurrentTop(), width, mImageHeight), MyGUI::Align::Left | MyGUI::Align::Top,
parent->getName() + MyGUI::utility::toString(parent->getChildCount()));
mImageBox->setImageTexture(src);
mImageBox->setProperty("NeedMouse", "false");
}
int ImageElement::getHeight()
{
return mImageHeight;
}
int ImageElement::pageSplit()
{
// if the image is larger than the page, fall back to the default pageSplit implementation
if (mImageHeight > mPaginator.getPageHeight())
return GraphicElement::pageSplit();
return mPaginator.getCurrentTop();
}
}
}