diff --git a/.kde-ci.yml b/.kde-ci.yml index 97fe7cb..6a02477 100644 --- a/.kde-ci.yml +++ b/.kde-ci.yml @@ -1,19 +1,20 @@ # SPDX-FileCopyrightText: None # SPDX-License-Identifier: CC0-1.0 Dependencies: - 'on': ['@all'] 'require': 'frameworks/extra-cmake-modules': '@latest-kf6' 'frameworks/ki18n' : '@latest-kf6' 'frameworks/kcolorschemes' : '@latest-kf6' + 'frameworks/kitemmodels' : '@latest-kf6' 'frameworks/kiconthemes' : '@latest-kf6' 'frameworks/prison' : '@latest-kf6' Options: test-before-installing: True require-passing-tests-on: [ 'Linux', 'Windows' ] cppcheck-ignore-files: - tests cppcheck-arguments: - --suppress=useStlAlgorithm diff --git a/CMakeLists.txt b/CMakeLists.txt index 7cbb178..e89f466 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,43 +1,43 @@ cmake_minimum_required(VERSION 3.16) set(VERSION "0.0.1") project(gnupgpass VERSION 0.0.1) set(QT_MIN_VERSION "6.6.0") set(KF_MIN_VERSION "5.240.0") find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDECompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) include(ECMMarkAsTest) include(ECMAddTests) include(FeatureSummary) include(ECMAddAppIcon) include(ECMSetupVersion) include(KDEGitCommitHooks) include(KDEClangFormat) kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) # Generate header with version number ecm_setup_version(${VERSION} VARIABLE_PREFIX GPGPASS VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/gpgpass_version.h") find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Widgets Test) include_directories(${CMAKE_BINARY_DIR}) -find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Prison IconThemes I18n ColorScheme WidgetsAddons) +find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Prison IconThemes I18n ColorScheme WidgetsAddons ItemModels) find_package(QGpgmeQt6 1.19 CONFIG REQUIRED) add_subdirectory(main) add_subdirectory(src) add_subdirectory(tests) ki18n_install(po) install(FILES org.gnupg.gpgpass.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES org.gnupg.gpgpass.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES artwork/sc-gpgpass.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps RENAME org.gnupg.gpgpass.svg) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9ac3b3a..b54f911 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,50 +1,52 @@ # 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 + addfileinfoproxy.h 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 + addfileinfoproxy.cpp 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 +target_link_libraries(gpgpass_internal Qt6::Widgets KF6::Prison KF6::IconThemes KF6::I18n KF6::WidgetsAddons KF6::ItemModels QGpgmeQt6) diff --git a/src/addfileinfoproxy.cpp b/src/addfileinfoproxy.cpp new file mode 100644 index 0000000..75b57ea --- /dev/null +++ b/src/addfileinfoproxy.cpp @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2024 g10 Code GmbH + SPDX-FileContributor: Sune Stolborg Vuorela + + SPDX-License-Identifier: GPL-3.0-or-later +*/ +#include "addfileinfoproxy.h" +#include + +AddFileInfoProxy::AddFileInfoProxy(QObject *parent) + : QIdentityProxyModel{parent} +{ +} + +QVariant AddFileInfoProxy::data(const QModelIndex &index, int role) const +{ + if (!m_model) { + return QVariant(); + } + if (role == FileInfoRole) { + auto source_index = mapToSource(index); + return QVariant::fromValue(m_model->fileInfo(source_index)); + } + return QIdentityProxyModel::data(index, role); +} + +void AddFileInfoProxy::setSourceModel(QAbstractItemModel *newModel) +{ + m_model = qobject_cast(newModel); + QIdentityProxyModel::setSourceModel(m_model); +} diff --git a/src/addfileinfoproxy.h b/src/addfileinfoproxy.h new file mode 100644 index 0000000..2fe4cd1 --- /dev/null +++ b/src/addfileinfoproxy.h @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2024 g10 Code GmbH + SPDX-FileContributor: Sune Stolborg Vuorela + + SPDX-License-Identifier: GPL-3.0-or-later +*/ +#ifndef ADDFILEINFOPROXY_H +#define ADDFILEINFOPROXY_H + +#include + +class QFileSystemModel; + +/// Once we have a Qt with https://codereview.qt-project.org/c/qt/qtbase/+/550138 +/// This entire class can be removed +class AddFileInfoProxy : public QIdentityProxyModel +{ + Q_OBJECT +public: + static const int FileInfoRole = Qt::UserRole + 10; + explicit AddFileInfoProxy(QObject *parent = nullptr); + QVariant data(const QModelIndex &index, int role) const override; + void setSourceModel(QAbstractItemModel *model) override; + +private: + QFileSystemModel *m_model = nullptr; +}; + +#endif // ADDFILEINFOPROXY_H diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 1d074af..7b7a841 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,1005 +1,1016 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2016-2017 tezeb SPDX-FileCopyrightText: 2018 Lukas Vogel SPDX-FileCopyrightText: 2018 Claudio Maradonna SPDX-FileCopyrightText: 2019 Maciej S. Szmigiero SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #include "mainwindow.h" #include +#include "addfileinfoproxy.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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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()} , 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::changeRootItem(const QString &path) +{ + QModelIndex rootDir = model.setRootPath(path); + selectionModel->select(m_addRoleModel->mapFromSource(rootDir), QItemSelectionModel::ClearAndSelect); +} + 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); + selectionModel.reset(new QItemSelectionModel(&model)); + m_addRoleModel = new AddFileInfoProxy(this); + m_addRoleModel->setSourceModel(&model); - proxyModel.setSourceModel(&model); - selectionModel.reset(new QItemSelectionModel(&proxyModel)); + m_selectProxy = new KSelectionProxyModel(selectionModel.get(), this); + m_selectProxy->setFilterBehavior(KSelectionProxyModel::SubTreesWithoutRoots); + selectionModel->select(m_addRoleModel->mapFromSource(rootDir), QItemSelectionModel::ClearAndSelect); + m_selectProxy->setSourceModel(m_addRoleModel); + proxyModel.setSourceModel(m_selectProxy); 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); 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); } std::unique_ptr createTextBrowserForNotes() { auto textBrowser = std::make_unique(); if (Settings::isNoLineWrapping()) { textBrowser->setLineWrapMode(QTextBrowser::NoWrap); } textBrowser->setOpenExternalLinks(true); textBrowser->setContextMenuPolicy(Qt::DefaultContextMenu); textBrowser->document()->setDocumentMargin(0); return textBrowser; } 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(); } /** * @brief MainWindow::config pops up the configuration screen and handles all * inter-window communication */ void MainWindow::config() { QScopedPointer d(new ConfigDialog(this)); d->setModal(true); if (d->exec()) { if (d->result() == QDialog::Accepted) { this->show(); updateProfileBox(); - ui->treeView->setRootIndex(proxyModel.mapFromSource(model.setRootPath(Settings::getPassStore()))); + changeRootItem(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::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(); } if (!output.isEmpty()) { output = output.toHtmlEscaped(); output.replace(Util::protocolRegex(), QStringLiteral(R"(\1)")); output.replace(QStringLiteral("\n"), QStringLiteral("
")); auto textBrowser = createTextBrowserForNotes(); textBrowser->setHtml(output); ui->contentLayout->addRow(new QLabel(i18nc("@label", "Notes:")), textBrowser.release()); } setUiElementsEnabled(true); m_errorMessage->animatedHide(); } /** * @brief MainWindow::clearPanel hide the information from shoulder surfers */ void MainWindow::clearPanel() { clearTemplateWidgets(); ui->passwordName->setText(QString{}); ui->copyPasswordName->hide(); } /** * @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); + 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) { - QModelIndex index = parentIndex; - int numRows = proxyModel.rowCount(parentIndex); + auto model = parentIndex.model(); + int numRows = model->rowCount(parentIndex); for (int row = 0; row < numRows; ++row) { - index = proxyModel.index(row, 0, parentIndex); - if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) + auto index = model->index(row, 0, parentIndex); + if (index.data(AddFileInfoProxy::FileInfoRole).value().isFile()) return index; - if (proxyModel.hasChildren(index)) + if (model->hasChildren(index)) return firstFile(index); } - return index; + return parentIndex; } /** * @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("
") + i18nc("extra warning during certain folder deletions", "Attention: " "there are unexpected files in the given folder, " "check them before continue") + QStringLiteral(""); 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 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 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()))); + changeRootItem(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(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())); + QFileInfo fileOrFolder = ui->treeView->currentIndex().data(AddFileInfoProxy::FileInfoRole).value(); 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"(\1)")); 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.h b/src/mainwindow.h index 987b3ab..eb3579b 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,130 +1,135 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2016-2017 tezeb SPDX-FileCopyrightText: 2018 Lukas Vogel SPDX-FileCopyrightText: 2018 Claudio Maradonna SPDX-FileCopyrightText: 2019 Maciej S. Szmigiero SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #ifndef MAINWINDOW_H_ #define MAINWINDOW_H_ #include "storemodel.h" +#include #include #include #include #include #include #ifdef __APPLE__ // http://doc.qt.io/qt-5/qkeysequence.html#qt_set_sequence_auto_mnemonic void qt_set_sequence_auto_mnemonic(bool b); #endif namespace Ui { class MainWindow; } /*! \class MainWindow \brief The MainWindow class does way too much, not only is it a switchboard, configuration handler and more, it's also the process-manager. This class could really do with an overhaul. */ class QComboBox; class ClipboardHelper; class Pass; class KMessageWidget; +class AddFileInfoProxy; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); void restoreWindow(); void userDialog(QString = {}); void config(); void setUiElementsEnabled(bool state); const QModelIndex getCurrentTreeViewIndex(); void setVisible(bool visible) override; protected: void closeEvent(QCloseEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void changeEvent(QEvent *event) override; bool eventFilter(QObject *obj, QEvent *event) override; public Q_SLOTS: void deselect(); void critical(const QString &, const QString &); void passShowHandler(const QString &); void selectTreeItem(const QModelIndex &index); void startReencryptPath(); void endReencryptPath(); private Q_SLOTS: void addPassword(); void addFolder(); void onEdit(); void onDelete(); void onUsers(); void onConfig(); void editTreeItem(const QModelIndex &index); void clearPanel(); void filterList(const QString &arg1); void selectFromSearch(); void selectProfile(QString); void showContextMenu(const QPoint &pos); void renameFolder(); void editPassword(const QString &); void renamePassword(); void focusInput(); void onTimeoutSearch(); void verifyInitialized(); + void changeRootItem(const QString &path); private: std::unique_ptr m_pass; ClipboardHelper *m_clipboardHelper; QScopedPointer ui; QFileSystemModel model; StoreModel proxyModel; QScopedPointer selectionModel; + KSelectionProxyModel *m_selectProxy = nullptr; + AddFileInfoProxy *m_addRoleModel = nullptr; QTimer clearPanelTimer, searchTimer; KMessageWidget *m_notInitialized; KMessageWidget *m_errorMessage; QAction *m_profiles; QComboBox *m_profileBox; bool firstShow = true; void initToolBarButtons(); void initStatusBar(); void updateText(); void selectFirstFile(); QModelIndex firstFile(QModelIndex parentIndex); void setPassword(QString, bool isNew = true); void updateProfileBox(); void initTrayIcon(); void destroyTrayIcon(); void clearTemplateWidgets(); void reencryptPath(QString dir); void addToGridLayout(const QString &field, const QString &value); }; #endif // MAINWINDOW_H_ diff --git a/src/storemodel.cpp b/src/storemodel.cpp index 2b7cad0..cce7613 100644 --- a/src/storemodel.cpp +++ b/src/storemodel.cpp @@ -1,278 +1,278 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2018 Claudio Maradonna SPDX-FileCopyrightText: 2019 Maciej S. Szmigiero SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #include "storemodel.h" +#include "addfileinfoproxy.h" #include "pass.h" #include "util.h" #include #include #include #include #include #include #include static const QString mimeType = QStringLiteral("application/vnd+gnupgpass.dragAndDropInfoPasswordStore"); QDataStream &operator<<(QDataStream &out, const dragAndDropInfoPasswordStore &dragAndDropInfoPasswordStore) { out << dragAndDropInfoPasswordStore.isDir << dragAndDropInfoPasswordStore.isFile << dragAndDropInfoPasswordStore.path; return out; } QDataStream &operator>>(QDataStream &in, dragAndDropInfoPasswordStore &dragAndDropInfoPasswordStore) { in >> dragAndDropInfoPasswordStore.isDir >> dragAndDropInfoPasswordStore.isFile >> dragAndDropInfoPasswordStore.path; return in; } /** * @brief StoreModel::StoreModel * SubClass of QSortFilterProxyModel via * http://www.qtcentre.org/threads/46471-QTreeView-Filter */ StoreModel::StoreModel(Pass &pass) : m_pass(pass) { setRecursiveFilteringEnabled(true); setAutoAcceptChildRows(true); } /** * @brief StoreModel::data don't show the .gpg at the end of a file. * @param index * @param role * @return */ QVariant StoreModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); auto initial_value = QSortFilterProxyModel::data(index, role); if (role == Qt::DisplayRole) { QString name = initial_value.toString(); name.replace(Util::endsWithGpg(), QString{}); return name; } return initial_value; } /** * @brief StoreModel::supportedDropActions enable drop. * @return */ Qt::DropActions StoreModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } /** * @brief StoreModel::supportedDragActions enable drag. * @return */ Qt::DropActions StoreModel::supportedDragActions() const { return Qt::CopyAction | Qt::MoveAction; } /** * @brief StoreModel::flags * @param index * @return */ Qt::ItemFlags StoreModel::flags(const QModelIndex &index) const { Qt::ItemFlags defaultFlags = QSortFilterProxyModel::flags(index); if (index.isValid()) { return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; } return Qt::ItemIsDropEnabled | defaultFlags; } /** * @brief StoreModel::mimeTypes * @return */ QStringList StoreModel::mimeTypes() const { QStringList types; types << mimeType; return types; } /** * @brief StoreModel::mimeData * @param indexes * @return */ QMimeData *StoreModel::mimeData(const QModelIndexList &indexes) const { dragAndDropInfoPasswordStore info; QByteArray encodedData; // only use the first, otherwise we should enable multiselection QModelIndex index = indexes.at(0); if (index.isValid()) { - QModelIndex useIndex = mapToSource(index); + auto fileInfo = index.data(AddFileInfoProxy::FileInfoRole).value(); - info.isDir = fs()->fileInfo(useIndex).isDir(); - info.isFile = fs()->fileInfo(useIndex).isFile(); - info.path = fs()->fileInfo(useIndex).absoluteFilePath(); + info.isDir = fileInfo.isDir(); + info.isFile = fileInfo.isFile(); + info.path = fileInfo.absoluteFilePath(); QDataStream stream(&encodedData, QIODevice::WriteOnly); stream << info; } auto *mimeData = new QMimeData(); mimeData->setData(mimeType, encodedData); return mimeData; } /** * @brief StoreModel::canDropMimeData * @param data * @param action * @param row * @param column * @param parent * @return */ bool StoreModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const { #ifdef QT_DEBUG qDebug() << action << row; #else Q_UNUSED(action) Q_UNUSED(row) #endif - if (!fs()) { + if (!parent.isValid()) { + return false; + } + if (!sourceModel()) { return false; } QModelIndex useIndex = this->index(parent.row(), parent.column(), parent.parent()); QByteArray encodedData = data->data(mimeType); QDataStream stream(&encodedData, QIODevice::ReadOnly); dragAndDropInfoPasswordStore info; stream >> info; if (!data->hasFormat(mimeType)) return false; if (column > 0) { return false; } + auto fileInfo = useIndex.data(AddFileInfoProxy::FileInfoRole).value(); // you can drop a folder on a folder - if (fs()->fileInfo(mapToSource(useIndex)).isDir() && info.isDir) { + if (fileInfo.isDir() && info.isDir) { return true; } // you can drop a file on a folder - if (fs()->fileInfo(mapToSource(useIndex)).isDir() && info.isFile) { + if (fileInfo.isDir() && info.isFile) { return true; } // you can drop a file on a file - if (fs()->fileInfo(mapToSource(useIndex)).isFile() && info.isFile) { + if (fileInfo.isFile() && info.isFile) { return true; } return false; } /** * @brief StoreModel::dropMimeData * @param data * @param action * @param row * @param column * @param parent * @return */ bool StoreModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { if (!canDropMimeData(data, action, row, column, parent)) return false; if (action == Qt::IgnoreAction) { return true; } QByteArray encodedData = data->data(mimeType); QDataStream stream(&encodedData, QIODevice::ReadOnly); dragAndDropInfoPasswordStore info; stream >> info; QModelIndex destIndex = this->index(parent.row(), parent.column(), parent.parent()); - QFileInfo destFileinfo = fs()->fileInfo(mapToSource(destIndex)); + QFileInfo destFileinfo = destIndex.data(AddFileInfoProxy::FileInfoRole).value(); QFileInfo srcFileInfo = QFileInfo(info.path); QString cleanedSrc = QDir::cleanPath(srcFileInfo.absoluteFilePath()); QString cleanedDest = QDir::cleanPath(destFileinfo.absoluteFilePath()); if (info.isDir) { // dropped dir onto dir if (destFileinfo.isDir()) { QDir destDir = QDir(cleanedDest).filePath(srcFileInfo.fileName()); QString cleanedDestDir = QDir::cleanPath(destDir.absolutePath()); if (action == Qt::MoveAction) { m_pass.Move(cleanedSrc, cleanedDestDir); } else if (action == Qt::CopyAction) { m_pass.Copy(cleanedSrc, cleanedDestDir); } } } else if (info.isFile) { // dropped file onto a directory if (destFileinfo.isDir()) { if (action == Qt::MoveAction) { m_pass.Move(cleanedSrc, cleanedDest); } else if (action == Qt::CopyAction) { m_pass.Copy(cleanedSrc, cleanedDest); } } else if (destFileinfo.isFile()) { // dropped file onto a file int answer = QMessageBox::question(nullptr, i18n("Force overwrite?"), i18nc("Overwrite DestinationFile with SourceFile", "Overwrite %1 with %2?", cleanedDest, cleanedSrc), QMessageBox::Yes | QMessageBox::No); bool force = answer == QMessageBox::Yes; if (action == Qt::MoveAction) { m_pass.Move(cleanedSrc, cleanedDest, force); } else if (action == Qt::CopyAction) { m_pass.Copy(cleanedSrc, cleanedDest, force); } } } return true; } /** * @brief StoreModel::lessThan * @param source_left * @param source_right * @return */ bool StoreModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { /* matches logic in QFileSystemModelSorter::compareNodes() */ #ifndef Q_OS_MAC - if (fs() && (source_left.column() == 0 || source_left.column() == 1)) { - bool leftD = fs()->isDir(source_left); - bool rightD = fs()->isDir(source_right); + if (source_left.column() == 0 || source_left.column() == 1) { + bool leftD = source_left.data(AddFileInfoProxy::FileInfoRole).value().isDir(); + bool rightD = source_right.data(AddFileInfoProxy::FileInfoRole).value().isDir(); if (leftD ^ rightD) return leftD; } #endif return QSortFilterProxyModel::lessThan(source_left, source_right); } - -QFileSystemModel *StoreModel::fs() const -{ - return static_cast(sourceModel()); -} diff --git a/src/storemodel.h b/src/storemodel.h index 180c66e..74a890d 100644 --- a/src/storemodel.h +++ b/src/storemodel.h @@ -1,56 +1,55 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2018 Claudio Maradonna SPDX-FileCopyrightText: 2019 Maciej S. Szmigiero SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #ifndef STOREMODEL_H_ #define STOREMODEL_H_ #include /*! \class StoreModel \brief The QSortFilterProxyModel for handling filesystem searches. */ class QFileSystemModel; class Pass; class StoreModel : public QSortFilterProxyModel { Q_OBJECT private: - QFileSystemModel *fs() const; Pass &m_pass; public: explicit StoreModel(Pass &pass); QVariant data(const QModelIndex &index, int role) const override; bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; // QAbstractItemModel interface public: Qt::DropActions supportedDropActions() const override; Qt::DropActions supportedDragActions() const override; Qt::ItemFlags flags(const QModelIndex &index) const override; QStringList mimeTypes() const override; QMimeData *mimeData(const QModelIndexList &indexes) const override; bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; }; /*! \struct dragAndDropInfo \brief holds values to share beetween drag and drop on the passwordstorage view */ struct dragAndDropInfoPasswordStore { bool isDir = false; bool isFile = false; QString path; }; #endif // STOREMODEL_H_