diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fed5b79..9ac3b3a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,47 +1,50 @@ # let's put most of the "meat" in a static library this way, we can also unit # test parts of it add_library(gpgpass_internal STATIC) target_sources( gpgpass_internal PRIVATE clipboardhelper.h configdialog.h deselectabletreeview.h filecontent.h firsttimedialog.h gpgmehelpers.h mainwindow.h pass.h passwordconfiguration.h passworddialog.h qpushbuttonfactory.h settings.h storemodel.h userinfo.h usersdialog.h util.h + welcomewidget.h clipboardhelper.cpp configdialog.cpp filecontent.cpp firsttimedialog.cpp mainwindow.cpp pass.cpp passworddialog.cpp settings.cpp storemodel.cpp usersdialog.cpp util.cpp + welcomewidget.cpp ../resources.qrc ) ki18n_wrap_ui(gpgpass_internal mainwindow.ui configdialog.ui usersdialog.ui passworddialog.ui userswidget.ui + welcomewidget.ui ) target_link_libraries(gpgpass_internal Qt6::Widgets KF6::Prison KF6::IconThemes KF6::I18n KF6::WidgetsAddons QGpgmeQt6) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 4bfee0b..89429c0 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,1004 +1,1009 @@ /* 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: 2019Maciej 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 "configdialog.h" #include "filecontent.h" #include "pass.h" #include "passworddialog.h" #include "qpushbuttonfactory.h" #include "settings.h" #include "ui_mainwindow.h" #include "usersdialog.h" #include "util.h" #include <Prison/Barcode> #include <KMessageWidget> #include <KLocalizedString> #include <KPasswordLineEdit> #include <QCloseEvent> #include <QFileInfo> #include <QInputDialog> #include <QLabel> #include <QMenu> #include <QMessageBox> #include <QShortcut> #include <QTimer> #include <QComboBox> #include <QDebug> 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) : QMainWindow(parent) , m_pass{std::make_unique<Pass>()} , ui(new Ui::MainWindow) , proxyModel{*m_pass} { #ifdef __APPLE__ // extra treatment for mac os // see http://doc.qt.io/qt-5/qkeysequence.html#qt_set_sequence_auto_mnemonic qt_set_sequence_auto_mnemonic(true); #endif ui->setupUi(this); m_clipboardHelper = new ClipboardHelper(this); ui->mainLayout->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); m_errorMessage->setPosition(KMessageWidget::Position::Header); m_errorMessage->hide(); ui->messagesArea->addWidget(m_errorMessage); connect(m_pass.get(), &Pass::errorString, this, [this](auto str) { m_errorMessage->setText(str); m_errorMessage->animatedShow(); setUiElementsEnabled(true); }); connect(m_pass.get(), &Pass::critical, this, &MainWindow::critical); connect(m_pass.get(), &Pass::finishedShow, this, &MainWindow::passShowHandler); connect(m_pass.get(), &Pass::finishedInsert, this, [this]() { this->selectTreeItem(this->getCurrentTreeViewIndex()); }); connect(m_pass.get(), &Pass::startReencryptPath, this, &MainWindow::startReencryptPath); connect(m_pass.get(), &Pass::endReencryptPath, this, &MainWindow::endReencryptPath); { m_notInitialized = new KMessageWidget(i18n("Password store not initialized")); m_notInitialized->setPosition(KMessageWidget::Position::Header); m_notInitialized->setCloseButtonVisible(false); QAction* action = new QAction(i18n("Initialize with users")); connect(action, &QAction::triggered, this, [this](){this->userDialog();}); m_notInitialized->addAction(action); m_notInitialized->setMessageType(KMessageWidget::Error); ui->messagesArea->addWidget(m_notInitialized); m_notInitialized->hide(); } { auto w = new QWidget(); w->setLayout(new QHBoxLayout); auto l = new QLabel(i18n("Select profile")); auto sep = ui->toolBar->addSeparator(); w->layout()->addWidget(l); m_profileBox = new QComboBox(); w->layout()->addWidget(m_profileBox); m_profiles = ui->toolBar->addWidget(w); connect(m_profiles, &QAction::changed, sep, [this, sep]() { sep->setVisible(this->m_profiles->isVisible()); }); } ui->copyPasswordName->hide(); connect(ui->copyPasswordName, &QPushButton::clicked, this, [this](){ m_clipboardHelper->copyTextToClipboard(ui->passwordName->text().trimmed()); }); } 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())); model.setNameFilters(QStringList() << QStringLiteral("*.gpg")); model.setNameFilterDisables(false); /* * I added this to solve Windows bug but now on GNU/Linux the main folder, * if hidden, disappear * * model.setFilter(QDir::NoDot); */ auto defaultPassStore = Util::findPasswordStore(); QString passStore = Settings::getPassStore(defaultPassStore); if (passStore == defaultPassStore) { // let's write it back Settings::setPassStore(passStore); } QModelIndex rootDir = model.setRootPath(passStore); model.fetchMore(rootDir); proxyModel.setSourceModel(&model); selectionModel.reset(new QItemSelectionModel(&proxyModel)); ui->treeView->setModel(&proxyModel); ui->treeView->setRootIndex(proxyModel.mapFromSource(rootDir)); ui->treeView->setColumnHidden(1, true); ui->treeView->setColumnHidden(2, true); ui->treeView->setColumnHidden(3, true); 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); if (Settings::isNoLineWrapping()) { ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap); } ui->textBrowser->setOpenExternalLinks(true); ui->textBrowser->setContextMenuPolicy(Qt::DefaultContextMenu); ui->textBrowser->document()->setDocumentMargin(0); updateProfileBox(); clearPanelTimer.setInterval(1000 * Settings::getAutoclearPanelSeconds()); clearPanelTimer.setSingleShot(true); connect(&clearPanelTimer, SIGNAL(timeout()), this, SLOT(clearPanel())); searchTimer.setInterval(350); searchTimer.setSingleShot(true); connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch); initToolBarButtons(); initStatusBar(); ui->lineEdit->setClearButtonEnabled(true); setUiElementsEnabled(true); QTimer::singleShot(10, this, SLOT(focusInput())); verifyInitialized(); } QMainWindow::setVisible(visible); } MainWindow::~MainWindow() = default; /** * @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() { connect(ui->actionAddPassword, &QAction::triggered, this, &MainWindow::addPassword); connect(ui->actionAddFolder, &QAction::triggered, this, &MainWindow::addFolder); connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit); connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete); connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers); connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig); connect(ui->treeView, &QTreeView::clicked, this, &MainWindow::selectTreeItem); connect(ui->treeView, &QTreeView::doubleClicked, this, &MainWindow::editTreeItem); connect(m_profileBox, &QComboBox::currentTextChanged, this, &MainWindow::selectProfile); connect(ui->lineEdit, &QLineEdit::textChanged, this, &MainWindow::filterList); connect(ui->lineEdit, &QLineEdit::returnPressed, this, &MainWindow::selectFromSearch); ui->actionAddPassword->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); ui->actionAddFolder->setIcon(QIcon::fromTheme(QStringLiteral("folder-new"))); ui->actionEdit->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); ui->actionDelete->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); ui->actionUsers->setIcon(QIcon::fromTheme(QStringLiteral("x-office-address-book"))); ui->actionConfig->setIcon(QIcon::fromTheme(QStringLiteral("configure-symbolic"))); } /** * @brief MainWindow::initStatusBar init statusBar with default message and logo */ void MainWindow::initStatusBar() { ui->statusBar->showMessage(i18nc("placeholder is version number","Welcome to GnuPG Password Manager %1", QString::fromLocal8Bit(GPGPASS_VERSION_STRING))); QPixmap logo = QPixmap(QStringLiteral(":/artwork/32-gpgpass.png")); QLabel *logoApp = new QLabel(statusBar()); logoApp->setPixmap(logo); statusBar()->addPermanentWidget(logoApp); } const QModelIndex MainWindow::getCurrentTreeViewIndex() { return ui->treeView->currentIndex(); } void MainWindow::showRemainingHtml(const QString &text) { QString _text = text; if (!ui->textBrowser->toPlainText().isEmpty()) _text = ui->textBrowser->toHtml() + _text; ui->textBrowser->setHtml(_text); } /** * @brief MainWindow::config pops up the configuration screen and handles all * inter-window communication */ void MainWindow::config() { QScopedPointer<ConfigDialog> d(new ConfigDialog(this)); d->setModal(true); if (d->exec()) { if (d->result() == QDialog::Accepted) { // Update the textBrowser line wrap mode if (Settings::isNoLineWrapping()) { ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap); } else { ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth); } this->show(); updateProfileBox(); ui->treeView->setRootIndex(proxyModel.mapFromSource(model.setRootPath(Settings::getPassStore()))); clearPanelTimer.setInterval(1000 * Settings::getAutoclearPanelSeconds()); 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; // TODO(bezet): "Could not decrypt"; m_clipboardHelper->clearClippedText(); QString file = index.data(QFileSystemModel::FilePathRole).toString(); ui->passwordName->setText(index.data().toString()); ui->copyPasswordName->show(); if (!file.isEmpty() && QFileInfo(file).isFile() && !cleared) { m_pass->Show(file); + ui->stackedWidget->setCurrentIndex(1); } else { clearPanel(); ui->actionEdit->setEnabled(false); ui->actionDelete->setEnabled(true); + ui->stackedWidget->setCurrentIndex(0); } } /** * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on * TreeViewItem, open the edit Window * @param index */ void MainWindow::editTreeItem(const QModelIndex &index) { QFileInfo fileOrFolder{index.data(QFileSystemModel::Roles::FilePathRole).toString()}; if (fileOrFolder.isFile()) { editPassword(fileOrFolder.absoluteFilePath()); } } /** * @brief MainWindow::deselect clear the selection, password and copy buffer */ void MainWindow::deselect() { m_clipboardHelper->clearClipboard(); ui->treeView->clearSelection(); ui->actionEdit->setEnabled(false); ui->actionDelete->setEnabled(false); ui->passwordName->setText(QString{}); ui->copyPasswordName->hide(); clearPanel(); + ui->stackedWidget->setCurrentIndex(0); } void MainWindow::passShowHandler(const QString &p_output) { QStringList templ = Settings::isUseTemplate() ? Settings::getPassTemplate().split(QStringLiteral("\n")) : QStringList(); bool allFields = Settings::isUseTemplate() && Settings::isTemplateAllFields(); FileContent fileContent = FileContent::parse(p_output, templ, allFields); QString output = p_output; QString password = fileContent.getPassword(); // set clipped text m_clipboardHelper->setClippedText(password); // first clear the current view: clearPanel(); // show what is needed: if (Settings::isHideContent()) { output = i18n("*** Content hidden ***"); } else if (!Settings::isDisplayAsIs()) { if (!password.isEmpty()) { // set the password, it is hidden if needed in addToGridLayout addToGridLayout(i18n("Password:"), password); } const NamedValues namedValues = fileContent.getNamedValues(); for (const auto& nv : namedValues) { addToGridLayout(i18nc("Field label", "%1:", nv.name), nv.value); } output = fileContent.getRemainingDataForDisplay(); } if (Settings::isUseAutoclearPanel()) { clearPanelTimer.start(); } output = output.toHtmlEscaped(); output.replace(Util::protocolRegex(), QStringLiteral(R"(<a href="\1">\1</a>)")); output.replace(QStringLiteral("\n"), QStringLiteral("<br />")); showRemainingHtml(i18nc("@label", "Additional Notes:") + QLatin1Char(' ') + output); setUiElementsEnabled(true); m_errorMessage->animatedHide(); } /** * @brief MainWindow::clearPanel hide the information from shoulder surfers */ void MainWindow::clearPanel() { clearTemplateWidgets(); - ui->textBrowser->setHtml(QString{}); + ui->passwordName->setText(QString{}); + ui->copyPasswordName->hide(); + ui->stackedWidget->setCurrentIndex(0); } /** * @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); ui->actionAddPassword->setEnabled(state); ui->actionAddFolder->setEnabled(state); ui->actionUsers->setEnabled(state); ui->actionConfig->setEnabled(state); // is a file selected? state &= ui->treeView->currentIndex().isValid(); ui->actionDelete->setEnabled(state); ui->actionEdit->setEnabled(state); } /** * @brief MainWindow::on_configButton_clicked run Mainwindow::config */ void MainWindow::onConfig() { config(); } /** * @brief Executes when the string in the search box changes, collapses the * TreeView * @param arg1 */ void MainWindow::filterList(const QString &arg1) { ui->statusBar->showMessage(i18n("Looking for: %1", arg1), 1000); ui->treeView->expandAll(); clearPanel(); ui->passwordName->setText(QString{}); ui->copyPasswordName->hide(); ui->actionEdit->setEnabled(false); ui->actionDelete->setEnabled(false); 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); proxyModel.setFilterRegularExpression(regExp); ui->treeView->setRootIndex(proxyModel.mapFromSource(model.setRootPath(Settings::getPassStore()))); if (proxyModel.rowCount() > 0 && !query.isEmpty()) { selectFirstFile(); } else { ui->actionEdit->setEnabled(false); ui->actionDelete->setEnabled(false); } } /** * @brief MainWindow::on_lineEdit_returnPressed get searching * * Select the first possible file in the tree */ void MainWindow::selectFromSearch() { if (proxyModel.rowCount() > 0) { selectFirstFile(); selectTreeItem(ui->treeView->currentIndex()); } } /** * @brief MainWindow::selectFirstFile select the first possible file in the * tree */ void MainWindow::selectFirstFile() { QModelIndex index = proxyModel.mapFromSource(model.setRootPath(Settings::getPassStore())); index = firstFile(index); ui->treeView->setCurrentIndex(index); } /** * @brief MainWindow::firstFile return location of first possible file * @param parentIndex * @return QModelIndex */ QModelIndex MainWindow::firstFile(QModelIndex parentIndex) { QModelIndex index = parentIndex; int numRows = proxyModel.rowCount(parentIndex); for (int row = 0; row < numRows; ++row) { index = proxyModel.index(row, 0, parentIndex); if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) return index; if (proxyModel.hasChildren(index)) return firstFile(index); } return index; } /** * @brief MainWindow::setPassword open passworddialog * @param file which pgp file * @param isNew insert (not update) */ void MainWindow::setPassword(QString file, bool isNew) { PasswordDialog d(*m_pass, file, isNew, this); if (!d.exec()) { ui->treeView->setFocus(); } } /** * @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 = Settings::getPassStore(); } QString file = QInputDialog::getText(this, i18n("New file"), i18n("New password file: \n(Will be placed in %1 )", dir), QLineEdit::Normal, QString{}, &ok); if (!ok || file.isEmpty()) return; file = QDir(dir).absoluteFilePath(file + QStringLiteral(".gpg")); setPassword(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 = i18nc("deleting a folder; placeholder is folder name","Are you sure you want to delete %1 and the whole content?", file); QDirIterator it(model.rootPath() + QLatin1Char('/') + 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 = i18nc("deleting a file; placeholder is file name","Are you sure you want to delete %1?", file); } if (QMessageBox::question(this, isDir ? i18n("Delete folder?") : i18n("Delete password?"), message, QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) return; m_pass->Remove(file, isDir); } /** * @brief MainWindow::onEdit try and edit (selected) password. */ void MainWindow::onEdit() { QString file = ui->treeView->currentIndex().data(QFileSystemModel::FilePathRole).toString(); if (!file.isEmpty()) { editPassword(file); } } /** * @brief MainWindow::userDialog see MainWindow::onUsers() * @param dir folder to edit users for. */ void MainWindow::userDialog(QString dir) { if (dir.isEmpty()) { dir = Settings::getPassStore(); } QFileInfo fi(dir); if (!fi.isDir()) { dir = fi.absolutePath(); } UsersDialog d(dir, *m_pass, this); if (!d.exec()) { ui->treeView->setFocus(); } verifyInitialized(); } /** * @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 = Settings::getPassStore(); } else { QFileInfo fi(dir); if (!fi.isDir()) { dir = fi.absolutePath(); } dir = Util::normalizeFolderPath(dir); } userDialog(dir); } /** * @brief MainWindow::updateProfileBox update the list of profiles, optionally * select a more appropriate one to view too */ void MainWindow::updateProfileBox() { QHash<QString, QString> profiles = Settings::getProfiles(); if (profiles.isEmpty()) { m_profiles->setVisible(false); } else { m_profiles->setVisible(true); m_profileBox->setEnabled(profiles.size() > 1); m_profileBox->clear(); QHashIterator<QString, QString> i(profiles); while (i.hasNext()) { i.next(); if (!i.key().isEmpty()) m_profileBox->addItem(i.key()); } m_profileBox->model()->sort(0); } int index = m_profileBox->findText(Settings::getProfile()); if (index != -1) // -1 for not found m_profileBox->setCurrentIndex(index); } /** * @brief MainWindow::on_profileBox_currentIndexChanged make sure we show the * correct "profile" * @param name */ void MainWindow::selectProfile(QString name) { if (name == Settings::getProfile()) return; ui->lineEdit->clear(); clearPanel(); Settings::setProfile(name); Settings::setPassStore(Settings::getProfiles().value(name)); ui->statusBar->showMessage(i18n("Profile changed to %1", name), 2000); ui->treeView->selectionModel()->clear(); ui->treeView->setRootIndex(proxyModel.mapFromSource(model.setRootPath(Settings::getPassStore()))); verifyInitialized(); ui->actionEdit->setEnabled(false); ui->actionDelete->setEnabled(false); } void MainWindow::verifyInitialized() { bool actionsEnabled; if (!QFile::exists(Settings::getPassStore() + QStringLiteral("/.gpg-id"))) { m_notInitialized->animatedShow(); actionsEnabled = false; } else { m_notInitialized->animatedHide(); actionsEnabled = true; } ui->actionAddFolder->setEnabled(actionsEnabled); ui->actionAddPassword->setEnabled(actionsEnabled); ui->actionDelete->setEnabled(ui->actionDelete->isEnabled() && actionsEnabled); ui->actionEdit->setEnabled(ui->actionEdit->isEnabled() && actionsEnabled); } /** * @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 (proxyModel.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(); ui->actionDelete->setEnabled(false); ui->actionEdit->setEnabled(false); selected = false; } ui->treeView->setCurrentIndex(index); QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos); QFileInfo fileOrFolder = model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex())); QMenu contextMenu; if (!selected || fileOrFolder.isDir()) { const QAction *addFolderAction = contextMenu.addAction(i18n("Add folder")); const QAction *addPasswordAction = contextMenu.addAction(i18n("Add password")); const QAction *usersAction = contextMenu.addAction(i18n("Users")); connect(addFolderAction, &QAction::triggered, this, &MainWindow::addFolder); connect(addPasswordAction, &QAction::triggered, this, &MainWindow::addPassword); connect(usersAction, &QAction::triggered, this, &MainWindow::onUsers); } else if (fileOrFolder.isFile()) { const QAction *edit = contextMenu.addAction(i18n("Edit")); connect(edit, &QAction::triggered, this, &MainWindow::onEdit); } if (selected) { contextMenu.addSeparator(); if (fileOrFolder.isDir()) { const QAction *renameFolderAction = contextMenu.addAction(i18n("Rename folder")); connect(renameFolderAction, &QAction::triggered, this, &MainWindow::renameFolder); } else if (fileOrFolder.isFile()) { const QAction *renamePasswordAction = contextMenu.addAction(i18n("Rename password")); connect(renamePasswordAction, &QAction::triggered, this, &MainWindow::renamePassword); } const QAction *deleteItem = contextMenu.addAction(i18n("Delete")); connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete); } 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 = Settings::getPassStore(); } QString newdir = QInputDialog::getText(this, i18n("New file"), i18n("New Folder: \n(Will be placed in %1 )", 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, i18n("Rename file"), i18n("Rename Folder To: "), QLineEdit::Normal, srcDirName, &ok); if (!ok || newName.isEmpty()) return; QString destDir = srcDir; destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName); m_pass->Move(srcDir, destDir); } /** * @brief MainWindow::editPassword read password and open edit window via * MainWindow::onEdit() */ void MainWindow::editPassword(const QString &file) { if (!file.isEmpty()) { setPassword(file, false); } } /** * @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, i18n("Rename file"), i18n("Rename File To: "), QLineEdit::Normal, fileName, &ok); if (!ok || newName.isEmpty()) return; QString newFile = QDir(filePath).filePath(newName); m_pass->Move(file, newFile); } /** * @brief MainWindow::clearTemplateWidgets empty the template widget fields in * the UI */ void MainWindow::clearTemplateWidgets() { while (ui->contentLayout->count() > 0) { ui->contentLayout->removeRow(ui->contentLayout->rowCount() -1); } } /** * @brief MainWindow::addToGridLayout add a field to the template grid * @param position * @param field * @param value */ void MainWindow::addToGridLayout(const QString &field, const QString &value) { QString trimmedField = field.trimmed(); QString trimmedValue = value.trimmed(); // Combine the Copy button and the line edit in one widget auto rowLayout = new QHBoxLayout(); rowLayout->setContentsMargins(0, 2, 0, 2); if (trimmedField == i18n("Password:")) { auto *line = new KPasswordLineEdit(); line->setRevealPasswordMode(KPassword::RevealMode::Always); line->setObjectName(trimmedField); line->setPassword(trimmedValue); line->setReadOnly(true); line->setContentsMargins(0, 0, 0, 0); line->setEchoMode(QLineEdit::Password); auto icon = QIcon::fromTheme(QStringLiteral("password-show-on")); icon.addFile(QStringLiteral("password-show-off"), QSize(), QIcon::Normal, QIcon::Off); rowLayout->addWidget(line); } else { auto *line = new QLabel(); line->setOpenExternalLinks(true); line->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard); line->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum)); line->setObjectName(trimmedField); trimmedValue.replace(Util::protocolRegex(), QStringLiteral(R"(<a href="\1">\1</a>)")); line->setText(trimmedValue); line->setContentsMargins(5, 0, 0, 0); rowLayout->addWidget(line); } auto fieldName = trimmedField; fieldName.removeLast(); // remove ':' from the end of the label auto fieldLabel = createPushButton(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy '%1' to clipboard", fieldName), m_clipboardHelper, [this, trimmedValue] { m_clipboardHelper->copyTextToClipboard(trimmedValue); }); rowLayout->addWidget(fieldLabel.release()); auto qrButton = createPushButton(QIcon::fromTheme(QStringLiteral("view-barcode-qr")), i18n("View '%1' QR Code", fieldName), m_clipboardHelper, [this, trimmedValue]() { auto barcode = Prison::Barcode::create(Prison::QRCode); if (!barcode) { return; } barcode->setData(trimmedValue); auto image = barcode->toImage(barcode->preferredSize(window()->devicePixelRatioF())); QDialog popup(nullptr, Qt::Popup | Qt::FramelessWindowHint); QVBoxLayout *layout = new QVBoxLayout; QLabel *popupLabel = new QLabel(); layout->addWidget(popupLabel); popupLabel->setPixmap(QPixmap::fromImage(image)); popupLabel->setScaledContents(true); popupLabel->show(); popup.setLayout(layout); popup.move(QCursor::pos()); popup.exec(); }); rowLayout->addWidget(qrButton.release()); // set into the layout ui->contentLayout->addRow(trimmedField, rowLayout); } /** * @brief MainWindow::startReencryptPath disable ui elements and treeview */ void MainWindow::startReencryptPath() { statusBar()->showMessage(i18n("Re-encrypting folders"), 3000); setUiElementsEnabled(false); ui->treeView->setDisabled(true); } /** * @brief MainWindow::endReencryptPath re-enable ui elements */ void MainWindow::endReencryptPath() { setUiElementsEnabled(true); } /** * @brief MainWindow::critical critical message popup wrapper. * @param title * @param msg */ void MainWindow::critical(const QString& title, const QString& msg) { QMessageBox::critical(this, title, msg); } diff --git a/src/mainwindow.ui b/src/mainwindow.ui index d4d37ff..82b31b9 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -1,302 +1,320 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>MainWindow</class> <widget class="QMainWindow" name="MainWindow"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>801</width> <height>484</height> </rect> </property> <property name="windowTitle"> <string>GnuPG Password Manager</string> </property> <widget class="QWidget" name="centralWidget"> <layout class="QVBoxLayout" name="gridLayout_2"> - <property name="leftMargin"> + <property name="spacing"> <number>0</number> </property> - <property name="rightMargin"> + <property name="leftMargin"> <number>0</number> </property> <property name="topMargin"> <number>0</number> </property> - <property name="bottomMargin"> + <property name="rightMargin"> <number>0</number> </property> - <property name="spacing"> + <property name="bottomMargin"> <number>0</number> </property> <item> <layout class="QVBoxLayout" name="messagesArea"/> </item> <item> <layout class="QHBoxLayout" name="mainLayout"> <item> <layout class="QVBoxLayout" name="verticalLayout"> <item> <widget class="QLineEdit" name="lineEdit"> <property name="minimumSize"> <size> <width>0</width> <height>26</height> </size> </property> <property name="focusPolicy"> <enum>Qt::StrongFocus</enum> </property> <property name="text"> <string/> </property> <property name="placeholderText"> <string>Search Password</string> </property> </widget> </item> <item> <widget class="DeselectableTreeView" name="treeView"> <property name="sizePolicy"> <sizepolicy hsizetype="Minimum" vsizetype="MinimumExpanding"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="acceptDrops"> <bool>true</bool> </property> <property name="sizeAdjustPolicy"> <enum>QAbstractScrollArea::AdjustIgnored</enum> </property> <property name="dragEnabled"> <bool>true</bool> </property> <property name="dragDropMode"> <enum>QAbstractItemView::InternalMove</enum> </property> <property name="sortingEnabled"> <bool>true</bool> </property> <property name="animated"> <bool>true</bool> </property> <attribute name="headerStretchLastSection"> <bool>false</bool> </attribute> </widget> </item> </layout> </item> <item> - <layout class="QVBoxLayout" name="verticalLayoutPassword"> - <property name="spacing"> - <number>0</number> - </property> - <property name="topMargin"> - <number>7</number> - </property> - <item> - <layout class="QHBoxLayout" name="titleContainer"> + <widget class="QStackedWidget" name="stackedWidget"> + <widget class="WelcomeWidget" name="welcomeWidget"/> + <widget class="QWidget" name="widget"> + <layout class="QVBoxLayout" name="verticalLayoutPassword"> + <property name="spacing"> + <number>0</number> + </property> + <property name="topMargin"> + <number>7</number> + </property> + <item> + <layout class="QHBoxLayout" name="titleContainer"> + <item> + <widget class="KTitleWidget" name="passwordName" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>20</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>10000000</width> + <height>30</height> + </size> + </property> + <property name="text" stdset="0"> + <string>Welcome to GnuPG Password Manager</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="copyPasswordName"> + <property name="toolTip"> + <string>Copy title to clipboard</string> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset theme="edit-copy"> + <normaloff>.</normaloff>.</iconset> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QFormLayout" name="contentLayout"/> + </item> <item> - <widget class="KTitleWidget" name="passwordName" native="true"> + <widget class="QTextBrowser" name="textBrowser"> <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="minimumSize"> <size> <width>0</width> <height>20</height> </size> </property> - <property name="maximumSize"> - <size> - <width>10000000</width> - <height>30</height> - </size> + <property name="autoFillBackground"> + <bool>false</bool> </property> - <property name="text" stdset="0"> - <string>Welcome to GnuPG Password Manager</string> + <property name="styleSheet"> + <string notr="true">background: transparent</string> </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="copyPasswordName"> - <property name="toolTip"> - <string>Copy title to clipboard</string> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> </property> - <property name="text"> - <string/> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> </property> - <property name="icon"> - <iconset theme="edit-copy"> - <normaloff>.</normaloff>.</iconset> + <property name="lineWidth"> + <number>0</number> + </property> + <property name="html"> + <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> +p, li { white-space: pre-wrap; } +hr { height: 1px; border-width: 0; } +li.unchecked::marker { content: "\2610"; } +li.checked::marker { content: "\2612"; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Noto Sans'; font-size:10pt;"><br /></p></body></html></string> + </property> + <property name="acceptRichText"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> </property> </widget> </item> </layout> - </item> - <item> - <layout class="QFormLayout" name="contentLayout"/> - </item> - <item> - <widget class="QTextBrowser" name="textBrowser"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>0</width> - <height>20</height> - </size> - </property> - <property name="autoFillBackground"> - <bool>false</bool> - </property> - <property name="styleSheet"> - <string notr="true">background: transparent</string> - </property> - <property name="frameShape"> - <enum>QFrame::NoFrame</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Plain</enum> - </property> - <property name="lineWidth"> - <number>0</number> - </property> - <property name="html"> - <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html></string> - </property> - <property name="acceptRichText"> - <bool>true</bool> - </property> - <property name="openExternalLinks"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> + </widget> + </widget> </item> </layout> </item> </layout> </widget> <widget class="QStatusBar" name="statusBar"/> <widget class="QToolBar" name="toolBar"> <property name="contextMenuPolicy"> <enum>Qt::PreventContextMenu</enum> </property> <property name="acceptDrops"> <bool>false</bool> </property> <property name="windowTitle"> <string/> </property> <property name="movable"> <bool>false</bool> </property> <property name="toolButtonStyle"> <enum>Qt::ToolButtonFollowStyle</enum> </property> <attribute name="toolBarArea"> <enum>TopToolBarArea</enum> </attribute> <attribute name="toolBarBreak"> <bool>false</bool> </attribute> <addaction name="actionAddPassword"/> <addaction name="actionAddFolder"/> <addaction name="separator"/> <addaction name="actionEdit"/> <addaction name="actionDelete"/> <addaction name="separator"/> <addaction name="actionUsers"/> <addaction name="actionConfig"/> </widget> <action name="actionAddPassword"> <property name="text"> <string>Add password</string> </property> <property name="toolTip"> <string>Add password</string> </property> <property name="shortcut"> <string>Ctrl+N</string> </property> </action> <action name="actionAddFolder"> <property name="text"> <string>Add folder</string> </property> <property name="toolTip"> <string>Add folder</string> </property> </action> <action name="actionEdit"> <property name="text"> <string>Edit</string> </property> <property name="toolTip"> <string>Edit</string> </property> </action> <action name="actionDelete"> <property name="text"> <string>Delete</string> </property> <property name="toolTip"> <string>Delete</string> </property> </action> <action name="actionUsers"> <property name="text"> <string>Users</string> </property> <property name="toolTip"> <string>Manage who can read password in folder</string> </property> </action> <action name="actionConfig"> <property name="text"> <string>Config</string> </property> <property name="toolTip"> <string>Configuration</string> </property> </action> </widget> <layoutdefault spacing="6" margin="11"/> <customwidgets> <customwidget> <class>DeselectableTreeView</class> <extends>QTreeView</extends> <header location="global">deselectabletreeview.h</header> <slots> <signal>signal1()</signal> </slots> </customwidget> + <customwidget> + <class>WelcomeWidget</class> + <extends>QWidget</extends> + <header location="global">welcomewidget.h</header> + </customwidget> + <customwidget> + <class>KTitleWidget</class> + <extends>QWidget</extends> + <header>ktitlewidget.h</header> + </customwidget> </customwidgets> <tabstops> <tabstop>lineEdit</tabstop> <tabstop>treeView</tabstop> <tabstop>copyPasswordName</tabstop> <tabstop>textBrowser</tabstop> </tabstops> <resources/> <connections/> <slots> <slot>deselect()</slot> </slots> </ui> diff --git a/src/welcomewidget.cpp b/src/welcomewidget.cpp new file mode 100644 index 0000000..ee4bd2d --- /dev/null +++ b/src/welcomewidget.cpp @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2024 g10 Code GmbH +// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com> +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "welcomewidget.h" +#include "ui_welcomewidget.h" +#include <gpgpass_version.h> + +using namespace Qt::Literals::StringLiterals; + +WelcomeWidget::WelcomeWidget(QWidget *parent) + : QWidget(parent) + , ui(new Ui::WelcomeWidget) +{ + ui->setupUi(this); + + // center vertically + ui->mainLayout->insertStretch(0); + ui->mainLayout->addStretch(); + + const double titleFontSize = QApplication::font().pointSize() * 2; + auto font = ui->title->font(); + font.setPointSize(titleFontSize); + ui->title->setFont(font); + + QPixmap logo = QPixmap(QStringLiteral(":/artwork/64-gpgpass.png")); + ui->logo->setPixmap(logo); + + ui->version->setText(i18nc("@info", "Version: %1", QString::fromLocal8Bit(GPGPASS_VERSION_STRING))); +} + +WelcomeWidget::~WelcomeWidget() = default; diff --git a/src/welcomewidget.h b/src/welcomewidget.h new file mode 100644 index 0000000..8af6b3d --- /dev/null +++ b/src/welcomewidget.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 g10 Code GmbH +// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com> +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <QLabel> + +namespace Ui +{ +class WelcomeWidget; +} + +class WelcomeWidget : public QWidget +{ + Q_OBJECT +public: + WelcomeWidget(QWidget *parent = nullptr); + ~WelcomeWidget(); + +private: + QScopedPointer<Ui::WelcomeWidget> ui; +}; diff --git a/src/welcomewidget.ui b/src/welcomewidget.ui new file mode 100644 index 0000000..dabdf21 --- /dev/null +++ b/src/welcomewidget.ui @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> +<!-- +SPDX-FileCopyrightText: 2024 g10 Code GmbH +SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com> +SPDX-License-Identifier: GPL-2.0-or-later +--> + <class>WelcomeWidget</class> + <widget class="QWidget" name="WelcomeWidget"> + <layout class="QVBoxLayout" name="mainLayout"> + <item> + <widget class="QLabel" name="logo" /> + </item> + <item> + <widget class="QLabel" name="title"> + <property name="text"> + <string>Welcome to GnuPG Password Manager</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="version" /> + </item> + </layout> + </widget> +</ui>