/*************************************************************************** contactframe.cpp - description ------------------- begin : Thu Jan 16 2003 copyright : (C) 2003 by Mike K. Bennett email : mkb137b@hotmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "contactframe.h" #include "../contact/contact.h" #include "../contact/msnstatus.h" #include "../dialogs/contactpropertiesdialog.h" #include "../model/contactlist.h" #include "../utils/kmessshared.h" #include "../utils/richtextparser.h" #include "../currentaccount.h" #include "../emoticonmanager.h" #include "../kmessdebug.h" #include <QClipboard> #include <QFileInfo> #include <QMouseEvent> #include <KApplication> #include <KActionMenu> #include <KIconEffect> #include <KIconLoader> #include <KMenu> #include <KStandardDirs> // Time in milliseconds before the glowing done for a typing user fades away #define GLOWING_PICTURE_TIMEOUT 5000 // The constructor ContactFrame::ContactFrame( QWidget *parent ) : QWidget(parent) , Ui::ContactFrame() , activated_(false) , contact_(0) , contactPixmapLabelEnabled_(true) , contactPropertiesDialog_(0) , detailedContact_(0) , handle_(QString::null) , currentMode_(ModeNormal) , infoLabelEnabled_(true) , locked_(false) { // Setup the UI as first thing setupUi( this ); layout()->setAlignment( Qt::AlignTop | Qt::AlignLeft ); // Just for show const QPixmap& pixmap( KGlobal::dirs()->findResource( "data", "kmess/pics/unknown.png" ) ); contactPixmapLabel_->setPixmap( pixmap ); installEventFilter( this ); contactPixmapLabel_->installEventFilter( this ); friendlyNameLabel_ ->installEventFilter( this ); infoLabel_ ->installEventFilter( this ); // Set up and connect the typing timer typingTimer_.setSingleShot( true ); connect( &typingTimer_, SIGNAL( timeout() ), this, SLOT ( stopTyping() ) ); } ContactFrame::~ContactFrame() { qDeleteAll( copyLinkActionsList_ ); } // Activate the frame by giving it a contact void ContactFrame::activate( ContactBase *contact ) { if ( contact_ && contact_ != contact ) { disconnect( contact_, 0, this, 0 ); } if( contact == 0 ) { #ifdef KMESSDEBUG_CONTACTFRAME kDebug() << "Deactivating frame for contact:" << handle_; #endif contact_ = 0; detailedContact_ = 0; activated_ = false; locked_ = true; qDeleteAll( copyLinkActionsList_ ); copyLinkActionsList_.clear(); } else { contact_ = contact; handle_ = contact_->getHandle(); detailedContact_ = CurrentAccount::instance()->getContactList()->getContactByHandle( handle_ ); if( detailedContact_ == 0 ) { activated_ = false; locked_ = true; } else { // Connect the contact, to update the frame on presence info changes connect( detailedContact_, SIGNAL( changedFriendlyName() ), this, SLOT ( updateStatusWidgets() ) ); connect( detailedContact_, SIGNAL( changedStatus() ), this, SLOT ( updateStatusWidgets() ) ); connect( detailedContact_, SIGNAL( changedPicture() ), this, SLOT ( updatePicture() ) ); connect( detailedContact_, SIGNAL( changedPersonalMessage(Contact*) ), this, SLOT ( updateStatusWidgets() ) ); connect( detailedContact_, SIGNAL( destroyed(QObject*) ), this, SLOT ( contactDestroyed(QObject*) ) ); activated_ = true; locked_ = false; } } // Activate the right-click context menu initContactPopup(); // And prepare the widgets updatePicture(); updateStatusWidgets(); show(); } // Allow this contact to see our MSN status void ContactFrame::allowContact() { if( detailedContact_ == 0 ) { return; } emit contactAllowed( handle_ ); updateStatusWidgets(); } // The contact was destroyed void ContactFrame::contactDestroyed( QObject *object ) { // This method is relevant for invited contacts, the following happens: // - an InvitedContact leaves the chat, SB notifies // - ContactFrame::setDisplayMode() will be called, and updates the widgets. // - CurrentAccount deletes the InvitedContact object. // - somehow a code paths leads to this class (Chat::setSwitchboardConnection(0) is already patched). // - crash. #ifdef KMESSDEBUG_CONTACTFRAME if( contact_ && contact_ != object ) { kWarning() << "Received unexpected contact reference to" << object->metaObject()->className(); } else { kDebug() << "Resetting contact reference."; } #else Q_UNUSED( object ); #endif activated_ = false; contact_ = 0; detailedContact_ = 0; } // Copy some details of the contact to the clipboard. // Only one method is used to copy all different details, and that's to avoid having three identical methods which only copy different // text bits on the clipboard. void ContactFrame::copyText() { // Get the signal sender to find out who called us KAction *action = static_cast<KAction*>( const_cast<QObject*>( sender() ) ); if(KMESS_NULL(action)) return; if( action == popupCopyFriendlyName_ ) { kapp->clipboard()->setText( contact_->getFriendlyName( STRING_CLEANED ) ); } else if( action == popupCopyHandle_ ) { kapp->clipboard()->setText( handle_ ); } else if( action == popupCopyPersonalMessage_ && detailedContact_ != 0 ) { kapp->clipboard()->setText( detailedContact_->getPersonalMessage( STRING_CLEANED ) ); } else if( detailedContact_ != 0 ) { // Copy the link from PM or Friendly Name kapp->clipboard()->setText( action->toolTip() ); } } void ContactFrame::editNotes() { // Check if there are details for contact if( detailedContact_ != 0 && detailedContact_->getExtension() != 0 ) { // Create the contact properties dialog if( contactPropertiesDialog_ == 0 ) { contactPropertiesDialog_ = new ContactPropertiesDialog( this ); } else { contactPropertiesDialog_->clearLists(); } contactPropertiesDialog_->launch( detailedContact_, true ); } } // The personal status message received an event. bool ContactFrame::eventFilter( QObject *obj, QEvent *event ) { int eventType = event->type(); // Check if the obj is contactPixmapLabel if( obj == contactPixmapLabel_ && detailedContact_ && ( eventType == QEvent::Enter || eventType == QEvent::Leave ) ) { QImage image( detailedContact_->getContactPicturePath() ); // Apply glow effect to image if( eventType == QEvent::Enter ) { KIconEffect::toGamma( image, 0.8f ); contactPixmapLabel_->setPixmap( QPixmap::fromImage( image ) ); } else if( eventType == QEvent::Leave ) { contactPixmapLabel_->setPixmap( QPixmap::fromImage( image ) ); } } if( eventType != QEvent::MouseButtonRelease ) { return false; // don't stop processing. } QMouseEvent *mouseEvent = static_cast<QMouseEvent*>( event ); // Show the menu regardlessly of where the mouse button has been pressed showContactPopup( mouseEvent->globalPos() ); return false; // don't stop processing. } // Return the handle of this frame's contact QString ContactFrame::getHandle() const { return handle_; } // Initialize the contact popup bool ContactFrame::initContactPopup() { if( contact_ == 0 ) { return false; } // Initialize context popup actions popupStartPrivateChat_ = new KAction( KIcon("user-group-new"), i18n("&Start Private Chat"), this); popupEmailContact_ = new KAction( KIcon("mail-message-new"), i18n("&Send Email"), this ); popupMsnProfile_ = new KAction( KIcon("preferences-desktop-user"), i18n("&View Profile"), this ); popupEditNotes_ = new KAction( KIcon("user-properties"), i18n("Ed&it Notes"), this ); popupAddContact_ = new KAction( KIcon("list-add"), i18n("&Add Contact"), this ); popupAllowContact_ = new KAction( KIcon("dialog-ok-apply"), i18n("A&llow Contact"), this ); popupRemoveContact_ = new KAction( KIcon("list-remove-user"), i18n("&Delete Contact"), this ); popupBlockContact_ = new KAction( KIcon("dialog-cancel"), i18n("&Block Contact"), this ); popupUnblockContact_ = new KAction( KIcon("dialog-ok"), i18n("&Unblock Contact"), this ); popupCopyFriendlyName_ = new KAction( i18n("&Friendly Name"), this ); popupCopyPersonalMessage_ = new KAction( i18n("&Personal Message"), this ); popupCopyHandle_ = new KAction( i18n("&Email Address"), this ); // Connect the actions connect( popupStartPrivateChat_, SIGNAL(triggered( bool )), this, SLOT( slotStartPrivateChat() ) ); connect( popupEmailContact_, SIGNAL(triggered(bool)), this, SLOT( sendEmail() ) ); connect( popupMsnProfile_, SIGNAL(triggered(bool)), this, SLOT( showProfile() ) ); connect( popupEditNotes_, SIGNAL(triggered(bool)), this, SLOT( editNotes() ) ); connect( popupAddContact_, SIGNAL(triggered(bool)), this, SLOT( toggleContactAdded() ) ); connect( popupAllowContact_, SIGNAL(triggered(bool)), this, SLOT( allowContact() ) ); connect( popupRemoveContact_, SIGNAL(triggered(bool)), this, SLOT( toggleContactAdded() ) ); connect( popupBlockContact_, SIGNAL(triggered(bool)), this, SLOT( toggleContactBlocked() ) ); connect( popupUnblockContact_, SIGNAL(triggered(bool)), this, SLOT( toggleContactBlocked() ) ); connect( popupCopyFriendlyName_, SIGNAL(triggered(bool)), this, SLOT( copyText() ) ); connect( popupCopyPersonalMessage_, SIGNAL(triggered(bool)), this, SLOT( copyText() ) ); connect( popupCopyHandle_, SIGNAL(triggered(bool)), this, SLOT( copyText() ) ); // Initialize sub popups popupCopyMenu_ = new KActionMenu( i18n("&Copy"), this ); popupCopyMenu_ ->addAction( popupCopyFriendlyName_ ); popupCopyMenu_ ->addAction( popupCopyPersonalMessage_ ); popupCopyMenu_ ->addAction( popupCopyHandle_ ); popupCopyMenu_->addSeparator(); // Initialize the popup popup contactActionPopup_ = new KMenu( handle_, this ); // Attach the actions to the menu // Order is as consistent as possible with the menu in KMessView contactActionPopup_->addTitle( contact_->getHandle() ); contactActionPopup_->addAction( popupStartPrivateChat_ ); contactActionPopup_->addAction( popupEmailContact_ ); contactActionPopup_->addAction( popupMsnProfile_ ); contactActionPopup_->addAction( popupCopyMenu_ ); contactActionPopup_->addAction( popupEditNotes_ ); contactActionPopup_->addSeparator(); contactActionPopup_->addAction( popupAddContact_ ); contactActionPopup_->addAction( popupAllowContact_ ); contactActionPopup_->addAction( popupBlockContact_ ); contactActionPopup_->addAction( popupUnblockContact_ ); contactActionPopup_->addAction( popupRemoveContact_ ); return true; } // Whether or not the frame has been activated bool ContactFrame::isActivated() const { return activated_; } // The user received a message from this contact void ContactFrame::messageReceived() { // Stop the "user is typing" glow typingTimer_.stop(); stopTyping(); } // Email the contact void ContactFrame::sendEmail() { if ( contact_ != 0 ) { CurrentAccount::instance()->openMailAtCompose( handle_ ); } } // Change the display mode of the frame when the contact leaves the chat or there are many active contacts void ContactFrame::setDisplayMode( DisplayMode mode ) { if( currentMode_ == mode ) { return; } // Save the new current mode to avoid useless updates currentMode_ = mode; #ifdef KMESSDEBUG_CONTACTFRAME kDebug() << "Frame mode:" << mode; #endif // Change the displayed widgets switch( mode ) { case ModeSingle: popupStartPrivateChat_->setVisible( false ); contactPixmapLabelEnabled_ = true; infoLabelEnabled_ = true; break; case ModeNormal: popupStartPrivateChat_->setVisible( true ); contactPixmapLabelEnabled_ = true; infoLabelEnabled_ = true; break; case ModeSmall: popupStartPrivateChat_->setVisible( true ); contactPixmapLabelEnabled_ = false; infoLabelEnabled_ = true; break; case ModeTiny: popupStartPrivateChat_->setVisible( true ); contactPixmapLabelEnabled_ = false; infoLabelEnabled_ = false; break; } // Update the status widgets updateStatusWidgets(); } // Enable or disable the frame void ContactFrame::setEnabled( bool isEnabled ) { contactPixmapLabel_->setEnabled( isEnabled ); } // Update and show the contact popup menu void ContactFrame::showContactPopup( const QPoint &point ) { bool isAdded = false; bool isAddable = ! locked_; bool isAllowed = true; bool isBlocked = false; bool isBlockable = ! locked_; bool hasPersonalMessage = false; if( detailedContact_ != 0 ) { isAddable = true; isBlockable = true; isAdded = detailedContact_->isFriend(); isAllowed = detailedContact_->isAllowed(); isBlocked = detailedContact_->isBlocked(); hasPersonalMessage = ! detailedContact_->getPersonalMessage().isEmpty(); } popupAllowContact_ ->setVisible( ! isAllowed ); popupCopyPersonalMessage_->setVisible( hasPersonalMessage ); popupAddContact_ ->setVisible( isAddable && ! isAdded ); popupRemoveContact_ ->setVisible( isAddable && isAdded ); popupBlockContact_ ->setVisible( isBlockable && ! isBlocked ); popupUnblockContact_ ->setVisible( isBlockable && isBlocked ); // If detailed contact information is available, search for links if( detailedContact_ != 0 ) { qDeleteAll( copyLinkActionsList_ ); copyLinkActionsList_.clear(); // Append friendly name and personal message so cycle only one time to search links const QString& nameAndPm( detailedContact_->getFriendlyName() + " " + detailedContact_->getPersonalMessage() ); QStringList links; //Initialize index, link RegExp and found boolean for insert separator only at first found link int pos = 0; QRegExp linkRegExp( "(http://|https://|ftp://|sftp://|www\\..)\\S+" ); QString foundLink; while( ( pos = linkRegExp.indexIn( nameAndPm, pos ) ) != -1 ) { // Grep the found link and update the position for the next cycle foundLink =linkRegExp.cap( 0 ); pos += linkRegExp.matchedLength(); // Skip duplicated links if( links.contains( foundLink ) ) { continue; } links.append( foundLink ); // Put the found link into KAction, replace & with && to avoid shortcut problem and use tooltip // for future grep of link ( to avoid shortcut problem too ) popupCopyLink_ = new KAction( foundLink.replace( "&", "&&" ), this ); popupCopyLink_->setToolTip( foundLink ); connect( popupCopyLink_, SIGNAL( triggered( bool ) ), this, SLOT( copyText() ) ); // Append current KAction to the copy link list copyLinkActionsList_.append( popupCopyLink_ ); // Add action to copy menu popupCopyMenu_->addAction( popupCopyLink_ ); } } contactActionPopup_->popup( point ); } // Show the contact's profile void ContactFrame::showProfile() { if( contact_ == 0 ) { return; } // Create a URL to the msn profile page, localized with our system's locale. KUrl url = KUrl( "http://members.msn.com/default.msnw?mem=" + handle_ + "&mkt=" + KGlobal::locale()->language() ); // Launch the browser for the given URL KMessShared::openBrowser( url ); } // Request to start private chat void ContactFrame::slotStartPrivateChat() { emit startPrivateChat( handle_ ); } // Receive notice that the contact is typing void ContactFrame::startTyping() { contactPixmapLabel_->setPixmap( contactTypingPicture_ ); // Make the contact picture glow // Start the timer to disable the label typingTimer_.start( GLOWING_PICTURE_TIMEOUT ); } // Disable the typing label when the timer has timed out void ContactFrame::stopTyping() { contactPixmapLabel_->setPixmap( contactPicture_ ); } // Add or remove the contact from the contact list void ContactFrame::toggleContactAdded() { if( detailedContact_ == 0 ) { emit contactAdded( handle_, true ); // Disallow adding unknown contacts more than once locked_ = true; } else { emit contactAdded( handle_, ! detailedContact_->isFriend() ); } updateStatusWidgets(); } // Change the contact's blocked/unblocked status void ContactFrame::toggleContactBlocked() { if( detailedContact_ == 0 ) { emit contactBlocked( handle_, true ); // Disallow adding unknown contacts more than once locked_ = true; } else { emit contactBlocked( handle_, ! detailedContact_->isBlocked() ); } updateStatusWidgets(); } // Update the contact picture void ContactFrame::updatePicture() { if ( contact_ == 0 ) { return; } // See if the contact picture is not updated. Avoids GUI flashing on older boxes const QString& contactPicturePath( contact_->getContactPicturePath() ); const QFileInfo& contactPictureInfo( contactPicturePath ); const QDateTime& contactPictureDate( contactPictureInfo.lastModified() ); if( contactPicturePath == contactPicturePath_ && contactPictureDate == contactPictureDate_ ) { #ifdef KMESSDEBUG_CONTACTFRAME kDebug() << "Picture was already loaded."; #endif return; } #ifdef KMESSDEBUG_CONTACTFRAME kDebug() << "New contact picture:" << contactPicturePath; #endif QImage contactImage( contactPicturePath ); bool loaded = ! contactImage.isNull(); if( loaded ) { // Loaded contactPicturePath_ = contactPicturePath; contactPictureDate_ = contactPictureDate; } else { // Not loaded (PNG error?). load default if custom image could not be loaded loaded = contactImage.load( contact_->getContactDefaultPicturePath() ); if( ! loaded ) { return; } } // Setup the normal and typing pixmaps contactPicture_ = QPixmap::fromImage( contactImage ); KIconEffect::toGamma( contactImage, 0.8f ); contactTypingPicture_ = QPixmap::fromImage( contactImage ); // Display the image contactPixmapLabel_->setPixmap( contactPicture_ ); contactPixmapLabel_->setMinimumSize( contactImage.width(), contactImage.height() ); } // Update the status widgets void ContactFrame::updateStatusWidgets() { if( contact_ == 0 ) { return; } QString label, statusIdentifier; const Status status( contact_->getStatus() ); Flags flags = FlagNone; if( detailedContact_ != 0 && detailedContact_->isBlocked()) { // Show blocked regardless of status statusIdentifier = i18n( "Blocked" ); flags = FlagBlocked; } else { statusIdentifier = MsnStatus::getName( status ); } friendlyNameLabel_->setText( contact_->getFriendlyName( STRING_CHAT_SETTING_ESCAPED ) ); friendlyNameLabel_->setToolTip( contact_->getFriendlyName() ); statusPixmapLabel_->setToolTip( i18nc( "Tooltip for a contact's status icon, " "arg %1 is the MSN Status, like 'Online'", "The contact is %1", statusIdentifier ) ); statusPixmapLabel_->setPixmap( MsnStatus::getIcon( status, flags ) ); // Update the informative label if( detailedContact_ && infoLabelEnabled_ ) { const QString& mediaString( detailedContact_->getCurrentMediaString() ); const QString& messageString( detailedContact_->getPersonalMessage( STRING_CHAT_SETTING_ESCAPED ) ); if( ! mediaString.isEmpty() ) { // Determine the icon for the various media types, if none no icon will be shown QString mediaEmoticon; const QString& mediaType( detailedContact_->getCurrentMediaType() ); if( mediaType == "Music" ) { mediaEmoticon = EmoticonManager::instance()->getReplacement( "(8)", true ); } else if( mediaType == "Gaming" ) { mediaEmoticon = EmoticonManager::instance()->getReplacement( "(xx)", true ); } infoLabel_->setVisible( true ); infoLabel_->setText( mediaEmoticon + Qt::escape( mediaString ) ); infoLabel_->setToolTip( mediaString ); } else if( ! messageString.isEmpty() ) { infoLabel_->setVisible( true ); infoLabel_->setText( messageString ); infoLabel_->setToolTip( detailedContact_->getPersonalMessage( STRING_CHAT_SETTING ) ); } else { infoLabel_->setVisible( false ); } } else { infoLabel_->setVisible( false ); } contactPixmapLabel_->setVisible( contactPixmapLabelEnabled_ ); } #include "contactframe.moc"