/* [<][>][^][v][top][bottom][index][help] */
DEFINITIONS
This source file includes following definitions.
- _selectable
- _selectable
- init
- removeTextField
- show_cursor
- cursorRecord
- display
- add_invalidated_bounds
- setRestrict
- replaceSelection
- setSelection
- notifyEvent
- topmostMouseEntity
- updateText
- updateText
- updateHtmlText
- setTextValue
- get_text_value
- get_htmltext_value
- setTextFormat
- align_line
- setFont
- insertTab
- format_text
- scrollLines
- newLine
- handleChar
- getDefinitionVersion
- parseTextVariableRef
- registerTextVariable
- parseHTML
- set_variable_name
- pointInShape
- getDrawBorder
- setDrawBorder
- getBorderColor
- setBorderColor
- getDrawBackground
- setDrawBackground
- getBackgroundColor
- setBackgroundColor
- setTextColor
- setEmbedFonts
- setWordWrap
- setLeading
- setUnderlined
- setBullet
- setTabStops
- setURL
- setTarget
- setDisplay
- setAlignment
- setIndent
- setBlockIndent
- setRightMargin
- setLeftMargin
- setFontHeight
- parseTypeValue
- typeValueName
- setAutoSize
- getTextAlignment
- onChanged
- handleFocus
- killFocus
- setWidth
- setHeight
// TextField.cpp: User-editable text regions, for Gnash.
//
// Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010,
// 2011 Free Software Foundation, Inc
//
// 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, write to the Free Software
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
//
// Things implemented:
// - setTextFormat does not discard target, url, tabStops, display or
// bullets
// - Above five fields are now implemented (except for target != blank)
// - Call movie_root getURL function to properly open url and target
// Things to work on:
// - For the url cases (url property and anchor tag in html) we should
// change the mouse cursor to the hand cursor standard for linkable
// text
#include "TextField.h"
#include <algorithm>
#include <string>
#include <cstdlib>
#include <cctype>
#include <utility>
#include <map>
#include <boost/assign/list_of.hpp>
#include <boost/bind.hpp>
#include <boost/tuple/tuple.hpp>
#include "utf8.h"
#include "log.h"
#include "swf/DefineEditTextTag.h"
#include "MovieClip.h"
#include "movie_root.h" // for killing focus
#include "as_environment.h"
#include "Font.h"
#include "fontlib.h"
#include "namedStrings.h"
#include "StringPredicates.h"
#include "TextFormat_as.h"
#include "GnashKey.h"
#include "TextRecord.h"
#include "Point2d.h"
#include "GnashNumeric.h"
#include "MouseButtonState.h"
#include "Global_as.h"
#include "Renderer.h"
#include "Transform.h"
#include "ObjectURI.h"
// Text fields have a fixed 2 pixel padding for each side (regardless of border)
#define PADDING_TWIPS 40
// Define the following to get detailed log information about
// textfield bounds and HTML tags:
//#define GNASH_DEBUG_TEXTFIELDS 1
// Define this to get debugging info about text formatting
//#define GNASH_DEBUG_TEXT_FORMATTING 1
namespace gnash {
TextField::TextField(as_object* object, DisplayObject* parent,
const SWF::DefineEditTextTag& def)
:
InteractiveObject(object, parent),
_tag(&def),
_url(""),
_target(""),
_display(),
_tabStops(),
_variable_name(def.variableName()),
_backgroundColor(255,255,255,255),
_borderColor(0,0,0,255),
_textColor(def.color()),
_alignment(def.alignment()),
_font(0),
m_cursor(0u),
_glyphcount(0u),
_scroll(0u),
_maxScroll(1u),
_hScroll(0u),
_maxHScroll(0u),
_bottomScroll(0u),
_linesindisplay(0u),
_maxChars(def.maxChars()),
_autoSize(def.autoSize() ? AUTOSIZE_LEFT : AUTOSIZE_NONE),
_type(def.readOnly() ? typeDynamic : typeInput),
_bounds(def.bounds()),
_selection(0, 0),
_leading(def.leading()),
_indent(def.indent()),
_blockIndent(0),
_leftMargin(def.leftMargin()),
_rightMargin(def.rightMargin()),
_fontHeight(def.textHeight()),
_textDefined(def.hasText()),
_restrictDefined(false),
_underlined(false),
_bullet(false),
m_has_focus(false),
_multiline(def.multiline()),
_password(def.password()),
_text_variable_registered(false),
_drawBackground(def.border()),
_drawBorder(def.border()),
_embedFonts(def.getUseEmbeddedGlyphs()),
_wordWrap(def.wordWrap()),
_html(def.html()),
_selectable(!def.noSelect())
{
assert(object);
// WARNING! remember to set the font *before* setting text value!
boost::intrusive_ptr<const Font> f = def.getFont();
if (!f) f = fontlib::get_default_font();
setFont(f);
const int version = getSWFVersion(*object);
// set default text *before* calling registerTextVariable
// (if the textvariable already exist and has a value
// the text will be replaced with it)
if (_textDefined) {
setTextValue(utf8::decodeCanonicalString(def.defaultText(), version));
}
init();
}
TextField::TextField(as_object* object, DisplayObject* parent,
const SWFRect& bounds)
:
InteractiveObject(object, parent),
_url(""),
_target(""),
_display(),
_tabStops(),
_backgroundColor(255,255,255,255),
_borderColor(0, 0, 0, 255),
_textColor(0, 0, 0, 255),
_alignment(ALIGN_LEFT),
_font(0),
m_cursor(0u),
_glyphcount(0u),
_scroll(0u),
_maxScroll(1u),
_hScroll(0u),
_maxHScroll(0u),
_bottomScroll(0u),
_linesindisplay(0u),
_maxChars(0),
_autoSize(AUTOSIZE_NONE),
_type(typeDynamic),
_bounds(bounds),
_selection(0, 0),
_leading(0),
_indent(0),
_blockIndent(0),
_leftMargin(0),
_rightMargin(0),
_fontHeight(12 * 20),
_textDefined(false),
_restrictDefined(false),
_underlined(false),
_bullet(false),
m_has_focus(false),
_multiline(false),
_password(false),
_text_variable_registered(false),
_drawBackground(false),
_drawBorder(false),
_embedFonts(false),
_wordWrap(false),
_html(false),
_selectable(true)
{
assert(object);
// Use the default font (Times New Roman for Windows, Times for Mac
// according to docs. They don't say what it is for Linux.
boost::intrusive_ptr<const Font> f = fontlib::get_default_font();
setFont(f);
init();
}
void
TextField::init()
{
registerTextVariable();
reset_bounding_box(0, 0);
}
TextField::~TextField()
{
}
void
TextField::removeTextField()
{
int depth = get_depth();
if ( depth < 0 || depth > 1048575 )
{
//IF_VERBOSE_ASCODING_ERRORS(
log_debug(_("CHECKME: removeTextField(%s): TextField depth (%d) "
"out of the 'dynamic' zone [0..1048575], won't remove"),
getTarget(), depth);
//);
return;
}
DisplayObject* p = parent();
assert(p); // every TextField must have a parent, right ?
MovieClip* parentSprite = p->to_movie();
if (!parentSprite) {
log_error("FIXME: attempt to remove a TextField being a child of a %s",
typeName(*p));
return;
}
// second argument is arbitrary, see comments above
// the function declaration in MovieClip.h
parentSprite->remove_display_object(depth, 0);
}
void
TextField::show_cursor(Renderer& renderer, const SWFMatrix& mat)
{
if (_textRecords.empty()) {
return;
}
boost::uint16_t x;
boost::uint16_t y;
boost::uint16_t h;
size_t i = cursorRecord();
SWF::TextRecord record = _textRecords[i];
x = record.xOffset();
y = record.yOffset() - record.textHeight() + getLeading();
h = record.textHeight();
if (!record.glyphs().empty()) {
for (unsigned int p = 0 ; p < (m_cursor - _recordStarts[i]); ++p) {
x += record.glyphs()[p].advance;
}
}
const std::vector<point> line = boost::assign::list_of
(point(x, y))
(point(x, y + h));
renderer.drawLine(line, rgba(0, 0, 0, 255), mat);
}
size_t
TextField::cursorRecord()
{
SWF::TextRecord record;
size_t i = 0;
if (_textRecords.size() != 0) {
while (i < _textRecords.size() && m_cursor >= _recordStarts[i]) {
++i;
}
return i-1;
}
return 0;
}
void
TextField::display(Renderer& renderer, const Transform& base)
{
const DisplayObject::MaskRenderer mr(renderer, *this);
registerTextVariable();
const bool drawBorder = getDrawBorder();
const bool drawBackground = getDrawBackground();
Transform xform = base * transform();
// This is a hack to handle device fonts, which are not affected by
// color transform.
if (!getEmbedFonts()) xform.colorTransform = SWFCxForm();
if ((drawBorder || drawBackground) && !_bounds.is_null()) {
boost::int32_t xmin = _bounds.get_x_min();
boost::int32_t xmax = _bounds.get_x_max();
boost::int32_t ymin = _bounds.get_y_min();
boost::int32_t ymax = _bounds.get_y_max();
const std::vector<point> coords = boost::assign::list_of
(point(xmin, ymin))
(point(xmax, ymin))
(point(xmax, ymax))
(point(xmin, ymax));
rgba borderColor = drawBorder ? getBorderColor() : rgba(0,0,0,0);
rgba backgroundColor = drawBackground ? getBackgroundColor() :
rgba(0,0,0,0);
SWFCxForm cx = xform.colorTransform;
if (drawBorder) borderColor = cx.transform(borderColor);
if (drawBackground) backgroundColor = cx.transform(backgroundColor);
#ifdef GNASH_DEBUG_TEXTFIELDS
log_debug("rendering a Pol composed by corners %s", _bounds);
#endif
renderer.draw_poly(coords, backgroundColor,
borderColor, xform.matrix, true);
}
// Draw our actual text.
// Using a SWFMatrix to translate to def bounds seems an hack to me.
// A cleaner implementation is likely correctly setting the
// _xOffset and _yOffset memebers in glyph records.
// Anyway, see bug #17954 for a testcase.
if (!_bounds.is_null()) {
xform.matrix.concatenate_translation(_bounds.get_x_min(),
_bounds.get_y_min());
}
_displayRecords.clear();
// TODO: work out how leading should be implemented.
const float fontLeading = 0;
//offset the lines
int yoffset = (getFontHeight() + fontLeading) + PADDING_TWIPS;
size_t recordline;
for (size_t i = 0; i < _textRecords.size(); ++i) {
recordline = 0;
//find the line the record is on
while (recordline < _line_starts.size() &&
_line_starts[recordline] <= _recordStarts[i]) {
++recordline;
}
//offset the line
_textRecords[i].setYOffset((recordline-_scroll)*yoffset);
//add the lines we want to the display record
if (_textRecords[i].yOffset() > 0 &&
_textRecords[i].yOffset() < _bounds.height()) {
_displayRecords.push_back(_textRecords[i]);
}
}
SWF::TextRecord::displayRecords(renderer, xform, _displayRecords,
_embedFonts);
if (m_has_focus && !isReadOnly()) show_cursor(renderer, xform.matrix);
clear_invalidated();
}
void
TextField::add_invalidated_bounds(InvalidatedRanges& ranges, bool force)
{
if (!force && !invalidated()) return; // no need to redraw
ranges.add(m_old_invalidated_ranges);
const SWFMatrix& wm = getWorldMatrix(*this);
SWFRect bounds = getBounds();
bounds.expand_to_rect(m_text_bounding_box);
wm.transform(bounds);
ranges.add(bounds.getRange());
}
void
TextField::setRestrict(const std::string& restrict)
{
_restrictDefined = true;
std::string::const_iterator rit = restrict.begin();
std::string::const_iterator re = restrict.end();
std::set<wchar_t>::const_iterator locate;
if (*rit == '^') { //then this is a true RESTRICT pattern, add all chars to _restrictedchars
for (unsigned int i = 0; i <= 255; ++i) {
_restrictedchars.insert(char(i));
}
} else { //then this is an ALLOW pattern, _restrictedchars should remain empty
_restrictedchars.clear();
}
while (rit != re) {
while (rit != re && *rit != '^') { //This loop allows chars
if (*rit == '-') {
log_error("invalid restrict string");
return;
} else if (*(rit+1) == '-') {
if (re - (rit+2) != 0) {
unsigned int q = *(rit+2);
for (unsigned int p = *rit; p <= q; (++p)){
_restrictedchars.insert(char(p));
}
rit += 3;
} else {
log_error("invalid restrict string");
return;
}
} else if (*rit == '\\') {
++rit;
_restrictedchars.insert(*rit);
++rit;
} else {
_restrictedchars.insert(*rit);
++rit;
}
}
if (rit != re) {
++rit;
}
while (rit != re && *rit != '^') { //This loop restricts chars
locate = _restrictedchars.find(*rit);
if (*rit == '-') {
log_error("invalid restrict string");
return;
} else if (*(rit+1) == '-') {
if (re - (rit+2) != 0) {
unsigned int q = *(rit+2);
for (unsigned int p = *rit; p <= q; ++p){
locate = _restrictedchars.find(p);
if(locate != _restrictedchars.end()) {
_restrictedchars.erase(locate);
}
}
++rit;
++rit;
++rit;
} else {
log_error("invalid restrict string");
return;
}
} else if (*rit == '\\') {
++rit;
locate = _restrictedchars.find(*rit);
if(locate != _restrictedchars.end()) {
_restrictedchars.erase(locate);
}
++rit;
} else {
if(locate != _restrictedchars.end()) {
_restrictedchars.erase(locate);
}
++rit;
}
}
if (rit != re) {
++rit;
}
}
_restrict = restrict;
}
void
TextField::replaceSelection(const std::string& replace)
{
const int version = getSWFVersion(*getObject(this));
const std::wstring& wstr = utf8::decodeCanonicalString(replace, version);
assert(_selection.second >= _selection.first);
assert(_selection.second <= _text.size());
assert(_selection.first <= _text.size());
// If the text has changed but the selection hasn't, make sure we
// don't access it out of bounds.
const size_t start = _selection.first;
const size_t end = _selection.second;
const size_t replaceLength = wstr.size();
_text.replace(start, end - start, wstr);
_selection = std::make_pair(start + replaceLength, start + replaceLength);
}
void
TextField::setSelection(int start, int end)
{
if (_text.empty()) {
_selection = std::make_pair(0, 0);
return;
}
const size_t textLength = _text.size();
if (start < 0) start = 0;
else start = std::min<size_t>(start, textLength);
if (end < 0) end = 0;
else end = std::min<size_t>(end, textLength);
// The cursor position is always set to the end value, even if the
// two values are swapped to obtain the selection. Equal values are
// fine.
m_cursor = end;
if (start > end) std::swap(start, end);
_selection = std::make_pair(start, end);
}
void
TextField::notifyEvent(const event_id& ev)
{
switch (ev.id())
{
case event_id::PRESS:
{
movie_root& root = stage();
boost::int32_t x_mouse, y_mouse;
boost::tie(x_mouse, y_mouse) = root.mousePosition();
SWFMatrix m = getMatrix(*this);
x_mouse -= m.get_x_translation();
y_mouse -= m.get_y_translation();
SWF::TextRecord rec;
for (size_t i=0; i < _textRecords.size(); ++i) {
if ((x_mouse > _textRecords[i].xOffset()) &&
(x_mouse < _textRecords[i].xOffset()+_textRecords[i].recordWidth()) &&
(y_mouse > _textRecords[i].yOffset()-_textRecords[i].textHeight()) &&
(y_mouse < _textRecords[i].yOffset())) {
rec = _textRecords[i];
break;
}
}
if (!rec.getURL().empty()) {
root.getURL(rec.getURL(), rec.getTarget(), "",
MovieClip::METHOD_NONE);
}
break;
}
case event_id::KEY_PRESS:
{
setHtml(false); //editable html fields are not yet implemented
std::wstring s = _text;
// id.keyCode is the unique gnash::key::code for a DisplayObject/key.
// The maximum value is about 265, including function keys.
// It seems that typing in DisplayObjects outside the Latin-1 set
// (256 DisplayObject codes, identical to the first 256 of UTF-8)
// is not supported, though a much greater number UTF-8 codes can be
// stored and displayed. See utf.h for more information.
// This is a limit on the number of key codes, not on the
// capacity of strings.
gnash::key::code c = ev.keyCode();
// maybe _text is changed in ActionScript
m_cursor = std::min<size_t>(m_cursor, _text.size());
size_t cur_cursor = m_cursor;
size_t previouslinesize = 0;
size_t nextlinesize = 0;
size_t manylines = _line_starts.size();
LineStarts::iterator linestartit = _line_starts.begin();
LineStarts::const_iterator linestartend = _line_starts.end();
switch (c)
{
case key::BACKSPACE:
if (isReadOnly()) return;
if (m_cursor > 0)
{
s.erase(m_cursor - 1, 1);
m_cursor--;
setTextValue(s);
}
break;
case key::DELETEKEY:
if (isReadOnly()) return;
if (_glyphcount > m_cursor)
{
s.erase(m_cursor, 1);
setTextValue(s);
}
break;
case key::INSERT: // TODO
if (isReadOnly()) return;
break;
case key::HOME:
while ( linestartit < linestartend && *linestartit <= m_cursor ) {
cur_cursor = *linestartit;
linestartit++;
}
m_cursor = cur_cursor;
break;
case key::PGUP:
// if going a page up is too far...
if(_scroll < _linesindisplay) {
_scroll = 0;
m_cursor = 0;
} else { // go a page up
_scroll -= _linesindisplay;
m_cursor = _line_starts[_scroll];
}
scrollLines();
break;
case key::UP:
while ( linestartit < linestartend && *linestartit <= m_cursor ) {
cur_cursor = *linestartit;
linestartit++;
}
//if there is no previous line
if ( linestartit-_line_starts.begin() - 2 < 0 ) {
m_cursor = 0;
break;
}
previouslinesize = _textRecords[linestartit-_line_starts.begin() - 2].glyphs().size();
//if the previous line is smaller
if (m_cursor - cur_cursor > previouslinesize) {
m_cursor = *(--(--linestartit)) + previouslinesize;
} else {
m_cursor = *(--(--linestartit)) + (m_cursor - cur_cursor);
}
if (m_cursor < _line_starts[_scroll] && _line_starts[_scroll] != 0) {
--_scroll;
}
scrollLines();
break;
case key::END:
while ( linestartit < linestartend && *linestartit <= m_cursor ) {
linestartit++;
}
m_cursor = linestartit != linestartend ? *linestartit - 1 : _text.size();
break;
case key::PGDN:
//if going another page down is too far...
if(_scroll + _linesindisplay >= manylines) {
if(manylines - _linesindisplay <= 0) {
_scroll = 0;
} else {
_scroll = manylines - _linesindisplay;
}
if(m_cursor < _line_starts[_scroll-1]) {
m_cursor = _line_starts[_scroll-1];
} else {
m_cursor = _text.size();
}
} else { //go a page down
_scroll += _linesindisplay;
m_cursor = _line_starts[_scroll];
}
scrollLines();
break;
case key::DOWN:
{
while (linestartit < linestartend &&
*linestartit <= m_cursor ) {
cur_cursor = *linestartit;
linestartit++;
}
// linestartit should never be before _line_starts.begin()
const size_t currentLine = linestartit -
_line_starts.begin();
//if there is no next line
if (currentLine >= manylines ) {
m_cursor = _text.size();
break;
}
nextlinesize = _textRecords[currentLine].glyphs().size();
//if the next line is smaller
if (m_cursor - cur_cursor > nextlinesize) {
m_cursor = *linestartit + nextlinesize;
} else {
//put the cursor at the same character distance
m_cursor = *(linestartit) + (m_cursor - cur_cursor);
}
if (_line_starts.size() > _linesindisplay &&
m_cursor >= _line_starts[_scroll+_linesindisplay]) {
++_scroll;
}
scrollLines();
break;
}
case key::LEFT:
m_cursor = m_cursor > 0 ? m_cursor - 1 : 0;
break;
case key::RIGHT:
m_cursor = m_cursor < _glyphcount ? m_cursor + 1 :
_glyphcount;
break;
case key::ENTER:
if (isReadOnly()) return;
if (!multiline()) break;
default:
if (maxChars() != 0) {
if (_maxChars <= _glyphcount) {
break;
}
}
if (isReadOnly()) return;
wchar_t t = static_cast<wchar_t>(
gnash::key::codeMap[c][key::ASCII]);
if (t != 0) {
if (!_restrictDefined) {
// Insert one copy of the character
// at the cursor position.
s.insert(m_cursor, 1, t);
m_cursor++;
} else if (_restrictedchars.count(t)) {
// Insert one copy of the character
// at the cursor position.
s.insert(m_cursor, 1, t);
m_cursor++;
} else if (_restrictedchars.count(tolower(t))) {
// restrict substitutes the opposite case
s.insert(m_cursor, 1, tolower(t));
m_cursor++;
} else if (_restrictedchars.count(toupper(t))) {
// restrict substitutes the opposite case
s.insert(m_cursor, 1, toupper(t));
m_cursor++;
}
}
setTextValue(s);
}
onChanged();
set_invalidated();
}
default:
return;
};
}
InteractiveObject*
TextField::topmostMouseEntity(boost::int32_t x, boost::int32_t y)
{
if (!visible()) return 0;
// Not selectable, so don't catch mouse events!
if (!_selectable) return 0;
SWFMatrix m = getMatrix(*this);
point p(x, y);
m.invert().transform(p);
if (_bounds.point_test(p.x, p.y)) return this;
return 0;
}
void
TextField::updateText(const std::string& str)
{
const int version = getSWFVersion(*getObject(this));
const std::wstring& wstr = utf8::decodeCanonicalString(str, version);
updateText(wstr);
}
void
TextField::updateText(const std::wstring& wstr)
{
_textDefined = true;
if (_text == wstr) return;
set_invalidated();
_text = wstr;
_selection.first = std::min(_selection.first, _text.size());
_selection.second = std::min(_selection.second, _text.size());
format_text();
}
void
TextField::updateHtmlText(const std::wstring& wstr)
{
if (_htmlText == wstr) return;
set_invalidated();
_htmlText = wstr;
format_text();
}
void
TextField::setTextValue(const std::wstring& wstr)
{
updateHtmlText(wstr);
updateText(wstr);
if (!_variable_name.empty() && _text_variable_registered) {
// TODO: notify MovieClip if we have a variable name !
VariableRef ref = parseTextVariableRef(_variable_name);
as_object* tgt = ref.first;
if (tgt) {
const int version = getSWFVersion(*getObject(this));
// we shouldn't truncate, right?
tgt->set_member(ref.second, utf8::encodeCanonicalString(wstr,
version));
}
else {
// nothing to do (too early ?)
log_debug("setTextValue: variable name %s points to a non-existent"
" target, I guess we would not be registered if this was "
"true, or the sprite we've registered our variable name "
"has been unloaded", _variable_name);
}
}
}
std::string
TextField::get_text_value() const
{
// we need the const_cast here because registerTextVariable
// *might* change our text value, calling the non-const
// setTextValue().
// This happens if the TextVariable has not been already registered
// and during registration comes out to name an existing variable
// with a pre-existing value.
const_cast<TextField*>(this)->registerTextVariable();
const int version = getSWFVersion(*getObject(this));
return utf8::encodeCanonicalString(_text, version);
}
std::string
TextField::get_htmltext_value() const
{
const_cast<TextField*>(this)->registerTextVariable();
const int version = getSWFVersion(*getObject(this));
return utf8::encodeCanonicalString(_htmlText, version);
}
void
TextField::setTextFormat(TextFormat_as& tf)
{
//TODO: this is lazy. we should set all the TextFormat variables HERE, i think
//This is just so we can set individual variables without having to call format_text()
//This calls format_text() at the end of setting TextFormat
if (tf.align()) setAlignment(*tf.align());
if (tf.size()) setFontHeight(*tf.size()); // keep twips
if (tf.indent()) setIndent(*tf.indent());
if (tf.blockIndent()) setBlockIndent(*tf.blockIndent());
if (tf.leading()) setLeading(*tf.leading());
if (tf.leftMargin()) setLeftMargin(*tf.leftMargin());
if (tf.rightMargin()) setRightMargin(*tf.rightMargin());
if (tf.color()) setTextColor(*tf.color());
if (tf.underlined()) setUnderlined(*tf.underlined());
if (tf.bullet()) setBullet(*tf.bullet());
setDisplay(tf.display());
if (tf.tabStops()) setTabStops(*tf.tabStops());
// NEED TO IMPLEMENT THESE TWO
if (tf.url()) setURL(*tf.url());
if (tf.target()) setTarget(*tf.target());
format_text();
}
float
TextField::align_line(TextAlignment align, int last_line_start_record, float x)
{
float width = _bounds.width();
float right_margin = getRightMargin();
float extra_space = (width - right_margin) - x - PADDING_TWIPS;
if (extra_space <= 0.0f) {
#ifdef GNASH_DEBUG_TEXTFIELDS
log_debug(_("TextField text doesn't fit in its boundaries: "
"width %g, margin %g - nothing to align"),
width, right_margin);
#endif
return 0.0f;
}
float shift_right = 0.0f;
switch (align) {
case ALIGN_LEFT:
// Nothing to do; already aligned left.
return 0.0f;
case ALIGN_CENTER:
// Distribute the space evenly on both sides.
shift_right = extra_space / 2;
break;
case ALIGN_RIGHT:
// Shift all the way to the right.
shift_right = extra_space;
break;
case ALIGN_JUSTIFY:
// What should we do here?
break;
}
// Shift the beginnings of the records on this line.
for (size_t i = last_line_start_record; i < _textRecords.size(); ++i) {
SWF::TextRecord& rec = _textRecords[i];
rec.setXOffset(rec.xOffset() + shift_right);
}
return shift_right;
}
boost::intrusive_ptr<const Font>
TextField::setFont(boost::intrusive_ptr<const Font> newfont)
{
if (newfont == _font) return _font;
boost::intrusive_ptr<const Font> oldfont = _font;
set_invalidated();
_font = newfont;
format_text();
return oldfont;
}
void
TextField::insertTab(SWF::TextRecord& rec, boost::int32_t& x, float scale)
{
// tab (ASCII HT)
const int space = 32;
int index = rec.getFont()->get_glyph_index(space, _embedFonts);
if (index == -1) {
IF_VERBOSE_MALFORMED_SWF (
log_error(_("TextField: missing glyph for space char (needed "
"for TAB). Make sure DisplayObject shapes for font "
"%s are being exported into your SWF file."),
rec.getFont()->name());
);
}
else {
// TODO: why is there a copy of the vector?
std::vector<int> tabStops = _tabStops;
std::sort(_tabStops.begin(), _tabStops.end());
int tab = 0;
if (!_tabStops.empty()) {
tab = _tabStops.back() + 1;
for (size_t i = 0; i < tabStops.size(); ++i) {
if (tabStops[i] > x) {
if((tabStops[i] - x) < tab) {
tab = tabStops[i] - x;
}
}
}
// This is necessary in case the number of tabs in the text
// are more than the actual number of tabStops inside the
// vector
if (tab != _tabStops.back() + 1) {
SWF::TextRecord::GlyphEntry ge;
ge.index = rec.getFont()->get_glyph_index(32, _embedFonts);
ge.advance = tab;
rec.addGlyph(ge);
x+=ge.advance;
}
}
else {
SWF::TextRecord::GlyphEntry ge;
ge.index = index;
ge.advance = scale * rec.getFont()->get_advance(index,
_embedFonts);
const int tabstop = 4;
rec.addGlyph(ge, tabstop);
x += ge.advance * tabstop;
}
}
}
void
TextField::format_text()
{
_textRecords.clear();
_line_starts.clear();
_recordStarts.clear();
_glyphcount = 0;
_recordStarts.push_back(0);
// nothing more to do if text is empty
if (_text.empty()) {
// TODO: should we still reset _bounds if autoSize != AUTOSIZE_NONE ?
// not sure we should...
reset_bounding_box(0, 0);
return;
}
LineStarts::iterator linestartit = _line_starts.begin();
LineStarts::const_iterator linestartend = _line_starts.end();
AutoSize autoSize = getAutoSize();
if (autoSize != AUTOSIZE_NONE) {
// When doing WordWrap we don't want to change
// the boundaries. See bug #24348
if (!doWordWrap()) {
_bounds.set_to_rect(0, 0, 0, 0); // this is correct for 'true'
}
}
// FIXME: I don't think we should query the definition
// to find the appropriate font to use, as ActionScript
// code should be able to change the font of a TextField
if (!_font) {
log_error(_("No font for TextField!"));
return;
}
boost::uint16_t fontHeight = getFontHeight();
const float scale = fontHeight /
static_cast<float>(_font->unitsPerEM(_embedFonts));
// TODO: work out how leading affects things.
const float fontLeading = 0;
const boost::uint16_t leftMargin = getLeftMargin();
const boost::uint16_t indent = getIndent();
const boost::uint16_t blockIndent = getBlockIndent();
const bool underlined = getUnderlined();
/// Remember the current bounds for autosize.
SWFRect oldBounds(_bounds);
SWF::TextRecord rec; // one to work on
rec.setFont(_font.get());
rec.setUnderline(underlined);
rec.setColor(getTextColor());
rec.setXOffset(PADDING_TWIPS +
std::max(0, leftMargin + indent + blockIndent));
rec.setYOffset(PADDING_TWIPS + fontHeight + fontLeading);
rec.setTextHeight(fontHeight);
// create in textrecord.h
rec.setURL(_url);
rec.setTarget(_target);
// BULLET CASE:
// First, we indent 10 spaces, and then place the bullet
// character (in this case, an asterisk), then we pad it
// again with 10 spaces
// Note: this works only for additional lines of a
// bulleted list, so that is why there is a bullet format
// in the beginning of format_text()
if (_bullet) {
int space = rec.getFont()->get_glyph_index(32, _embedFonts);
SWF::TextRecord::GlyphEntry ge;
ge.index = space;
ge.advance = scale * rec.getFont()->get_advance(space, _embedFonts);
rec.addGlyph(ge, 5);
// We use an asterisk instead of a bullet
int bullet = rec.getFont()->get_glyph_index(42, _embedFonts);
ge.index = bullet;
ge.advance = scale * rec.getFont()->get_advance(bullet, _embedFonts);
rec.addGlyph(ge);
space = rec.getFont()->get_glyph_index(32, _embedFonts);
ge.index = space;
ge.advance = scale * rec.getFont()->get_advance(space, _embedFonts);
rec.addGlyph(ge, 4);
}
boost::int32_t x = static_cast<boost::int32_t>(rec.xOffset());
boost::int32_t y = static_cast<boost::int32_t>(rec.yOffset());
// Start the bbox at the upper-left corner of the first glyph.
//reset_bounding_box(x, y + fontHeight);
int last_code = -1; // only used if _embedFonts
int last_space_glyph = -1;
size_t last_line_start_record = 0;
_line_starts.push_back(0);
// String iterators are very sensitive to
// potential changes to the string (to allow for copy-on-write).
// So there must be no external changes to the string or
// calls to most non-const member functions during this loop.
// Especially not c_str() or data().
std::wstring::const_iterator it = _text.begin();
const std::wstring::const_iterator e = _text.end();
///handleChar takes care of placing the glyphs
handleChar(it, e, x, y, rec, last_code, last_space_glyph,
last_line_start_record);
// Expand bounding box to include the whole text (if autoSize and wordWrap
// is not in operation.
if (_autoSize != AUTOSIZE_NONE && !doWordWrap())
{
_bounds.expand_to_point(x + PADDING_TWIPS, y + PADDING_TWIPS);
if (_autoSize == AUTOSIZE_RIGHT) {
/// Autosize right expands from the previous right margin.
SWFMatrix m;
m.set_x_translation(oldBounds.get_x_max() - _bounds.width());
m.transform(_bounds);
}
else if (_autoSize == AUTOSIZE_CENTER) {
// Autosize center expands from the previous center.
SWFMatrix m;
m.set_x_translation(oldBounds.get_x_min() + oldBounds.width() / 2.0 -
_bounds.width() / 2.0);
m.transform(_bounds);
}
}
// Add the last line to our output.
_textRecords.push_back(rec);
// align the last (or single) line
align_line(getTextAlignment(), last_line_start_record, x);
scrollLines();
set_invalidated(); //redraw
}
void
TextField::scrollLines()
{
boost::uint16_t fontHeight = getFontHeight();
const float fontLeading = 0;
_linesindisplay = _bounds.height() / (fontHeight + fontLeading + PADDING_TWIPS);
if (_linesindisplay > 0) { //no need to place lines if we can't fit any
size_t manylines = _line_starts.size();
size_t lastvisibleline = _scroll + _linesindisplay;
size_t line = 0;
// If there aren't as many lines as we have scrolled, display the
// end of the text.
if (manylines < _scroll) {
_scroll = manylines - _linesindisplay;
return;
}
// which line is the cursor on?
while (line < manylines && _line_starts[line] <= m_cursor) {
++line;
}
if (manylines - _scroll <= _linesindisplay) {
// This is for if we delete a line
if (manylines < _linesindisplay) _scroll = 0;
else {
_scroll = manylines - _linesindisplay;
}
} else if (line < _scroll) {
//if we are at a higher position, scroll the lines down
_scroll -= _scroll - line;
} else if (manylines > _scroll + _linesindisplay) {
//if we are at a lower position, scroll the lines up
if (line >= (_scroll+_linesindisplay)) {
_scroll += line - (lastvisibleline);
}
}
}
}
void
TextField::newLine(boost::int32_t& x, boost::int32_t& y,
SWF::TextRecord& rec, int& last_space_glyph,
LineStarts::value_type& last_line_start_record, float div)
{
// newline.
LineStarts::iterator linestartit = _line_starts.begin();
LineStarts::const_iterator linestartend = _line_starts.end();
// TODO: work out how leading affects things.
const float leading = 0;
// Close out this stretch of glyphs.
++_glyphcount;
_textRecords.push_back(rec);
_recordStarts.push_back(_glyphcount);
align_line(getTextAlignment(), last_line_start_record, x);
// Expand bounding box to include last column of text ...
if (!doWordWrap() && _autoSize != AUTOSIZE_NONE) {
_bounds.expand_to_point(x + PADDING_TWIPS, y + PADDING_TWIPS);
}
// new paragraphs get the indent.
x = std::max(0, getLeftMargin() + getIndent() + getBlockIndent()) +
PADDING_TWIPS;
y += div * (getFontHeight() + leading);
if (y >= _bounds.height()) {
++_maxScroll;
}
// Start a new record on the next line. Other properties of the
// TextRecord should be left unchanged.
rec.clearGlyphs();
rec.setXOffset(x);
rec.setYOffset(y);
last_space_glyph = -1;
last_line_start_record = _textRecords.size();
linestartit = _line_starts.begin();
linestartend = _line_starts.end();
//Fit a line_start in the correct place
const size_t currentPos = _glyphcount;
while (linestartit < linestartend && *linestartit < currentPos)
{
++linestartit;
}
_line_starts.insert(linestartit, currentPos);
// BULLET CASE:
// First, we indent 10 spaces, and then place the bullet
// character (in this case, an asterisk), then we pad it
// again with 10 spaces
// Note: this works only for additional lines of a
// bulleted list, so that is why there is a bullet format
// in the beginning of format_text()
if (_bullet)
{
int space = rec.getFont()->get_glyph_index(32, _embedFonts);
SWF::TextRecord::GlyphEntry ge;
ge.index = space;
const float scale = getFontHeight() /
static_cast<float>(_font->unitsPerEM(_embedFonts));
ge.advance = scale * rec.getFont()->get_advance(space, _embedFonts);
rec.addGlyph(ge,5);
_glyphcount += 5;
int bullet = rec.getFont()->get_glyph_index(42, _embedFonts);
ge.index = bullet;
ge.advance = scale * rec.getFont()->get_advance(bullet, _embedFonts);
rec.addGlyph(ge);
++_glyphcount;
ge.index = space;
ge.advance = scale * rec.getFont()->get_advance(space, _embedFonts);
rec.addGlyph(ge,4);
_glyphcount += 4;
}
}
void
TextField::handleChar(std::wstring::const_iterator& it,
const std::wstring::const_iterator& e, boost::int32_t& x,
boost::int32_t& y, SWF::TextRecord& rec, int& last_code,
int& last_space_glyph, LineStarts::value_type& last_line_start_record)
{
LineStarts::iterator linestartit = _line_starts.begin();
LineStarts::const_iterator linestartend = _line_starts.end();
float scale = _fontHeight /
static_cast<float>(_font->unitsPerEM(_embedFonts));
float fontDescent = _font->descent(_embedFonts) * scale;
// TODO: work out how leading should be implemented.
const float leading = 0;
const float fontLeading = 0;
boost::uint32_t code = 0;
while (it != e)
{
code = *it++;
if (!code) break;
if ( _embedFonts )
{
x += rec.getFont()->get_kerning_adjustment(last_code,
static_cast<int>(code)) * scale;
last_code = static_cast<int>(code);
}
// Expand the bounding-box to the lower-right corner of each glyph as
// we generate it.
m_text_bounding_box.expand_to_point(x, y + fontDescent);
switch (code)
{
case 27:
// Ignore escape
break;
case 9:
insertTab(rec, x, scale);
break;
case 8:
// Backspace
// This is a limited hack to enable overstrike effects.
// It backs the cursor up by one DisplayObject and then continues
// the layout. E.g. you can use this to display an underline
// cursor inside a simulated text-entry box.
//
// ActionScript understands the '\b' escape sequence
// for inserting a BS DisplayObject.
//
// ONLY WORKS FOR BACKSPACING OVER ONE CHARACTER, WON'T BS
// OVER NEWLINES, ETC.
if (!rec.glyphs().empty())
{
// Peek at the previous glyph, and zero out its advance
// value, so the next char overwrites it.
float advance = rec.glyphs().back().advance;
x -= advance;
// Remove one glyph
rec.clearGlyphs(1);
}
continue;
case 13:
case 10:
{
newLine(x,y,rec,last_space_glyph,last_line_start_record,1.0);
break;
}
case '<':
if (doHtml())
{
//close out this stretch of glyphs
_textRecords.push_back(rec);
rec.clearGlyphs();
_recordStarts.push_back(_glyphcount);
if (*it == '/') {
while (it != e && *it != '>') {
++it;
}
++it;
return;
}
LOG_ONCE(log_debug(_("HTML in a text field is unsupported, "
"gnash will just ignore the tags and "
"print their content")));
std::wstring discard;
std::map<std::string,std::string> attributes;
SWF::TextRecord newrec;
newrec.setFont(rec.getFont());
newrec.setUnderline(rec.underline());
newrec.setColor(rec.color());
newrec.setTextHeight(rec.textHeight());
newrec.setXOffset(x);
newrec.setYOffset(y);
bool selfclosing = false;
bool complete = parseHTML(discard, attributes, it, e, selfclosing);
std::string s(discard.begin(), discard.end());
std::map<std::string,std::string>::const_iterator attloc;
if (!complete) {
//parsing went wrong
continue;
} else {
// Don't think this is the best way to match with
// tags...
// TODO: assumes tags are properly nested. This isn't
// correct.
if (s == "U") {
//underline
newrec.setUnderline(true);
handleChar(it, e, x, y, newrec, last_code,
last_space_glyph, last_line_start_record);
}
else if (s == "A") {
// anchor (blue text).
rgba color(0, 0, 0xff, 0xff);
newrec.setColor(color);
newrec.setUnderline(true);
attloc = attributes.find("HREF");
if (attloc != attributes.end()) {
newrec.setURL(attloc->second);
}
attloc = attributes.find("TARGET");
if (attloc !=attributes.end()) {
newrec.setTarget(attloc->second);
}
handleChar(it, e, x, y, newrec, last_code,
last_space_glyph, last_line_start_record);
}
else if (s == "B") {
//bold
Font* boldfont =
fontlib::get_font(rec.getFont()->name(),
true, rec.getFont()->isItalic());
newrec.setFont(boldfont);
handleChar(it, e, x, y, newrec, last_code,
last_space_glyph, last_line_start_record);
}
else if (s == "FONT") {
//font
boost::uint16_t originalsize = _fontHeight;
attloc = attributes.find("COLOR");
if (attloc != attributes.end()) {
std::string hexval(attloc->second);
if (hexval.empty() || hexval[0] != '#') {
// FIXME: should this be a log_aserror
// or log_unimpl ? It is triggered
// by TextFieldHTML.as
log_error("Unexpected value '%s' in "
"TextField font color attribute",
hexval);
}
else {
hexval.erase(0, 1);
// font COLOR attribute
const rgba color =
colorFromHexString(hexval);
newrec.setColor(color);
}
}
attloc = attributes.find("FACE");
if (attloc != attributes.end()) {
if (attloc->second.empty()) {
IF_VERBOSE_ASCODING_ERRORS(
log_aserror(_("Expected a font name in FACE attribute."))
);
} else {
//font FACE attribute
Font* newfont =
fontlib::get_font(attloc->second,
rec.getFont()->isBold(),
rec.getFont()->isItalic());
newrec.setFont(newfont);
}
}
attloc = attributes.find("SIZE");
if (attloc != attributes.end()) {
//font SIZE attribute
std::string firstchar = attloc->second.substr(0,1);
if (firstchar == "+") {
newrec.setTextHeight(rec.textHeight() +
(pixelsToTwips(std::strtol(
attloc->second.substr(1,attloc->second.length()-1).data(),
NULL,10))));
newrec.setYOffset(PADDING_TWIPS +
newrec.textHeight() +
(fontLeading - fontDescent));
_fontHeight += pixelsToTwips(std::strtol(
attloc->second.substr(1,attloc->second.length()-1).data(),
NULL,10));
} else if (firstchar == "-") {
newrec.setTextHeight(rec.textHeight() -
(pixelsToTwips(std::strtol(
attloc->second.substr(1,attloc->second.length()-1).data(),
NULL,10))));
newrec.setYOffset(PADDING_TWIPS +
newrec.textHeight() +
(fontLeading - fontDescent));
_fontHeight -= pixelsToTwips(std::strtol(
attloc->second.substr(1,attloc->second.length()-1).data(),
NULL,10));
} else {
newrec.setTextHeight(pixelsToTwips(std::strtol(
attloc->second.data(), NULL, 10)));
newrec.setYOffset(PADDING_TWIPS + newrec.textHeight() +
(fontLeading - fontDescent));
_fontHeight = pixelsToTwips(std::strtol(
attloc->second.data(), NULL, 10));
}
}
handleChar(it, e, x, y, newrec, last_code,
last_space_glyph, last_line_start_record);
_fontHeight = originalsize;
y = newrec.yOffset();
}
else if (s == "IMG") {
//image
log_unimpl("<img> html tag in TextField");
handleChar(it, e, x, y, newrec, last_code,
last_space_glyph, last_line_start_record);
}
else if (s == "I") {
//italic
Font* italicfont =
fontlib::get_font(rec.getFont()->name(),
rec.getFont()->isBold(), true);
newrec.setFont(italicfont);
handleChar(it, e, x, y, newrec, last_code,
last_space_glyph, last_line_start_record);
} else if (s == "LI") {
//list item (bullet)
int space = newrec.getFont()->get_glyph_index(32, _embedFonts);
SWF::TextRecord::GlyphEntry ge;
ge.index = space;
ge.advance = scale * newrec.getFont()->get_advance(space, _embedFonts);
newrec.addGlyph(ge, 5);
// We use an asterisk instead of a bullet
int bullet = newrec.getFont()->get_glyph_index(42, _embedFonts);
ge.index = bullet;
ge.advance = scale * newrec.getFont()->get_advance(bullet, _embedFonts);
newrec.addGlyph(ge);
space = newrec.getFont()->get_glyph_index(32, _embedFonts);
ge.index = space;
ge.advance = scale * newrec.getFont()->get_advance(space, _embedFonts);
newrec.addGlyph(ge, 4);
handleChar(it, e, x, y, newrec, last_code,
last_space_glyph, last_line_start_record);
newLine(x, y, newrec, last_space_glyph,
last_line_start_record, 1.0);
}
else if (s == "SPAN") {
//span
log_unimpl("<span> html tag in TextField");
handleChar(it, e, x, y, newrec, last_code,
last_space_glyph, last_line_start_record);
}
else if (s == "TEXTFORMAT") {
log_debug("in textformat");
//textformat
boost::uint16_t originalblockindent = getBlockIndent();
boost::uint16_t originalindent = getIndent();
boost::uint16_t originalleading = getLeading();
boost::uint16_t originalleftmargin = getLeftMargin();
boost::uint16_t originalrightmargin = getRightMargin();
std::vector<int> originaltabstops = getTabStops();
attloc = attributes.find("BLOCKINDENT");
if (attloc != attributes.end()) {
//textformat BLOCKINDENT attribute
setBlockIndent(pixelsToTwips(std::strtol(
attloc->second.data(), NULL, 10)));
if (newrec.xOffset() == std::max(0, originalleftmargin +
originalindent + originalblockindent) + PADDING_TWIPS) {
//if beginning of line, indent
x = std::max(0, getLeftMargin() +
getIndent() + getBlockIndent())
+ PADDING_TWIPS;
newrec.setXOffset(x);
}
}
attloc = attributes.find("INDENT");
if (attloc != attributes.end()) {
//textformat INDENT attribute
setIndent(pixelsToTwips(std::strtol(
attloc->second.data(), NULL, 10)));
if (newrec.xOffset() == std::max(0, originalleftmargin +
originalindent + getBlockIndent()) + PADDING_TWIPS) {
//if beginning of line, indent
x = std::max(0, getLeftMargin() +
getIndent() + getBlockIndent())
+ PADDING_TWIPS;
newrec.setXOffset(x);
}
}
attloc = attributes.find("LEADING");
if (attloc != attributes.end()) {
//textformat LEADING attribute
setLeading(pixelsToTwips(std::strtol(
attloc->second.data(), NULL, 10)));
}
attloc = attributes.find("LEFTMARGIN");
if (attloc != attributes.end()) {
//textformat LEFTMARGIN attribute
setLeftMargin(pixelsToTwips(std::strtol(
attloc->second.data(), NULL, 10)));
if (newrec.xOffset() == std::max(0, originalleftmargin +
getIndent() + getBlockIndent()) + PADDING_TWIPS) {
//if beginning of line, indent
x = std::max(0, getLeftMargin() +
getIndent() + getBlockIndent())
+ PADDING_TWIPS;
newrec.setXOffset(x);
}
}
attloc = attributes.find("RIGHTMARGIN");
if (attloc != attributes.end()) {
//textformat RIGHTMARGIN attribute
setRightMargin(pixelsToTwips(std::strtol(
attloc->second.data(), NULL, 10)));
//FIXME:Should not apply this to this line if we are not at
//beginning of line. Not sure how to do that.
}
attloc = attributes.find("TABSTOPS");
if (attloc != attributes.end()) {
//textformat TABSTOPS attribute
log_unimpl("html <textformat> tag tabstops attribute");
}
handleChar(it, e, x, y, newrec, last_code,
last_space_glyph, last_line_start_record);
setBlockIndent(originalblockindent);
setIndent(originalindent);
setLeading(originalleading);
setLeftMargin(originalleftmargin);
setRightMargin(originalrightmargin);
setTabStops(originaltabstops);
}
else if (s == "P") {
//paragraph
if (_display == TEXTFORMAT_BLOCK) {
handleChar(it, e, x, y, newrec, last_code,
last_space_glyph,
last_line_start_record);
newLine(x, y, rec, last_space_glyph,
last_line_start_record, 1.0);
newLine(x, y, rec, last_space_glyph,
last_line_start_record, 1.5);
}
else {
handleChar(it, e, x, y, newrec, last_code,
last_space_glyph,
last_line_start_record);
}
}
else if (s == "BR" || s == "SBR") {
//line break
newLine(x, y, rec, last_space_glyph,
last_line_start_record, 1.0);
}
else {
log_debug("<%s> tag is unsupported", s);
if (!selfclosing) { //then recurse, look for closing tag
handleChar(it, e, x, y, newrec, last_code,
last_space_glyph, last_line_start_record);
}
}
}
rec.setXOffset(x);
rec.setYOffset(y);
continue;
}
// If HTML isn't enabled, carry on and insert the glyph.
// FIXME: do we also want to be changing last_space_glyph?
// ...because we are...
case 32:
last_space_glyph = rec.glyphs().size();
// Don't break, as we still need to insert the space glyph.
default:
{
if ( password() )
{
SWF::TextRecord::GlyphEntry ge;
int bullet = rec.getFont()->get_glyph_index(42, _embedFonts);
ge.index = bullet;
ge.advance = scale * rec.getFont()->get_advance(bullet,
_embedFonts);
rec.addGlyph(ge);
++_glyphcount;
break;
}
// The font table holds up to 65535 glyphs. Casting
// from uint32_t would, in the event that the code
// is higher than 65535, result in the wrong DisplayObject
// being chosen. Flash can currently only handle 16-bit
// values.
int index = rec.getFont()->get_glyph_index(
static_cast<boost::uint16_t>(code), _embedFonts);
IF_VERBOSE_MALFORMED_SWF (
if (index == -1)
{
// Missing glyph! Log the first few errors.
static int s_log_count = 0;
if (s_log_count < 10)
{
s_log_count++;
if (_embedFonts)
{
log_swferror(_("TextField: missing embedded "
"glyph for char %d. Make sure DisplayObject "
"shapes for font %s are being exported "
"into your SWF file"),
code, _font->name());
}
else
{
log_swferror(_("TextField: missing device "
"glyph for char %d. Maybe you don't have "
"font '%s' installed in your system."),
code, _font->name());
}
}
// Drop through and use index == -1; this will display
// using the empty-box glyph
}
);
SWF::TextRecord::GlyphEntry ge;
ge.index = index;
ge.advance = scale * rec.getFont()->get_advance(index,
_embedFonts);
rec.addGlyph(ge);
x += ge.advance;
++_glyphcount;
}
}
float width = _bounds.width();
if (x >= width - getRightMargin() - PADDING_TWIPS)
{
#ifdef GNASH_DEBUG_TEXT_FORMATTING
log_debug("Text in TextField %s exceeds width [ _bounds %s ]",
getTarget(), _bounds);
#endif
// No wrap and no resize: truncate
if (!doWordWrap() && getAutoSize() == AUTOSIZE_NONE)
{
#ifdef GNASH_DEBUG_TEXT_FORMATTING
log_debug(" wordWrap=false, autoSize=none");
#endif
// Truncate long line, but keep expanding text box
bool newlinefound = false;
while (it != e)
{
code = *it++;
if (_embedFonts)
{
x += rec.getFont()->get_kerning_adjustment(last_code,
static_cast<int>(code)) * scale;
last_code = code;
}
// Expand the bounding-box to the lower-right corner
// of each glyph, even if we don't display it
m_text_bounding_box.expand_to_point(x, y + fontDescent);
#ifdef GNASH_DEBUG_TEXT_FORMATTING
log_debug("Text bbox expanded to %s (width: %f)",
m_text_bounding_box, m_text_bounding_box.width());
#endif
if (code == 13 || code == 10)
{
newlinefound = true;
break;
}
int index = rec.getFont()->get_glyph_index(
static_cast<boost::uint16_t>(code), _embedFonts);
x += scale * rec.getFont()->get_advance(index, _embedFonts);
}
if (!newlinefound) break;
}
else if (doWordWrap()) {
#ifdef GNASH_DEBUG_TEXT_FORMATTING
log_debug(" wordWrap=true");
#endif
// Insert newline if there's space or autosize != none
// Close out this stretch of glyphs.
_textRecords.push_back(rec);
float previous_x = x;
x = std::max(0, getLeftMargin() + getBlockIndent()) + PADDING_TWIPS;
y += _fontHeight + leading;
if (y >= _bounds.height()) {
++_maxScroll;
}
// Start a new record on the next line.
rec.clearGlyphs();
rec.setXOffset(x);
rec.setYOffset(y);
// TODO : what if m_text_glyph_records is empty ?
// Is it possible ?
assert(!_textRecords.empty());
SWF::TextRecord& last_line = _textRecords.back();
linestartit = _line_starts.begin();
linestartend = _line_starts.end();
if (last_space_glyph == -1)
{
// Pull the previous glyph down onto the
// new line.
if (!last_line.glyphs().empty())
{
rec.addGlyph(last_line.glyphs().back());
x += last_line.glyphs().back().advance;
previous_x -= last_line.glyphs().back().advance;
last_line.clearGlyphs(1);
//record the new line start
//
const size_t currentPos = _glyphcount;
while (linestartit != linestartend &&
*linestartit + 1 <= currentPos)
{
linestartit++;
}
_line_starts.insert(linestartit, currentPos);
_recordStarts.push_back(currentPos);
}
} else {
// Move the previous word down onto the next line.
previous_x -= last_line.glyphs()[last_space_glyph].advance;
const SWF::TextRecord::Glyphs::size_type lineSize =
last_line.glyphs().size();
for (unsigned int i = last_space_glyph + 1; i < lineSize;
++i)
{
rec.addGlyph(last_line.glyphs()[i]);
x += last_line.glyphs()[i].advance;
previous_x -= last_line.glyphs()[i].advance;
}
last_line.clearGlyphs(lineSize - last_space_glyph);
// record the position at the start of this line as
// a line_start
const size_t linestartpos = _glyphcount -
rec.glyphs().size();
while (linestartit < linestartend &&
*linestartit < linestartpos)
{
++linestartit;
}
_line_starts.insert(linestartit, linestartpos);
_recordStarts.push_back(linestartpos);
}
align_line(getTextAlignment(), last_line_start_record, previous_x);
last_space_glyph = -1;
last_line_start_record = _textRecords.size();
}
else
{
#ifdef GNASH_DEBUG_TEXT_FORMATTING
log_debug(" wordWrap=%d, autoSize=%d", _wordWrap, _autoSize);
#endif
}
}
}
}
int
TextField::getDefinitionVersion() const
{
// TODO: work out if this correct.
return get_root()->getDefinitionVersion();
}
TextField::VariableRef
TextField::parseTextVariableRef(const std::string& variableName) const
{
VariableRef ret;
ret.first = 0;
#ifdef DEBUG_DYNTEXT_VARIABLES
log_debug(_("VariableName: %s"), variableName);
#endif
/// Why isn't get_environment const again ?
const as_environment& env = const_cast<TextField*>(this)->get_environment();
as_object* target = getObject(env.target());
if (!target) {
IF_VERBOSE_MALFORMED_SWF(
log_swferror(_("Current environment has no target, "
"can't bind VariableName (%s) associated to "
"text field. Gnash will try to register "
"again on next access."), variableName);
);
return ret;
}
// If the variable string contains a path, we extract
// the appropriate target from it and update the variable
// name. We copy the string so we can assign to it if necessary.
std::string parsedName = variableName;
std::string path, var;
if (parsePath(variableName, path, var)) {
#ifdef DEBUG_DYNTEXT_VARIABLES
log_debug(_("Variable text Path: %s, Var: %s"), path, var);
#endif
// find target for the path component
// we use our parent's environment for this
target = findObject(env, path);
parsedName = var;
}
if (!target) {
IF_VERBOSE_MALFORMED_SWF(
log_swferror(_("VariableName associated to text field refers "
"to an unknown target (%s). It is possible that the "
"DisplayObject will be instantiated later in the SWF "
"stream. Gnash will try to register again on next "
"access."), path);
);
return ret;
}
ret.first = target;
ret.second = getURI(getVM(*object()), parsedName);
return ret;
}
void
TextField::registerTextVariable()
{
//#define DEBUG_DYNTEXT_VARIABLES 1
#ifdef DEBUG_DYNTEXT_VARIABLES
log_debug(_("registerTextVariable() called"));
#endif
if (_text_variable_registered) {
return;
}
if (_variable_name.empty()) {
_text_variable_registered = true;
return;
}
VariableRef varRef = parseTextVariableRef(_variable_name);
as_object* target = varRef.first;
if (!target) {
log_debug(_("VariableName associated to text field (%s) refer to "
"an unknown target. It is possible that the DisplayObject "
"will be instantiated later in the SWF stream. "
"Gnash will try to register again on next access."),
_variable_name);
return;
}
const ObjectURI& key = varRef.second;
as_object* obj = getObject(this);
const int version = getSWFVersion(*obj);
// check if the VariableName already has a value,
// in that case update text value
as_value val;
if (target->get_member(key, &val)) {
// TODO: pass environment to to_string ?
// as_environment& env = get_environment();
setTextValue(utf8::decodeCanonicalString(val.to_string(), version));
}
else if (_textDefined) {
as_value newVal = as_value(utf8::encodeCanonicalString(_text, version));
target->set_member(key, newVal);
}
MovieClip* sprite = get<MovieClip>(target);
if (sprite) {
// add the textfield variable to the target sprite
// TODO: have set_textfield_variable take a string_table::key instead ?
sprite->set_textfield_variable(key, this);
}
_text_variable_registered = true;
}
/// Parses an HTML tag (between < and >) and puts
/// the contents into tag. Returns false if the
/// tag was incomplete. The iterator is moved to after
/// the closing tag or the end of the string.
bool
TextField::parseHTML(std::wstring& tag,
std::map<std::string, std::string>& attributes,
std::wstring::const_iterator& it,
const std::wstring::const_iterator& e,
bool& selfclosing) const
{
while (it != e && *it != ' ') {
if (*it == '/') {
++it;
if (*it == '>') {
++it;
selfclosing = true;
return true;
} else {
while (it != e) {
++it;
}
log_error("invalid html tag");
return false;
}
}
if (*it == '>') {
++it;
return true;
}
// Check for NULL character
if (*it == 0) {
log_error("found NULL character in htmlText");
return false;
}
tag.push_back(std::toupper(*it));
++it;
}
while (it != e && *it == ' ') {
++it; //skip over spaces
}
if (*it == '>') {
++it;
return true;
}
if (*it == '/') {
++it;
if (*it == '>') {
++it;
selfclosing = true;
return true;
} else {
while (it != e) {
++it;
}
log_error("invalid html tag");
return false;
}
}
std::string attname;
std::string attvalue;
//attributes
while (it != e && *it != '>') {
while (it != e && *it != '=' && *it != ' ') {
if (*it == 0) {
log_error("found NULL character in htmlText");
return false;
}
if (*it == '>') {
log_error("malformed HTML tag, invalid attribute name");
while (it != e) {
++it;
}
return false;
}
attname.push_back(std::toupper(*it));
++it;
}
while (it != e && (*it == ' ' || *it == '=')) {
++it; //skip over spaces and '='
}
if (it == e) return false;
const char q = *it;
if (q != '"' && q != '\'') {
// This is not an attribute.
while (it != e) ++it;
return false;
}
// Advance past attribute opener
++it;
while (it != e && *it != q) {
if (*it == 0) {
log_error("found NULL character in htmlText");
return false;
}
attvalue.push_back(std::toupper(*it));
++it;
}
if (it == e) return false;
if (*it != q) {
while (it != e) ++it;
return false;
}
// Skip attribute closer.
++it;
attributes.insert(std::make_pair(attname, attvalue));
attname.clear();
attvalue.clear();
if ((*it != ' ') && (*it != '/') && (*it != '>')) {
log_error("malformed HTML tag, invalid attribute value");
while (it != e) {
++it;
}
return false;
}
if (*it == ' ') {
while (it != e && *it == ' ') {
++it; //skip over spaces
}
}
if (*it == '>') {
++it;
return true;
} else if (*it == '/') {
++it;
if (*it == '>') {
++it;
selfclosing = true;
return true;
} else {
while (it != e) {
++it;
}
log_error("invalid html tag");
return false;
}
}
}
#ifdef GNASH_DEBUG_TEXTFIELDS
log_debug ("HTML tag: %s", utf8::encodeCanonicalString(tag, 7));
#endif
log_error("I declare this a HTML syntax error");
return false; //since we did not return already, must be malformed...?
}
void
TextField::set_variable_name(const std::string& newname)
{
if (newname != _variable_name) {
_variable_name = newname;
// The name was empty or undefined, so there's nothing more to do.
if (_variable_name.empty()) return;
_text_variable_registered = false;
#ifdef DEBUG_DYNTEXT_VARIABLES
log_debug("Calling updateText after change of variable name");
#endif
// Use the original definition text if this isn't dynamically
// created.
if (_tag) updateText(_tag->defaultText());
#ifdef DEBUG_DYNTEXT_VARIABLES
log_debug("Calling registerTextVariable after change of variable "
"name and updateText call");
#endif
registerTextVariable();
}
}
bool
TextField::pointInShape(boost::int32_t x, boost::int32_t y) const
{
const SWFMatrix wm = getWorldMatrix(*this).invert();
point lp(x, y);
wm.transform(lp);
return _bounds.point_test(lp.x, lp.y);
}
bool
TextField::getDrawBorder() const
{
return _drawBorder;
}
void
TextField::setDrawBorder(bool val)
{
if (_drawBorder != val) {
set_invalidated();
_drawBorder = val;
}
}
rgba
TextField::getBorderColor() const
{
return _borderColor;
}
void
TextField::setBorderColor(const rgba& col)
{
if (_borderColor != col) {
set_invalidated();
_borderColor = col;
}
}
bool
TextField::getDrawBackground() const
{
return _drawBackground;
}
void
TextField::setDrawBackground(bool val)
{
if (_drawBackground != val) {
set_invalidated();
_drawBackground = val;
}
}
rgba
TextField::getBackgroundColor() const
{
return _backgroundColor;
}
void
TextField::setBackgroundColor(const rgba& col)
{
if (_backgroundColor != col) {
set_invalidated();
_backgroundColor = col;
}
}
void
TextField::setTextColor(const rgba& col)
{
if (_textColor != col) {
set_invalidated();
_textColor = col;
std::for_each(_displayRecords.begin(), _displayRecords.end(),
boost::bind(&SWF::TextRecord::setColor, _1, _textColor));
}
}
void
TextField::setEmbedFonts(bool use)
{
if (_embedFonts != use) {
set_invalidated();
_embedFonts=use;
format_text();
}
}
void
TextField::setWordWrap(bool wrap)
{
if (_wordWrap != wrap) {
set_invalidated();
_wordWrap = wrap;
format_text();
}
}
void
TextField::setLeading(boost::int16_t h)
{
if (_leading != h) {
set_invalidated();
_leading = h;
}
}
void
TextField::setUnderlined(bool v)
{
if (_underlined != v) {
set_invalidated();
_underlined = v;
}
}
void
TextField::setBullet(bool b)
{
if (_bullet != b) {
_bullet = b;
}
}
void
TextField::setTabStops(const std::vector<int>& tabStops)
{
_tabStops.resize(tabStops.size());
for (size_t i = 0; i < tabStops.size(); i ++) {
_tabStops[i] = pixelsToTwips(tabStops[i]);
}
set_invalidated();
}
void
TextField::setURL(std::string url)
{
if (_url != url) {
set_invalidated();
_url = url;
}
}
void
TextField::setTarget(std::string target)
{
if (_target != target) {
set_invalidated();
_target = target;
}
}
void
TextField::setDisplay(TextFormatDisplay display)
{
if (_display != display) {
set_invalidated();
_display = display;
}
}
void
TextField::setAlignment(TextAlignment h)
{
if (_alignment != h) {
set_invalidated();
_alignment = h;
}
}
void
TextField::setIndent(boost::uint16_t h)
{
if (_indent != h) {
set_invalidated();
_indent = h;
}
}
void
TextField::setBlockIndent(boost::uint16_t h)
{
if (_blockIndent != h) {
set_invalidated();
_blockIndent = h;
}
}
void
TextField::setRightMargin(boost::uint16_t h)
{
if (_rightMargin != h) {
set_invalidated();
_rightMargin = h;
}
}
void
TextField::setLeftMargin(boost::uint16_t h)
{
if (_leftMargin != h) {
set_invalidated();
_leftMargin = h;
}
}
void
TextField::setFontHeight(boost::uint16_t h)
{
if (_fontHeight != h) {
set_invalidated();
_fontHeight = h;
}
}
TextField::TypeValue
TextField::parseTypeValue(const std::string& val)
{
StringNoCaseEqual cmp;
if (cmp(val, "input")) return typeInput;
if (cmp(val, "dynamic")) return typeDynamic;
return typeInvalid;
}
const char*
TextField::typeValueName(TypeValue val)
{
switch (val) {
case typeInput:
//log_debug("typeInput returned as 'input'");
return "input";
case typeDynamic:
//log_debug("typeDynamic returned as 'dynamic'");
return "dynamic";
default:
//log_debug("invalid type %d returned as 'invalid'", (int)val);
return "invalid";
}
}
void
TextField::setAutoSize(AutoSize val)
{
if (val == _autoSize) return;
set_invalidated();
_autoSize = val;
format_text();
}
TextField::TextAlignment
TextField::getTextAlignment()
{
TextAlignment textAlignment = getAlignment();
switch (_autoSize) {
case AUTOSIZE_CENTER:
textAlignment = ALIGN_CENTER;
break;
case AUTOSIZE_LEFT:
textAlignment = ALIGN_LEFT;
break;
case AUTOSIZE_RIGHT:
textAlignment = ALIGN_RIGHT;
break;
default:
// Leave it as it was.
break;
}
return textAlignment;
}
void
TextField::onChanged()
{
as_object* obj = getObject(this);
callMethod(obj, NSV::PROP_BROADCAST_MESSAGE, "onChanged", obj);
}
/// This is called by movie_root when focus is applied to this TextField.
//
/// The return value is true if the TextField can receive focus.
/// The swfdec testsuite suggests that version 5 textfields cannot ever
/// handle focus.
bool
TextField::handleFocus()
{
set_invalidated();
/// Select the entire text on focus.
setSelection(0, _text.length());
m_has_focus = true;
m_cursor = _text.size();
format_text();
return true;
}
/// This is called by movie_root when focus is removed from the
/// current TextField.
void
TextField::killFocus()
{
if (!m_has_focus) return;
set_invalidated();
m_has_focus = false;
format_text(); // is this needed ?
}
void
TextField::setWidth(double newwidth)
{
const SWFRect& bounds = getBounds();
_bounds.set_to_rect(bounds.get_x_min(),
bounds.get_y_min(),
bounds.get_x_min() + newwidth,
bounds.get_y_max());
}
void
TextField::setHeight(double newheight)
{
const SWFRect& bounds = getBounds();
_bounds.set_to_rect(bounds.get_x_min(),
bounds.get_y_min(),
bounds.get_x_max(),
bounds.get_y_min() + newheight);
}
} // namespace gnash
// Local Variables:
// mode: C++
// indent-tabs-mode: t
// End: