/***************************************************************************
                          chatwindow.cpp  -  description
                             -------------------
    begin                : Tue Apr 23 2002
    copyright            : (C) 2002 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 "chatwindow.h"

#include "../contact/contact.h"
#include "../contact/msnobject.h"
#include "../contact/invitedcontact.h"
#include "../dialogs/chathistorydialog.h"
#include "../dialogs/invitedialog.h"
#include "../model/contactlist.h"
#include "../network/msnswitchboardconnection.h"
#include "../utils/richtextparser.h"
#include "../utils/kmessconfig.h"
#include "../utils/kmessshared.h"
#include "../currentaccount.h"
#include "../kmess.h"
#include "../kmessapplication.h"
#include "../kmessdebug.h"
#include "chat.h"
#include "config-kmess.h"
#include "chathistorymanager.h"
#include "contactswidget.h"
#include "emoticonswidget.h"
#include "winkswidget.h"

#include <IsfQt>
#include <IsfQtDrawing>

#include <QBuffer>
#include <QDir>
#include <QDockWidget>
#include <QShortcut>
#include <QTextDocumentFragment>
#include <QDesktopWidget>

#include <KAction>
#include <KActionMenu>
#include <KColorDialog>
#include <KConfigGroup>
#include <KFontDialog>
#include <KHelpMenu>
#include <KMenu>
#include <KMenuBar>
#include <KMessageBox>
#include <KStandardAction>
#include <KStatusBar>
#include <KTabBar>
#include <KToolBar>
#include <KToolInvocation>
#include <KWindowSystem>
#include <KStandardShortcut>

#ifdef KMESSDEBUG_CHATWINDOW
  #define KMESSDEBUG_CHATWINDOW_TYPING_MESSAGES
#endif

#define TYPING_NOTIFICATION_TIME       4000



// The constructor
ChatWindow::ChatWindow( QWidget *parent )
: KXmlGuiWindow( parent )
, Ui::ChatWindow()
, actionCollection_( actionCollection() )
, blinkToUpper_( true )
, currentAccount_( 0 )
, doSendTypingMessages_( true )
, indexSentences_( 0 )
, initialized_( false )
, zoomLevel_( 100 )
{
  setObjectName( "ChatWindow#" );

  // Create the interface
  QWidget *mainWidget = new QWidget( this );
  setupUi( mainWidget );
  setCentralWidget( mainWidget );

#if KMESS_ENABLE_INK == 0
  // Only enable the Ink button if Ink sending support is available
  inkButton_->hide();
  inkCanvas_->setEnabled( false );
#endif

  // Connect the UI signals
  connect( sendButton_,              SIGNAL(                  clicked() ),
           this,                     SLOT  (    slotSendButtonClicked() ) );
  connect( newLineButton_,           SIGNAL(                  clicked() ),
           this,                     SLOT  ( slotNewLineButtonClicked() ) );
  connect( messageEdit_,             SIGNAL(              textChanged() ),
           this,                     SLOT  (       slotMessageChanged() ) );
  connect( inkCanvas_,               SIGNAL(               inkChanged() ),
           this,                     SLOT  (       slotMessageChanged() ) );
  connect( textButton_,              SIGNAL(                  clicked() ),
           this,                     SLOT  (         slotSwitchEditor() ) );
  connect( inkButton_,               SIGNAL(                  clicked() ),
           this,                     SLOT  (         slotSwitchEditor() ) );
  connect( winksButton_,             SIGNAL(                  clicked() ),
           this,                     SLOT  (         slotSwitchEditor() ) );
  connect( standardEmoticonButton_,  SIGNAL(                  clicked() ),
           this,                     SLOT  (         slotSwitchEditor() ) );
  connect( customEmoticonButton_,    SIGNAL(                  clicked() ),
           this,                     SLOT  (         slotSwitchEditor() ) );
  connect( fontButton_,              SIGNAL(                  clicked() ),
           this,                     SLOT  (                 editFont() ) );
  connect( fontColorButton_,         SIGNAL(                  clicked() ),
           this,                     SLOT  (            editFontColor() ) );

  // Create the contacts dock
  contactsDock_ = new QDockWidget( i18n( "Contacts" ), this );
  contactsDock_->setObjectName( "ContactsDockWidget" );
  contactsDock_->setAllowedAreas( Qt::AllDockWidgetAreas );
  contactsDock_->setFeatures( QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );

  // Create the standard emoticons dock
  standardEmoticonsDock_ = new QDockWidget( i18n( "Emoticons" ), this );
  standardEmoticonsDock_->setObjectName( "StandardEmoticonsDockWidget" );
  standardEmoticonsDock_->setAllowedAreas( Qt::AllDockWidgetAreas );
  standardEmoticonsDock_->setFeatures( QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
  standardEmoticonsDock_->hide(); // Hide it by default

  // Create the custom emoticons dock
  customEmoticonsDock_ = new QDockWidget( i18n( "My Emoticons" ), this );
  customEmoticonsDock_->setObjectName( "CustomEmoticonsDockWidget" );
  customEmoticonsDock_->setAllowedAreas( Qt::AllDockWidgetAreas );
  customEmoticonsDock_->setFeatures( QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
  customEmoticonsDock_->hide(); // Hide it by default

  // Add them to the window
  setDockOptions( QMainWindow::AnimatedDocks | QMainWindow::AllowTabbedDocks );
  Qt::DockWidgetArea position = QApplication::isLeftToRight()
                                ? Qt::RightDockWidgetArea
                                : Qt::LeftDockWidgetArea;
  addDockWidget( position, contactsDock_          );
  addDockWidget( position, standardEmoticonsDock_ );
  addDockWidget( position, customEmoticonsDock_   );

  //Install the event filters to catch events
  installEventFilter( this );
  inkCanvas_  ->installEventFilter( this );
  messageEdit_->installEventFilter( this );
  winksWidget_->installEventFilter( this );
  messageEdit_->viewport()->installEventFilter( this );

  // Set the button icons
  standardEmoticonButton_->setIcon( KIcon( "face-smile" ) );
  customEmoticonButton_  ->setIcon( KIcon( "face-smile-gearhead-female" ) );
  inkButton_             ->setIcon( KIcon( "draw-brush" ) );
  winksButton_           ->setIcon( KIcon( "applications-toys" ) );
  textButton_            ->setIcon( KIcon( "draw-text" ) );
  fontButton_            ->setIcon( KIcon( "preferences-desktop-font" ) );
  fontColorButton_       ->setIcon( KIcon( "format-stroke-color" ) );

  // Set up the buttons for setting ink colour/stroke size/etc.
  inkCanvas_->setPenSize( inkPenSize_->value() );

  inkColorButton_->setIcon( KIcon( "format-stroke-color" ) );
  inkEraseButton_->setIcon( KIcon( "draw-eraser"         ) );
  inkClearButton_->setIcon( KIcon( "edit-clear"          ) );

  connect( inkColorButton_,        SIGNAL(            clicked()    ),
           this,                   SLOT  ( slotChangeInkColor()    ) );
  connect( inkEraseButton_,        SIGNAL(            clicked()    ),
           this,                   SLOT  ( slotChangeInkBrush()    ) );
  connect( inkClearButton_,        SIGNAL(            clicked()    ),
           inkCanvas_,             SLOT  (              clear()    ) );
  connect( inkPenSize_,            SIGNAL(       valueChanged(int) ),
           inkCanvas_,             SLOT  (         setPenSize(int) ) );

  // Connect the message edit so that if its displayed color changes,
  // it's checked to match the user's chosen color.
  connect( messageEdit_,           SIGNAL(      currentColorChanged(const QColor&) ),
           this,                   SLOT  (       editorColorChanged(const QColor&) ) );
  // Connect the docks signals
  connect( standardEmoticonsDock_, SIGNAL(        visibilityChanged(bool)          ),
           this,                   SLOT  ( slotEmoticonDocksToggled()              ) );
  connect( customEmoticonsDock_,   SIGNAL(        visibilityChanged(bool)          ),
           this,                   SLOT  ( slotEmoticonDocksToggled()              ) );
  // And the tab widget signals
  connect( chatTabs_,              SIGNAL(         mouseMiddleClick(QWidget*)      ),
           this,                   SLOT  (           slotTabRemoved(QWidget*)      ) );
  connect( chatTabs_,              SIGNAL(             closeRequest(QWidget*)      ),
           this,                   SLOT  (           slotTabRemoved(QWidget*)      ) );
  connect( chatTabs_,              SIGNAL(           currentChanged(int)           ),
           this,                   SLOT  (           slotTabChanged(int)           ) );

  // Workaround for tab close buttons in KDE 4.0: Enable the buttons on hover
#if KDE_IS_VERSION( 4, 1, 0 )
  chatTabs_->setCloseButtonEnabled( true );
#else
  chatTabs_->setHoverCloseButton( true );
  chatTabs_->setHoverCloseButtonDelayed( true );
#endif

  // Create the status bar
  statusLabel_ = new ChatStatusBar( this );
  statusBar()->addWidget( statusLabel_, 1 );

  // Create the menus and actions
  createMenus();

  // Build the gui from xml
  setupGUI( Default, "chatwindowui.rc" );


  // Lastly, initialize the "Typing notification" timer
  userTypingTimer_.setInterval( TYPING_NOTIFICATION_TIME );
  userTypingTimer_.setSingleShot( true );


  // Forward reconnect signal
  connect( statusLabel_,   SIGNAL(        reconnect()  ),
           this,           SIGNAL(        reconnect()  ) );
  // Setup the window title blinking timer
  connect( &blinkTimer_,   SIGNAL(           timeout() ),
           this,           SLOT  (  slotBlinkCaption() ) );
  // Setup the status bar text timer
  statusTimer_.setSingleShot( true );
  connect( &statusTimer_,  SIGNAL(           timeout() ),
           this,           SLOT  ( showStatusMessage() ) );

  inkCtlFrame_->setVisible( false );
}



// The destructor
ChatWindow::~ChatWindow()
{
  // Save the window properties
  saveProperties();

#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "DESTROYED.";
#endif
}



// Add a new chat tab
Chat *ChatWindow::addChatTab( Chat *newChat, bool foreground )
{
  // Connect its main signals
  connect( newChat, SIGNAL(      chatInfoChanged()                         ),
           this,    SLOT  (   slotUpdateChatInfo()                         ) );
  connect( newChat, SIGNAL(             gotNudge()                         ),
           this,    SLOT  (         slotGotNudge()                         ) );
  connect( newChat, SIGNAL(       gotChatMessage(const ChatMessage&,Chat*) ),
           this,    SLOT  (   slotGotChatMessage(const ChatMessage&,Chat*) ) );
  connect( newChat, SIGNAL(     gotTypingMessage(Chat*)                    ),
           this,    SLOT  ( slotGotTypingMessage(Chat*)                    ) );

  // Also create a tab for the new chat
  int newTabIndex = chatTabs_->addTab( newChat, QString() );
  slotUpdateChatInfo( newChat );

  // The chat is the new active one: raise it and update the chat window
  if( foreground )
  {
    chatTabs_->setCurrentIndex( newTabIndex );
  }

  // Set the appropriate parent of the contactswidget.
  // This is needed when a chat is moved to another window.
  newChat->getContactsWidget()->setParent( contactsDock_ );

  // Apply the window's settings to the tab
  newChat->getContactsWidget()->setDockWidget( contactsDock_, dockWidgetArea( contactsDock_ ) );
  newChat->setZoomFactor( zoomLevel_ );

  // Enable the spell checker for the new tab.
  // FIXME KDE4.x bug this needs to be unset then set
  messageEdit_->setCheckSpellingEnabled( ! spellCheckAction_->isChecked() );
  messageEdit_->setCheckSpellingEnabled( spellCheckAction_->isChecked() );

  // If there's more than one tab now, enable the Close All option
  bool hasMultipleTabs = ( chatTabs_->count() > 1 );
  closeAllAction_->setEnabled( hasMultipleTabs );
  prevTabAction_->setEnabled( hasMultipleTabs );
  nextTabAction_->setEnabled( hasMultipleTabs );

  // Tab grouping is disabled, hide the bar
  chatTabs_->setTabBarHidden( currentAccount_->getTabbedChatMode() == 2 );

  return newChat;
}



// Set the zoom factor of the text
void ChatWindow::changeZoomFactor( bool increase )
{
  int newZoom = zoomLevel_;

  if( increase )
  {
    newZoom += 25;
  }
  else
  {
    newZoom -= 25;
  }

  if( newZoom < 20 || newZoom > 300 )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Zoom level out of bounds, not updating.";
#endif
    return;
  }

  // Apply the zoom level to all chats
  zoomLevel_ = newZoom;

  for( int i = 0; i < chatTabs_->count(); i++ )
  {
    getChat( i )->setZoomFactor( zoomLevel_ );
  }
}



// Check if the window must be closed
bool ChatWindow::checkAndCloseWindow()
{
  // Check how many chat are still opened
  int chatsNumber = chatTabs_->count();
  if( chatsNumber > 0 )
  {
    // If there's more than one tab, show the Close All option and close button
    bool hasMultipleTabs = ( chatTabs_->count() > 1 );
    closeAllAction_->setEnabled( hasMultipleTabs );
    prevTabAction_->setEnabled( hasMultipleTabs );
    nextTabAction_->setEnabled( hasMultipleTabs );
    return false;
  }

  // Save GUI settings if needed
  if ( settingsDirty() && autoSaveSettings() )
  {
    saveAutoSaveSettings();
  }

  // Kill this window
  hide();
  deleteLater();

  return true;
}



// The chat window is closing
bool ChatWindow::closeAllTabs()
{
  // Ask the user for confirmation
  if( chatTabs_->count() > 1       // Do not ask if there only is one tab
  &&  sender() != closeAllAction_  // Do not ask if the user chose the "close all tabs" action
  &&  ! kapp->sessionSaving() )    // Do not ask if the KDE session is closing
  {
    int res = KMessageBox::questionYesNoCancel( this,
                                                i18n( "<html>There are multiple tabs open in this chat window. "
                                                      "Do you want to close the current tab only, or all tabs?<br /><br />"
                                                      "<i>Note: You can close all tabs at once by pressing Alt+F4.</i></html>" ),
                                                i18nc( "Dialog box caption: closing a chatwindow with a single tab",
                                                       "Closing a Chat Tab" ),
                                                KGuiItem( i18n( "Close All Tabs" ), "window-close" ), // Yes option
                                                KGuiItem( i18n( "Close Current Tab" ), "tab-close" ), // No option
                                                KStandardGuiItem::cancel(),
                                                "closeOneChatTabInfo" );
    switch( res )
    {
      case KMessageBox::Yes   : break;
      case KMessageBox::No    : return closeTab();
      case KMessageBox::Cancel:
      default                 : return false;
    }
  }

  // Just close all tabs one by one, until the last one closes the window too
  while( chatTabs_->count() > 0 )
  {
    closeTab();
  }

  return true;
}



// A chat tab is closing
bool ChatWindow::closeTab()
{
#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "Close request detected.";
#endif

  Chat *currentChat = getCurrentChat();
  if( currentChat != 0 )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Closing current chat tab " << currentChat;
#endif

    if( ! slotTabRemoved( currentChat ) )
    {
      kmWarning() << "Unable to close the current chat!";
    }
  }

   // If there still are open tabs, don't close
  return checkAndCloseWindow();
}



// Close any open widget or close the tab
void ChatWindow::closeWidgetOrTab()
{
  // if the Esc button was pressend on emoticon/ink editor then
  // set the editor to Message Editor
  if( ! messageEdit_->isVisible() )
  {
    slotSwitchEditor();
  }
  else
  {
    // Else close the current tab
    closeTab();
  }
}



// Create the menus.
void ChatWindow::createMenus()
{
  KAction       *closeAction, *saveAction, *quitAction;
  KAction       *copy, *findAction, *zoomIn, *zoomOut, *clearChatAction;
  QAction       *contactsDockAction, *standardEmoticonsDockAction, *customEmoticonsDockAction;


  // Create the actions for "Chat" menu
  inviteButton_       = new KAction      ( KIcon("list-add-user"),                         i18n("&Invite..."),         this );
  sendAction_         = new KAction      ( KIcon("folder-remote"),                         i18n("Send a &File..."),    this );
  nudgeAction_        = new KAction      ( KIcon("preferences-desktop-notification-bell"), i18n("Send a &Nudge!"),     this );
  historyMenuAction_  = new KActionMenu  ( KIcon("chronometer"),                           i18n("Chat &History..."),   this );
  saveAction          = new KAction      ( KIcon("document-save"),                         i18n("Save Chat..."),       this );
  closeAllAction_     = new KAction      ( KIcon("dialog-close"),                          i18n("Close &All Tabs"),    this );
  closeAction         = KStandardAction::close  ( this, SLOT( closeWidgetOrTab() ) , actionCollection_ );
  quitAction          = KStandardAction::quit   ( kapp, SLOT( quit() ), actionCollection_ );

  // Create the actions for "Edit" menu
  changeFontAction_   = new KAction      ( KIcon("preferences-desktop-font"),              i18n("Change &Font"),       this );
  changeFontColorAction_ = new KAction      ( KIcon("format-stroke-color"),                   i18n("Change Font &Color"), this );
  cutAction_          = KStandardAction::cut    ( messageEdit_, SLOT(       cut() ), actionCollection_ );
  copy                = KStandardAction::copy   ( messageEdit_, SLOT(      copy() ), actionCollection_ );
  pasteAction_        = KStandardAction::paste  ( messageEdit_, SLOT(     paste() ), actionCollection_ );
  findAction          = KStandardAction::find   ( this, SLOT(      editFind() ), actionCollection_ );

  // Create the actions for "View" menu
  emoticonAction_     = new KToggleAction( KIcon("face-smile-big"),                   i18n("Show &Emoticons"),         this );
  sessionInfoAction_  = new KToggleAction( KIcon("flag-green"),                       i18n("Show S&tatus Messages"),   this );
  zoomIn              = KStandardAction::zoomIn ( this, SLOT(             viewZoomIn() ), actionCollection_ );
  zoomOut             = KStandardAction::zoomOut( this, SLOT(            viewZoomOut() ), actionCollection_ );
  prevTabAction_      = KStandardAction::back   ( this, SLOT(    slotSwitchToLeftTab() ), actionCollection_ );
  nextTabAction_      = KStandardAction::forward( this, SLOT(   slotSwitchToRightTab() ), actionCollection_ );
  clearChatAction     = KStandardAction::clear  ( this, SLOT(          viewClearChat() ), actionCollection_ );
  panelsMenuAction_   = new KActionMenu  ( KIcon("view-choose"),                      i18n("&Panels"),                 this );

  // Create the actions for "Settings" menu
  spellCheckAction_   = new KToggleAction( KIcon("tools-check-spelling"),             i18n("Use &Spell Checking"),     this );
  showMenuBar_        = KStandardAction::showMenubar( this, SLOT( showMenuBar() ), actionCollection_ );

  // Add shorter texts for the toolbar items
  nudgeAction_  ->setIconText( i18n("Nudge") );
  sendAction_   ->setIconText( i18n("Send a &File") );

  // Rename the tab navigation actions for clarity
  prevTabAction_->setText    ( i18n("P&revious Tab") );
  prevTabAction_->setIconText( i18n("P&revious Tab") );
  nextTabAction_->setText    ( i18n("Ne&xt Tab") );
  nextTabAction_->setIconText( i18n("Ne&xt Tab") );

  // Enable some default action shortcuts (preserve the original ones where needed)
  nudgeAction_->setShortcut( QKeySequence( "Alt+Z" ) );
  closeAction->setShortcuts( QList<QKeySequence>() << QKeySequence( "Ctrl+W" ) << closeAction->shortcut()  );
  closeAllAction_->setShortcuts( QList<QKeySequence>() << QKeySequence( "Ctrl+Q" ) << closeAllAction_->shortcut()  );
  prevTabAction_->setShortcuts( QList<QKeySequence>() << QKeySequence( QKeySequence::PreviousChild ) << prevTabAction_->shortcut() );
  nextTabAction_->setShortcuts( QList<QKeySequence>() << QKeySequence( QKeySequence::NextChild ) << nextTabAction_->shortcut() );

  // Set up the dock actions
  contactsDockAction          = contactsDock_         ->toggleViewAction();
  standardEmoticonsDockAction = standardEmoticonsDock_->toggleViewAction();
  customEmoticonsDockAction   = customEmoticonsDock_  ->toggleViewAction();

  contactsDockAction->setIcon( KIcon("meeting-attending") );
  contactsDockAction->setToolTip( i18n("Enable or disable the contacts panel") );
  contactsDockAction->setText( i18nc("Toolbar button","Contacts") );
  contactsDockAction->setIconText( i18nc("Toolbar button","Contacts") );
  contactsDockAction->setShortcut( QKeySequence( "Ctrl+D" ) );

  standardEmoticonsDockAction->setIcon( KIcon("face-smile") );
  standardEmoticonsDockAction->setToolTip( i18n("Enable or disable the standard emoticons panel") );
  standardEmoticonsDockAction->setText( i18nc("Toolbar button","Emoticons") );
  standardEmoticonsDockAction->setIconText( i18nc("Toolbar button","Emoticons") );
  standardEmoticonsDockAction->setShortcut( QKeySequence( "Ctrl+E" ) );

  customEmoticonsDockAction->setIcon( KIcon("face-smile-gearhead-female") );
  customEmoticonsDockAction->setToolTip( i18n("Enable or disable the custom emoticons panel") );
  customEmoticonsDockAction->setText( i18nc("Toolbar button","My Emoticons") );
  customEmoticonsDockAction->setIconText( i18nc("Toolbar button","My Emoticons") );
  customEmoticonsDockAction->setShortcut( QKeySequence( "Ctrl+Y" ) );

  // Add the dock toggling actions to the Panels menu
  panelsMenuAction_->addAction( contactsDockAction          );
  panelsMenuAction_->addAction( standardEmoticonsDockAction );
  panelsMenuAction_->addAction( customEmoticonsDockAction   );

  // Initially the chat has one tab only, so the Close All option is not useful
  closeAllAction_->setEnabled( false );

  // Add shorter texts for the toolbar
  changeFontAction_     ->setIconText( i18n("&Font") );
  changeFontColorAction_->setIconText( i18n("Font &Color") );
  clearChatAction->setText( i18n("C&lear Chat Window") );
  clearChatAction->setIconText( i18n("C&lear Chat") );

  // Connect slots to signals for "Chat" menu
  connect( inviteButton_,       SIGNAL(           triggered(bool) ),
           this,                SLOT  (   slotInviteContact()     ) );
  connect( sendAction_,         SIGNAL(           triggered(bool) ),
           this,                SLOT  (   startFileTransfer()     ) );
  connect( nudgeAction_,        SIGNAL(           triggered(bool) ),
           this,                SLOT  (           sendNudge()     ) );
  connect( historyMenuAction_,  SIGNAL(           triggered(bool) ),
           this,                SLOT  ( slotShowChatHistory()     ) );
  connect( saveAction,          SIGNAL(           triggered(bool) ),
           this,                SLOT  (            saveChat()     ) );
  connect( closeAllAction_,     SIGNAL(           triggered()     ),
           this,                SLOT  (        closeAllTabs()     ) );

  // Connect slots to signals for "Edit" menu
  connect( changeFontAction_,   SIGNAL(           triggered(bool) ),
           this,                SLOT  (            editFont()     ) );
  connect( changeFontColorAction_,SIGNAL(          triggered(bool) ),
           this,                SLOT  (       editFontColor()     ) );
  connect( clearChatAction,     SIGNAL(           triggered()     ),
           this,                SLOT  (       viewClearChat()     ) );

  // Connect slots to signals for "Settings" menu
  connect( emoticonAction_,     SIGNAL(             toggled(bool) ),
           this,                SLOT  (     toggleEmoticons(bool) ) );
  connect( spellCheckAction_,   SIGNAL(             toggled(bool) ),
           this,                SLOT  (    toggleSpellCheck(bool) ) );
  connect( sessionInfoAction_,  SIGNAL(             toggled(bool) ),
           this,                SLOT  (   toggleSessionInfo(bool) ) );


  // Add actions to actionCollection for "Chat" menu
  actionCollection_->addAction( "send",         sendAction_        );
  actionCollection_->addAction( "nudge",        nudgeAction_       );
  actionCollection_->addAction( "save",         saveAction         );
  actionCollection_->addAction( "historyMenu",  historyMenuAction_ );
  actionCollection_->addAction( "closeAll",     closeAllAction_    );
  actionCollection_->addAction( "invite",       inviteButton_      );

  // Add actions to actionCollection for "Edit" menu
  actionCollection_->addAction( "font",         changeFontAction_      );
  actionCollection_->addAction( "fontColor",    changeFontColorAction_ );

  // Add actions to actionCollection for "View" menu
  actionCollection_->addAction( "prevTab",      prevTabAction_      );
  actionCollection_->addAction( "nextTab",      nextTabAction_      );
  actionCollection_->addAction( "clearChat",    clearChatAction     );
  actionCollection_->addAction( "emoticons",    emoticonAction_     );
  actionCollection_->addAction( "panelsMenu",   panelsMenuAction_   );
  actionCollection_->addAction( "contactsDock", contactsDockAction  );
  actionCollection_->addAction( "sessionInfo",  sessionInfoAction_  );
  actionCollection_->addAction( "standardEmoticonsDock", standardEmoticonsDockAction );
  actionCollection_->addAction( "customEmoticonsDock",   customEmoticonsDockAction   );

  // Add actions to actionCollection for "Settings" menu
  actionCollection_->addAction( "spellCheck",   spellCheckAction_   );
}



// Copy the currently selected text
void ChatWindow::editCopy()
{
  // Get the cursor reference
  QTextCursor cursor = messageEdit_->textCursor();

  if( cursor.hasSelection() )
  {
    messageEdit_->copy();
  }
  else

    getCurrentChat()->editCopy();
}



// put the marked text/object into the clipboard and remove
//  it from the document
void ChatWindow::editCut()
{
  // If another editor has focus, don't actually cut anything
  if( ! messageEdit_->hasFocus() )
  {
    return;
  }

   messageEdit_->cut();
}



// Bring up a find dialog to search for text in the chat.
void ChatWindow::editFind()
{
  getCurrentChat()->slotFindChatText();
}



// Bring up a dialog to change the message font color.
void ChatWindow::editFont()
{
  QFont selection( currentAccount_->getFont() );

  int result = KFontDialog::getFont( selection, KFontChooser::NoDisplayFlags, this );

  if( result != KFontDialog::Accepted )
  {
    return;
  }

  currentAccount_->setFont( selection );

  // Commit changes to disk, now
  currentAccount_->saveProperties();
}



// Bring up a dialog to change the message font color.
void ChatWindow::editFontColor()
{
  QColor color( currentAccount_->getFontColor() );

  // Show the color dialog
  int result = KColorDialog::getColor( color, this );
  if ( result == KColorDialog::Accepted )
  {
    // Get the string version of the selected color
    const QString fontColor( color.name() );
    // Set it to the stored account
    currentAccount_->setFontColor(fontColor);

    // Commit changes to disk, now
    currentAccount_->saveProperties();
  }
}



// The color in the text box changed.
void ChatWindow::editorColorChanged(const QColor &color)
  {
#ifdef KMESSTEST
  KMESS_ASSERT( currentAccount_ != 0 );
#endif

  // Whenever the QTextEdit's contents is entirely deleted, the font color spontaneously resets to black;
  // if this happens, set the color back to the user's color.
  // HACK: This possibly is a Qt <= 4.4 bug exposed.
  // UPDATE: Happens with Qt 4.5 too. Also resets the font weight.
  if ( color.name() != currentAccount_->getFontColor() )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Color was reset to" << color.name() << ". Restoring it to the account setting.";
#endif
    messageEdit_->setTextColor( QColor( currentAccount_->getFontColor() ) );
    messageEdit_->setFont( QFont ( currentAccount_->getFont() ) );
  }

  // Adjust the message editor's minimum height to match the current font size
  QFontMetrics metrics( messageEdit_->currentFont() );
  messageEdit_->setMinimumSize( 100, metrics.height() + metrics.lineSpacing() );
}



// paste the clipboard into the document
void ChatWindow::editPaste()
{
  // Regardless of focus, always paste in message edit.
  messageEdit_->paste();
}



// Filter to catch window activation events
bool ChatWindow::eventFilter( QObject *obj, QEvent *event )
{
  if( obj == this && event->type() == QEvent::WindowActivate )
  {
    // Stop blinking immediately when the window is activated
    if( blinkTimer_.isActive() )
    {
      blinkTimer_.stop();
      blinkToUpper_ = true;

      setWindowTitle();
    }

    // Give focus to the current message editor
    if( messageEdit_->isVisible() )
    {
      messageEdit_->setFocus();
    }
    else if( inkCanvas_->isVisible() )
    {
      inkCanvas_->setFocus();
    }

    return false;
  }

  if ( event->type() == QEvent::KeyPress )
  {
    QKeyEvent *keyEvent = static_cast<QKeyEvent*>( event );
    if ( keyEvent->key() == Qt::Key_Escape )
    {
      closeWidgetOrTab();
      return true;
    }
  }

  // Only manage events for the editors from now on (the event filter code
  // is complex enough as it is now...)
  if( obj != messageEdit_
  &&  obj != messageEdit_->viewport()
  &&  obj != inkCanvas_
  &&  obj != winksWidget_
  &&  obj != standardEmoticonsWidget_
  &&  obj != customEmoticonsWidget_ )
  {
    return false;
  }

  switch( event->type() )
  {
    // If a key was pressed, check if it's return and if so, send the drawing
    case QEvent::KeyPress:
    {
      QKeyEvent *keyEvent = static_cast<QKeyEvent*>( event );
      bool       ctrl     = keyEvent->modifiers() & Qt::ControlModifier;
      bool       shift    = keyEvent->modifiers() & Qt::ShiftModifier;
      bool       alt      = keyEvent->modifiers() & Qt::AltModifier;
      int        key      = keyEvent->key();

      if( key == Qt::Key_Return || key == Qt::Key_Enter )
      {
        if( shift || ctrl )
        {
          // ctrl+enter or shift+enter, insert a new line instead.
          messageEdit_->insertPlainText( "\n" );
        }
        else
        {
          slotSendButtonClicked();
        }

        // Blocks further handling of this event.
        keyEvent->ignore();
        return true;
      }
      // Detect scrolling commands. Alt scrolls faster (shift is used for text selection in the editor)
      else if( ( key == Qt::Key_PageUp || key == Qt::Key_PageDown ) && ( ! shift ) )
      {
        getCurrentChat()->scrollTo( ( key == Qt::Key_PageDown ), alt );
        return true;
      }
      else if( alt )
      {
        bool ok = false;
        int chatIndex = keyEvent->text().toInt( &ok );

        // Technically the index can't be more than 9, of course, but keep things safe & simple
        if( ok && chatIndex > 0 && chatIndex <= chatTabs_->count() )
        {
          chatTabs_->setCurrentIndex( chatIndex - 1 );
          keyEvent->ignore();
          return true;
        }
      }
      else if( ctrl )
      {
        QStringList& quickRetypeList = getCurrentChat()->getQuickRetypeList();
        switch( key )
        {
          // Detect text copying from the message editor, or the Message View
          case Qt::Key_C:
            editCopy();
            break;

          // Detect 'save chat' command
          case Qt::Key_S:
            getCurrentChat()->showSaveChatDialog();
            break;

          // Detect fast-retype (ctrl+UpArrow)
          case Qt::Key_Up:
            if( quickRetypeList.isEmpty() )
            {
              break;
            }

            if( indexSentences_ == quickRetypeList.size() )
            {
              // store the last sentence in writing
              lastSentence_ = messageEdit_->toPlainText();
            }

            if( indexSentences_ > 0 )
            {
              indexSentences_--;
            }

            if( indexSentences_ >= 0 )
            {
              messageEdit_->setPlainText( quickRetypeList[ indexSentences_ ] );
            }
            break;

          // Detect fast-retype (ctrl+DownArrow)
          case Qt::Key_Down:
            if( quickRetypeList.isEmpty() )
            {
              break;
            }

            if( indexSentences_ == quickRetypeList.size() - 1 )
            {
              messageEdit_->setPlainText( lastSentence_ );
              indexSentences_ = quickRetypeList.size();
            }

            if( indexSentences_ < quickRetypeList.size() - 1 )
            {
              indexSentences_++;
            }

            if( indexSentences_ < quickRetypeList.size() )
            {
              messageEdit_->setPlainText( quickRetypeList[ indexSentences_ ] );
            }
            break;

          default:
            break;
        }
      }

      // check if this is a Find key sequence. if so, stop it before it gets to the message edit
      // this prevents the Ctrl+F key sequence being consumed by KTextEdit.
      // KTextEdit does not stop consuming the find/replace key sequences if we disable find/replace
      // (bug has been filed with KDE) so this hack is necessary.
      if ( KStandardShortcut::find().contains(key | keyEvent->modifiers() ) )
      {
        editFind();
        return true;
      }

      // Another key was pressed: switch to the message editor if needed
      if( editorChooser_->currentWidget() != textPage_ )
      {
        slotSwitchEditor();
      }

      return false;
    }

    case QEvent::WindowActivate:
      if( messageEdit_->isVisible() )
      {
        messageEdit_->setFocus();
      }
      else if( inkCanvas_->isVisible() )
      {
        inkCanvas_->setFocus();
      }
      break;

    case QEvent::FocusOut:
    case QEvent::Show:
    {
      QWidget *focusedWidget = QApplication::focusWidget();

      // Allow this chat's search bar to take focus away from the message editor
      if( focusedWidget && QString( focusedWidget->metaObject()->className() ) == "KHistoryComboBox" )
      {
        break;
      }

      messageEdit_->setFocus( Qt::OtherFocusReason );
      break;
    }

    case QEvent::DragEnter:
    case QEvent::DragMove:
    {
      QDragEnterEvent *dragEvent = static_cast<QDragEnterEvent*>( event );

      if( ! dragEvent )
      {
#ifdef KMESSDEBUG_CHATWINDOW
        kmDebug() << "Ignoring invalid drag event!";
#endif
        return false;
      }

      const QMimeData *mimeData = dragEvent->mimeData();
      if( mimeData->hasUrls()
      ||  mimeData->hasImage() )
      {
#ifdef KMESSDEBUG_CHATWINDOW
        kmDebug() << "Accepting drag event of mimetypes:" << dragEvent->mimeData()->formats();
#endif
        dragEvent->acceptProposedAction();
        return true;
      }

#ifdef KMESSDEBUG_CHATWINDOW
      kmDebug() << "Ignoring invalid drop having mimetypes:" << dragEvent->mimeData()->formats();
#endif

      break;
    }

    case QEvent::Drop:
    {
      QDropEvent *dropEvent = static_cast<QDropEvent*>( event );

      if( ! dropEvent )
      {
#ifdef KMESSDEBUG_CHATWINDOW
        kmDebug() << "Ignoring invalid drop!";
#endif
        return true;
      }

      const QMimeData *mimeData = dropEvent->mimeData();

      // Check for drops of ISF drawings
      if( mimeData->hasImage() && ! mimeData->urls().isEmpty() )
      {
        const QString marker( "data:image/png;base64," );
        const QString url( mimeData->urls().first().toString() );

#ifdef KMESSDEBUG_CHATWINDOW
        kmDebug() << "Checking if the dropped inline image is a fortified-PNG...";
#endif
        if( url.startsWith( marker ) )
        {
#ifdef KMESSDEBUG_CHATWINDOW
          kmDebug() << "Setting drawing to the Ink Canvas.";
#endif
          QByteArray isfData( url.mid( marker.length() ).toAscii() );
          Isf::Drawing* drawing = &( Isf::Stream::readerPng( isfData, true ) );

          // We need to both set it to the canvas and change the object stored by the chat tab,
          // so the old drawing will be destroyed
          getCurrentChat()->setInkDrawing( drawing );
          inkCanvas_->setDrawing( drawing );

          // Update the canvas and switch to it
          inkButton_->setChecked( true );
          slotSwitchEditor( inkButton_ );
          sendButton_->setDisabled( drawing->isNull() );

          dropEvent->acceptProposedAction();
          return true;
        }
      }


      // Process the drop only if it comes from out of our application.
      if( dropEvent->source() != 0 )
      {
#ifdef KMESSDEBUG_CHATWINDOW
        kmDebug() << "Ignoring internal drop!";
#endif
        dropEvent->ignore();
        return true;
      }

      // Handle drag and drop of files
      if( mimeData->hasUrls() )
      {
#ifdef KMESSDEBUG_CHATWINDOW
        kmDebug() << "Drag'n'dropped files:" << mimeData->urls();
#endif

        // Send the files to the contact
        getCurrentChat()->startFileTransfer( mimeData->urls() );

        dropEvent->acceptProposedAction();
        return true;
      }

      if( mimeData->hasImage() )
      {
        QImage image( qvariant_cast<QImage>( mimeData->imageData() ) );

        if( image.isNull() )
        {
#ifdef KMESSDEBUG_CHATVIEW
          kmDebug() << "Drag'n'dropped an invalid image!";
#endif
          return true;
        }

#ifdef KMESSDEBUG_CHATWINDOW
        kmDebug() << "Drag'n'dropped a" << image.size() << "image";
#endif

        // Create a temporary file where the image will be stored.
        // TODO: The file is not destroyed, as it needs to be kept
        // available until the transfer is completed. Find a way
        // to remove it when KMess quits or as soon as the transfer
        // is done.
        QTemporaryFile file;
        file.setAutoRemove( false );
        file.setFileTemplate( QDir::tempPath() + "/kmess.XXXXXX.jpg" );

        if( ! file.open() )
        {
#ifdef KMESSDEBUG_CHATWINDOW
          kmDebug() << "Cannot create temporary image file!";
#endif
          return true;
        }

        dropEvent->acceptProposedAction();

        // Save the image and get its local URL
        image.save( &file, "JPG" );
        file.close();
        QUrl imageUrl( "file://" + file.fileName() );

        // Send the image to the contact
        QList<QUrl> urlList;
        urlList.append( imageUrl );
        getCurrentChat()->startFileTransfer( urlList );

        return true;
      }

      break;
    }

    default:
      break;
  }

  return false;
}



/**
 * @brief Return a reference to a chat
 *
 * When called without parameters, this method returns the active chat.
 * If the given index is not valid, the current index is returned, too.
 *
 * @param index  Number of the chat to retrieve, or nothing to get the currently active one.
 * @return Chat *
 */
