/***************************************************************************
                          kmessview.cpp  -  description
                             -------------------
    begin                : Thu Jan 9 2003
    copyright            : (C) 2003 by Mike K. Bennett
                           (C) 2005 by Diederik van der Boor
    email                : mkb137b@hotmail.com
                           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 "kmessview.h"

#include "contact/contact.h"
#include "contact/group.h"
#include "contact/msnstatus.h"
#include "contact/specialgroups.h"
#include "dialogs/chathistorydialog.h"
#include "dialogs/contactpropertiesdialog.h"
#include "model/contactlist.h"
#include "model/contactlistmodelfilter.h"
#include "utils/kmessshared.h"
#include "utils/kmessconfig.h"
#include "utils/richtextparser.h"
#include "currentaccount.h"
#include "emoticonmanager.h"
#include "kmessdebug.h"
#include "kmessviewdelegate.h"

#include <QClipboard>
#include <QKeyEvent>
#include <QImage>
#include <QItemSelectionModel>
#include <QPixmap>
#include <QSignalMapper>
#include <QStringList>
#include <QToolTip>

#include <KAction>
#include <KActionMenu>
#include <KApplication>
#include <KGlobal>
#include <KIO/NetAccess>
#include <KIconLoader>
#include <KIconEffect>
#include <KLocale>
#include <KMenu>
#include <KMessageBox>
#include <KRun>
#include <KUrl>


#ifdef KMESSDEBUG_CONTACTLISTMODELTEST
  #include "../tests/modeltest/modeltest.h"
#endif


#ifdef KMESSDEBUG_KMESSVIEW
  #define KMESSDEBUG_KMESSVIEW_GENERAL

  #ifdef KMESSDEBUG_CONTACTLISTMODEL
    #define KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
  #endif
#endif


// The constructor
KMessView::KMessView( QWidget *parent )
  : QWidget( parent ),
    Ui::KMessView(),
    contactCopyMapper_(0),
    contactMoveMapper_(0),
    currentAccount_(0),
    initialized_(false),
    viewModel_(0)
{
  setupUi( this );
  searchFrame_->hide();

  // Capture focus events without subclassing the widget from the .UI file
  currentAccountDisplayPic_->installEventFilter(this);
  contactListView_->installEventFilter(this);
  contactListView_->viewport()->installEventFilter(this);

  // Windows Live Messenger 8.5 and 2009 (latter only in the chat screen) don't
  // display any part further than 129 characters.
  personalMessageInput_->setMaxLength( 129 );

  friendlyNameInput_->setFormattingMode( STRING_LIST_SETTING );
  personalMessageInput_->setFormattingMode( STRING_LIST_SETTING );

  // Set up the group update timer
  updateTimer_.setSingleShot( true );
  connect( &updateTimer_, SIGNAL(          timeout() ),
           this,          SLOT  ( slotUpdateGroups() ) );

  // Set the owner window to always show tooltips on request,
  // even if it doesn't have focus.
  window()->setAttribute( Qt::WA_AlwaysShowToolTips );
}



// The destructor
KMessView::~KMessView()
{
  // Delete the contact actions for copy/move to group
  qDeleteAll( groupCopyActionsList_ );
  qDeleteAll( groupMoveActionsList_ );
  qDeleteAll( groupCopyLinkActionsList_ );
  delete contactCopyMapper_;
  delete contactMoveMapper_;

  delete selectionModel_;
  delete viewModel_;
  selectionModel_ = 0;
  viewModel_ = 0;

#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
  kDebug() << "DESTROYED.";
#endif
}



// The currently playing song was changed.
void KMessView::changedSong( const QString &artist, const QString &/*album*/, const QString &track, bool playing )
{
  if( playing )
  {
    // Show status line
    if( ! nowListeningFrame_->isVisible() )
    {
      nowListeningFrame_->setHidden(false);
      nowListeningFrame_->setMaximumHeight(40);
    }

    // Update label
    bool showDash = ( ! track.isEmpty() && ! artist.isEmpty() );
    QString longTitle( track + ( showDash ? " - " : "" ) + artist );
    nowListeningLabel_->setText( longTitle );
  }
  else
  {
    // Hide status line
    nowListeningFrame_->setHidden(true);
    nowListeningFrame_->setMaximumHeight(0);
  }
}



// Get the specified item's or the current item's data
const ModelDataList KMessView::getItemData( const QModelIndex &index )
{
  const QModelIndexList& selection( selectionModel_->selectedIndexes() );
  QModelIndex itemIndex( index );

  // If the index is not valid, get the current selection
  // TODO is this check necessary? isn't it better to return ModelDataList() to avoid any problems?
  if( ! itemIndex.isValid() && ! selection.isEmpty() )
  {
    // Only one index can be returned at once by the contact list (has single selection mode)
    itemIndex = QModelIndex( selection.first() );
   // return ModelDataList();
  }

  // The index or the selection aren't valid, abort.
  if( ! itemIndex.isValid() || itemIndex.model() != viewModel_ )
  {
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
    kDebug() << "Skipping an invalid item at index:" << index;
#endif
    return ModelDataList();
  }

  // Get the item data
  const QVariant& data( itemIndex.data() );

  // If the data is not a variant map, abort
  if( ! data.canConvert( QVariant::Map ) )
  {
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
    kDebug() << "Skipping an item with invalid data at index:" << index;
#endif
    return ModelDataList();
  }

  // Convert the variant data to a normal map
  const ModelDataList& itemData( data.toMap() );

  // Do nothing if there's no data
  if( itemData.isEmpty() )
  {
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
      kDebug() << "Skipping an empty item at index:" << index;
#endif
    return ModelDataList();
  }

  int type = itemData[ "type" ].toInt();

  // Do nothing if the item is not of a valid type
  if(    type != ContactListModelItem::ItemGroup
      && type != ContactListModelItem::ItemContact )
  {
    return ModelDataList();
  }

  return itemData;
}



