Logo Search packages:      
Sourcecode: kmess version File versions  Download package

chatmessageview.cpp

/***************************************************************************
                          chatmessageview.cpp -  description
                             -------------------
    begin                : Sat Nov 8 2005
    copyright            : (C) 2005 by Diederik van der Boor
    email                : "vdboor" --at-- "codingdomain.com"
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 "chatmessageview.h"

#include "../dialogs/addemoticondialog.h"
#include "../utils/kmessshared.h"
#include "../currentaccount.h"
#include "../emoticonmanager.h"
#include "../kmessdebug.h"
#include "chatmessagestyle.h"

#include <QStringList>
#include <QTimer>
#include <QClipboard>

#include <kdeversion.h>
#include <DOM/DOMString>
#include <DOM/Text>
#include <DOM/HTMLDocument>
#include <DOM/HTMLElement>
#include <KHTMLView>

#include <KApplication>
#include <KAction>
#include <KStandardAction>
#include <KMenu>

// The constructor
ChatMessageView::ChatMessageView( QWidget *parentWidget, QObject *parent )
 : KHTMLPart( parentWidget, parent, DefaultGUI )
 , chatClearingMark_(0)
 , isEmpty_(true)
 , lastMessageId_(0)
{
  // Disable features that might do harm or are not useful
  setJScriptEnabled( false );
  setJavaEnabled( false );
  setMetaRefreshEnabled( false );
  setOnlyLocalReferences( false );
  setSuppressedPopupIndicator( false );

  // Enable winks
  setPluginsEnabled( true );

#if KDE_IS_VERSION( 4, 1, 0 )
  // Add smooth scrolling when possible
  view()->setSmoothScrollingMode( KHTMLView::SSMWhenEfficient );
#endif

  // Connect signals for browsing
  connect( browserExtension(), SIGNAL(  openUrlRequestDelayed(const KUrl&,const KParts::OpenUrlArguments&,const KParts::BrowserArguments&) ),
           this,               SIGNAL(         openUrlRequest(const KUrl&) ) );

  // Make sure this widget can't get any focus.
  view()->setFocusPolicy( Qt::NoFocus );

  // Set a border to the view
  view()->setFrameStyle( QFrame::Sunken | QFrame::StyledPanel );

  // Initialize the chat style parser
  chatStyle_ = new ChatMessageStyle();

  // Initialize the current account
  currentAccount_ = CurrentAccount::instance();

  // create context menu actions
  createPopupMenuActions();

  // Everything has been initialized: now load a standard empty HTML page
  clearView();
}



// The destructor
ChatMessageView::~ChatMessageView()
{
  // Delete the saved chat messages.
  qDeleteAll( chatMessages_ );
  chatMessages_.clear();
  lastContactMessages_.clear();

  delete chatStyle_;
}



/**
 * Remove from the chat all links to add an emoticon
 *
 * This method is called when you've added a custom emoticon that a contact has sent you.
 * It searches the chat for the emoticon you have added; and removes the "add this emoticon"
 * link from all the occurrences of it.
 * This way, you can't add twice an emoticon. This behavior mimicks that of MSN/Windows Live
 * Messenger.
 */
00112 void ChatMessageView::addedEmoticon( QString shortcut )
{
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
  kDebug() << "Replacing add emoticon links with shortcut '" << shortcut << "'.";
#endif

  // The shortcut in emoticon links is url-encoded, convert the original one to match it
  shortcut = KUrl::toPercentEncoding( shortcut );

  DOM::HTMLElement link, parent, image;
  DOM::HTMLDocument document = htmlDocument();

  // Find all the list's links which point to this emoticon
  DOM::NodeList linksList = document.getElementsByName( "newEmoticon_" + shortcut );

  if( linksList.isNull() )
  {
    return;
  }

  // Check all the links in the list. The search is done backwards, because when we delete one of the items
  // of the list, the list itself shortens down reassigning the indices, and a regular loop would fail
  for( long i = (linksList.length() - 1); i >= 0; i-- )
  {
    link = linksList.item( i );
    if( link.isNull() || ! link.isHTMLElement() )
    {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
      kDebug() << "Link null, skipping.";
#endif
      continue;
    }

    // Replace the link with its first child (the emoticon image)
    parent = link.parentNode();
    image = link.firstChild();

    if( parent.isNull() )
    {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
      kDebug() << "Parent null, skipping.";
#endif
      continue;
    }

    if( image.isNull() )
    {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
      kDebug() << "Image null, skipping.";
#endif
      continue;
    }

    parent.replaceChild( image, link );
  }
}