Chat *ChatWindow::getChat( int index )
{
#ifdef KMESSTEST
  KMESS_ASSERT( chatTabs_->count() > 0 );
#endif

  if( index < 0 || index >= chatTabs_->count() )
  {
    return getCurrentChat();
  }

  return qobject_cast<Chat*>( chatTabs_->widget( index ) );
}



// Return a reference to the active chat
Chat *ChatWindow::getCurrentChat()
{
  return qobject_cast<Chat*>( chatTabs_->currentWidget() );
}




// Handle a text command (e.g. '/online')
bool ChatWindow::handleCommand( QString command )
{
  if( command.isEmpty() )
  {
    return true;
  }

  // Parse arguments
  QStringList arguments( command.split( " ", QString::SkipEmptyParts ) );
  if( arguments.isEmpty() )
  {
    return true;
  }

  command = arguments.takeFirst();

  // Make it lower-case
  command = command.toLower();

  // Get us a kmess handle to call setStatus upon
  KMessApplication *kmessApp = static_cast<KMessApplication*>( kapp );
  KMess *kmess = kmessApp->getContactListWindow();

  bool isStatusCommand = ( command == "status" );

  // Check syntax errors
  if( isStatusCommand && arguments.isEmpty() )
  {
    KMessageBox::sorry( this,
                        i18n( "<html>You used an incorrect syntax for the /status command. The correct syntax "
                              "is: <b>/status online|away|idle|brb|busy|lunch|phone|invisible</b>.<br/>"
                              "You can also use shortcuts like <b>/online</b> or <b>/phone</b>.</html>" ),
                        i18nc( "Dialog box caption for wrong command syntax warning", "Incorrect /status Syntax" )
                        );
    return true;
  }

  QString toStatus;
  const QStringList participants( getCurrentChat()->getParticipants() );

  if( isStatusCommand )
  {
    toStatus = arguments.takeFirst(); // we take the first, so we can get to the other arguments.
  }

  // Switch status
  if( command == "online" || ( isStatusCommand && toStatus == "online" ) ||
      command == "back"   || ( isStatusCommand && toStatus == "back"   ) )
  {
    kmess->changeStatus( STATUS_ONLINE );
  }
  else if( command == "away" || ( isStatusCommand && toStatus == "away" ) )
  {
    // Away message?
    QString awayMessage;
    if( ! arguments.isEmpty() )
    {
      awayMessage = arguments.join(" ");
      kmess->changeStatus( STATUS_AWAY_AUTOREPLY, awayMessage );
    }
    else
    {
      kmess->changeStatus( STATUS_AWAY );
    }
  }
  else if( command == "idle" || ( isStatusCommand && toStatus == "idle" ) )
  {
    kmess->changeStatus( STATUS_IDLE );
  }
  else if( command == "brb" || ( isStatusCommand && toStatus == "brb" ) )
  {
    kmess->changeStatus( STATUS_BE_RIGHT_BACK );
  }
  else if( command == "busy" || ( isStatusCommand && toStatus == "busy" ) )
  {
    kmess->changeStatus( STATUS_BUSY );
  }
  else if( command == "lunch" || ( isStatusCommand && toStatus == "lunch" ) )
  {
    kmess->changeStatus( STATUS_OUT_TO_LUNCH );
  }
  else if( command == "phone" || ( isStatusCommand && toStatus == "phone" ) )
  {
    kmess->changeStatus( STATUS_ON_THE_PHONE );
  }
  else if( command == "invisible" || ( isStatusCommand && toStatus == "invisible" ) )
  {
    kmess->changeStatus( STATUS_INVISIBLE );
  }
  // Block and unblock user
  else if( command == "block" )
  {
    if( participants.count() != 1 )
    {
      KMessageBox::sorry( this,
                          i18n( "<html>You cannot use the /block command in a group chat.</html>" ),
                          i18nc( "Caption when trying to block someone in a group chat",
                              "Cannot use /block command" )
                          );
      return true;
    }
    getCurrentChat()->setContactBlocked( participants.first(), true );
  }
  else if( command == "unblock" )
  {
    if( participants.count() != 1 )
    {
      KMessageBox::sorry( this,
                          i18n( "<html>You cannot use the /unblock command in a group chat.</html>" ),
                          i18nc( "Caption when trying to unblock someone in a group chat",
                              "Cannot use /unblock command!" )
                          );
      return true;
    }
    getCurrentChat()->setContactBlocked( participants.first(), false );
  }
  // Send special messages
  else if( command == "me" )
  {
    const QString message( "* " + currentAccount_->getFriendlyName() + " " + arguments.join( " " ) );
    getCurrentChat()->sendChatMessage( message );
    ChatMessage chatmessage( ChatMessage::TYPE_OUTGOING, ChatMessage::CONTENT_MESSAGE, false,
                             message, currentAccount_->getHandle(),
                             currentAccount_->getFriendlyName( STRING_ORIGINAL ),
                             currentAccount_->getPicturePath(),
                             currentAccount_->getFont(), currentAccount_->getFontColor() );
    getCurrentChat()->showMessage( chatmessage );
  }
  // Unknown command
  else
  {
    // If an unknown command contains more than one '/', it's very likely to be a path.
    // Don't show the error and return with 'false' to send the command/path.

    if( command.contains( '/' ) )
    {
      return false;
    }

    KMessageBox::sorry( this,
                        i18n( "<html>Unknown command <b>%1</b>. If you did not want this "
                              "message to be a command, prepend your message with another"
                              " /.</html>", command ),
                        i18nc( "Caption when an unknown command was requested", "Unknown Command" ) );
    return true;
  }

  // Clear the message edit field, because the command was executed succesfully.
  messageEdit_->clear();
  messageEdit_->setFocus();

  return true;
}