// Initialize the class
bool KMessView::initialize( QAbstractItemModel *viewModel )
{
  bool         success;
  KIconLoader *loader = KIconLoader::global();

  // Get a ref to the current account
  currentAccount_ = CurrentAccount::instance();

  if ( initialized_ )
  {
    kDebug() << "already initialized.";
    return false;
  }

  success = initContactListView( viewModel );
  if(! success)
  {
    kDebug() << "Couldn't initialize contact list view.";
    return false;
  }

  success = initContactPopup();
  if(! success)
  {
    kDebug() << "Couldn't initialize contact popup.";
    return false;
  }

  success = initGroupPopup();
  if(! success)
  {
    kDebug() << "Couldn't initialize group popup.";
    return false;
  }

  // Set an icon for the email label
  // Using "knotify" icon because it's available everywhere. "multimedia" is SUSE-specific.
  emailPixmapLabel_       ->setPixmap( loader->loadIcon( "mail-mark-unread",  KIconLoader::NoGroup ) );
  nowListeningPixmapLabel_->setPixmap( loader->loadIcon( "speaker",           KIconLoader::NoGroup ) );
  searchIcon_             ->setPixmap( loader->loadIcon( "edit-find-user",    KIconLoader::Small ) );

  // Create a menu for the status button

  // Set the menu into button and connect the signals
  KMenu *menu = MsnStatus::getStatusMenu();
  statusButton_->setMenu( menu );
  connect( statusButton_, SIGNAL(      clicked()         ),
           statusButton_, SLOT  (     showMenu()         ) );

  // Enable the labels
  emailLabel_->setEnabled(true);

  QFont appFont( QApplication::font() );
  int appFontSize = appFont.pointSize();
  friendlyNameInput_->setStyleSheet( "QLabel { font-size: " + QString::number( appFontSize + 2 )  + "pt; }" );
  personalMessageInput_->setStyleSheet( "QLabel { font-size: " + QString::number( appFontSize - 1 )  + "pt; }" );

  // Hide the now listening until something is playing
  changedSong( QString(), QString(), QString(), false );

  // Connect signals for the current account
  connect( currentAccount_, SIGNAL(            changedMsnObject() ),
           this,            SLOT  (    slotUpdateDisplayPicture() ) );  // Changed users display pic
  connect( currentAccount_, SIGNAL(             changedNoEmails() ),    // Changed email count
           this,            SLOT  (          slotUpdateNoEmails() ) );
  connect( currentAccount_, SIGNAL(               changedStatus() ),    // Changed status
           this,            SLOT  (        slotUpdateUserStatus() ) );
  connect( currentAccount_, SIGNAL(         changedFriendlyName() ),    // Changed name
           this,            SLOT  (        slotUpdateUserStatus() ) );
  connect( currentAccount_, SIGNAL(      changedPersonalMessage() ),    // Changed personal message
           this,            SLOT  (        slotUpdateUserStatus() ) );
  connect( currentAccount_, SIGNAL( changedEmailDisplaySettings() ),    // Changed email display settings
           this,            SLOT  (      slotUpdateEmailDisplay() ) );
  connect( currentAccount_, SIGNAL(      changedDisplaySettings() ),
           this,            SLOT  (            slotUpdateGroups() ) );
  connect( currentAccount_, SIGNAL(             changedViewMode() ),
           this,            SLOT  (            slotUpdateGroups() ) );
  connect( currentAccount_, SIGNAL(   changedContactListOptions() ),     // Changed contact list options
           this,            SLOT  (              slotUpdateView() ) );
  connect( currentAccount_, SIGNAL(   changedContactListOptions() ),     // Changed contact list options
           this,            SLOT  (        slotUpdateUserStatus() ) );
  // These update the display pic size depending on the email & now listening options
  connect( currentAccount_, SIGNAL( changedNowListeningSettings() ),
           this,            SLOT  (    slotUpdateDisplayPicture() ) );
  connect( currentAccount_, SIGNAL( changedEmailDisplaySettings() ),
           this,            SLOT  (    slotUpdateDisplayPicture() ) );

  // Connect signals for the contactlist
  const ContactList *contactList = currentAccount_->getContactList();
  connect( contactList, SIGNAL(            groupAdded(const Group*)  ),   // A group was added
           this,          SLOT( rebuildContactActions()              ) );
  connect( contactList, SIGNAL(          groupChanged(const Group*)  ),   // A group was modified
           this,          SLOT( rebuildContactActions()              ) );
  connect( contactList, SIGNAL(          groupRemoved(const Group*)  ),   // A group was removed
           this,          SLOT( rebuildContactActions()              ) );

  // Connect signals for the history combobox and put the first string into it
  connect( contactList, SIGNAL(            contactOnline( Contact*,  bool ) ),
           this,          SLOT( slotContactChangedStatus( Contact *, bool ) ) );
  connect( contactList, SIGNAL(           contactOffline( Contact*,  bool ) ),
           this,          SLOT( slotContactChangedStatus( Contact *, bool ) ) );
  connect( contactList, SIGNAL(            contactOnline( Contact*,  bool ) ),
           this,          SLOT(  slotScheduleGroupUpdate()                  ) );
  connect( contactList, SIGNAL(           contactOffline( Contact*,  bool ) ),
           this,          SLOT(  slotScheduleGroupUpdate()                  ) );

  historyBox_->insertItem( 0, i18n( "[%1] Logged in with %2", QTime::currentTime ().toString( "HH:mm:ss" )
                                                         , currentAccount_->getHandle() ) );
  // Enable changing inline the friendly name and PM
  connect( friendlyNameInput_,    SIGNAL(               changed() ),
           this,                  SLOT  (    updateFriendlyName() ) );
  connect( personalMessageInput_, SIGNAL(               changed() ),
           this,                  SLOT  ( updatePersonalMessage() ) );

  // Connect the search edit line with slot for searching
  connect( searchEdit_, SIGNAL(       textChanged(QString) ),
           this,        SLOT  ( slotSearchContact(QString) ) );

  // Enable the search box if the user has left it open at the last disconnection
  toggleShowSearchFrame( currentAccount_->getShowSearchBar() );

  // Enable the history box
  toggleShowHistoryBox( currentAccount_->getShowHistoryBox() );

  // Run the update functions with the current user data
  slotUpdateEmailDisplay();
  slotUpdateUserStatus();
  slotUpdateNoEmails();
  slotUpdateDisplayPicture();
  slotUpdateGroups();

  updateFriendlyName( true );
  updatePersonalMessage( true );

  return true;
}



// Called when a contact is now online/offline
void KMessView::slotContactChangedStatus( Contact *contact, bool showNotify )
{
  // One contact is now online/offline, put it in history combobox
  if( showNotify )
  {
    const QString& friendlyname( contact->getFriendlyName() );
    QString item;

    if( contact->isOnline() )
    {
      item = i18n( "[%1] %2 goes online", QTime::currentTime ().toString( "HH:mm:ss" )
                                        , friendlyname );
    }
    else
    {
      item = i18n( "[%1] %2 goes offline", QTime::currentTime ().toString( "HH:mm:ss" )
                                         , friendlyname );
    }

    // Remove oldest item if the count is >= 500 elements
    if( historyBox_->count() >= 500 )
    {
      historyBox_->removeItem( historyBox_->count() - 1 );
    }

    historyBox_->insertItem( 0, item );
    historyBox_->setCurrentIndex ( 0 );
  }
}



// The connection was closed, clean up before deletion
void KMessView::slotDisconnected()
{
  updateTimer_.stop();

  contactListView_->setModel( 0 );
}



// The "show history box" menu item has been toggled.
void KMessView::toggleShowHistoryBox( bool show )
{
  historyBox_->setVisible( show );

  // Save the current state to the Account
  currentAccount_->setShowHistoryBox( show );
}



// The "show search in contact list" menu item has been toggled.
void KMessView::toggleShowSearchFrame( bool show )
{
  if( show )
  {
    searchFrame_->show();
    searchEdit_->setFocus();
  }
  else
  {
    // Remove the query when closing the search bar
    searchEdit_->clearFocus();
    searchEdit_->setText( QString() );

    searchFrame_->hide();
    contactListView_->setFocus();
  }

  // Save the current state to the Account
  currentAccount_->setShowSearchBar( show );
}



// The personal status message received an event.
bool KMessView::eventFilter(QObject *obj, QEvent *event)
{
  if(obj != currentAccountDisplayPic_
  &&  obj != contactListView_
  &&  obj != contactListView_->viewport() )
  {
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
    kWarning() << "Received event '" << event->type() << "' from object '" << obj->objectName() << "'.";
#endif
    return false;
  }

  QEvent::Type eventType = event->type();

  if( obj == currentAccountDisplayPic_ )
  {
    if( eventType == QEvent::Enter )
    {
      QImage glowingImage( currentAccount_->getPicturePath() );
      KIconEffect::toGamma( glowingImage, 0.8f );
      currentAccountDisplayPic_->setPixmap( QPixmap::fromImage( glowingImage ) );
    }
    else if( eventType == QEvent::Leave )
    {
      currentAccountDisplayPic_->setPixmap( QPixmap( currentAccount_->getPicturePath() ) );
    }
    else if( eventType == QEvent::MouseButtonRelease )
    {
      emit showSettings();
    }
    else if( eventType == QEvent::DragEnter )
    {
      QDragEnterEvent *dragEvent = static_cast<QDragEnterEvent*>( event );
      const QMimeData *mimeData = dragEvent->mimeData();

      // The mimeData object can contain one of the following objects: an image, HTML text, plain text, or a list of URLs.
      // We need only the first URL object
      if( mimeData->hasUrls() )
      {
        dragEvent->acceptProposedAction();
      }
      else
      {
        dragEvent->ignore();
      }
    }
    else if( eventType == QEvent::Drop )
    {
      QDropEvent* dropEvent = static_cast<QDropEvent*>( event );
      const QMimeData *mimeData = dropEvent->mimeData();

      // The mimeData object can contain one of the following objects: an image, HTML text, plain text, or a list of URLs.
      // We need only the first URL object
      if( ! mimeData->hasUrls() )
      {
        return true;
      }
      else
      {
        KUrl mimeUrl = mimeData->urls().first();
        bool isRemoteFile = false;
        QString localFilePath;
        QImage  pictureData;

        if( mimeUrl.isEmpty() )
        {
          kWarning() << "Dropped empty MIME URL";
          return true;
        }

        if( mimeUrl.isLocalFile() )
        {
          localFilePath = mimeUrl.path();
        }
        else
        {
          // File is remote, download it to a local folder
          // first because QPixmap doesn't have KIO magic.
          // KIO::NetAccess::download fills the localFilePath field.
          if( ! KIO::NetAccess::download(mimeUrl, localFilePath, this ) )
          {
            KMessageBox::sorry( this, i18n( "Downloading of display picture failed" ), i18n("KMess") );

            return true;
          }

          isRemoteFile = true;
        }

        if( ! pictureData.load( localFilePath ) )
        {
          kWarning() << "The file dropped is not an image or doesn't exist";
          return true;
        }

        if( isRemoteFile )
        {
          KIO::NetAccess::removeTempFile( localFilePath );
        }

        pictureData = pictureData.scaled( 96, 96, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation ); // Makes smallest edge 96 pixels, preserving aspect ratio

        if( pictureData.width() != 96 || pictureData.height() != 96 )
        {
          // Copy the middle part of the picture.
          pictureData = pictureData.copy( (pictureData.width() - 96) / 2,  // X
                                          (pictureData.height() - 96) / 2, // Y
                                          96, 96 );
        }

        QString pictureDir = KMessConfig::instance()->getAccountDirectory( currentAccount_->getHandle() ) + "/displaypics/";
        QString tempPictureFile = pictureDir + "temporary-picture.png";

        if( ! pictureData.save( tempPictureFile, "PNG" ) )
        {
          KMessageBox::sorry( this,
                              i18n( "An error occurred while trying to change the display picture.\n"
                                    "Make sure that you have selected an existing image file." ),
                              i18n( "KMess" ) );
        }

        QString msnObjectHash( KMessShared::generateFileHash( tempPictureFile ).toBase64() );
        const QString safeMsnObjectHash( msnObjectHash.replace( QRegExp("[^a-zA-Z0-9+=]"), "_" ) );

        // Save the new display picture with its hash as name, to ensure correct caching
        QString pictureName = msnObjectHash + ".png";
        QDir pictureFolder = QDir( pictureDir );

        // Do not overwrite identical pictures
        if( ! pictureFolder.exists( pictureName ) )
        {
          if( ! pictureFolder.rename( tempPictureFile, pictureName ) )
          {
            kWarning() << "The display picture file could not be renamed from" << tempPictureFile << "to" << pictureName << ".";
            return true;
          }
        }

        currentAccount_->setPicturePath( pictureDir + pictureName );
        currentAccount_->setOriginalPicturePath( mimeUrl.path() );

        dropEvent->acceptProposedAction();
      }
    }
  }
  else if( obj == contactListView_->viewport() )
  {
    if( eventType == QEvent::ToolTip )
    {
      showToolTip( QCursor::pos() );
      return true;
    }
  }
  else if( obj == contactListView_ )
  {
    if( eventType == QEvent::KeyPress )
    {
      int key = static_cast<QKeyEvent*>( event )->key();

      // Detect enter key press to open a chat with the selected contact
      if( key == Qt::Key_Return || key == Qt::Key_Enter )
      {
        const QModelIndexList& selection( selectionModel_->selectedIndexes() );

        // Get the currently selected item
        if( ! selection.isEmpty() )
        {
          slotItemClicked( selection.first(), false );
        }
      }
    }
  }

  return false;  // don't stop processing.
}



