Files
2026-02-02 04:50:13 +01:00

1124 lines
33 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#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<TextType>(stream.readByte());
_textAlign = static_cast<TextAlignType>(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<TextType>(stream.readByte());
_textAlign = static_cast<TextAlignType>(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<ButtonType>(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<int>(dims.right, dims.left + _initialRect.width());
dims.bottom = MIN<int>(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<int>(dims.right, dims.left + _initialRect.width());
dims.bottom = MAX<int>(dims.bottom, dims.top + MAX<int>(_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<Channel *> 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