• Skip to content
  • Skip to link menu
KDE 4.2 API Reference
  • KDE API Reference
  • kdenetwork
  • Sitemap
  • Contact Us
 

kopete/kopete

chatmessagepart.cpp

Go to the documentation of this file.
00001 /*
00002     chatmessagepart.cpp - Chat Message KPart
00003 
00004     Copyright (c) 2002-2005 by Olivier Goffart       <ogoffart@kde.org>
00005     Copyright (c) 2002-2003 by Martijn Klingens      <klingens@kde.org>
00006     Copyright (c) 2004      by Richard Smith         <kde@metafoo.co.uk>
00007     Copyright (c) 2005-2006 by Michaƫl Larouche     <larouche@kde.org>
00008     Copyright (c) 2008      by Roman Jarosz          <kedgedev@centrum.cz>
00009 
00010     Kopete    (c) 2002-2008 by the Kopete developers <kopete-devel@kde.org>
00011 
00012     *************************************************************************
00013     *                                                                       *
00014     * This program is free software; you can redistribute it and/or modify  *
00015     * it under the terms of the GNU General Public License as published by  *
00016     * the Free Software Foundation; either version 2 of the License, or     *
00017     * (at your option) any later version.                                   *
00018     *                                                                       *
00019     *************************************************************************
00020 */
00021 
00022 #include "chatmessagepart.h"
00023 
00024 // STYLE_TIMETEST is for time staticstic gathering.
00025 //#define STYLE_TIMETEST
00026 
00027 #include <ctime>
00028 
00029 // Qt includes
00030 #include <QtCore/QByteArray>
00031 #include <QtCore/QLatin1String>
00032 #include <QtCore/QList>
00033 #include <QtCore/QPointer>
00034 #include <QtCore/QRect>
00035 #include <QtCore/QRegExp>
00036 #include <QtCore/QTextCodec>
00037 #include <QtCore/QTextStream>
00038 #include <QtCore/QTimer>
00039 #include <QtCore/QBuffer>
00040 #include <QtGui/QClipboard>
00041 #include <QtGui/QCursor>
00042 #include <QtGui/QPixmap>
00043 #include <QtGui/QTextDocument>
00044 #include <QtGui/QScrollBar>
00045 #include <QMimeData>
00046 #include <QApplication>
00047 
00048 // KHTML::DOM includes
00049 #include <dom/dom_doc.h>
00050 #include <dom/dom_text.h>
00051 #include <dom/dom_element.h>
00052 #include <dom/html_base.h>
00053 #include <dom/html_document.h>
00054 #include <dom/html_inline.h>
00055 #include <dom/html_form.h>
00056 #include <dom/dom2_events.h>
00057 
00058 
00059 // KDE includes
00060 #include <kactioncollection.h>
00061 #include <kdebug.h>
00062 #include <kdeversion.h>
00063 #include <kfiledialog.h>
00064 #include <khtmlview.h>
00065 #include <klocale.h>
00066 #include <kmessagebox.h>
00067 #include <kmenu.h>
00068 #include <krun.h>
00069 #include <kstringhandler.h>
00070 #include <ktemporaryfile.h>
00071 #include <kio/copyjob.h>
00072 #include <kstandardaction.h>
00073 #include <kiconloader.h>
00074 #include <kcodecs.h>
00075 #include <kicon.h>
00076 
00077 // Kopete includes
00078 #include "kopetecontact.h"
00079 #include "kopetecontactlist.h"
00080 #include "kopetechatwindow.h"
00081 #include "kopetechatsession.h"
00082 #include "kopetemetacontact.h"
00083 #include "kopetepluginmanager.h"
00084 #include "kopeteprotocol.h"
00085 #include "kopeteaccount.h"
00086 #include "kopeteglobal.h"
00087 #include "kopeteemoticons.h"
00088 #include "kopeteview.h"
00089 #include "kopetepicture.h"
00090 #include "kopeteappearancesettings.h"
00091 #include "kopetebehaviorsettings.h"
00092 #include "kopetechatwindowsettings.h"
00093 #include "kopetetransfermanager.h"
00094 
00095 #include "kopetechatwindowstyle.h"
00096 #include "kopetechatwindowstylemanager.h"
00097 
00098 class ToolTip;
00099 
00100 class ChatMessagePart::Private
00101 {
00102 public:
00103     Private()
00104      : /*tt(0L),*/ scrollPressed(false), manager(0),
00105        copyAction(0), saveAction(0), printAction(0),
00106        closeAction(0),copyURLAction(0), currentChatStyle(0),
00107        latestDirection(Kopete::Message::Inbound), latestType(Kopete::Message::TypeNormal),
00108        htmlEventListener(0)
00109     {}
00110 
00111     ~Private()
00112     {
00113         // Don't delete manager and latestContact, because they could be still used.
00114         // Don't delete currentChatStyle, it is handled by ChatWindowStyleManager.
00115     }
00116 
00117     bool bgOverride;
00118     bool fgOverride;
00119     bool rtfOverride;
00120 
00121 //  ToolTip *tt;
00122     bool scrollPressed;
00123     Kopete::ChatSession *manager;
00124 
00125     DOM::HTMLElement activeElement;
00126 
00127     KAction *copyAction;
00128     KAction *saveAction;
00129     KAction *printAction;
00130     KAction *closeAction;
00131     KAction *copyURLAction;
00132 
00133     // We can't use QPointer because ChatWindowStyle is not a QObject
00134     ChatWindowStyle *currentChatStyle;
00135     QPointer<Kopete::Contact> latestContact;
00136     Kopete::Message::MessageDirection latestDirection;
00137     Kopete::Message::MessageType latestType;
00138     // Yep I know it will take memory, but I don't have choice
00139     // to enable on-the-fly style changing.
00140     QList<Kopete::Message> allMessages;
00141     
00142     // No need to delete, HTMLEventListener is ref counted.
00143     QPointer<HTMLEventListener> htmlEventListener;
00144 };
00145 /*
00146 class ChatMessagePart::ToolTip : public Q3ToolTip
00147 {
00148 public:
00149     ToolTip( ChatMessagePart *c ) : Q3ToolTip( c->view()->viewport() )
00150     {
00151         m_chat = c;
00152     }
00153 
00154     void maybeTip( const QPoint &p )
00155     {
00156         // FIXME: it's wrong to look for the node under the mouse - this makes too many
00157         //        assumptions about how tooltips work. but there is no nodeAtPoint.
00158         DOM::Node node = m_chat->nodeUnderMouse();
00159         Kopete::Contact *contact = m_chat->contactFromNode( node );
00160         QString toolTipText;
00161 
00162         if(node.isNull())
00163             return;
00164 
00165         // this tooltip is attached to the viewport widget, so translate the node's rect
00166         // into its coordinates.
00167         QRect rect = node.getRect();
00168         rect = QRect( m_chat->view()->contentsToViewport( rect.topLeft() ),
00169                   m_chat->view()->contentsToViewport( rect.bottomRight() ) );
00170 
00171         if( contact )
00172         {
00173             toolTipText = contact->toolTip();
00174         }
00175         else
00176         {
00177             m_chat->emitTooltipEvent( m_chat->textUnderMouse(), toolTipText );
00178 
00179             if( toolTipText.isEmpty() )
00180             {
00181                 //Fall back to the title attribute
00182                 for( DOM::HTMLElement element = node; !element.isNull(); element = element.parentNode() )
00183                 {
00184                     if( element.hasAttribute( "title" ) )
00185                     {
00186                         toolTipText = element.getAttribute( "title" ).string();
00187                         break;
00188                     }
00189                 }
00190             }
00191         }
00192 
00193         if( !toolTipText.isEmpty() )
00194             tip( rect, toolTipText );
00195     }
00196 
00197 private:
00198     ChatMessagePart *m_chat;
00199 };
00200 */
00201 
00202 ChatMessagePart::ChatMessagePart( Kopete::ChatSession *mgr, QWidget *parent )
00203     : KHTMLPart( parent ), d( new Private )
00204 {
00205     d->manager = mgr;
00206 
00207     d->currentChatStyle = ChatWindowStyleManager::self()->getStyleFromPool(
00208              KopeteChatWindowSettings::self()->styleName() );
00209 
00210     kDebug(14000) << d->currentChatStyle->getStyleName();
00211 
00212     //Security settings, we don't need this stuff
00213     setJScriptEnabled( false ) ;
00214     setJavaEnabled( false );
00215     setPluginsEnabled( false );
00216     setMetaRefreshEnabled( false );
00217     setOnlyLocalReferences( true );
00218 
00219     // Write the template to KHTMLPart
00220     writeTemplate();
00221 
00222     // It is not possible to drag and drop on our widget
00223     view()->setAcceptDrops(false);
00224 
00225     connect( Kopete::AppearanceSettings::self(), SIGNAL(messageOverridesChanged()),
00226              this, SLOT( slotAppearanceChanged() ) );
00227     connect( KopeteChatWindowSettings::self(), SIGNAL(chatwindowAppearanceChanged()),
00228              this, SLOT( slotRefreshView() ) );
00229     connect( KopeteChatWindowSettings::self(), SIGNAL(styleChanged(const QString &)),
00230              this, SLOT( setStyle(const QString &) ) );
00231     connect( KopeteChatWindowSettings::self(), SIGNAL(styleVariantChanged(const QString &)),
00232              this, SLOT( setStyleVariant(const QString &) ) );
00233 
00234     // Refresh the style if the display name change.
00235     connect( d->manager, SIGNAL(displayNameChanged()), this, SLOT(slotUpdateHeaderDisplayName()) );
00236     connect( d->manager, SIGNAL(photoChanged()), this, SLOT(slotUpdateHeaderPhoto()) );
00237 
00238     connect( d->manager, SIGNAL(messageStateChanged(uint, Kopete::Message::MessageState)),
00239              this, SLOT(messageStateChanged(uint, Kopete::Message::MessageState)) );
00240 
00241     connect ( browserExtension(), SIGNAL( openUrlRequestDelayed( const KUrl &, const KParts::OpenUrlArguments &, const KParts::BrowserArguments & ) ),
00242               this, SLOT( slotOpenURLRequest( const KUrl &, const KParts::OpenUrlArguments &, const KParts::BrowserArguments & ) ) );
00243 
00244     connect( this, SIGNAL(popupMenu(const QString &, const QPoint &)),
00245              this, SLOT(slotRightClick(const QString &, const QPoint &)) );
00246     connect( view()->verticalScrollBar(), SIGNAL(sliderMoved(int)),
00247              this, SLOT(slotScrollingTo(int)) );
00248 
00249     connect( Kopete::TransferManager::transferManager(), SIGNAL(askIncomingDone(unsigned int)),
00250              this, SLOT(slotFileTransferIncomingDone(unsigned int)) );
00251     
00252     //initActions
00253     d->copyAction = KStandardAction::copy( this, SLOT(copy()), actionCollection() );
00254     d->saveAction = KStandardAction::saveAs( this, SLOT(save()), actionCollection() );
00255     d->printAction = KStandardAction::print( this, SLOT(print()),actionCollection() );
00256     d->closeAction = KStandardAction::close( this, SLOT(slotCloseView()),actionCollection() );
00257     d->copyURLAction = new KAction( KIcon("edit-copy"), i18n( "Copy Link Address" ), actionCollection() );
00258         actionCollection()->addAction( "editcopy", d->copyURLAction );
00259     connect( d->copyURLAction, SIGNAL( triggered(bool) ), this, SLOT( slotCopyURL() ) );
00260 
00261     // read formatting override flags
00262     readOverrides();
00263 }
00264 
00265 ChatMessagePart::~ChatMessagePart()
00266 {
00267     kDebug(14000) ;
00268 
00269     // Cancel all pending file transfer requests
00270     QList<Kopete::Message>::ConstIterator it, itEnd = d->allMessages.constEnd();
00271     for ( it = d->allMessages.constBegin(); it != itEnd; ++it )
00272     {
00273         if ( (*it).type() == Kopete::Message::TypeFileTransferRequest && !(*it).fileTransferDisabled() )
00274         {
00275             Kopete::TransferManager::transferManager()->cancelIncomingTransfer( (*it).id() );
00276         }
00277     }
00278 
00279     //delete d->tt;
00280     delete d;
00281 }
00282 
00283 void ChatMessagePart::slotScrollingTo( int y )
00284 {
00285     int scrolledTo = y + view()->visibleHeight();
00286     d->scrollPressed = scrolledTo < ( view()->contentsHeight() - 10 );
00287 }
00288 
00289 void ChatMessagePart::save()
00290 {
00291     const KUrl dummyUrl;
00292     KFileDialog dlg( dummyUrl, QLatin1String( "text/html text/plain" ), view() );
00293     dlg.setCaption( i18n( "Save Conversation" ) );
00294     dlg.setOperationMode( KFileDialog::Saving );
00295 
00296     if ( dlg.exec() != QDialog::Accepted )
00297         return;
00298 
00299     KUrl saveURL = dlg.selectedUrl();
00300     KTemporaryFile *tempFile = new KTemporaryFile();
00301     tempFile->setAutoRemove(false);
00302     tempFile->open();
00303 
00304     QTextStream stream ( tempFile );
00305     stream.setCodec(QTextCodec::codecForName("UTF-8"));
00306 
00307     if ( dlg.currentFilter() == QLatin1String( "text/plain" ) )
00308     {
00309         QList<Kopete::Message>::ConstIterator it, itEnd = d->allMessages.constEnd();
00310         for(it = d->allMessages.constBegin(); it != itEnd; ++it)
00311         {
00312             Kopete::Message tempMessage = *it;
00313             stream << "[" << KGlobal::locale()->formatDateTime(tempMessage.timestamp()) << "] ";
00314             if( tempMessage.from() && tempMessage.from()->metaContact() )
00315             {
00316                 stream << formatName(tempMessage.from()->metaContact()->displayName(), Qt::RichText);
00317             }
00318             stream << ": " << tempMessage.plainBody() << "\n";
00319         }
00320     }
00321     else
00322     {
00323         stream << htmlDocument().toString().string() << '\n';
00324     }
00325 
00326     stream.flush();
00327     QString fileName = tempFile->fileName();
00328     delete tempFile;
00329 
00330     KIO::CopyJob *moveJob = KIO::move( KUrl( fileName ), saveURL, KIO::HideProgressInfo );
00331 
00332     if ( !moveJob )
00333     {
00334         KMessageBox::queuedMessageBox( view(), KMessageBox::Error,
00335                 i18n("<qt>Could not open <b>%1</b> for writing.</qt>", saveURL.prettyUrl() ), // Message
00336                 i18n("Error While Saving") ); //Caption
00337     }
00338 }
00339 
00340 void ChatMessagePart::pageUp()
00341 {
00342     view()->scrollBy( 0, -view()->visibleHeight() );
00343 }
00344 
00345 void ChatMessagePart::pageDown()
00346 {
00347     view()->scrollBy( 0, view()->visibleHeight() );
00348 }
00349 
00350 void ChatMessagePart::slotOpenURLRequest(const KUrl &url, const KParts::OpenUrlArguments &, const KParts::BrowserArguments &)
00351 {
00352     kDebug(14000) << "url=" << url.url();
00353     if ( url.protocol() == QLatin1String("kopetemessage") )
00354     {
00355         Kopete::Contact *contact = d->manager->account()->contacts()[ url.host() ];
00356         if ( contact )
00357             contact->execute();
00358     }
00359     else
00360     {
00361         KRun *runner = new KRun( url, 0, 0, false ); // false = non-local files
00362         runner->setRunExecutables( false ); //security
00363         //KRun autodeletes itself by default when finished.
00364     }
00365 }
00366 
00367 void ChatMessagePart::slotFileTransferIncomingDone( unsigned int id )
00368 {
00369     QList<Kopete::Message>::Iterator it = d->allMessages.end();
00370     while ( it != d->allMessages.begin() )
00371     {
00372         --it;
00373         if ( (*it).id() == id )
00374         {
00375             (*it).setFileTransferDisabled( true );
00376             disableFileTransferButtons( id );
00377             break;
00378         }
00379     }
00380 }
00381 
00382 void ChatMessagePart::readOverrides()
00383 {
00384     d->bgOverride = Kopete::AppearanceSettings::self()->chatBgOverride();
00385     d->fgOverride = Kopete::AppearanceSettings::self()->chatFgOverride();
00386     d->rtfOverride = Kopete::AppearanceSettings::self()->chatRtfOverride();
00387 }
00388 
00389 void ChatMessagePart::setStyle( const QString &styleName )
00390 {
00391     // Create a new ChatWindowStyle
00392     d->currentChatStyle = ChatWindowStyleManager::self()->getStyleFromPool(styleName);
00393 
00394     // Do the actual style switch
00395     // Wait for the event loop before switching the style
00396     QTimer::singleShot( 0, this, SLOT(changeStyle()) );
00397 }
00398 
00399 void ChatMessagePart::setStyle( ChatWindowStyle *style )
00400 {
00401     // Change the current style
00402     d->currentChatStyle = style;
00403 
00404     // Do the actual style switch
00405     // Wait for the event loop before switching the style
00406     QTimer::singleShot( 0, this, SLOT(changeStyle()) );
00407 }
00408 
00409 void ChatMessagePart::setStyleVariant( const QString &variantPath )
00410 {
00411     DOM::HTMLElement variantNode = document().getElementById( QString("mainStyle") );
00412     if( !variantNode.isNull() )
00413         variantNode.setInnerText( QString("@import url(\"%1\");").arg( adjustStyleVariantForChatSession( variantPath) ) );
00414 }
00415 
00416 void ChatMessagePart::messageStateChanged( uint messageId, Kopete::Message::MessageState state )
00417 {
00418     QList<Kopete::Message>::Iterator it = d->allMessages.end();
00419     while ( it != d->allMessages.begin() )
00420     {
00421         --it;
00422         if ( (*it).id() == messageId )
00423         {
00424             (*it).setState( state );
00425             changeMessageStateElement( messageId, state );
00426             break;
00427         }
00428     }
00429 }
00430 
00431 void ChatMessagePart::slotAppearanceChanged()
00432 {
00433     readOverrides();
00434 
00435     changeStyle();
00436 }
00437 
00438 void ChatMessagePart::appendMessage( Kopete::Message &message, bool restoring )
00439 {
00440     message.setBackgroundOverride( d->bgOverride );
00441     message.setForegroundOverride( d->fgOverride );
00442     message.setRichTextOverride( d->rtfOverride );
00443 
00444     // parse emoticons and URL now.
00445     // Do not reparse emoticons on restoring, because it cause very intensive CPU usage on long chats.
00446     if( !restoring )
00447         message.setHtmlBody( message.parsedBody() );
00448 
00449 #ifdef STYLE_TIMETEST
00450     QTime beforeMessage = QTime::currentTime();
00451 #endif
00452 
00453     QString formattedMessageHtml;
00454     bool isConsecutiveMessage = false;
00455     int bufferLen = Kopete::BehaviorSettings::self()->chatWindowBufferViewSize();
00456 
00457     // Find the "Chat" div element.
00458     // If the "Chat" div element is not found, do nothing. It's the central part of Adium format.
00459     DOM::HTMLElement chatNode = htmlDocument().getElementById( "Chat" );
00460 
00461     if( chatNode.isNull() )
00462     {
00463         kDebug(14000) << "WARNING: Chat Node was null !";
00464         return;
00465     }
00466 
00467     // Check if it's a consecutive Message
00468     // Consecutive messages are only for normal messages, status messages do not have a <div id="insert" />
00469     // We check if the from() is the latestContact, because consecutive incoming/outgoing message can come from differents peopole(in groupchat and IRC)
00470     // Group only if the user want it.
00471     if( KopeteChatWindowSettings::self()->groupConsecutiveMessages() )
00472     {
00473         isConsecutiveMessage = (message.direction() == d->latestDirection && !d->latestContact.isNull()
00474                                 && d->latestContact == message.from() && message.type() == d->latestType
00475                                 && message.type() != Kopete::Message::TypeFileTransferRequest );
00476     }
00477 
00478     // Don't test it in the switch to don't break consecutive messages.
00479     if(message.type() == Kopete::Message::TypeAction)
00480     {
00481         // Check if chat style support Action template (Kopete extension)
00482         if( d->currentChatStyle->hasActionTemplate() )
00483         {
00484             switch(message.direction())
00485             {
00486                 case Kopete::Message::Inbound:
00487                     formattedMessageHtml = d->currentChatStyle->getActionIncomingHtml();
00488                     break;
00489                 case Kopete::Message::Outbound:
00490                     formattedMessageHtml = d->currentChatStyle->getActionOutgoingHtml();
00491                     break;
00492                 default:
00493                     break;
00494             }
00495         }
00496         // Use status template if no Action template.
00497         else
00498         {
00499             formattedMessageHtml = d->currentChatStyle->getStatusHtml();
00500         }
00501     }
00502     else if(message.type() == Kopete::Message::TypeFileTransferRequest)
00503     {
00504         formattedMessageHtml = d->currentChatStyle->getFileTransferIncomingHtml();
00505     }
00506     else
00507     {
00508         switch(message.direction())
00509         {
00510             case Kopete::Message::Inbound:
00511             {
00512                 if(isConsecutiveMessage)
00513                 {
00514                     formattedMessageHtml = d->currentChatStyle->getNextIncomingHtml();
00515                 }
00516                 else
00517                 {
00518                     formattedMessageHtml = d->currentChatStyle->getIncomingHtml();
00519                 }
00520                 break;
00521             }
00522             case Kopete::Message::Outbound:
00523             {
00524                 if(isConsecutiveMessage)
00525                 {
00526                     formattedMessageHtml = d->currentChatStyle->getNextOutgoingHtml();
00527                 }
00528                 else
00529                 {
00530                     formattedMessageHtml = d->currentChatStyle->getOutgoingHtml();
00531                 }
00532                 break;
00533             }
00534             case Kopete::Message::Internal:
00535             {
00536                 formattedMessageHtml = d->currentChatStyle->getStatusHtml();
00537                 break;
00538             }
00539         }
00540     }
00541 
00542     formattedMessageHtml = formatStyleKeywords( formattedMessageHtml, message );
00543 
00544     // newMessageNode is common to both code path
00545     // FIXME: Find a better than to create a dummy span.
00546     DOM::HTMLElement newMessageNode = document().createElement( QString("span") );
00547     newMessageNode.setInnerHTML( formattedMessageHtml );
00548 
00549     // Find the insert Node
00550     DOM::HTMLElement insertNode = document().getElementById( QString("insert") );
00551 
00552     if( isConsecutiveMessage && !insertNode.isNull() )
00553     {
00554         // Replace the insert block, because it's a consecutive message.
00555         insertNode.parentNode().replaceChild(newMessageNode, insertNode);
00556     }
00557     else
00558     {
00559         // Remove the insert block, because it's a new message.
00560         if( !insertNode.isNull() )
00561             insertNode.parentNode().removeChild(insertNode);
00562         // Append to the chat.
00563         chatNode.appendChild(newMessageNode);
00564     }
00565 
00566     if ( message.type() == Kopete::Message::TypeNormal ) 
00567     {
00568         if ( message.direction() == Kopete::Message::Outbound )
00569             changeMessageStateElement( message.id(), message.state() );
00570     }
00571     else if ( message.type() == Kopete::Message::TypeFileTransferRequest )
00572     {
00573         if ( message.fileTransferDisabled() )
00574             disableFileTransferButtons( message.id() );
00575         else
00576             addFileTransferButtonsEventListener( message.id() );
00577     }
00578 
00579     // Keep the direction to see on next message
00580     // if it's a consecutive message
00581     // Keep also the from() contact.
00582     d->latestDirection = message.direction();
00583     d->latestType = message.type();
00584     d->latestContact = const_cast<Kopete::Contact*>(message.from());
00585 
00586     // Add the message to the list for futher restoring if needed
00587     if(!restoring)
00588         d->allMessages.append(message);
00589 
00590     while ( bufferLen>0 && d->allMessages.count() >= bufferLen )
00591     {
00592         d->allMessages.pop_front();
00593 
00594         // FIXME: Find a way to make work Chat View Buffer efficiently with consecutives messages.
00595         // Before it was calling changeStyle() but it's damn too slow.
00596         if( !KopeteChatWindowSettings::self()->groupConsecutiveMessages() )
00597         {
00598             chatNode.removeChild( chatNode.firstChild() );
00599         }
00600     }
00601 
00602     if ( !d->scrollPressed )
00603         QTimer::singleShot( 1, this, SLOT( slotScrollView() ) );
00604 
00605 #ifdef STYLE_TIMETEST
00606     kDebug(14000) << "Message time: " << beforeMessage.msecsTo( QTime::currentTime());
00607 #endif
00608 }
00609 
00610 void ChatMessagePart::slotRefreshView()
00611 {
00612     DOM::HTMLElement kopeteNode = document().getElementById( QString("KopeteStyle") );
00613     if( !kopeteNode.isNull() )
00614         kopeteNode.setInnerText( styleHTML() );
00615 
00616     DOM::HTMLBodyElement bodyElement = htmlDocument().body();
00617     bodyElement.setBgColor( Kopete::AppearanceSettings::self()->chatBackgroundColor().name() );
00618 }
00619 
00620 void ChatMessagePart::keepScrolledDown()
00621 {
00622     if ( !d->scrollPressed )
00623         QTimer::singleShot( 1, this, SLOT( slotScrollView() ) );
00624 }
00625 
00626 const QString ChatMessagePart::styleHTML() const
00627 {
00628     Kopete::AppearanceSettings *settings = Kopete::AppearanceSettings::self();
00629 
00630     QString style = QString(
00631         "body{background-color:%1;font-family:%2;font-size:%3pt;color:%4}"
00632         "td{font-family:%5;font-size:%6pt;color:%7}"
00633         "input{font-family:%8;font-size:%9pt;color:%10}"
00634         "a{color:%11}a.visited{color:%12}"
00635         "a.KopeteDisplayName{text-decoration:none;color:inherit;}"
00636         "a.KopeteDisplayName:hover{text-decoration:underline;color:inherit}"
00637         ".KopeteLink{cursor:pointer;}.KopeteLink:hover{text-decoration:underline}"
00638         ".KopeteMessageBody > p:first-child{margin:0;padding:0;display:inline;}" /* some html messages are encapsuled into a <p> */ )
00639         .arg( settings->chatBackgroundColor().name() )
00640         .arg( settings->chatFont().family() )
00641         .arg( settings->chatFont().pointSize() )
00642         .arg( settings->chatTextColor().name() )
00643         .arg( settings->chatFont().family() )
00644         .arg( settings->chatFont().pointSize() )
00645         .arg( settings->chatTextColor().name() )
00646         .arg( settings->chatFont().family() )
00647         .arg( settings->chatFont().pointSize() )
00648         .arg( settings->chatTextColor().name() )
00649         .arg( settings->chatLinkColor().name() )
00650         .arg( settings->chatLinkColor().name() );
00651 
00652     return style;
00653 }
00654 
00655 void ChatMessagePart::clear()
00656 {
00657     // writeTemplate actually reset the HTML chat session from the beginning.
00658     writeTemplate();
00659 
00660     // Reset consecutive messages
00661     d->latestContact = 0;
00662 
00663     // Cancel all pending file transfer requests
00664     QList<Kopete::Message>::ConstIterator it, itEnd = d->allMessages.constEnd();
00665     for ( it = d->allMessages.constBegin(); it != itEnd; ++it )
00666     {
00667         if ( (*it).type() == Kopete::Message::TypeFileTransferRequest && !(*it).fileTransferDisabled() )
00668         {
00669             Kopete::TransferManager::transferManager()->cancelIncomingTransfer( (*it).id() );
00670         }
00671     }
00672 
00673     // Remove all stored messages.
00674     d->allMessages.clear();
00675 }
00676 
00677 Kopete::Contact *ChatMessagePart::contactFromNode( const DOM::Node &n ) const
00678 {
00679     DOM::Node node = n;
00680     int i;
00681     QList<Kopete::Contact*> m;
00682 
00683     if ( node.isNull() )
00684         return 0;
00685 
00686     while ( !node.isNull() && ( node.nodeType() == DOM::Node::TEXT_NODE || ((DOM::HTMLElement)node).className() != "KopeteDisplayName" ) )
00687         node = node.parentNode();
00688 
00689     DOM::HTMLElement element = node;
00690     if ( element.className() != "KopeteDisplayName" )
00691         return 0;
00692 
00693     m = d->manager->members();
00694     if ( element.hasAttribute( "contactid" ) )
00695     {
00696         QString contactId = element.getAttribute( "contactid" ).string();
00697         for ( i =0; i != m.size(); i++ )
00698             if ( m.at(i)->contactId() == contactId )
00699                 return m[i];
00700     }
00701     else
00702     {
00703         QString nick = element.innerText().string().trimmed();
00704         foreach ( Kopete::Contact *contact, m )
00705         {
00706             QString contactNick;
00707             if( contact->metaContact() && contact->metaContact() != Kopete::ContactList::self()->myself() )
00708                 contactNick = contact->metaContact()->displayName();
00709             else
00710                 contactNick = contact->nickName();
00711 
00712             if ( contactNick == nick )
00713                 return contact;
00714         }
00715     }
00716 
00717     return 0;
00718 }
00719 
00720 void ChatMessagePart::slotRightClick( const QString &, const QPoint &point )
00721 {
00722     // look through parents until we find an Element
00723     DOM::Node activeNode = nodeUnderMouse();
00724     while ( !activeNode.isNull() && activeNode.nodeType() != DOM::Node::ELEMENT_NODE )
00725         activeNode = activeNode.parentNode();
00726 
00727     // make sure it's valid
00728     d->activeElement = activeNode;
00729     if ( d->activeElement.isNull() )
00730         return;
00731 
00732     KMenu *chatWindowPopup = 0L;
00733 
00734     if ( Kopete::Contact *contact = contactFromNode( d->activeElement ) )
00735     {
00736         chatWindowPopup = contact->popupMenu( d->manager );
00737         connect( chatWindowPopup, SIGNAL( aboutToHide() ), chatWindowPopup , SLOT( deleteLater() ) );
00738     }
00739     else
00740     {
00741         chatWindowPopup = new KMenu();
00742 
00743         QAction *action;
00744         if ( d->activeElement.className() == QLatin1String("KopeteDisplayName") )
00745         {
00746             action = chatWindowPopup->addAction( i18n( "User Has Left" ) );
00747             action->setEnabled(false);
00748             chatWindowPopup->addSeparator();
00749         }
00750         else if ( d->activeElement.tagName().lower() == QLatin1String( "a" ) )
00751         {
00752             chatWindowPopup->addAction( d->copyURLAction );
00753             chatWindowPopup->addSeparator();
00754         }
00755 
00756         d->copyAction->setEnabled( hasSelection() );
00757         chatWindowPopup->addAction( d->copyAction );
00758         chatWindowPopup->addAction( d->saveAction );
00759         chatWindowPopup->addAction( d->printAction );
00760         chatWindowPopup->addSeparator();
00761         chatWindowPopup->addAction( d->closeAction );
00762 
00763         connect( chatWindowPopup, SIGNAL( aboutToHide() ), chatWindowPopup, SLOT( deleteLater() ) );
00764         chatWindowPopup->popup( point );
00765     }
00766 
00767     //Emit for plugin hooks
00768     emit contextMenuEvent( textUnderMouse(), chatWindowPopup );
00769 
00770     chatWindowPopup->popup( point );
00771 }
00772 
00773 QString ChatMessagePart::textUnderMouse()
00774 {
00775     DOM::Node activeNode = nodeUnderMouse();
00776     if( activeNode.nodeType() != DOM::Node::TEXT_NODE )
00777         return QString();
00778 
00779     DOM::Text textNode = activeNode;
00780     QString data = textNode.data().string();
00781 
00782     //Ok, we have the whole node. Now, find the text under the mouse.
00783     int mouseLeft = view()->mapFromGlobal( QCursor::pos() ).x(),
00784         nodeLeft = activeNode.getRect().x(),
00785         cPos = 0,
00786         dataLen = data.length();
00787 
00788     QFontMetrics metrics( Kopete::AppearanceSettings::self()->chatFont() );
00789     QString buffer;
00790     while( cPos < dataLen && nodeLeft < mouseLeft )
00791     {
00792         QChar c = data[cPos++];
00793         if( c.isSpace() )
00794             buffer.truncate(0);
00795         else
00796             buffer += c;
00797 
00798         nodeLeft += metrics.width(c);
00799     }
00800 
00801     if( cPos < dataLen )
00802     {
00803         QChar c = data[cPos++];
00804         while( cPos < dataLen && !c.isSpace() )
00805         {
00806             buffer += c;
00807             c = data[cPos++];
00808         }
00809     }
00810 
00811     return buffer;
00812 }
00813 
00814 void ChatMessagePart::slotCopyURL()
00815 {
00816     DOM::HTMLAnchorElement a = d->activeElement;
00817     if ( !a.isNull() )
00818     {
00819         QApplication::clipboard()->setText( a.href().string(), QClipboard::Clipboard );
00820         QApplication::clipboard()->setText( a.href().string(), QClipboard::Selection );
00821     }
00822 }
00823 
00824 void ChatMessagePart::slotScrollView()
00825 {
00826     // NB: view()->contentsHeight() is incorrect before the view has been shown in its window.
00827     // Until this happens, the geometry has not been correctly calculated, so this scrollBy call
00828     // will usually scroll to the top of the view.
00829     view()->scrollBy( 0, view()->contentsHeight() );
00830 }
00831 
00832 void ChatMessagePart::copy(bool justselection /* default false */)
00833 {
00834     /*
00835     * The objective of this function is to keep the text of emoticons (or of LaTeX image) when copying.
00836     *   see Bug 61676
00837     * This also copies the text as type text/html
00838     * RangeImpl::toHTML  was not implemented before KDE 3.4
00839     */
00840     QString htmltext = selectedTextAsHTML();
00841     QString text = selectedText();
00842         //selectedText is now sufficient
00843 //      text=Kopete::Message::unescape( htmltext ).trimmed();
00844         // Message::unsescape will replace image by his title attribute
00845         // trimmed is for removing the newline added by the <!DOCTYPE> and other xml things of RangeImpl::toHTML
00846 
00847     if(text.isEmpty())
00848             return;
00849 
00850     disconnect( QApplication::clipboard(), SIGNAL( selectionChanged()), this, SLOT( slotClearSelection()));
00851 
00852 #ifndef QT_NO_MIMECLIPBOARD
00853     if(!justselection)
00854     {
00855         QMimeData *mimeData = new QMimeData();
00856         mimeData->setText(text);
00857 
00858         if(!htmltext.isEmpty()) {
00859             htmltext.replace( QChar( 0xa0 ), ' ' );
00860             mimeData->setHtml(htmltext);
00861         }
00862 
00863         QApplication::clipboard()->setMimeData( mimeData, QClipboard::Clipboard );
00864     }
00865     QApplication::clipboard()->setText( text, QClipboard::Selection );
00866 #else
00867     if(!justselection)
00868         QApplication::clipboard()->setText( text, QClipboard::Clipboard );
00869     QApplication::clipboard()->setText( text, QClipboard::Selection );
00870 #endif
00871     connect( QApplication::clipboard(), SIGNAL( selectionChanged()), SLOT( slotClearSelection()));
00872 
00873 }
00874 
00875 void ChatMessagePart::print()
00876 {
00877     view()->print();
00878 }
00879 
00880 void ChatMessagePart::khtmlDrawContentsEvent( khtml::DrawContentsEvent * event) //virtual
00881 {
00882     KHTMLPart::khtmlDrawContentsEvent(event);
00883     //copy(true /*selection only*/); not needed anymore.
00884 }
00885 void ChatMessagePart::slotCloseView( bool force )
00886 {
00887     d->manager->view()->closeView( force );
00888 }
00889 
00890 void ChatMessagePart::emitTooltipEvent(  const QString &textUnderMouse, QString &toolTip )
00891 {
00892     emit tooltipEvent(  textUnderMouse, toolTip );
00893 }
00894 
00895 // Style formatting for messages(incoming, outgoing, status)
00896 QString ChatMessagePart::formatStyleKeywords( const QString &sourceHTML, const Kopete::Message &_message )
00897 {
00898     Kopete::Message message=_message; //we will eventually need to modify it before showing it.
00899     QString resultHTML = sourceHTML;
00900     QString nick, contactId, service, protocolIcon, nickLink;
00901 
00902     if( message.from() )
00903     {
00904         nick = formatName(message.from(), Qt::RichText);
00905         contactId = message.from()->contactId();
00906         // protocol() returns NULL here in the style preview in appearance config.
00907         // this isn't the right place to work around it, since contacts should never have
00908         // no protocol, but it works for now.
00909         //
00910         // Use default if protocol() and protocol()->displayName() is NULL.
00911         // For preview and unit tests.
00912         QString iconName = QLatin1String("kopete");
00913         service = QLatin1String("Kopete");
00914         if(message.from()->protocol() && !message.from()->protocol()->displayName().isNull())
00915         {
00916             service =  message.from()->protocol()->displayName();
00917             iconName = message.from()->protocol()->pluginIcon();
00918         }
00919 
00920         protocolIcon = KIconLoader::global()->iconPath( iconName, KIconLoader::Small );
00921 
00922         nickLink=QString("<a href=\"kopetemessage://%1/?protocolId=%2&amp;accountId=%3\" class=\"KopeteDisplayName\">")
00923                 .arg( Qt::escape(message.from()->contactId()).replace('"',"&quot;"),
00924                       Qt::escape(message.from()->protocol()->pluginId()).replace('"',"&quot;"),
00925                       Qt::escape(message.from()->account()->accountId() ).replace('"',"&quot;"));
00926     }
00927     else
00928     {
00929         nickLink="<a>";
00930     }
00931 
00932 
00933     // Replace sender (contact nick)
00934     resultHTML.replace( QLatin1String("%sender%"), nickLink+nick+"</a>" );
00935     // Replace time, by default display only time and display seconds(that was true means).
00936     if ( Kopete::BehaviorSettings::showDates() )
00937         resultHTML.replace( QLatin1String("%time%"), KGlobal::locale()->formatDateTime(message.timestamp(), KLocale::ShortDate, true) );
00938     else
00939         resultHTML.replace( QLatin1String("%time%"), KGlobal::locale()->formatTime(message.timestamp().time(), true) );
00940     // Replace %screenName% (contact ID)
00941     resultHTML.replace( QLatin1String("%senderScreenName%"), nickLink+Qt::escape(contactId)+"</a>" );
00942     // Replace service name (protocol name)
00943     resultHTML.replace( QLatin1String("%service%"), Qt::escape(service) );
00944     // Replace protocolIcon (sender statusIcon)
00945     resultHTML.replace( QLatin1String("%senderStatusIcon%"), Qt::escape(protocolIcon).replace('"',"&quot;") );
00946 
00947     // Look for %time{X}%
00948     QRegExp timeRegExp("%time\\{([^}]*)\\}%");
00949     int pos=0;
00950     while( (pos=timeRegExp.indexIn(resultHTML , pos) ) != -1 )
00951     {
00952         QString timeKeyword = formatTime( timeRegExp.cap(1), message.timestamp() );
00953         resultHTML.replace( pos , timeRegExp.cap(0).length() , timeKeyword );
00954     }
00955 
00956     // Look for %textbackgroundcolor{X}%
00957     // TODO: use the X value.
00958     // Replace with user-selected highlight color if to be highlighted or
00959     // with "inherit" otherwise to keep CSS clean
00960     QString bgColor = QLatin1String("inherit");
00961     if( message.importance() == Kopete::Message::Highlight && Kopete::BehaviorSettings::self()->highlightEnabled() )
00962     {
00963         bgColor = Kopete::AppearanceSettings::self()->highlightBackgroundColor().name();
00964     }
00965 
00966     QRegExp textBackgroundRegExp("%textbackgroundcolor\\{([^}]*)\\}%");
00967     int textPos=0;
00968     while( (textPos=textBackgroundRegExp.indexIn(resultHTML, textPos) ) != -1 )
00969     {
00970         resultHTML.replace( textPos , textBackgroundRegExp.cap(0).length() , bgColor );
00971     }
00972 
00973     // Replace userIconPath
00974     if( message.from() )
00975     {
00976         QString photoPath = photoForContact( message.from() );
00977         if( photoPath.isEmpty() )
00978         {
00979             if(message.direction() == Kopete::Message::Inbound)
00980                 photoPath = d->currentChatStyle->getStyleBaseHref() + QLatin1String("Incoming/buddy_icon.png");
00981             else if(message.direction() == Kopete::Message::Outbound)
00982                 photoPath = d->currentChatStyle->getStyleBaseHref() + QLatin1String("Outgoing/buddy_icon.png");
00983         }
00984         resultHTML.replace(QLatin1String("%userIconPath%"), photoPath);
00985     }
00986 
00987     // Replace messages.
00988     // Build the action message if the currentChatStyle do not have Action template.
00989     if( message.type() == Kopete::Message::TypeAction && !d->currentChatStyle->hasActionTemplate() )
00990     {
00991         kDebug(14000) << "Map Action message to Status template. ";
00992 
00993         QString boldNick = QString("%1<b>%2</b></a> ").arg(nickLink,nick);
00994         QString newBody = boldNick + message.parsedBody();
00995         message.setHtmlBody(newBody );
00996     }
00997 
00998     // Set message direction("rtl"(Right-To-Left) or "ltr"(Left-to-right))
00999     resultHTML.replace( QLatin1String("%messageDirection%"), message.isRightToLeft() ? "rtl" : "ltr" );
01000 
01001     // These colors are used for coloring nicknames. I tried to use
01002     // colors both visible on light and dark background.
01003     static const char* const nameColors[] =
01004     {
01005         "red", "blue" , "gray", "magenta", "violet", /*"olive"*/ "#808000", "yellowgreen",
01006         "darkred", "darkgreen", "darksalmon", "darkcyan", /*"darkyellow"*/   "#B07D2B",
01007         "mediumpurple", "peru", "olivedrab", /*"royalred"*/ "#B01712", "darkorange", "slateblue",
01008         "slategray", "goldenrod", "orangered", "tomato", /*"dogderblue"*/ "#1E90FF", "steelblue",
01009         "deeppink", "saddlebrown", "coral", "royalblue"
01010     };
01011 
01012     static const int nameColorsLen = sizeof(nameColors) / sizeof(nameColors[0]) - 1;
01013     // hash contactId to deterministically pick a color for the contact
01014     int hash = 0;
01015     for( int f = 0; f < contactId.length(); ++f )
01016         hash += contactId[f].unicode() * f;
01017     const QString colorName = nameColors[ hash % nameColorsLen ];
01018     QString lightColorName; // Do not initialize, QColor::name() is expensive!
01019     kDebug(14000) << "Hash " << hash << " has color " << colorName;
01020     QRegExp senderColorRegExp("%senderColor(?:\\{([^}]*)\\})?%");
01021     textPos=0;
01022     while( (textPos=senderColorRegExp.indexIn(resultHTML, textPos) ) != -1 )
01023     {
01024         int light=100;
01025         bool doLight=false;
01026         if(senderColorRegExp.numCaptures()>=1)
01027         {
01028             light=senderColorRegExp.cap(1).toUInt(&doLight);
01029         }
01030 
01031         // Lazily init light color
01032         if ( doLight && lightColorName.isNull() )
01033             lightColorName = QColor( colorName ).light( light ).name();
01034 
01035         resultHTML.replace( textPos , senderColorRegExp.cap(0).length(),
01036             doLight ? lightColorName : colorName );
01037     }
01038 
01039     if ( message.type() == Kopete::Message::TypeFileTransferRequest )
01040     {
01041         QString fileIcon;
01042         if ( !message.filePreview().isNull() )
01043         {
01044             QByteArray tempArray;
01045             QBuffer tempBuffer( &tempArray );
01046             tempBuffer.open( QIODevice::WriteOnly );
01047             if( message.filePreview().save( &tempBuffer, "PNG" ) )
01048                 fileIcon = QString( "data:image/png;base64," ) + tempArray.toBase64();
01049         }
01050 
01051         if ( fileIcon.isEmpty() )
01052         {
01053             QString iconName = KMimeType::iconNameForUrl( message.fileName() );
01054             fileIcon = KIconLoader::global()->iconPath( iconName, -KIconLoader::SizeMedium );
01055         }
01056 
01057         resultHTML.replace( QLatin1String("%fileName%"), Qt::escape( message.fileName() ).replace('"',"&quot;") );
01058         resultHTML.replace( QLatin1String("%fileSize%"), KGlobal::locale()->formatByteSize( message.fileSize() / 8 ).replace('"',"&quot;") );
01059         resultHTML.replace( QLatin1String("%fileIconPath%"), fileIcon );
01060 
01061         resultHTML.replace( QLatin1String("%saveFileHandlerId%"), QString( "ftSV%1" ).arg( message.id() ) );
01062         resultHTML.replace( QLatin1String("%saveFileAsHandlerId%"), QString( "ftSA%1" ).arg( message.id() ) );
01063         resultHTML.replace( QLatin1String("%cancelRequestHandlerId%"), QString( "ftCC%1" ).arg( message.id() ) );
01064     }
01065     
01066     if ( message.type() == Kopete::Message::TypeNormal && message.direction() == Kopete::Message::Outbound )
01067         resultHTML.replace( QLatin1String( "%stateElementId%" ), QString( "msST%1" ).arg( message.id() ) );
01068 
01069     // Replace message at the end, maybe someone could put a Adium keyword in his message :P
01070     resultHTML.replace( QLatin1String("%message%"), formatMessageBody(message) );
01071 
01072     // TODO: %status
01073 //  resultHTML = addNickLinks( resultHTML );
01074     return resultHTML;
01075 }
01076 
01077 // Style formatting for header and footer.
01078 QString ChatMessagePart::formatStyleKeywords( const QString &sourceHTML )
01079 {
01080     QString resultHTML = sourceHTML;
01081 
01082     // Verify that all contacts are not null before doing anything
01083     if( !d->manager->members().isEmpty() && d->manager->myself() )
01084     {
01085         QString sourceName, destinationName;
01086 
01087         Kopete::Contact *remoteContact = d->manager->members().first();
01088 
01089         // Use contact nickname for ourselfs, Myself metacontact display name isn't a reliable source.
01090         sourceName = d->manager->myself()->nickName();
01091         if( remoteContact->metaContact() )
01092             destinationName = remoteContact->metaContact()->displayName();
01093         else
01094             destinationName = remoteContact->nickName();
01095 
01096         // Replace %chatName%, create a internal span to update it by DOM when asked.
01097         resultHTML.replace( QLatin1String("%chatName%"), QString("<span id=\"KopeteHeaderChatNameInternal\">%1</span>").arg( formatName(d->manager->displayName(), Qt::RichText) ) );
01098         // Replace %sourceName%
01099         resultHTML.replace( QLatin1String("%sourceName%"), formatName(sourceName, Qt::RichText) );
01100         // Replace %destinationName%
01101         resultHTML.replace( QLatin1String("%destinationName%"), formatName(destinationName, Qt::RichText) );
01102         // For %timeOpened%, display the date and time (also the seconds).
01103         resultHTML.replace( QLatin1String("%timeOpened%"), KGlobal::locale()->formatDateTime( QDateTime::currentDateTime(), KLocale::ShortDate, true ) );
01104 
01105         // Look for %timeOpened{X}%
01106         QRegExp timeRegExp("%timeOpened\\{([^}]*)\\}%");
01107         int pos=0;
01108         while( (pos=timeRegExp.indexIn(resultHTML, pos) ) != -1 )
01109         {
01110             QString timeKeyword = formatTime( timeRegExp.cap(1), QDateTime::currentDateTime() );
01111             resultHTML.replace( pos , timeRegExp.cap(0).length() , timeKeyword );
01112         }
01113         // Get contact image paths
01114         QString photoIncoming = photoForContact( remoteContact );
01115         QString photoOutgoing = photoForContact( d->manager->myself() );
01116         if( photoIncoming.isEmpty() )
01117         {
01118             photoIncoming = d->currentChatStyle->getStyleBaseHref() + QLatin1String("Incoming/buddy_icon.png");
01119         }
01120 
01121         if( photoOutgoing.isEmpty() )
01122         {
01123             photoOutgoing = d->currentChatStyle->getStyleBaseHref() + QLatin1String("Outgoing/buddy_icon.png");
01124         }
01125 
01126         resultHTML.replace( QLatin1String("%incomingIconPath%"), photoIncoming );
01127         resultHTML.replace( QLatin1String("%outgoingIconPath%"), photoOutgoing );
01128     }
01129 
01130     return resultHTML;
01131 }
01132 
01133 QString ChatMessagePart::formatTime(const QString &timeFormat, const QDateTime &dateTime)
01134 {
01135     char buffer[256];
01136 
01137     time_t timeT;
01138     struct tm *loctime;
01139     // Get current time
01140     timeT = dateTime.toTime_t();
01141     // Convert it to local time representation.
01142     loctime = localtime (&timeT);
01143     strftime (buffer, 256, timeFormat.toAscii(), loctime);
01144 
01145     return QString(buffer);
01146 }
01147 
01148 QString ChatMessagePart::formatName(const