Merge pull request #1298 from frostasm/improve-entry-attachments-view

Improve entry attachments view
This commit is contained in:
Janek Bevendorff 2017-12-25 14:12:15 +01:00 committed by GitHub
commit d7a83f1b83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 636 additions and 344 deletions

View File

@ -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

View File

@ -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()) {

View File

@ -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;

View File

@ -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);

View File

@ -21,12 +21,16 @@
#include <QDebug>
#include <QTimer>
#include <QDir>
#include <QDesktopServices>
#include <QTemporaryFile>
#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<int>(AttributesTab), m_attributesWidget, "Attributes");
m_ui->tabWidget->insertTab(static_cast<int>(AutotypeTab), m_autotypeWidget, "Autotype");
if (m_ui->tabWidget->count() < 5) {
m_ui->tabWidget->insertTab(static_cast<int>(AttributesTab), m_attributesTabWidget, tr("Attributes"));
m_ui->tabWidget->insertTab(static_cast<int>(AttachmentsTab), m_attachmentsTabWidget, tr("Attachments"));
m_ui->tabWidget->insertTab(static_cast<int>(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<QTreeWidgetItem*> 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);
}

View File

@ -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);

View File

@ -2,14 +2,6 @@
<ui version="4.0">
<class>DetailsWidget</class>
<widget class="QWidget" name="DetailsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>200</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,0,0,0">
@ -454,6 +446,16 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="attachmentsTab">
<attribute name="title">
<string>Attachments</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="EntryAttachmentsWidget" name="attachmentsWidget" native="true"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="notesTab">
<attribute name="title">
<string>Notes</string>
@ -533,6 +535,14 @@
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>EntryAttachmentsWidget</class>
<extends>QWidget</extends>
<header>gui/entry/EntryAttachmentsWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -29,6 +29,8 @@
#include <QMenu>
#include <QSortFilterProxyModel>
#include <QTemporaryFile>
#include <QMimeData>
#include <QEvent>
#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();

View File

@ -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;

View File

@ -2,14 +2,6 @@
<ui version="4.0">
<class>EditEntryWidgetAdvanced</class>
<widget class="QWidget" name="EditEntryWidgetAdvanced">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>366</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
@ -153,75 +145,27 @@
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QListView" name="attachmentsView">
<property name="flow">
<enum>QListView::LeftToRight</enum>
</property>
<property name="isWrapping" stdset="0">
<bool>true</bool>
<widget class="EntryAttachmentsWidget" name="attachmentsWidget" native="true">
<property name="minimumSize">
<size>
<width>100</width>
<height>100</height>
</size>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="attachmentsButtonLayout">
<item>
<widget class="QPushButton" name="addAttachmentButton">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeAttachmentButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="openAttachmentButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="saveAttachmentButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Save</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>EntryAttachmentsWidget</class>
<extends>QWidget</extends>
<header>gui/entry/EntryAttachmentsWidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>AttributesListView</class>
<extends>QListView</extends>
@ -234,11 +178,6 @@
<tabstop>addAttributeButton</tabstop>
<tabstop>removeAttributeButton</tabstop>
<tabstop>editAttributeButton</tabstop>
<tabstop>attachmentsView</tabstop>
<tabstop>addAttachmentButton</tabstop>
<tabstop>removeAttachmentButton</tabstop>
<tabstop>openAttachmentButton</tabstop>
<tabstop>saveAttachmentButton</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,353 @@
#include "EntryAttachmentsWidget.h"
#include "ui_EntryAttachmentsWidget.h"
#include <QDesktopServices>
#include <QDir>
#include <QDropEvent>
#include <QFile>
#include <QFileInfo>
#include <QMimeData>
#include <QTemporaryFile>
#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<QTemporaryFile> 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<QDropEvent*>(e);
const QMimeData* mimeData = dropEv->mimeData();
if (mimeData->hasUrls()) {
dropEv->acceptProposedAction();
return true;
}
} else if (eventType == QEvent::Drop) {
QDropEvent* dropEv = static_cast<QDropEvent*>(e);
const QMimeData* mimeData = dropEv->mimeData();
if (mimeData->hasUrls()) {
dropEv->acceptProposedAction();
QStringList filenames;
const QList<QUrl> 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);
}

View File

@ -0,0 +1,59 @@
#ifndef ENTRYATTACHMENTSWIDGET_H
#define ENTRYATTACHMENTSWIDGET_H
#include <QPointer>
#include <QWidget>
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<Ui::EntryAttachmentsWidget> m_ui;
QPointer<EntryAttachments> m_entryAttachments;
QPointer<EntryAttachmentsModel> m_attachmentsModel;
bool m_readOnly;
bool m_buttonsVisible;
};
#endif // ENTRYATTACHMENTSWIDGET_H

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EntryAttachmentsWidget</class>
<widget class="QWidget" name="EntryAttachmentsWidget">
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableView" name="attachmentsView"/>
</item>
<item>
<widget class="QWidget" name="actionsWidget" native="true">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="addAttachmentButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeAttachmentButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="openAttachmentButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="saveAttachmentButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Save</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>173</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -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");