// Add the given html to the chat browser and scroll to the end
void ChatMessageView::addHtmlMessage(const QString &text)
{
  lastMessageId_++;

  // Create new HTML node
  DOM::HTMLElement newNode = document().createElement("div");
  newNode.setAttribute( "id", "message" + QString::number(lastMessageId_) );
  newNode.setAttribute( "class", "messageContainer" );
  newNode.setInnerHTML( text );

  DOM::HTMLElement messageRoot = htmlDocument().getElementById("messageRoot");
  if( messageRoot.isNull() )
  {
    messageRoot = htmlDocument().body();
  }
  messageRoot.appendChild( document().createTextNode("\n") );
  messageRoot.appendChild( newNode );
  messageRoot.appendChild( document().createTextNode("\n") );

  isEmpty_ = false;
}



// Delete the viewed contents, and optionally the saved message history, too
void ChatMessageView::clearView( bool clearHistory )
{
  // Remove any content currently shown, by setting up a new empty HTML page
  setHtml( QString() );

  lastContactMessages_.clear();

  // Also delete the saved messages
  if( clearHistory )
  {
    qDeleteAll( chatMessages_ );
    chatMessages_.clear();

    chatClearingMark_ = 0;
    lastMessageId_ = 0;
    isEmpty_ = true;
  }
  else if( ! chatMessages_.isEmpty() )
  {
    // Set the chat clearing marker. When the 'clear chat history' action is
    // triggered, the message following this one will be the first shown
    chatClearingMark_ = chatMessages_.last();
  }
}



