diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index bc267b34b..38defd940 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,1003 +1,1003 @@ /* -*- mode: c++; c-basic-offset:4 -*- mainwindow.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include <config-kleopatra.h> #include "aboutdata.h" #include "kleopatraapplication.h" #include "mainwindow.h" #include "settings.h" #include <interfaces/focusfirstchild.h> #include "view/keycacheoverlay.h" #include "view/keylistcontroller.h" #include "view/padwidget.h" #include "view/searchbar.h" #include "view/smartcardwidget.h" #include "view/tabwidget.h" #include "view/welcomewidget.h" #include "commands/decryptverifyfilescommand.h" #include "commands/importcertificatefromfilecommand.h" #include "commands/importcrlcommand.h" #include "commands/selftestcommand.h" #include "commands/signencryptfilescommand.h" #include "utils/action_data.h" #include "utils/clipboardmenu.h" #include "utils/detail_p.h" #include "utils/filedialog.h" #include "utils/gui-helper.h" #include "utils/keyexportdraghandler.h" #include "utils/userinfo.h" #include <Libkleo/GnuPG> #include "dialogs/debugdialog.h" #include "dialogs/updatenotification.h" // needed for GPGME_VERSION_NUMBER #include <gpgme.h> #include "kleopatra_debug.h" #include <KAboutApplicationDialog> #include <KAboutData> #include <KActionCollection> #include <KActionMenu> #include <KColorScheme> #include <KConfigDialog> #include <KConfigGroup> #include <KEditToolBar> #include <KHelpMenu> #include <KLocalizedString> #include <KMessageBox> #include <KShortcutsDialog> #include <KStandardAction> #include <KStandardGuiItem> #include <KToolBar> #include <KXMLGUIFactory> #include <QAction> #include <QApplication> #include <QLineEdit> #include <QSize> #include <QAbstractItemView> #include <QCloseEvent> #include <QDesktopServices> #include <QDir> #include <QLabel> #include <QMenu> #include <QMimeData> #include <QPixmap> #include <QProcess> #include <QSettings> #include <QStackedWidget> #include <QStatusBar> #include <QTimer> #include <QVBoxLayout> #include <Libkleo/Classify> #include <Libkleo/Compliance> #include <Libkleo/DocAction> #include <Libkleo/Formatting> #include <Libkleo/GnuPG> #include <Libkleo/KeyCache> #include <Libkleo/KeyListModel> #include <Libkleo/KeyListSortFilterProxyModel> #include <Libkleo/Stl_Util> #include <Libkleo/SystemInfo> #include <KSharedConfig> #ifdef Q_OS_UNIX #include <KWaylandExtras> #endif #include <chrono> #include <vector> using namespace std::chrono_literals; using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; static KGuiItem KStandardGuiItem_quit() { static const QString app = KAboutData::applicationData().displayName(); KGuiItem item = KStandardGuiItem::quit(); item.setText(xi18nc("@action:button", "&Quit <application>%1</application>", app)); return item; } static KGuiItem KStandardGuiItem_close() { KGuiItem item = KStandardGuiItem::close(); item.setText(i18nc("@action:button", "Only &Close Window")); return item; } static bool isQuitting = false; namespace { static const std::vector<QString> mainViewActionNames = { QStringLiteral("view_certificate_overview"), QStringLiteral("manage_smartcard"), QStringLiteral("pad_view"), }; class CertificateView : public QWidget, public FocusFirstChild { Q_OBJECT public: CertificateView(QWidget *parent = nullptr) : QWidget{parent} , ui{this} { } SearchBar *searchBar() const { return ui.searchBar; } TabWidget *tabWidget() const { return ui.tabWidget; } void focusFirstChild(Qt::FocusReason reason) override { ui.searchBar->lineEdit()->setFocus(reason); } private: struct UI { TabWidget *tabWidget = nullptr; SearchBar *searchBar = nullptr; explicit UI(CertificateView *q) { auto vbox = new QVBoxLayout{q}; vbox->setSpacing(0); searchBar = new SearchBar{q}; vbox->addWidget(searchBar); - tabWidget = new TabWidget{q}; + tabWidget = new TabWidget{KeyTreeView::Option::NoDefaultContextMenu, q}; vbox->addWidget(tabWidget); tabWidget->connectSearchBar(searchBar); } } ui; }; } class MainWindow::Private { friend class ::MainWindow; MainWindow *const q; public: explicit Private(MainWindow *qq); ~Private(); template<typename T> void createAndStart() { (new T(this->currentView(), &this->controller))->start(); } template<typename T> void createAndStart(QAbstractItemView *view) { (new T(view, &this->controller))->start(); } template<typename T> void createAndStart(const QStringList &a) { (new T(a, this->currentView(), &this->controller))->start(); } template<typename T> void createAndStart(const QStringList &a, QAbstractItemView *view) { (new T(a, view, &this->controller))->start(); } void closeAndQuit() { if (Kleo::userIsElevated()) { // For users running Kleo with elevated permissions on Windows we // always quit the application to avoid some problems. qApp->quit(); } const QString app = KAboutData::applicationData().displayName(); const int rc = KMessageBox::questionTwoActionsCancel(q, xi18n("<application>%1</application> may be used by other applications as a service.<nl/>" "You may instead want to close this window without exiting <application>%1</application>.", app), i18nc("@title:window", "Really Quit?"), KStandardGuiItem_close(), KStandardGuiItem_quit(), KStandardGuiItem::cancel(), QLatin1StringView("really-quit-") + app.toLower()); if (rc == KMessageBox::Cancel) { return; } isQuitting = rc == KMessageBox::ButtonCode::SecondaryAction; if (!q->close()) { return; } // WARNING: 'this' might be deleted at this point! if (rc == KMessageBox::ButtonCode::SecondaryAction) { qApp->quit(); } } void configureToolbars() { KEditToolBar dlg(q->factory()); dlg.exec(); } void editKeybindings() { KShortcutsDialog::showDialog(q->actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, q); updateSearchBarClickMessage(); } void updateSearchBarClickMessage() { const QString shortcutStr = focusToClickSearchAction->shortcut().toString(QKeySequence::NativeText); ui.searchTab->searchBar()->updateClickMessage(shortcutStr); } void updateStatusBar() { auto statusBar = std::make_unique<QStatusBar>(); auto settings = KleopatraApplication::instance()->distributionSettings(); bool showStatusbar = false; if (settings) { const QString statusline = settings->value(QStringLiteral("statusline"), {}).toString(); if (!statusline.isEmpty()) { auto customStatusLbl = new QLabel(statusline); statusBar->insertWidget(0, customStatusLbl); showStatusbar = true; } } if (DeVSCompliance::isActive()) { auto statusLbl = std::make_unique<QLabel>(DeVSCompliance::name()); if (!SystemInfo::isHighContrastModeActive()) { const auto color = KColorScheme(QPalette::Active, KColorScheme::View) .foreground(DeVSCompliance::isCompliant() ? KColorScheme::NormalText : KColorScheme::NegativeText) .color(); const auto background = KColorScheme(QPalette::Active, KColorScheme::View) .background(DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground) .color(); statusLbl->setStyleSheet(QStringLiteral("QLabel { color: %1; background-color: %2; }").arg(color.name()).arg(background.name())); } statusBar->insertPermanentWidget(0, statusLbl.release()); showStatusbar = true; } if (showStatusbar) { q->setStatusBar(statusBar.release()); // QMainWindow takes ownership } else { q->setStatusBar(nullptr); } } void selfTest() { createAndStart<SelfTestCommand>(); } void configureGroups() { // open groups config dialog as independent top-level window KleopatraApplication::instance()->openOrRaiseGroupsConfigDialog(nullptr); } void showHandbook(); void gnupgLogViewer() { // Warning: Don't assume that the program needs to be in PATH. On Windows, it will also be found next to the calling process. if (!QProcess::startDetached(QStringLiteral("kwatchgnupg"), QStringList())) KMessageBox::error(q, i18n("Could not start the GnuPG Log Viewer (kwatchgnupg). " "Please check your installation."), i18n("Error Starting KWatchGnuPG")); } void forceUpdateCheck() { UpdateNotification::forceUpdateCheck(q); } void slotConfigCommitted(); void slotContextMenuRequested(QAbstractItemView *, const QPoint &p) { if (auto const menu = qobject_cast<QMenu *>(q->factory()->container(QStringLiteral("listview_popup"), q))) { menu->exec(p); } else { qCDebug(KLEOPATRA_LOG) << "no \"listview_popup\" <Menu> in kleopatra's ui.rc file"; } } void slotFocusQuickSearch() { ui.searchTab->searchBar()->lineEdit()->setFocus(); } void showView(const QString &actionName, QWidget *widget) { const auto coll = q->actionCollection(); if (coll) { for (const QString &name : mainViewActionNames) { if (auto action = coll->action(name)) { action->setChecked(name == actionName); } } } ui.stackWidget->setCurrentWidget(widget); if (auto ffci = dynamic_cast<Kleo::FocusFirstChild *>(widget)) { ffci->focusFirstChild(Qt::TabFocusReason); } } void showCertificateView() { if (KeyCache::instance()->keys().empty()) { showView(QStringLiteral("view_certificate_overview"), ui.welcomeWidget); } else { showView(QStringLiteral("view_certificate_overview"), ui.searchTab); } } void showSmartcardView() { showView(QStringLiteral("manage_smartcard"), ui.scWidget); } void showPadView() { if (!ui.padWidget) { ui.padWidget = new PadWidget; ui.stackWidget->addWidget(ui.padWidget); } showView(QStringLiteral("pad_view"), ui.padWidget); ui.stackWidget->resize(ui.padWidget->sizeHint()); } void restartDaemons() { Kleo::restartGpgAgent(); } void showAboutDialog() { // we show the About dialog ourselves so that we can pass up-to-date about data to it; // KXmlGuiWindow takes a copy of the about data on creation and this copy might not // contain the backend version information that's set by a background thread if (!aboutDialog) { qCDebug(KLEOPATRA_LOG) << __func__ << "Creating About dialog"; aboutDialog = new KAboutApplicationDialog(KAboutData::applicationData(), q); aboutDialog->setAttribute(Qt::WA_DeleteOnClose); } if (aboutDialog->isMinimized()) { qCDebug(KLEOPATRA_LOG) << __func__ << "Unminimizing About dialog"; aboutDialog->setWindowState((aboutDialog->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); } qCDebug(KLEOPATRA_LOG) << __func__ << "Showing About dialog"; aboutDialog->show(); } private: void setupActions(); QAbstractItemView *currentView() const { return ui.searchTab->tabWidget()->currentView(); } void keyListingDone() { const auto curWidget = ui.stackWidget->currentWidget(); if (curWidget == ui.scWidget || curWidget == ui.padWidget) { return; } showCertificateView(); } private: Kleo::KeyListController controller; bool firstShow : 1; struct UI { CertificateView *searchTab = nullptr; PadWidget *padWidget = nullptr; SmartCardWidget *scWidget = nullptr; WelcomeWidget *welcomeWidget = nullptr; QStackedWidget *stackWidget = nullptr; explicit UI(MainWindow *q); } ui; QAction *focusToClickSearchAction = nullptr; ClipboardMenu *clipboadMenu = nullptr; QPointer<KAboutApplicationDialog> aboutDialog; }; MainWindow::Private::UI::UI(MainWindow *q) : padWidget(nullptr) { auto mainWidget = new QWidget{q}; auto mainLayout = new QVBoxLayout(mainWidget); mainLayout->setContentsMargins({}); stackWidget = new QStackedWidget{q}; searchTab = new CertificateView{q}; stackWidget->addWidget(searchTab); new KeyCacheOverlay(mainWidget, q); scWidget = new SmartCardWidget{q}; stackWidget->addWidget(scWidget); welcomeWidget = new WelcomeWidget{q}; stackWidget->addWidget(welcomeWidget); mainLayout->addWidget(stackWidget); q->setCentralWidget(mainWidget); } MainWindow::Private::Private(MainWindow *qq) : q(qq) , controller(q) , firstShow(true) , ui(q) { Q_SET_OBJECT_NAME(controller); AbstractKeyListModel *flatModel = AbstractKeyListModel::createFlatKeyListModel(q); AbstractKeyListModel *hierarchicalModel = AbstractKeyListModel::createHierarchicalKeyListModel(q); Q_SET_OBJECT_NAME(flatModel); Q_SET_OBJECT_NAME(hierarchicalModel); #if GPGME_VERSION_NUMBER >= 0x011800 // 1.24.0 auto keyExportDragHandler = std::make_shared<KeyExportDragHandler>(); flatModel->setDragHandler(keyExportDragHandler); hierarchicalModel->setDragHandler(keyExportDragHandler); #endif controller.setFlatModel(flatModel); controller.setHierarchicalModel(hierarchicalModel); controller.setTabWidget(ui.searchTab->tabWidget()); ui.searchTab->tabWidget()->setFlatModel(flatModel); ui.searchTab->tabWidget()->setHierarchicalModel(hierarchicalModel); #ifdef Q_OS_UNIX connect(KWaylandExtras::self(), &KWaylandExtras::windowExported, q, [this](const auto &window, const auto &token) { if (window == q->windowHandle()) { qputenv("PINENTRY_GEOM_HINT", QUrl::toPercentEncoding(token)); } }); q->exportWindow(); #endif setupActions(); ui.stackWidget->setCurrentWidget(ui.searchTab); if (auto action = q->actionCollection()->action(QStringLiteral("view_certificate_overview"))) { action->setChecked(true); } connect(&controller, SIGNAL(contextMenuRequested(QAbstractItemView *, QPoint)), q, SLOT(slotContextMenuRequested(QAbstractItemView *, QPoint))); connect(KeyCache::instance().get(), &KeyCache::keyListingDone, q, [this]() { keyListingDone(); }); q->createGUI(QStringLiteral("kleopatra.rc")); if (auto helpMenu = q->findChild<KHelpMenu *>()) { qCDebug(KLEOPATRA_LOG) << "Hook into the help menu to show the About dialog ourselves"; connect(helpMenu, &KHelpMenu::showAboutApplication, q, [this]() { showAboutDialog(); }); } // make toolbar buttons accessible by keyboard auto toolbar = q->findChild<KToolBar *>(); if (toolbar) { auto toolbarButtons = toolbar->findChildren<QToolButton *>(); for (auto b : toolbarButtons) { b->setFocusPolicy(Qt::TabFocus); } // move toolbar and its child widgets before the central widget in the tab order; // this is necessary to make Shift+Tab work as expected forceSetTabOrder(q, toolbar); auto toolbarChildren = toolbar->findChildren<QWidget *>(); std::for_each(std::rbegin(toolbarChildren), std::rend(toolbarChildren), [toolbar](auto w) { forceSetTabOrder(toolbar, w); }); } if (auto action = q->actionCollection()->action(QStringLiteral("help_whats_this"))) { delete action; } q->setAcceptDrops(true); // set default window size q->resize(QSize(1024, 500)); q->setAutoSaveSettings(); updateSearchBarClickMessage(); updateStatusBar(); if (KeyCache::instance()->initialized()) { keyListingDone(); } // delay setting the models to use the key cache so that the UI (including // the "Loading certificate cache..." overlay) is shown before the // blocking key cache initialization happens QMetaObject::invokeMethod( q, [flatModel, hierarchicalModel]() { flatModel->useKeyCache(true, KeyList::AllKeys); hierarchicalModel->useKeyCache(true, KeyList::AllKeys); }, Qt::QueuedConnection); } MainWindow::Private::~Private() { } MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags) : KXmlGuiWindow(parent, flags) , d(new Private(this)) { } MainWindow::~MainWindow() { } void MainWindow::Private::setupActions() { KActionCollection *const coll = q->actionCollection(); const std::vector<action_data> action_data = { // see keylistcontroller.cpp for more actions // Tools menu #ifndef Q_OS_WIN { "tools_start_kwatchgnupg", i18n("GnuPG Log Viewer"), QString(), "kwatchgnupg", q, [this](bool) { gnupgLogViewer(); }, QString(), }, #endif { "tools_debug_view", i18n("Debug View"), QString(), "", q, [this](bool) { auto dialog = new DebugDialog(q); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->open(); }, QString(), }, { "tools_restart_backend", i18nc("@action:inmenu", "Restart Background Processes"), i18nc("@info:tooltip", "Restart the background processes, e.g. after making changes to the configuration."), "view-refresh", q, [this](bool) { restartDaemons(); }, {}, }, // Help menu #ifdef Q_OS_WIN { "help_check_updates", i18n("Check for updates"), QString(), "gpg4win-compact", q, [this](bool) { forceUpdateCheck(); }, QString(), }, #endif // View menu { "view_certificate_overview", i18nc("@action show certificate overview", "Certificates"), i18n("Show certificate overview"), "view-certificate", q, [this](bool) { showCertificateView(); }, QString(), }, { "pad_view", i18nc("@action show input / output area for encrypting/signing resp. decrypting/verifying text", "Notepad"), i18n("Show pad for encrypting/decrypting and signing/verifying text"), "note", q, [this](bool) { showPadView(); }, QString(), }, { "manage_smartcard", i18nc("@action show smartcard management view", "Smartcards"), i18n("Show smartcard management"), "auth-sim-locked", q, [this](bool) { showSmartcardView(); }, QString(), }, // Settings menu { "settings_self_test", i18n("Perform Self-Test"), QString(), nullptr, q, [this](bool) { selfTest(); }, QString(), }, { "configure_groups", i18n("Configure Groups..."), QString(), "group", q, [this](bool) { configureGroups(); }, QString(), }, // Toolbar { "configure_groups_toolbar", i18nc("@action:intoolbar", "Groups"), QString(), "group", q, [this](bool) { configureGroups(); }, QString(), }}; make_actions_from_data(action_data, coll); if (!Settings().groupsEnabled()) { if (auto action = coll->action(QStringLiteral("configure_groups"))) { delete action; } } for (const QString &name : mainViewActionNames) { if (auto action = coll->action(name)) { action->setCheckable(true); } } KStandardAction::close(q, SLOT(close()), coll); KStandardAction::quit(q, SLOT(closeAndQuit()), coll); KStandardAction::configureToolbars(q, SLOT(configureToolbars()), coll); KStandardAction::keyBindings(q, SLOT(editKeybindings()), coll); KStandardAction::preferences(qApp, SLOT(openOrRaiseConfigDialog()), coll); focusToClickSearchAction = new QAction(i18nc("@action", "Set Focus to Quick Search"), q); coll->addAction(QStringLiteral("focus_to_quickseach"), focusToClickSearchAction); coll->setDefaultShortcut(focusToClickSearchAction, QKeySequence(Qt::ALT | Qt::Key_Q)); connect(focusToClickSearchAction, SIGNAL(triggered(bool)), q, SLOT(slotFocusQuickSearch())); clipboadMenu = new ClipboardMenu(q); clipboadMenu->setMainWindow(q); clipboadMenu->clipboardMenu()->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste"))); clipboadMenu->clipboardMenu()->setPopupMode(QToolButton::InstantPopup); coll->addAction(QStringLiteral("clipboard_menu"), clipboadMenu->clipboardMenu()); /* Add additional help actions for documentation */ const auto compendium = new DocAction(QIcon::fromTheme(QStringLiteral("gpg4win-compact")), i18n("Gpg4win Compendium"), i18nc("The Gpg4win compendium is only available" "at this point (24.7.2017) in german and english." "Please check with Gpg4win before translating this filename.", "gpg4win-compendium-en.pdf"), QStringLiteral("../share/gpg4win"), QUrl(), coll); coll->addAction(QStringLiteral("help_doc_compendium"), compendium); /* Documentation centered around the german approved VS-NfD mode for official * RESTRICTED communication. This is only available in some distributions with * the focus on official communications. */ const auto quickguide = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Quick Guide Encrypt and Sign"), i18nc("Only available in German and English. Leave to English for other languages.", "encrypt_and_sign_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), QUrl(), coll); coll->addAction(QStringLiteral("help_doc_quickguide"), quickguide); const auto symguide = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Password-based Encryption"), i18nc("Only available in German and English. Leave to English for other languages.", "symmetric_encryption_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), QUrl(), coll); coll->addAction(QStringLiteral("help_doc_symenc"), symguide); const auto groups = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("Certificate &Groups"), i18nc("Only available in German and English. Leave to English for other languages.", "groupfeature_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), QUrl(), coll); coll->addAction(QStringLiteral("help_doc_groups"), groups); #ifdef Q_OS_WIN const auto gpgol = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Mail Encryption in Outlook"), i18nc("Only available in German and English. Leave to English for other languages. Only shown on Windows.", "gpgol_outlook_addin_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), QUrl(), coll); coll->addAction(QStringLiteral("help_doc_gpgol"), gpgol); #endif /* The submenu with advanced topics */ const auto certmngmnt = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Certification Management"), i18nc("Only available in German and English. Leave to English for other languages.", "certification_management_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), QUrl(), coll); coll->addAction(QStringLiteral("help_doc_cert_management"), certmngmnt); const auto smartcard = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Smartcard Setup"), i18nc("Only available in German and English. Leave to English for other languages.", "smartcard_setup_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), QUrl(), coll); coll->addAction(QStringLiteral("help_doc_smartcard"), smartcard); const auto man_gnupg = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("GnuPG Command&line"), QStringLiteral("gnupg_manual_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), QUrl(QStringLiteral("https://gnupg.org/documentation/manuals/gnupg/")), coll); coll->addAction(QStringLiteral("help_doc_gnupg"), man_gnupg); /* The secops */ const auto approvalmanual = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("Manual for VS-NfD Approval (German)"), i18nc("Only available in German. Keep German file name for all languages", "Handbuch-Zulassung-gnupgvsd-v3.2.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), QUrl(), coll); coll->addAction(QStringLiteral("help_doc_approval_manual"), approvalmanual); const auto vsa10573 = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("SecOps VSA-10573"), i18nc("Only available in German and English. Leave to English for other languages.", "BSI-VSA-10573-ENG_secops-20220207.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), QUrl(), coll); coll->addAction(QStringLiteral("help_doc_vsa10573"), vsa10573); const auto vsa10584 = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("SecOps VSA-10584"), i18nc("Only available in German and English. Leave to English for other languages.", "BSI-VSA-10584-ENG_secops-20220207.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), QUrl(), coll); coll->addAction(QStringLiteral("help_doc_vsa10584"), vsa10584); q->setStandardToolBarMenuEnabled(true); controller.createActions(coll); ui.searchTab->tabWidget()->createActions(coll); } void MainWindow::Private::slotConfigCommitted() { controller.updateConfig(); updateStatusBar(); } void MainWindow::closeEvent(QCloseEvent *e) { // KMainWindow::closeEvent() insists on quitting the application, // so do not let it touch the event... qCDebug(KLEOPATRA_LOG); if (d->controller.hasRunningCommands()) { if (d->controller.shutdownWarningRequired()) { const int ret = KMessageBox::warningContinueCancel(this, i18n("There are still some background operations ongoing. " "These will be terminated when closing the window. " "Proceed?"), i18n("Ongoing Background Tasks")); if (ret != KMessageBox::Continue) { e->ignore(); return; } } d->controller.cancelCommands(); if (d->controller.hasRunningCommands()) { // wait for them to be finished: setEnabled(false); QEventLoop ev; QTimer::singleShot(100ms, &ev, &QEventLoop::quit); connect(&d->controller, &KeyListController::commandsExecuting, &ev, &QEventLoop::quit); ev.exec(); if (d->controller.hasRunningCommands()) qCWarning(KLEOPATRA_LOG) << "controller still has commands running, this may crash now..."; setEnabled(true); } } unexportWindow(); if (isQuitting || qApp->isSavingSession() || Kleo::userIsElevated()) { d->ui.searchTab->tabWidget()->saveViews(); KConfigGroup grp(KConfigGroup(KSharedConfig::openConfig(), autoSaveGroup())); saveMainWindowSettings(grp); e->accept(); } else { e->ignore(); hide(); } } void MainWindow::showEvent(QShowEvent *e) { KXmlGuiWindow::showEvent(e); if (d->firstShow) { d->ui.searchTab->tabWidget()->loadViews(KSharedConfig::openStateConfig(), QStringLiteral("KeyList")); d->firstShow = false; } if (!savedGeometry.isEmpty()) { restoreGeometry(savedGeometry); } } void MainWindow::hideEvent(QHideEvent *e) { savedGeometry = saveGeometry(); KXmlGuiWindow::hideEvent(e); } void MainWindow::importCertificatesFromFile(const QStringList &files) { if (!files.empty()) { d->createAndStart<ImportCertificateFromFileCommand>(files); } } static QStringList extract_local_files(const QMimeData *data) { const QList<QUrl> urls = data->urls(); // begin workaround KDE/Qt misinterpretation of text/uri-list QList<QUrl>::const_iterator end = urls.end(); if (urls.size() > 1 && !urls.back().isValid()) { --end; } // end workaround QStringList result; std::transform(urls.begin(), end, std::back_inserter(result), std::mem_fn(&QUrl::toLocalFile)); result.erase(std::remove_if(result.begin(), result.end(), std::mem_fn(&QString::isEmpty)), result.end()); return result; } static bool can_decode_local_files(const QMimeData *data) { if (!data) { return false; } return !extract_local_files(data).empty(); } void MainWindow::dragEnterEvent(QDragEnterEvent *e) { qCDebug(KLEOPATRA_LOG); if (can_decode_local_files(e->mimeData())) { e->acceptProposedAction(); } } void MainWindow::dropEvent(QDropEvent *e) { qCDebug(KLEOPATRA_LOG); if (!can_decode_local_files(e->mimeData())) { return; } e->setDropAction(Qt::CopyAction); const QStringList files = extract_local_files(e->mimeData()); KleopatraApplication::instance()->handleFiles(files); e->accept(); } void MainWindow::readProperties(const KConfigGroup &cg) { qCDebug(KLEOPATRA_LOG); KXmlGuiWindow::readProperties(cg); setHidden(cg.readEntry("hidden", false)); } void MainWindow::saveProperties(KConfigGroup &cg) { qCDebug(KLEOPATRA_LOG); KXmlGuiWindow::saveProperties(cg); cg.writeEntry("hidden", isHidden()); } void MainWindow::exportWindow() { #ifdef Q_OS_UNIX (void)winId(); // Ensures that windowHandle() returns the window KWaylandExtras::self()->exportWindow(windowHandle()); #endif } void MainWindow::unexportWindow() { #ifdef Q_OS_UNIX KWaylandExtras::self()->unexportWindow(windowHandle()); #endif } KeyListController *MainWindow::keyListController() { return &d->controller; } #include "mainwindow.moc" #include "moc_mainwindow.cpp" diff --git a/src/view/keytreeview.cpp b/src/view/keytreeview.cpp index 8a0ab79c6..b5bc9e215 100644 --- a/src/view/keytreeview.cpp +++ b/src/view/keytreeview.cpp @@ -1,677 +1,698 @@ /* -*- mode: c++; c-basic-offset:4 -*- view/keytreeview.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include <config-kleopatra.h> #include "keytreeview.h" #include "searchbar.h" #include <Libkleo/KeyList> #include <Libkleo/KeyListModel> #include <Libkleo/KeyListSortFilterProxyModel> #include <Libkleo/KeyRearrangeColumnsProxyModel> #include <Libkleo/Predicates> #include <Libkleo/TreeView> #include "utils/headerview.h" #include "utils/tags.h" #include <Libkleo/KeyCache> #include <Libkleo/KeyFilter> #include <Libkleo/Stl_Util> #include <gpgme++/key.h> #include "kleopatra_debug.h" #include <QAction> #include <QClipboard> #include <QContextMenuEvent> #include <QEvent> #include <QGuiApplication> #include <QHeaderView> #include <QItemSelection> #include <QItemSelectionModel> #include <QLayout> #include <QList> #include <QMenu> #include <QTimer> #include <KLocalizedString> #include <KSharedConfig> +#include <KStandardAction> +#include <qnamespace.h> static int tagsColumn; using namespace Kleo; using namespace GpgME; Q_DECLARE_METATYPE(GpgME::Key) namespace { class TreeViewInternal : public Kleo::TreeView { public: explicit TreeViewInternal(QWidget *parent = nullptr) : Kleo::TreeView{parent} { connect(this, &TreeView::columnEnabled, this, [this](int column) { if (column == tagsColumn) { Tags::enableTags(); } }); } QSize minimumSizeHint() const override { const QSize min = QTreeView::minimumSizeHint(); return QSize(min.width(), min.height() + 5 * fontMetrics().height()); } protected: void focusInEvent(QFocusEvent *event) override { QTreeView::focusInEvent(event); // queue the invokation, so that it happens after the widget itself got focus QMetaObject::invokeMethod(this, &TreeViewInternal::forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection); } private: void forceAccessibleFocusEventForCurrentItem() { // force Qt to send a focus event for the current item to accessibility // tools; otherwise, the user has no idea which item is selected when the // list gets keyboard input focus const auto current = currentIndex(); setCurrentIndex({}); setCurrentIndex(current); } private: QMenu *mHeaderPopup = nullptr; QList<QAction *> mColumnActions; }; const KeyListModelInterface *keyListModel(const QTreeView &view) { const KeyListModelInterface *const klmi = dynamic_cast<KeyListModelInterface *>(view.model()); Q_ASSERT(klmi); return klmi; } } // anon namespace KeyTreeView::KeyTreeView(QWidget *parent) : QWidget(parent) , m_proxy(new KeyListSortFilterProxyModel(this)) , m_additionalProxy(nullptr) , m_view(new TreeViewInternal(this)) , m_flatModel(nullptr) , m_hierarchicalModel(nullptr) , m_stringFilter() , m_keyFilter() , m_isHierarchical(true) + , m_showDefaultContextMenu(true) { init(); } KeyTreeView::KeyTreeView(const KeyTreeView &other) : QWidget(nullptr) , m_proxy(new KeyListSortFilterProxyModel(this)) , m_additionalProxy(other.m_additionalProxy ? other.m_additionalProxy->clone() : nullptr) , m_view(new TreeViewInternal(this)) , m_flatModel(other.m_flatModel) , m_hierarchicalModel(other.m_hierarchicalModel) , m_stringFilter(other.m_stringFilter) , m_keyFilter(other.m_keyFilter) , m_group(other.m_group) , m_isHierarchical(other.m_isHierarchical) + , m_showDefaultContextMenu(other.m_showDefaultContextMenu) { init(); setColumnSizes(other.columnSizes()); setSortColumn(other.sortColumn(), other.sortOrder()); } KeyTreeView::KeyTreeView(const QString &text, const std::shared_ptr<KeyFilter> &kf, AbstractKeyListSortFilterProxyModel *proxy, QWidget *parent, - const KConfigGroup &group) + const KConfigGroup &group, + Options options) : QWidget(parent) , m_proxy(new KeyListSortFilterProxyModel(this)) , m_additionalProxy(proxy) , m_view(new TreeViewInternal(this)) , m_flatModel(nullptr) , m_hierarchicalModel(nullptr) , m_stringFilter(text) , m_keyFilter(kf) , m_group(group) , m_isHierarchical(true) , m_onceResized(false) + , m_showDefaultContextMenu(!(options & Option::NoDefaultContextMenu)) { init(); } void KeyTreeView::setColumnSizes(const std::vector<int> &sizes) { if (sizes.empty()) { return; } Q_ASSERT(m_view); Q_ASSERT(m_view->header()); Q_ASSERT(qobject_cast<HeaderView *>(m_view->header()) == static_cast<HeaderView *>(m_view->header())); if (auto const hv = static_cast<HeaderView *>(m_view->header())) { hv->setSectionSizes(sizes); } } void KeyTreeView::setSortColumn(int sortColumn, Qt::SortOrder sortOrder) { Q_ASSERT(m_view); m_view->sortByColumn(sortColumn, sortOrder); } int KeyTreeView::sortColumn() const { Q_ASSERT(m_view); Q_ASSERT(m_view->header()); return m_view->header()->sortIndicatorSection(); } Qt::SortOrder KeyTreeView::sortOrder() const { Q_ASSERT(m_view); Q_ASSERT(m_view->header()); return m_view->header()->sortIndicatorOrder(); } std::vector<int> KeyTreeView::columnSizes() const { Q_ASSERT(m_view); Q_ASSERT(m_view->header()); Q_ASSERT(qobject_cast<HeaderView *>(m_view->header()) == static_cast<HeaderView *>(m_view->header())); if (auto const hv = static_cast<HeaderView *>(m_view->header())) { return hv->sectionSizes(); } else { return std::vector<int>(); } } void KeyTreeView::restoreLayout(const KConfigGroup &group) { if (!group.isValid() || !m_view->restoreColumnLayout(group.name())) { // if config is empty then use default settings // The numbers have to be in line with the order in // setsSourceColumns above m_view->hideColumn(5); for (int i = 7; i < m_view->model()->columnCount(); ++i) { m_view->hideColumn(i); } if (KeyCache::instance()->initialized()) { QTimer::singleShot(0, this, &KeyTreeView::initializeColumnSizes); } } else { m_onceResized = true; } if (!m_view->isColumnHidden(tagsColumn)) { Tags::enableTags(); } } void KeyTreeView::init() { Q_SET_OBJECT_NAME(m_proxy); Q_SET_OBJECT_NAME(m_view); if (m_group.isValid()) { // Reopen as non const KConfig *conf = m_group.config(); m_group = conf->group(m_group.name()); } if (m_additionalProxy && m_additionalProxy->objectName().isEmpty()) { Q_SET_OBJECT_NAME(m_additionalProxy); } QLayout *layout = new QVBoxLayout(this); Q_SET_OBJECT_NAME(layout); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(m_view); auto headerView = new HeaderView(Qt::Horizontal); Q_SET_OBJECT_NAME(headerView); headerView->installEventFilter(m_view); headerView->setSectionsMovable(true); m_view->setHeader(headerView); m_view->setSelectionBehavior(QAbstractItemView::SelectRows); m_view->setSelectionMode(QAbstractItemView::ExtendedSelection); m_view->setAllColumnsShowFocus(false); m_view->setSortingEnabled(true); m_view->setAccessibleName(i18n("Certificates")); m_view->setAccessibleDescription(m_isHierarchical ? i18n("Hierarchical list of certificates") : i18n("List of certificates")); // we show details on double-click m_view->setExpandsOnDoubleClick(false); + if (m_showDefaultContextMenu) { + m_view->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_view, &KeyTreeView::customContextMenuRequested, this, [this](const auto &pos) { + auto menu = new QMenu; + menu->setAttribute(Qt::WA_DeleteOnClose, true); + menu->addAction(KStandardAction::copy( + this, + [this]() { + QGuiApplication::clipboard()->setText(m_view->currentIndex().data(KeyList::ClipboardRole).toString()); + }, + this)); + menu->popup(m_view->mapToGlobal(pos)); + }); + } + if (model()) { if (m_additionalProxy) { m_additionalProxy->setSourceModel(model()); } else { m_proxy->setSourceModel(model()); } } if (m_additionalProxy) { m_proxy->setSourceModel(m_additionalProxy); if (!m_additionalProxy->parent()) { m_additionalProxy->setParent(this); } } m_proxy->setFilterRegularExpression(QRegularExpression::escape(m_stringFilter.trimmed())); m_proxy->setKeyFilter(m_keyFilter); m_proxy->setSortCaseSensitivity(Qt::CaseInsensitive); auto rearangingModel = new KeyRearrangeColumnsProxyModel(this); rearangingModel->setSourceModel(m_proxy); QList<int> columns = { KeyList::PrettyName, KeyList::PrettyEMail, KeyList::Validity, KeyList::ValidFrom, KeyList::ValidUntil, KeyList::TechnicalDetails, KeyList::KeyID, KeyList::Fingerprint, KeyList::OwnerTrust, KeyList::Origin, KeyList::LastUpdate, KeyList::Issuer, KeyList::SerialNumber, KeyList::Remarks, KeyList::Algorithm, KeyList::Keygrip, }; tagsColumn = columns.indexOf(KeyList::Remarks); rearangingModel->setSourceColumns(columns); m_view->setModel(rearangingModel); /* Handle expansion state */ if (m_group.isValid()) { m_expandedKeys = m_group.readEntry("Expanded", QStringList()); } connect(m_view, &QTreeView::expanded, this, [this](const QModelIndex &index) { if (!index.isValid()) { return; } const auto &key = index.data(KeyList::KeyRole).value<GpgME::Key>(); if (key.isNull()) { return; } const auto fpr = QString::fromLatin1(key.primaryFingerprint()); if (m_expandedKeys.contains(fpr)) { return; } m_expandedKeys << fpr; if (m_group.isValid()) { m_group.writeEntry("Expanded", m_expandedKeys); } }); connect(m_view, &QTreeView::collapsed, this, [this](const QModelIndex &index) { if (!index.isValid()) { return; } const auto &key = index.data(KeyList::KeyRole).value<GpgME::Key>(); if (key.isNull()) { return; } m_expandedKeys.removeAll(QString::fromLatin1(key.primaryFingerprint())); if (m_group.isValid()) { m_group.writeEntry("Expanded", m_expandedKeys); } }); updateModelConnections(nullptr, model()); } void KeyTreeView::restoreExpandState() { if (!KeyCache::instance()->initialized()) { qCWarning(KLEOPATRA_LOG) << "Restore expand state before keycache available. Aborting."; return; } for (const auto &fpr : std::as_const(m_expandedKeys)) { const KeyListModelInterface *const km = keyListModel(*m_view); if (!km) { qCWarning(KLEOPATRA_LOG) << "invalid model"; return; } const auto key = KeyCache::instance()->findByFingerprint(fpr.toLatin1().constData()); if (key.isNull()) { qCDebug(KLEOPATRA_LOG) << "Cannot find:" << fpr << "anymore in cache"; m_expandedKeys.removeAll(fpr); return; } const auto idx = km->index(key); if (!idx.isValid()) { qCDebug(KLEOPATRA_LOG) << "Cannot find:" << fpr << "anymore in model"; m_expandedKeys.removeAll(fpr); return; } m_view->expand(idx); } } void KeyTreeView::setUpTagKeys() { const auto tagKeys = Tags::tagKeys(); if (m_hierarchicalModel) { m_hierarchicalModel->setRemarkKeys(tagKeys); } if (m_flatModel) { m_flatModel->setRemarkKeys(tagKeys); } } KeyTreeView::~KeyTreeView() = default; static QAbstractProxyModel *find_last_proxy(QAbstractProxyModel *pm) { Q_ASSERT(pm); while (auto const sm = qobject_cast<QAbstractProxyModel *>(pm->sourceModel())) { pm = sm; } return pm; } void KeyTreeView::updateModelConnections(AbstractKeyListModel *oldModel, AbstractKeyListModel *newModel) { if (oldModel == newModel) { return; } if (oldModel) { disconnect(oldModel, &QAbstractItemModel::modelAboutToBeReset, this, &KeyTreeView::saveStateBeforeModelChange); disconnect(oldModel, &QAbstractItemModel::modelReset, this, &KeyTreeView::restoreStateAfterModelChange); disconnect(oldModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &KeyTreeView::saveStateBeforeModelChange); disconnect(oldModel, &QAbstractItemModel::rowsInserted, this, &KeyTreeView::restoreStateAfterModelChange); disconnect(oldModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &KeyTreeView::saveStateBeforeModelChange); disconnect(oldModel, &QAbstractItemModel::rowsRemoved, this, &KeyTreeView::restoreStateAfterModelChange); } if (newModel) { connect(newModel, &QAbstractItemModel::modelAboutToBeReset, this, &KeyTreeView::saveStateBeforeModelChange); connect(newModel, &QAbstractItemModel::modelReset, this, &KeyTreeView::restoreStateAfterModelChange); connect(newModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &KeyTreeView::saveStateBeforeModelChange); connect(newModel, &QAbstractItemModel::rowsInserted, this, &KeyTreeView::restoreStateAfterModelChange); connect(newModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &KeyTreeView::saveStateBeforeModelChange); connect(newModel, &QAbstractItemModel::rowsRemoved, this, &KeyTreeView::restoreStateAfterModelChange); } } void KeyTreeView::setFlatModel(AbstractKeyListModel *model) { if (model == m_flatModel) { return; } auto oldModel = m_flatModel; m_flatModel = model; if (!m_isHierarchical) // TODO: this fails when called after setHierarchicalView( false )... { find_last_proxy(m_proxy)->setSourceModel(model); updateModelConnections(oldModel, model); } } void KeyTreeView::setHierarchicalModel(AbstractKeyListModel *model) { if (model == m_hierarchicalModel) { return; } auto oldModel = m_hierarchicalModel; m_hierarchicalModel = model; if (m_isHierarchical) { find_last_proxy(m_proxy)->setSourceModel(model); updateModelConnections(oldModel, model); m_view->expandAll(); for (int column = 0; column < m_view->header()->count(); ++column) { m_view->header()->resizeSection(column, qMax(m_view->header()->sectionSize(column), m_view->header()->sectionSizeHint(column))); } } } void KeyTreeView::setStringFilter(const QString &filter) { if (filter == m_stringFilter) { return; } m_stringFilter = filter; m_proxy->setFilterRegularExpression(QRegularExpression::escape(filter.trimmed())); Q_EMIT stringFilterChanged(filter); } void KeyTreeView::setKeyFilter(const std::shared_ptr<KeyFilter> &filter) { if (filter == m_keyFilter || (filter && m_keyFilter && filter->id() == m_keyFilter->id())) { return; } m_keyFilter = filter; m_proxy->setKeyFilter(filter); Q_EMIT keyFilterChanged(filter); } namespace { QItemSelection itemSelectionFromKeys(const std::vector<Key> &keys, const QTreeView &view) { const QModelIndexList indexes = keyListModel(view)->indexes(keys); return std::accumulate(indexes.cbegin(), indexes.cend(), QItemSelection(), [](QItemSelection selection, const QModelIndex &index) { if (index.isValid()) { selection.merge(QItemSelection(index, index), QItemSelectionModel::Select); } return selection; }); } } void KeyTreeView::selectKeys(const std::vector<Key> &keys) { m_view->selectionModel()->select(itemSelectionFromKeys(keys, *m_view), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } std::vector<Key> KeyTreeView::selectedKeys() const { return keyListModel(*m_view)->keys(m_view->selectionModel()->selectedRows()); } void KeyTreeView::setHierarchicalView(bool on) { if (on == m_isHierarchical) { return; } if (on && !hierarchicalModel()) { qCWarning(KLEOPATRA_LOG) << "hierarchical view requested, but no hierarchical model set"; return; } if (!on && !flatModel()) { qCWarning(KLEOPATRA_LOG) << "flat view requested, but no flat model set"; return; } const std::vector<Key> selectedKeys = this->selectedKeys(); const Key currentKey = keyListModel(*m_view)->key(m_view->currentIndex()); auto oldModel = model(); m_isHierarchical = on; find_last_proxy(m_proxy)->setSourceModel(model()); updateModelConnections(oldModel, model()); if (on) { m_view->expandAll(); } selectKeys(selectedKeys); if (!currentKey.isNull()) { const QModelIndex currentIndex = keyListModel(*m_view)->index(currentKey); if (currentIndex.isValid()) { m_view->selectionModel()->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate); m_view->scrollTo(currentIndex); } } m_view->setAccessibleDescription(m_isHierarchical ? i18n("Hierarchical list of certificates") : i18n("List of certificates")); Q_EMIT hierarchicalChanged(on); } void KeyTreeView::setKeys(const std::vector<Key> &keys, const std::vector<Key::Origin> &extraOrigins) { std::vector<Key> sorted = keys; if (extraOrigins.empty()) { _detail::sort_by_fpr(sorted); _detail::remove_duplicates_by_fpr(sorted); } m_keys = sorted; if (m_flatModel) { m_flatModel->setKeys(sorted, extraOrigins); } if (m_hierarchicalModel) { m_hierarchicalModel->setKeys(sorted, extraOrigins); } } void KeyTreeView::addKeysImpl(const std::vector<Key> &keys, bool select) { if (keys.empty()) { return; } if (m_keys.empty()) { setKeys(keys); return; } std::vector<Key> sorted = keys; _detail::sort_by_fpr(sorted); _detail::remove_duplicates_by_fpr(sorted); std::vector<Key> newKeys = _detail::union_by_fpr(sorted, m_keys); m_keys.swap(newKeys); if (m_flatModel) { m_flatModel->addKeys(sorted); } if (m_hierarchicalModel) { m_hierarchicalModel->addKeys(sorted); } if (select) { selectKeys(sorted); } } void KeyTreeView::addKeysSelected(const std::vector<Key> &keys) { addKeysImpl(keys, true); } void KeyTreeView::addKeysUnselected(const std::vector<Key> &keys) { addKeysImpl(keys, false); } void KeyTreeView::removeKeys(const std::vector<Key> &keys) { if (keys.empty()) { return; } std::vector<Key> sorted = keys; _detail::sort_by_fpr(sorted); _detail::remove_duplicates_by_fpr(sorted); std::vector<Key> newKeys; newKeys.reserve(m_keys.size()); std::set_difference(m_keys.begin(), m_keys.end(), sorted.begin(), sorted.end(), std::back_inserter(newKeys), _detail::ByFingerprint<std::less>()); m_keys.swap(newKeys); if (m_flatModel) { std::for_each(sorted.cbegin(), sorted.cend(), [this](const Key &key) { m_flatModel->removeKey(key); }); } if (m_hierarchicalModel) { std::for_each(sorted.cbegin(), sorted.cend(), [this](const Key &key) { m_hierarchicalModel->removeKey(key); }); } } void KeyTreeView::disconnectSearchBar() { for (const auto &connection : m_connections) { disconnect(connection); } m_connections.clear(); } bool KeyTreeView::connectSearchBar(const SearchBar *bar) { m_connections.reserve(4); m_connections.push_back(connect(this, &KeyTreeView::stringFilterChanged, bar, &SearchBar::setStringFilter)); m_connections.push_back(connect(bar, &SearchBar::stringFilterChanged, this, &KeyTreeView::setStringFilter)); m_connections.push_back(connect(this, &KeyTreeView::keyFilterChanged, bar, &SearchBar::setKeyFilter)); m_connections.push_back(connect(bar, &SearchBar::keyFilterChanged, this, &KeyTreeView::setKeyFilter)); return std::all_of(m_connections.cbegin(), m_connections.cend(), [](const QMetaObject::Connection &conn) { return conn; }); } void KeyTreeView::initializeColumnSizes() { if (m_onceResized || m_view->model()->rowCount() == 0) { return; } m_onceResized = true; m_view->setColumnWidth(KeyList::PrettyName, 260); m_view->setColumnWidth(KeyList::PrettyEMail, 260); for (int i = 2; i < m_view->model()->columnCount(); ++i) { m_view->resizeColumnToContents(i); } } void KeyTreeView::saveStateBeforeModelChange() { m_currentKey = keyListModel(*m_view)->key(m_view->currentIndex()); m_selectedKeys = selectedKeys(); } void KeyTreeView::restoreStateAfterModelChange() { restoreExpandState(); selectKeys(m_selectedKeys); if (!m_currentKey.isNull()) { const QModelIndex currentIndex = keyListModel(*m_view)->index(m_currentKey); if (currentIndex.isValid()) { m_view->selectionModel()->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate); m_view->scrollTo(currentIndex); } } setUpTagKeys(); initializeColumnSizes(); } void KeyTreeView::keyPressEvent(QKeyEvent *event) { if (event == QKeySequence::Copy) { QGuiApplication::clipboard()->setText(view()->currentIndex().data(KeyList::ClipboardRole).toString()); event->accept(); } } #include "moc_keytreeview.cpp" diff --git a/src/view/keytreeview.h b/src/view/keytreeview.h index 07cb3a1e4..68306870f 100644 --- a/src/view/keytreeview.h +++ b/src/view/keytreeview.h @@ -1,170 +1,180 @@ /* -*- mode: c++; c-basic-offset:4 -*- view/keytreeview.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include <QWidget> #include <QString> #include <QStringList> #include <gpgme++/key.h> #include <memory> #include <vector> #include <KConfigGroup> #include <Libkleo/TreeView> class QTreeView; namespace Kleo { class KeyFilter; class AbstractKeyListModel; class AbstractKeyListSortFilterProxyModel; class KeyListSortFilterProxyModel; class SearchBar; class KeyTreeView : public QWidget { Q_OBJECT + + Q_FLAGS(Options) public: + enum Option { + Default = 0x0, + NoDefaultContextMenu = 0x1, + }; + Q_DECLARE_FLAGS(Options, Option) + explicit KeyTreeView(QWidget *parent = nullptr); KeyTreeView(const QString &stringFilter, const std::shared_ptr<KeyFilter> &keyFilter, AbstractKeyListSortFilterProxyModel *additionalProxy, QWidget *parent, - const KConfigGroup &group); + const KConfigGroup &group, + Options options = Option::Default); ~KeyTreeView() override; TreeView *view() const { return m_view; } AbstractKeyListModel *model() const { return m_isHierarchical ? hierarchicalModel() : flatModel(); } AbstractKeyListModel *flatModel() const { return m_flatModel; } AbstractKeyListModel *hierarchicalModel() const { return m_hierarchicalModel; } void setFlatModel(AbstractKeyListModel *model); void setHierarchicalModel(AbstractKeyListModel *model); // extraOrigins contains additional origin information for the keys. It must be in the same order as the keys themselves. // For this reason, setKeys will NOT perform any sorting and filtering if extraOrigins is not empty. void setKeys(const std::vector<GpgME::Key> &keys, const std::vector<GpgME::Key::Origin> &extraOrigins = {}); const std::vector<GpgME::Key> &keys() const { return m_keys; } void selectKeys(const std::vector<GpgME::Key> &keys); std::vector<GpgME::Key> selectedKeys() const; void addKeysUnselected(const std::vector<GpgME::Key> &keys); void addKeysSelected(const std::vector<GpgME::Key> &keys); void removeKeys(const std::vector<GpgME::Key> &keys); #if 0 void setToolTipOptions(int options); int toolTipOptions() const; #endif QString stringFilter() const { return m_stringFilter; } const std::shared_ptr<KeyFilter> &keyFilter() const { return m_keyFilter; } bool isHierarchicalView() const { return m_isHierarchical; } void setColumnSizes(const std::vector<int> &sizes); std::vector<int> columnSizes() const; void setSortColumn(int sortColumn, Qt::SortOrder sortOrder); int sortColumn() const; Qt::SortOrder sortOrder() const; virtual KeyTreeView *clone() const { return new KeyTreeView(*this); } void disconnectSearchBar(); bool connectSearchBar(const SearchBar *bar); void restoreLayout(const KConfigGroup &group); public Q_SLOTS: virtual void setStringFilter(const QString &text); virtual void setKeyFilter(const std::shared_ptr<Kleo::KeyFilter> &filter); virtual void setHierarchicalView(bool on); Q_SIGNALS: void stringFilterChanged(const QString &filter); void keyFilterChanged(const std::shared_ptr<Kleo::KeyFilter> &filter); void hierarchicalChanged(bool on); protected: KeyTreeView(const KeyTreeView &); void keyPressEvent(QKeyEvent *event) override; private: void init(); void initializeColumnSizes(); void addKeysImpl(const std::vector<GpgME::Key> &, bool); void restoreExpandState(); void setUpTagKeys(); void updateModelConnections(AbstractKeyListModel *oldModel, AbstractKeyListModel *newModel); void saveStateBeforeModelChange(); void restoreStateAfterModelChange(); private: std::vector<GpgME::Key> m_keys; KeyListSortFilterProxyModel *m_proxy; AbstractKeyListSortFilterProxyModel *m_additionalProxy; TreeView *m_view; AbstractKeyListModel *m_flatModel; AbstractKeyListModel *m_hierarchicalModel; QString m_stringFilter; std::shared_ptr<KeyFilter> m_keyFilter; QStringList m_expandedKeys; std::vector<GpgME::Key> m_selectedKeys; GpgME::Key m_currentKey; std::vector<QMetaObject::Connection> m_connections; KConfigGroup m_group; bool m_isHierarchical : 1; bool m_onceResized : 1; + bool m_showDefaultContextMenu : 1; }; } diff --git a/src/view/tabwidget.cpp b/src/view/tabwidget.cpp index 804bf3b37..fa36167e8 100644 --- a/src/view/tabwidget.cpp +++ b/src/view/tabwidget.cpp @@ -1,1154 +1,1164 @@ /* -*- mode: c++; c-basic-offset:4 -*- view/tabwidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include <config-kleopatra.h> #include "tabwidget.h" #include "keytreeview.h" #include "kleopatra_debug.h" #include "searchbar.h" #include <settings.h> #include <utils/action_data.h> #include <Libkleo/KeyFilter> #include <Libkleo/KeyFilterManager> #include <Libkleo/KeyListModel> #include <Libkleo/KeyListSortFilterProxyModel> #include <Libkleo/Stl_Util> #include <Libkleo/UserIDProxyModel> #include <gpgme++/key.h> // needed for GPGME_VERSION_NUMBER #include <gpgme.h> #include <KActionCollection> #include <KConfig> #include <KConfigGroup> #include <KLocalizedString> #include <KSharedConfig> #include <QAction> #include <QInputDialog> #include <QTabWidget> #include <QAbstractProxyModel> #include <QHeaderView> #include <QMenu> #include <QRegularExpression> #include <QTimer> #include <QToolButton> #include <QTreeView> #include <QVBoxLayout> #include <map> using namespace Kleo; using namespace GpgME; namespace { class Page : public Kleo::KeyTreeView { Q_OBJECT Page(const Page &other); public: Page(const QString &title, const QString &id, const QString &text, AbstractKeyListSortFilterProxyModel *proxy = nullptr, const QString &toolTip = QString(), QWidget *parent = nullptr, - const KConfigGroup &group = KConfigGroup()); - Page(const KConfigGroup &group, QWidget *parent = nullptr); + const KConfigGroup &group = KConfigGroup(), + KeyTreeView::Options options = KeyTreeView::Default); + Page(const KConfigGroup &group, KeyTreeView::Options options = KeyTreeView::Default, QWidget *parent = nullptr); ~Page() override; void setTemporary(bool temporary); bool isTemporary() const { return m_isTemporary; } void setHierarchicalView(bool hierarchical) override; void setStringFilter(const QString &filter) override; void setKeyFilter(const std::shared_ptr<KeyFilter> &filter) override; QString title() const { return m_title.isEmpty() && keyFilter() ? keyFilter()->name() : m_title; } void setTitle(const QString &title); QString toolTip() const { return m_toolTip.isEmpty() ? title() : m_toolTip; } // not used void setToolTip(const QString &tip); bool canBeClosed() const { return m_canBeClosed; } bool canBeRenamed() const { return m_canBeRenamed; } bool canChangeStringFilter() const { return m_canChangeStringFilter; } bool canChangeKeyFilter() const { return m_canChangeKeyFilter && !m_isTemporary; } bool canChangeHierarchical() const { return m_canChangeHierarchical; } Page *clone() const override { return new Page(*this); } void liftAllRestrictions() { m_canBeClosed = m_canBeRenamed = m_canChangeStringFilter = m_canChangeKeyFilter = m_canChangeHierarchical = true; } void closePage() { m_configGroup.deleteGroup(); m_configGroup.sync(); } KConfigGroup configGroup() const { return m_configGroup; } Q_SIGNALS: void titleChanged(const QString &title); private: void init(); private: QString m_title; QString m_toolTip; bool m_isTemporary : 1; bool m_canBeClosed : 1; bool m_canBeRenamed : 1; bool m_canChangeStringFilter : 1; bool m_canChangeKeyFilter : 1; bool m_canChangeHierarchical : 1; KConfigGroup m_configGroup; }; } // anon namespace Page::Page(const Page &other) : KeyTreeView(other) , m_title(other.m_title) , m_toolTip(other.m_toolTip) , m_isTemporary(other.m_isTemporary) , m_canBeClosed(other.m_canBeClosed) , m_canBeRenamed(other.m_canBeRenamed) , m_canChangeStringFilter(other.m_canChangeStringFilter) , m_canChangeKeyFilter(other.m_canChangeKeyFilter) , m_canChangeHierarchical(other.m_canChangeHierarchical) , m_configGroup(other.configGroup().config()->group(QUuid::createUuid().toString())) { init(); } Page::Page(const QString &title, const QString &id, const QString &text, AbstractKeyListSortFilterProxyModel *proxy, const QString &toolTip, QWidget *parent, - const KConfigGroup &group) - : KeyTreeView(text, KeyFilterManager::instance()->keyFilterByID(id), proxy, parent, group) + const KConfigGroup &group, + KeyTreeView::Options options) + : KeyTreeView(text, KeyFilterManager::instance()->keyFilterByID(id), proxy, parent, group, options) , m_title(title) , m_toolTip(toolTip) , m_isTemporary(false) , m_canBeClosed(true) , m_canBeRenamed(true) , m_canChangeStringFilter(true) , m_canChangeKeyFilter(true) , m_canChangeHierarchical(true) , m_configGroup(group) { init(); } static const char TITLE_ENTRY[] = "title"; static const char STRING_FILTER_ENTRY[] = "string-filter"; static const char KEY_FILTER_ENTRY[] = "key-filter"; static const char HIERARCHICAL_VIEW_ENTRY[] = "hierarchical-view"; static const char COLUMN_SIZES[] = "column-sizes"; static const char SORT_COLUMN[] = "sort-column"; static const char SORT_DESCENDING[] = "sort-descending"; -Page::Page(const KConfigGroup &group, QWidget *parent) - : KeyTreeView(group.readEntry(STRING_FILTER_ENTRY), KeyFilterManager::instance()->keyFilterByID(group.readEntry(KEY_FILTER_ENTRY)), nullptr, parent, group) +Page::Page(const KConfigGroup &group, KeyTreeView::Options options, QWidget *parent) + : KeyTreeView(group.readEntry(STRING_FILTER_ENTRY), + KeyFilterManager::instance()->keyFilterByID(group.readEntry(KEY_FILTER_ENTRY)), + nullptr, + parent, + group, + options) , m_title(group.readEntry(TITLE_ENTRY)) , m_toolTip() , m_isTemporary(false) , m_canBeClosed(!group.isImmutable()) , m_canBeRenamed(!group.isEntryImmutable(TITLE_ENTRY)) , m_canChangeStringFilter(!group.isEntryImmutable(STRING_FILTER_ENTRY)) , m_canChangeKeyFilter(!group.isEntryImmutable(KEY_FILTER_ENTRY)) , m_canChangeHierarchical(!group.isEntryImmutable(HIERARCHICAL_VIEW_ENTRY)) , m_configGroup(group) { init(); setHierarchicalView(group.readEntry(HIERARCHICAL_VIEW_ENTRY, true)); const QList<int> settings = group.readEntry(COLUMN_SIZES, QList<int>()); std::vector<int> sizes; sizes.reserve(settings.size()); std::copy(settings.cbegin(), settings.cend(), std::back_inserter(sizes)); setColumnSizes(sizes); setSortColumn(group.readEntry(SORT_COLUMN, 0), group.readEntry(SORT_DESCENDING, true) ? Qt::DescendingOrder : Qt::AscendingOrder); } void Page::init() { #if GPGME_VERSION_NUMBER >= 0x011800 // 1.24.0 view()->setDragDropMode(QAbstractItemView::DragOnly); view()->setDragEnabled(true); #endif } Page::~Page() { } void Page::setStringFilter(const QString &filter) { if (!m_canChangeStringFilter) { return; } KeyTreeView::setStringFilter(filter); m_configGroup.writeEntry(STRING_FILTER_ENTRY, stringFilter()); m_configGroup.sync(); } void Page::setKeyFilter(const std::shared_ptr<KeyFilter> &filter) { if (!canChangeKeyFilter()) { return; } const QString oldTitle = title(); KeyTreeView::setKeyFilter(filter); const QString newTitle = title(); if (oldTitle != newTitle) { Q_EMIT titleChanged(newTitle); } m_configGroup.writeEntry(KEY_FILTER_ENTRY, keyFilter() ? keyFilter()->id() : QString()); m_configGroup.sync(); } void Page::setTitle(const QString &t) { if (t == m_title) { return; } if (!m_canBeRenamed) { return; } const QString oldTitle = title(); m_title = t; const QString newTitle = title(); if (oldTitle != newTitle) { Q_EMIT titleChanged(newTitle); m_configGroup.writeEntry(TITLE_ENTRY, m_title); m_configGroup.sync(); } } #if 0 // not used void Page::setToolTip(const QString &tip) { if (tip == m_toolTip) { return; } if (!m_canBeRenamed) { return; } const QString oldTip = toolTip(); m_toolTip = tip; const QString newTip = toolTip(); if (oldTip != newTip) { Q_EMIT titleChanged(title()); } } #endif void Page::setHierarchicalView(bool on) { if (!m_canChangeHierarchical) { return; } KeyTreeView::setHierarchicalView(on); m_configGroup.writeEntry(HIERARCHICAL_VIEW_ENTRY, on); m_configGroup.sync(); } void Page::setTemporary(bool on) { if (on == m_isTemporary) { return; } m_isTemporary = on; if (on) { setKeyFilter(std::shared_ptr<KeyFilter>()); } } namespace { class Actions { public: constexpr static const char *Rename = "window_rename_tab"; constexpr static const char *Duplicate = "window_duplicate_tab"; constexpr static const char *Close = "window_close_tab"; constexpr static const char *MoveLeft = "window_move_tab_left"; constexpr static const char *MoveRight = "window_move_tab_right"; constexpr static const char *Hierarchical = "window_view_hierarchical"; constexpr static const char *ExpandAll = "window_expand_all"; constexpr static const char *CollapseAll = "window_collapse_all"; explicit Actions() { } void insert(const std::string &name, QAction *action) { actions.insert({name, action}); } auto get(const std::string &name) const { const auto it = actions.find(name); return (it != actions.end()) ? it->second : nullptr; } void setChecked(const std::string &name, bool checked) const { if (auto action = get(name)) { action->setChecked(checked); } } void setEnabled(const std::string &name, bool enabled) const { if (auto action = get(name)) { action->setEnabled(enabled); } } void setVisible(const std::string &name, bool visible) const { if (auto action = get(name)) { action->setVisible(visible); } } private: std::map<std::string, QAction *> actions; }; } // // // TabWidget // // class TabWidget::Private { friend class ::Kleo::TabWidget; TabWidget *const q; public: - explicit Private(TabWidget *qq); + explicit Private(TabWidget *qq, KeyTreeView::Options options); ~Private() { } private: void slotContextMenu(const QPoint &p); void currentIndexChanged(int index); void slotPageTitleChanged(const QString &title); void slotPageKeyFilterChanged(const std::shared_ptr<KeyFilter> &filter); void slotPageStringFilterChanged(const QString &filter); void slotPageHierarchyChanged(bool on); #ifndef QT_NO_INPUTDIALOG void slotRenameCurrentTab() { renamePage(currentPage()); } #endif // QT_NO_INPUTDIALOG void slotNewTab(); void slotDuplicateCurrentTab() { duplicatePage(currentPage()); } void slotCloseCurrentTab() { closePage(currentPage()); } void slotMoveCurrentTabLeft() { movePageLeft(currentPage()); } void slotMoveCurrentTabRight() { movePageRight(currentPage()); } void slotToggleHierarchicalView(bool on) { toggleHierarchicalView(currentPage(), on); } void slotExpandAll() { expandAll(currentPage()); } void slotCollapseAll() { collapseAll(currentPage()); } #ifndef QT_NO_INPUTDIALOG void renamePage(Page *page); #endif void duplicatePage(Page *page); void closePage(Page *page); void movePageLeft(Page *page); void movePageRight(Page *page); void toggleHierarchicalView(Page *page, bool on); void expandAll(Page *page); void collapseAll(Page *page); void enableDisableCurrentPageActions(); void enableDisablePageActions(const Actions &actions, const Page *page); Page *currentPage() const { Q_ASSERT(!tabWidget->currentWidget() || qobject_cast<Page *>(tabWidget->currentWidget())); return static_cast<Page *>(tabWidget->currentWidget()); } Page *page(unsigned int idx) const { Q_ASSERT(!tabWidget->widget(idx) || qobject_cast<Page *>(tabWidget->widget(idx))); return static_cast<Page *>(tabWidget->widget(idx)); } Page *senderPage() const { QObject *const sender = q->sender(); Q_ASSERT(!sender || qobject_cast<Page *>(sender)); return static_cast<Page *>(sender); } bool isSenderCurrentPage() const { Page *const sp = senderPage(); return sp && sp == currentPage(); } QTreeView *addView(Page *page, Page *columnReference); private: AbstractKeyListModel *flatModel = nullptr; AbstractKeyListModel *hierarchicalModel = nullptr; QToolButton *newTabButton = nullptr; QToolButton *closeTabButton = nullptr; QTabWidget *tabWidget = nullptr; QAction *newAction = nullptr; Actions currentPageActions; Actions otherPageActions; bool actionsCreated = false; KSharedConfig::Ptr config; QString configKey; + KeyTreeView::Options keyTreeViewOptions; }; -TabWidget::Private::Private(TabWidget *qq) +TabWidget::Private::Private(TabWidget *qq, KeyTreeView::Options options) : q{qq} + , keyTreeViewOptions(options) { auto layout = new QVBoxLayout{q}; layout->setContentsMargins(0, 0, 0, 0); // create "New Tab" button before tab widget to ensure correct tab order newTabButton = new QToolButton{q}; tabWidget = new QTabWidget{q}; Q_SET_OBJECT_NAME(tabWidget); layout->addWidget(tabWidget); tabWidget->setMovable(true); tabWidget->tabBar()->setContextMenuPolicy(Qt::CustomContextMenu); // create "Close Tab" button after tab widget to ensure correct tab order closeTabButton = new QToolButton{q}; connect(tabWidget, &QTabWidget::currentChanged, q, [this](int index) { currentIndexChanged(index); }); connect(tabWidget->tabBar(), &QWidget::customContextMenuRequested, q, [this](const QPoint &p) { slotContextMenu(p); }); connect(tabWidget->tabBar(), &QTabBar::tabMoved, q, [this]() { q->saveViews(); }); } void TabWidget::Private::slotContextMenu(const QPoint &p) { const int tabUnderPos = tabWidget->tabBar()->tabAt(p); Page *const contextMenuPage = static_cast<Page *>(tabWidget->widget(tabUnderPos)); const Page *const current = currentPage(); const auto actions = contextMenuPage == current ? currentPageActions : otherPageActions; enableDisablePageActions(actions, contextMenuPage); QMenu menu; if (auto action = actions.get(Actions::Rename)) { menu.addAction(action); } menu.addSeparator(); menu.addAction(newAction); if (auto action = actions.get(Actions::Duplicate)) { menu.addAction(action); } menu.addSeparator(); if (auto action = actions.get(Actions::MoveLeft)) { menu.addAction(action); } if (auto action = actions.get(Actions::MoveRight)) { menu.addAction(action); } menu.addSeparator(); if (auto action = actions.get(Actions::Close)) { menu.addAction(action); } const QAction *const action = menu.exec(tabWidget->tabBar()->mapToGlobal(p)); if (!action) { return; } if (contextMenuPage == current || action == newAction) { return; // performed through signal/slot connections... } #ifndef QT_NO_INPUTDIALOG if (action == otherPageActions.get(Actions::Rename)) { renamePage(contextMenuPage); } #endif // QT_NO_INPUTDIALOG else if (action == otherPageActions.get(Actions::Duplicate)) { duplicatePage(contextMenuPage); } else if (action == otherPageActions.get(Actions::Close)) { closePage(contextMenuPage); } else if (action == otherPageActions.get(Actions::MoveLeft)) { movePageLeft(contextMenuPage); } else if (action == otherPageActions.get(Actions::MoveRight)) { movePageRight(contextMenuPage); } } void TabWidget::Private::currentIndexChanged(int index) { const Page *const page = this->page(index); Q_EMIT q->currentViewChanged(page ? page->view() : nullptr); Q_EMIT q->keyFilterChanged(page ? page->keyFilter() : std::shared_ptr<KeyFilter>()); Q_EMIT q->stringFilterChanged(page ? page->stringFilter() : QString()); enableDisableCurrentPageActions(); } void TabWidget::Private::enableDisableCurrentPageActions() { const Page *const page = currentPage(); Q_EMIT q->enableChangeStringFilter(page && page->canChangeStringFilter()); Q_EMIT q->enableChangeKeyFilter(page && page->canChangeKeyFilter()); enableDisablePageActions(currentPageActions, page); } void TabWidget::Private::enableDisablePageActions(const Actions &actions, const Page *p) { actions.setEnabled(Actions::Rename, p && p->canBeRenamed()); actions.setEnabled(Actions::Duplicate, p); actions.setEnabled(Actions::Close, p && p->canBeClosed() && tabWidget->count() > 1); actions.setEnabled(Actions::MoveLeft, p && tabWidget->indexOf(const_cast<Page *>(p)) != 0); actions.setEnabled(Actions::MoveRight, p && tabWidget->indexOf(const_cast<Page *>(p)) != tabWidget->count() - 1); actions.setEnabled(Actions::Hierarchical, p && p->canChangeHierarchical()); actions.setChecked(Actions::Hierarchical, p && p->isHierarchicalView()); actions.setVisible(Actions::Hierarchical, Kleo::Settings{}.cmsEnabled()); actions.setEnabled(Actions::ExpandAll, p && p->isHierarchicalView()); actions.setEnabled(Actions::CollapseAll, p && p->isHierarchicalView()); } void TabWidget::Private::slotPageTitleChanged(const QString &) { if (Page *const page = senderPage()) { const int idx = tabWidget->indexOf(page); tabWidget->setTabText(idx, page->title()); tabWidget->setTabToolTip(idx, page->toolTip()); } } void TabWidget::Private::slotPageKeyFilterChanged(const std::shared_ptr<KeyFilter> &kf) { if (isSenderCurrentPage()) { Q_EMIT q->keyFilterChanged(kf); } } void TabWidget::Private::slotPageStringFilterChanged(const QString &filter) { if (isSenderCurrentPage()) { Q_EMIT q->stringFilterChanged(filter); } } void TabWidget::Private::slotPageHierarchyChanged(bool) { enableDisableCurrentPageActions(); } void TabWidget::Private::slotNewTab() { auto group = KSharedConfig::openStateConfig()->group(QStringLiteral("%1:View %2").arg(configKey, QUuid::createUuid().toString())); - Page *page = new Page(QString(), QStringLiteral("all-certificates"), QString(), nullptr, QString(), nullptr, group); + Page *page = new Page(QString(), QStringLiteral("all-certificates"), QString(), nullptr, QString(), nullptr, group, keyTreeViewOptions); group.writeEntry(KEY_FILTER_ENTRY, QStringLiteral("all-certificates")); group.sync(); addView(page, currentPage()); tabWidget->setCurrentIndex(tabWidget->count() - 1); q->saveViews(); } void TabWidget::Private::renamePage(Page *page) { if (!page) { return; } bool ok; const QString text = QInputDialog::getText(q, i18n("Rename Tab"), i18n("New tab title:"), QLineEdit::Normal, page->title(), &ok); if (!ok) { return; } page->setTitle(text); } void TabWidget::Private::duplicatePage(Page *page) { if (!page) { return; } Page *const clone = page->clone(); Q_ASSERT(clone); clone->liftAllRestrictions(); addView(clone, page); } void TabWidget::Private::closePage(Page *page) { if (!page || !page->canBeClosed() || tabWidget->count() <= 1) { return; } Q_EMIT q->viewAboutToBeRemoved(page->view()); page->closePage(); tabWidget->removeTab(tabWidget->indexOf(page)); q->saveViews(); enableDisableCurrentPageActions(); } void TabWidget::Private::movePageLeft(Page *page) { if (!page) { return; } const int idx = tabWidget->indexOf(page); if (idx <= 0) { return; } tabWidget->tabBar()->moveTab(idx, idx - 1); enableDisableCurrentPageActions(); } void TabWidget::Private::movePageRight(Page *page) { if (!page) { return; } const int idx = tabWidget->indexOf(page); if (idx < 0 || idx >= tabWidget->count() - 1) { return; } tabWidget->tabBar()->moveTab(idx, idx + 1); enableDisableCurrentPageActions(); } void TabWidget::Private::toggleHierarchicalView(Page *page, bool on) { if (!page) { return; } page->setHierarchicalView(on); } void TabWidget::Private::expandAll(Page *page) { if (!page || !page->view()) { return; } page->view()->expandAll(); } void TabWidget::Private::collapseAll(Page *page) { if (!page || !page->view()) { return; } page->view()->collapseAll(); } -TabWidget::TabWidget(QWidget *p, Qt::WindowFlags f) +TabWidget::TabWidget(KeyTreeView::Options options, QWidget *p, Qt::WindowFlags f) : QWidget(p, f) - , d(new Private(this)) + , d(new Private(this, options)) { } TabWidget::~TabWidget() { saveViews(); } void TabWidget::setFlatModel(AbstractKeyListModel *model) { if (model == d->flatModel) { return; } d->flatModel = model; for (unsigned int i = 0, end = count(); i != end; ++i) if (Page *const page = d->page(i)) { page->setFlatModel(model); } } AbstractKeyListModel *TabWidget::flatModel() const { return d->flatModel; } void TabWidget::setHierarchicalModel(AbstractKeyListModel *model) { if (model == d->hierarchicalModel) { return; } d->hierarchicalModel = model; for (unsigned int i = 0, end = count(); i != end; ++i) if (Page *const page = d->page(i)) { page->setHierarchicalModel(model); } } AbstractKeyListModel *TabWidget::hierarchicalModel() const { return d->hierarchicalModel; } QString TabWidget::stringFilter() const { return d->currentPage() ? d->currentPage()->stringFilter() : QString{}; } void TabWidget::setStringFilter(const QString &filter) { if (Page *const page = d->currentPage()) { page->setStringFilter(filter); } } void TabWidget::setKeyFilter(const std::shared_ptr<KeyFilter> &filter) { if (!filter) { qCDebug(KLEOPATRA_LOG) << "TabWidget::setKeyFilter() trial to set filter=NULL"; return; } if (Page *const page = d->currentPage()) { page->setKeyFilter(filter); } } std::vector<QAbstractItemView *> TabWidget::views() const { std::vector<QAbstractItemView *> result; const unsigned int N = count(); result.reserve(N); for (unsigned int i = 0; i != N; ++i) if (const Page *const p = d->page(i)) { result.push_back(p->view()); } return result; } QAbstractItemView *TabWidget::currentView() const { if (Page *const page = d->currentPage()) { return page->view(); } else { return nullptr; } } KeyListModelInterface *TabWidget::currentModel() const { const QAbstractItemView *const view = currentView(); if (!view) { return nullptr; } auto const proxy = qobject_cast<QAbstractProxyModel *>(view->model()); if (!proxy) { return nullptr; } return dynamic_cast<KeyListModelInterface *>(proxy); } unsigned int TabWidget::count() const { return d->tabWidget->count(); } void TabWidget::setMultiSelection(bool on) { for (unsigned int i = 0, end = count(); i != end; ++i) if (const Page *const p = d->page(i)) if (QTreeView *const view = p->view()) { view->setSelectionMode(on ? QAbstractItemView::ExtendedSelection : QAbstractItemView::SingleSelection); } } void TabWidget::createActions(KActionCollection *coll) { if (!coll) { return; } const action_data actionDataNew = { "window_new_tab", i18n("New Tab"), i18n("Open a new tab"), "tab-new-background", this, [this](bool) { d->slotNewTab(); }, QStringLiteral("CTRL+SHIFT+N"), }; d->newAction = make_action_from_data(actionDataNew, coll); const std::vector<action_data> actionData = { { Actions::Rename, i18n("Rename Tab..."), i18n("Rename this tab"), "edit-rename", this, [this](bool) { d->slotRenameCurrentTab(); }, QStringLiteral("CTRL+SHIFT+R"), RegularQAction, Disabled, }, { Actions::Duplicate, i18n("Duplicate Tab"), i18n("Duplicate this tab"), "tab-duplicate", this, [this](bool) { d->slotDuplicateCurrentTab(); }, QStringLiteral("CTRL+SHIFT+D"), }, { Actions::Close, i18n("Close Tab"), i18n("Close this tab"), "tab-close", this, [this](bool) { d->slotCloseCurrentTab(); }, QStringLiteral("CTRL+SHIFT+W"), RegularQAction, Disabled, }, // ### CTRL-W when available { Actions::MoveLeft, i18n("Move Tab Left"), i18n("Move this tab left"), nullptr, this, [this](bool) { d->slotMoveCurrentTabLeft(); }, QStringLiteral("CTRL+SHIFT+LEFT"), RegularQAction, Disabled, }, { Actions::MoveRight, i18n("Move Tab Right"), i18n("Move this tab right"), nullptr, this, [this](bool) { d->slotMoveCurrentTabRight(); }, QStringLiteral("CTRL+SHIFT+RIGHT"), RegularQAction, Disabled, }, { Actions::Hierarchical, i18n("Hierarchical Certificate List"), QString(), nullptr, this, [this](bool on) { d->slotToggleHierarchicalView(on); }, QString(), KFToggleAction, Disabled, }, { Actions::ExpandAll, i18n("Expand All"), QString(), nullptr, this, [this](bool) { d->slotExpandAll(); }, QStringLiteral("CTRL+."), RegularQAction, Disabled, }, { Actions::CollapseAll, i18n("Collapse All"), QString(), nullptr, this, [this](bool) { d->slotCollapseAll(); }, QStringLiteral("CTRL+,"), RegularQAction, Disabled, }, }; for (const auto &ad : actionData) { d->currentPageActions.insert(ad.name, make_action_from_data(ad, coll)); } for (const auto &ad : actionData) { // create actions for the context menu of the currently not active tabs, // but do not add those actions to the action collection auto action = new QAction(ad.text, coll); if (ad.icon) { action->setIcon(QIcon::fromTheme(QLatin1StringView(ad.icon))); } action->setEnabled(ad.actionState == Enabled); d->otherPageActions.insert(ad.name, action); } d->newTabButton->setDefaultAction(d->newAction); d->tabWidget->setCornerWidget(d->newTabButton, Qt::TopLeftCorner); if (auto action = d->currentPageActions.get(Actions::Close)) { d->closeTabButton->setDefaultAction(action); d->tabWidget->setCornerWidget(d->closeTabButton, Qt::TopRightCorner); } else { d->closeTabButton->setVisible(false); } d->actionsCreated = true; } QAbstractItemView *TabWidget::addView(const QString &title, const QString &id, const QString &text) { auto group = KSharedConfig::openStateConfig()->group(QStringLiteral("%1:View %2").arg(d->configKey, QUuid::createUuid().toString())); - Page *page = new Page(title, id, text, nullptr, QString(), nullptr, group); + Page *page = new Page(title, id, text, nullptr, QString(), nullptr, group, d->keyTreeViewOptions); group.writeEntry(KEY_FILTER_ENTRY, id); group.sync(); return d->addView(page, d->currentPage()); } QAbstractItemView *TabWidget::addView(const KConfigGroup &group, Options options) { Page *page = nullptr; if (options & ShowUserIDs) { page = new Page(group.readEntry(TITLE_ENTRY), group.readEntry(KEY_FILTER_ENTRY), group.readEntry(STRING_FILTER_ENTRY), new UserIDProxyModel(this), {}, nullptr, - group); + group, + d->keyTreeViewOptions); } else { - page = new Page(group); + page = new Page(group, d->keyTreeViewOptions); } QMetaObject::invokeMethod( this, [page, group]() { page->restoreLayout(group); }, Qt::QueuedConnection); return d->addView(page, nullptr); } QAbstractItemView *TabWidget::addTemporaryView(const QString &title, AbstractKeyListSortFilterProxyModel *proxy, const QString &tabToolTip) { const KConfigGroup group = KSharedConfig::openConfig()->group(QStringLiteral("KeyTreeView_default")); - Page *const page = new Page(title, QString(), QString(), proxy, tabToolTip, nullptr, group); + Page *const page = new Page(title, QString(), QString(), proxy, tabToolTip, nullptr, group, d->keyTreeViewOptions); page->setTemporary(true); QAbstractItemView *v = d->addView(page, d->currentPage()); d->tabWidget->setCurrentIndex(d->tabWidget->count() - 1); return v; } QTreeView *TabWidget::Private::addView(Page *page, Page *columnReference) { if (!page) { return nullptr; } if (!actionsCreated) { auto coll = new KActionCollection(q); q->createActions(coll); } page->setFlatModel(flatModel); page->setHierarchicalModel(hierarchicalModel); connect(page, &Page::titleChanged, q, [this](const QString &text) { slotPageTitleChanged(text); }); connect(page, &Page::keyFilterChanged, q, [this](const std::shared_ptr<Kleo::KeyFilter> &filter) { slotPageKeyFilterChanged(filter); }); connect(page, &Page::stringFilterChanged, q, [this](const QString &text) { slotPageStringFilterChanged(text); }); connect(page, &Page::hierarchicalChanged, q, [this](bool on) { slotPageHierarchyChanged(on); }); if (columnReference) { QMetaObject::invokeMethod( q, [=]() { page->setColumnSizes(columnReference->columnSizes()); page->setSortColumn(columnReference->sortColumn(), columnReference->sortOrder()); page->view()->saveColumnLayout(page->configGroup().name()); }, Qt::QueuedConnection); for (auto i = 0; i < columnReference->view()->model()->columnCount(); i++) { page->view()->setColumnHidden(i, columnReference->view()->isColumnHidden(i)); page->view()->header()->moveSection(page->view()->header()->visualIndex(i), columnReference->view()->header()->visualIndex(i)); } for (auto row = 0; row < page->view()->model()->rowCount(); row++) { page->view()->setExpanded(page->view()->model()->index(row, 0), columnReference->view()->isExpanded(columnReference->view()->model()->index(row, 0))); } } QAbstractItemView *const previous = q->currentView(); const int tabIndex = tabWidget->addTab(page, page->title()); setTabOrder(closeTabButton, page->view()); tabWidget->setTabToolTip(tabIndex, page->toolTip()); // work around a bug in QTabWidget (tested with 4.3.2) not emitting currentChanged() when the first widget is inserted QAbstractItemView *const current = q->currentView(); if (previous != current) { currentIndexChanged(tabWidget->currentIndex()); } enableDisableCurrentPageActions(); QTreeView *view = page->view(); Q_EMIT q->viewAdded(view); return view; } static QStringList extractViewGroups(const KConfigGroup &config) { return config.readEntry("Tabs", QStringList()); } void TabWidget::loadViews(const KSharedConfig::Ptr &config, const QString &configKey, Options options) { d->config = config; d->configKey = configKey; QStringList groupList = extractViewGroups(config->group(configKey)); for (const QString &view : std::as_const(groupList)) { addView(KConfigGroup(config, view), options); } if (!count()) { // add default view: addView({}, QStringLiteral("all-certificates")); } } void TabWidget::saveViews() { if (!d->config) { return; } QStringList tabs; for (unsigned int i = 0, end = count(); i != end; ++i) { if (Page *const p = d->page(i)) { if (p->isTemporary()) { continue; } tabs += p->configGroup().name(); } } d->config->group(d->configKey).writeEntry("Tabs", tabs); d->config->sync(); } void TabWidget::connectSearchBar(SearchBar *sb) { connect(sb, &SearchBar::stringFilterChanged, this, &TabWidget::setStringFilter); connect(this, &TabWidget::stringFilterChanged, sb, &SearchBar::setStringFilter); connect(sb, &SearchBar::keyFilterChanged, this, &TabWidget::setKeyFilter); connect(this, &TabWidget::keyFilterChanged, sb, &SearchBar::setKeyFilter); connect(this, &TabWidget::enableChangeStringFilter, sb, &SearchBar::setChangeStringFilterEnabled); connect(this, &TabWidget::enableChangeKeyFilter, sb, &SearchBar::setChangeKeyFilterEnabled); } #include "moc_tabwidget.cpp" #include "tabwidget.moc" diff --git a/src/view/tabwidget.h b/src/view/tabwidget.h index b1f53547d..3a99fba82 100644 --- a/src/view/tabwidget.h +++ b/src/view/tabwidget.h @@ -1,92 +1,94 @@ /* -*- mode: c++; c-basic-offset:4 -*- view/tabwidget.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once +#include "keytreeview.h" + #include <KSharedConfig> #include <QWidget> #include <memory> #include <vector> class QAbstractItemView; class KConfigGroup; class KActionCollection; namespace Kleo { class AbstractKeyListModel; class AbstractKeyListSortFilterProxyModel; class KeyFilter; class KeyListModelInterface; class SearchBar; class TabWidget : public QWidget { Q_OBJECT public: enum Option { ShowKeys = 0x00, ShowUserIDs = 0x01, }; Q_DECLARE_FLAGS(Options, Option) - explicit TabWidget(QWidget *parent = nullptr, Qt::WindowFlags f = {}); + explicit TabWidget(KeyTreeView::Options options = KeyTreeView::Option::Default, QWidget *parent = nullptr, Qt::WindowFlags f = {}); ~TabWidget() override; void setFlatModel(AbstractKeyListModel *model); AbstractKeyListModel *flatModel() const; void setHierarchicalModel(AbstractKeyListModel *model); AbstractKeyListModel *hierarchicalModel() const; QAbstractItemView *addView(const QString &title = QString(), const QString &keyFilterID = QString(), const QString &searchString = QString()); QAbstractItemView *addView(const KConfigGroup &group, Options options); QAbstractItemView * addTemporaryView(const QString &title = QString(), AbstractKeyListSortFilterProxyModel *proxy = nullptr, const QString &tabToolTip = QString()); void loadViews(const KSharedConfig::Ptr &config, const QString &configKeys, Options options = ShowKeys); void saveViews(); std::vector<QAbstractItemView *> views() const; QAbstractItemView *currentView() const; KeyListModelInterface *currentModel() const; unsigned int count() const; void createActions(KActionCollection *collection); void connectSearchBar(SearchBar *sb); void setMultiSelection(bool on); QString stringFilter() const; public Q_SLOTS: void setKeyFilter(const std::shared_ptr<Kleo::KeyFilter> &filter); void setStringFilter(const QString &filter); Q_SIGNALS: void viewAdded(QAbstractItemView *view); void viewAboutToBeRemoved(QAbstractItemView *view); void currentViewChanged(QAbstractItemView *view); void stringFilterChanged(const QString &filter); void keyFilterChanged(const std::shared_ptr<Kleo::KeyFilter> &filter); void enableChangeStringFilter(bool enable); void enableChangeKeyFilter(bool enable); private: class Private; const std::unique_ptr<Private> d; }; }