Merge pull request #1139 from frostasm/add-support-for-working-with-multiple-attachments

Add support for working with multiple entry attachments at once
This commit is contained in:
TheZ3ro 2017-11-12 16:55:07 +01:00 committed by GitHub
commit b28333ea0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 265 additions and 69 deletions

View File

@ -17,6 +17,8 @@
#include "EntryAttachments.h" #include "EntryAttachments.h"
#include <QStringList>
EntryAttachments::EntryAttachments(QObject* parent) EntryAttachments::EntryAttachments(QObject* parent)
: QObject(parent) : QObject(parent)
{ {
@ -71,7 +73,8 @@ void EntryAttachments::set(const QString& key, const QByteArray& value)
void EntryAttachments::remove(const QString& key) void EntryAttachments::remove(const QString& key)
{ {
if (!m_attachments.contains(key)) { if (!m_attachments.contains(key)) {
Q_ASSERT(false); Q_ASSERT_X(false, "EntryAttachments::remove",
qPrintable(QString("Can't find attachment for key %1").arg(key)));
return; return;
} }
@ -83,6 +86,31 @@ void EntryAttachments::remove(const QString& key)
emit modified(); emit modified();
} }
void EntryAttachments::remove(const QStringList &keys)
{
if (keys.isEmpty()) {
return;
}
bool isModified = false;
for (const QString &key: keys) {
if (!m_attachments.contains(key)) {
Q_ASSERT_X(false, "EntryAttachments::remove",
qPrintable(QString("Can't find attachment for key %1").arg(key)));
continue;
}
isModified = true;
emit aboutToBeRemoved(key);
m_attachments.remove(key);
emit removed(key);
}
if (isModified) {
emit modified();
}
}
void EntryAttachments::clear() void EntryAttachments::clear()
{ {
if (m_attachments.isEmpty()) { if (m_attachments.isEmpty()) {

View File

@ -21,6 +21,8 @@
#include <QMap> #include <QMap>
#include <QObject> #include <QObject>
class QStringList;
class EntryAttachments : public QObject class EntryAttachments : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -33,6 +35,7 @@ public:
QByteArray value(const QString& key) const; QByteArray value(const QString& key) const;
void set(const QString& key, const QByteArray& value); void set(const QString& key, const QByteArray& value);
void remove(const QString& key); void remove(const QString& key);
void remove(const QStringList& keys);
void clear(); void clear();
void copyDataFrom(const EntryAttachments* other); void copyDataFrom(const EntryAttachments* other);
bool operator==(const EntryAttachments& other) const; bool operator==(const EntryAttachments& other) const;

View File

@ -27,7 +27,7 @@ QString FileDialog::getOpenFileName(QWidget* parent, const QString& caption, QSt
{ {
if (!m_nextFileName.isEmpty()) { if (!m_nextFileName.isEmpty()) {
QString result = m_nextFileName; QString result = m_nextFileName;
m_nextFileName = ""; m_nextFileName.clear();
return result; return result;
} }
else { else {
@ -51,13 +51,43 @@ QString FileDialog::getOpenFileName(QWidget* parent, const QString& caption, QSt
} }
} }
QStringList FileDialog::getOpenFileNames(QWidget *parent, const QString &caption, QString dir,
const QString &filter, QString *selectedFilter,
QFileDialog::Options options)
{
if (!m_nextFileNames.isEmpty()) {
QStringList results = m_nextFileNames;
m_nextFileNames.clear();
return results;
}
else {
if (dir.isEmpty()) {
dir = config()->get("LastDir").toString();
}
QStringList results = QFileDialog::getOpenFileNames(parent, caption, dir, filter,
selectedFilter, options);
// on Mac OS X the focus is lost after closing the native dialog
if (parent) {
parent->activateWindow();
}
if (!results.isEmpty()) {
config()->set("LastDir", QFileInfo(results[0]).absolutePath());
}
return results;
}
}
QString FileDialog::getSaveFileName(QWidget* parent, const QString& caption, QString dir, QString FileDialog::getSaveFileName(QWidget* parent, const QString& caption, QString dir,
const QString& filter, QString* selectedFilter, const QString& filter, QString* selectedFilter,
QFileDialog::Options options, const QString& defaultExtension) QFileDialog::Options options, const QString& defaultExtension)
{ {
if (!m_nextFileName.isEmpty()) { if (!m_nextFileName.isEmpty()) {
QString result = m_nextFileName; QString result = m_nextFileName;
m_nextFileName = ""; m_nextFileName.clear();
return result; return result;
} }
else { else {
@ -103,11 +133,49 @@ QString FileDialog::getSaveFileName(QWidget* parent, const QString& caption, QSt
} }
} }
QString FileDialog::getExistingDirectory(QWidget *parent, const QString &caption, QString dir,
QFileDialog::Options options)
{
if (!m_nextDirName.isEmpty()) {
QString result = m_nextDirName;
m_nextDirName.clear();
return result;
}
else {
if (dir.isEmpty()) {
dir = config()->get("LastDir").toString();
}
dir = QFileDialog::getExistingDirectory(parent, caption, dir, options);
// on Mac OS X the focus is lost after closing the native dialog
if (parent) {
parent->activateWindow();
}
if (!dir.isEmpty()) {
config()->set("LastDir", QFileInfo(dir).absolutePath());
}
return dir;
}
}
void FileDialog::setNextFileName(const QString& fileName) void FileDialog::setNextFileName(const QString& fileName)
{ {
m_nextFileName = fileName; m_nextFileName = fileName;
} }
void FileDialog::setNextFileNames(const QStringList &fileNames)
{
m_nextFileNames = fileNames;
}
void FileDialog::setNextDirName(const QString &dirName)
{
m_nextDirName = dirName;
}
FileDialog::FileDialog() FileDialog::FileDialog()
{ {
} }

View File

@ -26,22 +26,31 @@ public:
QString getOpenFileName(QWidget* parent = nullptr, const QString& caption = QString(), QString getOpenFileName(QWidget* parent = nullptr, const QString& caption = QString(),
QString dir = QString(), const QString& filter = QString(), QString dir = QString(), const QString& filter = QString(),
QString* selectedFilter = nullptr, QFileDialog::Options options = 0); QString* selectedFilter = nullptr, QFileDialog::Options options = 0);
QStringList getOpenFileNames(QWidget* parent = nullptr, const QString& caption = QString(),
QString dir = QString(), const QString& filter = QString(),
QString* selectedFilter = nullptr, QFileDialog::Options options = 0);
QString getSaveFileName(QWidget* parent = nullptr, const QString& caption = QString(), QString getSaveFileName(QWidget* parent = nullptr, const QString& caption = QString(),
QString dir = QString(), const QString& filter = QString(), QString dir = QString(), const QString& filter = QString(),
QString* selectedFilter = nullptr, QFileDialog::Options options = 0, QString* selectedFilter = nullptr, QFileDialog::Options options = 0,
const QString& defaultExtension = QString()); const QString& defaultExtension = QString());
QString getExistingDirectory(QWidget* parent = nullptr, const QString& caption = QString(),
QString dir = QString(), QFileDialog::Options options = QFileDialog::ShowDirsOnly);
/** /**
* Sets the result of the next get* method call. * Sets the result of the next get* method call.
* Use only for testing. * Use only for testing.
*/ */
void setNextFileName(const QString& fileName); void setNextFileName(const QString& fileName);
void setNextFileNames(const QStringList& fileNames);
void setNextDirName(const QString& dirName);
static FileDialog* instance(); static FileDialog* instance();
private: private:
FileDialog(); FileDialog();
QString m_nextFileName; QString m_nextFileName;
QStringList m_nextFileNames;
QString m_nextDirName;
static FileDialog* m_instance; static FileDialog* m_instance;

View File

@ -122,13 +122,14 @@ void EditEntryWidget::setupAdvanced()
m_attachmentsModel->setEntryAttachments(m_entryAttachments); m_attachmentsModel->setEntryAttachments(m_entryAttachments);
m_advancedUi->attachmentsView->setModel(m_attachmentsModel); m_advancedUi->attachmentsView->setModel(m_attachmentsModel);
m_advancedUi->attachmentsView->setSelectionMode(QAbstractItemView::ExtendedSelection);
connect(m_advancedUi->attachmentsView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), connect(m_advancedUi->attachmentsView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
SLOT(updateAttachmentButtonsEnabled(QModelIndex))); SLOT(updateAttachmentButtonsEnabled(QModelIndex)));
connect(m_advancedUi->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(openAttachment(QModelIndex))); connect(m_advancedUi->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(openAttachment(QModelIndex)));
connect(m_advancedUi->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveCurrentAttachment())); connect(m_advancedUi->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveSelectedAttachments()));
connect(m_advancedUi->openAttachmentButton, SIGNAL(clicked()), SLOT(openCurrentAttachment())); connect(m_advancedUi->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments()));
connect(m_advancedUi->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachment())); connect(m_advancedUi->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachments()));
connect(m_advancedUi->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeCurrentAttachment())); connect(m_advancedUi->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments()));
m_attributesModel->setEntryAttributes(m_entryAttributes); m_attributesModel->setEntryAttributes(m_entryAttributes);
m_advancedUi->attributesView->setModel(m_attributesModel); m_advancedUi->attributesView->setModel(m_attributesModel);
@ -672,6 +673,32 @@ void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected)
m_advancedUi->protectAttributeButton->blockSignals(false); m_advancedUi->protectAttributeButton->blockSignals(false);
} }
bool EditEntryWidget::openAttachment(const QModelIndex &index, QString *errorMessage)
{
const QString filename = m_attachmentsModel->keyByIndex(index);
const QByteArray attachmentData = m_entryAttachments->value(filename);
// tmp file will be removed once the database (or the application) has been closed
const QString tmpFileTemplate = QDir::temp().absoluteFilePath(QString("XXXXXX.").append(filename));
QTemporaryFile* tmpFile = new QTemporaryFile(tmpFileTemplate, this);
const bool saveOk = tmpFile->open()
&& tmpFile->write(attachmentData) == attachmentData.size()
&& tmpFile->flush();
if (!saveOk) {
if (errorMessage) {
*errorMessage = tr("Unable to save the attachment:\n").append(tmpFile->errorString());
}
delete tmpFile;
return false;
}
tmpFile->close();
QDesktopServices::openUrl(QUrl::fromLocalFile(tmpFile->fileName()));
return true;
}
void EditEntryWidget::protectCurrentAttribute(bool state) void EditEntryWidget::protectCurrentAttribute(bool state)
{ {
QModelIndex index = m_advancedUi->attributesView->currentIndex(); QModelIndex index = m_advancedUi->attributesView->currentIndex();
@ -702,7 +729,7 @@ void EditEntryWidget::revealCurrentAttribute()
} }
} }
void EditEntryWidget::insertAttachment() void EditEntryWidget::insertAttachments()
{ {
Q_ASSERT(!m_history); Q_ASSERT(!m_history);
@ -710,53 +737,115 @@ void EditEntryWidget::insertAttachment()
if (defaultDir.isEmpty() || !QDir(defaultDir).exists()) { if (defaultDir.isEmpty() || !QDir(defaultDir).exists()) {
defaultDir = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).value(0); defaultDir = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).value(0);
} }
QString filename = fileDialog()->getOpenFileName(this, tr("Select file"), defaultDir);
if (filename.isEmpty() || !QFile::exists(filename)) { const QStringList filenames = fileDialog()->getOpenFileNames(this, tr("Select files"), defaultDir);
if (filenames.isEmpty()) {
return; return;
} }
QFile file(filename); config()->set("LastAttachmentDir", QFileInfo(filenames.first()).absolutePath());
if (!file.open(QIODevice::ReadOnly)) {
showMessage(tr("Unable to open file").append(":\n").append(file.errorString()), MessageWidget::Error); QStringList errors;
return; for (const QString &filename: filenames) {
const QFileInfo fInfo(filename);
QFile file(filename);
QByteArray data;
const bool readOk = file.open(QIODevice::ReadOnly) && Tools::readAllFromDevice(&file, data);
if (!readOk) {
errors.append(QString("%1 - %2").arg(fInfo.fileName(), file.errorString()));
continue;
}
m_entryAttachments->set(fInfo.fileName(), data);
} }
QByteArray data; if (!errors.isEmpty()) {
if (!Tools::readAllFromDevice(&file, data)) { showMessage(tr("Unable to open files:\n%1").arg(errors.join('\n')), MessageWidget::Error);
showMessage(tr("Unable to open file").append(":\n").append(file.errorString()), MessageWidget::Error);
return;
} }
m_entryAttachments->set(QFileInfo(filename).fileName(), data);
} }
void EditEntryWidget::saveCurrentAttachment() void EditEntryWidget::saveSelectedAttachment()
{ {
QModelIndex index = m_advancedUi->attachmentsView->currentIndex(); const QModelIndex index = m_advancedUi->attachmentsView->currentIndex();
if (!index.isValid()) { if (!index.isValid()) {
return; return;
} }
QString filename = m_attachmentsModel->keyByIndex(index); const QString filename = m_attachmentsModel->keyByIndex(index);
QString defaultDirName = config()->get("LastAttachmentDir").toString(); QString defaultDirName = config()->get("LastAttachmentDir").toString();
if (defaultDirName.isEmpty() || !QDir(defaultDirName).exists()) { if (defaultDirName.isEmpty() || !QDir(defaultDirName).exists()) {
defaultDirName = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); defaultDirName = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
} }
QDir dir(defaultDirName);
QString savePath = fileDialog()->getSaveFileName(this, tr("Save attachment"), const QString savePath = fileDialog()->getSaveFileName(this, tr("Save attachment"),
dir.filePath(filename)); QDir(defaultDirName).filePath(filename));
if (!savePath.isEmpty()) { if (!savePath.isEmpty()) {
QByteArray attachmentData = m_entryAttachments->value(filename); config()->set("LastAttachmentDir", QFileInfo(savePath).absolutePath());
QFile file(savePath); QFile file(savePath);
if (!file.open(QIODevice::WriteOnly)) { const QByteArray attachmentData = m_entryAttachments->value(filename);
const bool saveOk = file.open(QIODevice::WriteOnly) && file.write(attachmentData) == attachmentData.size();
if (!saveOk) {
showMessage(tr("Unable to save the attachment:\n").append(file.errorString()), MessageWidget::Error); showMessage(tr("Unable to save the attachment:\n").append(file.errorString()), MessageWidget::Error);
}
}
}
void EditEntryWidget::saveSelectedAttachments()
{
const QModelIndexList indexes = m_advancedUi->attachmentsView->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) {
return;
} else if (indexes.count() == 1) {
saveSelectedAttachment();
return;
}
QString defaultDirName = config()->get("LastAttachmentDir").toString();
if (defaultDirName.isEmpty() || !QDir(defaultDirName).exists()) {
defaultDirName = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
}
const QString savePath = fileDialog()->getExistingDirectory(this, tr("Save attachments"), defaultDirName);
if (savePath.isEmpty()) {
return;
}
QDir saveDir(savePath);
if (!saveDir.exists()) {
if (saveDir.mkpath(saveDir.absolutePath())) {
showMessage(tr("Unable to create the directory:\n").append(saveDir.absolutePath()), MessageWidget::Error);
return; return;
} }
if (file.write(attachmentData) != attachmentData.size()) { }
showMessage(tr("Unable to save the attachment:\n").append(file.errorString()), MessageWidget::Error); config()->set("LastAttachmentDir", QFileInfo(saveDir.absolutePath()).absolutePath());
return;
QStringList errors;
for (const QModelIndex &index: indexes) {
const QString filename = m_attachmentsModel->keyByIndex(index);
const QString attachmentPath = saveDir.absoluteFilePath(filename);
if (QFileInfo::exists(attachmentPath)) {
const QString question(tr("Are you sure you want to overwrite existing file \"%1\" with the attachment?"));
auto ans = MessageBox::question(this, tr("Confirm overwrite"), question.arg(filename),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (ans == QMessageBox::No) {
continue;
} else if (ans == QMessageBox::Cancel) {
return;
}
} }
QFile file(attachmentPath);
const QByteArray attachmentData = m_entryAttachments->value(filename);
const bool saveOk = file.open(QIODevice::WriteOnly) && file.write(attachmentData) == attachmentData.size();
if (!saveOk) {
errors.append(QString("%1 - %2").arg(filename, file.errorString()));
}
}
if (!errors.isEmpty()) {
showMessage(tr("Unable to save the attachments:\n").append(errors.join('\n')), MessageWidget::Error);
} }
} }
@ -767,55 +856,51 @@ void EditEntryWidget::openAttachment(const QModelIndex& index)
return; return;
} }
QString filename = m_attachmentsModel->keyByIndex(index); QString errorMessage;
QByteArray attachmentData = m_entryAttachments->value(filename); if (!openAttachment(index, &errorMessage)) {
showMessage(errorMessage, MessageWidget::Error);
// tmp file will be removed once the database (or the application) has been closed
QString tmpFileTemplate = QDir::temp().absoluteFilePath(QString("XXXXXX.").append(filename));
QTemporaryFile* file = new QTemporaryFile(tmpFileTemplate, this);
if (!file->open()) {
showMessage(tr("Unable to save the attachment:\n").append(file->errorString()), MessageWidget::Error);
return;
} }
if (file->write(attachmentData) != attachmentData.size()) {
showMessage(tr("Unable to save the attachment:\n").append(file->errorString()), MessageWidget::Error);
return;
}
if (!file->flush()) {
showMessage(tr("Unable to save the attachment:\n").append(file->errorString()), MessageWidget::Error);
return;
}
file->close();
QDesktopServices::openUrl(QUrl::fromLocalFile(file->fileName()));
} }
void EditEntryWidget::openCurrentAttachment() void EditEntryWidget::openSelectedAttachments()
{ {
QModelIndex index = m_advancedUi->attachmentsView->currentIndex(); const QModelIndexList indexes = m_advancedUi->attachmentsView->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) {
return;
}
openAttachment(index); QStringList errors;
for (const QModelIndex &index: indexes) {
QString errorMessage;
if (!openAttachment(index, &errorMessage)) {
const QString filename = m_attachmentsModel->keyByIndex(index);
errors.append(QString("%1 - %2").arg(filename, errorMessage));
};
}
if (!errors.isEmpty()) {
showMessage(tr("Unable to open the attachments:\n").append(errors.join('\n')), MessageWidget::Error);
}
} }
void EditEntryWidget::removeCurrentAttachment() void EditEntryWidget::removeSelectedAttachments()
{ {
Q_ASSERT(!m_history); Q_ASSERT(!m_history);
QModelIndex index = m_advancedUi->attachmentsView->currentIndex(); const QModelIndexList indexes = m_advancedUi->attachmentsView->selectionModel()->selectedIndexes();
if (!index.isValid()) { if (indexes.isEmpty()) {
return; return;
} }
const QString question = tr("Are you sure you want to remove %n attachments?", "", indexes.count());
QMessageBox::StandardButton ans = MessageBox::question(this, tr("Confirm Remove"), QMessageBox::StandardButton ans = MessageBox::question(this, tr("Confirm Remove"),
tr("Are you sure you want to remove this attachment?"), question, QMessageBox::Yes | QMessageBox::No);
QMessageBox::Yes | QMessageBox::No);
if (ans == QMessageBox::Yes) { if (ans == QMessageBox::Yes) {
QString key = m_attachmentsModel->keyByIndex(index); QStringList keys;
m_entryAttachments->remove(key); for (const QModelIndex &index: indexes) {
keys.append(m_attachmentsModel->keyByIndex(index));
}
m_entryAttachments->remove(keys);
} }
} }

View File

@ -80,11 +80,12 @@ private slots:
void updateCurrentAttribute(); void updateCurrentAttribute();
void protectCurrentAttribute(bool state); void protectCurrentAttribute(bool state);
void revealCurrentAttribute(); void revealCurrentAttribute();
void insertAttachment(); void insertAttachments();
void saveCurrentAttachment(); void saveSelectedAttachment();
void saveSelectedAttachments();
void openAttachment(const QModelIndex& index); void openAttachment(const QModelIndex& index);
void openCurrentAttachment(); void openSelectedAttachments();
void removeCurrentAttachment(); void removeSelectedAttachments();
void updateAutoTypeEnabled(); void updateAutoTypeEnabled();
void insertAutoTypeAssoc(); void insertAutoTypeAssoc();
void removeAutoTypeAssoc(); void removeAutoTypeAssoc();
@ -117,6 +118,8 @@ private:
void displayAttribute(QModelIndex index, bool showProtected); void displayAttribute(QModelIndex index, bool showProtected);
bool openAttachment(const QModelIndex& index, QString *errorMessage);
Entry* m_entry; Entry* m_entry;
Database* m_database; Database* m_database;