// Return the HTML source of the page.
QString ChatMessageView::getHistory( Account::ChatExportFormat format, bool append, QString &appendPoint )
{
  QString chatHistory;
  QString handle( chatMessages_.first()->getContactHandle() );
  uint date  ( chatMessages_.first()->getDateTime().toTime_t() );

  bool oldAllowEmoticonLinks = chatStyle_->getAllowEmoticonLinks();
  chatStyle_->setAllowEmoticonLinks( false );

  // There are things which happen inside a chat, but which we don't want in our logs - for example,
  // we want links to add new emoticons in our chats, but these make no sense in our logs.
  // TODO: fix the situation below:
  //  - Save the XML data for each chat while it is running in the background
  //  - Make sure the saved XML data does not include these emoticon links, while the displayed
  //    text does include them (hint: chatStyle_->setAllowEmoticonLinks() )

  switch( format )
  {
    case Account::EXPORT_XML:
    {
      // Convert every message in the history to XML
      // TODO - Make this extremely slow process faster by caching the chat's XML.
      QList<ChatMessage*> groupedMessages;
      QListIterator<ChatMessage*> it( chatMessages_ );

      while( it.hasNext() )
      {
        ChatMessage *chatMessage = it.next();

        // Chat messages from the same person are always grouped together
        if( chatMessage->isNormalMessage() )
        {
          if( groupedMessages.isEmpty() )
          {
            groupedMessages.append( chatMessage );
            continue;
          }

          ChatMessage *lastContactMessage = groupedMessages.last();

          // Check for contact handle and message type, so offline messages won't be
          // grouped with normal incoming messages.
          if( lastContactMessage->getContactHandle() == chatMessage->getContactHandle()
          &&  lastContactMessage->getType()          == chatMessage->getType() )
          {
            groupedMessages.append( chatMessage );
            continue;
          }
        }

        // There are messages to flush
        if( ! groupedMessages.isEmpty() )
        {
          // Do not add message groups with only one message
          if( groupedMessages.count() > 1 )
          {
            chatHistory += "<messagegroup>\n";
            foreach( ChatMessage *groupMessage, groupedMessages )
            {
              chatHistory += chatStyle_->convertMessageToXml( *groupMessage, true );
            }
            chatHistory += "</messagegroup>\n";
          }
          else
          {
            chatHistory += chatStyle_->convertMessageToXml( *( groupedMessages.first() ), true );
          }
            groupedMessages.clear();
        }

        // All chat messages are grouped, special messages never are
        if( chatMessage->isNormalMessage() )
        {
          groupedMessages.append( chatMessage );
        }
        else
        {
          chatHistory += chatStyle_->convertMessageToXml( *chatMessage, true );
        }
      }

      // Flush the last group of messages, if any
      if( ! groupedMessages.isEmpty() )
      {
        chatHistory += "<messagegroup>\n";
        foreach( ChatMessage *groupMessage, groupedMessages )
        {
          chatHistory += chatStyle_->convertMessageToXml( *groupMessage, true );
        }
        chatHistory += "</messagegroup>\n";
      }

      chatHistory = "\n<conversation timestamp=\"" + QString::number( date ) + "\">\n" + chatHistory + "</conversation>\n";

      // we've changed it at line 2 of this method.
      // we MUST change it back here otherwise the style tag written 
      // below could be incorrect!
      chatStyle_->setAllowEmoticonLinks( oldAllowEmoticonLinks );

      if( append )
      {
        // Tell the caller where this chat should be inserted
        appendPoint = "</messageRoot>";

        return chatHistory;
      }

      // Not appending: also add a root element to the history
      return "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
             "<?xml-stylesheet type=\"text/xsl\" href=\"file://" + chatStyle_->getStyleSheet() + "\" media=\"all\" ?>\n"
             "<!-- " + getStyleTag() + " -->\n"
             "<messageRoot xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n"
             + chatHistory +
             "\n</messageRoot>\n";
    }

    case Account::EXPORT_HTML:
    {
      chatHistory += "\n<div class=\"conversation\">"
                  +  i18nc( "Header of a chat file saved in HTML: %1 is the contact, %2 the date and time",
                            "Chat with %1<br/>Started on: %2",
                            handle,
                            KGlobal::locale()->formatDateTime( QDateTime::fromTime_t( date ), KLocale::ShortDate )  )
                  +  "\n</div>\n";

      chatHistory += rebuildHistory( true );

      // When appending is on, the job is a lot quicker :)
      if( append )
      {
        appendPoint = "</body>";
        chatStyle_->setAllowEmoticonLinks( oldAllowEmoticonLinks );
        return chatHistory;
      }

      // Get a (deep) copy of the current page source
      // TODO: document().cloneNode( true ) doesn't work, so this workaround is
      // required. Test if it's been fixed, and add a #define to exclude the
      // workaround on fixed KHTML versions.
      DOM::Document copy( document().implementation().createHTMLDocument( "" ) );
      copy.removeChild( copy.documentElement() );
      copy.appendChild( copy.importNode( document().documentElement(), true ) );

      // Replace the html body with the chat history to be saved
      DOM::HTMLElement messageRoot( copy.getElementById( "messageRoot" ) );
      if( messageRoot.isNull() )
      {
        kWarning() << "Current style does not define the 'messageRoot' element!";
        chatStyle_->setAllowEmoticonLinks( oldAllowEmoticonLinks );
        return chatHistory;
      }
      messageRoot.setInnerHTML( "\n" + chatHistory + "\n" );

      chatStyle_->setAllowEmoticonLinks( oldAllowEmoticonLinks );
      return copy.toString().string();

    }

    case Account::EXPORT_TEXT:
    {
      // Convert every message in the history to text
      QString lastHandle;
      bool groupFollowups = currentAccount_->getGroupFollowupMessages();

      // Before the first message, add a simple header
      chatHistory += "\n\n"
                  +  i18nc( "Header of a single chat saved as plain text chat log: %1 is the chat date and time",
                            "Chat started on: %1",
                            KGlobal::locale()->formatDateTime( QDateTime::fromTime_t( date ), KLocale::ShortDate ) )
                  +  "\n------------------------------------------------------------\n";

      QListIterator<ChatMessage*> it( chatMessages_ );
      while( it.hasNext() )
      {
        const ChatMessage *chatMessage = it.next();

        // Add chat messages
        if( chatMessage->isNormalMessage() )
        {
          if( groupFollowups && lastHandle == chatMessage->getContactHandle() )
          {
            chatHistory += "  " + chatMessage->getBody().trimmed() + "\n";
          }
          else
          {
            chatHistory += "("  + chatMessage->getTime().toString( Qt::DefaultLocaleShortDate ) + ")"
                           " "  + chatMessage->getContactHandle() + ":\n"
                           "  " + chatMessage->getBody().trimmed() + "\n";
          }
          lastHandle = chatMessage->getContactHandle();
        }
        // Add system messages etc
        else
        {
          chatHistory += "(" + chatMessage->getTime().toString( Qt::DefaultLocaleShortDate ) + ")"
                         " " + chatMessage->getBody().trimmed() + "\n";
          lastHandle = QString();
        }
      }

      // NOTE If appending is on, the chat will be inserted anyways at the end of file:
      // so the append point is kept empty to instantly match at the last line of the file.

      // Add an header to the file if needed
      if( ! append )
      {
        chatHistory.prepend( i18nc( "Header of a chat file saved in plain text: %1 is the contact",
                                    "Saved KMess chats with %1", handle )
                           + "\n######################################################################\n" );
      }

      chatStyle_->setAllowEmoticonLinks( oldAllowEmoticonLinks );
      return chatHistory;
    }

    default:
      kWarning() << "Invalid export format: code" << format;
      break;
  }

  chatStyle_->setAllowEmoticonLinks( oldAllowEmoticonLinks );

  return QString();
}



// Return a pointer to the message style parser
ChatMessageStyle *ChatMessageView::getStyle() const
{
  return chatStyle_;
}



