diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index afa30056..216db839 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -130,6 +130,7 @@ set(keepassx_SOURCES gui/entry/EditEntryWidget.cpp gui/entry/EditEntryWidget_p.h gui/entry/EntryAttachmentsModel.cpp + gui/entry/EntryAttachmentsWidget.cpp gui/entry/EntryAttributesModel.cpp gui/entry/EntryHistoryModel.cpp gui/entry/EntryModel.cpp diff --git a/src/core/EntryAttachments.cpp b/src/core/EntryAttachments.cpp index bab11057..95755860 100644 --- a/src/core/EntryAttachments.cpp +++ b/src/core/EntryAttachments.cpp @@ -111,6 +111,11 @@ void EntryAttachments::remove(const QStringList& keys) } } +bool EntryAttachments::isEmpty() const +{ + return m_attachments.isEmpty(); +} + void EntryAttachments::clear() { if (m_attachments.isEmpty()) { diff --git a/src/core/EntryAttachments.h b/src/core/EntryAttachments.h index 8fa7c717..0dba9543 100644 --- a/src/core/EntryAttachments.h +++ b/src/core/EntryAttachments.h @@ -36,6 +36,7 @@ public: void set(const QString& key, const QByteArray& value); void remove(const QString& key); void remove(const QStringList& keys); + bool isEmpty() const; void clear(); void copyDataFrom(const EntryAttachments* other); bool operator==(const EntryAttachments& other) const; diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index b851839a..1ef91a4f 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -111,6 +111,9 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) "border-radius: 5px;"); m_detailsView = new DetailsWidget(this); + connect(m_detailsView, &DetailsWidget::errorOccurred, this, [this](const QString& error) { + showMessage(error, MessageWidget::MessageType::Error); + }); QVBoxLayout* vLayout = new QVBoxLayout(rightHandSideWidget); vLayout->setMargin(0); diff --git a/src/gui/DetailsWidget.cpp b/src/gui/DetailsWidget.cpp index 23c3485a..1d20984b 100644 --- a/src/gui/DetailsWidget.cpp +++ b/src/gui/DetailsWidget.cpp @@ -21,12 +21,16 @@ #include #include +#include +#include +#include #include "core/Config.h" #include "core/FilePath.h" #include "core/TimeInfo.h" #include "gui/Clipboard.h" #include "gui/DatabaseWidget.h" +#include "entry/EntryAttachmentsModel.h" DetailsWidget::DetailsWidget(QWidget* parent) : QWidget(parent) @@ -35,8 +39,9 @@ DetailsWidget::DetailsWidget(QWidget* parent) , m_currentEntry(nullptr) , m_currentGroup(nullptr) , m_timer(nullptr) - , m_attributesWidget(nullptr) - , m_autotypeWidget(nullptr) + , m_attributesTabWidget(nullptr) + , m_attachmentsTabWidget(nullptr) + , m_autotypeTabWidget(nullptr) , m_selectedTabEntry(0) , m_selectedTabGroup(0) { @@ -53,6 +58,13 @@ DetailsWidget::DetailsWidget(QWidget* parent) connect(m_ui->closeButton, SIGNAL(toggled(bool)), SLOT(hideDetails())); connect(m_ui->tabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndex(int))); + m_ui->attachmentsWidget->setReadOnly(true); + m_ui->attachmentsWidget->setButtonsVisible(false); + + m_attributesTabWidget = m_ui->tabWidget->widget(AttributesTab); + m_attachmentsTabWidget = m_ui->tabWidget->widget(AttachmentsTab); + m_autotypeTabWidget = m_ui->tabWidget->widget(AutotypeTab); + this->hide(); } @@ -75,9 +87,10 @@ void DetailsWidget::getSelectedEntry(Entry* selectedEntry) m_ui->stackedWidget->setCurrentIndex(EntryPreview); - if (m_ui->tabWidget->count() < 4) { - m_ui->tabWidget->insertTab(static_cast(AttributesTab), m_attributesWidget, "Attributes"); - m_ui->tabWidget->insertTab(static_cast(AutotypeTab), m_autotypeWidget, "Autotype"); + if (m_ui->tabWidget->count() < 5) { + m_ui->tabWidget->insertTab(static_cast(AttributesTab), m_attributesTabWidget, tr("Attributes")); + m_ui->tabWidget->insertTab(static_cast(AttachmentsTab), m_attachmentsTabWidget, tr("Attachments")); + m_ui->tabWidget->insertTab(static_cast(AutotypeTab), m_autotypeTabWidget, tr("Autotype")); } m_ui->tabWidget->setTabEnabled(AttributesTab, false); @@ -173,6 +186,10 @@ void DetailsWidget::getSelectedEntry(Entry* selectedEntry) m_ui->attributesEdit->setText(attributesText); } + const bool hasAttachments = !m_currentEntry->attachments()->isEmpty(); + m_ui->tabWidget->setTabEnabled(AttachmentsTab, hasAttachments); + m_ui->attachmentsWidget->setEntryAttachments(m_currentEntry->attachments()); + m_ui->autotypeTree->clear(); AutoTypeAssociations* autotypeAssociations = m_currentEntry->autoTypeAssociations(); QList items; @@ -209,9 +226,8 @@ void DetailsWidget::getSelectedGroup(Group* selectedGroup) m_ui->stackedWidget->setCurrentIndex(GroupPreview); if (m_ui->tabWidget->count() > 2) { - m_autotypeWidget = m_ui->tabWidget->widget(AutotypeTab); - m_attributesWidget = m_ui->tabWidget->widget(AttributesTab); m_ui->tabWidget->removeTab(AutotypeTab); + m_ui->tabWidget->removeTab(AttachmentsTab); m_ui->tabWidget->removeTab(AttributesTab); } diff --git a/src/gui/DetailsWidget.h b/src/gui/DetailsWidget.h index 780fe558..832839bb 100644 --- a/src/gui/DetailsWidget.h +++ b/src/gui/DetailsWidget.h @@ -44,10 +44,14 @@ public: GeneralTab = 0, AttributesTab = 1, GroupNotesTab = 1, - NotesTab = 2, - AutotypeTab = 3, + AttachmentsTab = 2, + NotesTab = 3, + AutotypeTab = 4, }; +signals: + void errorOccurred(const QString& error); + private slots: void getSelectedEntry(Entry* selectedEntry); void getSelectedGroup(Group* selectedGroup); @@ -64,8 +68,9 @@ private: Group* m_currentGroup; quint8 m_step; QTimer* m_timer; - QWidget* m_attributesWidget; - QWidget* m_autotypeWidget; + QWidget* m_attributesTabWidget; + QWidget* m_attachmentsTabWidget; + QWidget* m_autotypeTabWidget; quint8 m_selectedTabEntry; quint8 m_selectedTabGroup; QString shortUrl(QString url); diff --git a/src/gui/DetailsWidget.ui b/src/gui/DetailsWidget.ui index 3a0b2f2c..9c705569 100644 --- a/src/gui/DetailsWidget.ui +++ b/src/gui/DetailsWidget.ui @@ -2,14 +2,6 @@ DetailsWidget - - - 0 - 0 - 600 - 200 - - @@ -454,6 +446,16 @@ + + + Attachments + + + + + + + Notes @@ -533,6 +535,14 @@ + + + EntryAttachmentsWidget + QWidget +
gui/entry/EntryAttachmentsWidget.h
+ 1 +
+
diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index db35b2f8..b922f417 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -29,6 +29,8 @@ #include #include #include +#include +#include #include "core/Config.h" #include "core/Database.h" @@ -68,8 +70,6 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) , m_sshAgentWidget(new QWidget()) , m_editWidgetProperties(new EditWidgetProperties()) , m_historyWidget(new QWidget()) - , m_entryAttachments(new EntryAttachments(this)) - , m_attachmentsModel(new EntryAttachmentsModel(m_advancedWidget)) , m_entryAttributes(new EntryAttributes(this)) , m_attributesModel(new EntryAttributesModel(m_advancedWidget)) , m_historyModel(new EntryHistoryModel(this)) @@ -138,16 +138,12 @@ void EditEntryWidget::setupAdvanced() m_advancedUi->setupUi(m_advancedWidget); addPage(tr("Advanced"), FilePath::instance()->icon("categories", "preferences-other"), m_advancedWidget); - m_attachmentsModel->setEntryAttachments(m_entryAttachments); - m_advancedUi->attachmentsView->setModel(m_attachmentsModel); - m_advancedUi->attachmentsView->setSelectionMode(QAbstractItemView::ExtendedSelection); - connect(m_advancedUi->attachmentsView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), - SLOT(updateAttachmentButtonsEnabled(QModelIndex))); - connect(m_advancedUi->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(openAttachment(QModelIndex))); - connect(m_advancedUi->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveSelectedAttachments())); - connect(m_advancedUi->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments())); - connect(m_advancedUi->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachments())); - connect(m_advancedUi->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments())); + m_advancedUi->attachmentsWidget->setReadOnly(false); + m_advancedUi->attachmentsWidget->setButtonsVisible(true); + + connect(m_advancedUi->attachmentsWidget, &EntryAttachmentsWidget::errorOccurred, this, [this](const QString &error) { + showMessage(error, MessageWidget::Error); + }); m_attributesModel->setEntryAttributes(m_entryAttributes); m_advancedUi->attributesView->setModel(m_attributesModel); @@ -510,15 +506,6 @@ void EditEntryWidget::useExpiryPreset(QAction* action) m_mainUi->expireDatePicker->setDateTime(expiryDateTime); } -void EditEntryWidget::updateAttachmentButtonsEnabled(const QModelIndex& current) -{ - bool enable = current.isValid(); - - m_advancedUi->saveAttachmentButton->setEnabled(enable); - m_advancedUi->openAttachmentButton->setEnabled(enable); - m_advancedUi->removeAttachmentButton->setEnabled(enable && !m_history); -} - void EditEntryWidget::toggleHideNotes(bool visible) { m_mainUi->notesEdit->setVisible(visible); @@ -580,8 +567,8 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore) m_mainUi->togglePasswordGeneratorButton->setChecked(false); m_mainUi->togglePasswordGeneratorButton->setDisabled(m_history); m_mainUi->passwordGenerator->reset(); - m_advancedUi->addAttachmentButton->setEnabled(!m_history); - updateAttachmentButtonsEnabled(m_advancedUi->attachmentsView->currentIndex()); + + m_advancedUi->attachmentsWidget->setReadOnly(m_history); m_advancedUi->addAttributeButton->setEnabled(!m_history); m_advancedUi->editAttributeButton->setEnabled(false); m_advancedUi->removeAttributeButton->setEnabled(false); @@ -612,7 +599,7 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore) m_mainUi->notesEdit->setPlainText(entry->notes()); - m_entryAttachments->copyDataFrom(entry->attachments()); + m_advancedUi->attachmentsWidget->setEntryAttachments(entry->attachments()); m_entryAttributes->copyCustomKeysFrom(entry->attributes()); if (m_attributesModel->rowCount() != 0) { @@ -748,8 +735,8 @@ void EditEntryWidget::acceptEntry() void EditEntryWidget::updateEntryData(Entry* entry) const { entry->attributes()->copyCustomKeysFrom(m_entryAttributes); - entry->attachments()->copyDataFrom(m_entryAttachments); - + entry->attachments()->copyDataFrom(m_advancedUi->attachmentsWidget->entryAttachments()); + entry->setTitle(m_mainUi->titleEdit->text()); entry->setUsername(m_mainUi->usernameEdit->text()); entry->setUrl(m_mainUi->urlEdit->text()); @@ -806,7 +793,7 @@ void EditEntryWidget::clear() m_entry = nullptr; m_database = nullptr; m_entryAttributes->clear(); - m_entryAttachments->clear(); + m_advancedUi->attachmentsWidget->clearAttachments(); m_autoTypeAssoc->clear(); m_historyModel->clear(); m_iconsWidget->reset(); @@ -949,32 +936,6 @@ void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected) 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) { QModelIndex index = m_advancedUi->attributesView->currentIndex(); @@ -1005,181 +966,6 @@ void EditEntryWidget::revealCurrentAttribute() } } -void EditEntryWidget::insertAttachments() -{ - Q_ASSERT(!m_history); - - QString defaultDir = config()->get("LastAttachmentDir").toString(); - if (defaultDir.isEmpty() || !QDir(defaultDir).exists()) { - defaultDir = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).value(0); - } - - const QStringList filenames = fileDialog()->getOpenFileNames(this, tr("Select files"), defaultDir); - if (filenames.isEmpty()) { - return; - } - - config()->set("LastAttachmentDir", QFileInfo(filenames.first()).absolutePath()); - - QStringList errors; - 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); - } - - if (!errors.isEmpty()) { - showMessage(tr("Unable to open files:\n%1").arg(errors.join('\n')), MessageWidget::Error); - } -} - -void EditEntryWidget::saveSelectedAttachment() -{ - const QModelIndex index = m_advancedUi->attachmentsView->currentIndex(); - if (!index.isValid()) { - return; - } - - const QString filename = m_attachmentsModel->keyByIndex(index); - QString defaultDirName = config()->get("LastAttachmentDir").toString(); - if (defaultDirName.isEmpty() || !QDir(defaultDirName).exists()) { - defaultDirName = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); - } - - const QString savePath = fileDialog()->getSaveFileName(this, tr("Save attachment"), - QDir(defaultDirName).filePath(filename)); - if (!savePath.isEmpty()) { - config()->set("LastAttachmentDir", QFileInfo(savePath).absolutePath()); - - QFile file(savePath); - 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); - } - } -} - -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; - } - } - config()->set("LastAttachmentDir", QFileInfo(saveDir.absolutePath()).absolutePath()); - - 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); - } -} - -void EditEntryWidget::openAttachment(const QModelIndex& index) -{ - if (!index.isValid()) { - Q_ASSERT(false); - return; - } - - QString errorMessage; - if (!openAttachment(index, &errorMessage)) { - showMessage(errorMessage, MessageWidget::Error); - } -} - -void EditEntryWidget::openSelectedAttachments() -{ - const QModelIndexList indexes = m_advancedUi->attachmentsView->selectionModel()->selectedIndexes(); - if (indexes.isEmpty()) { - return; - } - - 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::removeSelectedAttachments() -{ - Q_ASSERT(!m_history); - - const QModelIndexList indexes = m_advancedUi->attachmentsView->selectionModel()->selectedIndexes(); - if (indexes.isEmpty()) { - 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"), - question, QMessageBox::Yes | QMessageBox::No); - if (ans == QMessageBox::Yes) { - QStringList keys; - for (const QModelIndex &index: indexes) { - keys.append(m_attachmentsModel->keyByIndex(index)); - } - m_entryAttachments->remove(keys); - } -} - void EditEntryWidget::updateAutoTypeEnabled() { bool autoTypeEnabled = m_autoTypeUi->enableButton->isChecked(); diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index 883e7e7f..b23f7b65 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -31,8 +31,6 @@ class Database; class EditWidgetIcons; class EditWidgetProperties; class Entry; -class EntryAttachments; -class EntryAttachmentsModel; class EntryAttributes; class EntryAttributesModel; class EntryHistoryModel; @@ -86,12 +84,6 @@ private slots: void updateCurrentAttribute(); void protectCurrentAttribute(bool state); void revealCurrentAttribute(); - void insertAttachments(); - void saveSelectedAttachment(); - void saveSelectedAttachments(); - void openAttachment(const QModelIndex& index); - void openSelectedAttachments(); - void removeSelectedAttachments(); void updateAutoTypeEnabled(); void insertAutoTypeAssoc(); void removeAutoTypeAssoc(); @@ -106,7 +98,6 @@ private slots: void histEntryActivated(const QModelIndex& index); void updateHistoryButtons(const QModelIndex& current, const QModelIndex& previous); void useExpiryPreset(QAction* action); - void updateAttachmentButtonsEnabled(const QModelIndex& current); void toggleHideNotes(bool visible); #ifdef WITH_XC_SSHAGENT void updateSSHAgent(); @@ -140,8 +131,6 @@ private: void displayAttribute(QModelIndex index, bool showProtected); - bool openAttachment(const QModelIndex& index, QString *errorMessage); - Entry* m_entry; Database* m_database; @@ -164,8 +153,6 @@ private: QWidget* const m_sshAgentWidget; EditWidgetProperties* const m_editWidgetProperties; QWidget* const m_historyWidget; - EntryAttachments* const m_entryAttachments; - EntryAttachmentsModel* const m_attachmentsModel; EntryAttributes* const m_entryAttributes; EntryAttributesModel* const m_attributesModel; EntryHistoryModel* const m_historyModel; diff --git a/src/gui/entry/EditEntryWidgetAdvanced.ui b/src/gui/entry/EditEntryWidgetAdvanced.ui index 2c7f95dd..8c729fd7 100644 --- a/src/gui/entry/EditEntryWidgetAdvanced.ui +++ b/src/gui/entry/EditEntryWidgetAdvanced.ui @@ -2,14 +2,6 @@ EditEntryWidgetAdvanced - - - 0 - 0 - 400 - 366 - - 0 @@ -153,75 +145,27 @@ - - - QListView::LeftToRight - - - true + + + + 100 + 100 + - - - - - - Add - - - - - - - false - - - Remove - - - - - - - false - - - Open - - - - - - - false - - - Save - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - + + EntryAttachmentsWidget + QWidget +
gui/entry/EntryAttachmentsWidget.h
+ 1 +
AttributesListView QListView @@ -234,11 +178,6 @@ addAttributeButton removeAttributeButton editAttributeButton - attachmentsView - addAttachmentButton - removeAttachmentButton - openAttachmentButton - saveAttachmentButton diff --git a/src/gui/entry/EntryAttachmentsModel.cpp b/src/gui/entry/EntryAttachmentsModel.cpp index 08264138..60874484 100644 --- a/src/gui/entry/EntryAttachmentsModel.cpp +++ b/src/gui/entry/EntryAttachmentsModel.cpp @@ -26,6 +26,8 @@ EntryAttachmentsModel::EntryAttachmentsModel(QObject* parent) : QAbstractListModel(parent) , m_entryAttachments(nullptr) { + m_headers << tr("Name") + << tr("Size"); } void EntryAttachmentsModel::setEntryAttachments(EntryAttachments* entryAttachments) @@ -65,7 +67,17 @@ int EntryAttachmentsModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); - return 1; + return Columns::ColumnsCount; +} + +QVariant EntryAttachmentsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { + Q_ASSERT(m_headers.size() == columnCount()); + return m_headers[section]; + } + + return QAbstractListModel::headerData(section, orientation, role); } QVariant EntryAttachmentsModel::data(const QModelIndex& index, int role) const @@ -74,15 +86,21 @@ QVariant EntryAttachmentsModel::data(const QModelIndex& index, int role) const return QVariant(); } - if (role == Qt::DisplayRole && index.column() == 0) { - QString key = keyByIndex(index); + if (role == Qt::DisplayRole || role == Qt::EditRole) { + const QString key = keyByIndex(index); + const int column = index.column(); + if (column == Columns::NameColumn) { + return key; + } else if (column == SizeColumn) { + const int attachmentSize = m_entryAttachments->value(key).size(); + if (role == Qt::DisplayRole) { + return Tools::humanReadableFileSize(attachmentSize); + } + return attachmentSize; + } + } - return QString("%1 (%2)").arg(key, - Tools::humanReadableFileSize(m_entryAttachments->value(key).size())); - } - else { - return QVariant(); - } + return QVariant(); } QString EntryAttachmentsModel::keyByIndex(const QModelIndex& index) const diff --git a/src/gui/entry/EntryAttachmentsModel.h b/src/gui/entry/EntryAttachmentsModel.h index 6abcdc2e..fa8d3554 100644 --- a/src/gui/entry/EntryAttachmentsModel.h +++ b/src/gui/entry/EntryAttachmentsModel.h @@ -27,10 +27,17 @@ class EntryAttachmentsModel : public QAbstractListModel Q_OBJECT public: + enum Columns { + NameColumn, + SizeColumn, + ColumnsCount + }; + explicit EntryAttachmentsModel(QObject* parent = nullptr); void setEntryAttachments(EntryAttachments* entry); int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QString keyByIndex(const QModelIndex& index) const; @@ -45,6 +52,7 @@ private slots: private: EntryAttachments* m_entryAttachments; + QStringList m_headers; }; #endif // KEEPASSX_ENTRYATTACHMENTSMODEL_H diff --git a/src/gui/entry/EntryAttachmentsWidget.cpp b/src/gui/entry/EntryAttachmentsWidget.cpp new file mode 100644 index 00000000..93b73a16 --- /dev/null +++ b/src/gui/entry/EntryAttachmentsWidget.cpp @@ -0,0 +1,353 @@ +#include "EntryAttachmentsWidget.h" +#include "ui_EntryAttachmentsWidget.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "EntryAttachmentsModel.h" +#include "core/Config.h" +#include "core/EntryAttachments.h" +#include "core/Tools.h" +#include "gui/FileDialog.h" +#include "gui/MessageBox.h" + + +EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent) : + QWidget(parent) + , m_ui(new Ui::EntryAttachmentsWidget) + , m_entryAttachments(new EntryAttachments(this)) + , m_attachmentsModel(new EntryAttachmentsModel(this)) + , m_readOnly(false) + , m_buttonsVisible(true) +{ + m_ui->setupUi(this); + + m_ui->attachmentsView->setAcceptDrops(false); + m_ui->attachmentsView->viewport()->setAcceptDrops(true); + m_ui->attachmentsView->viewport()->installEventFilter(this); + + m_attachmentsModel->setEntryAttachments(m_entryAttachments); + m_ui->attachmentsView->setModel(m_attachmentsModel); + m_ui->attachmentsView->verticalHeader()->hide(); + m_ui->attachmentsView->horizontalHeader()->setStretchLastSection(true); + m_ui->attachmentsView->horizontalHeader()->resizeSection(EntryAttachmentsModel::NameColumn, 400); + m_ui->attachmentsView->setSelectionBehavior(QAbstractItemView::SelectRows); + m_ui->attachmentsView->setSelectionMode(QAbstractItemView::ExtendedSelection); + + m_ui->actionsWidget->setVisible(m_buttonsVisible); + connect(this, SIGNAL(buttonsVisibleChanged(bool)), m_ui->actionsWidget, SLOT(setVisible(bool))); + + connect(this, SIGNAL(readOnlyChanged(bool)), SLOT(updateButtonsEnabled())); + connect(m_attachmentsModel, SIGNAL(modelReset()), SLOT(updateButtonsEnabled())); + connect(m_ui->attachmentsView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), + SLOT(updateButtonsEnabled())); + + connect(m_ui->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(openAttachment(QModelIndex))); + connect(m_ui->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveSelectedAttachments())); + connect(m_ui->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments())); + connect(m_ui->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachments())); + connect(m_ui->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments())); + + updateButtonsEnabled(); +} + +EntryAttachmentsWidget::~EntryAttachmentsWidget() +{ +} + +const EntryAttachments* EntryAttachmentsWidget::entryAttachments() const +{ + return m_entryAttachments; +} + +bool EntryAttachmentsWidget::isReadOnly() const +{ + return m_readOnly; +} + +bool EntryAttachmentsWidget::isButtonsVisible() const +{ + return m_buttonsVisible; +} + +void EntryAttachmentsWidget::setEntryAttachments(const EntryAttachments* attachments) +{ + Q_ASSERT(attachments != nullptr); + m_entryAttachments->copyDataFrom(attachments); +} + +void EntryAttachmentsWidget::clearAttachments() +{ + m_entryAttachments->clear(); +} + +void EntryAttachmentsWidget::setReadOnly(bool readOnly) +{ + if (m_readOnly == readOnly) { + return; + } + + m_readOnly = readOnly; + emit readOnlyChanged(m_readOnly); +} + +void EntryAttachmentsWidget::setButtonsVisible(bool buttonsVisible) +{ + if (m_buttonsVisible == buttonsVisible) { + return; + } + + m_buttonsVisible = buttonsVisible; + emit buttonsVisibleChanged(m_buttonsVisible); +} + +void EntryAttachmentsWidget::insertAttachments() +{ + Q_ASSERT(!isReadOnly()); + if (isReadOnly()) { + return; + } + + QString defaultDirPath = config()->get("LastAttachmentDir").toString(); + const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); + if (!dirExists) { + defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); + } + + const QStringList filenames = fileDialog()->getOpenFileNames(this, tr("Select files"), defaultDirPath); + if (filenames.isEmpty()) { + return; + } + + config()->set("LastAttachmentDir", QFileInfo(filenames.first()).absolutePath()); + + QString errorMessage; + if (!insertAttachments(filenames, errorMessage)) { + errorOccurred(errorMessage); + } +} + +void EntryAttachmentsWidget::removeSelectedAttachments() +{ + Q_ASSERT(!isReadOnly()); + if (isReadOnly()) { + return; + } + + const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0); + if (indexes.isEmpty()) { + return; + } + + const QString question = tr("Are you sure you want to remove %n attachment(s)?", "", indexes.count()); + QMessageBox::StandardButton answer = MessageBox::question(this, tr("Confirm Remove"), + question, QMessageBox::Yes | QMessageBox::No); + if (answer == QMessageBox::Yes) { + QStringList keys; + for (const QModelIndex& index: indexes) { + keys.append(m_attachmentsModel->keyByIndex(index)); + } + m_entryAttachments->remove(keys); + } +} + +void EntryAttachmentsWidget::saveSelectedAttachments() +{ + const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0); + if (indexes.isEmpty()) { + return; + } + + QString defaultDirPath = config()->get("LastAttachmentDir").toString(); + const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); + if (!dirExists) { + defaultDirPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + } + + const QString saveDirPath = fileDialog()->getExistingDirectory(this, tr("Save attachments"), defaultDirPath); + if (saveDirPath.isEmpty()) { + return; + } + + QDir saveDir(saveDirPath); + if (!saveDir.exists()) { + if (saveDir.mkpath(saveDir.absolutePath())) { + errorOccurred(tr("Unable to create directory:\n%1").arg(saveDir.absolutePath())); + return; + } + } + config()->set("LastAttachmentDir", QFileInfo(saveDir.absolutePath()).absolutePath()); + + 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 the existing file \"%1\" with the attachment?")); + auto answer = MessageBox::question(this, tr("Confirm overwrite"), question.arg(filename), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (answer == QMessageBox::No) { + continue; + } else if (answer == 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()) { + errorOccurred(tr("Unable to save attachments:\n%1").arg(errors.join('\n'))); + } +} + +void EntryAttachmentsWidget::openAttachment(const QModelIndex& index) +{ + Q_ASSERT(index.isValid()); + if (!index.isValid()) { + return; + } + + QString errorMessage; + if (!openAttachment(index, errorMessage)) { + errorOccurred(tr("Unable to open attachment:\n%1").arg(errorMessage)); + } +} + +void EntryAttachmentsWidget::openSelectedAttachments() +{ + const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0); + if (indexes.isEmpty()) { + return; + } + + 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()) { + errorOccurred(tr("Unable to open attachments:\n%1").arg(errors.join('\n'))); + } +} + +void EntryAttachmentsWidget::updateButtonsEnabled() +{ + const bool hasSelection = m_ui->attachmentsView->selectionModel()->hasSelection(); + + m_ui->addAttachmentButton->setEnabled(!m_readOnly); + m_ui->removeAttachmentButton->setEnabled(hasSelection && !m_readOnly); + + m_ui->saveAttachmentButton->setEnabled(hasSelection); + m_ui->openAttachmentButton->setEnabled(hasSelection); +} + +bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QString& errorMessage) +{ + Q_ASSERT(!isReadOnly()); + if (isReadOnly()) { + return false; + } + + QStringList errors; + for (const QString &filename: filenames) { + QByteArray data; + QFile file(filename); + const QFileInfo fInfo(filename); + const bool readOk = file.open(QIODevice::ReadOnly) && Tools::readAllFromDevice(&file, data); + if (readOk) { + m_entryAttachments->set(fInfo.fileName(), data); + } else { + errors.append(QString("%1 - %2").arg(fInfo.fileName(), file.errorString())); + } + } + + if (!errors.isEmpty()) { + errorMessage = tr("Unable to open files:\n%1").arg(errors.join('\n')); + } + + return errors.isEmpty(); +} + +bool EntryAttachmentsWidget::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)); + + QScopedPointer tmpFile(new QTemporaryFile(tmpFileTemplate, this)); + + const bool saveOk = tmpFile->open() + && tmpFile->write(attachmentData) == attachmentData.size() + && tmpFile->flush(); + if (!saveOk) { + errorMessage = QString("%1 - %2").arg(filename, tmpFile->errorString()); + return false; + } + + tmpFile->close(); + const bool openOk = QDesktopServices::openUrl(QUrl::fromLocalFile(tmpFile->fileName())); + if (!openOk) { + errorMessage = QString("Can't open file \"%1\"").arg(filename); + return false; + } + + // take ownership of the tmpFile pointer + tmpFile.take(); + return true; +} + +bool EntryAttachmentsWidget::eventFilter(QObject* watched, QEvent* e) +{ + if (watched == m_ui->attachmentsView->viewport() && !isReadOnly()) { + const QEvent::Type eventType = e->type(); + if (eventType == QEvent::DragEnter || eventType == QEvent::DragMove) { + QDropEvent* dropEv = static_cast(e); + const QMimeData* mimeData = dropEv->mimeData(); + if (mimeData->hasUrls()) { + dropEv->acceptProposedAction(); + return true; + } + } else if (eventType == QEvent::Drop) { + QDropEvent* dropEv = static_cast(e); + const QMimeData* mimeData = dropEv->mimeData(); + if (mimeData->hasUrls()) { + dropEv->acceptProposedAction(); + QStringList filenames; + const QList urls = mimeData->urls(); + for (const QUrl& url: urls) { + const QFileInfo fInfo(url.toLocalFile()); + if (fInfo.isFile()) { + filenames.append(fInfo.absoluteFilePath()); + } + } + + QString errorMessage; + if (!insertAttachments(filenames, errorMessage)) { + errorOccurred(errorMessage); + } + + return true; + } + } + } + + return QWidget::eventFilter(watched, e); +} diff --git a/src/gui/entry/EntryAttachmentsWidget.h b/src/gui/entry/EntryAttachmentsWidget.h new file mode 100644 index 00000000..41a54d47 --- /dev/null +++ b/src/gui/entry/EntryAttachmentsWidget.h @@ -0,0 +1,59 @@ +#ifndef ENTRYATTACHMENTSWIDGET_H +#define ENTRYATTACHMENTSWIDGET_H + +#include +#include + +namespace Ui { +class EntryAttachmentsWidget; +} + +class EntryAttachments; +class EntryAttachmentsModel; + +class EntryAttachmentsWidget : public QWidget +{ + Q_OBJECT + Q_PROPERTY(bool readOnly READ isReadOnly WRITE setReadOnly NOTIFY readOnlyChanged) + Q_PROPERTY(bool isButtonsVisible READ isButtonsVisible WRITE setButtonsVisible NOTIFY buttonsVisibleChanged) +public: + explicit EntryAttachmentsWidget(QWidget* parent = nullptr); + ~EntryAttachmentsWidget(); + + const EntryAttachments* entryAttachments() const; + bool isReadOnly() const; + bool isButtonsVisible() const; + +public slots: + void setEntryAttachments(const EntryAttachments* attachments); + void clearAttachments(); + void setReadOnly(bool readOnly); + void setButtonsVisible(bool isButtonsVisible); + +signals: + void errorOccurred(const QString& error); + void readOnlyChanged(bool readOnly); + void buttonsVisibleChanged(bool isButtonsVisible); + +private slots: + void insertAttachments(); + void removeSelectedAttachments(); + void saveSelectedAttachments(); + void openAttachment(const QModelIndex& index); + void openSelectedAttachments(); + void updateButtonsEnabled(); + +private: + bool insertAttachments(const QStringList& fileNames, QString& errorMessage); + bool openAttachment(const QModelIndex& index, QString& errorMessage); + + bool eventFilter(QObject* watched, QEvent* event) override; + + QScopedPointer m_ui; + QPointer m_entryAttachments; + QPointer m_attachmentsModel; + bool m_readOnly; + bool m_buttonsVisible; +}; + +#endif // ENTRYATTACHMENTSWIDGET_H diff --git a/src/gui/entry/EntryAttachmentsWidget.ui b/src/gui/entry/EntryAttachmentsWidget.ui new file mode 100644 index 00000000..60292309 --- /dev/null +++ b/src/gui/entry/EntryAttachmentsWidget.ui @@ -0,0 +1,99 @@ + + + EntryAttachmentsWidget + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + false + + + Add + + + + + + + false + + + Remove + + + + + + + false + + + Open + + + + + + + false + + + Save + + + + + + + Qt::Vertical + + + + 20 + 173 + + + + + + + + + + + + diff --git a/tests/TestEntryModel.cpp b/tests/TestEntryModel.cpp index e0c8bb49..ad7c9060 100644 --- a/tests/TestEntryModel.cpp +++ b/tests/TestEntryModel.cpp @@ -122,9 +122,11 @@ void TestEntryModel::testAttachmentsModel() entryAttachments->set("first", QByteArray("123")); entryAttachments->set("2nd", QByteArray("456")); - entryAttachments->set("2nd", QByteArray("789")); + entryAttachments->set("2nd", QByteArray("7890")); - QCOMPARE(model->data(model->index(0, 0)).toString().left(4), QString("2nd ")); + const int firstRow = 0; + QCOMPARE(model->data(model->index(firstRow, EntryAttachmentsModel::NameColumn)).toString(), QString("2nd")); + QCOMPARE(model->data(model->index(firstRow, EntryAttachmentsModel::SizeColumn), Qt::EditRole).toInt(), 4); entryAttachments->remove("first");