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