/* 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 . * */ #include "common/file.h" #include "common/timer.h" #include "common/tokenizer.h" #include "common/unicode-bidi.h" #include "common/compression/unzip.h" #include "graphics/font.h" #include "graphics/macgui/mactext.h" #include "graphics/macgui/macwindowmanager.h" #include "graphics/macgui/macfontmanager.h" #include "graphics/macgui/macmenu.h" #include "graphics/macgui/macwidget.h" #include "graphics/macgui/macwindow.h" #ifdef USE_PNG #include "image/png.h" #endif namespace Graphics { enum { kConScrollStep = 12, kCursorMaxHeight = 100 }; static void cursorTimerHandler(void *refCon); #define DEBUG 0 #if DEBUG #define D(...) debug(__VA_ARGS__) #else #define D(...) ; #endif const Font *MacFontRun::getFont() { if (font) return font; MacFont macFont = MacFont(fontId, fontSize, textSlant); font = wm->_fontMan->getFont(macFont); return font; } const Common::String MacFontRun::toString() { return Common::String::format("\001\016%04x%02x%04x%04x%04x%04x", fontId, textSlant, fontSize, palinfo1, palinfo2, palinfo3); } bool MacFontRun::equals(MacFontRun &to) { return (fontId == to.fontId && textSlant == to.textSlant && fontSize == to.fontSize && palinfo1 == to.palinfo1 && palinfo2 == to.palinfo2 && palinfo3 == to.palinfo3); } Common::CodePage MacFontRun::getEncoding() { if (wm->_mode & kWMModeWin95) return Common::kUtf8; return wm->_fontMan->getFontEncoding(fontId); } bool MacFontRun::plainByteMode() { Common::CodePage encoding = getEncoding(); // This return statement accounts for utf8, invalid. // For future Unicode font compatibility, it should account for all codepages instead. return encoding != Common::kUtf8 && encoding != Common::kCodePageInvalid; } Common::String MacFontRun::getEncodedText() { Common::CodePage encoding = getEncoding(); return Common::convertFromU32String(text, encoding); } uint MacTextLine::getChunkNum(int *col) { int pos = *col; uint i; for (i = 0; i < chunks.size(); i++) { if (pos >= (int)chunks[i].text.size()) { pos -= chunks[i].text.size(); } else { break; } } if (i == chunks.size()) { i--; // touch the last chunk pos = chunks[i].text.size(); } *col = pos; return i; } MacText::MacText(MacWidget *parent, int x, int y, int w, int h, MacWindowManager *wm, const Common::U32String &s, const MacFont *macFont, uint32 fgcolor, uint32 bgcolor, int maxWidth, TextAlign textAlignment, int interlinear, uint16 border, uint16 gutter, uint16 boxShadow, uint16 textShadow, bool fixedDims) : MacWidget(parent, x, y, w, h, wm, true, border, gutter, boxShadow), _macFont(macFont) { D(6, "MacText::MacText(): fgcolor: %d, bgcolor: %d s: \"%s\"", fgcolor, bgcolor, Common::toPrintable(s.encode()).c_str()); _str = s; _fullRefresh = true; _fixedDims = fixedDims; _wm = wm; _canvas._maxWidth = maxWidth; _canvas._textAlignment = textAlignment; _canvas._textShadow = textShadow; _canvas._interLinear = interlinear; _canvas._wm = wm; _canvas._tfgcolor = fgcolor; _canvas._tbgcolor = bgcolor; _canvas._macFontMode = true; _canvas._macText = this; if (macFont) { _defaultFormatting = MacFontRun(_wm); _defaultFormatting.font = wm->_fontMan->getFont(*macFont); byte r, g, b; _wm->_pixelformat.colorToRGB(fgcolor, r, g, b); _defaultFormatting.setValues(_wm, macFont->getId(), macFont->getSlant(), macFont->getSize(), r, g, b); } else { _defaultFormatting.font = NULL; } init(); } // NOTE: This constructor and the one afterward are for MacText engines that don't use widgets. This is the classic was MacText was constructed. MacText::MacText(const Common::U32String &s, MacWindowManager *wm, const MacFont *macFont, uint32 fgcolor, uint32 bgcolor, int maxWidth, TextAlign textAlignment, int interlinear, bool fixedDims) : MacWidget(nullptr, 0, 0, 0, 0, wm, false, 0, 0, 0), _macFont(macFont) { _str = s; _fixedDims = fixedDims; _wm = wm; _canvas._maxWidth = maxWidth; _canvas._textAlignment = textAlignment; _canvas._textShadow = 0; _canvas._interLinear = interlinear; _canvas._wm = wm; _canvas._tfgcolor = fgcolor; _canvas._tbgcolor = bgcolor; _canvas._macFontMode = true; _canvas._macText = this; if (macFont) { _defaultFormatting = MacFontRun(_wm, macFont->getId(), macFont->getSlant(), macFont->getSize(), 0, 0, 0); _defaultFormatting.font = wm->_fontMan->getFont(*macFont); byte r, g, b; _wm->_pixelformat.colorToRGB(fgcolor, r, g, b); _defaultFormatting.setValues(_wm, macFont->getId(), macFont->getSlant(), macFont->getSize(), r, g, b); } else { _defaultFormatting.font = NULL; } init(); } // Working with plain Font MacText::MacText(const Common::U32String &s, MacWindowManager *wm, const Font *font, uint32 fgcolor, uint32 bgcolor, int maxWidth, TextAlign textAlignment, int interlinear, bool fixedDims) : MacWidget(nullptr, 0, 0, 0, 0, wm, false, 0, 0, 0), _macFont(nullptr) { _str = s; _fixedDims = fixedDims; _wm = wm; _canvas._maxWidth = maxWidth; _canvas._textAlignment = textAlignment; _canvas._textShadow = 0; _canvas._interLinear = interlinear; _canvas._wm = wm; _canvas._tfgcolor = fgcolor; _canvas._tbgcolor = bgcolor; _canvas._macFontMode = false; _canvas._macText = this; if (font) { _defaultFormatting = MacFontRun(_wm, font, 0, font->getFontHeight(), 0, 0, 0); _defaultFormatting.font = font; } else { _defaultFormatting.font = NULL; } init(); } void MacText::init() { _fullRefresh = true; _canvas._textMaxWidth = 0; _canvas._textMaxHeight = 0; _canvas._surface = nullptr; _canvas._shadowSurface = nullptr; if (!_fixedDims) { int right = _dims.right; _dims.right = MAX(_dims.right, _dims.left + _canvas._maxWidth + (2 * _border) + (2 * _gutter) + _shadow); if (right != _dims.right) { delete _composeSurface; _composeSurface = new ManagedSurface(_dims.width(), _dims.height(), _wm->_pixelformat); } } _selEnd = -1; _selStart = -1; _defaultFormatting.wm = _wm; _canvas.splitString(_str, -1, _defaultFormatting); recalcDims(); _fullRefresh = true; _inTextSelection = false; _scrollPos = 0; _editable = false; _selectable = false; _editableRow = 0; _menu = nullptr; _cursorX = 0; _cursorY = 0; _cursorState = false; _cursorOff = false; _cursorRow = getLineCount() - 1; _cursorCol = _canvas.getLineCharWidth(_cursorRow); _cursorRect = new Common::Rect(0, 0, 1, 1); // currently, we are not using fg color to render text. And we are not passing fg color correctly, thus we read it our self. MacFontRun colorFontRun = getFgColor(); if (!colorFontRun.text.empty()) { _canvas._tfgcolor = colorFontRun.fgcolor; colorFontRun.text.clear(); debug(9, "Reading fg color though text, instead of the argument, read %d", _canvas._tfgcolor); _defaultFormatting = colorFontRun; _defaultFormatting.wm = _wm; } _currentFormatting = _defaultFormatting; _composeSurface->clear(_canvas._tbgcolor); _cursorSurface = new ManagedSurface(1, kCursorMaxHeight, _wm->_pixelformat); _cursorSurface->clear(_canvas._tfgcolor); _cursorSurface2 = new ManagedSurface(1, kCursorMaxHeight, _wm->_pixelformat); _cursorSurface2->clear(_canvas._tbgcolor); _canvas.reallocSurface(); setAlignOffset(_canvas._textAlignment); updateCursorPos(); render(); } MacText::~MacText() { if (_wm->getActiveWidget() == this) _wm->setActiveWidget(nullptr); delete _cursorRect; delete _cursorSurface; delete _cursorSurface2; #ifdef USE_PNG for (auto &i : _imageCache) delete i._value; #endif delete _imageArchive; } // this func returns the fg color of the first character we met in text MacFontRun MacText::getFgColor() { if (_canvas._text.empty()) return MacFontRun(); for (uint i = 0; i < _canvas._text.size(); i++) { for (uint j = 0; j < _canvas._text[i].chunks.size(); j++) { if (!_canvas._text[i].chunks[j].text.empty()) return _canvas._text[i].chunks[j]; } } return MacFontRun(); } // we are doing this because we may need to dealing with the plain byte. See ctor of mactext which contains String str instead of U32String str // thus, if we are passing the str, meaning we are using plainByteMode. And when we calculate the string width. we need to convert it to it's original state first; int getStringWidth(MacFontRun &format, const Common::U32String &str) { if (format.plainByteMode()) return format.getFont()->getStringWidth(Common::convertFromU32String(str, format.getEncoding())); else return format.getFont()->getStringWidth(str); } int getStringMaxWordWidth(MacFontRun &format, const Common::U32String &str) { if (format.plainByteMode()) { Common::StringTokenizer tok(Common::convertFromU32String(str, format.getEncoding())); int maxW = 0; while (!tok.empty()) { int w = format.getFont()->getStringWidth(tok.nextToken()); maxW = MAX(maxW, w); } return maxW; } else { Common::U32StringTokenizer tok(str); int maxW = 0; while (!tok.empty()) { int w = format.getFont()->getStringWidth(tok.nextToken()); maxW = MAX(maxW, w); } return maxW; } } void MacText::setMaxWidth(int maxWidth) { if (maxWidth == _canvas._maxWidth) return; if (maxWidth < 0) { warning("trying to set maxWidth to %d", maxWidth); return; } Common::U32String str = getTextChunk(0, 0, -1, -1, true, true); // keep the cursor pos int ppos = 0; for (int i = 0; i < _cursorRow; i++) ppos += _canvas.getLineCharWidth(i); ppos += _cursorCol; _canvas._maxWidth = maxWidth; _canvas._text.clear(); _canvas.splitString(str, -1, _defaultFormatting); // restore the cursor pos _cursorRow = 0; warning("FIXME, bad design"); while (ppos > _canvas.getLineCharWidth(_cursorRow, true)) { ppos -= _canvas.getLineCharWidth(_cursorRow, true); _cursorRow++; } _cursorCol = ppos; // after we set maxWidth, we reset the selection _selectedText.endY = -1; recalcDims(); updateCursorPos(); _fullRefresh = true; _contentIsDirty = true; } void MacText::setColors(uint32 fg, uint32 bg) { _canvas._tbgcolor = bg; _canvas._tfgcolor = fg; // also set the cursor color _cursorSurface->clear(_canvas._tfgcolor); for (uint i = 0; i < _canvas._text.size(); i++) setTextColor(fg, i); _fullRefresh = true; render(); _contentIsDirty = true; } void MacText::enforceTextFont(uint16 fontId) { for (uint i = 0; i < _canvas._text.size(); i++) { for (uint j = 0; j < _canvas._text[i].chunks.size(); j++) { _canvas._text[i].chunks[j].fontId = fontId; } } _fullRefresh = true; render(); _contentIsDirty = true; } void MacText::setTextSize(int textSize) { for (uint i = 0; i < _canvas._text.size(); i++) { for (uint j = 0; j < _canvas._text[i].chunks.size(); j++) { _canvas._text[i].chunks[j].fontSize = textSize; } } _fullRefresh = true; render(); _contentIsDirty = true; } void MacText::setTextColor(uint32 color, uint32 line) { if (line >= _canvas._text.size()) { warning("MacText::setTextColor(): line %d is out of bounds", line); return; } uint32 fgcol = _wm->findBestColor(color); for (uint j = 0; j < _canvas._text[line].chunks.size(); j++) { _canvas._text[line].chunks[j].fgcolor = fgcol; } // if we are calling this func separately, then here need a refresh } void MacText::getChunkPosFromIndex(int index, uint &lineNum, uint &chunkNum, uint &offset) { if (_canvas._text.empty()) { lineNum = chunkNum = offset = 0; return; } for (uint i = 0; i < _canvas._text.size(); i++) { if (_canvas.getLineCharWidth(i) <= index) { index -= _canvas.getLineCharWidth(i); } else { lineNum = i; chunkNum = _canvas._text[i].getChunkNum(&index); offset = index; return; } } lineNum = _canvas._text.size() - 1; chunkNum = _canvas._text[lineNum].chunks.size() - 1; offset = 0; } void setTextColorCallback(MacFontRun &macFontRun, int color) { macFontRun.fgcolor = color; } void MacText::setTextColor(uint32 color, uint32 start, uint32 end) { uint32 col = _wm->findBestColor(color); setTextChunks(start, end, col, setTextColorCallback); } void setTextSizeCallback(MacFontRun &macFontRun, int textSize) { macFontRun.fontSize = textSize; } void MacText::setTextSize(int textSize, int start, int end) { setTextChunks(start, end, textSize, setTextSizeCallback); } void MacText::setTextChunks(int start, int end, int param, void (*callback)(MacFontRun &, int)) { if (_canvas._text.empty()) return; if (start > end) SWAP(start, end); uint startRow, startCol; uint endRow, endCol; uint offset; getChunkPosFromIndex(start, startRow, startCol, offset); // if offset != 0, then we need to split the chunk if (offset != 0) { uint textSize = _canvas._text[startRow].chunks[startCol].text.size(); MacFontRun newChunk = _canvas._text[startRow].chunks[startCol]; newChunk.text = newChunk.text.substr(offset, textSize - offset); _canvas._text[startRow].chunks[startCol].text = _canvas._text[startRow].chunks[startCol].text.substr(0, offset); _canvas._text[startRow].chunks.insert_at(startCol + 1, newChunk); startCol++; } getChunkPosFromIndex(end, endRow, endCol, offset); if (offset != 0) { uint textSize = _canvas._text[endRow].chunks[endCol].text.size(); MacFontRun newChunk = _canvas._text[endRow].chunks[endCol]; newChunk.text = newChunk.text.substr(offset, textSize - offset); _canvas._text[endRow].chunks[endCol].text = _canvas._text[endRow].chunks[endCol].text.substr(0, offset); _canvas._text[endRow].chunks.insert_at(endCol + 1, newChunk); endCol++; } for (uint i = startRow; i <= endRow; i++) { uint from, to; if (i == startRow && i == endRow) { from = startCol; to = endCol; } else if (i == startRow) { from = startCol; to = _canvas._text[startRow].chunks.size(); } else if (i == endRow) { from = 0; to = endCol; } else { from = 0; to = _canvas._text[i].chunks.size(); } for (uint j = from; j < to; j++) { callback(_canvas._text[i].chunks[j], param); } } _fullRefresh = true; render(); _contentIsDirty = true; } void setTextFontCallback(MacFontRun &macFontRun, int fontId) { macFontRun.fontId = fontId; } void MacText::setTextFont(int fontId, int start, int end) { setTextChunks(start, end, fontId, setTextFontCallback); } void setTextSlantCallback(MacFontRun &macFontRun, int textSlant) { macFontRun.textSlant = textSlant; } void MacText::setTextSlant(int textSlant, int start, int end) { setTextChunks(start, end, textSlant, setTextSlantCallback); } void MacText::enforceTextSlant(int textSlant) { for (uint i = 0; i < _canvas._text.size(); i++) { for (uint j = 0; j < _canvas._text[i].chunks.size(); j++) { if (textSlant) { _canvas._text[i].chunks[j].textSlant |= textSlant; } else { _canvas._text[i].chunks[j].textSlant = textSlant; } } } _fullRefresh = true; render(); _contentIsDirty = true; } // this maybe need to amend // currently, we just return the text size of first character. int MacText::getTextSize(int start, int end) { return getTextChunks(start, end).fontSize; } uint32 MacText::getTextColor(int start, int end) { return getTextChunks(start, end).fgcolor; } int MacText::getTextFont(int start, int end) { return getTextChunks(start, end).fontId; } int MacText::getTextSlant(int start, int end) { return getTextChunks(start, end).textSlant; } // only getting the first chunk for the selected area MacFontRun MacText::getTextChunks(int start, int end) { if (_canvas._text.empty()) return _defaultFormatting; if (start > end) SWAP(start, end); uint startRow, startCol; uint offset; getChunkPosFromIndex(start, startRow, startCol, offset); return _canvas._text[startRow].chunks[startCol]; } void MacText::setDefaultFormatting(uint16 fontId, byte textSlant, uint16 fontSize, uint16 palinfo1, uint16 palinfo2, uint16 palinfo3) { _defaultFormatting.setValues(_defaultFormatting.wm, fontId, textSlant, fontSize, palinfo1, palinfo2, palinfo3); MacFont macFont = MacFont(fontId, fontSize, textSlant); _defaultFormatting.font = _wm->_fontMan->getFont(macFont); } // Adds the given string to the end of the last line/chunk // while observing the _canvas._maxWidth and keeping this chunk's // formatting void MacTextCanvas::chopChunk(const Common::U32String &str, int *curLinePtr, int indent, int maxWidth) { int curLine = *curLinePtr; int curChunk; MacFontRun *chunk; curChunk = _text[curLine].chunks.size() - 1; chunk = &_text[curLine].chunks[curChunk]; // Check if there is nothing to add, then remove the last chunk // This happens when the previous run is finished only with // empty formatting, or when we were adding text for the first time if (chunk->text.empty() && str.empty()) { D(9, "** chopChunk, replaced formatting, line %d", curLine); _text[curLine].chunks.pop_back(); return; } if (maxWidth == -1) { chunk->text += str; return; } Common::Array text; int w = getLineWidth(curLine, true); D(9, "** chopChunk before wrap \"%s\"", Common::toPrintable(str.encode()).c_str()); chunk->getFont()->wordWrapText(str, maxWidth, text, w); if (text.size() == 0) { warning("chopChunk: too narrow width, >%d", maxWidth); chunk->text += str; getLineCharWidth(curLine, true); return; } for (int i = 0; i < (int)text.size(); i++) { D(9, "** chopChunk result %d \"%s\"", i, toPrintable(text[i].encode()).c_str()); } chunk->text += text[0]; // Recalc dims getLineWidth(curLine, true); D(9, "** chopChunk, subchunk: \"%s\" (%d lines, maxW: %d)", toPrintable(text[0].encode()).c_str(), text.size(), maxWidth); // We do not overlap, so we're done if (text.size() == 1) return; // Now add rest of the chunks MacFontRun newchunk = _text[curLine].chunks[curChunk]; for (uint i = 1; i < text.size(); i++) { newchunk.text = text[i]; curLine++; _text.insert_at(curLine, MacTextLine()); _text[curLine].chunks.push_back(newchunk); _text[curLine].indent = indent; _text[curLine].firstLineIndent = 0; D(9, "** chopChunk, added line (firstIndent: %d): \"%s\"", _text[curLine].firstLineIndent, toPrintable(text[i].encode()).c_str()); } *curLinePtr = curLine; } void MacTextCanvas::splitString(const Common::U32String &str, int curLine, MacFontRun &defaultFormatting) { D(9, "** splitString(\"%s\", %d)", toPrintable(str.encode()).c_str(), curLine); if (str.empty()) { D(9, "** splitString, empty line"); return; } (void)splitString(str.c_str(), curLine, defaultFormatting); } const Common::U32String::value_type *MacTextCanvas::splitString(const Common::U32String::value_type *s, int curLine, MacFontRun &defaultFormatting) { if (_text.empty()) { _text.resize(1); _text[0].chunks.push_back(defaultFormatting); D(9, "** splitString, added default formatting"); } else { D(9, "** splitString, continuing, %d lines", _text.size()); } Common::U32String tmp; if (curLine == -1 || curLine >= (int)_text.size()) curLine = _text.size() - 1; int curChunk = _text[curLine].chunks.size() - 1; MacFontRun chunk = _text[curLine].chunks[curChunk]; int indentSize = 0; int firstLineIndent = 0; bool inTable = false; while (*s) { firstLineIndent = 0; tmp.clear(); MacTextLine *curTextLine = &_text[curLine]; while (*s) { bool endOfLine = false; // Scan till next font change or end of line while (*s && *s != '\001') { if (*s == '\r') { s++; if (*s == '\n') // Skip whole '\r\n' s++; endOfLine = true; break; } // deal with single \n if (*s == '\n') { s++; endOfLine = true; break; } tmp += *s; s++; } if (*s == '\001') // If it was \001, skip it s++; if (*s == '\001') { // \001\001 -> \001 tmp += *s++; if (*s) // Check we reached end of line continue; } D(9, "** splitString, chunk: \"%s\"", Common::toPrintable(tmp.encode()).c_str()); // Okay, now we are either at the end of the line, or in the next // chunk definition. That means, that we have to store the previous chunk chopChunk(tmp, &curLine, indentSize, _maxWidth > 0 ? _maxWidth - indentSize : _maxWidth); curTextLine = &_text[curLine]; firstLineIndent = curTextLine->firstLineIndent; tmp.clear(); // If it is end of the line, we're done if (!*s) { D(9, "** splitString, end of line"); break; } // get format (sync with stripFormat() ) if (*s == '\016') { // human-readable format s++; // First two digits is slant, third digit is Header number switch (*s) { case '+': { // \016+XXYZ -- opening textSlant, H, indent<+Z> uint16 textSlant, headSize, indent; s++; s = readHex(&textSlant, s, 2); chunk.textSlant |= textSlant; // Setting the specified bit s = readHex(&headSize, s, 1); if (headSize >= 1 && headSize <= 6) { // set const float sizes[] = { 1, 2.0f, 1.41f, 1.155f, 1.0f, .894f, .816f }; chunk.fontSize = defaultFormatting.fontSize * sizes[headSize]; } s = readHex(&indent, s, 1); if (s) indentSize += indent * chunk.fontSize * 2; D(9, "** splitString+: fontId: %d, textSlant: %d, fontSize: %d, indent: %d", chunk.fontId, chunk.textSlant, chunk.fontSize, indent); break; } case '-': { // \016-XXYZ -- closing textSlant, H, indent<+Z> uint16 textSlant, headSize, indent; s++; s = readHex(&textSlant, s, 2); chunk.textSlant &= ~textSlant; // Clearing the specified bit s = readHex(&headSize, s, 1); if (headSize == 0xf) // reset chunk.fontSize = defaultFormatting.fontSize; s = readHex(&indent, s, 1); if (s) indentSize -= indent * chunk.fontSize * 2; D(9, "** splitString-: fontId: %d, textSlant: %d, fontSize: %d, indent: %d", chunk.fontId, chunk.textSlant, chunk.fontSize, indent); break; } case '[': { // \016[RRGGBB -- setting color uint16 palinfo1, palinfo2, palinfo3; s++; s = readHex(&palinfo1, s, 4); s = readHex(&palinfo2, s, 4); s = readHex(&palinfo3, s, 4); chunk.palinfo1 = palinfo1; chunk.palinfo2 = palinfo2; chunk.palinfo3 = palinfo3; chunk.fgcolor = _wm->findBestColor(palinfo1 & 0xff, palinfo2 & 0xff, palinfo3 & 0xff); D(9, "** splitString[: %08x", chunk.fgcolor); break; } case ']': { // \016] -- setting default color s++; chunk.palinfo1 = defaultFormatting.palinfo1; chunk.palinfo2 = defaultFormatting.palinfo2; chunk.palinfo3 = defaultFormatting.palinfo3; chunk.fgcolor = defaultFormatting.fgcolor; D(9, "** splitString]: %08x", chunk.fgcolor); break; } case '*': { // \016*XXsssssss -- negative indent, XX size, sssss is the string s++; uint16 len; s = readHex(&len, s, 2); Common::U32String bullet = Common::U32String(s, len); s += len; firstLineIndent = -chunk.getFont()->getStringWidth(bullet); D(9, "** splitString*: %02x '%s' (%d)", len, bullet.encode().c_str(), firstLineIndent); break; } case 'i': { // \016iXXNNnnnnAAaaaaTTttt -- image, XX% width, // NN, nnnn -- filename len and text // AA, aaaa -- alt len and text // TT, tttt -- text (tooltip) len and text s++; uint16 len; s = readHex(&_text[curLine].picpercent, s, 2); s = readHex(&len, s, 2); _text[curLine].picfname = Common::U32String(s, len).encode(); s += len; s = readHex(&len, s, 2); _text[curLine].picalt = Common::U32String(s, len); s += len; s = readHex(&len, s, 2); _text[curLine].pictitle = Common::U32String(s, len); s += len; D(9, "** splitString[i]: %d%% fname: '%s' alt: '%s' title: '%s'", _text[curLine].picpercent, _text[curLine].picfname.c_str(), _text[curLine].picalt.encode().c_str(), _text[curLine].pictitle.encode().c_str()); break; } case 't': { // \016tXXXX -- switch to the requested font id s++; uint16 fontId; s = readHex(&fontId, s, 4); chunk.fontId = fontId == 0xffff ? defaultFormatting.fontId : fontId; D(9, "** splitString[t]: fontId: %d", fontId); break; } case 'l': { // \016lLLllll -- link len and text s++; uint16 len; s = readHex(&len, s, 2); chunk.link = Common::U32String(s, len); s += len; D(9, "** splitString[l]: link: %s", chunk.link.c_str()); break; } case 'T': { // \016T -- table s++; char cmd = *s++; if (cmd == 'h') { // Header, beginning of the table curTextLine->table = new Common::Array(); inTable = true; D(9, "** splitString[table header]"); } else if (cmd == 'b') { // Body start D(9, "** splitString[body start]"); } else if (cmd == 'B') { // Body end inTable = false; D(9, "** splitString[body end]"); processTable(curLine, _maxWidth); continue; } else if (cmd == 'r') { // Row curTextLine->table->push_back(MacTextTableRow()); D(9, "** splitString[row]"); } else if (cmd == 'c') { // Cell start uint16 align; s = readHex(&align, s, 2); curTextLine->table->back().cells.push_back(MacTextCanvas()); MacTextCanvas *cellCanvas = &curTextLine->table->back().cells.back(); cellCanvas->_textAlignment = (TextAlign)align; cellCanvas->_wm = _wm; cellCanvas->_macText = _macText; cellCanvas->_maxWidth = -1; cellCanvas->_macFontMode = _macFontMode; cellCanvas->_tfgcolor = _tfgcolor; cellCanvas->_tbgcolor = _tbgcolor; D(9, "** splitString[cell start]: align: %d", align); D(9, "** splitString[RECURSION start]"); s = cellCanvas->splitString(s, curLine, defaultFormatting); D(9, "** splitString[RECURSION end]"); } else if (cmd == 'C') { // Cell end D(9, "** splitString[cell end]"); return s; } else { error("MacText: Unknown table subcommand (%c)", cmd); } break; } default: { uint16 fontId, textSlant, fontSize, palinfo1, palinfo2, palinfo3; s = readHex(&fontId, s, 4); s = readHex(&textSlant, s, 2); s = readHex(&fontSize, s, 4); s = readHex(&palinfo1, s, 4); s = readHex(&palinfo2, s, 4); s = readHex(&palinfo3, s, 4); chunk.setValues(_wm, fontId, textSlant, fontSize, palinfo1, palinfo2, palinfo3); D(9, "** splitString: fontId: %d, textSlant: %d, fontSize: %d, fg: %04x", fontId, textSlant, fontSize, chunk.fgcolor); // So far, we enforce single font here, though in the future, font size could be altered if (!_macFontMode) chunk.font = defaultFormatting.font; } } } curTextLine->indent = indentSize; curTextLine->firstLineIndent = firstLineIndent; // Push new formatting curTextLine->chunks.push_back(chunk); // If we reached end of paragraph, go to outer loop if (endOfLine) break; } // We avoid adding new lines while in table. Recursive cell rendering // has this flag as false (obviously) if (inTable) continue; // Add new line D(9, "** splitString: new line"); curTextLine->paragraphEnd = true; // if the chunks is empty, which means the line will not be rendered properly // so we add a empty string here if (curTextLine->chunks.empty()) { curTextLine->chunks.push_back(defaultFormatting); } curLine++; _text.insert_at(curLine, MacTextLine()); _text[curLine].chunks.push_back(chunk); curTextLine = &_text[curLine]; } #if DEBUG for (uint i = 0; i < _text.size(); i++) { debugN(9, "** splitString: %2d ", i); for (uint j = 0; j < _text[i].chunks.size(); j++) debugN(9, "[%d] \"%s\"", _text[i].chunks[j].text.size(), Common::toPrintable(_text[i].chunks[j].text.encode()).c_str()); debugN(9, "\n"); } debug(9, "** splitString: done"); #endif return s; } void MacTextCanvas::reallocSurface() { // round to closest 10 //TODO: work out why this rounding doesn't correctly fill the entire width //int requiredH = (_text.size() + (_text.size() * 10 + 9) / 10) * lineH if (!_surface) { _surface = new ManagedSurface(_maxWidth, _textMaxHeight, _wm->_pixelformat); if (_textShadow) _shadowSurface = new ManagedSurface(_maxWidth, _textMaxHeight, _wm->_pixelformat); return; } if (_surface->w < _maxWidth || _surface->h < _textMaxHeight) { // realloc surface and copy old content ManagedSurface *n = new ManagedSurface(_maxWidth, _textMaxHeight, _wm->_pixelformat); n->clear(_tbgcolor); n->blitFrom(*_surface, Common::Point(0, 0)); delete _surface; _surface = n; // same as shadow surface if (_textShadow) { ManagedSurface *newShadowSurface = new ManagedSurface(_maxWidth, _textMaxHeight, _wm->_pixelformat); newShadowSurface->clear(_tbgcolor); newShadowSurface->blitFrom(*_shadowSurface, Common::Point(0, 0)); delete _shadowSurface; _shadowSurface = newShadowSurface; } } } void MacText::render() { if (_fullRefresh) { _canvas._surface->clear(_canvas._tbgcolor); if (_canvas._textShadow) _canvas._shadowSurface->clear(_canvas._tbgcolor); _canvas.render(0, _canvas._text.size()); _fullRefresh = false; #if 0 Common::DumpFile out; Common::String filename = Common::String::format("z-%p.png", (void *)this); if (out.open(filename)) { warning("Wrote: %s", filename.c_str()); Image::writePNG(out, _canvas._surface->rawSurface()); } #endif } } void MacTextCanvas::render(int from, int to, int shadow) { int w = MIN(_maxWidth, _textMaxWidth); ManagedSurface *surface = shadow ? _shadowSurface : _surface; int myFrom = from, myTo = to + 1, delta = 1; if (_wm->_language == Common::HE_ISR) { myFrom = to; myTo = from - 1; delta = -1; } for (int i = myFrom; i != myTo; i += delta) { if (!_text[i].picfname.empty()) { const Surface *image = _macText->getImageSurface(_text[i].picfname); int xOffset = (_text[i].width - _text[i].charwidth) / 2; Common::Rect bbox(xOffset, _text[i].y, xOffset + _text[i].charwidth, _text[i].y + _text[i].height); if (image) { surface->blitFrom(image, Common::Rect(0, 0, image->w, image->h), bbox); D(9, "MacTextCanvas::render: Image %d x %d bbox: %d, %d, %d, %d", image->w, image->h, bbox.left, bbox.top, bbox.right, bbox.bottom); } continue; } if (_text[i].tableSurface) { surface->blitFrom(*_text[i].tableSurface, Common::Point(0, _text[i].y)); D(9, "MacTextCanvas::render: Table %d x %d at: %d, %d", _text[i].tableSurface->w, _text[i].tableSurface->h, 0, _text[i].y); continue; } int xOffset = getAlignOffset(i) + _text[i].indent + _text[i].firstLineIndent; xOffset++; int start = 0, end = _text[i].chunks.size(); if (_wm->_language == Common::HE_ISR) { start = _text[i].chunks.size() - 1; end = -1; } int maxAscentForRow = 0; for (int j = start; j != end; j += delta) { if (_text[i].chunks[j].font->getFontAscent() > maxAscentForRow) maxAscentForRow = _text[i].chunks[j].font->getFontAscent(); } // TODO: _canvas._textMaxWidth, when -1, was not rendering ANY text. for (int j = start; j != end; j += delta) { D(9, "MacTextCanvas::render: line %d[%d] h:%d at %d,%d (%s) fontid: %d fontsize: %d on %dx%d, fgcolor: %08x bgcolor: %08x", i, j, _text[i].height, xOffset, _text[i].y, _text[i].chunks[j].text.encode().c_str(), _text[i].chunks[j].fontId, _text[i].chunks[j].fontSize, _surface->w, _surface->h, _text[i].chunks[j].fgcolor, _tbgcolor); if (_text[i].chunks[j].text.empty()) continue; int yOffset = 0; if (_text[i].chunks[j].font->getFontAscent() < maxAscentForRow) { yOffset = maxAscentForRow - _text[i].chunks[j].font->getFontAscent(); } if (_text[i].chunks[j].plainByteMode()) { Common::String str = _text[i].chunks[j].getEncodedText(); _text[i].chunks[j].getFont()->drawString(surface, str, xOffset, _text[i].y + yOffset, w, shadow ? _wm->_colorBlack : _text[i].chunks[j].fgcolor, kTextAlignLeft, 0, true); xOffset += _text[i].chunks[j].getFont()->getStringWidth(str); } else { if (_wm->_language == Common::HE_ISR) _text[i].chunks[j].getFont()->drawString(surface, convertBiDiU32String(_text[i].chunks[j].text, Common::BIDI_PAR_RTL), xOffset, _text[i].y + yOffset, w, shadow ? _wm->_colorBlack : _text[i].chunks[j].fgcolor, kTextAlignLeft, 0, true); else _text[i].chunks[j].getFont()->drawString(surface, convertBiDiU32String(_text[i].chunks[j].text), xOffset, _text[i].y + yOffset, w, shadow ? _wm->_colorBlack : _text[i].chunks[j].fgcolor, kTextAlignLeft, 0, true); xOffset += _text[i].chunks[j].getFont()->getStringWidth(_text[i].chunks[j].text); } } } } void MacTextCanvas::render(int from, int to) { if (_text.empty()) return; reallocSurface(); from = MAX(0, from); to = MIN(to, _text.size() - 1); // Clear the screen _surface->fillRect(Common::Rect(0, _text[from].y, _surface->w, _text[to].y + getLineHeight(to)), _tbgcolor); // render the shadow surface; if (_textShadow) render(from, to, _textShadow); render(from, to, 0); for (uint i = 0; i < _text.size(); i++) { debugN(9, "MacTextCanvas::render: %2d (firstInd: %d indent: %d) ", i, _text[i].firstLineIndent, _text[i].indent); for (uint j = 0; j < _text[i].chunks.size(); j++) debugN(9, "[%d (%d)] \"%s\" ", _text[i].chunks[j].fontId, _text[i].chunks[j].textSlant, _text[i].chunks[j].text.encode().c_str()); debug(9, "%s", ""); } } int MacTextCanvas::getLineWidth(int lineNum, bool enforce, int col) { if ((uint)lineNum >= _text.size()) return 0; MacTextLine *line = &_text[lineNum]; if (line->width != -1 && !enforce && col == -1) return line->width; if (!line->picfname.empty()) { const Surface *image = _macText->getImageSurface(line->picfname); if (image) { float ratio = _maxWidth * line->picpercent / 100.0 / (float)image->w; line->width = _maxWidth; line->height = image->h * ratio; line->charwidth = image->w * ratio; } else { line->width = _maxWidth; line->height = 1; line->charwidth = 1; } return line->width; } if (line->table) { line->width = _maxWidth; line->height = line->tableSurface->h; line->charwidth = _maxWidth; return line->width; } int width = line->indent + line->firstLineIndent; int height = 0; int charwidth = 0; int minWidth = 0; bool firstWord = true; for (uint i = 0; i < line->chunks.size(); i++) { if (enforce && _macFontMode) line->chunks[i].font = nullptr; if (col >= 0) { if (col >= (int)line->chunks[i].text.size()) { col -= line->chunks[i].text.size(); } else { Common::U32String tmp = line->chunks[i].text.substr(0, col); width += getStringWidth(line->chunks[i], tmp); return width; } } if (!line->chunks[i].text.empty()) { int w = getStringWidth(line->chunks[i], line->chunks[i].text); int mW = getStringMaxWordWidth(line->chunks[i], line->chunks[i].text); if (firstWord) { minWidth = mW + width; // Take indent into account firstWord = false; } else { minWidth = MAX(minWidth, mW); } width += w; charwidth += line->chunks[i].text.size(); } height = MAX(height, line->chunks[i].getFont()->getFontHeight()); } line->width = width; line->minWidth = minWidth; line->height = height; line->charwidth = charwidth; return width; } int MacTextCanvas::getLineCharWidth(int line, bool enforce) { if ((uint)line >= _text.size()) return 0; if (_text[line].charwidth != -1 && !enforce) return _text[line].charwidth; int width = 0; for (uint i = 0; i < _text[line].chunks.size(); i++) { if (!_text[line].chunks[i].text.empty()) width += _text[line].chunks[i].text.size(); } _text[line].charwidth = width; return width; } int MacText::getLastLineWidth() { if (_canvas._text.size() == 0) return 0; return _canvas.getLineWidth(_canvas._text.size() - 1, true); } int MacText::getLineHeight(int line) { return _canvas.getLineHeight(line); } int MacTextCanvas::getLineHeight(int line) { if ((uint)line >= _text.size()) return 0; (void)getLineWidth(line); // This calculates height also return _text[line].height; } void MacText::setInterLinear(int interLinear) { _canvas._interLinear = interLinear; recalcDims(); _fullRefresh = true; render(); _contentIsDirty = true; } void MacText::recalcDims() { _canvas.recalcDims(); if (!_fixedDims) { int newBottom = _dims.top + _canvas._textMaxHeight + (2 * _border) + _gutter + _shadow; if (newBottom > _dims.bottom) { _dims.bottom = newBottom; delete _composeSurface; _composeSurface = new ManagedSurface(_dims.width(), _dims.height(), _wm->_pixelformat); _canvas.reallocSurface(); if (!_fullRefresh) { _fullRefresh = true; render(); } _fullRefresh = true; _contentIsDirty = true; } } } void MacTextCanvas::recalcDims() { if (_text.empty()) return; int y = 0; _textMaxWidth = 0; for (uint i = 0; i < _text.size(); i++) { _text[i].y = y; // We must calculate width first, because it enforces // the computation. Calling Height() will return cached value! _textMaxWidth = MAX(_textMaxWidth, getLineWidth(i, true)); y += MAX(getLineHeight(i), _interLinear); } _textMaxHeight = y; } void MacText::setAlignOffset(TextAlign align) { if (_canvas._textAlignment == align) return; _contentIsDirty = true; _fullRefresh = true; _canvas._textAlignment = align; } Common::Point MacText::calculateOffset() { return Common::Point(_border + _gutter, _border + _gutter / 2); } void MacText::setSelRange(int selStart, int selEnd) { if (selStart == _selStart && selEnd == _selEnd) return; _selStart = selStart; _selEnd = selEnd; } void MacText::setActive(bool active) { if (_active == active) return; MacWidget::setActive(active); g_system->getTimerManager()->removeTimerProc(&cursorTimerHandler); if (_active) { g_system->getTimerManager()->installTimerProc(&cursorTimerHandler, 200000, this, "macEditableText"); // inactive -> active, we reset the selection setSelection(_selStart, true); setSelection(_selEnd, false); } else { // clear the selection and cursor _selectedText.endY = -1; _cursorState = false; _inTextSelection = false; } // after we change the status of active, we need to do a refresh to clear the stuff we don't need _contentIsDirty = true; if (!_cursorOff && _cursorState == true) undrawCursor(); } void MacText::setEditable(bool editable) { if (editable == _editable) return; // if we are not editable, then we also update the state of active, and tell wm too if (!editable) { setActive(false); if (_wm->getActiveWidget() == this) _wm->setActiveWidget(nullptr); } _editable = editable; _cursorOff = !editable; _selectable = editable; _focusable = editable; if (!editable) { undrawCursor(); } } void MacText::resize(int w, int h) { if (_canvas._surface->w == w && _canvas._surface->h == h) return; setMaxWidth(w); if (_composeSurface->w != w || _composeSurface->h != h) { delete _composeSurface; _composeSurface = new ManagedSurface(w, h, _wm->_pixelformat); _dims.right = _dims.left + w; _dims.bottom = _dims.top + h; _contentIsDirty = true; _fullRefresh = true; } } void MacText::appendText(const Common::U32String &str, int fontId, int fontSize, int fontSlant, bool skipAdd) { appendText(str, fontId, fontSize, fontSlant, 0, 0, 0, skipAdd); } void MacText::appendText(const Common::U32String &str, int fontId, int fontSize, int fontSlant, uint16 r, uint16 g, uint16 b, bool skipAdd) { uint oldLen = _canvas._text.size(); MacFontRun fontRun = MacFontRun(_wm, fontId, fontSlant, fontSize, r, g, b); _currentFormatting = fontRun; // we check _str here, if _str is empty but _canvas._text is not empty, and they are not the end of paragraph // then we remove those empty lines // too many special check may cause some strange problem in the future if (_str.empty()) { while (!_canvas._text.empty() && !_canvas._text.back().paragraphEnd) removeLastLine(); } // we need to split the string with the font, in order to get the correct font Common::U32String strWithFont = Common::U32String(fontRun.toString()) + str; if (!skipAdd) _str += strWithFont; appendText_(strWithFont, oldLen); } void MacText::appendText(const Common::U32String &str, const Font *font, uint16 r, uint16 g, uint16 b, bool skipAdd) { uint oldLen = _canvas._text.size(); MacFontRun fontRun = MacFontRun(_wm, font, 0, font->getFontHeight(), r, g, b); _currentFormatting = fontRun; if (_str.empty()) { while (!_canvas._text.empty() && !_canvas._text.back().paragraphEnd) removeLastLine(); } Common::U32String strWithFont = Common::U32String(fontRun.toString()) + str; if (!skipAdd) _str += strWithFont; appendText_(strWithFont, oldLen); } void MacText::appendText_(const Common::U32String &strWithFont, uint oldLen) { _canvas.splitString(strWithFont, -1, _defaultFormatting); recalcDims(); _canvas.render(oldLen - 1, _canvas._text.size()); _contentIsDirty = true; if (_editable) { _scrollPos = MAX(0, getTextHeight() - getDimensions().height()); _cursorRow = getLineCount(); _cursorCol = _canvas.getLineCharWidth(_cursorRow); updateCursorPos(); } } void MacText::appendTextDefault(const Common::U32String &str, bool skipAdd) { uint oldLen = _canvas._text.size(); _currentFormatting = _defaultFormatting; Common::U32String strWithFont = Common::U32String(_defaultFormatting.toString()) + str; if (!skipAdd) { _str += strWithFont; } _canvas.splitString(strWithFont, -1, _defaultFormatting); recalcDims(); _canvas.render(oldLen - 1, _canvas._text.size()); } void MacText::appendTextDefault(const Common::String &str, bool skipAdd) { appendTextDefault(Common::U32String(str), skipAdd); } void MacText::clearText() { _contentIsDirty = true; _canvas._text.clear(); _str.clear(); if (_canvas._surface) _canvas._surface->clear(_canvas._tbgcolor); recalcDims(); _cursorRow = _cursorCol = 0; updateCursorPos(); } void MacText::removeLastLine() { if (!_canvas._text.size()) return; int h = getLineHeight(_canvas._text.size() - 1) + _canvas._interLinear; _canvas._surface->fillRect(Common::Rect(0, _canvas._textMaxHeight - h, _canvas._surface->w, _canvas._textMaxHeight), _canvas._tbgcolor); _canvas._text.pop_back(); _canvas._textMaxHeight -= h; } void MacText::draw(ManagedSurface *g, int x, int y, int w, int h, int xoff, int yoff) { if (_canvas._text.empty()) return; render(); if (x + w < _canvas._surface->w || y + h < _canvas._surface->h) g->fillRect(Common::Rect(x + xoff, y + yoff, x + w + xoff, y + h + yoff), _canvas._tbgcolor); // blit shadow surface first if (_canvas._textShadow) g->blitFrom(*_canvas._shadowSurface, Common::Rect(MIN(_canvas._surface->w, x), MIN(_canvas._surface->h, y), MIN(_canvas._surface->w, x + w), MIN(_canvas._surface->h, y + h)), Common::Point(xoff + _canvas._textShadow, yoff + _canvas._textShadow)); uint32 bgcolor = _canvas._tbgcolor < 0xff ? _canvas._tbgcolor : 0; g->transBlitFrom(*_canvas._surface, Common::Rect(MIN(_canvas._surface->w, x), MIN(_canvas._surface->h, y), MIN(_canvas._surface->w, x + w), MIN(_canvas._surface->h, y + h)), Common::Point(xoff, yoff), bgcolor); _contentIsDirty = false; _cursorDirty = false; } bool MacText::draw(bool forceRedraw) { if (!needsRedraw() && !forceRedraw) return false; if (!_canvas._surface) { warning("MacText::draw: Null surface"); return false; } // we need to find out a way to judge whether we need to clear the surface // currently, we just use the _contentIsDirty if (_contentIsDirty) _composeSurface->clear(_canvas._tbgcolor); // TODO: Clear surface fully when background colour changes. _cursorDirty = false; Common::Point offset(calculateOffset()); // if we are drawing the selection text or we are selecting, then we don't draw the cursor if (!((_inTextSelection || _selectedText.endY != -1) && _active)) { if (!_cursorState) _composeSurface->blitFrom(*_cursorSurface2, *_cursorRect, Common::Point(_cursorX + offset.x, _cursorY + offset.y)); else _composeSurface->blitFrom(*_cursorSurface, *_cursorRect, Common::Point(_cursorX + offset.x, _cursorY + offset.y)); } if (!(_contentIsDirty || forceRedraw)) return true; draw(_composeSurface, 0, _scrollPos, _canvas._surface->w, _scrollPos + _canvas._surface->h, offset.x, offset.y); for (int bb = 0; bb < _shadow; bb++) { _composeSurface->hLine(_shadow, _composeSurface->h - _shadow + bb, _composeSurface->w, 0); _composeSurface->vLine(_composeSurface->w - _shadow + bb, _shadow, _composeSurface->h - _shadow, 0); } for (int bb = 0; bb < _border; bb++) { Common::Rect borderRect(bb, bb, _composeSurface->w - bb, _composeSurface->h - bb); _composeSurface->frameRect(borderRect, 0xff); } if (_selectedText.endY != -1) drawSelection(offset.x, offset.y); _contentIsDirty = false; return true; } bool MacText::draw(ManagedSurface *g, bool forceRedraw) { if (!draw(forceRedraw)) return false; g->transBlitFrom(*_composeSurface, _composeSurface->getBounds(), Common::Point(_dims.left, _dims.top), _wm->_colorGreen2); return true; } void MacText::drawToPoint(ManagedSurface *g, Common::Rect srcRect, Common::Point dstPoint) { if (_canvas._text.empty()) return; render(); srcRect.clip(_canvas._surface->getBounds()); if (srcRect.isEmpty()) return; g->blitFrom(*_canvas._surface, srcRect, dstPoint); } void MacText::drawToPoint(ManagedSurface *g, Common::Point dstPoint) { if (_canvas._text.empty()) return; render(); g->blitFrom(*_canvas._surface, dstPoint); } // Count newline characters in String uint getNewlinesInString(const Common::U32String &str) { Common::U32String::const_iterator p = str.begin(); uint newLines = 0; while (*p) { if (*p == '\n') newLines++; p++; } return newLines; } void MacText::drawSelection(int xoff, int yoff) { if (_selectedText.endY == -1) return; // we check if the selection size is 0, then we don't draw it anymore, and we set the cursor here // it's a small optimize, but can bring us correct behavior if (!_inTextSelection && _selectedText.startX == _selectedText.endX && _selectedText.startY == _selectedText.endY) { _cursorRow = _selectedText.startRow; _cursorCol = _selectedText.startCol; updateCursorPos(); _selectedText.startY = _selectedText.endY = -1; return; } SelectedText s = _selectedText; if (s.startY > s.endY || (s.startY == s.endY && s.startX > s.endX)) { SWAP(s.startX, s.endX); SWAP(s.startY, s.endY); SWAP(s.startRow, s.endRow); SWAP(s.startCol, s.endCol); } int lastLineStart = s.endY; s.endY += getLineHeight(s.endRow); int start = s.startY - _scrollPos; start = MAX(0, start); if (start > getDimensions().height()) return; int end = s.endY - _scrollPos; if (end < 0) return; int maxSelectionHeight = getDimensions().height() - _border - _gutter / 2; int maxSelectionWidth = getDimensions().width() - _border - _gutter; if (s.endCol == _canvas.getLineCharWidth(s.endRow)) s.endX = maxSelectionWidth; end = MIN((int)maxSelectionHeight, end); // if we are selecting all text, then we invert the whole area if ((uint)s.endRow == _canvas._text.size() - 1) end = maxSelectionHeight; int numLines = 0; int x1 = 0, x2 = maxSelectionWidth; int row = s.startRow; int alignOffset = 0; // we may draw part of the selection, so we need to calc the height of first line if (s.startY < _scrollPos) { int start_row = 0; getRowCol(s.startX, _scrollPos, nullptr, &numLines, &start_row, nullptr); numLines = getLineHeight(start_row) - (_scrollPos - numLines); if (start_row == s.startRow) x1 = s.startX; if (start_row == s.endRow) x2 = s.endX; // deal with the first line, which is not a complete line if (numLines) { alignOffset = _canvas.getAlignOffset(start_row); if (start_row == s.startRow && s.startCol != 0) { x1 = MIN(x1 + xoff + alignOffset, maxSelectionWidth); x2 = MIN(x2 + xoff + alignOffset, maxSelectionWidth); } else { x1 = MIN(x1 + xoff, maxSelectionWidth); x2 = MIN(x2 + xoff + alignOffset, maxSelectionWidth); } row = start_row + 1; } } end = MIN(end, maxSelectionHeight - yoff); for (int y = start; y < end; y++) { if (!numLines && (uint)row < _canvas._text.size()) { x1 = 0; x2 = maxSelectionWidth; alignOffset = _canvas.getAlignOffset(row); numLines = getLineHeight(row); if (y + _scrollPos == s.startY && s.startX > 0) x1 = s.startX; if (y + _scrollPos >= lastLineStart) x2 = s.endX; // if we are selecting text reversely, and we are at the first line but not the select from beginning, then we add offset to x1 // the reason here is if we are not drawing the single line, then we draw selection from x1 to x2 + offset. i.e. we draw from begin // otherwise, we draw selection from x1 + offset to x2 + offset if (row == s.startRow && s.startCol != 0) { x1 = MIN(x1 + xoff + alignOffset, maxSelectionWidth); x2 = MIN(x2 + xoff + alignOffset, maxSelectionWidth); } else { x1 = MIN(x1 + xoff, maxSelectionWidth); x2 = MIN(x2 + xoff + alignOffset, maxSelectionWidth); } row++; } numLines--; byte *ptr = (byte *)_composeSurface->getBasePtr(x1, MIN(y + yoff, maxSelectionHeight - 1)); for (int x = x1; x < x2; x++, ptr++) if (*ptr == _canvas._tfgcolor) *ptr = _canvas._tbgcolor; else *ptr = _canvas._tfgcolor; } } Common::U32String MacText::getSelection(bool formatted, bool newlines) { if (_selectedText.endY == -1) return Common::U32String(); SelectedText s = _selectedText; if (s.startY > s.endY || (s.startY == s.endY && s.startX > s.endX)) { SWAP(s.startRow, s.endRow); SWAP(s.startCol, s.endCol); } return getTextChunk(s.startRow, s.startCol, s.endRow, s.endCol, formatted, newlines); } void MacText::clearSelection() { _selectedText.endY = _selectedText.startY = -1; } uint MacText::getSelectionIndex(bool start) { int pos = 0; if (!_inTextSelection && (_selectedText.startY < 0 && _selectedText.endY < 0)) return pos; if (start) { for (int row = 0; row < _selectedText.startRow; row++) pos += _canvas.getLineCharWidth(row); pos += _selectedText.startCol; return pos; } else { for (int row = 0; row < _selectedText.endRow; row++) pos += _canvas.getLineCharWidth(row); pos += _selectedText.endCol; return pos; } } void MacText::setSelection(int pos, bool start) { // -1 for start represent the beginning of text, i.e. 0 if (pos == -1 && start) pos = 0; int row = 0, col = 0; int colX = 0; if (pos > 0) { while (pos > 0) { if (pos < _canvas.getLineCharWidth(row)) { for (uint i = 0; i < _canvas._text[row].chunks.size(); i++) { if ((uint)pos < _canvas._text[row].chunks[i].text.size()) { colX += getStringWidth(_canvas._text[row].chunks[i], _canvas._text[row].chunks[i].text.substr(0, pos)); col += pos; pos = 0; break; } else { colX += getStringWidth(_canvas._text[row].chunks[i], _canvas._text[row].chunks[i].text); pos -= _canvas._text[row].chunks[i].text.size(); col += _canvas._text[row].chunks[i].text.size(); } } break; } else { pos -= _canvas.getLineCharWidth(row); // (row ? 1 : 0); } row++; if ((uint)row >= _canvas._text.size()) { row = _canvas._text.size() - 1; colX = _canvas._surface->w; col = _canvas.getLineCharWidth(row); break; } } } else if (pos == 0) { colX = col = row = 0; } else { row = _canvas._text.size() - 1; col = _canvas.getLineCharWidth(row); // if we don't have any text, then we won't select the whole area. if (_canvas._textMaxWidth == 0) colX = 0; else colX = _canvas._textMaxWidth; } int rowY = _canvas._text[row].y; if (start) { _selectedText.startX = colX; _selectedText.startY = rowY; _selectedText.startCol = col; _selectedText.startRow = row; } else { _selectedText.endX = colX; _selectedText.endY = rowY; _selectedText.endCol = col; _selectedText.endRow = row; } _contentIsDirty = true; } bool MacText::isCutAllowed() { if (_selectedText.startRow >= _editableRow && _selectedText.endRow >= _editableRow) return true; return false; } Common::U32String MacText::getEditedString() { return getTextChunk(_editableRow, 0, -1, -1); } Common::U32String MacText::getPlainText() { Common::U32String res; for (uint i = 0; i < _canvas._text.size(); i++) { for (uint j = 0; j < _canvas._text[i].chunks.size(); j++) { res += _canvas._text[i].chunks[j].text; } } return res; } Common::U32String MacText::cutSelection() { if (!isCutAllowed()) return Common::U32String(); SelectedText s = _selectedText; if (s.startY > s.endY || (s.startY == s.endY && s.startX > s.endX)) { SWAP(s.startRow, s.endRow); SWAP(s.startCol, s.endCol); } Common::U32String selection = MacText::getTextChunk(s.startRow, s.startCol, s.endRow, s.endCol, true, true); deleteSelection(); clearSelection(); return selection; } bool MacText::processEvent(Common::Event &event) { if (event.type == Common::EVENT_KEYDOWN) { if (!_editable) return false; setActive(true); if (event.kbd.flags & (Common::KBD_ALT | Common::KBD_CTRL | Common::KBD_META)) { switch (event.kbd.keycode) { case Common::KEYCODE_x: _wm->setTextInClipboard(cutSelection()); return true; case Common::KEYCODE_c: _wm->setTextInClipboard(getSelection(true, true)); return true; case Common::KEYCODE_v: if (g_system->hasTextInClipboard()) { if (_selectedText.endY != -1) cutSelection(); insertTextFromClipboard(); } return true; default: break; } return false; } switch (event.kbd.keycode) { case Common::KEYCODE_BACKSPACE: // if we have the selectedText, then we delete it if (_selectedText.endY != -1) { cutSelection(); _contentIsDirty = true; return true; } if (_cursorRow > 0 || _cursorCol > 0) { deletePreviousChar(&_cursorRow, &_cursorCol); updateCursorPos(); _contentIsDirty = true; } return true; case Common::KEYCODE_RETURN: addNewLine(&_cursorRow, &_cursorCol); updateCursorPos(); _contentIsDirty = true; return true; case Common::KEYCODE_LEFT: if (_cursorCol == 0) { if (_cursorRow == 0) { // Nowhere to go return true; } _cursorRow--; _cursorCol = _canvas.getLineCharWidth(_cursorRow); } else { _cursorCol--; } updateCursorPos(); return true; case Common::KEYCODE_RIGHT: if (_cursorCol >= _canvas.getLineCharWidth(_cursorRow)) { if (_cursorRow == getLineCount() - 1) { // Nowhere to go return true; } _cursorRow++; _cursorCol = 0; } else { _cursorCol++; } updateCursorPos(); return true; case Common::KEYCODE_UP: if (_cursorRow == 0) return true; _cursorRow--; getRowCol(_cursorX, _canvas._text[_cursorRow].y, nullptr, nullptr, &_cursorRow, &_cursorCol); updateCursorPos(); return true; case Common::KEYCODE_DOWN: if (_cursorRow == getLineCount() - 1) return true; _cursorRow++; getRowCol(_cursorX, _canvas._text[_cursorRow].y, nullptr, nullptr, &_cursorRow, &_cursorCol); updateCursorPos(); return true; case Common::KEYCODE_DELETE: // first try to delete the selected text if (_selectedText.endY != -1) { cutSelection(); _contentIsDirty = true; return true; } // move cursor to next one and delete previous char if (_cursorCol >= _canvas.getLineCharWidth(_cursorRow)) { if (_cursorRow == getLineCount() - 1) { return true; } _cursorRow++; _cursorCol = 0; } else { _cursorCol++; } deletePreviousChar(&_cursorRow, &_cursorCol); updateCursorPos(); _contentIsDirty = true; return true; default: if (event.kbd.ascii == '~') return false; if (event.kbd.ascii >= 0x20 && event.kbd.ascii <= 0x7f) { // if we have selected text, then we delete it, then we try to insert char if (_selectedText.endY != -1) { cutSelection(); _contentIsDirty = true; } insertChar((byte)event.kbd.ascii, &_cursorRow, &_cursorCol); updateCursorPos(); _contentIsDirty = true; return true; } break; } } if (event.type == Common::EVENT_WHEELUP) { scroll(-2); return true; } if (event.type == Common::EVENT_WHEELDOWN) { scroll(2); return true; } if (!_selectable) return false; if (event.type == Common::EVENT_LBUTTONDOWN) { bool active = _active; _wm->setActiveWidget(this); if (active == true) { // inactive -> active switching, we don't start marking the selection, because we have initial selection startMarking(event.mouse.x, event.mouse.y); } return true; } else if (event.type == Common::EVENT_LBUTTONUP) { if (_inTextSelection) { _inTextSelection = false; if (_selectedText.endY == -1 || (_selectedText.endX == _selectedText.startX && _selectedText.endY == _selectedText.startY)) { _selectedText.startY = _selectedText.endY = -1; _contentIsDirty = true; if (_menu) _menu->enableCommand("Edit", "Copy", false); Common::Point offset = calculateOffset(); int x = event.mouse.x - getDimensions().left - offset.x; int y = event.mouse.y - getDimensions().top + _scrollPos - offset.y; getRowCol(x, y, nullptr, nullptr, &_cursorRow, &_cursorCol); updateCursorPos(); } else { if (_menu) { _menu->enableCommand("Edit", "Copy", true); bool cutAllowed = isCutAllowed(); _menu->enableCommand("Edit", "Cut", cutAllowed); _menu->enableCommand("Edit", "Clear", cutAllowed); } } } return true; } else if (event.type == Common::EVENT_MOUSEMOVE) { if (_inTextSelection) { updateTextSelection(event.mouse.x, event.mouse.y); return true; } } return false; } void MacText::scroll(int delta) { // disable the scroll for auto expand text // should be amend to check the text type. e.g. auto expand type, fixed type, scroll type. if (!_fixedDims) return; int oldScrollPos = _scrollPos; _scrollPos += delta * kConScrollStep; if (_editable) _scrollPos = CLIP(_scrollPos, 0, MacText::getTextHeight() - kConScrollStep); else _scrollPos = CLIP(_scrollPos, 0, MAX(0, MacText::getTextHeight() - getDimensions().height())); undrawCursor(); _cursorY -= (_scrollPos - oldScrollPos); _contentIsDirty = true; } void MacText::startMarking(int x, int y) { if (_canvas._text.size() == 0) return; Common::Point offset = calculateOffset(); x -= getDimensions().left - offset.x; y -= getDimensions().top - offset.y; y += _scrollPos; getRowCol(x, y, &_selectedText.startX, &_selectedText.startY, &_selectedText.startRow, &_selectedText.startCol); _selectedText.endY = -1; _inTextSelection = true; } void MacText::updateTextSelection(int x, int y) { Common::Point offset = calculateOffset(); x -= getDimensions().left - offset.x; y -= getDimensions().top - offset.y; y += _scrollPos; getRowCol(x, y, &_selectedText.endX, &_selectedText.endY, &_selectedText.endRow, &_selectedText.endCol); debug(3, "s: %d,%d (%d, %d) e: %d,%d (%d, %d)", _selectedText.startX, _selectedText.startY, _selectedText.startRow, _selectedText.startCol, _selectedText.endX, _selectedText.endY, _selectedText.endRow, _selectedText.endCol); _contentIsDirty = true; } int MacText::getMouseChar(int x, int y) { Common::Point offset = calculateOffset(); x -= getDimensions().left - offset.x; y -= getDimensions().top - offset.y; y += _scrollPos; int dx, dy, row, col; getRowCol(x, y, &dx, &dy, &row, &col); int index = 0; for (int r = 0; r < row; r++) index += _canvas.getLineCharWidth(r); index += col; return index + 1; } int MacText::getMouseWord(int x, int y) { Common::Point offset = calculateOffset(); x -= getDimensions().left - offset.x; y -= getDimensions().top - offset.y; y += _scrollPos; int dx, dy, row, col; getRowCol(x, y, &dx, &dy, &row, &col); int index = 0; for (int i = 0; i < row; i++) { for (uint j = 0; j < _canvas._text[i].chunks.size(); j++) { if (_canvas._text[i].chunks[j].text.empty()) continue; index++; } } int cur = 0; for (uint j = 0; j < _canvas._text[row].chunks.size(); j++) { if (_canvas._text[row].chunks[j].text.empty()) continue; cur += _canvas._text[row].chunks[j].text.size(); // Avoid overflowing the word index if we run out of line; // it should count as part of the last chunk if ((cur <= col) && (j < _canvas._text[row].chunks.size() - 1)) index++; else break; } return index + 1; } int MacText::getMouseItem(int x, int y) { Common::Point offset = calculateOffset(); x -= getDimensions().left - offset.x; y -= getDimensions().top - offset.y; y += _scrollPos; int dx, dy, row, col; getRowCol(x, y, &dx, &dy, &row, &col); int index = 0; for (int i = 0; i < row; i++) { for (uint j = 0; j < _canvas._text[i].chunks.size(); j++) { if (_canvas._text[i].chunks[j].text.empty()) continue; if (_canvas._text[i].chunks[j].getEncodedText().contains(',')) index++; } } int cur = 0; for (uint i = 0; i < _canvas._text[row].chunks.size(); i++) { if (_canvas._text[row].chunks[i].text.empty()) continue; for (uint j = 0; j < _canvas._text[row].chunks[i].text.size(); j++) { cur++; if (cur > col) break; if (_canvas._text[row].chunks[i].text[j] == ',') index++; } if (cur > col) break; } return index + 1; } int MacText::getMouseLine(int x, int y) { Common::Point offset = calculateOffset(); x -= getDimensions().left - offset.x; y -= getDimensions().top - offset.y; y += _scrollPos; int dx, dy, row, col; getRowCol(x, y, &dx, &dy, &row, &col); return row + 1; } Common::U32String MacText::getMouseLink(int x, int y) { Common::Point offset = calculateOffset(); x -= getDimensions().left - offset.x; y -= getDimensions().top - offset.y; y += _scrollPos; int row, chunk; getRowCol(x, y, nullptr, nullptr, &row, nullptr, &chunk); if (chunk < 0) return Common::U32String(); if (!_canvas._text[row].picfname.empty()) return _canvas._text[row].pictitle; if (!_canvas._text[row].chunks[chunk].link.empty()) return _canvas._text[row].chunks[chunk].link; return Common::U32String(); } int MacTextCanvas::getAlignOffset(int row) { int alignOffset = 0; if (_textAlignment == kTextAlignRight) alignOffset = MAX(0, _maxWidth - getLineWidth(row) - 1); else if (_textAlignment == kTextAlignCenter) alignOffset = (_maxWidth / 2) - (getLineWidth(row) / 2); return alignOffset; } void MacText::getRowCol(int x, int y, int *sx, int *sy, int *row, int *col, int *chunk_) { int nsx = 0, nsy = 0, nrow = 0, ncol = 0; if (y > _canvas._textMaxHeight) { x = _canvas._surface->w; } y = CLIP(y, 0, _canvas._textMaxHeight); nrow = _canvas._text.size(); // use [lb, ub) bsearch here, final answer would be lb int lb = 0, ub = nrow; while (ub - lb > 1) { int mid = (ub + lb) / 2; if (_canvas._text[mid].y <= y) { lb = mid; } else { ub = mid; } } nrow = lb; nsy = _canvas._text[nrow].y; int chunk = -1; if (_canvas._text[nrow].chunks.size() > 0) { int alignOffset = _canvas.getAlignOffset(nrow) + _canvas._text[nrow].indent + _canvas._text[nrow].firstLineIndent;; int width = 0, pwidth = 0; int mcol = 0, pmcol = 0; for (chunk = 0; chunk < (int)_canvas._text[nrow].chunks.size(); chunk++) { pwidth = width; pmcol = mcol; if (!_canvas._text[nrow].chunks[chunk].text.empty()) { width += getStringWidth(_canvas._text[nrow].chunks[chunk], _canvas._text[nrow].chunks[chunk].text); mcol += _canvas._text[nrow].chunks[chunk].text.size(); } if (width + alignOffset > x) break; } if (chunk >= (int)_canvas._text[nrow].chunks.size()) chunk = _canvas._text[nrow].chunks.size() - 1; if (chunk_) *chunk_ = (int)chunk; Common::U32String str = _canvas._text[nrow].chunks[chunk].text; ncol = mcol; nsx = pwidth; for (int i = str.size(); i >= 0; i--) { int strw = getStringWidth(_canvas._text[nrow].chunks[chunk], str); if (strw + pwidth + alignOffset <= x) { ncol = pmcol + i; nsx = strw + pwidth; break; } str.deleteLastChar(); } } if (sx) *sx = nsx; if (sy) *sy = nsy; if (col) *col = ncol; if (row) *row = nrow; if (chunk_) *chunk_ = (int)chunk; } // If adjacent chunks have same format, then skip the format definition // This happens when a long paragraph is split into several lines #define ADDFORMATTING() \ if (formatted) { \ formatting = Common::U32String(_canvas._text[i].chunks[chunk].toString()); \ if (formatting != prevformatting) { \ res += formatting; \ prevformatting = formatting; \ } \ } Common::U32String MacText::getTextChunk(int startRow, int startCol, int endRow, int endCol, bool formatted, bool newlines) { Common::U32String res(""); if (endRow == -1) endRow = _canvas._text.size() - 1; if (endCol == -1) endCol = _canvas.getLineCharWidth(endRow); if (_canvas._text.empty()) { return res; } startRow = CLIP(startRow, 0, (int)_canvas._text.size() - 1); endRow = CLIP(endRow, 0, (int)_canvas._text.size() - 1); Common::U32String formatting(""), prevformatting(""); for (int i = startRow; i <= endRow; i++) { // We requested only part of one line if (i == startRow && i == endRow) { for (uint chunk = 0; chunk < _canvas._text[i].chunks.size(); chunk++) { if (_canvas._text[i].chunks[chunk].text.empty()) { // skip empty chunks, but keep them formatted, // a text input box needs to keep the formatting even when all text is removed. ADDFORMATTING(); continue; } if (startCol <= 0) { ADDFORMATTING(); if (endCol >= (int)_canvas._text[i].chunks[chunk].text.size()) res += _canvas._text[i].chunks[chunk].text; else res += _canvas._text[i].chunks[chunk].text.substr(0, endCol); } else if ((int)_canvas._text[i].chunks[chunk].text.size() > startCol) { ADDFORMATTING(); res += _canvas._text[i].chunks[chunk].text.substr(startCol, endCol - startCol); } startCol -= _canvas._text[i].chunks[chunk].text.size(); endCol -= _canvas._text[i].chunks[chunk].text.size(); if (endCol <= 0) break; } // We are at the top line and it is not completely requested } else if (i == startRow && startCol != 0) { for (uint chunk = 0; chunk < _canvas._text[i].chunks.size(); chunk++) { if (_canvas._text[i].chunks[chunk].text.empty()) // skip empty chunks continue; if (startCol <= 0) { ADDFORMATTING(); res += _canvas._text[i].chunks[chunk].text; } else if ((int)_canvas._text[i].chunks[chunk].text.size() > startCol) { ADDFORMATTING(); res += _canvas._text[i].chunks[chunk].text.substr(startCol); } startCol -= _canvas._text[i].chunks[chunk].text.size(); } if (newlines && _canvas._text[i].paragraphEnd) res += '\n'; // We are at the end row, and it could be not completely requested } else if (i == endRow) { for (uint chunk = 0; chunk < _canvas._text[i].chunks.size(); chunk++) { if (_canvas._text[i].chunks[chunk].text.empty()) // skip empty chunks continue; ADDFORMATTING(); if (endCol >= (int)_canvas._text[i].chunks[chunk].text.size()) res += _canvas._text[i].chunks[chunk].text; else res += _canvas._text[i].chunks[chunk].text.substr(0, endCol); endCol -= _canvas._text[i].chunks[chunk].text.size(); if (endCol <= 0) break; } // We are in the middle of requested range, pass whole line } else { for (uint chunk = 0; chunk < _canvas._text[i].chunks.size(); chunk++) { if (_canvas._text[i].chunks[chunk].text.empty()) // skip empty chunks continue; ADDFORMATTING(); res += _canvas._text[i].chunks[chunk].text; } if (newlines && _canvas._text[i].paragraphEnd) res += '\n'; } } return res; } // mostly, we refering reshuffleParagraph to implement this function void MacText::insertTextFromClipboard() { int ppos = 0; Common::U32String str = _wm->getTextFromClipboard(Common::U32String(_defaultFormatting.toString()), &ppos); if (_canvas._text.empty()) { _canvas.splitString(str, -1, _defaultFormatting); } else { int start = _cursorRow, end = _cursorRow; while (start && !_canvas._text[start - 1].paragraphEnd) start--; while (end < (int)_canvas._text.size() - 1 && !_canvas._text[end].paragraphEnd) end++; for (int i = start; i < _cursorRow; i++) ppos += _canvas.getLineCharWidth(i); ppos += _cursorCol; Common::U32String pre_str = getTextChunk(start, 0, _cursorRow, _cursorCol, true, true); Common::U32String sub_str = getTextChunk(_cursorRow, _cursorCol, end, _canvas.getLineCharWidth(end, true), true, true); // Remove it from the text for (int i = start; i <= end; i++) { _canvas._text.remove_at(start); } _canvas.splitString(pre_str + str + sub_str, start, _defaultFormatting); _cursorRow = start; } warning("FIXME, bad design"); while (ppos > _canvas.getLineCharWidth(_cursorRow, true)) { ppos -= _canvas.getLineCharWidth(_cursorRow, true); _cursorRow++; } _cursorCol = ppos; recalcDims(); updateCursorPos(); _fullRefresh = true; render(); } void MacText::setText(const Common::U32String &str) { _str = str; _canvas._text.clear(); _canvas.splitString(_str, -1, _defaultFormatting); _cursorRow = _cursorCol = 0; recalcDims(); updateCursorPos(); _fullRefresh = true; render(); if (_selectable) { setSelection(_selStart, true); setSelection(_selEnd, false); } _contentIsDirty = true; } ////////////////// // Text editing void MacText::insertChar(byte c, int *row, int *col) { if (_canvas._text.empty()) { appendTextDefault(Common::String(c)); (*col)++; return; } MacTextLine *line = &_canvas._text[*row]; int pos = *col; uint ch = line->getChunkNum(&pos); Common::U32String newchunk = line->chunks[ch].text; if (pos >= (int)newchunk.size()) newchunk += c; else newchunk.insertChar(c, pos); int chunkw = getStringWidth(line->chunks[ch], newchunk); int oldw = getStringWidth(line->chunks[ch], line->chunks[ch].text); line->chunks[ch].text = newchunk; line->width = -1; // Force recalc (*col)++; if (_canvas.getLineWidth(*row) - oldw + chunkw > _canvas._maxWidth) { // Needs reshuffle reshuffleParagraph(row, col); _fullRefresh = true; recalcDims(); render(); } else { recalcDims(); _canvas.render(*row, *row); } for (int i = 0; i < (int)_canvas._text.size(); i++) { D(9, "**insertChar line %d isEnd %d", i, _canvas._text[i].paragraphEnd); for (int j = 0; j < (int)_canvas._text[i].chunks.size(); j++) { D(9, "[%d] \"%s\"",_canvas._text[i].chunks[j].text.size(), Common::toPrintable(_canvas._text[i].chunks[j].text.encode()).c_str()); } } D(9, "**insertChar cursor row %d col %d", _cursorRow, _cursorCol); } void MacText::deleteSelection() { // TODO: maybe we need to implement an individual delete part for mactext if (_selectedText.endY == -1 || (_selectedText.startX == _selectedText.endX && _selectedText.startY == _selectedText.endY)) return; if (_selectedText.startRow == -1 || _selectedText.startCol == -1 || _selectedText.endRow == -1 || _selectedText.endCol == -1) error("deleting non-existing selected area"); SelectedText s = _selectedText; if (s.startY > s.endY || (s.startY == s.endY && s.startX > s.endX)) { SWAP(s.startX, s.endX); SWAP(s.startY, s.endY); SWAP(s.startRow, s.endRow); SWAP(s.startCol, s.endCol); } int row = s.endRow, col = s.endCol; while (row != s.startRow || col != s.startCol) { if (row == 0 && col == 0) break; deletePreviousCharInternal(&row, &col); } reshuffleParagraph(&row, &col); _fullRefresh = true; recalcDims(); render(); // update cursor position _cursorRow = row; _cursorCol = col; updateCursorPos(); } void MacText::deletePreviousCharInternal(int *row, int *col) { if (*col == 0) { // Need to glue the lines *col = _canvas.getLineCharWidth(*row - 1); (*row)--; // formatting matches, glue texts as normal if (_canvas._text[*row].lastChunk().equals(_canvas._text[*row + 1].firstChunk())) { _canvas._text[*row].lastChunk().text += _canvas._text[*row + 1].firstChunk().text; _canvas._text[*row + 1].firstChunk().text.clear(); } else { // formatting doesn't match, move whole chunk _canvas._text[*row].chunks.push_back(MacFontRun(_canvas._text[*row + 1].firstChunk())); _canvas._text[*row].firstChunk().text.clear(); } _canvas._text[*row].paragraphEnd = false; for (uint i = 1; i < _canvas._text[*row + 1].chunks.size(); i++) _canvas._text[*row].chunks.push_back(MacFontRun(_canvas._text[*row + 1].chunks[i])); _canvas._text.remove_at(*row + 1); } else { int pos = *col - 1; uint ch = _canvas._text[*row].getChunkNum(&pos); if (pos == (int)_canvas._text[*row].chunks[ch].text.size()) pos--; _canvas._text[*row].chunks[ch].text.deleteChar(pos); (*col)--; } _canvas._text[*row].width = -1; // flush the cache } void MacText::deletePreviousChar(int *row, int *col) { if (*col == 0 && *row == 0) // nothing to do return; deletePreviousCharInternal(row, col); for (int i = 0; i < (int)_canvas._text.size(); i++) { D(9, "**deleteChar line %d", i); for (int j = 0; j < (int)_canvas._text[i].chunks.size(); j++) { D(9, "[%d] \"%s\"",_canvas._text[i].chunks[j].text.size(), Common::toPrintable(_canvas._text[i].chunks[j].text.encode()).c_str()); } } D(9, "**deleteChar cursor row %d col %d", _cursorRow, _cursorCol); reshuffleParagraph(row, col); _fullRefresh = true; recalcDims(); render(); } void MacText::addNewLine(int *row, int *col) { if (_canvas._text.empty()) { appendTextDefault(Common::String("\n")); (*row)++; return; } MacTextLine *line = &_canvas._text[*row]; int pos = *col; uint ch = line->getChunkNum(&pos); MacFontRun newchunk = line->chunks[ch]; MacTextLine newline; // we have to inherit paragraphEnd from the origin line newline.paragraphEnd = line->paragraphEnd; newchunk.text = line->chunks[ch].text.substr(pos); line->chunks[ch].text = line->chunks[ch].text.substr(0, pos); line->paragraphEnd = true; newline.chunks.push_back(newchunk); for (uint i = ch + 1; i < line->chunks.size(); i++) { newline.chunks.push_back(MacFontRun(line->chunks[i])); } for (uint i = line->chunks.size() - 1; i >= ch + 1; i--) { line->chunks.pop_back(); } line->width = -1; // Drop cache _canvas._text[*row].width = -1; // flush the cache _canvas._text.insert_at(*row + 1, newline); (*row)++; *col = 0; reshuffleParagraph(row, col); for (int i = 0; i < (int)_canvas._text.size(); i++) { D(9, "** addNewLine line %d", i); for (int j = 0; j < (int)_canvas._text[i].chunks.size(); j++) { D(9, "[%d] \"%s\"",_canvas._text[i].chunks[j].text.size(), Common::toPrintable(_canvas._text[i].chunks[j].text.encode()).c_str()); } } D(9, "** addNewLine cursor row %d col %d", _cursorRow, _cursorCol); _fullRefresh = true; recalcDims(); render(); } void MacText::reshuffleParagraph(int *row, int *col) { // First, we looking for the paragraph start and end int start = *row, end = *row; while (start && !_canvas._text[start - 1].paragraphEnd) start--; while (end < (int)_canvas._text.size() - 1 && !_canvas._text[end].paragraphEnd) // stop at last line end++; // Get character pos within paragraph int ppos = 0; for (int i = start; i < *row; i++) ppos += _canvas.getLineCharWidth(i); ppos += *col; // Get whole paragraph Common::U32String paragraph = getTextChunk(start, 0, end, _canvas.getLineCharWidth(end, true), true, true); // Remove it from the text for (int i = start; i <= end; i++) { _canvas._text.remove_at(start); } // And now read it D(9, "start %d end %d", start, end); _canvas.splitString(paragraph, start, _defaultFormatting); // Find new pos within paragraph after reshuffling *row = start; warning("FIXME, bad design"); while (ppos > _canvas.getLineCharWidth(*row, true)) { ppos -= _canvas.getLineCharWidth(*row, true); (*row)++; } *col = ppos; } ////////////////// // Cursor stuff static void cursorTimerHandler(void *refCon) { MacText *w = (MacText *)refCon; if (!w->_cursorOff) w->_cursorState = !w->_cursorState; w->_cursorDirty = true; } void MacText::updateCursorPos() { if (_canvas._text.empty()) { _cursorX = _cursorY = 0; } else { undrawCursor(); _cursorRow = MIN(_cursorRow, _canvas._text.size() - 1); int alignOffset = _canvas.getAlignOffset(_cursorRow); _cursorY = _canvas._text[_cursorRow].y - _scrollPos; _cursorX = _canvas.getLineWidth(_cursorRow, false, _cursorCol) + alignOffset; } int cursorHeight = getLineHeight(_cursorRow); if (cursorHeight == 0) cursorHeight = 12; // Do not exceed max height and widget height cursorHeight = MIN(MIN(cursorHeight, kCursorMaxHeight), _dims.height()); _cursorRect->setHeight(cursorHeight); _cursorDirty = true; } void MacText::undrawCursor() { _cursorDirty = true; Common::Point offset(calculateOffset()); _composeSurface->blitFrom(*_cursorSurface2, *_cursorRect, Common::Point(_cursorX + offset.x, _cursorY + offset.y)); } void MacText::setImageArchive(Common::String fname) { _imageArchive = Common::makeZipArchive(fname); if (!_imageArchive) warning("MacText::setImageArchive(): Could not find %s. Images will not be rendered", fname.c_str()); } const Surface *MacText::getImageSurface(Common::String &fname) { #ifndef USE_PNG warning("MacText::getImageSurface(): PNG support not compiled. Cannot load file %s", fname.c_str()); return nullptr; #else if (_imageCache.contains(fname)) return _imageCache[fname]->getSurface(); if (!_imageArchive) { warning("MacText::getImageSurface(): Image Archive was not loaded. Use setImageArchive()"); return nullptr; } Common::SeekableReadStream *stream = _imageArchive->createReadStreamForMember(fname); if (!stream) { warning("MacText::getImageSurface(): Cannot open file %s", fname.c_str()); return nullptr; } _imageCache[fname] = new Image::PNGDecoder(); if (!_imageCache[fname]->loadStream(*stream)) { delete _imageCache[fname]; warning("MacText::getImageSurface(): Cannot load file %s", fname.c_str()); return nullptr; } return _imageCache[fname]->getSurface(); #endif // USE_PNG } void MacTextCanvas::processTable(int line, int maxWidth) { Common::Array *table = _text[line].table; uint numCols = table->front().cells.size(); uint numRows = table->size(); Common::Array maxW(numCols), maxL(numCols), colW(numCols), rowH(numRows); Common::Array flex(numCols), wrap(numCols); int width = maxWidth * 0.9; int gutter = 10; // Compute column widths, both minimal and maximal for (auto &row : *table) { int i = 0; for (auto &cell : row.cells) { int cW = 0, cL = 0; for (uint l = 0; l < cell._text.size(); l++) { (void)cell.getLineWidth(l); // calculate it cW = MAX(cW, cell._text[l].width); cL = MAX(cL, cell._text[l].minWidth); } maxW[i] = MAX(maxW[i], cW); maxL[i] = MAX(maxL[i], cL); i++; } } for (uint i = 0; i < numCols; i++) { warning("%d: %d - %d", i, maxL[i], maxW[i]); wrap[i] = (maxW[i] != maxL[i]); } int left = width - (numCols - 1) * gutter; int avg = left / numCols; int nflex = 0; // determine whether columns should be flexible and assign // width of non-flexible cells for (uint i = 0; i < numCols; i++) { flex[i] = (maxW[i] > 2 * avg); if (flex[i]) { nflex++; } else { colW[i] = maxW[i]; left -= colW[i]; } } // if there is not enough space, make columns that could // be word-wrapped flexible, too if (left < nflex * avg) { for (uint i = 0; i < numCols; i++) { if (!flex[i] && wrap[i]) { left += colW[i]; colW[i] = 0; flex[i] = true; nflex += 1; } } } // Calculate weights for flexible columns. The max width // is capped at the page width to treat columns that have to // be wrapped more or less equal int tot = 0; for (uint i = 0; i < numCols; i++) { if (flex[i]) { maxW[i] = MIN(maxW[i], width); tot += maxW[i]; } } // Now assign the actual width for flexible columns. Make // sure that it is at least as long as the longest word length for (uint i = 0; i < numCols; i++) { if (flex[i]) { colW[i] = left * maxW[i] / tot; colW[i] = MAX(colW[i], maxL[i]); left -= colW[i]; } } for (uint i = 0; i < numCols; i++) { warning("%d: %d", i, colW[i]); } int r = 0; for (auto &row : *table) { int c = 0; rowH[r] = 0; for (auto &cell : row.cells) { cell._maxWidth = colW[c]; cell.recalcDims(); cell.reallocSurface(); cell._surface->clear(_tbgcolor); cell.render(0, cell._text.size()); rowH[r] = MAX(rowH[r], cell._textMaxHeight); c++; } r++; } int tW = 1, tH = 1; for (uint i = 0; i < table->size(); i++) tH += rowH[i] + gutter * 2 + 1; for (uint i = 0; i < table->front().cells.size(); i++) tW += colW[i] + gutter * 2 + 1; ManagedSurface *surf = new ManagedSurface(tW, tH, _wm->_pixelformat); _text[line].tableSurface = surf; _text[line].height = tH; _text[line].width = tW; surf->clear(_tbgcolor); surf->hLine(0, 0, tW, _tfgcolor); surf->vLine(0, 0, tH, _tfgcolor); int y = 1; for (uint i = 0; i < table->size(); i++) { y += gutter * 2 + rowH[i]; surf->hLine(0, y, tW, _tfgcolor); y++; } int x = 1; for (uint i = 0; i < table->front().cells.size(); i++) { x += gutter * 2 + colW[i]; surf->vLine(x, 0, tH, _tfgcolor); x++; } r = 0; y = 1 + gutter; for (auto &row : *table) { int c = 0; x = 1 + gutter; for (auto &cell : row.cells) { surf->blitFrom(*cell._surface, Common::Point(x, y)); x += gutter * 2 + 1 + colW[c]; c++; } y += gutter * 2 + 1 + rowH[r]; r++; } } } // End of namespace Graphics