/* 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/unicode-bidi.h" #include "common/timer.h" #include "common/system.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" 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() { return wm->_fontMan->getFontEncoding(fontId); } bool MacFontRun::plainByteMode() { Common::CodePage encoding = getEncoding(); // This return statement accounts for utf8, invalid, and Mac Roman. // For future Unicode font compatibility, it should account for all codepages instead. return encoding != Common::kUtf8 && encoding != Common::kCodePageInvalid && encoding != Common::kMacRoman; } 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), _maxWidth(maxWidth), _textAlignment(textAlignment), _interLinear(interlinear) { 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; _fgcolor = fgcolor; _bgcolor = bgcolor; _textShadow = textShadow; _macFontMode = true; if (macFont) { _defaultFormatting = MacFontRun(_wm); _defaultFormatting.font = wm->_fontMan->getFont(*macFont); _defaultFormatting.setValues(_wm, macFont->getId(), macFont->getSlant(), macFont->getSize(), 0, 0, 0); } 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), _maxWidth(maxWidth), _textAlignment(textAlignment), _interLinear(interlinear) { _str = s; _fixedDims = fixedDims; _wm = wm; _fgcolor = fgcolor; _bgcolor = bgcolor; _textShadow = 0; _macFontMode = true; if (macFont) { _defaultFormatting = MacFontRun(_wm, macFont->getId(), macFont->getSlant(), macFont->getSize(), 0, 0, 0); _defaultFormatting.font = wm->_fontMan->getFont(*macFont); } 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), _maxWidth(maxWidth), _textAlignment(textAlignment), _interLinear(interlinear) { _str = s; _fixedDims = fixedDims; _wm = wm; _fgcolor = fgcolor; _bgcolor = bgcolor; _textShadow = 0; _macFontMode = false; 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; _textMaxWidth = 0; _textMaxHeight = 0; _surface = nullptr; _shadowSurface = nullptr; if (!_fixedDims) { int right = _dims.right; _dims.right = MAX(_dims.right, _dims.left + _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; splitString(_str); 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 = 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()) { _fgcolor = colorFontRun.fgcolor; colorFontRun.text.clear(); debug(9, "Reading fg color though text, instead of the argument, read %x", _fgcolor); _defaultFormatting = colorFontRun; _defaultFormatting.wm = _wm; } _currentFormatting = _defaultFormatting; _composeSurface->clear(_bgcolor); _cursorSurface = new ManagedSurface(1, kCursorMaxHeight, _wm->_pixelformat); _cursorSurface->clear(_fgcolor); _cursorSurface2 = new ManagedSurface(1, kCursorMaxHeight, _wm->_pixelformat); _cursorSurface2->clear(_bgcolor); reallocSurface(); setAlignOffset(_textAlignment); updateCursorPos(); render(); } MacText::~MacText() { if (_wm->getActiveWidget() == this) _wm->setActiveWidget(nullptr); delete _cursorRect; delete _surface; delete _shadowSurface; delete _cursorSurface; delete _cursorSurface2; } // this func returns the fg color of the first character we met in text MacFontRun MacText::getFgColor() { if (_textLines.empty()) return MacFontRun(); for (uint i = 0; i < _textLines.size(); i++) { for (uint j = 0; j < _textLines[i].chunks.size(); j++) { if (!_textLines[i].chunks[j].text.empty()) return _textLines[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 MacText::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); } void MacText::setMaxWidth(int maxWidth) { if (maxWidth == _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 += getLineCharWidth(i); ppos += _cursorCol; _maxWidth = maxWidth; _textLines.clear(); splitString(str); // restore the cursor pos _cursorRow = 0; while (ppos > getLineCharWidth(_cursorRow, true)) { ppos -= 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) { _bgcolor = bg; _fgcolor = fg; // also set the cursor color _cursorSurface->clear(_fgcolor); for (uint i = 0; i < _textLines.size(); i++) setTextColor(fg, i); _fullRefresh = true; render(); _contentIsDirty = true; } void MacText::enforceTextFont(uint16 fontId) { for (uint i = 0; i < _textLines.size(); i++) { for (uint j = 0; j < _textLines[i].chunks.size(); j++) { _textLines[i].chunks[j].fontId = fontId; } } _fullRefresh = true; render(); _contentIsDirty = true; } void MacText::setTextSize(int textSize) { for (uint i = 0; i < _textLines.size(); i++) { for (uint j = 0; j < _textLines[i].chunks.size(); j++) { _textLines[i].chunks[j].fontSize = textSize; } } _fullRefresh = true; render(); _contentIsDirty = true; } void MacText::setTextColor(uint32 color, uint32 line) { if (line >= _textLines.size()) { warning("MacText::setTextColor(): line %d is out of bounds", line); return; } uint32 fgcol = _wm->findBestColor(color); for (uint j = 0; j < _textLines[line].chunks.size(); j++) { _textLines[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 (_textLines.empty()) { lineNum = chunkNum = offset = 0; return; } for (uint i = 0; i < _textLines.size(); i++) { if (getLineCharWidth(i) <= index) { index -= getLineCharWidth(i); } else { lineNum = i; chunkNum = _textLines[i].getChunkNum(&index); offset = index; return; } } lineNum = _textLines.size() - 1; chunkNum = _textLines[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 (_textLines.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 = _textLines[startRow].chunks[startCol].text.size(); MacFontRun newChunk = _textLines[startRow].chunks[startCol]; newChunk.text = newChunk.text.substr(offset, textSize - offset); _textLines[startRow].chunks[startCol].text = _textLines[startRow].chunks[startCol].text.substr(0, offset); _textLines[startRow].chunks.insert_at(startCol + 1, newChunk); startCol++; } getChunkPosFromIndex(end, endRow, endCol, offset); if (offset != 0) { uint textSize = _textLines[endRow].chunks[endCol].text.size(); MacFontRun newChunk = _textLines[endRow].chunks[endCol]; newChunk.text = newChunk.text.substr(offset, textSize - offset); _textLines[endRow].chunks[endCol].text = _textLines[endRow].chunks[endCol].text.substr(0, offset); _textLines[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 = _textLines[startRow].chunks.size(); } else if (i == endRow) { from = 0; to = endCol; } else { from = 0; to = _textLines[i].chunks.size(); } for (uint j = from; j < to; j++) { callback(_textLines[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 < _textLines.size(); i++) { for (uint j = 0; j < _textLines[i].chunks.size(); j++) { if (textSlant) { _textLines[i].chunks[j].textSlant |= textSlant; } else { _textLines[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 (_textLines.empty()) return _defaultFormatting; if (start > end) SWAP(start, end); uint startRow, startCol; uint offset; getChunkPosFromIndex(start, startRow, startCol, offset); return _textLines[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); } static const Common::U32String::value_type *readHex(uint16 *res, const Common::U32String::value_type *s, int len) { *res = 0; for (int i = 0; i < len; i++) { char b = (char)*s++; *res <<= 4; if (tolower(b) >= 'a') *res |= tolower(b) - 'a' + 10; else *res |= tolower(b) - '0'; } return s; } // Adds the given string to the end of the last line/chunk // while observing the _maxWidth and keeping this chunk's // formatting void MacText::chopChunk(const Common::U32String &str, int *curLinePtr) { int curLine = *curLinePtr; int curChunk = _textLines[curLine].chunks.size() - 1; MacFontRun *chunk = &_textLines[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); _textLines[curLine].chunks.pop_back(); 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 = _textLines[curLine].chunks[curChunk]; for (uint i = 1; i < text.size(); i++) { newchunk.text = text[i]; curLine++; _textLines.insert_at(curLine, MacTextLine()); _textLines[curLine].chunks.push_back(newchunk); D(9, "** chopChunk, added line: \"%s\"", toPrintable(text[i].encode()).c_str()); } *curLinePtr = curLine; } void MacText::splitString(const Common::U32String &str, int curLine) { const Common::U32String::value_type *l = str.c_str(); D(9, "** splitString(\"%s\")", toPrintable(str.encode()).c_str()); // TODO::code is not elegant, we need to figure out a way which include all situations if (curLine == -1) curLine = _textLines.size(); if (_textLines.empty()) { _textLines.resize(1); _textLines[0].chunks.push_back(_defaultFormatting); D(9, "** splitString, added default formatting"); } else { _textLines.insert_at(curLine, MacTextLine()); D(9, "** splitString, continuing, %d lines", _textLines.size()); } if (curLine == -1) curLine = _textLines.size() - 1; if (str.empty()) { _textLines[curLine].chunks.push_back(_defaultFormatting); debug(9,"** splitString, empty line"); return; } Common::U32String paragraph, tmp; MacFontRun current_format = _defaultFormatting; while (*l) { paragraph.clear(); // First, get the whole paragraph while (*l) { if (*l == '\r') { l++; if (*l == '\n') // Skip whole '\r\n' l++; break; } // deal with single \n if (*l == '\n') { l++; break; } paragraph += *l++; } D(9, "** splitString, paragraph: \"%s\"", Common::toPrintable(paragraph.encode()).c_str()); // Now process whole paragraph const Common::U32String::value_type *s = paragraph.c_str(); while (*s) { tmp.clear(); // Skip \001 if (*s == '\001') { s++; if (*s == '\001') { tmp += *s; s++; } } // get format if (*s == '\015') { // binary format s++; uint16 fontId = *s++; fontId = (fontId << 8) | *s++; byte textSlant = *s++; uint16 fontSize = *s++; fontSize = (fontSize << 8) | *s++; uint16 palinfo1 = *s++; palinfo1 = (palinfo1 << 8) | *s++; uint16 palinfo2 = *s++; palinfo2 = (palinfo2 << 8) | *s++; uint16 palinfo3 = *s++; palinfo3 = (palinfo3 << 8) | *s++; D(9, "** splitString: fontId: %d, textSlant: %d, fontSize: %d, p0: %x p1: %x p2: %x", fontId, textSlant, fontSize, palinfo1, palinfo2, palinfo3); current_format.setValues(_wm, fontId, textSlant, fontSize, palinfo1, palinfo2, palinfo3); if (!_macFontMode) current_format.font = _defaultFormatting.font; } else if (*s == '\016') { // human-readable format s++; 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); D(9, "** splitString: fontId: %d, textSlant: %d, fontSize: %d, p0: %x p1: %x p2: %x", fontId, textSlant, fontSize, palinfo1, palinfo2, palinfo3); current_format.setValues(_wm, fontId, textSlant, fontSize, palinfo1, palinfo2, palinfo3); // So far, we enforce single font here, though in the future, font size could be altered if (!_macFontMode) current_format.font = _defaultFormatting.font; } while (*s && *s != ' ' && *s != '\001') { tmp += *s; s++; } // meaning there is a word with multifont if (*s == '\001') { _textLines[curLine].chunks.push_back(current_format); _textLines[curLine].lastChunk().wordContinuation = true; _textLines[curLine].lastChunk().text = tmp; continue; } // calc word_width, the trick we define here is we don`t count the space int word_width = getStringWidth(current_format, tmp); // add all spaces left while (*s == ' ') { tmp += *s; s++; } // now let`s try to split // first we have to try to get the whole word Common::Array word; word.push_back(current_format); word[0].text = tmp; while (!_textLines[curLine].chunks.empty() && _textLines[curLine].lastChunk().wordContinuation) { word.push_back(_textLines[curLine].lastChunk()); _textLines[curLine].chunks.pop_back(); } for (int i = 1; i < (int)word.size(); i++) { word_width += getStringWidth(word[i], word[i].text); D(9, "** word \"%s\" textslant [%d]", Common::toPrintable(word[i].text.encode()).c_str(), word[i].textSlant); } int cur_width = getLineWidth(curLine, true); D(9, "curWidth %d word_width %d", cur_width, word_width); // if cur_width == 0 but there`s chunks, meaning there must be empty string here // if cur_width == 0, then you don`t have to add a newline for it if (cur_width + word_width >= _maxWidth && cur_width != 0) { ++curLine; _textLines.insert_at(curLine, MacTextLine()); } // deal with the super long word situation if (word_width > _maxWidth) { for (int i = word.size() - 1; i >= 0; i--) { cur_width = getLineWidth(curLine, true); // count the size without space // because you don`t want to split a word just for space // actually i think this part can be optimized because only word[0] have space // we just need to deal it specially // meaning you have to split this word; int tmp_width = 0; _textLines[curLine].chunks.push_back(word[i]); // empty the string _textLines[curLine].lastChunk().text = Common::U32String(); for (Common::U32String::const_iterator it = word[i].text.begin(); it != word[i].text.end(); it++) { Common::U32String::unsigned_type c = *it; if (c == ' ') { // add the space left while (it != word[i].text.end()) { c = *it; _textLines[curLine].lastChunk().text += c; it++; } break; } // here, if we are in the plainByteMode, then we need to get the original text width, because current font may not resolve that u32string int char_width = 0; if (word[i].plainByteMode()) { char_width = word[i].getFont()->getCharWidth(Common::convertFromU32String(Common::U32String(it, 1), word[i].getEncoding())[0]); } else { char_width = word[i].getFont()->getCharWidth(c); } if (char_width + tmp_width + cur_width >= _maxWidth) { ++curLine; _textLines.insert_at(curLine, MacTextLine()); _textLines[curLine].chunks.push_back(word[i]); _textLines[curLine].lastChunk().text = Common::U32String(); tmp_width = 0; cur_width = 0; } tmp_width += char_width; _textLines[curLine].lastChunk().text += c; } } } else { for (int i = word.size() - 1; i >= 0; i--) { _textLines[curLine].chunks.push_back(word[i]); } } // If it is end of the line, we're done if (!*s) { _textLines[curLine].paragraphEnd = true; D(9, "** splitString, end of line"); break; } } if (!*l) { // If this is end of the string, we're done here break; } // Add new line D(9, "** splitString: new line"); _textLines[curLine].paragraphEnd = true; // if the chunks is empty, which means the line will not be rendered properly // so we add a empty string here if (_textLines[curLine].chunks.empty()) { _textLines[curLine].chunks.push_back(_defaultFormatting); } curLine++; _textLines.insert_at(curLine, MacTextLine()); } #if DEBUG for (uint i = 0; i < _textLines.size(); i++) { debugN(9, "** splitString: %2d ", i); for (uint j = 0; j < _textLines[i].chunks.size(); j++) debugN(9, "[%d] \"%s\"", _textLines[i].chunks[j].text.size(), Common::toPrintable(_textLines[i].chunks[j].text.encode()).c_str()); debugN(9, "\n"); } debug(9, "** splitString: done"); #endif } void MacText::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(_bgcolor); 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(_bgcolor); newShadowSurface->blitFrom(*_shadowSurface, Common::Point(0, 0)); delete _shadowSurface; _shadowSurface = newShadowSurface; } } } void MacText::render() { if (_fullRefresh) { _surface->clear(_bgcolor); if (_textShadow) _shadowSurface->clear(_bgcolor); render(0, _textLines.size()); _fullRefresh = false; } } void MacText::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) { int xOffset = getAlignOffset(i); xOffset++; int start = 0, end = _textLines[i].chunks.size(); if (_wm->_language == Common::HE_ISR) { start = _textLines[i].chunks.size() - 1; end = -1; } int maxAscentForRow = 0; for (int j = start; j != end; j += delta) { if (_textLines[i].chunks[j].font->getFontAscent() > maxAscentForRow) maxAscentForRow = _textLines[i].chunks[j].font->getFontAscent(); } // TODO: _textMaxWidth, when -1, was not rendering ANY text. for (int j = start; j != end; j += delta) { debug(9, "MacText::render: line %d[%d] h:%d at %d,%d (%s) fontid: %d on %dx%d, fgcolor: %d bgcolor: %d, font: %p", i, j, _textLines[i].height, xOffset, _textLines[i].y, _textLines[i].chunks[j].text.encode().c_str(), _textLines[i].chunks[j].fontId, _surface->w, _surface->h, _textLines[i].chunks[j].fgcolor, _bgcolor, (const void *)_textLines[i].chunks[j].getFont()); if (_textLines[i].chunks[j].text.empty()) continue; int yOffset = 0; if (_textLines[i].chunks[j].font->getFontAscent() < maxAscentForRow) { yOffset = maxAscentForRow - _textLines[i].chunks[j].font->getFontAscent(); } if (_textLines[i].chunks[j].plainByteMode()) { Common::String str = _textLines[i].chunks[j].getEncodedText(); _textLines[i].chunks[j].getFont()->drawString(surface, str, xOffset, _textLines[i].y + yOffset, w, shadow ? _wm->_colorBlack : _textLines[i].chunks[j].fgcolor, Graphics::kTextAlignLeft, 0, true); xOffset += _textLines[i].chunks[j].getFont()->getStringWidth(str); } else { if (_wm->_language == Common::HE_ISR) _textLines[i].chunks[j].getFont()->drawString(surface, convertBiDiU32String(_textLines[i].chunks[j].text, Common::BIDI_PAR_RTL), xOffset, _textLines[i].y + yOffset, w, shadow ? _wm->_colorBlack : _textLines[i].chunks[j].fgcolor, Graphics::kTextAlignLeft, 0, true); else _textLines[i].chunks[j].getFont()->drawString(surface, convertBiDiU32String(_textLines[i].chunks[j].text), xOffset, _textLines[i].y + yOffset, w, shadow ? _wm->_colorBlack : _textLines[i].chunks[j].fgcolor, Graphics::kTextAlignLeft, 0, true); xOffset += _textLines[i].chunks[j].getFont()->getStringWidth(_textLines[i].chunks[j].text); } } } } void MacText::render(int from, int to) { if (_textLines.empty()) return; reallocSurface(); from = MAX(0, from); to = MIN(to, _textLines.size() - 1); // Clear the screen _surface->fillRect(Common::Rect(0, _textLines[from].y, _surface->w, _textLines[to].y + getLineHeight(to)), _bgcolor); // render the shadow surface; if (_textShadow) render(from, to, _textShadow); render(from, to, 0); for (uint i = 0; i < _textLines.size(); i++) { debugN(9, "MacText::render: %2d ", i); for (uint j = 0; j < _textLines[i].chunks.size(); j++) debugN(9, "[%d (%d)] \"%s\" ", _textLines[i].chunks[j].fontId, _textLines[i].chunks[j].textSlant, _textLines[i].chunks[j].text.encode().c_str()); debug(9, "%s", ""); } } int MacText::getLineWidth(int line, bool enforce, int col) { if ((uint)line >= _textLines.size()) return 0; if (_textLines[line].width != -1 && !enforce && col == -1) return _textLines[line].width; int width = 0; int height = 0; int charwidth = 0; bool hastext = false; for (uint i = 0; i < _textLines[line].chunks.size(); i++) { if (enforce && _macFontMode) _textLines[line].chunks[i].font = nullptr; if (col >= 0) { if (col >= (int)_textLines[line].chunks[i].text.size()) { col -= _textLines[line].chunks[i].text.size(); } else { Common::U32String tmp = _textLines[line].chunks[i].text.substr(0, col); width += getStringWidth(_textLines[line].chunks[i], tmp); return width; } } if (!_textLines[line].chunks[i].text.empty()) { width += getStringWidth(_textLines[line].chunks[i], _textLines[line].chunks[i].text); charwidth += _textLines[line].chunks[i].text.size(); hastext = true; } height = MAX(height, _textLines[line].chunks[i].getFont()->getFontHeight()); } if (!hastext && _textLines.size() > 1) height = height > 3 ? height - 3 : 0; _textLines[line].width = width; _textLines[line].height = height; _textLines[line].charwidth = charwidth; return width; } int MacText::getLineCharWidth(int line, bool enforce) { if ((uint)line >= _textLines.size()) return 0; if (_textLines[line].charwidth != -1 && !enforce) return _textLines[line].charwidth; int width = 0; for (uint i = 0; i < _textLines[line].chunks.size(); i++) { if (!_textLines[line].chunks[i].text.empty()) width += _textLines[line].chunks[i].text.size(); } _textLines[line].charwidth = width; return width; } int MacText::getLastLineWidth() { if (_textLines.size() == 0) return 0; return getLineWidth(_textLines.size() - 1, true); } int MacText::getLineHeight(int line) { if ((uint)line >= _textLines.size()) return 0; getLineWidth(line); // This calculates height also return _textLines[line].height; } void MacText::setInterLinear(int interLinear) { _interLinear = interLinear; recalcDims(); _fullRefresh = true; render(); _contentIsDirty = true; } void MacText::recalcDims() { if (_textLines.empty()) return; int y = 0; _textMaxWidth = 0; for (uint i = 0; i < _textLines.size(); i++) { _textLines[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; if (!_fixedDims) { int newBottom = _dims.top + _textMaxHeight + (2 * _border) + _gutter + _shadow; if (newBottom > _dims.bottom) { _dims.bottom = newBottom; delete _composeSurface; _composeSurface = new ManagedSurface(_dims.width(), _dims.height(), _wm->_pixelformat); reallocSurface(); if (!_fullRefresh) { _fullRefresh = true; render(); } _fullRefresh = true; _contentIsDirty = true; } } } void MacText::setAlignOffset(TextAlign align) { if (_textAlignment == align) return; _contentIsDirty = true; _fullRefresh = true; _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 (_surface->w == w && _surface->h == h) return; setMaxWidth(w); if (_composeSurface->w != w || _composeSurface->h != h) { delete _composeSurface; _composeSurface = new Graphics::ManagedSurface(w, h); _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 = _textLines.size(); MacFontRun fontRun = MacFontRun(_wm, fontId, fontSlant, fontSize, r, g, b); _currentFormatting = fontRun; // we check _str here, if _str is empty but _textLines 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 (!_textLines.empty() && !_textLines.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 = _textLines.size(); MacFontRun fontRun = MacFontRun(_wm, font, 0, font->getFontHeight(), r, g, b); _currentFormatting = fontRun; if (_str.empty()) { while (!_textLines.empty() && !_textLines.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) { splitString(strWithFont); recalcDims(); render(oldLen - 1, _textLines.size()); _contentIsDirty = true; if (_editable) { _scrollPos = MAX(0, getTextHeight() - getDimensions().height()); _cursorRow = getLineCount(); _cursorCol = getLineCharWidth(_cursorRow); updateCursorPos(); } } void MacText::appendTextDefault(const Common::U32String &str, bool skipAdd) { uint oldLen = _textLines.size(); _currentFormatting = _defaultFormatting; Common::U32String strWithFont = Common::U32String(_defaultFormatting.toString()) + str; if (!skipAdd) { _str += strWithFont; } splitString(strWithFont); recalcDims(); render(oldLen - 1, _textLines.size()); } void MacText::appendTextDefault(const Common::String &str, bool skipAdd) { appendTextDefault(Common::U32String(str), skipAdd); } void MacText::clearText() { _contentIsDirty = true; _textLines.clear(); _str.clear(); if (_surface) _surface->clear(_bgcolor); recalcDims(); _cursorRow = _cursorCol = 0; updateCursorPos(); } void MacText::removeLastLine() { if (!_textLines.size()) return; int h = getLineHeight(_textLines.size() - 1) + _interLinear; _surface->fillRect(Common::Rect(0, _textMaxHeight - h, _surface->w, _textMaxHeight), _bgcolor); _textLines.pop_back(); _textMaxHeight -= h; } void MacText::draw(ManagedSurface *g, int x, int y, int w, int h, int xoff, int yoff) { if (_textLines.empty()) return; render(); if (x + w < _surface->w || y + h < _surface->h) g->fillRect(Common::Rect(x + xoff, y + yoff, x + w + xoff, y + h + yoff), _bgcolor); // blit shadow surface first if (_textShadow) g->blitFrom(*_shadowSurface, Common::Rect(MIN(_surface->w, x), MIN(_surface->h, y), MIN(_surface->w, x + w), MIN(_surface->h, y + h)), Common::Point(xoff + _textShadow, yoff + _textShadow)); g->transBlitFrom(*_surface, Common::Rect(MIN(_surface->w, x), MIN(_surface->h, y), MIN(_surface->w, x + w), MIN(_surface->h, y + h)), Common::Point(xoff, yoff), _bgcolor); _contentIsDirty = false; _cursorDirty = false; } bool MacText::draw(bool forceRedraw) { if (!needsRedraw() && !forceRedraw) return false; if (!_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(_bgcolor); // 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, _surface->w, _scrollPos + _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, 0); } 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 (_textLines.empty()) return; render(); srcRect.clip(_surface->getBounds()); if (srcRect.isEmpty()) return; g->blitFrom(*_surface, srcRect, dstPoint); } void MacText::drawToPoint(ManagedSurface *g, Common::Point dstPoint) { if (_textLines.empty()) return; render(); g->blitFrom(*_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 == 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 == _textLines.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 = 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 < _textLines.size()) { x1 = 0; x2 = maxSelectionWidth; alignOffset = 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 == _fgcolor) *ptr = _bgcolor; else *ptr = _fgcolor; } } 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 += getLineCharWidth(row); pos += _selectedText.startCol; return pos; } else { for (int row = 0; row < _selectedText.endRow; row++) pos += 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 < getLineCharWidth(row)) { for (uint i = 0; i < _textLines[row].chunks.size(); i++) { if ((uint)pos < _textLines[row].chunks[i].text.size()) { colX += getStringWidth(_textLines[row].chunks[i], _textLines[row].chunks[i].text.substr(0, pos)); col += pos; pos = 0; break; } else { colX += getStringWidth(_textLines[row].chunks[i], _textLines[row].chunks[i].text); pos -= _textLines[row].chunks[i].text.size(); col += _textLines[row].chunks[i].text.size(); } } break; } else { pos -= getLineCharWidth(row); // (row ? 1 : 0); } row++; if ((uint)row >= _textLines.size()) { row = _textLines.size() - 1; colX = _surface->w; col = getLineCharWidth(row); break; } } } else if (pos == 0) { colX = col = row = 0; } else { row = _textLines.size() - 1; col = getLineCharWidth(row); // if we don't have any text, then we won't select the whole area. if (_textMaxWidth == 0) colX = 0; else colX = _textMaxWidth; } int rowY = _textLines[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 < _textLines.size(); i++) { for (uint j = 0; j < _textLines[i].chunks.size(); j++) { res += _textLines[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 = getLineCharWidth(_cursorRow); } else { _cursorCol--; } updateCursorPos(); return true; case Common::KEYCODE_RIGHT: if (_cursorCol >= 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, _textLines[_cursorRow].y, nullptr, nullptr, &_cursorRow, &_cursorCol); updateCursorPos(); return true; case Common::KEYCODE_DOWN: if (_cursorRow == getLineCount() - 1) return true; _cursorRow++; getRowCol(_cursorX, _textLines[_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 >= 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 (_textLines.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 += 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 < _textLines[i].chunks.size(); j++) { if (_textLines[i].chunks[j].text.empty()) continue; index++; } } int cur = 0; for (uint j = 0; j < _textLines[row].chunks.size(); j++) { if (_textLines[row].chunks[j].text.empty()) continue; cur += _textLines[row].chunks[j].text.size(); if (cur <= col) 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 < _textLines[i].chunks.size(); j++) { if (_textLines[i].chunks[j].text.empty()) continue; if (_textLines[i].chunks[j].getEncodedText().contains(',')) index++; } } int cur = 0; for (uint i = 0; i < _textLines[row].chunks.size(); i++) { if (_textLines[row].chunks[i].text.empty()) continue; for (uint j = 0; j < _textLines[row].chunks[i].text.size(); j++) { cur++; if (cur > col) break; if (_textLines[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; } int MacText::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 nsx = 0, nsy = 0, nrow = 0, ncol = 0; if (y > _textMaxHeight) { x = _surface->w; } y = CLIP(y, 0, _textMaxHeight); nrow = _textLines.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 (_textLines[mid].y <= y) { lb = mid; } else { ub = mid; } } nrow = lb; nsy = _textLines[nrow].y; if (_textLines[nrow].chunks.size() > 0) { int alignOffset = getAlignOffset(nrow); int width = 0, pwidth = 0; int mcol = 0, pmcol = 0; uint chunk; for (chunk = 0; chunk < _textLines[nrow].chunks.size(); chunk++) { pwidth = width; pmcol = mcol; if (!_textLines[nrow].chunks[chunk].text.empty()) { width += getStringWidth(_textLines[nrow].chunks[chunk], _textLines[nrow].chunks[chunk].text); mcol += _textLines[nrow].chunks[chunk].text.size(); } if (width + alignOffset > x) break; } if (chunk == _textLines[nrow].chunks.size()) chunk--; Common::U32String str = _textLines[nrow].chunks[chunk].text; ncol = mcol; nsx = pwidth; for (int i = str.size(); i >= 0; i--) { int strw = getStringWidth(_textLines[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 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(_textLines[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 = _textLines.size() - 1; if (endCol == -1) endCol = getLineCharWidth(endRow); if (_textLines.empty()) { return res; } startRow = CLIP(startRow, 0, (int)_textLines.size() - 1); endRow = CLIP(endRow, 0, (int)_textLines.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 < _textLines[i].chunks.size(); chunk++) { if (_textLines[i].chunks[chunk].text.empty()) // skip empty chunks continue; if (startCol <= 0) { ADDFORMATTING(); if (endCol >= (int)_textLines[i].chunks[chunk].text.size()) res += _textLines[i].chunks[chunk].text; else res += _textLines[i].chunks[chunk].text.substr(0, endCol); } else if ((int)_textLines[i].chunks[chunk].text.size() > startCol) { ADDFORMATTING(); res += _textLines[i].chunks[chunk].text.substr(startCol, endCol - startCol); } startCol -= _textLines[i].chunks[chunk].text.size(); endCol -= _textLines[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 < _textLines[i].chunks.size(); chunk++) { if (_textLines[i].chunks[chunk].text.empty()) // skip empty chunks continue; if (startCol <= 0) { ADDFORMATTING(); res += _textLines[i].chunks[chunk].text; } else if ((int)_textLines[i].chunks[chunk].text.size() > startCol) { ADDFORMATTING(); res += _textLines[i].chunks[chunk].text.substr(startCol); } startCol -= _textLines[i].chunks[chunk].text.size(); } if (newlines && _textLines[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 < _textLines[i].chunks.size(); chunk++) { if (_textLines[i].chunks[chunk].text.empty()) // skip empty chunks continue; ADDFORMATTING(); if (endCol >= (int)_textLines[i].chunks[chunk].text.size()) res += _textLines[i].chunks[chunk].text; else res += _textLines[i].chunks[chunk].text.substr(0, endCol); endCol -= _textLines[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 < _textLines[i].chunks.size(); chunk++) { if (_textLines[i].chunks[chunk].text.empty()) // skip empty chunks continue; ADDFORMATTING(); res += _textLines[i].chunks[chunk].text; } if (newlines && _textLines[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 (_textLines.empty()) { splitString(str, 0); } else { int start = _cursorRow, end = _cursorRow; while (start && !_textLines[start - 1].paragraphEnd) start--; while (end < (int)_textLines.size() - 1 && !_textLines[end].paragraphEnd) end++; for (int i = start; i < _cursorRow; i++) ppos += getLineCharWidth(i); ppos += _cursorCol; Common::U32String pre_str = getTextChunk(start, 0, _cursorRow, _cursorCol, true, true); Common::U32String sub_str = getTextChunk(_cursorRow, _cursorCol, end, getLineCharWidth(end, true), true, true); // Remove it from the text for (int i = start; i <= end; i++) { _textLines.remove_at(start); } splitString(pre_str + str + sub_str, start); _cursorRow = start; } while (ppos > getLineCharWidth(_cursorRow, true)) { ppos -= getLineCharWidth(_cursorRow, true); _cursorRow++; } _cursorCol = ppos; recalcDims(); updateCursorPos(); _fullRefresh = true; render(); } void MacText::setText(const Common::U32String &str) { _str = str; _textLines.clear(); splitString(_str); _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 (_textLines.empty()) { appendTextDefault(Common::String(c)); (*col)++; return; } MacTextLine *line = &_textLines[*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 (getLineWidth(*row) - oldw + chunkw > _maxWidth) { // Needs reshuffle reshuffleParagraph(row, col); _fullRefresh = true; recalcDims(); render(); } else { recalcDims(); render(*row, *row); } for (int i = 0; i < (int)_textLines.size(); i++) { D(9, "**insertChar line %d isEnd %d", i, _textLines[i].paragraphEnd); for (int j = 0; j < (int)_textLines[i].chunks.size(); j++) { D(9, "[%d] \"%s\"",_textLines[i].chunks[j].text.size(), Common::toPrintable(_textLines[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 = getLineCharWidth(*row - 1); (*row)--; // formatting matches, glue texts as normal if (_textLines[*row].lastChunk().equals(_textLines[*row + 1].firstChunk())) { _textLines[*row].lastChunk().text += _textLines[*row + 1].firstChunk().text; _textLines[*row + 1].firstChunk().text.clear(); } else { // formatting doesn't match, move whole chunk _textLines[*row].chunks.push_back(MacFontRun(_textLines[*row + 1].firstChunk())); _textLines[*row].firstChunk().text.clear(); } _textLines[*row].paragraphEnd = false; for (uint i = 1; i < _textLines[*row + 1].chunks.size(); i++) _textLines[*row].chunks.push_back(MacFontRun(_textLines[*row + 1].chunks[i])); _textLines.remove_at(*row + 1); } else { int pos = *col - 1; uint ch = _textLines[*row].getChunkNum(&pos); if (pos == (int)_textLines[*row].chunks[ch].text.size()) pos--; _textLines[*row].chunks[ch].text.deleteChar(pos); (*col)--; } _textLines[*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)_textLines.size(); i++) { D(9, "**deleteChar line %d", i); for (int j = 0; j < (int)_textLines[i].chunks.size(); j++) { D(9, "[%d] \"%s\"",_textLines[i].chunks[j].text.size(), Common::toPrintable(_textLines[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 (_textLines.empty()) { appendTextDefault(Common::String("\n")); (*row)++; return; } MacTextLine *line = &_textLines[*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 _textLines[*row].width = -1; // flush the cache _textLines.insert_at(*row + 1, newline); (*row)++; *col = 0; reshuffleParagraph(row, col); for (int i = 0; i < (int)_textLines.size(); i++) { D(9, "** addNewLine line %d", i); for (int j = 0; j < (int)_textLines[i].chunks.size(); j++) { D(9, "[%d] \"%s\"",_textLines[i].chunks[j].text.size(), Common::toPrintable(_textLines[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 && !_textLines[start - 1].paragraphEnd) start--; while (end < (int)_textLines.size() - 1 && !_textLines[end].paragraphEnd) // stop at last line end++; // Get character pos within paragraph int ppos = 0; for (int i = start; i < *row; i++) ppos += getLineCharWidth(i); ppos += *col; // Get whole paragraph Common::U32String paragraph = getTextChunk(start, 0, end, getLineCharWidth(end, true), true, true); // Remove it from the text for (int i = start; i <= end; i++) { _textLines.remove_at(start); } // And now read it D(9, "start %d end %d", start, end); splitString(paragraph, start); // Find new pos within paragraph after reshuffling *row = start; while (ppos > getLineCharWidth(*row, true)) { ppos -= 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 (_textLines.empty()) { _cursorX = _cursorY = 0; } else { undrawCursor(); _cursorRow = MIN(_cursorRow, _textLines.size() - 1); int alignOffset = getAlignOffset(_cursorRow); _cursorY = _textLines[_cursorRow].y - _scrollPos; _cursorX = 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)); } } // End of namespace Graphics