// Initialize the user interface
bool ChatWindow::initialize()
{
  if( initialized_ )
  {
    kmDebug() << "already initialized.";
    return false;
  }

  if( ! initializeCurrentAccount() )
  {
    kmDebug() << "Couldn't setup the current account.";
    return false;
  }

  // Autosave all GUI settings
#if KDE_IS_VERSION(4,0,70)
  setAutoSaveSettings( KMessConfig::instance()->getAccountConfig( currentAccount_->getHandle(), "ChatWindow" ),
                       true /* save WindowSize */ );
#else
  setAutoSaveSettings( "ChatWindow", true /* save WindowSize */ );
#endif

  // Read the window properties
  readProperties();

  // Enable the emoticon widgets
  standardEmoticonsWidget_->initialize( false );
  customEmoticonsWidget_  ->initialize( true  );
  connect( standardEmoticonsWidget_, SIGNAL( insertEmoticon(QString) ),
           this,                     SLOT  ( insertEmoticon(QString) ) );
  connect( customEmoticonsWidget_,   SIGNAL( insertEmoticon(QString) ),
           this,                     SLOT  ( insertEmoticon(QString) ) );

  // Create the emoticon widgets for the docks (they're not the same ones used for the chat tabs).
  // Before we create the widgets, the docks have to be in place already, to be able to tell the
  // widgets how to initially layout themselves.
  EmoticonsWidget *standardEmoticonsDockWidget = new EmoticonsWidget( standardEmoticonsDock_ );
  EmoticonsWidget *customEmoticonsDockWidget   = new EmoticonsWidget( customEmoticonsDock_ );
  standardEmoticonsDockWidget->initialize( false );
  customEmoticonsDockWidget  ->initialize( true  );
  standardEmoticonsDockWidget->setDockWidget( standardEmoticonsDock_, dockWidgetArea( standardEmoticonsDock_ ) );
  customEmoticonsDockWidget  ->setDockWidget( customEmoticonsDock_,   dockWidgetArea( customEmoticonsDock_ ) );
  standardEmoticonsDock_     ->setWidget( standardEmoticonsDockWidget );
  customEmoticonsDock_       ->setWidget( customEmoticonsDockWidget   );
  connect( standardEmoticonsDockWidget, SIGNAL( insertEmoticon(const QString&) ),
           this,                        SLOT  ( insertEmoticon(const QString&) ) );
  connect( customEmoticonsDockWidget,   SIGNAL( insertEmoticon(const QString&) ),
           this,                        SLOT  ( insertEmoticon(const QString&) ) );

  initialized_ = true;

  return true;
}



