/*************************************************************************** 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 * */ 01101 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"