mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-03-13 01:26:37 -04:00
Add New/Preview Entry Attachments dialog and functionality (#11637)
Closes #11506 Closes #3383 * This change adds a new opportunity to add attachments that don’t require a real file in the file system. * Add a new dialog window to add and preview attachments and integrate it into the EntryAttachmentsWidget. * Attachment preview support for images and plain text files. Additional enhancements: * Fix sizing of attachment columns * Add padding to attachment table items * Fix targeting of preview widget styling to not impact unintended children
This commit is contained in:
parent
ef2b5e7c26
commit
99c8936568
@ -3697,6 +3697,21 @@ This may cause the affected plugins to malfunction.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EntryAttachmentsDialog</name>
|
||||
<message>
|
||||
<source>Form</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>File name</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>File contents...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EntryAttachmentsModel</name>
|
||||
<message>
|
||||
@ -3734,14 +3749,6 @@ This may cause the affected plugins to malfunction.</source>
|
||||
<source>Remove</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Rename selected attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Rename</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Open selected attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@ -3851,6 +3858,18 @@ Error: %1</source>
|
||||
Would you like to overwrite the existing attachment?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>New</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Preview</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to preview an attachment: Attachment not found</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EntryAttributesModel</name>
|
||||
@ -6039,6 +6058,25 @@ We recommend you use the AppImage available on our downloads page.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>NewEntryAttachmentsDialog</name>
|
||||
<message>
|
||||
<source>Attachment name cannot be empty</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Attachment with the same name already exists</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Save attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>New entry attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>NixUtils</name>
|
||||
<message>
|
||||
@ -6785,6 +6823,21 @@ Do you want to overwrite it?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PreviewEntryAttachmentsDialog</name>
|
||||
<message>
|
||||
<source>Preview entry attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No preview available</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Image format not supported</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QMessageBox</name>
|
||||
<message>
|
||||
|
@ -130,6 +130,8 @@ set(keepassx_SOURCES
|
||||
gui/entry/EntryAttachmentsModel.cpp
|
||||
gui/entry/EntryAttachmentsWidget.cpp
|
||||
gui/entry/EntryAttributesModel.cpp
|
||||
gui/entry/NewEntryAttachmentsDialog.cpp
|
||||
gui/entry/PreviewEntryAttachmentsDialog.cpp
|
||||
gui/entry/EntryHistoryModel.cpp
|
||||
gui/entry/EntryModel.cpp
|
||||
gui/entry/EntryView.cpp
|
||||
|
@ -477,4 +477,32 @@ namespace Tools
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
MimeType toMimeType(const QString& mimeName)
|
||||
{
|
||||
static QStringList textFormats = {
|
||||
"text/",
|
||||
"application/json",
|
||||
"application/xml",
|
||||
"application/soap+xml",
|
||||
"application/x-yaml",
|
||||
"application/protobuf",
|
||||
};
|
||||
static QStringList imageFormats = {"image/"};
|
||||
|
||||
static auto isCompatible = [](const QString& format, const QStringList& list) {
|
||||
return std::any_of(
|
||||
list.cbegin(), list.cend(), [&format](const auto& item) { return format.startsWith(item); });
|
||||
};
|
||||
|
||||
if (isCompatible(mimeName, imageFormats)) {
|
||||
return MimeType::Image;
|
||||
}
|
||||
|
||||
if (isCompatible(mimeName, textFormats)) {
|
||||
return MimeType::PlainText;
|
||||
}
|
||||
|
||||
return MimeType::Unknown;
|
||||
}
|
||||
} // namespace Tools
|
||||
|
@ -118,6 +118,15 @@ namespace Tools
|
||||
QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties = {"objectName"});
|
||||
|
||||
QString substituteBackupFilePath(QString pattern, const QString& databasePath);
|
||||
|
||||
enum class MimeType : uint8_t
|
||||
{
|
||||
Image,
|
||||
PlainText,
|
||||
Unknown
|
||||
};
|
||||
|
||||
MimeType toMimeType(const QString& mimeName);
|
||||
} // namespace Tools
|
||||
|
||||
#endif // KEEPASSX_TOOLS_H
|
||||
|
@ -288,6 +288,9 @@
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
<property name="blendIn" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
@ -325,6 +328,9 @@
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextBrowserInteraction</set>
|
||||
</property>
|
||||
<property name="blendIn" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
@ -409,6 +415,9 @@
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="blendIn" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
@ -482,6 +491,9 @@
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="blendIn" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
@ -494,6 +506,9 @@
|
||||
<property name="accessibleName">
|
||||
<string>Tags list</string>
|
||||
</property>
|
||||
<property name="blendIn" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="5">
|
||||
@ -516,6 +531,9 @@
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
<property name="blendIn" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
@ -1109,6 +1127,9 @@
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="blendIn" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
55
src/gui/entry/EntryAttachmentsDialog.ui
Normal file
55
src/gui/entry/EntryAttachmentsDialog.ui
Normal file
@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>EntryAttachmentsDialog</class>
|
||||
<widget class="QDialog" name="EntryAttachmentsDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>402</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="titleEdit">
|
||||
<property name="placeholderText">
|
||||
<string>File name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="errorLabel">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #FF9696</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="attachmentTextEdit">
|
||||
<property name="placeholderText">
|
||||
<string>File contents...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="dialogButtons">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -16,16 +16,19 @@
|
||||
*/
|
||||
|
||||
#include "EntryAttachmentsWidget.h"
|
||||
|
||||
#include "EntryAttachmentsModel.h"
|
||||
#include "NewEntryAttachmentsDialog.h"
|
||||
#include "PreviewEntryAttachmentsDialog.h"
|
||||
#include "ui_EntryAttachmentsWidget.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
#include <QDropEvent>
|
||||
#include <QMimeData>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
#include "EntryAttachmentsModel.h"
|
||||
#include "core/Config.h"
|
||||
#include "core/EntryAttachments.h"
|
||||
#include "core/Tools.h"
|
||||
#include "gui/FileDialog.h"
|
||||
@ -46,12 +49,12 @@ EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
|
||||
m_ui->attachmentsView->viewport()->installEventFilter(this);
|
||||
|
||||
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->attachmentsView->setEditTriggers(QAbstractItemView::SelectedClicked);
|
||||
m_ui->attachmentsView->horizontalHeader()->setMinimumSectionSize(70);
|
||||
m_ui->attachmentsView->horizontalHeader()->setSectionResizeMode(EntryAttachmentsModel::NameColumn,
|
||||
QHeaderView::Stretch);
|
||||
m_ui->attachmentsView->horizontalHeader()->setSectionResizeMode(EntryAttachmentsModel::SizeColumn,
|
||||
QHeaderView::ResizeToContents);
|
||||
m_ui->attachmentsView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
|
||||
connect(this, SIGNAL(buttonsVisibleChanged(bool)), this, SLOT(updateButtonsVisible()));
|
||||
connect(this, SIGNAL(readOnlyChanged(bool)), SLOT(updateButtonsEnabled()));
|
||||
@ -64,12 +67,13 @@ EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
|
||||
// clang-format on
|
||||
connect(this, SIGNAL(readOnlyChanged(bool)), m_attachmentsModel, SLOT(setReadOnly(bool)));
|
||||
|
||||
connect(m_ui->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(openAttachment(QModelIndex)));
|
||||
connect(m_ui->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(previewSelectedAttachment()));
|
||||
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->newAttachmentButton, SIGNAL(clicked()), SLOT(newAttachments()));
|
||||
connect(m_ui->previewAttachmentButton, SIGNAL(clicked()), SLOT(previewSelectedAttachment()));
|
||||
connect(m_ui->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments()));
|
||||
connect(m_ui->renameAttachmentButton, SIGNAL(clicked()), SLOT(renameSelectedAttachments()));
|
||||
|
||||
updateButtonsVisible();
|
||||
updateButtonsEnabled();
|
||||
@ -165,6 +169,57 @@ void EntryAttachmentsWidget::insertAttachments()
|
||||
emit widgetUpdated();
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::newAttachments()
|
||||
{
|
||||
Q_ASSERT(m_entryAttachments);
|
||||
Q_ASSERT(!isReadOnly());
|
||||
if (isReadOnly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
NewEntryAttachmentsDialog newEntryDialog(m_entryAttachments, this);
|
||||
if (newEntryDialog.exec() == QDialog::Accepted) {
|
||||
emit widgetUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::previewSelectedAttachment()
|
||||
{
|
||||
Q_ASSERT(m_entryAttachments);
|
||||
|
||||
const auto index = m_ui->attachmentsView->selectionModel()->selectedIndexes().first();
|
||||
if (!index.isValid()) {
|
||||
qWarning() << tr("Failed to preview an attachment: Attachment not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set selection to the first
|
||||
m_ui->attachmentsView->setCurrentIndex(index);
|
||||
|
||||
auto name = m_attachmentsModel->keyByIndex(index);
|
||||
auto data = m_entryAttachments->value(name);
|
||||
|
||||
PreviewEntryAttachmentsDialog previewDialog(this);
|
||||
previewDialog.setAttachment(name, data);
|
||||
|
||||
connect(&previewDialog, SIGNAL(openAttachment(QString)), SLOT(openSelectedAttachments()));
|
||||
connect(&previewDialog, SIGNAL(saveAttachment(QString)), SLOT(saveSelectedAttachments()));
|
||||
// Refresh the preview if the attachment changes
|
||||
connect(m_entryAttachments,
|
||||
&EntryAttachments::keyModified,
|
||||
&previewDialog,
|
||||
[&previewDialog, &name, this](const QString& key) {
|
||||
if (key == name) {
|
||||
previewDialog.setAttachment(name, m_entryAttachments->value(name));
|
||||
}
|
||||
});
|
||||
|
||||
previewDialog.exec();
|
||||
|
||||
// Set focus back to the widget to allow keyboard navigation
|
||||
setFocus();
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::removeSelectedAttachments()
|
||||
{
|
||||
Q_ASSERT(m_entryAttachments);
|
||||
@ -194,12 +249,6 @@ void EntryAttachmentsWidget::removeSelectedAttachments()
|
||||
}
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::renameSelectedAttachments()
|
||||
{
|
||||
Q_ASSERT(m_entryAttachments);
|
||||
m_ui->attachmentsView->edit(m_ui->attachmentsView->selectionModel()->selectedIndexes().first());
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::saveSelectedAttachments()
|
||||
{
|
||||
Q_ASSERT(m_entryAttachments);
|
||||
@ -289,7 +338,7 @@ void EntryAttachmentsWidget::openSelectedAttachments()
|
||||
if (!m_entryAttachments->openAttachment(m_attachmentsModel->keyByIndex(index), &errorMessage)) {
|
||||
const QString filename = m_attachmentsModel->keyByIndex(index);
|
||||
errors.append(QString("%1 - %2").arg(filename, errorMessage));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
@ -302,18 +351,32 @@ void EntryAttachmentsWidget::updateButtonsEnabled()
|
||||
const bool hasSelection = m_ui->attachmentsView->selectionModel()->hasSelection();
|
||||
|
||||
m_ui->addAttachmentButton->setEnabled(!m_readOnly);
|
||||
m_ui->newAttachmentButton->setEnabled(!m_readOnly);
|
||||
m_ui->removeAttachmentButton->setEnabled(hasSelection && !m_readOnly);
|
||||
m_ui->renameAttachmentButton->setEnabled(hasSelection && !m_readOnly);
|
||||
|
||||
m_ui->saveAttachmentButton->setEnabled(hasSelection);
|
||||
m_ui->previewAttachmentButton->setEnabled(hasSelection);
|
||||
m_ui->openAttachmentButton->setEnabled(hasSelection);
|
||||
|
||||
updateSpacers();
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::updateSpacers()
|
||||
{
|
||||
if (m_buttonsVisible && !m_readOnly) {
|
||||
m_ui->previewVSpacer->changeSize(20, 40, QSizePolicy::Fixed, QSizePolicy::Expanding);
|
||||
} else {
|
||||
m_ui->previewVSpacer->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
}
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::updateButtonsVisible()
|
||||
{
|
||||
m_ui->addAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
||||
m_ui->newAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
||||
m_ui->removeAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
||||
m_ui->renameAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
||||
|
||||
updateSpacers();
|
||||
}
|
||||
|
||||
bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QString& errorMessage)
|
||||
|
@ -57,8 +57,9 @@ signals:
|
||||
|
||||
private slots:
|
||||
void insertAttachments();
|
||||
void newAttachments();
|
||||
void previewSelectedAttachment();
|
||||
void removeSelectedAttachments();
|
||||
void renameSelectedAttachments();
|
||||
void saveSelectedAttachments();
|
||||
void openAttachment(const QModelIndex& index);
|
||||
void openSelectedAttachments();
|
||||
@ -67,6 +68,8 @@ private slots:
|
||||
void attachmentModifiedExternally(const QString& key, const QString& filePath);
|
||||
|
||||
private:
|
||||
void updateSpacers();
|
||||
|
||||
bool insertAttachments(const QStringList& fileNames, QString& errorMessage);
|
||||
|
||||
QStringList confirmAttachmentSelection(const QStringList& filenames);
|
||||
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>337</width>
|
||||
<height>289</height>
|
||||
<height>258</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -34,11 +34,20 @@
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="cornerButtonEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="actionsWidget" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1,0,0,0,1,0,0">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
@ -51,6 +60,16 @@
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="newAttachmentButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>New</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="addAttachmentButton">
|
||||
<property name="enabled">
|
||||
@ -65,28 +84,25 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removeAttachmentButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
<spacer name="previewVSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string>Remove selected attachment</string>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove</string>
|
||||
</property>
|
||||
</widget>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="renameAttachmentButton">
|
||||
<widget class="QPushButton" name="previewAttachmentButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string>Rename selected attachment</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Rename</string>
|
||||
<string>Preview</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -129,6 +145,35 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removeAttachmentButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string>Remove selected attachment</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
92
src/gui/entry/NewEntryAttachmentsDialog.cpp
Normal file
92
src/gui/entry/NewEntryAttachmentsDialog.cpp
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 2 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "NewEntryAttachmentsDialog.h"
|
||||
#include "core/EntryAttachments.h"
|
||||
#include "ui_EntryAttachmentsDialog.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
|
||||
NewEntryAttachmentsDialog::NewEntryAttachmentsDialog(QPointer<EntryAttachments> attachments, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_attachments(std::move(attachments))
|
||||
, m_ui(new Ui::EntryAttachmentsDialog)
|
||||
{
|
||||
Q_ASSERT(m_attachments);
|
||||
|
||||
m_ui->setupUi(this);
|
||||
|
||||
setWindowTitle(tr("New entry attachment"));
|
||||
|
||||
m_ui->dialogButtons->clear();
|
||||
m_ui->dialogButtons->addButton(QDialogButtonBox::Ok);
|
||||
m_ui->dialogButtons->addButton(QDialogButtonBox::Cancel);
|
||||
|
||||
connect(m_ui->dialogButtons, SIGNAL(accepted()), this, SLOT(saveAttachment()));
|
||||
connect(m_ui->dialogButtons, SIGNAL(rejected()), this, SLOT(reject()));
|
||||
connect(m_ui->titleEdit, SIGNAL(textChanged(const QString&)), this, SLOT(fileNameTextChanged(const QString&)));
|
||||
|
||||
fileNameTextChanged(m_ui->titleEdit->text());
|
||||
}
|
||||
|
||||
NewEntryAttachmentsDialog::~NewEntryAttachmentsDialog() = default;
|
||||
|
||||
bool NewEntryAttachmentsDialog::validateFileName(const QString& fileName, QString& error) const
|
||||
{
|
||||
if (fileName.isEmpty()) {
|
||||
error = tr("Attachment name cannot be empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_attachments->hasKey(fileName)) {
|
||||
error = tr("Attachment with the same name already exists");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void NewEntryAttachmentsDialog::saveAttachment()
|
||||
{
|
||||
auto fileName = m_ui->titleEdit->text();
|
||||
auto text = m_ui->attachmentTextEdit->toPlainText().toUtf8();
|
||||
|
||||
QString error;
|
||||
if (validateFileName(fileName, error)) {
|
||||
QMessageBox::warning(this, tr("Save attachment"), error);
|
||||
return;
|
||||
}
|
||||
|
||||
m_attachments->set(fileName, text);
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
void NewEntryAttachmentsDialog::fileNameTextChanged(const QString& fileName)
|
||||
{
|
||||
QString error;
|
||||
bool valid = validateFileName(fileName, error);
|
||||
|
||||
m_ui->errorLabel->setText(error);
|
||||
m_ui->errorLabel->setVisible(!valid);
|
||||
|
||||
auto okButton = m_ui->dialogButtons->button(QDialogButtonBox::Ok);
|
||||
if (okButton) {
|
||||
okButton->setDisabled(!valid);
|
||||
}
|
||||
}
|
48
src/gui/entry/NewEntryAttachmentsDialog.h
Normal file
48
src/gui/entry/NewEntryAttachmentsDialog.h
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 2 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPointer>
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class EntryAttachmentsDialog;
|
||||
}
|
||||
|
||||
class QByteArray;
|
||||
class EntryAttachments;
|
||||
|
||||
class NewEntryAttachmentsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit NewEntryAttachmentsDialog(QPointer<EntryAttachments> attachments, QWidget* parent = nullptr);
|
||||
~NewEntryAttachmentsDialog() override;
|
||||
|
||||
private slots:
|
||||
void saveAttachment();
|
||||
void fileNameTextChanged(const QString& fileName);
|
||||
|
||||
private:
|
||||
bool validateFileName(const QString& fileName, QString& error) const;
|
||||
|
||||
QPointer<EntryAttachments> m_attachments;
|
||||
QScopedPointer<Ui::EntryAttachmentsDialog> m_ui;
|
||||
};
|
123
src/gui/entry/PreviewEntryAttachmentsDialog.cpp
Normal file
123
src/gui/entry/PreviewEntryAttachmentsDialog.cpp
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 2 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "PreviewEntryAttachmentsDialog.h"
|
||||
#include "ui_EntryAttachmentsDialog.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QMimeDatabase>
|
||||
#include <QPushButton>
|
||||
#include <QTextCursor>
|
||||
#include <QtDebug>
|
||||
|
||||
PreviewEntryAttachmentsDialog::PreviewEntryAttachmentsDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_ui(new Ui::EntryAttachmentsDialog)
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
|
||||
setWindowTitle(tr("Preview entry attachment"));
|
||||
// Disable the help button in the title bar
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
// Set to read-only
|
||||
m_ui->titleEdit->setReadOnly(true);
|
||||
m_ui->attachmentTextEdit->setReadOnly(true);
|
||||
m_ui->errorLabel->setVisible(false);
|
||||
|
||||
// Initialize dialog buttons
|
||||
m_ui->dialogButtons->setStandardButtons(QDialogButtonBox::Close | QDialogButtonBox::Open | QDialogButtonBox::Save);
|
||||
auto closeButton = m_ui->dialogButtons->button(QDialogButtonBox::Close);
|
||||
closeButton->setDefault(true);
|
||||
|
||||
connect(m_ui->dialogButtons, SIGNAL(rejected()), this, SLOT(reject()));
|
||||
connect(m_ui->dialogButtons, &QDialogButtonBox::clicked, [this](QAbstractButton* button) {
|
||||
auto pressedButton = m_ui->dialogButtons->standardButton(button);
|
||||
if (pressedButton == QDialogButtonBox::Open) {
|
||||
emit openAttachment(m_name);
|
||||
} else if (pressedButton == QDialogButtonBox::Save) {
|
||||
emit saveAttachment(m_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
PreviewEntryAttachmentsDialog::~PreviewEntryAttachmentsDialog() = default;
|
||||
|
||||
void PreviewEntryAttachmentsDialog::setAttachment(const QString& name, const QByteArray& data)
|
||||
{
|
||||
m_name = name;
|
||||
m_ui->titleEdit->setText(m_name);
|
||||
|
||||
m_type = attachmentType(data);
|
||||
m_data = data;
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void PreviewEntryAttachmentsDialog::update()
|
||||
{
|
||||
if (m_type == Tools::MimeType::Unknown) {
|
||||
updateTextAttachment(tr("No preview available").toUtf8());
|
||||
} else if (m_type == Tools::MimeType::Image) {
|
||||
updateImageAttachment(m_data);
|
||||
} else if (m_type == Tools::MimeType::PlainText) {
|
||||
updateTextAttachment(m_data);
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewEntryAttachmentsDialog::updateTextAttachment(const QByteArray& data)
|
||||
{
|
||||
m_ui->attachmentTextEdit->setPlainText(QString::fromUtf8(data));
|
||||
}
|
||||
|
||||
void PreviewEntryAttachmentsDialog::updateImageAttachment(const QByteArray& data)
|
||||
{
|
||||
QImage image{};
|
||||
if (!image.loadFromData(data)) {
|
||||
updateTextAttachment(tr("Image format not supported").toUtf8());
|
||||
return;
|
||||
}
|
||||
|
||||
m_ui->attachmentTextEdit->clear();
|
||||
auto cursor = m_ui->attachmentTextEdit->textCursor();
|
||||
|
||||
// Scale the image to the contents rect minus another set of margins to avoid scrollbars
|
||||
auto margins = m_ui->attachmentTextEdit->contentsMargins();
|
||||
auto size = m_ui->attachmentTextEdit->contentsRect().size();
|
||||
size.setWidth(size.width() - margins.left() - margins.right());
|
||||
size.setHeight(size.height() - margins.top() - margins.bottom());
|
||||
image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
|
||||
cursor.insertImage(image);
|
||||
}
|
||||
|
||||
Tools::MimeType PreviewEntryAttachmentsDialog::attachmentType(const QByteArray& data) const
|
||||
{
|
||||
QMimeDatabase mimeDb{};
|
||||
const auto mime = mimeDb.mimeTypeForData(data);
|
||||
|
||||
return Tools::toMimeType(mime.name());
|
||||
}
|
||||
|
||||
void PreviewEntryAttachmentsDialog::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QDialog::resizeEvent(event);
|
||||
|
||||
if (m_type == Tools::MimeType::Image) {
|
||||
update();
|
||||
}
|
||||
}
|
59
src/gui/entry/PreviewEntryAttachmentsDialog.h
Normal file
59
src/gui/entry/PreviewEntryAttachmentsDialog.h
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 2 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/Tools.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPointer>
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class EntryAttachmentsDialog;
|
||||
}
|
||||
|
||||
class PreviewEntryAttachmentsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PreviewEntryAttachmentsDialog(QWidget* parent = nullptr);
|
||||
~PreviewEntryAttachmentsDialog() override;
|
||||
|
||||
void setAttachment(const QString& name, const QByteArray& data);
|
||||
|
||||
signals:
|
||||
void openAttachment(const QString& name);
|
||||
void saveAttachment(const QString& name);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
|
||||
private:
|
||||
Tools::MimeType attachmentType(const QByteArray& data) const;
|
||||
|
||||
void update();
|
||||
void updateTextAttachment(const QByteArray& data);
|
||||
void updateImageAttachment(const QByteArray& data);
|
||||
|
||||
QScopedPointer<Ui::EntryAttachmentsDialog> m_ui;
|
||||
|
||||
QString m_name;
|
||||
QByteArray m_data;
|
||||
Tools::MimeType m_type{Tools::MimeType::Unknown};
|
||||
};
|
@ -21,7 +21,9 @@ QCheckBox, QRadioButton {
|
||||
spacing: 10px;
|
||||
}
|
||||
|
||||
ReportsDialog QTableView::item {
|
||||
ReportsDialog QTableView::item,
|
||||
EntryAttachmentsWidget QTableView::item
|
||||
{
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
@ -30,8 +32,7 @@ DatabaseWidget, DatabaseWidget #groupView, DatabaseWidget #tagView {
|
||||
border: none;
|
||||
}
|
||||
|
||||
EntryPreviewWidget QLineEdit, EntryPreviewWidget QTextEdit,
|
||||
EntryPreviewWidget TagsEdit
|
||||
EntryPreviewWidget *[blendIn="true"]
|
||||
{
|
||||
background-color: palette(window);
|
||||
border: none;
|
||||
|
@ -272,3 +272,70 @@ void TestTools::testArrayContainsValues()
|
||||
const auto result3 = Tools::getMissingValuesFromList<int>(numberValues, QList<int>({6, 7, 8}));
|
||||
QCOMPARE(result3.length(), 3);
|
||||
}
|
||||
|
||||
void TestTools::testMimeTypes()
|
||||
{
|
||||
const QStringList TextMimeTypes = {
|
||||
"text/plain", // Plain text
|
||||
"text/html", // HTML documents
|
||||
"text/css", // CSS stylesheets
|
||||
"text/javascript", // JavaScript files
|
||||
"text/markdown", // Markdown documents
|
||||
"text/xml", // XML documents
|
||||
"text/rtf", // Rich Text Format
|
||||
"text/vcard", // vCard files
|
||||
"text/tab-separated-values", // Tab-separated values
|
||||
"application/json", // JSON data
|
||||
"application/xml", // XML data
|
||||
"application/soap+xml", // SOAP messages
|
||||
"application/x-yaml", // YAML data
|
||||
"application/protobuf", // Protocol Buffers
|
||||
};
|
||||
|
||||
const QStringList ImageMimeTypes = {
|
||||
"image/jpeg", // JPEG images
|
||||
"image/png", // PNG images
|
||||
"image/gif", // GIF images
|
||||
"image/bmp", // BMP images
|
||||
"image/webp", // WEBP images
|
||||
"image/svg+xml" // SVG images
|
||||
};
|
||||
|
||||
const QStringList UnknownMimeTypes = {
|
||||
"audio/mpeg", // MPEG audio files
|
||||
"video/mp4", // MP4 video files
|
||||
"application/pdf", // PDF documents
|
||||
"application/zip", // ZIP archives
|
||||
"application/x-tar", // TAR archives
|
||||
"application/x-rar-compressed", // RAR archives
|
||||
"application/x-7z-compressed", // 7z archives
|
||||
"application/x-shockwave-flash", // Adobe Flash files
|
||||
"application/vnd.ms-excel", // Microsoft Excel files
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // Microsoft Excel (OpenXML) files
|
||||
"application/vnd.ms-powerpoint", // Microsoft PowerPoint files
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation", // Microsoft PowerPoint (OpenXML)
|
||||
// files
|
||||
"application/msword", // Microsoft Word files
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", // Microsoft Word (OpenXML) files
|
||||
"application/vnd.oasis.opendocument.text", // OpenDocument Text
|
||||
"application/vnd.oasis.opendocument.spreadsheet", // OpenDocument Spreadsheet
|
||||
"application/vnd.oasis.opendocument.presentation", // OpenDocument Presentation
|
||||
"application/x-httpd-php", // PHP files
|
||||
"application/x-perl", // Perl scripts
|
||||
"application/x-python", // Python scripts
|
||||
"application/x-ruby", // Ruby scripts
|
||||
"application/x-shellscript", // Shell scripts
|
||||
};
|
||||
|
||||
for (const auto& mime : TextMimeTypes) {
|
||||
QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::PlainText);
|
||||
}
|
||||
|
||||
for (const auto& mime : ImageMimeTypes) {
|
||||
QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::Image);
|
||||
}
|
||||
|
||||
for (const auto& mime : UnknownMimeTypes) {
|
||||
QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::Unknown);
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ private slots:
|
||||
void testConvertToRegex();
|
||||
void testConvertToRegex_data();
|
||||
void testArrayContainsValues();
|
||||
void testMimeTypes();
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_TESTTOOLS_H
|
||||
|
Loading…
x
Reference in New Issue
Block a user