// Initialize the contact list view
bool KMessView::initContactListView( QAbstractItemModel *viewModel )
{
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
  kDebug() << "Initializing the contact list view.";
#endif

  // Create a proxy model to filter and sort the raw contact list view
  viewModel_ = new ContactListModelFilter( this );
  viewModel_->setSourceModel( viewModel );

  // Create a selection model to get data about the list selections
  selectionModel_ = new QItemSelectionModel( viewModel_ );

  // Activate the custom list item painter
  contactListView_->setItemDelegate( new KMessViewDelegate( contactListView_ ) );

  // Set the contact list view's properties
#if QT_VERSION >= 0x040400
  contactListView_->setHeaderHidden( true );
#else
  contactListView_->header()->hide();
#endif
  contactListView_->setAnimated( true );
  contactListView_->setDragEnabled( true );
  contactListView_->setDropIndicatorShown( true );
  contactListView_->setDragDropOverwriteMode( false );

  // TODO: support drag/drop contacts from another application!
  contactListView_->setDragDropMode( QAbstractItemView::InternalMove );
  contactListView_->viewport()->setAcceptDrops( true );
  contactListView_->setAcceptDrops( true );

  // Set the list stylesheet
  slotUpdateView();

  // Connect the list to the models
  contactListView_->setModel( viewModel_ );
  contactListView_->setSelectionModel( selectionModel_ );

  // Connect the list's signals
  connect( contactListView_, SIGNAL(               clicked(const QModelIndex&) ),
           this,               SLOT( slotItemSingleClicked(const QModelIndex&) ) );
  connect( contactListView_, SIGNAL(         doubleClicked(const QModelIndex&) ),
           this,               SLOT( slotItemDoubleClicked(const QModelIndex&) ) );
  connect( contactListView_, SIGNAL(             collapsed(const QModelIndex&) ),
           this,               SLOT(      slotGroupChanged(const QModelIndex&) ) );
  connect( contactListView_, SIGNAL(              expanded(const QModelIndex&) ),
           this,               SLOT(      slotGroupChanged(const QModelIndex&) ) );

  // Enable the right-click context menu
  contactListView_->setContextMenuPolicy( Qt::CustomContextMenu );
  connect( contactListView_, SIGNAL( customContextMenuRequested(const QPoint&) ),
           this,               SLOT(            showContextMenu(const QPoint&) ) );

  // Connect the selection model's signals
  connect( selectionModel_,  SIGNAL( selectionChanged(const QItemSelection&,const QItemSelection&) ),
           this,             SIGNAL( selectionChanged(const QItemSelection&) ) );

  return true;
}



// Initialize the contact popup
bool KMessView::initContactPopup()
{
  // Initialize context menu actions
  chatWithContact_   = new KAction( KIcon("user-group-new"),   i18n("Cha&t"), this );
  emailContact_      = new KAction( KIcon("mail-message-new"), i18n("&Send Email"), this );
  msnProfile_        = new KAction( KIcon("preferences-desktop-user"),    i18n("&View Profile"), this );
  chatHistory_       = new KAction( KIcon("chronometer"),      i18n("Show Chat &History..."), this );
  contactProperties_ = new KAction( KIcon("user-properties"),  i18n("&Properties"), this );

  addContact_        = new KAction( KIcon("list-add"),         i18n("&Add Contact"), this );
  allowContact_      = new KAction( KIcon("dialog-ok-apply"),  i18n("A&llow Contact"), this );
  blockContact_      = new KAction( KIcon("dialog-cancel"),    i18n("&Block Contact"), this );
  unblockContact_    = new KAction( KIcon("dialog-ok"),        i18n("&Unblock Contact"), this );
  removeContact_     = new KAction( KIcon("list-remove-user"), i18n("&Delete Contact"), this );
  removeFromGroup_   = new KAction( KIcon("list-remove"),      i18n("&Remove From Group"), this );

  popupCopyFriendlyName_    = new KAction(                     i18n("&Friendly Name"),    this );
  popupCopyPersonalMessage_ = new KAction(                     i18n("&Personal Message"), this );
  popupCopyHandle_          = new KAction(                     i18n("&Email Address"),    this );
  popupCopyMusic_           = new KAction(                     i18n("Song &Name"),        this );

  // Connect the actions
  connect( chatWithContact_,   SIGNAL(activated()),   this,  SLOT(slotForwardStartChat())      );
  connect( emailContact_,      SIGNAL(activated()),   this,  SLOT(slotEmailContact())          );
  connect( msnProfile_,        SIGNAL(activated()),   this,  SLOT(slotShowContactProfile())    );
  connect( chatHistory_,       SIGNAL(activated()),   this,  SLOT(slotShowChatHistory())       );
  connect( contactProperties_, SIGNAL(activated()),   this,  SLOT(slotShowContactProperties()) );

  connect( addContact_,        SIGNAL(activated()),   this,  SLOT(slotForwardAddContact())     );
  connect( allowContact_,      SIGNAL(activated()),   this,  SLOT(slotForwardAllowContact())   );
  connect( blockContact_,      SIGNAL(activated()),   this,  SLOT(slotForwardBlockContact())   );
  connect( unblockContact_,    SIGNAL(activated()),   this,  SLOT(slotForwardUnblockContact()) );
  connect( removeContact_,     SIGNAL(activated()),   this,  SLOT(slotForwardRemoveContact())  );
  connect( removeFromGroup_,   SIGNAL(activated()),   this,  SLOT(slotForwardRemoveFromGroup()));

  connect( popupCopyFriendlyName_,    SIGNAL(triggered(bool)),   this,  SLOT( copyText()     ) );
  connect( popupCopyPersonalMessage_, SIGNAL(triggered(bool)),   this,  SLOT( copyText()     ) );
  connect( popupCopyHandle_,          SIGNAL(triggered(bool)),   this,  SLOT( copyText()     ) );
  connect( popupCopyMusic_,           SIGNAL(triggered(bool)),   this,  SLOT( copyText()     ) );

  // Create the signal mappers needed to assign all actions to the move/copy contact slots
  contactCopyMapper_ = new QSignalMapper( this );
  contactMoveMapper_ = new QSignalMapper( this );
  connect( contactCopyMapper_, SIGNAL(mapped(const QString&)),   this,  SLOT(slotForwardCopyContact(const QString&)) );
  connect( contactMoveMapper_, SIGNAL(mapped(const QString&)),   this,  SLOT(slotForwardMoveContact(const QString&)) );

  // Initialize the sub menus to copy/move contacts
  copyContactToGroup_ = new KActionMenu( i18n("&Copy to Group"), this );
  moveContactToGroup_ = new KActionMenu( i18n("&Move to Group"), this );

  // Fill them with the current groups names
  rebuildContactActions();

  // Initialize Copy sub popups
  popupCopyMenu_ = new KActionMenu( i18n("&Copy"), this );
  popupCopyMenu_ ->addAction( popupCopyFriendlyName_ );
  popupCopyMenu_ ->addAction( popupCopyPersonalMessage_ );
  popupCopyMenu_ ->addAction( popupCopyHandle_ );
  popupCopyMenu_ ->addAction( popupCopyMusic_ );
  popupCopyMenu_->addSeparator();

  // Initialize the popup menu
  // If you re-organize the menu, also consider updating ContactFrame.
  contactActionPopup_ = new KMenu( "KMess", this );

  contactActionPopup_->addAction( chatWithContact_ );
  contactActionPopup_->addAction( emailContact_ );
  contactActionPopup_->addAction( msnProfile_ );
  contactActionPopup_->addAction( chatHistory_ );
  contactActionPopup_->addAction( popupCopyMenu_ );

  contactActionPopup_->addSeparator();

  contactActionPopup_->addAction( moveContactToGroup_ );
  contactActionPopup_->addAction( copyContactToGroup_ );
  contactActionPopup_->addAction( removeFromGroup_ );

  contactActionPopup_->addSeparator();

  contactActionPopup_->addAction( addContact_ );
  contactActionPopup_->addAction( allowContact_ );
  contactActionPopup_->addAction( removeContact_ );

  contactActionPopup_->addAction( blockContact_ );
  contactActionPopup_->addAction( unblockContact_ );

  contactActionPopup_->addSeparator();

  contactActionPopup_->addAction( contactProperties_ );

  return true;
}



