You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdenetwork/ksirc/kstextview.cpp

2270 lines
56 KiB

/* This file is part of the KDE project
Copyright (C) 2001 Simon Hausmann <hausmann@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library 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
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "kstextview.h"
#include "stringparserstate.h"
#include <tqpainter.h>
#include <tqvaluestack.h>
#include <tqdragobject.h>
#include <tqtimer.h>
#include <tqclipboard.h>
#include <tqdict.h>
#include <kcharsets.h>
#include <kapplication.h>
#include <kmimesourcefactory.h>
#include <kcursor.h>
#include <kurldrag.h>
#include <kdebug.h>
#include <assert.h>
using namespace KSirc;
typedef StringParserState<TQChar> ParsingState;
static const int PaintBufferExtend = 128;
// temporary(!)
static TQDict<TQPixmap> *ksTextViewPixmapDict = 0;
static void cleanupKSTextViewPixmapDict()
{
delete ksTextViewPixmapDict;
ksTextViewPixmapDict = 0;
}
TQPixmap ksTextViewLoadPixmap( const TQString &icon )
{
if ( !ksTextViewPixmapDict )
{
ksTextViewPixmapDict = new TQDict<TQPixmap>;
ksTextViewPixmapDict->setAutoDelete( true );
tqAddPostRoutine( cleanupKSTextViewPixmapDict );
}
TQPixmap *pix = ksTextViewPixmapDict->find( icon );
if ( !pix )
{
TQImage img;
const TQMimeSource *src = kapp->mimeSourceFactory()->data( icon, TQString() );
if ( !src || !TQImageDrag::decode( src, img ) || img.isNull() )
return TQPixmap();
pix = new TQPixmap( img );
ksTextViewPixmapDict->insert( icon, pix );
}
return *pix;
}
Item::Item( TextParag *parag, const ItemProperties &props )
: m_extendsDirty( true ), m_minWidth( -1 ), m_width( - 1 ),
m_height( - 1 ), m_selection( NoSelection ), m_line( 0 ),
m_parag( parag ), m_props( props )
{
}
Item::~Item()
{
}
int Item::width() const
{
if ( m_extendsDirty )
{
calcExtends();
m_extendsDirty = false;
}
return m_width;
}
int Item::minWidth() const
{
if ( m_extendsDirty )
{
calcExtends();
m_extendsDirty = false;
}
return m_minWidth;
}
int Item::height() const
{
if ( m_extendsDirty )
{
calcExtends();
m_extendsDirty = false;
}
return m_height;
}
Item *Item::breakLine( int )
{
return 0; // can't break by default...
}
int Item::calcSelectionOffset( int x )
{
return x; // default ...
}
void Item::selectionOffsets( int &startOffset, int &endOffset )
{
m_parag->textView()->selectionOffsets( startOffset, endOffset );
}
int Item::maxSelectionOffset() const
{
return text().len - 1;
}
StringPtr Item::text() const
{
return StringPtr();
}
void Item::setProps( const ItemProperties &props )
{
m_props = props;
m_extendsDirty = true;
}
Item *Item::create( TextParag *parag, const Token &tok, const ItemProperties &props )
{
assert( tok.id != Token::TagClose );
if ( tok.id == Token::Text )
return new TextChunk( parag, tok.value, props );
if ( tok.value == "img" )
{
TQString url = CONSTSTRING( tok.attributes[ "src" ] );
if ( url.isEmpty() )
return 0;
TQPixmap pixmap = ksTextViewLoadPixmap( url );
if ( pixmap.isNull() )
return 0;
return new ImageItem( parag, pixmap );
}
return 0;
}
void Item::setLine(TextLine *line)
{
m_line = line;
}
TextChunk::TextChunk( TextParag *parag, const StringPtr &text, const ItemProperties &props )
: Item( parag, props ), m_text( text ), m_originalTextLength( text.len ),
m_metrics( props.font ), m_parent(0)
{
}
void TextChunk::paint( TQPainter &p )
{
p.setFont( m_props.font );
if ( m_selection == NoSelection )
paintText( p, 0, m_text );
else
paintSelection( p );
}
Item *TextChunk::breakLine( int width )
{
ParsingState state( m_text.ptr, m_text.len );
const int requestedWidth = width;
const int spaceWidth = m_metrics.width( ' ' );
state.skip( ' ' );
if ( state.atEnd() ) // eh?
return 0;
StringPtr firstWord; firstWord.ptr = state.current();
firstWord.len = state.advanceTo( ' ' );
const int firstWordWidth = m_metrics.width( CONSTSTRING( firstWord ) );
if ( !state.atBegin() ) // some leading spaces?
width -= spaceWidth;
width -= firstWordWidth;
if ( width < 0 ) {
StringPtr rightHandSide = breakInTheMiddle( requestedWidth );
if ( rightHandSide.isNull() )
return 0;
return hardBreak( rightHandSide );
}
while ( !state.atEnd() )
{
bool spaceSeen = state.skip( ' ' ) > 0;
if ( state.atEnd() )
break;
StringPtr word; word.ptr = state.current();
word.len = state.advanceTo( ' ' );
const int wordWidth = m_metrics.width( CONSTSTRING( word ) );
if ( spaceSeen )
{
width -= spaceWidth;
spaceSeen = false;
}
width -= wordWidth;
if ( width > 0 )
continue;
StringPtr split( word.ptr, state.end() - word.ptr );
return hardBreak( split );
}
return 0;
}
Item::LayoutResetStatus TextChunk::resetLayout()
{
if ( m_originalTextLength == 0 )
{
if ( m_parent )
{
if ( m_selection == SelectionStart )
m_parent->mergeSelection( this, m_parag->textView()->selectionStart() );
else if ( m_selection == SelectionEnd )
m_parent->mergeSelection( this, m_parag->textView()->selectionEnd() );
else if ( m_selection == SelectionBoth )
{
m_parent->mergeSelection( this, m_parag->textView()->selectionStart() );
m_parent->mergeSelection( this, m_parag->textView()->selectionEnd() );
}
}
return DeleteItem;
}
m_extendsDirty |= ( m_text.len != m_originalTextLength );
m_text.len = m_originalTextLength;
return KeepItem;
}
int TextChunk::calcSelectionOffset( int x )
{
// ### how to optimize?
TQConstString tmp( m_text.ptr, m_text.len );
const TQString &s = tmp.string();
uint i = 0;
int px = 0;
for (; i < m_text.len; ++i )
{
const int partialWidth = m_metrics.width( s, i + 1 );
if ( px <= x && x <= partialWidth )
return i;
px = partialWidth;
}
kdDebug(5008) << "calcSelectionOffset bug width:" << width() << " partialWidth: " << m_metrics.width( s, i + 1 )<< endl;
//assert( false );
return m_text.len-1;
}
StringPtr TextChunk::text() const
{
return m_text;
}
void TextChunk::setProps( const ItemProperties &props )
{
Item::setProps( props );
m_metrics = TQFontMetrics( props.font );
}
void TextChunk::calcExtends() const
{
TQConstString tmp( m_text.ptr, m_text.len );
const TQString &text = tmp.string();
m_width = m_metrics.width( text );
m_height = m_metrics.lineSpacing();
//m_minWidth = 0;
m_minWidth = m_metrics.charWidth( text, 1 );
/*
ParsingState state( m_text.ptr, m_text.len );
state.skip( ' ' );
if ( state.atEnd() ) // eh?
return;
StringPtr firstWord; firstWord.ptr = state.current();
firstWord.len = state.advanceTo( ' ' );
m_minWidth = m_metrics.width( CONSTSTRING( firstWord ) );
*/
}
StringPtr TextChunk::breakInTheMiddle( int width )
{
TQConstString tmp( m_text.ptr, m_text.len );
const TQString &s = tmp.string();
uint i = 0;
for (; i < m_text.len; ++i )
{
const int partialWidth = m_metrics.width( s, i + 1 );
if ( partialWidth >= width ) {
if ( i == 0 )
return StringPtr();
return StringPtr( m_text.ptr + i, m_text.len - i );
}
}
return StringPtr();
}
Item *TextChunk::hardBreak( const StringPtr &rightHandSide )
{
TextChunk *chunk = new TextChunk( m_parag, rightHandSide, m_props );
chunk->m_originalTextLength = 0; // ### hack... You make the last line dynamic so if it's 1 word it doesn't chop itself up
if(m_parent == 0x0)
chunk->m_parent = this;
else
chunk->m_parent = m_parent;
m_text.len = rightHandSide.ptr - m_text.ptr;
m_extendsDirty = true;
SelectionPoint *selection = 0;
if ( m_selection == SelectionStart )
selection = m_parag->textView()->selectionStart();
else if ( m_selection == SelectionEnd )
selection = m_parag->textView()->selectionEnd();
else if ( m_selection == SelectionBoth ) {
SelectionPoint *selStart = m_parag->textView()->selectionStart();
SelectionPoint *selEnd = m_parag->textView()->selectionEnd();
if ( selStart->offset >= m_text.len ) {
selStart->offset -= m_text.len;
selEnd->offset -= m_text.len;
selStart->item = selEnd->item = chunk;
chunk->setSelectionStatus( m_selection );
m_selection = NoSelection;
} else if ( selEnd->offset >= m_text.len ) {
selEnd->offset -= m_text.len;
selEnd->item = chunk;
chunk->setSelectionStatus( SelectionEnd );
m_selection = SelectionStart;
}
}
if ( selection && selection->offset >= m_text.len ) {
selection->offset -= m_text.len;
selection->item = chunk;
chunk->setSelectionStatus( m_selection );
m_selection = NoSelection;
}
return chunk;
}
void TextChunk::paintSelection( TQPainter &p )
{
int selectionStart = 0;
int selectionEnd = 0;
selectionOffsets( selectionStart, selectionEnd );
if ( m_selection == SelectionStart )
{
const int width = paintText( p, 0, StringPtr( m_text.ptr, selectionStart ) );
paintSelection( p, width, StringPtr( m_text.ptr + selectionStart,
m_text.len - selectionStart ) );
}
else if ( m_selection == InSelection )
paintSelection( p, 0, m_text );
else if ( m_selection == SelectionEnd )
{
const int width = paintSelection( p, 0, StringPtr( m_text.ptr, selectionEnd + 1 ) );
paintText( p, width, StringPtr( m_text.ptr + selectionEnd + 1,
m_text.len - selectionEnd - 1 ) );
}
else if ( m_selection == SelectionBoth )
{
int width = paintText( p, 0, StringPtr( m_text.ptr, selectionStart ) );
width += paintSelection( p, width, StringPtr( m_text.ptr + selectionStart,
selectionEnd - selectionStart + 1 ) );
paintText( p, width, StringPtr( m_text.ptr + selectionEnd + 1,
m_text.len - selectionEnd - 1 ) );
}
}
int TextChunk::paintSelection( TQPainter &p, int x, const StringPtr &text )
{
TQConstString constString( text.ptr, text.len );
const TQString &str = constString.string();
const int width = m_metrics.width( str );
const TQColorGroup &cg = m_parag->textView()->colorGroup();
if (m_props.bgSelColor.isValid())
p.fillRect( x, 0, width, height(), m_props.bgSelColor );
else
p.fillRect( x, 0, width, height(), cg.highlight() );
if (m_props.selColor.isValid())
p.setPen( m_props.selColor );
else
p.setPen( cg.highlightedText() );
p.drawText( x, m_metrics.ascent(), str );
return width;
}
int TextChunk::paintText( TQPainter &p, int x, const StringPtr &text )
{
TQConstString constString( text.ptr, text.len );
const TQString &str = constString.string();
const int width = m_metrics.width( str );
if ( m_props.bgColor.isValid() )
p.fillRect( x, 0, width, height(), m_props.bgColor );
if ( m_props.color.isValid() )
p.setPen( m_props.color );
else
p.setPen( m_parag->textView()->foregroundColor() );
p.drawText( x, m_metrics.ascent(), str );
return width;
}
void TextChunk::mergeSelection( TextChunk *child, SelectionPoint *selection )
{
selection->offset += child->m_text.ptr - m_text.ptr;
if(selection->offset > m_originalTextLength){
kdDebug(5008) << "Child: " << child->m_text.toTQString() << " Parent: " << m_text.toTQString() << endl;
kdDebug(5008) << "Length all wrong!" << endl;
//assert(0);
}
selection->item = this;
if ( ( m_selection == SelectionStart && child->selectionStatus() == SelectionEnd ) ||
( m_selection == SelectionEnd && child->selectionStatus() == SelectionStart ) )
m_selection = SelectionBoth;
else
m_selection = child->selectionStatus();
}
ImageItem::ImageItem( TextParag *parag, const TQPixmap &pixmap )
: Item( parag ), m_pixmap( pixmap )
{
}
void ImageItem::paint( TQPainter &painter )
{
int y = 0;
if(m_line) {
y = (m_line->maxHeight() - m_pixmap.height())/2;
}
if ( m_selection != NoSelection ) {
int h;
if(m_line)
h = m_line->maxHeight();
else
h = height();
if (m_props.bgSelColor.isValid())
painter.fillRect( 0, 0, width(), h, m_props.bgSelColor );
else {
const TQColorGroup &cg = m_parag->textView()->colorGroup();
painter.fillRect( 0, 0, width(), h, cg.highlight() );
}
}
painter.drawPixmap( 0, y, m_pixmap );
}
Item::LayoutResetStatus ImageItem::resetLayout()
{
// nothin' to do
return KeepItem;
}
void ImageItem::calcExtends() const
{
m_width = m_minWidth = m_pixmap.width();
m_height = m_pixmap.height();
}
Tokenizer::Tokenizer( PString &text )
: m_text( text.data ), m_tags( text.tags ),
m_textBeforeFirstTagProcessed( false ), m_done( false )
{
//tqDebug( "Tokenizer::Tokenizer( %s )", m_text.ascii() );
m_lastTag = m_tags.begin();
if ( !m_tags.isEmpty() ) {
if ( ( *m_tags.begin() ).type != TagIndex::Open ) {
tqDebug( "something went awfully wrong! bailing out with an assertion" );
tqDebug( "text input was: %s", text.data.ascii() );
}
assert( ( *m_tags.begin() ).type == TagIndex::Open );
}
}
Tokenizer::PString Tokenizer::preprocess( const TQString &richText )
{
PString result;
result.data = richText;
result.tags = scanTagIndices( result.data );
resolveEntities( result.data, result.tags );
return result;
}
TQString Tokenizer::convertToRichText( const PString &ptext )
{
if ( ptext.tags.isEmpty() )
return ptext.data;
TQString result = ptext.data;
uint i = 0;
TagIndexList tags = ptext.tags;
TagIndexList::Iterator it = tags.begin();
TagIndexList::Iterator end = tags.end();
for (; i < result.length(); ++i )
{
if ( it != end && i == (*it).index )
{
++it;
continue;
}
unsigned short indexAdjustment = 0;
const TQChar ch = result[ i ];
// ### incomplete!
/* this doesn't work quite right (when resolving back
* KCharSet's fromEntity breaks)
if ( ch == '<' || ch == '>' || ch == '&' )
{
TQString entity = KGlobal::charsets()->toEntity( ch );
indexAdjustment = entity.length() - 1;
result.replace( i, 1, entity );
}
*/
if ( ch == '<' )
{
result.replace( i, 1, "&lt;" );
indexAdjustment = 3;
}
else if ( ch == '>' )
{
result.replace( i, 1, "&gt;" );
indexAdjustment = 3;
}
else if ( ch == '&' )
{
result.replace( i, 1, "&amp;" );
indexAdjustment = 4;
}
if ( indexAdjustment > 0 )
{
TagIndexList::Iterator tmpIt = it;
for (; tmpIt != end; ++tmpIt )
(*tmpIt).index += indexAdjustment;
}
}
return result;
}
bool Tokenizer::parseNextToken( Token &tok )
{
if ( m_done )
{
//tqDebug( "Tokenizer: premature end" );
return false;
}
if ( m_tags.isEmpty() )
{
tok.id = Token::Text;
tok.attributes.clear();
tok.value = StringPtr( m_text );
m_done = true;
return true;
}
TagIndexList::ConstIterator it = m_lastTag;
++it;
if ( it == m_tags.end() )
{
m_done = true;
const uint idx = (*m_lastTag).index + 1;
if ( idx >= m_text.length() )
return false;
tok.id = Token::Text;
tok.value = StringPtr( m_text.unicode() + idx,
m_text.length() - idx );
tok.attributes.clear();
return true;
}
// text before first tag opening?
if ( m_lastTag == m_tags.begin() &&
(*m_lastTag).index > 0 &&
!m_textBeforeFirstTagProcessed )
{
tok.id = Token::Text;
tok.attributes.clear();
tok.value = StringPtr( m_text.unicode(),
(*m_lastTag).index );
m_textBeforeFirstTagProcessed = true;
return true;
}
const uint index = (*it).index;
const int type = (*it).type;
const uint lastIndex = (*m_lastTag).index;
const uint lastType = (*m_lastTag).type;
assert( lastIndex < index );
// a tag
if ( lastType == TagIndex::Open &&
type == TagIndex::Close )
{
const TQChar *tagStart = m_text.unicode() + lastIndex + 1;
uint tagLen = ( index - 1 ) - ( lastIndex + 1 ) + 1;
// </bleh> ?
if ( *tagStart == '/' )
{
++tagStart;
--tagLen;
tok.id = Token::TagClose;
}
else
tok.id = Token::TagOpen;
parseTag( StringPtr( tagStart, tagLen ), tok.value, tok.attributes );
m_lastTag = it;
return true;
}
// text
else if ( lastType == TagIndex::Close &&
type == TagIndex::Open )
{
tok.id = Token::Text;
tok.attributes.clear();
tok.value = StringPtr( m_text.unicode() + lastIndex + 1,
( index - 1 ) - lastIndex );
m_lastTag = it;
return true;
}
else {
tqDebug( "EEK, this should never happen. input text was: %s", m_text.ascii() );
assert( false );
}
return false;
}
Tokenizer::TagIndexList Tokenizer::scanTagIndices( const TQString &text )
{
const TQChar *start = text.unicode();
const TQChar *p = start;
const TQChar *endP = p + text.length();
bool quoted = false;
bool inTag = false;
TagIndexList tags;
for (; p < endP; ++p )
{
const TQChar ch = *p;
if ( ch == '"' && inTag )
{
quoted = quoted ? false : true;
continue;
}
if( quoted )
continue;
if ( ch == '<' )
{
inTag = true;
tags.append( TagIndex( p - start, TagIndex::Open ) );
continue;
}
else if ( ch == '>' )
{
inTag = false;
tags.append( TagIndex( p - start, TagIndex::Close ) );
continue;
}
}
return tags;
}
void Tokenizer::resolveEntities( TQString &text, TagIndexList &tags )
{
const TQChar *p = text.unicode();
const TQChar *endP = p + text.length();
uint i = 0;
bool scanForSemicolon = false;
const TQChar *ampersand = 0;
TagIndexList::Iterator tagInfoIt = tags.begin();
TagIndexList::Iterator tagsEnd = tags.end();
for (; p < endP; ++p, ++i )
{
if ( tagInfoIt != tagsEnd &&
i > (*tagInfoIt).index )
++tagInfoIt;
const TQChar ch = *p;
if ( ch == '&' ) {
ampersand = p;
scanForSemicolon = true;
continue;
}
if ( ch != ';' || !scanForSemicolon )
continue;
assert( ampersand );
scanForSemicolon = false;
const TQChar *entityBegin = ampersand + 1;
const uint entityLength = p - entityBegin;
if ( entityLength == 0 )
continue;
const TQChar entityValue = KCharsets::fromEntity( TQConstString( entityBegin, entityLength ).string() );
if ( entityValue.isNull() )
continue;
const uint ampersandPos = ampersand - text.unicode();
text[ ampersandPos ] = entityValue;
text.remove( ampersandPos + 1, entityLength + 1 );
i = ampersandPos;
p = text.unicode() + i;
endP = text.unicode() + text.length();
ampersand = 0;
uint adjustment = entityLength + 1;
TagIndexList::Iterator it = tagInfoIt;
for (; it != tags.end(); ++it )
(*it).index -= adjustment;
}
}
void Tokenizer::parseTag( const StringPtr &text,
StringPtr &tag,
AttributeMap &attributes )
{
assert( text.len > 0 );
attributes.clear();
tag = StringPtr();
const TQChar *p = text.ptr;
const TQChar *endP = p + text.len;
const TQChar *start = p;
int state = ScanForName;
StringPtr key;
while ( p < endP )
{
const TQChar ch = *p;
if ( ch == ' ' )
{
start = ++p;
continue;
}
if ( state == ScanForEqual )
{
if ( ch == '=' )
{
state = ScanForValue;
++p;
continue;
}
state = ScanForName;
}
if ( state == ScanForValue )
{
if ( ch == '=' ) // eh?
{
tqDebug( "EH?" );
++p;
continue;
}
if ( key.isNull() )
{
tqDebug( "Tokenizer: Error, attribute value without key." );
// reset
state = ScanForName;
++p;
continue;
}
start = 0x0;
if ( *p == '"' )
{
++p;
start = p;
while ( p < endP && *p != '"' ) {
++p;
}
}
else {
while ( p < endP && *p != ' ' && *p != '>') {
if(!start)
start = p;
++p;
}
}
if(start == 0x0) {
state = ScanForName;
tqDebug( "Never found start \" in tag." );
++p;
continue;
}
const TQChar *valueEnd = p;
StringPtr value = StringPtr( start, valueEnd - start );
attributes[ key ] = value;
if(*p == '"')
++p; // move p beyond the last "
state = ScanForName;
continue;
}
if ( state == ScanForName )
{
while ( p < endP && *p != ' ' && *p != '=' ){
++p;
}
key = StringPtr( start, p - start );
if ( tag.isNull() )
tag = key;
else
attributes[ key ] = StringPtr();
state = ScanForEqual;
continue;
}
assert( false ); // never reached.
}
/*
kdDebug(5008) << "tagName: " << tag.toTQString() << endl;
AttributeMap::ConstIterator it = attributes.begin();
for (; it != attributes.end(); ++it )
kdDebug(5008) << "attribute: " << it.key().toTQString() <<
" -> " << it.data().toTQString() << endl;
*/
}
ItemProperties::ItemProperties()
: reversed( false )
{
}
ItemProperties::ItemProperties( const TQFont &defaultFont )
: font( defaultFont ), reversed( false )
{
}
ItemProperties::ItemProperties( const ItemProperties &other,
const Token &token,
TextView *textView )
: attributes( token.attributes )
{
// inherit
font = other.font;
color = other.color;
bgColor = other.bgColor;
bgSelColor = other.bgSelColor;
selColor = other.selColor;
reversed = other.reversed;
if ( token.value == "b" )
font.setBold( true );
else if ( token.value == "i" )
font.setItalic( true );
else if ( token.value == "u" )
font.setUnderline( true );
else if ( token.value == "r" ) {
reversed = true;
if(other.bgColor.isValid())
color = other.bgColor;
else
color = textView->paletteBackgroundColor();
if(other.color.isValid())
bgColor = other.color;
else
bgColor = textView->foregroundColor();
}
else if ( token.value == "font" )
{
StringPtr colAttr = attributes[ "color" ];
if ( !colAttr.isNull() )
{
TQColor col( CONSTSTRING( colAttr ) );
if ( col.isValid() ){
if(!reversed)
color = col;
else
bgColor = col;
}
}
colAttr = attributes[ "bgcolor" ];
if ( !colAttr.isNull() )
{
TQColor col( CONSTSTRING( colAttr ) );
if ( col.isValid() ) {
if(!reversed)
bgColor = col;
else
color = col;
}
}
}
else if ( token.value == "a" )
{
color = textView->linkColor();
font.setUnderline( true );
}
}
ItemProperties::ItemProperties( const ItemProperties &rhs )
{
( *this ) = rhs;
}
ItemProperties &ItemProperties::operator=( const ItemProperties &rhs )
{
font = rhs.font;
color = rhs.color;
bgColor = rhs.bgColor;
bgSelColor = rhs.bgSelColor;
selColor = rhs.selColor;
reversed = rhs.reversed;
attributes = rhs.attributes;
return *this;
}
void ItemProperties::updateFont( const TQFont &newFont )
{
TQFont f = newFont;
f.setUnderline( font.underline() );
f.setBold( font.bold() );
f.setItalic( font.italic() );
font = f;
}
TextLine::TextLine()
: m_maxHeight( 0 )
{
m_items.setAutoDelete( true );
}
TextLine::TextLine( const TQPtrList<Item> &items )
: m_maxHeight( 0 )
{
m_items.setAutoDelete( true );
assert( !items.autoDelete() );
TQPtrListIterator<Item> it( items );
for (; it.current(); ++it )
appendItem( it.current(), UpdateMaxHeight );
}
TQString TextLine::updateSelection( const SelectionPoint &start, const SelectionPoint &end )
{
TQString selectedText;
// fixes a crash where because of an empty list i becomes null
if ( m_items.isEmpty() )
return TQString();
if ( start.line == this )
{
const int idx = m_items.findRef( start.item );
assert( idx != -1 );
}
else
m_items.first();
Item *i = m_items.current();
Item *lastItem = 0;
if ( end.line == this )
{
const int oldCurrent = m_items.at();
const int idx = m_items.findRef( end.item );
assert( idx != -1 );
lastItem = m_items.next();
m_items.at( oldCurrent );
}
for (; i != lastItem && i!=0L; i = m_items.next() )
{
if ( i == start.item )
{
i->setSelectionStatus( Item::SelectionStart );
StringPtr txt = i->text();
if ( !txt.isNull() )
selectedText += TQString( txt.ptr + start.offset,
txt.len - start.offset );
}
else if ( i == end.item )
{
i->setSelectionStatus( Item::SelectionEnd );
StringPtr txt = i->text();
if ( !txt.isNull() )
selectedText += TQString( txt.ptr, end.offset + 1 );
}
else
{
i->setSelectionStatus( Item::InSelection );
selectedText += i->text().toTQString();
}
}
return selectedText;
}
void TextLine::clearSelection()
{
Item *i = m_items.first();
for (; i; i = m_items.next() )
i->setSelectionStatus( Item::NoSelection );
}
void TextLine::appendItem( Item *i, int layoutUpdatePolicy )
{
m_items.append( i );
i->setLine(this);
if ( layoutUpdatePolicy == UpdateMaxHeight )
m_maxHeight = kMax( m_maxHeight, i->height() );
}
Item *TextLine::resetLayout( TQPtrList<Item> &remainingItems)
{
Item *lastLineItem = m_items.getLast();
Item *it = m_items.first();
// We have to use the tobeDel structure or we call resetLayout on the Item
// twice for each item we want to delete
TQPtrList<Item> tobeDel;
while ( it )
{
if ( it->resetLayout() == Item::KeepItem )
remainingItems.append( m_items.take() );
else
tobeDel.append( m_items.take() );
it = m_items.current();
}
m_items = tobeDel;
return lastLineItem;
}
void TextLine::paint( TQPainter &p, int y )
{
TQPtrListIterator<Item> it( m_items );
int x = 0;
for (; it.current(); ++it )
{
p.translate( x, y );
it.current()->paint( p );
p.translate( -x, -y );
x += it.current()->width();
}
}
Item *TextLine::itemAt( int px, SelectionPoint *selectionInfo,
Item::SelectionAccuracy accuracy )
{
TQPtrListIterator<Item> it( m_items );
int x = 0;
int width = 0;
for (; it.current(); ++it )
{
width = it.current()->width();
if ( x < px && px < ( x + width ) )
{
Item *i = it.current();
if ( selectionInfo )
{
selectionInfo->pos.setX( x );
selectionInfo->offset = i->calcSelectionOffset( px - x );
selectionInfo->item = i;
selectionInfo->line = this;
}
return i;
}
x += width;
}
if ( accuracy == Item::SelectFuzzy && selectionInfo &&
!m_items.isEmpty() && width > 0 )
{
Item *i = m_items.getLast();
selectionInfo->pos.setX( x - width );
selectionInfo->offset = i->maxSelectionOffset();
selectionInfo->item = i;
selectionInfo->line = this;
}
return 0;
}
TQString TextLine::plainText() const
{
TQString res;
TQPtrListIterator<Item> it( m_items );
for (; it.current(); ++it )
res += it.current()->text().toTQString();
return res;
}
void TextLine::fontChange( const TQFont &newFont )
{
TQPtrListIterator<Item> it( m_items );
for (; it.current(); ++it )
{
ItemProperties props = it.current()->props();
props.updateFont( newFont );
it.current()->setProps( props );
}
}
TextParag::TextParag( TextView *textView, const TQString &richText )
: m_layouted( false ), m_height( 0 ), m_minWidth( 0 ),
m_textView( textView )
{
setRichText( richText );
m_lines.setAutoDelete(true);
}
TextParag::~TextParag()
{
}
void TextParag::layout( int width )
{
TQPtrList<Item> items;
TextLine *row = m_lines.first();
for(;row;row = m_lines.next()){
row->resetLayout( items );
}
m_lines.clear();
// all text chunks are now in a flat list. break them into
// pieces of lists of chunks, so they fit with the given width
m_height = 0;
m_minWidth = 0;
int remainingWidth = width;
SelectionPoint *selStart = m_textView->selectionStart();
SelectionPoint *selEnd = m_textView->selectionEnd();
assert( selStart && selEnd );
TQPtrListIterator<Item> it( items );
while ( it.current() )
{
m_minWidth = kMax( m_minWidth, it.current()->minWidth() );
Item *item = it.current();
int itemWidth = item->width();
if ( remainingWidth >= itemWidth )
{
remainingWidth -= itemWidth;
++it;
continue;
}
Item *newChunk = 0;
if ( itemWidth > item->minWidth() )
newChunk = item->breakLine( remainingWidth );
if ( newChunk || it.atFirst() )
++it;
TextLine *line = new TextLine;
Item *next = it.current();
items.first();
while ( items.current() != next )
{
Item *i = items.take();
if ( selStart->item == i )
selStart->line = line;
else if ( selEnd->item == i )
selEnd->line = line;
line->appendItem( i, TextLine::UpdateMaxHeight );
}
assert( !line->isEmpty() );
m_height += line->maxHeight();
m_lines.append( line );
if ( newChunk )
items.prepend( newChunk );
it.toFirst();
remainingWidth = width;
}
// append what's left
if ( items.count() > 0 )
{
TextLine *line = new TextLine( items );
m_height += line->maxHeight();
m_lines.append( line );
if ( selStart->parag == this ||
selEnd->parag == this )
{
// ### move to TextLine?
TQPtrListIterator<Item> it( line->iterator() );
for (; it.current(); ++it )
{
if ( selStart->item == it.current() )
selStart->line = line;
if ( selEnd->item == it.current() )
selEnd->line = line;
}
}
}
m_layouted = true;
}
void TextParag::paint( TQPainter &p, int y, int maxY )
{
TextLine *row = m_lines.first();
for (; row; row = m_lines.next() )
{
if( (y + row->maxHeight()) >= 0 )
row->paint( p, y );
y += row->maxHeight();
if( y > maxY )
break;
}
}
Item *TextParag::itemAt( int px, int py, SelectionPoint *selectionInfo,
Item::SelectionAccuracy accuracy )
{
int y = 0;
int height = 0;
TextLine *row = m_lines.first();
for (; row; row = m_lines.next() )
{
height = row->maxHeight();
if ( y <= py && py <= ( y + height ) )
{
Item *i = row->itemAt( px, selectionInfo, accuracy );
if ( selectionInfo )
{
selectionInfo->pos.setY( y );
selectionInfo->parag = this;
}
return i;
}
y += height;
}
if ( accuracy == Item::SelectFuzzy && selectionInfo && !m_lines.isEmpty() )
{
TextLine *row = m_lines.getLast();
row->itemAt( px, selectionInfo, accuracy );
selectionInfo->pos.setY( y - height );
selectionInfo->parag = this;
}
return 0;
}
TQString TextParag::updateSelection( const SelectionPoint &start, const SelectionPoint &end )
{
TQString selectedText;
// sanity check
// (don't put it lower because it changes the current list item)
if ( end.parag == this )
assert( m_lines.findRef( end.line ) != -1 );
if ( start.parag == this )
{
int idx = m_lines.findRef( start.line );
assert( idx != -1 );
}
else
m_lines.first();
TextLine *line = m_lines.current();
TextLine *lastLine = m_lines.getLast();
if ( end.parag == this )
lastLine = end.line;
for (; line != lastLine; line = m_lines.next() )
selectedText += line->updateSelection( start, end );
if ( lastLine )
selectedText += lastLine->updateSelection( start, end );
return selectedText;
}
void TextParag::clearSelection()
{
// ### optimize, add 'selectionDirty' flag to TextLine and TextParag!
TextLine *line = m_lines.first();
for (; line; line = m_lines.next() )
line->clearSelection();
}
void TextParag::setRichText( const TQString &richText )
{
m_layouted = false;
m_height = 0;
m_minWidth = 0;
// ### FIXME SELECTION!!!!!!!!!
if ( m_textView->selectionStart()->parag == this ||
m_textView->selectionEnd()->parag == this )
m_textView->clearSelection();
m_lines.clear();
m_processedRichText = Tokenizer::preprocess( richText );
Tokenizer tokenizer( m_processedRichText );
Token tok;
Token lastTextToken;
TQValueStack<Tag> tagStack;
TextLine *line = new TextLine;
while ( tokenizer.parseNextToken( tok ) )
{
if ( tok.id == Token::TagOpen )
{
ItemProperties oldProps( m_textView->font() );
if ( !tagStack.isEmpty() )
oldProps = tagStack.top().props;
// ...bleh<foo>... -> finish off 'bleh' first
if ( lastTextToken.id != -1 )
{
Item *item = Item::create( this, lastTextToken, oldProps );
if ( item )
line->appendItem( item );
lastTextToken = Token();
}
ItemProperties props( oldProps, tok, m_textView );
tagStack.push( Tag( tok.value, props ) );
Item *item = Item::create( this, tok, props );
if ( item )
line->appendItem( item );
continue;
}
else if ( tok.id == Token::TagClose )
{
assert( !tagStack.isEmpty() );
Tag tag = tagStack.pop();
if( !( tok.value == tag.name ) ) {
kdDebug(5008) << "ASSERT failed! tok.value=" << tok.value.toTQString() << " tag.name=" << tag.name.toTQString() << endl;
kdDebug(5008) << "while parsing " << richText << endl;
}
// ...foo</bleh>... -> finish off 'foo'
if ( !lastTextToken.value.isNull() )
{
Item *item = Item::create( this, lastTextToken, tag.props );
if ( item )
line->appendItem( item );
}
lastTextToken = Token();
}
else
lastTextToken = tok;
}
// some plain text at the very end, outside of any tag?
if ( !lastTextToken.value.isNull() )
{
Item *item = Item::create( this, lastTextToken );
if ( item )
line->appendItem( item );
}
m_lines.append( line );
}
TQString TextParag::plainText() const
{
TQString result;
TQPtrListIterator<TextLine> it( m_lines );
for (; it.current(); ++it )
result += it.current()->plainText();
return result;
}
void TextParag::fontChange( const TQFont &newFont )
{
TQPtrListIterator<TextLine> it( m_lines );
for (; it.current(); ++it )
it.current()->fontChange( newFont );
}
ContentsPaintAlgorithm::ContentsPaintAlgorithm( const TQPtrListIterator<TextParag> &paragIt,
TQWidget *viewport, TQPixmap &paintBuffer,
TQPainter &painter, int clipX, int clipY,
int clipHeight )
: m_paragIt( paragIt ), m_viewport( viewport ), m_paintBuffer( paintBuffer ),
m_painter( painter ), m_clipX( clipX ), m_clipY( clipY ), m_clipHeight( clipHeight ),
m_overshoot( 0 )
{
}
int ContentsPaintAlgorithm::goToFirstVisibleParagraph()
{
int y = 0;
while ( y < m_clipY && m_paragIt.current() ) {
y += m_paragIt.current()->height();
++m_paragIt;
}
y = adjustYAndIterator( y, y, m_clipY );
return y;
}
int ContentsPaintAlgorithm::paint( TQPainter &bufferedPainter, int currentY )
{
const int startY = currentY;
int nextY = startY + PaintBufferExtend;
if ( !m_paragIt.current() )
return nextY;
while ( currentY < nextY && m_paragIt.current() ) {
TextParag *parag = m_paragIt.current();
//kdDebug(5008) << "Painting[" << currentY << "/" << parag->height() << "]: " << parag->plainText() << endl;
int drawPos = currentY;
int newY = parag->height();
if(m_overshoot != 0) {
drawPos = currentY - parag->height() + m_overshoot;
newY = m_overshoot;
m_overshoot = 0;
}
parag->paint( bufferedPainter, drawPos, nextY );
currentY += newY;
++m_paragIt;
}
nextY = adjustYAndIterator( startY, currentY, nextY );
return nextY;
}
int ContentsPaintAlgorithm::adjustYAndIterator( int , int currentY, int nextY )
{
// nothing to adjust?
if ( currentY <= nextY || m_paragIt.atFirst() )
return currentY;
if ( m_paragIt.current() )
--m_paragIt;
else
m_paragIt.toLast();
m_overshoot = currentY - nextY;
if(m_overshoot < 0)
m_overshoot = 0;
return nextY;
}
void ContentsPaintAlgorithm::paint()
{
int y = goToFirstVisibleParagraph();
int yEnd = m_clipY + m_clipHeight;
while ( y < yEnd )
{
m_paintBuffer.fill( m_viewport, 0, y );
TQPainter bufferedPainter( &m_paintBuffer );
bufferedPainter.translate( -m_clipX, -y );
int nextY = paint( bufferedPainter, y );
bufferedPainter.end();
m_painter.drawPixmap( m_clipX, y, m_paintBuffer );
y = nextY;
}
}
TextView::TextView( TQWidget *parent, const char *name )
: TQScrollView( parent, name, WRepaintNoErase ),
m_paintBuffer( PaintBufferExtend, PaintBufferExtend ),
m_selectionEndBeforeStart( false ), m_mousePressed( false ),
m_mmbPressed( false ),
m_linkColor( TQt::blue ), m_height(-1), m_inScroll(false),
m_lastScroll(0)
{
m_parags.setAutoDelete( true );
viewport()->setBackgroundMode( PaletteBase );
viewport()->setMouseTracking( true );
m_autoScrollTimer = new TQTimer( this );
connect(verticalScrollBar(), TQT_SIGNAL(valueChanged( int ) ),
this, TQT_SLOT(scrolling( int )));
setDragAutoScroll( false );
// setStaticBackground( true );
}
TextView::~TextView()
{
}
void TextView::drawContents( TQPainter *painter, int clipX, int clipY, int , int clipHeight )
{
if ( m_parags.isEmpty() )
return;
if ( m_paintBuffer.width() != visibleWidth() )
m_paintBuffer.resize( visibleWidth(), PaintBufferExtend );
TQPtrListIterator<TextParag> paragIt( m_parags );
ContentsPaintAlgorithm( paragIt,
viewport(), m_paintBuffer, *painter, clipX, clipY,
clipHeight )
.paint();
}
void TextView::viewportResizeEvent( TQResizeEvent *ev )
{
TQScrollView::viewportResizeEvent(ev);
if ( ev->size().width() != ev->oldSize().width() )
layout();
int newdiff = ev->size().height() - ev->oldSize().height();
setContentsPos( 0, contentsY()-newdiff );
if(m_lastScroll == newdiff){
m_inScroll = false;
m_lastScroll = 0;
}
scrollToBottom();
}
void TextView::scrolling( int value )
{
int tl = m_height-visibleHeight();
int offset = 25;
TextParag *parag = m_parags.last();
if(parag){
if(parag->height() > offset)
offset = parag->height();
}
if((tl - value) > offset)
m_inScroll = true;
else
m_inScroll = false;
m_lastScroll = tl-value;
}
void TextView::scrollToBottom( bool force )
{
bool scroll = true;
if(force == true){
scroll = true;
}
else {
if(m_inScroll){
scroll = false;
}
else {
if(m_mousePressed == true){
scroll = false;
}
else {
scroll = true;
}
}
}
if(scroll == true)
setContentsPos( 0, m_height-visibleHeight() );
}
void TextView::clear()
{
stopAutoScroll();
clearSelection();
m_parags.clear();
layout();
viewport()->erase();
}
TextParagIterator TextView::appendParag( const TQString &richText )
{
TextParag *parag = new TextParag( this, richText );
m_parags.append( parag );
layout( false );
scrollToBottom();
TQPtrListIterator<TextParag> it( m_parags );
it.toLast();
return TextParagIterator( it );
}
bool TextView::removeParag( const TextParagIterator &parag )
{
if ( parag.atEnd() )
return false;
TextParag *paragPtr = parag.m_paragIt.current();
const int idx = m_parags.findRef( paragPtr );
if ( idx == -1 )
return false;
if ( m_selectionStart.parag == paragPtr ||
m_selectionEnd.parag == paragPtr )
clearSelection( false );
int height = paragPtr->height();
m_parags.removeRef( paragPtr );
if(m_selectionStart.item != 0)
m_selectionStart.pos.ry() -= height;
if(m_selectionEnd.item != 0)
m_selectionEnd.pos.ry() -= height;
//layout( false );
contentsChange(-height, true);
if ( isUpdatesEnabled() )
updateContents();
return true;
}
void TextView::clearSelection( bool repaint )
{
m_selectionStart = SelectionPoint();
m_selectionEnd = SelectionPoint();
m_selectionEndBeforeStart = false;
m_selectedText = TQString();
clearSelectionInternal();
if ( repaint )
updateContents();
}
TextParagIterator TextView::firstParag() const
{
return TextParagIterator( TQPtrListIterator<TextParag>( m_parags ) );
}
TQString TextView::plainText() const
{
if ( m_parags.isEmpty() )
return TQString();
TQString result;
TQPtrListIterator<TextParag> paragIt( m_parags );
while ( paragIt.current() )
{
result += paragIt.current()->plainText();
++paragIt;
if ( paragIt.current() )
result += '\n';
}
return result;
}
TQColor TextView::linkColor() const
{
return m_linkColor;
}
void TextView::setLinkColor( const TQColor &linkColor )
{
m_linkColor = linkColor;
}
void TextView::copy()
{
TQApplication::clipboard()->setText( m_selectedText );
}
void TextView::clearSelectionInternal()
{
m_selectionEndBeforeStart = false;
TextParag *p = m_parags.first();
for (; p; p = m_parags.next() )
p->clearSelection();
}
void TextView::contentsMousePressEvent( TQMouseEvent *ev )
{
if ( ev->button() & Qt::RightButton ) {
emitLinkClickedForMouseEvent( ev );
return;
}
if ( !( ev->button() & Qt::LeftButton ) && !(ev->button() & Qt::MidButton ) )
return;
clearSelection( true );
SelectionPoint p;
Item *itemUnderMouse = itemAt( ev->pos(), &p, Item::SelectFuzzy );
if ( p.item && ( ev->button() & Qt::LeftButton ) ) {
m_selectionMaybeStart = p;
p.item->setSelectionStatus( Item::NoSelection );
}
if ( TextChunk *text = dynamic_cast<TextChunk *>( itemUnderMouse ) ) {
StringPtr href = text->props().attributes[ "href" ];
if ( !href.isNull() ) {
m_dragStartPos = ev->pos();
m_dragURL = href.toTQString();
if ( ev->button() & Qt::LeftButton )
m_mousePressed = true;
else
m_mmbPressed = true;
}
}
}
void TextView::contentsMouseMoveEvent( TQMouseEvent *ev )
{
if ( m_mousePressed && ev->state() == Qt::NoButton )
{
m_mousePressed = false;
m_mmbPressed = false;
}
if ( m_mousePressed && !m_dragURL.isEmpty() &&
( m_dragStartPos - ev->pos() ).manhattanLength() > TQApplication::startDragDistance() ) {
m_mousePressed = false;
m_dragStartPos = TQPoint();
startDrag();
m_dragURL = TQString();
return;
}
SelectionPoint p;
Item *i = itemAt( ev->pos(), &p, Item::SelectFuzzy );
if ( !i && !p.item )
return;
if ( (ev->state() & Qt::LeftButton && m_selectionStart.item && p.item) ||
(ev->state() & Qt::LeftButton && m_selectionMaybeStart.item && p.item))
{
if(m_selectionMaybeStart.item != 0){
m_selectionStart = m_selectionMaybeStart;
m_selectionMaybeStart = SelectionPoint();
}
m_selectionEnd = p;
clearSelectionInternal();
updateSelectionOrder();
SelectionPoint start = m_selectionStart;
SelectionPoint end = m_selectionEnd;
if ( m_selectionEndBeforeStart )
{
if ( start.item == end.item )
{
if ( start.offset > end.offset )
tqSwap( start.offset, end.offset );
}
else
tqSwap( start, end );
}
m_selectedText = updateSelection( start, end );
emit selectionChanged();
updateContents();
startAutoScroll();
return;
}
else if ( i )
{
TextChunk *text = dynamic_cast<TextChunk *>( i );
if ( text )
{
StringPtr href = text->props().attributes[ "href" ];
if ( !href.isNull() )
{
viewport()->setCursor( KCursor::handCursor() );
return;
}
}
}
TQCursor c = KCursor::arrowCursor();
if ( viewport()->cursor().handle() != c.handle() )
viewport()->setCursor( c );
}
void TextView::contentsMouseReleaseEvent( TQMouseEvent *ev )
{
stopAutoScroll();
bool clicked = (m_mousePressed || m_mmbPressed) &&
(m_dragStartPos - ev->pos()).manhattanLength() < TQApplication::startDragDistance();
m_mousePressed = false;
m_mmbPressed = false;
m_dragStartPos = TQPoint();
m_dragURL = TQString();
m_selectionMaybeStart = SelectionPoint();
if ( (ev->button() & Qt::LeftButton) && !m_selectedText.isEmpty() )
TQApplication::clipboard()->setText( m_selectedText, TQClipboard::Selection );
if ( clicked ) {
emitLinkClickedForMouseEvent( ev );
return;
}
if (ev->button() & Qt::MidButton)
{
emit pasteReq( KApplication::clipboard()->text( TQClipboard::Selection ) );
return;
}
}
void TextView::fontChange( const TQFont & )
{
TQPtrListIterator<TextParag> it( m_parags );
for (; it.current(); ++it )
it.current()->fontChange( font() );
layout( true );
}
void TextView::startDrag()
{
TQDragObject *dragObj = dragObject( m_dragURL );
if ( !dragObj )
return;
stopAutoScroll();
dragObj->drag();
}
TQDragObject *TextView::dragObject( const TQString &dragURL )
{
#if KDE_IS_VERSION(3,1,92)
return new KURLDrag( KURL( dragURL ), viewport() );
#else
return KURLDrag::newDrag( KURL( dragURL ), viewport() );
#endif
}
void TextView::autoScroll()
{
TQPoint cursor = viewport()->mapFromGlobal( TQCursor::pos() );
TQPoint contentsPos = viewportToContents( cursor );
cursor.rx() -= viewport()->x();
cursor.ry() -= viewport()->y();
if ( ( cursor.x() < 0 || cursor.x() > visibleWidth() ) ||
( cursor.y() < 0 || cursor.y() > visibleHeight() ) ) {
ensureVisible( contentsPos.x(), contentsPos.y(),
0, 5 );
}
}
void TextView::emitLinkClickedForMouseEvent( TQMouseEvent *ev )
{
TextChunk *text = dynamic_cast<TextChunk *>( itemAt( ev->pos() ) );
if ( !text )
return;
StringPtr href = text->props().attributes[ "href" ];
if ( href.isNull() )
return;
emit linkClicked( ev, CONSTSTRING( href ) );
}
void TextView::startAutoScroll()
{
if(m_autoScrollTimer->isActive() == false){
connect( m_autoScrollTimer, TQT_SIGNAL( timeout() ),
this, TQT_SLOT( autoScroll() ) );
m_autoScrollTimer->start( 75, false );
}
}
void TextView::stopAutoScroll()
{
disconnect( m_autoScrollTimer, TQT_SIGNAL( timeout() ),
this, TQT_SLOT( autoScroll() ) );
m_autoScrollTimer->stop();
}
void TextView::selectionOffsets( int &startOffset, int &endOffset )
{
assert( m_selectionStart.item );
if ( m_selectionEndBeforeStart )
{
startOffset = m_selectionEnd.offset;
endOffset = m_selectionStart.offset;
}
else
{
startOffset = m_selectionStart.offset;
endOffset = m_selectionEnd.offset;
}
if ( m_selectionStart.item == m_selectionEnd.item && startOffset > endOffset )
tqSwap( startOffset, endOffset );
}
void TextView::updateSelectionOrder()
{
int start = m_selectionStart.pos.y();
int end = m_selectionEnd.pos.y();
if ( start == end )
{
start = m_selectionStart.pos.x();
end = m_selectionEnd.pos.x();
if ( start == end )
{
start = m_selectionStart.offset;
end = m_selectionEnd.offset;
}
}
m_selectionEndBeforeStart = end < start;
}
SelectionPoint *TextView::selectionStart()
{
return m_selectionEndBeforeStart ? &m_selectionEnd : &m_selectionStart;
}
SelectionPoint *TextView::selectionEnd()
{
return m_selectionEndBeforeStart ? &m_selectionStart : &m_selectionEnd;
}
TQString TextView::updateSelection( const SelectionPoint &start, const SelectionPoint &end )
{
TQString selectedText;
if ( start.item == end.item )
{
Item *i = start.item;
if ( start.offset == end.offset )
{
if ( start.pos.x() == end.pos.x() )
{
i->setSelectionStatus( Item::NoSelection );
return TQString();
}
i->setSelectionStatus( Item::SelectionBoth );
// ### ugly
const TextChunk *t = dynamic_cast<TextChunk *>( i );
if ( t )
{
StringPtr text = t->text();
selectedText = TQString( text.ptr + start.offset, 1 );
}
}
else
{
i->setSelectionStatus( Item::SelectionBoth );
// ### ugly
TextChunk *t = dynamic_cast<TextChunk *>( i );
if ( t )
{
StringPtr text = t->text();
if (end.offset > start.offset)
selectedText = TQString( text.ptr + start.offset,
end.offset - start.offset + 1 );
else
selectedText = TQString( text.ptr + end.offset,
start.offset - end.offset + 1 );
}
}
}
else
{
assert( m_parags.findRef( end.parag ) != -1 );
const int idx = m_parags.findRef( start.parag );
assert( idx != -1 );
TextParag *p = m_parags.current();
for (; p && p != end.parag; p = m_parags.next() )
{
selectedText += p->updateSelection( start, end );
selectedText += '\n';
}
if ( p )
selectedText += p->updateSelection( start, end );
}
return selectedText;
}
void TextView::layout( bool force )
{
int height = 0;
int contentsWidth = visibleWidth();
int width = contentsWidth;
TQPtrListIterator<TextParag> it( m_parags );
for (; it.current(); ++it )
{
if ( !it.current()->isLayouted() || force )
it.current()->layout( width );
height += it.current()->height();
contentsWidth = kMax( contentsWidth, it.current()->minWidth() );
}
if ( m_selectionStart.item && m_selectionEnd.item )
updateSelection( *selectionStart(), *selectionEnd() );
m_height = height;
resizeContents( contentsWidth, height );
}
void TextView::contentsChange(int heightChange, bool force)
{
if(m_height == -1){
layout(force);
}
else {
m_height += heightChange;
resizeContents( visibleWidth(), m_height );
}
if ( m_selectionStart.item && m_selectionEnd.item )
updateSelection( *selectionStart(), *selectionEnd() );
}
Item *TextView::itemAt( const TQPoint &pos, SelectionPoint *selectionInfo,
Item::SelectionAccuracy accuracy )
{
int px = pos.x();
int py = pos.y();
int y = 0;
int height = 0;
TQPtrListIterator<TextParag> it( m_parags );
for (; it.current(); ++it )
{
height = it.current()->height();
if ( y <= py && py <= ( y + height ) )
{
Item *res = it.current()->itemAt( px, py - y, selectionInfo, accuracy );
if ( selectionInfo )
{
selectionInfo->pos.ry() += y;
selectionInfo->pos.rx() = px;
}
return res;
}
y += height;
}
if ( accuracy == Item::SelectFuzzy && selectionInfo && !m_parags.isEmpty() )
{
TextParag *parag = m_parags.getLast();
parag->itemAt( px, height - 1, selectionInfo, accuracy );
selectionInfo->pos.ry() += y - height;
selectionInfo->pos.rx() = px;
}
return 0;
}
TQString TextParagIterator::richText() const
{
if ( atEnd() )
return TQString();
return Tokenizer::convertToRichText( m_paragIt.current()->processedRichText() );
}
void TextParagIterator::setRichText( const TQString &richText )
{
if ( atEnd() )
return;
m_paragIt.current()->setRichText( richText );
TextView *textView = m_paragIt.current()->textView();
textView->layout( false );
if ( textView->isUpdatesEnabled() )
textView->updateContents();
}
TQString TextParagIterator::plainText() const
{
if ( atEnd() )
return TQString();
return m_paragIt.current()->plainText();
}
#include "kstextview.moc"
/*
* vim: et sw=4
*/