Index: trunk/kdenetwork/ksirc/kstextview.cpp =================================================================== --- trunk/kdenetwork/ksirc/kstextview.cpp (revision 123091) +++ trunk/kdenetwork/ksirc/kstextview.cpp (revision 123092) @@ -1,1525 +1,1522 @@ #include "kstextview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const int PaintBufferExtend = 128; // temporary(!) static QDict *ksTextViewPixmapDict = 0; static void cleanupKSTextViewPixmapDict() { delete ksTextViewPixmapDict; ksTextViewPixmapDict = 0; } QPixmap ksTextViewLoadPixmap( const QString &icon ) { if ( !ksTextViewPixmapDict ) { ksTextViewPixmapDict = new QDict; 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 ) : m_extendsDirty( true ), m_minWidth( -1 ), m_width( - 1 ), m_height( - 1 ), m_selection( NoSelection ), m_parag( parag ) { } 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 ); } StringPtr Item::text() const { return StringPtr(); } 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; } TextChunk::TextChunk( TextParag *parag, const StringPtr &text, const ItemProperties &props ) : Item( parag ), m_text( text ), m_originalTextLength( text.len ), m_props( props ), m_metrics( props.font ) { } void TextChunk::paint( QPainter *p, int x, int y ) { p->save(); if ( m_props.color.isValid() ) p->setPen( m_props.color ); p->setFont( m_props.font ); int textYPos = y + height() - m_metrics.descent(); if ( m_selection == NoSelection ) paintText( p, x, y, textYPos, m_text ); else { int selectionStart = 0; int selectionEnd = 0; selectionOffsets( selectionStart, selectionEnd ); if ( m_selection == SelectionStart ) { x += paintText( p, x, y, textYPos, StringPtr( m_text.ptr, selectionStart ) ); paintSelection( p, x, y, textYPos, StringPtr( m_text.ptr + selectionStart, m_text.len - selectionStart ) ); } else if ( m_selection == InSelection ) paintSelection( p, x, y, textYPos, m_text ); else if ( m_selection == SelectionEnd ) { x += paintSelection( p, x, y, textYPos, StringPtr( m_text.ptr, selectionEnd + 1 ) ), p->drawText( x, textYPos, CONSTSTRING( StringPtr( m_text.ptr + selectionEnd + 1, m_text.len - selectionEnd - 1 ) ) ); } else if ( m_selection == SelectionBoth ) { x += paintText( p, x, y, textYPos, StringPtr( m_text.ptr, selectionStart ) ); x += paintSelection( p, x, y, textYPos, StringPtr( m_text.ptr + selectionStart, selectionEnd - selectionStart + 1 ) ); p->drawText( x, textYPos, CONSTSTRING( StringPtr( m_text.ptr + selectionEnd + 1, m_text.len - selectionEnd - 1 ) ) ); } } p->restore(); } Item *TextChunk::breakLine( int width ) { const QChar *p = m_text.ptr; const QChar *endP = p + m_text.len; int spaceWidth = m_metrics.width( ' ' ); while ( p < endP && *p == ' ' ) ++p; if ( p == endP ) // eh? return 0; const QChar *firstWordStart = p; while ( p < endP && *p != ' ' ) ++p; StringPtr firstWord( firstWordStart, p - firstWordStart ); int firstWordWidth = m_metrics.width( CONSTSTRING( firstWord ) ); if ( p > m_text.ptr ) // some leading spaces? width -= spaceWidth; width -= firstWordWidth; bool spaceSeen = false; while ( p < endP ) { if ( *p == ' ' ) { spaceSeen = true; ++p; continue; } const QChar *wordStart = p; while ( p < endP && *p != ' ' ) ++p; StringPtr word( wordStart, p - wordStart ); int wordWidth = m_metrics.width( CONSTSTRING( word ) ); if ( spaceSeen ) { width -= spaceWidth; spaceSeen = false; } width -= wordWidth; if ( width > 0 ) continue; StringPtr split( wordStart, endP - wordStart ); TextChunk *chunk = new TextChunk( m_parag, split, m_props ); chunk->m_originalTextLength = 0; // ### hack... m_text.len = wordStart - m_text.ptr; m_extendsDirty = true; return chunk; } return 0; } Item::LayoutResetStatus TextChunk::resetLayout() { if ( m_originalTextLength == 0 ) return DeleteItem; m_text.len = m_originalTextLength; m_selection = NoSelection; // ### m_extendsDirty = true; 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 ) { int cw = m_metrics.charWidth( s, i ); if ( px <= x && x <= ( px + cw ) ) return i; px += cw; } assert( false ); return -1; } StringPtr TextChunk::text() const { return m_text; } void TextChunk::calcExtends() const { m_width = m_metrics.width( CONSTSTRING( m_text ) ); m_height = m_metrics.lineSpacing(); m_minWidth = 0; const QChar *p = m_text.ptr; const QChar *endP = p + m_text.len; while ( p < endP && *p == ' ' ) ++p; if ( p == endP ) // eh? return; const QChar *firstWordStart = p; while ( p < endP && *p != ' ' ) ++p; StringPtr firstWord( firstWordStart, p - firstWordStart ); m_minWidth = m_metrics.width( CONSTSTRING( firstWord ) ); } int TextChunk::paintSelection( QPainter *p, int x, int y, int yTextPos, const StringPtr &text ) { QConstString constString( text.ptr, text.len ); const QString &str = constString.string(); int width = m_metrics.width( str ); const QColorGroup &cg = m_parag->textView()->colorGroup(); p->save(); p->fillRect( x, y, width, height(), cg.highlight() ); p->setPen( cg.highlightedText() ); p->drawText( x, yTextPos, str ); p->restore(); return width; } int TextChunk::paintText( QPainter *p, int x, int y, int yTextPos, const StringPtr &text ) { QConstString constString( text.ptr, text.len ); const QString &str = constString.string(); int width = m_metrics.width( str ); if ( m_props.bgColor.isValid() ) p->fillRect( x, y, width, height(), m_props.bgColor ); p->drawText( x, yTextPos, str ); return width; } ImageItem::ImageItem( TextParag *parag, const QPixmap &pixmap ) : Item( parag ), m_pixmap( pixmap ) { } void ImageItem::paint( QPainter *painter, int x, int y ) { painter->drawPixmap( x, 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() ) 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; } 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; 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; } uint index = (*it).index; int type = (*it).type; uint lastIndex = (*m_lastTag).index; 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; // ? 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 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(); TagIndexList tags; for (; p < endP; ++p ) { QChar ch = *p; /* breaks "bleh if ( ch == '\"' ) { ++p; while ( p < endP && *p != '\"' ) ++p; continue; } else */ if ( ch == '<' ) { tags.append( TagIndex( p - start, TagIndex::Open ) ); continue; } else if ( ch == '>' ) { 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; if ( *p == '&' ) { ampersand = p; scanForSemicolon = true; continue; } if ( *p != ';' || !scanForSemicolon ) continue; assert( ampersand ); scanForSemicolon = false; const QChar *entityBegin = ampersand + 1; if ( entityBegin == p ) continue; uint ampersandPos = ampersand - text.unicode(); uint entityLength = i - ampersandPos + 1; QChar entityVal = KGlobal::charsets()->fromEntity( QConstString( entityBegin, entityLength - 2 ).string() ); if ( entityVal == QChar::null ) continue; text.remove( ampersandPos + 1, entityLength - 1 ); text[ ampersandPos ] = entityVal; 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 ) { 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 = p; while ( p < endP && *p != ' ' ) { if ( *p == '\"' ) { ++p; while ( p < endP && *p != '\"' ) ++p; continue; } ++p; } const QChar *valueEnd = p - 1; // skip leading and trailing quotes if ( *start == '\"' && start != valueEnd && *valueEnd == '\"' ) ++start; else ++valueEnd; StringPtr value = StringPtr( start, valueEnd - start ); attributes[ key ] = value; 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. } /* qDebug( "tagName: %s", tag.constString().string().ascii() ); qDebug( "attribute:" ); AttributeMap::ConstIterator it = attributes.begin(); for (; it != attributes.end(); ++it ) qDebug( "%s -> %s", it.key().constString().string().ascii(), it.data().constString().string().ascii() ); */ } ItemProperties::ItemProperties( const ItemProperties &other, const Token &token ) : attributes( token.attributes ) { // inherit font = other.font; color = other.color; bgColor = other.bgColor; 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 == "font" ) { StringPtr colAttr = attributes[ "color" ]; if ( !colAttr.isNull() ) { QColor col( CONSTSTRING( colAttr ) ); if ( col.isValid() ) color = col; } colAttr = attributes[ "bgcolor" ]; if ( !colAttr.isNull() ) { QColor col( CONSTSTRING( colAttr ) ); if ( col.isValid() ) bgColor = col; } } else if ( token.value == "a" ) { color = Qt::blue; // ### font.setUnderline( true ); } } TextLine::TextLine() : m_maxHeight( 0 ) { m_items.setAutoDelete( true ); } TextLine::TextLine( const QPtrList &items ) : m_maxHeight( 0 ) { m_items.setAutoDelete( true ); assert( !items.autoDelete() ); QPtrListIterator it( items ); for (; it.current(); ++it ) appendItem( it.current(), UpdateMaxHeight ); } QString TextLine::updateSelection( const SelectionPoint &start, const SelectionPoint &end ) { QString selectedText; if ( start.line == this ) { 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 ) { int oldCurrent = m_items.at(); int idx = m_items.findRef( end.item ); assert( idx != -1 ); lastItem = m_items.next(); m_items.at( oldCurrent ); } for (; i != lastItem; 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().qString(); } } 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 ); if ( layoutUpdatePolicy == UpdateMaxHeight ) m_maxHeight = kMax( m_maxHeight, i->height() ); } void TextLine::resetLayout( QPtrList &remainingItems ) { Item *it = m_items.first(); while ( it ) { if ( it->resetLayout() == Item::KeepItem ) { remainingItems.append( m_items.take() ); it = m_items.current(); } else it = m_items.next(); } //m_items.clear(); } void TextLine::paint( QPainter *p, int y ) { QPtrListIterator it( m_items ); int x = 0; for (; it.current(); ++it ) { it.current()->paint( p, x, y ); x += it.current()->width(); } } Item *TextLine::itemAt( int px, SelectionPoint *selectionInfo, Item::SelectionAccuracy accuracy ) { QPtrListIterator 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() ) { Item *i = m_items.getLast(); selectionInfo->pos.setX( x - width ); selectionInfo->offset = i->calcSelectionOffset( width - 1 ); selectionInfo->item = i; selectionInfo->line = this; } return 0; } TextParag::TextParag( KSTextView *textView, const QString &richText ) : m_layouted( false ), m_height( 0 ), m_minWidth( 0 ), m_textView( textView ) { m_lines.setAutoDelete( true ); m_processedRichText = Tokenizer::preprocess( richText ); Tokenizer tokenizer( m_processedRichText ); Token tok; Token lastTextToken; QValueStack tagStack; TextLine *line = new TextLine; while ( tokenizer.parseNextToken( tok ) ) { if ( tok.id == Token::TagOpen ) { ItemProperties oldProps; if ( !tagStack.isEmpty() ) oldProps = tagStack.top().props; // ...bleh... -> 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 ); 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(); assert( tok.value == tag.name ); // ...foo... -> 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 ); } TextParag::~TextParag() { } void TextParag::layout( int width ) { QPtrList items; TextLine *row = m_lines.first(); for (; row; row = m_lines.next() ) row->resetLayout( items ); m_lines.clear(); //qDebug( "layout( %i )", width ); // all text chunks are now in a flat list. break them into // pieces of lists of chunks, so they fit with the give width m_height = 0; m_minWidth = 0; int remainingWidth = width; QPtrListIterator it( items ); while ( it.current() ) { m_minWidth = kMax( m_minWidth, it.current()->minWidth() ); Item *item = it.current(); int itemWidth = item->width(); ++it; if ( remainingWidth >= itemWidth ) { remainingWidth -= itemWidth; continue; } Item *newChunk = item->breakLine( remainingWidth ); TextLine *line = new TextLine; Item *next = it.current(); items.first(); while ( items.current() != next ) line->appendItem( items.take(), 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 ); } m_layouted = true; } void TextParag::paint( QPainter *p, int y ) { TextLine *row = m_lines.first(); for (; row; row = m_lines.next() ) { row->paint( p, y ); y += row->maxHeight(); } } 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 ); selectedText += '\n'; } 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(); } KSTextView::KSTextView( QWidget *parent, const char *name ) : QScrollView( parent, name, WRepaintNoErase ), m_paintBuffer( PaintBufferExtend, PaintBufferExtend ), - m_lastWidth( 0 ), m_selectionEndBeforeStart( false ) + m_selectionEndBeforeStart( false ) { m_parags.setAutoDelete( true ); viewport()->setBackgroundMode( PaletteBase ); viewport()->setMouseTracking( true ); m_autoScrollTimer = new QTimer( this ); } KSTextView::~KSTextView() { } void KSTextView::drawContents( QPainter *p, int cx, int cy, int , int ch ) { if ( m_parags.isEmpty() ) return; if ( m_paintBuffer.width() != visibleWidth() ) m_paintBuffer.resize( visibleWidth(), PaintBufferExtend ); QPtrListIterator it( m_parags ); int y = 0; while ( y < cy && it.current() ) { y += it.current()->height(); ++it; } if ( y > cy && !it.atFirst() ) { if ( it.current() ) --it; else it.toLast(); y -= it.current()->height(); } int yEnd = cy + ch; while ( y < yEnd ) { m_paintBuffer.fill( colorGroup().base() ); QPainter painter( &m_paintBuffer ); painter.translate( -cx, -y ); int nextY = y + PaintBufferExtend; if ( it.current() ) { int tmpY = y; while ( tmpY < nextY && it.current() ) { it.current()->paint( &painter, tmpY ); tmpY += it.current()->height(); ++it; } if ( tmpY > nextY && !it.atFirst() ) { if ( it.current() ) --it; else it.toLast(); int adjustedY = tmpY - it.current()->height(); if ( adjustedY != y ) nextY = adjustedY; } } painter.end(); p->drawPixmap( cx, y, m_paintBuffer ); y = nextY; } } -void KSTextView::viewportResizeEvent( QResizeEvent * ) +void KSTextView::viewportResizeEvent( QResizeEvent *ev ) { - if ( visibleWidth() != m_lastWidth ) - { + if ( ev->size().width() != ev->oldSize().width() ) layout(); - m_lastWidth = visibleWidth(); - } } void KSTextView::clear() { stopAutoScroll(); clearSelection(); m_parags.clear(); layout(); } void KSTextView::appendParag( const QString &richText ) { TextParag *parag = new TextParag( this, richText ); m_parags.append( parag ); layout( false ); } void KSTextView::clearSelection( bool repaint ) { m_selectionStart = SelectionPoint(); m_selectionEnd = SelectionPoint(); m_selectionEndBeforeStart = false; m_selectedText = QString::null; clearSelectionInternal(); if ( repaint ) updateContents(); } void KSTextView::copy() { QApplication::clipboard()->setText( m_selectedText ); } void KSTextView::clearSelectionInternal() { m_selectionEndBeforeStart = false; TextParag *p = m_parags.first(); for (; p; p = m_parags.next() ) p->clearSelection(); } void KSTextView::viewportMousePressEvent( QMouseEvent *ev ) { if ( ev->button() & LeftButton ) { clearSelection( true ); SelectionPoint p; itemAt( viewportToContents( ev->pos() ), &p, Item::SelectFuzzy ); if ( p.item ) { m_selectionStart = p; m_selectionEnd = p; p.item->setSelectionStatus( Item::NoSelection ); } } } void KSTextView::viewportMouseMoveEvent( QMouseEvent *ev ) { SelectionPoint p; Item *i = itemAt( viewportToContents( ev->pos() ), &p, Item::SelectFuzzy ); if ( !i && !p.item ) return; if ( ev->state() & LeftButton && m_selectionStart.item && p.item ) { m_selectionEnd = p; clearSelectionInternal(); updateSelectionOrder(); SelectionPoint start = m_selectionStart; SelectionPoint end = m_selectionEnd; if ( m_selectionEndBeforeStart ) { if ( start.item == end.item ) 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( 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 KSTextView::viewportMouseReleaseEvent( QMouseEvent *ev ) { stopAutoScroll(); if ( !m_selectedText.isEmpty() ) { QClipboard *cb = QApplication::clipboard(); if ( cb->supportsSelection() ) { cb->setSelectionMode( true ); cb->setText( m_selectedText ); cb->setSelectionMode( false ); } } Item *i = itemAt( viewportToContents( ev->pos() ) ); if ( i ) { TextChunk *text = dynamic_cast( i ); if ( text ) { StringPtr href = text->props().attributes[ "href" ]; if ( !href.isNull() ) { emitLinkClicked( CONSTSTRING( href ) ); return; } } } } void KSTextView::emitLinkClicked( const QString & ) { } void KSTextView::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 KSTextView::startAutoScroll() { connect( m_autoScrollTimer, SIGNAL( timeout() ), this, SLOT( autoScroll() ) ); m_autoScrollTimer->start( 100, false ); } void KSTextView::stopAutoScroll() { disconnect( m_autoScrollTimer, SIGNAL( timeout() ), this, SLOT( autoScroll() ) ); m_autoScrollTimer->stop(); } void KSTextView::selectionOffsets( int &startOffset, int &endOffset ) { assert( m_selectionStart.item ); if ( m_selectionEndBeforeStart ) { startOffset = m_selectionEnd.offset; endOffset = m_selectionStart.offset; return; } startOffset = m_selectionStart.offset; endOffset = m_selectionEnd.offset; } void KSTextView::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; } QString KSTextView::updateSelection( const SelectionPoint &start, const SelectionPoint &end ) { QString selectedText; if ( start.item == end.item ) { Item *i = start.item; if ( start.offset == end.offset ) i->setSelectionStatus( Item::NoSelection ); else { i->setSelectionStatus( Item::SelectionBoth ); // ### ugly TextChunk *t = dynamic_cast( i ); if ( t ) { StringPtr text = t->text(); selectedText = QString( text.ptr + start.offset, end.offset - start.offset + 1 ); } } } else { assert( m_parags.findRef( end.parag ) != -1 ); 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 KSTextView::layout( bool force ) { int height = 0; int contentsWidth = visibleWidth(); int width = contentsWidth; if ( force ) clearSelection(); // ### QPtrListIterator 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() ); } resizeContents( contentsWidth, height ); } Item *KSTextView::itemAt( const QPoint &pos, SelectionPoint *selectionInfo, Item::SelectionAccuracy accuracy ) { int px = pos.x(); int py = pos.y(); int y = 0; int height = 0; QPtrListIterator 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; 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; } return 0; } #include "kstextview.moc" /* * vim: et sw=4 */ Index: trunk/kdenetwork/ksirc/kstextview.h =================================================================== --- trunk/kdenetwork/ksirc/kstextview.h (revision 123091) +++ trunk/kdenetwork/ksirc/kstextview.h (revision 123092) @@ -1,413 +1,412 @@ #ifndef __kstextview_h__ #define __kstextview_h__ #include #include #include #include #include #include class QTimer; struct StringPtr { StringPtr() : ptr( 0 ), len( 0 ) {} StringPtr( const QChar *_ptr, uint _len ) : ptr( _ptr ), len( _len ) {} explicit StringPtr( const QString &s ) // use with care! : ptr( s.unicode() ), len( s.length() ) {} inline bool isNull() const { return ptr == 0; } // makes deep copy inline QString qString() const { return ( ptr && len > 0 ) ? QString( ptr, len ) : QString::null; } const QChar *ptr; uint len; }; #define CONSTSTRING( substr ) QConstString( substr.ptr, substr.len ).string() inline bool operator<( const StringPtr &s1, const StringPtr &s2 ) { return CONSTSTRING( s1 ) < CONSTSTRING( s2 ); } inline bool operator==( const StringPtr &s1, const StringPtr &s2 ) { return CONSTSTRING( s1 ) == CONSTSTRING( s2 ); } inline bool operator==( const StringPtr &s1, const char *s2 ) { return CONSTSTRING( s1 ) == s2; } class AttributeMap : public QMap { public: AttributeMap() {} AttributeMap( const AttributeMap &rhs ) : QMap( rhs ) {} AttributeMap &operator=( const AttributeMap &rhs ) { QMap::operator=( rhs ); return *this; } // helper for 'const char *' key... ConstIterator findAttribute( const char *key ) const { QString qkey( key ); return find( StringPtr( qkey ) ); } Iterator findAttribute( const char *key ) { QString qkey( key ); return find( StringPtr( qkey ) ); } StringPtr operator[]( const char *key ) const { ConstIterator it = findAttribute( key ); if ( it == end() ) return StringPtr(); return it.data(); } StringPtr &operator[]( const StringPtr &key ) { return QMap::operator[]( key ); } }; struct Token { Token() : id( -1 ) {} enum Id { TagOpen, Text, TagClose }; int id; StringPtr value; AttributeMap attributes; }; struct ItemProperties { ItemProperties() {} ItemProperties( const ItemProperties &other, const Token &token ); // these three are inherited/merged QFont font; QColor color; QColor bgColor; // ### todo: inherit these, too AttributeMap attributes; }; class TextParag; class Item { public: enum LayoutResetStatus { DeleteItem, KeepItem }; enum SelectionStatus { SelectionStart, InSelection, SelectionEnd, SelectionBoth, NoSelection }; enum SelectionAccuracy { SelectExact, SelectFuzzy }; Item( TextParag *parag ); virtual ~Item(); virtual void paint( QPainter *painter, int x, int y ) = 0; int width() const; int minWidth() const; int height() const; virtual Item *breakLine( int width ); virtual LayoutResetStatus resetLayout() = 0; virtual int calcSelectionOffset( int x ); void setSelectionStatus( SelectionStatus status ) { m_selection = status; } void selectionOffsets( int &startOffset, int &endOffset ); // ### virtual StringPtr text() const; static Item *create( TextParag *parag, const Token &tok, const ItemProperties &props = ItemProperties() ); protected: mutable bool m_extendsDirty; mutable int m_minWidth; mutable int m_width; mutable int m_height; virtual void calcExtends() const = 0; SelectionStatus m_selection; TextParag *m_parag; }; class TextChunk : public Item { public: TextChunk( TextParag *parag, const StringPtr &text, const ItemProperties &props ); virtual void paint( QPainter *painter, int x, int y ); virtual Item *breakLine( int width ); virtual LayoutResetStatus resetLayout(); virtual int calcSelectionOffset( int x ); virtual StringPtr text() const; ItemProperties &props() { return m_props; } protected: virtual void calcExtends() const; private: int paintSelection( QPainter *p, int x, int y, int yTextPos, const StringPtr &text ); int paintText( QPainter *p, int x, int y, int yTextPos, const StringPtr &text ); StringPtr m_text; uint m_originalTextLength; ItemProperties m_props; QFontMetrics m_metrics; }; class ImageItem : public Item { public: ImageItem( TextParag *parag, const QPixmap &pixmap ); virtual void paint( QPainter *painter, int x, int y ); virtual LayoutResetStatus resetLayout(); protected: virtual void calcExtends() const; private: QPixmap m_pixmap; }; class Tokenizer { public: struct TagIndex { enum Type { Open, Close }; TagIndex() : index( 0 ), type( -1 ) {} TagIndex( int _index, int _type ) : index( _index ), type( _type ) {} uint index; int type; }; typedef QValueList TagIndexList; // preprocessed string struct PString { QString data; TagIndexList tags; }; Tokenizer( PString &text ); static PString preprocess( const QString &richText ); bool parseNextToken( Token &tok ); private: void parseTag( const StringPtr &text, StringPtr &tag, AttributeMap &attributes ); static TagIndexList scanTagIndices( const QString &text ); static void resolveEntities( QString &text, TagIndexList &tags ); enum TagParsingState { ScanForName, ScanForEqual, ScanForValue }; QString &m_text; TagIndexList m_tags; TagIndexList::ConstIterator m_lastTag; bool m_textBeforeFirstTagProcessed; bool m_done; Tokenizer( const Tokenizer & ); Tokenizer &operator=( const Tokenizer & ); }; class SelectionPoint; class TextLine { public: enum LayoutPolicy { NoUpdate, UpdateMaxHeight }; TextLine(); // tranfers ownership of items! make sure that 'items' does not // have autodeletion enabled! TextLine( const QPtrList &items ); int maxHeight() const { return m_maxHeight; } QString updateSelection( const SelectionPoint &start, const SelectionPoint &end ); void clearSelection(); // transfers ownership void appendItem( Item *i, int layoutUpdatePolicy = NoUpdate ); bool isEmpty() const { return m_items.isEmpty(); } void resetLayout( QPtrList &remainingItems ); void paint( QPainter *p, int y ); Item *itemAt( int px, SelectionPoint *selectionInfo, Item::SelectionAccuracy accuracy = Item::SelectExact ); private: QPtrList m_items; int m_maxHeight; }; class KSTextView; class SelectionPoint; class TextParag { public: TextParag( KSTextView *textView, const QString &richText ); ~TextParag(); void layout( int width ); void paint( QPainter *p, int y ); inline void setLayouted( bool l ) { m_layouted = l; } inline bool isLayouted() const { return m_layouted; } inline int minWidth() const { return m_minWidth; } inline int height() const { return m_height; } Item *itemAt( int px, int py, SelectionPoint *selectionInfo, Item::SelectionAccuracy accuracy = Item::SelectExact ); KSTextView *textView() const { return m_textView; } QString updateSelection( const SelectionPoint &start, const SelectionPoint &end ); void clearSelection(); private: Tokenizer::PString m_processedRichText; QPtrList m_lines; bool m_layouted; int m_height; int m_minWidth; KSTextView *m_textView; struct Tag { Tag() {} Tag( const StringPtr &_name, const ItemProperties &_props ) : name( _name ), props( _props ) {} StringPtr name; ItemProperties props; }; TextParag( const TextParag & ); TextParag &operator=( const TextParag & ); }; struct SelectionPoint { SelectionPoint() : item( 0 ), line( 0 ), parag( 0 ), offset( 0 ) {} Item *item; TextLine *line; TextParag *parag; int offset; QPoint pos; }; class KSTextView : public QScrollView { Q_OBJECT friend class Item; public: KSTextView( QWidget *parent, const char *name = 0 ); virtual ~KSTextView(); virtual void clear(); void appendParag( const QString &richText ); void clearSelection( bool repaint = false ); // ### re-consider the repaint arg... QString selectedText() const { return m_selectedText; } signals: void selectionChanged(); public slots: void copy(); protected: - virtual void viewportResizeEvent( QResizeEvent * ); + virtual void viewportResizeEvent( QResizeEvent *ev ); virtual void drawContents( QPainter *p, int cx, int cy, int cw, int ch ); virtual void viewportMousePressEvent( QMouseEvent *ev ); virtual void viewportMouseMoveEvent( QMouseEvent *ev ); virtual void viewportMouseReleaseEvent( QMouseEvent *ev ); virtual void emitLinkClicked( const QString &url ); private slots: void autoScroll(); private: void startAutoScroll(); void stopAutoScroll(); void selectionOffsets( int &startOffset, int &endOffset ); void updateSelectionOrder(); QString updateSelection( const SelectionPoint &start, const SelectionPoint &end ); void layout( bool force = true ); Item *itemAt( const QPoint &pos, SelectionPoint *selectionInfo = 0, Item::SelectionAccuracy accuracy = Item::SelectExact ); void clearSelectionInternal(); QPtrList m_parags; QPixmap m_paintBuffer; - int m_lastWidth; SelectionPoint m_selectionStart; SelectionPoint m_selectionEnd; bool m_selectionEndBeforeStart; QTimer *m_autoScrollTimer; QString m_selectedText; }; #endif /* * vim: et sw=4 */