Warning, file /network/ktp-contact-list/global-presence-chooser.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002  * Global Presence - A Drop down menu for selecting presence
0003  *
0004  * Copyright (C) 2011 David Edmundson <kde@davidedmundson.co.uk>
0005  *
0006  * This library is free software; you can redistribute it and/or
0007  * modify it under the terms of the GNU Lesser General Public
0008  * License as published by the Free Software Foundation; either
0009  * version 2.1 of the License, or (at your option) any later version.
0010  *
0011  * This library is distributed in the hope that it will be useful,
0012  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0013  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0014  * Lesser General Public License for more details.
0015  *
0016  * You should have received a copy of the GNU Lesser General Public
0017  * License along with this library; if not, write to the Free Software
0018  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
0019  */
0020 
0021 #include "global-presence-chooser.h"
0022 
0023 #include "dialogs/custom-presence-dialog.h"
0024 #include "dialogs/advanced-presence-dialog.h"
0025 
0026 #include <KTp/presence.h>
0027 #include <KTp/global-presence.h>
0028 #include <KTp/Models/presence-model.h>
0029 
0030 #include <KLocalizedString>
0031 #include <KSharedConfig>
0032 #include <KLineEdit>
0033 #include <KPixmapSequence>
0034 #include <KPixmapSequenceOverlayPainter>
0035 #include <KMessageBox>
0036 #include <KIconLoader>
0037 
0038 #include <TelepathyQt/Account>
0039 
0040 #include <QFontDatabase>
0041 #include <QMouseEvent>
0042 #include <QToolTip>
0043 #include <QStyle>
0044 #include <QPushButton>
0045 #include <QMenu>
0046 #include <QPointer>
0047 
0048 extern const QString KDED_STATUS_MESSAGE_PARSER_WHATSTHIS(
0049         i18n("<p>Tokens can be used wherever a status message can be set to create a dynamic status message.</p>")
0050         + i18n("<p><strong>%tr+&lt;val&gt;</strong>: Countdown to 0 from <strong>&lt;val&gt;</strong> minutes. e.g. %tr+30</p>")
0051         + i18n("<p><strong>%time+[&lt;val&gt;]</strong>: The current local time, or if a value is specified, the local time <strong>&lt;val&gt;</strong> minutes in the future. e.g. %time+10</p>")
0052         + i18n("<p><strong>%utc+[&lt;val&gt;]</strong>: The current UTC time, or if a value is specified, the UTC time <strong>&lt;val&gt;</strong> minutes into the future. e.g. %utc</p>")
0053         + i18n("<p><strong>%te+[&lt;val&gt;]</strong>: Time elapsed from message activation. Append an initial elapsed time &quot;&lt;val&gt;&quot; in minutes. e.g. %te+5</p>")
0054         + i18n("<p><strong>%title</strong>: Now Playing track title.</p>")
0055         + i18n("<p><strong>%artist</strong>: Now Playing track or album artist.</p>")
0056         + i18n("<p><strong>%album</strong>: Now Playing album.</p>")
0057         + i18n("<p><strong>%track</strong>: Now Playing track number.</p>")
0058         + i18n("<p><strong>%um+[&lt;val&gt;]</strong>: When specified globally or in an account presence status message, overrides all automatic presence messages. When specified in an automatic presence status message, is substituted for the global or account presence status message (if specified). When <strong>val = g</strong> in an account presence status message or an automatic presence status message, overrides the account presence status message or automatic presence status message with the global presence status message. e.g. %um, %um+g</p>")
0059         + i18n("<p><strong>%tu+&lt;val&gt;</strong>: Refresh the status message every <strong>&lt;val&gt;</strong> minutes. e.g. %tu+2</p>")
0060         + i18n("<p><strong>%tx+&lt;val&gt;</strong>: Expire the status message after <strong>&lt;val&gt;</strong> minutes, or when the Now Playing active player stops (<strong>val = np</strong>). e.g. %tx+20, %tx+np</p>")
0061         + i18n("<p><strong>%xm+&quot;&lt;val&gt;&quot;</strong>: Specify a message to follow %tr, %time, %utc, and %tx token expiry. e.g. %xm+&quot;Back %time. %tx+3 %xm+&quot;Running late&quot;&quot;</p>")
0062         + i18n("<p><strong>%tf+&quot;&lt;val&gt;&quot;</strong>: Specify the format for local time using QDateTime::toString() expressions. e.g. %tf+&quot;h:mm AP t&quot;</p>")
0063         + i18n("<p><strong>%uf+&quot;&lt;val&gt;&quot;</strong>: Specify the format for UTC time using QDateTime::toString() expressions. e.g. %uf+&quot;hh:mm t&quot;</p>")
0064         + i18n("<p><strong>%sp+&quot;&lt;val&gt;&quot;</strong>: Change the separator for empty fields. e.g. %sp+&quot;-&quot;</p>")
0065         + i18n("<p>Using tokens requires the Telepathy KDED module to be loaded. Tokens can be escaped by prepending a backslash character, e.g. &#92;%sp</p>")
0066         );
0067 
0068 //A sneaky class that adds an extra entries to the end of the presence model.
0069 class PresenceModelExtended : public QAbstractListModel
0070 {
0071     Q_OBJECT
0072 public:
0073     PresenceModelExtended(KTp::PresenceModel *presenceModel, QObject *parent);
0074     int rowCount(const QModelIndex &parent = QModelIndex()) const;
0075     QVariant data(const QModelIndex &index, int role) const;
0076     KTp::Presence temporaryPresence() const;
0077     /** Adds a presence to the model which is to be used when the presence has been set externally and we need to show it, but not save it to the config*/
0078     QModelIndex addTemporaryPresence(const KTp::Presence &presence);
0079     void removeTemporaryPresence();
0080 private slots:
0081     void sourceRowsInserted(const QModelIndex &index, int start, int end);
0082     void sourceRowsRemoved(const QModelIndex &index, int start, int end);
0083 private:
0084     KTp::Presence m_temporaryPresence;
0085     KTp::PresenceModel *m_model;
0086 };
0087 
0088 PresenceModelExtended::PresenceModelExtended(KTp::PresenceModel *presenceModel, QObject *parent) :
0089     QAbstractListModel(parent),
0090     m_model(presenceModel)
0091 {
0092     connect(m_model, &QAbstractItemModel::rowsInserted, this, &PresenceModelExtended::sourceRowsInserted);
0093     connect(m_model, &QAbstractItemModel::rowsRemoved, this, &PresenceModelExtended::sourceRowsRemoved);
0094 }
0095 
0096 //return number of rows + the extra items added to end of list
0097 int PresenceModelExtended::rowCount(const QModelIndex &parent) const
0098 {
0099     if (parent.isValid()) {
0100         return 0;
0101     }
0102     int rowCount = m_model->rowCount(parent) + 2;
0103     if (m_temporaryPresence.isValid()) {
0104         rowCount++;
0105     }
0106     return rowCount;
0107 }
0108 
0109 QVariant PresenceModelExtended::data(const QModelIndex &index, int role) const
0110 {
0111     if (role == Qt::SizeHintRole) {
0112         const QFontMetrics fontMetrics(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
0113         return QSize(0, qMax(fontMetrics.height(), (int)(KIconLoader::SizeSmall)) + 8);
0114     }
0115     if (index.row() == rowCount() - 1) {
0116         switch (role) {
0117         case Qt::DisplayRole:
0118             return i18n("Configure Custom Presences...");
0119         case Qt::DecorationRole:
0120             return QIcon::fromTheme("configure");
0121         }
0122     } else if (index.row() == rowCount() - 2) {
0123         switch (role) {
0124         case Qt::DisplayRole:
0125             return i18n("Advanced Presence Setting...");
0126         case Qt::DecorationRole:
0127             return QIcon::fromTheme("configure");
0128         }
0129     } else if (m_temporaryPresence.isValid() && index.row() == rowCount() - 3) {
0130         switch (role) {
0131         case Qt::DisplayRole:
0132             return m_temporaryPresence.statusMessage();
0133         case Qt::DecorationRole:
0134             return m_temporaryPresence.icon();
0135         case KTp::PresenceModel::PresenceRole:
0136             return QVariant::fromValue<KTp::Presence>(m_temporaryPresence);
0137         }
0138     } else {
0139         return m_model->data(m_model->index(index.row()), role);
0140     }
0141     return QVariant();
0142 }
0143 
0144 KTp::Presence PresenceModelExtended::temporaryPresence() const
0145 {
0146     return m_temporaryPresence;
0147 }
0148 
0149 void PresenceModelExtended::sourceRowsInserted(const QModelIndex &index, int start, int end)
0150 {
0151     beginInsertRows(createIndex(index.row(), 0), start, end);
0152     endInsertRows();
0153 }
0154 
0155 void PresenceModelExtended::sourceRowsRemoved(const QModelIndex &index, int start, int end)
0156 {
0157     beginRemoveRows(createIndex(index.row(), 0), start, end);
0158     endRemoveRows();
0159 }
0160 
0161 QModelIndex PresenceModelExtended::addTemporaryPresence(const KTp::Presence &presence)
0162 {
0163     int row = m_model->rowCount();
0164 
0165     //if the temp presence already exists, don't remove and readd it
0166     //but simply replace it
0167     if (m_temporaryPresence.isValid()) {
0168         m_temporaryPresence = presence;
0169         emit dataChanged(this->createIndex(row, 0), this->createIndex(row, 0));
0170     } else {
0171         beginInsertRows(QModelIndex(), row, row);
0172         m_temporaryPresence = presence;
0173         endInsertRows();
0174     }
0175 
0176     return this->createIndex(row, 0);
0177 }
0178 
0179 void PresenceModelExtended::removeTemporaryPresence()
0180 {
0181     if (!m_temporaryPresence.isValid()) {
0182         return; //if not already set, do nothing.
0183     }
0184 
0185     int row = m_model->rowCount();
0186     beginRemoveRows(QModelIndex(), row, row);
0187     m_temporaryPresence = KTp::Presence();
0188     endRemoveRows();
0189 }
0190 
0191 //----------------------------------------------------------------------------------------------------------
0192 
0193 GlobalPresenceChooser::GlobalPresenceChooser(QWidget *parent) :
0194     KComboBox(parent),
0195     m_globalPresence(new KTp::GlobalPresence(this)),
0196     m_model(new KTp::PresenceModel(this)),
0197     m_modelExtended(new PresenceModelExtended(m_model, this)),
0198     m_busyOverlay(new KPixmapSequenceOverlayPainter(this)),
0199     m_changePresenceMessageButton(new QPushButton(this))
0200 {
0201     this->setModel(m_modelExtended);
0202 
0203     m_busyOverlay->setSequence(KIconLoader::global()->loadPixmapSequence("process-working", KIconLoader::SizeSmallMedium));
0204     setEditable(false);
0205 
0206     m_changePresenceMessageButton->setIcon(QIcon::fromTheme("document-edit"));
0207     m_changePresenceMessageButton->setFlat(true);
0208     m_changePresenceMessageButton->setToolTip(i18n("Click to change your presence message"));
0209 
0210     connect(this, static_cast<void(GlobalPresenceChooser::*)(int)>(&KComboBox::currentIndexChanged), &GlobalPresenceChooser::onAllComboChanges);
0211     connect(this, static_cast<void(GlobalPresenceChooser::*)(int)>(&KComboBox::activated), &GlobalPresenceChooser::onUserActivatedComboChange);
0212     connect(m_globalPresence, &KTp::GlobalPresence::currentPresenceChanged, this, &GlobalPresenceChooser::onPresenceChanged);
0213     connect(m_globalPresence, &KTp::GlobalPresence::connectionStatusChanged, this, &GlobalPresenceChooser::onConnectionStatusChanged);
0214     connect(m_changePresenceMessageButton, &QPushButton::clicked, this, &GlobalPresenceChooser::onChangePresenceMessageClicked);
0215 
0216     onPresenceChanged(m_globalPresence->currentPresence());
0217     //we need to check if there is some account connecting and if so, spin the spinner
0218     onConnectionStatusChanged(m_globalPresence->connectionStatus());
0219 }
0220 
0221 bool GlobalPresenceChooser::event(QEvent *e)
0222 {
0223     if (e->type() == QEvent::ToolTip) {
0224         QHelpEvent *helpEvent = static_cast<QHelpEvent *>(e);
0225 
0226         QString toolTipText;
0227 
0228         if (isEditable()) {
0229             toolTipText = KDED_STATUS_MESSAGE_PARSER_WHATSTHIS;
0230         } else {
0231             if (m_globalPresence->accountManager().isNull()) {
0232                 return false;
0233             }
0234 
0235             toolTipText.append("<table>");
0236 
0237             for (const Tp::AccountPtr &account : m_globalPresence->accountManager()->enabledAccounts()->accounts()) {
0238                 KTp::Presence accountPresence(account->currentPresence());
0239                 QString presenceIconPath = KIconLoader::global()->iconPath(accountPresence.icon().name(), 1);
0240                 QString presenceIconString = QString::fromLatin1("<img src=\"%1\">").arg(presenceIconPath);
0241                 QString accountIconPath = KIconLoader::global()->iconPath(account->iconName(), 1);
0242                 QString accountIconString = QString::fromLatin1("<img src=\"%1\" width=\"%2\" height=\"%2\">").arg(accountIconPath).arg(KIconLoader::SizeSmallMedium);
0243                 QString presenceString;
0244                 if (account->connectionStatus() == Tp::ConnectionStatusConnecting) {
0245                     presenceString = i18nc("Presence string when the account is connecting", "Connecting...");
0246                 } else if (!account->currentPresence().statusMessage().isEmpty()){
0247                     presenceString = QString::fromLatin1("(%1) ").arg(accountPresence.displayString()) + accountPresence.statusMessage();
0248                 } else {
0249                     presenceString = accountPresence.displayString();
0250                 }
0251                 toolTipText.append(QString::fromLatin1("<tr><td>%1 %2</td></tr><tr><td style=\"padding-left: 24px\">%3&nbsp;%4</td></tr>").arg(accountIconString, account->displayName(), presenceIconString, presenceString));
0252             }
0253 
0254             toolTipText.append("</table>");
0255         }
0256 
0257         QToolTip::showText(helpEvent->globalPos(), toolTipText, this);
0258         return true;
0259     }
0260 
0261     if (e->type() == QEvent::Resize) {
0262         repositionOverlays();
0263     }
0264 
0265     if (e->type() == QEvent::ContextMenu) {
0266         if (isEditable()) {
0267             m_lineEditContextMenu = lineEdit()->createStandardContextMenu();
0268 
0269             return true;
0270         }
0271     }
0272 
0273     if (e->type() == QEvent::KeyPress) {
0274         QKeyEvent *ke = static_cast<QKeyEvent*>(e);
0275 
0276         if (ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Return) {
0277             if (isEditable()) {
0278                 onConfirmPresenceMessageClicked();
0279                 return true;
0280             }
0281         }
0282         if (ke->key() == Qt::Key_Escape) {
0283             if (isEditable()) {
0284                 setEditable(false);
0285                 m_changePresenceMessageButton->show();
0286             }
0287         }
0288         if (ke->key() == Qt::Key_Down || ke->key() == Qt::Key_Up) {
0289             if (!isEditable()) {
0290                 showPopup();
0291                 return true;
0292             }
0293         }
0294     }
0295 
0296     if (e->type() == QEvent::FocusOut) {
0297         //just cancel editable and let it exec parent event()
0298         if (!m_lineEditContextMenu.isNull()) {
0299             if (!m_lineEditContextMenu.data()->isHidden()) {
0300                 //if we're showing the context menu, do not process this event further
0301                 return true;
0302             }
0303             //...otherwise delete the menu and hide the lineedit
0304             m_lineEditContextMenu.data()->deleteLater();
0305         }
0306         if (isEditable()) {
0307             setEditable(false);
0308             m_changePresenceMessageButton->show();
0309         }
0310     }
0311 
0312     return KComboBox::event(e); // krazy:exclude=qclasses
0313 }
0314 
0315 void GlobalPresenceChooser::setEditable(bool editable)
0316 {
0317     if (editable) {
0318         m_busyOverlay->setWidget(0);
0319     } else {
0320         m_busyOverlay->setWidget(this);
0321         if (m_globalPresence->connectionStatus() == KTp::GlobalPresence::Connecting) {
0322             m_busyOverlay->start(); // If telepathy is still connecting, overlay must be spinning again.
0323         }
0324     }
0325     KComboBox::setEditable(editable);
0326 }
0327 
0328 void GlobalPresenceChooser::onUserActivatedComboChange(int index)
0329 {
0330     if ((index == -1) || (index == count() - 3)) {
0331         return;
0332     }
0333 
0334     if (index == count() - 2) {
0335         QPointer<AdvancedPresenceDialog> dialog = new AdvancedPresenceDialog(m_model, m_globalPresence, this);
0336         dialog.data()->exec();
0337         delete dialog.data();
0338     } else if (index == count() - 1) {
0339         QPointer<CustomPresenceDialog> dialog = new CustomPresenceDialog(m_model, this);
0340         dialog.data()->exec();
0341         delete dialog.data();
0342     } else {
0343         KTp::Presence presence = itemData(index, KTp::PresenceModel::PresenceRole).value<KTp::Presence>();
0344         m_globalPresence->setPresence(presence);
0345     }
0346 
0347     onPresenceChanged(m_globalPresence->currentPresence());
0348 }
0349 
0350 void GlobalPresenceChooser::onAllComboChanges(int index)
0351 {
0352     int lastPresenceIndex = m_model->rowCount();
0353     if (index < lastPresenceIndex) {
0354         KTp::Presence presence = itemData(index, KTp::PresenceModel::PresenceRole).value<KTp::Presence>();
0355         if ((presence.type() == Tp::ConnectionPresenceTypeOffline) ||
0356                 (presence.type() == Tp::ConnectionPresenceTypeHidden)) {
0357             m_changePresenceMessageButton->hide();
0358         } else {
0359             m_changePresenceMessageButton->show();
0360         }
0361     }
0362 
0363     clearFocus();
0364 }
0365 
0366 void GlobalPresenceChooser::onPresenceChanged(const KTp::Presence &presence)
0367 {
0368     if (presence.type() == Tp::ConnectionPresenceTypeUnset) {
0369         setCurrentIndex(-1);
0370         m_busyOverlay->start();
0371         return;
0372     }
0373 
0374     const QModelIndexList &matchIndexList = m_model->match(m_model->index(0, 0), KTp::PresenceModel::PresenceRole, QVariant::fromValue<KTp::Presence>(presence));
0375     if (!matchIndexList.isEmpty()) {
0376         m_modelExtended->removeTemporaryPresence();
0377         setCurrentIndex(matchIndexList.at(0).row());
0378     } else {
0379         const QModelIndex &index = m_modelExtended->addTemporaryPresence(presence);
0380         setCurrentIndex(index.row());
0381     }
0382 
0383     m_busyOverlay->stop();
0384 }
0385 
0386 void GlobalPresenceChooser::onConnectionStatusChanged(KTp::GlobalPresence::ConnectionStatus connectionStatus)
0387 {
0388     if (connectionStatus == KTp::GlobalPresence::Connecting) {
0389         repositionOverlays();
0390         m_busyOverlay->start();
0391     } else {
0392         m_busyOverlay->stop();
0393     }
0394 }
0395 
0396 void GlobalPresenceChooser::repositionOverlays()
0397 {
0398     //set 2px margins so that the button is not bigger than the combo
0399     m_changePresenceMessageButton->setMaximumHeight(height() - 2);
0400     m_changePresenceMessageButton->setMaximumWidth(height() - 2);
0401     QPoint topLeft;
0402     if (m_changePresenceMessageButton->layoutDirection() == Qt::RightToLeft) {
0403         //move the button 22px from the left edge
0404         m_changePresenceMessageButton->move(22, 0);
0405         //place the spinner 2px right from the button, 4 is added to take the margin.
0406         topLeft.setX(m_changePresenceMessageButton->pos().x() + m_busyOverlay->sequence().frameSize().width() + 4);
0407         topLeft.setY((height() - m_busyOverlay->sequence().frameSize().height()) / 2);
0408     } else {
0409         //move the button 22px from the right edge
0410         m_changePresenceMessageButton->move(width() - m_changePresenceMessageButton->width() - 22, 0);
0411         //place the spinner 2px left from the button
0412         topLeft.setX(m_changePresenceMessageButton->pos().x() - m_busyOverlay->sequence().frameSize().width() - 2);
0413         topLeft.setY((height() - m_busyOverlay->sequence().frameSize().height()) / 2);
0414     }
0415     m_busyOverlay->setRect(QRect(topLeft, m_busyOverlay->sequence().frameSize()));
0416 }
0417 
0418 void GlobalPresenceChooser::onChangePresenceMessageClicked()
0419 {
0420     m_changePresenceMessageButton->hide();
0421 
0422     setEditable(true);
0423 
0424     //if current presence has no presence message, delete the text
0425     if (m_globalPresence->globalPresence().statusMessage().isEmpty()) {
0426         lineEdit()->clear();
0427     } else {
0428         lineEdit()->setText(m_globalPresence->globalPresence().statusMessage());
0429     }
0430 
0431     lineEdit()->setFocus();
0432 }
0433 
0434 void GlobalPresenceChooser::onConfirmPresenceMessageClicked()
0435 {
0436     m_changePresenceMessageButton->show();
0437     KTp::Presence presence = itemData(currentIndex(), KTp::PresenceModel::PresenceRole).value<KTp::Presence>();
0438     presence.setStatusMessage(lineEdit()->text());
0439 
0440     setEditable(false);
0441 
0442     m_globalPresence->setPresence(presence);
0443 }
0444 
0445 
0446 #include "global-presence-chooser.moc"
0447 #include "moc_global-presence-chooser.cpp" //hack because we have two QObjects in the same file