// Return a tag to use to compare styles and their options
QString ChatMessageView::getStyleTag() const
{
  return "KMess-Style: "
         + chatStyle_->getName() + "-"
         + ( chatStyle_->getUseEmoticons() ? "1" : "0" )
         + ( chatStyle_->getUseFontEffects() ? "1" : "0" )
         + ( chatStyle_->getUseFormatting() ? "1" : "0" )
         + ( chatStyle_->getAllowEmoticonLinks() ? "1" : "0" );
}



// Whether or not the message area is empty
bool ChatMessageView::isEmpty() const
{
  return isEmpty_;
}



// Generate a new HTML chat log
QString ChatMessageView::rebuildHistory( bool fullHistory )
{
  // Support grouping of follow-up messages.
  bool groupFollowups = currentAccount_->getGroupFollowupMessages();
  lastContactMessages_.clear();

  // Recreate the entire chat history
  QString newHtml;
  bool clearingMarkReached = false;
  QListIterator<ChatMessage *> it(chatMessages_);
  while( it.hasNext() )
  {
    ChatMessage *chatMessage = it.next();

    if( chatClearingMark_ && ! fullHistory && ! clearingMarkReached )
    {
      if( chatMessage == chatClearingMark_ )
      {
        clearingMarkReached = true;
      }
      else
      {
        continue;
      }
    }

    if( groupFollowups && chatMessage->isNormalMessage() )
    {
      // See if the message should be added to the queue.
      // There could be another follow-up message later.
      if( ! lastContactMessages_.isEmpty() )
      {
        // Check whether the contact differs or it's a different type,
        // the handle to difference between "offline_incoming" and "incoming" messages.
        ChatMessage *lastMessage = lastContactMessages_.last();
        if( lastMessage->getType()          != chatMessage->getType()
        ||  lastMessage->getContactHandle() != chatMessage->getContactHandle() )
        {
          // New message should not be added to queue,
          // so previous message was the last one. Flush the queue.
          newHtml += "\n<div class=\"messageContainer messageListContainer\">"
                  +  chatStyle_->convertMessageList( lastContactMessages_ )
                  +  "</div>\n";
          lastContactMessages_.clear();
        }
      }

      // Append new contact message to the queue.
      lastContactMessages_.append(chatMessage);
    }
    else
    {
      // Not a contact message.
      // First flush the queue, then add the new message.
      if( ! lastContactMessages_.isEmpty() )
      {
        newHtml += "\n<div class=\"messageContainer messageListContainer\">"
                +  chatStyle_->convertMessageList( lastContactMessages_ )
                +  "</div>\n";
        lastContactMessages_.clear();
      }

      // Convert the current message.
      newHtml += "\n<div class=\"messageContainer\">"
              +  chatStyle_->convertMessage( *chatMessage )
              +  "</div>\n";
    }
  }

  // If there are still grouped messages, add them after setHtml().
  // This allows the chatMessageView_ to replace them later with replaceLastMessage();
  if( ! lastContactMessages_.isEmpty() )
  {
    newHtml += "\n<div class=\"messageContainer messageListContainer\">"
            +  chatStyle_->convertMessageList( lastContactMessages_ )
            +  "</div>\n";
    lastContactMessages_.clear();
  }

  return newHtml;
}



// Delete an emoticon from the chat.
void ChatMessageView::removeCustomEmoticon( const QString &shortcut )
{
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
  kDebug() << "Deleting emoticons with shortcut:" << shortcut;
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( ! shortcut.isEmpty() );
#endif

  // Process all IMG tags
#if KDE_IS_VERSION( 4, 1, 0 )
  DOM::NodeList emoticonTags( htmlDocument().getElementsByClassName( "customEmoticon" ) );
#else
  // See below (within the loop) for the actual filtering by class name
  DOM::NodeList emoticonTags( htmlDocument().getElementsByTagName( "img" ) );
#endif

  // Get the original (non-HTML) shortcut
  QString originalShortcut( KMessShared::htmlUnescape( shortcut ) );

  // Proceed one by one: while removing elements, the list shortens too
  for( unsigned long item = emoticonTags.length(); item > 0; --item )
  {
    DOM::HTMLElement img( emoticonTags.item( item - 1 ) );

#if KDE_IS_VERSION( 4, 1, 0 )
#else
    // KDE4.0's KHTML DOM didn't have getElementsByClassName(). We therefore
    // have to filter the class name here.
    if( ! img.className().string().contains( "customEmoticon" ) )
    {
      continue;
    }
#endif

    // Check if the element is valid
    if( img.isNull() )
    {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
      kDebug() << "Skipped null element.";
#endif
      continue;
    }

    // Get the image's alternative text attribute; if it's not our custom emoticon, skip
    // HACK: Search both by the html and plain version, to avoid skipping strange shortcuts
    QString altAttribute( img.getAttribute("alt").string() );
    if( altAttribute != shortcut && altAttribute != originalShortcut )
    {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
      kDebug() << "Skipped element:" << img.getAttribute("alt").string() << "while searching for" << originalShortcut;
#endif
      continue;
    }

    // We've found one of the emoticons to delete

    DOM::HTMLElement parent( img.parentNode() );

    // If the emoticon is contained in a link, remove both.
    if( parent.tagName().string() == "a" )
    {
      img = parent;
      parent = parent.parentNode();
    }

    // The text should be that of the original shortcut, not the html-encoded one
    DOM::Text textNode( htmlDocument().createTextNode( originalShortcut ) );

    // Replace the link with the image inside with the simple text
    parent.replaceChild( textNode, img );

    // Force updating the view to instantly display the new image
    view()->updateContents( img.getRect() );
  }
}