// Initialize the group popup
bool KMessView::initGroupPopup()
{
  // Initialize context menu actions
  moveGroupDown_ = new KAction( KIcon("arrow-down"),  i18n("Move Group &Down"), this );
  moveGroupUp_   = new KAction( KIcon("arrow-up"),    i18n("Move Group &Up"),   this );
  removeGroup_   = new KAction( KIcon("edit-delete"), i18n("Re&move Group"),    this );
  renameGroup_   = new KAction( KIcon("edit-rename"), i18n("Re&name Group"),    this );

  // Connect the actions
  connect( moveGroupDown_, SIGNAL(activated()),   this,  SLOT(     slotMoveGroupDown()) );
  connect( moveGroupUp_,   SIGNAL(activated()),   this,  SLOT(       slotMoveGroupUp()) );
  connect( removeGroup_,   SIGNAL(activated()),   this,  SLOT(slotForwardRemoveGroup()) );
  connect( renameGroup_,   SIGNAL(activated()),   this,  SLOT(slotForwardRenameGroup()) );

  // Initialize the popup menu
  groupActionPopup_ = new KMenu( "KMess", this );

  groupActionPopup_->addAction( moveGroupUp_ );
  groupActionPopup_->addAction( moveGroupDown_ );
  groupActionPopup_->addSeparator();

  groupActionPopup_->addAction( renameGroup_ );
  groupActionPopup_->addAction( removeGroup_ );

  return true;
}



// Generate the lists of actions for the "copy/move contact to group" menus
void KMessView::rebuildContactActions()
{
  // Remove all copy and move actions
  foreach( KAction *action, groupCopyActionsList_ )
  {
    copyContactToGroup_->removeAction( action );
    groupCopyActionsList_.removeAll( action );
    delete action;
  }
  foreach( KAction *action, groupMoveActionsList_ )
  {
    moveContactToGroup_->removeAction( action );
    groupMoveActionsList_.removeAll( action );
    delete action;
  }

  // Create the list of groups between which contacts can be moved & copied
  QList<Group*>groupsList( currentAccount_->getContactList()->getGroupList() );
  foreach( Group *group, groupsList )
  {
    // Disallow copying and moving to special groups
    if( group->isSpecialGroup() )
    {
      continue;
    }

    // Since we need both a name to display and a group ID to distinguish groups,
    // but KActions only support the name, we'll use the Qt objectName to store
    // their internal id
    const QString& id( group->getId() );
    KAction *copyAction = new KAction( group->getName(), copyContactToGroup_ );
    KAction *moveAction = new KAction( group->getName(), moveContactToGroup_ );
    copyAction->setObjectName( id );
    moveAction->setObjectName( id );

    copyContactToGroup_->addAction( copyAction );
    moveContactToGroup_->addAction( moveAction );

    connect( copyAction, SIGNAL(triggered(bool)), contactCopyMapper_, SLOT(map()) );
    connect( moveAction, SIGNAL(triggered(bool)), contactMoveMapper_, SLOT(map()) );
    contactCopyMapper_->setMapping( copyAction, id );
    contactMoveMapper_->setMapping( moveAction, id );

    groupCopyActionsList_.append( copyAction );
    groupMoveActionsList_.append( moveAction );
  }
}



// The email label was clicked so open the user's preferred email url
void KMessView::slotEmailLabelClicked()
{
  currentAccount_->openMailAtInbox();
}



// A group was expanded or collapsed
void KMessView::slotGroupChanged( const QModelIndex &index )
{
  Q_ASSERT( viewModel_ != 0 );

  // When searching, the expanded status of the groups is altered, but the settings
  // should not be applied to the actual groups
  if( ! viewModel_->filterRegExp().isEmpty() )
  {
    return;
  }

  // Do nothing if the group is empty
  if( index.child( 0, 0 ) == QModelIndex() )
  {
    return;
  }

#ifdef KMESSDEBUG_CONTACTLISTMODEL
  kDebug() << "Changed index:" << index;
  kDebug() << "Current index at that row:" << viewModel_->index( index.row(), 0, QModelIndex() );
#endif

  // Skip invalid items
  if( ! index.isValid()
  ||  ! viewModel_->hasIndex( index.row(), 0, QModelIndex() )
  ||  ! index.data().canConvert( QVariant::Map ) )
  {
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
    kDebug() << "Skipping an invalid index:" << index;
#endif
    return;
  }

  // Get the data from the selected index
  const ModelDataList& itemData( index.data().toMap() );

  // Do nothing if the input index was not valid
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemGroup )
  {
    return;
  }

  Group *group = currentAccount_->getContactList()->getGroupById( itemData[ "id" ].toString() );

  // Stop if the group had not been found
  if( ! group )
  {
    return;
  }

  group->setExpanded( contactListView_->isExpanded( index ) );

  // Save the changed setting
  currentAccount_->getContactList()->saveProperties( group );
}



