diff options
Diffstat (limited to 'ksirc/kstextview.cpp')
-rw-r--r-- | ksirc/kstextview.cpp | 2269 |
1 files changed, 2269 insertions, 0 deletions
diff --git a/ksirc/kstextview.cpp b/ksirc/kstextview.cpp new file mode 100644 index 00000000..aedf31ad --- /dev/null +++ b/ksirc/kstextview.cpp @@ -0,0 +1,2269 @@ +/* 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., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ + +#include "kstextview.h" +#include "stringparserstate.h" + +#include <qpainter.h> +#include <qvaluestack.h> +#include <qdragobject.h> +#include <qtimer.h> +#include <qclipboard.h> +#include <qdict.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<QChar> ParsingState; + +static const int PaintBufferExtend = 128; + +// temporary(!) +static QDict<QPixmap> *ksTextViewPixmapDict = 0; +static void cleanupKSTextViewPixmapDict() +{ + delete ksTextViewPixmapDict; + ksTextViewPixmapDict = 0; +} + +QPixmap ksTextViewLoadPixmap( const QString &icon ) +{ + if ( !ksTextViewPixmapDict ) + { + ksTextViewPixmapDict = new QDict<QPixmap>; + ksTextViewPixmapDict->setAutoDelete( true ); + qAddPostRoutine( cleanupKSTextViewPixmapDict ); + } + + QPixmap *pix = ksTextViewPixmapDict->find( icon ); + if ( !pix ) + { + QImage img; + + const QMimeSource *src = kapp->mimeSourceFactory()->data( icon, QString::null ); + if ( !src || !QImageDrag::decode( src, img ) || img.isNull() ) + return QPixmap(); + + pix = new QPixmap( 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" ) + { + QString url = CONSTSTRING( tok.attributes[ "src" ] ); + if ( url.isEmpty() ) + return 0; + + QPixmap 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( QPainter &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? + + QConstString tmp( m_text.ptr, m_text.len ); + const QString &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 = QFontMetrics( props.font ); +} + +void TextChunk::calcExtends() const +{ + QConstString tmp( m_text.ptr, m_text.len ); + const QString &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 ) +{ + QConstString tmp( m_text.ptr, m_text.len ); + const QString &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( QPainter &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( QPainter &p, int x, const StringPtr &text ) +{ + QConstString constString( text.ptr, text.len ); + const QString &str = constString.string(); + + const int width = m_metrics.width( str ); + + const QColorGroup &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( QPainter &p, int x, const StringPtr &text ) +{ + QConstString constString( text.ptr, text.len ); + const QString &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.toQString() << " Parent: " << m_text.toQString() << 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 QPixmap &pixmap ) + : Item( parag ), m_pixmap( pixmap ) +{ +} + +void ImageItem::paint( QPainter &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 QColorGroup &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 ) +{ + //qDebug( "Tokenizer::Tokenizer( %s )", m_text.ascii() ); + + m_lastTag = m_tags.begin(); + + if ( !m_tags.isEmpty() ) { + if ( ( *m_tags.begin() ).type != TagIndex::Open ) { + qDebug( "something went awfully wrong! bailing out with an assertion" ); + qDebug( "text input was: %s", text.data.ascii() ); + } + assert( ( *m_tags.begin() ).type == TagIndex::Open ); + } +} + +Tokenizer::PString Tokenizer::preprocess( const QString &richText ) +{ + PString result; + result.data = richText; + result.tags = scanTagIndices( result.data ); + resolveEntities( result.data, result.tags ); + return result; +} + +QString Tokenizer::convertToRichText( const PString &ptext ) +{ + if ( ptext.tags.isEmpty() ) + return ptext.data; + + QString 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 QChar ch = result[ i ]; + // ### incomplete! + /* this doesn't work quite right (when resolving back + * KCharSet's fromEntity breaks) + if ( ch == '<' || ch == '>' || ch == '&' ) + { + QString entity = KGlobal::charsets()->toEntity( ch ); + indexAdjustment = entity.length() - 1; + result.replace( i, 1, entity ); + } + */ + if ( ch == '<' ) + { + result.replace( i, 1, "<" ); + indexAdjustment = 3; + } + else if ( ch == '>' ) + { + result.replace( i, 1, ">" ); + indexAdjustment = 3; + } + else if ( ch == '&' ) + { + result.replace( i, 1, "&" ); + 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 ) + { + //qDebug( "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 QChar *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 { + qDebug( "EEK, this should never happen. input text was: %s", m_text.ascii() ); + assert( false ); + } + + return false; +} + +Tokenizer::TagIndexList Tokenizer::scanTagIndices( const QString &text ) +{ + const QChar *start = text.unicode(); + const QChar *p = start; + const QChar *endP = p + text.length(); + bool quoted = false; + bool inTag = false; + + TagIndexList tags; + + for (; p < endP; ++p ) + { + const QChar 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( QString &text, TagIndexList &tags ) +{ + const QChar *p = text.unicode(); + const QChar *endP = p + text.length(); + uint i = 0; + bool scanForSemicolon = false; + const QChar *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 QChar ch = *p; + + if ( ch == '&' ) { + ampersand = p; + scanForSemicolon = true; + continue; + } + + if ( ch != ';' || !scanForSemicolon ) + continue; + + assert( ampersand ); + + scanForSemicolon = false; + + const QChar *entityBegin = ampersand + 1; + + const uint entityLength = p - entityBegin; + if ( entityLength == 0 ) + continue; + + const QChar entityValue = KCharsets::fromEntity( QConstString( 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 QChar *p = text.ptr; + const QChar *endP = p + text.len; + const QChar *start = p; + + int state = ScanForName; + + StringPtr key; + + while ( p < endP ) + { + const QChar ch = *p; + + if ( ch == ' ' ) + { + start = ++p; + continue; + } + + if ( state == ScanForEqual ) + { + if ( ch == '=' ) + { + state = ScanForValue; + ++p; + continue; + } + state = ScanForName; + } + + if ( state == ScanForValue ) + { + if ( ch == '=' ) // eh? + { + qDebug( "EH?" ); + ++p; + continue; + } + + if ( key.isNull() ) + { + qDebug( "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; + qDebug( "Never found start \" in tag." ); + ++p; + continue; + } + + const QChar *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.toQString() << endl; + AttributeMap::ConstIterator it = attributes.begin(); + for (; it != attributes.end(); ++it ) + kdDebug(5008) << "attribute: " << it.key().toQString() << + " -> " << it.data().toQString() << endl; +*/ +} + +ItemProperties::ItemProperties() + : reversed( false ) +{ +} + +ItemProperties::ItemProperties( const QFont &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() ) + { + QColor col( CONSTSTRING( colAttr ) ); + if ( col.isValid() ){ + if(!reversed) + color = col; + else + bgColor = col; + } + } + colAttr = attributes[ "bgcolor" ]; + if ( !colAttr.isNull() ) + { + QColor 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 QFont &newFont ) +{ + QFont 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 QPtrList<Item> &items ) + : m_maxHeight( 0 ) +{ + m_items.setAutoDelete( true ); + + assert( !items.autoDelete() ); + + QPtrListIterator<Item> it( items ); + for (; it.current(); ++it ) + appendItem( it.current(), UpdateMaxHeight ); +} + +QString TextLine::updateSelection( const SelectionPoint &start, const SelectionPoint &end ) +{ + QString selectedText; + + // fixes a crash where because of an empty list i becomes null + if ( m_items.isEmpty() ) + return QString::null; + + 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 += QString( 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 += QString( txt.ptr, end.offset + 1 ); + } + else + { + i->setSelectionStatus( Item::InSelection ); + + selectedText += i->text().toQString(); + } + } + + 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( QPtrList<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 + QPtrList<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( QPainter &p, int y ) +{ + QPtrListIterator<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 ) +{ + QPtrListIterator<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; +} + +QString TextLine::plainText() const +{ + QString res; + QPtrListIterator<Item> it( m_items ); + for (; it.current(); ++it ) + res += it.current()->text().toQString(); + return res; +} + +void TextLine::fontChange( const QFont &newFont ) +{ + QPtrListIterator<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 QString &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 ) +{ + QPtrList<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 ); + + QPtrListIterator<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? + QPtrListIterator<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( QPainter &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; +} + +QString TextParag::updateSelection( const SelectionPoint &start, const SelectionPoint &end ) +{ + QString 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 QString &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; + + QValueStack<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.toQString() << " tag.name=" << tag.name.toQString() << 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 ); +} + +QString TextParag::plainText() const +{ + QString result; + QPtrListIterator<TextLine> it( m_lines ); + for (; it.current(); ++it ) + result += it.current()->plainText(); + return result; +} + +void TextParag::fontChange( const QFont &newFont ) +{ + QPtrListIterator<TextLine> it( m_lines ); + for (; it.current(); ++it ) + it.current()->fontChange( newFont ); +} + +ContentsPaintAlgorithm::ContentsPaintAlgorithm( const QPtrListIterator<TextParag> ¶gIt, + QWidget *viewport, QPixmap &paintBuffer, + QPainter &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( QPainter &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 ); + QPainter 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( QWidget *parent, const char *name ) + : QScrollView( parent, name, WRepaintNoErase ), + m_paintBuffer( PaintBufferExtend, PaintBufferExtend ), + m_selectionEndBeforeStart( false ), m_mousePressed( false ), + m_mmbPressed( false ), + m_linkColor( Qt::blue ), m_height(-1), m_inScroll(false), + m_lastScroll(0) +{ + m_parags.setAutoDelete( true ); + viewport()->setBackgroundMode( PaletteBase ); + viewport()->setMouseTracking( true ); + m_autoScrollTimer = new QTimer( this ); + connect(verticalScrollBar(), SIGNAL(valueChanged( int ) ), + this, SLOT(scrolling( int ))); + + setDragAutoScroll( false ); + +// setStaticBackground( true ); +} + +TextView::~TextView() +{ +} + +void TextView::drawContents( QPainter *painter, int clipX, int clipY, int , int clipHeight ) +{ + if ( m_parags.isEmpty() ) + return; + + if ( m_paintBuffer.width() != visibleWidth() ) + m_paintBuffer.resize( visibleWidth(), PaintBufferExtend ); + + QPtrListIterator<TextParag> paragIt( m_parags ); + + ContentsPaintAlgorithm( paragIt, + viewport(), m_paintBuffer, *painter, clipX, clipY, + clipHeight ) + .paint(); +} + +void TextView::viewportResizeEvent( QResizeEvent *ev ) +{ + QScrollView::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 QString &richText ) +{ + TextParag *parag = new TextParag( this, richText ); + m_parags.append( parag ); + layout( false ); + scrollToBottom(); + QPtrListIterator<TextParag> it( m_parags ); + it.toLast(); + return TextParagIterator( it ); +} + +bool TextView::removeParag( const TextParagIterator ¶g ) +{ + + 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 = QString::null; + clearSelectionInternal(); + if ( repaint ) + updateContents(); +} + +TextParagIterator TextView::firstParag() const +{ + return TextParagIterator( QPtrListIterator<TextParag>( m_parags ) ); +} + +QString TextView::plainText() const +{ + if ( m_parags.isEmpty() ) + return QString::null; + + QString result; + QPtrListIterator<TextParag> paragIt( m_parags ); + while ( paragIt.current() ) + { + result += paragIt.current()->plainText(); + ++paragIt; + if ( paragIt.current() ) + result += '\n'; + } + + return result; +} + +QColor TextView::linkColor() const +{ + return m_linkColor; +} + +void TextView::setLinkColor( const QColor &linkColor ) +{ + m_linkColor = linkColor; +} + +void TextView::copy() +{ + QApplication::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( QMouseEvent *ev ) +{ + if ( ev->button() & RightButton ) { + emitLinkClickedForMouseEvent( ev ); + return; + } + + if ( !( ev->button() & LeftButton ) && !(ev->button() & MidButton ) ) + return; + + clearSelection( true ); + SelectionPoint p; + Item *itemUnderMouse = itemAt( ev->pos(), &p, Item::SelectFuzzy ); + if ( p.item && ( ev->button() & 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.toQString(); + if ( ev->button() & LeftButton ) + m_mousePressed = true; + else + m_mmbPressed = true; + } + } +} + +void TextView::contentsMouseMoveEvent( QMouseEvent *ev ) +{ + if ( m_mousePressed && ev->state() == NoButton ) + { + m_mousePressed = false; + m_mmbPressed = false; + } + + if ( m_mousePressed && !m_dragURL.isEmpty() && + ( m_dragStartPos - ev->pos() ).manhattanLength() > QApplication::startDragDistance() ) { + + m_mousePressed = false; + m_dragStartPos = QPoint(); + + startDrag(); + + m_dragURL = QString::null; + return; + } + + + SelectionPoint p; + Item *i = itemAt( ev->pos(), &p, Item::SelectFuzzy ); + if ( !i && !p.item ) + return; + + if ( (ev->state() & LeftButton && m_selectionStart.item && p.item) || + (ev->state() & 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 ) + qSwap( start.offset, end.offset ); + } + else + qSwap( 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; + } + } + } + + QCursor c = KCursor::arrowCursor(); + if ( viewport()->cursor().handle() != c.handle() ) + viewport()->setCursor( c ); +} + +void TextView::contentsMouseReleaseEvent( QMouseEvent *ev ) +{ + stopAutoScroll(); + + bool clicked = (m_mousePressed || m_mmbPressed) && + (m_dragStartPos - ev->pos()).manhattanLength() < QApplication::startDragDistance(); + m_mousePressed = false; + m_mmbPressed = false; + m_dragStartPos = QPoint(); + m_dragURL = QString::null; + + m_selectionMaybeStart = SelectionPoint(); + + if ( (ev->button() & Qt::LeftButton) && !m_selectedText.isEmpty() ) + QApplication::clipboard()->setText( m_selectedText, QClipboard::Selection ); + + if ( clicked ) { + emitLinkClickedForMouseEvent( ev ); + return; + } + + if (ev->button() & Qt::MidButton) + { + emit pasteReq( KApplication::clipboard()->text( QClipboard::Selection ) ); + return; + } +} + +void TextView::fontChange( const QFont & ) +{ + QPtrListIterator<TextParag> it( m_parags ); + for (; it.current(); ++it ) + it.current()->fontChange( font() ); + + layout( true ); +} + +void TextView::startDrag() +{ + QDragObject *dragObj = dragObject( m_dragURL ); + if ( !dragObj ) + return; + + stopAutoScroll(); + + dragObj->drag(); +} + +QDragObject *TextView::dragObject( const QString &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() +{ + QPoint cursor = viewport()->mapFromGlobal( QCursor::pos() ); + + QPoint 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( QMouseEvent *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, SIGNAL( timeout() ), + this, SLOT( autoScroll() ) ); + m_autoScrollTimer->start( 75, false ); + } +} + +void TextView::stopAutoScroll() +{ + disconnect( m_autoScrollTimer, SIGNAL( timeout() ), + this, 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 ) + qSwap( 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; +} + +QString TextView::updateSelection( const SelectionPoint &start, const SelectionPoint &end ) +{ + QString 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 QString::null; + } + + i->setSelectionStatus( Item::SelectionBoth ); + + // ### ugly + const TextChunk *t = dynamic_cast<TextChunk *>( i ); + if ( t ) + { + StringPtr text = t->text(); + selectedText = QString( 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 = QString( text.ptr + start.offset, + end.offset - start.offset + 1 ); + else + selectedText = QString( 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; + + QPtrListIterator<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 QPoint &pos, SelectionPoint *selectionInfo, + Item::SelectionAccuracy accuracy ) +{ + int px = pos.x(); + int py = pos.y(); + + int y = 0; + int height = 0; + QPtrListIterator<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; +} + +QString TextParagIterator::richText() const +{ + if ( atEnd() ) + return QString::null; + + return Tokenizer::convertToRichText( m_paragIt.current()->processedRichText() ); +} + +void TextParagIterator::setRichText( const QString &richText ) +{ + if ( atEnd() ) + return; + + m_paragIt.current()->setRichText( richText ); + + TextView *textView = m_paragIt.current()->textView(); + textView->layout( false ); + + if ( textView->isUpdatesEnabled() ) + textView->updateContents(); +} + +QString TextParagIterator::plainText() const +{ + if ( atEnd() ) + return QString::null; + + return m_paragIt.current()->plainText(); +} + +#include "kstextview.moc" + +/* + * vim: et sw=4 + */ |