File indexing completed on 2024-05-12 05:12:43

0001 /*
0002     This file is part of Akonadi.
0003 
0004     SPDX-FileCopyrightText: 2006 Tobias Koenig <tokoe@kde.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "agentwidget.h"
0010 
0011 #include "agentconfigdialog.h"
0012 #include "akonadiconsole_debug.h"
0013 #include <Akonadi/AgentConfigurationDialog>
0014 #include <Akonadi/AgentFilterProxyModel>
0015 #include <Akonadi/AgentInstanceCreateJob>
0016 #include <Akonadi/AgentInstanceWidget>
0017 #include <Akonadi/AgentManager>
0018 #include <Akonadi/AgentTypeDialog>
0019 #include <Akonadi/ControlGui>
0020 
0021 #include <KLocalizedString>
0022 #include <KMessageBox>
0023 #include <KStandardGuiItem>
0024 #include <QIcon>
0025 
0026 #include <KGuiItem>
0027 #include <QDBusInterface>
0028 #include <QDBusMessage>
0029 #include <QDBusReply>
0030 #include <QDialogButtonBox>
0031 #include <QMenu>
0032 #include <QMetaMethod>
0033 #include <QPlainTextEdit>
0034 #include <QPointer>
0035 #include <QPushButton>
0036 #include <QResizeEvent>
0037 #include <QVBoxLayout>
0038 
0039 class TextDialog : public QDialog
0040 {
0041 public:
0042     explicit TextDialog(QWidget *parent = nullptr)
0043         : QDialog(parent)
0044         , mText(new QPlainTextEdit(this))
0045     {
0046         auto mainLayout = new QVBoxLayout(this);
0047         mText->setReadOnly(true);
0048 
0049         auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok, this);
0050         QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
0051         okButton->setDefault(true);
0052         okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
0053         connect(buttonBox, &QDialogButtonBox::accepted, this, &TextDialog::accept);
0054         connect(buttonBox, &QDialogButtonBox::rejected, this, &TextDialog::reject);
0055 
0056         mainLayout->addWidget(mText);
0057         mainLayout->addWidget(buttonBox);
0058         resize(QSize(400, 600));
0059     }
0060 
0061     void setText(const QString &text)
0062     {
0063         mText->setPlainText(text);
0064     }
0065 
0066 private:
0067     QPlainTextEdit *const mText;
0068 };
0069 
0070 using namespace Akonadi;
0071 
0072 AgentWidget::AgentWidget(QWidget *parent)
0073     : QWidget(parent)
0074 {
0075     ui.setupUi(this);
0076 
0077     connect(ui.instanceWidget, &Akonadi::AgentInstanceWidget::doubleClicked, this, &AgentWidget::configureAgent);
0078     connect(ui.instanceWidget, &Akonadi::AgentInstanceWidget::currentChanged, this, &AgentWidget::currentChanged);
0079     connect(ui.instanceWidget, &Akonadi::AgentInstanceWidget::customContextMenuRequested, this, &AgentWidget::showContextMenu);
0080 
0081     connect(ui.instanceWidget->view()->selectionModel(), &QItemSelectionModel::selectionChanged, this, &AgentWidget::selectionChanged);
0082     connect(ui.instanceWidget->view()->selectionModel(), &QItemSelectionModel::currentChanged, this, &AgentWidget::selectionChanged);
0083     connect(ui.instanceWidget->view()->model(), &QAbstractItemModel::dataChanged, this, &AgentWidget::slotDataChanged);
0084 
0085     currentChanged();
0086 
0087     KGuiItem::assign(ui.addButton, KStandardGuiItem::add());
0088     connect(ui.addButton, &QPushButton::clicked, this, &AgentWidget::addAgent);
0089 
0090     KGuiItem::assign(ui.removeButton, KStandardGuiItem::remove());
0091     connect(ui.removeButton, &QPushButton::clicked, this, &AgentWidget::removeAgent);
0092 
0093     mConfigMenu = new QMenu(i18n("Configure"), this);
0094     mConfigMenu->addAction(i18n("Configure Natively..."), this, &AgentWidget::configureAgent);
0095     mConfigMenu->addAction(i18n("Configure Remotely..."), this, &AgentWidget::configureAgentRemote);
0096     mConfigMenu->setIcon(KStandardGuiItem::configure().icon());
0097     KGuiItem::assign(ui.configButton, KStandardGuiItem::configure());
0098     ui.configButton->setMenu(mConfigMenu);
0099     connect(ui.configButton, &QPushButton::clicked, this, &AgentWidget::configureAgent);
0100 
0101     mSyncMenu = new QMenu(i18n("Synchronize"), this);
0102     mSyncMenu->addAction(i18n("Synchronize All"), this, &AgentWidget::synchronizeAgent);
0103     mSyncMenu->addAction(i18n("Synchronize Collection Tree"), this, &AgentWidget::synchronizeTree);
0104     mSyncMenu->addAction(i18n("Synchronize Tags"), this, [this]() {
0105         const AgentInstance::List list = ui.instanceWidget->selectedAgentInstances();
0106         for (auto agent : list) {
0107             agent.synchronizeTags();
0108         }
0109     });
0110     mSyncMenu->addAction(i18n("Synchronize Relations"), this, [this]() {
0111         const auto list = ui.instanceWidget->selectedAgentInstances();
0112         for (auto agent : list) {
0113             agent.synchronizeRelations();
0114         }
0115     });
0116     mSyncMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
0117     ui.syncButton->setMenu(mSyncMenu);
0118     ui.syncButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
0119     connect(ui.syncButton, &QPushButton::clicked, this, &AgentWidget::synchronizeAgent);
0120 
0121     ui.abortButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-cancel")));
0122     connect(ui.abortButton, &QPushButton::clicked, this, &AgentWidget::abortAgent);
0123     ui.restartButton->setIcon(QIcon::fromTheme(QStringLiteral("system-reboot"))); // FIXME: Is using system-reboot icon here a good idea?
0124     connect(ui.restartButton, &QPushButton::clicked, this, &AgentWidget::restartAgent);
0125 
0126     ui.instanceWidget->agentFilterProxyModel()->setFilterCaseSensitivity(Qt::CaseInsensitive);
0127     connect(ui.mFilterAccount, &QLineEdit::textChanged, this, &AgentWidget::slotSearchAgentType);
0128     ui.mFilterAccount->installEventFilter(this);
0129     ControlGui::widgetNeedsAkonadi(this);
0130 }
0131 
0132 void AgentWidget::slotSearchAgentType(const QString &str)
0133 {
0134     ui.instanceWidget->agentFilterProxyModel()->setFilterRegularExpression(str);
0135 }
0136 
0137 bool AgentWidget::eventFilter(QObject *obj, QEvent *event)
0138 {
0139     if (event->type() == QEvent::KeyPress && obj == ui.mFilterAccount) {
0140         auto key = static_cast<QKeyEvent *>(event);
0141         if ((key->key() == Qt::Key_Enter) || (key->key() == Qt::Key_Return)) {
0142             event->accept();
0143             return true;
0144         }
0145     }
0146     return QWidget::eventFilter(obj, event);
0147 }
0148 
0149 void AgentWidget::addAgent()
0150 {
0151     QPointer<Akonadi::AgentTypeDialog> dlg = new Akonadi::AgentTypeDialog(this);
0152     if (dlg->exec()) {
0153         const AgentType agentType = dlg->agentType();
0154 
0155         if (agentType.isValid()) {
0156             auto job = new AgentInstanceCreateJob(agentType, this);
0157             job->configure(this);
0158             job->start(); // TODO: check result
0159         }
0160     }
0161     delete dlg;
0162 }
0163 
0164 void AgentWidget::selectionChanged()
0165 {
0166     const bool multiSelection = ui.instanceWidget->view()->selectionModel()->selectedRows().size() > 1;
0167     // Only agent removal, sync and restart is possible when multiple items are selected.
0168     ui.configButton->setDisabled(multiSelection);
0169 
0170     // Restarting an agent is not possible if it's in Running status... (see AgentProcessInstance::restartWhenIdle)
0171     AgentInstance agent = ui.instanceWidget->currentAgentInstance();
0172     ui.restartButton->setEnabled(agent.isValid() && agent.status() != 1);
0173 }
0174 
0175 void AgentWidget::slotDataChanged(const QModelIndex &topLeft, const QModelIndex & /*bottomRight*/)
0176 {
0177     QList<QModelIndex> selectedRows = ui.instanceWidget->view()->selectionModel()->selectedRows();
0178     if (selectedRows.isEmpty()) {
0179         selectedRows.append(ui.instanceWidget->view()->selectionModel()->currentIndex());
0180     }
0181     QList<int> rows;
0182     for (const QModelIndex &index : std::as_const(selectedRows)) {
0183         rows.append(index.row());
0184     }
0185     std::sort(rows.begin(), rows.end());
0186     // Assume topLeft.row == bottomRight.row
0187     if (topLeft.row() >= rows.first() && topLeft.row() <= rows.last()) {
0188         selectionChanged(); // depends on status
0189         currentChanged();
0190     }
0191 }
0192 
0193 void AgentWidget::removeAgent()
0194 {
0195     const AgentInstance::List list = ui.instanceWidget->selectedAgentInstances();
0196     if (!list.isEmpty()) {
0197         if (KMessageBox::questionTwoActions(
0198                 this,
0199                 i18np("Do you really want to delete the selected agent instance?", "Do you really want to delete these %1 agent instances?", list.size()),
0200                 list.size() == 1 ? i18n("Agent Deletion") : i18n("Multiple Agent Deletion"),
0201                 KStandardGuiItem::del(),
0202                 KStandardGuiItem::cancel(),
0203                 QString(),
0204                 KMessageBox::Dangerous)
0205             == KMessageBox::ButtonCode::PrimaryAction) {
0206             for (const AgentInstance &agent : list) {
0207                 AgentManager::self()->removeInstance(agent);
0208             }
0209         }
0210     }
0211 }
0212 
0213 void AgentWidget::configureAgent()
0214 {
0215     AgentInstance agent = ui.instanceWidget->currentAgentInstance();
0216     if (agent.isValid()) {
0217         QPointer<AgentConfigurationDialog> dlg = new AgentConfigurationDialog(agent, this);
0218         dlg->exec();
0219         delete dlg;
0220     }
0221 }
0222 
0223 void AgentWidget::configureAgentRemote()
0224 {
0225     AgentInstance agent = ui.instanceWidget->currentAgentInstance();
0226     if (agent.isValid()) {
0227         QPointer<AgentConfigDialog> dlg = new AgentConfigDialog(this);
0228         dlg->setAgentInstance(agent);
0229         dlg->exec();
0230         delete dlg;
0231     }
0232 }
0233 
0234 void AgentWidget::synchronizeAgent()
0235 {
0236     const AgentInstance::List list = ui.instanceWidget->selectedAgentInstances();
0237     if (!list.isEmpty()) {
0238         for (AgentInstance agent : list) {
0239             agent.synchronize();
0240         }
0241     }
0242 }
0243 
0244 void AgentWidget::toggleOnline()
0245 {
0246     AgentInstance agent = ui.instanceWidget->currentAgentInstance();
0247     if (agent.isValid()) {
0248         agent.setIsOnline(!agent.isOnline());
0249     }
0250 }
0251 
0252 void AgentWidget::showTaskList()
0253 {
0254     AgentInstance agent = ui.instanceWidget->currentAgentInstance();
0255     if (!agent.isValid()) {
0256         return;
0257     }
0258 
0259     QDBusInterface iface(QStringLiteral("org.freedesktop.Akonadi.Agent.%1").arg(agent.identifier()), QStringLiteral("/Debug"), QString());
0260 
0261     QDBusReply<QString> reply = iface.call(QStringLiteral("dumpToString"));
0262     QString txt;
0263     if (reply.isValid()) {
0264         txt = reply.value();
0265     } else {
0266         txt = reply.error().message();
0267     }
0268 
0269     QPointer<TextDialog> dlg = new TextDialog(this);
0270     dlg->setWindowTitle(i18nc("@title:window", "Resource Task List"));
0271     dlg->setText(txt);
0272     dlg->exec();
0273     delete dlg;
0274 }
0275 
0276 void AgentWidget::showChangeNotifications()
0277 {
0278     AgentInstance agent = ui.instanceWidget->currentAgentInstance();
0279     if (!agent.isValid()) {
0280         return;
0281     }
0282 
0283     QDBusInterface iface(QStringLiteral("org.freedesktop.Akonadi.Agent.%1").arg(agent.identifier()), QStringLiteral("/Debug"), QString());
0284 
0285     QDBusReply<QString> reply = iface.call(QStringLiteral("dumpNotificationListToString"));
0286     QString txt;
0287     if (reply.isValid()) {
0288         txt = reply.value();
0289     } else {
0290         txt = reply.error().message();
0291     }
0292 
0293     QPointer<TextDialog> dlg = new TextDialog(this);
0294     dlg->setWindowTitle(i18nc("@title:window", "Change Notification Log"));
0295     dlg->setText(txt);
0296 
0297     dlg->exec();
0298     delete dlg;
0299 }
0300 
0301 void AgentWidget::synchronizeTree()
0302 {
0303     const AgentInstance::List list = ui.instanceWidget->selectedAgentInstances();
0304     for (AgentInstance agent : list) {
0305         agent.synchronizeCollectionTree();
0306     }
0307 }
0308 
0309 void AgentWidget::abortAgent()
0310 {
0311     const AgentInstance::List list = ui.instanceWidget->selectedAgentInstances();
0312     for (const AgentInstance &agent : list) {
0313         agent.abortCurrentTask();
0314     }
0315 }
0316 
0317 void AgentWidget::restartAgent()
0318 {
0319     AgentInstance agent = ui.instanceWidget->currentAgentInstance();
0320     if (agent.isValid()) {
0321         agent.restart();
0322     }
0323 }
0324 
0325 void AgentWidget::slotCloneAgent()
0326 {
0327     mCloneSource = ui.instanceWidget->currentAgentInstance();
0328     if (!mCloneSource.isValid()) {
0329         return;
0330     }
0331     const AgentType agentType = mCloneSource.type();
0332     if (agentType.isValid()) {
0333         auto job = new AgentInstanceCreateJob(agentType, this);
0334         connect(job, &KJob::result, this, &AgentWidget::cloneAgent);
0335         job->start();
0336     } else {
0337         qCWarning(AKONADICONSOLE_LOG) << "WTF?";
0338     }
0339 }
0340 
0341 void AgentWidget::cloneAgent(KJob *job)
0342 {
0343     if (job->error()) {
0344         KMessageBox::error(this, i18n("Cloning agent failed: %1.", job->errorText()));
0345         return;
0346     }
0347 
0348     AgentInstance cloneTarget = static_cast<AgentInstanceCreateJob *>(job)->instance();
0349     Q_ASSERT(cloneTarget.isValid());
0350     Q_ASSERT(mCloneSource.isValid());
0351 
0352     QDBusInterface sourceIface(QStringLiteral("org.freedesktop.Akonadi.Agent.%1").arg(mCloneSource.identifier()), QStringLiteral("/Settings"));
0353     if (!sourceIface.isValid()) {
0354         qCritical() << "Unable to obtain KConfigXT D-Bus interface of source agent" << mCloneSource.identifier();
0355         return;
0356     }
0357 
0358     QDBusInterface targetIface(QStringLiteral("org.freedesktop.Akonadi.Agent.%1").arg(cloneTarget.identifier()), QStringLiteral("/Settings"));
0359     if (!targetIface.isValid()) {
0360         qCritical() << "Unable to obtain KConfigXT D-Bus interface of target agent" << cloneTarget.identifier();
0361         return;
0362     }
0363 
0364     cloneTarget.setName(mCloneSource.name() + QStringLiteral(" (Clone)"));
0365 
0366     // iterate over all getter methods in the source interface and call the
0367     // corresponding setter in the target interface
0368     for (int i = 0; i < sourceIface.metaObject()->methodCount(); ++i) {
0369         const QMetaMethod method = sourceIface.metaObject()->method(i);
0370         if (QByteArray(method.typeName()).isEmpty()) { // returns void
0371             continue;
0372         }
0373         const QByteArray signature(method.methodSignature());
0374         if (signature.isEmpty()) {
0375             continue;
0376         }
0377         if (signature.startsWith("set") || !signature.contains("()")) { // setter or takes parameters // krazy:exclude=strings
0378             continue;
0379         }
0380         if (signature.startsWith("Introspect")) { // D-Bus stuff // krazy:exclude=strings
0381             continue;
0382         }
0383         const QString methodName = QLatin1StringView(signature.left(signature.indexOf('(')));
0384         const QDBusMessage reply = sourceIface.call(methodName);
0385         if (reply.arguments().count() != 1) {
0386             qCritical() << "call to method" << signature << "failed: " << reply.arguments() << reply.errorMessage();
0387             continue;
0388         }
0389         const QString setterName = QStringLiteral("set") + methodName.at(0).toUpper() + methodName.mid(1);
0390         targetIface.call(setterName, reply.arguments().at(0));
0391     }
0392 
0393     cloneTarget.reconfigure();
0394 }
0395 
0396 void AgentWidget::currentChanged()
0397 {
0398     AgentInstance instance = ui.instanceWidget->currentAgentInstance();
0399     ui.removeButton->setEnabled(instance.isValid());
0400     ui.configButton->setEnabled(instance.isValid());
0401     ui.syncButton->setEnabled(instance.isValid());
0402     ui.restartButton->setEnabled(instance.isValid());
0403 
0404     if (instance.isValid()) {
0405         ui.identifierLabel->setText(instance.identifier());
0406         ui.typeLabel->setText(instance.type().name());
0407         QString onlineStatus = (instance.isOnline() ? i18n("Online") : i18n("Offline"));
0408         QString agentStatus;
0409         switch (instance.status()) {
0410         case AgentInstance::Idle:
0411             agentStatus = i18n("Idle");
0412             break;
0413         case AgentInstance::Running:
0414             agentStatus = i18n("Running (%1%)", instance.progress());
0415             break;
0416         case AgentInstance::Broken:
0417             agentStatus = i18n("Broken");
0418             break;
0419         case AgentInstance::NotConfigured:
0420             agentStatus = i18n("Not Configured");
0421             break;
0422         }
0423         ui.statusLabel->setText(i18nc("Two statuses, for example \"Online, Running (66%)\" or \"Offline, Broken\"", "%1, %2", onlineStatus, agentStatus));
0424         ui.statusMessageLabel->setText(instance.statusMessage());
0425         ui.capabilitiesLabel->setText(instance.type().capabilities().join(QLatin1StringView(", ")));
0426         ui.mimeTypeLabel->setText(instance.type().mimeTypes().join(QLatin1StringView(", ")));
0427     } else {
0428         ui.identifierLabel->setText(QString());
0429         ui.typeLabel->setText(QString());
0430         ui.statusLabel->setText(QString());
0431         ui.capabilitiesLabel->setText(QString());
0432         ui.mimeTypeLabel->setText(QString());
0433     }
0434 }
0435 
0436 void AgentWidget::showContextMenu(const QPoint &pos)
0437 {
0438     QMenu menu(this);
0439     menu.addAction(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add Agent..."), this, &AgentWidget::addAgent);
0440     menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Clone Agent"), this, &AgentWidget::slotCloneAgent);
0441     menu.addSeparator();
0442     menu.addMenu(mSyncMenu);
0443     menu.addAction(QIcon::fromTheme(QStringLiteral("dialog-cancel")), i18n("Abort Activity"), this, &AgentWidget::abortAgent);
0444     menu.addAction(QIcon::fromTheme(QStringLiteral("system-reboot")),
0445                    i18n("Restart Agent"),
0446                    this,
0447                    &AgentWidget::restartAgent); // FIXME: Is using system-reboot icon here a good idea?
0448     menu.addAction(QIcon::fromTheme(QStringLiteral("network-disconnect")), i18n("Toggle Online/Offline"), this, &AgentWidget::toggleOnline);
0449     menu.addAction(i18n("Show task list"), this, &AgentWidget::showTaskList);
0450     menu.addAction(i18n("Show change-notification log"), this, &AgentWidget::showChangeNotifications);
0451     menu.addMenu(mConfigMenu);
0452     menu.addAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Remove Agent"), this, &AgentWidget::removeAgent);
0453     menu.exec(ui.instanceWidget->mapToGlobal(pos));
0454 }
0455 
0456 void AgentWidget::resizeEvent(QResizeEvent *event)
0457 {
0458     ui.detailsBox->setVisible(event->size().height() > 400);
0459 }
0460 
0461 #include "moc_agentwidget.cpp"