// Show the context menu
void KMessView::showContextMenu( const QPoint &point )
{
  const ContactList *contactList = currentAccount_->getContactList();

  // Get the data from the selected index
  const ModelDataList& itemData( getItemData( contactListView_->indexAt( point ) ) );

  // The index was empty
  if( itemData.isEmpty() )
  {
    return;
  }

  // Depending on the item type, show the appropriate menu
  if( itemData[ "type" ] == ContactListModelItem::ItemContact )
  {
    const QString& handle( itemData[ "handle" ].toString() );
    const Contact *contact = contactList->getContactByHandle( handle );

    bool isBlocked = contact->isBlocked();
    bool isFriend  = contact->isFriend();
    const QStringList& contactGroups( contact->getGroupIds() );

    // Only if not added yet
    addContact_           ->setVisible( ! isFriend );

    // Only allow if contact is on the "removed" group.
    allowContact_         ->setVisible( ! isFriend && ! contact->isAllowed() && ! isBlocked );

    // Only one of these is enabled:
    blockContact_         ->setVisible( ! isBlocked );
    unblockContact_       ->setVisible(   isBlocked );

    removeContact_        ->setVisible(   isFriend  );

    // Only show the group management options when it makes sense to; contacts
    // in the Allowed and Removed groups are technically not part of your friends list
    if( itemData[ "group" ] == SpecialGroups::ALLOWED
    ||  itemData[ "group" ] == SpecialGroups::REMOVED )
    {
      copyContactToGroup_->setVisible( false );
      moveContactToGroup_->setVisible( false );
      removeFromGroup_   ->setVisible( false );
    }
    else if(    currentAccount_->getContactListDisplayMode() == CurrentAccount::VIEW_BYSTATUS
                || ( currentAccount_->getContactListDisplayMode() == CurrentAccount::VIEW_MIXED
                     && ! contact->isOnline() )
           )
    {
      copyContactToGroup_->setVisible( true );
      moveContactToGroup_->setVisible( false );
      removeFromGroup_   ->setVisible( false);
    }
    else if( contactGroups.isEmpty() )
    {
      moveContactToGroup_->setVisible( true );
      copyContactToGroup_->setVisible( false );
      removeFromGroup_   ->setVisible( false );
    }
    else
    {
      moveContactToGroup_->setVisible( isFriend );
      copyContactToGroup_->setVisible( isFriend );
      removeFromGroup_   ->setVisible( isFriend );
    }

    // Show all copy and move actions
    foreach( KAction *action, groupCopyActionsList_ )
      action->setVisible( true );
    foreach( KAction *action, groupMoveActionsList_ )
      action->setVisible( true );

    // Then hide only the invalid choices
    int enabledCopyActions = groupCopyActionsList_.count();
    int enabledMoveActions = groupMoveActionsList_.count();
    foreach( const QString &groupId, contactGroups )
    {
      foreach( KAction *action, groupCopyActionsList_ )
      {
        // See initContactPopup() for the choice of using objectName to store the group ID
        if( action->objectName() == groupId )
        {
          enabledCopyActions--;
          action->setVisible( false );
          break;
        }
      }
      foreach( KAction *action, groupMoveActionsList_ )
      {
        // See initContactPopup() for the choice of using objectName to store the group ID
        if( action->objectName() == groupId )
        {
          enabledMoveActions--;
          action->setVisible( false );
          break;
        }
      }
    }

    // If no actions have been enabled in a menu, disable it too
    if( enabledCopyActions <= 0 )
    {
      copyContactToGroup_ ->setVisible( false );
    }
    if( enabledMoveActions <= 0 )
    {
      moveContactToGroup_ ->setVisible( false );
    }

    // Clear the previous actions to avoid duplication of links
    qDeleteAll( groupCopyLinkActionsList_ );
    groupCopyLinkActionsList_.clear();

     // If detailed contact information is available, search for links
    if( contact != 0 )
    {
      // Append friendly name and personal message so cycle only one time to search links
      const QString& nameAndPm( contact->getFriendlyName() + ' ' + contact->getPersonalMessage() );
      QStringList links;

      //Initialize index, link RegExp and found boolean for insert separator only at first found link
      int pos = 0;
      QRegExp linkRegExp( "(http://|https://|ftp://|sftp://|www\\..)\\S+" );

      while( ( pos = linkRegExp.indexIn( nameAndPm, pos ) ) != -1 )
      {
        // Grep the found link and update the position for the next cycle
        QString foundLink( linkRegExp.cap( 0 ) );
        pos += linkRegExp.matchedLength();

        // Skip duplicated links
        if( links.contains( foundLink ) )
        {
          continue;
        }
        links.append( foundLink );

        // Put the found link into KAction, replace & with && to avoid shortcut problem and use tooltip
        // for future grep of link ( to avoid shortcut problem too )
        KAction *popupCopyLink = new KAction( foundLink.replace( '&', "&&" ), this );
        popupCopyLink->setToolTip( foundLink );
        connect( popupCopyLink, SIGNAL( triggered( bool ) ), this, SLOT( copyText() ) );

        // Append current KAction to the copy link list
        groupCopyLinkActionsList_.append( popupCopyLink );

        // Add action to copy menu
        popupCopyMenu_->addAction( popupCopyLink );
      }
    }

    // Forbid chatting with yourself
    chatWithContact_->setEnabled( handle != currentAccount_->getHandle() );

    // Set tooltip ( informations ) of other copy actions
    popupCopyFriendlyName_   ->setToolTip( contact->getFriendlyName() );
    popupCopyPersonalMessage_->setToolTip( contact->getPersonalMessage() );
    popupCopyHandle_         ->setToolTip( contact->getHandle() );
    popupCopyMusic_          ->setToolTip( contact->getCurrentMediaString() );
    // Hide the unavailable options
    popupCopyPersonalMessage_->setVisible( ! contact->getPersonalMessage   ().isEmpty() );
    popupCopyMusic_          ->setVisible( ! contact->getCurrentMediaString().isEmpty() );

    // Show the popup
    contactActionPopup_->popup( QCursor::pos() );
  }
  else if( itemData[ "type" ] == ContactListModelItem::ItemGroup )
  {
    const Group *group = contactList->getGroupById( itemData[ "id" ].toString() );

    // No context menu for special groups
    if( ! group || group->isSpecialGroup() )
    {
      return;
    }

    int pos = group->getSortPosition();

    moveGroupUp_   ->setEnabled( contactList->getGroupBySortPosition( pos - 1, false ) != 0 );
    moveGroupDown_ ->setEnabled( contactList->getGroupBySortPosition( pos + 1, true  ) != 0 );

    // Show the popup
    groupActionPopup_->popup( QCursor::pos() );
  }
  else
  {
    kWarning() << "Invalid type of list item" << itemData[ "type" ];
  }
}



// Copy some details of the contact to the clipboard.
void KMessView::copyText()
{
  // Grep the action sender for copy the tooltip that contains the information
  KAction *action = static_cast<KAction*>( const_cast<QObject*>( sender() ) );
  kapp->clipboard()->setText( action->toolTip() );
}