// Replace the last message with a new contents.
void ChatMessageView::replaceLastMessage(const QString &text)
{
  // Fetch the HTML node
  QString lastId( "message" + QString::number( lastMessageId_ ) );
  DOM::HTMLElement lastNode( htmlDocument().getElementById( lastId ) );
  if( lastNode.isNull() || ! lastNode.isHTMLElement() )
  {
    kWarning() << "message block with id" + lastId + "not found, appending message instead.";
    addHtmlMessage( text );
    return;
  }

  // Replace contents
  lastNode.setInnerHTML( text );
}



// Scroll forward or backward within the chat browser
00666 void ChatMessageView::scrollChat( bool forward, bool fast )
{
  view()->scrollBy( 0, ( forward ? +1 : -1 ) * view()->visibleHeight() / ( fast ? 1 : 2 ) );
}



// Scroll to the bottom of the chat browser
void ChatMessageView::scrollChatToBottom()
{
  int contentsHeight = view()->contentsHeight();
  int visibleHeight = view()->visibleHeight();

  // If the user has scrolled up more than one viewport of height, don't move
  if( contentsHeight - ( view()->contentsY() + visibleHeight ) > visibleHeight )
  {
    return;
  }

  view()->scrollBy( 0, contentsHeight );
}



// Replace the entire contents with new HTML code
void ChatMessageView::setHtml( const QString &newHtmlBody )
{
  // Reset state variables
  lastMessageId_ = 0;

  // If the style is not working, prepare a standard HTML page
  if( ! chatStyle_->canConvert() )
  {
    QString cssFile   ( chatStyle_->getCssFile   () );
    QString baseFolder( chatStyle_->getBaseFolder() );

    if( ! cssFile.isEmpty() )
    {
      cssFile    = "    <link href=\"" + cssFile + "\" rel=\"stylesheet\" type=\"text/css\">\n";
    }
    if( ! baseFolder.isEmpty() )
    {
      baseFolder = "    <base href=\"" + baseFolder + "\" id=\"baseHrefTag\">\n";
    }

    begin();

    // Force standard colors, because chat messages will not work
    // correctly with the (dark) color scheme anyway.
    write( "<html id=\"ChatMessageView\">\n"
           "  <head>\n"
           "    <!-- " + getStyleTag() + " -->\n"
           "    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=" + encoding() + "\">\n"
           + baseFolder +
           "    <style type=\"text/css\">\n"
           "      /* standard colors for compatibility with dark color schemes */\n"
           "      body      { font-size: 10pt; margin: 0; padding: 5px; background-color: #fff; color: #000; }\n"
           "      a:link    { color: blue; }\n"
           "      a:visited { color: purple; }\n"
           "      a:hover   { color: red; }\n"
           "      a:active  { color: red; }\n"
           "    </style>\n"
           + cssFile +
           "  </head>\n"
           "  <body>\n"
           "    <div id=\"messageRoot\">\n"
           + newHtmlBody +
           "    </div>\n"
           "  </body>\n"
           "</html>\n" );
    end();

    return;
  }

  begin();

  // Write the style's root elements
  write( chatStyle_->convertMessageRoot() );

  // Add the style tag
  DOM::NodeList list( document().getElementsByTagName("head") );
  if( ! list.isNull() )
  {
    DOM::Node head = list.item( 0 );
    head.insertBefore( document().createComment( getStyleTag() ), head.firstChild() );
  }


  // Insert under root the given HTML body, but do nothing if we're just clearing it up
  // (the new page is empty already)
  if( ! newHtmlBody.isEmpty() )
  {
    DOM::HTMLElement messageRoot = document().getElementById( "messageRoot" );
    if( messageRoot.isNull() )
    {
      kWarning() << "Chat style does not define the 'messageRoot' element!";
      messageRoot = htmlDocument().body();
    }
    messageRoot.setInnerHTML( "\n" + newHtmlBody + "\n" );
  }

  // Complete loading
  end();
}



// Replace the entire contents with a new chat in XML
void ChatMessageView::setXml( const QString &newXmlBody )
{
  setHtml( chatStyle_->convertXmlMessageList( newXmlBody ) );
}



