Files
kmess/src/kmessview.cpp
T
2026-07-06 09:37:59 +02:00

2319 lines
75 KiB
C++

/***************************************************************************
kmessview.cpp - description
-------------------
begin : Thu Jan 9 2003
copyright : (C) 2003 by Mike K. Bennett
(C) 2005 by Diederik van der Boor
email : mkb137b@hotmail.com
vdboor --at-- codingdomain.com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "kmessview.h"
#include "contact/contact.h"
#include "contact/group.h"
#include "contact/msnstatus.h"
#include "contact/specialgroups.h"
#include "dialogs/chathistorydialog.h"
#include "dialogs/contactpropertiesdialog.h"
#include "model/contactlist.h"
#include "model/contactlistmodelfilter.h"
#include "utils/kmessshared.h"
#include "utils/kmessconfig.h"
#include "utils/richtextparser.h"
#include "currentaccount.h"
#include "emoticonmanager.h"
#include "kmessdebug.h"
#include "kmessviewdelegate.h"
#include <QClipboard>
#include <QGuiApplication>
#include <QKeyEvent>
#include <QImage>
#include <QItemSelectionModel>
#include <QMimeData>
#include <QPixmap>
#include <QRegularExpression>
#include <QSignalMapper>
#include <QStyleHints>
#include <QStringList>
#include <QToolTip>
#include <QAction>
#include <KActionMenu>
#include <QApplication>
#include <QIcon>
#include <KIconLoader>
#include <KIconEffect>
#include <KLocalizedString>
#include <QMenu>
#include <KMessageBox>
#include <QUrl>
#ifdef KMESSDEBUG_CONTACTLISTMODELTEST
#include "../tests/modeltest/modeltest.h"
#endif
#ifdef KMESSDEBUG_KMESSVIEW
#define KMESSDEBUG_KMESSVIEW_GENERAL
#ifdef KMESSDEBUG_CONTACTLISTMODEL
#define KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
#endif
#endif
// The constructor
KMessView::KMessView( QWidget *parent )
: QWidget( parent ),
Ui::KMessView(),
contactCopyMapper_(0),
contactMoveMapper_(0),
currentAccount_(0),
initialized_(false),
viewModel_(0)
{
setupUi( this );
searchFrame_->hide();
// Capture focus events without subclassing the widget from the .UI file
currentAccountDisplayPic_->installEventFilter(this);
contactListView_->installEventFilter(this);
contactListView_->viewport()->installEventFilter(this);
// Windows Live Messenger 8.5 and 2009 (latter only in the chat screen) don't
// display any part further than 129 characters.
personalMessageInput_->setMaxLength( 129 );
friendlyNameInput_->setFormattingMode( STRING_LIST_SETTING );
personalMessageInput_->setFormattingMode( STRING_LIST_SETTING );
// Set up the group update timer
updateTimer_.setSingleShot( true );
connect( &updateTimer_, SIGNAL( timeout() ),
this, SLOT ( slotUpdateGroups() ) );
// Set the owner window to always show tooltips on request,
// even if it doesn't have focus.
window()->setAttribute( Qt::WA_AlwaysShowToolTips );
}
// The destructor
KMessView::~KMessView()
{
// Delete the contact actions for copy/move to group
qDeleteAll( groupCopyActionsList_ );
qDeleteAll( groupMoveActionsList_ );
qDeleteAll( groupCopyLinkActionsList_ );
delete contactCopyMapper_;
delete contactMoveMapper_;
delete selectionModel_;
delete viewModel_;
selectionModel_ = 0;
viewModel_ = 0;
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmDebug() << "DESTROYED.";
#endif
}
// The currently playing song was changed.
void KMessView::changedSong( const QString &artist, const QString &/*album*/, const QString &track, bool playing )
{
songPlaying_ = playing;
if( playing )
{
// Show status line
if( ! nowListeningFrame_->isVisible() )
{
nowListeningFrame_->setHidden(false);
nowListeningFrame_->setMaximumHeight(40);
}
// Update label
bool showDash = ( ! track.isEmpty() && ! artist.isEmpty() );
QString longTitle( track + ( showDash ? " - " : "" ) + artist );
nowListeningLabel_->setText( longTitle );
}
else
{
// Hide status line
nowListeningFrame_->setHidden(true);
nowListeningFrame_->setMaximumHeight(0);
}
// update size of the DP.
slotUpdateDisplayPicture();
}
// Get the specified item's or the current item's data
const ModelDataList KMessView::getItemData( const QModelIndex &index )
{
const QModelIndexList selection( selectionModel_->selectedIndexes() );
QModelIndex itemIndex( index );
// If the index is not valid, get the current selection
// TODO is this check necessary? isn't it better to return ModelDataList() to avoid any problems?
if( ! itemIndex.isValid() && ! selection.isEmpty() )
{
// Only one index can be returned at once by the contact list (has single selection mode)
itemIndex = QModelIndex( selection.first() );
// return ModelDataList();
}
// The index or the selection aren't valid, abort.
if( ! itemIndex.isValid() || itemIndex.model() != viewModel_ )
{
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Skipping an invalid item at index:" << index;
#endif
return ModelDataList();
}
// Get the item data
const QVariant data( itemIndex.data() );
// If the data is not a variant map, abort
if( ! data.canConvert<QVariantMap>() )
{
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Skipping an item with invalid data at index:" << index;
#endif
return ModelDataList();
}
// Convert the variant data to a normal map
const ModelDataList itemData( data.toMap() );
// Do nothing if there's no data
if( itemData.isEmpty() )
{
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Skipping an empty item at index:" << index;
#endif
return ModelDataList();
}
int type = itemData[ "type" ].toInt();
// Do nothing if the item is not of a valid type
if( type != ContactListModelItem::ItemGroup
&& type != ContactListModelItem::ItemContact )
{
return ModelDataList();
}
return itemData;
}
// Initialize the class
bool KMessView::initialize( QAbstractItemModel *viewModel )
{
bool success;
KIconLoader *loader = KIconLoader::global();
// Get a ref to the current account
currentAccount_ = CurrentAccount::instance();
if ( initialized_ )
{
kmDebug() << "already initialized.";
return false;
}
success = initContactListView( viewModel );
if(! success)
{
kmDebug() << "Couldn't initialize contact list view.";
return false;
}
success = initContactPopup();
if(! success)
{
kmDebug() << "Couldn't initialize contact popup.";
return false;
}
success = initGroupPopup();
if(! success)
{
kmDebug() << "Couldn't initialize group popup.";
return false;
}
//set the nowListening button
slotUpdateNowListeningToggle();
// Set an icon for the email label
// Using "knotify" icon because it's available everywhere. "multimedia" is SUSE-specific.
emailPixmapLabel_ ->setPixmap( loader->loadIcon( "mail-mark-unread", KIconLoader::Small ) );
nowListeningPixmapLabel_->setPixmap( loader->loadIcon( "speaker", KIconLoader::Small ) );
searchIcon_ ->setPixmap( loader->loadIcon( "edit-find-user", KIconLoader::Small ) );
// Create a menu for the status button
// Set the menu into button and connect the signals
QMenu *menu = MsnStatus::getStatusMenu();
statusButton_->setMenu( menu );
connect( statusButton_, SIGNAL( clicked() ),
statusButton_, SLOT ( showMenu() ) );
// Enable the labels
emailLabel_->setEnabled(true);
QFont appFont( QApplication::font() );
int appFontSize = appFont.pointSize();
friendlyNameInput_->setStyleSheet( "QLabel { font-size: " + QString::number( appFontSize + 2 ) + "pt; }" );
personalMessageInput_->setStyleSheet( "QLabel { font-size: " + QString::number( appFontSize - 1 ) + "pt; }" );
nowListeningLabel_->setStyleSheet( "QLabel { font-style: italic; font-size: " + QString::number( appFontSize - 1 ) +"pt; }" );
// Hide the now listening until something is playing
changedSong( QString(), QString(), QString(), false );
// Connect signals for the current account
connect( currentAccount_, SIGNAL( changedMsnObject() ),
this, SLOT ( slotUpdateDisplayPicture() ) ); // Changed users display pic
connect( currentAccount_, SIGNAL( changedNoEmails() ), // Changed email count
this, SLOT ( slotUpdateNoEmails() ) );
connect( currentAccount_, SIGNAL( changedStatus() ), // Changed status
this, SLOT ( slotUpdateUserStatus() ) );
connect( currentAccount_, SIGNAL( changedFriendlyName() ), // Changed name
this, SLOT ( slotUpdateUserStatus() ) );
connect( currentAccount_, SIGNAL( changedPersonalMessage() ), // Changed personal message
this, SLOT ( slotUpdateUserStatus() ) );
connect( currentAccount_, SIGNAL( changedEmailDisplaySettings() ), // Changed email display settings
this, SLOT ( slotUpdateEmailDisplay() ) );
connect( currentAccount_, SIGNAL( changedDisplaySettings() ),
this, SLOT ( slotUpdateGroups() ) );
connect( currentAccount_, SIGNAL( changedViewMode() ),
this, SLOT ( slotUpdateGroups() ) );
connect( currentAccount_, SIGNAL( changedContactListOptions() ), // Changed contact list options
this, SLOT ( slotUpdateView() ) );
connect( currentAccount_, SIGNAL( changedContactListOptions() ), // Changed contact list options
this, SLOT ( slotUpdateUserStatus() ) );
// These update the display pic size depending on the email & now listening options
connect( currentAccount_, SIGNAL( changedNowListeningSettings() ),
this, SLOT ( slotUpdateDisplayPicture() ) );
connect( currentAccount_, SIGNAL( changedNowListeningSettings() ),
this, SLOT (slotUpdateNowListeningToggle() ) );
connect( currentAccount_, SIGNAL( changedEmailDisplaySettings() ),
this, SLOT ( slotUpdateDisplayPicture() ) );
// Connect signals for the contactlist
const ContactList *contactList = currentAccount_->getContactList();
connect( contactList, SIGNAL( groupAdded(const Group*) ), // A group was added
this, SLOT( rebuildContactActions() ) );
connect( contactList, SIGNAL( groupChanged(const Group*) ), // A group was modified
this, SLOT( rebuildContactActions() ) );
connect( contactList, SIGNAL( groupRemoved(const Group*) ), // A group was removed
this, SLOT( rebuildContactActions() ) );
// Connect signals for the history combobox and put the first string into it
connect( contactList, SIGNAL( contactOnline( Contact*, bool ) ),
this, SLOT( slotContactChangedStatus( Contact *, bool ) ) );
connect( contactList, SIGNAL( contactOffline( Contact*, bool ) ),
this, SLOT( slotContactChangedStatus( Contact *, bool ) ) );
connect( contactList, SIGNAL( contactOnline( Contact*, bool ) ),
this, SLOT( slotScheduleGroupUpdate() ) );
connect( contactList, SIGNAL( contactOffline( Contact*, bool ) ),
this, SLOT( slotScheduleGroupUpdate() ) );
historyBox_->insertItem( 0, i18n( "[%1] Logged in with %2", QTime::currentTime ().toString( "HH:mm:ss" )
, currentAccount_->getHandle() ) );
// Enable changing inline the friendly name and PM
connect( friendlyNameInput_, SIGNAL( changed() ),
this, SLOT ( updateFriendlyName() ) );
connect( personalMessageInput_, SIGNAL( changed() ),
this, SLOT ( updatePersonalMessage() ) );
// Connect the search edit line with slot for searching
connect( searchEdit_, SIGNAL( textChanged(QString) ),
this, SLOT ( slotSearchContact(QString) ) );
// Connect toggle now listening button
connect( nowListeningPushButton_, SIGNAL( clicked() ),
this, SLOT ( slotToggleNowListening()) );
// Enable the search box if the user has left it open at the last disconnection
toggleShowSearchFrame( currentAccount_->getShowSearchBar() );
// Enable the history box
toggleShowHistoryBox( currentAccount_->getShowHistoryBox() );
// Run the update functions with the current user data
slotUpdateEmailDisplay();
slotUpdateUserStatus();
slotUpdateNoEmails();
slotUpdateDisplayPicture();
slotUpdateGroups();
updateFriendlyName( true );
updatePersonalMessage( true );
return true;
}
// Called when a contact is now online/offline
void KMessView::slotContactChangedStatus( Contact *contact, bool showNotify )
{
// One contact is now online/offline, put it in history combobox
if( showNotify )
{
const QString& contactName = currentAccount_->getShowContactEmail()
? contact->getHandle()
: contact->getFriendlyName();
QString item;
if( contact->isOnline() )
{
item = i18n( "[%1] %2 goes online", QTime::currentTime ().toString( "HH:mm:ss" )
, contactName );
}
else
{
item = i18n( "[%1] %2 goes offline", QTime::currentTime ().toString( "HH:mm:ss" )
, contactName );
}
// Remove oldest item if the count is >= 500 elements
if( historyBox_->count() >= 500 )
{
historyBox_->removeItem( historyBox_->count() - 1 );
}
historyBox_->insertItem( 0, item );
historyBox_->setCurrentIndex ( 0 );
}
}
// The connection was closed, clean up before deletion
void KMessView::slotDisconnected()
{
updateTimer_.stop();
contactListView_->setModel( 0 );
}
// The "show history box" menu item has been toggled.
void KMessView::toggleShowHistoryBox( bool show )
{
historyBox_->setVisible( show );
// Save the current state to the Account
currentAccount_->setShowHistoryBox( show );
}
// The "show search in contact list" menu item has been toggled.
void KMessView::toggleShowSearchFrame( bool show )
{
if( show )
{
searchFrame_->show();
searchEdit_->setPlaceholderText( i18n( "Search in contact list..." ) );
}
else
{
// Remove the query when closing the search bar
searchEdit_->setText( QString() );
searchFrame_->hide();
contactListView_->setFocus();
}
// Save the current state to the Account
currentAccount_->setShowSearchBar( show );
}
// The personal status message received an event.
bool KMessView::eventFilter(QObject *obj, QEvent *event)
{
if(obj != currentAccountDisplayPic_
&& obj != contactListView_
&& obj != contactListView_->viewport() )
{
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmWarning() << "Received event '" << event->type() << "' from object '" << obj->objectName() << "'.";
#endif
return false;
}
QEvent::Type eventType = event->type();
if( obj == currentAccountDisplayPic_ )
{
if( eventType == QEvent::Enter )
{
QImage glowingImage( currentAccount_->getPicturePath() );
KIconEffect::toGamma( glowingImage, 0.8f );
currentAccountDisplayPic_->setPixmap( QPixmap::fromImage( glowingImage ) );
}
else if( eventType == QEvent::Leave )
{
currentAccountDisplayPic_->setPixmap( QPixmap( currentAccount_->getPicturePath() ) );
}
else if( eventType == QEvent::MouseButtonRelease )
{
emit showSettings();
}
else if( eventType == QEvent::DragEnter )
{
QDragEnterEvent *dragEvent = static_cast<QDragEnterEvent*>( event );
const QMimeData *mimeData = dragEvent->mimeData();
// The mimeData object can contain one of the following objects: an image, HTML text, plain text, or a list of URLs.
// We need only the first URL object
if( mimeData->hasUrls() )
{
dragEvent->acceptProposedAction();
}
else
{
dragEvent->ignore();
}
}
else if( eventType == QEvent::Drop )
{
QDropEvent* dropEvent = static_cast<QDropEvent*>( event );
const QMimeData *mimeData = dropEvent->mimeData();
// The mimeData object can contain one of the following objects: an image, HTML text, plain text, or a list of URLs.
// We need only the first URL object
if( ! mimeData->hasUrls() )
{
return true;
}
else
{
QUrl mimeUrl = mimeData->urls().first();
QString localFilePath;
QString temporaryFilePath;
QImage pictureData;
if( mimeUrl.isEmpty() )
{
kmWarning() << "Dropped empty MIME URL";
return true;
}
if( ! KMessShared::downloadToLocalFile( mimeUrl, localFilePath, &temporaryFilePath ) )
{
KMessageBox::error( this, i18n( "Downloading of display picture failed" ), i18n("KMess") );
return true;
}
if( ! pictureData.load( localFilePath ) )
{
KMessShared::removeDownloadedTempFile( temporaryFilePath );
kmWarning() << "The file dropped is not an image or doesn't exist";
return true;
}
KMessShared::removeDownloadedTempFile( temporaryFilePath );
pictureData = pictureData.scaled( 96, 96, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation ); // Makes smallest edge 96 pixels, preserving aspect ratio
if( pictureData.width() != 96 || pictureData.height() != 96 )
{
// Copy the middle part of the picture.
pictureData = pictureData.copy( (pictureData.width() - 96) / 2, // X
(pictureData.height() - 96) / 2, // Y
96, 96 );
}
QString pictureDir = KMessConfig::instance()->getAccountDirectory( currentAccount_->getHandle() ) + "/displaypics/";
QString tempPictureFile = pictureDir + "temporary-picture.png";
if( ! pictureData.save( tempPictureFile, "PNG" ) )
{
KMessageBox::error( this,
i18n( "An error occurred while trying to change the display picture.\n"
"Make sure that you have selected an existing image file." ),
i18n( "KMess" ) );
}
QString msnObjectHash( KMessShared::generateFileHash( tempPictureFile ).toBase64() );
const QString safeMsnObjectHash( msnObjectHash.replace( QRegularExpression("[^a-zA-Z0-9+=]"), "_" ) );
// Save the new display picture with its hash as name, to ensure correct caching
QString pictureName = msnObjectHash + ".png";
QDir pictureFolder = QDir( pictureDir );
// Do not overwrite identical pictures
if( ! pictureFolder.exists( pictureName ) )
{
if( ! pictureFolder.rename( tempPictureFile, pictureName ) )
{
kmWarning() << "The display picture file could not be renamed from" << tempPictureFile << "to" << pictureName << ".";
return true;
}
}
currentAccount_->setPicturePath( pictureDir + pictureName );
currentAccount_->setOriginalPicturePath( mimeUrl.path() );
dropEvent->acceptProposedAction();
}
}
}
else if( obj == contactListView_->viewport() )
{
if( eventType == QEvent::ToolTip )
{
showToolTip( QCursor::pos() );
return true;
}
}
else if( obj == contactListView_ )
{
if( eventType == QEvent::KeyPress )
{
int key = static_cast<QKeyEvent*>( event )->key();
// Detect enter key press to open a chat with the selected contact
if( key == Qt::Key_Return || key == Qt::Key_Enter )
{
const QModelIndexList selection( selectionModel_->selectedIndexes() );
// Get the currently selected item
if( ! selection.isEmpty() )
{
slotItemClicked( selection.first(), false );
}
}
}
}
return false; // don't stop processing.
}
// Initialize the contact list view
bool KMessView::initContactListView( QAbstractItemModel *viewModel )
{
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmDebug() << "Initializing the contact list view.";
#endif
// Create a proxy model to filter and sort the raw contact list view
viewModel_ = new ContactListModelFilter( this );
viewModel_->setSourceModel( viewModel );
// Create a selection model to get data about the list selections
selectionModel_ = new QItemSelectionModel( viewModel_ );
// Activate the custom list item painter
contactListView_->setItemDelegate( new KMessViewDelegate( contactListView_ ) );
// Set the contact list view's properties
#if QT_VERSION >= 0x040400
contactListView_->setHeaderHidden( true );
#else
contactListView_->header()->hide();
#endif
contactListView_->setAnimated( true );
contactListView_->setDragEnabled( true );
contactListView_->setDropIndicatorShown( true );
contactListView_->setDragDropOverwriteMode( false );
// TODO: support drag/drop contacts from another application!
contactListView_->setDragDropMode( QAbstractItemView::InternalMove );
contactListView_->viewport()->setAcceptDrops( true );
contactListView_->setAcceptDrops( true );
// Set the list stylesheet
slotUpdateView();
// Connect the list to the models
contactListView_->setModel( viewModel_ );
contactListView_->setSelectionModel( selectionModel_ );
// Connect the list's signals
connect( contactListView_, SIGNAL( clicked(const QModelIndex&) ),
this, SLOT( slotItemSingleClicked(const QModelIndex&) ) );
connect( contactListView_, SIGNAL( doubleClicked(const QModelIndex&) ),
this, SLOT( slotItemDoubleClicked(const QModelIndex&) ) );
connect( contactListView_, SIGNAL( collapsed(const QModelIndex&) ),
this, SLOT( slotGroupChanged(const QModelIndex&) ) );
connect( contactListView_, SIGNAL( expanded(const QModelIndex&) ),
this, SLOT( slotGroupChanged(const QModelIndex&) ) );
// Enable the right-click context menu
contactListView_->setContextMenuPolicy( Qt::CustomContextMenu );
connect( contactListView_, SIGNAL( customContextMenuRequested(const QPoint&) ),
this, SLOT( showContextMenu(const QPoint&) ) );
// Connect the selection model's signals
connect( selectionModel_, SIGNAL( selectionChanged(const QItemSelection&,const QItemSelection&) ),
this, SIGNAL( selectionChanged(const QItemSelection&) ) );
return true;
}
// Initialize the contact popup
bool KMessView::initContactPopup()
{
// Initialize context menu actions
chatWithContact_ = new QAction( QIcon::fromTheme( "user-group-new" ), i18n("Cha&t"), this );
emailContact_ = new QAction( QIcon::fromTheme( "mail-message-new" ), i18n("&Send Email"), this );
msnProfile_ = new QAction( QIcon::fromTheme( "preferences-desktop-user" ), i18n("&View Profile"), this );
chatHistory_ = new QAction( QIcon::fromTheme( "chronometer" ), i18n("Show Chat &History..."), this );
contactProperties_ = new QAction( QIcon::fromTheme( "user-properties" ), i18n("&Properties"), this );
addContact_ = new QAction( QIcon::fromTheme( "list-add" ), i18n("&Add Contact"), this );
allowContact_ = new QAction( QIcon::fromTheme( "dialog-ok-apply" ), i18n("A&llow Contact"), this );
blockContact_ = new QAction( QIcon::fromTheme( "dialog-cancel" ), i18n("&Block Contact"), this );
unblockContact_ = new QAction( QIcon::fromTheme( "dialog-ok" ), i18n("&Unblock Contact"), this );
removeContact_ = new QAction( QIcon::fromTheme( "list-remove-user" ), i18n("&Delete Contact"), this );
removeFromGroup_ = new QAction( QIcon::fromTheme( "list-remove" ), i18n("&Remove From Group"), this );
popupCopyFriendlyName_ = new QAction( i18n("&Friendly Name"), this );
popupCopyPersonalMessage_ = new QAction( i18n("&Personal Message"), this );
popupCopyHandle_ = new QAction( i18n("&Email Address"), this );
popupCopyMusic_ = new QAction( i18n("Song &Name"), this );
// Connect the actions
connect( chatWithContact_, SIGNAL(triggered(bool)), this, SLOT(slotForwardStartChat()) );
connect( emailContact_, SIGNAL(triggered(bool)), this, SLOT(slotEmailContact()) );
connect( msnProfile_, SIGNAL(triggered(bool)), this, SLOT(slotShowContactProfile()) );
connect( chatHistory_, SIGNAL(triggered(bool)), this, SLOT(slotShowChatHistory()) );
connect( contactProperties_, SIGNAL(triggered(bool)), this, SLOT(slotShowContactProperties()) );
connect( addContact_, SIGNAL(triggered(bool)), this, SLOT(slotForwardAddContact()) );
connect( allowContact_, SIGNAL(triggered(bool)), this, SLOT(slotForwardAllowContact()) );
connect( blockContact_, SIGNAL(triggered(bool)), this, SLOT(slotForwardBlockContact()) );
connect( unblockContact_, SIGNAL(triggered(bool)), this, SLOT(slotForwardUnblockContact()) );
connect( removeContact_, SIGNAL(triggered(bool)), this, SLOT(slotForwardRemoveContact()) );
connect( removeFromGroup_, SIGNAL(triggered(bool)), this, SLOT(slotForwardRemoveFromGroup()));
connect( popupCopyFriendlyName_, SIGNAL(triggered(bool)), this, SLOT( copyText() ) );
connect( popupCopyPersonalMessage_, SIGNAL(triggered(bool)), this, SLOT( copyText() ) );
connect( popupCopyHandle_, SIGNAL(triggered(bool)), this, SLOT( copyText() ) );
connect( popupCopyMusic_, SIGNAL(triggered(bool)), this, SLOT( copyText() ) );
// Create the signal mappers needed to assign all actions to the move/copy contact slots
contactCopyMapper_ = new QSignalMapper( this );
contactMoveMapper_ = new QSignalMapper( this );
connect( contactCopyMapper_, SIGNAL(mappedString(const QString&)), this, SLOT(slotForwardCopyContact(const QString&)) );
connect( contactMoveMapper_, SIGNAL(mappedString(const QString&)), this, SLOT(slotForwardMoveContact(const QString&)) );
// Initialize the sub menus to copy/move contacts
copyContactToGroup_ = new KActionMenu( i18n("&Copy to Group"), this );
moveContactToGroup_ = new KActionMenu( i18n("&Move to Group"), this );
// Fill them with the current groups names
rebuildContactActions();
// Initialize Copy sub popups
popupCopyMenu_ = new KActionMenu( i18n("&Copy"), this );
popupCopyMenu_ ->addAction( popupCopyFriendlyName_ );
popupCopyMenu_ ->addAction( popupCopyPersonalMessage_ );
popupCopyMenu_ ->addAction( popupCopyHandle_ );
popupCopyMenu_ ->addAction( popupCopyMusic_ );
popupCopyMenu_->addSeparator();
// Initialize the popup menu
// If you re-organize the menu, also consider updating ContactFrame.
contactActionPopup_ = new QMenu( "KMess", this );
contactActionPopup_->addAction( chatWithContact_ );
contactActionPopup_->addAction( emailContact_ );
contactActionPopup_->addAction( msnProfile_ );
contactActionPopup_->addAction( chatHistory_ );
contactActionPopup_->addAction( popupCopyMenu_ );
contactActionPopup_->addSeparator();
contactActionPopup_->addAction( moveContactToGroup_ );
contactActionPopup_->addAction( copyContactToGroup_ );
contactActionPopup_->addAction( removeFromGroup_ );
contactActionPopup_->addSeparator();
contactActionPopup_->addAction( addContact_ );
contactActionPopup_->addAction( allowContact_ );
contactActionPopup_->addAction( removeContact_ );
contactActionPopup_->addAction( blockContact_ );
contactActionPopup_->addAction( unblockContact_ );
contactActionPopup_->addSeparator();
contactActionPopup_->addAction( contactProperties_ );
return true;
}
// Initialize the group popup
bool KMessView::initGroupPopup()
{
// Initialize context menu actions
moveGroupDown_ = new QAction( QIcon::fromTheme( "arrow-down" ), i18n("Move Group &Down"), this );
moveGroupUp_ = new QAction( QIcon::fromTheme( "arrow-up" ), i18n("Move Group &Up"), this );
removeGroup_ = new QAction( QIcon::fromTheme( "edit-delete" ), i18n("Re&move Group"), this );
renameGroup_ = new QAction( QIcon::fromTheme( "edit-rename" ), i18n("Re&name Group"), this );
// Connect the actions
connect( moveGroupDown_, SIGNAL(triggered(bool)), this, SLOT( slotMoveGroupDown()) );
connect( moveGroupUp_, SIGNAL(triggered(bool)), this, SLOT( slotMoveGroupUp()) );
connect( removeGroup_, SIGNAL(triggered(bool)), this, SLOT(slotForwardRemoveGroup()) );
connect( renameGroup_, SIGNAL(triggered(bool)), this, SLOT(slotForwardRenameGroup()) );
// Initialize the popup menu
groupActionPopup_ = new QMenu( "KMess", this );
groupActionPopup_->addAction( moveGroupUp_ );
groupActionPopup_->addAction( moveGroupDown_ );
groupActionPopup_->addSeparator();
groupActionPopup_->addAction( renameGroup_ );
groupActionPopup_->addAction( removeGroup_ );
return true;
}
// Generate the lists of actions for the "copy/move contact to group" menus
void KMessView::rebuildContactActions()
{
// Remove all copy and move actions
foreach( QAction *action, groupCopyActionsList_ )
{
copyContactToGroup_->removeAction( action );
groupCopyActionsList_.removeAll( action );
delete action;
}
foreach( QAction *action, groupMoveActionsList_ )
{
moveContactToGroup_->removeAction( action );
groupMoveActionsList_.removeAll( action );
delete action;
}
// Create the list of groups between which contacts can be moved & copied
QList<Group*>groupsList( currentAccount_->getContactList()->getGroupList() );
foreach( Group *group, groupsList )
{
// Disallow copying and moving to special groups
if( group->isSpecialGroup() )
{
continue;
}
// Since we need both a name to display and a group ID to distinguish groups,
// but KActions only support the name, we'll use the Qt objectName to store
// their internal id
const QString& id = group->getId();
QAction *copyAction = new QAction( group->getName(), copyContactToGroup_ );
QAction *moveAction = new QAction( group->getName(), moveContactToGroup_ );
copyAction->setObjectName( id );
moveAction->setObjectName( id );
copyContactToGroup_->addAction( copyAction );
moveContactToGroup_->addAction( moveAction );
connect( copyAction, SIGNAL(triggered(bool)), contactCopyMapper_, SLOT(map()) );
connect( moveAction, SIGNAL(triggered(bool)), contactMoveMapper_, SLOT(map()) );
contactCopyMapper_->setMapping( copyAction, id );
contactMoveMapper_->setMapping( moveAction, id );
groupCopyActionsList_.append( copyAction );
groupMoveActionsList_.append( moveAction );
}
}
// The email label was clicked so open the user's preferred email url
void KMessView::slotEmailLabelClicked()
{
currentAccount_->openMailAtInbox();
}
// A group was expanded or collapsed
void KMessView::slotGroupChanged( const QModelIndex &index )
{
Q_ASSERT( viewModel_ != 0 );
// When searching, the expanded status of the groups is altered, but the settings
// should not be applied to the actual groups
if( ! viewModel_->filterRegExp().isEmpty() )
{
return;
}
// Do nothing if the group is empty
if( viewModel_->index( 0, 0, index ) == QModelIndex() )
{
return;
}
#ifdef KMESSDEBUG_CONTACTLISTMODEL
kmDebug() << "Changed index:" << index;
kmDebug() << "Current index at that row:" << viewModel_->index( index.row(), 0, QModelIndex() );
#endif
// Skip invalid items
if( ! index.isValid()
|| ! viewModel_->hasIndex( index.row(), 0, QModelIndex() )
|| ! index.data().canConvert<QVariantMap>() )
{
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Skipping an invalid index:" << index;
#endif
return;
}
// Get the data from the selected index
const ModelDataList itemData( index.data().toMap() );
// Do nothing if the input index was not valid
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemGroup )
{
return;
}
Group *group = currentAccount_->getContactList()->getGroupById( itemData[ "id" ].toString() );
// Stop if the group had not been found
if( ! group )
{
return;
}
group->setExpanded( contactListView_->isExpanded( index ) );
// Save the changed setting
currentAccount_->getContactList()->saveProperties( group );
}
// Show the context menu
void KMessView::showContextMenu( const QPoint &point )
{
const ContactList *contactList = currentAccount_->getContactList();
// Get the data from the selected index
const ModelDataList itemData( getItemData( contactListView_->indexAt( point ) ) );
// The index was empty
if( itemData.isEmpty() )
{
return;
}
// Depending on the item type, show the appropriate menu
if( itemData[ "type" ] == ContactListModelItem::ItemContact )
{
const QString handle( itemData[ "handle" ].toString() );
const Contact *contact = contactList->getContactByHandle( handle );
bool isBlocked = contact->isBlocked();
bool isFriend = contact->isFriend();
const QStringList& contactGroups = contact->getGroupIds();
// Only if not added yet
addContact_ ->setVisible( ! isFriend );
// Only allow if contact is on the "removed" group.
allowContact_ ->setVisible( ! isFriend && ! contact->isAllowed() && ! isBlocked );
// Only one of these is enabled:
blockContact_ ->setVisible( ! isBlocked );
unblockContact_ ->setVisible( isBlocked );
removeContact_ ->setVisible( isFriend );
// Only show the group management options when it makes sense to; contacts
// in the Allowed and Removed groups are technically not part of your friends list
if( itemData[ "group" ] == SpecialGroups::ALLOWED
|| itemData[ "group" ] == SpecialGroups::REMOVED )
{
copyContactToGroup_->setVisible( false );
moveContactToGroup_->setVisible( false );
removeFromGroup_ ->setVisible( false );
}
else if( currentAccount_->getContactListDisplayMode() == CurrentAccount::VIEW_BYSTATUS
|| ( currentAccount_->getContactListDisplayMode() == CurrentAccount::VIEW_MIXED
&& ! contact->isOnline() )
)
{
copyContactToGroup_->setVisible( true );
moveContactToGroup_->setVisible( false );
removeFromGroup_ ->setVisible( false);
}
else if( contactGroups.isEmpty() )
{
moveContactToGroup_->setVisible( true );
copyContactToGroup_->setVisible( false );
removeFromGroup_ ->setVisible( false );
}
else
{
moveContactToGroup_->setVisible( isFriend );
copyContactToGroup_->setVisible( isFriend );
removeFromGroup_ ->setVisible( isFriend );
}
// Show all copy and move actions
foreach( QAction *action, groupCopyActionsList_ )
action->setVisible( true );
foreach( QAction *action, groupMoveActionsList_ )
action->setVisible( true );
// Then hide only the invalid choices
int enabledCopyActions = groupCopyActionsList_.count();
int enabledMoveActions = groupMoveActionsList_.count();
foreach( const QString &groupId, contactGroups )
{
foreach( QAction *action, groupCopyActionsList_ )
{
// See initContactPopup() for the choice of using objectName to store the group ID
if( action->objectName() == groupId )
{
enabledCopyActions--;
action->setVisible( false );
break;
}
}
foreach( QAction *action, groupMoveActionsList_ )
{
// See initContactPopup() for the choice of using objectName to store the group ID
if( action->objectName() == groupId )
{
enabledMoveActions--;
action->setVisible( false );
break;
}
}
}
// If no actions have been enabled in a menu, disable it too
if( enabledCopyActions <= 0 )
{
copyContactToGroup_ ->setVisible( false );
}
if( enabledMoveActions <= 0 )
{
moveContactToGroup_ ->setVisible( false );
}
// Clear the previous actions to avoid duplication of links
qDeleteAll( groupCopyLinkActionsList_ );
groupCopyLinkActionsList_.clear();
// If detailed contact information is available, search for links
if( contact != 0 )
{
// Append friendly name and personal message so cycle only one time to search links
const QString nameAndPm( contact->getFriendlyName() + ' ' + contact->getPersonalMessage() );
QStringList links;
//Initialize index, link RegExp and found boolean for insert separator only at first found link
int pos = 0;
QRegularExpression linkRegExp( "(http://|https://|ftp://|sftp://|www\\..)\\S+" );
QRegularExpressionMatch match;
while( ( match = linkRegExp.match( nameAndPm, pos ) ).hasMatch() )
{
// Grep the found link and update the position for the next cycle
QString foundLink( match.captured( 0 ) );
pos = match.capturedEnd();
// Skip duplicated links
if( links.contains( foundLink ) )
{
continue;
}
links.append( foundLink );
// Put the found link into QAction, replace & with && to avoid shortcut problem and use tooltip
// for future grep of link ( to avoid shortcut problem too )
QAction *popupCopyLink = new QAction( foundLink.replace( '&', "&&" ), this );
popupCopyLink->setToolTip( foundLink );
connect( popupCopyLink, SIGNAL( triggered( bool ) ), this, SLOT( copyText() ) );
// Append current QAction to the copy link list
groupCopyLinkActionsList_.append( popupCopyLink );
// Add action to copy menu
popupCopyMenu_->addAction( popupCopyLink );
}
}
// Forbid chatting with yourself
chatWithContact_->setEnabled( handle != currentAccount_->getHandle() );
// Set tooltip ( informations ) of other copy actions
popupCopyFriendlyName_ ->setToolTip( contact->getFriendlyName() );
popupCopyPersonalMessage_->setToolTip( contact->getPersonalMessage() );
popupCopyHandle_ ->setToolTip( contact->getHandle() );
popupCopyMusic_ ->setToolTip( contact->getCurrentMediaString() );
// Hide the unavailable options
popupCopyPersonalMessage_->setVisible( ! contact->getPersonalMessage ().isEmpty() );
popupCopyMusic_ ->setVisible( ! contact->getCurrentMediaString().isEmpty() );
// Show the popup
contactActionPopup_->popup( QCursor::pos() );
}
else if( itemData[ "type" ] == ContactListModelItem::ItemGroup )
{
const Group *group = contactList->getGroupById( itemData[ "id" ].toString() );
// No context menu for special groups
if( ! group || group->isSpecialGroup() )
{
return;
}
int pos = group->getSortPosition();
moveGroupUp_ ->setEnabled( contactList->getGroupBySortPosition( pos - 1, false ) != 0 );
moveGroupDown_ ->setEnabled( contactList->getGroupBySortPosition( pos + 1, true ) != 0 );
// Show the popup
groupActionPopup_->popup( QCursor::pos() );
}
else
{
kmWarning() << "Invalid type of list item" << itemData[ "type" ];
}
}
// Copy some details of the contact to the clipboard.
void KMessView::copyText()
{
// Grep the action sender for copy the tooltip that contains the information
QAction *action = static_cast<QAction*>( const_cast<QObject*>( sender() ) );
QApplication::clipboard()->setText( action->toolTip() );
}
// Show the contact & group tool tip
void KMessView::showToolTip( const QPoint &point )
{
const QModelIndex index( contactListView_->indexAt( contactListView_->mapFromGlobal( point ) ) );
QString tipText;
// Read the actual data off the model index
const ModelDataList itemData( index.data().toMap() );
// Do nothing if the index is not valid
if( ! index.isValid() || itemData.isEmpty() )
{
return;
}
const QString textAlignment( QApplication::isRightToLeft() ? "right" : "left" );
const QString textDirection( "dir='" + QString(QApplication::isRightToLeft() ? "rtl" : "ltr") + "'" );
// Paint the different types of list items
// see ContactListModelItem::data() for a list of itemData elements (role == DataRole)
switch( itemData[ "type" ].toInt() )
{
case ContactListModelItem::ItemContact:
{
tipText = "<html " + textDirection + "><table border='0' " + textDirection + ">"
"<tr>"
"<td valign='top'>"
"<table style='border-width:1px;border-color:palette(mid);border-style:solid;'>"
"<tr><td><img src='%1' width='96' height='96'/></td></tr>"
"</table>"
"</td>"
"<td align='" + textAlignment + "' valign='top' style='padding-" + textAlignment + ":10px;'>"
"<b>%2</b> (%3)<br/>"
"%4 %5 %6"
"</td>"
"</tr>"
"<tr>"
"<td colspan='2'><hr width='98%'/></td>"
"</tr>"
"</table>"
"<table border='0' " + textDirection + " align='" + textAlignment + "'>"
"%7%8%9%10%11"
"</table>"
"</html>";
// Add the display picture (if there is any), or the default KMess logo
tipText = tipText.arg( itemData[ "displayPicture" ].toString() );
// Add the contact's real name - the alias is not shown:
// you can read it in the contact list, after all
QString friendlyName( itemData[ "trueFriendly" ].toString() );
// Escape HTML, replace standard emoticons, but no links and formatting
RichTextParser::parseMsnString( friendlyName, true, true, false, false );
tipText = tipText.arg( friendlyName );
// Add status
tipText = tipText.arg( MsnStatus::getName( (Status) itemData[ "status" ].toInt() ) );
// Add the personal message
QString personalMessage( itemData[ "personalMessage" ].toString() );
if( ! personalMessage.isEmpty() )
{
// Escape HTML, replace standard emoticons, but no links and formatting
RichTextParser::parseMsnString( personalMessage, true, true, false, false );
tipText = tipText.arg( "<i>" + personalMessage + "</i><br/>" );
}
else
{
tipText = tipText.arg( "" );
}
// If the contact is listening to music, show it
const QString mediaType ( itemData[ "mediaType" ].toString() );
const QString mediaString( itemData[ "mediaString" ].toString() );
if( ! mediaString.isEmpty() )
{
QString mediaEmoticon;
// Determine icon for special media types
if( mediaType == "Music" )
{
mediaEmoticon = EmoticonManager::instance()->getReplacement( "(8)", true );
}
else if( mediaType == "Gaming" )
{
mediaEmoticon = EmoticonManager::instance()->getReplacement( "(xx)", true );
}
tipText = tipText.arg( "<i><small>" +
mediaEmoticon + mediaString.toHtmlEscaped() +
"</small></i><br/>" );
}
else
{
tipText = tipText.arg( "" );
}
// Add a message about our presence in this contact's list
// fontknocker: removed until reverse list memberships are resolved.
// if( itemData[ "isReverse" ].toBool() )
// {
tipText = tipText.arg( "" );
// }
// else
// {
// tipText = tipText.arg( "<i>" +
// i18nc( "Message in list tooltip",
// "This contact does not have you in his or her contact list." ) +
// "</i>" );
// }
// Here starts the list of simple property:value lines
// Adding a space to better separate the properties and values
QString propertyLine;
if( QApplication::isLeftToRight() )
{
propertyLine = "<tr><td align='right'><i>%1:</i>&nbsp;</td><td align='left'>%2</td></tr>";
}
else
{
propertyLine = "<tr><td align='right'>%2</td><td align='left'>&nbsp;<i>%1:</i></td></tr>";
}
// Add handle
tipText = tipText.arg( propertyLine.arg( i18nc( "Contact email label in list tooltip", "Email address" ) )
.arg( itemData[ "handle" ].toString() ) );
// Add client identifier
const QString clientName( itemData[ "clientName" ].toString() );
if( ! clientName.isEmpty() )
{
tipText = tipText.arg( propertyLine.arg( i18nc( "Contact Live Messenger client label in list tooltip", "Client" ) )
.arg( clientName.toHtmlEscaped() ) );
}
else
{
tipText = tipText.arg( "" );
}
//add info whether the contact is blocked
QString blockedString;
if( itemData[ "isBlocked" ].toBool() )
{
blockedString = i18n( "Yes" );
}
else
{
blockedString = i18n( "No" );
}
tipText = tipText.arg( propertyLine.arg( i18nc( "Contact blocked status label in list tooltip", "Blocked" ) )
.arg( blockedString ) );
// Add last seen date
const QDateTime lastSeen( itemData[ "lastSeen" ].toDateTime() );
QString lastSeenString;
if( ( (Status) itemData[ "status" ].toInt() ) != STATUS_OFFLINE )
{
lastSeenString = i18n( "Connected" );
}
else if( lastSeen.toSecsSinceEpoch() == 0
|| lastSeen.toSecsSinceEpoch() == UINT_MAX
|| lastSeen.isNull() )
{
lastSeenString = i18n( "Not seen yet" );
}
else
{
lastSeenString = lastSeen.toString();
}
tipText = tipText.arg( propertyLine.arg( i18nc( "Contact last presence label in list tooltip", "Last seen" ) )
.arg( lastSeenString ) );
// Add last message date
const QDateTime lastMessage( itemData[ "lastMessage" ].toDateTime() );
QString lastMessageString;
if( lastMessage.toSecsSinceEpoch() == 0
|| lastMessage.toSecsSinceEpoch() == UINT_MAX
|| lastMessage.isNull() )
{
lastMessageString = i18n( "No messages yet" );
}
else
{
lastMessageString = lastMessage.toString();
}
tipText = tipText.arg( propertyLine.arg( i18nc( "Contact last message label in list tooltip", "Last message" ) )
.arg( lastMessageString ) );
}
break;
case ContactListModelItem::ItemGroup:
tipText = "<qt><div " + textDirection + ">%1<br/><i>(%2)</i></div></qt>";
// Add the group name
tipText = tipText.arg( i18nc( "Group name in group tooltip", "Group <b>%1</b>", itemData[ "name" ].toString() ) );
// Non-empty and non-special groups also have an Online contacts count.
// (Individuals is treated as a non-special group)
if( ( ! itemData[ "isSpecialGroup" ].toBool() || itemData[ "id" ].toString() == SpecialGroups::INDIVIDUALS )
&& itemData[ "totalContacts" ].toInt() > 0 )
{
tipText = tipText.arg( "%1%2" ).arg( i18ncp( "Contact counters in normal group tooltip, first part",
"%1 contact, ", "%1 contacts, ",
itemData[ "totalContacts" ].toInt() ) )
.arg( i18ncp( "Contact counters in normal group tooltip, second part",
"%1 online", "%1 online",
itemData[ "onlineContacts" ].toInt() ) );
}
else
{
tipText = tipText.arg( i18ncp( "Contacts count in special group tooltip",
"%1 contact", "%1 contacts", itemData[ "totalContacts" ].toInt() ) );
}
break;
default:
// Nothing to do!
return;
}
// Show the tooltip
QToolTip::showText( point, tipText, contactListView_, contactListView_->visualRect( index ) );
}
// Email the current contact
void KMessView::slotEmailContact()
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was not valid, do nothing
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
{
return;
}
// Send mail to that account
currentAccount_->openMailAtCompose( itemData[ "handle" ].toString() );
}
// An item was double clicked
void KMessView::slotItemDoubleClicked( const QModelIndex &index )
{
// Detect accidental clicks
if( QGuiApplication::styleHints()->singleClickActivation() )
{
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmDebug() << "Accidental double click detected, skipping click event.";
#endif
return;
}
slotItemClicked( index );
}
void KMessView::slotItemSingleClicked( const QModelIndex &index )
{
bool accidental = false;
if( ! QGuiApplication::styleHints()->singleClickActivation() )
{
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmDebug() << "Accidental single click detected, skipping click event.";
#endif
accidental = true;
}
slotItemClicked( index, accidental );
}
// An item was clicked
void KMessView::slotItemClicked( const QModelIndex &index, const bool accidental )
{
// Get the data from the selected index
const ModelDataList itemData( getItemData( index ) );
// The index was empty
if( itemData.isEmpty() )
{
kmWarning() << "Invalid list index given!";
return;
}
switch( itemData[ "type" ].toInt() )
{
// Expand/collapse the groups when clicked
case ContactListModelItem::ItemGroup:
// Do nothing if the group is empty
if( viewModel_->index( 0, 0, index ) == QModelIndex() )
{
return;
}
// Keep the group expansion state volatile while filtering (don't save it)
if( ! viewModel_->filterRegExp().isEmpty() )
{
contactListView_->setExpanded( index, ! contactListView_->isExpanded( index ) );
}
else
{
contactListView_->setExpanded( index, ! itemData[ "isExpanded" ].toBool() );
}
break;
// Request a chat with that contact
case ContactListModelItem::ItemContact:
if( ! accidental )
{
// Tell the Chat Master we'd like to start a chat.
emit requestChat( itemData[ "handle" ].toString() );
}
break;
default:
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmWarning() << "Nothing to do!";
#endif
break;
}
}
// Move down the selected group
void KMessView::slotMoveGroupDown()
{
const ContactList *contactList = currentAccount_->getContactList();
// Get the group data from the selected index
const ModelDataList itemData( getItemData() );
Group *group = contactList->getGroupById( itemData[ "id" ].toString() );
if( ! group || itemData.isEmpty() )
{
return;
}
// Obtain the next listed group
Group *nextGroup = contactList->getGroupBySortPosition( group->getSortPosition() + 1, true );
if( ! nextGroup )
{
return;
}
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmDebug() << "Selected group:" << group->getId() << "to move down in place of group:" << nextGroup->getId();
#endif
// Get the group sorting position for both groups
int oldGroupPos = group ->getSortPosition();
int newGroupPos = nextGroup->getSortPosition();
// Swap them
group ->setSortPosition( newGroupPos );
nextGroup->setSortPosition( oldGroupPos );
// Reset old sort value so KMess is not confused (XXX HACK FIXME OMG)
group ->setOldSortPosition( 0 );
nextGroup->setOldSortPosition( 0 );
}
// Move up the selected group
void KMessView::slotMoveGroupUp()
{
const ContactList *contactList = currentAccount_->getContactList();
// Get the group data from the selected index
const ModelDataList itemData( getItemData() );
Group *group = contactList->getGroupById( itemData[ "id" ].toString() );
if( ! group || itemData.isEmpty() )
{
return;
}
// Obtain the previous listed group
Group *prevGroup = contactList->getGroupBySortPosition( group->getSortPosition() - 1, false );
if( ! prevGroup )
{
return;
}
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmDebug() << "Selected group:" << group->getId() << "to move up in place of group:" << prevGroup->getId();
#endif
// Get the group sorting position for both groups
int oldGroupPos = group ->getSortPosition();
int newGroupPos = prevGroup->getSortPosition();
// Swap them
group ->setSortPosition( newGroupPos );
prevGroup->setSortPosition( oldGroupPos );
// Save the changed setting
contactList->saveProperties( group );
contactList->saveProperties( prevGroup );
// Reset old sort value so KMess is not confused (XXX HACK FIXME OMG)
group ->setOldSortPosition( 0 );
prevGroup->setOldSortPosition( 0 );
}
// Forward the "add contact" menu action
void KMessView::slotForwardAddContact()
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was not empty
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
{
return;
}
// Add that contact
emit addContact( itemData[ "handle" ].toString() );
}
// Forward the "allow contact" menu action
void KMessView::slotForwardAllowContact()
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was not empty
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
{
return;
}
// Allow that contact
emit allowContact( itemData[ "handle" ].toString() );
}
// Forward the "block contact" menu action
void KMessView::slotForwardBlockContact()
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was not empty
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
{
return;
}
// Block that contact
emit blockContact( itemData[ "handle" ].toString() );
}
// Called when a contact should be copied
void KMessView::slotForwardCopyContact( const QString &newGroupId )
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was not empty
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
{
return;
}
// Copy that contact
emit copyContact( itemData[ "handle" ].toString(), newGroupId );
}
// Called when a contact should be moved
void KMessView::slotForwardMoveContact( const QString &newGroupId )
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The selection was not valid
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
{
return;
}
// Get the old group ID, and if it's not valid, set it to zero
QString oldGroupId;
if( itemData[ "isInSpecialGroup" ].toBool() == false )
{
oldGroupId = itemData[ "group" ].toString();
}
else
{
oldGroupId = "0";
}
// If the old and new groups are the same one, do nothing
if( oldGroupId == newGroupId )
{
return;
}
// Move that contact
emit moveContact( itemData[ "handle" ].toString(), oldGroupId, newGroupId );
}
// Forward the "remove contact" menu action
void KMessView::slotForwardRemoveContact()
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was empty
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
{
return;
}
// Remove that contact
emit removeContact( itemData[ "handle" ].toString() );
}
void KMessView::slotForwardRemoveFromGroup()
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was invalid
if( itemData.isEmpty() )
{
return;
}
// Get the ID of the group which contained this instance of the contact
const ModelDataList parentItemData( getItemData( selectionModel_->selectedIndexes().first().parent() ) );
if( parentItemData.isEmpty() || parentItemData[ "type" ] != ContactListModelItem::ItemGroup )
{
return;
}
// Remove that contact
emit removeContactFromGroup( itemData[ "handle" ].toString(), parentItemData[ "id" ].toString() );
}
// Forward the "remove group" menu action
void KMessView::slotForwardRemoveGroup()
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was invalid
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemGroup )
{
return;
}
// Remove that group
emit removeGroup( itemData[ "id" ].toString() );
}
// Forward the "rename group" menu action
void KMessView::slotForwardRenameGroup()
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was invalid
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemGroup )
{
return;
}
// Rename that group
emit renameGroup( itemData[ "id" ].toString() );
}
// Forward the "start chat" menu action
void KMessView::slotForwardStartChat()
{
// Only one index can be returned at once by the contact list (has single selection mode)
slotItemClicked( selectionModel_->selectedIndexes().first() );
}
// Forward the "unblock contact" menu action
void KMessView::slotForwardUnblockContact()
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was not empty
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
{
return;
}
// Unblock that contact
emit unblockContact( itemData[ "handle" ].toString() );
}
/**
* @brief Update the friendly name from the inline editing label.
*
* Sends to the server the new friendly name selected by clicking
* the friendly name inline editing label.
*
* @param refreshOnly When true, the FN widget is only updated with the
* current account's name. When false, it is updated
* with the default message, and nothing is sent to
* the server.
*/
void KMessView::updateFriendlyName( bool refreshOnly )
{
const QString defaultTooltip( i18nc( "Default friendly name tooltip", "Click here to change your friendly name" ) );
const QString defaultName( currentAccount_->getFriendlyName( STRING_ORIGINAL ).trimmed() );
// When refreshing the message or when the focus losing isn't voluntary, don't update it on the server
if( refreshOnly || ! isActiveWindow() || ! topLevelWidget()->isActiveWindow() )
{
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmDebug() << "Refreshing name only: initial update or non-intentional focus lose.";
#endif
// Refresh the message
friendlyNameInput_->setToolTip( defaultTooltip );
friendlyNameInput_->setText( defaultName );
return;
}
// Get message
QString newName( friendlyNameInput_->text().trimmed() );
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmDebug() << "Updating friendly name to:" << newName;
#endif
// Set a default name (the email address) if the field is left empty
if( newName.isEmpty() )
{
newName = currentAccount_->getHandle();
}
// Only update the name if it's been changed
if( defaultName == newName )
{
return;
}
// Update the name and save the changes
currentAccount_->setFriendlyName( newName );
friendlyNameInput_->setToolTip( newName );
// Commit changes to disk, now
currentAccount_->saveProperties();
// Notify changes to the MSN server
emit changeFriendlyName( newName );
}
/**
* @brief Update the personal message from the inline editing label.
*
* Sends to the server the new personal message selected by clicking
* the personal message inline editing label.
*
* @param refreshOnly When true, the PM widget is only updated with the
* current account's message. When false, it is updated
* with the default message, and nothing is sent to
* the server.
*/
void KMessView::updatePersonalMessage( bool refreshOnly )
{
const QString defaultMessage( i18nc( "Default personal message shown in the contact list", "[i]Click to set a personal message[/i]" ) );
const QString defaultTooltip( i18nc( "Default personal message tooltip", "Click here to insert a message to show to your contacts: they will see it along with your friendly name" ) );
// When refreshing the message or when the focus losing isn't voluntary, don't update it on the server
if( refreshOnly || ! isActiveWindow() || ! topLevelWidget()->isActiveWindow() )
{
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmDebug() << "Refreshing message only: initial update or non-intentional focus lose.";
#endif
// Refresh the message
const QString message( currentAccount_->getPersonalMessage( STRING_ORIGINAL ).trimmed() );
if( message.isEmpty() )
{
personalMessageInput_->setToolTip( defaultTooltip );
personalMessageInput_->setText( defaultMessage, true );
}
else
{
personalMessageInput_->setToolTip( message );
personalMessageInput_->setText( message );
}
return;
}
// Get message
const QString message( personalMessageInput_->text().trimmed() );
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmDebug() << "Updating personal message to:" << message;
#endif
// Protect against weird situations where the label is seen as text.
// The lost focus event is also sent when the while window looses focus
if( message == defaultMessage )
{
kmWarning() << "Personal message label was about to be used as personal message!";
return;
}
// Only update the message if it's been changed
if( currentAccount_->getPersonalMessage( STRING_ORIGINAL ) != message )
{
// Update message and save the changes
currentAccount_->setPersonalMessage( message );
personalMessageInput_->setToolTip( message );
// Commit changes to disk, now
currentAccount_->saveProperties();
// Notify changes to the MSN server
emit changePersonalMessage( message );
}
// Restore the placeholder.
if( message.isEmpty() )
{
personalMessageInput_->setToolTip( defaultTooltip );
personalMessageInput_->setText( defaultMessage, true );
}
}
// Delay a bit updating the groups status
void KMessView::slotScheduleGroupUpdate()
{
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Scheduling another groups update";
#endif
updateTimer_.start( 250 );
}
// Slot for searching contact
void KMessView::slotSearchContact( const QString& searchFor )
{
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Search expression changed to:" << searchFor;
#endif
QString searchExpression;
const QString regexpMagic( "regexp:" );
// If the search string starts with "regexp:", allow searching with a regexp :)
if( searchFor.startsWith( regexpMagic ) )
{
searchExpression = searchFor.mid( regexpMagic.length() );
// Colorize the search bar when using regexps, according to if they're valid or invalid
if( QRegularExpression( searchExpression ).isValid() )
{
searchEdit_->setStyleSheet( "background-color: #bbffbb" );
}
else
{
searchEdit_->setStyleSheet( "background-color: #ffbbbb" );
}
}
else
{
// Remove trailing spaces and other unneeded (invisible) characters, then escape the searchstring
searchExpression = QRegularExpression::escape( searchFor.simplified() );
searchEdit_->setStyleSheet( "" );
}
// True if the user was filtering for something and wanted to filter for something else;
// used to maintain the group expansion states while refining the search terms
bool isFilteringAgain = ( ! searchExpression.isEmpty() && ! viewModel_->filterRegExp().isEmpty() );
viewModel_->setFilterRegExp( searchExpression );
// Show or hide groups which do or do not contain results
for( int row = viewModel_->rowCount() - 1; row >= 0; --row )
{
const QModelIndex index( viewModel_->index( row, 0, QModelIndex() ) );
// Skip invalid items
if( ! index.isValid() )
{
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Skipping invalid index" << index;
#endif
continue;
}
// Access the index's data
const ModelDataList itemData( getItemData( index ) );
// Skip items which have no children
if( itemData.isEmpty() || itemData[ "type" ].toInt() != ContactListModelItem::ItemGroup )
{
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Skipping non-group index" << index;
kmDebug() << "-- Index data:" << itemData;
#endif
continue;
}
// Hide groups which don't contain results
if( ! searchFor.isEmpty() )
{
bool isHidden = ( ! viewModel_->hasChildren( index ) );
contactListView_->setRowHidden( row, QModelIndex(), isHidden );
if( ! isFilteringAgain )
{
contactListView_->setExpanded( index, true );
}
}
// When the search terms are removed, enable back all groups
else
{
contactListView_->setRowHidden( row, QModelIndex(), false );
contactListView_->setExpanded( index, itemData[ "isExpanded" ].toBool() );
}
}
}
// Display the chat history of the current contact
void KMessView::slotShowChatHistory()
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was not empty
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
{
return;
}
ChatHistoryDialog *dialog = new ChatHistoryDialog( this );
// Show the selected contact. If no log exists
// (no chats with that person yet), then don't show the History dialog
if( dialog->setInitialContact( currentAccount_->getHandle(), itemData[ "handle" ].toString() ) )
{
dialog->show();
}
else
{
if( currentAccount_->getSaveChats() )
{
KMessageBox::error( this,
i18n( "No chat logs could be found for this contact." ),
i18n( "No chat history found" ) );
}
else
{
KMessageBox::error( this,
i18n( "No chat logs could be found for this contact. Note that new chats are not logged; if you want your chats to be logged, you can enable it in your account settings." ),
i18n( "No chat history found") );
}
delete dialog;
}
}
// Display the profile of the current contact
void KMessView::slotShowContactProfile()
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was not empty
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
{
return;
}
const QString handle( itemData[ "handle" ].toString() );
Q_UNUSED( handle );
KMessShared::openBrowser( QUrl( "https://nina.chat/contacts/" ) );
}
// Display the properties of the current contact
void KMessView::slotShowContactProperties()
{
// Get the data from the currently selected index
const ModelDataList itemData( getItemData() );
// The index was not empty
if( itemData.isEmpty() || itemData[ "type" ] != ContactListModelItem::ItemContact )
{
return;
}
// Get the contact data to pass to the properties dialog
Contact *contact = currentAccount_->getContactList()->getContactByHandle( itemData[ "handle" ].toString() );
// If no corresponding contact has been found, do nothing
if( ! contact )
{
return;
}
ContactPropertiesDialog *dialog = new ContactPropertiesDialog( this );
connect( dialog, SIGNAL( removeContactFromGroup(QString,QString) ),
this, SIGNAL( removeContactFromGroup(QString,QString) ) );
connect( dialog, SIGNAL( addContactToGroup(QString,QString) ),
this, SIGNAL( copyContact(QString,QString) ) );
dialog->launch( contact );
delete dialog;
}
//toggle now listening
void KMessView::slotToggleNowListening()
{
currentAccount_->setShowNowListening( ! currentAccount_->getShowNowListening() );
}
// Update the users display pic
void KMessView::slotUpdateDisplayPicture()
{
// Adjust the picture size depending on the rest of the widgets:
// it should be small only when both now playing and new email info are off.
int size = 96;
if( ( ! currentAccount_->getShowNowListening() || ! songPlaying_ )
&& ( ! currentAccount_->getShowEmail() || ! currentAccount_->getEmailSupported() ) )
{
size = 64;
}
currentAccountDisplayPic_->setFixedSize( size, size );
// The user does not want to show a display pic, so just show an icon for their sign in status
if( ! currentAccount_->getShowPicture() )
{
displayPicFrame_->hide();
return;
}
// Show the picture frame first
displayPicFrame_->show();
// The picture is unchanged, do nothing
const QString pictureFileName( currentAccount_->getPicturePath() );
if( displayPicFrame_->property( "PictureFileName" ) == pictureFileName )
{
#ifdef KMESSDEBUG_KMESSVIEW
kmDebug() << "No need to change the display picture";
#endif
return;
}
#ifdef KMESSDEBUG_KMESSVIEW
kmDebug() << "Changing display picture to path:" << pictureFileName;
#endif
// Change the picture
const QPixmap image( pictureFileName );
currentAccountDisplayPic_->setPixmap( image );
displayPicFrame_->setProperty( "PictureFileName", pictureFileName );
}
// Change whether or not the email label is displayed based on account settings.
void KMessView::slotUpdateEmailDisplay()
{
if ( currentAccount_->getShowEmail() && currentAccount_->getEmailSupported() )
{
emailFrame_->setHidden(false);
//emailFrame_->setMargin(statusLayout_->margin());
emailFrame_->setMaximumHeight(40);
}
else
{
emailFrame_->setHidden(true);
//emailFrame_->setMargin(0);
emailFrame_->setMaximumHeight(0);
}
}
// Update the expanded status of the list groups
void KMessView::slotUpdateGroups()
{
Q_ASSERT( viewModel_ != 0 );
// When searching, the expanded status of the groups is altered, but the settings
// should not be applied to the actual groups
if( ! viewModel_->filterRegExp().isEmpty() )
{
return;
}
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Updating groups expanded status for" << viewModel_->rowCount() << "rows";
#endif
for( int row = viewModel_->rowCount() - 1; row >= 0; row-- )
{
const QModelIndex index( viewModel_->index( row, 0, QModelIndex() ) );
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Current row:" << row << "of" << viewModel_->rowCount();
kmDebug() << "Index:" << index;
kmDebug() << "Model has it?" << viewModel_->hasIndex( row, 0, QModelIndex() );
#endif
// Skip invalid items
if( ! index.isValid()
|| ! viewModel_->hasIndex( row, 0, QModelIndex() )
|| ! index.data().canConvert<QVariantMap>() )
{
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Skipping an invalid item at row" << row << ":" << index;
#endif
continue;
}
// Access the index's data
const ModelDataList itemData( index.data().toMap() );
// Skip items which have no children
if( itemData.isEmpty() || itemData[ "type" ].toInt() != ContactListModelItem::ItemGroup )
{
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Skipping a non-group at row" << row << ":" << index;
kmDebug() << "Skipped item's details:" << itemData;
#endif
continue;
}
// Set the expanded status
contactListView_->setExpanded( index, itemData[ "isExpanded" ].toBool() );
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Group" << itemData[ "name" ].toString() << "is"
<< ( itemData[ "isExpanded" ].toBool() ? "expanded" : "contracted" );
#endif
}
#ifdef KMESSDEBUG_KMESSVIEW_ITEM_VISIBILITY
kmDebug() << "Update done.";
#endif
}
// Update the email count.
void KMessView::slotUpdateNoEmails()
{
const QString message( i18np( "%1 new email message",
"%1 new email messages",
currentAccount_->getNoEmails() ) );
// Update the label
emailLabel_->setText( message );
// Set the user's preferred email website.
emailLabel_->setUrl( currentAccount_->getEmailUrl() );
}
// set the icon and tooltip for the now listening toggle button.
void KMessView::slotUpdateNowListeningToggle()
{
if( currentAccount_->getShowNowListening() )
{
nowListeningPushButton_->setIcon( KIconLoader::global()->loadIcon( "audio-volume-high", KIconLoader::Small, 0, KIconLoader::DisabledState ) );
nowListeningPushButton_->setToolTip( i18n( "Show contacts what you are listening to (click to disable)" ) );
}
else
{
nowListeningPushButton_->setIcon( KIconLoader::global()->loadIcon( "audio-volume-muted", KIconLoader::Small, 0, KIconLoader::DisabledState ) );
nowListeningPushButton_->setToolTip( i18n( "Show contacts what you are listening to (click to enable)" ) );
}
}
// Update the user's status.
void KMessView::slotUpdateUserStatus()
{
const Status status( currentAccount_->getStatus() );
#ifdef KMESSDEBUG_KMESSVIEW_GENERAL
kmDebug() << "Updating user status to:" << MsnStatus::getName( status );
#endif
// Update the widgets
updateFriendlyName( true );
updatePersonalMessage( true );
statusButton_->setText( MsnStatus::getName( status ) );
statusButton_->setIcon( MsnStatus::getIcon( status ) );
}
// Update the contact list widgets
void KMessView::slotUpdateView()
{
QString birdStyle;
// Hide the tree branches
QString listStyle( "QTreeView::branch:has-siblings:adjoins-item { "
" border-image: url(empty.png) 0; "
"} "
"QTreeView::branch:!has-children:!has-siblings:adjoins-item { "
" border-image: url(empty.png) 0; "
"} " );
// Set a background pixmap (but allow not to, by popular request)
if( currentAccount_->getShowContactListBird() )
{
birdStyle = "QTreeView { "
" background-attachment: fixed; "
" background-origin: content; "
" background-position: bottom right; "
" background-repeat: no-repeat; "
" background-image: url(" +
KIconLoader::global()->iconPath( "background", KIconLoader::User ) + ");"
"}"
;
}
contactListView_->setStyleSheet( listStyle + birdStyle );
}