// Show the contact & group tool tip
void KMessView::showToolTip( const QPoint &point )
{
  const QModelIndex& index( contactListView_->indexAt( contactListView_->mapFromGlobal( point ) ) );
  QString tipText;

  // Read the actual data off the model index
  const ModelDataList& itemData( index.data().toMap() );

  // Do nothing if the index is not valid
  if( ! index.isValid() || itemData.isEmpty() )
  {
    return;
  }

  const QString textAlignment( QApplication::isRightToLeft() ? "right" : "left" );
  const QString textDirection( "dir='" + QString(QApplication::isRightToLeft() ? "rtl" : "ltr") + "'" );

  // Paint the different types of list items
  // see ContactListModelItem::data() for a list of itemData elements (role == DataRole)
  switch( itemData[ "type" ].toInt() )
  {
    case ContactListModelItem::ItemContact:
    {
      tipText = "<html " + textDirection + "><table border='0' " + textDirection + ">"
                  "<tr>"
                    "<td valign='top'>"
                      "<table style='border-width:1px;border-color:palette(mid);border-style:solid;'>"
                        "<tr><td><img src='%1' width='96' height='96'/></td></tr>"
                      "</table>"
                    "</td>"
                    "<td align='" + textAlignment + "' valign='top' style='padding-" + textAlignment + ":10px;'>"
                      "<b>%2</b> (%3)<br/>"
                      "%4 %5 %6"
                    "</td>"
                  "</tr>"
                  "<tr>"
                    "<td colspan='2'><hr width='98%'/></td>"
                  "</tr>"
                  "</table>"
                  "<table border='0' " + textDirection + " align='" + textAlignment + "'>"
                    "%7%8%9%10%11"
                  "</table>"
                "</html>";

      // Add the display picture (if there is any), or the default KMess logo
      tipText = tipText.arg( itemData[ "displayPicture" ].toString() );

      // Add the contact's real name - the alias is not shown:
      // you can read it in the contact list, after all
      QString friendlyName( itemData[ "trueFriendly" ].toString() );
      // Escape HTML, replace standard emoticons, but no links and formatting
      RichTextParser::parseMsnString( friendlyName, true, true, false, false );
      tipText = tipText.arg( friendlyName );

      // Add status
      tipText = tipText.arg( MsnStatus::getName( (Status) itemData[ "status" ].toInt() ) );

      // Add the personal message
      QString personalMessage( itemData[ "personalMessage" ].toString() );
      if( ! personalMessage.isEmpty() )
      {
        // Escape HTML, replace standard emoticons, but no links and formatting
        RichTextParser::parseMsnString( personalMessage, true, true, false, false );
        tipText = tipText.arg( "<i>" + personalMessage + "</i><br/>" );
      }
      else
      {
        tipText = tipText.arg( "" );
      }

      // If the contact is listening to music, show it
      const QString& mediaType  ( itemData[ "mediaType"   ].toString() );
      const QString& mediaString( itemData[ "mediaString" ].toString() );
      if( ! mediaString.isEmpty() )
      {
        QString mediaEmoticon;

        // Determine icon for special media types
        if( mediaType == "Music" )
        {
          mediaEmoticon = EmoticonManager::instance()->getReplacement( "(8)", true );
        }
        else if( mediaType == "Gaming" )
        {
          mediaEmoticon = EmoticonManager::instance()->getReplacement( "(xx)", true );
        }

        tipText = tipText.arg( "<i><small>" +
                               mediaEmoticon + Qt::escape( mediaString ) +
                               "</small></i><br/>" );
      }
      else
      {
        tipText = tipText.arg( "" );
      }

      // Add a message about our presence in this contact's list
      if( itemData[ "isReverse" ].toBool() )
      {
        tipText = tipText.arg( "" );
      }
      else
      {
        tipText = tipText.arg( "<i>" +
                               i18nc( "Message in list tooltip",
                                      "This contact does not have you in his or her contact list." ) +
                               "</i>" );
      }

      // Here starts the list of simple property:value lines

      // Adding a space to better separate the properties and values
      QString propertyLine;
      if( QApplication::isLeftToRight() )
      {
        propertyLine = "<tr><td align='right'><i>%1:</i>&nbsp;</td><td align='left'>%2</td></tr>";
      }
      else
      {
        propertyLine = "<tr><td align='right'>%2</td><td align='left'>&nbsp;<i>%1:</i></td></tr>";
      }

      // Add handle
      tipText = tipText.arg( propertyLine.arg( i18nc( "Contact email label in list tooltip", "Email address" ) )
                                         .arg( itemData[ "handle" ].toString() ) );

      // Add client identifier
      const QString& clientName( itemData[ "clientName" ].toString() );
      if( ! clientName.isEmpty() )
      {
        tipText = tipText.arg( propertyLine.arg( i18nc( "Contact Live Messenger client label in list tooltip", "Client" ) )
                                           .arg( Qt::escape( clientName ) ) );
      }
      else
      {
        tipText = tipText.arg( "" );
      }

      //add info whether the contact is blocked
      QString blockedString;
      if( itemData[ "isBlocked" ].toBool() )
      {
        blockedString = i18n( "Yes" );
      }
      else
      {
        blockedString = i18n( "No" );
      }

      tipText = tipText.arg( propertyLine.arg( i18nc( "Contact blocked status label in list tooltip", "Blocked" ) )
                                         .arg( blockedString ) );

      // Add last seen date
      const QDateTime& lastSeen( itemData[ "lastSeen" ].toDateTime() );
      QString lastSeenString;

      if( ( (Status) itemData[ "status" ].toInt() ) != STATUS_OFFLINE )
      {
        lastSeenString = i18n( "Connected" );
      }
      else if( lastSeen.toTime_t() == 0
                || lastSeen.toTime_t() == UINT_MAX
                || lastSeen.isNull() )
      {
        lastSeenString = i18n( "Not seen yet" );
      }
      else
      {
        lastSeenString = lastSeen.toString();
      }

      tipText = tipText.arg( propertyLine.arg( i18nc( "Contact last presence label in list tooltip", "Last seen" ) )
                                         .arg( lastSeenString ) );

      // Add last message date
      const QDateTime& lastMessage( itemData[ "lastMessage" ].toDateTime() );
      QString lastMessageString;
      if( lastMessage.toTime_t() == 0
          || lastMessage.toTime_t() == UINT_MAX
          || lastMessage.isNull() )
      {
        lastMessageString = i18n( "No messages yet" );
      }
      else
      {
        lastMessageString = lastMessage.toString();
      }

      tipText = tipText.arg( propertyLine.arg( i18nc( "Contact last message label in list tooltip", "Last message" ) )
                                         .arg( lastMessageString ) );
    }
    break;


    case ContactListModelItem::ItemGroup:
      tipText = "<qt><div " + textDirection + ">%1<br/><i>(%2)</i></div></qt>";

      // Add the group name
      tipText = tipText.arg( i18nc( "Group name in group tooltip", "Group <b>%1</b>", itemData[ "name" ].toString() ) );

      // Non-empty and non-special groups also have an Online contacts count.
      // (Individuals is treated as a non-special group)
      if( ( ! itemData[ "isSpecialGroup" ].toBool() || itemData[ "id" ].toString() == SpecialGroups::INDIVIDUALS )
      &&      itemData[ "totalContacts"  ].toInt() > 0 )
      {
        tipText = tipText.arg( "%1%2" ).arg( i18ncp( "Contact counters in normal group tooltip, first part",
                                             "%1 contact, ", "%1 contacts, ",
                                             itemData[ "totalContacts"  ].toInt() ) )
                                       .arg( i18ncp( "Contact counters in normal group tooltip, second part",
                                             "%1 online", "%1 online",
                                             itemData[ "onlineContacts" ].toInt() ) );
      }
      else
      {
        tipText = tipText.arg( i18ncp( "Contacts count in special group tooltip",
                                       "%1 contact", "%1 contacts", itemData[ "totalContacts" ].toInt() ) );
      }
      break;


    default:
      // Nothing to do!
      return;
  }

  // Show the tooltip
  QToolTip::showText( point, tipText, contactListView_, contactListView_->visualRect( index ) );
}



// Email the current contact
void KMessView::slotEmailContact()
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was not valid, do nothing
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
  {
    return;
  }

  // Send mail to that account
  currentAccount_->openMailAtCompose( itemData[ "handle" ].toString() );
}


// An item was double clicked
void KMessView::slotItemDoubleClicked( const QModelIndex &index )
{
  // Detect accidental clicks
  if( KGlobalSettings::singleClick() )
  {
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
    kDebug() << "Accidental double click detected, skipping click event.";
#endif
    return;
  }

  slotItemClicked( index );
}



void KMessView::slotItemSingleClicked( const QModelIndex &index )
{
  bool accidental = false;
  if( ! KGlobalSettings::singleClick() )
  {
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
    kDebug() << "Accidental single click detected, skipping click event.";
#endif

    accidental = true;
  }

  slotItemClicked( index, accidental );
}



// An item was clicked
void KMessView::slotItemClicked( const QModelIndex &index, const bool accidental )
{
  // Get the data from the selected index
  const ModelDataList& itemData( getItemData( index ) );

  // The index was empty
  if( itemData.isEmpty() )
  {
    kWarning() << "Invalid list index given!";
    return;
  }

  switch( itemData[ "type" ].toInt() )
  {
    // Expand/collapse the groups when clicked
    case ContactListModelItem::ItemGroup:

      // Do nothing if the group is empty
      if( index.child( 0, 0 ) == QModelIndex() )
      {
        return;
      }

      contactListView_->setExpanded( index, ! itemData[ "isExpanded" ].toBool() );
      break;


    //  Request a chat with that contact
    case ContactListModelItem::ItemContact:
      if( ! accidental )
      {
        // Tell the Chat Master we'd like to start a chat.
        emit requestChat( itemData[ "handle" ].toString() );
      }
      break;


    default:
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
      kWarning() << "Nothing to do!";
#endif
      break;
  }
}



// Move down the selected group
void KMessView::slotMoveGroupDown()
{
  const ContactList *contactList = currentAccount_->getContactList();

  // Get the group data from the selected index
  const ModelDataList& itemData( getItemData() );
  Group *group = contactList->getGroupById( itemData[ "id" ].toString() );
  if( ! group || itemData.isEmpty() )
  {
    return;
  }

  // Obtain the next listed group
  Group *nextGroup = contactList->getGroupBySortPosition( group->getSortPosition() + 1, true );
  if( ! nextGroup )
  {
    return;
  }

#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
  kDebug() << "Selected group:" << group->getId() << "to move down in place of group:" << nextGroup->getId();
#endif

  // Get the group sorting position for both groups
  int oldGroupPos = group    ->getSortPosition();
  int newGroupPos = nextGroup->getSortPosition();

  // Swap them
  group    ->setSortPosition( newGroupPos );
  nextGroup->setSortPosition( oldGroupPos );

  // Reset old sort value so KMess is not confused (XXX HACK FIXME OMG)
  group    ->setOldSortPosition( 0 );
  nextGroup->setOldSortPosition( 0 );
}



// Move up the selected group
void KMessView::slotMoveGroupUp()
{
  const ContactList *contactList = currentAccount_->getContactList();

  // Get the group data from the selected index
  const ModelDataList& itemData( getItemData() );
  Group *group = contactList->getGroupById( itemData[ "id" ].toString() );
  if( ! group || itemData.isEmpty() )
  {
    return;
  }

  // Obtain the previous listed group
  Group *prevGroup = contactList->getGroupBySortPosition( group->getSortPosition() - 1, false );
  if( ! prevGroup )
  {
    return;
  }

#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
  kDebug() << "Selected group:" << group->getId() << "to move up in place of group:" << prevGroup->getId();
#endif

  // Get the group sorting position for both groups
  int oldGroupPos = group    ->getSortPosition();
  int newGroupPos = prevGroup->getSortPosition();

  // Swap them
  group    ->setSortPosition( newGroupPos );
  prevGroup->setSortPosition( oldGroupPos );

  // Save the changed setting
  contactList->saveProperties( group     );
  contactList->saveProperties( prevGroup );

  // Reset old sort value so KMess is not confused (XXX HACK FIXME OMG)
  group    ->setOldSortPosition( 0 );
  prevGroup->setOldSortPosition( 0 );
}



// Forward the "add contact" menu action
void KMessView::slotForwardAddContact()
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was not empty
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
  {
    return;
  }

  // Add that contact
  emit addContact( itemData[ "handle" ].toString() );
}



