/* chatmessagepart.cpp - Chat Message KPart Copyright (c) 2002-2005 by Olivier Goffart Copyright (c) 2002-2003 by Martijn Klingens Copyright (c) 2004 by Richard Smith Copyright (c) 2005-2006 by Michaƫl Larouche Kopete (c) 2002-2005 by the Kopete developers ************************************************************************* * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ************************************************************************* */ #include "chatmessagepart.h" // STYLE_TIMETEST is for time staticstic gathering. //#define STYLE_TIMETEST #include // TQt includes #include #include #include #include #include #include #include #include #include // TDEHTML::DOM includes #include #include #include #include #include #include #include // KDE includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Kopete includes #include "chatmemberslistwidget.h" #include "kopetecontact.h" #include "kopetecontactlist.h" #include "kopetechatwindow.h" #include "kopetechatsession.h" #include "kopetemetacontact.h" #include "kopetepluginmanager.h" #include "kopeteprefs.h" #include "kopeteprotocol.h" #include "kopeteaccount.h" #include "kopeteglobal.h" #include "kopeteemoticons.h" #include "kopeteview.h" #include "kopetepicture.h" #include "kopetechatwindowstyle.h" #include "kopetechatwindowstylemanager.h" #if !(KDE_IS_VERSION(3,3,90)) //From tdelibs/tdehtml/misc/htmltags.h // used in ChatMessagePart::copy() #define ID_BLOCKQUOTE 12 #define ID_BR 14 #define ID_DD 22 #define ID_DIV 26 #define ID_DL 27 #define ID_DT 28 #define ID_H1 36 #define ID_H2 37 #define ID_H3 38 #define ID_H4 39 #define ID_H5 40 #define ID_H6 41 #define ID_HR 43 #define ID_IMG 48 #define ID_LI 57 #define ID_OL 69 #define ID_P 72 #define ID_PRE 75 #define ID_TD 90 #define ID_TH 93 #define ID_TR 96 #define ID_TT 97 #define ID_UL 99 #endif class ToolTip; class ChatMessagePart::Private { public: Private() : tt(0L), manager(0L), scrollPressed(false), copyAction(0L), saveAction(0L), printAction(0L), closeAction(0L),copyURLAction(0L), currentChatStyle(0L), latestContact(0L), latestDirection(Kopete::Message::Inbound), latestType(Kopete::Message::TypeNormal) {} ~Private() { // Don't delete manager and latestContact, because they could be still used. // Don't delete currentChatStyle, it is handled by ChatWindowStyleManager. } bool bgOverride; bool fgOverride; bool rtfOverride; ToolTip *tt; Kopete::ChatSession *manager; bool scrollPressed; DOM::HTMLElement activeElement; TDEAction *copyAction; TDEAction *saveAction; TDEAction *printAction; TDEAction *closeAction; TDEAction *copyURLAction; TDEAction *importEmoticon; ChatWindowStyle *currentChatStyle; Kopete::Contact *latestContact; Kopete::Message::MessageDirection latestDirection; Kopete::Message::MessageType latestType; // Yep I know it will take memory, but I don't have choice // to enable on-the-fly style changing. TQValueList allMessages; }; class ChatMessagePart::ToolTip : public TQToolTip { public: ToolTip( ChatMessagePart *c ) : TQToolTip( c->view()->viewport() ) { m_chat = c; } void maybeTip( const TQPoint &/*p*/ ) { // FIXME: it's wrong to look for the node under the mouse - this makes too many // assumptions about how tooltips work. but there is no nodeAtPoint. DOM::Node node = m_chat->nodeUnderMouse(); Kopete::Contact *contact = m_chat->contactFromNode( node ); TQString toolTipText; if(node.isNull()) return; // this tooltip is attached to the viewport widget, so translate the node's rect // into its coordinates. TQRect rect = node.getRect(); rect = TQRect( m_chat->view()->contentsToViewport( rect.topLeft() ), m_chat->view()->contentsToViewport( rect.bottomRight() ) ); if( contact ) { toolTipText = contact->toolTip(); } else { m_chat->emitTooltipEvent( m_chat->textUnderMouse(), toolTipText ); if( toolTipText.isEmpty() ) { //Fall back to the title attribute for( DOM::HTMLElement element = node; !element.isNull(); element = element.parentNode() ) { if( element.hasAttribute( "title" ) ) { toolTipText = element.getAttribute( "title" ).string(); break; } } } } if( !toolTipText.isEmpty() ) tip( rect, toolTipText ); } private: ChatMessagePart *m_chat; }; ChatMessagePart::ChatMessagePart( Kopete::ChatSession *mgr, TQWidget *parent, const char *name) : TDEHTMLPart( parent, name ), d( new Private ) { d->manager = mgr; KopetePrefs *kopetePrefs = KopetePrefs::prefs(); d->currentChatStyle = ChatWindowStyleManager::self()->getStyleFromPool( kopetePrefs->stylePath() ); //Security settings, we don't need this stuff setJScriptEnabled( false ) ; setJavaEnabled( false ); setPluginsEnabled( false ); setMetaRefreshEnabled( false ); setOnlyLocalReferences( true ); // Write the template to TDEHTMLPart writeTemplate(); view()->setFocusPolicy( TQWidget::NoFocus ); d->tt=new ToolTip( this ); // It is not possible to drag and drop on our widget view()->setAcceptDrops(false); connect( KopetePrefs::prefs(), TQT_SIGNAL(messageAppearanceChanged()), this, TQT_SLOT( slotAppearanceChanged() ) ); connect( KopetePrefs::prefs(), TQT_SIGNAL(windowAppearanceChanged()), this, TQT_SLOT( slotRefreshView() ) ); connect( KopetePrefs::prefs(), TQT_SIGNAL(styleChanged(const TQString &)), this, TQT_SLOT( setStyle(const TQString &) ) ); connect( KopetePrefs::prefs(), TQT_SIGNAL(styleVariantChanged(const TQString &)), this, TQT_SLOT( setStyleVariant(const TQString &) ) ); // Refresh the style if the display name change. connect( d->manager, TQT_SIGNAL(displayNameChanged()), this, TQT_SLOT(slotUpdateHeaderDisplayName()) ); connect( d->manager, TQT_SIGNAL(photoChanged()), this, TQT_SLOT(slotUpdateHeaderPhoto()) ); connect ( browserExtension(), TQT_SIGNAL( openURLRequestDelayed( const KURL &, const KParts::URLArgs & ) ), this, TQT_SLOT( slotOpenURLRequest( const KURL &, const KParts::URLArgs & ) ) ); connect( this, TQT_SIGNAL(popupMenu(const TQString &, const TQPoint &)), this, TQT_SLOT(slotRightClick(const TQString &, const TQPoint &)) ); connect( view(), TQT_SIGNAL(contentsMoving(int,int)), this, TQT_SLOT(slotScrollingTo(int,int)) ); //initActions d->copyAction = KStdAction::copy( this, TQT_SLOT(copy()), actionCollection() ); d->saveAction = KStdAction::saveAs( this, TQT_SLOT(save()), actionCollection() ); d->printAction = KStdAction::print( this, TQT_SLOT(print()),actionCollection() ); d->closeAction = KStdAction::close( this, TQT_SLOT(slotCloseView()),actionCollection() ); d->importEmoticon = new TDEAction( i18n( "Import Emoticon"), TQString::fromLatin1( "importemot" ), 0, this, TQT_SLOT( slotImportEmoticon() ), actionCollection() ); d->copyURLAction = new TDEAction( i18n( "Copy Link Address" ), TQString::fromLatin1( "edit-copy" ), 0, this, TQT_SLOT( slotCopyURL() ), actionCollection() ); // read formatting override flags readOverrides(); } ChatMessagePart::~ChatMessagePart() { kdDebug(14000) << k_funcinfo << endl; delete d->tt; delete d; } void ChatMessagePart::slotScrollingTo( int /*x*/, int y ) { int scrolledTo = y + view()->visibleHeight(); if ( scrolledTo >= ( view()->contentsHeight() - 10 ) ) d->scrollPressed = false; else d->scrollPressed = true; } void ChatMessagePart::slotImportEmoticon() { TQString emoticonString = KInputDialog::getText( i18n("Import Emoticon"), i18n("
Insert the string for the emoticon
separated by space if you want multiple strings
").arg( d->activeElement.getAttribute("src").string() ) ); if (emoticonString.isNull() ) return; TQString emo = d->activeElement.getAttribute("src").string(); TQString themeName = KopetePrefs::prefs()->iconTheme(); TDEIO::copy(emo, TDEGlobal::dirs()->saveLocation( "emoticons", themeName, false )); TQFile *fp = new TQFile(TDEGlobal::dirs()->saveLocation( "emoticons", themeName, false ) + "/emoticons.xml"); TQDomDocument themeXml; if(!fp->exists() || !fp->open( IO_ReadOnly ) || !themeXml.setContent(fp)) return; fp->close(); TQDomNode lc = themeXml.lastChild(); if(lc.isNull()) return; TQDomElement emoticon = themeXml.createElement("emoticon"); emoticon.setAttribute("file", TQFileInfo(emo).baseName()); lc.appendChild(emoticon); TQStringList splitted = TQStringList::split(" ", emoticonString); TQStringList::const_iterator constIterator; for(constIterator = splitted.begin(); constIterator != splitted.end(); constIterator++) { TQDomElement emotext = themeXml.createElement("string"); TQDomText txt = themeXml.createTextNode((*constIterator).stripWhiteSpace()); emotext.appendChild(txt); emoticon.appendChild(emotext); } if(!fp->open( IO_WriteOnly )) return; TQTextStream emoStream(fp); emoStream << themeXml.toString(4); fp->close(); TQTimer::singleShot( 1500, Kopete::Emoticons::self(), TQT_SLOT( reload() ) ); } void ChatMessagePart::save() { KFileDialog dlg( TQString(), TQString::fromLatin1( "text/html text/plain" ), view(), "fileSaveDialog", false ); dlg.setCaption( i18n( "Save Conversation" ) ); dlg.setOperationMode( KFileDialog::Saving ); if ( dlg.exec() != TQDialog::Accepted ) return; KURL saveURL = dlg.selectedURL(); KTempFile tempFile; tempFile.setAutoDelete( true ); TQFile* file = tempFile.file(); TQTextStream stream ( file ); stream.setEncoding(TQTextStream::UnicodeUTF8); if ( dlg.currentFilter() == TQString::fromLatin1( "text/plain" ) ) { TQValueList::ConstIterator it, itEnd = d->allMessages.constEnd(); for(it = d->allMessages.constBegin(); it != itEnd; ++it) { Kopete::Message tempMessage = *it; stream << "[" << TDEGlobal::locale()->formatDateTime(tempMessage.timestamp()) << "] "; if( tempMessage.from() && tempMessage.from()->metaContact() ) { stream << formatName(tempMessage.from()->metaContact()->displayName()); } stream << ": " << tempMessage.plainBody() << "\n"; } } else { stream << htmlDocument().toHTML() << '\n'; } tempFile.close(); if ( !TDEIO::NetAccess::move( KURL( tempFile.name() ), saveURL ) ) { KMessageBox::queuedMessageBox( view(), KMessageBox::Error, i18n("Could not open %1 for writing.").arg( saveURL.prettyURL() ), // Message i18n("Error While Saving") ); //Caption } } void ChatMessagePart::pageUp() { view()->scrollBy( 0, -view()->visibleHeight() ); } void ChatMessagePart::pageDown() { view()->scrollBy( 0, view()->visibleHeight() ); } void ChatMessagePart::slotOpenURLRequest(const KURL &url, const KParts::URLArgs &/*args*/) { kdDebug(14000) << k_funcinfo << "url=" << url.url() << endl; if ( url.protocol() == TQString::fromLatin1("kopetemessage") ) { Kopete::Contact *contact = d->manager->account()->contacts()[ url.host() ]; if ( contact ) contact->execute(); } else { KRun *runner = new KRun( url, 0, false ); // false = non-local files runner->setRunExecutables( false ); //security //KRun autodeletes itself by default when finished. } } void ChatMessagePart::readOverrides() { d->bgOverride = KopetePrefs::prefs()->bgOverride(); d->fgOverride = KopetePrefs::prefs()->fgOverride(); d->rtfOverride = KopetePrefs::prefs()->rtfOverride(); } void ChatMessagePart::setStyle( const TQString &stylePath ) { // Create a new ChatWindowStyle d->currentChatStyle = ChatWindowStyleManager::self()->getStyleFromPool(stylePath); // Do the actual style switch // Wait for the event loop before switching the style TQTimer::singleShot( 0, this, TQT_SLOT(changeStyle()) ); } void ChatMessagePart::setStyle( ChatWindowStyle *style ) { // Change the current style d->currentChatStyle = style; // Do the actual style switch // Wait for the event loop before switching the style TQTimer::singleShot( 0, this, TQT_SLOT(changeStyle()) ); } void ChatMessagePart::setStyleVariant( const TQString &variantPath ) { DOM::HTMLElement variantNode = document().getElementById( TQString::fromUtf8("mainStyle") ); if( !variantNode.isNull() ) variantNode.setInnerText( TQString("@import url(\"%1\");").arg(variantPath) ); } void ChatMessagePart::slotAppearanceChanged() { readOverrides(); changeStyle(); } void ChatMessagePart::appendMessage( Kopete::Message &message, bool restoring ) { message.setBgOverride( d->bgOverride ); message.setFgOverride( d->fgOverride ); message.setRtfOverride( d->rtfOverride ); // parse emoticons and URL now. // Do not reparse emoticons on restoring, because it cause very intensive CPU usage on long chats. if( !restoring ) message.setBody( message.parsedBody() , Kopete::Message::ParsedHTML ); #ifdef STYLE_TIMETEST TQTime beforeMessage = TQTime::currentTime(); #endif TQString formattedMessageHtml; bool isConsecutiveMessage = false; uint bufferLen = (uint)KopetePrefs::prefs()->chatViewBufferSize(); // Find the "Chat" div element. // If the "Chat" div element is not found, do nothing. It's the central part of Adium format. DOM::HTMLElement chatNode = htmlDocument().getElementById( "Chat" ); if( chatNode.isNull() ) { kdDebug(14000) << k_funcinfo << "WARNING: Chat Node was null !" << endl; return; } // Check if it's a consecutive Message // Consecutive messages are only for normal messages, status messages do not have a
// We check if the from() is the latestContact, because consecutive incoming/outgoing message can come from differents peopole(in groupchat and IRC) // Group only if the user want it. if( KopetePrefs::prefs()->groupConsecutiveMessages() ) { isConsecutiveMessage = (message.direction() == d->latestDirection && d->latestContact && d->latestContact == message.from() && message.type() == d->latestType); } // Don't test it in the switch to don't break consecutive messages. if(message.type() == Kopete::Message::TypeAction) { // Check if chat style support Action template (Kopete extension) if( d->currentChatStyle->hasActionTemplate() ) { switch(message.direction()) { case Kopete::Message::Inbound: formattedMessageHtml = d->currentChatStyle->getActionIncomingHtml(); break; case Kopete::Message::Outbound: formattedMessageHtml = d->currentChatStyle->getActionOutgoingHtml(); break; default: break; } } // Use status template if no Action template. else { formattedMessageHtml = d->currentChatStyle->getStatusHtml(); } } else { switch(message.direction()) { case Kopete::Message::Inbound: { if(isConsecutiveMessage) { formattedMessageHtml = d->currentChatStyle->getNextIncomingHtml(); } else { formattedMessageHtml = d->currentChatStyle->getIncomingHtml(); } break; } case Kopete::Message::Outbound: { if(isConsecutiveMessage) { formattedMessageHtml = d->currentChatStyle->getNextOutgoingHtml(); } else { formattedMessageHtml = d->currentChatStyle->getOutgoingHtml(); } break; } case Kopete::Message::Internal: { formattedMessageHtml = d->currentChatStyle->getStatusHtml(); break; } } } formattedMessageHtml = formatStyleKeywords( formattedMessageHtml, message ); // newMessageNode is common to both code path // FIXME: Find a better than to create a dummy span. DOM::HTMLElement newMessageNode = document().createElement( TQString::fromUtf8("span") ); newMessageNode.setInnerHTML( formattedMessageHtml ); // Find the insert Node DOM::HTMLElement insertNode = document().getElementById( TQString::fromUtf8("insert") ); if( isConsecutiveMessage && !insertNode.isNull() ) { // Replace the insert block, because it's a consecutive message. insertNode.parentNode().replaceChild(newMessageNode, insertNode); } else { // Remove the insert block, because it's a new message. if( !insertNode.isNull() ) insertNode.parentNode().removeChild(insertNode); // Append to the chat. chatNode.appendChild(newMessageNode); } // Keep the direction to see on next message // if it's a consecutive message // Keep also the from() contact. d->latestDirection = message.direction(); d->latestType = message.type(); d->latestContact = const_cast(message.from()); // Add the message to the list for futher restoring if needed if(!restoring) d->allMessages.append(message); while ( bufferLen>0 && d->allMessages.count() >= bufferLen ) { d->allMessages.pop_front(); // FIXME: Find a way to make work Chat View Buffer efficiently with consecutives messages. // Before it was calling changeStyle() but it's damn too slow. if( !KopetePrefs::prefs()->groupConsecutiveMessages() ) { chatNode.removeChild( chatNode.firstChild() ); } } if ( !d->scrollPressed ) TQTimer::singleShot( 1, this, TQT_SLOT( slotScrollView() ) ); #ifdef STYLE_TIMETEST kdDebug(14000) << "Message time: " << beforeMessage.msecsTo( TQTime::currentTime()) << endl; #endif } void ChatMessagePart::slotRefreshView() { DOM::HTMLElement kopeteNode = document().getElementById( TQString::fromUtf8("KopeteStyle") ); if( !kopeteNode.isNull() ) kopeteNode.setInnerText( styleHTML() ); DOM::HTMLBodyElement bodyElement = htmlDocument().body(); bodyElement.setBgColor( TQString(KopetePrefs::prefs()->bgColor().name()) ); } void ChatMessagePart::keepScrolledDown() { if ( !d->scrollPressed ) TQTimer::singleShot( 1, this, TQT_SLOT( slotScrollView() ) ); } const TQString ChatMessagePart::styleHTML() const { KopetePrefs *p = KopetePrefs::prefs(); int fontSize = 0; TQString fontSizeCss; // Use correct font size unit, depending of how the TQFont was build. if( p->fontFace().pointSize() != -1 ) { fontSize = p->fontFace().pointSize(); fontSizeCss = TQString::fromUtf8("%1pt;").arg(fontSize); } else if( p->fontFace().pixelSize() != -1 ) { fontSize = p->fontFace().pixelSize(); fontSizeCss = TQString::fromUtf8("%1px;").arg(fontSize); } TQString style = TQString::fromLatin1( "body{background-color:%1;font-family:%2;font-size:%3;color:%4}" "td{font-family:%5;font-size:%6;color:%7}" "a{color:%8}a.visited{color:%9}" "a.KopeteDisplayName{text-decoration:none;color:inherit;}" "a.KopeteDisplayName:hover{text-decoration:underline;color:inherit}" ".KopeteLink{cursor:pointer;}.KopeteLink:hover{text-decoration:underline}" ".KopeteMessageBody > p:first-child{margin:0;padding:0;display:inline;}" /* some html messages are encapsuled into a

*/ ) .arg( p->bgColor().name() ) .arg( p->fontFace().family() ) .arg( fontSizeCss ) .arg( p->textColor().name() ) .arg( p->fontFace().family() ) .arg( fontSizeCss ) .arg( p->textColor().name() ) .arg( p->linkColor().name() ) .arg( p->linkColor().name() ); return style; } void ChatMessagePart::clear() { // writeTemplate actually reset the HTML chat session from the beginning. writeTemplate(); // Reset consecutive messages d->latestContact = 0; // Remove all stored messages. d->allMessages.clear(); } Kopete::Contact *ChatMessagePart::contactFromNode( const DOM::Node &n ) const { DOM::Node node = n; if ( node.isNull() ) return 0; while ( !node.isNull() && ( node.nodeType() == DOM::Node::TEXT_NODE || ((DOM::HTMLElement)node).className() != "KopeteDisplayName" ) ) node = node.parentNode(); DOM::HTMLElement element = node; if ( element.className() != "KopeteDisplayName" ) return 0; if ( element.hasAttribute( "contactid" ) ) { TQString contactId = element.getAttribute( "contactid" ).string(); for ( TQPtrListIterator it ( d->manager->members() ); it.current(); ++it ) if ( (*it)->contactId() == contactId ) return *it; } else { TQString nick = element.innerText().string().stripWhiteSpace(); for ( TQPtrListIterator it ( d->manager->members() ); it.current(); ++it ) if ( (*it)->property( Kopete::Global::Properties::self()->nickName().key() ).value().toString() == nick ) return *it; } return 0; } void ChatMessagePart::slotRightClick( const TQString &, const TQPoint &point ) { // look through parents until we find an Element DOM::Node activeNode = nodeUnderMouse(); while ( !activeNode.isNull() && activeNode.nodeType() != DOM::Node::ELEMENT_NODE ) activeNode = activeNode.parentNode(); // make sure it's valid d->activeElement = activeNode; if ( d->activeElement.isNull() ) return; TDEPopupMenu *chatWindowPopup = 0L; if ( Kopete::Contact *contact = contactFromNode( d->activeElement ) ) { chatWindowPopup = contact->popupMenu( d->manager ); connect( chatWindowPopup, TQT_SIGNAL( aboutToHide() ), chatWindowPopup , TQT_SLOT( deleteLater() ) ); } else { chatWindowPopup = new TDEPopupMenu(); if ( d->activeElement.className() == "KopeteDisplayName" ) { chatWindowPopup->insertItem( i18n( "User Has Left" ), 1 ); chatWindowPopup->setItemEnabled( 1, false ); chatWindowPopup->insertSeparator(); } else if ( d->activeElement.tagName().lower() == TQString::fromLatin1( "a" ) ) { d->copyURLAction->plug( chatWindowPopup ); chatWindowPopup->insertSeparator(); } kdDebug() << "ChatMessagePart::slotRightClick(): " << d->activeElement.tagName().lower() << endl; d->copyAction->setEnabled( hasSelection() ); d->copyAction->plug( chatWindowPopup ); d->saveAction->plug( chatWindowPopup ); d->printAction->plug( chatWindowPopup ); if( d->activeElement.tagName().lower() == "img" ) d->importEmoticon->plug( chatWindowPopup ); chatWindowPopup->insertSeparator(); d->closeAction->plug( chatWindowPopup ); connect( chatWindowPopup, TQT_SIGNAL( aboutToHide() ), chatWindowPopup, TQT_SLOT( deleteLater() ) ); chatWindowPopup->popup( point ); } //Emit for plugin hooks emit contextMenuEvent( textUnderMouse(), chatWindowPopup ); chatWindowPopup->popup( point ); } TQString ChatMessagePart::textUnderMouse() { DOM::Node activeNode = nodeUnderMouse(); if( activeNode.nodeType() != DOM::Node::TEXT_NODE ) return TQString(); DOM::Text textNode = activeNode; TQString data = textNode.data().string(); //Ok, we have the whole node. Now, find the text under the mouse. int mouseLeft = view()->mapFromGlobal( TQCursor::pos() ).x(), nodeLeft = activeNode.getRect().x(), cPos = 0, dataLen = data.length(); TQFontMetrics metrics( KopetePrefs::prefs()->fontFace() ); TQString buffer; while( cPos < dataLen && nodeLeft < mouseLeft ) { TQChar c = data[cPos++]; if( c.isSpace() ) buffer.truncate(0); else buffer += c; nodeLeft += metrics.width(c); } if( cPos < dataLen ) { TQChar c = data[cPos++]; while( cPos < dataLen && !c.isSpace() ) { buffer += c; c = data[cPos++]; } } return buffer; } void ChatMessagePart::slotCopyURL() { DOM::HTMLAnchorElement a = d->activeElement; if ( !a.isNull() ) { TQApplication::clipboard()->setText( a.href().string(), TQClipboard::Clipboard ); TQApplication::clipboard()->setText( a.href().string(), TQClipboard::Selection ); } } void ChatMessagePart::slotScrollView() { // NB: view()->contentsHeight() is incorrect before the view has been shown in its window. // Until this happens, the geometry has not been correctly calculated, so this scrollBy call // will usually scroll to the top of the view. view()->scrollBy( 0, view()->contentsHeight() ); } void ChatMessagePart::copy(bool justselection /* default false */) { /* * The objective of this function is to keep the text of emoticons (or of latex image) when copying. * see Bug 61676 * This also copies the text as type text/html * RangeImpl::toHTML was not implemented before KDE 3.4 */ TQString text; TQString htmltext; #if KDE_IS_VERSION(3,3,90) htmltext = selectedTextAsHTML(); text = selectedText(); //selectedText is now sufficent // text=Kopete::Message::unescape( htmltext ).stripWhiteSpace(); // Message::unsescape will replace image by his title attribute // stripWhiteSpace is for removing the newline added by the and other xml things of RangeImpl::toHTML #else DOM::Node startNode, endNode; long startOffset, endOffset; selection( startNode, startOffset, endNode, endOffset ); //BEGIN: copied from TDEHTMLPart::selectedText bool hasNewLine = true; DOM::Node n = startNode; while(!n.isNull()) { if(n.nodeType() == DOM::Node::TEXT_NODE /*&& n.handle()->renderer()*/) { TQString str = n.nodeValue().string(); hasNewLine = false; if(n == startNode && n == endNode) text = str.mid(startOffset, endOffset - startOffset); else if(n == startNode) text = str.mid(startOffset); else if(n == endNode) text += str.left(endOffset); else text += str; } else { // This is our simple HTML -> ASCII transformation: unsigned short id = n.elementId(); switch(id) { case ID_IMG: //here is the main difference with TDEHTMLView::selectedText { DOM::HTMLElement e = n; if( !e.isNull() && e.hasAttribute( "title" ) ) text+=e.getAttribute( "title" ).string(); break; } case ID_BR: text += "\n"; hasNewLine = true; break; case ID_TD: case ID_TH: case ID_HR: case ID_OL: case ID_UL: case ID_LI: case ID_DD: case ID_DL: case ID_DT: case ID_PRE: case ID_BLOCKQUOTE: case ID_DIV: if (!hasNewLine) text += "\n"; hasNewLine = true; break; case ID_P: case ID_TR: case ID_H1: case ID_H2: case ID_H3: case ID_H4: case ID_H5: case ID_H6: if (!hasNewLine) text += "\n"; text += "\n"; hasNewLine = true; break; } } if(n == endNode) break; DOM::Node next = n.firstChild(); if(next.isNull()) next = n.nextSibling(); while( next.isNull() && !n.parentNode().isNull() ) { n = n.parentNode(); next = n.nextSibling(); unsigned short id = n.elementId(); switch(id) { case ID_TD: case ID_TH: case ID_HR: case ID_OL: case ID_UL: case ID_LI: case ID_DD: case ID_DL: case ID_DT: case ID_PRE: case ID_BLOCKQUOTE: case ID_DIV: if (!hasNewLine) text += "\n"; hasNewLine = true; break; case ID_P: case ID_TR: case ID_H1: case ID_H2: case ID_H3: case ID_H4: case ID_H5: case ID_H6: if (!hasNewLine) text += "\n"; text += "\n"; hasNewLine = true; break; } } n = next; } if(text.isEmpty()) return; int start = 0; int end = text.length(); // Strip leading LFs while ((start < end) && (text[start] == '\n')) start++; // Strip excessive trailing LFs while ((start < (end-1)) && (text[end-1] == '\n') && (text[end-2] == '\n')) end--; text=text.mid(start, end-start); //END: copied from TDEHTMLPart::selectedText #endif if(text.isEmpty()) return; disconnect( kapp->clipboard(), TQT_SIGNAL( selectionChanged()), this, TQT_SLOT( slotClearSelection())); #ifndef TQT_NO_MIMECLIPBOARD if(!justselection) { TQTextDrag *textdrag = new TQTextDrag(text, 0L); KMultipleDrag *drag = new KMultipleDrag( ); drag->addDragObject( textdrag ); if(!htmltext.isEmpty()) { htmltext.replace( TQChar( 0xa0 ), ' ' ); TQTextDrag *htmltextdrag = new TQTextDrag(htmltext, 0L); htmltextdrag->setSubtype("html"); drag->addDragObject( htmltextdrag ); } TQApplication::clipboard()->setData( drag, TQClipboard::Clipboard ); } TQApplication::clipboard()->setText( text, TQClipboard::Selection ); #else if(!justselection) TQApplication::clipboard()->setText( text, TQClipboard::Clipboard ); TQApplication::clipboard()->setText( text, TQClipboard::Selection ); #endif connect( kapp->clipboard(), TQT_SIGNAL( selectionChanged()), TQT_SLOT( slotClearSelection())); } void ChatMessagePart::print() { view()->print(); } void ChatMessagePart::tdehtmlDrawContentsEvent( tdehtml::DrawContentsEvent * event) //virtual { TDEHTMLPart::tdehtmlDrawContentsEvent(event); //copy(true /*selection only*/); not needed anymore. } void ChatMessagePart::slotCloseView( bool force ) { d->manager->view()->closeView( force ); } void ChatMessagePart::emitTooltipEvent( const TQString &textUnderMouse, TQString &toolTip ) { emit tooltipEvent( textUnderMouse, toolTip ); } // Style formatting for messages(incoming, outgoing, status) TQString ChatMessagePart::formatStyleKeywords( const TQString &sourceHTML, const Kopete::Message &_message ) { Kopete::Message message=_message; //we will eventually need to modify it before showing it. TQString resultHTML = sourceHTML; TQString nick, contactId, service, protocolIcon, nickLink; if( message.from() ) { // Use metacontact display name if the metacontact exists and if its not the myself metacontact. if( message.from()->metaContact() && message.from()->metaContact() != Kopete::ContactList::self()->myself() ) { nick = message.from()->metaContact()->displayName(); } // Use contact nickname for no metacontact or myself. else { nick = message.from()->nickName(); } nick = formatName(nick); contactId = message.from()->contactId(); // protocol() returns NULL here in the style preview in appearance config. // this isn't the right place to work around it, since contacts should never have // no protocol, but it works for now. // // Use default if protocol() and protocol()->displayName() is NULL. // For preview and unit tests. TQString iconName = TQString::fromUtf8("kopete"); service = TQString::fromUtf8("Kopete"); if(message.from()->protocol() && !message.from()->protocol()->displayName().isNull()) { service = message.from()->protocol()->displayName(); iconName = message.from()->protocol()->pluginIcon(); } protocolIcon = TDEGlobal::iconLoader()->iconPath( iconName, TDEIcon::Small ); nickLink=TQString::fromLatin1("") .arg( TQStyleSheet::escape(message.from()->contactId()).replace('"',"""), TQStyleSheet::escape(message.from()->protocol()->pluginId()).replace('"',"""), TQStyleSheet::escape(message.from()->account()->accountId() ).replace('"',""")); } else { nickLink=""; } // Replace sender (contact nick) resultHTML = resultHTML.replace( TQString::fromUtf8("%sender%"), nickLink+nick+"" ); // Replace time, by default display only time and display seconds(that was true means). resultHTML = resultHTML.replace( TQString::fromUtf8("%time%"), TDEGlobal::locale()->formatTime(message.timestamp().time(), true) ); // Replace %screenName% (contact ID) resultHTML = resultHTML.replace( TQString::fromUtf8("%senderScreenName%"), nickLink+TQStyleSheet::escape(contactId)+"" ); // Replace service name (protocol name) resultHTML = resultHTML.replace( TQString::fromUtf8("%service%"), TQStyleSheet::escape(service) ); // Replace protocolIcon (sender statusIcon) resultHTML = resultHTML.replace( TQString::fromUtf8("%senderStatusIcon%"), TQStyleSheet::escape(protocolIcon).replace('"',""") ); // Look for %time{X}% TQRegExp timeRegExp("%time\\{([^}]*)\\}%"); int pos=0; while( (pos=timeRegExp.search(resultHTML , pos) ) != -1 ) { TQString timeKeyword = formatTime( timeRegExp.cap(1), message.timestamp() ); resultHTML = resultHTML.replace( pos , timeRegExp.cap(0).length() , timeKeyword ); } // Look for %textbackgroundcolor{X}% // TODO: use the X value. // Replace with user-selected highlight color if to be highlighted or // with "inherit" otherwise to keep CSS clean TQString bgColor = TQString::fromUtf8("inherit"); if( message.importance() == Kopete::Message::Highlight && KopetePrefs::prefs()->highlightEnabled() ) { bgColor = KopetePrefs::prefs()->highlightBackground().name(); } TQRegExp textBackgroundRegExp("%textbackgroundcolor\\{([^}]*)\\}%"); int textPos=0; while( (textPos=textBackgroundRegExp.search(resultHTML, textPos) ) != -1 ) { resultHTML = resultHTML.replace( textPos , textBackgroundRegExp.cap(0).length() , bgColor ); } // Replace userIconPath if( message.from() ) { TQString photoPath; #if 0 photoPath = message.from()->property(Kopete::Global::Properties::self()->photo().key()).value().toString(); // If the photo path is empty, set the default buddy icon for the theme if( photoPath.isEmpty() ) { if(message.direction() == Kopete::Message::Inbound) photoPath = TQString::fromUtf8("Incoming/buddy_icon.png"); else if(message.direction() == Kopete::Message::Outbound) photoPath = TQString::fromUtf8("Outgoing/buddy_icon.png"); } #endif if( !message.from()->metaContact()->picture().isNull() ) { photoPath = TQString( "data:image/png;base64," ) + message.from()->metaContact()->picture().base64(); } else { if(message.direction() == Kopete::Message::Inbound) photoPath = TQString::fromUtf8("Incoming/buddy_icon.png"); else if(message.direction() == Kopete::Message::Outbound) photoPath = TQString::fromUtf8("Outgoing/buddy_icon.png"); } resultHTML = resultHTML.replace(TQString::fromUtf8("%userIconPath%"), photoPath); } // Replace messages. // Build the action message if the currentChatStyle do not have Action template. if( message.type() == Kopete::Message::TypeAction && !d->currentChatStyle->hasActionTemplate() ) { kdDebug(14000) << k_funcinfo << "Map Action message to Status template. " << endl; TQString boldNick = TQString::fromUtf8("%1%2 ").arg(nickLink,nick); TQString newBody = boldNick + message.parsedBody(); message.setBody(newBody, Kopete::Message::ParsedHTML ); } // Set message direction("rtl"(Right-To-Left) or "ltr"(Left-to-right)) resultHTML = resultHTML.replace( TQString::fromUtf8("%messageDirection%"), message.isRightToLeft() ? "rtl" : "ltr" ); // These colors are used for coloring nicknames. I tried to use // colors both visible on light and dark background. static const char* const nameColors[] = { "red", "blue" , "gray", "magenta", "violet", /*"olive"*/ "#808000", "yellowgreen", "darkred", "darkgreen", "darksalmon", "darkcyan", /*"darkyellow"*/ "#B07D2B", "mediumpurple", "peru", "olivedrab", /*"royalred"*/ "#B01712", "darkorange", "slateblue", "slategray", "goldenrod", "orangered", "tomato", /*"dogderblue"*/ "#1E90FF", "steelblue", "deeppink", "saddlebrown", "coral", "royalblue" }; static const int nameColorsLen = sizeof(nameColors) / sizeof(nameColors[0]) - 1; // hash contactId to deterministically pick a color for the contact int hash = 0; for( uint f = 0; f < contactId.length(); ++f ) hash += contactId[f].unicode() * f; const TQString colorName = nameColors[ hash % nameColorsLen ]; TQString lightColorName; // Do not initialize, TQColor::name() is expensive! kdDebug(14000) << k_funcinfo << "Hash " << hash << " has color " << colorName << endl; TQRegExp senderColorRegExp("%senderColor(?:\\{([^}]*)\\})?%"); textPos=0; while( (textPos=senderColorRegExp.search(resultHTML, textPos) ) != -1 ) { int light=100; bool doLight=false; if(senderColorRegExp.numCaptures()>=1) { light=senderColorRegExp.cap(1).toUInt(&doLight); } // Lazily init light color if ( doLight && lightColorName.isNull() ) lightColorName = TQColor( colorName ).light( light ).name(); resultHTML = resultHTML.replace( textPos , senderColorRegExp.cap(0).length(), doLight ? lightColorName : colorName ); } // Replace message at the end, maybe someone could put a Adium keyword in his message :P resultHTML = resultHTML.replace( TQString::fromUtf8("%message%"), formatMessageBody(message) ); // TODO: %status // resultHTML = addNickLinks( resultHTML ); return resultHTML; } // Style formatting for header and footer. TQString ChatMessagePart::formatStyleKeywords( const TQString &sourceHTML ) { TQString resultHTML = sourceHTML; Kopete::Contact *remoteContact = d->manager->members().getFirst(); // Verify that all contacts are not null before doing anything if( remoteContact && d->manager->myself() ) { TQString sourceName, destinationName; // Use contact nickname for ourselfs, Myself metacontact display name isn't a reliable source. sourceName = d->manager->myself()->nickName(); if( remoteContact->metaContact() ) destinationName = remoteContact->metaContact()->displayName(); else destinationName = remoteContact->nickName(); // Replace %chatName%, create a internal span to update it by DOM when asked. resultHTML = resultHTML.replace( TQString::fromUtf8("%chatName%"), TQString("%1").arg( formatName(d->manager->displayName()) ) ); // Replace %sourceName% resultHTML = resultHTML.replace( TQString::fromUtf8("%sourceName%"), formatName(sourceName) ); // Replace %destinationName% resultHTML = resultHTML.replace( TQString::fromUtf8("%destinationName%"), formatName(destinationName) ); // For %timeOpened%, display the date and time (also the seconds). resultHTML = resultHTML.replace( TQString::fromUtf8("%timeOpened%"), TDEGlobal::locale()->formatDateTime( TQDateTime::currentDateTime(), true, true ) ); // Look for %timeOpened{X}% TQRegExp timeRegExp("%timeOpened\\{([^}]*)\\}%"); int pos=0; while( (pos=timeRegExp.search(resultHTML, pos) ) != -1 ) { TQString timeKeyword = formatTime( timeRegExp.cap(1), TQDateTime::currentDateTime() ); resultHTML = resultHTML.replace( pos , timeRegExp.cap(0).length() , timeKeyword ); } // Get contact image paths #if 0 TQString photoIncomingPath, photoOutgoingPath; photoIncomingPath = remoteContact->property( Kopete::Global::Properties::self()->photo().key()).value().toString(); photoOutgoingPath = d->manager->myself()->property(Kopete::Global::Properties::self()->photo().key()).value().toString(); if( photoIncomingPath.isEmpty() ) photoIncomingPath = TQString::fromUtf8("Incoming/buddy_icon.png"); if( photoOutgoingPath.isEmpty() ) photoOutgoingPath = TQString::fromUtf8("Outgoing/buddy_icon.png"); resultHTML = resultHTML.replace( TQString::fromUtf8("%incomingIconPath%"), photoIncomingPath); resultHTML = resultHTML.replace( TQString::fromUtf8("%outgoingIconPath%"), photoOutgoingPath); #endif TQString photoIncoming, photoOutgoing; if( remoteContact->metaContact() && !remoteContact->metaContact()->picture().isNull() ) { photoIncoming = TQString("data:image/png;base64,%1").arg( remoteContact->metaContact()->picture().base64() ); } else { photoIncoming = TQString::fromUtf8("Incoming/buddy_icon.png"); } if( d->manager->myself()->metaContact() && !d->manager->myself()->metaContact()->picture().isNull() ) { photoOutgoing = TQString("data:image/png;base64,%1").arg( d->manager->myself()->metaContact()->picture().base64() ); } else { photoOutgoing = TQString::fromUtf8("Outgoing/buddy_icon.png"); } resultHTML = resultHTML.replace( TQString::fromUtf8("%incomingIconPath%"), photoIncoming); resultHTML = resultHTML.replace( TQString::fromUtf8("%outgoingIconPath%"), photoOutgoing ); } return resultHTML; } TQString ChatMessagePart::formatTime(const TQString &timeFormat, const TQDateTime &dateTime) { char buffer[256]; time_t timeT; struct tm *loctime; // Get current time timeT = dateTime.toTime_t(); // Convert it to local time representation. loctime = localtime (&timeT); strftime (buffer, 256, timeFormat.ascii(), loctime); return TQString(buffer); } TQString ChatMessagePart::formatName(const TQString &sourceName) { TQString formattedName = sourceName; // Escape the name. formattedName = Kopete::Message::escape(formattedName); // Squeeze the nickname if the user want it if( KopetePrefs::prefs()->truncateContactNames() ) { formattedName = KStringHandler::csqueeze( sourceName, KopetePrefs::prefs()->maxConactNameLength() ); } return formattedName; } TQString ChatMessagePart::formatMessageBody(const Kopete::Message &message) { TQString formattedBody("%1").arg(message.parsedBody()); return formattedBody; } void ChatMessagePart::slotUpdateHeaderDisplayName() { kdDebug(14000) << k_funcinfo << endl; DOM::HTMLElement kopeteChatNameNode = document().getElementById( TQString::fromUtf8("KopeteHeaderChatNameInternal") ); if( !kopeteChatNameNode.isNull() ) kopeteChatNameNode.setInnerText( formatName(d->manager->displayName()) ); } void ChatMessagePart::slotUpdateHeaderPhoto() { // Do the actual style switch // Wait for the event loop before switching the style TQTimer::singleShot( 0, this, TQT_SLOT(changeStyle()) ); } void ChatMessagePart::changeStyle() { #ifdef STYLE_TIMETEST TQTime beforeChange = TQTime::currentTime(); #endif // Make latestContact null to reset consecutives messages. d->latestContact = 0; // Rewrite the header and footer. writeTemplate(); // Readd all current messages. TQValueList::ConstIterator it, itEnd = d->allMessages.constEnd(); for(it = d->allMessages.constBegin(); it != itEnd; ++it) { Kopete::Message tempMessage = *it; appendMessage(tempMessage, true); // true means that we are restoring. } kdDebug(14000) << k_funcinfo << "Finish changing style." << endl; #ifdef STYLE_TIMETEST kdDebug(14000) << "Change time: " << beforeChange.msecsTo( TQTime::currentTime()) << endl; #endif } void ChatMessagePart::writeTemplate() { kdDebug(14000) << k_funcinfo << endl; #ifdef STYLE_TIMETEST TQTime beforeHeader = TQTime::currentTime(); #endif // Clear all the page, and begin a new page. begin(); // NOTE: About styles // Order of style tag in the template is important. // mainStyle take over all other style definition (which is what we want). // // KopeteStyle: Kopete appearance configuration into a style. It loaded first because // we don't want Kopete settings to override CSS Chat Window Style. // baseStyle: Import the main.css from the Chat Window Style // mainStyle: Currrent variant CSS url. // FIXME: Maybe this string should be load from a file, then parsed for args. TQString xhtmlBase; xhtmlBase += TQString("\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "%2\n" "

\n
\n" "%3\n" "" "" ).arg( d->currentChatStyle->getStyleBaseHref() ) .arg( formatStyleKeywords(d->currentChatStyle->getHeaderHtml()) ) .arg( formatStyleKeywords(d->currentChatStyle->getFooterHtml()) ) .arg( KopetePrefs::prefs()->styleVariant() ) .arg( styleHTML() ); write(xhtmlBase); end(); #ifdef STYLE_TIMETEST kdDebug(14000) << "Header time: " << beforeHeader.msecsTo( TQTime::currentTime()) << endl; #endif } #include "chatmessagepart.moc"