// Set up the current account
bool ChatWindow::initializeCurrentAccount()
{
#ifdef KMESSTEST
  KMESS_ASSERT( currentAccount_ == 0 );
#endif

  currentAccount_ = CurrentAccount::instance();
  if ( currentAccount_ == 0 )
  {
    kmWarning() << "Could not get an instance of the current account.";
    return false;
  }

  // Set up some widgets based on account settings
  emoticonAction_   ->setChecked( currentAccount_->getUseEmoticons()    );
  sessionInfoAction_->setChecked( currentAccount_->getShowSessionInfo() );

#ifdef KMESSTEST
  KMESS_ASSERT( currentAccount_ != 0 );
#endif

  // Connect needed signals
  connect( currentAccount_, SIGNAL( changedFontSettings() ),
           this,            SLOT  (    updateEditorFont() ) );

  return true;
}



// Insert an emoticon into the message editor
void ChatWindow::insertEmoticon( const QString &shortcut )
{
  // Insert the text at the cursor
  messageEdit_->insertPlainText( shortcut );

  // Switch to the message editor then
  slotSwitchEditor();
}



// The window is closing, called by KMainWindow
bool ChatWindow::queryClose()
{
#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "Closing all open chats";
#endif

  return closeAllTabs();
}



// The application is exiting, called by KMainWindow
bool ChatWindow::queryExit()
{
#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "Closing all open chats";
#endif

  // Allow performing last operations to all open chats and delete them
  for( int i = 0; i < chatTabs_->count(); i++ )
  {
    Chat *chat = getChat( i );
    chat->queryExit();
  }

  // Clear the list of chat tabs
  chatTabs_->clear();

  // Force a call to the destructor, to ensure correct cleanup by ChatMaster
  deleteLater();

  // Always accept this one, otherwise the chat window won't close.
  // The quit is presented in KMessApplication::slotLastWindowClosed() too.
#ifdef KMESSDEBUG_KMESSINTERFACE
  kmDebug() << "Accepting exit request.";
#endif

  return true;
}



