/* 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 "graphics/macgui/mactext.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() { 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 && 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; if (macFont) { _defaultFormatting = MacFontRun(_wm); _defaultFormatting.font = wm->_fontMan->getFont(*macFont); byte r, g, b; if (_wm->_pixelformat.bytesPerPixel == 4) { _wm->decomposeColor(fgcolor, r, g, b); } else { _wm->decomposeColor(fgcolor, r, g, b); } _defaultFormatting.setValues(_wm, macFont->getId(), macFont->getSlant(), macFont->getSize(), r, g, b); } else { _defaultFormatting.font = NULL; } init(fgcolor, bgcolor, maxWidth, textAlignment, interlinear, textShadow, true); } // 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; 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(fgcolor, bgcolor, maxWidth, textAlignment, interlinear, 0, true); } // 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; if (font) { _defaultFormatting = MacFontRun(_wm, font, 0, font->getFontHeight(), 0, 0, 0); _defaultFormatting.font = font; } else { _defaultFormatting.font = NULL; } init(fgcolor, bgcolor, maxWidth, textAlignment, interlinear, 0, false); } void MacText::init(uint32 fgcolor, uint32 bgcolor, int maxWidth, TextAlign textAlignment, int interlinear, uint16 textShadow, bool macFontMode) { _fullRefresh = true; _canvas._maxWidth = maxWidth; _canvas._textAlignment = textAlignment; _canvas._textShadow = textShadow; _canvas._interLinear = interlinear; _canvas._wm = _wm; _canvas._tfgcolor = fgcolor; _canvas._tbgcolor = bgcolor; _canvas._macFontMode = macFontMode; _canvas._macText = this; _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; } // 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); } void MacText::setMaxWidth(int maxWidth) { if (maxWidth == _canvas._maxWidth) return; if (maxWidth < 0) { warning("trying to set maxWidth to %d", maxWidth); return; } for (uint i = 0; i < _canvas._text.size(); i++) { if (_canvas._text[i].table) { // TODO debug(0, "MacText::setMaxWidth(): Skipping resize for MacText with tables"); return; } } // keep the cursor pos int ppos = 0; for (int i = 0; i < _cursorRow; i++) ppos += _canvas.getLineCharWidth(i); ppos += _cursorCol; _canvas.setMaxWidth(maxWidth, _defaultFormatting); // restore the cursor pos _cursorRow = 0; while (ppos > _canvas.getLineCharWidth(_cursorRow, true)) { ppos -= _canvas.getLineCharWidth(_cursorRow, true); if (_cursorRow >= (int)_canvas._text.size() - 1) break; _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++) { _canvas._text[i].chunks[j].textSlant = textSlant; } } _fullRefresh = true; render(); _contentIsDirty = true; } // Return the number of rows of text in the rendered output. // This means e.g. a line of text that wraps will count as 2 or more rows. int MacText::getRowCount() { return (int)_canvas._text.size(); } // 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); } 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 } } 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); } 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 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) { clearChunkInput(); _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::clearChunkInput() { int canvasTextSize = _canvas._text.size() - 1; if (canvasTextSize >= 0 && _editable) { int lastChunkIdx = _canvas._text[canvasTextSize].chunks.size() - 1; if (lastChunkIdx >= 0) _canvas._text[canvasTextSize].chunks[lastChunkIdx].text = ""; } } 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)); 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), _canvas._tbgcolor); _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 _canvas.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 = 0; if (!_canvas._text.empty()) 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 _canvas.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 = _canvas.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 = 1; bool inWhitespace = true; // getMouseWord: // - starts from index 1 // - a word goes from the first leading whitespace character // up until the end of the word // - trailing whitespace or empty space at the end of a line // counts as part of the word on the next line // - empty space at the end of the text counts as a word 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; Common::String data = _canvas._text[i].chunks[j].getEncodedText(); for (auto it : data) { if (it == ' ' && !inWhitespace) { index++; inWhitespace = true; } else if (it != ' ' && inWhitespace) { inWhitespace = false; } } } } int cur = 0; for (uint j = 0; j < _canvas._text[row].chunks.size(); j++) { if (_canvas._text[row].chunks[j].text.empty()) continue; Common::String data = _canvas._text[row].chunks[j].getEncodedText(); for (auto it : data) { cur++; if (it == ' ' && !inWhitespace) { index++; inWhitespace = true; } else if (it != ' ' && inWhitespace) { inWhitespace = false; } if (cur > col) break; } if (cur > col) break; } return index; } 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); // getMouseItem // - starts from index 1 // - an item goes from the first non-comma character up until a comma // - trailing whitespace or empty space at the end of a line // counts as part of the item on the next line // - empty space at the end of the text counts as an item int index = 1; bool onComma = false; 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; Common::String data = _canvas._text[i].chunks[j].getEncodedText(); for (auto it : data) { if (onComma) { index += 1; onComma = false; } if (it == ',') { onComma = true; } } } } int cur = 0; for (uint i = 0; i < _canvas._text[row].chunks.size(); i++) { if (_canvas._text[row].chunks[i].text.empty()) continue; Common::String data = _canvas._text[row].chunks[i].getEncodedText(); for (auto it : data) { if (onComma) { index += 1; onComma = false; } if (it == ',') { onComma = true; } cur++; if (cur > col) break; } if (cur > col) break; } return index; } 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(); } 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; } Common::U32String MacText::getTextChunk(int startRow, int startCol, int endRow, int endCol, bool formatted, bool newlines) { return _canvas.getTextChunk(startRow, startCol, endRow, endCol, formatted, newlines); } // mostly, we referring 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 = _canvas.getTextChunk(start, 0, _cursorRow, _cursorCol, true, true); Common::U32String sub_str = _canvas.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; } while (ppos > _canvas.getLineCharWidth(_cursorRow, true)) { ppos -= _canvas.getLineCharWidth(_cursorRow, true); if (_cursorRow == (int)_canvas._text.size() - 1) break; _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() || _canvas._text[*row].chunks.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 _canvas.reshuffleParagraph(row, col, _defaultFormatting); _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); } _canvas.reshuffleParagraph(&row, &col, _defaultFormatting); _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)--; #if DEBUG D(9, "MacText::deletePreviousCharInternal: Chunks: "); for (auto &ch : _canvas._text[*row].chunks) ch.debugPrint(); D(9, ""); #endif // 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 = _canvas._text[*row + 1].paragraphEnd; 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); _canvas.reshuffleParagraph(row, col, _defaultFormatting); _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); #if DEBUG D(9, "MacText::addNewLine: Chunks: "); for (auto &c : line->chunks) c.debugPrint(); D(9, ""); #endif 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; _canvas.reshuffleParagraph(row, col, _defaultFormatting); 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(); } ////////////////// // 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)); } } // End of namespace Graphics