Files
kmess/src/chat/chatwindow.cpp
T
2026-07-06 11:48:10 +02:00

3018 lines
92 KiB
C++

/***************************************************************************
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 <QDragEnterEvent>
#include <QDropEvent>
#include <QKeyEvent>
#include <QLocale>
#include <QMimeData>
#include <QMouseEvent>
#include <QRegularExpression>
#include <QShortcut>
#include <QTabBar>
#include <QTabWidget>
#include <QTextDocumentFragment>
#include <QAction>
#include <KActionMenu>
#include <KConfigGroup>
#include <KHelpMenu>
#include <KLocalizedString>
#include <QMenu>
#include <QMenuBar>
#include <KMessageBox>
#include <KStandardAction>
#include <KStandardGuiItem>
#include <QStatusBar>
#include <KToolBar>
#include <kwindowsystem.h>
#include <KStandardShortcut>
#include <KIconLoader>
#include <QColorDialog>
#include <QFontDialog>
#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( QIcon::fromTheme( "face-smile" ) );
customEmoticonButton_ ->setIcon( QIcon::fromTheme( "face-smile-gearhead-female" ) );
inkButton_ ->setIcon( QIcon::fromTheme( "draw-brush" ) );
winksButton_ ->setIcon( QIcon::fromTheme( "applications-toys" ) );
textButton_ ->setIcon( QIcon::fromTheme( "draw-text" ) );
fontButton_ ->setIcon( QIcon::fromTheme( "preferences-desktop-font" ) );
fontColorButton_ ->setIcon( QIcon::fromTheme( "format-stroke-color" ) );
// Set up the buttons for setting ink colour/stroke size/etc.
inkCanvas_->setPenSize( inkPenSize_->value() );
inkColorButton_->setIcon( QIcon::fromTheme( "format-stroke-color" ) );
inkEraseButton_->setIcon( QIcon::fromTheme( "draw-eraser" ) );
inkClearButton_->setIcon( QIcon::fromTheme( "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
chatTabs_->tabBar()->installEventFilter( this );
connect( chatTabs_, &QTabWidget::tabCloseRequested, this, [this]( int index )
{
slotTabRemoved( chatTabs_->widget( index ) );
} );
connect( chatTabs_, SIGNAL( currentChanged(int) ),
this, SLOT ( slotTabChanged(int) ) );
chatTabs_->setTabsClosable( true );
// 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_->tabBar()->setVisible( 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
&& ! static_cast<KMessApplication *>(QApplication::instance())->sessionSaving() ) // Do not ask if the KDE session is closing
{
int res = KMessageBox::questionTwoActionsCancel( 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::PrimaryAction : break;
case KMessageBox::SecondaryAction : 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()
{
QAction *closeAction, *saveAction, *clearChatAction;
QAction *contactsDockAction, *standardEmoticonsDockAction, *customEmoticonsDockAction;
// Create the actions for "Chat" menu
inviteButton_ = new QAction ( QIcon::fromTheme( "list-add-user" ), i18n("&Invite..."), this );
sendAction_ = new QAction ( QIcon::fromTheme( "folder-remote" ), i18n("Send a &File..."), this );
nudgeAction_ = new QAction ( QIcon::fromTheme( "preferences-desktop-notification-bell" ), i18n("Send a &Nudge!"), this );
historyMenuAction_ = new KActionMenu ( QIcon::fromTheme( "chronometer" ), i18n("Chat &History..."), this );
saveAction = new QAction ( QIcon::fromTheme( "document-save" ), i18n("Save Chat..."), this );
closeAllAction_ = new QAction ( QIcon::fromTheme( "dialog-close" ), i18n("Close &All Tabs"), this );
closeAction = KStandardAction::close ( this, SLOT( closeWidgetOrTab() ) , actionCollection_ );
KStandardAction::quit ( QApplication::instance(), SLOT( quit() ), actionCollection_ );
// Create the actions for "Edit" menu
changeFontAction_ = new QAction ( QIcon::fromTheme( "preferences-desktop-font" ), i18n("Change &Font"), this );
changeFontColorAction_ = new QAction ( QIcon::fromTheme( "format-stroke-color" ), i18n("Change Font &Color"), this );
cutAction_ = KStandardAction::cut ( messageEdit_, SLOT( cut() ), actionCollection_ );
KStandardAction::copy ( messageEdit_, SLOT( copy() ), actionCollection_ );
pasteAction_ = KStandardAction::paste ( messageEdit_, SLOT( paste() ), actionCollection_ );
KStandardAction::find ( this, SLOT( editFind() ), actionCollection_ );
// Create the actions for "View" menu
emoticonAction_ = new KToggleAction( QIcon::fromTheme( "face-smile-big" ), i18n("Show &Emoticons"), this );
sessionInfoAction_ = new KToggleAction( QIcon::fromTheme( "flag-green" ), i18n("Show S&tatus Messages"), this );
KStandardAction::zoomIn ( this, SLOT( viewZoomIn() ), actionCollection_ );
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 ( QIcon::fromTheme( "view-choose" ), i18n("&Panels"), this );
// Create the actions for "Settings" menu
spellCheckAction_ = new KToggleAction( QIcon::fromTheme( "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( QIcon::fromTheme( "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( QIcon::fromTheme( "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( QIcon::fromTheme( "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() );
bool accepted = false;
selection = QFontDialog::getFont( &accepted, selection, this );
if( ! 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
color = QColorDialog::getColor( color, this );
if ( color.isValid() )
{
// 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( obj == chatTabs_->tabBar() && event->type() == QEvent::MouseButtonRelease )
{
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>( event );
if( mouseEvent->button() == Qt::MiddleButton )
{
const int tabIndex = chatTabs_->tabBar()->tabAt( mouseEvent->position().toPoint() );
if( tabIndex >= 0 )
{
return slotTabRemoved( chatTabs_->widget( tabIndex ) );
}
}
}
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() ).toLatin1() );
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( " ", Qt::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 *>(QApplication::instance());
KMess *kmess = kmessApp->getContactListWindow();
bool isStatusCommand = ( command == "status" );
// Check syntax errors
if( isStatusCommand && arguments.isEmpty() )
{
KMessageBox::error( 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::error( 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::error( 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::error( 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
setAutoSaveSettings( KMessConfig::instance()->getAccountConfig( currentAccount_->getHandle(), "ChatWindow" ),
true /* save WindowSize */ );
// 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( KMessConfig::instance()->getAccountConfig( currentAccount_->getHandle(), "ChatWindow" ) );
readProperties( config );
}
// 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.
restoreGeometry( group.readEntry( "WindowGeometry", QByteArray() ) );
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( KMessConfig::instance()->getAccountConfig( currentAccount_->getHandle(), "ChatWindow" ) );
saveProperties( config );
}
// 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
group.writeEntry( "WindowGeometry", saveGeometry() );
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( QRegularExpression( "\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 = QIcon::fromTheme( "user-identity" ).pixmap( 60, 60 );
}
else
{
userIcon = QIcon::fromTheme( "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::questionTwoActions( 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().toString( QKeySequence::NativeText ) ),
i18nc( "Dialog box caption: hiding the menu bar", "Hiding the Menu" ),
KGuiItem( i18n( "Yes" ) ),
KGuiItem( i18n( "No" ) ),
"hideMenuBarQuestion" );
if( res == KMessageBox::PrimaryAction )
{
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;
color = QColorDialog::getColor( color, this );
if ( color.isValid() )
{
inkCanvas_->setPenColor( color );
}
}
// Erase brush was selected
void ChatWindow::slotChangeInkBrush()
{
if( inkCanvas_->penType() == Isf::InkCanvas::DrawingPen )
{
inkCanvas_->setPenType( Isf::InkCanvas::EraserPen );
inkEraseButton_->setIcon( QIcon::fromTheme( "draw-freehand" ) );
inkEraseButton_->setToolTip( i18n( "Drawing brush" ) );
}
else
{
inkCanvas_->setPenType( Isf::InkCanvas::DrawingPen );
inkEraseButton_->setIcon( QIcon::fromTheme( "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_->tabBar()->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_.isValid() && lastShake_.elapsed() < 15000 )
{
#ifdef KMESSDEBUG_CHATWINDOW
kmDebug() << "Not shaking the window until"
<< ( 15000 - lastShake_.elapsed() ) / 1000 << "more seconds.";
#endif
return;
}
lastShake_.restart();
QElapsedTimer 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; )
{
QApplication::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_->tabBar()->tabTextColor( tabIndex ) != QColor( "red" ) )
{
QColor color;
if( typingContacts.isEmpty() )
{
color = palette.color( QPalette::WindowText );
}
else
{
color = palette.color( QPalette::Highlight );
}
chatTabs_->tabBar()->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 ) == "/" && 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 QAction* action = qobject_cast< const QAction* >( 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 QAction* >( 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_->tabBar()->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 QAction( QIcon::fromTheme( 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>" ),
QLocale().toString( chat->getStartTime(), QLocale::LongFormat ),
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 );
}