Logo Search packages:      
Sourcecode: kmess version File versions

chatview.cpp

/***************************************************************************
                          chatview.cpp  -  description
                             -------------------
    begin                : Wed Jan 15 2003
    copyright            : (C) 2003 by Mike K. Bennett
    email                : mkb137b@hotmail.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 "chatview.h"

#include <qcolor.h>
#include <qpushbutton.h>
#include <qregexp.h>
#include <qtextbrowser.h>
#include <qtextedit.h>
#include <qtoolbox.h>
#include <qlayout.h>
#include <qclipboard.h>
#include <qstringlist.h>
#include <qtextcodec.h>

#include <kapplication.h>
#include <kdebug.h>
#include <kglobal.h>
#include <klocale.h>
#include <kstddirs.h>
#include <ktextbrowser.h>
#include <kurl.h>
#include <khtmlview.h>
#include <kfiledialog.h>
#include <kaction.h>
#include <kpopupmenu.h>

#include "chatmessage.h"
#include "chatmessagestyle.h"
#include "chatmessageview.h"

#include "../contact/contactbase.h"
#include "../currentaccount.h"
#include "../kmessdebug.h"

// The constructor
ChatView::ChatView(QWidget *parent, const char *name )
 : ChatViewInterface(parent,name),
   chatStyle_(0),
   currentAccount_(0),
   doSendTypingMessages_(true),
   initialized_(false)
{
  delete contactSidebar_;
  contactSidebar_ = 0;


  // Insert a KHTMLPart in the placeholder
  chatMessageView_ = new ChatMessageView( khtmlPlaceholder_, "ChatMessageView" );
  connect( chatMessageView_, SIGNAL(          appCommand(QString,QString,QString)       ),
           this,             SLOT  (   forwardAppCommand(QString,QString,QString)       ));
  connect( chatMessageView_, SIGNAL(           popupMenu(const QString&, const QPoint&) ),
           this,             SLOT  ( slotShowContextMenu(const QString&, const QPoint&) ));

  // Create a layout to maximize the KHTMLPart
  QBoxLayout *layout = new QHBoxLayout( khtmlPlaceholder_ );
  layout->addWidget( chatMessageView_->view() );   // Stretches widget

  // Set default colors of message edit, because the text color will be overwritten with the defined one.
  messageEdit_->setPaletteBackgroundColor( Qt::white );
  messageEdit_->setPaletteForegroundColor( Qt::black );

  // Connect the message edit so that if its displayed color changes,
  //  it checked to match the user's chosen color.
  connect( messageEdit_, SIGNAL( currentColorChanged( const QColor & ) ),
           this,         SLOT  (  editorColorChanged( const QColor & ) ) );

  // Set initial widget state
  sendButton_->setEnabled( false );
  messageEdit_->setAcceptDrops(true);
  messageEdit_->setFocus();
  sidebarSplitter_->setResizeMode( sidebar_, QSplitter::KeepSize );

  // Enable auto-delete
  chatMessages_.setAutoDelete(true);
}



// The destructor
ChatView::~ChatView()
{
  // Clear messages, chat style
  chatMessages_.clear();
  delete chatStyle_;
}



// Delete the newline behind the message edit's cursor.
void ChatView::deleteNewlineAtCursor()
{
  int      endpara, endindex, startpara, startindex;

  // Get the cursor's position in the message edit
  messageEdit_->getCursorPosition(&endpara, &endindex);

  // The position before the cursor will be one index behind the end,
  //  unless the index is zero.
  if ( endindex == 0 )
  {
    startpara = endpara - 1;
    startindex = messageEdit_->paragraphLength( startpara );
    // Select the character behind the cursor
    messageEdit_->setSelection(startpara, startindex, endpara, endindex);
    // Delete the selection, which should be a newline.
    messageEdit_->del();
  }
}



// The color in the text box changed.
void ChatView::editorColorChanged(const QColor &color)
{
#ifdef KMESSTEST
  ASSERT( currentAccount_ != 0 );
#endif
  // Sometimes the text box color seems to spontaneously reset to black.
  // If this happened, set the color back to the user's color.
  if ( color.name() != currentAccount_->getFontColor() )
  {
#ifdef KMESSDEBUG_CHATVIEW
    kdDebug() << "ChatView - Restore the color to the account setting by calling 'updateEditorFont'." << endl;
#endif
    updateEditorFont();
  }
}



// Copy the currently selected text
void ChatView::editCopy()
{
  if( chatMessageView_->hasSelection() )  // never has focus, only selection.
  {
    kapp->clipboard()->setText( chatMessageView_->selectedText() );
    // setData() for HTML version.
  }
  else if( messageEdit_->hasFocus() )
  {
    messageEdit_->copy();
  }
}


// The user pressed return in the message editor, so send the message
void ChatView::enterPressed()
{
  if(! messageEdit_->text().isEmpty())
  {
    // Don't send any typing messages while preparing to send the message in the text box...
    doSendTypingMessages_ = false;

    // Pressing enter caused a newline to be entered under the cursor... remove it.
    deleteNewlineAtCursor();

    // Send the message.
    sendMessage();

    // Now messages can again be sent
    doSendTypingMessages_ = true;
  }
}



// Initialize the object
bool ChatView::initialize()
{
#ifdef KMESSDEBUG_CHATVIEW
  kdDebug() << "ChatView: initializing." << endl;
#endif

  if ( initialized_ )
  {
    kdDebug() << "ChatView already initialized." << endl;
    return false;
  }
  if ( !initializeCurrentAccount() )
  {
    return false;
  }

  // Get chat style
  chatStyle_ = new ChatMessageStyle();
  updateChatStyle();

  initialized_ = true;
  return true;
}



// Initialize the current account
bool ChatView::initializeCurrentAccount()
{
  currentAccount_ = CurrentAccount::instance();
  if ( currentAccount_ == 0 )
  {
    kdDebug() << "ChatView - Couldn't get the instance of the current account." << endl;
    return false;
  }
  connect( currentAccount_, SIGNAL(      changedFontSettings() ),
           this,            SLOT  (         updateEditorFont() ) );
  connect( currentAccount_, SIGNAL( changedChatStyleSettings() ),
           this,            SLOT  (          updateChatStyle() ) );
  updateEditorFont();
  return true;
}



// Return the encoding of the HTML document.
QString ChatView::getEncoding() const
{
  return chatMessageView_->encoding();
}



// Return the HTML source of the page.
QString ChatView::getHtml() const
{
  return chatMessageView_->getHtml();
}



// Return the selected text in the message area.
QString ChatView::getSelectedText(bool asHtml) const
{
  if( asHtml )
  {
#if KDE_IS_VERSION(3,4,0)
    return chatMessageView_->selectedTextAsHTML();
#else
    return chatMessageView_->selectedText();
#endif
  }
  else
  {
    return chatMessageView_->selectedText();
  }
}



// Insert an emoticon into the message editor
void ChatView::insertEmoticon( QString emoticonText )
{
  // Insert the text at the cursor.
  messageEdit_->insert( emoticonText );
  // Delete the newline there
  deleteNewlineAtCursor();
}



// Whether or not the message area is empty
bool ChatView::isEmpty() const
{
  return chatMessageView_->isEmpty();
}



// Forward the app command from the chat message view
void ChatView::forwardAppCommand(QString cookie, QString contact, QString method)
{
  emit appCommand(cookie, contact, method);
}



// The message text changed, so the user is typing
void ChatView::messageTextChanged()
{
  if ( doSendTypingMessages_ )
  {
    // Disable or enable the send button depending on whether or not the message edit is empty
    if ( messageEdit_->text().isEmpty() )
    {
      sendButton_->setEnabled( false );
    }
    else
    {
      sendButton_->setEnabled( true );

      // If the last character of the message is a newline, sending the typing signal will
      //  cause the actual text message to be received twice, so check the last character...
      if ( messageEdit_->text().right(1) != "\n" )
      {
        // If the typing timer isn't already going...
        if ( !userTypingTimer_.isActive() )
        {
          emit userIsTyping();
          userTypingTimer_.start( 4000, true );
        }
      }
    }
  }
}



// The user clicked the new line button so insert a new line in the editor
void ChatView::newLineClicked()
{
  messageEdit_->insert("\n");
}



// Save the chat to the given file
void ChatView::saveChatToFile( const QString &path )
{
#ifdef KMESSDEBUG_CHATVIEW
  kdDebug() << "ChatView: saving chat to '" << path << "'." << endl;
#endif

  // Create and open the file.
  QFile file( path );
  if( ! file.open( IO_WriteOnly ) )
  {
    kdWarning() << "ChatView: File save failed - couldn't open file '" << path << "'." << endl;
  }

  // Get the text
  // Make the text show nicely when viewed in an editor.
  QString text = getHtml();
  text = text.replace( QRegExp("<br>"), "<br>\n" );

  // Get encoding
  QTextCodec *codec = QTextCodec::codecForName(getEncoding());
  if(codec == 0)
  {
    kdWarning() << "Could not find codec '" << getEncoding() << ", special characters might not be saved correctly!" << endl;
  }
#ifdef KMESSDEBUG_CHATVIEW
  else
  {
    kdDebug() << "ChatView: found codec '" << codec->name() << "' for '" << getEncoding() << "'." << endl;
  }
#endif

  // Output the HTML with the right text encoding
  QTextStream textStream( &file );
  if(codec != 0)
  {
    textStream.setCodec( codec );
  }
  textStream << text;
  file.close();
}



// Scroll to the bottom of the chat browser
void ChatView::scrollChatToBottom()
{
  chatMessageView_->scrollChatToBottom();
}



// The user clicked send, so send the message
void ChatView::sendClicked()
{
  sendMessage();

  // Ensure the typing area always gets focus,
  // so the user can type the next message directly.
  // This area looses focus when you do other things like choosing emoticons.
  messageEdit_->setFocus();
}



// Send a message via the server
void ChatView::sendMessage()
{
  uint maxSendableMessageLength = 1400;

  QString text = messageEdit_->text();

  if(! text.isEmpty())
  {
    messageEdit_->clear();

    // If the text is longer than the sendable amount, put the remainder back in the text edit.
    // Since the message will be sent as UTF8, it's the UTF8 length we have to consider.
    QCString utf8Text = text.utf8();
    if ( utf8Text.length() > maxSendableMessageLength )
    {
      // If so, then divide the text into the first part and a remainder.
      QCString remainder = utf8Text.right( utf8Text.length() - maxSendableMessageLength );
      text               = QString::fromUtf8( utf8Text.left( maxSendableMessageLength ) );

      // Return the remainder to the message edit.
      messageEdit_->setText( QString::fromUtf8( remainder ) );
    }

    // Replace "\n"s with "\r\n"s
    text = text.replace( QRegExp("\n"), "\r\n" );

    // Ask the server to send the message to the contact(s)
    emit sendMessageToContact( text );

    // Reset the typing timer, so it restarts correctly
    userTypingTimer_.stop();

    // Show the message in the browser window
    ChatMessage message(ChatMessage::TYPE_OUTGOING,
                        text,
                        currentAccount_->getHandle(),
                        currentAccount_->getFriendlyName(),
                        currentAccount_->getCustomImagePath(),
                        currentAccount_->getFont(),
                        currentAccount_->getFontColor());
    showMessage(message);
  }
}



// Add the given message to the message browser.
void ChatView::showMessage(const ChatMessage &message)
{
  if(KMESS_NULL(chatStyle_)) return;

  // Clone the message so it can be stored in the local qptrlist 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( message.isNormalMessage() && currentAccount_->getGroupFollowupMessages() )
  {
    ChatMessage *lastContactMessage = lastContactMessages_.last();
    if( lastContactMessage != 0 )
    {
      // Check for contact handle and message type, so offline messages won't be
      // grouped with normal incoming messages.
      if( lastContactMessage->getContactHandle() == message.getContactHandle()
      &&  lastContactMessage->getType()          == message.getType() )
      {
        lastContactMessages_.append(chatMessage);
      }
      else
      {
        lastContactMessages_.clear();
      }
    }
  }
  else
  {
    lastContactMessages_.clear();
  }

  // Convert the message, add to the browser.
  if( lastContactMessages_.count() > 1 )
  {
#ifdef KMESSDEBUG_CHATVIEW
    kdDebug() << "ChatView::showMessage: replacing last contact message with new contents." << endl;
#endif

    messageHtml = chatStyle_->convertMessageList(lastContactMessages_);
    chatMessageView_->replaceLastMessage(messageHtml);
  }
  else
  {
#ifdef KMESSDEBUG_CHATVIEW
    kdDebug() << "ChatView::showMessage: appending new message." << endl;
#endif

    messageHtml = chatStyle_->convertMessage(message);
    chatMessageView_->addHtmlMessage(messageHtml);

    if( message.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 internal list.
    for(QStringList::const_iterator it = emoticonTags.begin(); it != emoticonTags.end(); ++it)
    {
      pendingEmoticonTags_.append(*it);
    }
  }


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



// Show a dialog to save the chat.
void ChatView::showSaveChatDialog()
{
  QString path;

  // Show a dialog to get a filename from the user.
  path = KFileDialog::getSaveFileName();
  if( ! path.isNull() )
  {
    saveChatToFile( path );
  }
}



// The user clicked the "copy text" option in the context menu.
void ChatView::slotCopyChatText()
{
  kapp->clipboard()->setText( this->getSelectedText( false ) );
}



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



// The user right clicked at the KHTMLPart to show a popup.
void ChatView::slotShowContextMenu(const QString &url, const QPoint &point)
{
  // Create items
  KAction *copyAction       = KStdAction::copy( this, SLOT(slotCopyChatText()), 0 );  // , actionCollection() );
  KAction *selectAllAction  = KStdAction::selectAll( this, SLOT(slotSelectAllChatText()), 0 ); // , actionCollection() );
  KAction *saveToFileAction = KStdAction::save( this, SLOT(showSaveChatDialog()), 0 );  // , actionCollection() );

  // Update the labels a bit though
  copyAction      ->setText( i18n("&Copy text") );
  selectAllAction ->setText( i18n("Select &All") );
  saveToFileAction->setText( i18n("Save to &File") );

  // Add to the context menu
  KPopupMenu *contextMenu = new KPopupMenu( this );
  copyAction->plug( contextMenu );
  selectAllAction->plug( contextMenu );

  contextMenu->insertSeparator();
  saveToFileAction->plug( contextMenu );

  // Set items disabled, depending on the text selection
  if( chatMessageView_->hasSelection() )
  {
    saveToFileAction->setEnabled( false );
  }
  else
  {
    copyAction->setEnabled( false );
  }

  // Show the menu
  contextMenu->exec( point );
  delete contextMenu;
  delete copyAction;
}


// Update the editor's font to match the account's font
void ChatView::updateEditorFont()
{
#ifdef KMESSTEST
  ASSERT( currentAccount_ != 0 );
#endif
  QColor color;

  if( currentAccount_ == 0 )
  {
    return;
  }

  // Change the color to the user's color
  color.setNamedColor( currentAccount_->getFontColor() );
  messageEdit_->setColor( color );
  messageEdit_->setPaletteForegroundColor( color );
  messageEdit_->setFont( currentAccount_->getFont() );
  messageEdit_->setCurrentFont( currentAccount_->getFont() );
  messageEdit_->setPointSize( currentAccount_->getFont().pointSize() );

  // If the contact font settings changed, regenerate the messages.
  if( chatStyle_ != 0 )
  {
    if( currentAccount_->getUseContactFont()   != chatStyle_->getUseContactFont()
    ||  currentAccount_->getContactFont()      != chatStyle_->getContactFont()
    ||  currentAccount_->getContactFontColor() != chatStyle_->getContactFontColor() )
    {
      updateChatStyle();
    }
  }
}



// Update the chat style
void ChatView::updateChatStyle()
{
#ifdef KMESSDEBUG_CHATVIEW
  kdDebug() << "ChatView::updateChatStyle: updating style." << endl;
#endif

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

  // Load new settings in the chat style object
  chatStyle_->setStyle           ( currentAccount_->getChatStyle()        );
  chatStyle_->setContactFont     ( currentAccount_->getContactFont()      );
  chatStyle_->setContactFontColor( currentAccount_->getContactFontColor() );
  chatStyle_->setShowTime        ( currentAccount_->getShowMessageTime()  );
  chatStyle_->setUseContactFont  ( currentAccount_->getUseContactFont()   );
  chatStyle_->setUseEmoticons    ( currentAccount_->getUseEmoticons()     );
  chatStyle_->setUseFontEffects  ( currentAccount_->getUseFontEffects()   );

  // Support grouping of follow-up messages.
  bool groupFollowups = currentAccount_->getGroupFollowupMessages();
  lastContactMessages_.clear();

  // Replace the entire contents with the new style.
  QString newHtml;
  QPtrListIterator<ChatMessage> it(chatMessages_);
  while( it.current() != 0 )
  {
    ChatMessage *chatMessage = it.current();

    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 mesage should not be added to queue,
          // so previous mesage was last the 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";
    }
    ++it;
  }

  QString messageRoot = chatStyle_->convertMessageRoot();
  if( messageRoot.isNull() )
  {
    // Assign standard HTML
    chatMessageView_->setStandardHtml( newHtml, chatStyle_->getCssFile(), chatStyle_->getBaseFolder() );
  }
  else
  {
    // Assign the custom HTML root
    chatMessageView_->setHtml( messageRoot, newHtml );
  }

  // If there are still grouped messages, add them after setHtml().
  // This allows the chatMessageView_ to replace them later with replaceLastMessage();
  if( ! lastContactMessages_.isEmpty() )
  {
    chatMessageView_->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, chatMessageView_, SLOT(scrollChatToBottom()));
}



// Update the messages which contain custom emoticons
void ChatView::updateCustomEmoticon( const QString &handle, const QString &code )
{
#ifdef KMESSTEST
  ASSERT( ! code.isEmpty() );
#endif

  // Check for empty replacements.
  if( code.isEmpty() )
  {
    kdWarning() << "ChatView: can't update custom emoticon, emoticon code not given (contact=" << handle << ")." << endl;
    return;
  }

  // Get contact emoticon replacements.
  const ContactBase *contact = currentAccount_->getContactByHandle(handle);
  if(KMESS_NULL(contact)) return;

  // Get emoticon replacement, instruct chatMesasgeView to replace it.
  const QString &replacement = contact->getEmoticonReplacements()[code];
  chatMessageView_->updateCustomEmoticon( code, replacement, handle, pendingEmoticonTags_ );
}


#include "chatview.moc"

Generated by  Doxygen 1.6.0   Back to index