// Add the given message to the message browser.
void ChatMessageView::showMessage( const ChatMessage &message )
{
  // Avoid duplicating the same presence message.
  if( message.getType() == ChatMessage::TYPE_PRESENCE
  &&  ! chatMessages_.isEmpty() )
  {
    ChatMessage *lastChatMessage = chatMessages_.last();
    if( lastChatMessage->getType()          == message.getType()
    &&  lastChatMessage->getContentsClass() == message.getContentsClass()
    &&  lastChatMessage->getContactHandle() == message.getContactHandle() )
    {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
      kDebug() << "Not showing presence message for the same contact again.";
#endif
      return;
    }
  }

  // Clone the message so it can be stored in the local qlist objects.
  ChatMessage *chatMessage = message.clone();
  QString messageHtml;

  // See if the same contact sent the previous message too.
  // In that case, we combine both messages for the chat style.
  // Otherwise the lastContactMessages_ list is reset.
  if( chatMessage->isNormalMessage() && chatStyle_->getGroupFollowupMessages() )
  {
    if( ! lastContactMessages_.isEmpty() )
    {
      ChatMessage *lastContactMessage = lastContactMessages_.last();

      // Check for contact handle and message type, so offline messages won't be
      // grouped with normal incoming messages.
      if( lastContactMessage->getContactHandle() == chatMessage->getContactHandle()
      &&  lastContactMessage->getType()          == chatMessage->getType() )
      {
        lastContactMessages_.append(chatMessage);
      }
      else
      {
        lastContactMessages_.clear();
      }
    }
  }
  else
  {
    lastContactMessages_.clear();
  }

  // Convert the message, add to the browser.
  if( lastContactMessages_.count() > 1 )
  {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
    kDebug() << "replacing last contact message with new contents.";
#endif

    messageHtml = chatStyle_->convertMessageList(lastContactMessages_);
    replaceLastMessage( messageHtml );
  }
  else
  {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
    kDebug() << "appending new message.";
#endif

    messageHtml = chatStyle_->convertMessage( *chatMessage );
    addHtmlMessage( messageHtml );

    if( chatMessage->isNormalMessage() )
    {
      lastContactMessages_.append( chatMessage );
    }
  }

  // If the parser found custom emoticons, add them.
  // The pending list of chatStyle is erased with each new convertMessage() call.
  const QStringList &emoticonTags = chatStyle_->getPendingEmoticonTagIds();
  if( ! emoticonTags.isEmpty() )
  {
    // Copy to the internal list
    foreach( const QString &tag, emoticonTags )
    {
      pendingEmoticonTags_.append( tag );
    }
  }

  // Add to memory, this is used for:
  // - changing chat styles
  // - regenerating messages to group them
  // - regenerating messages for received custom emoticons.
  chatMessages_.append( chatMessage );

  // Call the scroll function a bit later,
  // so Qt/kde get a chance to update the height before the scrolling starts.
  QTimer::singleShot( 50, this, SLOT( scrollChatToBottom() ) );
}



// Replace an application's accept/reject/cancel links with another text
void ChatMessageView::updateApplicationMessage( const QString &messageId, const QString &newMessage )
{
  DOM::HTMLDocument document( htmlDocument() );
  DOM::HTMLElement linksSpan = document.getElementById( "app" + messageId + "-links-block" );

  // This chat doesn't contain messages for this application, bail out
  if( linksSpan.isNull() )
  {
    return;
  }

  // Change the span's ID so it won't trigger changes anymore
  linksSpan.setId( DOM::DOMString( "app" + messageId ) );

  // Replace the links with the given text
  linksSpan.setInnerText( DOM::DOMString( newMessage ) );
}