// Forward the "allow contact" menu action
void KMessView::slotForwardAllowContact()
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was not empty
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
  {
    return;
  }

  // Allow that contact
  emit allowContact( itemData[ "handle" ].toString() );
}



// Forward the "block contact" menu action
void KMessView::slotForwardBlockContact()
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was not empty
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
  {
    return;
  }

  // Block that contact
  emit blockContact( itemData[ "handle" ].toString() );
}



// Called when a contact should be copied
void KMessView::slotForwardCopyContact( const QString &newGroupId )
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was not empty
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
  {
    return;
  }

  // Copy that contact
  emit copyContact( itemData[ "handle" ].toString(), newGroupId );
}



// Called when a contact should be moved
void KMessView::slotForwardMoveContact( const QString &newGroupId )
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The selection was not valid
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
  {
    return;
  }

  // Get the old group ID, and if it's not valid, set it to zero
  QString oldGroupId;
  if( itemData[ "isInSpecialGroup" ].toBool() == false )
  {
    oldGroupId = itemData[ "group" ].toString();
  }
  else
  {
    oldGroupId = "0";
  }

  // If the old and new groups are the same one, do nothing
  if( oldGroupId == newGroupId )
  {
    return;
  }

  // Move that contact
  emit moveContact( itemData[ "handle" ].toString(), oldGroupId, newGroupId );
}



// Forward the "remove contact" menu action
void KMessView::slotForwardRemoveContact()
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was empty
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
  {
    return;
  }

  // Remove that contact
  emit removeContact( itemData[ "handle" ].toString() );
}


void KMessView::slotForwardRemoveFromGroup()
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was invalid
  if( itemData.isEmpty() )
  {
    return;
  }

  // Get the ID of the group which contained this instance of the contact
  const ModelDataList& parentItemData( getItemData( selectionModel_->selectedIndexes().first().parent() ) );
  if( parentItemData.isEmpty() || parentItemData[ "type" ] != ContactListModelItem::ItemGroup )
  {
    return;
  }

  // Remove that contact
  emit removeContactFromGroup( itemData[ "handle" ].toString(), parentItemData[ "id" ].toString() );
}


// Forward the "remove group" menu action
void KMessView::slotForwardRemoveGroup()
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was invalid
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemGroup )
  {
    return;
  }

  // Remove that group
  emit removeGroup( itemData[ "id" ].toString() );
}




// Forward the "rename group" menu action
void KMessView::slotForwardRenameGroup()
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was invalid
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemGroup )
  {
    return;
  }

  // Rename that group
  emit renameGroup( itemData[ "id" ].toString() );
}



// Forward the "start chat" menu action
void KMessView::slotForwardStartChat()
{
  // Only one index can be returned at once by the contact list (has single selection mode)
  slotItemClicked( selectionModel_->selectedIndexes().first() );
}



// Forward the "unblock contact" menu action
void KMessView::slotForwardUnblockContact()
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was not empty
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
  {
    return;
  }

  // Unblock that contact
  emit unblockContact( itemData[ "handle" ].toString() );
}



/**
 * @brief Update the friendly name from the inline editing label.
 *
 * Sends to the server the new friendly name selected by clicking
 * the friendly name inline editing label.
 *
 * @param refreshOnly  When true, the FN widget is only updated with the
 *                     current account's name. When false, it is updated
 *                     with the default message, and nothing is sent to
 *                     the server.
 */
void KMessView::updateFriendlyName( bool refreshOnly )
{
  const QString& defaultTooltip( i18nc( "Default friendly name tooltip", "Click here to change your friendly name" ) );
  const QString& defaultName( currentAccount_->getFriendlyName( STRING_ORIGINAL ).trimmed() );

  // When refreshing the message or when the focus losing isn't voluntary, don't update it on the server
  if( refreshOnly || ! isActiveWindow() || ! topLevelWidget()->isActiveWindow() )
  {
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
    kDebug() << "Refreshing name only: initial update or non-intentional focus lose.";
#endif

    // Refresh the message
    friendlyNameInput_->setToolTip( defaultTooltip );
    friendlyNameInput_->setText( defaultName );
    return;
  }

  // Get message
  QString newName( friendlyNameInput_->text().trimmed() );

#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
  kDebug() << "Updating friendly name to:" << newName;
#endif

  // Set a default name (the email address) if the field is left empty
  if( newName.isEmpty() )
  {
    newName = currentAccount_->getHandle();
  }

  // Only update the name if it's been changed
  if( defaultName == newName )
  {
    return;
  }

  // Update the name and save the changes
  currentAccount_->setFriendlyName( newName );
  friendlyNameInput_->setToolTip( newName );
  // Commit changes to disk, now
  currentAccount_->saveProperties();

  // Notify changes to the MSN server
  emit changeFriendlyName( newName );
}



/**
 * @brief Update the personal message from the inline editing label.
 *
 * Sends to the server the new personal message selected by clicking
 * the personal message inline editing label.
 *
 * @param refreshOnly  When true, the PM widget is only updated with the
 *                     current account's message. When false, it is updated
 *                     with the default message, and nothing is sent to
 *                     the server.
 */
void KMessView::updatePersonalMessage( bool refreshOnly )
{
  const QString& defaultMessage( i18nc( "Default personal message shown in the contact list", "[i]Click to set a personal message[/i]" ) );
  const QString& defaultTooltip( i18nc( "Default personal message tooltip", "Click here to insert a message to show to your contacts: they will see it along with your friendly name" ) );

  // When refreshing the message or when the focus losing isn't voluntary, don't update it on the server
  if( refreshOnly || ! isActiveWindow() || ! topLevelWidget()->isActiveWindow() )
  {
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
    kDebug() << "Refreshing message only: initial update or non-intentional focus lose.";
#endif

    // Refresh the message
    const QString &message( currentAccount_->getPersonalMessage( STRING_ORIGINAL ).trimmed() );
    if( message.isEmpty() )
    {
      personalMessageInput_->setToolTip( defaultTooltip );
      personalMessageInput_->setText( defaultMessage, true );
    }
    else
    {
      personalMessageInput_->setToolTip( message );
      personalMessageInput_->setText( message );
    }

    return;
  }

  // Get message
  const QString& message( personalMessageInput_->text().trimmed() );

#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
  kDebug() << "Updating personal message to:" << message;
#endif

  // Protect against weird situations where the label is seen as text.
  // The lost focus event is also sent when the while window looses focus
  if( message == defaultMessage )
  {
    kWarning() << "Personal message label was about to be used as personal message!";
    return;
  }

  // Only update the message if it's been changed
  if( currentAccount_->getPersonalMessage( STRING_ORIGINAL ) != message )
  {
    // Update message and save the changes
    currentAccount_->setPersonalMessage( message );
    personalMessageInput_->setToolTip( message );
    // Commit changes to disk, now
    currentAccount_->saveProperties();

    // Notify changes to the MSN server
    emit changePersonalMessage( message );
  }

  // Restore the placeholder.
  if( message.isEmpty() )
  {
    personalMessageInput_->setToolTip( defaultTooltip );
    personalMessageInput_->setText( defaultMessage, true );
  }
}



// Delay a bit updating the groups status
void KMessView::slotScheduleGroupUpdate()
{
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
  kDebug() << "Scheduling another groups update";
#endif
  updateTimer_.start( 250 );
}



// Slot for searching contact
void KMessView::slotSearchContact( const QString& searchFor )
{
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
  kDebug() << "Search expression changed to:" << searchFor;
#endif

  QString searchExpression;
  const QString& regexpMagic( "regexp:" );

  // If the search string starts with "regexp:", allow searching with a regexp :)
  if( searchFor.startsWith( regexpMagic ) )
  {
    searchExpression = searchFor.mid( regexpMagic.length() );

    // Colorize the search bar when using regexps, according to if they're valid or invalid
    if( QRegExp( searchExpression ).isValid() )
    {
      searchEdit_->setStyleSheet( "background-color: #bbffbb" );
    }
    else
    {
      searchEdit_->setStyleSheet( "background-color: #ffbbbb" );
    }
  }
  else
  {
    // Remove trailing spaces and other unneeded (invisible) characters, then escape the searchstring
    searchExpression = QRegExp::escape( searchFor.simplified() );
    searchEdit_->setStyleSheet( "" );
  }

  viewModel_->setFilterRegExp( searchExpression );

  // Show or hide groups which do or do not contain results

  for( int row = viewModel_->rowCount() - 1; row >= 0; --row )
  {
    const QModelIndex& index( viewModel_->index( row, 0, QModelIndex() ) );

    // Skip invalid items
    if( ! index.isValid() )
    {
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
      kDebug() << "Skipping invalid index" << index;
#endif
      continue;
    }

    // Access the index's data
    const ModelDataList& itemData( getItemData( index ) );

    // Skip items which have no children
    if( itemData.isEmpty() || itemData[ "type" ].toInt() != ContactListModelItem::ItemGroup )
    {
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
      kDebug() << "Skipping non-group index" << index;
      kDebug() << "-- Index data:" << itemData;
#endif
      continue;
    }

    // Hide groups which don't contain results
    if( ! searchFor.isEmpty() )
    {
      bool isHidden = ( ! viewModel_->hasChildren( index ) );

      contactListView_->setRowHidden( row, QModelIndex(), isHidden );
      contactListView_->setExpanded( index, true );
    }
    // When the search terms are removed, enable back all groups
    else
    {
      contactListView_->setRowHidden( row, QModelIndex(), false );
      contactListView_->setExpanded( index, itemData[ "isExpanded" ].toBool() );
    }
  }
}



