File indexing completed on 2025-01-05 04:55:48

0001 /*
0002     keylistview.cpp
0003 
0004     This file is part of libkleopatra, the KDE keymanagement library
0005     SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include <config-libkleo.h>
0011 
0012 #include "keylistview.h"
0013 
0014 #include <kleo_ui_debug.h>
0015 
0016 #include <QColor>
0017 #include <QFont>
0018 #include <QFontMetrics>
0019 #include <QKeyEvent>
0020 #include <QPoint>
0021 #include <QTimer>
0022 #include <QToolTip>
0023 
0024 #include <gpgme++/key.h>
0025 
0026 #include <map>
0027 #include <vector>
0028 
0029 using namespace Kleo;
0030 
0031 static const int updateDelayMilliSecs = 500;
0032 
0033 class Q_DECL_HIDDEN KeyListView::KeyListViewPrivate
0034 {
0035 public:
0036     KeyListViewPrivate()
0037         : updateTimer(nullptr)
0038     {
0039     }
0040 
0041     std::vector<GpgME::Key> keyBuffer;
0042     QTimer *updateTimer = nullptr;
0043     std::map<QByteArray, KeyListViewItem *> itemMap;
0044 };
0045 
0046 // a list of signals where we want to replace QListViewItem with
0047 // Kleo:KeyListViewItem:
0048 static const struct {
0049     const char *source;
0050     const char *target;
0051 } signalReplacements[] = {
0052     {
0053         SIGNAL(itemDoubleClicked(QTreeWidgetItem *, int)),
0054         SLOT(slotEmitDoubleClicked(QTreeWidgetItem *, int)),
0055     },
0056     {
0057         SIGNAL(itemSelectionChanged()),
0058         SLOT(slotEmitSelectionChanged()),
0059     },
0060     {
0061         SIGNAL(customContextMenuRequested(QPoint)),
0062         SLOT(slotEmitContextMenu(QPoint)),
0063     },
0064 };
0065 static const int numSignalReplacements = sizeof signalReplacements / sizeof *signalReplacements;
0066 
0067 KeyListView::KeyListView(const ColumnStrategy *columnStrategy, const DisplayStrategy *displayStrategy, QWidget *parent, Qt::WindowFlags f)
0068     : TreeWidget(parent)
0069     , mColumnStrategy(columnStrategy)
0070     , mDisplayStrategy(displayStrategy)
0071     , mHierarchical(false)
0072     , d(new KeyListViewPrivate())
0073 {
0074     setWindowFlags(f);
0075     setContextMenuPolicy(Qt::CustomContextMenu);
0076 
0077     d->updateTimer = new QTimer(this);
0078     d->updateTimer->setSingleShot(true);
0079     connect(d->updateTimer, &QTimer::timeout, this, &KeyListView::slotUpdateTimeout);
0080     if (!columnStrategy) {
0081         qCWarning(KLEO_UI_LOG) << "Kleo::KeyListView: need a column strategy to work with!";
0082         return;
0083     }
0084 
0085     const QFontMetrics fm = fontMetrics();
0086 
0087     for (int col = 0; !columnStrategy->title(col).isEmpty(); ++col) {
0088         headerItem()->setText(col, columnStrategy->title(col));
0089         header()->resizeSection(col, columnStrategy->width(col, fm));
0090         header()->setSectionResizeMode(col, columnStrategy->resizeMode(col));
0091     }
0092 
0093     setAllColumnsShowFocus(false);
0094 
0095     for (int i = 0; i < numSignalReplacements; ++i) {
0096         connect(this, signalReplacements[i].source, signalReplacements[i].target);
0097     }
0098 
0099     this->setToolTip(QString());
0100     viewport()->setToolTip(QString()); // make double sure :)
0101 }
0102 
0103 KeyListView::~KeyListView()
0104 {
0105     d->updateTimer->stop();
0106     // need to clear here, since in ~QListView, our children won't have
0107     // a valid listView() pointing to us anymore, and their dtors try to
0108     // unregister from us.
0109     clear();
0110     Q_ASSERT(d->itemMap.size() == 0);
0111     // need to delete the tooltip ourselves, as ~QToolTip isn't virtual :o
0112     delete mColumnStrategy;
0113     mColumnStrategy = nullptr;
0114     delete mDisplayStrategy;
0115     mDisplayStrategy = nullptr;
0116 }
0117 
0118 void KeyListView::takeItem(QTreeWidgetItem *qlvi)
0119 {
0120     // qCDebug(KLEO_UI_LOG) <<"Kleo::KeyListView::takeItem(" << qlvi <<" )";
0121     if (auto *item = lvi_cast<KeyListViewItem>(qlvi)) {
0122         deregisterItem(item);
0123     }
0124     takeTopLevelItem(indexOfTopLevelItem(qlvi));
0125 }
0126 
0127 void KeyListView::setHierarchical(bool hier)
0128 {
0129     if (hier == mHierarchical) {
0130         return;
0131     }
0132     mHierarchical = hier;
0133     if (hier) {
0134         gatherScattered();
0135     } else {
0136         scatterGathered(firstChild());
0137     }
0138 }
0139 
0140 void KeyListView::slotAddKey(const GpgME::Key &key)
0141 {
0142     if (key.isNull()) {
0143         return;
0144     }
0145 
0146     d->keyBuffer.push_back(key);
0147     if (!d->updateTimer->isActive()) {
0148         d->updateTimer->start(updateDelayMilliSecs);
0149     }
0150 }
0151 
0152 void KeyListView::slotUpdateTimeout()
0153 {
0154     if (d->keyBuffer.empty()) {
0155         return;
0156     }
0157 
0158     const bool wasUpdatesEnabled = viewport()->updatesEnabled();
0159     if (wasUpdatesEnabled) {
0160         viewport()->setUpdatesEnabled(false);
0161     }
0162     qCDebug(KLEO_UI_LOG) << "Kleo::KeyListView::slotUpdateTimeout(): processing" << d->keyBuffer.size() << "items en block";
0163     if (hierarchical()) {
0164         for (std::vector<GpgME::Key>::const_iterator it = d->keyBuffer.begin(); it != d->keyBuffer.end(); ++it) {
0165             doHierarchicalInsert(*it);
0166         }
0167         gatherScattered();
0168     } else {
0169         for (std::vector<GpgME::Key>::const_iterator it = d->keyBuffer.begin(); it != d->keyBuffer.end(); ++it) {
0170             (void)new KeyListViewItem(this, *it);
0171         }
0172     }
0173     if (wasUpdatesEnabled) {
0174         viewport()->setUpdatesEnabled(true);
0175     }
0176     d->keyBuffer.clear();
0177 }
0178 
0179 void KeyListView::clear()
0180 {
0181     d->updateTimer->stop();
0182     d->keyBuffer.clear();
0183     while (QTreeWidgetItem *item = topLevelItem(0)) {
0184         delete item;
0185     }
0186     QTreeWidget::clear();
0187 }
0188 
0189 void KeyListView::registerItem(KeyListViewItem *item)
0190 {
0191     // qCDebug(KLEO_UI_LOG) <<"registerItem(" << item <<" )";
0192     if (!item) {
0193         return;
0194     }
0195     const QByteArray fpr = item->key().primaryFingerprint();
0196     if (!fpr.isEmpty()) {
0197         d->itemMap.insert(std::make_pair(fpr, item));
0198     }
0199 }
0200 
0201 void KeyListView::deregisterItem(const KeyListViewItem *item)
0202 {
0203     // qCDebug(KLEO_UI_LOG) <<"deregisterItem( KeyLVI:" << item <<" )";
0204     if (!item) {
0205         return;
0206     }
0207     auto it = d->itemMap.find(item->key().primaryFingerprint());
0208     if (it == d->itemMap.end()) {
0209         return;
0210     }
0211     // This Q_ASSERT triggers, though it shouldn't. Print some more
0212     // information when it happens.
0213     // Q_ASSERT( it->second == item );
0214     if (it->second != item) {
0215         qCWarning(KLEO_UI_LOG) << "deregisterItem:"
0216                                << "item      " << item->key().primaryFingerprint() //
0217                                << "it->second" << (it->second ? it->second->key().primaryFingerprint() : "is null");
0218         return;
0219     }
0220     d->itemMap.erase(it);
0221 }
0222 
0223 void KeyListView::doHierarchicalInsert(const GpgME::Key &key)
0224 {
0225     const QByteArray fpr = key.primaryFingerprint();
0226     if (fpr.isEmpty()) {
0227         return;
0228     }
0229     KeyListViewItem *item = nullptr;
0230     if (!key.isRoot()) {
0231         if (KeyListViewItem *parent = itemByFingerprint(key.chainID())) {
0232             item = new KeyListViewItem(parent, key);
0233             parent->setExpanded(true);
0234         }
0235     }
0236     if (!item) {
0237         item = new KeyListViewItem(this, key); // top-level (for now)
0238     }
0239 
0240     d->itemMap.insert(std::make_pair(fpr, item));
0241 }
0242 
0243 void KeyListView::gatherScattered()
0244 {
0245     KeyListViewItem *item = firstChild();
0246     while (item) {
0247         KeyListViewItem *cur = item;
0248         item = item->nextSibling();
0249         if (cur->key().isRoot()) {
0250             continue;
0251         }
0252         if (KeyListViewItem *parent = itemByFingerprint(cur->key().chainID())) {
0253             // found a new parent...
0254             // ### todo: optimize by suppressing removing/adding the item to the itemMap...
0255             takeTopLevelItem(indexOfTopLevelItem(cur));
0256             parent->addChild(cur);
0257             parent->setExpanded(true);
0258         }
0259     }
0260 }
0261 
0262 void KeyListView::scatterGathered(KeyListViewItem *start)
0263 {
0264     KeyListViewItem *item = start;
0265     while (item) {
0266         KeyListViewItem *cur = item;
0267         item = item->nextSibling();
0268 
0269         scatterGathered(lvi_cast<KeyListViewItem>(cur->child(0)));
0270         Q_ASSERT(cur->childCount() == 0);
0271 
0272         // ### todo: optimize by suppressing removing/adding the item to the itemMap...
0273         if (cur->parent()) {
0274             static_cast<KeyListViewItem *>(cur->parent())->takeItem(cur);
0275         } else {
0276             takeItem(cur);
0277         }
0278         addTopLevelItem(cur);
0279     }
0280 }
0281 
0282 KeyListViewItem *KeyListView::itemByFingerprint(const QByteArray &s) const
0283 {
0284     if (s.isEmpty()) {
0285         return nullptr;
0286     }
0287     const std::map<QByteArray, KeyListViewItem *>::const_iterator it = d->itemMap.find(s);
0288     if (it == d->itemMap.end()) {
0289         return nullptr;
0290     }
0291     return it->second;
0292 }
0293 
0294 void KeyListView::slotRefreshKey(const GpgME::Key &key)
0295 {
0296     const char *fpr = key.primaryFingerprint();
0297     if (!fpr) {
0298         return;
0299     }
0300     if (KeyListViewItem *item = itemByFingerprint(fpr)) {
0301         item->setKey(key);
0302     } else {
0303         // none found -> add it
0304         slotAddKey(key);
0305     }
0306 }
0307 
0308 // slots for the emission of covariant Q_SIGNALS:
0309 
0310 void KeyListView::slotEmitDoubleClicked(QTreeWidgetItem *item, int col)
0311 {
0312     if (!item || lvi_cast<KeyListViewItem>(item)) {
0313         Q_EMIT doubleClicked(static_cast<KeyListViewItem *>(item), col);
0314     }
0315 }
0316 
0317 void KeyListView::slotEmitReturnPressed(QTreeWidgetItem *item)
0318 {
0319     if (!item || lvi_cast<KeyListViewItem>(item)) {
0320         Q_EMIT returnPressed(static_cast<KeyListViewItem *>(item));
0321     }
0322 }
0323 
0324 void KeyListView::slotEmitSelectionChanged()
0325 {
0326     Q_EMIT selectionChanged(selectedItem());
0327 }
0328 
0329 void KeyListView::slotEmitContextMenu(const QPoint &pos)
0330 {
0331     QTreeWidgetItem *item = itemAt(pos);
0332     if (!item || lvi_cast<KeyListViewItem>(item)) {
0333         Q_EMIT contextMenu(static_cast<KeyListViewItem *>(item), viewport()->mapToGlobal(pos));
0334     }
0335 }
0336 
0337 //
0338 //
0339 // KeyListViewItem
0340 //
0341 //
0342 
0343 KeyListViewItem::KeyListViewItem(KeyListView *parent, const GpgME::Key &key)
0344     : QTreeWidgetItem(parent, RTTI)
0345 {
0346     Q_ASSERT(parent);
0347     setKey(key);
0348 }
0349 
0350 KeyListViewItem::KeyListViewItem(KeyListView *parent, KeyListViewItem *after, const GpgME::Key &key)
0351     : QTreeWidgetItem(parent, after, RTTI)
0352 {
0353     Q_ASSERT(parent);
0354     setKey(key);
0355 }
0356 
0357 KeyListViewItem::KeyListViewItem(KeyListViewItem *parent, const GpgME::Key &key)
0358     : QTreeWidgetItem(parent, RTTI)
0359 {
0360     Q_ASSERT(parent && parent->listView());
0361     setKey(key);
0362 }
0363 
0364 KeyListViewItem::KeyListViewItem(KeyListViewItem *parent, KeyListViewItem *after, const GpgME::Key &key)
0365     : QTreeWidgetItem(parent, after, RTTI)
0366 {
0367     Q_ASSERT(parent && parent->listView());
0368     setKey(key);
0369 }
0370 
0371 KeyListViewItem::~KeyListViewItem()
0372 {
0373     // delete the children first... When children are deleted in the
0374     // QLVI dtor, they don't have listView() anymore, thus they don't
0375     // call deregister( this ), leading to stale entries in the
0376     // itemMap...
0377     while (QTreeWidgetItem *item = child(0)) {
0378         delete item;
0379     }
0380     // better do this here, too, since deletion is top-down and thus
0381     // we're deleted when our parent item is no longer a
0382     // KeyListViewItem, but a mere QListViewItem, so our takeItem()
0383     // overload is gone by that time...
0384     if (KeyListView *lv = listView()) {
0385         lv->deregisterItem(this);
0386     }
0387 }
0388 
0389 void KeyListViewItem::setKey(const GpgME::Key &key)
0390 {
0391     KeyListView *lv = listView();
0392     if (lv) {
0393         lv->deregisterItem(this);
0394     }
0395     mKey = key;
0396     if (lv) {
0397         lv->registerItem(this);
0398     }
0399 
0400     // the ColumnStrategy operations might be very slow, so cache their
0401     // result here, where we're non-const :)
0402     const KeyListView::ColumnStrategy *cs = lv ? lv->columnStrategy() : nullptr;
0403     if (!cs) {
0404         return;
0405     }
0406     const KeyListView::DisplayStrategy *ds = lv->displayStrategy();
0407     const int numCols = lv ? lv->columnCount() : 0;
0408     for (int i = 0; i < numCols; ++i) {
0409         setText(i, cs->text(key, i));
0410         const auto accessibleText = cs->accessibleText(key, i);
0411         if (!accessibleText.isEmpty()) {
0412             setData(i, Qt::AccessibleTextRole, accessibleText);
0413         }
0414         setToolTip(i, cs->toolTip(key, i));
0415         const QIcon icon = cs->icon(key, i);
0416         if (!icon.isNull()) {
0417             setIcon(i, icon);
0418         }
0419         if (ds) {
0420             setForeground(i, QBrush(ds->keyForeground(key, foreground(i).color())));
0421             setBackground(i, QBrush(ds->keyBackground(key, background(i).color())));
0422             setFont(i, ds->keyFont(key, font(i)));
0423         }
0424     }
0425 }
0426 
0427 QString KeyListViewItem::toolTip(int col) const
0428 {
0429     return listView() && listView()->columnStrategy() ? listView()->columnStrategy()->toolTip(key(), col) : QString();
0430 }
0431 
0432 bool KeyListViewItem::operator<(const QTreeWidgetItem &other) const
0433 {
0434     if (other.type() != RTTI || !listView() || !listView()->columnStrategy()) {
0435         return QTreeWidgetItem::operator<(other);
0436     }
0437     const auto that = static_cast<const KeyListViewItem *>(&other);
0438     return listView()->columnStrategy()->compare(this->key(), that->key(), treeWidget()->sortColumn()) < 0;
0439 }
0440 
0441 void KeyListViewItem::takeItem(QTreeWidgetItem *qlvi)
0442 {
0443     // qCDebug(KLEO_UI_LOG) <<"Kleo::KeyListViewItem::takeItem(" << qlvi <<" )";
0444     if (auto *item = lvi_cast<KeyListViewItem>(qlvi)) {
0445         listView()->deregisterItem(item);
0446     }
0447     takeChild(indexOfChild(qlvi));
0448 }
0449 
0450 //
0451 //
0452 // ColumnStrategy
0453 //
0454 //
0455 
0456 KeyListView::ColumnStrategy::~ColumnStrategy()
0457 {
0458 }
0459 
0460 int KeyListView::ColumnStrategy::compare(const GpgME::Key &key1, const GpgME::Key &key2, const int col) const
0461 {
0462     return QString::localeAwareCompare(text(key1, col), text(key2, col));
0463 }
0464 
0465 int KeyListView::ColumnStrategy::width(int col, const QFontMetrics &fm) const
0466 {
0467     return fm.horizontalAdvance(title(col)) * 2;
0468 }
0469 
0470 QString KeyListView::ColumnStrategy::toolTip(const GpgME::Key &key, int col) const
0471 {
0472     return text(key, col);
0473 }
0474 
0475 //
0476 //
0477 // DisplayStrategy
0478 //
0479 //
0480 
0481 KeyListView::DisplayStrategy::~DisplayStrategy()
0482 {
0483 }
0484 
0485 // font
0486 QFont KeyListView::DisplayStrategy::keyFont(const GpgME::Key &, const QFont &font) const
0487 {
0488     return font;
0489 }
0490 
0491 // foreground
0492 QColor KeyListView::DisplayStrategy::keyForeground(const GpgME::Key &, const QColor &fg) const
0493 {
0494     return fg;
0495 }
0496 
0497 // background
0498 QColor KeyListView::DisplayStrategy::keyBackground(const GpgME::Key &, const QColor &bg) const
0499 {
0500     return bg;
0501 }
0502 
0503 //
0504 //
0505 // Collection of covariant return reimplementations of QListView(Item)
0506 // members:
0507 //
0508 //
0509 
0510 KeyListView *KeyListViewItem::listView() const
0511 {
0512     return static_cast<KeyListView *>(QTreeWidgetItem::treeWidget());
0513 }
0514 
0515 KeyListViewItem *KeyListViewItem::nextSibling() const
0516 {
0517     if (parent()) {
0518         const int myIndex = parent()->indexOfChild(const_cast<KeyListViewItem *>(this));
0519         return static_cast<KeyListViewItem *>(parent()->child(myIndex + 1));
0520     }
0521     const int myIndex = treeWidget()->indexOfTopLevelItem(const_cast<KeyListViewItem *>(this));
0522     return static_cast<KeyListViewItem *>(treeWidget()->topLevelItem(myIndex + 1));
0523 }
0524 
0525 KeyListViewItem *KeyListView::firstChild() const
0526 {
0527     return static_cast<KeyListViewItem *>(topLevelItem(0));
0528 }
0529 
0530 KeyListViewItem *KeyListView::selectedItem() const
0531 {
0532     QList<KeyListViewItem *> selection = selectedItems();
0533     if (selection.isEmpty()) {
0534         return nullptr;
0535     }
0536     return selection.first();
0537 }
0538 
0539 QList<KeyListViewItem *> KeyListView::selectedItems() const
0540 {
0541     QList<KeyListViewItem *> result;
0542     const auto selectedItems = QTreeWidget::selectedItems();
0543     for (QTreeWidgetItem *selectedItem : selectedItems) {
0544         if (auto *i = lvi_cast<KeyListViewItem>(selectedItem)) {
0545             result.append(i);
0546         }
0547     }
0548     return result;
0549 }
0550 
0551 bool KeyListView::isMultiSelection() const
0552 {
0553     return selectionMode() == ExtendedSelection || selectionMode() == MultiSelection;
0554 }
0555 
0556 void KeyListView::keyPressEvent(QKeyEvent *event)
0557 {
0558     if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
0559         if (selectedItem()) {
0560             slotEmitReturnPressed(selectedItem());
0561         }
0562     }
0563     QTreeView::keyPressEvent(event);
0564 }
0565 
0566 #include "moc_keylistview.cpp"