// Update the chat style
void ChatMessageView::updateChatStyle()
{
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
  kDebug() << "updating style.";
#endif

  if(KMESS_NULL(chatStyle_))      return;
  if(KMESS_NULL(currentAccount_)) return;

  // If the contact font settings have not changed, avoid doing useless work
  if( currentAccount_->getUseContactFont()        == chatStyle_->getUseContactFont()
  &&  currentAccount_->getContactFont()           == chatStyle_->getContactFont()
  &&  currentAccount_->getContactFontColor()      == chatStyle_->getContactFontColor()
  &&  currentAccount_->getGroupFollowupMessages() == chatStyle_->getGroupFollowupMessages()
  &&  currentAccount_->getShowMessageTime()       == chatStyle_->getShowMessageTime()
  &&  currentAccount_->getTimestampShowSeconds()  == chatStyle_->getShowMessageSeconds()
  &&  currentAccount_->getTimestampShowDate()     == chatStyle_->getShowMessageDate()
  &&  currentAccount_->getUseContactFont()        == chatStyle_->getUseContactFont()
  &&  currentAccount_->getUseEmoticons()          == chatStyle_->getUseEmoticons()
  &&  currentAccount_->getUseFontEffects()        == chatStyle_->getUseFontEffects()
  &&  currentAccount_->getUseChatFormatting()     == chatStyle_->getUseChatFormatting()
  &&  currentAccount_->getChatStyle()             == chatStyle_->getName()
  &&  currentAccount_->getEmoticonStyle()         == chatStyle_->getEmoticonStyle() )
  {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
    kDebug() << "Update not needed.";
#endif
    return;
  }

  // Load new settings in the chat style object
  chatStyle_->setStyle                ( currentAccount_->getChatStyle()             );
  chatStyle_->setContactFont          ( currentAccount_->getContactFont()           );
  chatStyle_->setContactFontColor     ( currentAccount_->getContactFontColor()      );
  chatStyle_->setEmoticonStyle        ( currentAccount_->getEmoticonStyle()         );
  chatStyle_->setShowTime             ( currentAccount_->getShowMessageTime()       );
  chatStyle_->setShowSeconds          ( currentAccount_->getTimestampShowSeconds()  );
  chatStyle_->setShowDate             ( currentAccount_->getTimestampShowDate()     );
  chatStyle_->setUseContactFont       ( currentAccount_->getUseContactFont()        );
  chatStyle_->setUseEmoticons         ( currentAccount_->getUseEmoticons()          );
  chatStyle_->setUseFontEffects       ( currentAccount_->getUseFontEffects()        );
  chatStyle_->setUseFormatting        ( currentAccount_->getUseChatFormatting()     );
  chatStyle_->setGroupFollowupMessages( currentAccount_->getGroupFollowupMessages() );

  // Change the view with the updated HTML
  setHtml( rebuildHistory( false ) );

  // If there are still grouped messages, add them after setHtml().
  // This allows the chatMessageView_ to replace them later with replaceLastMessage();
  if( ! lastContactMessages_.isEmpty() )
  {
    addHtmlMessage( chatStyle_->convertMessageList(lastContactMessages_) );
    // Don't clear queue here, so replaceLastMessage() will be called.
  }

  // Call the scroll function a bit later,
  // so Qt/kde get a chance to update the height before the scrolling starts.
  QTimer::singleShot( 50, this, SLOT( scrollChatToBottom() ) );
}