// Restore the window properties (called by KMainWindow)
void ChatWindow::readProperties(const KConfigGroup &config )
{
  // The parameter is kept to mantain the same arguments as KMainWindow::readProperties().
  Q_UNUSED( config );
  const KConfigGroup group( KMessConfig::instance()->getAccountConfig( currentAccount_->getHandle(), "ChatWindow" ) );

  // Read the window position
  const QPoint pos( group.readEntry( "Position", QPoint() ) );
  if( ! pos.isNull() )
  {
    move( pos );
  }

  // Resize the window to decent dimensions if there's no saved state.
  resize( 550, 450 );

  QByteArray state( group.readEntry( "ToolbarsAndDocksState", QByteArray()  ) );

  // adam (16Feb10): restoreWindowSize has to come before restoreState.
  //
  // when testing with KDE 4.4 I found, for some completely unknown reason,
  // that calling restoreState *first* causes the configuration file in KConfigGroup above (which has *nothing* to do with restoreState)
  // to reset its Width and Height settings to what seem to be default values.
  //
  // I have no idea why this happens - restoreState doesn't trigger a saveProperties call so it shouldn't happen at all!
  // It's dark magic, I tell you.
  //
  // Swapping the calls around fixes the problem. But again, I'll be damned if I can figure out
  // what's going on.

  restoreWindowSize( group );

  restoreState( state );

  // Read if the spelling check feature is enabled
  toggleSpellCheck( group.readEntry( "UseSpellCheck", false ) );

  // Set the default widget sizes. The message editor has a default read size of 1 so it'll be
  //  as small as possible, without becoming invisible
  int minChatViewHeight = chatTabs_->minimumSize().height();
  splitter_->setSizes( group.readEntry( "MessageSplitterSizes", QList<int>() << minChatViewHeight << 1 ) );

  // Load the level of zoom
  zoomLevel_ = group.readEntry( "CurrentZoom", 100 );
}



// Close a chat tab without user intervention
void ChatWindow::removeChatTab( Chat *chat )
{
  chatTabs_->removeTab( chatTabs_->indexOf( chat ) );
}



// Save the chat according to the user's request
void ChatWindow::saveChat()
{
  getCurrentChat()->showSaveChatDialog();
}



// Save the window properties (called by KMainWindow)
void ChatWindow::saveProperties( KConfigGroup &config )
{
  // The parameter is kept to mantain the same arguments as KMainWindow::saveProperties().
  Q_UNUSED( config );
  KConfigGroup group( KMessConfig::instance()->getAccountConfig( currentAccount_->getHandle(), "ChatWindow" ) );

  // Save the window position
  group.writeEntry( "Position", pos() );

  // Save the docks and toolbars state
  group.writeEntry( "ToolbarsAndDocksState", saveState()          );
  group.writeEntry( "UseSpellCheck",         spellCheckAction_->isChecked() );

  group.writeEntry( "MessageSplitterSizes", splitter_->sizes()            );
  group.writeEntry( "CurrentZoom",          zoomLevel_                         );

  // Save window size
  saveWindowSize( group );

  group.config()->sync();
}



// Send a ink via server
void ChatWindow::sendInk()
{
  Chat* currentChat = getCurrentChat();

  // Do not send empty drawings
  if( currentChat->getInkDrawing()->isNull() )
  {
    return;
  }

  bool supportGifInk = true;
  bool supportIsfInk = true;

  // Find out which ink formats are supported by all participants
  const QStringList participants( currentChat->getParticipants() );
  foreach( const QString &handle, participants )
  {
    const ContactBase *contact = currentAccount_->getContactByHandle( handle );
    if( contact == 0 )
    {
      continue;
    }

    supportGifInk &= contact->hasCapability( ContactBase::MSN_CAP_INK_GIF );
    supportIsfInk &= contact->hasCapability( ContactBase::MSN_CAP_INK_ISF );
  }

  // The contacts don't support either formats! Something is wrong, since the ink editor
  // was enabled...
  if( ! supportGifInk && ! supportIsfInk )
  {
    kmWarning() << "Neither Ink formats are supported by the participants!";

    currentChat->showMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
                                           ChatMessage::CONTENT_SYSTEM_ERROR,
                                           true,
                                           i18nc( "Error message shown in chat",
                                                  "Failed to send the handwritten message: "
                                                  "the contacts do not support it." ),
                                           QString(),
                                           QString() ) );
    return;
  }

  InkFormat format = FORMAT_ISF;
  QByteArray ink;

  if( supportIsfInk )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Retrieving ISF ink data";
#endif

    format = FORMAT_ISF;
    ink = Isf::Stream::writer( *( inkCanvas_->drawing() ) );
  }
  else if( supportGifInk && Isf::Stream::supportsGif() )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Retrieving GIF ink data";
#endif

    format = FORMAT_GIF;
    ink = Isf::Stream::writerGif( *( inkCanvas_->drawing() ) );
  }
  // Any other possibility has been excluded above

  if( ink.isEmpty() )
  {
    kmWarning() << "An error occured while creating the" << ( supportIsfInk ? "ISF" : "GIF" ) << "Ink data!";

    currentChat->showMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
                                           ChatMessage::CONTENT_SYSTEM_ERROR,
                                           true,
                                           i18nc( "Error message shown in chat",
                                                  "Failed to send the handwritten message: "
                                                  "an error has occurred while creating it." ),
                                           QString(),
                                           QString() ) );
    return;
  }

  currentChat->sendInkMessage( format, ink );

  // Show the message in the browser window
  const QString text( "<img src='data:image/png;base64," + Isf::Stream::writerPng( *( inkCanvas_->drawing() ), true ) + "'>" );

  ChatMessage message( ChatMessage::TYPE_OUTGOING,
                       ChatMessage::CONTENT_MESSAGE_INK,
                       false,
                       text,
                       currentAccount_->getHandle(),
                       currentAccount_->getFriendlyName( STRING_ORIGINAL ),
                       currentAccount_->getPicturePath() );

  currentChat->showMessage( message );

  // Clear and reset the state of the send button
  inkCanvas_->clear();
  sendButton_->setEnabled( false );
}


// Send a message via the server
void ChatWindow::sendMessage()
{
  // Don't send any typing messages while preparing to send the message in the text box...
  doSendTypingMessages_ = false;

  // Get the message field's contents
  QString text( messageEdit_->toPlainText() );

  // Don't send empty messages
  if( text.trimmed().isEmpty() )
  {
    return;
  }

  // Max length is fixed to 10 chunks ( multi-packet )
  int maxSendableMessageLength = 14000;

  // Messages like "//hello" should be sent as "/hello"
  // Messages like "/hello" are commands.
  // Check if this is a command, and if so, emit handleCommand signal
  if( text.size() > 2 && text.mid( 2, 1 ) != "/" )
  {
    if( text.left( 2 ) == "//" )
    {
      text = text.mid( 1 );
    }
    else if( text.left( 1 ) == "/" )
    {
      const QString command( text.mid( 1 ) );

      // Handle command. If 'false' is returned, send it anyway.
      if ( handleCommand( command ) )
      {
        return;
      }
    }
  }

  // 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.
  const QByteArray utf8Text( text.toUtf8() );
  if( utf8Text.length() > maxSendableMessageLength )
  {
    // If so, then divide the text into the first part and a remainder.
    const QByteArray remainder( utf8Text.right( utf8Text.length() - maxSendableMessageLength ) );
    text = QString::fromUtf8( utf8Text.left( maxSendableMessageLength ) );

    // Return the remainder to the message edit.
    const QString remainingText( QString::fromUtf8( remainder ) );
    messageEdit_->setText( remainingText );
    lastSentence_ = remainingText;
  }
  else
  {
    // Remove the message from the message field (allowing undo)
    lastSentence_.clear();

    QTextCursor cursor( messageEdit_->textCursor() );
    messageEdit_->setTextCursor( cursor );

    cursor.beginEditBlock();
    cursor.select( QTextCursor::Document );
    cursor.removeSelectedText();
    cursor.endEditBlock();
  }

  // Replace "\n" with "\r\n" (to follow the Windows linebreak style followed by the MSN servers)
  // @TODO Move this server stuff to the network/protocol code
  text = text.replace( QRegExp("\r?\n"), "\r\n" );

  // Send the message to the contact(s)
  getCurrentChat()->sendChatMessage( text );

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

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


  // Show the message in the browser window
  ChatMessage message( ChatMessage::TYPE_OUTGOING,
                       ChatMessage::CONTENT_MESSAGE,
                       false,
                       text,
                       currentAccount_->getHandle(),
                       currentAccount_->getFriendlyName( STRING_ORIGINAL ),
                       currentAccount_->getPicturePath(),
                       currentAccount_->getFont(),
                       currentAccount_->getFontColor() );

  getCurrentChat()->showMessage( message );

  // Add it to the list of sent messages, to allow fast retyping
  QStringList &quickRetypeList = getCurrentChat()->getQuickRetypeList();
  quickRetypeList.append( message.getBody() );
  lastSentence_ = QString();

  // Reset the last sentence counter.
  // showMessage() updates chatMessages_
  indexSentences_ = quickRetypeList.size();
}