// Display the chat history of the current contact
void KMessView::slotShowChatHistory()
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was not empty
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
  {
    return;
  }

  ChatHistoryDialog *dialog = new ChatHistoryDialog( this );

  // Show the selected contact. If no log exists
  // (no chats with that person yet), then don't show the History dialog
  if( dialog->setContact( itemData[ "handle" ].toString() ) )
  {
    dialog->show();
  }
  else
  {
    if( currentAccount_->getSaveChats() )
    {
      KMessageBox::sorry(	this,
                          i18n( "No chat logs could be found for this contact." ),
                          i18n( "No chat history found" ) );
    }
    else
    {
      KMessageBox::sorry( this,
                          i18n( "No chat logs could be found for this contact. Note that new chats are not logged; if you want your chats to be logged, you can enable it in your account settings." ),
                          i18n( "No chat history found") );
    }

    delete dialog;
  }
}



// Display the profile of the current contact
void KMessView::slotShowContactProfile()
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was not empty
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
  {
    return;
  }

  const QString& handle ( itemData[ "handle" ].toString() );
  KMessShared::openBrowser( KUrl( "http://members.msn.com/default.msnw?mem=" + handle + "&mkt=" + KGlobal::locale()->language() + "&pgmarket" ) );
}



// Display the properties of the current contact
void KMessView::slotShowContactProperties()
{
  // Get the data from the currently selected index
  const ModelDataList& itemData( getItemData() );

  // The index was not empty
  if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
  {
    return;
  }

  // Get the contact data to pass to the properties dialog
  Contact *contact = currentAccount_->getContactList()->getContactByHandle( itemData[ "handle" ].toString() );

  // If no corresponding contact has been found, do nothing
  if( ! contact )
  {
    return;
  }

  ContactPropertiesDialog *dialog = new ContactPropertiesDialog( this );
  connect( dialog, SIGNAL( removeContactFromGroup(QString,QString) ),
           this,   SIGNAL( removeContactFromGroup(QString,QString) ) );
  connect( dialog, SIGNAL(      addContactToGroup(QString,QString) ),
           this,   SIGNAL(            copyContact(QString,QString) ) );
  dialog->launch( contact );
  delete dialog;
}



// Update the users display pic
void KMessView::slotUpdateDisplayPicture()
{
  // Adjust the picture size depending on the rest of the widgets:
  // it should be small only when both now playing and new email info are off.
  int size = 96;
  if( ! currentAccount_->getShowNowListening()
  && ( ! currentAccount_->getShowEmail() || ! currentAccount_->getEmailSupported() ) )
  {
    size = 64;
  }

  currentAccountDisplayPic_->setFixedSize( size, size );

  // The user does not want to show a display pic, so just show an icon for their sign in status
  if( ! currentAccount_->getShowPicture() )
  {
    displayPicFrame_->hide();
    return;
  }

  // Show the picture frame first
  displayPicFrame_->show();

  // The picture is unchanged, do nothing
  const QString &pictureFileName( currentAccount_->getPicturePath() );
  if( displayPicFrame_->property( "PictureFileName" ) == pictureFileName )
  {
#ifdef KMESSDEBUG_KMESSVIEW
    kDebug() << "No need to change the display picture";
#endif
    return;
  }

#ifdef KMESSDEBUG_KMESSVIEW
  kDebug() << "Changing display picture to path:" << pictureFileName;
#endif

  // Change the picture
  const QPixmap& image( pictureFileName );
  currentAccountDisplayPic_->setPixmap( image );
  displayPicFrame_->setProperty( "PictureFileName", pictureFileName );
}



// Change whether or not the email label is displayed based on account settings.
void KMessView::slotUpdateEmailDisplay()
{
  if ( currentAccount_->getShowEmail() && currentAccount_->getEmailSupported() )
  {
    emailFrame_->setHidden(false);
    //emailFrame_->setMargin(statusLayout_->margin());
    emailFrame_->setMaximumHeight(40);
  }
  else
  {
    emailFrame_->setHidden(true);
    //emailFrame_->setMargin(0);
    emailFrame_->setMaximumHeight(0);
  }
}



// Update the expanded status of the list groups
void KMessView::slotUpdateGroups()
{
  Q_ASSERT( viewModel_ != 0 );

  // When searching, the expanded status of the groups is altered, but the settings
  // should not be applied to the actual groups
  if( ! viewModel_->filterRegExp().isEmpty() )
  {
    return;
  }

#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
  kDebug() << "Updating groups expanded status for" << viewModel_->rowCount() << "rows";
#endif

  for( int row = viewModel_->rowCount() - 1; row >= 0; row-- )
  {
    const QModelIndex& index( viewModel_->index( row, 0, QModelIndex() ) );

#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
  kDebug() << "Current row:" << row << "of" << viewModel_->rowCount();
  kDebug() << "Index:" << index;
  kDebug() << "Model has it?" << viewModel_->hasIndex( row, 0, QModelIndex() );
#endif

    // Skip invalid items
    if( ! index.isValid()
    ||  ! viewModel_->hasIndex( row, 0, QModelIndex() )
    ||  ! index.data().canConvert( QVariant::Map ) )
    {
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
      kDebug() << "Skipping an invalid item at row" << row << ":" << index;
#endif
      continue;
    }

    // Access the index's data
    const ModelDataList& itemData( index.data().toMap() );

    // Skip items which have no children
    if( itemData.isEmpty() || itemData[ "type" ].toInt() != ContactListModelItem::ItemGroup )
    {
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
      kDebug() << "Skipping a non-group at row" << row << ":" << index;
      kDebug() << "Skipped item's details:" << itemData;
#endif
      continue;
    }

    // Set the expanded status
    contactListView_->setExpanded( index, itemData[ "isExpanded" ].toBool() );

#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
    kDebug() << "Group" << itemData[ "name" ].toString() << "is"
             << ( itemData[ "isExpanded" ].toBool() ? "expanded" : "contracted" );
#endif
  }

#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
  kDebug() << "Update done.";
#endif
}



// Update the email count.
void KMessView::slotUpdateNoEmails()
{
  const QString& message( i18np( "%1 new email message",
                          "%1 new email messages",
                          currentAccount_->getNoEmails() ) );

  // Update the label
  emailLabel_->setText( message );

  // Set the user's preferred email website.
  emailLabel_->setUrl( currentAccount_->getEmailUrl() );
}



// Update the user's status.
void KMessView::slotUpdateUserStatus()
{
  const Status status( currentAccount_->getStatus() );

#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
  kDebug() << "Updating user status to:" << MsnStatus::getName( status );
#endif

  // Update the widgets

  updateFriendlyName( true );
  updatePersonalMessage( true );

  statusButton_->setText( MsnStatus::getName( status ) );
  statusButton_->setIcon( MsnStatus::getIcon( status ) );
}



// Update the contact list widgets
void KMessView::slotUpdateView()
{
  QString birdStyle;

  // Hide the tree branches
  QString listStyle( "QTreeView::branch:has-siblings:adjoins-item { "
                     "  border-image: url(empty.png) 0; "
                     "} "
                     "QTreeView::branch:!has-children:!has-siblings:adjoins-item { "
                     "  border-image: url(empty.png) 0; "
                     "} " );

  // Set a background pixmap (but allow not to, by popular request)
  if( currentAccount_->getShowContactListBird() )
  {
     birdStyle = "QTreeView { "
                 "  background-attachment: fixed; "
                 "  background-origin: content; "
                 "  background-position: bottom right; "
                 "  background-repeat: no-repeat; "
                 "  background-image: url(" +
                 KIconLoader::global()->iconPath( "background", KIconLoader::User ) + ");"
                 "}"
                 ;
  }

  contactListView_->setStyleSheet( listStyle + birdStyle );
}



#include "kmessview.moc"
