Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F35337276
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
49 KB
Subscribers
None
View Options
diff --git a/src/job/fileencryptjob.cpp b/src/job/fileencryptjob.cpp
index ab5518c..5d233e9 100644
--- a/src/job/fileencryptjob.cpp
+++ b/src/job/fileencryptjob.cpp
@@ -1,77 +1,86 @@
// SPDX-FileCopyrightText: 2024 g10 Code GmbH
// SPDX-FileContributor: Carl Schwan <carl@carlschwan.eu>
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "fileencryptjob.h"
#include <QSaveFile>
#include <KLocalizedString>
#include <Libkleo/Formatting>
#include <QGpgME/EncryptJob>
#include <QGpgME/Protocol>
#include <gpgme++/encryptionresult.h>
#include "gpgmehelpers.h"
FileEncryptJob::FileEncryptJob(const QString &filePath, const QByteArray &content, const QList<QByteArray> &recipientsKeys, QObject *parent)
: KJob(parent)
, m_filePath(filePath)
, m_content(content)
, m_recipientsKeys(recipientsKeys)
{
}
void FileEncryptJob::start()
{
QGpgME::Protocol *protocol = QGpgME::openpgp();
auto job = protocol->encryptJob();
std::vector<GpgME::Key> keys;
auto ctx = QGpgME::Job::context(job);
+
+ QString errorString;
for (const auto &keyId : std::as_const(m_recipientsKeys)) {
GpgME::Error error;
auto key = ctx->key(keyId.data(), error, false);
if (!error && !key.isNull()) {
keys.push_back(key);
+ } else if (error) {
+ errorString = i18nc("@info:status",
+ "Unable to find key for recipient with keyId \"%1\". Error: %2",
+ QString::fromUtf8(keyId),
+ Kleo::Formatting::errorAsString(error));
+ } else {
+ errorString = i18nc("@info:status", "Unable to find key for recipient with keyId \"%1\".", QString::fromUtf8(keyId));
}
}
- if (keys.empty()) {
+ if (!errorString.isEmpty()) {
setError(EncryptionError);
- setErrorText(i18n("Could not read encryption key to use, .gpg-id file missing or invalid."));
+ setErrorText(errorString);
emitResult();
return;
}
connect(job, &QGpgME::EncryptJob::result, this, [this](auto &&result, const QByteArray &ciphertext, const QString &log, auto &&auditResult) {
Q_UNUSED(log);
Q_UNUSED(auditResult);
if (!isSuccess(result.error())) {
setError(EncryptionError);
setErrorText(Kleo::Formatting::errorAsString(result.error()));
emitResult();
return;
}
QSaveFile f(m_filePath);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
setError(PermissionError);
setErrorText(f.errorString());
emitResult();
return;
}
f.write(ciphertext);
if (f.error() != QFileDevice::NoError) {
setError(PermissionError);
setErrorText(f.errorString());
}
if (!f.commit()) {
setError(PermissionError);
setErrorText(f.errorString());
}
emitResult();
});
job->start(keys, m_content);
}
diff --git a/src/models/storemodel.cpp b/src/models/storemodel.cpp
index 778964a..457767a 100644
--- a/src/models/storemodel.cpp
+++ b/src/models/storemodel.cpp
@@ -1,429 +1,430 @@
/*
SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.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 "storemodel.h"
#include "addfileinfoproxy.h"
#include "job/filereencryptjob.h"
#include "rootfolderconfig.h"
#include "rootfoldersmanager.h"
#include "util.h"
#include <KLocalizedString>
#include <KSelectionProxyModel>
#include <QDir>
#include <QFileSystemModel>
#include <QItemSelectionModel>
#include <QMessageBox>
#include <QMimeData>
#include <QRegularExpression>
#include <qstringliteral.h>
static const QString mimeType = QStringLiteral("application/vnd+gnupgpass.dragAndDropInfoPasswordStore");
/// \brief holds values to share beetween drag and drop on the passwordstorage view.
struct DragAndDropInfoPasswordStore {
bool isDir = false;
bool isFile = false;
QString path;
};
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;
}
StoreModel::StoreModel(QObject *parent)
: QSortFilterProxyModel(parent)
, m_fileSystemModel(new QFileSystemModel(this))
, m_addRoleModel(new AddFileInfoProxy(this))
, m_itemSelectionModel(new QItemSelectionModel(m_addRoleModel, this))
, m_selectionProxyModel(new KSelectionProxyModel(m_itemSelectionModel, this))
{
m_fileSystemModel->setNameFilters({QStringLiteral("*.gpg")});
m_fileSystemModel->setNameFilterDisables(false);
m_addRoleModel->setSourceModel(m_fileSystemModel);
m_selectionProxyModel->setFilterBehavior(KSelectionProxyModel::SubTrees);
m_selectionProxyModel->setSourceModel(m_addRoleModel);
setObjectName(QStringLiteral("StoreModel"));
setRecursiveFilteringEnabled(true);
setSourceModel(m_selectionProxyModel);
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
setAutoAcceptChildRows(true);
#endif
connect(m_fileSystemModel, &QFileSystemModel::directoryLoaded, this, &StoreModel::directoryLoaded);
}
int StoreModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
RootFoldersManager *StoreModel::rootFoldersManager() const
{
return m_rootFoldersManager;
}
void StoreModel::setRootFoldersManager(RootFoldersManager *rootFoldersManager)
{
if (m_rootFoldersManager) {
disconnect(m_rootFoldersManager, &RootFoldersManager::rootFoldersChanged, this, &StoreModel::updateRootFolders);
}
m_rootFoldersManager = rootFoldersManager;
if (m_rootFoldersManager) {
updateRootFolders();
connect(m_rootFoldersManager, &RootFoldersManager::rootFoldersChanged, this, &StoreModel::updateRootFolders);
}
}
void StoreModel::updateRootFolders()
{
m_itemSelectionModel->clear();
for (const auto &rootFolder : m_rootFoldersManager->rootFolders()) {
QDir dir(rootFolder->path());
if (!dir.exists()) {
dir.mkpath(QStringLiteral("."));
}
QModelIndex rootDirIndex = m_fileSystemModel->setRootPath(rootFolder->path());
m_fileSystemModel->fetchMore(rootDirIndex);
m_itemSelectionModel->select(m_addRoleModel->mapFromSource(rootDirIndex), QItemSelectionModel::Select);
}
Q_EMIT rootFoldersSizeChanged();
}
QVariant StoreModel::data(const QModelIndex &index, int role) const
{
Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid));
const auto initialValue = QSortFilterProxyModel::data(index, role);
if (index.column() == 0 && !index.parent().isValid() && m_rootFoldersManager) {
if (role == Qt::DisplayRole && m_rootFoldersManager->rootFolders().count() > 1) {
const auto uuid = initialValue.toString();
if (!uuid.isEmpty()) {
return m_rootFoldersManager->rootFolderName(uuid);
}
} else if (role == Qt::DecorationRole) {
return QIcon::fromTheme(QStringLiteral("folder-root-symbolic"));
}
}
if (role == Qt::ToolTipRole) {
const auto fileInfo = index.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>();
const bool isDir = fileInfo.isDir();
if (isDir) {
const auto recipients = recipientsForFile(fileInfo);
QString tooltip = i18nc("@info:tooltip", "<p>This directory is encrypted for the following users:</p><ul>");
for (const auto &recipient : recipients) {
tooltip += QStringLiteral("<li>") + QString::fromUtf8(recipient) + QStringLiteral("</li>");
}
tooltip += QStringLiteral("</ul>");
return tooltip;
}
} else if (role == Qt::DisplayRole) {
QString name = initialValue.toString();
name.replace(Util::endsWithGpg(), QString{});
return name;
}
return initialValue;
}
Qt::DropActions StoreModel::supportedDropActions() const
{
return Qt::CopyAction | Qt::MoveAction;
}
Qt::DropActions StoreModel::supportedDragActions() const
{
return Qt::CopyAction | Qt::MoveAction;
}
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;
}
QStringList StoreModel::mimeTypes() const
{
QStringList types;
types << mimeType;
return types;
}
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()) {
auto fileInfo = index.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>();
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;
}
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 (!sourceModel()) {
return false;
}
const QModelIndex idx = 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 = idx.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>();
const auto rootDir = fileInfo.absoluteFilePath().isEmpty();
// you can drop a folder on a folder
if ((fileInfo.isDir() || rootDir) && info.isDir) {
return true;
}
// you can drop a file on a folder
if ((fileInfo.isDir() || rootDir) && info.isFile) {
return true;
}
// you can drop a file on a file
if (fileInfo.isFile() && info.isFile) {
return true;
}
return false;
}
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 = destIndex.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>();
if (destFileinfo.absoluteFilePath().isEmpty()) {
auto rootFolder = m_rootFoldersManager->rootFolders().constFirst();
destFileinfo = QFileInfo(rootFolder->path());
}
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) {
move(cleanedSrc, cleanedDestDir);
} else if (action == Qt::CopyAction) {
copy(cleanedSrc, cleanedDestDir);
}
}
} else if (info.isFile) {
// dropped file onto a directory
if (destFileinfo.isDir()) {
if (action == Qt::MoveAction) {
move(cleanedSrc, cleanedDest);
} else if (action == Qt::CopyAction) {
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) {
move(cleanedSrc, cleanedDest, force);
} else if (action == Qt::CopyAction) {
copy(cleanedSrc, cleanedDest, force);
}
}
}
return true;
}
bool StoreModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
/* matches logic in QFileSystemModelSorter::compareNodes() */
#ifndef Q_OS_MAC
if (source_left.column() == 0 || source_left.column() == 1) {
bool leftD = source_left.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>().isDir();
bool rightD = source_right.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>().isDir();
if (leftD ^ rightD)
return leftD;
}
#endif
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
QList<QByteArray> StoreModel::recipientsForFile(const QFileInfo &fileInfo) const
{
QDir gpgIdPath(fileInfo.isDir() ? fileInfo.absoluteFilePath() : fileInfo.absoluteDir());
bool found = false;
QString rootDir;
while (gpgIdPath.exists()) {
bool isInRootFolder = false;
const auto rootFolders = m_rootFoldersManager->rootFolders();
for (const auto &rootFolder : rootFolders) {
auto rootFolderPath = rootFolder->path();
rootFolderPath.chop(1); // remove / at end
if (gpgIdPath.absolutePath().startsWith(rootFolderPath)) {
isInRootFolder = true;
rootDir = rootFolder->path();
}
}
if (!isInRootFolder) {
break;
}
if (QFile(gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id"))).exists()) {
found = true;
break;
}
if (!gpgIdPath.cdUp())
break;
}
QFile gpgId(found ? gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id")) : rootDir + QStringLiteral(".gpg-id"));
if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ qWarning() << "Unable to open" << (found ? gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id")) : rootDir + QStringLiteral(".gpg-id"));
return {};
}
QList<QByteArray> recipients;
while (!gpgId.atEnd()) {
const auto recipient(gpgId.readLine().trimmed());
if (!recipient.isEmpty()) {
recipients += recipient;
}
}
std::sort(recipients.begin(), recipients.end());
return recipients;
}
void StoreModel::move(const QString &source, const QString &destination, const bool force)
{
QFileInfo srcFileInfo(source);
QFileInfo destFileInfo(destination);
QString destFile;
QString srcFileBaseName = srcFileInfo.fileName();
if (srcFileInfo.isFile()) {
if (destFileInfo.isFile()) {
if (!force) {
return;
}
} else if (destFileInfo.isDir()) {
destFile = QDir(destination).filePath(srcFileBaseName);
} else {
destFile = destination;
}
if (destFile.endsWith(QStringLiteral(".gpg"), Qt::CaseInsensitive))
destFile.chop(4); // make sure suffix is lowercase
destFile.append(QStringLiteral(".gpg"));
} else if (srcFileInfo.isDir()) {
if (destFileInfo.isDir()) {
destFile = QDir(destination).filePath(srcFileBaseName);
} else if (destFileInfo.isFile()) {
return;
} else {
destFile = destination;
}
} else {
return;
}
QDir qDir;
if (force) {
qDir.remove(destFile);
}
qDir.rename(source, destFile);
}
void StoreModel::copy(const QString &source, const QString &destination, const bool force)
{
QDir qDir;
if (force) {
qDir.remove(destination);
}
if (!QFile::copy(source, destination)) {
Q_EMIT errorOccurred(QStringLiteral("Failed to copy file"));
}
const QFileInfo destinationInfo(destination);
const QFileInfo sourceInfo(source);
if (destinationInfo.isDir()) {
const auto recipients = recipientsForFile(destinationInfo);
auto fileReencryptJob = new FileReencryptJob(destinationInfo.absoluteFilePath() + u'/' + sourceInfo.fileName(), recipients);
fileReencryptJob->start();
} else if (destinationInfo.isFile()) {
const auto recipients = recipientsForFile(destinationInfo);
auto fileReencryptJob = new FileReencryptJob(destinationInfo.absoluteFilePath(), recipients);
fileReencryptJob->start();
}
}
QModelIndex StoreModel::indexForPath(const QString &filePath)
{
const auto sourceIndex = m_fileSystemModel->index(filePath);
auto proxyIndex = m_addRoleModel->mapFromSource(sourceIndex);
proxyIndex = m_selectionProxyModel->mapFromSource(proxyIndex);
return mapFromSource(proxyIndex);
}
diff --git a/src/widgets/mainwindow.cpp b/src/widgets/mainwindow.cpp
index 105d342..3876a84 100644
--- a/src/widgets/mainwindow.cpp
+++ b/src/widgets/mainwindow.cpp
@@ -1,867 +1,873 @@
/*
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 "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 <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>
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);
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 = KStandardAction::openNew(this, &MainWindow::addPassword, actionCollection());
m_actionAddEntry->setText(i18nc("@action", "Add Entry"));
m_actionAddEntry->setToolTip(i18nc("@info:tooltip", "Add Password Entry"));
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->treeView, &QTreeView::doubleClicked, this, &MainWindow::editTreeItem);
connect(ui->lineEdit, &QLineEdit::textChanged, this, &MainWindow::filterList);
connect(ui->lineEdit, &QLineEdit::returnPressed, this, &MainWindow::selectFromSearch);
}
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;
// TODO(bezet): "Could not decrypt";
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 insure 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, 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"));
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 = i18nc("deleting a folder; placeholder is folder name", "Are you sure you want to remove %1 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 = i18nc("deleting a file; placeholder is file name", "Are you sure you want to remove %1?", file);
}
if (KMessageBox::warningTwoActions(this,
message,
isDir ? i18n("Remove folder?") : i18n("Remove entry?"),
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, i18n("Unable to write user configuration at \"%1\". Error: %2", gpgIdFile, gpgId.errorString()));
return;
}
gpgId.write(keyId);
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, 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_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, i18n("Rename file"), i18n("Rename File To: "), QLineEdit::Normal, fileName, &ok);
if (!ok || newName.isEmpty())
return;
QString newFile = QDir(filePath).filePath(newName);
m_storeModel.move(file, newFile);
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Feb 6, 8:21 AM (10 h, 39 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
b5/fc/48bd59cea0272d7973bfa2a30ff2
Attached To
rGPGPASS GnuPG Password Manager
Event Timeline
Log In to Comment