// The 'Send nudge' button has been clicked
void ChatWindow::sendNudge()
{
  getCurrentChat()->slotSendNudge();
}



// Sets the window icon for this chat window
void ChatWindow::setChatWindowIcon()
{
  Chat *chat = getCurrentChat();
  if( chat == 0 )
  {
    return;
  }

  QPixmap kmessIcon = KIconLoader::global()->loadIcon( "kmess-shadow", KIconLoader::Panel );
  QPixmap userIcon;

  if ( chat->getParticipants().size() == 1 )
  {
    userIcon = KIcon( "user-identity" ).pixmap( 60, 60 );
  }
  else
  {
    userIcon = KIcon( "system-users" ).pixmap( 60, 60 );
  }

  setWindowIcon( KMessShared::drawIconOverlay( userIcon, kmessIcon, kmessIcon.size().width(), kmessIcon.size().height(), .60, .90 ) );
}



// Change the active chat
void ChatWindow::setCurrentChat( Chat *chat )
{
  chatTabs_->setCurrentIndex( chatTabs_->indexOf( chat ) );
}



// Enable or disable the parts of the window which allow user interaction
void ChatWindow::setEnabled( bool isEnabled )
{
  // Set the state of the main UI elements
  customEmoticonsWidget_  ->setEnabled( isEnabled );
  standardEmoticonsWidget_->setEnabled( isEnabled );
  customEmoticonsDock_    ->setEnabled( isEnabled );
  standardEmoticonsDock_  ->setEnabled( isEnabled );

  inkCanvas_              ->setEnabled( isEnabled );
  messageEdit_            ->setEnabled( isEnabled );
  newLineButton_          ->setEnabled( isEnabled );
  sendButton_             ->setEnabled( isEnabled );
  standardEmoticonButton_ ->setEnabled( isEnabled );
  customEmoticonButton_   ->setEnabled( isEnabled );
  fontButton_             ->setEnabled( isEnabled );
  fontColorButton_        ->setEnabled( isEnabled );
  inviteButton_           ->setEnabled( isEnabled );
  textButton_             ->setEnabled( isEnabled );


  // Set the state of the extra contact actions
  emoticonAction_         ->setEnabled( isEnabled );
  nudgeAction_            ->setEnabled( isEnabled );
  sendAction_             ->setEnabled( isEnabled );
  changeFontAction_       ->setEnabled( isEnabled );
  changeFontColorAction_  ->setEnabled( isEnabled );
  spellCheckAction_       ->setEnabled( isEnabled );

  // Set the state of cut and paste
  cutAction_              ->setEnabled( isEnabled );
  pasteAction_            ->setEnabled( isEnabled );

  // Update the winks and handwriting buttons
  if( isEnabled )
  {
    updateModeButtons();
  }
  else
  {
    inkButton_  ->setEnabled( false );
    winksButton_->setEnabled( false );
  }
  // The window is being enabled back, replace some stuff which changes
  // on every connection
  if( isEnabled )
  {
    // Re-read the current account pointer
    // because it likely is a different one
    currentAccount_ = CurrentAccount::instance();
  }
}



// Enable or disable the parts of the window which allow user interaction
void ChatWindow::setUIState( UIState state )
{
  uiState_ = state;

  switch( state )
  {
    case Disconnecting:
    case Disconnected:
      statusLabel_->setButtonEnabled( ChatStatusBar::Disconnected, true );
      setEnabled( false );
      break;

    case Connecting:
      statusLabel_->setButtonEnabled( ChatStatusBar::Disconnected, false );
      setEnabled( false );
      break;

    case Connected:
      setEnabled( true );
      break;

    default:
      break;
  }
}



// Change the caption of the chat window
void ChatWindow::setWindowTitle( const QString &caption )
{
  // Remember the window caption when it is fetched from the current chat
  if( caption.isEmpty() )
  {
    Chat *currentChat = getCurrentChat();
    if( currentChat != 0 )
    {
      // Avoid newlines which would cause problems to the UI layout
      caption_ = currentChat->getCaption();
    }
    else
    {
      caption_ = QString();
    }
  }

  // Always append the "Chat" prefix to the chat
  if( caption.isEmpty() && caption_.isEmpty() )
  {
    setCaption( i18nc( "Chat window caption, without contact name", "Chat" ) );
  }
  else
  {
    setCaption( i18nc( "Chat window caption, with contact name", "%1 - Chat", ( caption.isEmpty() ? caption_ : caption ) ) );
  }
}



// "Show menu bar" was toggled.
void ChatWindow::showMenuBar()
{
  // Just show the menubar if it was hidden
  if( ! menuBar()->isVisible() )
  {
    menuBar()->setVisible( true );
    return;
  }

  // Ask the user if he/she really wants to hide the menubar, to avoid mistakes
  // TODO: If the user disables the shortcut for the menu action, no shortcuts will be shown
  int res = KMessageBox::questionYesNo( this,
                                        i18nc( "Question dialog box message",
                                                "<html>Are you sure you want to hide the menu bar? "
                                                "You will be able to show it again by using this "
                                                "keyboard shortcut: <b>%1</b></html>",
                                                showMenuBar_->shortcut().primary().toString( QKeySequence::NativeText ) ),
                                        i18nc( "Dialog box caption: hiding the menu bar", "Hiding the Menu" ),
                                        KStandardGuiItem::yes(),
                                        KStandardGuiItem::no(),
                                        "hideMenuBarQuestion" );

  if( res == KMessageBox::Yes )
  {
    menuBar()->setVisible( showMenuBar_->isChecked() );
  }
  else
  {
    showMenuBar_->setChecked( true );
  }
}



// Show a status message in the statusbar
void ChatWindow::showStatusMessage( ChatStatusBar::MessageType type, QString message )
{
#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "Showing status message" << message << "(" << type << ")";
#endif
  statusLabel_->setMessage( type, message );
}



// Show a status message for some seconds in the status dialog
void ChatWindow::showStatusMessage( QString message, int duration )
{
  // Avoid newlines which cause problems to the UI layout.
  message = message.replace( "\n", " " );

#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "Showing status bar text" << message << "with duration" << duration;
#endif

  // HACK: Empty strings are not rendered, and any previously set text is not removed.
  // So we change empty strings to contain a single space, which is enough to clean the label up.
  if( message.isEmpty() )
  {
    statusLabel_->setMessage( ChatStatusBar::DefaultType, " " );
  }
  else
  {
    statusLabel_->setMessage( ChatStatusBar::ContactTyping, message );
  }

  // Each message will have an expiration time of its own or none at all
  statusTimer_.stop();

  // Non-expiring messages do not trigger the timer
  if( duration <= 0 || message.isEmpty() )
  {
    return;
  }

#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "Setting status bar update timer to" << duration << "sec.";
#endif
  statusTimer_.start( duration * 1000 );
}




// Change color for current pen
void ChatWindow::slotChangeInkColor()
{
  QColor color;
  if ( KColorDialog::getColor( color ) == QDialog::Accepted )
  {
    inkCanvas_->setPenColor( color );
  }
}



// Erase brush was selected
void ChatWindow::slotChangeInkBrush()
{
  if( inkCanvas_->penType() == Isf::InkCanvas::DrawingPen )
  {
    inkCanvas_->setPenType( Isf::InkCanvas::EraserPen );
    inkEraseButton_->setIcon( KIcon( "draw-freehand" ) );
    inkEraseButton_->setToolTip( i18n( "Drawing brush" ) );
  }
  else
  {
    inkCanvas_->setPenType( Isf::InkCanvas::DrawingPen );
    inkEraseButton_->setIcon( KIcon( "draw-eraser" ) );
    inkEraseButton_->setToolTip( i18n( "Erase brush" ) );
  }
}



// Make the caption blink if the window still doesn't have focus
void ChatWindow::slotBlinkCaption()
{
  // Make the caption alternate case
  if ( blinkToUpper_ )
  {
    const QString caption( QTextDocumentFragment::fromHtml( caption_.toUpper() ).toPlainText() );
    setWindowTitle( QString( "* " + caption ) );
  }
  else
  {
    setWindowTitle( QString( "* " + caption_.toLower() ) );
  }

  blinkToUpper_ = ! blinkToUpper_;
}



// Called when an emoticon dock is toggled
void ChatWindow::slotEmoticonDocksToggled()
{
  bool showStandardButton = ( ! standardEmoticonsDock_->toggleViewAction()->isChecked() );
  bool showCustomButton   = ( ! customEmoticonsDock_  ->toggleViewAction()->isChecked() );

  // If an emoticon widget is shown and the button needs to be hidden,
  // switch away from it
  if( ( ! showStandardButton && editorChooser_->currentWidget() == stdEmoPage_ )
  ||  ( ! showCustomButton   && editorChooser_->currentWidget() == cusEmoPage_ ) )
  {
    slotSwitchEditor();
  }

  standardEmoticonButton_->setVisible( showStandardButton );
  customEmoticonButton_  ->setVisible( showCustomButton   );
}



// A message has been received
void ChatWindow::slotGotChatMessage( const ChatMessage &message, Chat *chat )
{
  Q_UNUSED( message );

  // Display an highlighted tab title
  if( chat != getCurrentChat() )
  {
    // Paint the text red
    chatTabs_->setTabTextColor( chatTabs_->indexOf( chat ), QColor( "red" ) );

    // Highlight the window title with the chat with new messages
    caption_ = chat->getCaption();
  }

  // If the window does not have focus, blink the caption as visual cue of an event
  if( isActiveWindow() )
  {
    return;
  }

  // Start blinking the caption
  blinkTimer_.start( 1500 );
}



// A nudge has been received: shake the window.
void ChatWindow::slotGotNudge()
{
  if( ! currentAccount_->getShakeNudge() )
  {
    return;
  }


  // avoid shaking again in a short period.
  if( lastShake_.secsTo( QTime::currentTime() ) < 15 )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Not shaking the window until"
             << lastShake_.secsTo( QTime::currentTime() ) << "more seconds.";
#endif
    return;
  }
  lastShake_ = QTime::currentTime();

  QTime t;
  int xp = x();
  int yp = y();

  t.start();

  // Code example from: http://ariya.blogspot.com/2005/12/buzz-or-shake-my-window.html

  // Shake the window.
  for ( int i = 16; i > 0; )
  {
    kapp->processEvents();

    if ( t.elapsed() >= 50 )
    {
      int delta = i >> 2;
      int dir   = i & 3;
      int dx    = (dir == 1 || dir == 2) ? delta : -delta;
      int dy    = (dir < 2) ? delta : -delta;

      move( xp+dx, yp+dy );
      t.restart();
      i--;
    }
  }

  // Move to start again
  move( xp, yp );
}