// Update an emoticon image placeholder tag with the real replacement.
void ChatMessageView::updateCustomEmoticon( const QString &code, const QString &replacement,
                                            const QString &handle )
{
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
  kDebug() << "Replacing emoticon '" << code << "' with '" << replacement << "' for '" << handle << "'";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( ! code.isEmpty() );
  KMESS_ASSERT( ! replacement.isEmpty() );
  KMESS_ASSERT( ! handle.isEmpty() );
#endif

  // Check for empty replacements.
  if( replacement.isEmpty() )
  {
    kWarning() << "can't update custom emoticon, replacement not found (contact=" << handle << ").";
    return;
  }

  // The pattern used to find if the new custom emoticon is already in our theme
  QString customEmoticonsPattern( EmoticonManager::instance()->getHtmlPattern( true ).pattern() );

  // Process all pending tags, avoid parsing all <img> tags
  DOM::HTMLDocument document = htmlDocument();
  foreach( const QString &tag, pendingEmoticonTags_ )
  {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
    kDebug() << "Checking DOM element '" << tag << "'";
#endif

    // Check if the element is valid
    DOM::HTMLElement img = document.getElementById( tag );
    if( img.isNull() )
    {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
      kDebug() << "Skipped null element.";
#endif
      continue;
    }

    // Get the image's alternative text attribute
    QString imageAltText( img.getAttribute("alt").string() );

    // Before converting the alt attribute, save it; we'll use it later
    QString originalCode( imageAltText );

    // KHTML's DOM converts all HTML entities to text; so we have to convert them back,
    // to be able to compare the image's code to the emoticon's (which is already encoded)
    KMessShared::htmlEscape( imageAltText );

    // See if this element's ALT attribute matches the shortcut
    if( img.isNull() || imageAltText != code )
    {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
      kDebug() << "Skipped invalid element (searched '" << code << "', found '" << imageAltText << "').";
#endif
      continue;
    }

    // Also check whether the handle is also set, avoid replacing someone elses code.
    if( img.getAttribute("contact") != handle )
    {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
      kDebug() << "Emoticon code found, but different handle: " << img.getAttribute("contact");
#endif
      continue;
    }


    // Update image placeholder attributes (replacing childs is too much trouble).
    // Create a regexp to parse the replacement attributes.
    QRegExp attribRegExp(
                          "([a-z]+)="       // words followed by an =
                          "(?:"             // start of options
                          "'([^']*)'|"      // attrib separated by single quote, or..
                          "\"([^\"]*)\"|"    // attrib separated by double quote, or..
                          "([^ \t\r\n>]+)"  // attrib followed by space, newline, tab, endtag
                          ")"               // end of options
                         );
#ifdef KMESSTEST
    KMESS_ASSERT( attribRegExp.isValid() );
#endif

    int attribPos = 0;
    while(true)
    {
      // Find next attribute
      attribPos = attribRegExp.indexIn(replacement, attribPos);
      if( attribPos == -1 )
      {
        break;
      }

#ifdef KMESSDEBUG_CHATMESSAGEVIEW
      kDebug() << "Emoticon replacement has attribute: " << attribRegExp.cap(1) << "=" << attribRegExp.cap(2);
#endif
      img.setAttribute( attribRegExp.cap(1), attribRegExp.cap(2) );

      // Also change the image's class and reset its ID
      img.setAttribute( "class", "customEmoticon" );
      img.setAttribute( "id", "" );

      attribPos += attribRegExp.matchedLength();
    }


    // Allow the user to "steal" this emoticon
    if( handle != CurrentAccount::instance()->getHandle()
    &&  ! customEmoticonsPattern.contains( code ) )
    {
#ifdef KMESSDEBUG_CHATMESSAGEVIEW
      kDebug() << "Inserting emoticon addition link";
#endif
      // Copy the original image to a new one we'll modify
      DOM::HTMLElement newImage = img.cloneNode( true );

      // Remove any title in the emoticon image, as it would be displayed instead of the
      // title from the link we're creating
      newImage.setAttribute( "title", "" );

      // URL-encode the shortcut, so the emoticon adding dialog will get a correct representation,
      // even if it contains characters which would fool the link parser.
      QString urlCode( KUrl::toPercentEncoding( code ) );

      // Create a new 'link' element
      // The name attribute is required as, if the user adds the emoticon, we'll want to make all links like this unclickable
      DOM::HTMLElement newLink = document.createElement("a");
      newLink.setAttribute( "name", "newEmoticon_" + urlCode );
      newLink.setAttribute( "title", i18n( "Add this emoticon: %1", code ) ); // Not escaped for " or ', KHTML does it
      newLink.setAttribute( "href", "kmess://emoticon/" + handle + "/" + urlCode + "/"
                            + KUrl::toPercentEncoding( newImage.getAttribute("src").string() ) );

      // Add the new image as child of the new link
      newLink.appendChild( newImage );

      // And put the link in place of the old image
      img.parentNode().replaceChild( newLink, img );
    }
  }

  // Force updating the view to display instantly the new image
  view()->updateContents( QRect( view()->contentsX(),
                                 view()->contentsY(),
                                 view()->visibleWidth(),
                                 view()->visibleHeight() ) );
}

//
// Creates the default actions for a popup menu. Copy, Select All and Find.
// Attaches them to slots on this class.
//
// Users of this class can add their own entries to the popup menu by getting 
// the KMenu instance representing the popup using ChatMessageView::popupMenu().
//
void ChatMessageView::createPopupMenuActions()
{
  copyAction_       = KStandardAction::copy( this, SLOT(slotCopyChatText()), this );
  selectAllAction_  = KStandardAction::selectAll( this, SLOT(slotSelectAllChatText()), this );
  findAction_       = KStandardAction::find( this, SLOT(slotFindChatText()),   this );
}

//
// Returns a KMenu containing the "basic" items for a popup menu for a chatmessageview
// These basic items are "Select All", "Copy Text" and "Find text".
//
// NOTE: It is the CALLER'S responsibility to delete the KMenu instance. It is also their
// responsibility to actually show the menu using KMenu::exec().
//
KMenu *ChatMessageView::popupMenu()
{
  // Add items to this context menu
  KMenu *contextMenu = new KMenu( view() );

  // Update the labels a bit though
  copyAction_      ->setText( i18n("&Copy Text") );
  selectAllAction_ ->setText( i18n("Select &All") );
  findAction_      ->setText( i18n("Find &Text..." ) );

  contextMenu->addAction( copyAction_ );
  contextMenu->addAction( selectAllAction_ );
  contextMenu->addAction( findAction_ );

  // Set items disabled, depending on the text selection
  copyAction_->setEnabled( hasSelection() );

  return contextMenu;
}

// The user clicked the "copy text" option in the context menu.
void ChatMessageView::slotCopyChatText()
{
//  For HTML use selectedTextAsHTML();
  kapp->clipboard()->setText( selectedText() );
}

// The user clicked the "find text" option in the context menu
void ChatMessageView::slotFindChatText()
{
  findText();
}

// The user clicked the "select all" option in the context menu.
void ChatMessageView::slotSelectAllChatText()
{
  selectAll();
}

#include "chatmessageview.moc"

Generated by  Doxygen 1.6.0   Back to index