You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1379 lines
46 KiB
1379 lines
46 KiB
/*
|
|
chatmessagepart.cpp - Chat Message KPart
|
|
|
|
Copyright (c) 2002-2005 by Olivier Goffart <ogoffart @ kde.org>
|
|
Copyright (c) 2002-2003 by Martijn Klingens <klingens@kde.org>
|
|
Copyright (c) 2004 by Richard Smith <kde@metafoo.co.uk>
|
|
Copyright (c) 2005-2006 by Michaël Larouche <michael.larouche@kdemail.net>
|
|
|
|
Kopete (c) 2002-2005 by the Kopete developers <kopete-devel@kde.org>
|
|
|
|
*************************************************************************
|
|
* *
|
|
* 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 <ctime>
|
|
|
|
// TQt includes
|
|
#include <tqclipboard.h>
|
|
#include <tqtooltip.h>
|
|
#include <tqrect.h>
|
|
#include <tqcursor.h>
|
|
#include <tqptrlist.h>
|
|
#include <tqregexp.h>
|
|
#include <tqvaluelist.h>
|
|
#include <tqtimer.h>
|
|
#include <tqstylesheet.h>
|
|
|
|
// TDEHTML::DOM includes
|
|
#include <dom/dom_doc.h>
|
|
#include <dom/dom_text.h>
|
|
#include <dom/dom_element.h>
|
|
#include <dom/html_base.h>
|
|
#include <dom/html_document.h>
|
|
#include <dom/html_inline.h>
|
|
#include <tqurloperator.h>
|
|
|
|
|
|
// KDE includes
|
|
#include <tdeapplication.h>
|
|
#include <kdebug.h>
|
|
#include <tdeversion.h>
|
|
#include <tdefiledialog.h>
|
|
#include <tdehtmlview.h>
|
|
#include <tdelocale.h>
|
|
#include <tdemessagebox.h>
|
|
#include <tdemultipledrag.h>
|
|
#include <tdepopupmenu.h>
|
|
#include <krun.h>
|
|
#include <kstringhandler.h>
|
|
#include <tdetempfile.h>
|
|
#include <kurldrag.h>
|
|
#include <tdeio/netaccess.h>
|
|
#include <tdeio/job.h>
|
|
#include <kstandarddirs.h>
|
|
#include <kiconloader.h>
|
|
#include <kinputdialog.h>
|
|
|
|
// 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<Kopete::Message> 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( TQ_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("<qt><img src=\"%1\"><br>Insert the string for the emoticon<br>separated by space if you want multiple strings</qt>").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<Kopete::Message>::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("<qt>Could not open <b>%1</b> for writing.</qt>").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 <div id="insert" />
|
|
// 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<Kopete::Contact*>(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 <p> */ )
|
|
.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<Kopete::Contact> it ( d->manager->members() ); it.current(); ++it )
|
|
if ( (*it)->contactId() == contactId )
|
|
return *it;
|
|
}
|
|
else
|
|
{
|
|
TQString nick = element.innerText().string().stripWhiteSpace();
|
|
for ( TQPtrListIterator<Kopete::Contact> 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 <!DOCTYPE> 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("<a href=\"kopetemessage://%1/?protocolId=%2&accountId=%3\" class=\"KopeteDisplayName\">")
|
|
.arg( TQStyleSheet::escape(message.from()->contactId()).replace('"',"""),
|
|
TQStyleSheet::escape(message.from()->protocol()->pluginId()).replace('"',"""),
|
|
TQStyleSheet::escape(message.from()->account()->accountId() ).replace('"',"""));
|
|
}
|
|
else
|
|
{
|
|
nickLink="<a>";
|
|
}
|
|
|
|
|
|
// Replace sender (contact nick)
|
|
resultHTML = resultHTML.replace( TQString::fromUtf8("%sender%"), nickLink+nick+"</a>" );
|
|
// 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)+"</a>" );
|
|
// 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<b>%2</b></a> ").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("<span id=\"KopeteHeaderChatNameInternal\">%1</span>").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("<span ");
|
|
|
|
formattedBody += message.getHtmlStyleAttribute();
|
|
|
|
// Affect the parsed body.
|
|
formattedBody += TQString::fromUtf8("class=\"KopeteMessageBody\">%1</span>").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<Kopete::Message>::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("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
|
|
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\"\n"
|
|
"\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n"
|
|
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
|
|
"<head>\n"
|
|
"<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\n\" />\n"
|
|
"<base href=\"%1\">\n"
|
|
"<style id=\"KopeteStyle\" type=\"text/css\" media=\"screen,print\">\n"
|
|
" %5\n"
|
|
"</style>\n"
|
|
"<style id=\"baseStyle\" type=\"text/css\" media=\"screen,print\">\n"
|
|
" @import url(\"main.css\");\n"
|
|
" *{ word-wrap:break-word; }\n"
|
|
"</style>\n"
|
|
"<style id=\"mainStyle\" type=\"text/css\" media=\"screen,print\">\n"
|
|
" @import url(\"%4\");\n"
|
|
"</style>\n"
|
|
"</head>\n"
|
|
"<body>\n"
|
|
"%2\n"
|
|
"<div id=\"Chat\">\n</div>\n"
|
|
"%3\n"
|
|
"</body>"
|
|
"</html>"
|
|
).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"
|