// Update the contact typing messages in the status bar
void ChatWindow::slotGotTypingMessage( Chat *chat )
{
  // Get the active chat if called with no or invalid parameters
  if( chat == 0 )
  {
    chat = getCurrentChat();
#ifdef KMESSDEBUG_CHATWINDOW_TYPING_MESSAGES
    kmDebug() << "Using current chat" << chat;
#endif
  }
#ifdef KMESSDEBUG_CHATWINDOW_TYPING_MESSAGES
  else
  {
    kmDebug() << "Using chat:" << chat;
  }
#endif

  const QPalette palette;
  int tabIndex = chatTabs_->indexOf( chat );
  const QStringList typingContacts( chat->getTypingContacts() );

#ifdef KMESSDEBUG_CHATWINDOW_TYPING_MESSAGES
  kmDebug() << "These contacts are typing:" << typingContacts;
#endif

  // Update the color of this chat's tab
  // If the color is red, leave it
  if( chatTabs_->tabTextColor( tabIndex ) != QColor( "red" ) )
  {
    QColor color;
    if( typingContacts.isEmpty() )
    {
      color = palette.color( QPalette::WindowText );
    }
    else
    {
      color = palette.color( QPalette::Highlight );
    }

    chatTabs_->setTabTextColor( tabIndex, color  );
  }

  // If the typing message was not intended for the currently active tab, skip
  // the status bar processing altogether
  if( chat != getCurrentChat() )
  {
#ifdef KMESSDEBUG_CHATWINDOW_TYPING_MESSAGES
    kmDebug() << "Typing message was not for the active chat, skipping status bar update.";
#endif
    return;
  }

  // If no one is typing anymore, clean up the status bar
  if( typingContacts.isEmpty() )
  {
#ifdef KMESSDEBUG_CHATWINDOW_TYPING_MESSAGES
    kmDebug() << "No contacts are typing anymore, cleaning up the status bar.";
#endif
    showStatusMessage();
    return;
  }

  QString statusString;

  // Decide what string will be used depending on how many contacts are typing
  if( typingContacts.count() == 1 )
  {
    const QString& first = typingContacts.first();
    statusString = i18n( "%1 is typing...", first );
  }
  else
  {
    int     count  = typingContacts.count() - 2;
    const QString& first  = typingContacts[0];
    const QString& second = typingContacts[1];

    if( count == 0 )
    {
      statusString = i18n( "%1 and %2 are typing...", first, second );
    }
    else
    {
      statusString = i18n( "%1, %2 and %3 others are typing...", first, second, count );
    }
  }

  // Set the message to the status bar: the chat will send another signal when the current typing message expires
  showStatusMessage(statusString, CHAT_TYPING_EXPIRATION_TIME );
}



// Invite button was pressed
void ChatWindow::slotInviteContact()
{
  // Show invite dialog
  QStringList usersToInvite;
  InviteDialog inviteDialog( getCurrentChat()->getParticipants(), usersToInvite );

  // If the user press OK and the users to invite list isn't empty then invite contacts
  if( ! usersToInvite.isEmpty() )
  {
    getCurrentChat()->inviteContacts( usersToInvite );
  }
}



// The message text changed, the user is typing or drawing
void ChatWindow::slotMessageChanged()
{
  // The window is closing, no chats are active
  if( chatTabs_->count() == 0 )
  {
    return;
  }

  bool disable = true;

  // The message was changed
  if( ! inkButton_->isChecked() )
  {
    disable = ( messageEdit_->document()->isEmpty() );
  }
  // The ink drawing was changed
  else
  {
    disable = ( inkCanvas_->isEmpty() );
  }

  // Something has been typed, enable the button so the user can send the message
  sendButton_->setDisabled( disable );


  // There's some text, but the user doesn't want us to send typing notifications to the contacts
  if( ! doSendTypingMessages_ )
  {
    return;
  }

  // If the typing timer is already going, do nothing
  if( userTypingTimer_.isActive() )
  {
    return;
  }

  if( ! inkButton_->isChecked() )
  {
    // If the last character of the message is a newline, sending the typing signal would
    // cause the actual text message to be received twice, so check the last character
    if( messageEdit_->toPlainText().right(1) == "\n" )
    {
      return;
    }

    // If a command is being typed, don't send a typing message
    if( messageEdit_->toPlainText().left( 1 ) == "/" and messageEdit_->toPlainText().left( 2 ) != "//" )
    {
      return;
    }
  }

  // TODO Create a method to send an "user is drawing" message. WLM is able to do that.
  getCurrentChat()->sendTypingMessage();

  userTypingTimer_.start();
}



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



// The user pressed return in the message editor, so send the message
void ChatWindow::slotSendButtonClicked()
{
  if( inkButton_->isChecked() )
  {
    sendInk();
    inkCanvas_->setFocus();
  }
  else if( winksButton_->isChecked() )
  {
    const MsnObject &msnObject = winksWidget_->getMsnObjectWinkSelected();
    if( msnObject.isValid() )
    {
      getCurrentChat()->sendWink( msnObject );
    }
  }
  else
  {
    sendMessage();
    messageEdit_->setFocus();
  }
}




// Show the chat history for a contact
void ChatWindow::slotShowChatHistory()
{
  const KAction* action = qobject_cast< const KAction* >( sender() );

  if( ! action )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmWarning() << "Called directly, action is not valid!";
#endif
    return;
  }

  // The click came from the menu item itself, not from a sub item.
  // Select the first contact of the list
  if( action == historyMenuAction_ )
  {
    if( historyMenuAction_->children().isEmpty() )
    {
#ifdef KMESSDEBUG_CHATWINDOW
      kmWarning() << "No contacts available in the menu!";
#endif
      return;
    }

    action = qobject_cast< const KAction* >( historyMenuAction_->children().first() );
    KMESS_ASSERT( action );
  }

  const QString handle( action->data().toString() );

  if( handle.isEmpty() )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmWarning() << "Action did not contain a valid handle! (contained a" << action->data().typeName() << " instead)";
#endif
    return;
  }

  ChatHistoryDialog *dialog = new ChatHistoryDialog( this );
  dialog->setInitialContact( currentAccount_->getHandle(), handle );
  dialog->show();
}



// Switch to the message editor, or another one
void ChatWindow::slotSwitchEditor( QObject* widget )
{
  // If a specific widget was specified, use it instead
  const QObject *caller = widget ? widget : sender();
  bool disableSendButton = true;
  bool disableNewLineButton = true;

#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "Switching editors: caller:" << caller;
#endif

  // Some widgets are pretty high, hide them unless they're needed
  standardEmoticonsWidget_->hide();
  customEmoticonsWidget_->hide();
  winksWidget_->hide();
  inkCanvas_->hide();

  // Show the text control frame by default
  inkCtlFrame_->setVisible( false );
  textCtlFrame_->setVisible( true );

  // Called by the winks button, show the available winks
  if( caller == winksButton_ && winksButton_->isChecked() )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Switching to winks list.";
#endif
    winksWidget_->refresh();
    winksWidget_->show();

    editorChooser_->setCurrentWidget( winksPage_ );

    winksButton_->setChecked( true );
    inkButton_->setChecked( false );
    standardEmoticonButton_->setChecked( false );
    customEmoticonButton_->setChecked( false );
    textButton_->setChecked( false );

    disableSendButton = false;
  }
  // Called by the ink button, switch to the ink editor if it has been checked
  else if( caller == inkButton_ && inkButton_->isChecked() )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Switching to ink editor.";
#endif
    inkCanvas_->show();

    editorChooser_->setCurrentWidget( inkPage_ );
    winksButton_->setChecked( false );
    inkButton_->setChecked( true );
    standardEmoticonButton_->setChecked( false );
    customEmoticonButton_->setChecked( false );
    textButton_->setChecked( false );

    inkCtlFrame_->setVisible( true );
    textCtlFrame_->setVisible( false );

    disableSendButton = ( inkCanvas_->isEmpty() );
  }
  // Called by the standard emoticon button
  else if( caller == standardEmoticonButton_
       &&  standardEmoticonButton_->isChecked() )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Switching to standard emoticons widget.";
#endif
    standardEmoticonsWidget_->show();

    editorChooser_->setCurrentWidget( stdEmoPage_ );

    winksButton_->setChecked( false );
    inkButton_->setChecked( false );
    standardEmoticonButton_->setChecked( true );
    customEmoticonButton_->setChecked( false );
    textButton_->setChecked( false );
  }
  // Called by the custom emoticon button
  else if( caller == customEmoticonButton_
       &&  customEmoticonButton_->isChecked() )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Switching to custom emoticons widget.";
#endif
    customEmoticonsWidget_->show();

    editorChooser_->setCurrentWidget( cusEmoPage_ );

    winksButton_->setChecked( false );
    inkButton_->setChecked( false );
    standardEmoticonButton_->setChecked( false );
    customEmoticonButton_->setChecked( true );
    textButton_->setChecked( false );
  }
  else  // Otherwise, show the message editor
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Switching to the message editor.";
#endif
    editorChooser_->setCurrentWidget( textPage_ );
    winksButton_->setChecked( false );
    inkButton_->setChecked( false );
    standardEmoticonButton_->setChecked( false );
    customEmoticonButton_->setChecked( false );
    textButton_->setChecked( true );

    disableNewLineButton = false;
    disableSendButton = ( messageEdit_->document()->isEmpty() );
  }

  // Update the send button status
  sendButton_->setDisabled( disableSendButton );
  newLineButton_->setDisabled( disableNewLineButton );

  // Also scroll down the chat, in all cases
  getCurrentChat()->scrollToBottom( false /* no forced scrolling */ );
}



// Switch the window to the tab at the left of the current one
void ChatWindow::slotSwitchToLeftTab()
{
  // Find out at what tab are we and how many are there
  int current = chatTabs_->currentIndex();
  int count = chatTabs_->count();

  // Do nothing if there's only one tab
  if( count <= 1 )
  {
    return;
  }

  // If the first tab was selected, then restart from the last tab
  if( current == 0 )
  {
    chatTabs_->setCurrentIndex( count - 1 );
    return;
  }

  chatTabs_->setCurrentIndex( current - 1 );
}



// Switch the window to the tab at the right of the current one
void ChatWindow::slotSwitchToRightTab()
{
  // Find out at what tab are we and how many are there
  int current = chatTabs_->currentIndex();
  int count = chatTabs_->count();

  // Do nothing if there's only one tab
  if( count <= 1 )
  {
    return;
  }

  // If the last tab was selected, then restart from the first tab
  if( current >= ( count - 1 ) )
  {
    chatTabs_->setCurrentIndex( 0 );
    return;
  }

  chatTabs_->setCurrentIndex( current + 1 );
}



// The active tab was changed
void ChatWindow::slotTabChanged( int currentIndex )
{
  // Avoid switching to uninitialized widgets
  if( ! initialized_ )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Not switching tab while not initialized.";
#endif
    return;
  }

  if( currentIndex < 0 || currentIndex >= chatTabs_->count() )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    if( currentIndex == -1 )
    {
      kmDebug() << "The window is closing.";
    }
    else
    {
      kmDebug() << "Chat tab index" << currentIndex << "doesn't exist in the tabs list!";
    }
#endif

    messageEdit_->setDocument( new QTextDocument( messageEdit_ ) );
    updateModeButtons();
    updateEditorFont();
    return;
  }

  Chat *chat = getChat( currentIndex );

  if( chat == 0 )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Chat tab" << currentIndex << "was destroyed";
#endif
    return;
  }
