/* 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/events.h" #include "common/memstream.h" #include "graphics/macgui/macbutton.h" #include "graphics/macgui/macwindow.h" #include "graphics/macgui/macwindowmanager.h" #include "director/director.h" #include "director/cast.h" #include "director/channel.h" #include "director/movie.h" #include "director/score.h" #include "director/sprite.h" #include "director/window.h" #include "director/castmember/text.h" #include "director/lingo/lingo-the.h" namespace Director { TextCastMember::TextCastMember(Cast *cast, uint16 castId, Common::SeekableReadStreamEndian &stream, uint16 version, uint8 flags1, bool asButton) : CastMember(cast, castId, stream) { _type = kCastText; _borderSize = 0; _gutterSize = 0; _boxShadow = 0; _buttonType = kTypeButton; _editable = false; _maxHeight = _textHeight = 0; _bgcolor = 0; _fgcolor = 0xff; _textFlags = 0; _scroll = 0; _fontId = 1; _fontSize = 12; _textType = kTextTypeFixed; _textAlign = kTextAlignLeft; _textShadow = 0; _textSlant = 0; _bgpalinfo1 = _bgpalinfo2 = _bgpalinfo3 = 0; _fgpalinfo1 = _fgpalinfo2 = _fgpalinfo3 = 0xff; // seems like the line spacing is default to 1 in D4 _lineSpacing = g_director->getVersion() >= 400 ? 1 : 0; if (debugChannelSet(4, kDebugLoading)) { stream.hexdump(stream.size()); } if (version < kFileVer400) { _flags1 = flags1; // region: 0 - auto, 1 - matte, 2 - disabled _borderSize = stream.readByte(); _gutterSize = stream.readByte(); _boxShadow = stream.readByte(); _textType = static_cast(stream.readByte()); _textAlign = static_cast(stream.readUint16()); _bgpalinfo1 = stream.readUint16(); _bgpalinfo2 = stream.readUint16(); _bgpalinfo3 = stream.readUint16(); uint32 pad2; uint16 pad3; uint16 pad4 = 0; uint16 totalTextHeight; if (version < kFileVer300) { pad2 = stream.readUint16(); if (pad2 != 0) { // In D2 there are values warning("TextCastMember: pad2: %x", pad2); } _initialRect = Movie::readRect(stream); pad3 = stream.readUint16(); _textShadow = stream.readByte(); _textFlags = stream.readByte(); if (_textFlags & 0xf8) warning("Unprocessed text cast flags: %x", _textFlags & 0xf8); totalTextHeight = stream.readUint16(); } else { pad2 = stream.readUint16(); _initialRect = Movie::readRect(stream); pad3 = stream.readUint16(); _textFlags = stream.readUint16(); // 1: editable, 2: auto tab, 4: don't wrap _editable = _textFlags & 0x1; totalTextHeight = stream.readUint16(); } debugC(2, kDebugLoading, "TextCastMember(): flags1: %d, border: %d gutter: %d shadow: %d textType: %d align: %04x", _flags1, _borderSize, _gutterSize, _boxShadow, _textType, _textAlign); debugC(2, kDebugLoading, "TextCastMember(): background rgb: 0x%04x 0x%04x 0x%04x, pad2: %x pad3: %d pad4: %d shadow: %d flags: %d totHeight: %d", _bgpalinfo1, _bgpalinfo2, _bgpalinfo3, pad2, pad3, pad4, _textShadow, _textFlags, totalTextHeight); if (debugChannelSet(2, kDebugLoading)) { _initialRect.debugPrint(2, "TextCastMember(): rect:"); } } else if (version >= kFileVer400 && version < kFileVer1100) { _flags1 = flags1; _borderSize = stream.readByte(); _gutterSize = stream.readByte(); _boxShadow = stream.readByte(); _textType = static_cast(stream.readByte()); _textAlign = static_cast(stream.readSint16()); // this is because 'right' is -1? or should that be 255? _bgpalinfo1 = stream.readUint16(); _bgpalinfo2 = stream.readUint16(); _bgpalinfo3 = stream.readUint16(); _scroll = stream.readUint16(); _fontId = 1; // this is in STXT _initialRect = Movie::readRect(stream); _maxHeight = stream.readUint16(); _textShadow = stream.readByte(); _textFlags = stream.readByte(); // 1: editable, 2: auto tab 4: don't wrap _editable = _textFlags & 0x1; _textHeight = stream.readUint16(); _textSlant = 0; debugC(2, kDebugLoading, "TextCastMember(): flags1: %d, border: %d gutter: %d shadow: %d textType: %d align: %04x", _flags1, _borderSize, _gutterSize, _boxShadow, _textType, _textAlign); debugC(2, kDebugLoading, "TextCastMember(): background rgb: 0x%04x 0x%04x 0x%04x, shadow: %d flags: %d textHeight: %d", _bgpalinfo1, _bgpalinfo2, _bgpalinfo3, _textShadow, _textFlags, _textHeight); debugC(2, kDebugLoading, "TextCastMember(): rect: [%s]", _initialRect.toString().c_str()); } else { warning("STUB: Text/ButtonCastMember: Text not yet supported for version v%d (%d)", humanVersion(_cast->_version), _cast->_version); } if (asButton) { _type = kCastButton; _buttonType = static_cast(stream.readUint16BE() - 1); } _bgcolor = g_director->_wm->findBestColor(_bgpalinfo1 & 0xff, _bgpalinfo2 & 0xff, _bgpalinfo3 & 0xff); _modified = true; } TextCastMember::TextCastMember(Cast *cast, uint16 castId, TextCastMember &source) : CastMember(cast, castId) { _type = kCastText; // force a load so we can copy the cast resource information source.load(); _loaded = true; _initialRect = source._initialRect; _boundingRect = source._boundingRect; if (cast == source._cast) _children = source._children; _borderSize = source._borderSize; _gutterSize = source._gutterSize; _boxShadow = source._boxShadow; _maxHeight = source._maxHeight; _textHeight = source._textHeight; _fontId = source._fontId; _fontSize = source._fontSize; _textType = source._textType; _textAlign = source._textAlign; _textShadow = source._textShadow; _scroll = source._scroll; _textSlant = source._textSlant; _textFlags = source._textFlags; _bgpalinfo1 = source._bgpalinfo1; _bgpalinfo2 = source._bgpalinfo2; _bgpalinfo3 = source._bgpalinfo3; _fgpalinfo1 = source._fgpalinfo1; _fgpalinfo2 = source._fgpalinfo2; _fgpalinfo3 = source._fgpalinfo3; _buttonType = source._buttonType; _editable = source._editable; _lineSpacing = source._lineSpacing; _ftext = source._ftext; _ptext = source._ptext; _rtext = source._rtext; _bgcolor = source._bgcolor; _fgcolor = source._fgcolor; } void TextCastMember::setColors(uint32 *fgcolor, uint32 *bgcolor) { if (fgcolor) _fgcolor = *fgcolor; if (bgcolor) _bgcolor = *bgcolor; // if we want to keep the format unchanged, then we need to modify _ftext as well Graphics::MacText *target = getWidget(); if (target) { target->setColors(_fgcolor, _bgcolor); } else { _modified = true; } } Graphics::TextAlign TextCastMember::getAlignment() { switch (_textAlign) { case kTextAlignRight: return Graphics::kTextAlignRight; case kTextAlignCenter: return Graphics::kTextAlignCenter; case kTextAlignLeft: default: return Graphics::kTextAlignLeft; } } void TextCastMember::setBackColor(uint32 bgCol) { _bgcolor = bgCol; _modified = true; } uint32 TextCastMember::getForeColor(int start, int end) { Graphics::MacText *target = getWidget(); if (target) { return target->getTextColor(start, end); } return _fgcolor; } void TextCastMember::setForeColor(uint32 fgCol) { _fgcolor = fgCol; _modified = true; } void TextCastMember::setForeColor(uint32 fgCol, int start, int end) { Graphics::MacText *target = getWidget(); if (target) { return target->setTextColor(fgCol, start, end); } _modified = true; } void TextCastMember::importStxt(const Stxt *stxt) { _fontId = stxt->_style.fontId; _height = stxt->_style.height; _ascent = stxt->_style.ascent; _textSlant = stxt->_style.textSlant; _fontSize = stxt->_style.fontSize; _fgpalinfo1 = stxt->_style.r; _fgpalinfo2 = stxt->_style.g; _fgpalinfo3 = stxt->_style.b; // The default color in the Stxt will override the fgcolor, // e.g. empty editable text boxes will use the Stxt color _fgcolor = g_director->_wm->findBestColor(_fgpalinfo1 >> 8, _fgpalinfo2 >> 8, _fgpalinfo3 >> 8); _ftext = stxt->_ftext; _ptext = stxt->_ptext; _rtext = stxt->_rtext; // Rectifying _fontId in case of a fallback font Graphics::MacFont macFont(_fontId, _fontSize, _textSlant); g_director->_wm->_fontMan->getFont(&macFont); _fontId = macFont.getId(); // If the text is empty, that means we ignored the font and now // set the text height to a minimal one. // // This fixes `number of chars` in Lingo Workshop if (_textType == kTextTypeAdjustToFit && _ftext.empty()) _initialRect.setHeight(macFont.getSize() + (2 * _borderSize) + _gutterSize + _boxShadow); } bool textWindowCallback(Graphics::WindowClick click, Common::Event &event, void *ptr) { return g_director->getCurrentMovie()->processEvent(event); } Graphics::MacWidget *TextCastMember::createWindowOrWidget(Common::Rect &bbox, Common::Rect dims, Graphics::MacFont *macFont) { Graphics::MacText *widget = nullptr; widget = new Graphics::MacText(g_director->getCurrentWindow()->getMacWindow(), bbox.left, bbox.top, dims.width(), dims.height(), g_director->_wm, _ftext, macFont, getForeColor(), getBackColor(), _initialRect.width(), getAlignment(), _lineSpacing, _borderSize, _gutterSize, _boxShadow, _textShadow, _textType == kTextTypeFixed || _textType == kTextTypeScrolling, _textType == kTextTypeScrolling); widget->setSelRange(g_director->getCurrentMovie()->_selStart, g_director->getCurrentMovie()->_selEnd); widget->draw(); return widget; } Graphics::MacWidget *TextCastMember::createWidget(Common::Rect &bbox, Channel *channel, SpriteType spriteType) { Graphics::MacFont *macFont = new Graphics::MacFont(_fontId, _fontSize, _textSlant); Graphics::MacWidget *widget = nullptr; Common::Rect dims(bbox); CastType type = _type; ButtonType buttonType = _buttonType; // WORKAROUND: In D2/D3 there can be text casts that have button // information set in the sprite. if (type == kCastText && isButtonSprite(spriteType)) { type = kCastButton; buttonType = ButtonType(spriteType - 8); } switch (type) { case kCastText: // for mactext, we can expand now, but we can't shrink. so we may pass the small size when we have adjustToFit text style if (_textType == kTextTypeAdjustToFit) { dims.right = MIN(dims.right, dims.left + _initialRect.width()); dims.bottom = MIN(dims.bottom, dims.top + _initialRect.height()); } else if (_textType == kTextTypeFixed || _textType == kTextTypeScrolling) { // use initialRect to create widget for fixed style text, this maybe related to version. dims.right = MAX(dims.right, dims.left + _initialRect.width()); dims.bottom = MAX(dims.bottom, dims.top + MAX(_initialRect.height(), _maxHeight)); } widget = createWindowOrWidget(bbox, dims, macFont); if (_textType != kTextTypeScrolling) { ((Graphics::MacText *)widget)->setEditable(channel->_sprite->_editable || _editable); } // since we disable the ability of setActive in setEdtiable, then we need to set active widget manually if (channel->_sprite->_editable || _editable) { Graphics::MacWidget *activeWidget = g_director->_wm->getActiveWidget(); if (activeWidget == nullptr || !activeWidget->isEditable()) g_director->_wm->setActiveWidget(widget); } break; case kCastButton: // note that we use _initialRect for the dimensions of the button; // the values provided in the sprite bounding box are ignored widget = new Graphics::MacButton(Graphics::MacButtonType(buttonType), getAlignment(), g_director->getCurrentWindow()->getMacWindow(), bbox.left, bbox.top, _initialRect.width(), _initialRect.height(), g_director->_wm, _ftext, macFont, getForeColor(), getBackColor()); widget->_focusable = true; ((Graphics::MacButton *)widget)->setHilite(_hilite); ((Graphics::MacButton *)widget)->setCheckBoxType(g_director->getCurrentMovie()->_checkBoxType); ((Graphics::MacButton *)widget)->draw(); break; default: break; } delete macFont; return widget; } Graphics::MacText *TextCastMember::getWidget() { // FIXME: The cast member should be the source of truth for the widget. // You don't have the issue you have with e.g. bitmaps where the channel // can stretch: all sprites of the cast member have the same dimensions. // There is technically a small window between typing something in and hitting // enter/defocusing where other copies of the widget are out of sync, // but they will resync pretty quickly. Channel *toEdit = nullptr; Common::Array channels = g_director->getCurrentMovie()->getScore()->_channels; for (uint i = 0; i < channels.size(); i++) { if (channels[i]->_sprite->_cast == this) { toEdit = channels[i]; break; } } if (toEdit) { Common::Rect bbox = toEdit->getBbox(); if (!toEdit->_widget) toEdit->_widget = createWidget(bbox, toEdit, toEdit->_sprite->_spriteType); return (Graphics::MacText *)toEdit->_widget; } return (Graphics::MacText *)_widget; } CollisionTest TextCastMember::isWithin(const Common::Rect &bbox, const Common::Point &pos, InkType ink) { if (!bbox.contains(pos)) return kCollisionNo; Graphics::MacText *target = getWidget(); if (!target) return kCollisionYes; Graphics::MacWindowConstants::WindowClick result = target->isInScrollBar(pos.x, pos.y); if (result == Graphics::MacWindowConstants::kBorderScrollDown || result == Graphics::MacWindowConstants::kBorderScrollUp) return kCollisionHole; return kCollisionYes; } void TextCastMember::importRTE(byte *text) { //assert(rteList.size() == 3); //child0 is probably font data. //child1 is the raw text. _rtext = _ptext = _ftext = Common::String((char*)text); //child2 is positional? } void TextCastMember::setRawText(const Common::String &text) { // Do nothing if text did not change if (_ptext.equals(Common::U32String(text))) return; _rtext = text; _ptext = Common::U32String(text); // If text has changed, use the cached formatting from first STXT in this castmember. Common::U32String formatting = Common::String::format("\001\016%04x%02x%04x%04x%04x%04x", _fontId, _textSlant, _fontSize, _fgpalinfo1, _fgpalinfo2, _fgpalinfo3); _ftext = formatting + _ptext; _modified = true; } int TextCastMember::getLineCount() { Graphics::MacText *target = getWidget(); if (target) { return target->getRowCount(); } warning("TextCastMember::getLineCount(): no widget available, returning 0"); return 0; } int TextCastMember::getLineHeight(int line) { Graphics::MacText *target = getWidget(); if (target) { return target->getLineHeight(line); } warning("TextCastMember::getLineHeight(): no widget available, returning 0"); return 0; } // D4 dictionary book said this is line spacing int TextCastMember::getTextHeight() { Graphics::MacText *target = getWidget(); if (target) { return target->getLineSpacing(); } return _lineSpacing; } Common::String TextCastMember::getTextFont() { Graphics::MacText *target = getWidget(); if (target) { int fontId = target->getTextFont(); return g_director->_wm->_fontMan->getFontName(fontId); } return g_director->_wm->_fontMan->getFontName(_fontId); } Common::String TextCastMember::getTextFont(int start, int end) { Graphics::MacText *target = getWidget(); if (target) { int fontId = target->getTextFont(start, end); return g_director->_wm->_fontMan->getFontName(fontId); } return g_director->_wm->_fontMan->getFontName(_fontId); } void TextCastMember::setTextFont(const Common::String &fontName) { Graphics::MacText *target = getWidget(); if (!target) return; target->enforceTextFont((uint16) g_director->_wm->_fontMan->getFontIdByName(fontName)); _ptext = target->getPlainText(); _ftext = target->getTextChunk(0, 0, -1, -1, true); } void TextCastMember::setTextFont(const Common::String &fontName, int start, int end) { Graphics::MacText *target = getWidget(); if (!target) return; target->setTextFont((uint16) g_director->_wm->_fontMan->getFontIdByName(fontName), start, end); _ptext = target->getPlainText(); _ftext = target->getTextChunk(0, 0, -1, -1, true); } Common::U32String TextCastMember::getText() { return _ptext; } Common::String TextCastMember::getRawText() { return _rtext; } int TextCastMember::getTextSize() { Graphics::MacText *target = getWidget(); if (target) { return target->getTextSize(); } return _fontSize; } int TextCastMember::getTextSize(int start, int end) { Graphics::MacText *target = getWidget(); if (target) { return target->getTextSize(start, end); } return _fontSize; } void TextCastMember::setTextSize(int textSize) { Graphics::MacText *target = getWidget(); if (target) { target->setTextSize(textSize); _ptext = target->getPlainText(); _ftext = target->getTextChunk(0, 0, -1, -1, true); target->draw(); } _fontSize = textSize; _modified = true; } void TextCastMember::setTextSize(int textSize, int start, int end) { Graphics::MacText *target = getWidget(); if (target) { target->setTextSize(textSize, start, end); _ptext = target->getPlainText(); _ftext = target->getTextChunk(0, 0, -1, -1, true); target->draw(); } _modified = true; } Common::String TextCastMember::getTextStyle() { int slantVal = _textSlant; Graphics::MacText *target = getWidget(); if (target) { slantVal = target->getTextSlant(); } return g_director->_wm->_fontMan->getNameFromSlant(slantVal); } Common::String TextCastMember::getTextStyle(int start, int end) { int slantVal = _textSlant; Graphics::MacText *target = getWidget(); if (target) { slantVal = target->getTextSlant(start, end); } return g_director->_wm->_fontMan->getNameFromSlant(slantVal); } void TextCastMember::setTextStyle(const Common::String &textStyle) { Graphics::MacText *target = getWidget(); int slant = g_director->_wm->_fontMan->parseSlantFromName(textStyle); if (target) { target->enforceTextSlant(slant); _ptext = target->getPlainText(); _ftext = target->getTextChunk(0, 0, -1, -1, true); target->draw(); } _modified = true; } void TextCastMember::scrollByLine(int count) { Graphics::MacText *target = getWidget(); target->scroll(count); } void TextCastMember::setTextStyle(const Common::String &textStyle, int start, int end) { Graphics::MacText *target = getWidget(); int slant = g_director->_wm->_fontMan->parseSlantFromName(textStyle); if (target) { target->setTextSlant(slant, start, end); _ptext = target->getPlainText(); _ftext = target->getTextChunk(0, 0, -1, -1, true); target->draw(); } _modified = true; } void TextCastMember::updateFromWidget(Graphics::MacWidget *widget, bool spriteEditable) { if (widget && (spriteEditable || _editable)) { Common::String content = ((Graphics::MacText *)widget)->getEditedString(); content.replace('\n', '\r'); _ptext = content; // This string will be formatted with the default formatting Common::String format = Common::String::format("\001\016%04x%02x%04x%04x%04x%04x", _fontId, _textSlant, _fontSize, _fgpalinfo1, _fgpalinfo2, _fgpalinfo2); _ftext = format; _ftext += _ptext; } } Common::String TextCastMember::formatInfo() { // need to pull the data from the STXT resource before the // debug output will be visible load(); Common::String format = formatStringForDump(_ptext.encode()); return Common::String::format( "initialRect: %dx%d@%d,%d, boundingRect: %dx%d@%d,%d, foreColor: %d, backColor: %d, editable: %d, text: \"%s\"", _initialRect.width(), _initialRect.height(), _initialRect.left, _initialRect.top, _boundingRect.width(), _boundingRect.height(), _boundingRect.left, _boundingRect.top, getForeColor(), getBackColor(), _editable, formatStringForDump(format).c_str() ); } void TextCastMember::load() { if (_loaded) return; uint stxtid = 0; if (_cast->_version >= kFileVer400) { for (auto &it : _children) { if (it.tag == MKTAG('S', 'T', 'X', 'T')) { stxtid = it.index; break; } } if (!stxtid) { warning("TextCastMember::load(): No STXT resource found in %d children", _children.size()); } } else { stxtid = _castId; } if (_cast->_loadedStxts.contains(stxtid)) { const Stxt *stxt = _cast->_loadedStxts.getVal(stxtid); importStxt(stxt); _size = stxt->_size; } else { warning("TextCastMember::load(): stxtid %i isn't loaded", stxtid); } _loaded = true; } void TextCastMember::unload() { // No unload necessary. } bool TextCastMember::hasField(int field) { switch (field) { case kTheText: case kTheTextAlign: case kTheTextFont: case kTheTextHeight: case kTheTextSize: case kTheTextStyle: return true; case kTheAutoTab: case kTheBorder: case kTheBoxDropShadow: case kTheBoxType: case kTheDropShadow: case kTheEditable: case kTheLineCount: case kTheMargin: case kThePageHeight: case kTheScrollTop: case kTheWordWrap: return _type == kCastText; case kTheButtonType: return _type == kCastButton; default: break; } return CastMember::hasField(field); } Datum TextCastMember::getField(int field) { Datum d; switch (field) { case kTheText: d = getText().encode(Common::kUtf8); break; case kTheTextAlign: d.type = STRING; switch (_textAlign) { case kTextAlignLeft: d.u.s = new Common::String("left"); break; case kTextAlignCenter: d.u.s = new Common::String("center"); break; case kTextAlignRight: d.u.s = new Common::String("right"); break; default: warning("TextCastMember::getField(): Invalid text align spec"); break; } break; case kTheTextFont: d = getTextFont(); break; case kTheTextHeight: d = getTextHeight(); break; case kTheTextSize: d = getTextSize(); break; case kTheTextStyle: d = getTextStyle(); break; case kTheAutoTab: warning("STUB: TextCastMember::getField(): autoTab not implemented"); d = 1; break; case kTheBorder: d = _borderSize; break; case kTheBoxDropShadow: warning("STUB: TextCastMember::getField(): boxDropShadow not implemented"); d = 1; break; case kTheDropShadow: warning("STUB: TextCastMember::getField(): dropShadow not implemented"); d = 1; break; case kTheEditable: d = (int)_editable; break; case kTheLineCount: d = getLineCount(); break; case kTheMargin: warning("STUB: TextCastMember::getField(): margin not implemented"); d = 0; break; case kThePageHeight: warning("STUB: TextCastMember::getField(): pageHeight not implemented"); d = 0; break; case kTheScrollTop: d = _scroll; break; case kTheWordWrap: warning("STUB: TextCastMember::getField(): wordWrap not implemented"); d = 1; break; case kTheButtonType: switch (_buttonType) { case kTypeCheckBox: d = Datum("checkBox"); d.type = SYMBOL; break; case kTypeRadio: d = Datum("radioButton"); d.type = SYMBOL; break; case kTypeButton: default: d = Datum("pushButton"); d.type = SYMBOL; break; } break; default: d = CastMember::getField(field); } return d; } void TextCastMember::setField(int field, const Datum &d) { switch (field) { case kTheBackColor: { uint32 color = g_director->transformColor(d.asInt()); setColors(nullptr, &color); } return; case kTheForeColor: { uint32 color = g_director->transformColor(d.asInt()); setColors(&color, nullptr); } return; case kTheText: setRawText(d.asString()); return; case kTheTextAlign: { Common::String select = d.asString(); TextAlignType align; if (select.equalsIgnoreCase("left")) { align = kTextAlignLeft; } else if (select.equalsIgnoreCase("center")) { align = kTextAlignCenter; } else if (select.equalsIgnoreCase("right")) { align = kTextAlignRight; } else { warning("TextCastMember::setField(): Unknown text align spec: %s", d.asString(true).c_str()); break; } _textAlign = align; _modified = true; } return; case kTheTextFont: setTextFont(d.asString()); return; case kTheTextHeight: _lineSpacing = d.asInt(); _modified = true; return; case kTheTextSize: setTextSize(d.asInt()); return; case kTheTextStyle: setTextStyle(d.asString()); return; case kTheAutoTab: warning("STUB: TextCastMember::setField(): autoTab not implemented"); return; case kTheBorder: _borderSize = d.asInt(); setModified(true); return; case kTheBoxDropShadow: warning("STUB: TextCastMember::setField(): boxDropShadow not implemented"); return; case kTheBoxType: // The possible values are #adjust, #scroll, #fixed, and #limit. warning("STUB: TextCastMember::setField(): boxType not implemented"); return; case kTheDropShadow: warning("STUB: TextCastMember::setField(): dropShadow not implemented"); return; case kTheEditable: _editable = d.asInt(); setModified(true); return; case kTheLineCount: warning("BUILDBOT: TextCastMember::setField(): Attempt to set read-only field %s of cast %d", g_lingo->entity2str(field), _castId); return; case kTheMargin: warning("STUB: TextCastMember::setField(): margin not implemented"); return; case kThePageHeight: warning("BUILDBOT: TextCastMember::setField(): Attempt to set read-only field %s of cast %d", g_lingo->entity2str(field), _castId); return; case kTheScrollTop: _scroll = d.asInt(); setModified(true); return; case kTheWordWrap: warning("STUB: TextCastMember::setField(): wordWrap not implemented"); return; case kTheButtonType: if (d.type == SYMBOL) { if (d.u.s->equalsIgnoreCase("pushButton")) { _buttonType = kTypeButton; setModified(true); return; } else if (d.u.s->equalsIgnoreCase("radioButton")) { _buttonType = kTypeRadio; setModified(true); return; } else if (d.u.s->equalsIgnoreCase("checkBox")) { _buttonType = kTypeCheckBox; setModified(true); return; } } warning("TextCastMember: invalid button type %s", d.asString(true).c_str()); return; default: break; } CastMember::setField(field, d); } // This isn't documented particularly well by the Lingo Dictionary; // as well as letting you read/write properties on the cast member, // Director allows you to read/write some properties to a subset of the text // within the cast member defined by a chunk expression, e.g.: // // set the textStyle of char 2 to 4 of field "Pudge" to "bold" bool TextCastMember::hasChunkField(int field) { switch (field) { case kTheForeColor: case kTheTextFont: case kTheTextHeight: case kTheTextSize: case kTheTextStyle: return true; default: break; } return false; } Datum TextCastMember::getChunkField(int field, int start, int end) { Datum d; switch (field) { case kTheForeColor: d = (int)getForeColor(start, end); break; case kTheTextFont: d = getTextFont(start, end); break; case kTheTextHeight: warning("TextCastMember::getChunkField(): getting text height(line spacing) is not implemented yet, returning the default one"); d = (int)_lineSpacing; break; case kTheTextSize: d = getTextSize(start, end); break; case kTheTextStyle: d = getTextStyle(start, end); break; default: break; } return d; } bool TextCastMember::setChunkField(int field, int start, int end, const Datum &d) { switch (field) { case kTheForeColor: setForeColor(d.asInt(), start, end); return true; case kTheTextFont: setTextFont(d.asString(), start, end); return true; case kTheTextHeight: warning("TextCastMember::setChunkField(): setting text height(line spacing) is not implemented yet"); return false; case kTheTextSize: setTextSize(d.asInt(), start, end); return true; case kTheTextStyle: setTextStyle(d.asString(), start, end); return true; default: break; } return false; } void TextCastMember::writeCastData(Common::SeekableWriteStream *writeStream) { writeStream->writeByte(_borderSize); // 1 byte writeStream->writeByte(_gutterSize); // 2 bytes writeStream->writeByte(_boxShadow); // 3 bytes writeStream->writeByte(_textType); // 4 bytes writeStream->writeSint16BE(_textAlign); // 6 bytes writeStream->writeUint16BE(_bgpalinfo1); // 8 bytes writeStream->writeUint16BE(_bgpalinfo2); // 10 bytes writeStream->writeUint16BE(_bgpalinfo3); // 12 bytes writeStream->writeUint16BE(_scroll); // 14 bytes Movie::writeRect(writeStream, _initialRect); // (+8) 22 bytes writeStream->writeUint16BE(_maxHeight); // 24 bytes writeStream->writeByte(_textShadow); // 25 bytes writeStream->writeByte(_textFlags); // 26 bytes writeStream->writeUint16BE(_textHeight); // 28 bytes if (_type == kCastButton) { writeStream->writeUint16BE(_buttonType + 1); // 30 bytes } } uint32 TextCastMember::getCastDataSize() { // In total 30 bytes for text and 28 for button uint32 size = (_type == kCastButton) ? 30 : 28; // See Cast::loadCastData size += (_cast->_version >= kFileVer400 && _cast->_version < kFileVer500) ? 2 : 0; return size; } uint32 TextCastMember::writeSTXTResource(Common::SeekableWriteStream *writeStream, uint32 offset) { // Load it before writing if (!_loaded) { load(); } debugC(3, kDebugSaving, "writeSTXTResource(): _ptext: %s\n_ftext = %s\n_rtext: %s", _ptext.encode().c_str(), Common::toPrintable(_ftext).encode().c_str(), Common::toPrintable(_rtext).c_str()); uint32 stxtSize = getSTXTResourceSize() + 8; writeStream->seek(offset); writeStream->writeUint32LE(MKTAG('S', 'T', 'X', 'T')); writeStream->writeUint32LE(getSTXTResourceSize()); // Size of the STXT resource without the header and size writeStream->writeUint32BE(12); // This is the offset, if it's not 12, we throw an error, other offsets are not handled int8 formatting = getFormattingCount(); writeStream->writeUint32BE(_ptext.size()); // Length of the string // Encode only in one format, original may be encoded in multiple formats // Size of one Font Style is 20 + The number of encodings takes 2 bytes writeStream->writeUint32BE(20 * formatting + 2); // Data Length uint64 textPos = writeStream->pos(); writeStream->seek(_ptext.size(), SEEK_CUR); writeStream->writeUint16BE(formatting); FontStyle style; Common::String rawText; uint32 it = 0; uint32 pIndex = 0; if (!_ftext.empty()) { while (it < _ftext.size() - 1) { if (_ftext[it] == '\001' && _ftext[it + 1] == '\016') { // Styling header found debugC(3, kDebugSaving, "Format start offset: %d, text: %s", style.formatStartOffset, Common::toPrintable(_ptext.substr(style.formatStartOffset, pIndex - style.formatStartOffset)).encode().c_str()); Common::CodePage encoding = detectFontEncoding(_cast->_platform, style.fontId); rawText += _ptext.substr(style.formatStartOffset, pIndex - style.formatStartOffset).encode(encoding); debugC(3, kDebugSaving, "Formatting: %s", Common::toPrintable(_ftext.substr(it, 22)).encode().c_str()); it += 2; if (it + 22 > _ftext.size()) { warning("TextCastMember::writeSTXTResource: incorrect format sequence"); break; } // Ignoring height and ascent for now from FontStyle uint16 temp; style.formatStartOffset = pIndex; const Common::u32char_type_t *s = _ftext.substr(it, 22).c_str(); s = Graphics::readHex(&style.fontId, s, 4); s = Graphics::readHex(&temp, s, 2); s = Graphics::readHex(&style.fontSize, s, 4); s = Graphics::readHex(&style.r, s, 4); s = Graphics::readHex(&style.g, s, 4); s = Graphics::readHex(&style.b, s, 4); style.textSlant = temp; style.height = _height; style.ascent = _ascent; style.write(writeStream); it += 22; continue; } pIndex += 1; it++; } // Because we iterate over _ftext.size() - 1 pIndex += 1; } else { pIndex = _ptext.size() - 1; } debugC(3, kDebugSaving, "format start offset: %d, text: %s", style.formatStartOffset, Common::toPrintable(_ptext.substr(style.formatStartOffset, pIndex - style.formatStartOffset)).encode().c_str()); Common::CodePage encoding = detectFontEncoding(_cast->_platform, style.fontId); _ptext.substr(style.formatStartOffset, pIndex - style.formatStartOffset).encode(encoding); rawText += _ptext.substr(style.formatStartOffset, pIndex - style.formatStartOffset).encode(encoding); uint64 currentPos = writeStream->pos(); writeStream->seek(textPos); writeStream->writeString(rawText); writeStream->seek(currentPos); if (debugChannelSet(7, kDebugSaving)) { byte *dumpData = nullptr; dumpData = (byte *)calloc(stxtSize, sizeof(byte)); Common::MemoryWriteStream *dumpStream = new Common::SeekableMemoryWriteStream(dumpData, stxtSize); currentPos = writeStream->pos(); writeStream->seek(offset); dumpStream->write(writeStream, stxtSize); writeStream->seek(currentPos); dumpFile("TextData", _castId, MKTAG('S', 'T', 'X', 'T'), dumpData, getSTXTResourceSize() + 8); free(dumpData); delete dumpStream; } return stxtSize + 8; } uint32 TextCastMember::getSTXTResourceSize() { // Header (offset, string length, data length) + text string + data (FontStyle) return 12 + _ptext.size() + getFormattingCount() * 20 + 2; } uint8 TextCastMember::getFormattingCount() { if (_ftext.c_str() == nullptr) { warning("TextCastMember::getFormattingCount(): The Text cast member has invalid formatted text"); return 0; } if (_ftext.empty()) { return 0; } uint8 count = 0; for (uint32 i = 0; i < _ftext.size() - 1; i++) { if (_ftext[i] == '\001' && _ftext[i + 1] == '\016') { count++; } } return count; } } // End of namespace Director