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

kopete/kopete

  • sources
  • kde-4.14
  • kdenetwork
  • kopete
  • kopete
  • chatwindow
chatmessagepart.cpp
Go to the documentation of this file.
1 /*
2  chatmessagepart.cpp - Chat Message KPart
3 
4  Copyright (c) 2002-2005 by Olivier Goffart <ogoffart@kde.org>
5  Copyright (c) 2002-2003 by Martijn Klingens <klingens@kde.org>
6  Copyright (c) 2004 by Richard Smith <kde@metafoo.co.uk>
7  Copyright (c) 2005-2006 by MichaĆ«l Larouche <larouche@kde.org>
8  Copyright (c) 2008 by Roman Jarosz <kedgedev@centrum.cz>
9 
10  Kopete (c) 2002-2008 by the Kopete developers <kopete-devel@kde.org>
11 
12  *************************************************************************
13  * *
14  * This program is free software; you can redistribute it and/or modify *
15  * it under the terms of the GNU General Public License as published by *
16  * the Free Software Foundation; either version 2 of the License, or *
17  * (at your option) any later version. *
18  * *
19  *************************************************************************
20 */
21 
22 #include "chatmessagepart.h"
23 
24 // STYLE_TIMETEST is for time staticstic gathering.
25 //#define STYLE_TIMETEST
26 
27 #include <ctime>
28 
29 // Qt includes
30 #include <QtCore/QByteArray>
31 #include <QtCore/QLatin1String>
32 #include <QtCore/QList>
33 #include <QtCore/QPointer>
34 #include <QtCore/QRect>
35 #include <QtCore/QRegExp>
36 #include <QtCore/QTextCodec>
37 #include <QtCore/QTextStream>
38 #include <QtCore/QTimer>
39 #include <QtCore/QBuffer>
40 #include <QtGui/QClipboard>
41 #include <QtGui/QCursor>
42 #include <QtGui/QPixmap>
43 #include <QtGui/QTextDocument>
44 #include <QtGui/QScrollBar>
45 #include <QMimeData>
46 #include <QApplication>
47 #include <QFileDialog>
48 
49 #include <Phonon/MediaObject>
50 #include <Phonon/Path>
51 #include <Phonon/AudioOutput>
52 #include <Phonon/Global>
53 
54 // KHTML::DOM includes
55 #include <dom/dom_doc.h>
56 #include <dom/dom_text.h>
57 #include <dom/dom_element.h>
58 #include <dom/html_base.h>
59 #include <dom/html_document.h>
60 #include <dom/html_inline.h>
61 #include <dom/html_form.h>
62 #include <dom/dom2_events.h>
63 
64 
65 // KDE includes
66 #include <kactioncollection.h>
67 #include <kdebug.h>
68 #include <kdeversion.h>
69 #include <kfiledialog.h>
70 #include <khtmlview.h>
71 #include <klocale.h>
72 #include <kmessagebox.h>
73 #include <kmenu.h>
74 #include <krun.h>
75 #include <kstringhandler.h>
76 #include <ktemporaryfile.h>
77 #include <kio/copyjob.h>
78 #include <kstandardaction.h>
79 #include <kiconloader.h>
80 #include <kcodecs.h>
81 #include <kicon.h>
82 
83 // Kopete includes
84 #include "kopetecontact.h"
85 #include "kopetecontactlist.h"
86 #include "kopetechatwindow.h"
87 #include "kopetechatsession.h"
88 #include "kopetemetacontact.h"
89 #include "kopetepluginmanager.h"
90 #include "kopeteprotocol.h"
91 #include "kopeteaccount.h"
92 #include "kopeteglobal.h"
93 #include "kopeteemoticons.h"
94 #include "kopeteview.h"
95 #include "kopetepicture.h"
96 #include "kopeteappearancesettings.h"
97 #include "kopetebehaviorsettings.h"
98 #include "kopetechatwindowsettings.h"
99 #include "kopetetransfermanager.h"
100 
101 #include "kopetechatwindowstyle.h"
102 #include "kopetechatwindowstylemanager.h"
103 
104 static const uint ConsecutiveMessageTimeout = 15 /* minutes */ * 60 /* (seconds/minute) */;
105 
106 class ToolTip;
107 
108 class ChatMessagePart::Private
109 {
110 public:
111  Private()
112  : /*tt(0L),*/ scrollPressed(false), scrollToEndDelayed(false), manager(0),
113  copyAction(0), saveAction(0), printAction(0),
114  closeAction(0),copyURLAction(0), currentChatStyle(0),
115  latestDirection(Kopete::Message::Inbound), latestType(Kopete::Message::TypeNormal),
116  latestTime(0), htmlEventListener(0)
117  {}
118 
119  ~Private()
120  {
121  // Don't delete manager and latestContact, because they could be still used.
122  // Don't delete currentChatStyle, it is handled by ChatWindowStyleManager.
123  }
124 
125  bool fmtOverride;
126 
127 // ToolTip *tt;
128  bool scrollPressed;
129  bool scrollToEndDelayed;
130  Kopete::ChatSession *manager;
131 
132  DOM::HTMLElement activeElement;
133 
134  KAction *copyAction;
135  KAction *saveAction;
136  KAction *printAction;
137  KAction *closeAction;
138  KAction *copyURLAction;
139 
140  ChatWindowStyle* currentChatStyle;
141  QPointer<Kopete::Contact> latestContact;
142  Kopete::Message::MessageDirection latestDirection;
143  Kopete::Message::MessageType latestType;
144  uint latestTime;
145  // Yep I know it will take memory, but I don't have choice
146  // to enable on-the-fly style changing.
147  QList<Kopete::Message> allMessages;
148 
149  // No need to delete, HTMLEventListener is ref counted.
150  QPointer<HTMLEventListener> htmlEventListener;
151 
152  QFont chatFont;
153 };
154 /*
155 class ChatMessagePart::ToolTip : public Q3ToolTip
156 {
157 public:
158  ToolTip( ChatMessagePart *c ) : Q3ToolTip( c->view()->viewport() )
159  {
160  m_chat = c;
161  }
162 
163  void maybeTip( const QPoint &p )
164  {
165  // FIXME: it's wrong to look for the node under the mouse - this makes too many
166  // assumptions about how tooltips work. but there is no nodeAtPoint.
167  DOM::Node node = m_chat->nodeUnderMouse();
168  Kopete::Contact *contact = m_chat->contactFromNode( node );
169  QString toolTipText;
170 
171  if(node.isNull())
172  return;
173 
174  // this tooltip is attached to the viewport widget, so translate the node's rect
175  // into its coordinates.
176  QRect rect = node.getRect();
177  rect = QRect( m_chat->view()->contentsToViewport( rect.topLeft() ),
178  m_chat->view()->contentsToViewport( rect.bottomRight() ) );
179 
180  if( contact )
181  {
182  toolTipText = contact->toolTip();
183  }
184  else
185  {
186  m_chat->emitTooltipEvent( m_chat->textUnderMouse(), toolTipText );
187 
188  if( toolTipText.isEmpty() )
189  {
190  //Fall back to the title attribute
191  for( DOM::HTMLElement element = node; !element.isNull(); element = element.parentNode() )
192  {
193  if( element.hasAttribute( "title" ) )
194  {
195  toolTipText = element.getAttribute( "title" ).string();
196  break;
197  }
198  }
199  }
200  }
201 
202  if( !toolTipText.isEmpty() )
203  tip( rect, toolTipText );
204  }
205 
206 private:
207  ChatMessagePart *m_chat;
208 };
209 */
210 
211 ChatMessagePart::ChatMessagePart( Kopete::ChatSession *mgr, QWidget *parent )
212  : KHTMLPart( parent ), d( new Private )
213 {
214  d->manager = mgr;
215  d->currentChatStyle = ChatWindowStyleManager::self()->getValidStyleFromPool( KopeteChatWindowSettings::self()->styleName() );
216  if (d->currentChatStyle)
217  connect( d->currentChatStyle, SIGNAL(destroyed(QObject*)), this, SLOT(clearStyle()) );
218 
219  connect( this, SIGNAL(completed()), this, SLOT(slotRenderingFinished()) );
220 
221  //Security settings, we don't need this stuff
222  setJScriptEnabled( false ) ;
223  setJavaEnabled( false );
224  setPluginsEnabled( false );
225  setMetaRefreshEnabled( false );
226  setOnlyLocalReferences( true );
227 
228  // read the font for the chat
229  readChatFont();
230 
231  // Write the template to KHTMLPart
232  writeTemplate();
233 
234  // It is not possible to drag and drop on our widget
235  view()->setAcceptDrops(false);
236 
237  connect( Kopete::AppearanceSettings::self(), SIGNAL(messageOverridesChanged()),
238  this, SLOT(slotAppearanceChanged()) );
239  connect( Kopete::AppearanceSettings::self(), SIGNAL(appearanceChanged()),
240  this, SLOT(slotRefreshView()) );
241  connect( KopeteChatWindowSettings::self(), SIGNAL(chatwindowAppearanceChanged()),
242  this, SLOT(slotRefreshView()) );
243  connect( KopeteChatWindowSettings::self(), SIGNAL(styleChanged(QString)),
244  this, SLOT(setStyle(QString)) );
245  connect( KopeteChatWindowSettings::self(), SIGNAL(styleVariantChanged(QString)),
246  this, SLOT(setStyleVariant(QString)) );
247 
248  // Refresh the style if the display name change.
249  connect( d->manager, SIGNAL(displayNameChanged()), this, SLOT(slotUpdateHeaderDisplayName()) );
250  connect( d->manager, SIGNAL(photoChanged()), this, SLOT(slotUpdateHeaderPhoto()) );
251 
252  connect( d->manager, SIGNAL(messageStateChanged(uint,Kopete::Message::MessageState)),
253  this, SLOT(messageStateChanged(uint,Kopete::Message::MessageState)) );
254 
255  connect ( browserExtension(), SIGNAL(openUrlRequestDelayed(KUrl,KParts::OpenUrlArguments,KParts::BrowserArguments)),
256  this, SLOT(slotOpenURLRequest(KUrl,KParts::OpenUrlArguments,KParts::BrowserArguments)) );
257 
258  connect( this, SIGNAL(popupMenu(QString,QPoint)),
259  this, SLOT(slotRightClick(QString,QPoint)) );
260 
261  // setup scrollbar
262  view()->verticalScrollBar()->setTracking(true);
263  connect( view()->verticalScrollBar(), SIGNAL(valueChanged(int)),
264  this, SLOT(slotScrollingTo(int)) );
265 
266  connect( Kopete::TransferManager::transferManager(), SIGNAL(askIncomingDone(uint)),
267  this, SLOT(slotFileTransferIncomingDone(uint)) );
268 
269  connect( KGlobalSettings::self(), SIGNAL(kdisplayFontChanged()),
270  this, SLOT(slotRefreshView()) );
271 
272  //initActions
273  d->copyAction = KStandardAction::copy( this, SLOT(copy()), actionCollection() );
274  d->saveAction = KStandardAction::saveAs( this, SLOT(save()), actionCollection() );
275  d->printAction = KStandardAction::print( this, SLOT(print()),actionCollection() );
276  d->closeAction = KStandardAction::close( this, SLOT(slotCloseView()),actionCollection() );
277  d->copyURLAction = new KAction( KIcon("edit-copy"), i18n( "Copy Link Address" ), actionCollection() );
278  actionCollection()->addAction( "editcopy", d->copyURLAction );
279  connect( d->copyURLAction, SIGNAL(triggered(bool)), this, SLOT(slotCopyURL()) );
280 
281  // read formatting override flags
282  readOverrides();
283 }
284 
285 ChatMessagePart::~ChatMessagePart()
286 {
287  kDebug(14000) ;
288 
289  // Cancel all pending file transfer requests
290  QList<Kopete::Message>::ConstIterator it, itEnd = d->allMessages.constEnd();
291  for ( it = d->allMessages.constBegin(); it != itEnd; ++it )
292  {
293  if ( (*it).type() == Kopete::Message::TypeFileTransferRequest && !(*it).fileTransferDisabled() )
294  {
295  Kopete::TransferManager::transferManager()->cancelIncomingTransfer( (*it).id() );
296  }
297  }
298 
299  //delete d->tt;
300  delete d;
301 }
302 
303 void ChatMessagePart::slotScrollingTo( int y )
304 {
305  int scrolledTo = y + view()->visibleHeight();
306  d->scrollPressed = scrolledTo < ( view()->contentsHeight() - 10 );
307 }
308 
309 void ChatMessagePart::save()
310 {
311  const KUrl dummyUrl;
312  QPointer <KFileDialog> dlg = new KFileDialog( dummyUrl, QLatin1String( "text/html text/plain" ), view() );
313  dlg->setCaption( i18n( "Save Conversation" ) );
314  dlg->setOperationMode( KFileDialog::Saving );
315 
316  if ( dlg->exec() != QDialog::Accepted )
317  {
318  delete dlg;
319  return;
320  }
321 
322  if ( ! dlg )
323  return;
324 
325  KUrl saveURL = dlg->selectedUrl();
326  KTemporaryFile *tempFile = new KTemporaryFile();
327  tempFile->setAutoRemove(false);
328  tempFile->open();
329 
330  QTextStream stream ( tempFile );
331  stream.setCodec(QTextCodec::codecForName("UTF-8"));
332 
333  if ( dlg->currentFilter() == QLatin1String( "text/plain" ) )
334  {
335  QList<Kopete::Message>::ConstIterator it, itEnd = d->allMessages.constEnd();
336  for(it = d->allMessages.constBegin(); it != itEnd; ++it)
337  {
338  Kopete::Message tempMessage = *it;
339  stream << "[" << KGlobal::locale()->formatDateTime(tempMessage.timestamp()) << "] ";
340  if( tempMessage.from() && tempMessage.from()->metaContact() )
341  {
342  stream << formatName(tempMessage.from()->metaContact()->displayName(), Qt::RichText);
343  }
344  stream << ": " << tempMessage.plainBody() << "\n";
345  }
346  }
347  else
348  {
349  stream << htmlDocument().toString().string() << '\n';
350  }
351 
352  delete dlg;
353 
354  stream.flush();
355  QString fileName = tempFile->fileName();
356  delete tempFile;
357 
358  KIO::CopyJob *moveJob = KIO::move( KUrl( fileName ), saveURL, KIO::HideProgressInfo );
359 
360  if ( !moveJob )
361  {
362  KMessageBox::queuedMessageBox( view(), KMessageBox::Error,
363  i18n("<qt>Could not open <b>%1</b> for writing.</qt>", saveURL.prettyUrl() ), // Message
364  i18n("Error While Saving") ); //Caption
365  }
366 }
367 
368 void ChatMessagePart::pageUp()
369 {
370  view()->scrollBy( 0, -view()->visibleHeight() );
371 }
372 
373 void ChatMessagePart::pageDown()
374 {
375  view()->scrollBy( 0, view()->visibleHeight() );
376 }
377 
378 void ChatMessagePart::slotOpenURLRequest(const KUrl &url, const KParts::OpenUrlArguments &, const KParts::BrowserArguments &)
379 {
380  kDebug(14000) << "url=" << url.url();
381  if ( url.protocol() == QLatin1String("kopetemessage") )
382  {
383  Kopete::Contact *contact = d->manager->account()->contacts().value( url.host() );
384  if ( contact )
385  contact->execute();
386  }
387  else
388  {
389  KRun *runner = new KRun( url, 0, 0, false ); // false = non-local files
390  runner->setRunExecutables( false ); //security
391  //KRun autodeletes itself by default when finished.
392  }
393 }
394 
395 void ChatMessagePart::slotFileTransferIncomingDone( unsigned int id )
396 {
397  QList<Kopete::Message>::Iterator it = d->allMessages.end();
398  while ( it != d->allMessages.begin() )
399  {
400  --it;
401  if ( (*it).id() == id )
402  {
403  (*it).setFileTransferDisabled( true );
404  disableFileTransferButtons( id );
405  break;
406  }
407  }
408 }
409 
410 void ChatMessagePart::readOverrides()
411 {
412  d->fmtOverride = Kopete::AppearanceSettings::self()->chatFmtOverride();
413 }
414 
415 void ChatMessagePart::slotToggleGraphicOverride(bool)
416 {
417 }
418 
419 void ChatMessagePart::setStyle( const QString &styleName )
420 {
421  // Create a new ChatWindowStyle
422  setStyle( ChatWindowStyleManager::self()->getValidStyleFromPool(styleName) );
423 }
424 
425 void ChatMessagePart::setStyle( ChatWindowStyle *style )
426 {
427  // Change the current style
428  if (d->currentChatStyle)
429  disconnect( d->currentChatStyle, SIGNAL(destroyed(QObject*)), this, SLOT(clearStyle()) );
430 
431  d->currentChatStyle = style;
432  if (d->currentChatStyle)
433  connect( d->currentChatStyle, SIGNAL(destroyed(QObject*)), this, SLOT(clearStyle()) );
434 
435  // Do the actual style switch
436  // Wait for the event loop before switching the style
437  QTimer::singleShot( 0, this, SLOT(changeStyle()) );
438 }
439 
440 void ChatMessagePart::clearStyle()
441 {
442  setStyle( QString() );
443 }
444 
445 void ChatMessagePart::setStyleVariant( const QString &variantPath )
446 {
447  DOM::HTMLElement variantNode = document().getElementById( QString("mainStyle") );
448  if( !variantNode.isNull() )
449  variantNode.setInnerText( QString("@import url(\"%1\");").arg( adjustStyleVariantForChatSession( variantPath) ) );
450 }
451 
452 void ChatMessagePart::messageStateChanged( uint messageId, Kopete::Message::MessageState state )
453 {
454  QList<Kopete::Message>::Iterator it = d->allMessages.end();
455  while ( it != d->allMessages.begin() )
456  {
457  --it;
458  if ( (*it).id() == messageId )
459  {
460  (*it).setState( state );
461  changeMessageStateElement( messageId, state );
462  break;
463  }
464  }
465 }
466 
467 void ChatMessagePart::slotAppearanceChanged()
468 {
469  readOverrides();
470 
471  changeStyle();
472 }
473 
474 void ChatMessagePart::appendMessage( Kopete::Message &message, bool restoring )
475 {
476  if ( !d->currentChatStyle )
477  return;
478 
479  if ( !message.classes().contains("history") )
480  message.setFormattingOverride( d->fmtOverride );
481 
482 #ifdef STYLE_TIMETEST
483  QTime beforeMessage = QTime::currentTime();
484 #endif
485 
486  QString formattedMessageHtml;
487  bool isConsecutiveMessage = false;
488  int bufferLen = Kopete::BehaviorSettings::self()->chatWindowBufferViewSize();
489 
490  // Find the "Chat" div element.
491  // If the "Chat" div element is not found, do nothing. It's the central part of Adium format.
492  DOM::HTMLElement chatNode = htmlDocument().getElementById( "Chat" );
493 
494  if( chatNode.isNull() )
495  {
496  kDebug(14000) << "WARNING: Chat Node was null !";
497  return;
498  }
499 
500  // Check if it's a consecutive Message
501  // Consecutive messages are only for normal messages, status messages do not have a <div id="insert" />
502  // We check if the from() is the latestContact, because consecutive incoming/outgoing message can come from differents peopole(in groupchat and IRC)
503  // Group only if the user want it.
504  if( KopeteChatWindowSettings::self()->groupConsecutiveMessages() )
505  {
506  isConsecutiveMessage = (message.direction() == d->latestDirection && !d->latestContact.isNull()
507  && d->latestContact == message.from() && message.type() == d->latestType
508  && message.type() != Kopete::Message::TypeFileTransferRequest );
509 
510  if(message.timestamp().isValid()){
511  uint next = message.timestamp().toTime_t();
512  if(isConsecutiveMessage && (next - d->latestTime) > ConsecutiveMessageTimeout){
513  isConsecutiveMessage = false;
514  }
515  d->latestTime = next;
516  }
517  }
518 
519  // Don't test it in the switch to don't break consecutive messages.
520  if(message.type() == Kopete::Message::TypeAction)
521  {
522  // Check if chat style support Action template (Kopete extension)
523  if( d->currentChatStyle->hasActionTemplate() )
524  {
525  switch(message.direction())
526  {
527  case Kopete::Message::Inbound:
528  formattedMessageHtml = d->currentChatStyle->getActionIncomingHtml();
529  break;
530  case Kopete::Message::Outbound:
531  formattedMessageHtml = d->currentChatStyle->getActionOutgoingHtml();
532  break;
533  default:
534  break;
535  }
536  }
537  // Use status template if no Action template.
538  else
539  {
540  formattedMessageHtml = d->currentChatStyle->getStatusHtml();
541  }
542  }
543  else if(message.type() == Kopete::Message::TypeFileTransferRequest)
544  {
545  formattedMessageHtml = d->currentChatStyle->getFileTransferIncomingHtml();
546  }
547  else if(message.type() == Kopete::Message::TypeVoiceClipRequest)
548  {
549  formattedMessageHtml = d->currentChatStyle->getVoiceClipIncomingHtml();
550  }
551  else
552  {
553  switch(message.direction())
554  {
555  case Kopete::Message::Inbound:
556  {
557  if(isConsecutiveMessage)
558  {
559  formattedMessageHtml = d->currentChatStyle->getNextIncomingHtml();
560  }
561  else
562  {
563  formattedMessageHtml = d->currentChatStyle->getIncomingHtml();
564  }
565  break;
566  }
567  case Kopete::Message::Outbound:
568  {
569  if(isConsecutiveMessage)
570  {
571  formattedMessageHtml = d->currentChatStyle->getNextOutgoingHtml();
572  }
573  else
574  {
575  formattedMessageHtml = d->currentChatStyle->getOutgoingHtml();
576  }
577  break;
578  }
579  case Kopete::Message::Internal:
580  {
581  formattedMessageHtml = d->currentChatStyle->getStatusHtml();
582  break;
583  }
584  }
585  }
586 
587  formattedMessageHtml = formatStyleKeywords( formattedMessageHtml, message );
588 
589  // newMessageNode is common to both code path
590  // FIXME: Find a better than to create a dummy span.
591  DOM::HTMLElement newMessageNode = document().createElement( QString("span") );
592  newMessageNode.setInnerHTML( formattedMessageHtml );
593 
594  // Find the insert Node
595  DOM::HTMLElement insertNode = document().getElementById( QString("insert") );
596 
597  if( isConsecutiveMessage && !insertNode.isNull() )
598  {
599  // Replace the insert block, because it's a consecutive message.
600  insertNode.parentNode().replaceChild(newMessageNode, insertNode);
601  }
602  else
603  {
604  // Remove the insert block, because it's a new message.
605  if( !insertNode.isNull() )
606  insertNode.parentNode().removeChild(insertNode);
607  // Append to the chat.
608  chatNode.appendChild(newMessageNode);
609  }
610 
611  if ( message.type() == Kopete::Message::TypeNormal )
612  {
613  if ( message.direction() == Kopete::Message::Outbound )
614  changeMessageStateElement( message.id(), message.state() );
615  }
616  else if ( message.type() == Kopete::Message::TypeFileTransferRequest )
617  {
618  if ( message.fileTransferDisabled() )
619  disableFileTransferButtons( message.id() );
620  else
621  addFileTransferButtonsEventListener( message.id() );
622  }
623  else if ( message.type() == Kopete::Message::TypeVoiceClipRequest )
624  {
625  addVoiceClipsButtonsEventListener( message.id() );
626  }
627 
628  // Keep the direction to see on next message
629  // if it's a consecutive message
630  // Keep also the from() contact.
631  d->latestDirection = message.direction();
632  d->latestType = message.type();
633  d->latestContact = const_cast<Kopete::Contact*>(message.from());
634 
635  // Add the message to the list for futher restoring if needed
636  if(!restoring)
637  d->allMessages.append(message);
638 
639  while ( bufferLen>0 && d->allMessages.count() >= bufferLen )
640  {
641  d->allMessages.pop_front();
642 
643  // FIXME: Find a way to make work Chat View Buffer efficiently with consecutives messages.
644  // Before it was calling changeStyle() but it's damn too slow.
645  if( !KopeteChatWindowSettings::self()->groupConsecutiveMessages() )
646  {
647  chatNode.removeChild( chatNode.firstChild() );
648  }
649  }
650 
651  if ( !d->scrollPressed )
652  QTimer::singleShot( 1, this, SLOT(slotScrollView()) );
653 
654 #ifdef STYLE_TIMETEST
655  kDebug(14000) << "Message time: " << beforeMessage.msecsTo( QTime::currentTime());
656 #endif
657 }
658 
659 void ChatMessagePart::slotRefreshView()
660 {
661  // refresh the chat font
662  readChatFont();
663 
664  DOM::HTMLElement kopeteNode = document().getElementById( QString("KopeteStyle") );
665  if( !kopeteNode.isNull() )
666  kopeteNode.setInnerText( styleHTML() );
667 
668  DOM::HTMLBodyElement bodyElement = htmlDocument().body();
669  bodyElement.setBgColor( Kopete::AppearanceSettings::self()->chatBackgroundColor().name() );
670 }
671 
672 void ChatMessagePart::keepScrolledDown()
673 {
674  if ( !d->scrollPressed )
675  QTimer::singleShot( 1, this, SLOT(slotScrollView()) );
676 }
677 
678 const QString ChatMessagePart::styleHTML() const
679 {
680  Kopete::AppearanceSettings *settings = Kopete::AppearanceSettings::self();
681 
682  QString style = QString(
683  "body{background-color:%1;font-family:%2;font-size:%3pt;color:%4}"
684  "td{font-family:%5;font-size:%6pt;color:%7}"
685  "input{font-family:%8;font-size:%9pt;color:%10}"
686  "a{color:%11}a.visited{color:%12}"
687  "a.KopeteDisplayName{text-decoration:none;color:inherit;}"
688  "a.KopeteDisplayName:hover{text-decoration:underline;color:inherit}"
689  ".KopeteLink{cursor:pointer;}.KopeteLink:hover{text-decoration:underline}"
690  ".KopeteMessageBody > p:first-child{margin:0;padding:0;display:inline;}" /* some html messages are encapsuled into a <p> */ )
691  .arg( settings->chatBackgroundColor().name() )
692  .arg( d->chatFont.family() )
693  .arg( d->chatFont.pointSize() )
694  .arg( settings->chatTextColor().name() )
695  .arg( d->chatFont.family() )
696  .arg( d->chatFont.pointSize() )
697  .arg( settings->chatTextColor().name() )
698  .arg( d->chatFont.family() )
699  .arg( d->chatFont.pointSize() )
700  .arg( settings->chatTextColor().name() )
701  .arg( settings->chatLinkColor().name() )
702  .arg( settings->chatLinkColor().name() );
703 
704  return style;
705 }
706 
707 void ChatMessagePart::clear()
708 {
709  // writeTemplate actually reset the HTML chat session from the beginning.
710  writeTemplate();
711 
712  // Reset consecutive messages
713  d->latestContact = 0;
714 
715  // Cancel all pending file transfer requests
716  QList<Kopete::Message>::ConstIterator it, itEnd = d->allMessages.constEnd();
717  for ( it = d->allMessages.constBegin(); it != itEnd; ++it )
718  {
719  if ( (*it).type() == Kopete::Message::TypeFileTransferRequest && !(*it).fileTransferDisabled() )
720  {
721  Kopete::TransferManager::transferManager()->cancelIncomingTransfer( (*it).id() );
722  }
723  }
724 
725  // Remove all stored messages.
726  d->allMessages.clear();
727 }
728 
729 Kopete::Contact *ChatMessagePart::contactFromNode( const DOM::Node &n ) const
730 {
731  DOM::Node node = n;
732  QList<Kopete::Contact*> m;
733 
734  if ( node.isNull() )
735  return 0;
736 
737  while ( !node.isNull() && ( node.nodeType() == DOM::Node::TEXT_NODE || ((DOM::HTMLElement)node).className() != "KopeteDisplayName" ) )
738  node = node.parentNode();
739 
740  DOM::HTMLElement element = node;
741  if ( element.className() != "KopeteDisplayName" )
742  return 0;
743 
744  m = d->manager->members();
745  if ( element.hasAttribute( "contactid" ) )
746  {
747  QString contactId = element.getAttribute( "contactid" ).string();
748  for ( int i =0; i != m.size(); ++i )
749  if ( m.at(i)->contactId() == contactId )
750  return m[i];
751  }
752  else
753  {
754  QString nick = element.innerText().string().trimmed();
755  foreach ( Kopete::Contact *contact, m )
756  {
757  QString contactNick;
758  if( contact->metaContact() && contact->metaContact() != Kopete::ContactList::self()->myself() )
759  contactNick = contact->metaContact()->displayName();
760  else
761  contactNick = contact->displayName();
762 
763  if ( contactNick == nick )
764  return contact;
765  }
766  }
767 
768  return 0;
769 }
770 
771 void ChatMessagePart::slotRightClick( const QString &, const QPoint &point )
772 {
773  // look through parents until we find an Element
774  DOM::Node activeNode = nodeUnderMouse();
775  while ( !activeNode.isNull() && activeNode.nodeType() != DOM::Node::ELEMENT_NODE )
776  activeNode = activeNode.parentNode();
777 
778  // make sure it's valid
779  d->activeElement = activeNode;
780  if ( d->activeElement.isNull() )
781  return;
782 
783  KMenu *chatWindowPopup = 0L;
784 
785  if ( Kopete::Contact *contact = contactFromNode( d->activeElement ) )
786  {
787  chatWindowPopup = contact->popupMenu();
788  connect( chatWindowPopup, SIGNAL(aboutToHide()), chatWindowPopup , SLOT(deleteLater()) );
789  }
790  else
791  {
792  chatWindowPopup = new KMenu();
793 
794  QAction *action;
795  if ( d->activeElement.className() == QLatin1String("KopeteDisplayName") )
796  {
797  action = chatWindowPopup->addAction( i18n( "User Has Left" ) );
798  action->setEnabled(false);
799  chatWindowPopup->addSeparator();
800  }
801  else if ( d->activeElement.tagName().lower() == QLatin1String( "a" ) )
802  {
803  chatWindowPopup->addAction( d->copyURLAction );
804  chatWindowPopup->addSeparator();
805  }
806 
807  d->copyAction->setEnabled( hasSelection() );
808  chatWindowPopup->addAction( d->copyAction );
809  chatWindowPopup->addAction( d->saveAction );
810  chatWindowPopup->addAction( d->printAction );
811  chatWindowPopup->addSeparator();
812  chatWindowPopup->addAction( d->closeAction );
813 
814  connect( chatWindowPopup, SIGNAL(aboutToHide()), chatWindowPopup, SLOT(deleteLater()) );
815  chatWindowPopup->popup( point );
816  }
817 
818  //Emit for plugin hooks
819  emit contextMenuEvent( textUnderMouse(), chatWindowPopup );
820 
821  chatWindowPopup->popup( point );
822 }
823 
824 QString ChatMessagePart::textUnderMouse()
825 {
826  DOM::Node activeNode = nodeUnderMouse();
827  if( activeNode.nodeType() != DOM::Node::TEXT_NODE )
828  return QString();
829 
830  DOM::Text textNode = activeNode;
831  QString data = textNode.data().string();
832 
833  //Ok, we have the whole node. Now, find the text under the mouse.
834  int mouseLeft = view()->mapFromGlobal( QCursor::pos() ).x(),
835  nodeLeft = activeNode.getRect().x(),
836  cPos = 0,
837  dataLen = data.length();
838 
839  QFontMetrics metrics( d->chatFont );
840  QString buffer;
841  while( cPos < dataLen && nodeLeft < mouseLeft )
842  {
843  QChar c = data[cPos++];
844  if( c.isSpace() )
845  buffer.truncate(0);
846  else
847  buffer += c;
848 
849  nodeLeft += metrics.width(c);
850  }
851 
852  if( cPos < dataLen )
853  {
854  QChar c = data[cPos++];
855  while( cPos < dataLen && !c.isSpace() )
856  {
857  buffer += c;
858  c = data[cPos++];
859  }
860  }
861 
862  return buffer;
863 }
864 
865 void ChatMessagePart::slotCopyURL()
866 {
867  DOM::HTMLAnchorElement a = d->activeElement;
868  if ( !a.isNull() )
869  {
870  QApplication::clipboard()->setText( a.href().string(), QClipboard::Clipboard );
871  QApplication::clipboard()->setText( a.href().string(), QClipboard::Selection );
872  }
873 }
874 
875 void ChatMessagePart::slotScrollView()
876 {
877  if ( inProgress() )
878  d->scrollToEndDelayed = true;
879  else
880  view()->scrollBy( 0, view()->contentsHeight() );
881 }
882 
883 void ChatMessagePart::slotRenderingFinished()
884 {
885  if ( d->scrollToEndDelayed )
886  {
887  d->scrollToEndDelayed = false;
888  if ( !d->scrollPressed )
889  view()->scrollBy( 0, view()->contentsHeight() );
890  }
891 }
892 
893 void ChatMessagePart::copy(bool justselection /* default false */)
894 {
895  /*
896  * The objective of this function is to keep the text of emoticons (or of LaTeX image) when copying.
897  * see Bug 61676
898  * This also copies the text as type text/html
899  * RangeImpl::toHTML was not implemented before KDE 3.4
900  */
901  QString htmltext = selectedTextAsHTML();
902  QString text = selectedText();
903  //selectedText is now sufficient
904 // text=Kopete::Message::unescape( htmltext ).trimmed();
905  // Message::unsescape will replace image by his title attribute
906  // trimmed is for removing the newline added by the <!DOCTYPE> and other xml things of RangeImpl::toHTML
907 
908  if(text.isEmpty())
909  return;
910 
911  disconnect( QApplication::clipboard(), SIGNAL(selectionChanged()), this, SLOT(slotClearSelection()));
912 
913 #ifndef QT_NO_MIMECLIPBOARD
914  if(!justselection)
915  {
916  QMimeData *mimeData = new QMimeData();
917  mimeData->setText(text);
918 
919  if(!htmltext.isEmpty()) {
920  htmltext.replace( QChar( 0xa0 ), ' ' );
921  mimeData->setHtml(htmltext);
922  }
923 
924  QApplication::clipboard()->setMimeData( mimeData, QClipboard::Clipboard );
925  }
926  QApplication::clipboard()->setText( text, QClipboard::Selection );
927 #else
928  if(!justselection)
929  QApplication::clipboard()->setText( text, QClipboard::Clipboard );
930  QApplication::clipboard()->setText( text, QClipboard::Selection );
931 #endif
932  connect( QApplication::clipboard(), SIGNAL(selectionChanged()), SLOT(slotClearSelection()));
933 
934 }
935 
936 void ChatMessagePart::print()
937 {
938  view()->print();
939 }
940 
941 void ChatMessagePart::khtmlDrawContentsEvent( khtml::DrawContentsEvent * event) //virtual
942 {
943  KHTMLPart::khtmlDrawContentsEvent(event);
944  //copy(true /*selection only*/); not needed anymore.
945 }
946 
947 void ChatMessagePart::slotCloseView( bool force )
948 {
949  if (d->manager && d->manager->view())
950  d->manager->view()->closeView( force );
951 }
952 
953 void ChatMessagePart::emitTooltipEvent( const QString &textUnderMouse, QString &toolTip )
954 {
955  emit tooltipEvent( textUnderMouse, toolTip );
956 }
957 
958 // Style formatting for messages(incoming, outgoing, status)
959 QString ChatMessagePart::formatStyleKeywords( const QString &sourceHTML, const Kopete::Message &_message )
960 {
961  if ( !d->currentChatStyle )
962  return QString();
963 
964  Kopete::Message message=_message; //we will eventually need to modify it before showing it.
965  QString resultHTML = sourceHTML;
966  QString nick, contactId, service, protocolIcon, nickLink;
967 
968  if( message.from() )
969  {
970  nick = formatName(message.from(), Qt::RichText);
971  contactId = message.from()->contactId();
972  // protocol() returns NULL here in the style preview in appearance config.
973  // this isn't the right place to work around it, since contacts should never have
974  // no protocol, but it works for now.
975  //
976  // Use default if protocol() and protocol()->displayName() is NULL.
977  // For preview and unit tests.
978  QString iconName = QLatin1String("kopete");
979  service = QLatin1String("Kopete");
980  if(message.from()->protocol() && !message.from()->protocol()->displayName().isNull())
981  {
982  service = message.from()->protocol()->displayName();
983  iconName = message.from()->protocol()->pluginIcon();
984  }
985 
986  protocolIcon = KIconLoader::global()->iconPath( iconName, KIconLoader::Small );
987 
988  nickLink=QString("<a href=\"kopetemessage://%1/?protocolId=%2&amp;accountId=%3\" class=\"KopeteDisplayName\">")
989  .arg( Qt::escape(message.from()->contactId()).replace('"',"&quot;"),
990  Qt::escape(message.from()->protocol()->pluginId()).replace('"',"&quot;"),
991  Qt::escape(message.from()->account()->accountId() ).replace('"',"&quot;"));
992  }
993  else
994  {
995  nickLink="<a>";
996  }
997 
998 
999  // Replace sender (contact nick)
1000  resultHTML.replace( QLatin1String("%sender%"), nickLink+nick+"</a>" );
1001  // Replace time, by default display only time and display seconds(that was true means).
1002  if ( Kopete::BehaviorSettings::showDates() && message.timestamp().date() != QDate::currentDate() )
1003  resultHTML.replace( QLatin1String("%time%"), KGlobal::locale()->formatDateTime(message.timestamp(), KLocale::ShortDate, true) );
1004  else
1005  resultHTML.replace( QLatin1String("%time%"), KGlobal::locale()->formatTime(message.timestamp().time(), true) );
1006  // Replace %screenName% (contact ID)
1007  resultHTML.replace( QLatin1String("%senderScreenName%"), nickLink+Qt::escape(contactId)+"</a>" );
1008  // Replace service name (protocol name)
1009  resultHTML.replace( QLatin1String("%service%"), Qt::escape(service) );
1010  // Replace protocolIcon (sender statusIcon)
1011  resultHTML.replace( QLatin1String("%senderStatusIcon%"), Qt::escape(protocolIcon).replace('"',"&quot;") );
1012 
1013  // Look for %time{X}%
1014  QRegExp timeRegExp("%time\\{([^}]*)\\}%");
1015  int pos=0;
1016  while( (pos=timeRegExp.indexIn(resultHTML , pos) ) != -1 )
1017  {
1018  QString timeKeyword = formatTime( timeRegExp.cap(1), message.timestamp() );
1019  resultHTML.replace( pos , timeRegExp.cap(0).length() , timeKeyword );
1020  }
1021 
1022  // Look for %textbackgroundcolor{X}%
1023  // TODO: use the X value.
1024  // Replace with user-selected highlight color if to be highlighted or
1025  // with "inherit" otherwise to keep CSS clean
1026  QString bgColor = QLatin1String("inherit");
1027  if( message.importance() == Kopete::Message::Highlight && Kopete::BehaviorSettings::self()->highlightEnabled() )
1028  {
1029  bgColor = Kopete::AppearanceSettings::self()->highlightBackgroundColor().name();
1030  }
1031 
1032  QRegExp textBackgroundRegExp("%textbackgroundcolor\\{([^}]*)\\}%");
1033  int textPos=0;
1034  while( (textPos=textBackgroundRegExp.indexIn(resultHTML, textPos) ) != -1 )
1035  {
1036  resultHTML.replace( textPos , textBackgroundRegExp.cap(0).length() , bgColor );
1037  }
1038 
1039  // Replace userIconPath
1040  if( message.from() )
1041  {
1042  QString photoPath = photoForContact( message.from() );
1043  if( photoPath.isEmpty() )
1044  {
1045  if(message.direction() == Kopete::Message::Inbound)
1046  photoPath = d->currentChatStyle->getStyleBaseHref() + QLatin1String("Incoming/buddy_icon.png");
1047  else if(message.direction() == Kopete::Message::Outbound)
1048  photoPath = d->currentChatStyle->getStyleBaseHref() + QLatin1String("Outgoing/buddy_icon.png");
1049  }
1050  resultHTML.replace(QLatin1String("%userIconPath%"), photoPath);
1051  }
1052 
1053  // Replace messages.
1054  // Build the action message if the currentChatStyle do not have Action template.
1055  if( message.type() == Kopete::Message::TypeAction && !d->currentChatStyle->hasActionTemplate() )
1056  {
1057  kDebug(14000) << "Map Action message to Status template. ";
1058 
1059  QString boldNick = QString("%1<b>%2</b></a> ").arg(nickLink,nick);
1060  QString newBody = boldNick + message.parsedBody();
1061  message.setHtmlBody(newBody );
1062  }
1063 
1064  // Set message direction("rtl"(Right-To-Left) or "ltr"(Left-to-right))
1065  resultHTML.replace( QLatin1String("%messageDirection%"), message.isRightToLeft() ? "rtl" : "ltr" );
1066 
1067  // These colors are used for coloring nicknames. I tried to use
1068  // colors both visible on light and dark background.
1069  static const char* const nameColors[] =
1070  {
1071  "red", "blue" , "gray", "magenta", "violet", /*"olive"*/ "#808000", "yellowgreen",
1072  "darkred", "darkgreen", "darksalmon", "darkcyan", /*"darkyellow"*/ "#B07D2B",
1073  "mediumpurple", "peru", "olivedrab", /*"royalred"*/ "#B01712", "darkorange", "slateblue",
1074  "slategray", "goldenrod", "orangered", "tomato", /*"dogderblue"*/ "#1E90FF", "steelblue",
1075  "deeppink", "saddlebrown", "coral", "royalblue"
1076  };
1077 
1078  static const int nameColorsLen = sizeof(nameColors) / sizeof(nameColors[0]) - 1;
1079  // hash contactId to deterministically pick a color for the contact
1080  int hash = 0;
1081  for( int f = 0; f < contactId.length(); ++f )
1082  hash += contactId[f].unicode() * f;
1083  const QString colorName = nameColors[ hash % nameColorsLen ];
1084  QString lightColorName; // Do not initialize, QColor::name() is expensive!
1085  //kDebug(14000) << "Hash " << hash << " has color " << colorName;
1086  QRegExp senderColorRegExp("%senderColor(?:\\{([^}]*)\\})?%");
1087  textPos=0;
1088  while( (textPos=senderColorRegExp.indexIn(resultHTML, textPos) ) != -1 )
1089  {
1090  int light=100;
1091  bool doLight=false;
1092  if(senderColorRegExp.numCaptures()>=1)
1093  {
1094  light=senderColorRegExp.cap(1).toUInt(&doLight);
1095  }
1096 
1097  // Lazily init light color
1098  if ( doLight && lightColorName.isNull() )
1099  lightColorName = QColor( colorName ).light( light ).name();
1100 
1101  resultHTML.replace( textPos , senderColorRegExp.cap(0).length(),
1102  doLight ? lightColorName : colorName );
1103  }
1104 
1105  if ( message.type() == Kopete::Message::TypeFileTransferRequest )
1106  {
1107  QString fileIcon;
1108  if ( !message.filePreview().isNull() )
1109  {
1110  QByteArray tempArray;
1111  QBuffer tempBuffer( &tempArray );
1112  tempBuffer.open( QIODevice::WriteOnly );
1113  if( message.filePreview().save( &tempBuffer, "PNG" ) )
1114  fileIcon = QString( "data:image/png;base64," ) + tempArray.toBase64();
1115  }
1116 
1117  if ( fileIcon.isEmpty() )
1118  {
1119  QString iconName = KMimeType::iconNameForUrl( message.fileName() );
1120  fileIcon = KIconLoader::global()->iconPath( iconName, -KIconLoader::SizeMedium );
1121  }
1122 
1123  resultHTML.replace( QLatin1String("%fileName%"), Qt::escape( message.fileName() ).replace('"',"&quot;") );
1124  resultHTML.replace( QLatin1String("%fileSize%"), KGlobal::locale()->formatByteSize( message.fileSize() ).replace('"',"&quot;") );
1125  resultHTML.replace( QLatin1String("%fileIconPath%"), fileIcon );
1126 
1127  resultHTML.replace( QLatin1String("%saveFileHandlerId%"), QString( "ftSV%1" ).arg( message.id() ) );
1128  resultHTML.replace( QLatin1String("%saveFileAsHandlerId%"), QString( "ftSA%1" ).arg( message.id() ) );
1129  resultHTML.replace( QLatin1String("%cancelRequestHandlerId%"), QString( "ftCC%1" ).arg( message.id() ) );
1130  }
1131 
1132  if ( message.type() == Kopete::Message::TypeVoiceClipRequest )
1133  {
1134  QString fileIcon;
1135 
1136  QString iconName = KMimeType::iconNameForUrl( message.fileName() );
1137  fileIcon = KIconLoader::global()->iconPath( iconName, -KIconLoader::SizeMedium );
1138 
1139  resultHTML.replace( QLatin1String("%fileIconPath%"), fileIcon );
1140 
1141  resultHTML.replace( QLatin1String("%playVoiceHandlerId%"), QString( "vcPL%1" ).arg( message.id() ) );
1142  resultHTML.replace( QLatin1String("%saveAsVoiceHandlerId%"), QString( "vcSA%1" ).arg( message.id() ) );
1143  }
1144 
1145  if ( message.type() == Kopete::Message::TypeNormal && message.direction() == Kopete::Message::Outbound )
1146  resultHTML.replace( QLatin1String( "%stateElementId%" ), QString( "msST%1" ).arg( message.id() ) );
1147 
1148  // Replace message at the end, maybe someone could put a Adium keyword in his message :P
1149  resultHTML.replace( QLatin1String("%message%"), formatMessageBody(message) );
1150 
1151  // TODO: %status
1152 // resultHTML = addNickLinks( resultHTML );
1153  return resultHTML;
1154 }
1155 
1156 // Style formatting for header and footer.
1157 QString ChatMessagePart::formatStyleKeywords( const QString &sourceHTML )
1158 {
1159  QString resultHTML = sourceHTML;
1160 
1161  // Verify that all contacts are not null before doing anything
1162  if( !d->manager->members().isEmpty() && d->manager->myself() )
1163  {
1164  QString sourceName, destinationName;
1165 
1166  Kopete::Contact *remoteContact = d->manager->members().first();
1167 
1168  // Use contact nickname for ourselfs, Myself metacontact display name isn't a reliable source.
1169  sourceName = d->manager->myself()->displayName();
1170  if( remoteContact->metaContact() )
1171  destinationName = remoteContact->metaContact()->displayName();
1172  else
1173  destinationName = remoteContact->displayName();
1174 
1175  // Replace %chatName%, create a internal span to update it by DOM when asked.
1176  resultHTML.replace( QLatin1String("%chatName%"), QString("<span id=\"KopeteHeaderChatNameInternal\">%1</span>").arg( formatName(d->manager->displayName(), Qt::RichText) ) );
1177  // Replace %sourceName%
1178  resultHTML.replace( QLatin1String("%sourceName%"), formatName(sourceName, Qt::RichText) );
1179  // Replace %destinationName%
1180  resultHTML.replace( QLatin1String("%destinationName%"), formatName(destinationName, Qt::RichText) );
1181  // For %timeOpened%, display the date and time (also the seconds).
1182  resultHTML.replace( QLatin1String("%timeOpened%"), KGlobal::locale()->formatDateTime( QDateTime::currentDateTime(), KLocale::ShortDate, true ) );
1183 
1184  // Look for %timeOpened{X}%
1185  QRegExp timeRegExp("%timeOpened\\{([^}]*)\\}%");
1186  int pos=0;
1187  while( (pos=timeRegExp.indexIn(resultHTML, pos) ) != -1 )
1188  {
1189  QString timeKeyword = formatTime( timeRegExp.cap(1), QDateTime::currentDateTime() );
1190  resultHTML.replace( pos , timeRegExp.cap(0).length() , timeKeyword );
1191  }
1192  // Get contact image paths
1193  QString photoIncoming = photoForContact( remoteContact );
1194  QString photoOutgoing = photoForContact( d->manager->myself() );
1195  if( photoIncoming.isEmpty() )
1196  {
1197  photoIncoming = d->currentChatStyle->getStyleBaseHref() + QLatin1String("Incoming/buddy_icon.png");
1198  }
1199 
1200  if( photoOutgoing.isEmpty() )
1201  {
1202  photoOutgoing = d->currentChatStyle->getStyleBaseHref() + QLatin1String("Outgoing/buddy_icon.png");
1203  }
1204 
1205  resultHTML.replace( QLatin1String("%incomingIconPath%"), photoIncoming );
1206  resultHTML.replace( QLatin1String("%outgoingIconPath%"), photoOutgoing );
1207  }
1208 
1209  return resultHTML;
1210 }
1211 
1212 QString ChatMessagePart::formatTime(const QString &_timeFormat, const QDateTime &dateTime)
1213 {
1214  char buffer[256];
1215 #ifdef Q_WS_WIN
1216  QString timeFormat = _timeFormat;
1217  // some formats are not supported on windows (gnu extension?)
1218  timeFormat = timeFormat.replace(QLatin1String("%e"), QLatin1String("%d"));
1219  timeFormat = timeFormat.replace(QLatin1String("%T"), QLatin1String("%H:%M:%S"));
1220 #else
1221  const QString timeFormat = _timeFormat;
1222 #endif
1223  // Get current time
1224  time_t timeT = dateTime.toTime_t();
1225  // Convert it to local time representation.
1226  struct tm* loctime = localtime (&timeT);
1227  strftime (buffer, 256, timeFormat.toAscii(), loctime);
1228 
1229  return QString(buffer);
1230 }
1231 
1232 QString ChatMessagePart::formatName(const QString &sourceName, Qt::TextFormat format ) const
1233 {
1234  QString formattedName = sourceName;
1235 
1236  // Squeeze the nickname if the user want it
1237  if( Kopete::BehaviorSettings::self()->truncateContactName() )
1238  {
1239  formattedName = KStringHandler::csqueeze( sourceName, Kopete::BehaviorSettings::self()->truncateContactNameLength() );
1240  }
1241 
1242  if ( format == Qt::RichText )
1243  { // Escape the name.
1244  formattedName = Kopete::Message::escape(formattedName);
1245  }
1246 
1247  return formattedName;
1248 }
1249 
1250 QString ChatMessagePart::formatName( const Kopete::Contact* contact, Qt::TextFormat format ) const
1251 {
1252  if (!contact)
1253  {
1254  return QString();
1255  }
1256 
1257  // Use metacontact display name if the metacontact exists and if it is not the myself metacontact.
1258  // Myself metacontact is not a reliable source.
1259  if ( contact->metaContact() && contact->metaContact() != Kopete::ContactList::self()->myself() )
1260  {
1261  return formatName( contact->metaContact()->displayName(), format );
1262  }
1263  // Use contact nickname for no metacontact or myself.
1264  else
1265  {
1266  return formatName( contact->displayName(), format );
1267  }
1268 }
1269 
1270 QString ChatMessagePart::formatMessageBody(const Kopete::Message &message)
1271 {
1272  QString formattedBody("<span ");
1273 
1274  formattedBody += message.getHtmlStyleAttribute();
1275 
1276  QStringList classes("KopeteMessageBody");
1277  classes+=message.classes();
1278 
1279  // Affect the parsed body.
1280  formattedBody += QString("class=\"%1\">%2</span>")
1281  .arg(classes.join(" "), message.parsedBody());
1282 
1283  return formattedBody;
1284 }
1285 
1286 void ChatMessagePart::slotUpdateHeaderDisplayName()
1287 {
1288  kDebug(14000) ;
1289  DOM::HTMLElement kopeteChatNameNode = document().getElementById( QString("KopeteHeaderChatNameInternal") );
1290  if( !kopeteChatNameNode.isNull() )
1291  kopeteChatNameNode.setInnerText( formatName(d->manager->displayName(), Qt::RichText) );
1292 }
1293 
1294 void ChatMessagePart::slotUpdateHeaderPhoto()
1295 {
1296  // Do the actual style switch
1297  // Wait for the event loop before switching the style
1298  QTimer::singleShot( 0, this, SLOT(changeStyle()) );
1299 }
1300 
1301 void ChatMessagePart::changeStyle()
1302 {
1303 #ifdef STYLE_TIMETEST
1304  QTime beforeChange = QTime::currentTime();
1305 #endif
1306  // Make latestContact null to reset consecutives messages.
1307  d->latestContact = 0;
1308 
1309  // Rewrite the header and footer.
1310  writeTemplate();
1311 
1312  // Readd all current messages.
1313  QList<Kopete::Message>::ConstIterator it, itEnd = d->allMessages.constEnd();
1314  for(it = d->allMessages.constBegin(); it != itEnd; ++it)
1315  {
1316  Kopete::Message tempMessage = *it;
1317  appendMessage(tempMessage, true); // true means that we are restoring.
1318  }
1319  kDebug(14000) << "Finish changing style.";
1320 #ifdef STYLE_TIMETEST
1321  kDebug(14000) << "Change time: " << beforeChange.msecsTo( QTime::currentTime());
1322 #endif
1323 }
1324 
1325 void ChatMessagePart::writeTemplate()
1326 {
1327  kDebug(14000) ;
1328 
1329 #ifdef STYLE_TIMETEST
1330  QTime beforeHeader = QTime::currentTime();
1331 #endif
1332  // Clear all the page, and begin a new page.
1333  begin();
1334 
1335  // NOTE: About styles
1336  // Order of style tag in the template is important.
1337  // mainStyle take over all other style definition (which is what we want).
1338  //
1339  // KopeteStyle: Kopete appearance configuration into a style. It loaded first because
1340  // we don't want Kopete settings to override CSS Chat Window Style.
1341  // baseStyle: Import the main.css from the Chat Window Style
1342  // mainStyle: Currrent variant CSS url.
1343 
1344  QString xhtmlBase;
1345  if ( d->currentChatStyle )
1346  {
1347  // FIXME: Maybe this string should be load from a file, then parsed for args.
1348  xhtmlBase += QString("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
1349  "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\"\n"
1350  "\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n"
1351  "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
1352  "<head>\n"
1353  "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\n\" />\n"
1354  "<base href=\"%1\">\n"
1355  "<style id=\"KopeteStyle\" type=\"text/css\" media=\"screen,print\">\n"
1356  " %5\n"
1357  "</style>\n"
1358  "<style id=\"baseStyle\" type=\"text/css\" media=\"screen,print\">\n"
1359  " @import url(\"main.css\");\n"
1360  " *{ word-wrap:break-word; }\n"
1361  "</style>\n"
1362  "<style id=\"mainStyle\" type=\"text/css\" media=\"screen,print\">\n"
1363  " @import url(\"%4\");\n"
1364  "</style>\n"
1365  "</head>\n"
1366  "<body>\n"
1367  "%2\n"
1368  "<div id=\"Chat\">\n</div>\n"
1369  "%3\n"
1370  "</body>"
1371  "</html>"
1372  ).arg( d->currentChatStyle->getStyleBaseHref() )
1373  .arg( formatStyleKeywords(d->currentChatStyle->getHeaderHtml()) )
1374  .arg( formatStyleKeywords(d->currentChatStyle->getFooterHtml()) )
1375  .arg( adjustStyleVariantForChatSession( KopeteChatWindowSettings::self()->styleVariant() ) )
1376  .arg( styleHTML() );
1377  }
1378  else
1379  {
1380  xhtmlBase = i18n( "Chat style could not be found, or is invalid." );
1381  }
1382  write(xhtmlBase);
1383  end();
1384 #ifdef STYLE_TIMETEST
1385  kDebug(14000) << "Header time: " << beforeHeader.msecsTo( QTime::currentTime());
1386 #endif
1387 }
1388 
1389 void ChatMessagePart::resendMessage( uint messageId )
1390 {
1391  QList<Kopete::Message>::ConstIterator it, itEnd = d->allMessages.constEnd();
1392  for ( it = d->allMessages.constBegin(); it != itEnd; ++it )
1393  {
1394  if ( (*it).id() == messageId )
1395  {
1396  if ( !( d->manager->protocol()->capabilities() & Kopete::Protocol::CanSendOffline ) )
1397  {
1398  bool reachableContactFound = false;
1399  foreach ( Kopete::Contact* c, (*it).to() )
1400  {
1401  if ( c->isReachable() )
1402  {
1403  reachableContactFound = true;
1404  break;
1405  }
1406  }
1407 
1408  // no online contact found and can't send offline? can't send.
1409  if ( !reachableContactFound )
1410  return;
1411  }
1412 
1413  Kopete::Message msg( (*it).from(), (*it).to() );
1414  msg.setDirection( Kopete::Message::Outbound );
1415  msg.setBody( (*it).body() );
1416 
1417 // msg.setBackgroundColor( (*it).backgroundColor() );
1418  msg.setForegroundColor( (*it).foregroundColor() );
1419  msg.setFont( (*it).font() );
1420  d->manager->sendMessage( msg );
1421  break;
1422  }
1423  }
1424 }
1425 
1426 void ChatMessagePart::saveVoiceClip( uint messageId )
1427 {
1428  QList<Kopete::Message>::ConstIterator it, itEnd = d->allMessages.constEnd();
1429  for ( it = d->allMessages.constBegin(); it != itEnd; ++it )
1430  {
1431  if ( (*it).id() == messageId )
1432  {
1433  if(!(*it).fileName().isEmpty())
1434  {
1435  QString temporaryExtension = "*.wav";
1436  QString newFileName = QFileDialog::getSaveFileName(NULL,
1437  i18n("Save File as"), QString(), i18n("Wav file (*.wav)"), &temporaryExtension);
1438  if(!newFileName.isEmpty())
1439  QFile::copy((*it).fileName(), newFileName);
1440  }
1441  break;
1442  }
1443  }
1444 }
1445 
1446 void ChatMessagePart::playVoiceClip( uint messageId )
1447 {
1448  QList<Kopete::Message>::ConstIterator it, itEnd = d->allMessages.constEnd();
1449  for ( it = d->allMessages.constBegin(); it != itEnd; ++it )
1450  {
1451  if ( (*it).id() == messageId )
1452  {
1453  if(!(*it).fileName().isEmpty())
1454  {
1455  Phonon::MediaObject *media = new Phonon::MediaObject(this);
1456  Phonon::AudioOutput *audioOutput = new Phonon::AudioOutput(Phonon::MusicCategory, this);
1457  connect(media, SIGNAL(finished()), media, SLOT(deleteLater()));
1458  connect(media, SIGNAL(finished()), audioOutput, SLOT(deleteLater()));
1459  createPath(media, audioOutput);
1460  media->setCurrentSource((*it).fileName());
1461  media->play();
1462  }
1463  break;
1464  }
1465  }
1466 }
1467 
1468 QString ChatMessagePart::adjustStyleVariantForChatSession( const QString & styleVariant ) const
1469 {
1470  if ( d->currentChatStyle && d->manager->form() == Kopete::ChatSession::Chatroom
1471  && KopeteChatWindowSettings::self()->useCompact() ) {
1472  return d->currentChatStyle->compact( styleVariant );
1473  }
1474  return styleVariant;
1475 }
1476 
1477 QString ChatMessagePart::photoForContact( const Kopete::Contact *contact ) const
1478 {
1479  QString photo;
1480  if ( !contact )
1481  return photo;
1482 
1483  if( contact->metaContact() == Kopete::ContactList::self()->myself() )
1484  { // all myself contacts have the same metaContact so take photo directly from contact otherwise the photo could be wrong.
1485  photo = contact->property(Kopete::Global::Properties::self()->photo().key()).value().toString();
1486  }
1487  else if( !contact->metaContact()->picture().isNull() )
1488  {
1489  photo = QString( "data:image/png;base64," ) + contact->metaContact()->picture().base64();
1490  }
1491 
1492  return photo;
1493 }
1494 
1495 void ChatMessagePart::addFileTransferButtonsEventListener( unsigned int id )
1496 {
1497  QString elementId = QString( "ftSV%1" ).arg( id );
1498  registerClickEventListener( document().getElementById( elementId ) );
1499 
1500  elementId = QString( "ftSA%1" ).arg( id );
1501  registerClickEventListener( document().getElementById( elementId ) );
1502 
1503  elementId = QString( "ftCC%1" ).arg( id );
1504  registerClickEventListener( document().getElementById( elementId ) );
1505 }
1506 
1507 void ChatMessagePart::addVoiceClipsButtonsEventListener( unsigned int id )
1508 {
1509  QString elementId = QString( "vcSA%1" ).arg( id );
1510  registerClickEventListener( document().getElementById( elementId ) );
1511 
1512  elementId = QString( "vcPL%1" ).arg( id );
1513  registerClickEventListener( document().getElementById( elementId ) );
1514 }
1515 
1516 
1517 void ChatMessagePart::disableFileTransferButtons( unsigned int id )
1518 {
1519  QString elementId = QString( "ftSV%1" ).arg( id );
1520  DOM::HTMLInputElement element = document().getElementById( elementId );
1521  if ( !element.isNull() )
1522  element.setDisabled( true );
1523 
1524  elementId = QString( "ftSA%1" ).arg( id );
1525  element = document().getElementById( elementId );
1526  if ( !element.isNull() )
1527  element.setDisabled( true );
1528 
1529  elementId = QString( "ftCC%1" ).arg( id );
1530  element = document().getElementById( elementId );
1531  if ( !element.isNull() )
1532  element.setDisabled( true );
1533 }
1534 
1535 void ChatMessagePart::changeMessageStateElement( uint id, Kopete::Message::MessageState state )
1536 {
1537  if ( !d->currentChatStyle )
1538  return;
1539 
1540  QString elementId = QString( "msST%1" ).arg( id );
1541  DOM::HTMLElement element = document().getElementById( elementId );
1542  if ( element.isNull() )
1543  return;
1544 
1545  QString statusHTML;
1546  switch ( state )
1547  {
1548  case Kopete::Message::StateUnknown:
1549  statusHTML = d->currentChatStyle->getOutgoingStateUnknownHtml();
1550  break;
1551  case Kopete::Message::StateSending:
1552  statusHTML = d->currentChatStyle->getOutgoingStateSendingHtml();
1553  break;
1554  case Kopete::Message::StateSent:
1555  statusHTML = d->currentChatStyle->getOutgoingStateSentHtml();
1556  break;
1557  case Kopete::Message::StateError:
1558  statusHTML = d->currentChatStyle->getOutgoingStateErrorHtml();
1559  break;
1560  }
1561 
1562  QString resendId = QString( "msRS%1" ).arg( id );
1563  statusHTML.replace( QLatin1String( "%resendHandlerId%" ), resendId );
1564  element.setInnerHTML( statusHTML );
1565 
1566  registerClickEventListener( document().getElementById( resendId ) );
1567 }
1568 
1569 void ChatMessagePart::registerClickEventListener( DOM::HTMLElement element )
1570 {
1571  if ( element.isNull() )
1572  return;
1573 
1574  if ( !d->htmlEventListener )
1575  {
1576  d->htmlEventListener = new HTMLEventListener();
1577  connect( d->htmlEventListener, SIGNAL(resendMessage(uint)), this, SLOT(resendMessage(uint)) );
1578  connect( d->htmlEventListener, SIGNAL(playVoiceClip(uint)), this, SLOT(playVoiceClip(uint)) );
1579  connect( d->htmlEventListener, SIGNAL(saveVoiceClip(uint)), this, SLOT(saveVoiceClip(uint)) );
1580  }
1581  element.addEventListener( "click", d->htmlEventListener, false );
1582 }
1583 
1584 void ChatMessagePart::readChatFont()
1585 {
1586  Kopete::AppearanceSettings *settings = Kopete::AppearanceSettings::self();
1587 
1588  d->chatFont = KGlobalSettings::generalFont();
1589  if ( settings->chatFontSelection() == 1 )
1590  d->chatFont = settings->chatFont();
1591 }
1592 
1593 void HTMLEventListener::handleEvent( DOM::Event &event )
1594 {
1595  DOM::HTMLInputElement element = event.currentTarget();
1596  if ( !element.isNull() )
1597  {
1598  QString idType = element.id().string().left(4);
1599  unsigned int messageId = element.id().string().mid(4).toUInt();
1600 
1601  if ( idType == QLatin1String( "ftSV" ) )
1602  Kopete::TransferManager::transferManager()->saveIncomingTransfer( messageId );
1603  else if ( idType == QLatin1String( "ftSA" ) )
1604  Kopete::TransferManager::transferManager()->saveIncomingTransfer( messageId );
1605  else if ( idType == QLatin1String( "ftCC" ) )
1606  Kopete::TransferManager::transferManager()->cancelIncomingTransfer( messageId );
1607  else if ( idType == QLatin1String( "msRS" ) )
1608  emit resendMessage( messageId );
1609  else if ( idType == QLatin1String( "vcPL" ) )
1610  emit playVoiceClip( messageId );
1611  else if ( idType == QLatin1String( "vcSA" ) )
1612  emit saveVoiceClip( messageId );
1613  }
1614 }
1615 
1616 #include "chatmessagepart.moc"
1617 
1618 // vim: set noet ts=4 sts=4 sw=4:
QTextStream::setCodec
void setCodec(QTextCodec *codec)
QWidget
ChatMessagePart::setStyleVariant
void setStyleVariant(const QString &variantPath)
Change the current variant for the current style.
Definition: chatmessagepart.cpp:445
ChatMessagePart::clear
void clear()
Clear the message window.
Definition: chatmessagepart.cpp:707
ChatWindowStyle
This class represent a single chat window style.
Definition: kopetechatwindowstyle.h:30
QString::truncate
void truncate(int position)
ChatMessagePart::~ChatMessagePart
~ChatMessagePart()
Definition: chatmessagepart.cpp:285
ChatMessagePart::appendMessage
void appendMessage(Kopete::Message &message, bool restoring=false)
Appends a message to the messave view.
Definition: chatmessagepart.cpp:474
ChatWindowStyleManager::self
static ChatWindowStyleManager * self()
Singleton access to this class.
Definition: kopetechatwindowstylemanager.cpp:66
kopetechatwindowstylemanager.h
QColor::light
QColor light(int factor) const
KopeteChatWindowSettings::useCompact
static bool useCompact()
Get Use a compact variant of the chat style for chatrooms.
Definition: kopetechatwindowsettings.h:125
QByteArray
QColor::name
QString name() const
ChatMessagePart::keepScrolledDown
void keepScrolledDown()
Immediately scroll the chat to the bottom, as long as it has not been intentionally scrolled away fro...
Definition: chatmessagepart.cpp:672
QChar
Phonon::MediaObject::play
void play()
ChatMessagePart::ChatMessagePart
ChatMessagePart(Kopete::ChatSession *manager, QWidget *parent)
Create a new ChatMessage Part.
Definition: chatmessagepart.cpp:211
QFont
kopetechatwindow.h
QList::at
const T & at(int i) const
kopetechatwindowstyle.h
manager
virtual Kopete::ChatSession * manager(Kopete::Contact::CanCreateFlags)
Definition: chatwindowconfig.cpp:94
QPointer< Kopete::Contact >
KHTMLPart
KopeteChatWindowSettings::self
static KopeteChatWindowSettings * self()
Definition: kopetechatwindowsettings.cpp:17
ChatMessagePart::formatName
QString formatName(const Kopete::Contact *contact, Qt::TextFormat format) const
Format contact's nickname/displayname according to preferences.
Definition: chatmessagepart.cpp:1250
QTime::msecsTo
int msecsTo(const QTime &t) const
ChatMessagePart::setStyle
void setStyle(const QString &styleName)
Change the current style.
Definition: chatmessagepart.cpp:419
ChatMessagePart::pageDown
void pageDown()
Scroll the view down a page.
Definition: chatmessagepart.cpp:373
QBuffer
ChatMessagePart::khtmlDrawContentsEvent
virtual void khtmlDrawContentsEvent(khtml::DrawContentsEvent *)
Definition: chatmessagepart.cpp:941
QPoint
QFontMetrics
HTMLEventListener::playVoiceClip
void playVoiceClip(uint messageId)
ChatMessagePart::save
void save()
Save the contents of the chat to a file.
Definition: chatmessagepart.cpp:309
QTime
QMimeData
QFile::copy
bool copy(const QString &newName)
QTextStream
QList::size
int size() const
QString::isNull
bool isNull() const
HTMLEventListener::resendMessage
void resendMessage(uint messageId)
QRegExp
HTMLEventListener
Definition: chatmessagepart.h:307
kopetechatwindowsettings.h
QChar::isSpace
bool isSpace() const
QApplication::clipboard
QClipboard * clipboard()
QObject
Phonon::MediaObject
ChatMessagePart::pageUp
void pageUp()
Scroll the view up a page.
Definition: chatmessagepart.cpp:368
QString::isEmpty
bool isEmpty() const
QString::trimmed
QString trimmed() const
QMimeData::setText
void setText(const QString &text)
QString
QList< Kopete::Message >
QColor
QClipboard::setMimeData
void setMimeData(QMimeData *src, Mode mode)
QStringList
chatmessagepart.h
Phonon::createPath
Path createPath(MediaNode *source, MediaNode *sink)
Phonon::MediaObject::setCurrentSource
void setCurrentSource(const MediaSource &source)
HTMLEventListener::saveVoiceClip
void saveVoiceClip(uint messageId)
ChatMessagePart::tooltipEvent
void tooltipEvent(const QString &textUnderMouse, QString &toolTip)
Emits before the tooltip is about to show.
ChatMessagePart::contextMenuEvent
void contextMenuEvent(const QString &textUnderMouse, KMenu *popupMenu)
Emits before the context menu is about to show.
QDateTime::toTime_t
uint toTime_t() const
QTime::currentTime
QTime currentTime()
QString::replace
QString & replace(int position, int n, QChar after)
QTextStream::string
QString * string() const
QDateTime::currentDateTime
QDateTime currentDateTime()
QString::mid
QString mid(int position, int n) const
QCursor::pos
QPoint pos()
QTextStream::flush
void flush()
ChatMessagePart::print
void print()
Print out the contents of the chatwindow.
Definition: chatmessagepart.cpp:936
QLatin1String
HTMLEventListener::handleEvent
virtual void handleEvent(DOM::Event &event)
Definition: chatmessagepart.cpp:1593
Qt::escape
QString escape(const QString &plain)
QDate::currentDate
QDate currentDate()
QAction
KAction
QTextCodec::codecForName
QTextCodec * codecForName(const QByteArray &name)
ChatMessagePart::messageStateChanged
void messageStateChanged(uint messageId, Kopete::Message::MessageState state)
Definition: chatmessagepart.cpp:452
ChatWindowStyleManager::getValidStyleFromPool
ChatWindowStyle * getValidStyleFromPool(const QString &styleName)
Get a instance of a ChatWindowStyle from the pool.
Definition: kopetechatwindowstylemanager.cpp:324
QFileDialog::getSaveFileName
QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFlags< QFileDialog::Option > options)
QString::length
int length() const
ConsecutiveMessageTimeout
static const uint ConsecutiveMessageTimeout
Definition: chatmessagepart.cpp:104
QString::left
QString left(int n) const
ChatMessagePart::slotToggleGraphicOverride
KDE_DEPRECATED void slotToggleGraphicOverride(bool enable)
Definition: chatmessagepart.cpp:415
QClipboard::setText
void setText(const QString &text, Mode mode)
Phonon::AudioOutput
QByteArray::toBase64
QByteArray toBase64() const
QMimeData::setHtml
void setHtml(const QString &html)
QString::data
QChar * data()
QString::arg
QString arg(qlonglong a, int fieldWidth, int base, const QChar &fillChar) const
ChatMessagePart::copy
void copy(bool justselection=false)
Initiates a copy action If there is text selected in the HTML view, that text is copied Otherwise if ...
Definition: chatmessagepart.cpp:893
QAction::setEnabled
void setEnabled(bool)
QString::toAscii
QByteArray toAscii() const
QDateTime
QString::toUInt
uint toUInt(bool *ok, int base) const
QTimer::singleShot
singleShot
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Mon Jun 22 2020 13:29:08 by doxygen 1.8.7 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

kopete/kopete

Skip menu "kopete/kopete"
  • Main Page
  • Namespace List
  • Namespace Members
  • Alphabetical List
  • Class List
  • Class Hierarchy
  • Class Members
  • File List
  • File Members
  • Related Pages

kdenetwork API Reference

Skip menu "kdenetwork API Reference"
  • kget
  • kopete
  •   kopete
  •   libkopete
  • krdc
  • krfb

Search



Report problems with this website to our bug tracking system.
Contact the specific authors with questions and comments about the page contents.

KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal