diff --git a/src/widgets/mainwindow.cpp b/src/widgets/mainwindow.cpp index 9a5a04e..cb48fee 100644 --- a/src/widgets/mainwindow.cpp +++ b/src/widgets/mainwindow.cpp @@ -1,908 +1,909 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.com> SPDX-FileCopyrightText: 2016-2017 tezeb <tezeb+github@outoftheblue.pl> SPDX-FileCopyrightText: 2018 Lukas Vogel <lukedirtwalker@gmail.com> SPDX-FileCopyrightText: 2018 Claudio Maradonna <penguyman@stronzi.org> SPDX-FileCopyrightText: 2019 Maciej S. Szmigiero <mail@maciej.szmigiero.name> SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela <sune@vuorela.dk> SPDX-License-Identifier: GPL-3.0-or-later */ #include "mainwindow.h" #include <gpgpass_version.h> #include "clipboardhelper.h" #include "conf/configuredialog.h" #include "job/directoryreencryptjob.h" #include "job/filedecryptjob.h" #include "job/fileencryptjob.h" #include "kplaceholderwidget.h" #include "models/addfileinfoproxy.h" #include "passentry.h" #include "rootfoldersmanager.h" #include "setupwidget.h" #include "ui_mainwindow.h" #include "usersdialog.h" #include "util.h" #include "widgets/passwordeditorwidget.h" #include "widgets/passwordviewerwidget.h" #include <KActionCollection> #include <KActionMenu> #include <KColorSchemeManager> #include <KColorSchemeMenu> #include <KLocalizedString> #include <KMessageBox> #include <KMessageWidget> #include <KStandardAction> #include <KStandardGuiItem> #include <QCloseEvent> #include <QComboBox> #include <QDebug> #include <QDirIterator> #include <QFileInfo> #include <QInputDialog> #include <QMenu> #include <QMessageBox> #include <QShortcut> #include <qstringliteral.h> enum class StackLayer { WelcomePage = 0, PasswordViewer = 1, ConfigurationPage = 2, PasswordEditor = 3, }; static QString directoryName(const QString &dirOrFile) { QFileInfo fi{dirOrFile}; if (fi.isDir()) { return fi.absoluteFilePath(); } else { return fi.absolutePath(); } } /** * @brief MainWindow::MainWindow handles all of the main functionality and also * the main window. * @param searchText for searching from cli * @param parent pointer */ MainWindow::MainWindow(QWidget *parent) : KXmlGuiWindow(parent) , ui(new Ui::MainWindow) , m_clipboardHelper(new ClipboardHelper(this)) , m_passwordViewer(new PasswordViewerWidget(m_clipboardHelper, this)) , m_passwordEditor(new PasswordEditorWidget(m_clipboardHelper, this)) , m_rootFoldersManager(new RootFoldersManager(this)) { ui->setupUi(this); initToolBarButtons(); setupGUI(ToolBar | Keys | Save | Create, QStringLiteral("gpgpassui.rc")); setCentralWidget(ui->centralWidget); ui->stackedWidget->addWidget(m_passwordViewer); auto setupWidget = new SetupWidget(this); ui->stackedWidget->addWidget(setupWidget); ui->stackedWidget->addWidget(m_passwordEditor); connect(setupWidget, &SetupWidget::setupComplete, this, &MainWindow::slotSetupFinished); connect(m_passwordEditor, &PasswordEditorWidget::editorClosed, this, [this]() { m_actionEdit->setChecked(false); }); ui->separator->setFixedHeight(1); ui->separator->setFrameStyle(QFrame::HLine); ui->lineEditWrapper->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin), style()->pixelMetric(QStyle::PM_LayoutTopMargin), style()->pixelMetric(QStyle::PM_LayoutRightMargin), style()->pixelMetric(QStyle::PM_LayoutBottomMargin)); m_errorMessage = new KMessageWidget(); m_errorMessage->setMessageType(KMessageWidget::Error); #if QT_VERSION > QT_VERSION_CHECK(6, 0, 0) m_errorMessage->setPosition(KMessageWidget::Position::Header); #endif m_errorMessage->hide(); ui->messagesArea->addWidget(m_errorMessage); m_storeModel.setRootFoldersManager(m_rootFoldersManager); connect(&m_storeModel, &StoreModel::errorOccurred, this, [this](auto str) { m_errorMessage->setText(str); m_errorMessage->animatedShow(); setUiElementsEnabled(true); }); connect(&m_storeModel, &StoreModel::rootFoldersSizeChanged, this, &MainWindow::updateRootIndex); connect(m_passwordViewer, &PasswordViewerWidget::loaded, this, [this] { setUiElementsEnabled(true); }); } void MainWindow::setVisible(bool visible) { // This originated in the ctor, but we want this to happen after the first start wizard has been run // so for now, moved to show() on first call if (visible && firstShow) { firstShow = false; // register shortcut ctrl/cmd + Q to close the main window new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this, SLOT(close())); ui->treeView->setModel(&m_storeModel); ui->treeView->setHeaderHidden(true); ui->treeView->setIndentation(15); ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu); ui->treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch); ui->treeView->sortByColumn(0, Qt::AscendingOrder); connect(ui->treeView, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenu); connect(ui->treeView, &DeselectableTreeView::emptyClicked, this, &MainWindow::deselect); searchTimer.setInterval(350); searchTimer.setSingleShot(true); auto placeholderWidget = new KPlaceholderWidget(ui->treeView); placeholderWidget->setTitle(i18nc("@title", "No Passwords Found")); placeholderWidget->setText(i18nc("@info", "Add a password to proceed")); placeholderWidget->addAction(m_actionAddEntry); auto updatePlaceholder = [this, placeholderWidget](const QModelIndex &index = {}, int start = 0, int end = 0) { Q_UNUSED(start); Q_UNUSED(end); Q_UNUSED(index); placeholderWidget->setVisible(!m_storeModel.hasChildren(m_storeModel.index(0, 0))); }; connect(&m_storeModel, &QAbstractListModel::rowsInserted, this, updatePlaceholder); connect(&m_storeModel, &QAbstractListModel::rowsRemoved, this, updatePlaceholder); updatePlaceholder(); connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch); ui->lineEdit->setClearButtonEnabled(true); setUiElementsEnabled(true); QTimer::singleShot(10, this, SLOT(focusInput())); verifyInitialized(); updateRootIndex(); } QMainWindow::setVisible(visible); } MainWindow::~MainWindow() = default; void MainWindow::updateRootIndex() { if (m_rootFoldersManager->rootFolders().count() == 1) { ui->treeView->setRootIndex(m_storeModel.index(0, 0)); } else { ui->treeView->setRootIndex({}); } } /** * @brief MainWindow::focusInput selects any text (if applicable) in the search * box and sets focus to it. Allows for easy searching, called at application * start and when receiving empty message in MainWindow::messageAvailable when * compiled with SINGLE_APP=1 (default). */ void MainWindow::focusInput() { ui->lineEdit->selectAll(); ui->lineEdit->setFocus(); } /** * @brief MainWindow::changeEvent sets focus to the search box * @param event */ void MainWindow::changeEvent(QEvent *event) { QWidget::changeEvent(event); if (event->type() == QEvent::ActivationChange) { if (isActiveWindow()) { focusInput(); } } } /** * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions */ void MainWindow::initToolBarButtons() { m_actionAddEntry = new QAction(QIcon::fromTheme(QStringLiteral("entry-new")), i18nc("@action", "Add Entry"), this); m_actionAddEntry->setToolTip(i18nc("@info:tooltip", "Add Password Entry")); actionCollection()->addAction(QStringLiteral("add_entry"), m_actionAddEntry); connect(m_actionAddEntry, &QAction::triggered, this, &MainWindow::addPassword); m_actionAddFolder = new QAction(QIcon::fromTheme(QStringLiteral("folder-new-symbolic")), i18nc("@action", "Add Folder"), this); actionCollection()->addAction(QStringLiteral("add_folder"), m_actionAddFolder); connect(m_actionAddFolder, &QAction::triggered, this, &MainWindow::addFolder); m_actionDelete = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete-symbolic")), i18nc("@action", "Delete"), this); actionCollection()->addAction(QStringLiteral("gpgpass_delete"), m_actionDelete); connect(m_actionDelete, &QAction::triggered, this, &MainWindow::onDelete); m_actionEdit = new QAction(QIcon::fromTheme(QStringLiteral("document-properties-symbolic")), i18nc("@action", "Edit"), this); m_actionEdit->setCheckable(true); actionCollection()->addAction(QStringLiteral("gpgpass_edit"), m_actionEdit); connect(m_actionEdit, &QAction::toggled, this, &MainWindow::onEdit); m_actionUsers = new QAction(QIcon::fromTheme(QStringLiteral("x-office-address-book-symbolic")), i18nc("@action", "Users"), this); m_actionUsers->setToolTip(i18nc("@info:tooltip", "Manage who can read password in folder")); actionCollection()->addAction(QStringLiteral("gpgpass_users"), m_actionUsers); connect(m_actionUsers, &QAction::triggered, this, &MainWindow::onUsers); KStandardAction::quit(this, &MainWindow::close, actionCollection()); m_actionConfig = KStandardAction::preferences( this, [this] { openConfig(ConfigureDialog::Page::None); }, actionCollection()); connect(ui->treeView, &QTreeView::clicked, this, &MainWindow::selectTreeItem); connect(ui->lineEdit, &QLineEdit::textChanged, this, &MainWindow::filterList); connect(ui->lineEdit, &QLineEdit::returnPressed, this, &MainWindow::selectFromSearch); #if QT_VERSION > QT_VERSION_CHECK(6, 0, 0) auto manager = KColorSchemeManager::instance(); actionCollection()->addAction(QStringLiteral("colorscheme_menu"), KColorSchemeMenu::createMenu(manager, this)); #endif } const QModelIndex MainWindow::getCurrentTreeViewIndex() { return ui->treeView->currentIndex(); } void MainWindow::openConfig(ConfigureDialog::Page page) { QScopedPointer<ConfigureDialog> dialog(new ConfigureDialog(m_rootFoldersManager, this)); dialog->setModal(true); dialog->openPage(page); if (dialog->exec()) { if (dialog->result() == QDialog::Accepted) { show(); m_passwordViewer->setPanelTimer(); m_clipboardHelper->setClipboardTimer(); } } } /** * @brief MainWindow::on_treeView_clicked read the selected password file * @param index */ void MainWindow::selectTreeItem(const QModelIndex &index) { bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags; m_clipboardHelper->clearClippedText(); QString file = index.data(QFileSystemModel::FilePathRole).toString(); m_actionEdit->setChecked(false); QFileInfo fileInfo(file); if (!file.isEmpty() && fileInfo.isFile() && !cleared) { m_selectedFile = file; const auto filePath = fileInfo.absoluteFilePath(); if (m_passwordViewer->filePath() != filePath) { m_passwordViewer->setFilePath(ui->treeView->selectionModel()->currentIndex().data().toString(), filePath); } ui->stackedWidget->setCurrentIndex((int)StackLayer::PasswordViewer); } else { m_passwordViewer->clear(); m_actionEdit->setEnabled(false); m_actionDelete->setEnabled(index.parent().isValid() || m_rootFoldersManager->rootFolders().count() == 1); ui->stackedWidget->setCurrentIndex((int)StackLayer::WelcomePage); } } /** * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on * TreeViewItem, open the edit Window * @param index */ void MainWindow::editTreeItem(const QModelIndex &index) { QFileInfo fileInfo{index.data(QFileSystemModel::Roles::FilePathRole).toString()}; if (!fileInfo.isFile()) { return; } const auto filePath = fileInfo.absoluteFilePath(); if (m_passwordViewer->filePath() == filePath && !m_passwordViewer->rawContent().isEmpty()) { switchToPasswordEditor(filePath, m_passwordViewer->rawContent()); } else { auto decryptJob = new FileDecryptJob(filePath); connect(decryptJob, &FileDecryptJob::finished, this, [this, decryptJob](KJob *) { if (decryptJob->error() != KJob::NoError) { m_errorMessage->setText(decryptJob->errorText()); m_errorMessage->animatedShow(); return; } switchToPasswordEditor(decryptJob->filePath(), decryptJob->content()); }); decryptJob->start(); } } void MainWindow::switchToPasswordEditor(const QString &filePath, const QString &content) { // Ensure we don't trigger the actionEdit disconnect(m_actionEdit, &QAction::toggled, this, &MainWindow::onEdit); m_actionEdit->setChecked(true); connect(m_actionEdit, &QAction::toggled, this, &MainWindow::onEdit); const QFileInfo fileInfo(filePath); const auto name = fileInfo.baseName(); const auto absoluteFilePath = fileInfo.absoluteFilePath(); PassEntry entry(name, content); m_passwordEditor->setPassEntry(entry); ui->stackedWidget->setCurrentIndex((int)StackLayer::PasswordEditor); disconnect(m_passwordEditor, &PasswordEditorWidget::save, this, nullptr); connect(m_passwordEditor, &PasswordEditorWidget::save, this, [this, name, fileInfo, absoluteFilePath](const QString &content) { const auto recipients = m_storeModel.recipientsForFile(fileInfo); if (recipients.isEmpty()) { m_errorMessage->setText(i18n("Could not read encryption key to use, .gpg-id file missing or invalid.")); m_errorMessage->animatedShow(); return; } auto encryptJob = new FileEncryptJob(absoluteFilePath, content.toUtf8(), recipients); connect(encryptJob, &FileEncryptJob::finished, this, [absoluteFilePath, name, encryptJob, content, this](KJob *) { if (encryptJob->error() != KJob::NoError) { m_errorMessage->setText(encryptJob->errorText()); m_errorMessage->animatedShow(); qWarning() << encryptJob->errorText(); return; } ui->treeView->setFocus(); // Ensure we don't trigger the actionEdit disconnect(m_actionEdit, &QAction::toggled, this, &MainWindow::onEdit); m_actionEdit->setChecked(false); connect(m_actionEdit, &QAction::toggled, this, &MainWindow::onEdit); if (m_passwordViewer->filePath() == absoluteFilePath) { m_passwordViewer->setRawContent(content); } else { // slower but ensure state is correct m_passwordViewer->setFilePath(name, absoluteFilePath); } ui->stackedWidget->setCurrentIndex((int)StackLayer::PasswordViewer); auto index = m_storeModel.indexForPath(absoluteFilePath); ui->treeView->setCurrentIndex(index); selectTreeItem(index); }); encryptJob->start(); }); } /** * @brief MainWindow::deselect clear the selection, password and copy buffer */ void MainWindow::deselect() { m_clipboardHelper->clearClipboard(); ui->treeView->clearSelection(); m_actionEdit->setEnabled(false); m_actionDelete->setEnabled(false); m_passwordViewer->clear(); ui->stackedWidget->setCurrentIndex((int)StackLayer::WelcomePage); } /** * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI * elements * @param state */ void MainWindow::setUiElementsEnabled(bool state) { ui->treeView->setEnabled(state); ui->lineEdit->setEnabled(state); ui->lineEdit->installEventFilter(this); m_actionAddEntry->setEnabled(state); m_actionAddFolder->setEnabled(state); m_actionUsers->setEnabled(state); m_actionConfig->setEnabled(state); // is a file selected? state &= ui->treeView->currentIndex().isValid(); m_actionDelete->setEnabled(state); m_actionEdit->setEnabled(state); } /** * @brief Executes when the string in the search box changes, collapses the * TreeView * @param arg1 */ void MainWindow::filterList(const QString &arg1) { ui->treeView->expandAll(); searchTimer.start(); } /** * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much * time from two keypresses is elapsed */ void MainWindow::onTimeoutSearch() { QString query = ui->lineEdit->text(); if (query.isEmpty()) { ui->treeView->collapseAll(); deselect(); } query.replace(QStringLiteral(" "), QStringLiteral(".*")); QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption); m_storeModel.setFilterRegularExpression(regExp); if (m_storeModel.rowCount() > 0 && !query.isEmpty()) { selectFirstFile(); } else { m_actionEdit->setEnabled(false); m_actionDelete->setEnabled(false); } } /** * @brief MainWindow::on_lineEdit_returnPressed get searching * * Select the first possible file in the tree */ void MainWindow::selectFromSearch() { if (m_storeModel.rowCount() > 0) { selectFirstFile(); selectTreeItem(ui->treeView->currentIndex()); } } /** * @brief MainWindow::selectFirstFile select the first possible file in the * tree */ void MainWindow::selectFirstFile() { auto model = ui->treeView->model(); auto index = firstFile(model->index(0, 0)); ui->treeView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect); } /** * @brief MainWindow::firstFile return location of first possible file * @param parentIndex * @return QModelIndex */ QModelIndex MainWindow::firstFile(QModelIndex parentIndex) { auto model = parentIndex.model(); int numRows = model->rowCount(parentIndex); for (int row = 0; row < numRows; ++row) { auto index = model->index(row, 0, parentIndex); if (index.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>().isFile()) return index; if (model->hasChildren(index)) return firstFile(index); } return parentIndex; } QString MainWindow::fallbackStore() { const auto rootFolders = m_rootFoldersManager->rootFolders(); if (rootFolders.isEmpty()) { QMessageBox::critical(this, i18nc("@title:dialog", "No Password Store Found"), i18nc("@info", "Please add a password store first.")); return {}; } return rootFolders[0]->path(); } /** * @brief MainWindow::addPassword add a new password by showing a * number of dialogs. */ void MainWindow::addPassword() { bool ok; QString dir = directoryName(ui->treeView->currentIndex().data(QFileSystemModel::Roles::FilePathRole).toString()); if (dir.isEmpty()) { dir = fallbackStore(); if (dir.isEmpty()) { return; } } QString file = QInputDialog::getText(this, i18nc("@title:dialog", "New File"), xi18nc("@info", "<para>New password file:</para><para>(Will be placed in <filename>%1</filename>)</para>", dir), QLineEdit::Normal, QString{}, &ok); if (!ok || file.isEmpty()) return; file = QDir(dir).absoluteFilePath(file + QStringLiteral(".gpg")); switchToPasswordEditor(file); } /** * @brief MainWindow::onDelete remove password, if you are * sure. */ void MainWindow::onDelete() { QModelIndex currentIndex = ui->treeView->currentIndex(); if (!currentIndex.isValid()) { // If not valid, we might end up passing empty string // to delete, and taht might delete unexpected things on disk return; } QFileInfo fileOrFolder{currentIndex.data(QFileSystemModel::FilePathRole).toString()}; bool isDir = fileOrFolder.isDir(); QString file = fileOrFolder.absoluteFilePath(); QString message; if (isDir) { message = xi18nc("@info deleting a folder; placeholder is folder name", "Are you sure you want to remove <filename>%1</filename> and the whole content?", file); QDirIterator it(file, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); if (auto fi = it.fileInfo(); fi.isFile()) { if (fi.suffix() != QStringLiteral("gpg")) { message += QStringLiteral("<br><strong>") + i18nc("extra warning during certain folder deletions", "Attention: " "there are unexpected files in the given folder, " "check them before continue") + QStringLiteral("</strong>"); break; } } } } else { message = xi18nc("@info deleting a file; placeholder is file name", "Are you sure you want to remove <filename>%1</filename>?", file); } if (KMessageBox::warningTwoActions(this, message, i18nc("@title:dialog", "Confirm Deletion"), KStandardGuiItem::remove(), KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction) return; if (!isDir) { QFile(file).remove(); } else { QDir dir(file); dir.removeRecursively(); } } /** * @brief MainWindow::onEdit try and edit (selected) password. */ void MainWindow::onEdit(bool edit) { if (edit) { editTreeItem(ui->treeView->currentIndex()); } else { selectTreeItem(getCurrentTreeViewIndex()); m_actionEdit->setChecked(false); } } /** * @brief MainWindow::userDialog see MainWindow::onUsers() * @param dir folder to edit users for. */ void MainWindow::userDialog(QString dir) { if (dir.isEmpty()) { dir = fallbackStore(); if (dir.isEmpty()) { return; } } QFileInfo fi(dir); if (!fi.isDir()) { dir = fi.absolutePath(); } const auto recipients = m_storeModel.recipientsForFile(QFileInfo(dir)); auto usersDialog = new UsersDialog(recipients, this); usersDialog->setAttribute(Qt::WA_DeleteOnClose); connect(usersDialog, &UsersDialog::save, this, [dir, this, usersDialog](const QList<QByteArray> &recipients) { auto reencryptJob = new DirectoryReencryptJob(m_storeModel, recipients, dir); connect(reencryptJob, &DirectoryReencryptJob::result, this, [this, usersDialog](KJob *job) { if (job->error() != KJob::NoError) { usersDialog->showError(job->errorText()); return; } setUiElementsEnabled(true); ui->treeView->setFocus(); verifyInitialized(); usersDialog->deleteLater(); }); // statusBar()->showMessage(i18n("Re-encrypting folders"), 3000); setUiElementsEnabled(false); ui->treeView->setDisabled(true); reencryptJob->start(); }); usersDialog->show(); } /** * @brief MainWindow::onUsers edit users for the current * folder, * gets lists and opens UserDialog. */ void MainWindow::onUsers() { QString dir = ui->treeView->currentIndex().data(QFileSystemModel::Roles::FilePathRole).toString(); if (dir.isEmpty()) { dir = fallbackStore(); if (dir.isEmpty()) { return; } } else { QFileInfo fi(dir); if (!fi.isDir()) { dir = fi.absolutePath(); } dir = Util::normalizeFolderPath(dir); } userDialog(dir); } void MainWindow::slotSetupFinished(const QString &location, const QByteArray &keyId) { const QString gpgIdFile = location + QStringLiteral(".gpg-id"); QFile gpgId(gpgIdFile); if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::error(this, xi18nc("@info", "Unable to write user configuration at <filename>%1</filename>. Error: %2", gpgIdFile, gpgId.errorString())); return; } gpgId.write(keyId); + gpgId.write("\n"); gpgId.close(); m_rootFoldersManager->addRootFolder(i18nc("Default store name", "Local Store"), location); m_rootFoldersManager->save(); ui->lineEdit->clear(); m_passwordViewer->clear(); ui->treeView->selectionModel()->clear(); m_actionEdit->setEnabled(false); m_actionDelete->setEnabled(false); ui->stackedWidget->setCurrentIndex((int)StackLayer::WelcomePage); verifyInitialized(); } void MainWindow::verifyInitialized() { auto alreadyConfigured = !m_rootFoldersManager->rootFolders().isEmpty(); ui->sidebar->setVisible(alreadyConfigured); ui->stackedWidget->setCurrentIndex(alreadyConfigured ? (int)StackLayer::WelcomePage : (int)StackLayer::ConfigurationPage); m_actionAddFolder->setEnabled(alreadyConfigured); m_actionAddEntry->setEnabled(alreadyConfigured); m_actionDelete->setEnabled(m_actionDelete->isEnabled() && alreadyConfigured); m_actionEdit->setEnabled(m_actionEdit->isEnabled() && alreadyConfigured); } /** * @brief MainWindow::closeEvent hide or quit * @param event */ void MainWindow::closeEvent(QCloseEvent *event) { m_clipboardHelper->clearClipboard(); event->accept(); } /** * @brief MainWindow::eventFilter filter out some events and focus the * treeview * @param obj * @param event * @return */ bool MainWindow::eventFilter(QObject *obj, QEvent *event) { if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) { auto *key = dynamic_cast<QKeyEvent *>(event); if (key != nullptr && key->key() == Qt::Key_Down) { ui->treeView->setFocus(); } } return QObject::eventFilter(obj, event); } /** * @brief MainWindow::keyPressEvent did anyone press return, enter or escape? * @param event */ void MainWindow::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Delete: onDelete(); break; case Qt::Key_Return: case Qt::Key_Enter: if (m_storeModel.rowCount() > 0) selectTreeItem(ui->treeView->currentIndex()); break; case Qt::Key_Escape: ui->lineEdit->clear(); break; default: break; } } /** * @brief MainWindow::showContextMenu show us the (file or folder) context * menu * @param pos */ void MainWindow::showContextMenu(const QPoint &pos) { QModelIndex index = ui->treeView->indexAt(pos); bool selected = true; if (!index.isValid()) { ui->treeView->clearSelection(); m_actionDelete->setEnabled(false); m_actionEdit->setEnabled(false); selected = false; } ui->treeView->setCurrentIndex(index); QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos); QFileInfo fileOrFolder = ui->treeView->currentIndex().data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>(); QMenu contextMenu; if (!selected || fileOrFolder.isDir()) { QAction *addFolderAction = contextMenu.addAction(i18nc("@action:inmenu", "Add Folder")); addFolderAction->setIcon(QIcon::fromTheme(QStringLiteral("folder-add-symbolic"))); connect(addFolderAction, &QAction::triggered, this, &MainWindow::addFolder); QAction *addPasswordAction = contextMenu.addAction(i18nc("@action:inmenu", "Add Entry")); addPasswordAction->setIcon(QIcon::fromTheme(QStringLiteral("lock-symbolic"))); connect(addPasswordAction, &QAction::triggered, this, &MainWindow::addPassword); QAction *usersAction = contextMenu.addAction(i18nc("@action:inmenu", "Configure Users")); usersAction->setIcon(QIcon::fromTheme(QStringLiteral("system-users-symbolic"))); connect(usersAction, &QAction::triggered, this, &MainWindow::onUsers); } else if (fileOrFolder.isFile()) { QAction *edit = contextMenu.addAction(i18nc("@action:inmenu", "Edit Entry")); edit->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); connect(edit, &QAction::triggered, this, [this] { onEdit(true); }); } if (selected && (index.parent().isValid() || m_rootFoldersManager->rootFolders().count() == 1)) { contextMenu.addSeparator(); if (fileOrFolder.isDir()) { QAction *renameFolderAction = contextMenu.addAction(i18nc("@action:inmenu", "Rename Folder")); connect(renameFolderAction, &QAction::triggered, this, &MainWindow::renameFolder); renameFolderAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename-symbolic"))); } else if (fileOrFolder.isFile()) { QAction *renamePasswordAction = contextMenu.addAction(i18nc("@action:inmenu", "Rename Entry")); renamePasswordAction->setToolTip(i18nc("@info:tooltip", "Rename Password Entry")); renamePasswordAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename-symbolic"))); connect(renamePasswordAction, &QAction::triggered, this, &MainWindow::renamePassword); } QAction *deleteItem = contextMenu.addAction(fileOrFolder.isFile() ? i18nc("@action:inmenu", "Delete Entry") : i18nc("@action:inmenu", "Delete Folder")); deleteItem->setIcon(QIcon::fromTheme(QStringLiteral("delete-symbolic"))); connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete); } if (!index.parent().isValid() && m_rootFoldersManager->rootFolders().count() != 1) { contextMenu.addSeparator(); const auto configureAction = contextMenu.addAction(i18nc("@action:inmenu", "Configure Password Stores")); configureAction->setIcon(QIcon::fromTheme(QStringLiteral("configure-symbolic"))); connect(configureAction, &QAction::triggered, this, [this] { openConfig(ConfigureDialog::Page::PasswordsStore); }); } contextMenu.exec(globalPos); } /** * @brief MainWindow::addFolder add a new folder to store passwords in */ void MainWindow::addFolder() { bool ok; QString dir = directoryName(ui->treeView->currentIndex().data(QFileSystemModel::FilePathRole).toString()); if (dir.isEmpty()) { dir = fallbackStore(); if (dir.isEmpty()) { return; } } QString newdir = QInputDialog::getText(this, i18nc("@title:dialog", "New File"), xi18nc("@info", "<para>New Folder:</para><para>(Will be placed in <filename>%1</filename>)</para>", dir), QLineEdit::Normal, QString{}, &ok); if (!ok || newdir.isEmpty()) return; QDir(dir).mkdir(newdir); } /** * @brief MainWindow::renameFolder rename an existing folder */ void MainWindow::renameFolder() { bool ok; QString srcDir = QDir::cleanPath(directoryName(ui->treeView->currentIndex().data(QFileSystemModel::FilePathRole).toString())); if (srcDir.isEmpty()) { return; } QString srcDirName = QDir(srcDir).dirName(); QString newName = QInputDialog::getText(this, i18nc("@title:dialog", "Rename Folder"), i18nc("@label:textbox", "Rename folder to:"), QLineEdit::Normal, srcDirName, &ok); if (!ok || newName.isEmpty()) return; QString destDir = srcDir; destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName); m_storeModel.move(srcDir, destDir); } /** * @brief MainWindow::renamePassword rename an existing password */ void MainWindow::renamePassword() { bool ok; QString file = ui->treeView->currentIndex().data(QFileSystemModel::FilePathRole).toString(); QString filePath = QFileInfo(file).path(); QString fileName = QFileInfo(file).fileName(); if (fileName.endsWith(QStringLiteral(".gpg"), Qt::CaseInsensitive)) fileName.chop(4); QString newName = QInputDialog::getText(this, i18nc("@title:dialog", "Rename File"), i18nc("@label:textbox", "Rename file to: "), QLineEdit::Normal, fileName, &ok); if (!ok || newName.isEmpty()) return; QString newFile = QDir(filePath).filePath(newName); m_storeModel.move(file, newFile); }