#ifdef KMESSDEBUG_CHATWINDOW
  else
  {
    kmDebug() << "Activating chat tab" << currentIndex;
  }
#endif

  // update the icon for this window appropriately.
  setChatWindowIcon();

  // Reset the color
  const QPalette palette;
  chatTabs_->setTabTextColor( currentIndex, palette.color( QPalette::WindowText ) );

  // Display the new chat contents in the UI. Block the signals to avoid the editors
  // from firing the "message changed" signal, which sends a typing message.
  messageEdit_ ->blockSignals( true );
  inkCanvas_   ->blockSignals( true );
  contactsDock_->setWidget( chat->getContactsWidget() );
  messageEdit_ ->setDocument( chat->getMessageEditContents() );
  inkCanvas_   ->setDrawing( chat->getInkDrawing() );
  messageEdit_ ->blockSignals( false );
  inkCanvas_   ->blockSignals( false );

  // Reset quick re-type index
  indexSentences_ = chat->getQuickRetypeList().size();

  // Set the font style/color
  updateEditorFont();

  // Update the window UI with this chat's details
  slotSwitchEditor();
  setWindowTitle();
  updateModeButtons();
  slotGotTypingMessage( chat );

  QAction* action;
  qDeleteAll( historyMenuAction_->children() );

  const QStringList participantsList( chat->getParticipants() );
  const QStringList contactsWithHistory( ChatHistoryManager::contactsList() );
  foreach( const QString &handle, participantsList )
  {
    const ContactBase *contact = currentAccount_->getContactByHandle( handle );
    if( contact == 0 )
    {
      contact = currentAccount_->addInvitedContact( handle );
    }

    action = new KAction( KIcon( MsnStatus::getIconName( contact->getStatus() ) ),
                          contact->getFriendlyName( STRING_CLEANED ),
                          historyMenuAction_ );
    action->setData( handle );

    // Only connect the items whose contact has recorded history
    if( contactsWithHistory.contains( handle ) )
    {
      connect( action, SIGNAL(           triggered(bool) ),
               this,   SLOT  ( slotShowChatHistory()     ) );
    }
    else
    {
      action->setEnabled( false );
    }

    historyMenuAction_->addAction( action );
  }

  // Re-enable the spell checker for this tab.
  // FIXME KDE4.x bug this needs to be unset then set
  messageEdit_->setCheckSpellingEnabled( ! spellCheckAction_->isChecked() );
  messageEdit_->setCheckSpellingEnabled( spellCheckAction_->isChecked() );
}



// Remove a chat
bool ChatWindow::slotTabRemoved( Chat *chat )
{
  // When the function is called the pointer for chat is already checked by
  // queryClose() or slotTabRemoved function, so grep the index of it and remove
  // it from tab widget
  int closedTabIndex = chatTabs_->indexOf( chat );

#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "Removing chat with index" << closedTabIndex;
#endif

  // Remove the tab of the closed chat
  chatTabs_->removeTab( closedTabIndex );

  // Request query close to chat ( so we can close correctly the SB and save the log )
  chat->queryClose();

#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "Removed tab index" << closedTabIndex;
#endif

  // Free the memory
  delete chat;

  // If there are no more open chats, close the window too
  checkAndCloseWindow();

  return true;
}



// Delete an existing chat tab by widget
bool ChatWindow::slotTabRemoved( QWidget *chatWidget )
{
#ifdef KMESSTEST
  KMESS_ASSERT( chatTabs_->count() > 0 );
#endif

#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "Forwarding removal command for a chat widget.";
  kmDebug() << "Currently open chats:" << chatTabs_->count();
#endif

  // Cast the Widget to Chat pointer
  Chat *chat = qobject_cast<Chat*>( chatWidget );

  if( chat == 0 )
  {
    kmWarning() << "Unable to find chat for widget:" << chatWidget;
    return false;
  }

  // Relay the deletion to the main method
  return slotTabRemoved( chat );
}



// Update a chat's tab title when the participants names change
void ChatWindow::slotUpdateChatInfo( Chat *chat )
{
  // If called without a parameter, we've been called by a chat's signal
  if( chat == 0 )
  {
    // Find the chat which has emitted the signal
    chat = qobject_cast<Chat*>( sender() );
  }

  // Search it into the list
  int index = chatTabs_->indexOf( chat );
  if( index == -1 )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Cannot find source chat!";
#endif
    return;
  }

  const QStringList participantsList( chat->getParticipants() );

  if ( chat == getCurrentChat() )
  {
    // update the icon for this window appropriately.
    setChatWindowIcon();
  }

  // getCaption() should return a properly html-escaped string
  QString chatCaption( chat->getCaption() );

  // Change the tab caption
  chatTabs_->setTabText( index, chatCaption.replace( '&', "&&" ) );
  chatTabs_->setTabIcon( index, chat->getParticipantsTabIcon() );

  // Create a tool tip with details on the chat
  QStringList participants;
  foreach( const QString &handle, participantsList )
  {
    const ContactBase *contact = currentAccount_->getContactByHandle( handle );
    if( contact == 0 )
    {
      contact = currentAccount_->addInvitedContact( handle );
    }

    participants += QString( "%1 (%2)<br /><i>%3</i>" )
                           .arg( contact->getFriendlyName( STRING_CLEANED ) )
                           .arg( MsnStatus::getName( contact->getStatus() ) )
                           .arg( handle );
  }

  QString toolTip( i18nc( "Tool tip for chat tabs",
                          "<html><h2>Chat Info</h2>"
                          "<dl>"
                          "<dt><b>Contacts:</b></dt><dd><ul><li>%1</li></ul></dd>"
                          "<dt><b>Chat started on:</b></dt><dd>%2</dd>"
                          "<dt><b>Connected with account:</b></dt><dd>%3</dd>"
                          "</dl></html>",
                          participants.join( "</li><li>" ),
                          chat->getStartTime().toString( Qt::DefaultLocaleLongDate ),
                          currentAccount_->getHandle() ) );

  chatTabs_->setTabToolTip( index, toolTip );

  // If it is the current one, the UI must be updated
  if ( chat == getCurrentChat() )
  {
#ifdef KMESSDEBUG_CHATWINDOW
    kmDebug() << "Source chat is the current one, updating UI.";
#endif

    // Update the window title
    setWindowTitle();
    // Update the status of the writing mode buttons (winks, handwriting,...)
    // This is needed in case the contact has changed status
    updateModeButtons();
  }
}



// Start a file transfer
void ChatWindow::startFileTransfer()
{
  getCurrentChat()->startFileTransfer();
}



// Called when the "use emoticons" action is called.
void ChatWindow::toggleEmoticons( bool useEmoticons )
{
  // Set the new emoticon use value to the account
  currentAccount_->setUseEmoticons( useEmoticons );
}



// Called when the "use emoticons" action is called.
void ChatWindow::toggleSessionInfo( bool showSessionMessages )
{
  // Set the new emoticon use value to the account
  currentAccount_->setShowSessionInfo( showSessionMessages );
}



// Called when the "use spell checking" action is called.
void ChatWindow::toggleSpellCheck( bool useSpellCheck )
{
#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "Spell checking is now" << useSpellCheck;
#endif

  // Sync the action with a manually set flag (ie in readProperties())
  if( spellCheckAction_->isChecked() != useSpellCheck )
  {
    spellCheckAction_->setChecked( useSpellCheck );
  }

  // Change the spell checking feature status
  // FIXME KDE4.x bug this needs to be unset then set
  messageEdit_->setCheckSpellingEnabled( ! spellCheckAction_->isChecked() );
  messageEdit_->setCheckSpellingEnabled( spellCheckAction_->isChecked() );
}



// Update the status of the tiny writing mode buttons
void ChatWindow::updateModeButtons()
{
  // don't do anything when the window
  // chat was just closed or user is not connected
  if( getCurrentChat() == NULL || uiState_ != Connected)
  {
    return;
  }

  bool enableInkButton   = true;
  bool enableWinksButton = true;

#if KMESS_ENABLE_INK == 0
  enableInkButton = false;
#endif

  const QStringList participants( getCurrentChat()->getParticipants() );
  foreach( const QString &handle, participants )
  {
    const ContactBase *contact = currentAccount_->getContactByHandle( handle );
    if( contact == 0 )
    {
      continue;
    }

#if KMESS_ENABLE_INK == 1
    if( enableInkButton )
    {
      // Disable the Ink button if the contact doesn't support Ink
      if( ! contact->hasCapability( ContactBase::MSN_CAP_INK_GIF )
      &&  ! contact->hasCapability( ContactBase::MSN_CAP_INK_ISF ) )
      {
        enableInkButton = false;
      }
    }
#endif

    // Disable the Winks button if the contact doesn't support Winks
    if( enableWinksButton
    &&  ! contact->hasCapability( (ContactBase::MsnClientCapabilities) ( ContactBase::MSN_CAP_WINKS | ContactBase::MSN_CAP_MULTI_PACKET ) ) )
    {
      enableWinksButton = false;
    }
  }

#ifdef KMESSDEBUG_CHATWINDOW
  kmDebug() << "Winks/Ink button disabling check: all contacts support winks?" << enableWinksButton
           << "and ink?" << enableInkButton;
#endif

  inkButton_  ->setEnabled( enableInkButton   );
  winksButton_->setEnabled( enableWinksButton );

  // Set a tooltip or a status bar message to either buttons.
  // The tooltip covers the status message otherwise.

#if KMESS_ENABLE_INK == 1 // else, the ink button isn't shown anyway
  if( enableInkButton )
  {
    inkButton_->setToolTip  ( i18n( "Handwriting mode" ) );
    inkButton_->setStatusTip( QString() );
  }
  else
  {
    inkButton_->setToolTip  ( QString() );
    if( getCurrentChat()->getParticipants().count() > 1 )
    {
      inkButton_->setStatusTip( i18nc( "Label text",
                                       "Handwriting is disabled: some of the contacts do not "
                                       "support receiving handwritten messages." ) );
    }
    else
    {
      inkButton_->setStatusTip( i18nc( "Label text",
                                       "Handwriting is disabled: this contact does not "
                                       "support receiving handwritten messages." ) );
    }
  }
#endif

  if( enableWinksButton )
  {
    winksButton_->setToolTip  ( i18n( "Winks" ) );
    winksButton_->setStatusTip( QString() );
  }
  else
  {
    winksButton_->setToolTip  ( QString() );
    if( getCurrentChat()->getParticipants().count() > 1 )
    {
      winksButton_->setStatusTip( i18nc( "Label text",
                                       "Winks are disabled: some of the contacts do not "
                                       "support receiving winks." ) );
    }
    else
    {
      winksButton_->setStatusTip( i18nc( "Label text",
                                       "Winks are disabled: this contact does not "
                                       "support receiving winks." ) );
    }
  }
}



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

  // Set the font settings the user has chosen
  messageEdit_->setTextColor( QColor( currentAccount_->getFontColor() ) );
  messageEdit_->setFont( currentAccount_->getFont() );
  messageEdit_->setCurrentFont( currentAccount_->getFont() );
  messageEdit_->setFontPointSize( currentAccount_->getFont().pointSize() );
}



// Clean up the view of the current chat tab
void ChatWindow::viewClearChat()
{
  getCurrentChat()->slotClearChat();
}



// enlarge the font size
void ChatWindow::viewZoomIn()
{
  changeZoomFactor( true );
}



// decrease the font size
void ChatWindow::viewZoomOut()
{
  changeZoomFactor( false );
}



#include "chatwindow.moc"
